The Game Framework Component Manager is a Game Instance Subsystem in the Modular Gameplay plugin that provides functionality designed to be used with Game Feature Plugins. The functions implemented in this subsystem can be used by Game Feature Actions to support extensibility. Game Feature Actions are used by general gameplay code to coordinate communication between different gameplay objects. The manager implements two basic systems, Extension Handlers, and Initialization States.
Extension Handler System
The Extension Handler system allows the modification of game objects when game features are activated. There are two parts to this system: Actors work as Receivers that register to be extended, and Extension Handlers are delegates that are fired in response to events. These events include handling new receivers, removal of existing receivers, and arbitrary events that are called by gameplay code.
Receivers and Extension Handlers
To correctly register as a Receiver, an Actor should call the AddGameFrameworkComponentReceiver
function from the PreInitializeComponents
method, and the RemoveGameFrameworkComponentReceiver
function from the EndPlay
method. This ensures that it registers as a receiver as part of normal component initialization and unregisters when the Actor is removed or disabled.
Receivers can call the SendGameFrameworkComponentExtensionEvent
function to send an arbitrary event. Unlike the Initialization State System described below, these extension events are not stateful and will only modify handlers that are currently active.
To correctly register an Extension Handler, classes like GameFeatureAction_AddComponents
can either call AddExtensionHandler
to register a manual delegate, or call AddComponentRequest
to call a wrapper function which will automatically add the desired component.
In both cases, the handles returned by the add functions need to be stored like an array because the delegates only stay registered as long as there are live shared pointer references to the returned handle struct.
Lyra Example
For an example of how to use this system, you can look at the implementation in the Lyra Sample Game. The ALyraCharacter
class is used for all characters in the game and inherits from the AModularCharacter
class that handles registering as a receiver. Additionally, you can observe the LyraHUD Actor which calls this function manually to enable UI extensions.
Game feature plugins in Lyra like ShooterCore
use the engine-defined UGameFeatureAction_AddComponents
action to add components to spawned Actors. Lyra uses some game-specific actions like UGameFeatureAction_AddInputBinding
to handle some game-specific cases.
For the game-specific UGameFeatureAction_AddInputBinding
action, the HandlePawnExtension
function is registered as a manual extension handler and responds to several different extension events. Events like NAME_ExtensionRemoved
and NAME_ExtensionAdded
are called when the extension handler is first added or removed for all relevant Actors. It responds to a game-specific NAME_BindInputsNow
event that is emitted by the LyraHeroComponent, when it is time for binding feature-specific input events.
Initialization State System
The Initialization State system (Init State) provides functions for tracking the initialization and general life cycle of different features (usually implemented by components) attached to Actors in the game world. This system is not meant to be a generic gameplay state machine because the states are globally defined for an entire game, and are arranged linearly from creation to full initialization.
Synchronizing the initialization of components on an Actor is a complex process, especially when network replication is involved. This system provides registration and notification functions that make that coordination simpler. The low-level functions are implemented by the Game Framework Component Manager, and there is an optional native GameFrameworkInitStateInterface that can be inherited by components (or other gameplay objects) that implement a specified feature.
Actor Features
An Actor that has been registered with this system will have multiple Actor Features, which are defined as unique Names. These names are defined by the game and can either correspond to native class names or functional features.
The subsystem keeps track of the Init State and Implementer object (often a component) for all features registered for an Actor. For objects that implement the GameFrameworkInitStateInterface
, the feature name is returned by the GetFeatureName
interface function and used for all other operations.
Init States
Init States are implemented as Gameplay Tags and must be registered with the subsystem by calling RegisterInitState
during GameInstance initialization. These states are registered in order and shared by all Actors in a game. For instance, a game could support a simple 2 state system with InitState.Spawning
and InitState.Ready
or a more complex system like the Lyra example below.
Reporting and Querying States
All features registered with this system need to report to the Game Framework Component Manager whenever they change the init state because the manager stores this state for later querying. The manager does not enforce restrictions on changing the state and is designed to be flexible.
GameFrameworkInitStateInterface
provides the framework for a simple C++ state machine that can be quickly implemented by overriding a few functions:
Function | Override Description |
---|---|
CanChangeInitState |
This function should be overridden to return true if the requested state transition is allowed. This is where you would implement checks to see if the required data is available. |
HandleChangeInitState |
This function should be overridden to perform any object-specific changes that should occur on a specific state transition. |
CheckDefaultInitialization |
Can be overridden to attempt to follow the default initialization path for the feature. If the ContinueInitStateChain function is called with an array of init states, it will call CanChangeInitState and HandleChangeInitState to get as far as possible in the state chain. This function should be called from places like OnRep functions that may progress initialization. |
Additionally the subsystem and interface provide registration and query functions:
Function | Description |
---|---|
RegisterInitStateFeature |
Registers with the system but does not set a state, this is useful to call from component OnRegister . |
UnregisterInitStateFeature |
This should generally be called from EndPlay to unregister from the system and unbind notification delegates. |
HasReachedInitState |
This can be called to see if the feature has reached either the specified state or a later state in the initialization order. |
HaveAllFeaturesReachedInitState |
This is called on the manager to see if all features have reached a certain state. This is useful for coordinating extensions because you can set a central feature to wait for all other features to be ready before transitioning to the next state. |
Registering For State Changes
The most useful part of this system is the ability to register for init state changes and call delegates after they reach certain states. The register functions like RegisterAndCallForActorInitState
call the specified delegate when a feature reaches a certain state, and immediately call the delegate if it has already reached that state.
RegisterAndCallForClassInitState
can be called with a class name to listen for any feature anywhere reaching that state, which is useful for listening to global initialization. These functions can be called from either C++ code or Blueprints, and the versions on the interface fill in the feature name for you. The delegate execution logic was designed to handle multiple state transitions happening in a row and all relevant delegates will be called.
For ease of use, BindOnActorInitStateChanged
and OnActorInitStateChanged
can be used on the interface to quickly listen for changes made to other features on the same Actor. This can then be used to call functions like CheckDefaultInitialization
that may advance the init state of the feature.
Lyra Example
For an example of how to use this system, look at the implementation in the 5.1 or later version of Lyra Sample Game. The 5.0 release of Lyra predates the Initialization State system and has multiple race conditions this system was designed to help address. Here are the states used by the Lyra sample, as registered in ULyraGameInstance::Init
Init State | Description |
---|---|
InitState.Spawned |
The feature has finished spawning and initial replication, called from BeginPlay . |
InitState.DataAvailable |
All data needed by the feature has been replicated or loaded, including dependencies on other actors that may also need to be replicated. |
InitState.DataInitialized |
After all of the data becomes available, it is used to complete other initialization actions like adding gameplay abilities. |
InitState.GameplayReady |
The object has finished all initialization and is ready to be interacted with in normal gameplay. |
The two main components that use this system are the ULyraPawnExtensionComponent
which coordinates the overall initialization and the ULyraHeroComponent
that handles initialization of player-controlled systems like camera and input.
The initialization of both components depends on replicated data from multiple sources, and they call the RegisterInitStateFeature
function from the OnRegister
method to let the component manager know they exist. Both components later call the CheckDefaultInitialization
function from the BeginPlay
method after initial replication is done.
The full initialization state machine is needed for these two components because they also depend on data replicated by other Actors like LyraPlayerState
that can be slow to download. The list below displays the overall timeline of initialization for a Lyra character:
-
When a Character is initially spawned on the Client and Server, it attaches and registers all components, including the two init state components and others like the LyraAbilitySystemComponent.
-
When
BeginPlay
is called on the Character, it tries to callBeginPlay
on all components. On the server this happens right away, but on the client it will not callBeginPlay
until all the replicated properties have sent their initial data. This happens at different times for each component depending on how much data they need to replicate. -
When
BeginPlay
is called on either the Hero Component or Lyra Pawn Component, those components callBindOnActorInitStateChanged
to listen for init state changes and then callCheckDefaultInitialization
to attempt to follow the 4 state initialization chain. At this point both components will reachInitState.Spawned
and will try to continue initializing. -
When the Hero Component tries to transition to
InitState.DataAvailable
, it checks to see if the player state and input component are ready. If that data isn't available, the state machine stalls until something callsCheckDefaultInitialization
. If the required data is available, it transitions toDataAvailable
but cannot transition toDataInitialized
yet. -
When the Pawn Extension Component calls
CheckDefaultInitialization
, if possible, it first tells the other components (like Hero Component) to move their initialization state machine forward. Then when trying to move its own state forward toInitState.DataAvailable
it checks to see if thePawnData
and Controller are fully available. The Pawn Extension Component callsCheckDefaultInitialization
from variousOnRep
functions to try and move the state machine forward after important cross-actor references finish replicating. Another option is to call the initialization functions from a native tick function. -
When the Pawn Extension Component tries to move forward to
InitState.DataInitialized
, it will not do so until all other features (like the Hero Component) have reachedDataAvailable
. When it actually transitions, this activates theOnActorInitStateChanged
function on the Hero Component and anything else listening. -
Once that happens, the extension component moves to
InitState.DataInitialized
, which then causes the Hero Component to also move toDataInitialized
. During this transition, gameplay abilities are created and bound to player input. -
Both the Hero Component and Pawn Extension Component then transition to
InitState.GameplayReady
, which activates Blueprint callbacks in classes like W_Nameplate that registered to wait for this state to be reached.
The Lyra character initialization flow is complex, but many networked games require similarly complicated initialization flows. The init state system is designed to make it easier to set up complex systems and avoid race conditions or random delay loops.