UE5/Lyra Clone Coding

[UE5] PawnExtension(2) - 구성 요소 분석

검정색필통 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

의 설명을 참조하면 좋다.