콘텐츠로 이동

UE5 Mass Entity — 대규모 엔티티 시뮬레이션 (ECS)

Mass Entity는 UE5의 ECS(Entity Component System) 프레임워크입니다. 전통적인 Actor 기반 접근과 달리 데이터(Fragment)와 로직(Processor)을 분리하고 캐시 친화적 SoA(Structure of Arrays) 레이아웃으로 수천 개 엔티티를 효율적으로 처리합니다.


Entity — 고유 ID (데이터 없음, 순수 핸들)
Fragment — 엔티티에 붙는 데이터 컴포넌트 (USTRUCT)
Tag — 데이터 없는 마커 (필터링용)
Archetype — Fragment 조합이 동일한 엔티티 묶음
Processor — Archetype을 쿼리해 Fragment를 처리하는 시스템
Trait — 여러 Fragment/Processor를 묶은 재사용 단위

// Fragment: 순수 데이터 구조체
USTRUCT()
struct FMassVelocityFragment : public FMassFragment
{
GENERATED_BODY()
FVector Value = FVector::ZeroVector;
};
USTRUCT()
struct FMassHealthFragment : public FMassFragment
{
GENERATED_BODY()
float Current = 100.0f;
float Max = 100.0f;
};
// Tag: 상태 마커 (데이터 없음)
USTRUCT()
struct FMassDeadTag : public FMassTag
{
GENERATED_BODY()
};

UCLASS()
class UMoveProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UMoveProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
UMoveProcessor::UMoveProcessor()
{
// 이 Processor가 처리할 Phase 지정
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
ProcessingPhase = EMassProcessingPhase::PrePhysics;
}
void UMoveProcessor::ConfigureQueries()
{
// Velocity와 Transform이 모두 있는 엔티티만 쿼리
EntityQuery.AddRequirement<FMassVelocityFragment>(
EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FTransformFragment>(
EMassFragmentAccess::ReadWrite);
EntityQuery.AddTagRequirement<FMassDeadTag>(
EMassFragmentPresence::None); // Dead 태그 없는 것만
}
void UMoveProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
float DeltaTime = Context.GetDeltaTimeSeconds();
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[DeltaTime](FMassExecutionContext& Ctx)
{
// Chunk 단위로 SIMD 친화적 처리
TArrayView<const FMassVelocityFragment> Velocities =
Ctx.GetFragmentView<FMassVelocityFragment>();
TArrayView<FTransformFragment> Transforms =
Ctx.GetMutableFragmentView<FTransformFragment>();
const int32 Count = Ctx.GetNumEntities();
for (int32 i = 0; i < Count; ++i)
{
FVector NewLoc = Transforms[i].GetTransform().GetLocation()
+ Velocities[i].Value * DeltaTime;
Transforms[i].GetMutableTransform().SetLocation(NewLoc);
}
});
}

4. Trait 정의 — 재사용 가능한 설정 묶음

섹션 제목: “4. Trait 정의 — 재사용 가능한 설정 묶음”
UCLASS()
class UMovableTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
float MaxSpeed = 300.0f;
virtual void BuildTemplate(
FMassEntityTemplateBuildContext& BuildContext,
const UWorld& World) const override
{
// 이 Trait이 필요한 Fragment 추가
BuildContext.AddFragment<FMassVelocityFragment>();
BuildContext.AddFragment<FTransformFragment>();
// 초기값 설정
FMassVelocityFragment& Vel =
BuildContext.AddFragmentWithDefaultValue<FMassVelocityFragment>();
Vel.Value = FVector::ZeroVector;
}
};

// EntityManager를 통한 엔티티 생성
void AMySpawner::SpawnCrowd(int32 Count)
{
UMassEntitySubsystem* EntitySubsystem =
World->GetSubsystem<UMassEntitySubsystem>();
FMassEntityManager& EntityManager = EntitySubsystem->GetMutableEntityManager();
// 아키타입 빌드
FMassArchetypeHandle Archetype =
EntityManager.CreateArchetype(
{FMassVelocityFragment::StaticStruct(),
FTransformFragment::StaticStruct()});
// 배치 생성 (효율적)
TArray<FMassEntityHandle> Entities;
EntityManager.BatchCreateEntities(Archetype, Count, Entities);
// Fragment 초기화
for (auto& Entity : Entities)
{
FMassVelocityFragment& Vel =
EntityManager.GetFragmentDataChecked<FMassVelocityFragment>(Entity);
Vel.Value = FMath::VRand() * 200.0f;
}
}

Mass Entity는 렌더링이 없으므로 Niagara 또는 ISM(Instanced Static Mesh)으로 시각화합니다.

MassEntity → FTransformFragment 위치 데이터
MassVisualization Processor → ISMComponent 인스턴스 위치 동기화

플러그인: MassGameplay, MassRepresentation 활성화 필요


항목Actor 기반Mass Entity
1만 에이전트불가 (~10fps)가능 (60fps+)
메모리 레이아웃AoS (캐시 미스)SoA (캐시 친화적)
틱 오버헤드Actor마다 vtableChunk 배치 처리
렌더링개별 메시ISM/Niagara

8. Shared Fragment — 아키타입 공유 데이터

섹션 제목: “8. Shared Fragment — 아키타입 공유 데이터”

FMassSharedFragment는 같은 아키타입 내 모든 엔티티가 하나의 인스턴스를 공유합니다. 설정값(최대 속도, 충돌 반경)처럼 엔티티마다 다를 필요 없는 데이터에 적합합니다.

// Shared Fragment: 아키타입 전체가 공유하는 설정 데이터
USTRUCT()
struct FMassMovementConfigFragment : public FMassSharedFragment
{
GENERATED_BODY()
float MaxSpeed = 300.f;
float AccelRate = 600.f;
float AvoidanceRadius = 80.f;
};
// Processor에서 Shared Fragment 읽기
void UMoveProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
// Shared: Chunk 당 하나 (캐시 효율적)
const FMassMovementConfigFragment& Config =
Ctx.GetConstSharedFragment<FMassMovementConfigFragment>();
TArrayView<FMassVelocityFragment> Velocities =
Ctx.GetMutableFragmentView<FMassVelocityFragment>();
for (auto& Vel : Velocities)
{
Vel.Value = Vel.Value.GetClampedToMaxSize(Config.MaxSpeed);
}
});
}

9. Command Buffer — 지연 엔티티 조작

섹션 제목: “9. Command Buffer — 지연 엔티티 조작”

Processor 실행 중 엔티티를 직접 생성·삭제하면 Chunk 순회가 무효화됩니다. FMassCommandBuffer로 요청을 지연시킵니다.

void UHealthProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
auto Healths = Ctx.GetMutableFragmentView<FMassHealthFragment>();
TArrayView<FMassEntityHandle> Entities = Ctx.GetEntities();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
if (Healths[i].Current <= 0.f)
{
// 즉시 삭제 대신 명령 버퍼에 추가
Ctx.Defer().AddTag<FMassDeadTag>(Entities[i]);
Ctx.Defer().PushCommand<FMassCommandDestroyEntity>(Entities[i]);
}
}
}); // ForEachEntityChunk 종료 후 명령 버퍼 일괄 실행
}

Mass Entity는 수천~수만 개의 단순한 에이전트(군중, 트래픽, NPC 무리)를 60fps로 시뮬레이션할 수 있는 UE5의 ECS 솔루션입니다. Fragment로 데이터를 정의하고, Processor로 Chunk 단위 배치 처리를 구현하며, Trait으로 재사용 가능한 엔티티 템플릿을 구성하는 것이 핵심 패턴입니다.