Unity Development skill for Claude Code

Unity 6 C# that stays fast and stays organized. Claude writes Unity 6 gameplay code with clear lifecycle rules: references cached in Awake, events paired in OnEnable and OnDisable, physics in FixedUpdate, and design data in ScriptableObjects. It uses the Input System package and Awaitable, pools spawned objects, keeps Update allocation-free, and proves changes with headless tests and builds.

  • nameunity-development
  • size231 lines · ≈2,450 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Writes and reviews Unity 6 C# gameplay code with MonoBehaviour lifecycle discipline, ScriptableObject data, prefabs and variants, the Input System package, Awaitable async, object pooling, assembly definitions, and allocation-free Update loops. Use when editing Unity .cs scripts, prefabs, or ScriptableObjects, wiring player input, fixing null references or physics jitter, cutting GC spikes, setting up batch-mode builds and tests, or organizing a Unity project for a team.

Try asking

  • Convert this player controller from the old Input Manager to the Input System
  • The Profiler shows GC spikes every few seconds in combat, find and fix the allocations
  • Move these hard-coded enemy stats into ScriptableObjects with prefab variants

Use it when

  • Writing or reviewing MonoBehaviour and ScriptableObject code
  • Migrating input to the Input System package
  • Null references, physics jitter or double-fired events in Unity
  • Setting up command-line Unity builds and tests for CI

Skip it when

How to check the result

Play Mode runs with an empty console, the Profiler shows zero GC Alloc per frame for changed scripts, and batch-mode tests and builds pass.

The common mistake it prevents

Using ?. or ?? on UnityEngine.Object references, which skips Unity's destroyed-object check and calls methods on dead objects.

~/.claude/skills/unity-development/SKILL.md

--- name: unity-development description: Writes and reviews Unity 6 C# gameplay code with MonoBehaviour lifecycle discipline, ScriptableObject data, prefabs and variants, the Input… license: MIT ---

Done well means: data lives in ScriptableObjects and prefabs, behavior in small focused components, nothing allocates per frame in steady state, input goes through Input System actions, and a batch-mode build plus EditMode/PlayMode tests run from the command line.

Ground rules

  • Target Unity 6 (6000.x). Check ProjectSettings/ProjectVersion.txt and Packages/manifest.json before using an API; Unity 6 renamed several physics members (Rigidbody.linearVelocity, linearDamping, angularDamping).
  • Inspect the prefab or scene YAML and existing components before adding new ones; duplicating a manager singleton is a common source of double-fired events.
  • Never edit .meta files or GUIDs by hand, and never commit an asset without its .meta. Asset Serialization must be Force Text; Version Control mode Visible Meta Files.
  • Serialized fields: [SerializeField] private over public fields. Use [field: SerializeField] public float Speed { get; private set; } for read-only access.
  • Renaming a serialized field loses inspector data; add [FormerlySerializedAs("oldName")] when renaming.

Workflow

- [ ] 1. Read ProjectVersion.txt, manifest.json (Input System, URP/HDRP, Addressables)
- [ ] 2. Find the components, prefabs and ScriptableObjects involved
- [ ] 3. Decide where data and behavior go (table below)
- [ ] 4. Write the component; cache references in Awake, subscribe in OnEnable
- [ ] 5. Enter Play Mode with the Console on Error Pause; check the Profiler for GC Alloc
- [ ] 6. Run EditMode/PlayMode tests and a batch-mode build if build settings changed

Where things go

NeedUse
Static design data (weapon stats, enemy archetypes, loot tables)ScriptableObject asset with [CreateAssetMenu]
A reusable object configurationPrefab; variants for “same but different stats/visuals”
Per-instance runtime stateFields on a MonoBehaviour (never written back into a ScriptableObject at runtime)
Decoupled events between systemsC# events/Action on a component, or ScriptableObject event channels for designer wiring
Global services (audio, save)One bootstrap scene object or RuntimeInitializeOnLoadMethod init; avoid FindObjectOfType lookups
Large or remote assets, DLCAddressables, released with Addressables.Release

Lifecycle rules

CallbackUse for
AwakeCache own components (GetComponent), init internal state
OnEnable / OnDisableSubscribe / unsubscribe events and input actions (always paired)
StartRead other objects’ state (they have run Awake)
UpdateInput reading, timers, non-physics movement
FixedUpdateRigidbody forces and velocity changes
LateUpdateCamera follow, anything that must see final positions
OnDestroyRelease native resources, Addressables handles

Execution order across scripts is undefined unless set in Script Execution Order or [DefaultExecutionOrder(-100)]. Never rely on accidental ordering.

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody))]
public sealed class PlayerMover : MonoBehaviour
{
    [SerializeField] private MoverStats stats;            // ScriptableObject
    [SerializeField] private InputActionReference move;   // from an .inputactions asset
    [SerializeField] private InputActionReference jump;

    private Rigidbody _rb;
    private Vector2 _moveInput;
    private bool _jumpQueued;

    private void Awake() => _rb = GetComponent<Rigidbody>();

    private void OnEnable()
    {
        move.action.Enable();
        jump.action.Enable();
        jump.action.performed += OnJump;
    }

    private void OnDisable()
    {
        jump.action.performed -= OnJump;
        move.action.Disable();
        jump.action.Disable();
    }

    private void OnJump(InputAction.CallbackContext _) => _jumpQueued = true;

    private void Update() => _moveInput = move.action.ReadValue<Vector2>();

    private void FixedUpdate()
    {
        Vector3 v = _rb.linearVelocity;
        Vector3 target = new Vector3(_moveInput.x, 0f, _moveInput.y) * stats.MoveSpeed;
        v.x = target.x;
        v.z = target.z;
        if (_jumpQueued) { v.y = stats.JumpSpeed; _jumpQueued = false; }
        _rb.linearVelocity = v;
    }
}
[CreateAssetMenu(menuName = "Game/Mover Stats")]
public sealed class MoverStats : ScriptableObject
{
    [field: SerializeField] public float MoveSpeed { get; private set; } = 6f;
    [field: SerializeField] public float JumpSpeed { get; private set; } = 7f;
}

Note the pattern: input read in Update, discrete presses latched into a flag, physics applied in FixedUpdate. Reading WasPressedThisFrame() inside FixedUpdate drops presses when several frames pass between fixed steps, or none do.

Input System

  • Default for new Unity 6 projects. Player Settings > Active Input Handling should be “Input System Package (New)” unless legacy code still needs “Both”.
  • Prefer an .inputactions asset (project-wide actions are available via InputSystem.actions) or PlayerInput component for local multiplayer and rebinding.
  • Use composite bindings (WASD + left stick on one Move action) and control schemes; never branch on device type in gameplay code.
  • Rebinding: action.PerformInteractiveRebinding(); persist with SaveBindingOverridesAsJson().

Async, coroutines, and timing

  • Unity 6: Awaitable for async gameplay code. Pass destroyCancellationToken so the task stops when the object dies.
private async Awaitable FlashAsync()
{
    _renderer.enabled = false;
    await Awaitable.WaitForSecondsAsync(0.1f, destroyCancellationToken);
    _renderer.enabled = true;
}
  • Coroutines remain fine; they stop when the GameObject is deactivated, which surprises people who disable an object mid-sequence.
  • Time.deltaTime in Update, Time.fixedDeltaTime in FixedUpdate (Unity returns the fixed value from deltaTime inside FixedUpdate too). Use unscaledDeltaTime for pause menus when timeScale = 0.

Performance basics (steady state = zero GC Alloc)

  • Cache GetComponent, Camera.main, and transforms in Awake.
  • No LINQ, string concatenation, boxing, lambdas capturing locals, or new List<> in Update. Reuse buffers.
  • Physics queries with preallocated arrays: Physics.RaycastNonAlloc, Physics.OverlapSphereNonAlloc.
  • CompareTag("Enemy") instead of tag == "Enemy".
  • Pool frequently spawned objects with UnityEngine.Pool.ObjectPool<T>:
_pool = new ObjectPool<Bullet>(
    createFunc: () => Instantiate(bulletPrefab),
    actionOnGet: b => b.gameObject.SetActive(true),
    actionOnRelease: b => b.gameObject.SetActive(false),
    actionOnDestroy: b => Destroy(b.gameObject),
    collectionCheck: false, defaultCapacity: 64, maxSize: 512);
  • Many identical moving objects (thousands) → consider ECS/DOTS or GPU instancing; see the frame-budget workflow in game-performance before rewriting.

Project structure

  • Folder by feature under Assets/_Project/ (keeps your code above third-party assets).
  • Assembly definitions (.asmdef) per feature or layer: faster recompiles and enforced dependency direction. Tests in their own Tests asmdef referencing the runtime one.
  • Enter Play Mode Options with domain reload disabled speeds iteration, but static fields keep values between plays. Reset them:
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics() => s_instance = null;

Builds and tests from the command line

# Run EditMode tests headless
Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode \
  -testResults results/editmode.xml -logFile -
# Build via a static method that calls BuildPipeline.BuildPlayer
Unity -batchmode -quit -projectPath . -buildTarget StandaloneWindows64 \
  -executeMethod BuildScript.BuildWindows -logFile -
  • Do not pass -quit together with -runTests; the test runner exits by itself.
  • The build method must check BuildReport.summary.result and call EditorApplication.Exit(1) on failure so CI goes red.
  • Unity 6 Build Profiles can hold per-platform scenes and settings; keep them committed.

Gotchas

  • Unity overloads == for destroyed objects; ?. and ?? bypass it. Use if (target != null) or if (target) for UnityEngine.Object references.
  • Modifying a ScriptableObject at runtime in the Editor persists to the asset; in a build it resets on restart. Clone with Instantiate(so) for runtime-mutable copies.
  • transform.position on a Rigidbody object teleports it and breaks interpolation; use Rigidbody.MovePosition for kinematic bodies or set velocity.
  • Interpolate on the player’s Rigidbody (Interpolation = Interpolate) removes jitter when the camera follows in LateUpdate.
  • Destroy is deferred to end of frame; the object still exists for the rest of the current frame.
  • async void swallows exceptions on some paths; return Awaitable and await it.
  • Prefab overrides applied by accident (“Apply All”) propagate to every instance; review the overrides dropdown before applying.

Verification

  1. Console clean in Play Mode with Error Pause enabled; no new warnings.
  2. Profiler (CPU module, Hierarchy view sorted by GC Alloc) shows 0 B per frame for the changed scripts after warm-up.
  3. EditMode and PlayMode tests pass headless; add a PlayMode test for gameplay logic that changed.
  4. A batch-mode build succeeds and the player launches on the target platform.
  5. git status shows a .meta file for every new asset and no unintended prefab or scene changes.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc