Lifecycle gates¶
The lifecycle gate system centralizes readiness assembled from independent runtime systems. Producers publish small, object-scoped facts; consumers wait for an AND-set of those facts without coupling gameplay, UI, networking, or backend classes to one another.
This page is the canonical contract for the gate system. The behavior below is verified against the current C++ implementation.
System boundary¶
UBrickLeafLifecycleSubsystem is a UGameInstanceSubsystem. It owns the gate registry, feature adapters, public subscriptions, world cleanup, and the adjacent travel coordinator. It depends on the UI transition, Tank UI, and Nakama subsystems so adapters can safely sample them during initialization.
The subsystem deliberately does not exist in a dedicated-server process: ShouldCreateSubsystem returns false when IsRunningDedicatedServer() is true. Consequently, no lifecycle facts, gates, adapters, or presentation broadcasts in this document run on a dedicated server. Their underlying gameplay producers may still run normally. Listen servers, standalone games, and clients do create the subsystem.
flowchart LR
Producers[Character / PlayerState / UI / Team / Auth / GameMode] -->|native events + queries| Adapters[Feature adapters]
Adapters -->|SetReady| Registry[Lifecycle gate registry]
Consumers[Blueprint and native consumers] -->|register requirements| Registry
Registry -->|one-shot callback when all facts are ready| Consumers
World[World teardown / relationship reset] -->|retire scopes| Registry
The dependency direction is one way:
- Domain classes expose their existing state and native notifications.
- Coordinator-owned feature adapters observe those sources.
- Adapters publish facts and define feature-specific gates.
- Consumers subscribe through the subsystem or native registry.
Domain classes do not include, resolve, or call the lifecycle subsystem. This keeps the generic registry free of feature and network-role policy.
Vocabulary¶
| Term | Meaning |
|---|---|
| Fact | A ready/not-ready FGameplayTag cached on one object scope. |
| Gate | A one-shot wait for every requirement in an AND-set. A public gate may also be published as a cached tag for late subscribers. |
| Subject | The object whose readiness is being observed, such as a pawn or PlayerState. |
| Owner | The object scope that owns a native waiter and its request name. |
| Listener | The object whose validity guards a public subscription callback. |
| Lifetime | The object, normally a UWorld or UGameInstance, that retires a group of scopes together. |
| Generation | A monotonically increasing identity assigned whenever an object receives a new scope. |
| Request name | An FName that identifies a native wait operation. It is not a readiness fact. |
Facts and public gates remain FGameplayTag values throughout the registry. There is no GetTagName() conversion to an FName cache. Request names remain FName because they identify operations rather than readiness state.
Registry contract¶
FLifecycleGateRegistry is game-thread-only and stores weak object identities. Its core operations are:
Scope(Object, Lifetime)returns the currentFLifecycleScopeToken. If the same object is presented with a different lifetime, its old scope is reset.SetReady(Token, Tag, true)publishes a fact and pumps eligible waiters.SetReady(Token, Tag, false)closes a reversible fact in the same generation.IsReady(Requirement)andIsReady(Object, Tag)query cached readiness.WaitForAll(OwnerToken, RequestName, Requirements, Callback)creates a one-shot AND gate.WaitForAllGuarded(...)also cancels delivery when its guard object becomes invalid.Cancel(Owner, RequestName)removes a pending native request.ResetScope(Object)retires that object's generation and every waiter that owns or references it.ResetLifetime(Lifetime)retires all scopes associated with that lifetime.GetBlockingFacts(Owner, RequestName)returns the unsatisfied gameplay tags for a pending request.
A wait is accepted only when the owner and every requirement token are current. Registering the same owner and request name replaces the previous pending request. Different names are independent. A completed name may be registered again to explicitly request another notification.
The callback is removed from the waiter collection before it is invoked. This makes reentrant cancellation, reset, publication, and registration safe. Nested updates are drained by the outer pump after the current callback returns.
Ready-before-register behavior¶
Facts are cached. If all requirements are already ready when WaitForAll is called, the callback runs synchronously during registration. Code after the call must therefore tolerate that the callback has already executed.
Public gameplay-tag subscriptions have the same immediate replay behavior. The older multicast gate-open delegates do not replay for late subscribers.
Generations and stale asynchronous work¶
The generation prevents an old async result from opening readiness for a replacement relationship:
auto& Gates = Lifecycle->GetReadinessGates();
const FLifecycleScopeToken PawnScope = Gates.Scope(Character, World);
BeginAsyncWork([PawnScope, &Gates]()
{
// Return to the game thread first. This fails if the scope was reset.
Gates.SetReady(PawnScope, BrickLeafReadinessFacts::Fact_Character_BeginPlayReady);
});
Never acquire a fresh scope inside the completion of old asynchronous work. Doing so discards stale-result protection and can publish into a replacement pawn, PlayerState relationship, or world.
Gameplay tag catalog¶
Native constants are declared in Source/Metal_terra/Public/Core/Subsystems/LifecycleReadinessFacts.h and defined in Source/Metal_terra/Private/Core/Subsystems/LifecycleReadinessFacts.cpp. The tag tree is also listed in Config/Tags/LifecycleSubsystemTags.ini for editor discovery.
Public gates¶
| Gameplay tag | Native constant | Subject | Opens when |
|---|---|---|---|
Lifecycle.Gate.Character.LocalReady |
Gate_Character_LocalReady |
Local ATankPlayerCharacter |
The character adapter has latched and dispatched the local character-init gate. |
Lifecycle.Gate.Character.RemoteReady |
Gate_Character_RemoteReady |
Remote ATankPlayerCharacter |
The character adapter has dispatched remote readiness and replayed deferred Blueprint RepNotifies. |
Lifecycle.Gate.LocalPlayer.WidgetControllerAndTeamReady |
Gate_LocalPlayer_WidgetControllerAndTeamReady |
Local ATankPlayerState; also cached on the world |
The same local PlayerState has a ready widget controller and a valid cached team. |
Public gate matching is exact. Registering a parent tag does not subscribe to all children.
Internal facts¶
| Gameplay tag | Scope | Lifetime | Producer / meaning |
|---|---|---|---|
Lifecycle.Fact.Character.BeginPlayReady |
Character | World | Character has begun play, with the adapter's authority/local exception. |
Lifecycle.Fact.Character.CharacterDefMatches |
Character | World | A remote client pawn class matches its replicated CharacterDef; local and authority paths satisfy this directly. |
Lifecycle.Fact.Character.InitGateOpened |
Character | World | The character gate-open dispatch path completed. This is not arbitrary async initialization completion. |
Lifecycle.Fact.Character.LocalDispatchReturned |
LocalPlayer | World | Synchronous local gate-open dispatch returned for that viewer. |
Lifecycle.Fact.Character.OwningClientPossessionAcknowledged |
Character | World | The owning client controller has accepted this pawn through AcknowledgePossession; non-client paths satisfy this directly. |
Lifecycle.Fact.Character.PresentationReady |
Character | World | Required widget-controller presentation is ready; authority remote pawns do not require simulated-proxy UI. |
Lifecycle.Fact.PlayerState.CharacterDefReady |
PlayerState | World | A CharacterDef is available. Required by the local character gate. |
Lifecycle.Fact.PlayerState.TeamReady |
PlayerState | World | The PlayerState has processed its match team ID. |
Lifecycle.Fact.Team.AssignmentReady |
PlayerState | World | Team cache contains a processed, valid team assignment for presentation. |
Lifecycle.Fact.UI.WidgetControllerReady |
PlayerState | World | The UI subsystem reports the PlayerState's controller and view models ready. |
Lifecycle.Fact.LocalPlayer.TeamReady |
World | World | The TeamSubsystem's current local controlled PlayerState has a valid cached team. |
Lifecycle.Fact.World.BeginPlay |
World | World | UWorld::OnWorldBeginPlay fired. |
Lifecycle.Fact.Nakama.LoginSucceeded |
NakamaConnectionSubsystem | GameInstance | The validated Nakama login-success state is active. |
Lifecycle.Fact.GameMode.PostLogin |
Controller | World | ATankGameMode completed its PostLogin publication for that controller. |
Internal facts are available to native composition but are not all intended as stable Blueprint-facing gates. Prefer a documented Lifecycle.Gate.* tag for a Blueprint feature contract.
Character readiness¶
The character adapter is driven by ATankPlayerCharacter::OnClientInitSourceChanged, CharacterDef readiness, UI readiness, and current-state sampling when it observes a world. OnRep_PlayerState, controller changes, BeginPlay, possession changes, and other pawn code can notify the character source without knowing that a lifecycle subsystem exists.
Local versus remote routing¶
The adapter does not rely on IsLocallyControlled() during early BeginPlay:
ROLE_AutonomousProxyis local.ROLE_SimulatedProxyis remote.ROLE_Authorityuses its resolved controller'sIsLocalController()value.- An authority pawn without a controller remains unresolved and is evaluated again after its relationship changes.
Routing is exclusive. One character generation cannot open both the local and remote character gate.
Local character requirements¶
The local character gate requires:
- PlayerState team processing ready.
- PlayerState CharacterDef ready.
- Pawn BeginPlay ready.
- CharacterDef/class policy satisfied.
- The owning client controller has acknowledged this pawn through
AcknowledgePossession. - The local PlayerState widget controller ready.
Before external callbacks, the adapter latches bClientInitGateOpened. It then broadcasts OnLocalCharacterInitGateOpened, publishes Lifecycle.Gate.Character.LocalReady, and publishes Lifecycle.Fact.Character.LocalDispatchReturned on the owning ULocalPlayer.
Remote character requirements¶
The remote character gate requires:
- PlayerState team processing ready.
- Pawn BeginPlay ready.
- Remote client CharacterDef/class match.
- Simulated-proxy widget-controller presentation ready.
Lifecycle.Fact.Character.LocalDispatchReturnedfor every local viewer.
This means remote readiness is ordered after local synchronous gate dispatch; it does not wait for latent Blueprint work. On remote release, the adapter broadcasts OnRemoteCharacterInitGateOpened, replays deferred Blueprint RepNotifies, and publishes Lifecycle.Gate.Character.RemoteReady.
Authority remote pawns on a listen server do not require simulated-proxy UI. Client simulated proxies do. A process with no local players cannot release its remote presentation gate.
Relationship reset¶
Unpossession or source removal clears bClientInitGateOpened, retires the pawn scope, and revokes the local viewer's dispatch-returned fact when applicable. A replacement pawn must satisfy a new generation. This is why readiness is keyed by pawn identity rather than a broad network-role bucket.
UI and team readiness¶
The UI adapter mirrors UTankUISubsystem readiness onto each PlayerState. It observes OnWidgetControllerReadyNative, clears a PlayerState scope on removal, and closes widget readiness during UI tracking resets. It also samples existing PlayerStates when attaching to a world, so it does not depend solely on event arrival order.
See UI readiness flow for the controller/view-model pipeline beneath this fact.
Presentation-only OnTeamChanged¶
The TeamSubsystem updates its cache immediately. The team adapter delays the presentation broadcast until the affected PlayerState has Lifecycle.Fact.UI.WidgetControllerReady. Repeated team changes while waiting replace the pending request, so the eventual broadcast uses the current cached team rather than replaying stale IDs.
This path is deliberately PlayerState-scoped and has no pawn requirement. See team presentation readiness.
Pawn team presentation¶
The pawn-specific presentation path requires both the target and local viewer to have:
- a valid team assignment;
- a ready widget controller; and
- an opened character-init gate on their current pawns.
The callback revalidates all live PlayerState, pawn, UI, local-control, and team-cache relationships before calling DispatchTeamPresentation. Cached facts wake the request; live state remains the final authority.
Local widget-controller and team gate¶
Lifecycle.Gate.LocalPlayer.WidgetControllerAndTeamReady combines:
- world-scoped
Lifecycle.Fact.LocalPlayer.TeamReady; and - PlayerState-scoped
Lifecycle.Fact.UI.WidgetControllerReadyfor the same local controlled PlayerState.
It does not require a pawn, possession, CharacterDef, or character-init gate. Remote PlayerState readiness cannot satisfy it. The gate is cached on both the world and the local PlayerState, and OnLocalPlayerWidgetControllerAndTeamReady(LocalPlayerState) supplies that PlayerState to specialized Blueprint listeners.
The gate fires once per continuous readiness period. UI loss, loss of a valid team, or replacement of the local PlayerState closes it and permits a later reopen. An ordinary valid-to-valid team change does not create a new period.
Login and world facts¶
Lifecycle.Fact.Nakama.LoginSucceeded follows validated Nakama login state. It is scoped to UNakamaConnectionSubsystem with GameInstance lifetime, so normal world travel does not clear it. Authentication loss, session replacement, shutdown, or adapter deinitialization closes or retires it.
Lifecycle.Fact.GameMode.PostLogin is scoped to the specific controller with world lifetime. It is published after the project GameMode reports PostLogin and is retired on logout. It does not imply possession, pawn readiness, or networked agreement with the client's Nakama fact. Because the lifecycle subsystem is not created on dedicated servers, this fact is available only on standalone/listen authority instances.
Lifecycle.Fact.World.BeginPlay is published on the world from OnWorldBeginPlay. World teardown and cleanup retire it with the rest of that world's scopes.
Blueprint API¶
Generic tag subscription¶
Use Register For Lifecycle Gate on the lifecycle subsystem and provide a documented Lifecycle.Gate.* gameplay tag and a matching dynamic callback receiving GateTag and Subject.
The callback owner is inferred as both listener and subject. For a character Blueprint, call the node on that character and choose either the local or remote character gate tag. The subscription is filtered to that pawn; a gate opened by another pawn cannot trigger it.
The call returns false when the callback owner, tag, callback, subject world, or GameInstance relationship is invalid. A successful registration is one-shot for the subject's current generation and may invoke the callback immediately when the tag is already cached.
No Blueprint rewiring is required when facts change internally as long as the public Lifecycle.Gate.* path and callback signature remain unchanged. After a C++ signature rebuild, stale Blueprint nodes may need Refresh Nodes followed by a Blueprint compile.
Specialized compatibility APIs¶
The subsystem also exposes:
OnLocalCharacterInitGateOpened(Character);OnRemoteCharacterInitGateOpened(Character);IsCharacterInitGateOpen(Character);OnLocalPlayerWidgetControllerAndTeamReady(LocalPlayerState); andIsLocalPlayerWidgetControllerAndTeamReady().
The multicast delegates announce gate dispatch and do not replay. Use a cached query or generic gameplay-tag subscription when late attachment is possible.
The former pawn Blueprint events UponLocalCharacterInitialized and UponRemoteCharacterInitialized are removed. Gate-open events mean prerequisites were satisfied, not that arbitrary Blueprint or asynchronous initialization has finished.
Native API¶
Use the generic registry when composing several facts:
auto& Gates = Lifecycle->GetReadinessGates();
const auto PlayerScope = Gates.Scope(PlayerState, World);
const auto PawnScope = Gates.Scope(Character, World);
Gates.WaitForAll(
PawnScope,
TEXT("Feature.Initialize"),
{
{PlayerScope, BrickLeafReadinessFacts::Fact_UI_WidgetControllerReady},
{PawnScope, BrickLeafReadinessFacts::Fact_Character_InitGateOpened}
},
[WeakCharacter]()
{
if (ATankPlayerCharacter* Character = WeakCharacter.Get())
{
// Initialize the feature.
}
});
Use RegisterForLifecycleGateNative when consuming one public gate for an explicit subject. Listener and subject may differ:
const FLifecycleGateSubscriptionHandle Handle =
Lifecycle->RegisterForLifecycleGateNative(
Listener,
Character,
BrickLeafReadinessFacts::Gate_Character_RemoteReady,
[](FGameplayTag GateTag, UObject* Subject)
{
// One-shot; may run synchronously during registration.
});
Store the returned handle only when explicit early cancellation is needed, then call CancelLifecycleGateSubscription. Invalid listener/subject pruning, subject generation reset, world teardown, and subsystem shutdown also cancel the subscription.
WaitForCharacterInitGateOpened is a native convenience wrapper around Lifecycle.Fact.Character.InitGateOpened. As with every gate, capture weak objects and revalidate mutable domain state in the callback.
Adding a fact or gate¶
- Decide the subject and lifetime first. Use the narrowest object whose identity owns the truth, and a lifetime that reliably retires it.
- Add the gameplay tag to
LifecycleReadinessFacts.h,LifecycleReadinessFacts.cpp, andConfig/Tags/LifecycleSubsystemTags.ini. - Publish it from a feature adapter by sampling both current state and native producer events. Do not make the producer depend on the lifecycle subsystem.
- Close reversible facts on source loss. Use
ResetScopewhen the entire object relationship is replaced. - Define requirements in the feature adapter, not in
FLifecycleGateRegistry. - Revalidate live source state in consequential callbacks; cached readiness is a wake-up mechanism, not ownership of domain truth.
- Expose a
Lifecycle.Gate.*tag when Blueprint or cross-feature consumers need a stable public contract. Keep implementation facts internal where possible. - Add automation coverage for ready-before-register, both arrival orders, duplicate source notifications, replacement generations, and world cleanup.
- Validate standalone, listen host, joining client, and dedicated-server expectations explicitly.
Implement a new FLifecycleFeatureAdapter when the source domain is new. Register it in UBrickLeafLifecycleSubsystem::Initialize, unsubscribe in Deinitialize, and use ObserveWorld to replay existing state. Adapters perform event-driven sampling rather than polling.
Failure modes and debugging¶
| Symptom | Likely cause | Check |
|---|---|---|
| Callback never fires | Wrong subject, invalid/unregistered tag, missing producer event, or a requirement remains false. | Call GetBlockingFacts for native waits; inspect subject and scope lifetime. |
| Callback fired during registration | All facts were already cached. | Prepare caller state before registering and avoid assuming deferred delivery. |
| Callback fires for the wrong pawn | A shared multicast delegate was used without subject filtering. | Use the tag subscription on the pawn or explicit-subject native API. |
| Callback fires twice | Two independent subscriptions were created, or a readiness period was intentionally reopened. | Track registration sites and handles; distinguish a new generation from duplicate source events. |
| Callback never fires after respawn | Consumer retained an old generation or did not re-register. | Acquire scopes for the replacement relationship and register a new wait. |
| Old async work opens a new pawn | Completion acquired a fresh scope instead of retaining its starting token. | Capture the original token and let SetReady reject it after reset. |
| Remote pawn appears stuck | Local dispatch-returned, remote UI, BeginPlay, team, or class-match fact is missing. | Inspect all remote character requirements and current pawn/PlayerState linkage. |
| Team UI misses initial color | Team cache arrived before UI readiness and consumer used only the raw team event. | Consume presentation OnTeamChanged or apply current team on the ready gate. |
| Nothing runs on a dedicated server | Expected design. | Put server-authoritative functionality in its gameplay subsystem; lifecycle gates are not instantiated there. |
Do not interpret an open fact as permanent. UI rebuilds and relationship changes can close facts or retire generations. Do not retain object references from a callback without normal Unreal lifetime checks.
Verification coverage¶
Automation tests under MetalTerra.Lifecycle cover:
- AND semantics and object identity isolation;
- cached ready-before-register delivery;
- one-shot delivery and named-request replacement;
- reentrant reset and stale-generation rejection;
- world lifetime cleanup;
- login fact composition and lifetime differences;
- local-before-remote character ordering;
- exact subject/tag filtering and multiple subscriptions;
- invalid listener cancellation;
- team-presentation prerequisite arrival orders; and
- local widget/team gate reopening after UI reset.
Automation does not replace multiplayer runtime validation. For changes to character or presentation policy, test standalone, listen server with joining clients, and dedicated-server simulation with at least two clients. Verify one local event per local pawn generation, one remote event per remote pawn generation, and no source-world callback after travel.
Source map¶
| Area | Source |
|---|---|
| Coordinator and public API | Source/Metal_terra/Public/Core/Subsystems/BrickLeafLifecycleSubsystem.h and matching private .cpp |
| Generic registry | Source/Metal_terra/Public/Core/Subsystems/LifecycleGateRegistry.h |
| Native tag declarations | Source/Metal_terra/Public/Core/Subsystems/LifecycleReadinessFacts.h |
| Native tag definitions | Source/Metal_terra/Private/Core/Subsystems/LifecycleReadinessFacts.cpp |
| Config tag list | Config/Tags/LifecycleSubsystemTags.ini |
| Adapter base | Source/Metal_terra/Public/Core/Subsystems/LifecycleFeatureAdapter.h |
| Feature adapters | Source/Metal_terra/Private/Core/Lifecycle/ |
| Automation | Source/Metal_terra/Private/Tests/LifecycleGateRegistryTests.cpp |
Related pages: