Skip to content

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:

  1. Domain classes expose their existing state and native notifications.
  2. Coordinator-owned feature adapters observe those sources.
  3. Adapters publish facts and define feature-specific gates.
  4. 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 current FLifecycleScopeToken. 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) and IsReady(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_AutonomousProxy is local.
  • ROLE_SimulatedProxy is remote.
  • ROLE_Authority uses its resolved controller's IsLocalController() 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:

  1. PlayerState team processing ready.
  2. PlayerState CharacterDef ready.
  3. Pawn BeginPlay ready.
  4. CharacterDef/class policy satisfied.
  5. The owning client controller has acknowledged this pawn through AcknowledgePossession.
  6. 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:

  1. PlayerState team processing ready.
  2. Pawn BeginPlay ready.
  3. Remote client CharacterDef/class match.
  4. Simulated-proxy widget-controller presentation ready.
  5. Lifecycle.Fact.Character.LocalDispatchReturned for 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.WidgetControllerReady for 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); and
  • IsLocalPlayerWidgetControllerAndTeamReady().

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

  1. Decide the subject and lifetime first. Use the narrowest object whose identity owns the truth, and a lifetime that reliably retires it.
  2. Add the gameplay tag to LifecycleReadinessFacts.h, LifecycleReadinessFacts.cpp, and Config/Tags/LifecycleSubsystemTags.ini.
  3. 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.
  4. Close reversible facts on source loss. Use ResetScope when the entire object relationship is replaced.
  5. Define requirements in the feature adapter, not in FLifecycleGateRegistry.
  6. Revalidate live source state in consequential callbacks; cached readiness is a wake-up mechanism, not ownership of domain truth.
  7. Expose a Lifecycle.Gate.* tag when Blueprint or cross-feature consumers need a stable public contract. Keep implementation facts internal where possible.
  8. Add automation coverage for ready-before-register, both arrival orders, duplicate source notifications, replacement generations, and world cleanup.
  9. 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: