ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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
Designed by Tistory.