Entity, Fragment, Tag
Mass Entity의 세 가지 핵심 데이터 타입을 깊이 있게 이해합니다
Entity - 엔티티
Mass Entity 세계의 기본 식별자
Entity는 고유 ID에 불과합니다. AActor처럼 무거운 객체가 아니라, Fragment들의 조합을 가리키는 경량 핸들입니다. Entity 자체에는 데이터도, 로직도 없습니다.
// FMassEntityHandle은 엔티티를 식별하는 경량 구조체
struct FMassEntityHandle
{
int32 Index; // 엔티티 배열 인덱스
int32 SerialNumber; // 재사용 방지용 시리얼 넘버
};
// Entity 생성
FMassEntityHandle NewEntity = EntityManager.CreateEntity(Archetype);
// Entity 유효성 검사
if (EntityManager.IsEntityValid(NewEntity))
{
// 이 엔티티는 아직 살아있다
}
// Entity 파괴
EntityManager.DestroyEntity(NewEntity);
AActor 하나는 메모리에서 수 KB를 차지하지만, Mass Entity 하나는 8바이트의 핸들과 연결된 Fragment 데이터만 존재합니다. 이 차이가 수만 개 규모에서 극적인 성능 차이를 만듭니다.
Fragment - 데이터 단위
엔티티에 부착되는 순수 데이터 구조체
Fragment는 FMassFragment를 상속하는 USTRUCT입니다. ECS에서 Component에 해당하며, 순수 데이터만 포함합니다. 로직은 절대 넣지 않습니다.
// 기본 Fragment: 위치 정보
USTRUCT()
struct FMyTransformFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
FTransform Transform;
};
// 속도 Fragment
USTRUCT()
struct FVelocityFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
FVector LinearVelocity = FVector::ZeroVector;
UPROPERTY(EditAnywhere)
float MaxSpeed = 600.0f;
};
// 생존 시간 Fragment
USTRUCT()
struct FLifeTimeFragment : public FMassFragment
{
GENERATED_BODY()
float ElapsedTime = 0.0f;
float MaxLifeTime = 10.0f;
};
SharedFragment - 공유 데이터
FMassSharedFragment는 여러 엔티티가 동일한 데이터를 공유할 때 사용합니다. 예를 들어, 같은 종류의 적이 공유하는 설정 값이 여기에 해당합니다.
USTRUCT()
struct FEnemyConfigSharedFragment : public FMassSharedFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
float AttackDamage = 10.0f;
UPROPERTY(EditAnywhere)
float DetectionRadius = 1500.0f;
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UStaticMesh> VisualMesh;
};
// SharedFragment는 동일한 값을 가진 엔티티들이
// 하나의 인스턴스를 참조하므로 메모리 절약이 된다
ChunkFragment - 청크 단위 데이터
FMassChunkFragment는 개별 엔티티가 아닌 Chunk 전체에 연결되는 Fragment입니다. LOD 계산처럼 청크 단위로 관리해야 하는 데이터에 사용합니다.
USTRUCT()
struct FMyChunkLODFragment : public FMassChunkFragment
{
GENERATED_BODY()
// 이 Chunk에 속한 모든 엔티티의 LOD 정보
int32 LODLevel = 0;
float DistanceToViewer = 0.0f;
};
| Fragment 유형 | 기본 클래스 | 저장 단위 | 사용 예 |
|---|---|---|---|
| Fragment | FMassFragment | 엔티티당 1개 | Transform, Velocity, Health |
| SharedFragment | FMassSharedFragment | 여러 엔티티 공유 | 적 설정, 메시 참조 |
| ChunkFragment | FMassChunkFragment | Chunk당 1개 | LOD 레벨, 가시성 |
Tag - 제로 사이즈 마커
데이터 없이 엔티티를 분류하는 태그 시스템
Tag는 FMassTag를 상속하는 빈 USTRUCT입니다. 멤버 변수를 가지지 않으며, 존재 자체가 의미를 가집니다. Processor가 특정 엔티티 그룹을 필터링하는 데 사용됩니다.
// Tag는 반드시 빈 구조체여야 한다 - 멤버 변수 금지!
USTRUCT()
struct FZombieTag : public FMassTag
{
GENERATED_BODY()
// 여기에 아무 멤버도 넣지 않는다
};
USTRUCT()
struct FIsDeadTag : public FMassTag
{
GENERATED_BODY()
};
USTRUCT()
struct FProjectileTag : public FMassTag
{
GENERATED_BODY()
};
// Query에서 Tag로 필터링
MyQuery.AddTagRequirement<FZombieTag>(EMassFragmentPresence::All);
MyQuery.AddTagRequirement<FIsDeadTag>(EMassFragmentPresence::None);
boolean 값을 저장하는 Fragment 대신 Tag를 사용하세요. Tag는 메모리를 차지하지 않으며, Archetype 분류 기준으로 작동하므로 필터링이 훨씬 효율적입니다. Tag 유무에 따라 엔티티가 다른 Archetype에 배치되어 배치 처리 시 성능 이점이 있습니다.
Tag의 동적 추가/제거
// Processor 실행 중 Tag 추가/제거는 Defer를 사용해야 한다
// (직접 변경 시 Archetype 이동으로 인한 문제 발생)
void UMyProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
MyQuery.ForEachEntityChunk(EntityManager, Context,
[&Context](FMassExecutionContext& Ctx)
{
auto Entities = Ctx.GetEntities();
auto HealthList = Ctx.GetFragmentView<FHealthFragment>();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
if (HealthList[i].Current <= 0.0f)
{
// 지연 실행: 현재 처리가 끝난 후 Tag 추가
Ctx.Defer().AddTag<FIsDeadTag>(Entities[i]);
}
}
});
}
Fragment 설계 원칙
효율적인 Fragment 구조를 설계하는 방법
DO - 권장
- Fragment는 작고 집중적으로 설계
- 함께 접근되는 데이터를 같은 Fragment에 배치
- POD(Plain Old Data) 타입 선호
- 상태 분류에는 Tag 사용
- 공유 설정은 SharedFragment 활용
DON'T - 주의
- Fragment에 로직(함수)을 넣지 않기
- 하나의 Fragment에 너무 많은 데이터
- UObject 포인터 직접 저장 지양
- Tag에 멤버 변수 추가하지 않기
- 자주 변경되는 데이터와 읽기 전용 혼합 금지
// 좋은 예: 관련 데이터끼리 묶기
USTRUCT()
struct FHealthFragment : public FMassFragment
{
GENERATED_BODY()
float Current = 100.0f;
float Max = 100.0f;
};
// 나쁜 예: 너무 많은 데이터를 하나에 넣기
// struct FEnemyDataFragment : public FMassFragment
// {
// FTransform Transform;
// FVector Velocity;
// float Health, MaxHealth;
// float AttackDamage, AttackRange;
// int32 Level, Experience;
// FName EnemyType;
// // ... 이동과 전투가 항상 동시에 필요하지 않다!
// };
핵심 요약
- Entity(FMassEntityHandle)는 8바이트의 경량 식별자로, Index와 SerialNumber로 구성된다
- Fragment(FMassFragment)는 엔티티에 부착되는 순수 데이터 구조체이며, 로직을 포함하지 않는다
- SharedFragment는 여러 엔티티가 동일한 설정을 공유할 때, ChunkFragment는 Chunk 단위 관리용이다
- Tag(FMassTag)는 제로 사이즈 마커로, 존재 자체가 데이터이며 Archetype 분류에 영향을 준다
- Fragment는 작고 집중적으로 설계해야 하며, 함께 접근되는 데이터를 같은 Fragment에 배치한다
- Tag/Fragment 동적 변경은 반드시 Defer 패턴을 사용하여 안전하게 처리한다
도전 과제
배운 내용을 직접 실습해보세요
FMassFragment를 상속받아 FHealthFragment(float CurrentHP, float MaxHP)와 FTeamFragment(uint8 TeamID)를 선언하세요. USTRUCT와 GENERATED_BODY 매크로를 올바르게 적용하세요.
FMassTag를 상속받아 FDeadTag, FInvincibleTag, FPoisonedTag를 만드세요. Tag는 데이터 없이 엔티티의 상태만 표현하므로 빈 구조체여야 합니다. 조건부 쿼리에서 활용할 수 있도록 준비하세요.
FMassSharedFragment를 상속받아 여러 엔티티가 공유하는 설정 데이터(FDifficultySettings 등)를 정의하세요. 동일 Archetype의 모든 엔티티가 하나의 Shared Fragment 인스턴스를 참조하는 구조를 확인하세요.