게임 프레임워크 컴포넌트 매니저(Game Framework Component Manager) 는 게임 피처 플러그인(Game Feature Plugins) 과 함께 사용하도록 설계된 기능을 제공하는 모듈형 게임플레이(Modular Gameplay) 플러그인의 게임 인스턴스 서브시스템(Game Instance Subsystem) 입니다. 이 서브시스템에 구현된 함수는 게임 피처 액션(Game Feature Actions) 에서 사용될 수 있어 확장성을 지원합니다. 게임 피처 액션은 여러 게임플레이 오브젝트 간의 커뮤니케이션을 조정하기 위해 일반 게임플레이 코드에 의해 사용됩니다. 매니저는 두 기본 시스템인 확장 핸들러(Extension Handlers) 및 초기화 스테이트(Initialization States) 를 구현합니다.
확장 핸들러 시스템
확장 핸들러 시스템은 게임 피처가 활성화되었을 때 게임 오브젝트를 수정하도록 허용합니다. 이 시스템에는 두 부분이 있습니다. 액터(Actors) 는 확장을 위해 등록될 리시버(Receivers) 역할을 하며, 확장 핸들러(Extension Handlers) 이벤트에 대응하여 실행되는 델리게이트입니다. 이러한 이벤트에는 새 리시버 처리, 기존 리시버 제거, 게임플레이 코드에 의해 호출된 임의의 이벤트 등이 있습니다.
리시버 및 확장 핸들러
올바르게 리시버로 등록되려면 액터는 PreInitializeComponents 메서드로부터 AddGameFrameworkComponentReceiver 함수를, EndPlay 메서드로부터 RemoveGameFrameworkComponentReceiver 함수를 호출해야 합니다. 이는 액터가 일반 컴포넌트 초기화의 일환으로 리시버로서 등록되고, 액터가 제거되거나 비활성화되면 등록 해제되게 합니다.
리시버는 SendGameFrameworkComponentExtensionEvent 함수를 호출하여 임의의 이벤트를 전송할 수 있습니다. 아래에 설명된 초기화 스테이트 시스템(Initialization State System)과 달리 이러한 확장 이벤트는 상태를 트래킹할 수 없으며 현재 활성화된 핸들러만 수정합니다.
확장 핸들러를 올바르게 등록하기 위해 GameFeatureAction_AddComponents 같은 클래스는 AddExtensionHandler 를 호출하여 수동 델리게이트를 등록하거나 AddComponentRequest 를 호출하여 자동으로 원하는 컴포넌트를 추가할 래퍼 함수를 호출할 수 있습니다.
두 경우 모두 add 함수를 통해 반환된 핸들을 배열처럼 저장해야 합니다. 델리게이트는 반환된 핸들 구조체에 대한 라이브 쉐어드 포인터 레퍼런스가 있는 동안에만 등록된 채로 유지되기 때문입니다.
라이라 샘플
이 시스템을 사용하는 예시로 라이라 샘플 게임의 구현을 살펴볼 수 있습니다. ALyraCharacter 클래스는 게임 내 모든 캐릭터에 사용되며, 리시버로서 등록을 처리하는 AModularCharacter 클래스를 상속합니다. 또한 LyraHUD 액터가 이 함수를 수동으로 호출하여 UI 확장을 활성화하는 것을 볼 수 있습니다.
ShooterCore 같은 라이라의 게임 피처 플러그인은 엔진에서 정의된 UGameFeatureAction_AddComponents 액션을 사용하여 스폰된 액터에 컴포넌트를 추가합니다. 라이라는 일부 게임 전용 사례를 처리할 때 UGameFeatureAction_AddInputBinding 등 일부 게임 전용 액션을 사용합니다.
게임 전용 UGameFeatureAction_AddInputBinding 액션에 대해서는 HandlePawnExtension 함수가 수동 확장 핸들러로 등록되며 다양한 확장 이벤트에 반응합니다. 모든 관련 액터에 대해 확장 핸들러가 처음으로 추가 또는 제거될 때는 NAME_ExtensionRemoved 및 NAME_ExtensionAdded 같은 이벤트가 호출됩니다. 이는 특정 피처 전용 입력 이벤트를 바인딩할 때 LyraHeroComponent 가 발생시키는 게임 전용 NAME_BindInputsNow 이벤트에 대응합니다.
초기화 스테이트 시스템
초기화 스테이트(Init State ) 시스템은 게임 월드에서 액터에 어태치되고 보통 컴포넌트에 의해 구현되는 여러 피처의 초기화 및 일반 수명 주기를 트래킹하기 위한 함수를 제공합니다. 이 시스템은 일반 게임플레이 스테이트 머신으로 의도되지 않았습니다. 스테이트가 전체 게임에 대해 글로벌하게 정의되며 생성부터 완전 초기화까지 선형으로 배열되기 때문입니다.
액터에서 컴포넌트 초기화를 동기화하는 것은 복잡한 프로세스입니다. 네트워크 리플리케이션이 관련된 경우 특히 복잡합니다. 이 시스템은 조정을 더 간단하게 만드는 등록 및 알림 기능을 제공합니다. 낮은 수준의 기능은 게임 프레임워크 컴포넌트 매니저에 의해 구현되며, 지정된 피처를 구현하는 컴포넌트(또는 기타 게임플레이 오브젝트)에 의해 상속될 수 있는 선택 사항인 네이티브 GameFrameworkInitStateInterface도 있습니다.
액터 피처
이 시스템으로 등록된 액터는 다수의 액터 피처(Actor Feature) 를 가지며, 이는 고유한 이름(Name) 으로 정의됩니다. 이 이름은 게임에 의해 정의되며 네이티브 클래스 이름 또는 기능적 피처에 대응할 수 있습니다.
서브시스템은 초기화 스테이트(Init State) 와 액터를 위해 등록된 모든 피처를 위한 구현 오브젝트(Implementer Object)를 트래킹합니다. 구현 오브젝트는 컴포넌트일 때가 많습니다. GameFrameworkInitStateInterface 를 구현하는 오브젝트의 경우, 피처 이름은 GetFeatureName 인터페이스 함수에 의해 반환되며 나머지 모든 작업에서 사용됩니다.
초기화 스테이트
초기화 스테이트(Init State) 는 게임플레이 태그(Gameplay Tags)로서 구현되며, 게임 인스턴스 초기화 도중 RegisterInitState 호출로 서브시스템에 등록되어야 합니다. 이러한 스테이트는 순서대로 등록되며 게임 내 모든 액터에 의해 공유됩니다. 예를 들어 게임에서 InitState.Spawning 과 InitState.Ready 의 단순한 스테이트 2개만 지원할 수도 있고, 아니면 아래의 라이라 샘플처럼 보다 복잡한 시스템을 지원할 수도 있습니다.
스테이트 보고 및 쿼리
이 시스템으로 등록된 모든 피처는 초기화 스테이트를 변경할 때마다 게임 프레임워크 컴포넌트 매니저에 보고되어야 합니다. 매니저가 향후 쿼리를 위해 이 스테이트를 저장하기 때문입니다. 매니저는 스테이트 변경 시 제한을 강제하지 않으며 유연성을 갖도록 설계되었습니다.
GameFrameworkInitStateInterface 는 다음 몇 개 함수를 오버라이드하여 빠르게 구현할 수 있는 단순한 C++ 스테이트 머신을 위한 프레임워크를 제공합니다.
| 함수 | 오버라이드 설명 |
|---|---|
CanChangeInitState |
이 함수는 요청된 스테이트 트랜지션이 허용되는 경우 true를 반환하도록 오버라이드되어야 합니다. 여기에 요청된 데이터의 사용 가능 여부 검사를 구현합니다. |
HandleChangeInitState |
이 함수는 특정 스테이트 트랜지션 시에 발생할 오브젝트별 변경 사항을 수행하도록 오버라이드되어야 합니다. |
CheckDefaultInitialization |
피처에 대한 디폴트 초기화 경로를 따르도록 오버라이드될 수 있습니다. ContinueInitStateChain 함수가 초기화 스테이트 배열과 함께 호출되면 CanChangeInitState 및 HandleChangeInitState 를 호출하여 스테이트 체인을 최대한 진행합니다. 이 함수는 초기화를 진행시킬 OnRep 등의 함수에서 호출되어야 합니다. |
또한 서브시스템과 인터페이스는 다음과 같은 등록 및 쿼리 함수를 제공합니다.
| 함수 | 설명 |
|---|---|
RegisterInitStateFeature |
시스템에 등록하지만 스테이트는 설정하지 않습니다. 컴포넌트 OnRegister 에서 호출하는 데 유용합니다. |
UnregisterInitStateFeature |
일반적으로 EndPlay 에서 호출되어 시스템에서 등록 해제하고 알림 델리게이트를 바인딩 해제해야 합니다. |
HasReachedInitState |
피처가 지정된 스테이트에 도달했는지 또는 초기화 순서에서 후반 스테이트에 도달했는지 확인할 때 이 함수를 호출합니다. |
HaveAllFeaturesReachedInitState |
매니저에서 호출되어 모든 피처가 특정 스테이트에 도달했는지 확인합니다. 이는 익스텐션 조정에 유용합니다. 다음 스테이트로 트랜지션하기 전에 모든 피처가 준비되기를 기다리는 중앙 피처를 설정할 수 있기 때문입니다. |
스테이트 변경 등록하기
이 시스템에서 가장 유용한 부분은 초기화 스테이트 변경을 등록하고 특정 스테이트에 도달한 뒤 델리게이트를 호출할 수 있다는 점입니다. RegisterAndCallForActorInitState 같은 등록 함수는 피처가 특정 스테이트에 도달했을 때 지정된 델리게이트를 호출하며, 이미 해당 스테이트에 도달한 상태라면 즉시 델리게이트를 호출합니다.
RegisterAndCallForClassInitState 는 클래스 이름과 함께 호출되어 어디서든 해당 스테이트에 도달하는 모든 피처를 리슨할 수 있습니다. 이는 글로벌 초기화를 리슨하는 데 유용합니다. 이 함수는 C++ 코드나 블루프린트로부터 호출될 수 있으며 인터페이스의 버전이 피처 이름에 채워집니다. 스테이트 실행 로직은 연속으로 발생하는 다수의 스테이트 트랜지션을 처리하도록 설계되었으며 연관성이 있는 모든 델리게이트가 호출됩니다.
사용 편의를 위해 BindOnActorInitStateChanged 및 OnActorInitStateChanged 를 인터페이스에 사용하여 동일한 액터의 다른 피처에 가해진 변경 사항을 빠르게 리슨할 수 있습니다. 그런 다음 피처의 초기화 스테이트를 진행시킬 수 있는 CheckDefaultInitialization 등의 함수를 호출하는 데 사용될 수 있습니다.
라이라 샘플
이 시스템을 사용하는 예시로 라이라 샘플 게임 5.1 또는 최신 버전의 구현을 살펴볼 수 있습니다. 초기화 스테이트(Initialization State) 시스템보다 먼저 나온 라이라 5.0 릴리스에는 다수의 경쟁 조건이 있으며, 이 시스템은 그런 조건을 처리하기 위해 설계되었습니다. 다음은 라이라 샘플에서 사용된 스테이트이며, ULyraGameInstance::Init 에 등록되어 있습니다.
| 초기화 스테이트 | 설명 |
|---|---|
InitState.Spawned |
피처가 스폰 및 초기 리플리케이션을 마쳤습니다. BeginPlay 에서 호출됩니다. |
InitState.DataAvailable |
피처에서 필요로 하는 모든 데이터가 리플리케이트 또는 로드되었습니다. 리플리케이션이 필요한 기타 액터에 대한 종속성도 포함됩니다. |
InitState.DataInitialized |
모든 데이터가 이용 가능해진 뒤 게임플레이 어빌리티 추가 등의 기타 초기화 액션을 완료하는 데 사용됩니다. |
InitState.GameplayReady |
오브젝트가 모든 초기화를 마쳤으며 일반 게임플레이와 상호작용할 준비가 됐습니다. |
이 시스템을 사용하는 두 개의 메인 컴포넌트는 초기화 전반을 조정하는 ULyraPawnExtensionComponent 와 카메라, 입력 등 플레이어가 제어하는 시스템의 초기화를 처리하는 ULyraHeroComponent 입니다.
두 컴포넌트의 초기화는 여러 소스로부터 리플리케이트되는 데이터에 의존하며, OnRegister 메서드로부터 RegisterInitStateFeature 함수를 호출하여 컴포넌트 매니저에게 그 존재를 알립니다. 두 컴포넌트는 이후 초기 리플리케이션이 완료된 뒤 BeginPlay 메서드로부터 CheckDefaultInitialization 함수를 호출합니다.
이 두 컴포넌트에 대해 완전 초기화 스테이트 머신이 필요합니다. 이 컴포넌트들도 다운로드가 느릴 수 있는 LyraPlayerState 등 다른 액터에 의해 리플리케이트되는 데이터에 의존하기 때문입니다. 아래 목록은 라이라 캐릭터 초기화의 전반적인 타임라인을 보여줍니다.
-
캐릭터가 초기에 클라이언트와 서버에 호출되면 모든 컴포넌트를 어태치하고 등록합니다. 여기에는 두 개의 초기화 스테이트 컴포넌트와 LyraAbilitySystemComponent 등의 기타 요소가 포함됩니다.
-
캐릭터에서
BeginPlay가 호출되면 모든 컴포넌트에서BeginPlay를 호출하려고 시도합니다. 서버에서는 즉시 호출하려 하지만 클라이언트에서는 리플리케이트된 프로퍼티 전부가 초기 데이터를 보내기 전까지BeginPlay를 호출하지 않습니다. 호출 시도는 리플리케이트할 데이터가 얼마나 많은지에 따라 각 컴포넌트에서 여러 차례 발생합니다. -
BeginPlay가 히어로(Hero) 컴포넌트나 라이라 폰(Lyra Pawn) 컴포넌트에서 호출되면, 이 컴포넌트들은BindOnActorInitStateChanged를 호출하여 초기화 스테이트 변경 사항을 리슨한 다음CheckDefaultInitialization을 호출하여 4 스테이트 초기화 체인을 따르고자 시도합니다. 이 시점에서 두 컴포넌트 모두InitState.Spawned에 도달하며 계속해서 초기화를 시도합니다. -
히어로 컴포넌트는
InitState.DataAvailable로 트랜지션을 시도할 때 플레이어 스테이트 및 입력 컴포넌트가 준비됐는지 확인합니다. 해당 데이터를 이용할 수 없다면 스테이트 머신은CheckDefaultInitialization이 호출될 때까지 정지됩니다. 필수 데이터가 이용 가능하다면DataAvailable로 트랜지션하지만, 아직DataInitialized로 트랜지션할 수는 없습니다. -
폰 익스텐션(Pawn Extension) 컴포넌트는
CheckDefaultInitialization을 호출할 때 가능한 경우 먼저 히어로 컴포넌트 등의 다른 컴포넌트에 초기화 스테이트 머신을 진행하도록 지시합니다. 그런 다음 자신의 스테이트를InitState.DataAvailable로 진행하려 할 때PawnData와 컨트롤러가 완전히 사용 가능한지 확인합니다. 폰 익스텐션 컴포넌트는 다양한OnRep함수로부터CheckDefaultInitialization을 호출하여 중요한 크로스 액터 레퍼런스가 리플리케이트를 마친 뒤 스테이트 머신을 진행시키려고 시도합니다. 또 한 가지 옵션은 네이티브 틱 함수로부터 초기화 함수를 호출하는 것입니다. -
폰 익스텐션 컴포넌트는 히어로 컴포넌트와 같은 나머지 피처가 전부
DataAvailable에 도달하기 전까지InitState.DataInitialized로 진행하지 않습니다. 트랜지션이 일어나면 히어로 컴포넌트와 기타 리슨 중인 모든 것에 대해OnActorInitStateChanged함수를 활성화합니다. -
그렇게 되면 익스텐션 컴포넌트는
InitState.DataInitialized로 넘어가며, 이에 따라 히어로 컴포넌트 또한DataInitialized로 넘어갑니다. 이 트랜지션 동안 게임플레이 어빌리티가 생성되어 플레이어 입력에 바인딩됩니다. -
그런 다음 히어로 컴포넌트와 폰 익스텐션 컴포넌트는
InitState.GameplayReady로 트랜지션합니다.InitState.GameplayReady는 이 스테이트가 도달하기까지 대기하도록 등록된 W_Nameplate 등의 클래스에서 블루프린트 콜백을 활성화합니다.
라이라의 캐릭터 초기화 플로는 복잡하지만, 많은 네트워크 게임에서 이와 유사하게 복잡한 초기화 플로를 필요로 합니다. 이 초기화 스테이트 시스템은 복잡한 시스템을 쉽게 구성하고 경쟁 조건이나 랜덤 딜레이 루프를 피하도록 설계되었습니다.