Scripts and the MonoBehaviour Lifecycle
How Unity calls your code — Awake, Start, Update, and everything in between.
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).
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 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:
Awake()— when the object is instantiated in the scene, before any other objects load and before subscriptions. This is usually whereGetComponent<>goes.OnEnable()— each time the component is enabled (or the object becomes active). This is where you subscribe to events.Start()— once before the firstUpdate, but only if the component is active. A good place for initialization that depends on other objects.FixedUpdate()— a fixed step (by default every 0.02 s = 50 Hz). This is for physics andRigidbody.AddForce.Update()— every frame. This is for input and logic not tied to physics.LateUpdate()— every frame, after allUpdatecalls. This is for a follow camera and final position adjustments.OnDisable()— when the component is disabled. This is where you unsubscribe from events.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() { /* ... */ }
}
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:
| Method | Frequency | What goes there |
|---|---|---|
FixedUpdate | Fixed (50 Hz by default) | Physics: AddForce, MovePosition for a Rigidbody |
Update | Every frame | Input, AI, non-physics logic |
LateUpdate | Every frame, after Update | A 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.
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 framenew WaitForSeconds(1f)— a pause in “game seconds” (respectsTime.timeScale)new WaitForSecondsRealtime(1f)— a pause in real seconds (ignores timescale)new WaitUntil(() => isReady)— wait for a conditionnew WaitForEndOfFrame()— after the frame is rendered (for screenshots)
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.
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.