[UE5] PawnExtension(2) - 구성 요소 분석
PawnExtensionComponent의 구성 요소를 하나하나 분석해보도록 하자.
const FName UZSPawnExtensionComponent::NAME_ActorFeatureName("PawnExtension");
- 이 문자열은 Feature Name으로 사용되며, 주로 초기화 상태 등록(Init State Feature Registration)이나 기능 구현 등록 시 식별자로 활용된다.
virtual FName GetFeatureName() const final { return NAME_ActorFeatureName; }
- 헤더 파일에서 GetFeatureName함수는 위와 같이 override되는데, 이 때문에 RegisterInitStateFeature 함수가 호출되면 GetFeatureName이 호출되어 이 컴포넌트의 FeatureName에 "PawnExtension"이 등록되게 된다.
void UZSPawnExtensionComponent::OnRegister()
{
Super::OnRegister();
// Verifying that it is registered to the correct Actor
{
if (!GetPawn<APawn>())
{
UE_LOG(LogZS, Error, TEXT("this component has been added to a BP whose base class is not a Pawn!"));
return;
}
RegisterInitStateFeature();
// Debugging function
UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(GetOwningActor());
}
}
void UZSPawnExtensionComponent::BeginPlay()
{
Super::BeginPlay();
// Detect all state changes for all components.
BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);
// Change State InitState_Spawned
ensure(TryToChangeInitState(FZSGameplayTags::Get().InitState_Spawned));
// ForceUpdateInitState
CheckDefaultInitialization();
}
- BindOnActorInitStateChanged함수는 모든 컴포넌트에서(NAME_NONE, 특정 FeatureName이 포함시에는 그것만 탐색) 모든 상태가 변경될때마다(FGamePlayTag(), 특정 상태가 설정된다면 그 상태로 변경될 경우에만 탐색) 액터의 초기화 상태가 변경될 때 특정 로직을 바인딩 하는 역할을 한다.
- TryToChangeInitState함수는 현재 객체의 초기화 상태(Init State)를 InitState_Spawned로 변경하려고 시도한다.
void UZSPawnExtensionComponent::CheckDefaultInitialization()
{
CheckDefaultInitializationForImplementers();
const FZSGameplayTags& InitTags = FZSGameplayTags::Get();
static const TArray<FGameplayTag> StateChain = { InitTags.InitState_Spawned, InitTags.InitState_DataAvailable, InitTags.InitState_DataInitialized, InitTags.InitState_GameplayReady };
ContinueInitStateChain(StateChain);
}
- 정의된 StateChain을 기반으로 초기화 상태를 순차적으로 진행하는 로직
- 현재 컴포넌트가 어느 초기화 상태에 있는지 확인한 후, 다음 단계로 넘어갈 준비가 되었으면 해당 상태로 전환
- 더 이상 진행할 수 없을 때까지 상태를 변경하기 때문에 ForceUpdate라고도 할 수 있음
- CheckDefaultInitializationForImplementers함수의 경우 하위 컴포넌트인 Hero컴포넌트에는 존재하지 않는데, 이 함수는 자신이 관리하는 하위 컴포넌트나 의존성 컴포넌트의 시스템 초기화를 처리하는 로직이기 때문
bool UZSPawnExtensionComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState,
FGameplayTag DesiredState) const
{
check(Manager);
APawn* Pawn = GetPawn<APawn>();
const FZSGameplayTags& InitTags = FZSGameplayTags::Get();
// Initialize InitState_Spawned
if (!CurrentState.IsValid() && DesiredState == InitTags.InitState_Spawned)
{
// Pawn이 잘 세팅만 되어있으면 바로 Spawned로 넘어감
if (Pawn)
{
return true;
}
}
// Spawned -> DataAvailable
if (CurrentState == InitTags.InitState_Spawned && DesiredState == InitTags.InitState_DataAvailable)
{
if (!PawnData)
{
return false;
}
// LocallyControlled인 Pawn이 Controller가 없으면 에러
const bool bIsLocallyControlled = Pawn->IsLocallyControlled();
if (bIsLocallyControlled)
{
if (!GetController<AController>())
{
return false;
}
}
return true;
}
// DataAvailable -> DataInitialized
if (CurrentState == InitTags.InitState_DataAvailable && DesiredState == InitTags.InitState_DataInitialized)
{
// Actor에 바인드된 모든 Feature들이 DataAvailable 상태일 때, DataInitialized로 넘어감
// - HaveAllFeaturesReachedInitState 확인
return Manager->HaveAllFeaturesReachedInitState(Pawn, InitTags.InitState_DataAvailable);
}
// DataInitialized -> GameplayReady
if (CurrentState == InitTags.InitState_DataInitialized && DesiredState == InitTags.InitState_GameplayReady)
{
return true;
}
// 위의 선형적인(linear) transition이 아니면 false
return false;
}
- CanChangeInitState는 상태를 변경할 수 있을지를 체크하는 함수
- InitState의 네 단계를 차례로 순회하면서 조건을 만족하는지 체크한다.
- DataAvailable -> DataInitialized로 넘어가는 부분에서 PawnExtension컴포넌트의 경우 하위의 모든 컴포넌트가 모두 변경되어야 넘어간다. 하위 컴포넌트에서는 부모의 PawnExtension컴포넌트만 참조해서 부모의 상태와 동일하게 맞춘다. 즉, 자식의 컴포넌트의 상태가 모두 available할 떄까지 못 넘어가게 막아주는 역할이다.
// DataAvailable -> DataInitialized
if (CurrentState == InitTags.InitState_DataAvailable && DesiredState == InitTags.InitState_DataInitialized)
{
// PawnExtensionComponent가 DataInitialized될 때까지 기다림 (== 모든 Feature Component가 DataAvailable인 상태)
return ZSPS && Manager->HasFeatureReachedInitState(Pawn, UZSPawnExtensionComponent::NAME_ActorFeatureName, InitTags.InitState_DataInitialized);
}

그런데 코드를 살펴보면 PawnData를 체크하는 로직이 있는데, PawnData를 받아오는 로직이 코드에 없다. 이는 어디서 일어날까?
PawnData는 Character를 Spawn할 경우 활용되는 데이터이다.
-> Experience 로딩 이후, Character가 Spawn되고 나서 PawnData를 설정하면 된다는 뜻.
-> 그러기 위해, 앞서 오버라이드 했던 SpawnDefaultPawnAtTransform_Implementation을 재구현 해주어야한다.
APawn* AZSGameModeBase::SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer,
const FTransform& SpawnTransform)
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.Instigator = GetInstigator();
SpawnInfo.ObjectFlags |= RF_Transient;
SpawnInfo.bDeferConstruction = true;
if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
{
if (APawn* SpawnedPawn = GetWorld()->SpawnActor<APawn>(PawnClass, SpawnTransform, SpawnInfo))
{
// FindPawnExtensionComponent 구현
if (UZSPawnExtensionComponent* PawnExtComp = UZSPawnExtensionComponent::FindPawnExtensionComponent(SpawnedPawn))
{
if (const UZSPawnData* PawnData = GetPawnDataForController(NewPlayer))
{
PawnExtComp->SetPawnData(PawnData);
}
}
SpawnedPawn->FinishSpawning(SpawnTransform);
return SpawnedPawn;
}
}
return nullptr;
}
- GameModeBase.cpp의 SpawnDefaultPawnAtTransform_Implementation에서
PawnExtComp->SetPawnData(PawnData)를 통해 PawnData를 업데이트 해준다.
다음 문제, PlayerController의 생성 시점에 관한 문제이다.


그림에서 보는 것처럼 Possess->HakCharacter 단계에서 PlayerController가 설정되는데 BeginPlay에서 PlayerController가 필요한 시점은 훨씬 앞이다. 따라서 여기서 잠시 상태의 업데이트가 멈추가 된다. 이 문제는 어떻게 해결해야 할까?
물론, PlayerController가 생성 되었을 때 상태 업데이트 하라고 다시 호출해주면 된다.
void AZSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Pawn이 Possess로서, Controller와 PlayerState 정보 접근이 가능한 상태가 되었음:
// - SetupPlayerInputComponent 확인
PawnExtComponent->SetupPlayerInputComponent();
}
도식에서 보듯이, Possess 이후에 SetupPlayerInputComponent가 실행이 되는데, 여기서 PawnExtComponent의 SetupPlayerInputComponent를 실행해준다. 여기에는 뭐가 담겨있냐면
void UZSPawnExtensionComponent::SetupPlayerInputComponent()
{
// ForceUpdate
CheckDefaultInitialization();
}
CheckDefaultInitialization이 또 다시 실행된다! 강제로 초기화 과정을 다시 실행한다는 것이다.
이런 식으로 Pawn의 초기화 과정은 문제가 생길 때마다 언리얼의 생성 주기를 잘 파악해서 다시 초기화 과정을 반복해서 실행해주는 방식으로 구성되어있다... 엔진 구조에 대한 자세한 이해가 필수적이라는것..!
결국 중요한 것은 두 가지이다.
- InitState를 활용한 초기화에서 우리가 기억해야할 것은 선형적 구현이다.
- PawnExtension을 활용하거나 이와 비슷한 롤을 담당하는 Component를 통해 초기화의 흐름을 제어해주자.
이 일련의 과정 - 게임 프레임워크 컴포넌트 매니저를 통한 초기화 과정은 언리얼 공식 문서
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-framework-component-manager-in-unreal-engine
의 설명을 참조하면 좋다.