개요
Lyra 분석 중 ModularActor 클래스들을 상속받고, IGameFrameworkInitStateInterface 를 구현하는 클래스들이 존재했다.
언리얼에서 특정 컴포넌트가 초기화될 때, 다른 컴포넌트들이 먼저 초기화 되는 경우에 순서를 보장하기 위한 시스템을 제공한다. UGameFrameworkComponentManager 이다. 처음 볼때는 되게 복잡했는데 로그 찍어가며 정리해봤더니 이해가 됐다. 그런데 역시나 소규모 프로젝트에 적용하기에는 비용이... 라는 생각이 먼저 든다.
이를 활용해 다중 컴포넌트 간의 초기화 순서를 보장하는 방법을 알아보자.
컴포넌트 초기화 순서 보장 플로우
1️⃣ 컴포넌트 등록 (OnRegister)
1. UGameFrameworkComponentManager
에 자신 등록
void UTestComponentA::OnRegister()
{
Super::OnRegister();
UE_LOG(LogBSInitState, Warning, TEXT("[TestComponentA] OnRegister - 컴포넌트 등록"));
RegisterInitStateFeature();
}
2️⃣ 초기화 시작 (BeginPlay) 및 상태 전환 시도
1. 각 컴포넌트는 IGameFrameworkInitStateInterface
를 상속 받으며 각자 자기만의 CurrentInitState 가 존재함.
2. 자신의 CurrentInitState 를 Spawned 로 변경 시도
void UTestComponentA::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogBSInitState, Warning, TEXT("[TestComponentA] BeginPlay - 게임 시작"));
// 다른 컴포넌트들의 상태 변화 감시
BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);
// Spawned 상태로 전환 시도
ensure(TryToChangeInitState(BSGamePlayTags::InitState_Spawned));
CheckDefaultInitialization();
}
3️⃣ 상태 전환 검증 및 처리
3. 그럼 CurrentInitState 를 바꿀 수 있는 CanChangeInitState 로 체크
- 기본적으로 Spawned 상태로 넘어갈 때는 true
bool UTestComponentA::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
UE_LOG(LogBSInitState, Warning, TEXT("[TestComponentA] CanChangeInitState: %s → %s"),
*CurrentState.ToString(), *DesiredState.ToString());
// Spawned 단계: 기본적으로 허용
if (!CurrentState.IsValid() && DesiredState == BSGamePlayTags::InitState_Spawned)
{
return true;
}
...
}
4. 만약 상태 변경에 성공했다면, HandleChangeInitState
호출
- Spawned 때는 LoadMyData 를 호출하도록 함.
void UTestComponentA::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState)
{
UE_LOG(LogBSInitState, Error, TEXT("[TestComponentA] 상태 변경 처리: %s → %s"),
*CurrentState.ToString(), *DesiredState.ToString());
if (DesiredState == BSGamePlayTags::InitState_Spawned)
{
UE_LOG(LogBSInitState, Error, TEXT("[TestComponentA] ✅ Spawned - 데이터 로딩 시작"));
LoadMyData();
}
...
}
4️⃣ 자동으로 다음 상태로 변경 진행
5. 상태가 변경 시도 후, 자동으로 다음 상태로 넘어가기 위해 CheckDefaultInitialization();
함수 호출
6. 이 함수는 자신의 CurrentInitState 를 확인해 자동으로 다음으로 넘어가도록 함.
- TryToChangeInitState(InitState_DataAvailable)
- TryToChangeInitState(InitState_DataInitialized)
- 현재 상태 확인해서 일일이 넘어가도록 하는 코드를 작성 안해도 됨.
void UTestComponentA::CheckDefaultInitialization()
{
static const TArray<FGameplayTag> StateChain = {
BSGamePlayTags::InitState_Spawned,
BSGamePlayTags::InitState_DataAvailable,
BSGamePlayTags::InitState_DataInitialized,
BSGamePlayTags::InitState_GameplayReady
};
ContinueInitStateChain(StateChain);
}
7. UTestComponentA 에서는 InitState_DataAvailable
까지 실패하지 않고 진행.
8. 하지만 InitState_DataInitialized
로 넘어가기 위해서는, 자신의 소유하는 액터가 가진 다른 컴포넌트들이 모두 InitState_DataAvailable
이면 넘어갈 수 있음.
bool UTestComponentA::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
UE_LOG(LogBSInitState, Warning, TEXT("[TestComponentA] CanChangeInitState: %s → %s"),
*CurrentState.ToString(), *DesiredState.ToString());
...
// DataInitialized 단계: 모든 컴포넌트가 DataAvailable이어야 함
if (CurrentState == BSGamePlayTags::InitState_DataAvailable && DesiredState == BSGamePlayTags::InitState_DataInitialized)
{
return Manager->HaveAllFeaturesReachedInitState(GetPawn<ACharacter>(), BSGamePlayTags::InitState_DataAvailable);
}
5️⃣ 컴포넌트 간 동기화 및 순서 보장
9. 다른 RegisterInitStateFeature
한 컴포넌트들도 BeginPlay
에서 "3. 자신의 CurrentInitState 를 Spawned 로 변경 시도" 부터 똑같이 진행함.
10. 다른 컴포넌트들의 IniState가 바뀔 때마다 OnActorInitStateChanged
함수 호출
void UTestComponentA::OnActorInitStateChanged(const FActorInitStateChangedParams& Params)
{
if (Params.FeatureName != NAME_TestComponentA)
{
UE_LOG(LogBSInitState, Log, TEXT("[TestComponentA] 다른 컴포넌트 상태 변화 감지: %s → %s"),
*Params.FeatureName.ToString(), *Params.FeatureState.ToString());
// 다른 컴포넌트가 변화하면 자신도 다음 단계 진행 시도
CheckDefaultInitialization();
}
}
11. ContinueInitStateChain -> CanChangeInitState
다시 호출되어 자신이 다음 단계로 넘어갈 수 있는지 다시 확인
12. CanChangeInitState
함수 안에서 다른 컴포넌트들이 먼저 초기화 되고, 자신의 초기화가 진행될 수 있도록 순서를 보장할 수 있음.
실제 시나리오 예시
상황: ComponentA, ComponentB 두 개가 같은 액터에 존재
1. ComponentA 진행:
Spawned
→ ✅ 성공DataAvailable
→ ✅ 성공 (데이터 로딩 완료)DataInitialized
→ ❌ 실패 (ComponentB가 아직 DataAvailable 미달성)- 대기 상태
2. ComponentB 진행:
Spawned
→ ✅ 성공DataAvailable
→ ✅ 성공
ComponentB가 DataAvailable 달성 순간:
- ComponentA의
OnActorInitStateChanged()
호출 CheckDefaultInitialization()
재실행- 이제
DataInitialized
→ ✅ 성공 (모든 컴포넌트가 DataAvailable) GameplayReady
→ ✅ 성공
Lyra 에서의 사용 예제
LyraHeroComponent 에서도 비슷한 방식으로 초기화 순서를 관리하고 있다.
HeroComponent가 PawnExtensionComponent 가 가지고 있는 캐릭터 데이터(PawnData) 가 먼저 보장된 다음에야 PawnExtensionComponent에서 ASC (AbilitySystemComponent) 를 초기화하게 하기 위해서이다.
void ULyraHeroComponent::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState)
if (CurrentState == LyraGameplayTags::InitState_DataAvailable && DesiredState == LyraGameplayTags::InitState_DataInitialized)
{
APawn* Pawn = GetPawn();
...
const ULyraPawnData* PawnData = nullptr;
if (ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
{
PawnData = PawnExtComp->GetPawnData();
// The player state holds the persistent data for this player (state that persists across deaths and multiple pawns).
// The ability system component and attribute sets live on the player state.
PawnExtComp->InitializeAbilitySystem(LyraPS->GetLyraAbilitySystemComponent(), LyraPS);
}
...
}
}
HeroComponent 에서 자신이 InitState_DataAvailable 상태가 되면, PawnExtensionComponent에서 어빌리티 시스템을 초기화한다.
그럼 왜 DataAvailable 상태가 될 때 초기화를 진행하는 걸까?
bool ULyraPawnExtensionComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
if (CurrentState == LyraGameplayTags::InitState_Spawned && DesiredState == LyraGameplayTags::InitState_DataAvailable)
{
// Pawn data is required.
if (!PawnData)
{
return false;
}
const bool bHasAuthority = Pawn->HasAuthority();
const bool bIsLocallyControlled = Pawn->IsLocallyControlled();
if (bHasAuthority || bIsLocallyControlled)
{
// Check for being possessed by a controller.
if (!GetController())
{
return false;
}
}
return true;
}
}
이유는 PawnExtensionComponent에서 InitState_DataAvailable 상태가 되면 PawnData가 반드시 존재하는 것을 보장할 수 있다. HeroComponent 에서 PawnData를 사용하기 위해서는 반드시 DataAvailable 상태가 되어야 한다.
정리하면 HeroComponent 는 PawnExtensionComponent 가 DataAvailable 상태가 될 때까지 기다리다가, 만족하면 그 때 어빌리티 시스템을 초기화한다.
- HeroComponent 에서 PawnExtensionComponent 안에 PawnData 가 있어야, 어빌리티 시스템을 초기화 할 수 있고 에서 어빌리티 시스템을 초기화 할 수 있는 시점은 PawnExtensionComponent 가 DataAvailable 상태가 될 때로 설정한 것이다.
- PawnData 는 GameMode에서 폰을 생성하기 위해 SpawnDefaultPawnFor 함수를 호출할 때, PawnExtensionComponent의 SetPawnData 함수를 이용해 초기화한다.
장단점
다중 컴포넌트를 가진 액터를 초기화할 때, 각 컴포넌트 간의 의존성을 명확하게 관리하고, 순서를 보장할 수 있는 강력한 시스템이다. 그렇지 않은 경우 오버 엔지니어링이 될 가능성이 높고, 초기 구조 설계에 비용이 많이 든다.
중요한 것은 State 의 이름이 좀 더 명확해야 할 것 같다. Lyra 는 State 이름이 명확하지는 않았다. 차라리 PawnDataInitialized, AbilitySystemReady, GameplayReady 등으로 명확하게 짓는 게 좋아보인다. 특정 상태에서 무엇을 하는지 코드를 봐야 상태의 목적을 알 수 있어서 코드의 흐름을 알기가 쉽지 않았다.
순서 보장이 가능
- 컴포넌트 간 의존성을
CanChangeInitState()
에서 명시적으로 관리 HaveAllFeaturesReachedInitState()
로 상태들의 동기화 지점 생성
ContinueInitStateChain 함수를 사용한 자동화
CheckDefaultInitialization()
로 조건 만족시 자동 진행- 수동으로 각 단계를 호출할 필요 없음
다른 컴포넌트들의 상태 변화를 감지할 수 있음
- 다른 컴포넌트 상태 변화에 즉시 반응
- 대기 중이던 컴포넌트들이 자동으로 다음 단계 진행
자기 상태의 변화는 무시에 무한 루프 방지