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.

  • Game Development
  • $unity-csharp
  • 232 lines
  • 2 files
  • ~2,450 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- unity-csharp

Installs to ~/.agents/skills/unity-csharp/ for every project on this machine.

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.

~/.agents/skills/unity-csharp/SKILL.md

--- name: unity-csharp description: Writes and refactors Unity C# gameplay code - MonoBehaviour components, ScriptableObject data and events, serialized fields, the new Input System,… license: MIT ---

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

  1. Read AGENTS.md, then ProjectSettings/ProjectVersion.txt for the editor version (m_EditorVersion: 6000.x is Unity 6). API names differ across versions (see table below).
  2. Check Packages/manifest.json for com.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.
  3. Look for *.asmdef files under Assets/. New scripts go in the assembly that owns that feature; tests go in a separate test assembly that references it.
  4. Check ProjectSettings/EditorSettings.asset for m_EnterPlayModeOptionsEnabled: 1. If domain reload is disabled, static fields survive between Play sessions (see Gotchas).
  5. Never edit .meta files 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 .meta with 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.dragrb.linearVelocity / rb.linearDamping
Coroutines for async waitsAwaitable (await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken))
Input.GetAxis (legacy)Input System InputAction / generated wrapper class

Component design

  • One responsibility per component: Health, Mover, WeaponController, not Player.cs with 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

CallbackUse for
AwakeCache own components; initialise own state. Do not touch other objects’ state
OnEnable / OnDisableSubscribe / unsubscribe events (always paired)
StartRead other objects (all Awakes have run)
UpdateInput reading, timers, non-physics movement
FixedUpdateRigidbody forces and velocity
LateUpdateCamera follow, anything that must see final positions
OnDestroyRelease 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")] (from UnityEngine.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 .inputactions asset with “Generate C# Class” enabled, or PlayerInput with Invoke Unity Events for 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

NeedUse
Wait N seconds, next frame, end of frame, tied to an objectAwaitable with destroyCancellationToken
Background CPU work, then back to main threadawait Awaitable.BackgroundThreadAsync() then await Awaitable.MainThreadAsync()
Existing coroutine-heavy codebaseKeep 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 == null is true after Destroy, but obj?.Foo() and obj ?? other bypass that check and throw MissingReferenceException. Use explicit if (obj != null) or TryGetComponent for UnityEngine.Objects.
  • Destroy takes 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), and Find* calls in Update cost time; cache in Awake.
  • String-based APIs (SendMessage, Invoke("Name"), Animator.SetTrigger("Jump")) break silently on rename. Use Animator.StringToHash once 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 -logFile output)
  • 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 .meta file 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.yamlgenerated from this page

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.

esc