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.
- name
unity-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
- Designing an engine-agnostic ECS from scratch — use ecs-architecture
- General C# and .NET backend code outside Unity — use csharp-dotnet
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.
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.txtandPackages/manifest.jsonbefore 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
.metafiles 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] privateoverpublicfields. 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
| Need | Use |
|---|---|
| Static design data (weapon stats, enemy archetypes, loot tables) | ScriptableObject asset with [CreateAssetMenu] |
| A reusable object configuration | Prefab; variants for “same but different stats/visuals” |
| Per-instance runtime state | Fields on a MonoBehaviour (never written back into a ScriptableObject at runtime) |
| Decoupled events between systems | C# 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, DLC | Addressables, released with Addressables.Release |
Lifecycle rules
| Callback | Use for |
|---|---|
Awake | Cache own components (GetComponent), init internal state |
OnEnable / OnDisable | Subscribe / unsubscribe events and input actions (always paired) |
Start | Read other objects’ state (they have run Awake) |
Update | Input reading, timers, non-physics movement |
FixedUpdate | Rigidbody forces and velocity changes |
LateUpdate | Camera follow, anything that must see final positions |
OnDestroy | Release 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
.inputactionsasset (project-wide actions are available viaInputSystem.actions) orPlayerInputcomponent for local multiplayer and rebinding. - Use composite bindings (WASD + left stick on one
Moveaction) and control schemes; never branch on device type in gameplay code. - Rebinding:
action.PerformInteractiveRebinding(); persist withSaveBindingOverridesAsJson().
Async, coroutines, and timing
- Unity 6:
Awaitablefor async gameplay code. PassdestroyCancellationTokenso 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.deltaTimein Update,Time.fixedDeltaTimein FixedUpdate (Unity returns the fixed value fromdeltaTimeinside FixedUpdate too). UseunscaledDeltaTimefor pause menus whentimeScale = 0.
Performance basics (steady state = zero GC Alloc)
- Cache
GetComponent,Camera.main, and transforms inAwake. - No LINQ, string concatenation, boxing, lambdas capturing locals, or
new List<>inUpdate. Reuse buffers. - Physics queries with preallocated arrays:
Physics.RaycastNonAlloc,Physics.OverlapSphereNonAlloc. CompareTag("Enemy")instead oftag == "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 ownTestsasmdef 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
-quittogether with-runTests; the test runner exits by itself. - The build method must check
BuildReport.summary.resultand callEditorApplication.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. Useif (target != null)orif (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.positionon a Rigidbody object teleports it and breaks interpolation; useRigidbody.MovePositionfor kinematic bodies or set velocity.- Interpolate on the player’s Rigidbody (Interpolation = Interpolate) removes jitter when the camera follows in LateUpdate.
Destroyis deferred to end of frame; the object still exists for the rest of the current frame.async voidswallows exceptions on some paths; returnAwaitableand await it.- Prefab overrides applied by accident (“Apply All”) propagate to every instance; review the overrides dropdown before applying.
Verification
- Console clean in Play Mode with Error Pause enabled; no new warnings.
- Profiler (CPU module, Hierarchy view sorted by GC Alloc) shows 0 B per frame for the changed scripts after warm-up.
- EditMode and PlayMode tests pass headless; add a PlayMode test for gameplay logic that changed.
- A batch-mode build succeeds and the player launches on the target platform.
git statusshows a.metafile 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.