-
[UE5] PawnExtension(2) - 구성 요소 분석UE5/Lyra Clone Coding 2025. 2. 3. 19:24
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를 받아오는 로직이 코드에 없다. 이는 어디서 일어날까?
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의 생성 시점에 관한 문제이다.
Controller의 생성 시점은? 그림에서 보는 것처럼 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의 설명을 참조하면 좋다.
'UE5 > Lyra Clone Coding' 카테고리의 다른 글
[UE5] CommonUser (0) 2025.02.06 [UE5] Camera (0) 2025.02.05 [UE5] PawnExtension (0) 2025.01.31 [UE5] Experience (0) 2025.01.23 [UE5] AssetManager Scan (1) 2025.01.21