PART 1 · 강의 2/3

Entity, Fragment, Tag

Mass Entity의 세 가지 핵심 데이터 타입을 깊이 있게 이해합니다

01

Entity - 엔티티

Mass Entity 세계의 기본 식별자

Entity는 고유 ID에 불과합니다. AActor처럼 무거운 객체가 아니라, Fragment들의 조합을 가리키는 경량 핸들입니다. Entity 자체에는 데이터도, 로직도 없습니다.

C++ - FMassEntityHandle // FMassEntityHandle은 엔티티를 식별하는 경량 구조체 struct FMassEntityHandle { int32 Index; // 엔티티 배열 인덱스 int32 SerialNumber; // 재사용 방지용 시리얼 넘버 }; // Entity 생성 FMassEntityHandle NewEntity = EntityManager.CreateEntity(Archetype); // Entity 유효성 검사 if (EntityManager.IsEntityValid(NewEntity)) { // 이 엔티티는 아직 살아있다 } // Entity 파괴 EntityManager.DestroyEntity(NewEntity);
Entity vs Actor

AActor 하나는 메모리에서 수 KB를 차지하지만, Mass Entity 하나는 8바이트의 핸들과 연결된 Fragment 데이터만 존재합니다. 이 차이가 수만 개 규모에서 극적인 성능 차이를 만듭니다.

02

Fragment - 데이터 단위

엔티티에 부착되는 순수 데이터 구조체

FragmentFMassFragment를 상속하는 USTRUCT입니다. ECS에서 Component에 해당하며, 순수 데이터만 포함합니다. 로직은 절대 넣지 않습니다.

C++ - Fragment 정의 예제 // 기본 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는 여러 엔티티가 동일한 데이터를 공유할 때 사용합니다. 예를 들어, 같은 종류의 적이 공유하는 설정 값이 여기에 해당합니다.

C++ - SharedFragment 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 계산처럼 청크 단위로 관리해야 하는 데이터에 사용합니다.

C++ - ChunkFragment 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 레벨, 가시성
03

Tag - 제로 사이즈 마커

데이터 없이 엔티티를 분류하는 태그 시스템

TagFMassTag를 상속하는 빈 USTRUCT입니다. 멤버 변수를 가지지 않으며, 존재 자체가 의미를 가집니다. Processor가 특정 엔티티 그룹을 필터링하는 데 사용됩니다.

C++ - Tag 정의 // 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);
Tag vs bool Fragment

boolean 값을 저장하는 Fragment 대신 Tag를 사용하세요. Tag는 메모리를 차지하지 않으며, Archetype 분류 기준으로 작동하므로 필터링이 훨씬 효율적입니다. Tag 유무에 따라 엔티티가 다른 Archetype에 배치되어 배치 처리 시 성능 이점이 있습니다.

Tag의 동적 추가/제거

C++ - 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]); } } }); }
04

Fragment 설계 원칙

효율적인 Fragment 구조를 설계하는 방법

DO - 권장

  • Fragment는 작고 집중적으로 설계
  • 함께 접근되는 데이터를 같은 Fragment에 배치
  • POD(Plain Old Data) 타입 선호
  • 상태 분류에는 Tag 사용
  • 공유 설정은 SharedFragment 활용

DON'T - 주의

  • Fragment에 로직(함수)을 넣지 않기
  • 하나의 Fragment에 너무 많은 데이터
  • UObject 포인터 직접 저장 지양
  • Tag에 멤버 변수 추가하지 않기
  • 자주 변경되는 데이터와 읽기 전용 혼합 금지
C++ - 좋은 Fragment 설계 예시 // 좋은 예: 관련 데이터끼리 묶기 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; // // ... 이동과 전투가 항상 동시에 필요하지 않다! // };
SUMMARY

핵심 요약

  • Entity(FMassEntityHandle)는 8바이트의 경량 식별자로, Index와 SerialNumber로 구성된다
  • Fragment(FMassFragment)는 엔티티에 부착되는 순수 데이터 구조체이며, 로직을 포함하지 않는다
  • SharedFragment는 여러 엔티티가 동일한 설정을 공유할 때, ChunkFragment는 Chunk 단위 관리용이다
  • Tag(FMassTag)는 제로 사이즈 마커로, 존재 자체가 데이터이며 Archetype 분류에 영향을 준다
  • Fragment는 작고 집중적으로 설계해야 하며, 함께 접근되는 데이터를 같은 Fragment에 배치한다
  • Tag/Fragment 동적 변경은 반드시 Defer 패턴을 사용하여 안전하게 처리한다
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 커스텀 Fragment 정의

FMassFragment를 상속받아 FHealthFragment(float CurrentHP, float MaxHP)와 FTeamFragment(uint8 TeamID)를 선언하세요. USTRUCT와 GENERATED_BODY 매크로를 올바르게 적용하세요.

실습 2: Tag를 활용한 상태 마킹

FMassTag를 상속받아 FDeadTag, FInvincibleTag, FPoisonedTag를 만드세요. Tag는 데이터 없이 엔티티의 상태만 표현하므로 빈 구조체여야 합니다. 조건부 쿼리에서 활용할 수 있도록 준비하세요.

심화 과제: Shared Fragment 활용

FMassSharedFragment를 상속받아 여러 엔티티가 공유하는 설정 데이터(FDifficultySettings 등)를 정의하세요. 동일 Archetype의 모든 엔티티가 하나의 Shared Fragment 인스턴스를 참조하는 구조를 확인하세요.