동적 스폰과 파괴
MassSpawner와 프로그래밍 방식으로 엔티티를 생성/파괴하는 방법을 학습합니다
MassSpawner Actor
레벨에 배치하여 엔티티를 자동 생성하는 스포너
AMassSpawner는 레벨에 배치하는 Actor로, 지정된 Config에 따라 엔티티를 대량 생성합니다. 가장 간편한 스폰 방법입니다.
// 레벨에 AMassSpawner 배치 후 디테일 패널:
//
// ┌─────────────────────────────────────────┐
// │ MassSpawner Details │
// │ │
// │ Entity Types: │
// │ [0] Entity Config: DA_Zombie │
// │ Spawn Count: 5000 │
// │ [1] Entity Config: DA_Civilian │
// │ Spawn Count: 3000 │
// │ │
// │ Spawn Data Generators: │
// │ [0] UMassEntityDistanceSpawnGenerator │
// │ SpreadRadius: 5000.0 │
// │ MinDistance: 100.0 │
// │ │
// │ Auto Spawn On Begin Play: true │
// └─────────────────────────────────────────┘
스폰 위치를 결정하는 Generator를 설정합니다. 기본 제공되는 UMassEntityDistanceSpawnGenerator는 반경 내에 균일하게 분산시킵니다. 커스텀 Generator를 만들면 도로 위, 빌딩 내부 등 원하는 패턴으로 배치할 수 있습니다.
코드에서 엔티티 생성
C++에서 프로그래밍 방식으로 엔티티를 동적 생성
void SpawnSingleEntity(UWorld* World,
UMassEntityConfigAsset* Config, FVector Location)
{
FMassEntityManager& EM =
UMassEntitySubsystem::GetEntityManager(World);
// Config에서 Template 획득
const FMassEntityTemplate& Template =
Config->GetOrCreateEntityTemplate(*World);
// 엔티티 생성
FMassEntityHandle Entity =
EM.CreateEntity(Template.GetArchetype());
// Template의 초기화 콜백 실행
Template.InitializeEntity(EM, Entity);
// 위치 설정
FMassEntityView View(EM, Entity);
FTransformFragment& Transform =
View.GetFragmentData<FTransformFragment>();
Transform.Transform.SetLocation(Location);
}
void SpawnBatch(UWorld* World,
UMassEntityConfigAsset* Config, int32 Count)
{
FMassEntityManager& EM =
UMassEntitySubsystem::GetEntityManager(World);
const FMassEntityTemplate& Template =
Config->GetOrCreateEntityTemplate(*World);
// 배치 생성: 한 번에 다수의 엔티티 생성
TArray<FMassEntityHandle> Entities;
EM.BatchCreateEntities(
Template.GetArchetype(),
Count,
Entities);
// 각 엔티티 초기화
for (int32 i = 0; i < Entities.Num(); ++i)
{
Template.InitializeEntity(EM, Entities[i]);
// 위치를 원형으로 배치
FMassEntityView View(EM, Entities[i]);
FTransformFragment& Transform =
View.GetFragmentData<FTransformFragment>();
float Angle = (2.0f * PI * i) / Count;
float Radius = 1000.0f;
Transform.Transform.SetLocation(FVector(
FMath::Cos(Angle) * Radius,
FMath::Sin(Angle) * Radius,
0.0f));
}
}
엔티티 파괴
안전하게 엔티티를 제거하는 패턴
// 방법 1: Processor 내부에서 Defer 파괴
void ULifeTimeProcessor::Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
LifeTimeQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
auto LifeTimes =
Ctx.GetMutableFragmentView<FLifeTimeFragment>();
auto Entities = Ctx.GetEntities();
const float DT = Ctx.GetDeltaTimeSeconds();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
LifeTimes[i].ElapsedTime += DT;
if (LifeTimes[i].ElapsedTime >= LifeTimes[i].MaxLifeTime)
{
// Defer를 통한 안전한 파괴
Ctx.Defer().DestroyEntity(Entities[i]);
}
}
});
}
// 방법 2: Processor 외부에서 직접 파괴
void DestroyDirectly(FMassEntityManager& EM,
FMassEntityHandle Entity)
{
if (EM.IsEntityValid(Entity))
{
EM.DestroyEntity(Entity);
}
}
// 방법 3: 배치 파괴
void BatchDestroy(FMassEntityManager& EM,
const TArray<FMassEntityHandle>& Entities)
{
EM.BatchDestroyEntities(Entities);
}
Processor 실행 중에는 반드시 Ctx.Defer().DestroyEntity()를 사용하세요. 직접 EntityManager.DestroyEntity()를 호출하면 현재 순회 중인 Chunk의 데이터가 망가져 크래시가 발생합니다.
Observer로 정리 처리
엔티티 생성/파괴 시 자동으로 실행되는 후처리
Observer Processor를 활용하면 엔티티 생성/파괴 시 자동으로 초기화/정리 로직을 실행할 수 있습니다.
UCLASS()
class UEntityInitObserver : public UMassObserverProcessor
{
GENERATED_BODY()
public:
UEntityInitObserver()
{
// Transform Fragment가 추가될 때 트리거
// (= 새 엔티티가 생성될 때)
ObservedType = FTransformFragment::StaticStruct();
Operation = EMassObservedOperation::Add;
}
};
// 파괴 시 정리 Observer
UCLASS()
class UEntityCleanupObserver : public UMassObserverProcessor
{
GENERATED_BODY()
public:
UEntityCleanupObserver()
{
// Transform Fragment가 제거될 때 트리거
// (= 엔티티가 파괴될 때)
ObservedType = FTransformFragment::StaticStruct();
Operation = EMassObservedOperation::Remove;
}
protected:
virtual void Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override
{
CleanupQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
auto Transforms =
Ctx.GetFragmentView<FTransformFragment>();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
// 파괴 이펙트 스폰, 리소스 해제 등
UE_LOG(LogTemp, Log,
TEXT("Entity cleaned up at %s"),
*Transforms[i].Transform
.GetLocation().ToString());
}
});
}
};
핵심 요약
- AMassSpawner는 레벨에 배치하여 Config 기반으로 대량 엔티티를 자동 생성하는 Actor이다
- C++ 코드에서 CreateEntity/BatchCreateEntities로 동적 생성하고, InitializeEntity로 초기화한다
- Processor 내부에서는 반드시 Ctx.Defer().DestroyEntity()로 지연 파괴해야 한다
- Processor 외부에서는 EntityManager.DestroyEntity()로 직접 파괴할 수 있다
- Observer Processor로 엔티티 생성/파괴 시 초기화/정리 로직을 자동 실행한다
- 대량 생성 시 BatchCreateEntities가 개별 CreateEntity보다 효율적이다
도전 과제
배운 내용을 직접 실습해보세요
C++에서 FMassEntityManager::BatchCreateEntities를 사용하여 런타임에 100개의 엔티티를 동적으로 생성하세요. FMassEntityTemplateBuildContext로 Fragment 조합을 지정하고 초기값을 설정하세요.
Processor에서 조건(HP <= 0)을 만족하는 엔티티를 DestroyEntity로 파괴하세요. Deferred Command로 처리해야 하는 이유를 이해하고, 즉시 파괴 시 발생하는 문제를 확인하세요.
매 프레임 수백 개의 엔티티를 스폰/디스폰해야 하는 시나리오(예: 총알)에서 BatchCreateEntities와 BatchDestroyEntities의 성능을 단건 처리와 비교하세요. 오브젝트 풀링 패턴도 구현해보세요.