~3 min read

Scripts and the MonoBehaviour Lifecycle

How Unity calls your code — Awake, Start, Update, and everything in between.

Иллюстрация: Scripts and the MonoBehaviour Lifecycle

MonoBehaviour — the base class of your scripts

Any C# script of yours that is attached to a GameObject as a component inherits from MonoBehaviour. This gives Unity entry points — methods with special names that the engine calls itself at the right moment.

using UnityEngine;

public class Hello : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    private void Start() {
        Debug.Log("Hello from Unity");
    }

    private void Update() {
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

The [SerializeField] attribute makes a private field visible in the Inspector — it’s the idiomatic way to expose parameters. Public fields are also serialized by default, but that’s considered bad form (it breaks encapsulation).

Web

A React component: useEffect(() => {}, []) is the analog of Start, while useEffect(() => {}) without dependencies is something like Update — except in React the render function is called reactively, not every frame.

Unity

Unity calls Update() every frame and Start() exactly once before the first Update. A component’s fields are its state, like useState.

The lifecycle — the order of calls

The full MonoBehaviour lifecycle is more involved, but here are the main methods, sorted by call order:

  1. Awake() — when the object is instantiated in the scene, before any other objects load and before subscriptions. This is usually where GetComponent<> goes.
  2. OnEnable() — each time the component is enabled (or the object becomes active). This is where you subscribe to events.
  3. Start() — once before the first Update, but only if the component is active. A good place for initialization that depends on other objects.
  4. FixedUpdate() — a fixed step (by default every 0.02 s = 50 Hz). This is for physics and Rigidbody.AddForce.
  5. Update() — every frame. This is for input and logic not tied to physics.
  6. LateUpdate() — every frame, after all Update calls. This is for a follow camera and final position adjustments.
  7. OnDisable() — when the component is disabled. This is where you unsubscribe from events.
  8. OnDestroy() — when the GameObject is destroyed (Destroy(obj)) or the scene is unloaded.
public class LifecycleExample : MonoBehaviour
{
    private Rigidbody _rb;

    private void Awake()        { _rb = GetComponent<Rigidbody>(); }
    private void OnEnable()     { Health.OnDeath += HandleDeath; }
    private void Start()        { /* initialization that depends on other objects */ }
    private void FixedUpdate()  { _rb.AddForce(Vector3.up); }
    private void Update()       { /* read input, logic */ }
    private void LateUpdate()   { /* move the camera to follow the player */ }
    private void OnDisable()    { Health.OnDeath -= HandleDeath; }
    private void OnDestroy()    { /* final cleanup */ }

    private void HandleDeath() { /* ... */ }
}
The OnEnable / OnDisable pairing

Subscribing to events in OnEnable and unsubscribing in OnDisable is standard practice. It survives enabling/disabling the object without leaks and without double subscriptions.

Update vs FixedUpdate vs LateUpdate

This is a three-stage cycle, and understanding the difference is critical:

MethodFrequencyWhat goes there
FixedUpdateFixed (50 Hz by default)Physics: AddForce, MovePosition for a Rigidbody
UpdateEvery frameInput, AI, non-physics logic
LateUpdateEvery frame, after UpdateA follow camera, “catching up” with the frame’s changes

Time.deltaTime in Update is the time since the last frame (variable). Time.fixedDeltaTime in FixedUpdate is the fixed step.

Never move a Rigidbody in Update

If an object has a Rigidbody (non-kinematic), changing transform.position or Rotate() directly every frame is a path to collision bugs and jitter. Use Rigidbody.MovePosition or physics forces in FixedUpdate.

GetComponent: obtaining other components

private Rigidbody _rb;
private Animator _animator;

private void Awake() {
    _rb = GetComponent<Rigidbody>();
    _animator = GetComponentInChildren<Animator>(); // search in children
}

Don’t call GetComponent in Update — it’s a lookup through the component list, an expensive operation. Cache it in Awake/Start.

Unity 6 has a convenient alternative — the [RequireComponent(typeof(Rigidbody))] attribute on a class, which guarantees the required component is present, and a lookup via TryGetComponent for cases where the component may not exist.

Linking to the scene: references in the Inspector

The most convenient way to connect two objects is a field in the Inspector:

public class Enemy : MonoBehaviour
{
    [SerializeField] private Transform target; // drag Player into the Inspector
    [SerializeField] private float speed = 3f;

    private void Update() {
        var dir = (target.position - transform.position).normalized;
        transform.position += dir * speed * Time.deltaTime;
    }
}

Alternatives are scene lookups (FindAnyObjectByType<Player>(), GameObject.FindWithTag("Player")). The latter are slower and more fragile, but handy for a prototype.

Coroutines: “coroutines” instead of async/await (often)

A coroutine is an IEnumerator that returns control to the engine via yield return. It’s a convenient way to spread logic across several frames without spawning your own timers.

private IEnumerator FadeOut(float duration) {
    float t = 0;
    var renderer = GetComponent<Renderer>();
    var color = renderer.material.color;
    while (t < duration) {
        t += Time.deltaTime;
        color.a = Mathf.Lerp(1f, 0f, t / duration);
        renderer.material.color = color;
        yield return null; // wait for the next frame
    }
    Destroy(gameObject);
}

// Start:
StartCoroutine(FadeOut(2f));

Useful yield return values:

  • null — the next frame
  • new WaitForSeconds(1f) — a pause in “game seconds” (respects Time.timeScale)
  • new WaitForSecondsRealtime(1f) — a pause in real seconds (ignores timescale)
  • new WaitUntil(() => isReady) — wait for a condition
  • new WaitForEndOfFrame() — after the frame is rendered (for screenshots)
Web

A chain of setTimeout/requestAnimationFrame with state. Or an RxJS Observable that emits the next value on each “tick.” Except the runtime doesn’t advance the steps itself — your code does.

Unity

StartCoroutine advances your IEnumerator every frame (or after the relevant yield). No threads — everything is on the main thread. The engine handles scheduling the steps.

Async/await: ever closer to the mainstream

Unity 6 supports async/await with the Awaitable type:

private async Awaitable LoadAndShow() {
    await Awaitable.WaitForSecondsAsync(1f);
    Debug.Log("A second has passed");
    await SceneManager.LoadSceneAsync("Level_02");
}

Awaitable is a zero-alloc alternative to Task for Unity. But almost all Unity APIs still work only on the main thread — call transform.position from an arbitrary thread and you’ll get an exception.

In the next section: input — the new Input System and the legacy Input class.