Game Development with Unity and C#

Unity is the world's most popular game engine, and C# is the language you write your gameplay in. By the end of this lesson you'll understand how GameObjects and Components fit together, how a MonoBehaviour script comes alive through the Start() / Update() lifecycle, how to move things with Time.deltaTime , wire up the Inspector with [SerializeField] , reuse objects as prefabs , drive physics with a Rigidbody , and spawn or remove objects at runtime.

Learn Game Development with Unity and C# in our free C# course — an interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a GameObject as an empty backpack and Components as the gear you put inside it . The backpack on its own does nothing. Clip on a Transform and it has a place in the world; add a MeshRenderer and it becomes visible; add a Rigidbody and gravity grabs it; add your MonoBehaviour script and it gains behaviour. A "player", an "enemy", a "bullet" — each is just a different mix of components clipped onto a GameObject. This is composition : you build complex things by combining small, focused parts rather than by deep inheritance.

In a normal console app, you own Main and call everything yourself. In Unity it's inverted: the engine owns the loop , and it calls specially-named methods on your scripts at the right moments. You never call Start() or Update() yourself — you just write them, and Unity invokes them.

Because the engine drives the timing, the key discipline is putting the right code in the right method: setup in Start() , per-frame logic in Update() , and physics in FixedUpdate() .

Tip: never tie game speed to the frame rate. Anything that moves over time should be scaled by Time.deltaTime .

1. MonoBehaviour and the Lifecycle

A script becomes a usable component by inheriting from MonoBehaviour . Once attached to a GameObject, Unity automatically calls its lifecycle methods. Awake() fires first as the object loads, Start() runs once just before the first frame, and Update() runs every frame thereafter. You write these methods; the engine calls them. Read the worked example and note which message prints when.

2. Movement with Time.deltaTime

If you move an object by a fixed amount every frame, it will travel faster on a fast machine and slower on a slow one. The fix is Time.deltaTime — the seconds that passed since the previous frame. Multiplying your speed by it converts "per frame" into "per second", so motion is identical regardless of frame rate. Notice the [SerializeField] on speed : it keeps the field private in code but editable in the Inspector.

Your turn. The mover below is almost complete — it just needs the per-frame delta and the step variable. Fill in the two ___ blanks using the hints.

3. GetComponent and Rigidbody Physics

A script often needs to talk to another component on the same GameObject. GetComponent T () fetches it — for example GetComponent Rigidbody () . A Rigidbody hands the object over to the physics engine, so instead of setting positions directly you apply forces with AddForce and let gravity and collisions do the rest. Cache the result of GetComponent in Start() rather than calling it every frame.

4. Prefabs, Instantiate, and Destroy

A prefab is a saved GameObject template — a fully configured bullet, enemy, or coin you can stamp out as many times as you like. At runtime, Instantiate() clones a prefab into the scene and returns the new copy, while Destroy() removes a GameObject (optionally after a delay). Together they power spawning and despawning. Drag the prefab into a [SerializeField] GameObject slot in the Inspector to assign it.

Now you try. Clone the enemy prefab into the scene, then schedule it to be removed after 10 seconds. Fill in the two ___ blanks:

5. Coroutines: Logic Across Frames

Some logic needs to wait — flash three times, fade out over a second, spawn a wave then pause. A coroutine is a method returning IEnumerator that can yield return to hand control back to Unity and resume later, all without blocking the game loop. Start one with StartCoroutine(...) . The most common yield is yield return new WaitForSeconds(t) .

Update() runs once per rendered frame , so its rate floats with the frame rate. FixedUpdate() runs on a steady physics clock (50 times a second by default), independent of frame rate. The rule of thumb: read input and do general game logic in Update() ; apply physics (forces, velocity on a Rigidbody) in FixedUpdate() .

Mixing these up — for example calling AddForce in Update — makes physics jittery and frame-rate dependent.

Here's a compact but realistic controller that ties the lesson together: it reads input with Input.GetAxis , builds a movement vector scaled by moveSpeed * Time.deltaTime , and drives a cached Rigidbody with MovePosition so collisions still work. The [RequireComponent] attribute guarantees a Rigidbody is present.

Every idea here is something you practiced above — combined, they make a character you can actually steer around a scene.

Q: Do I have to call Start() and Update() myself?

No. Unity's engine owns the game loop and calls these specially-named methods on every active MonoBehaviour . You just define them — Unity invokes them at the right time.

Q: When should I use [SerializeField] instead of public ?

Use [SerializeField] private when you want a designer to tune a value in the Inspector but don't want other scripts changing it freely. It gives Inspector access while keeping the field encapsulated.

Q: What's the difference between a GameObject and a prefab?

A GameObject is a live instance in the scene. A prefab is a saved template of a configured GameObject stored as an asset, which you can Instantiate repeatedly to create identical copies.

Q: Why move with a Rigidbody instead of setting transform.position directly?

Setting the Transform teleports the object and ignores physics, which can cause objects to pass through walls. Driving a Rigidbody (forces or MovePosition ) keeps collisions and the simulation consistent.

No blanks this time — just a brief and an outline. Make the object spin a constant 90 degrees per second around its Y axis, frame-rate independent, using transform.Rotate and Time.deltaTime . For a bonus, only rotate while the player holds the R key. Run it in your Unity project and watch the steady spin.

Practice quiz

What base class must a script inherit from to be attached to a GameObject as a component?

  • MonoBehaviour
  • GameObject
  • ScriptableObject
  • Component

Answer: MonoBehaviour. Scripts that attach to GameObjects inherit from MonoBehaviour, which provides the lifecycle hooks (Start, Update) and lets Unity treat the script as a component.

When is Start() called during a GameObject's lifecycle?

  • Every fixed physics step
  • Every frame while the object is active
  • Once, before the first frame the script is enabled
  • Only when the object is destroyed

Answer: Once, before the first frame the script is enabled. Start() runs exactly once, just before the first Update, after Awake. It is the place for initialization that can depend on other objects already existing.

How often is Update() called?

  • Once per fixed physics timestep
  • Once every frame
  • Only when input is received
  • Once at startup

Answer: Once every frame. Update() is called once per rendered frame, so its calling rate varies with frame rate. Per-frame logic like input polling and movement goes here.

Why do you multiply movement by Time.deltaTime in Update()?

  • To convert pixels to meters
  • To pause the game
  • To round positions to integers
  • To make movement frame-rate independent so speed is consistent

Answer: To make movement frame-rate independent so speed is consistent. Time.deltaTime is the seconds elapsed since the last frame. Multiplying by it makes movement per-second rather than per-frame, so speed stays the same across different frame rates.

Which component holds a GameObject's position, rotation, and scale?

  • Transform
  • Renderer
  • Rigidbody
  • Collider

Answer: Transform. Every GameObject has a Transform component storing its position, rotation, and scale, and defining its place in the scene hierarchy.

What does the [SerializeField] attribute do to a private field?

  • It prevents the field from changing
  • It exposes the private field in the Inspector while keeping it private in code
  • It makes the field public to other scripts
  • It saves the field to disk automatically

Answer: It exposes the private field in the Inspector while keeping it private in code. [SerializeField] lets Unity serialize and show a private field in the Inspector so designers can tweak it, without you having to make the field public.

What is a prefab in Unity?

  • A compiled C# assembly
  • A physics material
  • A type of shader
  • A reusable, saved GameObject template you can instantiate many times

Answer: A reusable, saved GameObject template you can instantiate many times. A prefab is a saved asset capturing a configured GameObject (with its components and children) so you can reuse and instantiate consistent copies, like bullets or enemies.

How do you access another component attached to the same GameObject from a script?

  • Component.Load()
  • new Component()
  • GetComponent<T>()
  • FindObjectOfType<T>() only

Answer: GetComponent<T>(). GetComponent<T>() returns the component of type T on the current GameObject, e.g. GetComponent<Rigidbody>(), so you can read or drive it from your script.

For physics-based movement, you should apply forces or set velocity on which component, ideally in FixedUpdate?

  • Camera
  • Rigidbody
  • AudioSource
  • Transform

Answer: Rigidbody. A Rigidbody makes a GameObject participate in the physics simulation. Forces and velocity changes belong on the Rigidbody and are best done in FixedUpdate, which runs on the physics clock.

Which pair of methods creates and removes GameObjects at runtime?

  • Instantiate() and Destroy()
  • Spawn() and Kill()
  • Add() and Remove()
  • Create() and Delete()

Answer: Instantiate() and Destroy(). Instantiate() clones a prefab or GameObject into the scene at runtime, and Destroy() removes a GameObject (optionally after a delay). They drive spawning bullets, enemies, and effects.