MonoBehaviour Patterns¶
Lifecycle Execution Order¶
Unity calls MonoBehaviour methods in a strict order each frame:
- Awake - Called once when the script instance loads. Use for self-initialization: cache own components, set default values. Called even if the component is disabled.
- OnEnable - Called when the component becomes active. Subscribe to events here.
- Start - Called once before the first Update, only if the component is enabled. Use for cross-object initialization: find other objects, resolve dependencies.
- FixedUpdate - Called on a fixed timestep (default 50Hz). Use exclusively for physics: apply forces, set velocities, move Rigidbodies.
- Update - Called once per frame. Use for input polling, non-physics movement, game logic.
- LateUpdate - Called after all Update calls complete. Use for camera follow, UI updates that depend on object positions.
- OnDisable - Called when the component is deactivated. Unsubscribe from events here.
- OnDestroy - Called when the GameObject is destroyed or the scene unloads. Final cleanup: unsubscribe from static events, release native resources.
Critical Rules¶
- NEVER put physics code (forces, velocity) in Update. Use FixedUpdate.
- NEVER put frame-dependent logic (input, animation) in FixedUpdate.
- NEVER reference other GameObjects in Awake. They may not exist yet. Use Start.
- NEVER call GetComponent in Update/FixedUpdate/LateUpdate. Cache in Awake.
Async Patterns with Awaitable¶
Unity 6 introduced the Awaitable class as the modern replacement for coroutines. Awaitable instances are pooled by the engine, producing near-zero garbage collection pressure.
Critical Rule: Single Await Only¶
An Awaitable instance must NEVER be awaited more than once. After the first await completes, the instance returns to the internal pool. A second await will cause undefined behavior.
// CORRECT: each call produces a fresh Awaitable
await Awaitable.NextFrameAsync();
await Awaitable.WaitForSecondsAsync(2f);
// WRONG: reusing an Awaitable reference
var wait = Awaitable.NextFrameAsync();
await wait;
await wait; // undefined behavior - instance already returned to pool
Common Awaitable Methods¶
| Method | Replaces |
|---|---|
Awaitable.NextFrameAsync() |
yield return null |
Awaitable.WaitForSecondsAsync(float) |
yield return new WaitForSeconds(float) |
Awaitable.EndOfFrameAsync() |
yield return new WaitForEndOfFrame() |
Awaitable.FixedUpdateAsync() |
yield return new WaitForFixedUpdate() |
Awaitable.FromAsyncOperation(op) |
yield return asyncOperation |
Example: Async Game Loop¶
private async void Start()
{
await SpawnWaveAsync();
await Awaitable.WaitForSecondsAsync(3f);
await SpawnWaveAsync();
}
private async Awaitable SpawnWaveAsync()
{
for (int i = 0; i < 5; i++)
{
SpawnEnemy();
await Awaitable.WaitForSecondsAsync(0.5f);
}
}
Cancellation with destroyCancellationToken¶
Every MonoBehaviour has a destroyCancellationToken that triggers when the object is destroyed:
private async void Start()
{
try
{
await Awaitable.WaitForSecondsAsync(10f, destroyCancellationToken);
}
catch (OperationCanceledException)
{
// Object was destroyed before the wait completed
}
}
Legacy Coroutines¶
Coroutines are still supported but are considered legacy for new code. Use them only when maintaining existing codebases.
private IEnumerator LegacySpawnWave()
{
for (int i = 0; i < 5; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(0.5f);
}
}
Common Design Patterns¶
Singleton¶
Use for managers that need exactly one instance (AudioManager, GameManager). See snippets/csharp/singleton-pattern.cs.
Component Caching¶
private Rigidbody _rb;
private Collider _col;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
_col = GetComponent<Collider>();
}
Tag Comparison¶
Always use CompareTag instead of string equality:
// CORRECT
if (other.CompareTag("Player")) { }
// WRONG - allocates a string
if (other.gameObject.tag == "Player") { }
TryGetComponent¶
Use when the component might not exist:
RequireComponent¶
Enforce component dependencies at the class level:
[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
// Rigidbody is guaranteed to exist
}
Null Checks for Unity Objects¶
Use the implicit bool operator, not C# null checks:
// CORRECT - uses Unity's lifetime check
if (myObject) { }
// AVOID - bypasses Unity's destroyed-object tracking
if (myObject != null) { }
FindFirstObjectByType (Not FindObjectOfType)¶
FindObjectOfType is deprecated. Use the modern replacement:
// CORRECT
var player = FindFirstObjectByType<PlayerController>();
// DEPRECATED
var player = FindObjectOfType<PlayerController>();
For finding multiple objects, use FindObjectsByType<T>(FindObjectsSortMode).