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.
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
BeginPlayis called on the Character, it tries to callBeginPlayon all components. On the server this happens right away, but on the client it will not callBeginPlayuntil 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
BeginPlayis called on either the Hero Component or Lyra Pawn Component, those components callBindOnActorInitStateChangedto listen for init state changes and then callCheckDefaultInitializationto attempt to follow the 4 state initialization chain. At this point both components will reachInitState.Spawnedand 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 toDataAvailablebut cannot transition toDataInitializedyet. -
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.DataAvailableit checks to see if thePawnDataand Controller are fully available. The Pawn Extension Component callsCheckDefaultInitializationfrom variousOnRepfunctions 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 theOnActorInitStateChangedfunction 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.