struct FLyraGameplayTags
{
public:
static const FLyraGameplayTags& Get() { return GameplayTags; }
static void InitializeNativeTags();
static FGameplayTag FindTagByString(FString TagString, bool bMatchPartialString = false);
public:
// ...
FGameplayTag InputTag_Move;
FGameplayTag InputTag_Look_Mouse;
FGameplayTag InputTag_Look_Stick;
FGameplayTag InputTag_Crouch;
FGameplayTag InputTag_AutoRun;
// ...
TMap<uint8, FGameplayTag> MovementModeTagMap;
TMap<uint8, FGameplayTag> CustomMovementModeTagMap;
protected:
void AddAllTags(UGameplayTagsManager& Manager);
void AddTag(FGameplayTag& OutTag, const ANSICHAR* TagName, const ANSICHAR* TagComment);
void AddMovementModeTag(FGameplayTag& OutTag, const ANSICHAR* TagName, uint8 MovementMode);
void AddCustomMovementModeTag(FGameplayTag& OutTag, const ANSICHAR* TagName, uint8 CustomMovementMode);
private:
static FLyraGameplayTags GameplayTags;
};
static 변수인 GameplayTags를 가지고 있다. 즉 Singletone으로 사용
헤더에서 선언한 FGameplayTag 변수를 에디터에서 추가할 때처럼 추가해보자
void FLyraGameplayTags::InitializeNativeTags()
{
UGameplayTagsManager& Manager = UGameplayTagsManager::Get();
GameplayTags.AddAllTags(Manager);
// ...
}
void FLyraGameplayTags::AddAllTags(UGameplayTagsManager& Manager)
{
// ...
AddTag(InputTag_Move, "InputTag.Move", "Move input.");
AddTag(InputTag_Look_Mouse, "InputTag.Look.Mouse", "Look (mouse) input.");
AddTag(InputTag_Look_Stick, "InputTag.Look.Stick", "Look (stick) input.");
AddTag(InputTag_Crouch, "InputTag.Crouch", "Crouch input.");
AddTag(InputTag_AutoRun, "InputTag.AutoRun", "Auto-run input.");
// ...
}
void FLyraGameplayTags::AddTag(FGameplayTag& OutTag, const ANSICHAR* TagName, const ANSICHAR* TagComment)
{
OutTag = UGameplayTagsManager::Get().AddNativeGameplayTag(FName(TagName), FString(TEXT("(Native) ")) + FString(TagComment));
}
InitializeNativeTags함수에서 헤더에 선언한 게임플레이 태그들을 에디터에 추가한다.
AddTag(InputTag_Move, "InputTag.Move", "Move input.");
아까 GameplayTag는 계층화된 enum이라고 했다.
이 코드는 InputTag>Move 태그를 에디터에 추가하는 코드다.
InputTag_Move: 에디터에 추가한 게임플레이 태그를 C++에서 사용하기 위한 변수
void ULyraAssetManager::InitializeAbilitySystem()
{
FLyraGameplayTags::InitializeNativeTags();
// ...
}
그렇다면 FLyraGameplayTags::InitializeNativeTags는 언제 호출될까
ULyraAssetManager::InitializeAbilitySystem에서, 즉 에디터가 켜지기도 전에 호출된다.
void ULyraGameInstance::Init()
{
Super::Init();
// Register our custom init states
UGameFrameworkComponentManager* ComponentManager = GetSubsystem<UGameFrameworkComponentManager>(this);
if (ensure(ComponentManager))
{
const FLyraGameplayTags& GameplayTags = FLyraGameplayTags::Get();
ComponentManager->RegisterInitState(GameplayTags.InitState_Spawned, false, FGameplayTag());
ComponentManager->RegisterInitState(GameplayTags.InitState_DataAvailable, false, GameplayTags.InitState_Spawned);
ComponentManager->RegisterInitState(GameplayTags.InitState_DataInitialized, false, GameplayTags.InitState_DataAvailable);
ComponentManager->RegisterInitState(GameplayTags.InitState_GameplayReady, false, GameplayTags.InitState_DataInitialized);
}
}
UGameFrameworkComponentManager와 GameplayTag를 이용해 Component들의 초기화 순서를 정할 수 있다.
초기화 순서를 정하는 이유: B 컴포넌트는 A 컴포넌트의 초기화 이후에 초기화 가능한 경우들이 있다. (ex. A:스킬 B:이펙트)
Lyra에서는 AModularGameplayActor를 상속받는 ALyraCharacter를 사용하고 있다.
ALyraGameMode::SpawnDefaultPawnAtTransform_Implementation 참고APawn::PossessedBy에서 Controller 세팅UCLASS(Config = Game, Meta = (ShortTooltip = "The base character pawn class used by this project."))
class LYRAGAME_API ALyraCharacter : public AModularCharacter, public IAbilitySystemInterface, public IGameplayCueInterface, public IGameplayTagAssetInterface, public ILyraTeamAgentInterface
{
GENERATED_BODY()
// ..
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Lyra|Character", Meta = (AllowPrivateAccess = "true"))
TObjectPtr<ULyraPawnExtensionComponent> PawnExtComponent;
// ..
};
ALyraCharacter::ALyraCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<ULyraCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
// Tick 비활성화하고 이벤트로 초기화해서 효율적
// LyraHeroComponent,LyraPawnExtensionComponent도 마찬가지라는데 얘네는 생성자에 안보인다..
PrimaryActorTick.bCanEverTick = false;
PawnExtComponent = CreateDefaultSubobject<ULyraPawnExtensionComponent>(TEXT("PawnExtensionComponent"));
PawnExtComponent->OnAbilitySystemInitialized_RegisterAndCall(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnAbilitySystemInitialized));
PawnExtComponent->OnAbilitySystemUninitialized_Register(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnAbilitySystemUninitialized));
// ..
}
이렇게 Native C++에서 LyraPawnExtensionComponent을 생성하고

에디터로 LyraHeroComponent를 추가하면
언리얼 엔진에서 에디터보다 Native C++을 먼저 실행하기 때문에 LyraPawnExtensionComponent가 먼저 생성됨을 보장할 수 있다.
초기화 순서 등을 관리하기 위해서는 UGameFrameworkComponentManager가 필요하다.
따라서 Actor에 컴포넌트를 부착할 때 UGameFrameworkComponentManager에 해당 컴포넌트를 등록한다.
= UGameFrameworkComponentManager에는 어떤 인스턴스에 어떤 컴포넌트가 부착되어있는지 저장하게 된다.
// 컴포넌트를 부착할 때 호출되는 함수
// UActorComponent::OnRegister() 오버라이드
void ULyraPawnExtensionComponent::OnRegister()
{
Super::OnRegister();
// OnRegister에서 올바른 Actor에 해당 컴포넌트가 제대로 부착되었는지 확인
const APawn* Pawn = GetPawn<APawn>();
ensureAlwaysMsgf((Pawn != nullptr), TEXT("LyraPawnExtensionComponent on [%s] can only be added to Pawn actors."), *GetNameSafe(GetOwner()));
TArray<UActorComponent*> PawnExtensionComponents;
Pawn->GetComponents(ULyraPawnExtensionComponent::StaticClass(), PawnExtensionComponents);
ensureAlwaysMsgf((PawnExtensionComponents.Num() == 1), TEXT("Only one LyraPawnExtensionComponent should exist on [%s]."), *GetNameSafe(GetOwner()));
// 상속받은 IGameFrameworkInitStateInterface의 함수
RegisterInitStateFeature();
}
void IGameFrameworkInitStateInterface::RegisterInitStateFeature()
{
UObject* ThisObject = Cast<UObject>(this);
AActor* MyActor = GetOwningActor();
UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
// FeatureName: 컴포넌트의 이름
// GetFeatureName: 각 컴포넌트에서 오버라이드해서 정의
const FName MyFeatureName = GetFeatureName();
if (MyActor && Manager)
{
Manager->RegisterFeatureImplementer(MyActor, MyFeatureName, ThisObject);
}
}
bool UGameFrameworkComponentManager::RegisterFeatureImplementer(AActor* Actor, FName FeatureName, UObject* Implementer)
{
if (Actor == nullptr || FeatureName.IsNone())
{
// TODO Ensure?
return false;
}
// 해당 Actor 인스턴스의 ActorStruct 구조체를 가져온다
// 같은 클래스여도 FObjectKey를 통해 다른 인스턴스임을 구분할 수 있다
FActorFeatureData& ActorStruct = FindOrAddActorData(Actor);
FActorFeatureState* FoundState = nullptr;
for (FActorFeatureState& State : ActorStruct.RegisteredStates) // RegisteredStates: 해당 Actor의 컴포넌트 정보 리스트
{
if (State.FeatureName == FeatureName)
{
FoundState = &State;
}
}
if (!FoundState)
{
FoundState = &ActorStruct.RegisteredStates.Emplace_GetRef(FeatureName);
}
FoundState->Implementer = Implementer; // Implementer: 컴포넌트
return true;
}
UGameFrameworkComponentManager::FActorFeatureData& UGameFrameworkComponentManager::FindOrAddActorData(AActor* Actor)
{
check(Actor);
// FObjectKey: 동일한 CDO로부터 생성된 인스턴스들을 구분하기 위한 키
FActorFeatureData& ActorStruct = ActorFeatureMap.FindOrAdd(FObjectKey(Actor));
if (!ActorStruct.ActorClass.IsValid())
{
ActorStruct.ActorClass = Actor->GetClass();
}
return ActorStruct;
}
void ULyraPawnExtensionComponent::BeginPlay()
{
Super::BeginPlay();
// BindOnActorInitStateChanged: 어떤 컴포넌트의 InitState변경에 대한 이벤트 등록 (ULyraGameInstance::Init 참고)
// PawnExtensionComponent는 모든 컴포넌트들의 초기화 동작을 관리하므로
// 모든 컴포넌트(NAME_None)의 모든 상태 변경(FGameplayTag())에 대한 이벤트를 받음
BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);
// 나의 InitState 변경 시도
ensure(TryToChangeInitState(FLyraGameplayTags::Get().InitState_Spawned));
// 나를 포함한 모든 컴포넌트의 InitState를 가능한 State까지 업데이트
CheckDefaultInitialization();
}
// Actor가 파괴되는 시점에 호출
void ULyraPawnExtensionComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// ..
// OnRegister에서 등록했던 InitStateFeature를 반드시 제거
UnregisterInitStateFeature();
Super::EndPlay(EndPlayReason);
}
나머지 함수들은 ULyraPawnExtensionComponent와 거의 동일해서 패스
void ULyraHeroComponent::BeginPlay()
{
Super::BeginPlay();
// 초기화를 담당하는 LyraPawnExtensionComponent의 State만 관찰
BindOnActorInitStateChanged(ULyraPawnExtensionComponent::NAME_ActorFeatureName, FGameplayTag(), false);
ensure(TryToChangeInitState(FLyraGameplayTags::Get().InitState_Spawned));
// 나의 InitState를 가능한 State까지 업데이트
// ULyraPawnExtensionComponent::CheckDefaultInitialization로도 호출된다
CheckDefaultInitialization();
}
Lyra InitState 과정
InitState_Spawned > InitState_DataAvailable > InitState_DataInitialized > InitState_GameplayReady
bool ULyraPawnExtensionComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
check(Manager);
APawn* Pawn = GetPawn<APawn>();
const FLyraGameplayTags& InitTags = FLyraGameplayTags::Get();
if (!CurrentState.IsValid() && DesiredState == InitTags.InitState_Spawned)
{
// As long as we are on a valid pawn, we count as spawned
if (Pawn)
{
return true;
}
}
if (CurrentState == InitTags.InitState_Spawned && DesiredState == InitTags.InitState_DataAvailable)
{
// ALyraGameMode::SpawnDefaultPawnAtTransform_Implementation 참고
if (!PawnData)
{
return false;
}
const bool bHasAuthority = Pawn->HasAuthority();
const bool bIsLocallyControlled = Pawn->IsLocallyControlled();
if (bHasAuthority || bIsLocallyControlled)
{
// Owner Actor의 Controller가 있는지 확인
// APawn::PossessedBy에서 Controller를 세팅하므로 그 이후에 InitState_DataAvailable로 변경 가능하다
// APawn::PossessedBy가 완료된 이후(Controller가 세팅된 이후) 호출되는 ALyraCharacter::SetupPlayerInputComponent에서
// ExtensionComponent의 CheckDefaultInitialization를 호출해서 State를 변경해준다.
if (!GetController<AController>())
{
return false;
}
}
return true;
}
else if (CurrentState == InitTags.InitState_DataAvailable && DesiredState == InitTags.InitState_DataInitialized)
{
// Transition to initialize if all features have their data available
return Manager->HaveAllFeaturesReachedInitState(Pawn, InitTags.InitState_DataAvailable);
}
else if (CurrentState == InitTags.InitState_DataInitialized && DesiredState == InitTags.InitState_GameplayReady)
{
return true;
}
return false;
}
void ULyraPawnExtensionComponent::OnActorInitStateChanged(const FActorInitStateChangedParams& Params)
{
// If another feature is now in DataAvailable, see if we should transition to DataInitialized
if (Params.FeatureName != NAME_ActorFeatureName)
{
const FLyraGameplayTags& InitTags = FLyraGameplayTags::Get();
if (Params.FeatureState == InitTags.InitState_DataAvailable)
{
CheckDefaultInitialization();
}
}
}
bool ULyraHeroComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
check(Manager);
const FLyraGameplayTags& InitTags = FLyraGameplayTags::Get();
APawn* Pawn = GetPawn<APawn>();
if (!CurrentState.IsValid() && DesiredState == InitTags.InitState_Spawned)
{
// As long as we have a real pawn, let us transition
if (Pawn)
{
return true;
}
}
else if (CurrentState == InitTags.InitState_Spawned && DesiredState == InitTags.InitState_DataAvailable)
{
// The player state is required.
if (!GetPlayerState<ALyraPlayerState>())
{
return false;
}
// If we're authority or autonomous, we need to wait for a controller with registered ownership of the player state.
if (Pawn->GetLocalRole() != ROLE_SimulatedProxy)
{
AController* Controller = GetController<AController>();
const bool bHasControllerPairedWithPS = (Controller != nullptr) && \
(Controller->PlayerState != nullptr) && \
(Controller->PlayerState->GetOwner() == Controller);
if (!bHasControllerPairedWithPS)
{
return false;
}
}
const bool bIsLocallyControlled = Pawn->IsLocallyControlled();
const bool bIsBot = Pawn->IsBotControlled();
if (bIsLocallyControlled && !bIsBot)
{
ALyraPlayerController* LyraPC = GetController<ALyraPlayerController>();
// The input component and local player is required when locally controlled.
if (!Pawn->InputComponent || !LyraPC || !LyraPC->GetLocalPlayer())
{
return false;
}
}
return true;
}
else if (CurrentState == InitTags.InitState_DataAvailable && DesiredState == InitTags.InitState_DataInitialized)
{
// Wait for player state and extension component
ALyraPlayerState* LyraPS = GetPlayerState<ALyraPlayerState>();
return LyraPS && Manager->HasFeatureReachedInitState(Pawn, ULyraPawnExtensionComponent::NAME_ActorFeatureName, InitTags.InitState_DataInitialized);
}
else if (CurrentState == InitTags.InitState_DataInitialized && DesiredState == InitTags.InitState_GameplayReady)
{
return true;
}
return false;
}
void ULyraHeroComponent::OnActorInitStateChanged(const FActorInitStateChangedParams& Params)
{
if (Params.FeatureName == ULyraPawnExtensionComponent::NAME_ActorFeatureName)
{
if (Params.FeatureState == FLyraGameplayTags::Get().InitState_DataInitialized)
{
// If the extension component says all all other components are initialized, try to progress to next state
CheckDefaultInitialization();
}
}
}
InitState_DataAvailable > InitState_DataInitialized 과정만 봤을 때 (다른 부분은 주석으로 설명..)
PawnExtensionComponent을 제외한 모든 컴포넌트가 InitState_DataAvailable
=> ULyraPawnExtensionComponent::OnActorInitStateChanged: PawnExtensionComponent가 InitState_DataInitialized로 상태 변경
=> LyraHeroComponent::OnActorInitStateChanged: 자신의 상태를 InitState_DataInitialized로 변경
이런 식으로 tick 없이 이벤트를 통해 ULyraPawnExtensionComponent와 그 외 컴포넌트들의 초기화 과정을 수행한다.