External Flow Owners

Many productions drive Luna from an external state machine: NodeCanvas, PlayMaker, a custom FSM, or plain game code with async flows. Luna needs no dependency on any of them, and none of them needs to own Luna's internals. This page is the ownership contract that makes the combination stable.

Who owns what

OwnerOwnsMust not own
Your flow layer (FSM, director, game code)Application flow: gates, start/retry/next decisions, save and purchase sequencing, waiting for modal and gameplay outcomesPanel trees, visual focus, transitions, a second copy of the back stack
LunaDestinations, stacks, channels, back semantics, modals, focus, transitions, occlusion, responsive presentationProgression, wallet truth, gameplay rules, business decisions
Your servicesContent, progress, currency, persistence: the source of truth the UI rendersVisual tree manipulation, navigation implementation

The test for any line of code: does it decide, or does it present? Decisions live in the flow layer and call Luna verbs. Presentation lives in views and reads services.

Drive Luna through its verbs

Keep the integration thin: one action per verb, ids from validated fields rather than raw strings where possible (a CatalogKey field constrained to the nav destination catalog gives you a dropdown and a stale-id warning in the inspector).

csharp
LunaNavigation.Push("hub.collection"); LunaNavigation.Push("hero.detail", new HeroDetailArgs { HeroId = id }); LunaNavigation.PopBackStack(); LunaNavigation.SwitchChannel("hub.shop");

Wait states map to awaitable pushes

PushAsync<T> is the bridge between a flow graph's wait state and a Luna modal. The task resolves exactly once, with either a value or a dismissal:

csharp
var result = await LunaNavigation.PushAsync<int>("confirm.retry", args); // result.IsDismissed -> closed by Esc / backdrop / cascade // result.Value -> what the modal passed to Pop<T>

Model the outcome as a single enum-like value in your graph (pending, confirmed, declined, dismissed) rather than several booleans whose stale values can fire a transition early. Keep no transition out of the pending state except the awaiter resolving.

Do not duplicate navigation state

Luna already knows the current destination, the active channel, and every stack. Read it; never mirror it:

  • Subscribe to DestinationChanged / ChannelChanged for flow-level reactions.
  • Query current state when deciding, instead of tracking your own "current screen" variable that drifts.

A mirrored back stack is the single most common integration bug: it disagrees with Luna's after the first cascade or channel restore, and every decision after that is wrong.

Gate per-view logic on first show

NavHost pre-spawns every destination hidden at boot. If an external controller starts its per-view logic in Awake, every screen's logic runs before the player has opened anything.

Gate on the view instead:

  • One-time wiring: view.WhenUILoaded(...).
  • Per-visit work: subscribe view.Fade.OnFadeInStart and read GetArgs<T>() there. See Typed Destinations.
  • Never synthesize show or fade events for Start Visible views: the host's boot seeding push already runs one real fade cycle, and firing your own double-counts every handler.

Defer your logic, never the view itself: views should spawn and bind with the host at boot. A view that binds after its parent's panel has painted is what produces template flashes and click-stealing at startup; in the host-owned flow that window does not exist, because born visibility is applied in the same panel update that builds the tree, before the first paint.

Occlusion is render state, not flow state

When a tab switches away or an opaque overlay settles, covered views stop rendering but stay logically open on their stacks. Do not pause, close, or reset flow logic from render occlusion; react to actual navigation events instead. See Occlusion.

Pause is two systems, on purpose

Luna owns the pause modal: its backdrop, focus, and back behavior. Your flow layer owns the wait state and the resume/retry/quit decision. Your session service owns the actual pause: time scale, input, simulation. Showing a modal alone is not a gameplay pause; wire all three.

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL