4.3 Unity Systems: Input, Physics, Animation, Audio, UI

Phase 4 · Unity (primary engine) · Study time: 50–90 h

Using the built-in systems: the Input System, physics, the Animator (Mecanim), audio, UI, and the render pipelines (URP and HDRP).

In the last two chapters you learned how a Unity project is built out of GameObjects and Components (4.1), and how you write behavior with MonoBehaviour scripts — Awake, Start, Update, FixedUpdate, coroutines, events, ScriptableObject (4.2). That is the skeleton. This chapter covers the built-in systems that give a Unity game its actual gameplay feel: reading player input, simulating physics, playing animation, making sound, and drawing UI (menus, health bars, buttons). Every Unity game, from a weekend prototype to a HoYoverse-scale title, leans on these same five systems.

Each section follows the same shape as before: a small script, what happens when it runs (the Console output, or a clear step-by-step trace), then a plain explanation. Some pieces of these systems — the Animator graph, the Input bindings, the UI layout — are normally built by hand in the Unity Editor rather than typed as code. For those, the diagrams and traces show you what the Editor is doing under the hood.

1. Overview: the five systems

Every one of these systems is a Component you attach to a GameObject — exactly like the Components you already met in 4.1. They just happen to be built into Unity instead of ones you write yourself.

GameObject "Player" +-- Transform (position, rotation, scale -- every GameObject has one) +-- Rigidbody (PHYSICS: mass, gravity, velocity) +-- CapsuleCollider (PHYSICS: the shape other things bump into) +-- Animator (ANIMATION: plays clips, runs the state machine) +-- AudioSource (AUDIO: plays an AudioClip in the scene) +-- PlayerController (your own script -- reads INPUT, talks to the rest) Canvas "HUD" (a separate GameObject tree, drawn on screen) +-- HealthBar (Image) +-- ScoreText (Text) +-- PauseButton (Button) UI system

A typical frame touches all five: Input reads what the player is doing right now, Physics moves and collides the Rigidbody, Animation picks which clip should play based on that movement, Audio plays a matching sound, and UI shows the result (health, score) on screen. The rest of this chapter walks through each one.

2. Reading input: movement and buttons

Unity has two ways to read input: the classic Input class (works immediately, zero setup) and the newer Input System package (more setup, more power). Learn the classic one first — plenty of shipped games still use it, and the ideas carry over directly once you move to the new one.

using UnityEngine;

public class InputDemo : MonoBehaviour
{
    void Update()
    {
        // GetAxis returns a smoothed value from -1 to 1.
        // "Horizontal" = A/D or Left/Right arrows (or a gamepad stick)
        // "Vertical"   = W/S or Up/Down arrows
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        if (h != 0f || v != 0f)
            Debug.Log($"move: ({h:F1}, {v:F1})");

        if (Input.GetButtonDown("Jump"))   // default binding: Space bar
            Debug.Log("Jump pressed!");

        if (Input.GetKeyDown(KeyCode.E))   // a specific key, no Input Manager entry needed
            Debug.Log("E pressed - interact!");
    }
}

Console output after holding the D key for a moment, then tapping Space, then tapping E:

move: (1.0, 0.0)
move: (1.0, 0.0)
move: (1.0, 0.0)
Jump pressed!
E pressed - interact!

"Horizontal" and "Vertical" are not magic strings — they are named axes defined in Edit > Project Settings > Input Manager, and Unity ships with those two, plus "Jump", already set up. GetAxis smooths the value over a few frames (it eases from 0 toward 1, which feels natural for movement); GetAxisRaw gives you the instant -1/0/1 with no smoothing, which some games prefer for tight, snappy controls.

Three ways to check a button, and the difference matters:

Common mistake Using GetButton where you meant GetButtonDown. A jump written with GetButton re-fires every single frame the key is held — dozens of times a second — launching the character over and over. If an action should happen once per press, it is almost always GetButtonDown.

The Input System package: actions instead of strings

Newer Unity projects use the Input System package (com.unity.inputsystem, installed separately through the Package Manager). Instead of hardcoded strings, you define Input Actions (like "Move" or "Jump") in an asset, then bind each action to one or more real controls — keyboard keys, a gamepad stick, a touchscreen swipe. Your script only asks for the action's current value; it does not care which physical device produced it.

using UnityEngine;
using UnityEngine.InputSystem;

public class InputSystemDemo : MonoBehaviour
{
    public InputAction moveAction;   // bound to WASD + left stick in the Inspector

    void OnEnable()  { moveAction.Enable(); }
    void OnDisable() { moveAction.Disable(); }

    void Update()
    {
        Vector2 move = moveAction.ReadValue<Vector2>();
        if (move != Vector2.zero)
            Debug.Log($"move: {move}");
    }
}

The payoff: one action, "Move", can be bound to keyboard and gamepad and touch at the same time, and players can rebind keys without you touching code. That matters for a shipping game (controller support, accessibility), but it adds real setup — installing the package, creating an Input Actions asset, wiring bindings in a graphical editor. For the rest of this chapter the classic Input class is enough to keep the examples short.

Tip Do not feel behind if you only know the classic Input class. It still works, it is still used in real projects, and everything about reading a value each frame and reacting to it transfers directly once you pick up the Input System package later.

3. Physics building blocks: Rigidbody and Colliders

Unity's physics engine only pays attention to a GameObject if it has the right components. Two matter here:

GameObject "Crate" GameObject "Ground" +-- Transform +-- Transform +-- BoxCollider +-- BoxCollider +-- Rigidbody <-- has mass, (no Rigidbody -> never moves, falls, can be pushed acts as a static, solid obstacle)

Once a Rigidbody exists, Unity's gravity pulls it down every physics step automatically — you do not write a "fall" script. You read and change its motion through the Rigidbody's own properties instead of moving the Transform directly:

using UnityEngine;

public class RigidbodyPeek : MonoBehaviour
{
    Rigidbody rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        rb.mass = 2f;                                   // heavier -> needs more force to push
        rb.linearVelocity = new Vector3(0f, 5f, 0f);     // an initial upward "pop"
    }

    void Start()
    {
        Debug.Log($"mass={rb.mass}, velocity={rb.linearVelocity}");
    }
}

Output:

mass=2, velocity=(0.00, 5.00, 0.00)

(Older Unity versions call this property rb.velocity instead of rb.linearVelocity — same idea, renamed in newer versions.) Setting linearVelocity directly is a hard override, handy for a one-off launch. For steady, physically-reactive motion — a car engine, a rocket thruster — you instead call rb.AddForce(...) every physics step, which nudges the velocity rather than replacing it. More on where to call that in section 5.

Tip A Collider with no Rigidbody is not "broken" — it is the normal setup for anything that never moves: the ground, walls, static level geometry. Only add a Rigidbody to things that should actually move or fall. (Chapter 10, Physics for Games, goes much deeper into rigid-body dynamics and collision detection if you want the full theory later.)

4. Collider vs Trigger

Every Collider has an Is Trigger checkbox in the Inspector. That one checkbox splits its behavior into two completely different modes:

SOLID collider (Is Trigger = OFF) TRIGGER collider (Is Trigger = ON) ball --> [WALL] ball --> (goal zone) ball --> [WALL] bounces off ball ----------> passes straight through physically blocked OnTriggerEnter still fires! used for: walls, floors, crates used for: pickups, checkpoints, damage zones, level exits

Your script finds out about either kind through a matching pair of callback methods that Unity calls automatically — you never call these yourself, Unity calls them when a contact happens:

solid-vs-solid contact trigger contact -------------------------- -------------------------- OnCollisionEnter(Collision c) OnTriggerEnter(Collider other) OnCollisionStay(Collision c) OnTriggerStay(Collider other) OnCollisionExit(Collision c) OnTriggerExit(Collider other)
using UnityEngine;

public class ContactDemo : MonoBehaviour
{
    // Fires when THIS object's solid collider physically hits another.
    void OnCollisionEnter(Collision collision)
    {
        Debug.Log($"bumped into {collision.gameObject.name}");
    }

    // Fires when THIS object's TRIGGER collider overlaps another collider.
    void OnTriggerEnter(Collider other)
    {
        Debug.Log($"entered trigger zone of {other.gameObject.name}");
    }
}

Worked trace: say this script sits on a ball that has a normal (non-trigger) SphereCollider and a Rigidbody. The ball rolls into a wall (solid collider) — OnCollisionEnter fires, printing bumped into Wall, and the ball physically stops. A few seconds later the ball rolls into a "Coin" GameObject whose collider has Is Trigger checked — the ball does not stop, it rolls straight through the coin, but OnTriggerEnter fires, printing entered trigger zone of Coin. Same ball, same script, two different collider settings, two different outcomes.

One requirement trips people up: at least one of the two objects in any contact (collision or trigger) must have a Rigidbody, or Unity will not detect the contact at all. A static wall (Collider only, no Rigidbody) still works because the other object — your player — has the Rigidbody.

Common mistake Checking "Is Trigger" on a collider and then wondering why OnCollisionEnter never fires — or leaving it unchecked and wondering why OnTriggerEnter never fires. The two callback families are mutually exclusive per collider: a trigger collider only ever produces OnTrigger* calls, a solid collider only ever produces OnCollision* calls.

5. Moving with physics: why FixedUpdate

You already met Update and FixedUpdate in 4.2. Here is why the split matters specifically for physics. Update runs once per rendered frame, and frames are not evenly spaced — 60 of them in one second, maybe 144 in another, depending on how fast the machine is. FixedUpdate runs on a fixed timer (0.02 seconds by default, i.e. 50 times a second) no matter how fast or slow rendering is. Unity's physics engine always steps on that fixed timer, so any code that touches a Rigidbody's motion belongs in FixedUpdate, not Update.

Update (once per RENDERED frame -- uneven spacing) |--|---|--|----|--|---|--| <-- fast machine: many, irregular calls FixedUpdate (fixed timer -- always evenly spaced) |----|----|----|----|----| <-- exactly every 0.02s, no matter the framerate

If you write physics code in Update, the amount of "push" you apply each call depends on how often Update happened to run that second — so the same script moves the object at different real speeds on different machines, and can look jittery. Writing it in FixedUpdate keeps physics consistent everywhere.

The common pattern: read input in Update (so a fast key tap is never missed), apply it to the Rigidbody in FixedUpdate (so the physics stays smooth and frame-rate independent):

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class PhysicsMover : MonoBehaviour
{
    public float speed = 6f;

    Rigidbody rb;
    Vector3 inputDir;     // cached between Update calls

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

    void Update()
    {
        // Read input every rendered frame so a quick tap is never missed.
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        inputDir = new Vector3(h, 0f, v);
    }

    void FixedUpdate()
    {
        // Apply it to the physics body on the fixed timer.
        Vector3 targetPos = rb.position + inputDir * speed * Time.fixedDeltaTime;
        rb.MovePosition(targetPos);
    }
}

Time.fixedDeltaTime is that fixed step size (0.02 by default) — multiplying by it converts "units per second" into "units for this one physics step", exactly like Time.deltaTime did for Update in 4.2. rb.MovePosition moves the Rigidbody the physically-correct way: it still collides with walls and other objects along the way, unlike setting transform.position directly, which teleports through everything.

Common mistake Writing transform.position += ... on an object that also has a Rigidbody. This silently fights the physics engine — the Rigidbody expects to own that object's position, and moving the Transform behind its back causes jitter, tunneling through walls, or collisions that stop registering. If it has a Rigidbody, move it with rb.MovePosition or rb.AddForce, inside FixedUpdate.

AddForce is the other tool, for anything that should feel like it has weight and momentum (a car, a ball, a ragdoll) instead of tight direct control:

void FixedUpdate()
{
    rb.AddForce(inputDir * accelForce);   // a push; velocity builds up over time
}

MovePosition sets where the body should be right now (still collision-aware) — good for a tightly-controlled platformer character. AddForce pushes the body and lets momentum and mass do the rest — good for anything that should feel heavy or slippery. Chapter 10 (Physics for Games) covers character controllers, ragdolls, and gameplay integration in much more depth; this section is the everyday 80% you need right now.

6. Animation: the Animator state machine

The Animator component plays animation clips on a GameObject. What it plays, and when it switches clips, is controlled by an asset called an Animator Controller — Unity's animation system is often called Mecanim, after the internal name of this system. You build the Animator Controller visually, in the Animator window, as a state machine: a graph of states (each one usually holding one animation clip, like "Idle" or "Run") connected by transitions (arrows that say "switch to this state when...").

[Entry] | v +-------------+ +---> | Idle | <---+ | +-------------+ | Speed<0.1 | Speed>0.1 | | v | | +-------------+ | +------| Walk |-----+ | ^ Speed>5 | | Speed<5 v | +-------------+ | Run | +-------------+ [Any State] --Jump (trigger)--> [Jump State]

Read the diagram like a flowchart: the character starts in Idle. Each transition has a condition written in terms of Parameters — named values that live on the Animator, similar to variables. When Speed climbs above 0.1, the Idle -> Walk transition fires and the Walk clip starts playing. The Any State box is special: its transitions can fire no matter which state you are currently in, which is how a "Jump" can interrupt Idle, Walk, or Run alike.

Four parameter types cover almost everything:

Tip Use Bool for a condition that stays true for a while (IsRunning, IsGrounded). Use Trigger for a one-off event (Jump, Attack, Hit). Using Bool for a one-off event is a common source of "it played twice" or "it never played again" bugs, because you forget to flip it back off.

7. Driving animation from code

You build the graph in the Editor, but your script sets the parameter values every frame, and the state machine reacts on its own. This is the normal way to connect gameplay to animation — you almost never tell the Animator "play this clip" directly; you tell it a fact about the world ("current speed is 4.2") and let the transitions decide.

using UnityEngine;

[RequireComponent(typeof(Animator))]
public class AnimationDriver : MonoBehaviour
{
    Animator animator;

    void Awake() { animator = GetComponent<Animator>(); }

    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        float speed = new Vector2(h, v).magnitude;   // 0 when still, up to ~1 when moving

        animator.SetFloat("Speed", speed);            // feeds the Idle/Walk/Run thresholds
        animator.SetBool("IsGrounded", CheckGrounded());

        if (Input.GetButtonDown("Jump"))
            animator.SetTrigger("Jump");              // fires once, resets itself
    }

    bool CheckGrounded() { return true; }   // stand-in; a real project raycasts downward
}

Worked trace, following the state machine from section 6: the player is standing still, Speed = 0, the Animator is in Idle. The player presses D — each Update now computes a bigger speed, say it reaches 0.6. Because 0.6 > 0.1, the Idle -> Walk transition condition becomes true, and the Animator switches to Walk, so the walking clip starts playing — you never called "play walk clip" anywhere, you only ever set Speed. The player then hits Space: SetTrigger("Jump") fires the Any State -> Jump transition immediately, interrupting Walk, and the trigger clears itself so it will not fire again next frame.

There is also a direct escape hatch, animator.Play("StateName"), which jumps straight to a state and skips its transition conditions entirely. It is useful for forcing a reset (say, snapping back to Idle when a cutscene ends) but overusing it defeats the point of having a state machine — most of the time, setting parameters and letting transitions decide is what you want.

Common mistake Misspelling a parameter name in SetFloat/SetBool/SetTrigger — Unity does not catch this at compile time, since the name is just a string. It fails silently at runtime: no error, the animation just never changes. Double-check the parameter name in the Animator window matches the string in your script exactly, including capitalization. (Chapter 9, Animation, goes deeper into blending, layers, and skeletal rigs beyond what Mecanim's state machine handles here.)

8. Audio: AudioSource and AudioClip

Two pieces, and the split mirrors what you already know from graphics: an AudioClip is the actual sound data (a file you imported, like a Texture is imported image data), and an AudioSource is a Component that plays a clip out into the scene (like a Renderer displays a Texture). No AudioSource, no sound — even with a clip assigned.

using UnityEngine;

public class AudioDemo : MonoBehaviour
{
    public AudioSource source;     // assigned in the Inspector
    public AudioClip jumpClip;
    public AudioClip musicClip;

    void Start()
    {
        // Background music: assign + loop + play, keeps playing across frames.
        source.clip = musicClip;
        source.loop = true;
        source.Play();
    }

    public void PlayJumpSound()
    {
        // A short one-off sound effect. PlayOneShot does NOT interrupt
        // whatever the AudioSource is already playing (the music keeps going).
        source.PlayOneShot(jumpClip);
    }
}

Call PlayJumpSound() from the same place you call animator.SetTrigger("Jump") in section 7, and the jump sound effect layers on top of the music without cutting it off. That is the difference between Play() (takes over the AudioSource's main clip, replacing whatever was playing) and PlayOneShot(clip) (plays an extra sound alongside the main clip, fire-and-forget — handy for footsteps, hits, and pickups that can overlap each other).

2D vs 3D sound

Every AudioSource has a Spatial Blend slider from 0 to 1:

2D sound (Spatial Blend = 0) 3D sound (Spatial Blend = 1) volume: =========== volume: === same everywhere ===== = = [Listener] .... [Source] [Source] <-- distance --> [Listener] anywhere on screen louder close, quieter far, panned L/R
void Awake()
{
    source.spatialBlend = 1f;     // 0 = flat/2D, 1 = full 3D positional
    source.minDistance   = 2f;    // stays at full volume within this range
    source.maxDistance   = 20f;   // fades to silent past this range
}

Footstep or gunshot AudioSources normally live on the GameObject making the sound (so their position tracks it automatically), set to full 3D. Music and UI sounds normally live on a fixed manager object, set to 2D, so they never fade out. (Chapter 13, Audio, covers spatial audio, mixing, and middleware like Wwise/FMOD that big studios layer on top of Unity's built-in AudioSource.)

9. UI: Canvas and UGUI elements

Unity's classic UI system is called UGUI ("Unity GUI") — it builds menus and HUDs out of ordinary GameObjects, the same GameObject/Component model from 4.1, just with UI-specific components. Every UGUI element must live under a Canvas, a special GameObject that is the root of one UI tree and decides how it gets drawn.

Canvas (Render Mode: Screen Space - Overlay) +-- HealthBar (Image) -- a rectangle filled with a sprite/color +-- ScoreText (Text) -- draws text +-- PauseButton (Button) -- Image + Text + click handling, combined +-- Text ("Pause") -- Button auto-creates a child Text label EventSystem -- a separate GameObject Unity needs in every UI scene; it routes clicks/taps to the UI

Every UI element sits on a RectTransform instead of a plain Transform — same idea (position, rotation, scale) plus a rect (width, height) and anchors, which say how the element should stick to a corner or edge as the screen resizes (a health bar anchored to the top-left stays in the top-left on a phone and on a monitor, instead of drifting).

The Canvas's Render Mode decides where in 3D space the UI actually lives:

Three elements cover most of a first UI: Image (a rectangle showing a sprite or a flat color — a health bar fill, an icon, a panel background), Text (draws a string — score, dialogue, labels; most current projects use the improved TextMeshPro version, usually written TMP_Text, which gives sharper rendering and richer formatting), and Button (an Image plus click detection, covered next).

Tip A GameObject under a Canvas with no RectTransform is not possible — Unity swaps in a RectTransform automatically the moment a GameObject becomes a child of a Canvas. That is your signal you are looking at UI, not regular 3D content. (Chapter 12, UI/UX Programming, goes further into the newer UI Toolkit, complex inventory grids, and localization.)

10. Hooking a Button to code

A Button component fires a UnityEvent called onClick whenever it is pressed and released while the pointer stays inside it. There are two ways to connect that event to your code, and both end up calling the same method:

Way 1: the Inspector Way 2: from code -------------------------- --------------------------- Button -> On Click () list button.onClick.AddListener(Method) drag in a GameObject + pick no dragging, no Inspector setup, a public method from a dropdown good when the button is spawned at runtime (e.g. an inventory slot)
using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    public Button healUpButton;
    public Image healthFillImage;   // Image type set to "Filled" in the Inspector

    int health = 60;
    const int maxHealth = 100;

    void Start()
    {
        healUpButton.onClick.AddListener(OnHealUpClicked);   // Way 2, wired in code
        RefreshBar();
    }

    void OnHealUpClicked()
    {
        health = Mathf.Min(health + 20, maxHealth);
        Debug.Log($"Button clicked! Health: {health}");
        RefreshBar();
    }

    void RefreshBar()
    {
        healthFillImage.fillAmount = (float)health / maxHealth;   // 0..1
    }
}

Trace: health starts at 60, so RefreshBar sets fillAmount to 0.6 — the bar shows 60% full. The player clicks the button once. OnHealUpClicked runs, health becomes 80, the Console prints Button clicked! Health: 80, and fillAmount updates to 0.8. Click it twice more and health is clamped at maxHealth = 100 by Mathf.Min, so it stops growing instead of overflowing past the bar.

Wiring it in the Inspector instead (Way 1) needs zero lines for the connection itself — you just drag the HealthUI GameObject into the button's On Click () list and choose OnHealUpClicked from the dropdown. Both ways call the exact same method; pick the Inspector way for buttons that already exist in the scene, and the code way for buttons your script creates or finds at runtime.

Tip A public method wired to a Button's onClick takes zero parameters (or exactly one string/int/float/bool/Object argument, if set up that way in the Inspector). If your method needs more information than that, have it read from fields on the same script instead of trying to pass extra arguments through the click event.

11. A note on render pipelines: Built-in, URP, HDRP

Everything above — physics, animation, audio, UI — works the same no matter how the scene actually gets drawn to the screen. That drawing job belongs to the render pipeline: the code that turns your scene's GameObjects, materials, and lights into pixels each frame. Unity ships three options, chosen once per project:

Built-in ---- older, flexible, being phased out URP ---- scales mobile -> console -> PC (lighter features, wide reach) HDRP ---- PC / console only, NOT mobile-capable (heavy features, best fidelity) mobile phone console / gaming PC | | URP <---- one pipeline, both ends | HDRP (PC/console only)

This is why the goal named in this curriculum's intro — HoYoverse-style games (Genshin Impact, Honkai: Star Rail, Zenless Zone Zero) — points straight at URP. Those games have to run on mid-range phones as well as PC and console from the same project, and HDRP is simply not an option on a phone GPU. URP's whole design goal is exactly that spread: acceptable, tunable performance across a very wide range of hardware, mobile included. If your target is a stylized, mobile-reaching game, default to URP unless you have a specific, proven reason not to.

Picking a pipeline is a project-level setting, not something you script here — this section is meant only to make the choice make sense. Shaders, lighting, and the stylized "toon" look HoYoverse is known for get their own dedicated later chapter (Chapter 7, Graphics & Rendering, especially 7.4, Stylized / Toon / NPR). For now, the one decision that matters: new project aimed at mobile or wide reach -> URP.

12. Glossary

13. Exercises

Exercise 1 A GameObject called "Coin" has a SphereCollider with Is Trigger checked, but no Rigidbody. The player has a CapsuleCollider (not a trigger) and a Rigidbody. The Coin's script only has this method:
void OnCollisionEnter(Collision collision)
{
    Debug.Log("collected " + collision.gameObject.name);
}
The player walks straight through the coin. Does the log line print? If not, what is the one-line fix?
Show answer

No, it does not print. The Coin's collider has Is Trigger checked, so it only ever produces OnTrigger* calls, never OnCollision* calls — the two families are mutually exclusive per collider, as covered in section 4. The fix is to rename the method (and its parameter type) to the trigger version:

void OnTriggerEnter(Collider other)
{
    Debug.Log("collected " + other.gameObject.name);
}

Note the parameter type also changes, from Collision to Collider — Unity passes different information for the two families. (The Rigidbody requirement is already satisfied here: the player has one, and at least one side of any contact needs one.)

Exercise 2 This script is attached to a GameObject that has a Rigidbody. In testing, the character sometimes clips straight through walls, and its movement speed changes depending on the machine's frame rate.
void Update()
{
    float h = Input.GetAxis("Horizontal");
    transform.position += new Vector3(h, 0, 0) * speed;
}
Name both problems, and rewrite it correctly using what you learned in section 5.
Show answer

Problem 1 (frame-rate dependence): the move amount is never multiplied by any time step, so on a machine running Update at 144 FPS the object moves noticeably farther per second than at 60 FPS — more calls per second, same fixed amount added each call. It needs * Time.deltaTime at minimum.

Problem 2 (tunneling through walls): writing directly to transform.position bypasses the physics engine entirely — it teleports the Transform instead of moving the Rigidbody through the world, so collisions are never checked along the way and the object can end up inside or past a wall. Since this GameObject has a Rigidbody, movement should happen in FixedUpdate, through the Rigidbody's own methods:

Rigidbody rb;
Vector3 inputDir;

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

void Update()
{
    float h = Input.GetAxis("Horizontal");
    inputDir = new Vector3(h, 0, 0);
}

void FixedUpdate()
{
    rb.MovePosition(rb.position + inputDir * speed * Time.fixedDeltaTime);
}

Input is still read every rendered frame (so no tap gets missed), but the actual move happens on the fixed physics timer through rb.MovePosition, which is collision-aware and frame-rate independent.

Exercise 3 Using the state machine from section 6 (Idle <-> Walk on Speed 0.1, Walk <-> Run on Speed 5, Any State -> Jump on the Jump trigger), trace the Animator's current state after each line below runs in order, starting from Idle:
animator.SetFloat("Speed", 3f);
animator.SetFloat("Speed", 6f);
animator.SetTrigger("Jump");
animator.SetFloat("Speed", 0f);
Show answer

Step through it against the diagram in section 6:

  • SetFloat("Speed", 3f)3 > 0.1, so Idle -> Walk. (3 is not > 5, so it does not reach Run.)
  • SetFloat("Speed", 6f)6 > 5, so Walk -> Run.
  • SetTrigger("Jump") — the Any State transition fires regardless of Speed, so Run -> Jump. The trigger then resets itself automatically.
  • SetFloat("Speed", 0f) — this line only changes the Speed parameter. It does not by itself move the Animator out of Jump; a real Jump state normally has its own transition (for example, back to Idle/Walk/Run once the jump clip finishes playing, or once IsGrounded becomes true again) that this simplified example does not model. So the state stays Jump.

The catch in the last step is the point of the exercise: setting a parameter only fires the transitions that actually depend on it. A state machine only leaves a state through a transition that is actually wired up — nothing happens automatically just because time passes or some other parameter changed.

That covers the five systems. Input tells your script what the player wants; Physics (Rigidbody, Colliders, FixedUpdate) turns that into believable motion and contact; Animation (the Animator state machine) shows that motion on screen; Audio gives it a voice; UI reports the results back to the player. The render pipeline note is the one project-wide choice sitting underneath all of it: pick URP for anything that has to reach mobile — exactly the HoYoverse-style target this curriculum is aimed at.

← Back to all chapters