# Screens

> Screens in FlatRedBall2. Use when working with screen lifecycle, screen transitions, MoveToScreen, passing data between screens, CustomInitialize/CustomActivity/CustomDestroy, starting the first screen, pause menu, or pausing/resuming a screen. Trigger on any screen management or game state transition question.

- Skill: `vchelaru/screens` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/screens`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/screens/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/screens

---


# Screens in FlatRedBall2

A `Screen` is the top-level container for a game state — a level, a menu, a game-over sequence. Each screen owns its entities, collision relationships, Gum UI, and camera. Only one screen is active at a time.

## Creating a Screen

Subclass `Screen` and override the lifecycle hooks you need:

```csharp
public class GameScreen : Screen
{
    private Factory<Player> _playerFactory = null!;

    public override void CustomInitialize()
    {
        // Create factories, entities, collision relationships, Gum UI
        _playerFactory = new Factory<Player>(this);
        _playerFactory.Create();
    }

    public override void CustomActivity(FrameTime time)
    {
        // Per-frame game logic (after physics and collision have already run)
    }

    public override void CustomDestroy()
    {
        // Optional cleanup before the screen is torn down
        // Factories and entities are destroyed automatically — only needed
        // for external resources you opened manually
    }
}
```

## Lifecycle Order Each Frame

See `engine-overview` for the full 8-step frame loop. Key ordering for screens:

- Entity `CustomActivity` runs **before** Screen `CustomActivity` — the screen always sees post-entity, post-collision state.
- `CustomInitialize` runs once when the screen is activated, before the first frame.

## What's Available on a Screen

| Property | Type | Notes |
|----------|------|-------|
| `Camera` | `Camera` | This screen's camera — set background color, world bounds, position |
| `Engine` | `FlatRedBallService` | Access to `Random`, `Input`, `Audio`, etc. |
| `ContentLoader` | `ContentLoader` | Load textures, fonts, and other content |
| `RenderList` | `IReadOnlyList<IRenderable>` | All renderables sorted and drawn each frame; use `Add`/`Remove` to modify |
| `Layers` | `List<Layer>` | Named layers for render ordering |

## Returning Data from a Sub-Screen

When a sub-screen (e.g., a battle screen) needs to pass results back to the parent, use the `configure` callback on the *return* transition. The configure runs before the destination screen's `CustomInitialize`, so the data is ready immediately:

```csharp
// In BattleScreen — when battle ends, navigate back with the result:
MoveToScreen<ExplorationScreen>(s => s.ReturnedBattleResult = result);

// In ExplorationScreen — declare the property and read it in CustomInitialize:
public BattleResult? ReturnedBattleResult { get; set; }  // set via configure

public override void CustomInitialize()
{
    if (ReturnedBattleResult != null) { /* resume world state based on result */ }
}
```

If the return data must survive a full screen teardown (e.g., the destination is always freshly initialized and the caller doesn't retain a reference to set the configure payload), use a static field cleared in `CustomInitialize`. There is no push/pop screen stack by design — `MoveToScreen` + configure covers the common cases, and pause/HUD overlays belong in Gum:

```csharp
public class ExplorationScreen : Screen
{
    public static BattleResult? PendingReturn;  // BattleScreen sets this before transitioning back

    public override void CustomInitialize()
    {
        var result = PendingReturn;
        PendingReturn = null;    // clear — stale value on next entry otherwise
        if (result != null) { /* apply */ }
    }
}
```

## Navigating Between Screens

Call `MoveToScreen<T>()` from anywhere inside a screen (e.g., `CustomActivity`, a collision event, a Gum button click). The transition is deferred to the start of the next frame so the current frame completes cleanly.

```csharp
// Navigate with no data
MoveToScreen<MainMenuScreen>();

// Navigate with data — configure runs before CustomInitialize
MoveToScreen<GameOverScreen>(s =>
{
    s.Winner = "Player 1";
    s.FinalScore = _score;
});
```

The old screen's `CustomDestroy` runs, its content is unloaded, and then the new screen's `CustomInitialize` runs — all in that order.

## Passing Data to a Screen

Declare public properties on the destination screen. The `configure` callback receives the new screen instance *before* `CustomInitialize` is called, so the data is available immediately:

```csharp
using Gum.Forms.Controls;

public class GameOverScreen : Screen
{
    // Set by the caller via MoveToScreen configure
    public string Winner { get; set; } = "";
    public int FinalScore { get; set; }

    public override void CustomInitialize()
    {
        var label = new Label();
        label.Text = $"{Winner} wins! Score: {FinalScore}";
        Add(label);
    }
}
```

Caller:

```csharp
// From inside another screen's CustomActivity or a collision event:
MoveToScreen<GameOverScreen>(s =>
{
    s.Winner = _winner;
    s.FinalScore = _score;
});
```

## Starting the First Screen

In `Game1.Initialize`, after `base.Initialize()`. One call sizes the window, initializes the engine
and starts the screen:

```csharp
FlatRedBallService.Default.Initialize<MainMenuScreen>(this);
FlatRedBallService.Default.Initialize<GameScreen>(this, configure: s => s.DebugMode = true);
FlatRedBallService.Default.Initialize(this, "Content/FrbEditor/MyGame.gluj");  // whole Glue project
```

The `configure` callback is the same one `MoveToScreen` takes. `Start<T>` is the boot without a
`Game` — a headless test, or a game that decides its first screen after initializing.

## Observability — Screen.Entities and SceneSnapshot

`Screen.Entities` exposes all managed entities as `IReadOnlyList<Entity>` (read-only — mutation still goes through `Factory<T>`). Combined with `SceneSnapshot.Capture(screen)`, this lets test code inspect the full game state:

```csharp
var harness = TestHarness.Create<GameScreen>();
harness.Screen.CustomInitialize();
harness.Step(10);
var snap = harness.Snapshot();
// Query by type, name, or proximity
snap.OfType<Player>().ShouldHaveSingleItem();
snap.Named("coin").Count.ShouldBe(5);
snap.NearPoint(new Vector2(0, 0), 100f).ShouldNotBeEmpty();
```

## Gotchas

- **Delay before transitioning** — to pause before a screen change (e.g., a flash effect), `await Engine.Time.DelaySeconds(t, Token)` first, then call `MoveToScreen<T>()`. Never use `Thread.Sleep` — it freezes the render thread and blocks the game loop entirely.
- **`MoveToScreen` is deferred** — the transition does not happen immediately. Code after `MoveToScreen<T>()` in the same frame still runs. If you want to stop processing, `return` after the call.
- **Do not cache entity references across `MoveToScreen`** — the source screen's entities are destroyed on transition. Singletons and static fields must store data values only, not object references to entities.
- **Save data is not a content asset** — player save files (`PlayerData`, inventory, progress) cannot be loaded via `ContentLoader.Load<T>()`. Use `System.IO.File.ReadAllText` + `System.Text.Json.JsonSerializer` directly.
- **All entities and factories are destroyed automatically** on screen change. You do not need to manually destroy them in `CustomDestroy`.
- **Gum elements are also cleared automatically** — no teardown needed for elements added via `Add`.
- **No persistent cross-screen UI (sidebar nav, shared HUD shell) — by design.** `MoveToScreen` tears down all Gum content along with the old screen; there is no chrome/host layer that survives the swap. For UI that should appear on every screen, re-add the same Gum component in each screen's `CustomInitialize` (a shared base class works well) rather than building a persistent-chrome or push/pop-style host — that was proposed and rejected to keep `MoveToScreen`'s full-teardown-and-rebuild model the only lifecycle to reason about.
- **`CustomDestroy` is for external resources only** — e.g., file handles or network connections you opened yourself.
- **Do not call `MoveToScreen` from `CustomInitialize`** — the screen hasn't finished initializing yet. Use `CustomActivity` or an event callback.
- **Restarting the current screen:** call `RestartScreen()`. The engine recreates a fresh instance of the same type and replays the most recently retained configure callback. Use this for death/retry. Like `MoveToScreen`, the transition is deferred to the next frame.

```csharp
// Death/retry — replays the retained configure
RestartScreen();

// Restart with a tweak — replaces the retained configure for this and future restarts
this.RestartScreen(s => s.LevelIndex++);
```

  For "advance to next level" you can use either `MoveToScreen<SameType>` or the typed `RestartScreen` overload — both fully tear down and recreate. Use whichever reads more clearly at the call site.

  Do **not** manually destroy entities or collision relationships before calling either — the engine handles all teardown automatically.

- **One configure slot, last-writer-wins.** The engine retains a single configure callback per session. `Start<T>(d)` and `MoveToScreen<T>(d)` set it. `RestartScreen(d)` (typed extension) replaces it. Plain `RestartScreen()` replays whatever is currently in the slot.

- **Closure gotcha — prefer literals to captured locals.** Because the retained callback is replayed against its current closure environment (not a snapshot), any mutable local the callback closed over will be re-read at restart time. Write `s => s.LevelIndex = 3` rather than `s => s.LevelIndex = level` if `level` may have changed by restart time.

## Hot-Reload Restart

When a content file changes on disk and you want to restart the screen *without* losing session state (player position, score, timer), use the hot-reload restart mode:

```csharp
RestartScreen(RestartMode.HotReload);
```

This is identical to a death/retry restart with two extra calls bracketing the teardown:

1. `SaveHotReloadState(state)` runs on the OLD instance, before teardown — live game state is still intact, so you can read your fields and stuff them into the typed bag.
2. `RestoreHotReloadState(state)` runs on the NEW instance, after `CustomInitialize` — the level has been freshly built, then your restore patches saved values on top.

```csharp
public override void SaveHotReloadState(HotReloadState state)
{
    state.Set("score", _score);
    state.Set("timeRemaining", _timeRemaining);
}

public override void RestoreHotReloadState(HotReloadState state)
{
    _score = state.Get<int>("score");
    _timeRemaining = state.Get<float>("timeRemaining");
}
```

`HotReloadState` is a typed key/value bag: `Set<T>(key, value)`, `Get<T>(key)` (throws if missing), `TryGet<T>(key, out value)`.

**Plain `RestartScreen()` is `RestartMode.DeathRetry` and never calls these hooks** — by design, so death/retry can't accidentally preserve stale state across a death.

**Restore runs after `CustomInitialize` intentionally.** `CustomInitialize` builds the level from scratch, then restore patches saved values on top. The reverse order would let `CustomInitialize` clobber whatever restore set.

**The engine does not auto-preserve anything.** Hot-reload preservation is entirely user-driven via `Save`/`RestoreHotReloadState`. In particular, **preserve player position to avoid a jarring camera pop**: if the player is restored to their pre-reload position, any `CameraControllingEntity` will follow them on the first frame and the camera lands correctly automatically. If you don't restore the player, the player respawns at the spawn marker and the camera snaps to the spawn point, even if you tried to preserve `Camera.X/Y` directly (the controller overwrites it on frame 1).

Canonical recipe:

```csharp
public override void SaveHotReloadState(HotReloadState state)
{
    state.Preserve(_player);           // position, velocity, acceleration, rotation, rotation velocity
    state.Set("score", _score);        // custom values still use Set/Get
}

public override void RestoreHotReloadState(HotReloadState state)
{
    state.Restore(_player);
    if (state.TryGet<int>("score", out var s)) _score = s;
}
```

`state.Preserve(entity)` / `state.Restore(entity)` bundle the eight kinematic properties (six for `Camera`, which has no rotation). Call them in **matching order** in Save and Restore — keys are auto-generated per type (`Player_1`, `Player_2`, etc.), so asymmetric call order silently swaps state between entities of the same type. For multi-instance scenarios where explicit names are clearer, pass the optional second argument: `state.Preserve(_heroA, "heroA")`.

Use `Set`/`Get`/`TryGet` directly for everything else (score, timers, inventory) — the helper only covers motion-and-rotation.

## Pausing

`Screen` has built-in pause support:

```csharp
PauseThisScreen();    // freeze entities
UnpauseThisScreen();  // resume
bool paused = IsPaused;
```

**What pauses:** entity physics, entity `CustomActivity`, collision processing.
**What keeps running:** `Screen.CustomActivity`, Gum UI, input — so pause-menu logic lives in `CustomActivity` and Gum overlays still update.

Typical pattern:

```csharp
public override void CustomActivity(FrameTime time)
{
    if (Engine.Input.Keyboard.WasKeyPressed(Keys.Escape))
    {
        if (IsPaused) { UnpauseThisScreen(); _pauseOverlay.IsVisible = false; }
        else          { PauseThisScreen();   _pauseOverlay.IsVisible = true;  }
    }
}
```

`await`-based delays from `Engine.Time.DelaySeconds` and `DelayUntil` **are** suspended while the screen is paused — screen time freezes, so timed tasks don't fire. `DelayFrames` is not pause-aware; frames always advance. See the `timing` skill for full async delay details.

