# Run and Observe Shared procedure for any skill that closes a story which changes something a player can see. Referenced from the point of use in `/dev-story` (Phase 6) and `/story-done` (Phase 3). Those skills keep the one load-bearing imperative inline — **a parse check is not a run, and a story nobody looked at does not close** — and cite this file for how to look. `.claude/docs/coding-standards.md` is the authority for *what* evidence a story needs. This file is the procedure for *producing* the visual half of it. ## The rule Every story that changes something player-observable is **launched and observed before it closes**, and the observation is a **retained screenshot** in `production/qa/evidence/`. Compiling proves the code loads. Tests prove the logic. Neither proves the text fits in the box. The agent can see images: Claude Code's `Read` tool renders a PNG. The whole problem is getting a picture out of the engine unattended, and every supported engine has a native way. ## The procedure 1. **Launch windowed** at a fixed resolution — `1280x720` unless the project says otherwise — using `commands.run` from `project.yaml`. Never the headless / batch / null-RHI flag: those exist to *skip* rendering. 2. **Launch straight into the scene or map the story touched.** A feature that is only reachable through the main menu and three clicks is a feature the run will not reach. Stories should be written so their surface is reachable from a launch argument (a scene path, a map URL, a `--scene` flag). 3. **Wait, capture, quit.** Let the engine render real frames — 60 is a safe floor — capture one, and exit. Each engine's mechanism is below. 4. **Look.** `Read` the PNG. Compare what is on screen to the acceptance criteria. Clipped text, an overflowing panel, a missing element, the wrong colour — these are defects, and this is the only step that finds them. 5. **Retain.** Copy the image to `production/qa/evidence/[story-slug]/[NN]-[what-it-shows].png` and reference it from the evidence doc. A screenshot that was taken and discarded is an assertion, not evidence. 6. **Report** one `Run result:` line in the implementation summary — vocabulary below. ## Per engine | `engine.name` | Launch | Capture | Output lands in | Never use | Scaffold needed | |---|---|---|---|---|---| | Godot 4 | `godot --path . --windowed --resolution 1280x720 .tscn` | `--write-movie .png --quit-after ` | beside the given path, `00000000.png` … | `--headless` | **none** | | Unity 6 | built player `.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene ` | `ScreenCapture.CaptureScreenshot()` from a bootstrap script | the absolute path you pass | `-batchmode`, `-nographics` | one MonoBehaviour | | Unreal 5 | `MSYS_NO_PATHCONV=1 "/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null || pwd)/.uproject" -game -windowed -ResX=1280 -ResY=720` | `HighResShot 1280x720 filename=` console command | the path you pass (else `Saved/Screenshots/WindowsEditor/`) | `-nullrhi`, `-unattended` alone | one actor or `-ExecCmds` | ### Godot 4 — zero scaffold ``` godot --path . --windowed --resolution 1280x720 --write-movie shots/shop.png --quit-after 60 res://scenes/shop.tscn ``` `--write-movie` is Movie Maker mode: with a `.png` target it writes a frame sequence and `--quit-after` sets the frame count. Filenames carry **8** zero-padded digits — `shop00000000.png` through `shop00000059.png` — so take the last. `--fixed-fps` is applied automatically. The scene path is positional; any `.tscn` can be launched directly, which is how step 2 is satisfied. Capture-on-trigger alternative, for a state a scene launch cannot reach: an autoload that on the wanted condition does `await RenderingServer.frame_post_draw`, then `get_viewport().get_texture().get_image().save_png(path)`, then `get_tree().quit()`. ### Unity 6 — one MonoBehaviour `-batchmode` displays nothing, so the run is either the **built player** or the editor in play mode. The player is the robust path: ``` Builds/Win64/Game.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene Shop --screenshot C:/abs/path/shop.png --after 60 ``` `-screen-*` are Unity's own player arguments. `--scene`, `--screenshot` and `--after` are **yours**, read by the script below. The template does not ship it: the first time a Unity story needs a screenshot, `/dev-story` asks to write it, verbatim, to `Assets/Scripts/ScreenshotOnArg.cs`. It needs no scene edit — it boots itself, and does nothing unless the player was launched with `--screenshot`: ```csharp using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; // Run-and-observe capture: Game.exe --scene Shop --screenshot C:/abs/shop.png --after 60 public class ScreenshotOnArg : MonoBehaviour { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { if (Arg("--screenshot") == null) return; var go = new GameObject(nameof(ScreenshotOnArg)); DontDestroyOnLoad(go); go.AddComponent(); } static string Arg(string name) { var args = System.Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length - 1; i++) if (args[i] == name) return args[i + 1]; return null; } IEnumerator Start() { var scene = Arg("--scene"); if (!string.IsNullOrEmpty(scene)) yield return SceneManager.LoadSceneAsync(scene); int after = int.TryParse(Arg("--after"), out var n) ? n : 60; for (int i = 0; i < after; i++) yield return null; yield return new WaitForEndOfFrame(); ScreenCapture.CaptureScreenshot(Arg("--screenshot")); for (int i = 0; i < 3; i++) yield return null; // the file is written after the frame Application.Quit(); } } ``` Pass an **absolute** path — the relative base differs between editor and player. `--scene` must be in the build settings. Building the player first costs 1–3 minutes on a small project; that is the price of looking at the thing you ship rather than the editor. ### Unreal 5 — one actor, or `-ExecCmds` ``` MSYS_NO_PATHCONV=1 timeout 60 "/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null || pwd)/Game.uproject" /Game/Maps/Shop -game -windowed -ResX=1280 -ResY=720 ``` That line is Windows'. On Linux the same arguments go to `"/Engine/Binaries/Linux/UnrealEditor"` (no `MSYS_NO_PATHCONV` needed); on macOS Epic documents only the `UnrealEditor.app` bundle, so run the map from the editor or a command you have confirmed (see `docs/engine-reference/unreal/current-best-practices.md`, "Command Line"). `-game` runs standalone without the editor UI. The map is a **positional URL immediately after the `.uproject`** — not a console command. **The `MSYS_NO_PATHCONV=1` prefix is required on Windows:** Claude Code runs commands in Git Bash, which rewrites any argument beginning with `/` as a Windows path, so `/Game/Maps/Shop` reaches the engine as `C:/Program Files/Git/Game/Maps/Shop` and the game stops on a "map not found" dialog nobody can see. Alternatively pass the map through `-ExecCmds="open /Game/Maps/Shop"`, which is not converted. **The project path must be absolute** — with `MSYS_NO_PATHCONV` set, only `$(pwd -W 2>/dev/null || pwd)` gives one: a relative `Game.uproject` is not found, and the launch waits on an error dialog forever (verified on 5.7). There is no `-unattended` on this line, so every failure is a dialog nobody clicks: always run it under `timeout`. Capture is the `HighResShot` console command, fired either from a dev actor (`Execute Console Command` on BeginPlay after a short delay, then `quit`) or from the launch line: ``` -ExecCmds="HighResShot 1280x720 filename=C:/abs/path/shop.png" ``` The size (or a multiplier such as `2`) comes **before** `filename=`: `HighResShot filename=…` alone is rejected as bad input and saves nothing. Without `filename=` the file lands in `Saved/Screenshots/WindowsEditor/`. **Do not put `Quit` in the same `-ExecCmds` list** — it runs on the same frame, before the file is written, and leaves nothing on disk. `-ExecCmds` has no delay, so let `timeout` end the launch (exit 124 is expected) and check the file exists; a dev actor can instead wait a second, then `quit`. The Blueprint node `Take High Res Screenshot` is editor-only and does not work in a `-game` launch. ### Any engine — OS capture fallback On Windows, PowerShell with `System.Drawing` can `CopyFromScreen` into a bitmap and save it. It needs a visible desktop and the game window on top, and it captures whatever is there — it cannot be told "the shop panel". Use it when the engine-native path is unavailable, and say so in the run result. Linux CI with no display needs `xvfb-run` in front of the launch; `--headless` is not a substitute. ## The `Run result:` line Exactly one of three shapes, in the implementation summary: - **`Run result: OBSERVED — `** with the retained path. *"Shop panel renders, all 6 slots inside the frame, prices legible at 720p — `production/qa/evidence/shop-panel/01-shop-open.png`."* - **`Run result: NOT VERIFIED — `**. Engine binary not found, `commands.run` unset, launch crashed, screenshot empty. This is a **blocker** at the default gate level for Visual/Feel and UI stories, not a note — a story nobody looked at does not close. Under an explicit `testing.strict.visual`/`.ui: false` it is recorded and the story may close with the gap in `## Completion Notes`. - **`Run result: N/A — `**, only for a story with genuinely nothing observable — a pure data migration, a save-format change with no surface. Say why. "It's a Logic story" is not a reason: a damage formula has a number on screen somewhere. The run is **not waived at `qa.level: minimal`**. Tests are waived there; the look is not. It is the cheapest verification in the pipeline and the one whose absence produces a well-tested game that looks wrong. ## What a still can and cannot show **Can:** layout, clipping, overflow, presence, alignment, colour, legibility, that the right screen appeared at all. This is where shipped visual defects actually live. **Cannot:** timing, responsiveness, weight, animation curves, audio sync — the "feel" half of Visual/Feel. Godot's `--write-movie` gives a frame sequence; the other engines need OS video capture. Feel stays a human check in `/team-qa`. Say which half the screenshot covered. ## Reaching the state A screenshot of the title screen verifies nothing about depth 2. Either launch the scene/map directly (Godot scene arg, Unreal map URL, Unity `--scene`) or give the bootstrap a debug argument that sets state (`--gold=500 --depth=2`). Write stories so their surface is reachable one of those ways; a story that is not is a story that will be marked `NOT VERIFIED` every time. ## Finding the engine None of the three engines installs onto `PATH` on Windows. `engine.path` in `project.yaml` is where the editor lives; resolve it before concluding "no engine on this machine". Absence of a path is not absence of an engine. --- *Commands above were run on Windows with Godot 4.6.1 (gdUnit4 6.1.3), Unity 6000.3.23f1 and Unreal Engine 5.7; the Unreal Linux and macOS notes come from Epic's documentation. `docs/engine-reference//` is the project's pinned authority; check it before trusting a version-qualified claim here.*