Unity Performance skill for Codex
Name the bottleneck before touching the code. Codex profiles a development build on the target device, classifies the frame as CPU, render thread, GPU or GC bound, and only then changes code. It removes per-frame allocations, pools spawns, splits dirty canvases, checks SRP Batcher and GPU Resident Drawer compatibility in the Frame Debugger, and reports median and p95 frame times before and after so wins are proven rather than assumed.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- unity-performance Installs to ~/.agents/skills/unity-performance/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install unity-performance The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project unity-performance Installs to .agents/skills/unity-performance/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/unity-performance.zipunzip -o unity-performance.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) unity-performance PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $unity-performance, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Profiles and fixes Unity frame-time problems on PC, console and mobile - CPU vs GPU bound diagnosis with the Profiler, Frame Debugger and Memory Profiler, GC allocation spikes, draw calls and SRP Batcher compatibility, GPU instancing, physics and UI rebuild costs, texture and mesh budgets, and device builds. Use when a Unity game stutters, drops below target FPS, hitches on spawn or scene load, overheats phones, runs out of memory, or when asked to optimize Update loops, pooling or batching.
Use it when
- A Unity build misses its frame budget or hitches periodically
- The Profiler shows GC.Collect spikes or high GC Alloc per frame
- Draw calls, overdraw or texture memory are too high on mobile
- Spawning or scene loading causes visible freezes
Use something else when
- You are writing new gameplay features rather than optimizing: use unity-csharp
- The slowdown is shader math you are authoring yourself: use glsl-shaders
How to check the result
Capture the same scene on the same device before and after; median and p95 frame time, GC Alloc per frame and draw calls must improve beyond run-to-run noise.
The mistake it prevents
Optimizing based on editor Play mode numbers, which include editor overhead and a desktop GPU, instead of a development build on the target device.
Done well means: every change is justified by a capture from a development build on the target device, the frame budget is stated in milliseconds, the bottleneck (CPU main thread, render thread, GPU, memory) is named before anything is changed, and the after-capture proves the gain.
Budgets
| Target | Frame budget | Practical headroom |
|---|---|---|
| 30 FPS mobile | 33.3 ms | Aim for ~22 ms to avoid thermal throttling |
| 60 FPS | 16.6 ms | ~13 ms |
| 90 FPS VR | 11.1 ms | ~9 ms, hard requirement |
| 120 FPS | 8.3 ms | ~7 ms |
Steady-state GC allocation per frame should be 0 B in gameplay. Mobile phones throttle under sustained load, so a 10-minute soak test matters more than a 10-second peak.
Workflow
- [ ] 1. Read AGENTS.md, ProjectVersion.txt, render pipeline asset (URP/HDRP/Built-in)
- [ ] 2. Build a Development Build with Autoconnect Profiler on the target device
- [ ] 3. Capture 300+ frames of the bad case; note the frame-time spike pattern
- [ ] 4. Classify: CPU main, CPU render thread, GPU, or GC/memory (table below)
- [ ] 5. Drill into the top 3 markers only; form one hypothesis per change
- [ ] 6. Make one change, recapture under the same conditions, compare
- [ ] 7. Record before/after numbers in the hand-off
Editor profiling lies: the editor adds its own overhead, runs with domain checks, and uses desktop GPUs. Use it for allocation hunting and hierarchy shape, never for final numbers.
Classify the bottleneck
| Profiler shows | Bound by | First tools |
|---|---|---|
PlayerLoop large, scripts dominate | CPU main thread (scripts) | Profiler Hierarchy, Deep Profile on a small repro only |
Gfx.WaitForPresentOnGfxThread large on main thread | GPU or render thread | GPU profiler module, Frame Debugger, platform GPU tools |
Physics.Simulate / FixedUpdate.PhysicsFixedUpdate large | Physics | Physics Debugger, collider counts, fixed timestep |
GC.Collect spikes, GC Alloc column nonzero | Managed allocations | Hierarchy sorted by GC Alloc, Memory Profiler |
Canvas.BuildBatch, Canvas.SendWillRenderCanvases | uGUI rebuilds | Split canvases |
Loading.ReadObject, Instantiate spikes | Asset loading / spawning | Pooling, async loading, Addressables |
Check the frame interval too: if WaitForTargetFPS is large, the game is already fast and
capped by Application.targetFrameRate or VSync.
Measure your own code
using Unity.Profiling;
public sealed class EnemyDirector : MonoBehaviour
{
static readonly ProfilerMarker s_Think = new("EnemyDirector.Think");
void Update()
{
using (s_Think.Auto())
{
ThinkAll();
}
}
}
Markers show up in the Profiler and in builds with the Profiler attached. For on-device
overlays use ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count") or
"Main Thread" and read LastValue.
CPU: scripts and GC
- Replace per-object
Updateon hundreds of objects with one manager that iterates an array. EachUpdatecall crosses from native to managed code. - Allocation sources to eliminate in hot paths:
new List<T>()per frame, LINQ, string concatenation andToString()for UI, boxing viaobjectparameters or enum keys inDictionarywithout a comparer, closures captured in lambdas,GetComponents<T>()without a list argument,foreachover non-struct enumerators,paramsarrays. - Physics queries: use the non-allocating overloads (
Physics.RaycastNonAlloc,Physics.OverlapSphereNonAlloc) with a preallocated buffer, and filter with aLayerMask. - Pool anything spawned more than a few times a second:
using UnityEngine.Pool;
ObjectPool<Projectile> pool;
void Awake() => pool = new ObjectPool<Projectile>(
createFunc: () => Instantiate(prefab),
actionOnGet: p => p.gameObject.SetActive(true),
actionOnRelease: p => p.gameObject.SetActive(false),
actionOnDestroy: p => Destroy(p.gameObject),
collectionCheck: false, defaultCapacity: 64, maxSize: 256);
- Incremental GC (Player Settings, on by default) spreads collections over frames; it does not remove the cost. Fix allocations first.
- Heavy data loops (thousands of agents, pathfinding batches) are candidates for the Job System plus Burst. Move only after profiling shows the loop dominates.
Rendering: draw calls and GPU
| Situation | Approach |
|---|---|
| URP/HDRP, many objects sharing shaders | SRP Batcher (on by default); keep shaders compatible (Frame Debugger shows why a batch broke) |
| Unity 6 URP/HDRP with many static/repeated meshes | GPU Resident Drawer (requires Forward+ in URP) |
| Thousands of identical meshes | GPU instancing or Graphics.RenderMeshInstanced |
| Many small static props | Static batching (memory cost) or merging meshes offline |
| Overdraw from particles and transparent UI | Reduce layered transparency; check with Scene view Overdraw mode |
| Mobile GPU bound | Lower render scale in the URP asset, cut post effects, fewer real-time lights and shadow cascades |
MaterialPropertyBlockbreaks SRP Batcher compatibility; in SRP projects prefer separate materials or per-instance data through instancing.- Accessing
renderer.materialcreates a material copy per object; usesharedMaterialwhen you mean the asset, and destroy copies you create. - Use LOD Groups and occlusion culling for 3D scenes with depth; verify with the Frame Debugger that culled objects are not drawn.
Memory and assets (mobile especially)
- Textures: ASTC on iOS/Android, max size set per platform, mipmaps off for UI, Read/Write off unless code reads pixels (it doubles memory).
- Meshes: Read/Write off; enable mesh compression where artefacts are acceptable.
- Audio: long music as Streaming, short SFX as Decompress On Load, everything else Compressed In Memory.
- Take two Memory Profiler snapshots (menu, after one level) and diff them to find leaks: assets still referenced by static fields or event subscriptions.
Resources.UnloadUnusedAssets()is slow and frees only unreferenced assets; it does not fix a leak.
UI (uGUI)
- Any change to a Graphic marks its whole Canvas dirty. Put frequently changing elements (timers, health bars) on their own nested Canvas.
- Disable
Raycast Targeton images and text that are not interactive. - Hiding with
CanvasGroup.alpha = 0still costs; disable the Canvas component instead of the GameObject to avoid a rebuild on re-enable.
Physics
- Keep
Fixed Timestepas large as feel allows (0.02 s default). Halving it doubles cost. - Set
Maximum Allowed Timestepto bound catch-up work after a hitch. - Use the Layer Collision Matrix to remove pairs that never interact.
- Prefer primitive colliders; avoid non-convex
MeshCollideron moving bodies.
Gotchas
- Deep Profile multiplies overhead and distorts the picture; use it only on a tiny repro.
Debug.Login builds allocates and writes to disk; strip with a conditional wrapper ([System.Diagnostics.Conditional("ENABLE_LOGS")]).- VSync and
targetFrameRateinteract: on mobiletargetFrameRatedefaults to 30. Set it explicitly and checkQualitySettings.vSyncCount. - Shader variant compilation hitches the first time an effect appears. Warm up with a
ShaderVariantCollection(or the Unity 6 PSO tracing tools) in the loading screen. - A fix that wins 2 ms in the editor can be noise on device. Compare medians across 300 frames, not single frames.
Verification
- Same scene, same device, same camera path, same build type, before and after.
- Report median and 95th percentile frame time, GC Alloc per frame, draw calls / SetPass calls, and memory after the soak test.
- If the change did not move the target metric by more than run-to-run noise, revert it.
Hand-off template
**Device:** Pixel 7, development build, URP, 60 FPS target (16.6 ms)
**Bottleneck:** CPU main thread, EnemyDirector.Think 6.1 ms + GC.Collect spikes every ~2 s
**Changes:** pooled projectiles (ObjectPool, max 256); RaycastNonAlloc in targeting; HUD timer on own Canvas
**Result:** median 19.4 -> 12.8 ms, p95 31 -> 15.2 ms, GC Alloc 14.2 KB -> 0 B per frame
**Not addressed:** shader warm-up hitch on first explosion (needs ShaderVariantCollection) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Unity Performance"
short_description: "Name the bottleneck before touching the code"
default_prompt: "$unity-performance Our Unity game stutters every few seconds on Android, find out why" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.