일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- BFS
- 유니티
- Team Fortress 2
- 시뮬레이션
- 구현
- 백트래킹
- 유니온 파인드
- 다이나믹 프로그래밍
- 그래프
- 투 포인터
- XR Interaction Toolkit
- Unreal Engine 5
- DFS
- 누적 합
- 수학
- VR
- 다익스트라
- 정렬
- 브루트포스
- c++
- 문자열
- 백준
- 자료구조
- 알고리즘
- 재귀
- 스택
- 우선순위 큐
- ue5
- 그리디 알고리즘
- 트리
- Today
- Total
1일1알
Lyra 분석 - IGameFrameworkInitStateInterface 본문
개인적으로 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 |