This page covers Unity-specific C# patterns — the engine APIs, idioms, and workflows that differ from standard C#.
For the full C# language reference (LINQ, async/await, generics, OOP, etc.) see CSharp.
For the full Unity engine reference (editor, physics, animation, UI, etc.) see Unity.
MonoBehaviour Lifecycle
Full Lifecycle Order
public class LifecycleDemo : MonoBehaviour{ // ── Initialization ────────────────────────────────────────── void Awake() { // First callback. Called even if component is disabled. // Use: get component references, initialize data. // All Awakes run before any Start. } void OnEnable() { // Called every time the GameObject/component is enabled. // Use: subscribe to events. } void Start() { // Called once on first frame the component is active. // All Awakes have already run. // Use: logic that depends on other objects being initialized. } // ── Per-Frame ──────────────────────────────────────────────── void Update() { // Every frame. Frame rate dependent. // Use: input, non-physics movement, timers. } void LateUpdate() { // After all Updates. Same frame. // Use: camera follow, IK, anything that reads final positions. } // ── Physics (Fixed Timestep) ───────────────────────────────── void FixedUpdate() { // Fixed timestep (default 0.02s = 50Hz). Frame-rate independent. // Use: Rigidbody forces, physics queries. } // ── Rendering ──────────────────────────────────────────────── void OnPreRender() { } // before camera renders void OnPostRender() { } // after camera renders void OnRenderObject() { } // after scene is rendered void OnWillRenderObject() { } // called for each camera that renders this object // ── Collision & Trigger ────────────────────────────────────── void OnCollisionEnter(Collision col) { } void OnCollisionStay(Collision col) { } void OnCollisionExit(Collision col) { } void OnTriggerEnter(Collider other) { } void OnTriggerStay(Collider other) { } void OnTriggerExit(Collider other) { } // 2D versions void OnCollisionEnter2D(Collision2D col) { } void OnTriggerEnter2D(Collider2D other) { } // ── Mouse ──────────────────────────────────────────────────── void OnMouseDown() { } // mouse button pressed over collider void OnMouseUp() { } // mouse button released void OnMouseEnter() { } // mouse enters collider bounds void OnMouseExit() { } // mouse leaves collider bounds void OnMouseOver() { } // mouse is over collider // ── Application ────────────────────────────────────────────── void OnApplicationPause(bool paused) { } // app paused/resumed void OnApplicationFocus(bool focused) { } // app gained/lost focus void OnApplicationQuit() { } // before app quits // ── Destruction ────────────────────────────────────────────── void OnDisable() { // Called when component/GameObject is disabled. // Use: unsubscribe from events. } void OnDestroy() { // Called when GameObject is destroyed. // Use: cleanup, unsubscribe remaining events. }}
Awake vs Start — When to Use Each
public class Enemy : MonoBehaviour{ Rigidbody rb; Health health; GameManager gm; void Awake() { // SELF-INITIALIZATION — get own components rb = GetComponent<Rigidbody>(); health = GetComponent<Health>(); // Don't access other objects here — they may not be Awake yet } void Start() { // CROSS-OBJECT INITIALIZATION — safe to access other objects gm = GameManager.Instance; gm.RegisterEnemy(this); health.OnDeath += OnDeath; }}
Unity-Specific Types
Vector2 & Vector3
// ConstructionVector3 pos = new Vector3(1f, 2f, 3f);Vector2 dir = new Vector2(0f, 1f);// ConstantsVector3.zero // (0,0,0)Vector3.one // (1,1,1)Vector3.up // (0,1,0)Vector3.down // (0,-1,0)Vector3.forward // (0,0,1)Vector3.back // (0,0,-1)Vector3.right // (1,0,0)Vector3.left // (-1,0,0)// Operationsfloat mag = pos.magnitude; // lengthfloat mag2 = pos.sqrMagnitude; // faster (no sqrt)Vector3 n = pos.normalized; // unit vectorfloat dist = Vector3.Distance(a, b);float dot = Vector3.Dot(a, b); // 1=same dir, 0=perp, -1=oppositeVector3 cross = Vector3.Cross(a, b); // perpendicular vector// InterpolationVector3.Lerp(a, b, t); // linear, t clamped 0-1Vector3.LerpUnclamped(a, b, t); // t can exceed 0-1Vector3.Slerp(a, b, t); // spherical (for directions)Vector3.MoveTowards(a, b, maxDelta); // move by max distanceVector3.SmoothDamp(a, b, ref vel, smoothTime); // smooth follow// Angle between vectorsfloat angle = Vector3.Angle(a, b); // 0-180 degreesfloat signed = Vector3.SignedAngle(a, b, Vector3.up); // -180 to 180// ProjectVector3 proj = Vector3.Project(a, b); // project a onto bVector3 perp = Vector3.ProjectOnPlane(a, normal); // project onto planeVector3 refl = Vector3.Reflect(dir, normal); // reflect off surface
// Construction (0-1 range)Color red = new Color(1f, 0f, 0f);Color semi = new Color(1f, 0f, 0f, 0.5f); // 50% transparent// Named colorsColor.red; Color.green; Color.blue; Color.white; Color.black;Color.yellow; Color.cyan; Color.magenta; Color.clear; Color.gray;// Hex (Color32 uses 0-255)Color32 c32 = new Color32(255, 128, 0, 255);Color fromHex;ColorUtility.TryParseHtmlString("#FF8000", out fromHex);string hex = "#" + ColorUtility.ToHtmlStringRGB(Color.red); // "FF0000"// InterpolateColor lerped = Color.Lerp(Color.red, Color.blue, 0.5f);// HSVColor.RGBToHSV(Color.red, out float h, out float s, out float v);Color fromHSV = Color.HSVToRGB(0.5f, 1f, 1f);
Time
Time.deltaTime // seconds since last frame (use in Update)Time.fixedDeltaTime // fixed physics timestep (use in FixedUpdate)Time.time // seconds since game startedTime.unscaledTime // time ignoring timeScale (for UI/menus)Time.timeScale // 1=normal, 0=paused, 0.5=half speed, 2=doubleTime.frameCount // total frames renderedTime.realtimeSinceStartup // real wall-clock time// Pause gameTime.timeScale = 0f;// Slow motionTime.timeScale = 0.3f;Time.fixedDeltaTime = 0.02f * Time.timeScale; // keep physics stable// Timer patternfloat timer = 0f;float interval = 2f;void Update() { timer += Time.deltaTime; if (timer >= interval) { timer = 0f; DoSomething(); }}
Mathf
// ConstantsMathf.PI // 3.14159...Mathf.Infinity // float.MaxValueMathf.Deg2Rad // multiply degrees to get radiansMathf.Rad2Deg // multiply radians to get degrees// Common functionsMathf.Abs(-5f) // 5Mathf.Clamp(val, 0f, 1f) // clamp between min and maxMathf.Clamp01(val) // clamp between 0 and 1Mathf.Min(a, b) // smaller valueMathf.Max(a, b) // larger valueMathf.Sqrt(16f) // 4Mathf.Pow(2f, 8f) // 256Mathf.Floor(3.7f) // 3Mathf.Ceil(3.2f) // 4Mathf.Round(3.5f) // 4Mathf.Sign(-5f) // -1// InterpolationMathf.Lerp(0f, 100f, 0.5f) // 50Mathf.LerpUnclamped(0f, 100f, 1.5f) // 150Mathf.InverseLerp(0f, 100f, 50f) // 0.5 (reverse lerp)Mathf.SmoothStep(0f, 1f, t) // smooth ease in/outMathf.MoveTowards(cur, target, maxDelta)Mathf.SmoothDamp(cur, target, ref vel, smoothTime)// TrigMathf.Sin(angle * Mathf.Deg2Rad)Mathf.Cos(angle * Mathf.Deg2Rad)Mathf.Atan2(y, x) * Mathf.Rad2Deg // angle from x-axis// Repeat / PingPongMathf.Repeat(Time.time, 1f) // 0→1→0→1 (sawtooth)Mathf.PingPong(Time.time, 1f) // 0→1→0→1 (triangle)// RandomRandom.Range(0, 10) // int: 0 to 9 (exclusive max)Random.Range(0f, 1f) // float: 0.0 to 1.0 (inclusive max)Random.insideUnitCircle // random Vector2 inside unit circleRandom.insideUnitSphere // random Vector3 inside unit sphereRandom.onUnitSphere // random Vector3 on unit sphere surfaceRandom.rotation // random Quaternion
Coroutines In Depth
All Yield Instructions
yield return null; // wait one frameyield return new WaitForSeconds(2f); // wait N real secondsyield return new WaitForSecondsRealtime(2f); // wait N seconds (ignores timeScale)yield return new WaitForFixedUpdate(); // wait for next FixedUpdateyield return new WaitForEndOfFrame(); // wait until end of frame (after rendering)yield return new WaitUntil(() => isReady); // wait until condition is trueyield return new WaitWhile(() => isLoading); // wait while condition is trueyield return StartCoroutine(OtherRoutine()); // wait for another coroutineyield return new AsyncOperation(); // wait for async operation (scene load, etc.)yield break; // exit coroutine early
Coroutine Patterns
// Cached coroutine reference (to stop it later)Coroutine flashRoutine;void StartFlash() { if (flashRoutine != null) StopCoroutine(flashRoutine); flashRoutine = StartCoroutine(Flash());}IEnumerator Flash() { for (int i = 0; i < 5; i++) { renderer.enabled = false; yield return new WaitForSeconds(0.1f); renderer.enabled = true; yield return new WaitForSeconds(0.1f); } flashRoutine = null;}// Countdown timerIEnumerator Countdown(int seconds) { while (seconds > 0) { timerText.text = seconds.ToString(); yield return new WaitForSeconds(1f); seconds--; } timerText.text = "GO!"; StartGame();}// Smooth value changeIEnumerator SmoothFOV(float targetFOV, float duration) { float startFOV = cam.fieldOfView; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; cam.fieldOfView = Mathf.Lerp(startFOV, targetFOV, elapsed / duration); yield return null; } cam.fieldOfView = targetFOV;}
ScriptableObjects
Data Container Pattern
// Item stats — shared across all instances of an item type[CreateAssetMenu(fileName = "NewItem", menuName = "Game/Item")]public class ItemData : ScriptableObject{ [Header("Identity")] public string itemName; public Sprite icon; [TextArea] public string description; [Header("Stats")] public int damage; public float attackSpeed; public float range; [Header("Audio")] public AudioClip useSound; public AudioClip pickupSound; [Header("Prefab")] public GameObject worldPrefab; // Methods are fine too public string GetTooltip() => $"{itemName}\nDamage: {damage}\nSpeed: {attackSpeed}";}// Enemy config[CreateAssetMenu(menuName = "Game/EnemyConfig")]public class EnemyConfig : ScriptableObject{ public string enemyName; public int maxHealth; public float moveSpeed; public float detectionRange; public float attackRange; public int damage; public float attackCooldown; public GameObject deathEffect; public int scoreValue;}
ScriptableObject Event System
// GameEvent.cs — the event asset[CreateAssetMenu(menuName = "Events/GameEvent")]public class GameEvent : ScriptableObject{ readonly List<GameEventListener> listeners = new(); public void Raise() { for (int i = listeners.Count - 1; i >= 0; i--) listeners[i].OnEventRaised(); } public void Register(GameEventListener l) => listeners.Add(l); public void Unregister(GameEventListener l) => listeners.Remove(l);}// GameEventListener.cs — attach to any GameObjectpublic class GameEventListener : MonoBehaviour{ [SerializeField] GameEvent gameEvent; [SerializeField] UnityEvent response; void OnEnable() => gameEvent.Register(this); void OnDisable() => gameEvent.Unregister(this); public void OnEventRaised() => response.Invoke();}// Usage:// 1. Create GameEvent asset: "OnPlayerDied"// 2. Raise from any script: onPlayerDied.Raise()// 3. Add GameEventListener to UI, sound, etc. — wire up in Inspector
Runtime Sets
// Track all active enemies without singletons or FindObjectsOfType[CreateAssetMenu(menuName = "Game/EnemySet")]public class EnemySet : ScriptableObject{ public List<Enemy> Items = new(); public void Add(Enemy e) => Items.Add(e); public void Remove(Enemy e) => Items.Remove(e); public int Count => Items.Count;}// Enemy.cs[SerializeField] EnemySet enemySet;void OnEnable() => enemySet.Add(this);void OnDisable() => enemySet.Remove(this);// WaveManager.cs[SerializeField] EnemySet enemySet;void Update() { if (enemySet.Count == 0) StartNextWave();}
Performance Patterns
Avoiding GC Allocations
// BAD — allocates every framevoid Update() { var enemies = FindObjectsOfType<Enemy>(); // allocation string msg = "Score: " + score; // string concat = allocation GetComponents<Collider>(); // allocation}// GOOD — cache referencesEnemy[] enemies;StringBuilder sb = new StringBuilder();Collider[] colliders = new Collider[10]; // pre-allocated buffervoid Awake() { enemies = FindObjectsOfType<Enemy>(); // once}void Update() { sb.Clear(); sb.Append("Score: "); sb.Append(score); scoreText.text = sb.ToString(); // Non-allocating overlap int count = Physics.OverlapSphereNonAlloc(pos, radius, colliders); for (int i = 0; i < count; i++) { /* use colliders[i] */ }}
Update Optimization
// BAD — expensive check every framevoid Update() { if (Vector3.Distance(transform.position, player.position) < 10f) Chase();}// GOOD — use sqrMagnitude (no sqrt)void Update() { if ((transform.position - player.position).sqrMagnitude < 100f) // 10*10 Chase();}// GOOD — throttle expensive checksfloat checkInterval = 0.2f;float nextCheck;void Update() { if (Time.time >= nextCheck) { nextCheck = Time.time + checkInterval; UpdatePathfinding(); }}// GOOD — use InvokeRepeating for periodic tasksvoid Start() { InvokeRepeating(nameof(SpawnEnemy), 2f, 5f); // start after 2s, repeat every 5s}void OnDisable() => CancelInvoke();
Object Pooling (Simple)
public class SimplePool<T> where T : Component{ readonly Queue<T> pool = new(); readonly T prefab; readonly Transform parent; public SimplePool(T prefab, int preload, Transform parent = null) { this.prefab = prefab; this.parent = parent; for (int i = 0; i < preload; i++) { var obj = Object.Instantiate(prefab, parent); obj.gameObject.SetActive(false); pool.Enqueue(obj); } } public T Get(Vector3 pos, Quaternion rot) { T obj = pool.Count > 0 ? pool.Dequeue() : Object.Instantiate(prefab, parent); obj.transform.SetPositionAndRotation(pos, rot); obj.gameObject.SetActive(true); return obj; } public void Return(T obj) { obj.gameObject.SetActive(false); pool.Enqueue(obj); }}
Common Unity Patterns
State Machine
public class EnemyStateMachine : MonoBehaviour{ enum State { Idle, Patrol, Chase, Attack, Dead } 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; } } void ChangeState(State newState) { OnExitState(currentState); currentState = newState; OnEnterState(currentState); } void OnEnterState(State s) { switch (s) { case State.Chase: agent.speed = runSpeed; break; case State.Attack: StartCoroutine(AttackRoutine()); break; } } void OnExitState(State s) { switch (s) { case State.Attack: StopAllCoroutines(); break; } } void UpdateIdle() { if (PlayerInRange(detectionRange)) ChangeState(State.Chase); } void UpdateChase() { agent.SetDestination(player.position); if (PlayerInRange(attackRange)) ChangeState(State.Attack); if (!PlayerInRange(detectionRange)) ChangeState(State.Patrol); } bool PlayerInRange(float range) => (player.position - transform.position).sqrMagnitude < range * range;}
Observer Pattern with Events
// Central event bus — no direct references neededpublic static class EventBus{ public static event Action<int> OnScoreChanged; public static event Action<float> OnHealthChanged; public static event Action OnPlayerDied; public static event Action<int> OnLevelCompleted; public static void ScoreChanged(int score) => OnScoreChanged?.Invoke(score); public static void HealthChanged(float hp) => OnHealthChanged?.Invoke(hp); public static void PlayerDied() => OnPlayerDied?.Invoke(); public static void LevelCompleted(int level) => OnLevelCompleted?.Invoke(level);}// Publisherpublic class Player : MonoBehaviour { void TakeDamage(float amount) { health -= amount; EventBus.HealthChanged(health); if (health <= 0) EventBus.PlayerDied(); }}// Subscriberpublic class HUD : MonoBehaviour { void OnEnable() => EventBus.OnHealthChanged += UpdateHealthBar; void OnDisable() => EventBus.OnHealthChanged -= UpdateHealthBar; void UpdateHealthBar(float hp) => healthBar.value = hp / maxHp;}
More Learn
CSharp - Full C# language reference (LINQ, async/await, generics, OOP)