Unity C# Standards

Coding standards for Unity C# projects — MonoBehaviour patterns, singleton guards, custom update cycles, serialization rules, and performance-safe hot-path practices.

AuthorNeexoCore
Apply to**/*.cs
Updated
unitycsharpgame-development

Naming Conventions

  • PascalCase for public fields, properties, methods, and class names.
  • camelCase for private fields, local variables, and parameters.
  • Prefix interfaces with I (e.g., IUpdateListener).
  • Use descriptive names — avoid abbreviations unless universally understood (fps, id).

MonoBehaviour Lifecycle

  • Awake() for self-initialization: singleton setup, internal state, component caching.
  • Start() only when initialization depends on other objects being ready.
  • OnEnable() / OnDisable() for registering / unregistering listeners — prevents stale references.
  • Prefer FixedUpdate() for physics and time-critical logic.
  • Avoid per-frame Update() when a custom update cycle (via a central manager) is available.

Singleton Pattern

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

Rules:

  • Guard against duplicates in Awake() with Destroy(this) or Destroy(gameObject).
  • Call DontDestroyOnLoad(gameObject) for persistent managers.
  • Never use singletons for data that should be per-scene.

Serialization & Inspector

  • Use [SerializeField] for private fields that need Inspector exposure — never make them public just for the Inspector.
  • Use [System.Serializable] for nested data classes shown in the Inspector.
  • Avoid exposing runtime-only fields to the Inspector.
  • Use [Header("Section")] and [Tooltip("...")] to organize complex inspectors.

Performance (Hot Paths)

  • Zero allocations in Update, FixedUpdate, and listener callbacks.
  • Cache component references in Awake() — never call GetComponent<T>() per frame.
  • Pre-allocate List<T> with expected capacity.
  • Prefer foreach over LINQ in hot paths — LINQ allocates enumerators.
  • Use object pooling for frequently instantiated/destroyed objects.
  • Prefer CompareTag("Enemy") over gameObject.tag == "Enemy" (avoids string alloc).

Interfaces & Custom Update Cycles

public interface IUpdateListener
{
    void OnTick(float deltaTime);
}

// Register in OnEnable, unregister in OnDisable
private void OnEnable() => UpdateManager.Instance.Register(this);
private void OnDisable() => UpdateManager.Instance.Unregister(this);

Benefits:

  • Decoupled communication between systems.
  • Controlled execution order via the manager.
  • Easy to pause/resume groups of listeners.

ScriptableObject for Data

  • Use ScriptableObject for shared configuration, game balance data, and event channels.
  • Create assets via [CreateAssetMenu(fileName = "New Config", menuName = "Game/Config")].
  • Prefer ScriptableObject events over direct references for loose coupling.

Assembly Definitions

  • Use .asmdef files to split code into assemblies for faster compile times.
  • Separate Editor code into its own assembly with Editor platform only.
  • Keep test assemblies separate with [TestAssembly] references.

Code Style

  • One primary responsibility per script/class.
  • Keep methods under ~30 lines — extract helpers for complex logic.
  • Use enums for fixed value sets (e.g., UpdateCycle, DamageType).
  • Use #region sparingly — prefer small classes over large regions.
  • Always add <summary> XML doc comments on public types and methods.

Common Pitfalls

  • Coroutine leak: Always StopCoroutine or StopAllCoroutines in OnDisable.
  • Null after Destroy: Unity overloads == on UnityEngine.Object — use if (obj != null) or the null-conditional pattern, but never rely on C# is null.
  • Awake order: Don't depend on Awake() order between scripts — use Start() or explicit initialization order via [DefaultExecutionOrder].
  • String-based APIs: Avoid SendMessage, Invoke("MethodName") — use direct calls or events.

Raw content

Copy this into your project — e.g. .instructions.md, .agent.md, or SKILL.md

## Naming Conventions

- PascalCase for public fields, properties, methods, and class names.
- camelCase for private fields, local variables, and parameters.
- Prefix interfaces with `I` (e.g., `IUpdateListener`).
- Use descriptive names — avoid abbreviations unless universally understood (`fps`, `id`).

## MonoBehaviour Lifecycle

- `Awake()` for self-initialization: singleton setup, internal state, component caching.
- `Start()` only when initialization depends on other objects being ready.
- `OnEnable()` / `OnDisable()` for registering / unregistering listeners — prevents stale references.
- Prefer `FixedUpdate()` for physics and time-critical logic.
- Avoid per-frame `Update()` when a custom update cycle (via a central manager) is available.

## Singleton Pattern

```csharp
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}
```

Rules:
- Guard against duplicates in `Awake()` with `Destroy(this)` or `Destroy(gameObject)`.
- Call `DontDestroyOnLoad(gameObject)` for persistent managers.
- Never use singletons for data that should be per-scene.

## Serialization & Inspector

- Use `[SerializeField]` for private fields that need Inspector exposure — never make them public just for the Inspector.
- Use `[System.Serializable]` for nested data classes shown in the Inspector.
- Avoid exposing runtime-only fields to the Inspector.
- Use `[Header("Section")]` and `[Tooltip("...")]` to organize complex inspectors.

## Performance (Hot Paths)

- **Zero allocations** in `Update`, `FixedUpdate`, and listener callbacks.
- Cache component references in `Awake()` — never call `GetComponent<T>()` per frame.
- Pre-allocate `List<T>` with expected capacity.
- Prefer `foreach` over LINQ in hot paths — LINQ allocates enumerators.
- Use object pooling for frequently instantiated/destroyed objects.
- Prefer `CompareTag("Enemy")` over `gameObject.tag == "Enemy"` (avoids string alloc).

## Interfaces & Custom Update Cycles

```csharp
public interface IUpdateListener
{
    void OnTick(float deltaTime);
}

// Register in OnEnable, unregister in OnDisable
private void OnEnable() => UpdateManager.Instance.Register(this);
private void OnDisable() => UpdateManager.Instance.Unregister(this);
```

Benefits:
- Decoupled communication between systems.
- Controlled execution order via the manager.
- Easy to pause/resume groups of listeners.

## ScriptableObject for Data

- Use `ScriptableObject` for shared configuration, game balance data, and event channels.
- Create assets via `[CreateAssetMenu(fileName = "New Config", menuName = "Game/Config")]`.
- Prefer ScriptableObject events over direct references for loose coupling.

## Assembly Definitions

- Use `.asmdef` files to split code into assemblies for faster compile times.
- Separate Editor code into its own assembly with `Editor` platform only.
- Keep test assemblies separate with `[TestAssembly]` references.

## Code Style

- One primary responsibility per script/class.
- Keep methods under ~30 lines — extract helpers for complex logic.
- Use enums for fixed value sets (e.g., `UpdateCycle`, `DamageType`).
- Use `#region` sparingly — prefer small classes over large regions.
- Always add `<summary>` XML doc comments on public types and methods.

## Common Pitfalls

- **Coroutine leak**: Always `StopCoroutine` or `StopAllCoroutines` in `OnDisable`.
- **Null after Destroy**: Unity overloads `==` on `UnityEngine.Object` — use `if (obj != null)` or the null-conditional pattern, but never rely on C# `is null`.
- **Awake order**: Don't depend on `Awake()` order between scripts — use `Start()` or explicit initialization order via `[DefaultExecutionOrder]`.
- **String-based APIs**: Avoid `SendMessage`, `Invoke("MethodName")` — use direct calls or events.