Files
Claude-Code-Game-Studios/docs/engine-reference/unity/current-best-practices.md
T
DonchitosandClaude Opus 5.5 b21fa0f7f2 Release 1.1.2 — the default path works end to end
Fix release for 1.1.1: the minimal path end to end, engine commands that
run on Godot, Unity and Unreal, hook warnings you can see, and verdicts
that say when they could not check. Details in CHANGELOG.md; upgrade
steps in UPGRADING.md (v1.1.1 → v1.1.2).

Fixes #67
Fixes #131

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJn2FdynEybfL5KC697nJ
2026-09-29 21:32:19 +10:00

9.9 KiB

Unity 6.3 LTS — Current Best Practices

Last verified: 2026-02-13

Modern Unity 6 patterns that may not be in the LLM's training data. These are production-ready recommendations as of Unity 6.3 LTS.


Project Setup

Use Unity 6.3 LTS for Production

  • Tech Stream (6.4+): Latest features, less stable
  • LTS (6.3): Production-ready, 2-year support (until Dec 2027)

Choose the Right Render Pipeline

  • URP (Universal): Mobile, cross-platform, good performance ✅ Recommended for most games
  • HDRP (High Definition): High-end PC/console, photorealistic
  • Built-in: Deprecated, avoid for new projects

Scripting

Use C# 9+ Features (Unity 6 Supports C# 9)

// ✅ Record types for data
public record PlayerData(string Name, int Level, float Health);

// ✅ Init-only properties
public class Config {
    public string GameMode { get; init; }
}

// ✅ Pattern matching
var result = enemy switch {
    Boss boss => boss.Enrage(),
    Minion minion => minion.Flee(),
    _ => null
};

Async/Await for Asset Loading

// ✅ Modern async pattern
public async Task<GameObject> LoadEnemyAsync(string key) {
    var handle = Addressables.LoadAssetAsync<GameObject>(key);
    return await handle.Task;
}

Use Source Generators for Serialization (Unity 6+)

// ✅ Source-generated serialization (faster, less reflection)
[GenerateSerializer]
public partial struct PlayerStats : IComponentData {
    public int Health;
    public int Mana;
}

DOTS/ECS (Production-Ready in Unity 6.3 LTS)

Use ISystem (Not ComponentSystem)

// ✅ Modern unmanaged ISystem (Burst-compatible)
public partial struct MovementSystem : ISystem {
    public void OnCreate(ref SystemState state) { }

    public void OnUpdate(ref SystemState state) {
        foreach (var (transform, speed) in
            SystemAPI.Query<RefRW<LocalTransform>, RefRO<MoveSpeed>>()) {
            transform.ValueRW.Position += speed.ValueRO.Value * SystemAPI.Time.DeltaTime;
        }
    }
}

Use IJobEntity for Parallel Jobs

// ✅ IJobEntity (replaces IJobForEach)
[BurstCompile]
public partial struct DamageJob : IJobEntity {
    public float DeltaTime;

    void Execute(ref Health health, in DamageOverTime dot) {
        health.Value -= dot.DamagePerSecond * DeltaTime;
    }
}

// Schedule it
var job = new DamageJob { DeltaTime = SystemAPI.Time.DeltaTime };
job.ScheduleParallel();

Input

Use Input System Package (Not Legacy Input)

// ✅ Input Actions (rebindable, cross-platform)
using UnityEngine.InputSystem;

public class PlayerInput : MonoBehaviour {
    private PlayerControls controls;

    void Awake() {
        controls = new PlayerControls();
        controls.Gameplay.Jump.performed += ctx => Jump();
    }

    void OnEnable() => controls.Enable();
    void OnDisable() => controls.Disable();
}

Create Input Actions asset in editor, generate C# class via inspector.


UI

Use UI Toolkit for Runtime UI (Production-Ready in Unity 6)

// ✅ UI Toolkit (replaces UGUI for new projects)
using UnityEngine.UIElements;

public class MainMenu : MonoBehaviour {
    void OnEnable() {
        var root = GetComponent<UIDocument>().rootVisualElement;

        var playButton = root.Q<Button>("play-button");
        playButton.clicked += StartGame;

        var scoreLabel = root.Q<Label>("score");
        scoreLabel.text = $"High Score: {PlayerPrefs.GetInt("HighScore")}";
    }
}

UXML (UI structure) + USS (styling) = HTML/CSS-like workflow.


Asset Management

Use Addressables (Not Resources)

// ✅ Addressables (async, memory-efficient)
using UnityEngine.AddressableAssets;

public async Task SpawnEnemyAsync(string enemyKey) {
    var handle = Addressables.InstantiateAsync(enemyKey);
    var enemy = await handle.Task;

    // Cleanup: release when destroyed
    Addressables.ReleaseInstance(enemy);
}

Benefits: Async loading, remote content delivery, better memory control.


Rendering

Use RenderGraph API for Custom Passes (URP/HDRP)

// ✅ RenderGraph API (Unity 6+)
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) {
    using (var builder = renderGraph.AddRasterRenderPass<PassData>("My Pass", out var passData)) {
        // Setup pass
        builder.SetRenderFunc((PassData data, RasterGraphContext context) => {
            // Execute commands
        });
    }
}

Replaces: Old CommandBuffer.Execute() pattern.

This is URP's API. RecordRenderGraph overrides URP's ScriptableRenderPass. HDRP custom passes are a separate API: a CustomPass subclass, run from a Custom Pass Volume, overrides Setup(ScriptableRenderContext renderContext, CommandBuffer cmd), Execute(CustomPassContext ctx) and Cleanup() (HDRP 17.3, Create a Custom Pass in a C# script: https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.3/manual/Custom-Pass-Scripting.html).


Performance

Use Burst Compiler + Jobs System

// ✅ Burst-compiled job (massive performance gain)
[BurstCompile]
struct ParticleUpdateJob : IJobParallelFor {
    public NativeArray<float3> Positions;
    public NativeArray<float3> Velocities;
    public float DeltaTime;

    public void Execute(int index) {
        Positions[index] += Velocities[index] * DeltaTime;
    }
}

// Schedule
var job = new ParticleUpdateJob {
    Positions = positions,
    Velocities = velocities,
    DeltaTime = Time.deltaTime
};
job.Schedule(positions.Length, 64).Complete();

20-100x faster than equivalent C# code.


Use GPU Instancing for Repeated Objects

// ✅ GPU Instancing (thousands of objects, minimal draw calls)
Graphics.RenderMeshInstanced(
    new RenderParams(material),
    mesh,
    0,
    matrices // NativeArray<Matrix4x4>
);

Memory Management

Use NativeContainers (Not Managed Arrays in Jobs)

// ✅ NativeArray (no GC, Burst-compatible)
NativeArray<int> data = new NativeArray<int>(1000, Allocator.TempJob);
// ... use in job
data.Dispose(); // Manual cleanup required

// ✅ Or use using statement
using var data = new NativeArray<int>(1000, Allocator.TempJob);
// Auto-disposed

Multiplayer

Use Netcode for GameObjects (Official)

// ✅ Unity's official netcode
using Unity.Netcode;

public class Player : NetworkBehaviour {
    private NetworkVariable<int> health = new NetworkVariable<int>(100);

    [ServerRpc]
    public void TakeDamageServerRpc(int damage) {
        health.Value -= damage;
    }
}

Replaces: UNet (deprecated), MLAPI (renamed to Netcode for GameObjects).


Testing

Use Unity Test Framework (NUnit-based)

// ✅ Play Mode Test
[UnityTest]
public IEnumerator Player_TakesDamage_HealthDecreases() {
    var player = new GameObject().AddComponent<Player>();
    player.Health = 100;

    player.TakeDamage(25);
    yield return null; // Wait one frame

    Assert.AreEqual(75, player.Health);
}

Command Line — Tests

Documented (Unity Manual, Run tests from the command line; Test Framework command-line reference): <Unity editor> -runTests -batchmode -projectPath <path> -testPlatform <platform> -testResults <file>. -batchmode removes the need for user input. -testPlatform takes EditMode, PlayMode (Play Mode tests in the Editor) or a player platform; with no value, tests run in Edit Mode. -testResults defaults to the project root, and the file follows NUnit's XML format: the <test-run> element carries testcasecount, result, passed and failed, and each <test-case> a fullname and a result of Passed, Failed, Inconclusive or Skipped. Unity defines no common exit codes for test runs.

Observed on Unity 6000.3.23f1 — not in the docs:

  • -runTests exits 0 when every test passes and 2 when one fails; 1 is a compile error (no results file is written), 3 a failed run, 4 an unknown -testPlatform. It exits by itself — no -quit.
  • Tests outside Assets/ are never compiled: the run reports testcasecount="0", result="Passed" and exits 0 — a false pass.
  • Without the com.unity.test-framework package, -runTests hangs rather than failing.
  • Unity on PATH may be Unity's separate CLI, not the editor; it rejects -batchmode and -version with exit 2. Name the editor executable by its full path.
  • -batchmode -quit -projectPath . -logFile - exits 1 on a compile error and 0 when clean.

Sources:


Debugging

Use Logging Best Practices

// ✅ Structured logging (Unity 6+)
using UnityEngine;

Debug.Log($"Player {playerName} scored {score} points");

// ✅ Conditional compilation for debug code
#if UNITY_EDITOR || DEVELOPMENT_BUILD
    Debug.DrawRay(transform.position, direction, Color.red);
#endif

Summary: Unity 6 Tech Stack

Feature Use This (2026) Avoid This (Legacy)
Input Input System package Input class
UI UI Toolkit UGUI (Canvas)
ECS ISystem + IJobEntity ComponentSystem
Rendering URP + RenderGraph Built-in pipeline
Assets Addressables Resources
Jobs Burst + IJobParallelFor Coroutines for heavy work
Multiplayer Netcode for GameObjects UNet

Sources: