Lifecycle readiness registry¶
Gameplay Tag subscriptions¶
Blueprint objects can call Register For Lifecycle Gate with only a GateTag and a
dynamic callback. The callback owner is inferred as both listener and subject, so no BeginPlay
role snapshot is needed and a shared subsystem event cannot leak another pawn's notification.
The callback receives the opened tag and subject. Each registration gets a unique internal
handle, is one-shot for the subject's current generation, and delivers synchronously if that
exact subject/tag is already open. Separate registrations never replace each other.
Initial public gate tags are:
Lifecycle.Gate.Character.LocalReadyLifecycle.Gate.Character.RemoteReadyLifecycle.Gate.LocalPlayer.WidgetControllerAndTeamReady(subject: local PlayerState)
Character routing is exclusive: autonomous proxies are local, simulated proxies are remote,
authority pawns use their resolved controller, and unresolved authority waits. Remote readiness
still depends on the local dispatch-returned fact. Native code can use
RegisterForLifecycleGateNative to choose a different listener and subject and retains a
unique handle for explicit cancellation. Tags remain FGameplayTag values throughout the
registry; subscriptions do not convert them to FName.
Registrations are cancelled by invalid listener/subject pruning, subject or listener generation
reset, world teardown, and subsystem shutdown. Gate tags use exact matching, not parent matching.
Definitions live in Config/Tags/LifecycleSubsystemTags.ini.
Local widget controller and team gate¶
Bind OnLocalPlayerWidgetControllerAndTeamReady(LocalPlayerState) on the lifecycle subsystem.
It waits for the widget controller of the team subsystem's local controlled PlayerState and
a processed, valid MatchTeamId for that same cached PlayerState. Remote UI cannot satisfy it.
It does not require a pawn, possession, CharacterDef, or a character-init gate.
The team adapter samples both sources on UI-ready/reset/removal, team-cache changes, and character relationship notifications. It also samples current state when attaching to a world. The event fires once per continuous readiness period; UI loss, local PlayerState replacement, or loss of a valid team closes the combined state. Ordinary valid-to-valid team changes do not repeat it. World teardown clears the state, and dedicated servers do not dispatch it.
Late Blueprint subscribers can call IsLocalPlayerWidgetControllerAndTeamReady().
The combined cache gate is
BrickLeafReadinessFacts::Gate_LocalPlayer_WidgetControllerAndTeamReady, scoped to the world.
Its prerequisites are world-scoped Fact_LocalPlayer_TeamReady and PlayerState-scoped
Fact_UI_WidgetControllerReady. The broadcast supplies the local PlayerState;
it does not initialize the UI or assign a team itself.
Ownership and adapters¶
UBrickLeafLifecycleSubsystem owns the generic registry, world-lifetime cleanup, travel coordinator, and native feature adapters. Each adapter has its own header/cpp under Private/Core/Lifecycle:
FCharacterLifecycleAdapter: samples pawn relationships/CharacterDef/presentation, applies local/remote policy, latches before dispatch, and replays remote Blueprint RepNotifies afterward.FUILifecycleAdapter: publishes PlayerState UI readiness; invalidates it on tracking reset/removal.FTeamLifecycleAdapter: observes immediate team-cache changes and schedules the existing presentation-onlyOnTeamChanged. It coalesces pending changes and rechecks live UI before broadcasting.FAuthenticationLifecycleAdapter: observes validated Nakama login state and connects finalized deployment travel to the coordinator.FGameModeLifecycleAdapter: observes post-login/logout and publishes controller-scoped login state.
Producers expose native domain events and queryable state; they do not include, resolve, or call the lifecycle subsystem or registry. Character, team, and GameMode source notifications are process-wide native delegates, filtered by each adapter's GameInstance/world. They carry no global readiness state. UI and authentication delegates are instance-local. Adapter shutdown removes every native subscription before destruction; world teardown cancels that world's pending gates. Existing source state is sampled when adapters/worlds are attached, so startup events are not the only readiness source.
To add a feature, implement FLifecycleFeatureAdapter, register it in the coordinator, publish facts with the correct object/lifetime token, and register an AND gate. Keep feature policy out of FLifecycleGateRegistry. These adapters perform no polling.
Login facts¶
Named constants are available in LifecycleReadinessFacts.h:
BrickLeafReadinessFacts::Fact_Nakama_LoginSucceeded: scopeUNakamaConnectionSubsystem, lifetimeGameInstance. Published on the validated account-success path immediately before the existing login-success broadcasts. It followsOnNakamaLoginSucceeded, not the nonfatal account-fetch-error path that emitsOnNakamaLoginComplete(Success, EmptyAccount). Session abandonment, authentication failure, EOS-session replacement, blocking, and shutdown close the fact. World travel alone does not clear it.BrickLeafReadinessFacts::Fact_GameMode_PostLogin: scope the specific controller, lifetime its world. Published afterSuper::OnPostLoginreturns (including BlueprintOnPostLoginand the engine event), before the engine proceeds to starting the player. Logout retires the controller scope. This does not imply possession/pawn readiness. Seamless travel does not fabricate a PostLogin event.
Both are optional facts; no existing character/UI requirements were changed. They are process-local: client Nakama success and server PostLogin are not automatically combined across the network. GameMode PostLogin can be consumed on listen/standalone hosts; the lifecycle subsystem remains disabled on dedicated servers.
auto& Gates = Lifecycle->GetReadinessGates();
const auto Auth = Gates.Scope(NakamaSubsystem, GameInstance);
const auto Controller = Gates.Scope(PlayerController, World);
Gates.WaitForAll(Controller, TEXT("Feature.AfterBothLogins"),
{{Auth, BrickLeafReadinessFacts::Fact_Nakama_LoginSucceeded},
{Controller, BrickLeafReadinessFacts::Fact_GameMode_PostLogin}}, Callback);
UBrickLeafLifecycleSubsystem::GetReadinessGates() exposes a native, game-thread-only registry. Facts are named booleans scoped to object identity and initialization generation. A named gate is a one-shot AND of requirements, potentially spanning multiple objects. Registering the same owner/name replaces its pending request; registering again after completion explicitly requests another notification.
auto& Gates = Lifecycle->GetReadinessGates();
const auto Player = Gates.Scope(PlayerState, World);
const auto Pawn = Gates.Scope(Character, World);
Gates.WaitForAll(Pawn, TEXT("Feature.Initialize"),
{{Player, BrickLeafReadinessFacts::Fact_UI_WidgetControllerReady},
{Pawn, BrickLeafReadinessFacts::Fact_Character_BeginPlayReady}},
[WeakCharacter]() { /* resolve weak pointer and initialize */ });
// Capture Pawn when starting asynchronous work, then return to the game thread:
Gates.SetReady(Pawn, BrickLeafReadinessFacts::Fact_Character_BeginPlayReady);
Use SetReady(Token, Fact, false) for reversible readiness within the same lifetime. ResetScope(Object) retires the entire generation and cancels gates referencing it. A delayed result with the old token is rejected; do not acquire a fresh token inside an old asynchronous completion. ResetLifetime(World) retires every scope associated with that world, including persistent LocalPlayer objects. Callers must re-register gates after a scope reset.
The cache uses weak identities and does not retain actors. Gate callbacks should also capture weak objects. Completion removes the request before invoking the callback; nested registrations are drained afterward. GetBlockingFacts(Owner, GateName) provides a lightweight diagnostic query. The registry currently scans pending requests on fact updates rather than maintaining a dependency index; this keeps cancellation and reentrant invalidation simple for the small number of startup gates.
Migrated policies¶
- Team presentation: PlayerState-scoped
Lifecycle.Fact.UI.WidgetControllerReady. Repeated team changes replace a pending request and dispatch the current team through the existing Blueprint event. No pawn dependency. - Character initialization: PlayerState team/CharacterDef facts plus pawn BeginPlay, class-match and presentation facts. Relationship changes replace the pawn's request. Network roles select policy; they are never cache keys.
OnLocalCharacterInitGateOpenedandOnRemoteCharacterInitGateOpenedannounce satisfied prerequisites, not completed initialization. The local notification returning publishesLifecycle.Fact.Character.LocalDispatchReturned. Remote gates require this fact for each local viewer. It means only that synchronous local gate-open dispatch returned; latent/asynchronous work is not awaited. The UI subsystem still has its existing single-local-player assumptions; this registry does not add full split-screen UI support.- World BeginPlay publishes
Lifecycle.Fact.World.BeginPlay. Existing travel-transition orchestration remains unchanged.
The adapters sample Character/PlayerState/UI source state into facts. UI readiness is invalidated on UI tracking resets; PlayerState removal retires its scope and associated pawn gates (including already-open remote gates); unpossession retires the pawn scope and revokes its local-player dispatch fact; teardown retires the world lifetime. Dedicated servers still do not instantiate this presentation lifecycle subsystem.
The former pawn Blueprint hooks UponLocalCharacterInitialized / UponRemoteCharacterInitialized have been removed. Blueprint consumers must migrate to the renamed subsystem gate-open delegates themselves; no Blueprint assets or redirect aliases were migrated. Team-change signatures remain unchanged. The character adapter records the pawn's gate-open state before broadcasting, preventing reentrant duplicate dispatch. Remote Blueprint RepNotifies are replayed after synchronous remote gate-open dispatch returns, without claiming that arbitrary initialization finished. These multicast delegates do not replay for late subscribers.
Late consumers¶
Blueprint consumers can query IsCharacterInitGateOpen(Character) and bind to OnLocalCharacterInitGateOpened / OnRemoteCharacterInitGateOpened. Native consumers can use WaitForCharacterInitGateOpened(Listener, RequestName, Character, Callback): it invokes once after gate dispatch, including immediately when dispatch already occurred. The helper uses the cached pawn fact Lifecycle.Fact.Character.InitGateOpened; it does not mean asynchronous initialization completed. Listener destruction, relationship resets, and world teardown cancel pending requests. Re-register after a reset. Callbacks should capture weak objects.
There is deliberately no generic “initialization completed” acknowledgement: these APIs promise open prerequisites and synchronous dispatch ordering only.
Automation coverage is under MetalTerra.Lifecycle.GateRegistry: AND semantics, independent identities, ready-before-registration, one-shot/repeated requests, coalescing, reentrant reset, stale generation rejection and world cleanup. Multiplayer arrival-order and travel/respawn scenarios still require PIE validation.