# Play-mode verification loop Part of the **`unity-cli`** skill. See that skill's `SKILL.md` for CLI install, global flags, environment variables, exit codes, and the general "drive a running Editor" workflow. All global flags (`--format json`, `--non-interactive`, `--yes`, `--proxy`, ...) apply to every command below. [integration-advanced.md](integration-advanced.md) covers the Pipeline command surface generally; this page is the recipe for one specific loop: enter Play mode, confirm the game is actually running (not frozen), look at it, read its console, and tune it, all without leaving the CLI. ## Why this loop, and not `editor_play` + a screenshot A screenshot renders on demand: it can capture the last frame a frozen Game view drew and look exactly like a live game. Treat "Play mode entered" and "a screenshot came back" as setup, not proof. The loop below adds one cheap, server-side check (`wait_for` on `Time.frameCount`) that actually proves frames are advancing before you trust anything else you observe. **Command names, parameters, and defaults below are the connected Editor's, not the CLI's own**: they come from the project's `com.unity.pipeline` package and can change between package versions. Confirm the exact set for your project with `unity command` / `unity list` before relying on any of this, the same caution [integration-advanced.md](integration-advanced.md#available-in-production--the-common-live-commands) already gives for `eval`. **Put pipeline-specific flags after a `--` fence.** `unity command` recognizes a small set of its own flags (`--project-path`, `--runtime`, `--runtime-path`, `--timeout`, `--detach`, `--result-only`, plus the global flags) anywhere in the tail, not just at the start, so a same-named Editor parameter (`eval`'s own `timeout`, in **milliseconds**) is swallowed by the CLI's `--timeout` (**seconds**) unless it comes after `--`. Every multi-flag example below uses the fence; a single positional argument (like a bare `eval` call at its default timeout) does not need one. ## The recipe ### 1. Confirm a connected Editor and discover its commands ```bash unity status --format json # look for state "ready" unity command --format json # the full catalog, with each command's parameter schema ``` If nothing is reachable, rule out Safe Mode and a sandboxed shell before concluding the Editor is down: see [integration-advanced.md → Recovering from Safe Mode](integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors) and [integration-advanced.md → Sandboxed agent tooling can hide a running Editor](integration-advanced.md#sandboxed-agent-tooling-can-hide-a-running-editor). ### 2. Keep the Editor ticking while it is unfocused Unity throttles `EditorApplication.update` when the Editor window does not have focus, which is the common case for an agent driving it headlessly or alongside other work. On an unfocused Editor, entering Play mode can leave the game genuinely stuck at frame 1 while `unity status` still reports it as playing, because the per-frame loop that would advance it is the thing being throttled. `com.unity.pipeline` ships a command for exactly this, `set_autotick`, which forces a tick at a throttled rate even while unfocused: ```bash unity command set_autotick # enable=true, interval_ms=16 (~60 Hz), persist=true by default ``` Call it once per Editor session, before `editor_play`. `persist=true` (the default) survives a domain reload, so you do not need to call it again after a recompile in the same session; pass `-- --persist false` for a one-off setting that should revert after the next unrelated recompile instead of sticking for the rest of the session. This is the confirmed, currently available fix for the freeze, not a temporary stand-in: it is a real Pipeline command with its own doc entry (`Documentation~/commands/editor-lifecycle-and-observability.md`), not something a future package release removes the need for. As of this writing the CLI does not call it automatically before `editor_play` (checked `apps/cli/src/Commands/Gaps/EditorPlaymode/PlaymodeHandler.cs`, which sends only the bare `editor_play` command). Call it yourself until that changes. The player loop has a second switch: the project's Run In Background Player Setting (`Application.runInBackground`). On macOS and Windows, with it off, Unity pauses the game while the Editor is unfocused, and `set_autotick` can't undo that pause. The setting ships in builds, so turn it on explicitly with the Pipeline command, which records the project's value and keeps it out of builds: ```bash unity command editor_set_run_in_background # on for this Editor session unity command editor_set_run_in_background -- --enable false # put the project's value back ``` The override doesn't touch the project's saved settings, so it lasts until you revert it or the Editor restarts. After a restart, run the command again. Alternatively, a project can turn on **Ensure editor runs in background in PlayMode** under Project Settings > Pipeline > Editor, which applies it only while playing. Check that your package has the command with `unity command --query editor_set_run_in_background --format json`. On an older package, set it with `eval` before `editor_play`, and put the project's value back after `editor_stop`, because nothing keeps it out of builds there: ```bash unity command eval "return UnityEngine.Application.runInBackground;" # note the project's value unity command eval "UnityEngine.Application.runInBackground = true;" ``` ### 3. Enter Play mode, then wait for the Pipeline server before the next command ```bash unity command editor_play unity status --until-ready --project-path --format json ``` `editor_play` drops the Pipeline listener for a few seconds while the domain reloads. Sending the next command immediately can hit `EDITOR_NOT_READY` or simply queue behind the reconnect. `unity status --until-ready` is the documented fix, not a hand-rolled polling loop: it keeps checking until a matching Editor reports `ready`, then returns. Full detail in [integration-advanced.md → status](integration-advanced.md#status--live-state-of-connected-editors). ### 4. Confirm the game is actually advancing, not frozen at frame 1 A `wait_for` on `Time.frameCount` with `op: changed` proves the frame counter has moved since the wait started, server-side, with no client polling loop: ```bash unity command wait_for -- \ --condition '{"member":"UnityEngine.Time.frameCount","op":"changed"}' \ --timeout_s 10 ``` `op: changed` needs no `value`: it is met the first time the member differs from the value observed when the wait was armed. A `met: true` result with `framesObserved` > 0 is your proof; `timedOut: true` with the frame count unmoved is the frozen-at-frame-1 symptom this whole step exists to catch, and step 2 (`set_autotick` and `runInBackground`) is the fix to check first. A package that has `editor_set_run_in_background` also reports the player loop in `unity status --format json` while in Play mode: `frameCount` and `playerLoopTicking` on the instance row. `playerLoopTicking: false` outside a pause is the same frozen symptom, and it costs one read instead of a wait. Keep the `wait_for` above as the proof; the status fields are a quick first check, and an older package omits them. `timeout_s` defaults to 30 if omitted; 10 seconds above is a shortened budget, since a ticking Editor should advance the frame count within a couple of frames of the wait being armed. `wait_for` is one `/api/exec` request that holds the exec queue for its own duration, so a synchronous call like the one above is only for a wait you expect to resolve in a few seconds. For anything longer, or a condition that depends on a command you still need to issue, add `--async true` and poll the result instead of blocking on it: ```bash unity command wait_for -- --condition '{"member":"UnityEngine.Time.frameCount","op":"changed"}' \ --timeout_s 120 --async true # returns { "waitId": "..." } immediately unity command wait_status -- --wait_id ``` `wait_for`'s own `--async` is unrelated to `unity command`'s `--detach`: the former is the connected Editor evaluating the condition in the background and reporting through `wait_status` / `wait_cancel`; the latter is the CLI's own client-side job tracking (`unity job status` / `unity job wait`) for a command that runs long on the Editor's main thread. Don't conflate the two. `wait_for` also accepts a `findType`/`target` + instance member path instead of a static one (for example a gameplay manager's own state field), and an `on_met.capture` follow-up that captures the game view in the same frame the condition first holds, atomically, when a client-side "wait, then screenshot" loop would risk missing a short-lived state. Discover the exact shape (and whether your project's package version has it at all) with `unity command --query wait_for --format json` rather than assuming it: `wait_for`/`wait_status`/`wait_cancel` are a comparatively new addition to `com.unity.pipeline`. ### 5. Capture the game view without bloating the response ```bash unity command capture_game_view -- --source screen --save_path Screenshots/playtest.png ``` Pass `save_path` and the result is a file path, not an inline base64 image, which keeps an agent's tool result small. The path is **project-relative** (the package's own docs give `Screenshots/foo.png` as the example); nothing in the package documentation states a restriction to paths under `Assets/`, so do not assume one. When you do need the image inline instead of, or as well as, the file (for example to look at it yourself in this turn), add `--include_inline_image true` and use `--max_resolution` to cap the inline copy's longest edge without touching the resolution of the saved file: ```bash unity command capture_game_view -- --max_resolution 512 # inline only, downscaled, no save_path ``` `source=screen` captures the composited game view (including Screen Space - Overlay UI such as HUDs); `source=camera` renders a single camera off-screen instead and never sees overlay canvases. Pass whichever you mean explicitly: the command's default value for `source` has changed between package versions (`com.unity.pipeline` 0.8.0-exp.1 switched the default from `camera` to `screen`), so relying on the default is one more thing to go stale as the project's package updates. ### 6. Read the console for exceptions before trusting a clean-looking capture ```bash unity command console -- --tail 50 --level error ``` `level` is a minimum-severity floor (`log` | `warn` | `error`, default `log`), not an exact match, so `--level error` returns errors and exceptions only. Prefer `console_status` (no arguments) when you only need the compile-failure flag and entry counts, not the entries themselves, to poll cheaply while something else runs. Older `com.unity.pipeline` documentation names this command `get_console_logs`; the currently shipping command is `console` (with `console_status` and `clear_console` alongside it), which is a case in point for confirming names against `unity command` rather than trusting any cached documentation, this page included. ### 7. Tune the running game with `eval`, not edit, recompile, and re-enter Play mode Once Play mode is confirmed live, prefer evaluating C# against the running instance over leaving Play mode to edit a script, recompiling, and re-entering: it is one round trip instead of a domain reload plus a fresh Play mode entry, and it does not lose whatever state you were verifying. ```bash unity command eval "UnityEngine.Time.timeScale = 0.5f;" ``` A bare positional string is `eval`'s `code` parameter at its default 5-second timeout. Override the timeout with the fully-named form (`timeout` is milliseconds on `eval`, not the seconds `unity command --timeout` takes, which is exactly the collision the `--` fence in this page's intro exists for): ```bash unity command eval -- --code 'return UnityEngine.Time.frameCount;' --timeout 15000 ``` Reach for `create_script` → `unity recompile` → `attach_script` (documented in [integration-advanced.md → Authoring custom `[CliCommand]` tools](integration-advanced.md#authoring-custom-clicommand-tools)) only when the change genuinely needs a new type or a persistent script asset. For a one-off runtime tweak inside a live Play mode session, `eval` is cheaper and keeps the loop in this page intact. ### 8. Exit Play mode when you are done ```bash unity command editor_stop ``` ## Summary: the ordered commands ```bash unity status --format json unity command set_autotick unity command editor_set_run_in_background unity command editor_play unity status --until-ready --project-path --format json unity command wait_for -- --condition '{"member":"UnityEngine.Time.frameCount","op":"changed"}' --timeout_s 10 unity command capture_game_view -- --source screen --save_path Screenshots/playtest.png unity command console -- --tail 50 --level error unity command eval "" unity command editor_stop unity command editor_set_run_in_background -- --enable false ```