Skip to main content

📝 Lesson 1.2: The Event Function Lifecycle

In Lesson 1.1 you learned that Unity calls into your MonoBehaviour rather than you calling it. This lesson is the map of when: the event functions — Awake, OnEnable, Start, Update, FixedUpdate, LateUpdate, OnDisable, OnDestroy — and the precise order Unity fires them.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Name the core event functions and the order Unity invokes them in
  • Choose correctly between Awake and Start for initialization
  • Explain why physics goes in FixedUpdate and per-frame logic in Update
  • Use LateUpdate for camera follow and other "after everyone moved" work
  • Understand OnEnable/OnDisable/OnDestroy for setup and cleanup
  • Recognize that these functions are called by signature, not by an interface or override

Estimated Time: 60 minutes

Project: Instrument a script that logs every lifecycle event, then reason about the exact console order it produces.

In This Lesson

Event Functions, Not Main

A console program has one timeline you control. A Unity game has a player loop that the engine runs many times per second, and at well-defined moments it calls the methods you've defined on your components. Those methods are the event functions (Unity also calls them "messages").

📖 Definition

An event function is a specially-named method (like Update or Start) that Unity calls automatically at a specific point in a component's life. You don't call these yourself and you don't register them — you just define a method with the right name and signature, and the engine finds it.

You already met two of them in Lesson 1.1: Start (for setup) and Update (for per-frame logic). There are more, and knowing which runs when is the difference between code that works and code that mysteriously reads a value before it was set.

💡 The one-line summary: Awake/OnEnable/Start run once at birth, Update/FixedUpdate/LateUpdate run every frame or physics step, and OnDisable/OnDestroy run at death. Get those three phases straight and the rest is detail.

The Lifecycle Order

Here is the sequence for a single component, from the moment it's created to the moment it's destroyed. Unity guarantees this ordering within one component:

graph TD A["Awake()
once — object created"] --> B["OnEnable()
each time it becomes enabled"] B --> C["Start()
once — before first Update"] C --> D{"Per-frame loop
(repeats every frame)"} D --> E["FixedUpdate()
0, 1, or many times
(physics step)"] E --> F["Update()
once per frame"] F --> G["LateUpdate()
once per frame, after all Updates"] G --> D D -->|"disabled or destroyed"| H["OnDisable()"] H --> I["OnDestroy()
once — object destroyed"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style F fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style I fill:#fdecea,stroke:#c0392b,stroke-width:2px

Two subtleties this diagram captures that trip people up:

  • FixedUpdate can run zero, one, or several times per rendered frame — it's tied to the physics clock, not the frame rate.
  • LateUpdate always runs after every component's Update that frame — that's what makes it perfect for "react to where things ended up."

⚠️ Watch Out: order across objects isn't guaranteed

Unity guarantees the order of phases within a component, but not the order in which the same function runs across different GameObjects. Object A's Update may run before or after Object B's Update. Never write code that depends on "my Update runs before that other object's Update" — use the initialization split (next section) or explicit references instead. (You can force it via Project Settings → Script Execution Order, but treat that as a last resort.)

Awake vs OnEnable vs Start

All three run near the beginning, but at different moments and for different jobs. Using the wrong one causes the classic "null on the first frame" bug.

FunctionWhenUse it for
Awake()Once, when the object is created — before any Start, even if the component is disabled.Setting up this object: caching your own components, initializing internal state. No dependence on other objects being ready.
OnEnable()Every time the component becomes enabled/active (so it can run many times over a life).Subscribing to events, resetting on re-activation (pooled objects). Pairs with OnDisable.
Start()Once, just before the first Update — after every object's Awake has run.Logic that depends on other objects already being initialized.

The key insight is the two-phase startup: Unity runs all Awake calls first, then all Start calls. So anything you set up in Awake is safe to read from any Start. That's how you dodge the cross-object ordering problem.

using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody body;   // a reference to our own physics component

    void Awake()
    {
        // Set up MYSELF here. This is safe — it doesn't need other objects.
        body = GetComponent<Rigidbody>();
    }

    void Start()
    {
        // By now every other object's Awake has run, so cross-object lookups are safe.
        GameManager.Instance.RegisterPlayer(this);
    }
}

✅ Simple rule

Set up yourself in Awake. Talk to others in Start. Subscribe/unsubscribe to events in OnEnable/OnDisable.

⚠️ Important: Remember from Lesson 1.1 — put initialization here, not in a constructor. Unity constructs your component itself, before the GameObject is wired up, so a constructor runs too early and on the wrong thread for most Unity APIs.

Update, FixedUpdate & LateUpdate

These three are the beating heart of gameplay — they run over and over. Choosing the right one is mostly about the clock each one follows.

Update() — once per rendered frame

Runs once every frame, so its rate rises and falls with performance (60 fps → 60 calls/sec; 30 fps → 30). Use it for input polling, timers, non-physics movement, and general game logic. Because frame time varies, multiply movement by Time.deltaTime to stay frame-rate independent:

void Update()
{
    // Move 5 units per SECOND regardless of frame rate.
    transform.Translate(0f, 0f, 5f * Time.deltaTime);
}

FixedUpdate() — on the physics clock

Runs on a fixed timestep (0.02s / 50 times per second by default), decoupled from the frame rate. This is where all physics code belongs — anything touching a Rigidbody (forces, velocity). Because its interval is constant, use Time.fixedDeltaTime here (or just rely on the physics engine's own integration):

private Rigidbody body;

void Awake() => body = GetComponent<Rigidbody>();

void FixedUpdate()
{
    // Apply physics forces here, NOT in Update.
    body.AddForce(Vector3.forward * 10f);
}

⚠️ Watch Out: read input in Update, apply it in FixedUpdate

Input events (like a key-down) can be missed if you poll them in FixedUpdate, because it doesn't run every frame. Read input in Update, store it in a field, and use that stored value inside FixedUpdate to drive physics.

LateUpdate() — after all Updates

Runs once per frame too, but guaranteed after every Update has finished. The textbook use is a camera that follows the player: you want the camera to reposition only after the player has already moved this frame, otherwise it lags or jitters.

public Transform target;   // the player, assigned in the Inspector

void LateUpdate()
{
    // The player already moved in its Update; now we follow.
    transform.position = target.position + new Vector3(0f, 5f, -10f);
}
graph LR I["Input & game logic
➜ Update()"] --> P["Physics & forces
➜ FixedUpdate()"] P --> L["Follow / cleanup after movement
➜ LateUpdate()"] style I fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style P fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style L fill:#f3e8ff,stroke:#8b5cf6,stroke-width:2px

💡 deltaTime vs fixedDeltaTime

Time.deltaTime = seconds since the last frame (varies) — use in Update/LateUpdate. Time.fixedDeltaTime = the fixed physics step (constant) — use in FixedUpdate. Handy fact: Time.deltaTime automatically returns fixedDeltaTime when read inside FixedUpdate, so * Time.deltaTime is safe in both.

OnDisable & OnDestroy

Every setup has a matching teardown. Skipping cleanup is a top source of Unity bugs — dangling event subscriptions that fire on dead objects, or "leaked" listeners.

FunctionWhenUse it for
OnDisable()Whenever the component is disabled or its GameObject deactivated (can happen many times).Undo what OnEnable did — unsubscribe from events, stop timers.
OnDestroy()Once, when the component/GameObject is destroyed (or the scene unloads).Final cleanup — release resources, save state, unregister from managers.

The most important pattern in the whole lesson: subscribe in OnEnable, unsubscribe in OnDisable. They're mirror images, so subscriptions never leak:

public class ScoreListener : MonoBehaviour
{
    void OnEnable()
    {
        GameEvents.ScoreChanged += HandleScoreChanged;   // subscribe
    }

    void OnDisable()
    {
        GameEvents.ScoreChanged -= HandleScoreChanged;   // ALWAYS undo it
    }

    void HandleScoreChanged(int newScore) => Debug.Log($"Score: {newScore}");
}
💡 Note on order: when an object is destroyed, Unity calls OnDisable first, then OnDestroy. So the unsubscribe in OnDisable also covers the destroy case — you rarely need to repeat it in OnDestroy. (We go deep on C# events and this pattern in Lesson 3.2.)

Why No override?

A sharp C# programmer notices something odd: you write void Update() with no override, no interface, and often private. How does Unity call a private method you never registered?

Unity finds these methods by name and signature when your script first runs, using a fast internal lookup (not ordinary reflection every frame). If a method with the right name exists, Unity wires it into the loop; if not, it simply isn't called. This is why:

  • An empty Update() still has a small cost — Unity is invoking it every frame. Delete event functions you don't use.
  • A typo silently does nothing: void Updaet() or void update() compiles fine and is simply never called. Case and spelling must be exact.
  • Signatures matter: Update() takes no parameters; OnCollisionEnter(Collision c) takes exactly one of the right type.

⚠️ The #1 beginner bug in this lesson

Your movement code "does nothing" and there's no error. Nine times out of ten it's a misspelled or mis-cased event function (Start vs start, Update vs Updte). The compiler can't help — the name just doesn't match, so Unity never calls it.

✅ Pro Tip

Let the editor generate these for you. In Visual Studio / Rider, typing the method name offers an autocomplete stub with the correct signature — the safest way to avoid typos. And keep Update lean: heavy work every frame is the most common Unity performance mistake (Lesson 5.1).

Exercise & Quiz

🏋️ Exercise: The Lifecycle Logger

Objective: See the lifecycle with your own eyes by logging every event, then predict the console output before you run it.

Instructions:

  1. Create a script LifecycleLogger.cs and attach it to any GameObject (e.g. an empty one named Probe).
  2. Implement Awake, OnEnable, Start, Update, OnDisable, and OnDestroy, each logging its own name with Debug.Log.
  3. Guard Update so it only logs the first few frames (otherwise it floods the console).
  4. Press Play, watch the Console, then stop Play mode. Predict the order first, then check.
  5. Bonus: toggle the component's checkbox off and on in the Inspector during Play — which functions fire?

Starter Code:

using UnityEngine;

public class LifecycleLogger : MonoBehaviour
{
    void Awake()  { Debug.Log("1. Awake"); }
    // TODO: add OnEnable, Start, Update (first 3 frames only), OnDisable, OnDestroy
}
💡 Hint

Keep a frame counter field and only log inside Update while it's below 3, so you can read the sequence. On stopping Play mode, Unity disables then destroys everything — so you'll see OnDisable before OnDestroy at the end.

✅ Solution
using UnityEngine;

public class LifecycleLogger : MonoBehaviour
{
    private int frame;

    void Awake()     { Debug.Log("1. Awake"); }
    void OnEnable()  { Debug.Log("2. OnEnable"); }
    void Start()     { Debug.Log("3. Start"); }

    void Update()
    {
        if (frame < 3)
        {
            Debug.Log($"4. Update (frame {frame})");
            frame++;
        }
    }

    void OnDisable() { Debug.Log("5. OnDisable"); }
    void OnDestroy() { Debug.Log("6. OnDestroy"); }
}

Expected order on Play then Stop: Awake → OnEnable → Start → Update(0) → Update(1) → Update(2) → … → (on stop) OnDisable → OnDestroy. Toggling the checkbox off fires OnDisable; toggling it back on fires OnEnable again — but not Awake or Start, which only ever run once.

🎯 Quick Quiz

Question 1: You need to look up another GameObject that must already be initialized. Where should that lookup go?

Question 2: Where should you apply a force to a Rigidbody?

Question 3: Your Update method never runs and there's no compile error. What's the most likely cause?

Summary

🎉 Key Takeaways

  • Unity calls event functions at defined moments: AwakeOnEnableStart (birth), FixedUpdate/Update/LateUpdate (loop), OnDisableOnDestroy (death).
  • Two-phase startup: all Awakes run before any Start. Set up yourself in Awake, reference others in Start.
  • Update = per frame (use Time.deltaTime); FixedUpdate = physics on a fixed step (Rigidbody forces); LateUpdate = after all Updates (camera follow).
  • Subscribe in OnEnable, unsubscribe in OnDisable — mirror images so nothing leaks.
  • Event functions are matched by name and signature, not override/interfaces — so a typo silently does nothing, and unused event functions still cost a little.
  • Order is guaranteed within a component, not across different GameObjects.

📚 Additional Resources

🚀 What's Next?

You can now write components that come alive at the right moments. But real behavior needs objects to find and talk to each other. In Lesson 1.3 we cover accessing GameObjects and components: GetComponent, references, and the [SerializeField] attribute for wiring things up in the Inspector.

🎉 You've mastered the game loop!

Knowing exactly when your code runs is what separates "it works by luck" from "it works by design." Great progress.