Game Development with Unity and C#

Reviewed & published by Brayan K

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.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

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.

The mental model: you write components, Unity runs the loop

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().

📊 The MonoBehaviour Lifecycle

MethodWhen it runsUse it for
Awake()Once, when object loadsSelf-setup, cache references
Start()Once, before first frameSetup needing other objects
Update()Every frameInput, movement, game logic
FixedUpdate()Every physics stepRigidbody forces & velocity
OnDestroy()When object is destroyedCleanup, unsubscribe

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.

using UnityEngine;

// A script becomes a COMPONENT by inheriting from MonoBehaviour.
// Attach this file to a GameObject in the scene and Unity will run
// its lifecycle methods for you — you never call them yourself.
public class PlayerController : MonoBehaviour
{
    private int frameCount = 0;

    // Awake() runs first, when the object loads — even if disabled.
    void Awake()
    {
        Debug.Log("Awake: the object exists now.");
    }

    // Start() runs ONCE, just before the first frame this script is enabled.
    // Good place for setup that depends on other objects already existing.
    void Start()
    {
        Debug.Log("Start: ready to play!");
    }

    // Update() runs ONCE PER FRAME. Per-frame logic lives here:
    // reading input, moving things, checking conditions.
    void Update()
    {
        frameCount++;
        if (frameCount == 1)
            Debug.Log("Update: first frame rendered.");
    }
}

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.

using UnityEngine;

// Moves the GameObject smoothly to the right at a constant SPEED,
// no matter whether the game runs at 30, 60, or 144 fps.
public class Mover : MonoBehaviour
{
    // [SerializeField] shows this private field in the Inspector so a
    // designer can tune the speed without editing code.
    [SerializeField] private float speed = 5f;   // units per SECOND

    void Update()
    {
        // Time.deltaTime = seconds since the previous frame.
        // Multiplying by it turns "per frame" into "per second" — so the
        // object covers 'speed' units every real-world second.
        float distanceThisFrame = speed * Time.deltaTime;

        // transform.Translate moves the Transform's position.
        transform.Translate(Vector3.right * distanceThisFrame);
    }
}

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.

using UnityEngine;

public class ForwardMover : MonoBehaviour
{
    [SerializeField] private float speed = 3f;   // units per second

    void Update()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        // 1) Make the movement frame-rate independent by multiplying
        //    speed by the seconds elapsed since the last frame.
        float step = speed * Time.___;            // 👉 the per-frame delta:  deltaTime

        // 2) Move forward (the object's blue +Z axis) by 'step'.
        transform.Translate(Vector3.forward * ___); // 👉 the variable:  step

        // ✅ Result: the object glides forward at a steady 3 units/second
        //    on any machine, fast or slow.
    }
}

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.

using UnityEngine;

// Pushes the object upward (a "jump") using the physics engine.
// Requires a Rigidbody component on the same GameObject.
public class Jumper : MonoBehaviour
{
    [SerializeField] private float jumpForce = 8f;

    // Cache the Rigidbody once instead of fetching it every frame.
    private Rigidbody rb;

    void Start()
    {
        // GetComponent<T>() returns the Rigidbody attached to THIS object.
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // Input check belongs in Update (per frame).
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // AddForce on the Rigidbody lets the physics engine handle
            // the motion — gravity will pull it back down naturally.
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            Debug.Log("Jump!");
        }
    }
}

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:

using UnityEngine;

public class Spawner : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;

    void SpawnEnemy()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        // 1) Clone the prefab into the scene at this spawner's position.
        GameObject enemy = ___(enemyPrefab, transform.position, Quaternion.identity); // 👉 the spawn call:  Instantiate

        // 2) Clean it up automatically after 10 seconds so the scene
        //    doesn't accumulate stale enemies.
        ___(enemy, 10f);                       // 👉 the cleanup call:  Destroy
    }
}

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).

using System.Collections;
using UnityEngine;

// A COROUTINE can pause itself and resume later across frames, without
// blocking the game. Perfect for timed sequences and waits.
public class Blinker : MonoBehaviour
{
    void Start()
    {
        // StartCoroutine kicks off the IEnumerator method below.
        StartCoroutine(BlinkRoutine());
    }

    // Return type IEnumerator marks this as a coroutine.
    IEnumerator BlinkRoutine()
    {
        for (int i = 0; i < 3; i++)
        {
            Debug.Log("On");
            // 'yield return' hands control back to Unity; the coroutine
            // resumes here AFTER the wait — the game keeps running meanwhile.
            yield return new WaitForSeconds(0.5f);

            Debug.Log("Off");
            yield return new WaitForSeconds(0.5f);
        }
        Debug.Log("Done blinking.");
    }
}

🔎 Deep Dive: Update vs FixedUpdate

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().

// Input belongs in Update (don't miss a key between physics steps)
void Update()
{
    if (Input.GetButtonDown("Jump")) wantsToJump = true;
}

// Physics belongs in FixedUpdate (consistent simulation)
void FixedUpdate()
{
    if (wantsToJump) { rb.AddForce(Vector3.up * 8f, ForceMode.Impulse); wantsToJump = false; }
}

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

Putting It Together: a Top-Down Player

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.

using UnityEngine;

// === A complete top-down player controller ===
// Combines Input, deltaTime movement, GetComponent, and a Rigidbody.
[RequireComponent(typeof(Rigidbody))]
public class TopDownPlayer : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 6f;
    private Rigidbody rb;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // Read the keyboard/stick: returns -1..1 on each axis.
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        // Build a movement direction and scale by speed * deltaTime.
        Vector3 move = new Vector3(h, 0f, v) * moveSpeed * Time.deltaTime;

        // MovePosition respects physics/collisions better than transform.
        rb.MovePosition(rb.position + move);
    }
}

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

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Be a component: MonoBehaviourInherit to attach to objects
Per-frame logicvoid Update()Runs every frame
Move over timespeed * Time.deltaTimeFrame-rate independent
Expose to Inspector[SerializeField] privateTune without public
Get a componentGetComponent<Rigidbody>()Cache the result
Spawn an objectInstantiate(prefab, pos, rot)Returns the clone
Remove an objectDestroy(go, 3f)Optional delay
Wait across framesyield return new WaitForSeconds(t)Inside a coroutine

Frequently Asked Questions

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.

Mini-Challenge: A Steady Rotator

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.

using UnityEngine;

public class Rotator : MonoBehaviour
{
    [SerializeField] private float degreesPerSecond = 90f;

    void Update()
    {
        // 🎯 MINI-CHALLENGE: spin this object smoothly
        // 1. Compute how many degrees to rotate THIS frame:
        //    degreesPerSecond * Time.deltaTime  (frame-rate independent).
        // 2. Rotate the Transform around the Y axis (Vector3.up) by that
        //    amount using transform.Rotate(...).
        // 3. BONUS: only spin while the player holds the R key
        //    (Input.GetKey(KeyCode.R)).
        //
        // ✅ Expected: the object turns a steady 90 degrees every second,
        //    identical on a slow or fast machine.

        // your code here
    }
}

🎉 Lesson Complete

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.

Continue this course