Unity C# Scripting skill for Codex
Components that designers can tune, not rewrite. Codex checks the Unity version, asmdefs and installed packages before writing a line, then builds small components with serialized private fields, moves tuning numbers into ScriptableObject assets, and replaces FindObjectOfType singletons with explicit wiring or event channels. It knows the Unity 6 renames, the fake-null trap and domain-reload leaks, and proves logic with EditMode tests run in batch mode.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- unity-csharp Installs to ~/.agents/skills/unity-csharp/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install unity-csharp 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-csharp Installs to .agents/skills/unity-csharp/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/unity-csharp.zipunzip -o unity-csharp.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-csharp 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-csharp, 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
Writes and refactors Unity C# gameplay code - MonoBehaviour components, ScriptableObject data and events, serialized fields, the new Input System, Awaitable and coroutines, assembly definitions and Unity Test Framework tests - following Unity 6 lifecycle and serialization rules. Use when adding a player controller, enemy, pickup or manager script, moving tuning data into ScriptableObjects, untangling singletons and FindObjectOfType calls, fixing null references after scene loads, or writing EditMode and PlayMode tests.
Use it when
- Adding or refactoring gameplay scripts in a Unity project
- Tuning data lives in hard-coded constants that designers keep asking to change
- Null references or duplicate events appear after scene loads or Play mode restarts
- You need EditMode or PlayMode tests for gameplay logic
Use something else when
- The problem is frame time, GC spikes or draw calls: use unity-performance
- You are building menus, HUDs or controller navigation: use game-ui
How to check the result
The project compiles without new Console warnings, EditMode tests pass in batch mode, and entering Play mode twice shows no leaked static state or duplicate subscriptions.
The mistake it prevents
Using ?. or ?? on a destroyed UnityEngine.Object, which skips Unity's overridden null check and throws MissingReferenceException.
Done well means: each component does one job, designers tune values in the Inspector or in ScriptableObject assets instead of code, references are wired explicitly (not found by name at runtime), nothing allocates per frame without a reason, and the change is covered by an EditMode or PlayMode test that runs headless.
Before writing code
- Read
AGENTS.md, thenProjectSettings/ProjectVersion.txtfor the editor version (m_EditorVersion: 6000.xis Unity 6). API names differ across versions (see table below). - Check
Packages/manifest.jsonforcom.unity.inputsystem,com.unity.addressables,com.unity.test-framework,com.unity.nuget.newtonsoft-json. Use what is installed; adding a package changes the project for everyone, so ask first. - Look for
*.asmdeffiles underAssets/. New scripts go in the assembly that owns that feature; tests go in a separate test assembly that references it. - Check
ProjectSettings/EditorSettings.assetform_EnterPlayModeOptionsEnabled: 1. If domain reload is disabled, static fields survive between Play sessions (see Gotchas). - Never edit
.metafiles by hand and never delete one without its asset: the GUID inside is what scenes and prefabs reference. When you create a script, Unity generates the meta on next import; when you move a script, move its.metawith it.
Version-sensitive API (Unity 6)
| Old (2021/2022) | Unity 6 |
|---|---|
FindObjectOfType<T>() | FindFirstObjectByType<T>() or FindAnyObjectByType<T>() (faster, unordered) |
FindObjectsOfType<T>() | FindObjectsByType<T>(FindObjectsSortMode.None) |
rb.velocity / rb.drag | rb.linearVelocity / rb.linearDamping |
| Coroutines for async waits | Awaitable (await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken)) |
Input.GetAxis (legacy) | Input System InputAction / generated wrapper class |
Component design
- One responsibility per component:
Health,Mover,WeaponController, notPlayer.cswith 900 lines. Compose on the prefab. - Serialize private fields; do not make fields public to expose them:
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public sealed class Mover : MonoBehaviour
{
[SerializeField, Min(0f)] float maxSpeed = 6f;
[SerializeField] float acceleration = 40f;
Rigidbody body;
Vector3 desired;
void Awake() => body = GetComponent<Rigidbody>(); // own references in Awake
public void SetMoveInput(Vector2 input) =>
desired = new Vector3(input.x, 0f, input.y) * maxSpeed;
void FixedUpdate() // physics in FixedUpdate
{
Vector3 v = body.linearVelocity;
Vector3 target = new(desired.x, v.y, desired.z);
body.linearVelocity = Vector3.MoveTowards(v, target, acceleration * Time.fixedDeltaTime);
}
}
Lifecycle rules
| Callback | Use for |
|---|---|
Awake | Cache own components; initialise own state. Do not touch other objects’ state |
OnEnable / OnDisable | Subscribe / unsubscribe events (always paired) |
Start | Read other objects (all Awakes have run) |
Update | Input reading, timers, non-physics movement |
FixedUpdate | Rigidbody forces and velocity |
LateUpdate | Camera follow, anything that must see final positions |
OnDestroy | Release native resources, unsubscribe from static events |
Order between scripts of the same callback is undefined unless set in Script Execution
Order or with [DefaultExecutionOrder(-100)]. Prefer removing the dependency over ordering.
Data in ScriptableObjects
Tuning numbers, item definitions, enemy stats and wave tables belong in assets:
[CreateAssetMenu(menuName = "Game/Weapon Definition", fileName = "Weapon_")]
public sealed class WeaponDefinition : ScriptableObject
{
[field: SerializeField] public string DisplayName { get; private set; }
[field: SerializeField, Min(0.01f)] public float FireInterval { get; private set; } = 0.2f;
[field: SerializeField] public int Damage { get; private set; } = 10;
[field: SerializeField] public GameObject ProjectilePrefab { get; private set; }
}
- Treat ScriptableObjects as read-only at runtime. In the editor, runtime writes to an asset persist after leaving Play mode; in builds they reset. Copy into a plain class for mutable state.
- Renaming a serialized field loses its data unless you add
[FormerlySerializedAs("oldName")](fromUnityEngine.Serialization). - Use
[SerializeReference]only for polymorphic plain classes; it serializes by type name, so renaming the class or namespace breaks saved data unless you add[MovedFrom].
Event channels instead of singletons
[CreateAssetMenu(menuName = "Game/Events/Int Event")]
public sealed class IntEventChannel : ScriptableObject
{
public event System.Action<int> Raised;
public void Raise(int value) => Raised?.Invoke(value);
}
The Health component raises onDamaged; the HUD subscribes in OnEnable. Neither knows
the other exists, and either scene can be tested alone. Keep one true singleton at most
(a bootstrap that owns services), created by a bootstrap scene, not lazily by FindObjectOfType.
Input System
- Prefer an
.inputactionsasset with “Generate C# Class” enabled, orPlayerInputwithInvoke Unity Eventsfor prototypes. Read values, do not poll devices directly:
public sealed class PlayerInputBridge : MonoBehaviour
{
[SerializeField] Mover mover;
GameControls controls; // generated from GameControls.inputactions
void Awake() => controls = new GameControls();
void OnEnable() => controls.Gameplay.Enable();
void OnDisable() => controls.Gameplay.Disable();
void OnDestroy() => controls.Dispose();
void Update() => mover.SetMoveInput(controls.Gameplay.Move.ReadValue<Vector2>());
}
- Check
Player Settings > Active Input Handling. “Both” works but hides mistakes.
Async: Awaitable vs coroutine
| Need | Use |
|---|---|
| Wait N seconds, next frame, end of frame, tied to an object | Awaitable with destroyCancellationToken |
| Background CPU work, then back to main thread | await Awaitable.BackgroundThreadAsync() then await Awaitable.MainThreadAsync() |
| Existing coroutine-heavy codebase | Keep coroutines; do not mix styles in one feature |
async Awaitable FlashAsync()
{
try
{
renderer.enabled = false;
await Awaitable.WaitForSecondsAsync(0.1f, destroyCancellationToken);
renderer.enabled = true;
}
catch (System.OperationCanceledException) { /* object destroyed mid-wait */ }
}
Never touch Unity objects from a background thread. async void swallows exceptions
into the log with no caller; use async Awaitable and await it, except in event handlers.
Gotchas
- Unity overrides
==for destroyed objects:obj == nullis true afterDestroy, butobj?.Foo()andobj ?? otherbypass that check and throwMissingReferenceException. Use explicitif (obj != null)orTryGetComponentforUnityEngine.Objects. Destroytakes effect at end of frame. Code later in the same frame still sees the object.- Disabled domain reload: static fields and static event subscriptions persist across
Play sessions. Reset them in a method marked
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]. GetComponent,Camera.main(cached in Unity 6 but still a lookup), andFind*calls inUpdatecost time; cache inAwake.- String-based APIs (
SendMessage,Invoke("Name"),Animator.SetTrigger("Jump")) break silently on rename. UseAnimator.StringToHashonce and direct method calls. - Prefab overrides: changing a default value in code does not change instances already serialized in scenes or prefabs. Update the prefab asset or migrate data.
- Coroutines stop when the GameObject is deactivated, and do not resume when reactivated.
Tests
Put tests in Assets/Tests/EditMode and Assets/Tests/PlayMode, each with an asmdef that
references the feature assembly and has UNITY_INCLUDE_TESTS in Define Constraints.
Keep pure logic (damage math, inventory rules) in plain C# classes so EditMode tests cover it.
public class HealthTests
{
[Test]
public void Damage_ClampsAtZero()
{
var health = new HealthModel(max: 50);
health.Apply(80);
Assert.AreEqual(0, health.Current);
Assert.IsTrue(health.IsDead);
}
}
Run headless (needs a Unity editor installed and licensed on the machine; do not pass -quit
with -runTests):
"$UNITY" -batchmode -projectPath . -runTests -testPlatform EditMode \
-testResults ./TestResults/editmode.xml -logFile -
Verification
- Project compiles with zero new warnings in the Console (or
-logFileoutput) - EditMode tests pass; PlayMode tests pass for anything that touches physics or scenes
- Enter and exit Play mode twice: no state leaks from statics, no duplicate subscriptions
- Inspector shows the new serialized fields with sensible defaults and tooltips
- No
.metafile was deleted, renamed without its asset, or hand-edited
If you cannot run the editor in this environment, say so and list exactly what the user should click through to verify.
Hand-off summary
**Changed:** Mover.cs, PlayerInputBridge.cs (new), WeaponDefinition.cs (new SO)
**Assets to create in editor:** Weapon_Rifle.asset via Create > Game > Weapon Definition
**Inspector wiring:** Player prefab > PlayerInputBridge.mover -> Mover
**Tests:** HealthTests (EditMode, 4 passing)
**Not verified:** feel of acceleration value; tune in Play mode 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 C# Scripting"
short_description: "Components that designers can tune, not rewrite"
default_prompt: "$unity-csharp Write a third-person player controller in Unity using the new Input System" 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.