Geometry Collection의 한계
Chaos Cache Manager의 분리
트리거 로직 분산
시퀀서


GameThread
RHI Thread (Render Hardware Interface Thread)
RenderThread
GPU0-Compute0 (GPU 컴퓨트 스레드)

다시 본론으로 돌아와서 GameThread를 자세히 보면 빨간색 박스 부분에 지연이 생기는 부분을 볼 수 있다

좀 더 가까이서 보면 GameThread에서 부모인 FEngineLoop::Tick이 있고 자식들이 전부 특정 작업에 대해서 기다리며 병목이 생기는 것을 볼 수 있다.

FEngineLoop::Tick을 관찰하고 Excl을 내림차순하면 WaitForTasks가 제일 높게 나오는데 Incl하고 Excl에 대해서 정리하자면
즉 WaitForTasks가 GameThread에서 한 Tick에 72ms를 먹고 있다
다만 GameThread가 원인이 아니라 다른 곳에서 생기는 병목을 기다리는 것이기 때문에 다른 트랙을 살펴봐야 한다


GameThread가 아닌 트랙에서 살펴보다 보면 GPU0-Compute0라는 트랙에서
RenderGraphExecute (71.3 ms) UpdateAllPrimitiveSceneInfos라는 것이 오래 걸리는 것을 확인할 수 있다.
카오스 디스트럭션이 일어날 때 프랙처로 정의한 클러스터대로 개별 물리 오브젝트가 Scene에 등록이 된다.
그래서 새로운 Primitive가 추가되기 때문에 UpdateAllPrimitiveSceneInfos에서
1. FPrimitiveSceneInfo 생성
2. Bounding Box 계산
3. Culling 정보 등록
4. Shadow Casting 설정
5. LOD 정보 설정
을 각 조각마다 수행하기 때문에 오래 걸리게 된다.
그리고 이후에도 각 조각에 대해서 Physics를 계산하기 때문에 성능 부하를 준다.

그래서 다시 인사이트를 보면 20ms를 유지하다가 카오스 디스트럭션이 처음 일어날 때 80ms가 되고 이후 40ms를 유지하는 것을 볼 수 있다.

캐시를 사용하게 되면 녹화 당시 데이터가 저장되어 있기 때문에 Transform만 업데이트 하면 된다.
캐시 실행이 되고 나서 ms가 조금 올라갔지만 20ms정도로 캐시를 사용하면 성능 부하를 막을 수 있다.
ChaosCacheManager를 상속받아 ASC와 Geometry Collection을 내장한 독립 액터 생성
// DestructibleCacheActor.h
#pragma once
#include "CoreMinimal.h"
#include "Chaos/CacheManagerActor.h"
#include "AbilitySystemInterface.h"
#include "GameplayTagContainer.h"
#include "DestructibleCacheActor.generated.h"
class UAbilitySystemComponent;
class UGeometryCollectionComponent;
UCLASS()
class PUZZLECHAOS_API ADestructibleCacheActor : public AChaosCacheManager, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
ADestructibleCacheActor();
// 지오메트리 컬렉션 컴포넌트
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Destruction")
UGeometryCollectionComponent* GeoComp;
// ASC
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities")
UAbilitySystemComponent* AbilitySystemComponent;
// 부여받은 태그와 비교하는 TriggerTag
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Destruction")
FGameplayTag TriggerTag;
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
UFUNCTION(BlueprintCallable, Category="Destruction")
void TriggerDestruction();
protected:
virtual void BeginPlay() override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
private:
UFUNCTION(Server, Reliable)
void ServerTriggerDestruction();
UPROPERTY(ReplicatedUsing=OnRep_IsDestroyed)
bool bIsDestroyed;
UFUNCTION()
void OnRep_IsDestroyed();
void OnTagChanged(const FGameplayTag Tag, int32 NewCount);
void ExecuteDestruction();
};
// DestructibleCacheActor.cpp
#include "DestructibleCacheActor.h"
#include "GeometryCollection/GeometryCollectionComponent.h"
#include "AbilitySystemComponent.h"
#include "Net/UnrealNetwork.h"
ADestructibleCacheActor::ADestructibleCacheActor()
{
SetReplicates(true);
// 지오메트리 컬렉션 생성 (부서질 메시)
GeoComp = CreateDefaultSubobject<UGeometryCollectionComponent>(TEXT("GeoComp"));
RootComponent = GeoComp;
GeoComp->SetSimulatePhysics(false); // Trigger 전까지 물리 비활성
GeoComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
// GAS 통합을 위한 ASC 생성
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
bIsDestroyed = false;
TriggerTag = FGameplayTag::RequestGameplayTag(FName("Effect.Destruction.Triggered"));
}
void ADestructibleCacheActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ADestructibleCacheActor, bIsDestroyed);
}
UAbilitySystemComponent* ADestructibleCacheActor::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void ADestructibleCacheActor::BeginPlay()
{
Super::BeginPlay();
// 서버에서만 초기화
if (HasAuthority())
{
// 카오스 캐시 매니저가 GeoComp 관찰 시작
FindOrAddObservedComponent(GeoComp, NAME_None, false);
if (AbilitySystemComponent)
{
// ASC 초기화
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// TriggerTag 변경 감지 등록
if (TriggerTag.IsValid())
{
AbilitySystemComponent->RegisterGameplayTagEvent(
TriggerTag,
EGameplayTagEventType::NewOrRemoved
).AddUObject(this, &ADestructibleCacheActor::OnTagChanged);
}
}
}
}
// TriggerTag가 추가/제거될 때 호출
void ADestructibleCacheActor::OnTagChanged(const FGameplayTag Tag, int32 NewCount)
{
// 서버에서만 실행, 추가된 태그가 지정한 태그와 같고 아직 파괴되지 않았으면
if (HasAuthority() && Tag.MatchesTagExact(TriggerTag) && NewCount > 0 && !bIsDestroyed)
{
bIsDestroyed = true;
ExecuteDestruction();
}
}
// 블루프린트나 외부에서 직접 호출 가능
void ADestructibleCacheActor::TriggerDestruction()
{
if (bIsDestroyed)
{
return;
}
if (HasAuthority())
{
bIsDestroyed = true;
ExecuteDestruction();
}
else
{
// 클라이언트면 서버에 요청
ServerTriggerDestruction();
}
}
void ADestructibleCacheActor::ServerTriggerDestruction_Implementation()
{
TriggerDestruction();
}
// bIsDestroyed 리플리케이션 콜백
void ADestructibleCacheActor::OnRep_IsDestroyed()
{
if (bIsDestroyed)
{
ExecuteDestruction();
}
}
// 실제 파괴 실행 (캐시 재생)
void ADestructibleCacheActor::ExecuteDestruction()
{
if (!GeoComp)
{
return;
}
// 카오스 캐시 매니저의 Trigger 모드 실행
TriggerComponent(GeoComp);
}
PublicDependencyModuleNames.AddRange(new string[]
{
"GameplayTags", // Gameplay Tag 시스템
"GameplayAbilities", // GAS
"ChaosCaching", // Chaos Cache Manager
"GeometryCollectionEngine" // Geometry Collection
});




투사체 블루프린트

슈터 캐릭터 블루프린트

서버가 총을 쏠 때
1. Server: Projectile Hit
2. Server: Character::ServerNotifyHit 직접 실행 (이미 서버)
3. Server: GE_DestructionTrigger 적용
4. Server: ASC에 Destruction.Trigger 태그 추가
5. Server: OnTagChanged 감지
6. Server: bIsDestroyed = true
7. Server: ExecuteDestruction() → TriggerComponent()
8. 리플리케이트 → All Clients
9. Clients: OnRep_IsDestroyed
10. Clients: ExecuteDestruction() → 캐시 재생
클라이언트가 총을 쏠 때
1. Client: Projectile Hit (로컬)
2. Client: Character::ServerNotifyHit (RPC 호출)
3. → Server로 전달
4. Server: ServerNotifyHit_Implementation 실행
5. Server: GE_DestructionTrigger 적용
6. Server: ASC에 Destruction.Trigger 태그 추가
7. Server: OnTagChanged 감지
8. Server: bIsDestroyed = true
9. Server: ExecuteDestruction() → TriggerComponent()
10. 리플리케이트 → All Clients
11. Clients: OnRep_IsDestroyed
12. Clients: ExecuteDestruction() → 캐시 재생
서버가 총을 쐈을 때

클라이언트가 총을 쐈을 때
