1일1알

Lyra 분석 - IGameFrameworkInitStateInterface 본문

언리얼/Lyra 프로젝트 분석

Lyra 분석 - IGameFrameworkInitStateInterface

영춘권의달인 2024. 7. 10. 12:32

개인적으로 Lyra 프로젝트를 분석한 것이고, 틀린 내용이 있을 수 있습니다.

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Lyra|Character", Meta = (AllowPrivateAccess = "true"))
TObjectPtr<ULyraPawnExtensionComponent> PawnExtComponent;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Lyra|Character", Meta = (AllowPrivateAccess = "true"))
TObjectPtr<ULyraHealthComponent> HealthComponent;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Lyra|Character", Meta = (AllowPrivateAccess = "true"))
TObjectPtr<ULyraCameraComponent> CameraComponent;

LyraCharacter의 코드에서는 ULyraPawnExtensionComponent, HealthComponent, CameraComponent컴포넌트를 가지고 있고 LyraChacater기반으로 만든 캐릭터의 블루프린트에서는 LyraHeroComponent 컴포넌트를 추가하여 사용하고 있다.

HealthComponent와 CameraComponent는 이름만 봐도 직관적으로 무슨 역할을 하는지 추측할 수 있지만 ULyraPawnExtensionComponent와 LyraHeroComponent는 살펴볼 필요가 있을 것 같다.

/**
 * Component that adds functionality to all Pawn classes so it can be used for characters/vehicles/etc.
 * This coordinates the initialization of other components.
 */
UCLASS()
class LYRAGAME_API ULyraPawnExtensionComponent : public UPawnComponent, public IGameFrameworkInitStateInterface

일단 눈여겨볼만한 점은 IGameFrameworkInitStateInterface라는 인터페이스를 상속받고 있다.

그리고 위에 달린 주석을 보면 모든 Pawn클래스에 추가할 수 있고 다른 컴포넌트들의 초기화를 도와주는 컴포넌트라고 한다. 음.. 주석만 봐서는 잘 와닿지가 않는다. 처음부터 천천히 살펴보자

void ULyraPawnExtensionComponent::OnRegister()
{
	Super::OnRegister();

	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()));

	// Register with the init state system early, this will only work if this is a game world
	RegisterInitStateFeature();
}

OnRegister함수에서 RegisterInitStateFeature라는 함수를 실행해주는데, 이 함수는 IGameFrameworkInitStateInterface인터페이스에서 선언된 함수이다.

내부를 살펴봤을때 UGameFrameworkComponentManager라는 UGameInstanceSubsystem를 상속받은 서브시스템에 자기를 등록하는 로직을 수행한다.

UGameFrameworkComponentManager는 아마 등록한 IGameFrameworkInitStateInterface들을 추적하고 관리하는 역할을 하는 것 같다.

void ULyraPawnExtensionComponent::BeginPlay()
{
	Super::BeginPlay();

	// Listen for changes to all features
	BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);
	
	// Notifies state manager that we have spawned, then try rest of default initialization
	ensure(TryToChangeInitState(LyraGameplayTags::InitState_Spawned));
	CheckDefaultInitialization();
}

BeginPlay에서는 우선 BindOnActorInitStateChanged라는 함수를 호출해주는데 이 함수는 IGameFrameworkInitStateInterface에서 선언된 함수이다.

void IGameFrameworkInitStateInterface::BindOnActorInitStateChanged(FName FeatureName, FGameplayTag RequiredState, bool bCallIfReached)
{
	UObject* ThisObject = Cast<UObject>(this);
	AActor* MyActor = GetOwningActor();
	UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);

	if (ensure(MyActor && Manager))
	{
		// Bind as a weak lambda because this is not a UObject but is guaranteed to be valid as long as ThisObject is
		FActorInitStateChangedDelegate Delegate = FActorInitStateChangedDelegate::CreateWeakLambda(ThisObject,
			[this](const FActorInitStateChangedParams& Params)
			{
				this->OnActorInitStateChanged(Params);
			});

		ActorInitStateChangedHandle = Manager->RegisterAndCallForActorInitState(MyActor, FeatureName, RequiredState, MoveTemp(Delegate), bCallIfReached);
	}
}

내부를 살펴보면 OnActorInitStateChanged함수를 델리게이트로 만들어서 UGameFrameworkComponentManager에 있는 함수를 통해 등록해주는 모습이다.

여기까지만 보면 초기화의 상태가 변경될때 OnActorInitStateChanged함수가 호출되도록 해주는 로직인 것 같다.

ensure(TryToChangeInitState(LyraGameplayTags::InitState_Spawned));

BindOnActorInitStateChanged 호출 뒤에는 TryToChangeInitState함수를 실행하는데, 인자로 DesiredState라는 이름의 LyraGameplayTags::InitState_Spawned를 넘겨준다. 초기화 상태를 InitState_Spawned로 변경하려는 시도를 하는 함수인 것 같다.

LYRAGAME_API	UE_DECLARE_GAMEPLAY_TAG_EXTERN(InitState_Spawned);
LYRAGAME_API	UE_DECLARE_GAMEPLAY_TAG_EXTERN(InitState_DataAvailable);
LYRAGAME_API	UE_DECLARE_GAMEPLAY_TAG_EXTERN(InitState_DataInitialized);
LYRAGAME_API	UE_DECLARE_GAMEPLAY_TAG_EXTERN(InitState_GameplayReady);

LyraGameplayTag에 가서 살펴보니 InitState관련 게임플레이 태그가 4개가 있는 것을 확인할 수 있다.

아마 초기화의 과정을 4단계로 나누고 순서대로 초기화를 진행하면서 현재 초기화 상태를 이 태그를 통해 판별하고 초기화 과정에 따른 작업을 수행하는 흐름인 것 같다.

bool IGameFrameworkInitStateInterface::TryToChangeInitState(FGameplayTag DesiredState)
{
	// 이것저것 체크하는듯
    
	// Perform the local change
	HandleChangeInitState(Manager, CurrentState, DesiredState);

	// The local change has completed, notify the system to register change and execute callbacks
	return ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));
}

TryToChangeInitState의 내부를 살펴보면 DesiredState로 갈 수 있는 상황인지 여러가지 확인을 하고 넘어갈 수 있다면 HandleChangeInitState까지 넘어온다.

초기화 상태에 따라서 어떤 작업을 수행하고자 한다면 해당 함수에 내용을 채워넣으면 될 것 같다.

 

그리고 ChangeFeatureInitState함수가 실행되는데, GameFrameworkComponentManager에서 관리하고 있는 다른 GameFrameworkInitStateInterface들에게 초기화 상태가 변경됐다는 것을 알려주는 함수이다.

여기서는 GameFrameworkComponentManager가 관리하고 있는 GameFrameworkInitStateInterface에 대해  BindOnActorInitStateChanged를 통해 등록된 OnActorInitStateChanged가 호출되도록 한다.

void ULyraPawnExtensionComponent::CheckDefaultInitialization()
{
	// Before checking our progress, try progressing any other features we might depend on
	CheckDefaultInitializationForImplementers();

	static const TArray<FGameplayTag> StateChain = { LyraGameplayTags::InitState_Spawned, LyraGameplayTags::InitState_DataAvailable, LyraGameplayTags::InitState_DataInitialized, LyraGameplayTags::InitState_GameplayReady };

	// This will try to progress from spawned (which is only set in BeginPlay) through the data initialization stages until it gets to gameplay ready
	ContinueInitStateChain(StateChain);
}

다음 CheckDefaultInitialization를 살펴보면 LyraPawnExtensionComponent에서는 CheckDefaultInitializationForImplementers를 먼저 실행해주는데, 이 함수는 Manager에서 관리하고 있는 다른 GameFrameworkInitStateInterface들의 CheckDefaultInitialization함수를 실행하도록 해주는 함수이다.

GameFrameworkInitStateInterface를 상속받는 다른 컴포넌트인 LyraHeroComponent에서는 해당 함수를 실행하지 않는것을 보니 LyraPawnExtensionComponent가 초기화 과정을 담당하는 리더 역할을 하는 것 같다.

 

다음으로는 초기화 과정들을 모아놓은 StateChain을 생성하여 ContinueInitStateChain함수에 넘겨준다.

FGameplayTag IGameFrameworkInitStateInterface::ContinueInitStateChain(const TArray<FGameplayTag>& InitStateChain)
{
	UObject* ThisObject = Cast<UObject>(this);
	AActor* MyActor = GetOwningActor();
	UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
	const FName MyFeatureName = GetFeatureName();

	if (!Manager || !ThisObject || !MyActor)
	{
		return FGameplayTag();
	}

	int32 ChainIndex = 0;
	FGameplayTag CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);

	// For each state in chain before the last, see if we can transition to the next state
	while (ChainIndex < InitStateChain.Num() - 1)
	{
		if (CurrentState == InitStateChain[ChainIndex])
		{
			FGameplayTag DesiredState = InitStateChain[ChainIndex + 1];
			if (CanChangeInitState(Manager, CurrentState, DesiredState))
			{
				UE_LOG(LogModularGameplay, Verbose, TEXT("ContinueInitStateChain: Transitioning %s:%s (role %d) from %s to %s"),
					*MyActor->GetName(), *MyFeatureName.ToString(), MyActor->GetLocalRole(), *CurrentState.ToString(), *DesiredState.ToString());

				// Perform the local change
				HandleChangeInitState(Manager, CurrentState, DesiredState);

				// The local change has completed, notify the system to register change and execute callbacks
				ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));

				// Update state and check again
				CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);
			}
			else
			{
				UE_LOG(LogModularGameplay, Verbose, TEXT("ContinueInitStateChain: Cannot transition %s:%s (role %d) from %s to %s"),
					*MyActor->GetName(), *MyFeatureName.ToString(), MyActor->GetLocalRole(), *CurrentState.ToString(), *DesiredState.ToString());
			}
		}

		ChainIndex++;
	}

	return CurrentState;
}

ContinueInitStateChain함수에서는 현재 상태와 Chain으로 넘겨받은 상태들을 비교하면서 같은 상태가 오면 그 시점부터 다음 상태로 넘어갈 수 있는지 확인하여 다음 상태로 넘어가려는 시도를 해준다.

여기서도 역시 Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState)를 실행해주면서 상태의 변경을 다른 컴포넌트들에게 알려준다.

 

여기까지의 내용을 토대로 대략적으로 정리해보면

1. GameFrameworkInitStateInterface를 상속받은 컴포넌트들은 초기화 과정이 진행될 때 초기화 상태에 따라 필요한 로직을 수행하도록 설계할 수 있다.

2. GameFrameworkInitStateInterface들은 GameFrameworkComponentManager를 통해 관리되고, 컴포넌트의 초기화 상태의 변경이 감지되면 GameFrameworkComponentManager를 통해 관리되는 다른 컴포넌트들에게도 전달해준다.

3. 초기화 상태 변화에 따라 필요한 로직들을 HandleChangeInitState, OnActorInitStateChanged에서 작성해준다. HandleChangeInitState : 초기화 상태가 변경되기 직전에 실행할 함수, OnActorInitStateChanged : 다른 컴포넌트의 초기화 상태 변경이 감지될 때 실행되는 함수

 

왜 이런 시스템을 사용할까 생각해봤을때 위상정렬 알고리즘이 떠올랐다. 

프로젝트가 복잡해질수록 초기화 과정을 진행할때 서로 연관성이 생기면서 먼저 해야 하는 작업이 있고 그 다음에 해야 하는 작업이 있을 것이다.

그런 상황에서 컴포넌트들을 붙였다 뗐다 할때마다 이 초기화 과정을 직접 컨트롤하기는 어려울 것이다.

따라서 GameFrameworkInitStateInterface와 GameFrameworkComponentManager를 사용해 초기화 과정을 자동적으로 처리해줄 수 있는 시스템을 만들어서 사용하고 있는 것 같다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'언리얼 > Lyra 프로젝트 분석' 카테고리의 다른 글

Lyra분석 - LyraGameMode, experience  (0) 2024.07.08
Input  (0) 2024.07.06
LyraAssetManager  (0) 2024.07.05