Entity Config
Trait과 Fragment에 대한 설명에 앞서, EntityConfig에 대해 알고 넘어가자. EntityConfig는 Data Asset으로 작성하는 매스 엔티티에 대한 명세이다. 소환되는 매스 엔티티가 실제로 어떤 기능을 할 지를 구현하는 것과는 무관하다. 하지만, 매스 엔티티의 기능이 구현되는 데 필요한 Trait이나 Fragment들을 미리 엔티티에 입력해주는 곳이라고 볼 수 있다. 예를 들어, 이 엔티티가 움직이는 엔티티다.라고 한다면, 엔티티의 이동속도나 이런 정보를 설정해주는
Movement Trait을 넣어줄 수 있다. 이 엔티티가 체력/공격력 등의 어트리뷰트를 가지게 하려면 추가한 스탯 프래그먼트를 가지게 하는 트레잇을 추가해줄 수 있다.
좌측은 테스트를 위해 제작한 EntityConfig이다. 여기서,
LOD관련은 Index 0-1. AI 관련은 3-10. 그 외의 11은 어트리뷰트 설정, 12는 VAT를 이용한 애니메이션 설정. 3은 추가 Fragment가 (Trait외로) 필요한 경우, 추가하는 항목이다.Trait
먼저, 언리얼 엔진에서 Trait에 대해 어떻게 설명하는지 Trait의 기본이 되는 클래스
UMassEntityTraitBase를 살펴보자./** * 매스 엔티티 트레이트의 기본 클래스입니다. * 엔티티 트레이트는 최종 사용에(예: 리플리케이션, 시각화) 의미 있는 논리적 트레이트를 생성하는 프래그먼트 집합입니다. * 템플릿 생성 메서드를 사용하면 속성이나 캐시된 값을 기반으로 일부 프래그먼트를 구성할 수 있습니다. * 예를 들어, 참조된 자산을 기반으로 프래그먼트를 추가하거나, 메모리 소모가 큰 일부 설정을 * 캐시하여 프래그먼트에 인덱스만 저장할 수 있습니다. * Base class for Mass Entity Traits. * An entity trait is a set of fragments that create a logical trait tha makes sense to end use (i.e. replication, visualization). * The template building method allows to configure some fragments based on properties or cached values. * For example, a fragment can be added based on a referenced asset, or some memory hungry settings can be * cached and just and index stored on a fragment. */UCLASS(MinimalAPI, Abstract, BlueprintType, EditInlineNew, CollapseCategories, config = Mass, defaultconfig)class UMassEntityTraitBase : public UObject{ GENERATED_BODY() public: /** * 이 타입은 기존 TArray를 감싸서, 사용자가 포함된 데이터와 상호작용할 수 있는 방식을 제한합니다. * 기본적으로 사용자가 요소를 추가하는 것만 허용하고, 그 외의 작업은 할 수 없도록 제한합니다. * This is a type wrapping an existing TArray to limit how users can interact with the contained data. * We essentially limit users to just adding elements, no other operations. */ struct FAdditionalTraitRequirements { explicit FAdditionalTraitRequirements(TArray<const UStruct*>& InTargetContainer) : TargetContainer(InTargetContainer) { } // Copying constructor and assignment deleted to prevent users storing copies of the type, // which wouldn't be safe due to this type hosting a reference to an array that can go out of scope. FAdditionalTraitRequirements(const FAdditionalTraitRequirements&) = delete; FAdditionalTraitRequirements& operator=(const FAdditionalTraitRequirements&) = delete; FAdditionalTraitRequirements& Add(const UScriptStruct* RequiredType) { TargetContainer.Add(RequiredType); return *this; } private: TArray<const UStruct*>& TargetContainer; }; /** Appends items into the entity template required for the trait. */ virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const PURE_VIRTUAL(UMassEntityTraitBase::BuildTemplate, return; ); virtual void DestroyTemplate(const UWorld& World) const {} /** * 모든 트레이트가 처리되고 프래그먼트 요구 사항이 확인된 후 호출됩니다. 이 함수를 재정의하여 * 추가적인 트레이트 구성 유효성 검사를 수행할 수 있습니다. `false`를 반환하면 트레이트 인스턴스가 * 유효성 검사 결과에 만족하지 않음을 나타내며, 이 결과는 오류로 처리됩니다. * @param OutTraitRequirements: 이 트레이트가 선언한 요구 사항을 포함하며, ValidateTemplate이 BuildContext의 상태를 기반으로 * 의존성을 추가할 수 있는 기회를 제공합니다. 이 시점에서 BuildContext에는 모든 관련 트레이트가 * 추가한 모든 요소가 포함되어 있어야 합니다. * @return: 유효성 검사가 성공했는지 여부 * Called once all traits have been processed and fragment requirements have been checked. Override this function * to perform additional Trait's configuration validation. Returning `false` will indicate that the trait instance * is not happy with the validation results - this result will be treated as an error. * @param OutTraitRequirements contains requirements declared by this trait and gives ValidateTemplate a chance to add * to the dependencies based on the state of BuildContext, which by this point should contain all the elements * added by all the relevant traits. * @return whether the validation was successful */ MASSSPAWNER_API virtual bool ValidateTemplate(const FMassEntityTemplateBuildContext& BuildContext, const UWorld& World, FAdditionalTraitRequirements& OutTraitRequirements) const;}; 아주 간단한 선언이다. 단순하게,
BuildTemplate 내부에서 BuildContext를 이용해서 프래그먼트를 추가하는 등의 동작을 수행할 수 있다. 또한 ValidationTemplate를 통해, 실제로 이 트레이트에게 유효하게 EntityConfig 구성이 되어있는지 검증할 수 있다. 하나 중요한 포인트는 Trait에 선언된 파라미터들은 EntityConfig에서 직접 수정할 수 있다.
void UInitStatsTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const{ FMassEntityManager& EntityManager = UE::Mass::Utils::GetEntityManagerChecked(World); BuildContext.AddTag<FDrawHealthBarTag>(); FStatFragment& StatFragment = BuildContext.AddFragment_GetRef<FStatFragment>(); StatFragment.Health = Spec.MaxHealth; StatFragment.MaxHealth = Spec.MaxHealth; StatFragment.AttackRange = Spec.AttackRange; BuildContext.AddFragment<FDeferredDamageFragment>(); const FConstSharedStruct HealthBarFragment = EntityManager.GetOrCreateConstSharedFragment(HealthBarParams); BuildContext.AddConstSharedFragment(HealthBarFragment);}위 이미지에 나온
UInitStatsTrait의 BuildTemplate 구현이다.BuildContext.AddTag<T>를 이용해 태그를 추가했다.프래그먼트 추가는 BuildContext.AddFragment<T>이지만, 만약 프래그먼트 내부 값을 미리 수정하고 싶다면, BuildContext.AddFragment_GetRef<T>를 통해 프래그먼트의 레퍼런스를 받아서 값을 수정할 수 있다.Fragment
Trait은 BuildContext에 Fragment들을 추가하는 역할을 하는
UCLASS 였다. Fragment는 기본적으로 데이터 컨테이너의 형식을 가지므로 USTRUCT로 작성된다.USTRUCT(BlueprintType)struct FMassAnimSharedFragment : public FMassConstSharedFragment{ GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) TMap<EMassAnimType, FAnimClips> AnimSettings; int32 GetClipIndex(const EMassAnimType AnimType) const { checkf(AnimSettings.Contains(AnimType), TEXT("%s is not found in AnimSettings"), *UEnum::GetValueAsString(AnimType)); return AnimSettings[AnimType].GetClipIndex(); } FAnimClip GetClip(const FEntityAnimFragment& AnimFragment) const { return AnimSettings[AnimFragment.CurrentAnimType].Clips[AnimFragment.ClipIndex]; }};위는 애니메이션 구현에 사용했던 프래그먼트의 한 종류이다.기본적으로
AnimSettings라는 세팅값을 저장하는 프래그먼트고, 외부에서 TMap 접근해서 데이터 처리하기에 용이하도록 함수들이 구현되어있다.프래그먼트는 Const와 Shared가 붙을 수 있다. 이름과 동일하게 Const는 상수처럼 동작할 때, Shared는 하나의 아키타입 내의 엔티티들이 공유할 때 사용한다.예를 들어, 대부분의 동일한 몬스터들은 동일한 공격력을 가질것이다. 이런 공격력 값은 각각의 엔티티가 보유하면 중복값인 경우가 아주아주 많을 것이다. 이런 데이터는 Shared로 처리해볼 수 있다.또한, 같은 타입의 몬스터들의 애니메이션 프레임 정보는 동일할 것이고, 또 런타임에 바뀔 일도 없을 것이다. 이경우 Const Shared를 사용할 수 있다.일반 프래그먼트는 아키타입 내 엔티티 수만큼의 공간을 차지하므로, 세팅값처럼 중복이 많은 경우나 그 데이터 크기가 큰 경우에는 Shared로 처리하기 위해 프래그먼트를 분리하는 등의 전략을 가질 수 있다.