Understanding when Unity calls each lifecycle method is fundamental to writing bug-free scripts. Timing issues are one of the most common sources of bugs in Unity projects — a reference that's null because Start hasn't fired yet, physics that stutter because you used Update instead of FixedUpdate, or a camera that jitters because it runs before the player moves.
This guide walks through the complete Unity execution order, from the moment a scene loads to the frame your object is destroyed. Bookmark it — you'll come back to it more often than you think.
The Full Lifecycle at a Glance
Here's the order Unity calls lifecycle methods on every MonoBehaviour. Methods in the same phase run in the order determined by Unity's Script Execution Order settings (Edit → Project Settings → Script Execution Order).
- Awake() — Called once when the script instance is loaded, even if the component is disabled. Use for self-initialization.
- OnEnable() — Called each time the object is enabled. Runs after Awake on first load, and again every time you call
gameObject.SetActive(true)or enable the component. - Start() — Called once, on the frame the script is first enabled, after all Awake calls have finished. Use for cross-references to other objects.
- FixedUpdate() — Called at a fixed interval (default 0.02s). Use for physics calculations and Rigidbody manipulation.
- Update() — Called once per frame. Use for input, non-physics movement, timers, and game logic.
- LateUpdate() — Called once per frame, after all Update methods have run. Use for cameras and anything that must follow another object's movement.
- OnDisable() — Called when the object or component is disabled. Pair with OnEnable for event subscriptions.
- OnDestroy() — Called when the object is destroyed or the scene is unloaded. Use for final cleanup.
The Initialization Phase: Awake, OnEnable, Start
When a scene loads, Unity processes all active GameObjects in two waves. First, Awake is called on every active script. This is your chance to grab your own components — GetComponent calls, internal state setup, and anything that doesn't depend on other objects existing yet. Awake fires even if the MonoBehaviour component is disabled (as long as the GameObject itself is active), which makes it reliable for setting up references.
After all Awake calls finish, OnEnable fires on every enabled script. This is the right place to subscribe to events, register with managers, or start coroutines that should run whenever the object is active. Finally, Start is called — but only on the first frame the script is enabled. By this point, every object in the scene has been Awake'd, so cross-references are safe.
using UnityEngine;
public class LifecycleDemo : MonoBehaviour
{
private Rigidbody2D rb;
private PlayerController player;
private void Awake()
{
// Phase 1: Self-initialization
rb = GetComponent<Rigidbody2D>();
Debug.Log($"{name}: Awake — got my own Rigidbody2D");
}
private void OnEnable()
{
// Phase 2: Subscribe to events
GameManager.OnScoreChanged += HandleScoreChanged;
Debug.Log($"{name}: OnEnable — subscribed to events");
}
private void Start()
{
// Phase 3: Cross-references (all objects are Awake'd)
player = FindAnyObjectByType<PlayerController>();
Debug.Log($"{name}: Start — found player: {player.name}");
}
private void OnDisable()
{
GameManager.OnScoreChanged -= HandleScoreChanged;
Debug.Log($"{name}: OnDisable — unsubscribed from events");
}
private void OnDestroy()
{
Debug.Log($"{name}: OnDestroy — final cleanup");
}
private void HandleScoreChanged(int score) { }
}Key rule: Use Awake for "me" initialization (GetComponent, field defaults). Use Start for "others" initialization (FindObjectOfType, cross-references). If you mix these up, you'll get null reference exceptions that only appear sometimes — depending on which script runs first.
The Game Loop: FixedUpdate, Update, LateUpdate
FixedUpdate runs at a fixed timestep (default 50 times per second, configurable in Edit → Project Settings → Time). It can run zero, one, or multiple times per frame depending on how much real time has elapsed. All physics calculations — Rigidbody.AddForce, Rigidbody.MovePosition, velocity changes — belong here. Using Update for physics causes inconsistent behavior because frame rates vary.
Update runs once per rendered frame. This is where you read input (Input.GetKeyDown, new Input System callbacks), update timers, run non-physics movement (transform-based), and execute general game logic. Use Time.deltaTime to make frame-rate-independent calculations.
LateUpdate also runs once per frame, but after every Update has finished. This is critical for cameras: if your camera follows the player in Update, and the player also moves in Update, the camera might run before or after the player depending on script execution order. Moving the camera to LateUpdate guarantees the player has already moved.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 10f;
private Rigidbody2D rb;
private float horizontalInput;
private bool jumpRequested;
private void Awake() => rb = GetComponent<Rigidbody2D>();
private void Update()
{
// Read input in Update (runs every frame)
horizontalInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
jumpRequested = true;
}
private void FixedUpdate()
{
// Apply physics in FixedUpdate (fixed timestep)
rb.linearVelocity = new Vector2(horizontalInput * moveSpeed, rb.linearVelocity.y);
if (jumpRequested)
{
rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
jumpRequested = false;
}
}
}
public class CameraFollow : MonoBehaviour
{
[SerializeField] private Transform target;
[SerializeField] private float smoothSpeed = 5f;
[SerializeField] private Vector3 offset = new Vector3(0, 2, -10);
private void LateUpdate()
{
// Camera follows in LateUpdate (after player has moved)
Vector3 desired = target.position + offset;
transform.position = Vector3.Lerp(transform.position, desired, smoothSpeed * Time.deltaTime);
}
}Physics Lifecycle: Collisions and Triggers
Physics callbacks fire during the physics step, between FixedUpdate calls. Unity's physics engine (PhysX for 3D, Box2D for 2D) processes all rigidbody movement, then detects collisions and triggers, then calls your callbacks. The order within the physics step is:
FixedUpdate()— your physics logic runs- Internal physics simulation — Unity moves rigidbodies, resolves collisions
OnTriggerEnter / OnTriggerStay / OnTriggerExit— trigger callbacks fireOnCollisionEnter / OnCollisionStay / OnCollisionExit— collision callbacks fire- Next
FixedUpdate()begins (if there's time budget remaining)
This means that if you move a Rigidbody in FixedUpdate, the collision callbacks from that movement fire after that same FixedUpdate — not in the next one. If you need to react to a collision and adjust physics in the same step, do it in the collision callback itself or set a flag and handle it in the next FixedUpdate.
using UnityEngine;
public class DamageZone : MonoBehaviour
{
[SerializeField] private int damage = 10;
[SerializeField] private float knockbackForce = 5f;
// Trigger: object entered our trigger collider (isTrigger = true)
private void OnTriggerEnter2D(Collider2D other)
{
if (other.TryGetComponent<HealthComponent>(out var health))
{
health.TakeDamage(damage);
Debug.Log($"{other.name} entered damage zone — took {damage} damage");
}
}
// Collision: solid physics collision (isTrigger = false on both)
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.rigidbody != null)
{
Vector2 knockDir = (collision.transform.position - transform.position).normalized;
collision.rigidbody.AddForce(knockDir * knockbackForce, ForceMode2D.Impulse);
}
}
}Coroutine Timing
Coroutines don't have their own slot in the lifecycle — they piggyback on existing phases. Understanding exactly when each yield instruction resumes is essential for writing correct coroutines:
yield return null— resumes after the nextUpdate(), beforeLateUpdate()yield return new WaitForFixedUpdate()— resumes after the nextFixedUpdate()yield return new WaitForEndOfFrame()— resumes after rendering is complete (useful for screenshots)yield return new WaitForSeconds(t)— resumes afterUpdate()once the timer expires (affected byTime.timeScale)yield return new WaitForSecondsRealtime(t)— same as above but ignoresTime.timeScale(useful for pause menus)yield return new WaitUntil(() => condition)— resumes afterUpdate()once the condition is true
A common mistake is assuming yield return null waits one physics frame — it doesn't. It waits one rendered frame. If you're writing a coroutine that manipulates physics, use WaitForFixedUpdate to stay in sync with the physics timeline.
using System.Collections;
using UnityEngine;
public class CoroutineTimingDemo : MonoBehaviour
{
private void Start()
{
StartCoroutine(SpawnWaves());
StartCoroutine(PhysicsSequence());
}
// Runs in Update timing — good for gameplay sequences
private IEnumerator SpawnWaves()
{
for (int wave = 1; wave <= 5; wave++)
{
Debug.Log($"Wave {wave} starting!");
SpawnEnemies(wave * 3);
// Wait 10 seconds between waves (pauses if Time.timeScale = 0)
yield return new WaitForSeconds(10f);
}
Debug.Log("All waves complete!");
}
// Runs in FixedUpdate timing — good for physics sequences
private IEnumerator PhysicsSequence()
{
Rigidbody2D rb = GetComponent<Rigidbody2D>();
// Apply force over 10 fixed frames
for (int i = 0; i < 10; i++)
{
rb.AddForce(Vector2.up * 5f, ForceMode2D.Force);
yield return new WaitForFixedUpdate();
}
}
private void SpawnEnemies(int count) { /* ... */ }
}Common Mistakes and How to Fix Them
Mistake 1: Using Start When You Need Awake
If script A's Start tries to access something that script B initializes in its Start, you have a race condition. The fix: move B's initialization to Awake. Reserve Start for cross-references that depend on other objects being initialized — and make sure those objects do their setup in Awake.
Mistake 2: Physics in Update
Moving a Rigidbody in Update causes jittery, inconsistent behavior because Update runs at a variable frame rate. Always use FixedUpdate for AddForce, MovePosition, and velocity changes. Read input in Update (so you don't miss key presses) and store it in a variable, then apply it in FixedUpdate.
Mistake 3: Camera in Update Instead of LateUpdate
If both the player and the camera move in Update, the camera might run before the player, causing a one-frame lag that appears as jitter. Always put camera follow logic in LateUpdate.
Mistake 4: Not Unsubscribing from Events
If you subscribe to an event in OnEnable but forget to unsubscribe in OnDisable, destroyed objects will still receive callbacks — causing MissingReferenceException. Always pair subscriptions: subscribe in OnEnable, unsubscribe in OnDisable.
Mistake 5: Assuming Awake Order Is Deterministic
Unity does not guarantee the order Awake is called across different scripts (unless you set it explicitly in Script Execution Order). If script A's Awake depends on script B's Awake having already run, you're relying on undefined behavior. Use Script Execution Order settings or move the dependent logic to Start.
Execution Order Cheat Sheet
// === INITIALIZATION (Scene Load) ===
// Awake() — self-init, GetComponent, field setup
// OnEnable() — subscribe to events, register with managers
// Start() — cross-references, FindObjectOfType
// === PHYSICS LOOP (Fixed Timestep, may run 0-N times per frame) ===
// FixedUpdate() — physics input, AddForce, MovePosition
// [Physics Sim] — internal collision detection
// OnTrigger/Collision callbacks
// yield WaitForFixedUpdate resumes here
// === GAME LOOP (Once Per Frame) ===
// Update() — input, timers, non-physics logic
// yield return null resumes here
// LateUpdate() — camera follow, post-processing
// === RENDERING ===
// OnBecameVisible / OnBecameInvisible
// OnRenderObject / OnGUI
// yield WaitForEndOfFrame resumes here
// === TEARDOWN ===
// OnDisable() — unsubscribe events, deregister
// OnDestroy() — final cleanup, release resourcesKeep this cheat sheet handy. When you encounter a timing bug — a null reference, jittery physics, a missed input — the answer is almost always in the execution order. Check which phase your code runs in and whether it's the right one for what you're doing.