Unity development toolkit
Everything that helps agents build the Unity client — batchmode build/test commands, the project skills in zoen/.claude/skills, and the reviewed GitHub tools (Unity MCP, GameCI, Unity samples, Sentry, OpenTelemetry, audio and asset validators) with licence, use and adoption gate.
The owner accepted ADR-024 in round 6: the client is Unity 6 LTS + URP (C#) for Windows, Android, iOS and the Web (Web capped at Low–Mid). This page lists what helps agents work on it, and the rules for adopting each tool. Nothing here is installed yet. No tool is installed without the gate below.
◆How agents drive Unity (no GUI needed)
The pin is Unity 6.3 LTS (newest 6000.3 patch; supported until December 2027, checked 2026-10-06). The owner installs Unity Hub and the editor, or an agent runs the Hub command line (allowed by the owner on 2026-10-06). The owner signs in once with their Unity ID; agents never create accounts or sign in. After that, agents use the editor binary headless:
UNITY=~/Unity/Hub/Editor/6000.3.25f1/Unity.app/Contents/MacOS/Unity # pinned in stack.json
P=apps/client-unity
# tests (do not combine -runTests with -quit)
"$UNITY" -batchmode -nographics -projectPath $P -runTests -testPlatform EditMode -testResults logs/test-output/editmode.xml -logFile -
"$UNITY" -batchmode -projectPath $P -runTests -testPlatform PlayMode -testResults logs/test-output/playmode.xml -logFile -
# builds through one static build script (Assets/Zoen/Editor/Build.cs)
"$UNITY" -batchmode -nographics -projectPath $P -buildTarget WebGL -executeMethod Zoen.Build.Web -quit -logFile -
"$UNITY" -batchmode -nographics -projectPath $P -buildTarget Android -executeMethod Zoen.Build.Android -quit -logFile -
Every run goes through node tools/task-log.mjs test <ID> "<name>" -- <command> so it lands in the ledger.
| Target | Built from this Mac? | Notes |
|---|---|---|
| macOS (test app) | yes | paused since 2026-10-11 (ADR-032): not built or tested in milestone work; code and scripts kept |
| Web (WebGL2) | yes | Low/Mid profiles only; Playwright opens it for smoke and perf |
| Android (APK now, AAB for a store later; IL2CPP ARM64) | yes | Android module (OpenJDK, SDK, NDK, Gradle) installed; first build ~19 min |
| iOS | Xcode project yes; Xcode 26.6 is installed (device build needs an Apple ID) | paused since 2026-10-11 (ADR-032) |
| Windows | Mono from this Mac; IL2CPP needs a Windows machine or CI runner | test build (ADR-032): the owner runs it on his Windows PC; not yet implemented in unity-run (WIN-01). Release builds use IL2CPP on a Windows runner (GameCI) |
◆The Zoen client (apps/client-unity)
Pinned editor: Unity 6000.3.25f1 (6.3 LTS, Apple silicon, in ~/Unity/Hub/Editor; stack.json). Code lives in
Assets/Zoen/Runtime (boot, quality tiers, FPS overlay, launch capture), Assets/Zoen/Editor (ProjectSetup.cs,
Build.cs) and Assets/Zoen/Tests/EditMode. Engine-free rules (the tier policy, the feel-quality reader) live in
packages/core-cs/Zoen.Core, which the client loads as the local package com.zoen.core (assembly Zoen.Core,
noEngineReferences; M0-09). Edit them there and run pnpm core:test (seconds, no editor); Unity writes the
package's .meta files, which are committed. Builds go to apps/client-unity/Builds/ (ignored by git). One Unity
process per project at a time; on a loaded Mac a Web build takes about 11 minutes, so run builds in the background.
Setup. ProjectSetup.Run generates the URP assets for Low/Mid/High/Ultra, the quality levels, the per-platform
caps and defaults, the player settings and the Boot scene from feel-quality.json. Never hand-edit the generated
assets in Assets/Zoen/Settings or the quality settings: change the data or ProjectSetup.cs and run it again.
"$UNITY" -batchmode -nographics -projectPath $P -executeMethod Zoen.ProjectSetup.Run -quit -logFile -
"$UNITY" -batchmode -nographics -projectPath $P -runTests -testPlatform EditMode -testResults "$PWD/logs/test-output/<ID>-editmode.xml" -logFile -
"$UNITY" -batchmode -nographics -projectPath $P -buildTarget WebGL -executeMethod Zoen.Build.Web -quit -logFile -
"$UNITY" -batchmode -nographics -projectPath $P -buildTarget StandaloneOSX -executeMethod Zoen.Build.MacOS -quit -logFile - # paused (ADR-032)
"$UNITY" -batchmode -nographics -projectPath $P -buildTarget Android -executeMethod Zoen.Build.Android -quit -logFile -
-testResults needs an absolute path. Add -zoen-development to a build for a development player. Each build writes
Builds/<Target>-build-summary.json and logs ZOEN_BUILD target=… result=… sizeBytes=….
The runner is the default way to run these (M0-10, ADR-029). node tools/unity-run.mjs editmode | playmode | build web|macos|android | setup (through task-log.mjs test) uses the editor pinned in ProjectVersion.txt,
refuses to start while another Unity holds the project (it prints that process's pid, kind and start time), runs
quietly (--verbose streams the log) and prints one report: PASS/FAIL, test counts or the build size on disk,
duration, the ZOEN_* lines and the first five errors with file:line (compiler errors, failed tests, exceptions,
licence and lock errors; harmless licensing/usbmuxd lines are ignored). Full log and NUnit XML:
logs/test-output/unity/<stamp>-<kind>.log|.xml (git-ignored); the summary .json is tracked. Exit code 0 pass,
1 fail, 2 refused, 3 not tested: build windows|ios only records "not tested" (ADR-032; the Windows build is not yet
implemented in unity-run, task WIN-01; iOS is paused). unity-run.mjs report <log>
re-reads a saved log. Parser tests: packages/game-data/tests/unity-run.test.mjs (part of pnpm data:test).
| Target | Output | Settings |
|---|---|---|
| Web | Builds/Web/ | WebGL2, Brotli (no fallback), ships Low+Mid only |
| macOS (paused) | Builds/macOS/Zoen.app | universal, Mono, 1280×720 resizable window, ad-hoc signed |
| Windows (WIN-01) | not yet implemented in unity-run | Mono x86_64 player for the owner's Windows PC |
| Android | Builds/Android/zoen.apk | APK for sideloading (ADR-032), IL2CPP ARM64, landscape, package com.zoen.client |
All players run in the background (runInBackground): an online client keeps ticking when its window or tab loses focus.
Smoke tests (evidence to logs/evidence/<ID>/, --out to change it):
| Command | What it checks |
|---|---|
node tools/unity/serve-web.mjs | serves Builds/Web on http://localhost:4350 with the Brotli headers |
node tools/unity/web-smoke.mjs --label mac-chrome | Playwright + Chrome: graphics API, tier per ?tier=, 0 console errors, Ultra refused |
node tools/unity/web-smoke.mjs --label phone-chrome --cdp http://127.0.0.1:9222 | the same in the phone's Chrome (after adb reverse tcp:4350 tcp:4350 and adb forward tcp:9222 localabstract:chrome_devtools_remote) |
node tools/unity/macos-smoke.mjs | paused (ADR-032): native app per tier (windows open briefly): boot, tier, 0 errors, own screenshot |
| Windows smoke kit (WIN-01, not yet built) | the owner runs the player per tier on his Windows PC with -logFile; a checker reads the ZOEN_* lines and the results come back as a file in logs/evidence/ |
node tools/unity/android-smoke.mjs | USB phone: install, per-tier launch, logcat (E/F lines from the app's pid), screenshots, an 8 s recording |
node tools/unity/probe.mjs macos-app|mac-chrome|phone-chrome|android-apk | reads the ZOEN_PROBE line on one owner-device target, checks the contract, adds the host facts (sysctl/system_profiler, adb getprop/dumpsys, userAgentData) → logs/evidence/M0-05C/probe-<target>.json and the merged probe-summary.txt |
node tools/unity/probe-contract.mjs <probe-<target>.json>… | re-checks saved probe records against the current contract (exit 0 only when every run passed and still holds it); the data test device-probes cross-checks ownerTestDevices[].probes against the same files |
node tools/unity/android-overlay-cycle.mjs | USB phone, installed APK: from cleared app data, taps the FPS overlay and checks every ZOEN_TIER against the data's cycle (Low/Mid/High while the mobile Ultra gate is closed); a screenshot per tap |
web-smoke.mjs serves the build itself on port 4350, so stop any serve-web.mjs first. The phone's Chrome must be running for the DevTools socket: adb shell monkey -p com.android.chrome -c android.intent.category.LAUNCHER 1.
adb is ~/Unity/Hub/Editor/6000.3.25f1/PlaybackEngines/AndroidPlayer/SDK/platform-tools/adb (or $ADB; use the binary whose server is running, e.g. ADB=~/Library/Android/sdk/platform-tools/adb). Android and the
GPU vendor write a few E lines into every app's process (gralloc format probes, the Swappy frame-pacing fallback,
framework window/audio notices). android-smoke.mjs lists them by exact tag and message with a reason
(PLATFORM_NOISE) and counts them as platformNoise in the report; any other E/F line from the app's pid fails.
Starting tier and frame cap (M0-04C, feel-quality.json → tierSelection, proposed). At boot TierPolicy picks the
tier in this order: a test launch option; the player's saved choice (an overlay pick, PlayerPrefs zoen.tier; it always
wins); a phone at or below lowMemoryPhone.maxSystemMemoryMb → Low; a browser at or below webLowEnd (deviceMemory or
cores) → Low; on native builds the cached or a new startup benchmark (the Boot scene at the platform default, p95 frame
time against the tier's budget, stepping down; result in PlayerPrefs zoen.benchmark, logged as ZOEN_PICK); else the
platform default. Phones reach Ultra only through the tier launch option until mobileUltraGate.passed (the M1
thermal/frame gate): the overlay cycle stops at High. Every tier sets its frame cap from frameRate.fps (Low 30, Mid
30, High 60, Ultra 60): phones and the Web through targetFrameRate (vSync 0), desktop through vSync when the cap
reaches the display rate. So Android runs its default Mid at 30 fps and High at 60 fps. ?fps= / -zoen-fps /
--es zoen-fps override the cap for tests. The rules reach the player as the generated
Assets/Zoen/Settings/Resources/Zoen_TierRules.asset (ProjectSetup.Run).
Launch options (BootOptions.cs):
| Option | Web | macOS / desktop | Android |
|---|---|---|---|
| quality tier | ?tier=low | -zoen-tier low | am start -n <activity> --es zoen-tier low |
| frame-rate cap | ?fps=60 | -zoen-fps 60 | --es zoen-fps 60 |
| own screenshot | — | -zoen-screenshot <png> [-zoen-capture-at 8] | — |
| quit after N s | — | -zoen-quit-at 13 | — |
A tier the platform does not ship (Ultra on the Web) is refused and the platform default is kept. On phones the tier option is the only way to Ultra while the M1 gate is closed.
Log lines the smokes parse (browser console, Player.log, logcat):
ZOEN_BOOT {json}– once before the first scene: product, version, Unity, platform, graphics API/version, GPU, device, OS, memory, screen, chosen tier, shipped tiers, requested tier,tierSource,tierRefused,capKey, the overlaycycle,ultraGate,benchmark(run/cached/off), browserdeviceMemoryGb/logicalCores,displayHz,fpsTarget, frame-rate target, vSync.ZOEN_PROBE {json}– once after the first scene (M0-05B,Runtime/Diagnostics/CapabilityProbe.cs; on the Web the browser block comes fromZoenWebProbeinZoenDevice.jslib): the capability probe whose contract isreference-devices.json → probe. On Android it arrives asZOEN_PROBE_PART i/n <chunk>lines of at most 800 bytes (Unity's Android player cuts a logcat entry at 1023 bytes; M0-05E), joined by number intools/unity/probe-contract.mjs(ProbeJson.Lines/Joinin the core).ZOEN_PICK {json}– the startup benchmark's pick: tier, source,steps(tier:p95 ms), frame cap.ZOEN_FPS tier=… api=… target=… fps=… avgMs p50Ms p95Ms p99Ms maxMs frames=… logErrors=… logWarnings=…– every 5 s.ZOEN_TIER <name>– the overlay button switched the tier.ZOEN_CAPTURE <png>– the launch screenshot was written.ZOEN_BUILD …(builds) andZOEN_SETUP done …(setup).ZOEN_SIM {json}– once after the first scene (M0-11 spike,Runtime/Sim/SimBootCheck.cs): the zoen-sim plugin's ABI version, every golden scenario replayed through it (match/allMatch, one scenario's final hash) and a step microbench (median ns per tick for 1 and 5,000 movers);loaded:falsewith a reason where the plugin is not linked yet.macos-smoke.mjs --simrequires a match;android-smoke.mjs --simandweb-smoke.mjs --simalso check the final hash againstpackages/sim-golden/vectors.jsonand the bench for 1 and 5,000 movers (tools/unity/sim-expect.mjs). The plugins come fromnode tools/sim/build-plugin.mjs macos|android|web|ios|windows(crates/zoen-sim-ffi, C headerinclude/zoen_sim.h; import settings inEditor/SimPluginImport.cs). Android (M0-11B):aarch64-linux-androidlinked by the clang of the NDK bundled with the pinned editor (r27c, API =AndroidMinSdkVersion), 16 KB LOAD alignment,llvm-strip, installed asPlugins/Android/arm64-v8a/libzoen_sim_ffi.so(Android player, ARM64 only, not the editor);--devicealso runs the C smoke on the adb phone. Web (M0-11C):wasm32-unknown-emscriptenstaticlib repacked for the editor's bundled Emscripten 3.1.39 (LLVM 17), installed asPlugins/WebGL/libzoen_sim_ffi.a(Web player only, not the editor or Android);Zoen.BuildaddsZOEN_SIM_STATICto a Web build when it exists, otherwise the Web calls stay stubs. iOS (M0-11D, built, not tested): staticlib atiOSTargetOSVersionString, C smoke linked with Xcode's iPhoneOS SDK, installed asPlugins/iOS/libzoen_sim_ffi.a(iOS player only;ZOEN_SIM_STATICfor iOS too). Windows (M0-11D): the staticlib builds; the DLL needs the MSVC CRT and Windows SDK libraries, so it is built on the owner's Windows PC (Rust + Visual Studio Build Tools, installed by the owner; steps in WIN-01; no SDK/CRT download on this Mac, ADR-032), then installs asPlugins/Windows/x86_64/zoen_sim_ffi.dll(64-bit Windows player only). Until it exists the Windows player runs without the native sim (the boot check reports it absent; not an error).
Tier memory. The player's pick and the benchmark result live in PlayerPrefs (Web: IndexedDB; macOS: the
com.zoen.client defaults domain; Android: app data); the boot sets the level itself, so Unity's own remembered level
does not decide. A launch option wins and is never saved. The smokes clear app data before their default run
(defaults delete com.zoen.client, pm clear com.zoen.client), so that run shows the benchmark pick. On the Web every page load logs one Unity warning, "Manual synchronization of Unity Application.persistentDataPath via JS_FileSystem_Sync() is deprecated": Unity's own framework prints it once per page when the engine flushes PlayerPrefs to IndexedDB, which happens on each boot because the boot applies its tier with QualitySettings.SetQualityLevel (Unity stores the level in PlayerPrefs). It is harmless; the opt-in config.autoSyncPersistentDataPath = true needs a custom WebGL template (M0-04D finding, not done).
Packages. Opening the project folder without ProjectSettings merges Unity's new-project defaults (IAP,
Analytics, Version Control) into Packages/manifest.json. They were removed (M0-04). Re-adding any of them, or any
other package, goes through the gate below. The local package com.zoen.core (M0-09) is the repo's own code, not a
third-party package, so it needs no gate.
◆Project skills (zoen/.claude/skills/)
| Skill | Use it when |
|---|---|
unity-batchmode | building or testing the Unity project from the CLI; the exact commands, log parsing and the per-target table above |
unity-toolchain-gate | someone proposes installing a Unity package, MCP server, CI action or third-party skill |
They are written for Zoen. The Unity-era skills in the workspace root .claude/skills/ stay archived.
◆Reviewed GitHub tools
Licences and adoption status as reviewed in October 2026 (re-check the pinned release before installing; stars and recent pushes are not security evidence). "Gate" = what must happen first.
| Tool | What it gives us | Licence | Zoen status | Gate / when |
|---|---|---|---|---|
| CoplayDev/unity-mcp | Lets an agent read/edit scenes, prefabs and assets in a running editor | MIT | Deferred — no MCP (owner direction, M0-07, 2026-10-08): the smoke test was skipped; agents drive Unity through batchmode and tools/unity-run.mjs | re-review a pinned release (e.g. the v10.2.0 line) only if the owner asks: disposable project, telemetry off, loopback only, one editor writer |
| IvanMurzak/Unity-MCP | Alternative Unity MCP | Apache-2.0 | Deferred — no MCP (M0-07) | only if the owner asks for an MCP |
| game-ci/unity-test-runner | Runs EditMode/PlayMode tests in CI | MIT | Planned | needs a git remote + a Unity licence secret from the owner |
| game-ci/unity-builder | CI builds for every target, incl. Windows IL2CPP on Windows runners | MIT | Planned | same as above; required for release Windows builds |
| Unity-Technologies/CharacterControllerSamples | Character controller patterns | Unity Companion Licence | Reference only | read, don't copy without checking third-party exceptions |
| Unity-Technologies/EntityComponentSystemSamples | Data-oriented crowd/projectile patterns | Unity Companion Licence | Reference only | M1 crowd spike |
| Unity-Technologies/Graphics | URP source and renderer-feature examples | Unity Companion Licence | Reference only | VFX distortion, decals |
| Unity-Technologies/VisualEffectGraph-Samples | VFX Graph examples for High/Ultra effects | Unity Companion Licence | Reference only | M2 Combat Lab |
| Unity-Technologies/audio-examples | AudioMixer / voice management patterns | Unity Companion Licence | Reference only | M2-05 |
| getsentry/sentry-unity | Crash and error reports from players | MIT (service has paid tiers) | Deferred | M11 alpha; a Sentry account is an owner decision (cost cap) |
| open-telemetry/opentelemetry-dotnet | Client/server traces and metrics | Apache-2.0 | Deferred | M6 online core |
| KhronosGroup/glTF-Validator | Validates Blender exports before import | Apache-2.0 | Deferred | M3 character pipeline |
| git-lfs/git-lfs | Large binary assets in git | MIT | Deferred | when git is initialised (owner) |
| csteinmetz1/pyloudnorm | LUFS loudness checks for ambience/music | MIT | Reference / optional | audio delivery step |
| audacity/audacity | Trim and layer SFX | GPLv3 (tool only) | Optional tool | outputs are ours |
| audiokinetic/waapi-client-python | Wwise automation | Apache-2.0 (Wwise itself is commercial) | Not planned | we use Unity's AudioMixer |
| ahujasid/mcp-for-blender | Agent control of Blender | MIT | Deferred | headless blender -b -P scripts first |
| Nice-Wolf-Studio/unity-claude-skills | Small Unity skill collection | MIT | Reference only | our own skills above replace it |
| anthropics/skills | Skill authoring examples (skill-creator is Apache-2.0) | per folder | Reference only | — |
| devdavv/unity-ai-workflow | Game-feel ideas | MIT | Reference only | uses a global timeScale hitstop, which Zoen forbids |
| alttester/AltTester-Unity-SDK | UI test automation | GPL | Rejected | GPL in the shipped client |
| denfry/claude-skills | Generic skills | MIT | Rejected | no advantage over our own |
Blender headless check (M0-02): pnpm blender:smoke runs tools/blender/smoke.py (Blender 4.5.9 LTS) — a 1 m cube exported to art/build/smoke/smoke_cube.glb with the glTF settings the Unity import expects (GLB, +Y up, metres, embedded PNG, transforms applied, no compression) — and validates the GLB and its 512 px Eevee render (logs/evidence/M0-02/).
◆Unity packages we expect to use (all first-party, free)
URP · Input System · Addressables · UI Toolkit · TextMeshPro · Cinemachine · VFX Graph · Shader Graph · Burst + Collections + Mathematics (hot paths) · Unity Test Framework · Performance Testing · Profiler/Memory Profiler. Paid Asset Store packages need owner approval and count toward the PHP 5,000 cap.
◆Test packages: gate record (M0-07, 2026-10-08) — adopted
| Package | Pinned | Source | Licence | Notes |
|---|---|---|---|---|
Unity Test Framework com.unity.test-framework | 1.6.0 | built-in core package of 6000.3.25f1 | Unity Companion License | runs EditMode/PlayMode in batchmode; brings Custom NUnit com.unity.ext.nunit 2.0.5 (Unity Package Distribution License) |
Performance Testing com.unity.test-framework.performance | 3.5.0 | packages.unity.com (was already resolved at depth 3 through Collections; now a direct pin) | Unity Companion License (bundled Perfolizer: MIT) | Measure.Method/Frames, GC.Alloc samples; its runtime assembly is filtered out of player builds unless test assemblies are included |
No network listeners, telemetry or post-install scripts. Files it writes: -perfTestResults <file> (the runner passes
logs/test-output/unity/<stamp>-<kind>-perf.json), PerformanceTestResults.json in the editor's
Application.persistentDataPath, and during every player build Assets/Resources/PerformanceTestRunInfo.json +
PerformanceTestRunSettings.json (build metadata and the editor command line; git-ignored, the package's post-build
cleanup removes them). They ship inside player builds: strip them before release builds (M11 release checklist).
Proof: Assets/Zoen/Tests/PlayMode/SkyPerformanceTests.cs — SkyModel.Sample + SkyLook.Evaluate over a whole
day and SkyController.Tick (rain, frozen cycle) record 0 GC allocations, checked against an allocating control;
Boot-scene frame times with the sky active are recorded (no budget: editor batchmode frames are not device numbers).
unity-run.mjs prints median/p95 per sample group. Evidence: logs/evidence/M0-07/.
◆The gate (any new tool)
- Read the exact release/tag, licence, install script and the last commits. Never run curl-pipe installers.
- Install into a disposable copy of the project first; telemetry off; servers bound to loopback; one editor writer.
- Log the smoke test with
task-log.mjs. Remove it if it fails or writes outside the project. - Commercial services, accounts, runtime AI or remote access need the owner's explicit approval.
Source: zoen/docs/tech/UNITY_TOOLKIT.md · 3,163 words · edit the Markdown, not this page.
