You already know C# on its own: classes, objects, generics, collections. Unity does not run plain C# classes as scripts — it runs classes that inherit from a special base class called MonoBehaviour. This chapter shows what that gives you: methods Unity calls for you automatically at exact moments (the "lifecycle"), fields that turn into editable boxes in the Editor, and the everyday tools almost every Unity script uses — spawning objects, reading input, and running code that plays out over several seconds instead of finishing in one line.
Every script attached to a GameObject (an object placed in a Unity scene) is a normal C# class — except it inherits from MonoBehaviour, a class Unity itself provides. Inheriting from it does two things: it lets you drag the script onto a GameObject in the Editor, and it gives your class a set of special methods Unity calls automatically, without you ever calling them yourself.
using UnityEngine;
public class Player : MonoBehaviour
{
// This class does nothing extra yet, but because it inherits
// from MonoBehaviour, Unity already treats it as a "live" script:
// you can drag this file onto any GameObject in the Hierarchy.
}
What happens: nothing prints to the Console yet, since the class body is empty. But in the Editor, this script now appears as a component you can attach: select a GameObject, drag Player.cs onto it in the Inspector panel, and Unity creates one instance of this class for that object. From here on, Unity is in charge of creating, running, and eventually destroying that instance — you never write new Player() yourself.
Compare this to a plain C# class you might have written before, like a small math helper or a linked-list node: you construct those yourself with new, and nothing calls their methods unless you call them. A MonoBehaviour is different. Once it is attached to a GameObject that exists in a running scene, Unity's engine loop calls specific methods on it automatically, at specific points every single frame. Those methods are the subject of the rest of this chapter.
Transform), and a list of components attached to it. A MonoBehaviour script is one kind of component. A single GameObject can hold many components at once — a Rigidbody for physics, a Collider for collision, your own script, and so on.Because Unity calls your methods for you, you need to know exactly which methods exist and when each one runs. Together these are called the "lifecycle" of a script — the order of events from the moment an object is created to the moment it is destroyed. Get the order wrong in your head and you will write bugs that only show up sometimes, like code that reads a value before another script has had a chance to set it up.
Every one of these is just a method name you write inside your own class. You do not have to implement all of them — Unity only calls the ones you actually define. A script with no Update method simply never receives an Update call; there is no cost to leaving one out.
The next few sections go through each of these methods one at a time, with runnable code for each.
These three methods all run once near the beginning of an object's life, but at slightly different moments, and mixing them up is a very common beginner bug.
Awake runs once, as early as possible — when the object is created (when the scene loads, or right when something calls Instantiate at runtime). It runs even if the script's own enabled checkbox is unticked in the Inspector. Use it for setup that only depends on the object itself: caching component references (section 9 covers this), setting default internal values.
OnEnable runs once right after Awake, and then again every single time the object becomes active after being turned off — for example if another script calls gameObject.SetActive(true) after it had been set to false. Use it for anything you want reset every time the object turns back on, such as subscribing to an event.
Start runs once, right before the object's first Update. The important difference from Awake: Unity finishes calling Awake on every object in the scene first, and only then starts calling Start on any of them. That means inside Start you can safely assume every other object's Awake has already run — useful when one script needs to read something another script set up in its own Awake.
using UnityEngine;
public class LifecycleLogger : MonoBehaviour
{
void Awake()
{
Debug.Log("Awake");
}
void OnEnable()
{
Debug.Log("OnEnable");
}
void Start()
{
Debug.Log("Start");
}
}
What happens: attach this script to any GameObject and press Play. The Console prints, in this exact order:
Awake
OnEnable
Start
This order — Awake, then OnEnable, then Start — never changes for a normal object that starts active in the scene. It is worth memorizing.
Awake — because a disabled GameObject is completely asleep. All three fire together the moment something calls SetActive(true) on it. This is different from the component's own enabled checkbox: with that, Awake still runs immediately, but OnEnable and Start wait until the component is enabled.Update is the method you will use the most. Unity calls it once per rendered frame — and frames do not take a fixed amount of time. A fast gaming PC might render 300 frames every second; a phone under heavy load might render only 20. Update runs once each time, no matter how long that frame took.
using UnityEngine;
public class Spinner : MonoBehaviour
{
void Update()
{
transform.Rotate(0f, 2f, 0f); // turn 2 degrees every call to Update
}
}
What happens: attach this to a cube and press Play — it spins. But watch closely: on a machine rendering 60 frames per second, Update is called 60 times every second, so the cube turns 60 x 2 = 120 degrees per second. On a slower machine rendering only 30 frames per second, the exact same code turns the cube just 30 x 2 = 60 degrees per second. The same script runs at a different visual speed depending on how fast the computer is. That is a bug — section 7 fixes it.
For now, remember what belongs in Update: reading input, gameplay logic, timers, anything that is not physics. Physics goes in the next method.
FixedUpdate differs from Update in one key way: it does not run once per rendered frame. It runs on its own fixed clock, by default every 0.02 seconds of game time — exactly 50 times per second — no matter how fast or slow the computer is actually rendering. Unity's physics engine (Rigidbody, colliders, forces) needs this kind of steady, predictable timestep to stay accurate and consistent, so anything that touches physics belongs here, not in Update.
using UnityEngine;
public class ConstantThrust : MonoBehaviour
{
public float thrust = 10f;
Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
rb.AddForce(Vector3.forward * thrust);
}
}
What happens: attach this to an object with a Rigidbody component and press Play — it accelerates forward smoothly. Because FixedUpdate runs on its own clock, it does not always line up one-to-one with rendered frames: at a steady 50 FPS it lines up almost exactly one call per frame; at 100 FPS, roughly every other frame has no FixedUpdate call at all; at 20 FPS, Unity calls FixedUpdate two or three times in a row to "catch up" before the next frame is drawn. This is exactly the "0, 1, or more times" from the diagram in section 2.
FixedUpdate runs from Project Settings, under Time, in the Fixed Timestep field. The default, 0.02 seconds, is a good balance between physics accuracy and CPU cost for most games.LateUpdate also runs once per frame, but always after every object's Update has already finished for that frame. Unity does not guarantee what order different scripts' Update methods run in relative to each other — your player-movement script and your camera script might run in either order on any given frame. LateUpdate exists to solve exactly that problem for things like a camera that must always react to where the player ended up, not where the player happened to be a moment ago.
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public Transform target;
public Vector3 offset = new Vector3(0f, 5f, -10f);
void LateUpdate()
{
transform.position = target.position + offset;
}
}
What happens: attach this to the Main Camera, drag the player's Transform into target, and press Play — the camera smoothly stays behind and above the player. If this code ran in Update instead, there would be frames where the camera happens to update before the player moves, showing the player's old position for one frame — a small but visible jitter, especially at low frame rates. Running it in LateUpdate guarantees the player has already finished moving this frame before the camera copies its position.
Section 4 showed the spinner bug: the exact same code turns an object at different real-world speeds depending on frame rate. The fix is Time.deltaTime — a number Unity hands you every frame equal to how many real seconds passed since the previous frame (a small number: about 0.0166 at 60 FPS, about 0.0333 at 30 FPS).
using UnityEngine;
public class Spinner : MonoBehaviour
{
public float degreesPerSecond = 120f;
void Update()
{
transform.Rotate(0f, degreesPerSecond * Time.deltaTime, 0f);
}
}
What happens: now the cube always turns 120 degrees every real second, no matter the frame rate.
The trick: a "fixed amount per call" becomes a "fixed amount per second" once you multiply it by however much of a second that particular call actually covers. A frame that took twice as long gets a deltaTime twice as big, so it moves the object twice as far to compensate — the total distance covered per real second stays the same.
This is the exact same idea you already used when interpolating between two values, blending from a start value to an end value with a fraction t between 0 and 1. There, t had to grow at a steady rate over real time, not over frame count, or the blend would speed up and slow down with the frame rate too. Time.deltaTime is how you build that steady growth: each frame you add a small slice of real elapsed time, t += Time.deltaTime / duration, instead of a fixed slice per call. Frame-rate independent movement and frame-rate independent interpolation are solved by the same rule: never assume how much time one "step" represents — always ask Time.deltaTime how much time actually passed.
FixedUpdate does not need this trick for physics forces, because its timestep is already fixed and predictable — that is the entire reason it exists. You will still see Time.fixedDeltaTime used inside FixedUpdate occasionally, but for a plain AddForce call like the one in section 5, Unity's physics engine already accounts for the fixed timestep for you.Unity automatically turns certain fields on a MonoBehaviour into editable boxes in the Inspector panel — no extra UI code required. There are two ways to do this:
using UnityEngine;
public class Health : MonoBehaviour
{
public int maxHealth = 100;
[SerializeField]
private int currentHealth = 100;
}
What happens: select a GameObject with this script attached, and the Inspector shows two editable fields, "Max Health" and "Current Health" (Unity turns the camelCase field name into a readable label automatically), both starting at 100, both editable by hand or by a designer without touching code.
The difference between the two shows up outside the Inspector, in other scripts:
public int maxHealth shows in the Inspector and any other script can read or overwrite it directly, e.g. otherScript.maxHealth = 9999; from anywhere.[SerializeField] private int currentHealth shows in the Inspector exactly the same way, but stays private — only code written inside the Health class itself can read or change currentHealth directly.[SerializeField] private. Only make a field public when other scripts genuinely need to read or write it directly — and even then, a public method (like TakeDamage(int amount)) is usually safer than a raw public field, because it lets you control exactly how the value is allowed to change.GetComponent<T>() searches a GameObject for a component of type T and returns it (or null if there is none). It is how one script reaches another component sitting on the same object — for example, a movement script reaching the object's Rigidbody.
using UnityEngine;
public class BadMover : MonoBehaviour
{
void Update()
{
// Do not do this: GetComponent searches all over again,
// every single frame, for a component that never changes.
GetComponent<Rigidbody>().AddForce(Vector3.up);
}
}
What happens: this works correctly, but GetComponent is not instant. It walks the list of components attached to the GameObject, comparing types until it finds a match. Doing that 60 or more times a second, for a result that is exactly the same every single time, wastes CPU time your game could spend on things that actually change frame to frame.
using UnityEngine;
public class GoodMover : MonoBehaviour
{
Rigidbody rb; // cached reference, found once
void Awake()
{
rb = GetComponent<Rigidbody>(); // search happens ONCE, here
}
void Update()
{
rb.AddForce(Vector3.up); // reuse the stored reference, no searching
}
}
What happens: identical behavior in the game, but the search only ever runs once, in Awake (which section 3 showed always runs before Update). Every later frame just reuses the field rb, which is basically free.
GetComponent inside Update is one of the most common beginner performance mistakes. It is harmless in a tiny test scene with one object, but the cost adds up fast once a scene has hundreds of objects all doing it every frame. Cache in Awake (or Start), and use the cached field everywhere else.Most games create and remove objects constantly while running — bullets, enemies, pickup items, explosion effects. Instantiate creates a new copy of an existing object at runtime; Destroy removes an object. Both work on a template called a prefab: a GameObject you set up once in the Editor (model, scripts, components, all configured) and save as a reusable asset in your Project window, so you can stamp out as many copies of it as you want at runtime.
using UnityEngine;
public class Gun : MonoBehaviour
{
public GameObject bulletPrefab; // drag the Bullet prefab here in the Inspector
public Transform muzzle; // an empty child Transform at the gun's tip
void Update()
{
if (Input.GetButtonDown("Fire1"))
{
GameObject bullet = Instantiate(bulletPrefab, muzzle.position, muzzle.rotation);
Destroy(bullet, 3f); // remove this exact bullet after 3 seconds
}
}
}
What happens: every time the Fire1 button (left mouse button, by default) is pressed, Instantiate creates one new live copy of the bullet prefab in the scene, positioned and rotated exactly like muzzle. That new bullet is a completely independent GameObject from that point on — moving it does not move the prefab asset, and firing again creates yet another separate copy. Three seconds later, Destroy(bullet, 3f) automatically removes that specific bullet from the scene.
Destroy(bullet) on its own, with no delay, does not remove the object instantly mid-line — it marks the object for removal at the end of the current frame. That small delay is intentional: other code that already grabbed a reference to bullet earlier in the same frame will not suddenly find itself holding a half-deleted object.
Destroy on them. Whenever you write an Instantiate call, immediately ask yourself: when and how will this object be destroyed?Unity's classic Input class is the simplest way to read the keyboard, mouse, and gamepads. Three methods cover most needs:
Input.GetKeyDown(KeyCode.Space) / GetKeyUp / GetKey — exact key presses, releases, and held state.Input.GetButtonDown("Fire1") — a named action configured in Project Settings, good for one-shot actions like firing or jumping.Input.GetAxis("Horizontal") / "Vertical" — a smoothed value from -1 to 1 that already blends keyboard and gamepad input together.using UnityEngine;
public class TopDownMover : MonoBehaviour
{
public float speed = 5f;
void Update()
{
float h = Input.GetAxis("Horizontal"); // -1 (A / Left) to +1 (D / Right)
float v = Input.GetAxis("Vertical"); // -1 (S / Down) to +1 (W / Up)
Vector3 move = new Vector3(h, 0f, v) * speed * Time.deltaTime;
transform.Translate(move);
}
}
What happens: holding D moves the object in the positive X direction, holding W moves it in the positive Z direction, and both together move it diagonally — all at a steady speed units per second, thanks to the same Time.deltaTime trick from section 7.
Unity also ships a newer, more flexible "Input System" package, better for rebindable controls and multiple players, but the classic Input class shown here needs no extra setup and is enough to build and understand everything in this book.
Everything so far runs top to bottom inside a single frame. Sometimes you want code that plays out over several seconds — a flashing effect, a delay before an enemy attacks, a countdown — without freezing the rest of the game while it waits. That is what a coroutine is for: a method that can pause partway through and pick up again later on its own, while everything else keeps running normally.
using UnityEngine;
using System.Collections;
public class Blinker : MonoBehaviour
{
public Renderer targetRenderer;
void Start()
{
StartCoroutine(Blink());
}
IEnumerator Blink()
{
while (true)
{
targetRenderer.enabled = false;
yield return new WaitForSeconds(0.2f);
targetRenderer.enabled = true;
yield return new WaitForSeconds(0.2f);
}
}
}
What happens: Start calls StartCoroutine(Blink()) once, which begins running Blink. The renderer turns off, then yield return new WaitForSeconds(0.2f) pauses this method only for 0.2 real seconds — the rest of the game, including this same object's own Update if it had one, keeps running normally the whole time. After 0.2 seconds, execution resumes on the very next line, turns the renderer back on, waits again, and loops forever — a steady blink, with no manual timer variable anywhere.
A method returning IEnumerator is what makes a coroutine possible: instead of running start-to-finish in one go like a normal method, it is built so Unity can pause it at each yield return and resume it later at exactly that point. You never call Blink() directly like a normal method — you always start it with StartCoroutine, which hands control of the pausing and resuming over to Unity.
Common things to yield return:
yield return new WaitForSeconds(1f) — pause for 1 real second.yield return null — pause until next frame (like one tick of Update).yield break; — stop the coroutine early, right here.StopCoroutine.ScriptableObject is another base class Unity provides, similar in spirit to MonoBehaviour but built for data instead of behavior. A ScriptableObject is not attached to a GameObject in a scene — it lives as its own asset file in the Project window, the same way a texture or an audio clip does.
using UnityEngine;
[CreateAssetMenu(fileName = "NewWeaponData", menuName = "Data/Weapon Data")]
public class WeaponData : ScriptableObject
{
public string weaponName = "Sword";
public int damage = 10;
public float attacksPerSecond = 1.5f;
}
What happens: [CreateAssetMenu(...)] adds an entry to the right-click "Create" menu in the Project window. Clicking it creates an actual .asset file, say Sword.asset, that a designer can open and edit in the Inspector exactly like any other object, filling in weaponName, damage, and attacksPerSecond with no code changes needed.
using UnityEngine;
public class Weapon : MonoBehaviour
{
[SerializeField] private WeaponData data;
public void Attack()
{
Debug.Log(data.weaponName + " hits for " + data.damage);
}
}
What happens: because data is a [SerializeField] field, the Inspector shows a slot where you drag in the Sword.asset file created above. Calling Attack() prints "Sword hits for 10" using whatever values are currently saved on that asset.
The payoff shows up once many objects should share the same data. Imagine fifty sword-wielding enemies in a scene. Putting damage and weaponName directly as fields on each enemy's own MonoBehaviour means fifty separate copies of the same numbers — change the sword's damage, and you must remember to update all fifty by hand, or they quietly drift out of sync. Point all fifty enemies' data field at the same Sword.asset instead, and there is exactly one copy of the numbers: change it once, and every enemy that references it picks up the new value immediately.
IEnumerator with yield return) that can pause and resume over time without blocking the rest of the game.OrderLogger that logs "Awake", "OnEnable", and "Start" (each exactly once) using Debug.Log, plus logs "First Update" the very first time Update runs, and never again after that. Attach it to a GameObject that starts active in the scene. Before running it, write down the order you expect the four log lines to appear in the Console.using UnityEngine;
public class OrderLogger : MonoBehaviour
{
private bool hasLoggedFirstUpdate = false;
void Awake()
{
Debug.Log("Awake");
}
void OnEnable()
{
Debug.Log("OnEnable");
}
void Start()
{
Debug.Log("Start");
}
void Update()
{
if (!hasLoggedFirstUpdate)
{
Debug.Log("First Update");
hasLoggedFirstUpdate = true;
}
}
}
Console output, in order:
Awake
OnEnable
Start
First Update
Awake and OnEnable fire before the first frame even begins. Start also fires before the first frame's Update, since Unity guarantees Start runs before any Update call on the same object. Only then does the per-frame loop begin, so "First Update" is always last.
GetComponent every frame instead of caching it, and it moves the object the same fixed distance every frame instead of scaling by Time.deltaTime, so it moves at different speeds on different machines. Rewrite it to fix both problems.
using UnityEngine;
public class BuggyMover : MonoBehaviour
{
public float speed = 5f;
void Update()
{
GetComponent<Rigidbody>().MovePosition(
GetComponent<Rigidbody>().position + Vector3.forward * speed);
}
}
using UnityEngine;
public class FixedMover : MonoBehaviour
{
public float speed = 5f;
private Rigidbody rb; // cached once, instead of searched every frame
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
rb.MovePosition(rb.position + Vector3.forward * speed * Time.deltaTime);
}
}
Two separate fixes: caching rb in Awake means GetComponent runs once instead of twice every single frame. Multiplying by Time.deltaTime turns speed from "units moved per call" into "units moved per second," so the object now covers the same real-world distance per second no matter the frame rate — the fix from section 7 applied to a new example.
SpawnLoop that, while a public bool field isSpawning is true, instantiates a public enemyPrefab at a public spawnPoint Transform's position every 2 seconds. Start the coroutine once in Start. The loop should stop spawning, without throwing an error, whenever isSpawning becomes false, and it should never start a second coroutine running at the same time.using UnityEngine;
using System.Collections;
public class EnemySpawner : MonoBehaviour
{
public GameObject enemyPrefab;
public Transform spawnPoint;
public bool isSpawning = true;
void Start()
{
StartCoroutine(SpawnLoop());
}
IEnumerator SpawnLoop()
{
while (isSpawning)
{
Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
yield return new WaitForSeconds(2f);
}
}
}
The while (isSpawning) check runs every time the coroutine wakes back up after a WaitForSeconds, not just once at the start — so setting isSpawning = false from anywhere else in the code (another script, a UI button) quietly ends the loop the next time it wakes up, with no extra code needed to cancel anything. Only one coroutine is ever running because StartCoroutine(SpawnLoop()) is called exactly once, in Start.