Back to list
henboffman

csharp-unity-patterns

by henboffman

1🍴 0📅 Jan 21, 2026

SKILL.md


name: csharp-unity-patterns description: C# coding patterns, best practices, and architectural guidelines for Unity game development. Use when writing C# scripts in Unity, implementing design patterns for games, optimizing Unity code, or establishing code architecture for Unity projects. Covers MonoBehaviour lifecycle, ScriptableObjects, events, coroutines, and Unity-specific C# patterns.

C# Unity Patterns

Best practices and patterns for writing clean, performant C# code in Unity.

MonoBehaviour Lifecycle

Execution Order

Awake()           → Called once when script instance loads
OnEnable()        → Called when object becomes active
Start()           → Called once before first Update
FixedUpdate()     → Physics updates (fixed timestep)
Update()          → Called every frame
LateUpdate()      → Called after all Updates
OnDisable()       → Called when object becomes inactive
OnDestroy()       → Called when object is destroyed

When to Use Each

public class LifecycleExample : MonoBehaviour
{
    // Awake: Initialize self, get own components
    // Called even if script is disabled
    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    // OnEnable: Subscribe to events, reset state
    void OnEnable()
    {
        GameEvents.OnGamePaused += HandlePause;
    }

    // Start: Initialize references to other objects
    // Called only if script is enabled
    void Start()
    {
        player = FindObjectOfType<Player>();
        uiManager = UIManager.Instance;
    }

    // Update: Input, non-physics game logic
    void Update()
    {
        HandleInput();
        UpdateAnimations();
    }

    // FixedUpdate: Physics, movement
    void FixedUpdate()
    {
        rb.MovePosition(rb.position + velocity * Time.fixedDeltaTime);
    }

    // LateUpdate: Camera follow, after-movement adjustments
    void LateUpdate()
    {
        UpdateCamera();
    }

    // OnDisable: Unsubscribe from events
    void OnDisable()
    {
        GameEvents.OnGamePaused -= HandlePause;
    }

    // OnDestroy: Cleanup, save state if needed
    void OnDestroy()
    {
        SaveProgress();
    }
}

Singleton Pattern

Basic Singleton

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

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

Lazy Singleton (Creates if Missing)

public class AudioManager : MonoBehaviour
{
    private static AudioManager instance;
    public static AudioManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<AudioManager>();
                if (instance == null)
                {
                    var go = new GameObject("AudioManager");
                    instance = go.AddComponent<AudioManager>();
                    DontDestroyOnLoad(go);
                }
            }
            return instance;
        }
    }
}

Generic Singleton Base Class

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    private static readonly object lockObj = new object();

    public static T Instance
    {
        get
        {
            lock (lockObj)
            {
                if (instance == null)
                {
                    instance = FindObjectOfType<T>();
                    if (instance == null)
                    {
                        var go = new GameObject(typeof(T).Name);
                        instance = go.AddComponent<T>();
                    }
                }
                return instance;
            }
        }
    }

    protected virtual void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this as T;
        DontDestroyOnLoad(gameObject);
    }
}

// Usage:
public class GameManager : Singleton<GameManager>
{
    protected override void Awake()
    {
        base.Awake();
        // Additional initialization
    }
}

Event Systems

C# Events

public class Player : MonoBehaviour
{
    // Simple event
    public event Action OnDeath;

    // Event with data
    public event Action<int> OnHealthChanged;
    public event Action<int, int> OnDamageTaken; // damage, currentHealth

    private int health;

    public void TakeDamage(int damage)
    {
        health -= damage;
        OnHealthChanged?.Invoke(health);
        OnDamageTaken?.Invoke(damage, health);

        if (health <= 0)
            OnDeath?.Invoke();
    }
}

// Subscriber
public class HealthUI : MonoBehaviour
{
    [SerializeField] private Player player;

    void OnEnable()
    {
        player.OnHealthChanged += UpdateHealthBar;
    }

    void OnDisable()
    {
        player.OnHealthChanged -= UpdateHealthBar;
    }

    private void UpdateHealthBar(int newHealth)
    {
        // Update UI
    }
}

Static Event Bus

public static class GameEvents
{
    public static event Action OnGamePaused;
    public static event Action OnGameResumed;
    public static event Action<int> OnScoreChanged;
    public static event Action<Season> OnSeasonChanged;

    public static void TriggerPause() => OnGamePaused?.Invoke();
    public static void TriggerResume() => OnGameResumed?.Invoke();
    public static void TriggerScoreChange(int score) => OnScoreChanged?.Invoke(score);
    public static void TriggerSeasonChange(Season season) => OnSeasonChanged?.Invoke(season);
}

ScriptableObject Event System

[CreateAssetMenu(fileName = "GameEvent", menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<GameEventListener> listeners = new();

    public void Raise()
    {
        for (int i = listeners.Count - 1; i >= 0; i--)
            listeners[i].OnEventRaised();
    }

    public void RegisterListener(GameEventListener listener) => listeners.Add(listener);
    public void UnregisterListener(GameEventListener listener) => listeners.Remove(listener);
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent gameEvent;
    [SerializeField] private UnityEvent response;

    void OnEnable() => gameEvent.RegisterListener(this);
    void OnDisable() => gameEvent.UnregisterListener(this);

    public void OnEventRaised() => response.Invoke();
}

ScriptableObject Patterns

Data Container

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
    [Header("Basic Info")]
    public string itemName;
    [TextArea(2, 5)] public string description;
    public Sprite icon;

    [Header("Properties")]
    public ItemType itemType;
    public int maxStack = 99;
    public int buyPrice;
    public int sellPrice;

    [Header("Usage")]
    public bool isConsumable;
    public int healAmount;
    public int energyRestore;
}

Runtime Data (Variables)

[CreateAssetMenu(fileName = "IntVariable", menuName = "Variables/Int")]
public class IntVariable : ScriptableObject
{
    [SerializeField] private int initialValue;
    [NonSerialized] public int RuntimeValue;

    public void OnEnable()
    {
        RuntimeValue = initialValue;
    }

    public void Add(int amount) => RuntimeValue += amount;
    public void Set(int value) => RuntimeValue = value;
}

// Usage in Inspector: Reference the same IntVariable asset
// across multiple components to share state

Configuration Assets

[CreateAssetMenu(fileName = "GameConfig", menuName = "Config/Game Config")]
public class GameConfig : ScriptableObject
{
    [Header("Time Settings")]
    public float secondsPerGameMinute = 0.5f;
    public int dayStartHour = 6;
    public int dayEndHour = 22;

    [Header("Player Settings")]
    public float walkSpeed = 3f;
    public float runSpeed = 6f;
    public int maxStamina = 100;

    [Header("Economy")]
    public float sellPriceMultiplier = 0.5f;
}

Object Pooling

Simple Pool

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 10;

    private Queue<GameObject> pool = new Queue<GameObject>();

    void Start()
    {
        for (int i = 0; i < initialSize; i++)
        {
            CreateNewObject();
        }
    }

    private GameObject CreateNewObject()
    {
        var obj = Instantiate(prefab, transform);
        obj.SetActive(false);
        pool.Enqueue(obj);
        return obj;
    }

    public GameObject Get()
    {
        if (pool.Count == 0)
            CreateNewObject();

        var obj = pool.Dequeue();
        obj.SetActive(true);
        return obj;
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        pool.Enqueue(obj);
    }
}

Generic Pool with Interface

public interface IPoolable
{
    void OnSpawn();
    void OnDespawn();
}

public class GenericPool<T> where T : MonoBehaviour, IPoolable
{
    private readonly T prefab;
    private readonly Transform parent;
    private readonly Queue<T> pool = new Queue<T>();

    public GenericPool(T prefab, Transform parent, int initialSize)
    {
        this.prefab = prefab;
        this.parent = parent;

        for (int i = 0; i < initialSize; i++)
            pool.Enqueue(CreateNew());
    }

    private T CreateNew()
    {
        var obj = Object.Instantiate(prefab, parent);
        obj.gameObject.SetActive(false);
        return obj;
    }

    public T Get(Vector3 position, Quaternion rotation)
    {
        var obj = pool.Count > 0 ? pool.Dequeue() : CreateNew();
        obj.transform.SetPositionAndRotation(position, rotation);
        obj.gameObject.SetActive(true);
        obj.OnSpawn();
        return obj;
    }

    public void Return(T obj)
    {
        obj.OnDespawn();
        obj.gameObject.SetActive(false);
        pool.Enqueue(obj);
    }
}

State Machine

Simple State Machine

public interface IState
{
    void Enter();
    void Update();
    void Exit();
}

public class StateMachine
{
    private IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter();
    }

    public void Update()
    {
        currentState?.Update();
    }
}

// Example states
public class IdleState : IState
{
    private readonly Player player;

    public IdleState(Player player) => this.player = player;

    public void Enter() => player.Animator.Play("Idle");
    public void Update() { /* Check for input */ }
    public void Exit() { }
}

Enum-Based State Machine

public class EnemyAI : MonoBehaviour
{
    public enum State { Idle, Patrol, Chase, Attack }

    private State currentState = State.Idle;

    void Update()
    {
        switch (currentState)
        {
            case State.Idle:
                UpdateIdle();
                break;
            case State.Patrol:
                UpdatePatrol();
                break;
            case State.Chase:
                UpdateChase();
                break;
            case State.Attack:
                UpdateAttack();
                break;
        }
    }

    public void SetState(State newState)
    {
        if (currentState == newState) return;

        ExitState(currentState);
        currentState = newState;
        EnterState(newState);
    }

    private void EnterState(State state)
    {
        switch (state)
        {
            case State.Chase:
                // Start chase animation
                break;
        }
    }

    private void ExitState(State state)
    {
        // Cleanup for state
    }
}

Coroutines

Common Patterns

public class CoroutineExamples : MonoBehaviour
{
    // Delayed action
    public void DoAfterDelay(float delay, Action action)
    {
        StartCoroutine(DelayedAction(delay, action));
    }

    private IEnumerator DelayedAction(float delay, Action action)
    {
        yield return new WaitForSeconds(delay);
        action?.Invoke();
    }

    // Fade effect
    public IEnumerator FadeOut(SpriteRenderer sr, float duration)
    {
        Color color = sr.color;
        float startAlpha = color.a;

        for (float t = 0; t < duration; t += Time.deltaTime)
        {
            color.a = Mathf.Lerp(startAlpha, 0, t / duration);
            sr.color = color;
            yield return null;
        }

        color.a = 0;
        sr.color = color;
    }

    // Wait for condition
    public IEnumerator WaitForCondition(Func<bool> condition, Action onComplete)
    {
        yield return new WaitUntil(condition);
        onComplete?.Invoke();
    }

    // Repeat with interval
    public IEnumerator RepeatAction(float interval, Action action)
    {
        while (true)
        {
            action?.Invoke();
            yield return new WaitForSeconds(interval);
        }
    }

    // Cancellable coroutine
    private Coroutine runningRoutine;

    public void StartMyRoutine()
    {
        if (runningRoutine != null)
            StopCoroutine(runningRoutine);
        runningRoutine = StartCoroutine(MyRoutine());
    }

    public void StopMyRoutine()
    {
        if (runningRoutine != null)
        {
            StopCoroutine(runningRoutine);
            runningRoutine = null;
        }
    }
}

Component Access Patterns

Cached Component References

public class Player : MonoBehaviour
{
    // Serialize for Inspector assignment (preferred)
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private Animator animator;
    [SerializeField] private SpriteRenderer spriteRenderer;

    // Or cache in Awake
    void Awake()
    {
        rb ??= GetComponent<Rigidbody2D>();
        animator ??= GetComponent<Animator>();
        spriteRenderer ??= GetComponent<SpriteRenderer>();
    }
}

RequireComponent Attribute

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Collider2D))]
public class PhysicsEntity : MonoBehaviour
{
    private Rigidbody2D rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>(); // Guaranteed to exist
    }
}

TryGetComponent (Unity 2019.2+)

void OnTriggerEnter2D(Collider2D other)
{
    // More efficient than GetComponent + null check
    if (other.TryGetComponent<IDamageable>(out var damageable))
    {
        damageable.TakeDamage(10);
    }
}

Performance Tips

Avoid in Update()

// BAD - allocates every frame
void Update()
{
    var enemies = FindObjectsOfType<Enemy>();
    string name = "Player" + score.ToString();
}

// GOOD - cache references
private Enemy[] enemies;
private StringBuilder nameBuilder = new StringBuilder();

void Start()
{
    enemies = FindObjectsOfType<Enemy>();
}

Use NonAlloc Methods

// BAD - allocates new array
var hits = Physics2D.RaycastAll(origin, direction);

// GOOD - reuse buffer
private RaycastHit2D[] hitBuffer = new RaycastHit2D[10];

void CheckHits()
{
    int hitCount = Physics2D.RaycastNonAlloc(origin, direction, hitBuffer);
    for (int i = 0; i < hitCount; i++)
    {
        // Process hitBuffer[i]
    }
}

CompareTag Instead of == for Tags

// BAD - allocates string
if (other.tag == "Player")

// GOOD - no allocation
if (other.CompareTag("Player"))

Reference Files

Score

Total Score

40/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon