How to use this course
Work top to bottom — each unit builds on the last and advances one continuous game. Read the lesson, watch the linked tutorial(s) to build along, then do the practice task.
Tick Done on each lesson to track progress in the sidebar. Your progress is saved in this browser on this device.
Conceptsthe ideas & components introduced
Note2026 / Unity 6 best practice
Practicethe hands-on task
▶ Watchcurated video tutorials
Unit goal: Install Unity 6.3 via the Hub, create a URP project, navigate the Editor, and understand the model everything rests on — GameObjects, Components, and Transforms — plus the game loop and version control.
Unity is the most widely used general-purpose game engine, and for a working software engineer it has one decisive advantage: it runs on C#. Your existing knowledge of classes, generics, LINQ, async, and the .NET standard library transfers directly — you're learning an engine and a domain, not a new language. Unity targets essentially every platform that matters (PC, console, mobile, web, XR) from one codebase, which is why it dominates both indie and mid-size studio development.
The current release is Unity 6.3 LTS, a long-term-support version supported into 2027. It's worth knowing the recent history: in 2023 Unity announced a controversial "Runtime Fee," then cancelled it in September 2024 and returned to a straightforward seat-based subscription model. Today there's a free Personal tier (with a revenue ceiling) that's fine for learning and small commercial projects, and paid Pro/Enterprise tiers above it. No per-install fees.
Unity offers three render pipelines. This course uses the Universal Render Pipeline (URP) — the recommended default that scales from mobile and web up to mid-range PC, with the widest platform reach. HDRP targets high-end PC/console visuals; the legacy Built-in pipeline is being phased out. Choosing URP up front keeps your options open.
Key concepts: Unity 6.3 LTS · C# as the language · Personal vs Pro tiers (no more Runtime Fee) · URP vs HDRP vs Built-in · Unity's platform reach.
Practice Install Unity Hub and Unity 6.3 LTS, create a new project from the URP (3D) template, and press Play on the sample scene.
Unity Hub is the launcher: it manages Editor versions, platform modules, and your projects. You install a specific Editor version (6.3 LTS) and add build modules (WebGL, Android) to it as needed, then create projects from templates. Always pin a project to one Editor version — upgrading mid-project is a deliberate act, not an accident.
The Editor itself is a set of dockable windows. The Scene view is your 3D workspace; the Game view shows what the player sees through the camera. The Hierarchy lists the GameObjects in the current scene; the Inspector shows the components and properties of whatever's selected; the Project window is your assets on disk; and the Console shows logs and errors. Learn the Scene-view navigation (orbit, pan, zoom, and "frame selected") early — you'll use it constantly.
One warning that saves everyone grief: changes you make to the scene while in Play mode are discarded when you stop. Get in the habit of checking whether the Play button is engaged before you start tweaking.
Key concepts: Unity Hub & Editor versions · platform modules · render-pipeline templates · Scene vs Game view · Hierarchy / Inspector / Project / Console · Play-mode edits are temporary.
Practice Create a URP project, add a few primitive objects to the scene, and practice orbit/pan/zoom and "frame selected" until it's second nature.
This is the mental model everything else builds on. A GameObject is essentially an empty container — it does nothing by itself. Its behavior and appearance come entirely from the Components attached to it: a MeshRenderer to draw, a Rigidbody to fall, a Collider to bump, your own scripts to think. You build a "thing" by adding components, not by writing one big class. This is composition at the engine level, and it's why Unity feels different from a typical OOP codebase.
Every GameObject has exactly one component it can't remove: the Transform, which holds its position, rotation, and scale. Transforms also define the hierarchy — parent a sword to a hand, and the sword inherits the hand's movement automatically. This parent/child relationship drives not just movement but organization. Understanding local space (relative to parent) versus world space (absolute) will save you many confusing bugs.
For someone from a classes-and-inheritance background, the key shift is this: instead of asking "what class should this be?", ask "what components does this thing need?" That reframing is the heart of thinking in Unity.
Key concepts: GameObject (container) · Component (behavior/data) · the Transform · parenting & the hierarchy · local vs world space · composition over classes.
Best practice Think in components. Reach for many small, focused components you can mix and match, rather than a deep inheritance tree — the whole engine is designed around it.
Practice Build a GameObject by adding components, parent two objects together, and confirm that moving the parent moves the child. Inspect local vs world position values.
A game runs as a loop, and Unity exposes that loop to your scripts through a set of lifecycle methods it calls automatically. When an object comes to life, Unity calls Awake() (initialize yourself), then OnEnable(), then Start() (safe to reference other objects, which have all had their Awake by now). Each frame it calls Update(); on the fixed physics timestep it calls FixedUpdate(); and after everything moves it calls LateUpdate() — the right place for camera follow.
The timing distinction matters. Update runs once per rendered frame, so its rate varies with performance; multiply anything continuous by Time.deltaTime to stay frame-rate independent. FixedUpdate runs at a fixed rate decoupled from rendering, which is exactly what physics needs — so Rigidbody forces belong there. Getting these two straight is one of the most important habits in Unity.
public class Demo : MonoBehaviour {
void Awake() { // initialize self }
void Start() { // other objects are ready }
void Update() { transform.position += Vector3.forward * Time.deltaTime; }
}
Key concepts: the frame loop · Awake → OnEnable → Start → Update/FixedUpdate/LateUpdate · Time.deltaTime · why physics uses FixedUpdate.
Practice Attach a script that logs each lifecycle method and moves an object using Time.deltaTime. Cap the frame rate and confirm the speed stays constant.
A Unity project is mostly three folders: Assets (everything you make), Packages (dependencies via the Package Manager), and ProjectSettings. There's also a Library folder — a large, regenerable cache that you never commit. Set up Git on day one so your capstone isn't your first commit.
Two Unity-specific things trip up newcomers. First, every asset has a companion .meta file holding its import settings and a stable GUID — you must commit .meta files, because references between assets rely on those GUIDs. Second, scenes and prefabs are YAML text, which merges poorly by default; Unity ships Smart Merge (UnityYAMLMerge) that you configure Git to use for these files. Add Git LFS for large binaries (models, textures, audio) and use a Unity-tailored .gitignore.
Key concepts: Assets/Packages/ProjectSettings · the ignored Library · committing .meta files & GUIDs · Smart Merge for scenes/prefabs · Git LFS.
Best practice Commit .meta files, never commit Library/, and configure Smart Merge before your first scene conflict — not after.
Practice Initialize Git with a Unity .gitignore (GitHub's Unity template is a good base), make a first commit, then move an asset and confirm its .meta moved with it.
Unit goal: Write MonoBehaviour scripts fluently, reference components efficiently, handle timing with coroutines and async, decouple with events, and use ScriptableObjects for data.
A Unity script is a C# class that inherits from MonoBehaviour, and when you attach it to a GameObject it becomes a component. Unity constructs it for you (you don't new it), calls its lifecycle methods, and shows its serialized fields in the Inspector. This is the bridge between your code and the visual editor.
Which fields show up is controlled by serialization. Public fields appear in the Inspector by default, but exposing everything as public breaks encapsulation. The idiomatic approach is [SerializeField] private float speed; — the field stays private to your code but is still tunable in the editor and saved with the scene/prefab. Attributes like [Range(0,10)], [Tooltip("…")], and [Header("Movement")] make the Inspector pleasant to use. Treat the Inspector as your configuration surface: values a designer might tweak belong there, not hard-coded.
public class PlayerMover : MonoBehaviour {
[SerializeField] private float speed = 5f; // tunable, still private
void Update() {
transform.position += transform.forward * speed * Time.deltaTime;
}
}
Key concepts: a script is a Component (MonoBehaviour) · [SerializeField] private vs public · the Inspector as config · [Range]/[Tooltip]/[Header].
Best practice Prefer [SerializeField] private over public for editor-tunable fields — you keep encapsulation and still get Inspector editing.
Practice Write a component with a [SerializeField] private speed and a [Range], attach it, and tune the value live in the Inspector while playing.
Your scripts constantly need to talk to other components — the Rigidbody on the same object, the Animator on a child, the player somewhere in the scene. The primary tool is GetComponent<T>(), which returns the requested component on the same GameObject. Because it does a lookup, you cache the result once in Awake() and reuse the field, never calling it every frame in Update().
Even better than looking things up is being given them: a [SerializeField] private Transform target; that you drag in from the Inspector is explicit, fast, and refactor-safe. Prefer serialized references for known relationships; use GetComponentInChildren/InParent for structural ones; and treat scene-wide searches like GameObject.Find or SendMessage as last resorts — they're slow and fragile. Tags and layers round this out: tags label objects ("Player"), and layers group them for physics and culling.
private Rigidbody _rb;
void Awake() { _rb = GetComponent<Rigidbody>(); } // cache once
void FixedUpdate() { _rb.AddForce(Vector3.up); }
Key concepts: GetComponent<T>() & caching in Awake · serialized references (drag-in) · GetComponentInChildren · tags & layers · why Find/SendMessage are last resorts.
Best practice Cache GetComponent results in Awake; never call it per frame. Prefer serialized references over runtime searches.
Practice Cache a Rigidbody in Awake and drive it in FixedUpdate; separately, wire a [SerializeField] target reference from the Inspector and follow it.
Often you need logic that unfolds over time without blocking the frame — fade this out over two seconds, wait then spawn, step through a sequence. Unity's classic tool is the coroutine: a method returning IEnumerator that you start with StartCoroutine and pause with yield return (e.g., yield return new WaitForSeconds(1f)). Coroutines run on the main thread, resuming a bit each frame, which makes them perfect for time-based sequencing and safe to touch Unity objects from.
Modern Unity also supports C# async/await, and Unity 6's Awaitable type integrates it cleanly with the engine (including awaiting frames and background threads). Use coroutines or Awaitable for game-time sequencing; reach for full async/Task when you're doing genuinely asynchronous work like I/O. The one rule that never changes: only touch Unity APIs from the main thread.
IEnumerator FadeOut(float dur) {
float t = 0f;
while (t < dur) { t += Time.deltaTime; SetAlpha(1 - t/dur); yield return null; }
}
Key concepts: coroutines (IEnumerator, yield, WaitForSeconds) · when to use them · async/await & Unity 6 Awaitable · the main-thread rule.
Practice Write a coroutine that fades an object's material alpha to zero over two seconds, then reimplement it with async/Awaitable.
To keep systems from tangling into direct references, you use events — the Observer pattern you already know. Plain C# events and Actions are the workhorse: a Health component exposes event Action<int> Changed;, raises it when HP changes, and the HUD subscribes without Health knowing the HUD exists. Unity also offers UnityEvents, which are serializable and can be wired up in the Inspector — handy for designer-configurable hooks like a button's OnClick.
The one discipline that matters: always unsubscribe. Subscribe in OnEnable and unsubscribe in OnDisable (or OnDestroy), or you'll leak references and get callbacks into destroyed objects. In Unit 7 you'll take this further with ScriptableObject event channels, which let even prefab assets communicate without scene references — but plain C# events cover most needs.
public event Action<int> HealthChanged;
void TakeDamage(int dmg) { _hp -= dmg; HealthChanged?.Invoke(_hp); }
// HUD: void OnEnable() => health.HealthChanged += Refresh;
// void OnDisable() => health.HealthChanged -= Refresh;
Key concepts: C# event/Action · UnityEvent (Inspector-wired) · the Observer pattern · subscribe in OnEnable, unsubscribe in OnDisable.
Practice Give a Health component a HealthChanged event, raise it on damage, and update a health bar that subscribes/unsubscribes correctly.
A MonoBehaviour lives on a GameObject in a scene; a ScriptableObject is a data container that lives as an asset in your project. Mark a class with [CreateAssetMenu], and you can right-click to create .asset files you edit in the Inspector like any other resource. It's Unity's built-in answer to "where should my data live?"
This unlocks data-driven design. An ItemDefinition ScriptableObject holds a name, icon, and value; you author dozens of item assets without writing code, and many objects can share one asset by reference (saving memory and keeping a single source of truth). Because the data is separate from behavior, designers can tune the whole game by editing assets, and you can build tools around them. You'll lean on ScriptableObjects heavily — for content in Unit 7 and for event channels — so get comfortable now.
[CreateAssetMenu(menuName = "Game/Item")]
public class ItemDefinition : ScriptableObject {
public string displayName;
public Sprite icon;
public int value;
}
Key concepts: ScriptableObject vs MonoBehaviour · [CreateAssetMenu] · authoring .asset data · sharing one asset across objects · memory & iteration benefits.
Practice Create an ItemDefinition ScriptableObject, author two or three item assets, and read one at runtime to log its fields.
Unit goal: Master prefabs (variants and nesting), design entities by composition, choose the right communication pattern, and keep a project organized as it grows.
A prefab is a GameObject saved as a reusable asset — Unity's version of a template or class instance you can stamp out. Drag a configured object into the Project window and it becomes a prefab; every instance you place stays linked to it, so editing the prefab updates them all. This is how you build enemies, pickups, props, and UI once and reuse them everywhere.
Two features make prefabs powerful. Overrides let an individual instance differ from its prefab (a specific enemy with more health), shown in the Inspector so you can apply or revert them. Prefab variants are a prefab that inherits from another — a "ReinforcedCrate" variant of "Crate" that changes only the material — giving you inheritance-like reuse for assets. Nested prefabs let prefabs contain other prefabs (a turret prefab inside a wall prefab). In code, you spawn prefabs with Instantiate(prefab, position, rotation).
[SerializeField] private GameObject enemyPrefab;
void Spawn(Vector3 at) { Instantiate(enemyPrefab, at, Quaternion.identity); }
Key concepts: prefabs & instances · the override system · prefab variants · nested prefabs · Instantiate in code.
Best practice Build almost everything as prefabs, and use variants for families of related objects instead of copy-pasting scene objects.
Practice Make a crate prefab, create a reinforced variant, and spawn a field of instances at runtime with Instantiate.
Coming from OOP, you might reach for Entity → Character → Enemy → FlyingEnemy. Unity pushes you toward a better pattern for games: build entities by attaching components, not by extending base classes. A player and an enemy don't need a shared superclass so much as they both carry a Health, a Mover, and maybe a Hurtbox — small components, each doing one job, combined per object.
The payoff is reuse without rigidity. That Health component drops onto the player, every enemy, and a destructible barrel unchanged. You compose capabilities à la carte instead of forcing everything into one inheritance line, sidestepping the "my flying, swimming, exploding enemy doesn't fit the tree" problem. Inheritance still earns its place for genuinely shared, stable behavior — but in Unity, reach for composition first, and prefer small components communicating through references and events.
Key concepts: component-based entities · small single-purpose components (Health, Mover) · reuse across unrelated objects · when a shared base class still helps.
Best practice Favor small components you can mix and match. A reusable Health component beats a subclass tree — it's the grain of the engine.
Practice Refactor a monolithic player script into a prefab that composes separate Health and Mover components.
Once you have many components, the question is how they talk. Pick the loosest coupling that works. For objects that clearly know each other — a weapon and the character holding it — a serialized direct reference is simple and fast. For one-to-many notifications within a system, use C# events or UnityEvents. For systems that are far apart and shouldn't know about each other — a coin in the level and the HUD score — use ScriptableObject event channels (Unit 7), which route messages through an asset with no scene references.
The anti-pattern to avoid is reaching for a global singleton every time two things need to talk; it works, but it quietly couples everything to everything. Learn to recognize the three tiers — direct reference, event, channel — and reach for the least powerful one that solves the problem.
Key concepts: serialized direct references (nearby) · C#/UnityEvents (one-to-many) · ScriptableObject event channels (distant) · avoiding singleton overuse.
Practice Connect a pickup to a score display via a direct reference, then via an event; note which one keeps working when you separate the objects.
Conventions keep a project navigable at scale. A common approach is a single top-level _Project folder (the underscore sorts it above imported assets), organized by feature — Actors/Player, Systems, Data — rather than by type. In scenes, use empty GameObjects as folders ("---Enemies---") to group the Hierarchy, and name things descriptively.
Keep scenes shallow and prefab-driven: a level is mostly instances of prefabs, so the heavy lifting lives in reusable assets, not one giant scene. As the codebase grows, assembly definitions split your scripts into separate compiled assemblies, which speeds up compile times and enforces clear dependencies (and makes testing easier in Unit 10). Adopt these habits early; retrofitting structure onto a messy project is painful.
Key concepts: a _Project folder, organized by feature · empty GameObjects as Hierarchy folders · shallow, prefab-driven scenes · assembly definitions (preview).
Practice Reorganize a messy sample (or your Unit 0–1 files) into a feature-based folder layout with consistent naming and Hierarchy folders.
Unit goal: Reason about 3D vectors and rotation, write frame-independent movement in the right callback, handle input with the Input System, and build a first character controller.
Unity uses a left-handed, Y-up coordinate system: X is right, Y is up, Z is forward. Positions and directions are Vector3, and the methods you'll use constantly are magnitude, normalized (a direction of length 1), Vector3.Dot (how aligned two directions are), Vector3.Cross (a perpendicular), and Vector3.Lerp. Each Transform also exposes handy direction vectors — transform.forward, right, up — in world space.
Rotation is where 3D differs sharply from 2D. Unity stores rotations as quaternions, not Euler angles, because quaternions interpolate smoothly and avoid gimbal lock. You rarely construct them by hand: Quaternion.LookRotation(direction) gives a rotation facing a direction, Quaternion.Slerp blends between rotations, and Quaternion.Euler(x,y,z) converts from angles when you must. Resist the urge to set eulerAngles directly for anything that accumulates — you'll hit gimbal lock. Get comfortable with these and most movement and aiming code becomes short.
var dir = (target.position - transform.position).normalized;
transform.rotation = Quaternion.LookRotation(dir); // face the target
Key concepts: left-handed Y-up space · Vector3 (magnitude, normalized, Dot, Cross, Lerp) · transform.forward/right · quaternions & why not Euler · world vs local.
Practice Move an object along its forward and rotate it to face a target with Quaternion.LookRotation. Try Slerp for a smooth turn.
Two callbacks handle "every frame," and using the right one prevents a class of bugs. Update runs once per rendered frame; its rate rises and falls with performance, so anything continuous must be scaled by Time.deltaTime (seconds since last frame) to move the same distance per second on any machine. FixedUpdate runs on a fixed timestep independent of frame rate — the correct home for physics, because the physics engine steps at that fixed rate. Rigidbody forces and velocity changes go in FixedUpdate; there you can use Time.fixedDeltaTime, though it's constant.
A simple rule keeps you out of trouble: read input and do non-physics movement in Update (with deltaTime); apply Rigidbody physics in FixedUpdate; and do camera work in LateUpdate so it follows objects after they've moved. Enable Rigidbody interpolation to smooth the visual gap between physics steps.
Key concepts: Update + Time.deltaTime · FixedUpdate for physics · LateUpdate for cameras · Rigidbody interpolation.
Best practice Input in Update, physics in FixedUpdate, camera in LateUpdate. Scale continuous motion by Time.deltaTime.
Practice Move one object with deltaTime and one without; cap the frame rate and watch the delta-less one change speed.
Unity has two input systems. The legacy Input Manager (Input.GetKey) is simple but limited; the modern Input System package is what you should use for new projects. It's built around Input Actions — named intents like "Move," "Look," and "Jump" defined in an action asset — that you bind to any number of physical controls (keys, mouse, gamepad, touch). Your code responds to actions, not hardware, so rebinding, multiple devices, and even local multiplayer come almost for free.
You can consume actions two ways: the PlayerInput component, which wires actions to methods in the Inspector, or a generated C# class you read directly for full control. For a character, you'll typically read a Vector2 Move value each frame and subscribe to a Jump "performed" callback. Setting this up takes a few minutes more than the legacy way, and pays for itself the first time you add gamepad support or let players rebind keys.
Key concepts: the Input System package vs legacy · Input Actions & action maps · bindings & control schemes · PlayerInput component vs generated C# class · rebinding & devices.
Best practice Use the Input System package for new projects. Define named actions, not raw keycodes — you get rebinding and multi-device support with little extra work.
Practice Create a Move/Look/Jump action asset and read it to drive an object, testing on both keyboard and a gamepad.
Time to combine input, vectors, and timing into a controllable character. You have two main options. The CharacterController component is a purpose-built, kinematic capsule: you call Move() with a displacement and it handles stepping, slopes, and collisions without full physics — predictable and easy to tune, ideal for most player characters. Alternatively, a Rigidbody-based controller uses real physics, which suits games where the player should be pushed by forces, at the cost of more tuning.
For a third-person game, movement is usually camera-relative: the input's up direction means "away from the camera," so you transform the input by the camera's yaw before moving, then rotate the character to face its movement direction. Add gravity manually with a CharacterController, ease velocity in and out for weight, and you have a character that feels good to steer. This is your Unit project and the body you'll animate next.
Key concepts: CharacterController (kinematic) vs Rigidbody movement · Move() · manual gravity & grounding · camera-relative movement · rotating toward movement.
Practice — Unit project: Third-Person Roamer. Build camera-relative third-person movement with a CharacterController, easing into motion and rotating to face travel direction.
Unit goal: Frame the action with Cinemachine, author PBR materials, light a scene with URP, add post-processing, and write your first Shader Graph.
A scene needs a Camera, and its basics matter: perspective (with a field of view) versus orthographic, and near/far clipping planes that bound what's drawn. But you rarely script camera movement by hand anymore. Cinemachine is Unity's camera system: you place lightweight virtual cameras that describe how to frame a target — follow it, look at it, orbit it — and a single brain blends between them. It gives you damping, framing, collision avoidance, and cinematic moves that would be tedious to code.
For a third-person game you'll add a Cinemachine camera that follows and orbits the character with smooth damping (a free-look/orbit rig). Because virtual cameras are just configuration, you can have several — gameplay, aim, cutscene — and blend between them by priority. This is one of the biggest quality-of-life wins in Unity; lean on it rather than reinventing camera math.
Key concepts: Camera (perspective vs ortho, FOV, clipping) · Cinemachine virtual cameras & the brain · follow/look-at/orbit · damping · blending by priority.
Best practice Use Cinemachine instead of hand-coding cameras — you get production-grade follow, framing, and blending out of the box.
Practice Add a Cinemachine third-person camera that follows and orbits the roamer with smooth damping.
3D objects are drawn by a MeshRenderer that renders a mesh using one or more materials. A material is an instance of a shader with specific values — in URP the default is the Lit shader, which implements physically-based rendering (PBR). PBR describes surfaces with a small set of texture maps: albedo (base color), metallic and smoothness (how metal/shiny), a normal map (fine surface detail without extra geometry), and ambient occlusion. Feed it good maps and materials respond believably to your lighting.
Getting this right is mostly about import settings and consistency. Set textures to the correct type (normal maps as "Normal map"), keep your world at a sensible scale, and reuse materials rather than creating one per object. You don't need to be a technical artist — understanding what each PBR channel does is enough to make a scene read as solid, lit surfaces rather than flat gray shapes.
Key concepts: MeshRenderer & meshes · materials vs shaders · the URP Lit shader · PBR maps (albedo, metallic/smoothness, normal, AO) · texture import settings.
Practice Create URP Lit materials with albedo and normal maps and apply them to your level's meshes; compare metallic vs dielectric settings.
Lighting is what turns materials into a scene with depth and mood. URP gives you Directional lights (the sun), Point lights, and Spot lights. The crucial distinction is realtime versus baked. Realtime lights update every frame and affect moving objects but cost performance; baked lighting is precomputed into lightmaps for static geometry, giving rich, cheap indirect light and soft shadows (Unity 6 uses the faster xAtlas lightmap packer by default). Most scenes mix the two: bake the static world, keep a realtime key light for dynamic objects.
Because baked light doesn't touch moving objects, you place light probes to sample the baked lighting so characters blend into the environment, and reflection probes for believable reflections. At the pipeline level, the URP Asset controls quality settings, and the Deferred+ rendering path efficiently handles scenes with many lights. Start with one strong directional key light and an ambient tone, then add fills — restraint reads better than a dozen competing lights.
Key concepts: Directional/Point/Spot lights · realtime vs baked (lightmapping with xAtlas) · light & reflection probes · the URP Asset & Deferred+ · ambient/environment light.
Practice Light your scene with a directional key light, bake the static geometry, and add light probes so your character blends in as it moves.
Post-processing grades the final image and adds a lot of perceived polish for little effort. In URP it's driven by the Volume framework: you add a Volume component (global or local to a trigger area) and a Volume Profile listing effects — bloom for glow, tonemapping to map HDR to screen, color adjustments and white balance, vignette, depth of field, motion blur. A global Volume with tasteful bloom and tonemapping instantly lifts a scene from flat to cinematic.
Two more environment pieces complete the look: fog (distance haze that adds depth and hides pop-in) and the skybox (the environment backdrop, which also contributes ambient light and reflections). Local volumes let you change the grade as the player moves — cooler and foggier in a cave, warm at the exit — a cheap, powerful storytelling tool.
Key concepts: the URP Volume framework · global vs local volumes · bloom, tonemapping, color grading, vignette, DoF · fog · skybox & environment.
Practice Add a global Volume with bloom and tonemapping plus distance fog, then a local volume that shifts the grade in one area.
Shader Graph lets you build custom shaders visually, wiring nodes instead of writing HLSL — a great on-ramp for programmers who want custom looks without a graphics-programming detour. You create a graph targeting URP, expose properties (colors, textures, sliders) that become material fields, and connect nodes into the master stack that outputs base color, emission, and so on. Beginner-friendly effects — a fresnel rim light, a scrolling texture, a dissolve that eats away a surface — teach the core ideas quickly.
The real power comes from driving exposed properties at runtime: set a "Dissolve Amount" from code and an object melts away on death; pulse an emission color for a powered-up state. In Unit 9 you'll use exactly this for hit-flashes and death effects. For now, the goal is comfort with the node workflow and the property-to-material connection.
Key concepts: Shader Graph nodes & the master stack · exposed properties → material fields · URP targets · a simple effect (rim/scroll/dissolve) · driving properties from code.
Practice Build a rim-light or dissolve shader in Shader Graph, expose a property, and drive it from a script.
Unit goal: Use Rigidbodies and colliders correctly, control what collides via layers, query the world with raycasts, and build interaction from those primitives.
Unity's 3D physics has two halves. A Collider defines an object's physical shape (box, sphere, capsule, or a mesh collider) — this is what bumps into things. A Rigidbody makes an object participate in the simulation: it gets gravity, mass, drag, and responds to forces and collisions. An object with a collider but no Rigidbody is static world geometry; add a Rigidbody and it becomes dynamic. A kinematic Rigidbody is moved by script but still collides — useful for platforms and controlled objects.
Two performance rules matter from the start. Prefer primitive colliders (box/sphere/capsule) over mesh colliders for anything that moves — they're far cheaper. And mesh colliders that aren't marked convex can't be on dynamic Rigidbodies. Physics materials add friction and bounciness (an ice floor, a rubber ball). Tune mass, drag, and gravity to get the weight you want; small numbers have big effects.
Key concepts: Colliders (box/sphere/capsule/mesh) · Rigidbody (mass, drag, gravity) · kinematic Rigidbodies · convex vs concave mesh colliders · physics materials.
Best practice Prefer primitive colliders for moving objects; concave mesh colliders can't be dynamic and are expensive.
Practice Drop Rigidbody objects with different masses and physics materials and observe the simulation; make one kinematic and move it by script.
Colliders come in two flavors. A solid collider produces collisions — objects physically stop each other, and you get OnCollisionEnter/Stay/Exit callbacks. A collider marked Is Trigger is a detection volume: things pass through it, but you get OnTriggerEnter/Stay/Exit. Triggers are how you build pickups, checkpoints, damage zones, and doors. A gotcha to remember: for either callback to fire, at least one of the two objects involved must have a Rigidbody.
To control what interacts with what, use layers and the Layer Collision Matrix (in Physics settings). Put the player, enemies, world, and pickups on named layers, then uncheck pairs that should ignore each other — enemies that don't collide with each other, projectiles that pass through triggers. This keeps interactions precise and cheap, and it pairs with raycast layer masks in the next lesson.
Key concepts: collisions vs triggers (Is Trigger) · OnCollisionEnter vs OnTriggerEnter · a Rigidbody is required for events · layers & the Layer Collision Matrix.
Practice Define a layer scheme and build a trigger volume that detects only the player, using the collision matrix to ignore other layers.
A raycast shoots an invisible line into the world and reports what it hits — the single most useful physics tool. Physics.Raycast(origin, direction, out hit, maxDistance, layerMask) fills a RaycastHit with the collider, point, distance, and surface normal. You'll use it constantly: ground checks under a character, "what am I aiming at?", hitscan shooting, and interaction ("is there something usable in front of me?"). A layer mask restricts what the ray can hit, which is both correct and fast.
Beyond a thin ray, Unity offers volume queries — SphereCast, BoxCast, CapsuleCast, and OverlapSphere — for "is anything in this area?" checks like explosion damage or a wider ground probe. Because these are invisible, draw them with Debug.DrawRay or Gizmos while developing so you can see what you're testing. Master raycasting and a huge amount of gameplay logic becomes a few lines.
if (Physics.Raycast(cam.position, cam.forward, out RaycastHit hit, 3f, interactMask))
hit.collider.GetComponent<Interactable>()?.Highlight();
Key concepts: Physics.Raycast & RaycastHit · layer masks · sphere/box/capsule casts & overlaps · ground checks, shooting, "what am I looking at?" · visualizing with Gizmos.
Practice Cast a ray from the camera and highlight the object under the crosshair, using a layer mask so only interactables respond.
To push objects around, apply forces to a Rigidbody with AddForce. The ForceMode you choose matters: Force for continuous thrust, Impulse for an instant kick (a jump, an explosion), Acceleration and VelocityChange to ignore mass. AddExplosionForce conveniently pushes everything in a radius. For connected objects, joints (hinge, spring, configurable) constrain how bodies move relative to each other — doors, ropes, ragdolls — though you'll use them sparingly at first.
Now combine the unit's pieces into an interaction system. Raycast from the camera each frame to find an Interactable in front of the player; highlight it; on the Use action, call its behavior — pick it up (parent it or apply forces), open a door, flip a switch. This raycast-plus-interface pattern is the backbone of first- and third-person interaction in countless games, and it's built entirely from what you now know.
Key concepts: AddForce & ForceMode · impulses & AddExplosionForce · joints (hinge/spring/configurable) overview · a raycast-based interaction system.
Practice — Unit project: Physics Playground. Build a room with dynamic objects, a trigger zone, a ground-checked character, and a raycast interaction system to pick up and throw objects.
Unit goal: Import rigged models, drive them with the Animator, build locomotion with blend trees and root motion, and stage cutscenes with Timeline and Cinemachine.
Characters usually come from outside Unity as FBX files (from Blender, Maya, or a service). On import you configure the rig — the skeleton that drives the mesh. Unity's Humanoid rig type is the key feature: it maps the model's bones onto a standard Avatar, which lets you retarget any humanoid animation onto any humanoid character. Animate one skeleton and the animation plays on all of them.
The fastest path to a rigged, animated character is Mixamo: upload or pick a model, choose animations, and download FBX files you import as Humanoid. Watch the common gotchas — set the correct scale factor, extract or reassign materials, and pick "Humanoid" in the Rig tab so the Avatar is created. Once imported, you preview animations in the Inspector and drop the model into your scene, ready to be driven by the Animator in the next lesson.
Key concepts: FBX import settings · the Humanoid rig & Avatar · animation retargeting · a Mixamo/Blender workflow · scale & material gotchas.
Practice Import a Mixamo character as Humanoid with idle/walk/run animations and preview them in the Inspector.
Animation is driven by an Animator Controller: a visual state machine where each state is an animation clip and arrows are transitions between them. You define parameters — floats, bools, ints, and one-shot triggers — and set transition conditions on them (transition from Idle to Walk when Speed > 0.1). Your code doesn't play clips directly; it sets parameters (animator.SetFloat("Speed", velocity)) and the controller decides what plays and blends the transitions.
This separation is deliberate and clean: gameplay code expresses intent ("I'm moving at this speed, I just jumped") while the controller owns the animation logic. Keep parameters minimal and meaningful, use triggers for momentary events like a jump, and lean on transition settings (duration, exit time) to get smooth blends. As states multiply, sub-state machines and the blend trees in the next lesson keep it manageable.
animator.SetFloat("Speed", planarVelocity.magnitude);
if (jumped) animator.SetTrigger("Jump");
Key concepts: Animator Controller state machines · states & transitions · parameters (float/bool/trigger) · transition conditions & blending · setting parameters from code.
Practice Build an Idle ↔ Walk ↔ Jump state machine and drive it from your character's movement parameters.
Hard cuts between Walk and Run look robotic. A blend tree is a special state that smoothly mixes several clips based on a parameter — a 1D tree blends idle→walk→run by Speed, and a 2D tree blends directional strafing by a movement vector. Your code just sets the parameter and the character transitions seamlessly across the whole gait. This is how natural locomotion is built.
You'll also choose between root motion and scripted movement. With root motion, the animation itself drives the character's displacement (great for realism and precise footwork); with in-place animation, your script moves the character and the legs just visually match. Many controllers use scripted movement with a speed-parameterized blend tree for simplicity, tuning to avoid foot sliding. Either way, the result should be a character that starts, moves, and stops believably.
Key concepts: 1D & 2D blend trees · parameterizing by speed/direction · root motion vs scripted movement · avoiding foot sliding.
Practice Replace discrete walk/run states with a 1D blend tree parameterized by speed; tune it to remove foot sliding.
For scripted moments — an intro, a boss reveal, a scene transition — Unity's Timeline is a track-based sequencer, like a video editor for your game. You add tracks that control animation, object activation, audio, and Cinemachine cameras, then arrange clips along a shared timeline. Pair it with Cinemachine and you can cut and blend between virtual cameras cinematically without a line of camera code.
A typical cutscene: a Cinemachine track sweeps the camera across the scene while an Animation track plays a character's wave, and a Signal at the end hands control back to the player. Timelines can be triggered on level start or by a trigger volume. Keep cutscenes short and skippable, and remember to disable player input during them and re-enable it after — a small detail that separates polished sequences from janky ones.
Key concepts: the Timeline (tracks, clips, signals) · animation/activation/Cinemachine tracks · triggering a cutscene · returning control to the player.
Practice — Unit project: Living Character. Give the roamer blend-tree locomotion and a short Timeline intro cutscene with a Cinemachine camera move, then return control to the player.
Unit goal: Model behavior with state machines, decouple systems with ScriptableObject event channels, use managers without abusing singletons, load scenes asynchronously, and drive content with data.
By now your character or enemy logic is probably a snarl of booleans — isJumping, isAttacking, canMove — whose interactions are hard to follow. A finite state machine replaces them with one clear idea: the entity is always in exactly one state, and each state owns its own behavior and its transitions. A clean C# implementation is a set of state classes with Enter, Tick, and Exit methods and a small runner that holds the current state and swaps it on request.
This is distinct from the Animator's state machine (which is about animation): here you're structuring gameplay logic. The two often mirror each other — a gameplay Attack state drives an Attack animation — but keep them separate. FSMs make behavior readable, testable, and debuggable, since you can always log or display the current state. You'll apply the same pattern to enemies in Unit 8.
Key concepts: the state pattern in C# · state classes (Enter/Tick/Exit) · a state-machine runner · gameplay FSM vs the Animator's state machine.
Practice Refactor your character into an FSM with Idle, Move, Jump, and Fall states, each a class with Enter/Tick/Exit.
Plain C# events are perfect for objects that reference each other, but they can't easily connect things that live in different scenes or in prefabs that never meet in the editor. The ScriptableObject event channel pattern — popularized by Unity's own Open Projects — solves this elegantly. A channel is a ScriptableObject asset (say, "OnPlayerDied") with a Raise() method and a list of listeners. Anything can hold a reference to the asset and raise it; anything can listen. Neither side knows about the other; they only know the shared channel asset.
This gives you the decoupling of a global event bus without a hard-coded singleton, and because channels are assets you wire them up visually and see all references in the project. Use them for genuinely cross-system events — player died, level completed, score changed — while keeping nearby communication as direct references and plain events. As always, don't route everything through channels; that just moves the tangle.
Key concepts: ScriptableObject event channels (assets with Raise/listeners) · decoupling without singletons · visual wiring · when to use channels vs plain C# events.
Best practice Use ScriptableObject event channels for cross-system events; keep nearby communication as direct references. It's the modern Unity architecture standard.
Practice Route score and health changes through ScriptableObject event channels that the HUD listens to.
Some things really are global — a game manager tracking score and lives, an audio manager, a save system. The common tool is the singleton: a manager with a static Instance that any script can reach. Combined with DontDestroyOnLoad, a singleton persists across scene loads, which is exactly what a game-wide manager needs. It's convenient and, used sparingly, perfectly fine.
The danger is overuse: when every system is a singleton reaching into every other, you've recreated global-variable spaghetti with extra steps. Two disciplines help. First, keep per-scene state out of persistent managers — because they survive scene changes, that state leaks into the next level. Second, consider a lightweight service locator or dependency injection so systems depend on interfaces rather than concrete singletons, which also makes them testable. Reserve globals for genuinely game-wide concerns; everything else stays local.
Key concepts: the singleton pattern & its pitfalls · DontDestroyOnLoad persistent managers · service locator / DI · keeping per-scene state out of managers.
Best practice Use singletons sparingly and only for game-wide managers. Prefer serialized references or event channels; never store per-scene state in a persistent manager.
Practice Build a persistent GameManager singleton (with DontDestroyOnLoad) that tracks score and lives across scene loads.
Real games span multiple scenes — menus, levels, a persistent manager scene. SceneManager.LoadScene swaps the current scene; more powerfully, additive loading (LoadSceneMode.Additive) loads a scene alongside others, which is how you keep a persistent "Managers" scene always loaded while swapping level scenes in and out. This multi-scene setup is a clean way to separate systems from content.
For anything but the smallest level you'll load asynchronously with LoadSceneAsync, which returns an AsyncOperation you can poll for progress to drive a loading bar, and whose allowSceneActivation lets you hold the new scene until you're ready to show it. A typical flow: fade out, additively load the next level while updating a progress bar, unload the old one, fade in. Addressables (Unit 11) extend this to stream content and scenes on demand.
IEnumerator LoadLevel(string name) {
var op = SceneManager.LoadSceneAsync(name, LoadSceneMode.Additive);
while (!op.isDone) { progressBar.value = op.progress; yield return null; }
}
Key concepts: SceneManager · single vs additive loading · LoadSceneAsync & progress · a persistent manager scene · allowSceneActivation.
Practice Load a level additively with an async loading bar and a persistent "Managers" scene that stays loaded.
Revisit ScriptableObjects with an architect's eye. Instead of hard-coding enemy stats in scripts, define an EnemyDefinition asset with speed, health, damage, and a prefab reference, then author one asset per enemy type. A single enemy script reads its definition and configures itself — so adding a new enemy is creating a data asset, not writing code. The same idea covers items, weapons, abilities, and level configs.
This data-driven style separates content from logic, which transforms iteration: you (or a designer) balance the whole game by editing assets, and you can build editor tools that generate or bulk-edit them. It keeps prefabs generic and code small. Combine it with the event channels and FSMs from this unit and you have the backbone of a maintainable Unity project — the architecture your capstone will rest on.
Key concepts: ScriptableObject definitions for enemies/items/levels · configuring prefabs from data · designer-friendly iteration · content tables.
Practice — Unit project: Architected Build. Refactor onto FSM characters, event channels, a persistent GameManager, async scene loading, and EnemyDefinition-driven enemies.
Unit goal: Implement combat, NavMesh enemies, spawning & pooling, collectibles, UI with UI Toolkit, and save/load — assembling them into a vertical slice.
Combat is best built from small, reusable pieces — the payoff of Unit 2. A Health component holds current/max HP and raises OnDamaged and OnDeath events. Define an IDamageable interface so anything that can take damage exposes a TakeDamage(amount) method; then a weapon or projectile doesn't care what it hit — it just checks for IDamageable and calls it. Hitboxes and hurtboxes are trigger colliders on dedicated layers that connect an attack to a target's Health.
Because these are components and interfaces, the same Health works unchanged on the player, every enemy, and a destructible barrel — you configure, you don't duplicate. Add the details that make combat feel fair: brief invincibility frames after a hit, and a bit of knockback via AddForce. Layers keep it tidy so the player's attacks hit enemies and vice versa without friendly fire.
public interface IDamageable { void TakeDamage(int amount); }
// weapon: if (hit.collider.TryGetComponent(out IDamageable d)) d.TakeDamage(10);
Key concepts: a Health component with events · an IDamageable interface · hitboxes/hurtboxes as trigger colliders on layers · i-frames & knockback.
Practice Build a component-based damage system with an IDamageable interface, reused by the player and an enemy.
Making enemies navigate a 3D level would be hard from scratch, so Unity provides NavMesh. You bake a navigation mesh over your level (the walkable surface), add a NavMeshAgent to an enemy, and call agent.SetDestination(target) — the agent pathfinds around obstacles, avoids other agents, and handles steering for you. NavMesh links bridge gaps like jumps or ladders. That's the movement solved; the behavior is up to you.
Layer the FSM from Unit 7 on top: a typical enemy cycles Patrol → Chase → Attack. In Patrol it moves between waypoints; a raycast or vision cone gives line-of-sight, and spotting the player switches to Chase (set destination to the player); in range it Attacks; losing sight returns it to Patrol. Expressing it as states keeps the behavior readable and lets you see exactly what an enemy is thinking. Start simple — a patroller that chases on sight is plenty for your slice.
Key concepts: baking a NavMesh · NavMeshAgent & SetDestination · patrol/chase/attack via FSM · line-of-sight with raycasts · NavMesh links.
Practice Bake a NavMesh and build a patrolling enemy that spots the player via line-of-sight, chases via NavMesh, and returns to patrol.
Spawning is Instantiate-ing a prefab at runtime — the pattern behind projectiles, enemy waves, and pickups, often driven by data (a wave definition asset). But Instantiate and Destroy are expensive, and doing them rapidly (bullets, impact effects) causes frame hitches and garbage-collection spikes. The fix is object pooling: pre-create a batch of objects and reuse them — deactivate instead of destroy, reactivate instead of instantiate — so you pay the allocation cost once.
Unity ships a built-in ObjectPool<T> so you don't have to hand-roll one; you provide create/get/release/destroy callbacks and it manages the rest. Pool anything short-lived and frequent. This is the same allocation-avoidance instinct you'd apply on a hot path in a backend service, and in Unit 10 you'll see its impact directly in the Profiler's GC graph.
Key concepts: Instantiate/Destroy & their cost · Unity's built-in ObjectPool<T> · pooling projectiles/VFX · data-driven spawners.
Best practice Pool frequently spawned objects with the built-in ObjectPool<T> instead of constant Instantiate/Destroy — it removes a major source of GC hitches.
Practice Build a projectile spawner backed by ObjectPool<T> that reuses a fixed set of instances.
Collectibles turn a space into a game. Mechanically a pickup is a trigger collider that, on the player entering, updates some state and disappears — announcing the change through an event channel so the HUD and audio react. From that primitive you build score counters, gated progression (keys that open matching doors), and a simple inventory backed by ItemDefinition ScriptableObjects.
Keep the data for progression — counts, unlocked items — in your GameManager or a dedicated inventory object, not scattered across pickups, so it survives scene changes and is trivial to save in the next lesson. Progression is where the architecture from Unit 7 proves itself: a coin raises a "collected" channel, the GameManager updates the total, and the HUD reflects it, with no object reaching across the scene to another.
Key concepts: trigger-based pickups · counters & score · inventory backed by ScriptableObject items · keys/doors gating · progression via the GameManager & channels.
Practice Add collectible keys that open a matching door and update an on-screen count stored in the GameManager.
Unity has two UI systems. The older uGUI (Canvas, Image, Button) is mature and great for world-space UI. The newer UI Toolkit is Unity's recommended direction for screen-space UI like HUDs and menus, and it will feel familiar: you define structure in UXML (like HTML) and style it in USS (like CSS), with a flexbox-based layout and a visual UI Builder. If you've built web UIs, you already understand the model.
Build your HUD and menus as UI Documents, then bind them to game state — query elements by name in C# and update them when your event channels fire (health changed → update the bar). Wire buttons to callbacks. Don't neglect focus and navigation so menus work with keyboard and gamepad, which also aids accessibility. Choose per need: UI Toolkit for menus and HUD, uGUI when you need UI attached to objects in the world.
Key concepts: UI Toolkit (UXML/USS, UI Builder) vs uGUI · flexbox-like layout · binding UI to state via events · buttons, focus & navigation.
Best practice UI Toolkit is Unity's recommended path for new HUD/menu UI; keep uGUI for world-space UI. Pick per need rather than forcing one everywhere.
Practice Build a HUD (health/score) and a pause menu with UI Toolkit, bound to your event channels.
Persistence has one firm rule: write to Application.persistentDataPath, the per-user writable folder Unity maps correctly on every platform — never beside your game files, which may be read-only once installed. From there, serialize your save data. Unity's built-in JsonUtility converts serializable classes to and from JSON, which is readable and easy to debug; for more complex data, many teams use a library like Newtonsoft's Json.NET. Decide deliberately what to save — usually progression, inventory, and settings, not the entire live scene.
Two habits save pain. Version your save format from the first release (store a version number) so later updates can migrate old saves instead of breaking them. And resist using PlayerPrefs for real save data — it's meant for small preferences (volume, resolution), not game state. A versioned JSON file of your GameManager's state in persistentDataPath is a clean, robust default for a single-player game.
var json = JsonUtility.ToJson(saveData);
File.WriteAllText(Path.Combine(Application.persistentDataPath, "save.json"), json);
Key concepts: Application.persistentDataPath · JsonUtility (and its limits) · what to save · versioning saves · PlayerPrefs is for settings only.
Best practice Save to persistentDataPath and version your format from day one. Use PlayerPrefs for settings, never for save games.
Practice — Unit project: Vertical Slice. Assemble one polished area: combat, a NavMesh enemy, keys & a door, a UI Toolkit HUD and pause menu, and working save/load to persistentDataPath.
Unit goal: Add mixed audio, animate with tweens and Cinemachine Impulse, build effects with the Particle System and VFX Graph, use shaders for feel, and set mood with lighting.
Audio in Unity flows from AudioSources (which play clips) to a single AudioListener (usually on the camera). A source can be 2D (non-positional — music, UI) or 3D (spatial), where the "Spatial Blend" and rolloff settings make a sound louder and panned as its source nears the listener. That spatialization is a big part of a 3D game's immersion — footsteps behind you, a machine humming to your left.
Above individual sources sits the Audio Mixer: route music to a Music group and effects to an SFX group, and you get master volume control, effects (reverb, compression), and snapshots you can blend between (e.g., muffle everything when paused). Ducking — dropping music under important sounds — is easy here. Wrap playback in an audio manager so any system can request a sound and your options menu can set group volumes in one place. Even a handful of good sounds transforms how a game reads.
Key concepts: AudioSource & AudioListener · 2D vs 3D (spatial) sound & rolloff · the Audio Mixer, groups & snapshots · ducking · an audio manager.
Practice Route music and SFX through separate mixer groups with independent volume, and add spatial footsteps to the character.
"Juice" is the layer of small feedback effects that make actions feel powerful. Tweening — smoothly animating a value with an easing curve — drives UI pops, pickup bounces, and damage flashes; you can write tweens with coroutines, but a library like DOTween makes them one-liners (transform.DOShakePosition(...), DOScale(...)). For impact, Cinemachine Impulse is the clean way to do screen shake: an Impulse Source emits a shake when something happens (a hit, an explosion) and the camera's Impulse Listener responds, all event-driven and tunable.
Round it out with hit-stop — freezing the game for a few frames on a heavy hit by briefly setting Time.timeScale low — and a subtle camera punch. The essential counter-skill is restraint: juice is seasoning, and too much shake and flash becomes fatiguing noise. Add effects deliberately, tune them down until they're felt but not distracting, and save the biggest reactions for the biggest moments.
Key concepts: tweening (DOTween/coroutines) for pops & flashes · Cinemachine Impulse for screen shake · hit-stop via Time.timeScale · restraint.
Practice Add Cinemachine Impulse shake and a few frames of hit-stop to a successful hit, then tune both down until they feel good.
Unity has two particle systems. The built-in Particle System (a.k.a. Shuriken) is CPU-based, deeply configurable through modules (emission, shape, velocity, color-over-lifetime, collision), and can raise events back to gameplay — ideal for the impact bursts, dust, sparks, and trails your slice needs. The VFX Graph is a newer, node-based, GPU-driven system that can push millions of particles for large-scale spectacle (storms, magic, big explosions) but doesn't interact with gameplay as directly.
The rule of thumb: reach for the Particle System for gameplay-facing effects and smaller counts (and better mobile compatibility), and the VFX Graph when you need massive numbers or complex GPU-driven looks. Both read the same visual language of emission and lifetime. For your slice, a well-timed impact burst and a bit of ambient dust do more for feel than any single big effect.
Key concepts: the built-in Particle System (Shuriken) & its modules · the GPU-based VFX Graph · when to use each · impact bursts, trails & dust.
Practice Build an impact burst with the Particle System and one large effect with the VFX Graph.
Return to Shader Graph with gameplay feedback in mind. Three effects earn their keep everywhere: a hit-flash (briefly tint a character white when damaged), a dissolve (edges burn away on death), and an outline/highlight (mark an interactable the player is looking at). Each is a modest graph exposing one or two properties you drive from code — set "FlashAmount" to 1 and tween it back to 0 on a hit.
The performance detail worth knowing: to change a material property on many instances without creating a material copy per object (which breaks batching), use a MaterialPropertyBlock. Hook these effects to the events you already have — your Health component's OnDamaged triggers the flash, OnDeath triggers the dissolve — and combat suddenly reads clearly and feels responsive.
Key concepts: hit-flash, dissolve & outline shaders · exposed properties driven from code · MaterialPropertyBlock for per-instance changes · hooking effects to Health events.
Practice Add a hit-flash and a dissolve-on-death effect driven from your Health component's events.
Lighting isn't just visibility — it's tone. Revisit URP lighting to shape a specific mood for your slice: a warm low sun for dusk, cold dim light and fog for tension, a single dynamic light for a spooky corridor. Emissive materials (glowing screens, lava, magic) read as light sources and pair beautifully with bloom, and animated or flickering lights add life. Combine this with the color grading from Unit 4 and you can shift the whole emotional register of a scene without changing any geometry.
Mind the performance trade-offs: realtime shadows and many dynamic lights are costly, so bake what's static and reserve dynamic lights for what needs them. As with juice, restraint wins — a single well-placed, well-colored light and a considered ambient tone almost always beat a scene crowded with competing sources.
Key concepts: lighting for mood · dynamic lights & emissive materials · fog & color grading for tone · performance trade-offs (bake static, dynamic where needed).
Practice — Unit project: Juiced Slice. Give the vertical slice a full feel pass: mixed audio, screen shake & hit-stop, particles/VFX, feel shaders, and mood lighting.
Unit goal: Debug effectively, profile and optimize, write automated tests with the Unity Test Framework, and set up CI and clean project structure.
Your debugging toolkit is richer than Debug.Log, though logs (and Debug.LogWarning/Error, plus assertions) are a fine start; the Console clusters duplicates and links stack traces to code. For real stepping, attach a debugger from Rider or Visual Studio to the Editor and set breakpoints in your C# — you can pause Play mode, inspect variables, and step exactly as in any .NET app.
The engine-specific superpowers are visual. Draw debug lines and shapes with Debug.DrawRay/DrawLine and OnDrawGizmos to see invisible state — a raycast, a detection radius, an enemy's target — right in the Scene view. The Frame Debugger steps through how a frame is drawn, invaluable for rendering issues. Most gameplay bugs come down to "the state isn't what I assumed," and visualizing that state is usually faster than guessing.
Key concepts: the Console, Debug.Log & assertions · breakpoints via Rider/VS · pausing & stepping in Play mode · Debug.DrawRay/Gizmos to visualize state · the Frame Debugger.
Practice Find a planted bug using a breakpoint, then add a Gizmo that draws an enemy's detection radius and current target.
Optimize by measuring, never by guessing. The Profiler breaks each frame into CPU, GPU, rendering, physics, and memory, so you can see where the time actually goes. Three culprits dominate 3D games: too many draw calls (mitigated by the SRP Batcher, GPU instancing, and sharing materials/atlases), expensive per-frame work, and garbage-collection spikes from per-frame allocations. That last one is why pooling (Unit 8) and avoiding allocations in Update matter — a GC spike is a visible stutter.
Use the Profiler and the Memory Profiler to find the real bottleneck, change one thing, and measure again. For a course-scale 3D game you'll rarely be near the limits, but building the profile-first habit here is what lets you ship smoothly on modest hardware — and it's the same discipline you'd apply to any performance problem in your day job.
Key concepts: the Profiler (CPU/GPU/memory) · draw calls & batching (SRP Batcher, GPU instancing) · GC allocations & avoiding per-frame garbage · the Memory Profiler.
Best practice Profile before optimizing, and watch GC allocations closely — per-frame garbage causes the stutters players notice most.
Practice Profile a deliberately heavy scene, find a hot spot or GC spike, fix it, and confirm the improvement in the Profiler.
Yes, you can test Unity code — and coming from professional development, you'll want to. The Unity Test Framework is built on NUnit and split into two modes. Edit Mode tests run without entering Play mode and are perfect for pure logic: your Health math, inventory rules, state-machine transitions, save/load round-trips. Play Mode tests run in a live scene and can span frames (via [UnityTest] coroutine tests), letting you assert on real engine behavior — "after landing, the controller reports grounded."
You don't need exhaustive coverage; a focused suite around your riskiest, most-reused systems catches the regressions that hurt most and lets you refactor with confidence. To make code testable, put it behind assembly definitions so tests can reference it cleanly. Write a failing test first when fixing a bug, then make it pass — the same TDD instincts you already have, applied to game logic.
Key concepts: the Unity Test Framework (NUnit) · Edit Mode vs Play Mode tests · the Test Runner · [UnityTest] coroutine tests · assembly definitions for testable code.
Best practice Unit-test pure logic in Edit Mode; use Play Mode for engine behavior. Put testable code behind assembly definitions and run it in CI.
Practice Write Edit Mode tests for your Health component and a Play Mode test that a pickup increments the score.
Assembly definitions (.asmdef) split your scripts into separate compiled assemblies. Beyond faster incremental compiles, they enforce explicit dependencies between modules and are what let test assemblies reference game code cleanly — structural hygiene that pays off as the project grows. Organize your code into a few well-bounded assemblies rather than one monolith.
Then automate the checks. Unity can run in batch mode (headless) from the command line to run tests and make builds, and the community GameCI project provides ready-made GitHub Actions to run your Test Framework suite on every push and even produce builds. Add the ordinary hygiene you already practice — consistent structure, a shared code style, small reviewable commits — and the software-engineering fundamentals from your career carry straight over. The engine is new; the discipline isn't.
Key concepts: assembly definitions (faster compiles, clear deps) · Unity batch mode (headless) · GameCI / GitHub Actions · code style & project discipline.
Practice — Unit project: Test & Tune. Add assembly definitions and a GameCI GitHub Actions workflow that runs your tests on push, plus a short profiling report showing one measured optimization.
Unit goal: Understand the build pipeline and publish to desktop, web, and mobile, then prepare a responsible release.
Producing a build starts in Build Settings (in Unity 6, Build Profiles): you add your scenes to the build list, pick a target platform, and if it's not your current one, "Switch Platform" (which reimports assets for that target — it can take a while the first time). You need the platform's build module installed via the Hub. Player settings control the app's identity and behavior — name, icon, resolution, splash, and the scripting backend.
One choice worth understanding: Mono vs IL2CPP. Mono compiles to .NET IL and builds fast (good for iterating); IL2CPP converts to C++ for better performance and is required for some platforms (iOS, WebGL, consoles) but builds slower. Use development builds (with the profiler and debugging attached) while iterating, and a release build to ship. Start by making a desktop build of your slice — it's the simplest target and confirms your setup works.
Key concepts: Build Profiles / Build Settings · the build scene list · platform modules & switching platform · Player settings · Mono vs IL2CPP · development vs release builds.
Practice Configure Player settings (name, icon, resolution) and produce a desktop build of your slice; run it outside the Editor.
Ship a lean build. The biggest lever is usually textures: set sensible max sizes and compression per platform, since art dominates build size. Audio compression, mesh import settings, and code stripping help too. For managing which assets ship and when, Addressables is Unity's system for referencing assets by address and loading them on demand — it enables smaller initial downloads, content updates without a full rebuild, and streaming, replacing the old asset-bundle workflow.
Use per-platform quality settings to dial shadows, textures, and effects for weaker targets (important for WebGL and mobile), and Unity's build report to see what's taking space. You don't need to master Addressables to finish the course, but understanding that it exists — and moving a few large or optional assets into it — is the modern approach to keeping builds small and updatable.
Key concepts: texture & audio compression · code stripping · Addressables for asset management & smaller builds · per-platform quality settings · the build report.
Practice Tune texture compression, move a few assets to Addressables, and measure the build-size difference.
Each platform has its own considerations. PC (Windows/Mac/Linux) is the easiest to ship, distributed via Steam or itch.io. WebGL is the most shareable — anyone plays in a browser — but it's the most constrained: builds use IL2CPP, take longer, have memory limits and no multithreading in the traditional sense, so keep the game modest and compress aggressively; you upload the build folder to a host like itch.io. Mobile (Android via an AAB with a signing keystore; iOS via Xcode) adds touch input, a wide range of screen sizes, and tighter performance budgets — design for touch and test on device.
The through-line is that input and performance differ per platform, so decide your target early and build toward it. For this course, WebGL is a great first public target: it's free to host and instantly playable by anyone you send the link to.
Key concepts: PC (Steam/itch.io) · WebGL (IL2CPP, memory limits, hosting, compression) · mobile (Android AAB/keystore, iOS/Xcode, touch & screen sizes) · per-platform input & performance.
Practice Build a WebGL version of your slice and host it (e.g., on itch.io) as a playable link.
Shipping is more than a build. Give it a version, write a store/itch page with screenshots and a clear description of controls, and include credits — yourself plus every third-party asset and its license (Mixamo animations, Asset Store packs, fonts, sounds). Depending on your Unity tier, you may be able to disable the "Made with Unity" splash; either way, honor the licenses of what you used.
Before you announce it, playtest with people who aren't you — five minutes of watching a stranger play teaches more than hours of your own testing. Fix what confuses them, then release. Plan for a small patch or two afterward. The real lesson of this unit is simply finishing: a shipped, modest, imperfect game is worth infinitely more than an unshipped ambitious one.
Key concepts: versioning · store/itch pages · credits & third-party licenses · the Unity splash by tier · playtesting & feedback · post-launch patches.
Practice — Unit project: Ship It. Publish the vertical slice as a hosted WebGL build (optionally desktop too), with a store page, controls, and credits.
Capstone goal: Take a game from concept to a public release on your own, applying everything from the course. Scope is deliberately small; completeness and polish are the point. Work through the six milestones in order.
Your first game won't fail from a lack of skill or the wrong engine — it'll fail from scope creep, and 3D makes scope even more expensive because art and animation are costly. So contain it ruthlessly. Write a one-page design doc: the core loop (what the player does over and over), the single hook that makes it interesting, and the target platform (WebGL is a fine, shareable choice). Then write a "stretch goals" list and put everything else on it. Whatever feels small, cut it in half. Lean on free assets (Mixamo, Synty/Kenney kits) so you're not modeling from scratch.
Deliverable: a one-page GDD with the core loop, the hook, the target platform, and an explicit out-of-scope list.
Practice Write your one-page GDD and list everything explicitly out of scope for v1.
Before any art, build the smallest playable version of your core loop with blockout/greybox assets. The only question here is: is it fun? Iterate quickly and cheaply until the answer is yes — or until you decide to change the idea. It's far better to learn a mechanic is flat with grey capsules than after a week of modeling and animation. Use primitives and Mixamo placeholders; polish comes later.
Deliverable: a grey-boxed, playable core loop you've confirmed is fun.
Practice Build the prototype and get at least one other person to play it before moving on.
Now build the real thing on the Unit 7 architecture — FSMs, ScriptableObject event channels, data-driven content — so it stays manageable. Create your levels and systems, and integrate models, animation, and audio. Data-driven design pays off here: author enemies, items, and configs as ScriptableObjects so you can add and tune content quickly without rewriting logic.
Deliverable: the full game's levels, systems, and content, built on clean architecture.
Practice Implement the complete content of your scoped game, committing regularly.
Apply the Unit 9 feel pass — audio, juice, lighting — plus a UX and accessibility pass: readable UI, remappable controls (free via the Input System), and options that respect players. Define a "bug bar" (which severities must be fixed before release) and drive your known issues down to it. Polish is finite; decide what "good enough to ship" means and stop there.
Deliverable: a game that feels good, is accessible, and meets your bug bar.
Practice Do a feel + accessibility pass and resolve all bugs above your defined bar.
Add automated tests around your riskiest systems (Unit 10) so last-minute changes don't break them, and do a profiling pass against a frame-time budget for your target platform — especially important for WebGL and mobile. You don't need exhaustive coverage; you need confidence in the parts that would ruin the game if they broke, and a build that holds its frame rate.
Deliverable: a test suite for critical systems and a build that hits your performance budget.
Practice Write tests for your two riskiest systems and profile until you meet your target frame time.
Build, make a store page, run a final round of outside playtests, and ship it. Then write a short postmortem: what went right, what went wrong, and what you'd do differently. Publishing — even something small and imperfect — is the milestone that turns you from someone learning game development into someone who makes games. This is your portfolio piece.
Deliverable: a shipped, playable 3D game on at least one platform, source under version control with tests and CI, plus a one-page postmortem.
Practice Release the game publicly and write your postmortem. You're done — go make the next one.
Essential channels & courses
Official documentation & tools
- Unity Manual & Scripting API: the authoritative reference for every class and workflow.
- Mixamo: free rigged characters and animations to prototype with.
- GameCI: GitHub Actions for running the Test Framework and builds in CI.
- itch.io: free hosting for your WebGL build and a source of free art/audio.
How to keep learning
- Rebuild a mechanic from a game you love in isolation (a grapple, a cover system, a dash) to study it.
- Join a short game jam after the capstone — a hard deadline is the best teacher of scope.
- Explore Unity's open-source sample projects and the DOTS/ECS stack once you want massive-scale performance.