Unity Mastery Handbook
Topic chapterT08P1 · Foundations: Editor, C#, Maths, Physics, 2D/3D

3D Physics: Rigidbodies, Colliders, Raycasts & Character Controllers

How Unity's 3D physics works and how to work with it: colliders and the shapes they really have, Rigidbodies and the four ways to push them, the fixed step and interpolation, collision and trigger messages, physics materials, layers and the collision matrix, raycasts, shape casts and overlaps, joints, and the three ways to build a character. The guided build brings a storm to Lighthouse Island, with supply crates that float, blow about, get pushed and get carried.

Study time
10–14 hours
Reading time
158 min read · 12,971 words
Prerequisites
T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable, T06 · Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation, T07 · Input System, Movement & Cameras (Cinemachine)
Courses merged
6 course files
Your progress
—
After this chapter you can…
  • Choose the right collider for an object, and explain why a flattened cylinder's Capsule Collider becomes a sphere
  • Set up static, dynamic and kinematic bodies, and move each one the right way: forces, velocity or MovePosition
  • Explain the fixed step, why physics code goes in FixedUpdate, and when to turn on interpolation
  • Predict which collision and trigger messages fire for any pair of objects, and use them for damage, pickups and zones
  • Control what collides with what using layers, the collision matrix, layer overrides and physics materials
  • Query the world with raycasts, shape casts and overlap tests, with layer masks and without per-frame garbage
  • Join bodies with hinges and springs, and choose between a CharacterController, a dynamic Rigidbody and a kinematic one for a character

Physics is how a game world pushes back. A crate falls and lands, a ball rolls down a slope, a door swings when you walk into it, a bullet stops at a wall. You describe the shapes and the masses; Unity's physics engine works out what happens next, fifty times a second.

This chapter covers Unity's built-in 3D physics: the components, the rules for when their messages fire, the queries you use to ask "what's over there?", and the three ways to build a character on top of it. Every script compiles against Unity 6.3 with no warnings and is in the sandbox under Assets/Handbook/T08. You need T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable's lifecycle (especially FixedUpdate), T06 · Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation's vectors, and T07 · Input System, Movement & Cameras (Cinemachine)'s keeper, who walks the island with a CharacterController.

The big picture#

Unity's 3D physics is built on NVIDIA PhysX. You never call PhysX yourself: you add components, and Unity hands their shapes and settings to PhysX and copies the results back to the Transforms. Each time the simulation runs, it does the same four things:

  1. Move. Every body that physics controls gets its velocity changed by gravity and by any forces you added, and moves a little.
  2. Detect. PhysX finds every pair of shapes that now touch or overlap.
  3. Solve. It pushes overlapping bodies apart and works out bounces and friction, so nothing ends up inside anything else.
  4. Report. Unity sends your scripts messages about what started touching, kept touching and stopped touching: OnCollisionEnter, OnTriggerExit and the rest.

Two components do almost all of this. A Collider gives an object a shape. A Rigidbody says physics may move it. An object's physics role depends on which of the two it has:

Static collider Collider, no Rigidbody Never moves Others bump into it ground, walls, rocks Dynamic Rigidbody Collider + Rigidbody Physics moves it: gravity, forces, bounces crates, balls, debris Kinematic Rigidbody … with Is Kinematic ticked Your script moves it Pushes dynamic bodies, isn't pushed back platforms, lifts, doors
The three kinds of physics object. What decides it is whether there's a Rigidbody, and whether it's kinematic.

A fourth kind sits beside these: the CharacterController from T07 · Input System, Movement & Cameras (Cinemachine). It's a capsule collider with its own movement code. It collides with the world but isn't moved by physics, which makes it predictable for players and awkward for anything physical. The last part of this chapter compares the ways to build a character.

The other thing to hold on to from the start: physics doesn't run every frame. It runs on a fixed step, 0.02 seconds by default, whatever the frame rate. That one fact explains why physics code lives in FixedUpdate, why fast objects can pass through walls, and why a Rigidbody can look jittery under a moving camera. It has a section of its own.

Colliders: the shape physics sees#

A collider is a shape for physics. It's separate from what you see: the Mesh Renderer draws the object, and the collider is an invisible stand-in that physics uses for contacts and queries. They don't have to match, and usually shouldn't match exactly. A detailed barrel can be a plain capsule to physics. Simple shapes are faster to test, and they roll and slide more smoothly than detailed ones.

Select an object and its collider shows as a green wireframe in the Scene view. That wireframe, not the mesh, is what other objects bump into.

The primitive shapes#

Unity has three primitive colliders. They're the cheapest to test and the ones to reach for first.

ColliderShapeSettingsGood for
Box ColliderA boxCenter, SizeCrates, walls, floors, buildings, anything boxy
Sphere ColliderA ballCenter, RadiusBalls, rough round objects, pickup zones
Capsule ColliderA cylinder with a half-sphere on each endCenter, Radius, Height, Direction (X, Y or Z)People, creatures, barrels, poles

When you create a 3D object from the GameObject > 3D Object menu, it gets a matching collider: a Cube gets a Box Collider, a Sphere gets a Sphere Collider, a Capsule and a Cylinder both get a Capsule Collider. There's no cylinder collider, and that causes a classic problem.

Why the beach was a giant ball#

In T02 · The Unity Editor: Scenes, GameObjects, Components & Prefabs you built Lighthouse Island's beach from a Cylinder scaled (34, 0.3, 34): a wide, flat disc. Its collider was the default Capsule Collider, and in T07 · Input System, Movement & Cameras (Cinemachine) you found it was a ball 34 m across, so you replaced it with a Mesh Collider before the keeper could walk on it. Here's why.

A capsule is a cylinder with a half-sphere cap at each end, and its height includes the caps. So a capsule can't be shorter than it is wide: if the height is less than twice the radius, there's no cylinder part left and the shape is just a sphere of that radius. Scaling makes it worse. A primitive collider must stay its own shape, so under a non-uniform scale Unity picks one size: for a Y-axis capsule, the radius follows the largest of the X and Z scales, and the height follows Y. Scale (34, 0.3, 34) makes the radius 0.5 × 34 = 17 m and the height 2 × 0.3 = 0.6 m. A 0.6 m capsule with a 17 m radius can only be a 17 m sphere.

The same rule applies to Sphere Colliders (scaled by the largest axis) and explains a whole family of bugs: a squashed sphere that still rolls like a ball, a stretched capsule that's fatter than it looks. When an object is scaled unevenly, check its collider in the Scene view. The fixes are a Box Collider for flat things, a Mesh Collider for odd shapes, or a child object with its own collider and a sensible scale.

Compound colliders#

An object can have several colliders: several on one GameObject, or one on each of several children. If they all sit under one Rigidbody, physics treats them as a single rigid shape, a compound collider. A chair can be a box for the seat, a box for the back and four thin boxes for the legs. A boat can be a box for the hull and a capsule for the mast.

Compound colliders made of primitives are usually better than one Mesh Collider: they're faster, they're accurate where it matters, and they're allowed on moving bodies without restrictions. The Rigidbody goes on the root; the colliders go wherever is convenient underneath it.

Mesh Colliders#

A Mesh Collider uses a mesh's actual triangles. It's the right choice for static scenery with shapes that primitives can't match: terrain-like ground, rocks, the island's beach. It has one important switch:

  • Non-convex (the default): the exact triangles, holes and hollows included. Only allowed on static colliders and kinematic bodies. A dynamic Rigidbody can't use one, and Unity says so in the Console if you try.
  • Convex: Unity wraps the mesh in a convex hull, the shape a sheet of cling film would make around it, with no dents or holes. Dynamic bodies can use these. The hull is limited to 255 triangles, so detailed meshes get simplified. A convex collider can also be a trigger; a non-convex one can't.

A mug as a convex Mesh Collider has no hole for the handle and no hollow inside: drop a marble in and it rests on top. If the hollow matters, build the mug from several primitive colliders instead.

Other colliders#

  • Terrain Collider: the collider for a Unity Terrain, built from its heightmap. It's added with the Terrain.
  • Wheel Collider: a special collider for vehicles, with suspension and tyre friction built in. Vehicle physics is a big topic of its own, which T65 · Vehicles & Racing: Arcade & Simulation Handling, Wheel Colliders, Drifting, AI Racers, Laps & Chase Cameras covers; the hovercraft exercise shows a simpler way to build a vehicle.

Contact offset#

PhysX starts generating contacts a little before shapes actually touch, so it can react smoothly instead of after they overlap. That distance is the collider's contact offset, 0.01 m by default (Default Contact Offset in the Physics settings). You rarely change it, but it explains why two resting boxes have a hair's gap between them in the Scene view. It's also why colliders much smaller than about 0.05 m behave badly: the offset is large compared with the shape.

Rigidbodies#

Add a Rigidbody to an object with a collider and physics takes over: press Play and it falls, lands, tips over and slides. Its settings, as they appear in the 6.3 Inspector:

SettingDefaultWhat it does
Mass1Kilograms. Matters when bodies push each other or when you use force modes that include mass. It doesn't change how fast things fall: without air resistance, everything falls at the same rate.
Linear Damping0Slows movement over time, like air resistance. 0 is none; a few per second feels underwater.
Angular Damping0.05Slows spinning over time.
Automatic Center of MassonWorks out the centre of mass from the colliders. Turn off to set it yourself: a low centre of mass stops cars rolling over.
Automatic TensoronWorks out how hard it is to spin around each axis, from the colliders.
Use GravityonWhether gravity pulls it.
Is KinematicoffPhysics stops moving it; your script does. See kinematic bodies.
InterpolateNoneSmooths its drawn position between physics steps. See interpolation.
Collision DetectionDiscreteHow hard physics tries to catch fast-moving collisions. See tunnelling.
ConstraintsnoneFreeze movement along, or rotation around, any world axis.
Layer OverridesnoneInclude or exclude layers for this body's colliders. See layers.

Rigidbodies and the hierarchy#

A Rigidbody moves its own GameObject and everything under it. Its colliders are the ones on the same object and on children that don't have a Rigidbody of their own. Two rules follow:

  • Don't nest dynamic Rigidbodies. A dynamic body under another dynamic body fights its parent: both try to move the child. If two parts need to move separately, make them siblings and connect them with a joint.
  • Scale the visuals, not the body. Scaling a Rigidbody's GameObject unevenly scales its colliders in ways primitives can't follow (the beach problem again). Keep the root at scale (1, 1, 1) and put scaled meshes and colliders on children.

Mass, and keeping it sensible#

Mass only matters relative to other masses. A 20 kg crate hit by a 1 kg ball barely moves; a 1 kg crate hit by the same ball flies off. The solver handles that well. It struggles when very heavy and very light bodies are stacked or jointed together: a 1,000 kg block resting on a 1 kg box jitters, and a chain of light links holding a heavy weight stretches. A useful rule of thumb is to keep bodies that touch or connect within about ten times each other's mass, and to give things plausible real-world masses rather than leaving everything at 1.

Sleeping#

A body that has come to rest goes to sleep: physics stops simulating it until something touches it, a force is added, or one of its properties changes. Sleeping is why a scene with a thousand crates lying still costs almost nothing. The threshold is the Sleep Threshold in the Physics settings (0.005 by default, a measure of motion energy per kilogram). You rarely need to touch it. If a body hangs in mid-air after the floor under it is removed by a script, call rb.WakeUp(); moving or destroying a collider normally wakes the bodies resting on it.

Moving bodies: forces, velocity and MovePosition#

Once an object has a dynamic Rigidbody, physics owns its position. You don't set transform.position on it; you tell physics what you'd like and let it move the body. There are three ways to do that, from most physical to most direct.

Adding forces#

AddForce(vector, mode) changes a body's velocity. The force mode says how to interpret the vector: whether mass counts, and whether it's spread over the step or applied all at once.

ForceModeUses mass?AppliedVelocity change per callUse it for
Force (default)yesover one stepvector ÷ mass × step timeEngines, wind on sails, anything pushed continuously
Accelerationnoover one stepvector × step timeContinuous effects that should move everything equally: custom gravity, steering
Impulseyesat oncevector ÷ massA kick, a hit, a gunshot
VelocityChangenoat oncevectorJumps, jump pads, "go this fast now"

Here's what one call of AddForce with a strength of 10 does to a 2 kg body during one 0.02 s step, worked out by a small program in the sandbox:

StepMaths/Program.cs.NET consoleC#
using System;

namespace StepMaths
{
    // The arithmetic behind fixed steps, force modes, tunnelling and the Floater.
    static class Program
    {
        const double FixedStep = 0.02;           // Project Settings > Time > Fixed Timestep
        const double MaxAllowed = 1.0 / 3.0;     // Maximum Allowed Timestep (shown as 0.3333333)

        static void Main()
        {
            Console.WriteLine("Fixed steps per frame over one second");
            foreach (int fps in new[] { 30, 60, 144 })
                StepsPerFrame(fps);

            Console.WriteLine();
            Console.WriteLine("One slow frame");
            foreach (double hitch in new[] { 0.25, 0.5 })
            {
                double counted = Math.Min(hitch, MaxAllowed);
                int steps = (int)Math.Floor(counted / FixedStep + 1e-9);
                Console.WriteLine($"{hitch:F2} s frame: counted as {counted:F3} s, {steps} fixed steps in one frame");
            }

            Console.WriteLine();
            Console.WriteLine("One step of AddForce(10 units) on a 2 kg body");
            const double amount = 10, mass = 2;
            Console.WriteLine($"Force           {amount / mass * FixedStep,6:F2} m/s");
            Console.WriteLine($"Acceleration    {amount * FixedStep,6:F2} m/s");
            Console.WriteLine($"Impulse         {amount / mass,6:F2} m/s");
            Console.WriteLine($"VelocityChange  {amount,6:F2} m/s");

            Console.WriteLine();
            Console.WriteLine("Distance moved per fixed step (wall 0.3 m + ball 0.2 m = 0.5 m)");
            foreach (double speed in new[] { 10.0, 25.0, 60.0 })
            {
                double perStep = speed * FixedStep;
                string verdict = perStep > 0.5 ? "can pass straight through" : "always caught";
                Console.WriteLine($"{speed,3:F0} m/s: {perStep:F2} m per step, {verdict}");
            }

            Console.WriteLine();
            Console.WriteLine("Floater: where a 0.8 m crate settles");
            foreach (double buoyancy in new[] { 1.25, 2.0, 4.0 })
            {
                double under = 1.0 / buoyancy;
                Console.WriteLine($"buoyancy {buoyancy:F2}: {under * 100:F0}% under, {under * 0.8:F2} m deep");
            }
        }

        // A model of Unity's scheduling: each frame adds its time, and fixed steps run while a whole step fits.
        static void StepsPerFrame(int fps)
        {
            double frame = 1.0 / fps, accumulator = 0;
            int total = 0;
            string pattern = "";
            for (int i = 0; i < fps; i++)
            {
                accumulator += frame;
                int steps = 0;
                while (accumulator >= FixedStep - 1e-9)
                {
                    accumulator -= FixedStep;
                    steps++;
                }
                total += steps;
                if (i < 8) pattern += steps + " ";
            }
            Console.WriteLine($"{fps,3} fps: first frames {pattern}... total {total}");
        }
    }
}
Text
Fixed steps per frame over one second
 30 fps: first frames 1 2 2 1 2 2 1 2 ... total 50
 60 fps: first frames 0 1 1 1 1 1 0 1 ... total 50
144 fps: first frames 0 0 1 0 0 1 0 0 ... total 50

One slow frame
0.25 s frame: counted as 0.250 s, 12 fixed steps in one frame
0.50 s frame: counted as 0.333 s, 16 fixed steps in one frame

One step of AddForce(10 units) on a 2 kg body
Force             0.10 m/s
Acceleration      0.20 m/s
Impulse           5.00 m/s
VelocityChange   10.00 m/s

Distance moved per fixed step (wall 0.3 m + ball 0.2 m = 0.5 m)
 10 m/s: 0.20 m per step, always caught
 25 m/s: 0.50 m per step, always caught
 60 m/s: 1.20 m per step, can pass straight through

Floater: where a 0.8 m crate settles
buoyancy 1.25: 80% under, 0.64 m deep
buoyancy 2.00: 50% under, 0.40 m deep
buoyancy 4.00: 25% under, 0.20 m deep

The middle block is the one to remember. The same "10" gives anything from 0.1 m/s to 10 m/s depending on the mode. Force and Acceleration are meant to be added every step: called fifty times, the 10 N force adds 5 m/s over a second, the same as one 10 N·s impulse. Impulse and VelocityChange are meant to be called once. Calling an impulse every step is the most common force bug: the body takes off like a rocket. (The rest of the output comes up later in the chapter.)

Other ways to push:

  • AddRelativeForce: the vector is in the body's local space, so "forward" means its own forward. Good for thrusters.
  • AddForceAtPosition(force, point): pushes at a point, so it also spins the body if the point is off-centre. Good for anything hit on one side, and for the hovercraft in the exercises.
  • AddTorque and AddRelativeTorque: spin the body, with the same four modes.
  • AddExplosionForce: pushes away from a point, weaker towards the edge of the radius. The overlap section uses it.

Here's a ball you roll with the Move action and hop with Jump. It reads input in Update, because that runs every frame and a one-frame press can't be missed, and it applies forces in FixedUpdate, because that runs once per physics step:

BallRoller.csUnity sandboxC#
using UnityEngine;
using UnityEngine.InputSystem;

namespace Handbook.T08
{
    // Rolls a ball with forces. Input is read every frame; forces are applied every physics step.
    [RequireComponent(typeof(Rigidbody))]
    public class BallRoller : MonoBehaviour
    {
        [SerializeField] float acceleration = 12f;   // metres per second, per second
        [SerializeField] float hopSpeed = 5f;        // metres per second, added at once

        Rigidbody body;
        InputAction move, jump;
        Vector2 moveInput;
        bool hopQueued;

        void Awake()
        {
            body = GetComponent<Rigidbody>();

            InputActionAsset actions = InputSystem.actions;
            if (actions == null)
            {
                Debug.LogError("No project-wide actions. Assign them in Project Settings > Input System Package.", this);
                enabled = false;
                return;
            }
            move = actions.FindAction("Player/Move", throwIfNotFound: true);
            jump = actions.FindAction("Player/Jump", throwIfNotFound: true);
        }

        void Update()
        {
            moveInput = move.ReadValue<Vector2>();
            if (jump.WasPressedThisFrame())
                hopQueued = true;   // kept until the next physics step uses it
        }

        void FixedUpdate()
        {
            Vector3 push = new Vector3(moveInput.x, 0f, moveInput.y);
            body.AddForce(push * acceleration, ForceMode.Acceleration);

            if (hopQueued)
            {
                hopQueued = false;
                body.AddForce(Vector3.up * hopSpeed, ForceMode.VelocityChange);
            }
        }
    }
}

Put it on a Sphere with a Rigidbody and press Play. The ball takes a moment to get going and keeps rolling after you let go, because a force changes speed rather than setting it. That's what makes it feel physical, and why force-based control feels "floaty" for a character that should stop on a coin. It hops in mid-air too, because nothing checks for ground; the Rigidbody character adds that.

Setting the velocity#

You can also write rb.linearVelocity directly. Physics then carries on from that velocity, with gravity and collisions still working. It's the right tool when you want exact control: a character that moves at exactly its walking speed, a projectile launched at exactly 30 m/s, the keeper's push in the guided build.

The danger is overwriting what physics was doing. Set the whole velocity every step and gravity never builds up, so the body floats down slowly; bounces and hits vanish because you replace them. The usual pattern is to change only the part you care about:

Change the horizontal speed, keep the verticalC#
Vector3 v = body.linearVelocity;
v.x = targetSpeed.x;
v.z = targetSpeed.z;          // v.y (falling, jumping) is left alone
body.linearVelocity = v;

Adding the change with AddForce in VelocityChange mode does the same job and combines better with other forces added in the same step, which is why the Rigidbody character uses it.

Kinematic bodies and MovePosition#

A kinematic Rigidbody (Is Kinematic ticked) is moved by your code and ignores forces, gravity and collisions. It still collides with dynamic bodies, which bounce off it. That makes it right for things that follow a script exactly but must push things around: a moving platform, a lift, a swinging blade, a door that slides on rails.

Move a kinematic body with rb.MovePosition and rb.MoveRotation in FixedUpdate. These don't jump the body to the new pose: they move it there over the next step, with a velocity, so dynamic bodies resting on it are carried along by friction and bodies in the way are pushed rather than overlapped.

MovingPlatform.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // A kinematic platform that eases back and forth. Moving it with MovePosition gives it a
    // velocity, so Rigidbodies resting on it ride along through friction.
    [RequireComponent(typeof(Rigidbody))]
    public class MovingPlatform : MonoBehaviour
    {
        [SerializeField] Vector3 offset = new Vector3(0f, 0f, 6f);   // from the start position
        [SerializeField] float period = 6f;                          // seconds for a round trip

        Rigidbody body;
        Vector3 start;

        void Awake()
        {
            body = GetComponent<Rigidbody>();
            body.isKinematic = true;
            body.interpolation = RigidbodyInterpolation.Interpolate;
            start = body.position;
        }

        void FixedUpdate()
        {
            // 0 → 1 → 0 over one period, slowing at each end.
            float t = 0.5f - 0.5f * Mathf.Cos(Time.fixedTime * 2f * Mathf.PI / period);
            body.MovePosition(start + offset * t);
        }
    }
}

Put a crate on top of it and the crate rides along. Put the keeper on it and the keeper doesn't, because a CharacterController isn't moved by friction; the character section comes back to that.

The fixed step#

Physics would give different results at different frame rates if it stepped once per frame: a big step lets things sink further into each other and lose energy differently. So Unity steps physics by a fixed amount of time, the Fixed Timestep (Edit > Project Settings > Time), 0.02 s by default. Each frame, Unity adds the frame's time to a running total and runs as many 0.02 s steps as fit, calling FixedUpdate just before each one.

Frames 30 fps Physics 0.02 s 1 step 2 steps 2 steps 1 step 2 steps 2 steps Each dot is one FixedUpdate followed by one physics step. At 30 fps they fall 1, 2, 2, 1, 2, 2 to a frame; at 144 fps most frames get none.
The fixed step at 30 frames per second. Frames and physics steps don't line up, so the number of physics steps per frame varies.

The program's first block shows the pattern. At 30 fps, frames get 1 or 2 steps; at 60 fps, mostly 1 and sometimes 0; at 144 fps, most frames get none and one in three gets a step. Every rate gets 50 steps a second. That's what keeps physics the same on a slow laptop and a fast desktop. (The program is a simple model of the scheduling; Unity's own bookkeeping differs in detail but gives the same totals.)

Three rules follow:

  1. Physics changes go in FixedUpdate, so they happen once per step. A force added in Update is applied once per frame: twice as strong at 60 fps as at 30 fps, and lumped into whichever step comes next.
  2. One-off input goes in Update, stored in a field for FixedUpdate to use. At 144 fps most frames have no fixed step, so WasPressedThisFrame checked in FixedUpdate misses most presses. BallRoller's hopQueued flag is the pattern.
  3. Time.deltaTime is right in both. Inside FixedUpdate, Unity makes it return the fixed step, so the same maths works in either. Writing Time.fixedDeltaTime there says what you mean more clearly.

The second block shows the safety valve. A frame that takes 0.25 s (loading something, a breakpoint, a slow device) runs 12 physics steps at once to catch up. Catching up costs time, which can make the next frame slow too, and so on: a spiral. Maximum Allowed Timestep, one third of a second by default, caps how much time one frame can count, so a 0.5 s frame runs only 16 steps and the game world simply runs slow for a moment instead of locking up.

Interpolation#

Because physics moves bodies only on steps, at 144 fps a body sits still for two frames, then jumps, then sits still. Usually you can't tell. Under a camera that follows it smoothly every frame, you can: the body visibly stutters while the background glides.

Interpolate fixes that. Unity draws the body between its last two physics positions, by however far the current frame is through the step. The drawn position is then always slightly behind the physics position, by up to one step, but it moves smoothly. Extrapolate guesses ahead from the velocity instead, which has no lag but overshoots when the body suddenly stops or turns.

Turn on Interpolate for bodies the player watches closely: the player's own physics character, a ball the camera follows, a held object. Leave it off for everything else; it costs a little per body and does nothing for objects nobody looks at. Two rules go with it: move interpolated bodies with forces, velocity or MovePosition (setting the Transform breaks the interpolation), and have cameras follow them in LateUpdate.

Who runs the simulation#

Simulation Mode in the Physics settings (or Physics.simulationMode) says when physics steps:

  • Fixed Update (default): after FixedUpdate, as described above.
  • Update: once per frame, after Update, with the frame's time. Simpler, but results depend on frame rate. Occasionally used for turn-based or non-gameplay physics.
  • Script: never on its own. You call Physics.Simulate(step) yourself. Going further uses this to pause and single-step physics.

Collisions and triggers#

When colliders touch, Unity tells the scripts involved. There are two families of message, depending on whether either collider is a trigger.

A normal collider is solid: things bounce off it, and you get collision messages. Tick Is Trigger and the collider stops being solid: things pass through it, and you get trigger messages when they enter, stay in and leave its volume. Triggers are for zones: a pickup, a checkpoint, a door sensor, a kill zone, the gale in the guided build.

MessageWhenArgument
OnCollisionEnterTwo solid colliders start touchingCollision
OnCollisionStayEvery step they keep touchingCollision
OnCollisionExitThey stop touchingCollision
OnTriggerEnterA collider enters a triggerthe other Collider
OnTriggerStayEvery step it stays insidethe other Collider
OnTriggerExitIt leavesthe other Collider

These go to scripts on both objects, and also to scripts on the GameObject that has the Rigidbody, if the collider is on a child. They're sent after the physics step, so they're part of the fixed-step cycle like FixedUpdate. You can declare any of them without the argument (void OnCollisionEnter()) when you don't need it, which skips building the Collision object.

Which pairs send messages#

The rules confuse everyone at first. They come from one idea: messages need something physics is simulating. Two static colliders never touch in a way physics notices, because neither moves.

Solid colliders: collision messages Static Dynamic Kinematic Static Dynamic Kinematic — collision — collision collision collision — collision — (unless enabled) Triggers: trigger messages A trigger and any collider send trigger messages if at least one of the two has a Rigidbody, dynamic or kinematic. A CharacterController counts. Two colliders with no Rigidbody send nothing.
Who gets messages. Collisions need at least one dynamic body; triggers need at least one Rigidbody of either kind.
  • Collision messages need at least one dynamic Rigidbody. A dynamic crate landing on the static ground: messages. A kinematic platform sliding into a static wall: nothing, because neither is simulated and the platform goes straight through. Two kinematic bodies pass through each other silently too, unless you change Contact Pairs Mode in the Physics settings to include kinematic pairs.
  • Trigger messages need at least one Rigidbody, of either kind. A static trigger zone with a dynamic crate entering: messages. The same zone with a static rock inside: nothing. Making a moving trigger (a sword's hit box, a moving sensor) kinematic is the usual way to make it work with static scenery.
  • A CharacterController counts as a body for triggers, so the keeper walking into a static trigger sends OnTriggerEnter. For solid things it has its own message, OnControllerColliderHit.
  • Disabled components still get messages. Unticking a script doesn't stop its OnCollisionEnter or OnTriggerEnter being called; it only stops Update and the like. Check enabled yourself if it matters, or disable the collider.

What a Collision tells you#

The Collision argument describes the contact:

  • collision.gameObject, .collider, .rigidbody, .transform: the other object.
  • collision.relativeVelocity: how fast the two were closing. Its magnitude is the best simple measure of "how hard was that?".
  • collision.impulse: the total impulse the solver applied to separate them, in newton-seconds. It accounts for mass, so a heavy crate landing gives a bigger impulse than a light one at the same speed.
  • collision.contactCount and collision.GetContact(i): the contact points, each with a point, a normal (pointing away from the other collider) and a separation.
ImpactReporter.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Logs how hard things hit this object, and draws the first contact's normal.
    public class ImpactReporter : MonoBehaviour
    {
        [SerializeField] float minSpeed = 1f;   // ignore gentle touches, in metres per second

        void OnCollisionEnter(Collision collision)
        {
            float speed = collision.relativeVelocity.magnitude;
            if (speed < minSpeed) return;

            ContactPoint contact = collision.GetContact(0);
            Debug.Log($"{collision.gameObject.name} hit {name} at {speed:F1} m/s " +
                      $"(impulse {collision.impulse.magnitude:F1} N s, {collision.contactCount} contacts)", this);
            Debug.DrawRay(contact.point, contact.normal, Color.yellow, 2f);
        }

        void OnCollisionExit(Collision collision)
        {
            Debug.Log($"{collision.gameObject.name} stopped touching {name}", this);
        }
    }
}

Put it on the ground and drop crates on it: each landing logs its speed and draws a yellow line along the contact normal for two seconds. Use GetContact(i), not the older collision.contacts array: the array is built fresh each time you read it, which is garbage. By default Unity also reuses one Collision object for every callback (Reuse Collision Callbacks, on in new projects), so don't keep a reference to it after the message returns; copy out what you need.

Triggers in practice#

A pickup is the classic trigger. This one looks for a Wallet component on whatever enters:

Wallet.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Counts what the player has picked up.
    public class Wallet : MonoBehaviour
    {
        public int Coins { get; private set; }

        public void Add(int amount)
        {
            Coins += amount;
            Debug.Log($"Coins: {Coins}", this);
        }
    }
}
Pickup.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // A trigger that anything carrying a Wallet can collect.
    [RequireComponent(typeof(Collider))]
    public class Pickup : MonoBehaviour
    {
        [SerializeField] int value = 1;

        void Reset() => GetComponent<Collider>().isTrigger = true;

        void OnTriggerEnter(Collider other)
        {
            // The collider that entered may be a child. A Rigidbody's colliders report it
            // as attachedRigidbody, so look for the Wallet there first.
            Wallet wallet = other.attachedRigidbody != null
                ? other.attachedRigidbody.GetComponent<Wallet>()
                : other.GetComponent<Wallet>();
            if (wallet == null) return;

            wallet.Add(value);
            Destroy(gameObject);
        }
    }
}

Three habits are visible here. Reset ticks Is Trigger when you add the component, so it can't be forgotten. The script looks for a component rather than comparing names or tags, so anything that can carry coins can collect them, as T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable recommended. And it looks on the attachedRigidbody first: the collider that entered may be a child (a hand, a foot, a compound collider's part), while the Wallet sits on the root with the Rigidbody. A CharacterController has no Rigidbody, so for the keeper it looks on the collider's own GameObject.

Tags still have a place for quick checks: other.CompareTag("Player") is fast and doesn't allocate, unlike comparing other.tag == "Player", which builds a new string each time.

Physics materials: friction and bounce#

How slippery and bouncy a surface is comes from its Physics Material, an asset you assign to a collider's Material field. Create one with Assets > Create > Physics Material.

SettingDefaultMeaning
Dynamic Friction0.6Friction while sliding. 0 is ice; 1 is very grippy.
Static Friction0.6Friction that stops a resting object starting to slide. Usually a little higher than dynamic.
Bounciness00 doesn't bounce; 1 bounces back with no loss of energy.
Friction CombineAverageHow to combine the two colliders' friction values.
Bounce CombineAverageHow to combine their bounciness.

A collider with no material uses those defaults. Every contact is between two surfaces, so the combine mode says how to mix them: Average, Minimum, Multiply or Maximum. When the two materials ask for different modes, the higher one in that list wins: Maximum beats Multiply, which beats Minimum, which beats Average. That's how an ice rink works: ice with friction 0 and combine Minimum makes everything slide on it, whatever the other surface says.

A few starting points: rubber ball, bounciness 0.8 with Bounce Combine Maximum; ice, frictions 0.02 with Friction Combine Minimum; a character that shouldn't stick to walls, frictions 0 with Minimum (the Rigidbody character does this). Bounces below 2 m/s are ignored, set by Bounce Threshold in the Physics settings, so a bouncy ball eventually settles instead of jittering forever.

Layers and the collision matrix#

Not everything should collide with everything. Bullets shouldn't hit the gun that fired them; the player's own trigger zones shouldn't stop the player's raycasts; crates in the guided build should fall through the sea's surface while the island stays solid. Layers control this.

Every GameObject is on one of 32 layers (the Layer drop-down at the top of the Inspector, from T02 · The Unity Editor: Scenes, GameObjects, Components & Prefabs). Unity uses 0, 1, 2, 4 and 5 (Default, TransparentFX, Ignore Raycast, Water and UI); the empty slots are yours to name in Edit > Project Settings > Tags and Layers. Starting your own at 6 keeps them clear of Unity's.

The Layer Collision Matrix in Edit > Project Settings > Physics is a triangle of checkboxes, one per pair of layers. Untick a box and colliders on those two layers pass through each other and send no messages. It's the cheapest filter there is: PhysX skips the pair before testing shapes.

Three finer tools sit on top of the matrix:

  • Layer Overrides on a collider or Rigidbody: Include Layers and Exclude Layers add or remove layers for that one object, and Layer Override Priority settles conflicts (higher wins). Use it for a one-off, such as a ghost enemy that passes through walls but not the player, without adding a layer just for it.
  • Physics.IgnoreCollision(a, b): turns off one specific pair, for example a projectile and the character who fired it. It isn't saved with the scene, so it's always called from a script at run time.
  • Layer masks on queries, covered next: they don't change what collides, only what a raycast or overlap test can see.

Layer masks#

A layer mask is a 32-bit number where each bit stands for one layer, as T03 · C# Fundamentals: Types, Logic, Methods & Classes's bitwise operators showed: 1 << 6 is "layer 6 only", and ~0 (every bit set) is everything. In scripts, you rarely build them by hand:

Three ways to get a layer maskC#
[SerializeField] LayerMask groundLayers;              // 1. ticked in the Inspector: best
int props = LayerMask.GetMask("Props", "Debris");     // 2. by name, in code
int notPlayer = ~(1 << LayerMask.NameToLayer("Keeper"));  // 3. everything except one layer

A serialized LayerMask field is the best: it shows as a multi-select drop-down, survives renaming layers, and needs no strings in code. LayerMask converts to an int automatically wherever a method wants one. Don't confuse the two numbers: gameObject.layer is a layer index (0 to 31), while a mask is a set of bits. Passing gameObject.layer where a mask is expected is a common bug that makes a raycast hit the wrong layers.

Asking the world: raycasts, shape casts and overlaps#

Collisions tell you when things touch. Queries let you ask whenever you like: what's under the mouse, is there ground below my feet, is there a wall in front of me, which enemies are within 5 m? They're the most-used part of the physics API, and they work on any collider, Rigidbody or not.

Raycast A line: the first thing it hits misses the small box below it SphereCast A swept ball: hits the small box first, because it's thicker Overlap A volume: everything inside no direction, no order
Raycasts, shape casts and overlaps. Casts travel and report hits along the way; overlaps look at one place.

Raycasts#

Physics.Raycast fires an infinitely thin line from a point in a direction and reports the first collider it hits. The full form is:

Physics.Raycast, every argument namedC#
bool hitSomething = Physics.Raycast(
    origin,                     // where the ray starts, in world space
    direction,                  // which way (doesn't need to be normalised)
    out RaycastHit hit,         // filled in if it hits
    maxDistance,                // how far to look; omit for infinity
    layerMask,                  // which layers it can hit; omit for all except Ignore Raycast
    QueryTriggerInteraction.Ignore);  // whether triggers count

RaycastHit tells you point (where), normal (the surface's facing there), distance, collider, rigidbody (null for static colliders), transform, and for Mesh Colliders, triangleIndex and textureCoord. Here's a pointer that reports what it's aimed at:

LaserPointer.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Casts a ray along this object's forward axis every frame and reports what it hits.
    public class LaserPointer : MonoBehaviour
    {
        [SerializeField] float range = 20f;
        [SerializeField] LayerMask hitLayers = ~0;   // Everything

        Collider lastHit;

        void Update()
        {
            Vector3 origin = transform.position;
            Vector3 direction = transform.forward;

            if (Physics.Raycast(origin, direction, out RaycastHit hit, range, hitLayers, QueryTriggerInteraction.Ignore))
            {
                Debug.DrawLine(origin, hit.point, Color.red);
                Debug.DrawRay(hit.point, hit.normal * 0.5f, Color.green);

                if (hit.collider != lastHit)
                    Debug.Log($"Pointing at {hit.collider.name}, {hit.distance:F2} m away", hit.collider);
                lastHit = hit.collider;
            }
            else
            {
                Debug.DrawRay(origin, direction * range, Color.gray);
                lastHit = null;
            }
        }
    }
}

Debug.DrawLine and Debug.DrawRay draw in the Scene view (and in the Game view with Gizmos turned on), for one frame or for a duration. Drawing your rays is the first step in debugging any query.

Things worth knowing about rays:

  • Rays don't hit colliders they start inside. A ray fired from a character's centre doesn't hit the character's own capsule. That's convenient, but it also means a ray starting inside a wall reports nothing.
  • Rays don't hit the back of triangles on Mesh Colliders unless Queries Hit Backfaces is on in the Physics settings (off by default). A ray from inside a closed mesh room going outwards hits nothing.
  • Triggers count by default. Queries Hit Triggers is on in new projects, so an aiming ray stops at the first pickup zone in the way. Pass QueryTriggerInteraction.Ignore when you're aiming at solid things, which is nearly always.
  • The Ignore Raycast layer is left out when you don't pass a mask. With a mask, only the mask counts.
  • Physics.Linecast(from, to) is a raycast between two points: "is there anything between me and the player?" in one call.
  • To aim from the mouse, build the ray with Camera.ScreenPointToRay, as T07's screen space section did.

Shape casts#

A ray is infinitely thin, so it slips through gaps and past the edges of things a body would hit. A shape cast sweeps a whole shape along the ray and reports the first thing that shape would touch:

  • Physics.SphereCast: a ball, given by the ray's origin and a radius. The standard ground check, and forgiving aim for picking things up.
  • Physics.CapsuleCast: a capsule, given by the centres of its two end spheres and a radius. "Would my character fit if it moved there?"
  • Physics.BoxCast: a box, given by its centre, half-size and rotation.
The three shape castsC#
Physics.SphereCast(origin, radius, direction, out RaycastHit hit, distance, mask);
Physics.CapsuleCast(point1, point2, radius, direction, out hit, distance, mask);
Physics.BoxCast(center, halfExtents, direction, out hit, orientation, distance, mask);

Like rays, they ignore colliders the shape overlaps at the start. So a sphere cast from a character's centre won't find a wall the sphere is already touching: start the cast a little back, or follow it with an overlap test. The hit.point is where the shape touched, and hit.distance is how far the shape's centre travelled to get there.

Overlap tests#

An overlap test asks "what's inside this volume right now?": Physics.OverlapSphere, OverlapBox and OverlapCapsule. There's no direction and no order; you get every collider that touches the shape. Use them for explosions, area attacks, "is this spot free to spawn in?" and "which crates are on the jetty?". Physics.CheckSphere and friends just answer yes or no, which is cheaper when that's all you need.

The basic versions return a new array each call, which is garbage. In code that runs often, use the NonAlloc versions, which fill an array you made once and return how many they found:

Blast.csUnity sandboxC#
using System.Collections.Generic;
using UnityEngine;

namespace Handbook.T08
{
    // Pushes every Rigidbody within range away from this point.
    // Call Detonate from code, or from the component's ⋮ menu in Play mode.
    public class Blast : MonoBehaviour
    {
        [SerializeField] float radius = 5f;
        [SerializeField] float speed = 8f;             // metres per second given at the centre
        [SerializeField] float upwardsModifier = 0.5f; // lifts things a little, which looks better
        [SerializeField] LayerMask affected = ~0;

        // Reused on every blast, so detonating makes no garbage.
        readonly Collider[] results = new Collider[32];
        readonly HashSet<Rigidbody> pushed = new HashSet<Rigidbody>();

        [ContextMenu("Detonate")]
        public void Detonate()
        {
            Vector3 centre = transform.position;
            int count = Physics.OverlapSphereNonAlloc(centre, radius, results, affected, QueryTriggerInteraction.Ignore);
            if (count == results.Length)
                Debug.LogWarning("The blast filled its buffer, so some bodies may have been missed.", this);

            pushed.Clear();
            for (int i = 0; i < count; i++)
            {
                Rigidbody body = results[i].attachedRigidbody;
                // One push per body, even if it has several colliders.
                if (body == null || body.isKinematic || !pushed.Add(body)) continue;
                body.AddExplosionForce(speed, centre, radius, upwardsModifier, ForceMode.VelocityChange);
            }
        }

        void OnDrawGizmosSelected()
        {
            Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f);
            Gizmos.DrawWireSphere(transform.position, radius);
        }
    }
}

Add it to an empty among some crates, press Play, and choose Detonate from the component's ⋮ menu: they scatter. The HashSet makes sure a body with three colliders gets one push, not three. If the buffer fills up, the extra colliders are silently left out; the warning tells you to make it bigger.

All hits, and many queries at once#

Physics.RaycastAll and SphereCastAll return every hit along the way, in no guaranteed order: sort by distance if order matters. Their NonAlloc versions (RaycastNonAlloc and so on) fill your array instead. For hundreds of rays a frame (sensors on many AI agents, a wide spread of shotgun pellets), RaycastCommand runs them in parallel through the job system, which T46 · DOTS: Entities (ECS), Jobs & Burst covers.

Joints#

A joint connects a Rigidbody to another Rigidbody, or to a fixed point in the world, and limits how they can move relative to each other. Add the joint to one body and drag the other into Connected Body; leave it empty to attach to the world.

JointAllowsUse it for
Fixed JointNothing: the two move as one, until it breaksThings stuck together that can be knocked apart
Hinge JointRotation around one axis, with optional limits, spring and motorDoors, gates, lids, wheels, pendulums, chains
Spring JointMovement, pulled back towards a rest distance like elasticBungees, soft attachments, grabbing with give
Character JointTwist and swing within cone-shaped limitsRagdoll shoulders, hips and necks
Configurable JointAny combination, per axis, with drivesAnything the others can't do; the others are presets of it

A Hinge Joint's key settings:

  • Anchor and Axis, in the object's local space: where the hinge is and which way it turns. A door's anchor is at one edge; its axis is up.
  • Use Limits, with Min and Max angles: a door that opens to 100° either way.
  • Use Spring, with Spring, Damper and Target Position: a gate that swings shut by itself. The damper stops it swinging back and forth forever.
  • Use Motor: drives it round at a target speed with a maximum force, like a wheel or a windmill.

Every joint has Break Force and Break Torque, infinite by default. Set them and the joint snaps when pulled harder than that, and Unity calls OnJointBreak(float force) on the body's scripts. By default a joint also turns off collisions between the two bodies it connects (Enable Collision off), so a hinge's door doesn't fight its own frame.

Joints are solved together with contacts, and long chains of light bodies stretch and wobble. Unity's ArticulationBody component is built for chains that must stay exact, like robot arms and machinery, and is worth knowing about if joints won't hold together.

Character controllers#

A player character has unusual needs. It must stop the instant you let go, turn on a coin, climb stairs, stand on slopes without sliding, and never be knocked over. Real physics does none of those things. There are three common ways to square that, and every studio has an opinion.

CharacterControllerDynamic RigidbodyKinematic Rigidbody
Moved byMove(delta), which slides along what it hitsVelocity and forces, set every stepMovePosition, after your own shape casts
Stairs and slopesBuilt in: Step Offset, Slope LimitYou write themYou write them
Pushed by the worldNo: explosions and moving platforms do nothingYes, naturallyNo
Pushes the worldOnly if you code it (OnControllerColliderHit)Yes, naturallyYes, unstoppably
FeelPrecise and predictablePhysical; needs care to feel tightAs precise as your code
EffortLowMediumHigh
Good forMost first- and third-person gamesPhysics-heavy games, vehicles, balls, ragdoll-ready charactersPlatformers with exacting movement; what large character packages use

The CharacterController in detail#

T07 · Input System, Movement & Cameras (Cinemachine)'s keeper uses a CharacterController. Now that you know the physics around it, here's what it's doing.

skin width capsule step offset climbs blocks angle ≤ slope limit walks up it Move()
The CharacterController's tuning. Skin Width keeps it from sticking, Step Offset decides what it climbs, Slope Limit decides what it walks up.
  • Move(delta) moves the capsule by delta right away, sliding along anything it hits rather than stopping dead. It applies no gravity: you add your own, as KeeperController does. (SimpleMove(speed) adds gravity and ignores the Y of what you pass, which is why jumping with it doesn't work.)
  • Skin Width is a thin margin around the capsule where it's allowed to overlap things. Unity's advice is about 10% of the radius; too small and the character gets stuck on walls.
  • Step Offset is the tallest step it climbs without jumping. Unity suggests 0.1 to 0.4 m for a 2 m character; it must be less than the height.
  • Slope Limit is the steepest slope it walks up, in degrees. As T06 · Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation worked out, a 45° limit means a surface is walkable when the dot product of its normal with up is at least cos 45° ≈ 0.707.
  • isGrounded and collisionFlags describe the last Move: whether the bottom, sides or top touched something. They're only as fresh as the last call, which is why the keeper always moves down a little, to stay in touch with the ground.
  • It's moved in Update, not FixedUpdate: it isn't part of the simulation, and moving every frame gives the smoothest result.

Pushing things, and being pushed#

A CharacterController walking into a crate stops, like walking into a wall: it doesn't push. To push, handle OnControllerColliderHit, which Move calls for every collider the capsule touches during that move. The guided build's KeeperPush does exactly that.

Going the other way, nothing physical moves a CharacterController. It doesn't ride moving platforms, explosions don't throw it, and a rolling boulder stops against it as if it were a wall. Games handle each case in the character's own code: add the platform's movement each frame, add a knock-back velocity that fades out, and so on.

A Rigidbody character#

For comparison, here's the same kind of walking with a dynamic Rigidbody. It shows the extra work each row of the table implies: a ground check, slope handling, a frictionless material, and turning gravity off on the ground.

RigidbodyWalker.csUnity sandboxC#
using UnityEngine;
using UnityEngine.InputSystem;

namespace Handbook.T08
{
    // A physics-driven character: a capsule Rigidbody steered by changing its velocity.
    // Compare it with T07's KeeperController, which uses a CharacterController.
    [RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))]
    public class RigidbodyWalker : MonoBehaviour
    {
        [SerializeField] float speed = 4f;
        [SerializeField] float groundAcceleration = 30f;   // metres per second, per second
        [SerializeField] float airAcceleration = 8f;
        [SerializeField] float turnSpeed = 720f;           // degrees per second
        [SerializeField] float jumpHeight = 1.2f;
        [SerializeField] float maxSlope = 45f;             // degrees
        [SerializeField] float groundStick = 20f;          // keeps it on the ground over bumps
        [SerializeField] LayerMask groundLayers = 1;       // Default
        [SerializeField] Transform cameraTransform;

        const float GroundProbe = 0.1f;   // how far below the feet still counts as standing

        Rigidbody body;
        CapsuleCollider capsule;
        InputAction move, jump;
        Vector2 moveInput;
        bool jumpQueued;
        bool grounded;
        Vector3 groundNormal = Vector3.up;
        float lastJumpTime = float.NegativeInfinity;

        void Awake()
        {
            body = GetComponent<Rigidbody>();
            capsule = GetComponent<CapsuleCollider>();
            body.freezeRotation = true;   // collisions mustn't tip the character over
            body.interpolation = RigidbodyInterpolation.Interpolate;

            // No friction, so it doesn't stick to walls. Standing still on slopes is handled below.
            capsule.sharedMaterial = new PhysicsMaterial("Frictionless")
            {
                dynamicFriction = 0f,
                staticFriction = 0f,
                frictionCombine = PhysicsMaterialCombine.Minimum
            };

            InputActionAsset actions = InputSystem.actions;
            if (actions == null)
            {
                Debug.LogError("No project-wide actions. Assign them in Project Settings > Input System Package.", this);
                enabled = false;
                return;
            }
            move = actions.FindAction("Player/Move", throwIfNotFound: true);
            jump = actions.FindAction("Player/Jump", throwIfNotFound: true);
        }

        void Start()
        {
            if (cameraTransform == null && Camera.main != null)
                cameraTransform = Camera.main.transform;
        }

        void Update()
        {
            moveInput = Vector2.ClampMagnitude(move.ReadValue<Vector2>(), 1f);
            if (jump.WasPressedThisFrame())
                jumpQueued = true;
        }

        void FixedUpdate()
        {
            float dt = Time.fixedDeltaTime;
            UpdateGround();

            Vector3 wish = CameraRelative(moveInput) * speed;
            Vector3 velocity = body.linearVelocity;

            if (grounded)
            {
                // Steer along the ground's surface, so slopes neither slow it nor launch it.
                Vector3 target = Vector3.ProjectOnPlane(wish, groundNormal).normalized * wish.magnitude;
                Vector3 along = Vector3.ProjectOnPlane(velocity, groundNormal);
                body.AddForce(Vector3.MoveTowards(along, target, groundAcceleration * dt) - along, ForceMode.VelocityChange);
                body.AddForce(-groundNormal * groundStick, ForceMode.Acceleration);
            }
            else
            {
                Vector3 flat = new Vector3(velocity.x, 0f, velocity.z);
                body.AddForce(Vector3.MoveTowards(flat, wish, airAcceleration * dt) - flat, ForceMode.VelocityChange);
            }

            // On a frictionless slope, gravity would slide it downhill, so it's off while grounded.
            body.useGravity = !grounded;

            if (jumpQueued)
            {
                jumpQueued = false;
                if (grounded)
                {
                    Vector3 v = body.linearVelocity;
                    v.y = Mathf.Sqrt(2f * jumpHeight * -Physics.gravity.y);
                    body.linearVelocity = v;
                    body.useGravity = true;
                    lastJumpTime = Time.time;
                }
            }

            if (wish.sqrMagnitude > 0.01f)
            {
                Quaternion facing = Quaternion.LookRotation(new Vector3(wish.x, 0f, wish.z));
                body.MoveRotation(Quaternion.RotateTowards(body.rotation, facing, turnSpeed * dt));
            }
        }

        void UpdateGround()
        {
            grounded = false;
            groundNormal = Vector3.up;
            if (Time.time < lastJumpTime + 0.1f) return;   // just left the ground on purpose

            // Sweep a slightly smaller sphere down from the capsule's centre to just below its feet.
            float radius = capsule.radius * 0.95f;
            Vector3 origin = body.position + body.rotation * capsule.center;
            float toLowerSphere = capsule.height * 0.5f - capsule.radius;
            float distance = toLowerSphere + (capsule.radius - radius) + GroundProbe;

            if (Physics.SphereCast(origin, radius, Vector3.down, out RaycastHit hit, distance,
                                   groundLayers, QueryTriggerInteraction.Ignore)
                && Vector3.Angle(hit.normal, Vector3.up) <= maxSlope)
            {
                grounded = true;
                groundNormal = hit.normal;
            }
        }

        Vector3 CameraRelative(Vector2 input)
        {
            if (cameraTransform == null) return new Vector3(input.x, 0f, input.y);
            Vector3 forward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up).normalized;
            Vector3 right = Vector3.Cross(Vector3.up, forward);
            return forward * input.y + right * input.x;
        }
    }
}

Put it on a Capsule with a Rigidbody (mass 70) on its own layer, and leave that layer out of Ground Layers. The interesting parts:

  • Rotation is frozen so collisions can't tip it over; it turns only through MoveRotation.
  • The material has no friction. With friction, a capsule pressed against a wall while falling sticks to it, and walking feels like pushing a box. Without it, it slides off walls, but it would also slide down every slope.
  • So gravity is off while grounded, replaced by a small push into the ground that keeps it in contact over bumps. On the ground, movement is steered along the surface (ProjectOnPlane), so walking uphill doesn't lose speed and walking downhill doesn't launch it.
  • The ground check is a sphere cast from the capsule's centre down to just below its feet, slightly narrower than the capsule so walls beside it don't count as ground. The slope test rejects surfaces steeper than the limit.
  • Velocity changes are added as VelocityChange, limited by an acceleration, so it reaches walking speed quickly but other forces (an explosion, a gale) still add to it.

Walk it into a stack of crates and it shoves them over, and a Blast next to it throws it, both without any extra code. Walk it up a step taller than a few centimetres and it stops: stairs need a step-up routine or a ramp collider over the steps, which is what many games use anyway.

Guided build: Lighthouse Island, the storm#

A storm is coming in, and the supply drop has just landed on the meadow. The keeper has to carry the crates down to the end of the jetty for the boat, while gusts push loose crates around and anything that ends up in the sea floats off. Along the way you'll set up layers and the collision matrix, a respawn trigger, Rigidbody crates with a physics material, buoyancy, pushing, carrying, wind, a hinged gate and an overlap test that counts the delivered crates. It builds on T07 · Input System, Movement & Cameras (Cinemachine)'s scene, with the keeper and the Mesh Colliders on the beach and meadow.

The build uses SeaRespawn, CrateDrop, Floater, KeeperPush, KeeperGrab, Gale and DockTally. Copy them into _Project/Scripts.

Step 1 · Layers and the collision matrix#

  1. Open IslandNight. In Edit > Project Settings > Tags and Layers, name User Layer 6 Keeper and User Layer 7 Props.
  2. Select Keeper and set its Layer to Keeper. When Unity asks, choose Yes, change children.
  3. Select Keeper/Nose and remove its Box Collider. It came with the Cube, and a collider on a CharacterController's child is a collider moving without a Rigidbody, which the controller can catch on. The nose is only there to be seen.
  4. Select Sea and set its Layer to Water, one of Unity's built-in layers.
  5. In Edit > Project Settings > Physics, find the Layer Collision Matrix. Untick the two boxes where Water meets Keeper and where Water meets Props.

The sea is a Plane with a Mesh Collider, so until now you could walk on the water. Now the keeper and the crates will drop through its surface, while the beach and meadow, on Default, stay solid. Press Play and walk the keeper off the beach to check: they fall into the sea and keep falling. Step 2 catches them.

Step 2 · Falling in#

SeaRespawn.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // A trigger under the sea that puts the keeper back on the jetty after a fall.
    [RequireComponent(typeof(Collider))]
    public class SeaRespawn : MonoBehaviour
    {
        [SerializeField] Transform respawnPoint;

        void Reset() => GetComponent<Collider>().isTrigger = true;

        void OnTriggerEnter(Collider other)
        {
            if (respawnPoint == null || !other.TryGetComponent(out CharacterController controller)) return;

            // A CharacterController keeps its own copy of its position. Moving the Transform alone
            // isn't enough: the next Move can start from the old spot. Turning the controller off
            // and on again makes it take the new position.
            controller.enabled = false;
            other.transform.SetPositionAndRotation(respawnPoint.position, respawnPoint.rotation);
            controller.enabled = true;
        }
    }
}
  1. Inside Gameplay, create an empty named JettySpawn at (0, 1.5, −14), on the beach at the landward end of the jetty. Rotate it to face along the jetty.
  2. Create an empty named SeaRespawn at (0, −3, 0). Add a Box Collider, set its Size to (120, 1, 120), then add SeaRespawn; its Reset ticks Is Trigger. Drag JettySpawn into Respawn Point.

Walk off the beach again. The keeper sinks, touches the trigger 3 m down and reappears by the jetty. It's a static trigger, but the CharacterController counts as a body, so OnTriggerEnter fires. The keeper lands with a thump and the camera shakes, because KeeperController still has the fall speed when it reappears: LandingShake from T07 · Input System, Movement & Cameras (Cinemachine) is doing its job.

Step 3 · Supply crates#

CrateDrop.csUnity sandboxC#
using System.Collections.Generic;
using UnityEngine;

namespace Handbook.T08
{
    // Drops supply crates from this point every few seconds, up to a limit.
    public class CrateDrop : MonoBehaviour
    {
        [SerializeField] Rigidbody cratePrefab;
        [SerializeField] float interval = 3f;
        [SerializeField] int maxCrates = 12;
        [SerializeField] float scatter = 1.5f;   // metres sideways
        [SerializeField] float spin = 3f;        // radians per second, at most

        readonly List<Rigidbody> crates = new List<Rigidbody>();
        float nextDrop;

        void Update()
        {
            if (cratePrefab == null || Time.time < nextDrop) return;
            nextDrop = Time.time + interval;

            crates.RemoveAll(crate => crate == null);   // forget crates that were destroyed
            if (crates.Count >= maxCrates) return;

            Vector2 offset = Random.insideUnitCircle * scatter;
            Vector3 position = transform.position + new Vector3(offset.x, 0f, offset.y);
            Rigidbody crate = Instantiate(cratePrefab, position, Random.rotation);
            crate.angularVelocity = Random.insideUnitSphere * spin;
            crates.Add(crate);
        }

        void OnDrawGizmos()
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(transform.position, scatter);
        }
    }
}
  1. Create a Physics Material in _Project/Materials named WetWood: Dynamic Friction 0.5, Static Friction 0.6, Bounciness 0.05.
  2. Create a Cube named Crate, scale (0.8, 0.8, 0.8), with the Wood material. Set its Layer to Props. Drag WetWood into its Box Collider's Material.
  3. Add a Rigidbody: Mass 20, Interpolate set to Interpolate (the camera will follow these in the keeper's arms).
  4. Add Floater (next step explains it) and leave its Height at 0.8.
  5. Drag the Crate into _Project/Prefabs to make a prefab, then delete it from the scene.
  6. Create an empty named SupplyDrop at (5, 7, −6), above the meadow near where the keeper starts. Add CrateDrop and drag the Crate prefab into Crate Prefab.

Press Play. Every 3 seconds a crate appears 7 m up, spinning, falls, lands on the meadow with a small bounce and tumbles to rest. After twelve, the drop stops. Watch the stack form: crates land on crates, slide off and settle. Each one goes to sleep once it stops; turn on the Physics Debugger (Window > Analysis > Physics Debugger) to see sleeping bodies drawn in a different colour.

Step 4 · Crates that float#

Floater.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Keeps a Rigidbody afloat on flat water, with extra drag while it's wet.
    // A simple model rather than real buoyancy: the body settles with 1 / buoyancy of its height under water.
    [RequireComponent(typeof(Rigidbody))]
    public class Floater : MonoBehaviour
    {
        [SerializeField] float waterLevel = 0f;
        [SerializeField] float height = 0.8f;          // the object's height, to work out how much is under
        [SerializeField] float buoyancy = 2f;          // 2 floats half under; 1 barely floats; 4 rides high
        [SerializeField] float waterDrag = 2f;         // per second, when fully under
        [SerializeField] float waterAngularDrag = 1f;

        Rigidbody body;

        void Awake() => body = GetComponent<Rigidbody>();

        void FixedUpdate()
        {
            float bottom = body.worldCenterOfMass.y - height * 0.5f;
            float submerged = Mathf.Clamp01((waterLevel - bottom) / height);
            if (submerged <= 0f) return;

            // Push up against gravity, harder the deeper it sits. Gravity and this balance
            // where buoyancy × submerged = 1.
            body.AddForce(-Physics.gravity * (buoyancy * submerged), ForceMode.Acceleration);
            body.AddForce(-body.linearVelocity * (waterDrag * submerged), ForceMode.Acceleration);
            body.AddTorque(-body.angularVelocity * (waterAngularDrag * submerged), ForceMode.Acceleration);
        }
    }
}

Pick up a crate in the Scene view during Play and drop it in the sea, or move SupplyDrop out over the water for a moment. The crate drops through the sea's surface (the collision matrix lets it), slows as it goes under, bobs back up and settles half under. Then it drifts with whatever speed it had, slowed by the water's drag.

The Floater isn't real buoyancy, which depends on the volume of water displaced. It's a model with one dial. Each step it pushes up with an acceleration of buoyancy × submerged fraction times gravity, so the push and gravity balance when submerged = 1 ÷ buoyancy. The last block of the program's output shows where a 0.8 m crate settles: 80% under at 1.25, half under at 2, a quarter under at 4. Try those values. Because it uses ForceMode.Acceleration, mass doesn't matter: a 20 kg crate and a 5 kg one float the same.

Step 5 · Pushing crates#

KeeperPush.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Lets a CharacterController push the Rigidbodies it walks into.
    // A CharacterController ignores forces and doesn't push anything by itself.
    [RequireComponent(typeof(CharacterController))]
    public class KeeperPush : MonoBehaviour
    {
        [SerializeField] float pushStrength = 60f;   // speed × mass: a 20 kg crate moves at 3 m/s
        [SerializeField] float maxPushSpeed = 3f;

        // Called during CharacterController.Move for every collider the capsule touches.
        void OnControllerColliderHit(ControllerColliderHit hit)
        {
            Rigidbody body = hit.rigidbody;
            if (body == null || body.isKinematic) return;
            if (hit.normal.y > 0.7f) return;   // standing on top of it, not walking into it

            Vector3 direction = Vector3.ProjectOnPlane(hit.moveDirection, Vector3.up);
            if (direction.sqrMagnitude < 0.01f) return;
            direction.Normalize();

            // Set the sideways speed and keep the vertical one, so it still falls.
            float speed = Mathf.Min(pushStrength / body.mass, maxPushSpeed);
            Vector3 velocity = body.linearVelocity;
            body.linearVelocity = new Vector3(direction.x * speed, velocity.y, direction.z * speed);
        }
    }
}

Add KeeperPush to the Keeper. Walk into a crate: it slides ahead of the keeper, at 3 m/s for a 20 kg crate (60 ÷ 20), and tips over small bumps. Push one off the edge of the meadow and it tumbles down to the beach; push it into the sea and it floats away.

Two details matter. The test on hit.normal skips crates the keeper is standing on; otherwise standing on a crate would push it out from under them. And the script sets only the horizontal velocity and keeps the vertical one, so a crate pushed off an edge still falls. Without this script the keeper stops dead against every crate, as against a wall: try removing it.

Step 6 · Carrying#

KeeperGrab.csUnity sandboxC#
using UnityEngine;
using UnityEngine.InputSystem;

namespace Handbook.T08
{
    // Picks up the crate in front of the keeper with Attack, carries it, and drops it with Attack again.
    // The crate stays a physics body while it's held: each step it's given the velocity that
    // would carry it to a point in front of the keeper.
    public class KeeperGrab : MonoBehaviour
    {
        [SerializeField] float reach = 1.5f;
        [SerializeField] float probeRadius = 0.4f;
        [SerializeField] LayerMask grabbable;                                // set to Props
        [SerializeField] Vector3 holdOffset = new Vector3(0f, 0.6f, 1.3f);  // in the keeper's local space
        [SerializeField] float maxCarrySpeed = 10f;
        [SerializeField] float maxCarryMass = 40f;

        InputAction grab;
        Rigidbody held;
        float heldAngularDamping;

        void Awake()
        {
            InputActionAsset actions = InputSystem.actions;
            if (actions == null)
            {
                Debug.LogError("No project-wide actions. Assign them in Project Settings > Input System Package.", this);
                enabled = false;
                return;
            }
            grab = actions.FindAction("Player/Attack", throwIfNotFound: true);
        }

        void Update()
        {
            if (!grab.WasPressedThisFrame()) return;
            if (held != null) Drop();
            else TryPickUp();
        }

        void FixedUpdate()
        {
            if (held == null) return;

            Vector3 toTarget = transform.TransformPoint(holdOffset) - held.position;
            if (toTarget.magnitude > reach * 2f)
            {
                Drop();   // snagged on something, or pulled away: let go
                return;
            }
            // The velocity that would arrive in one step, capped so it can't fling things.
            held.linearVelocity = Vector3.ClampMagnitude(toTarget / Time.fixedDeltaTime, maxCarrySpeed);
        }

        void OnDisable()
        {
            if (held != null) Drop();
        }

        void TryPickUp()
        {
            if (!Physics.SphereCast(transform.position, probeRadius, transform.forward, out RaycastHit hit,
                                    reach, grabbable, QueryTriggerInteraction.Ignore))
                return;

            Rigidbody body = hit.rigidbody;
            if (body == null || body.isKinematic || body.mass > maxCarryMass) return;

            held = body;
            held.useGravity = false;
            heldAngularDamping = held.angularDamping;
            held.angularDamping = 10f;   // stop it spinning in the keeper's arms
        }

        void Drop()
        {
            held.useGravity = true;
            held.angularDamping = heldAngularDamping;
            held = null;
        }
    }
}
  1. Add KeeperGrab to the Keeper. Set Grabbable to Props.
  2. Press Play, walk up to a crate and press Attack: the left mouse button (which also locks the cursor), Enter, or the gamepad's west button. The crate lifts into the keeper's arms. Walk, and it follows. Press Attack again to drop it.

The crate is never parented to the keeper and never made kinematic. Each step it gets the velocity that would carry it to the hold point in one step, capped at 10 m/s. Because it stays a dynamic body, it still collides: walk it into a wall and it stops there instead of going through, and if it gets stuck more than 3 m from where it should be, the keeper lets go. It's interpolated, so it moves smoothly under the camera. The sphere cast makes aiming forgiving: the crate only has to be roughly in front of the keeper, within 1.5 m.

Step 7 · The gale#

Gale.csUnity sandboxC#
using System.Collections.Generic;
using UnityEngine;

namespace Handbook.T08
{
    // A trigger volume where gusts of wind push every Rigidbody inside it.
    [RequireComponent(typeof(Collider))]
    public class Gale : MonoBehaviour
    {
        [SerializeField] Vector3 direction = new Vector3(1f, 0f, 0.3f);
        [SerializeField] float strength = 6f;      // metres per second, per second, in the strongest gust
        [SerializeField] float gustSpeed = 0.3f;   // how quickly gusts come and go

        readonly List<Rigidbody> inside = new List<Rigidbody>();

        void Reset() => GetComponent<Collider>().isTrigger = true;

        void OnTriggerEnter(Collider other)
        {
            Rigidbody body = other.attachedRigidbody;
            if (body != null && !body.isKinematic && !inside.Contains(body))
                inside.Add(body);
        }

        void OnTriggerExit(Collider other)
        {
            Rigidbody body = other.attachedRigidbody;
            if (body != null)
                inside.Remove(body);
        }

        void FixedUpdate()
        {
            float gust = Mathf.PerlinNoise1D(Time.time * gustSpeed);           // about 0 to 1, changing smoothly
            Vector3 wind = direction.normalized * (strength * gust * gust);    // squared, so calm spells stay calm

            for (int i = inside.Count - 1; i >= 0; i--)
            {
                Rigidbody body = inside[i];
                if (body == null)
                {
                    inside.RemoveAt(i);   // destroyed while inside the volume
                    continue;
                }
                body.AddForce(wind, ForceMode.Acceleration);
            }
        }
    }
}
  1. Create an empty named Gale at (0, 3, 0). Add a Box Collider of Size (40, 8, 40), then Gale, which ticks Is Trigger. The box covers the island and some of the sea around it.
  2. Press Play and watch the crates. Every so often a gust builds and pushes them across the meadow; a crate on the jetty can be blown into the sea. Floating crates drift faster in a gust and stop drifting once they float out of the box.

The gale is a static trigger and the crates are dynamic bodies, so it gets OnTriggerEnter and OnTriggerExit as they come and go, and keeps a list instead of using OnTriggerStay. A crate created inside the box (they all are) counts as entering on its first step. The keeper enters too, but has no attachedRigidbody, so the gale ignores them: a CharacterController isn't moved by forces anyway. Mathf.PerlinNoise1D from T06 · Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation gives gusts that rise and fall smoothly, and squaring it keeps the calm spells calm.

Wind is an acceleration here, so heavy and light crates blow alike. Real wind is a force that depends on the area facing it, so light things blow further. Change the mode to ForceMode.Force and raise Strength to about 100 to feel the difference: a 20 kg crate then accelerates at 100 ÷ 20 = 5 m/s² in a full gust.

Step 8 · The gate#

  1. On the beach between the meadow and the jetty, create a Cube named GatePost at (−0.8, 1, −15), scale (0.2, 1.4, 0.2), with the Wood material. It's static: no Rigidbody.
  2. Create a Cube named Gate at (0, 0.9, −15), scale (1.4, 1.1, 0.08), Wood. Its left edge touches the post. Leave it on Default, so the keeper can push it but not pick it up.
  3. Add a Rigidbody (Mass 10) and a Hinge Joint. Leave Connected Body empty, which hinges it to the world. Set Anchor to (−0.5, 0, 0), the gate's left edge in its local space, and Axis to (0, 1, 0).
  4. Tick Use Limits with Min −100 and Max 100. Tick Use Spring with Spring 15, Damper 3 and Target Position 0.

Walk through the gate. KeeperPush sets the gate's velocity, the hinge turns that into a swing, and it opens ahead of the keeper. Once you're through, the spring pulls it back to closed and the damper stops it swinging past. A strong gust nudges it too. Without the damper (set it to 0) it swings back and forth for a long time; without the spring it stays wherever it was left.

Step 9 · Counting the delivery#

DockTally.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Counts the crates sitting in a box-shaped area twice a second, and reports when the count changes.
    public class DockTally : MonoBehaviour
    {
        [SerializeField] Vector3 halfExtents = new Vector3(1.2f, 0.8f, 1.5f);
        [SerializeField] LayerMask crates;   // set to Props
        [SerializeField] int goal = 4;

        readonly Collider[] found = new Collider[16];
        float nextCount;
        int lastCount = -1;

        void Update()
        {
            if (Time.time < nextCount) return;
            nextCount = Time.time + 0.5f;

            // Counts colliders, which is the same as crates while each crate has one collider.
            int count = Physics.OverlapBoxNonAlloc(transform.position, halfExtents, found,
                                                   transform.rotation, crates, QueryTriggerInteraction.Ignore);
            if (count == lastCount) return;
            lastCount = count;

            if (count >= goal) Debug.Log($"Supplies in: {count} crates on the jetty.", this);
            else Debug.Log($"{count} of {goal} crates on the jetty.", this);
        }

        void OnDrawGizmos()
        {
            Gizmos.color = new Color(0.3f, 1f, 0.4f, 0.6f);
            Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
            Gizmos.DrawWireCube(Vector3.zero, halfExtents * 2f);
        }
    }
}
  1. Create an empty named DockTally over the far end of the jetty, about (0, 0.85, −20.5). Add DockTally and set Crates to Props.
  2. Adjust its position and Half Extents in the Scene view until the green box covers the last two or three planks and the space a crate takes up on them.
  3. Press Play and carry crates to the end of the jetty. The Console counts them: "1 of 4 crates on the jetty", up to "Supplies in: 4 crates on the jetty."

The overlap test only looks at the Props layer, so the planks, the keeper and the gale's trigger are ignored, and it runs twice a second rather than every frame because nothing needs it faster. If a gust blows a delivered crate into the sea, the count goes down: wind and water are now part of the game.

Step 10 · Break it on purpose#

  1. Tunnelling. Create a small Sphere, scale (0.2, 0.2, 0.2), with a Rigidbody, 100 m above the jetty, and press Play. It reaches about 44 m/s and moves almost 0.9 m per step, much more than a plank's 0.12 m, so it often drops straight through the jetty. Set its Collision Detection to Continuous Dynamic and try again: it lands. See tunnelling.
  2. No interpolation. On the Crate prefab, set Interpolate back to None. Carry a crate while turning the camera, ideally with the frame rate uncapped: the crate shudders in the keeper's arms while the world moves smoothly. Set it back.
  3. An impulse every step. In Gale, change ForceMode.Acceleration to ForceMode.Impulse. The next gust throws 20 kg crates off the island, because in a full gust a strength of 6 now adds 6 ÷ 20 = 0.3 m/s every step, 15 m/s each second. Change it back.
  4. Teleport without turning off. In SeaRespawn, comment out the two controller.enabled lines and fall into the sea a few times. Sometimes the keeper reappears on the beach; sometimes they snap straight back into the sea, because the controller's own position wasn't updated before its next Move. Put the lines back.
  5. The matrix. Tick Water–Props again. Crates now land on the sea's surface and slide on it like ice. Untick it.

Step 11 · Save and commit#

Save the scene and the prefab, check that the scripts, the WetWood material, the Crate prefab and ProjectSettings/DynamicsManager.asset (the collision matrix) and TagManager.asset (the layers) are in Git, and commit: git commit -m "The storm".

Check your work. You've finished when: the keeper falls through the sea and reappears on the beach; crates drop, stack and settle; crates in the sea float half under and drift; the keeper pushes crates by walking into them, and picks up and drops them with Attack; gusts move the crates now and then; the gate swings open when walked through and closes by itself; the Console counts crates on the end of the jetty; and the Console has no errors.

Going further#

Two later chapters build on this one: T56 · Advanced Physics: Ragdolls, Physical Characters, Joints in Depth, Cloth & Destruction for ragdolls, physical characters, cloth and destruction, and T61 · Interaction & Traversal: Interaction Systems, Grabbing & Carrying, Doors by Hand, Climbing, Vaulting, Parkour & Cover for grabbing and carrying, doors you open by hand, climbing and cover. The sections below go deeper into the physics engine itself.

Fast objects and continuous collision detection#

A body moves in jumps, one step at a time. If it's fast enough to go from one side of a thin wall to the other in a single step, the wall is never seen to overlap it, and it tunnels straight through. The program's fourth block puts numbers on it: at 0.02 s a step, a ball has to cover more than its own size plus the wall's thickness in one step, so a 0.2 m ball and a 0.3 m wall are safe at 25 m/s and not at 60 m/s. Bullets, fast balls and falling debris all reach those speeds.

The Collision Detection setting trades speed for safety:

  • Discrete (default): check positions at each step. Cheapest; tunnels when fast.
  • Continuous: sweeps the body between steps against static colliders. Use it for fast bodies that mostly hit scenery.
  • Continuous Dynamic: sweeps against static colliders and against other bodies set to Continuous or Continuous Dynamic. Use it for the fast body when both sides move.
  • Continuous Speculative: looks ahead along the velocity, and also handles fast rotation (a spinning bat). Cheaper than sweeping, and works for kinematic bodies, but can occasionally catch on things it would have missed.

Turn these on only for the bodies that need them. For bullets, most games skip physics bodies entirely and use a raycast from the previous position to the new one each frame, which can't tunnel.

Stepping physics yourself#

With Simulation Mode set to Script, physics only moves when you call Physics.Simulate. That lets you pause physics while the rest of the game keeps running, step one tick at a time to debug a collision, or run physics at a different rate:

ManualStepper.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Runs the physics simulation from script instead of automatically, so it can be
    // paused or stepped one tick at a time while everything else keeps running.
    public class ManualStepper : MonoBehaviour
    {
        [SerializeField] bool paused;
        [SerializeField] int maxStepsPerFrame = 8;   // a long hitch mustn't freeze the game

        float accumulator;

        void OnEnable() => Physics.simulationMode = SimulationMode.Script;
        void OnDisable() => Physics.simulationMode = SimulationMode.FixedUpdate;

        void Update()
        {
            if (paused) return;

            float step = Time.fixedDeltaTime;
            accumulator += Time.deltaTime;
            int steps = 0;
            while (accumulator >= step && steps < maxStepsPerFrame)
            {
                Physics.Simulate(step);
                accumulator -= step;
                steps++;
            }
            if (steps == maxStepsPerFrame)
                accumulator = 0f;   // drop the rest of the hitch rather than catching up later
        }

        [ContextMenu("Step once")]
        void StepOnce() => Physics.Simulate(Time.fixedDeltaTime);
    }
}

A step further is a separate physics scene. Load a scene with LocalPhysicsMode.Physics3D and its objects get a physics world of their own, which you step with PhysicsScene.Simulate. Games use this to predict trajectories (simulate a copy of a ball for two seconds and draw where it went) or to run a pool table's physics many times to aim an AI shot, without disturbing the main world.

Contacts in bulk, and changing contacts#

Collision messages are convenient but go through a C# call per pair. For many bodies, Physics.ContactEvent delivers all contacts of a step in one batch, for colliders with Provides Contacts ticked. For effects the solver should obey, such as one-way platforms, conveyor belts or ignoring a contact under certain conditions, colliders with hasModifiableContacts let you change contact points before the solver uses them, through Physics.ContactModifyEvent. Both run off the main thread, so they're for code that's careful about what it touches.

Determinism#

Run the same physics twice with exactly the same inputs on the same machine and build, and PhysX gives the same result; Unity's Enhanced Determinism setting extends that to scenes where bodies are added in different orders. Unity doesn't promise identical results on different machines, CPUs or platforms. Games that need that (replays shared online, lockstep multiplayer) either re-sync positions regularly or use a deterministic engine. The DOTS physics packages are covered in T46 · DOTS: Entities (ECS), Jobs & Burst, and networking in T44 · Multiplayer Foundations: Networking Theory & Netcode for GameObjects.

Common mistakes & debugging#

No collision or trigger message#

  • Neither side has a Rigidbody (or, for collisions, neither is dynamic). Check the rules. A kinematic trigger is the usual fix for moving zones.
  • The method name or signature is wrong. OnCollisionEnter(Collider c) instead of (Collision c), OnTriggerEnter(Collision c), or onTriggerEnter in lower case: Unity calls none of these, and doesn't warn. Copy the signatures from the cheat sheet.
  • You're using the 2D versions. OnCollisionEnter2D and Collider2D are for 2D physics only (T09 · 2D Development: Sprites, Tilemaps, 2D Physics, 2D Animation & Lights). 3D and 2D colliders never interact.
  • The layers don't collide in the collision matrix, or a Layer Override excludes them.
  • Is Trigger is ticked when you wanted a collision, or not ticked when you wanted a trigger.
  • The script is on the wrong object. Messages go to the two colliders' GameObjects and their Rigidbodies' GameObjects, not to parents in general or to unrelated managers.

Bodies behaving badly#

  • An object falls through the floor: the floor has no collider, or a convex/primitive collider much smaller than the visible floor, or the object is fast enough to tunnel. Select the floor and look at the green outline.
  • An object explodes away at the start: it starts overlapping another collider, and physics pushes them apart hard. Common with objects placed inside each other, or a child collider inside its own character. Move them apart, or lower maxDepenetrationVelocity on the body.
  • Jitter: a Rigidbody moved through its Transform; a camera following a body in Update instead of LateUpdate; no interpolation on a body the camera follows; or stacked bodies with very different masses.
  • Something rolls like a ball but looks flat: a scaled primitive collider that can't follow the scale. Check the green outline.
  • Forces do nothing: the body is kinematic, asleep with a tiny force (add more, or call WakeUp), frozen by Constraints, or the force is being added in Update and overwritten by a velocity you set in FixedUpdate.
  • Forces are far too strong: an Impulse or VelocityChange added every step instead of once.
  • Jumps fail at high frame rates: the input is read in FixedUpdate. Read it in Update and keep a flag.
  • "Non-convex MeshCollider with non-kinematic Rigidbody" in the Console: tick Convex on the Mesh Collider, or replace it with primitives.

Queries that miss#

  • The mask is wrong: a layer index passed as a mask, or a mask left at Nothing in the Inspector (a new LayerMask field starts at Nothing).
  • The ray starts inside the collider you expected to hit, or inside a wall, or the shape cast already overlaps it.
  • The distance is too short, or the direction is local when it should be world (Vector3.forward instead of transform.forward).
  • A trigger is in the way. Pass QueryTriggerInteraction.Ignore.
  • It hits yourself: a ray from a character's chest that starts outside its capsule, or a shape cast that's wider than expected. Put the character on its own layer and leave that layer out of the mask.

For all three groups, the same tools help: draw your rays with Debug.DrawRay, turn on the Physics Debugger, log the messages you expect with the object names, and select objects during Play to watch their Rigidbody's velocity in the Inspector's Info foldout.

Exercises#

Exercise solutions are in the sandbox under Assets/Handbook/T08/Exercises.

1. A rubber ball and an ice patch

Make a Sphere with a Rigidbody that bounces on the meadow, and a flat Cube on the meadow that crates slide across. Use only Physics Materials, no code. Then find out what happens when the ball lands on the ice.

Solution

Rubber: Bounciness 0.8, Bounce Combine Maximum, frictions about 0.8. With Maximum, the ball bounces off any surface as if both were rubber. Ice: frictions 0.02, Friction Combine Minimum, on the Cube's Box Collider. Push a crate onto it and it glides. When the ball lands on the ice, each property is combined separately: bounce uses Maximum (the ball's mode wins over the ice's Average) and friction uses Minimum (the ice's mode wins over the ball's Average). So the ball bounces high and skids. Drop it from lower and lower: below 2 m/s impact it stops bouncing, because of the Bounce Threshold.

2. A laser sight

Give the lighthouse lamp a visible red beam that stops at the first thing it hits, using a raycast and a Line Renderer.

Solution
Exercises/LaserSight.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Draws a visible beam from this object to whatever its forward ray hits.
    [RequireComponent(typeof(LineRenderer))]
    public class LaserSight : MonoBehaviour
    {
        [SerializeField] float range = 30f;
        [SerializeField] LayerMask hitLayers = ~0;

        LineRenderer line;

        void Awake()
        {
            line = GetComponent<LineRenderer>();
            line.positionCount = 2;
            line.useWorldSpace = true;
        }

        void LateUpdate()   // after everything has moved this frame
        {
            Vector3 origin = transform.position;
            Vector3 end = origin + transform.forward * range;
            if (Physics.Raycast(origin, transform.forward, out RaycastHit hit, range, hitLayers, QueryTriggerInteraction.Ignore))
                end = hit.point;

            line.SetPosition(0, origin);
            line.SetPosition(1, end);
        }
    }
}

Add a Line Renderer to the beam's object (or a child pointing the same way), set its width to about 0.05 and give it an unlit red material. It runs in LateUpdate so it uses the lamp's rotation after everything else has moved it this frame. Pass a mask without the Water layer if the beam should ignore the sea.

3. Crates that thud

Make crates play a sound when they land, louder for harder landings and silent for gentle touches. A tumbling crate shouldn't sound like a drum roll.

Solution
Exercises/CrateThud.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Plays a thud when the crate hits something, louder for harder hits.
    [RequireComponent(typeof(AudioSource))]
    public class CrateThud : MonoBehaviour
    {
        [SerializeField] AudioClip thud;
        [SerializeField] float quietSpeed = 1f;   // below this, no sound
        [SerializeField] float loudSpeed = 8f;    // at or above this, full volume
        [SerializeField] float minGap = 0.1f;     // seconds between thuds, so a tumble isn't a drum roll

        AudioSource source;
        float lastThud = float.NegativeInfinity;

        void Awake() => source = GetComponent<AudioSource>();

        void OnCollisionEnter(Collision collision)
        {
            float speed = collision.relativeVelocity.magnitude;
            if (thud == null || speed < quietSpeed || Time.time < lastThud + minGap) return;

            lastThud = Time.time;
            float volume = Mathf.InverseLerp(quietSpeed, loudSpeed, speed);
            source.pitch = Random.Range(0.9f, 1.1f);
            source.PlayOneShot(thud, volume);
        }
    }
}

Add an AudioSource and this script to the Crate prefab, with any short thump sound. InverseLerp turns the impact speed into a 0-to-1 volume, the random pitch stops it sounding mechanical, and the minimum gap keeps a tumble to a few thuds. Audio is covered properly in T10 · Your First Complete Game Loop: States, Scenes, UI, Audio & Builds and T21 · Audio: Mixers, Sound Design, Adaptive Music, FMOD & Wwise.

4. A jump pad

Make a trigger pad that throws any Rigidbody straight up to a height you set, whatever its mass and however fast it was falling.

Solution
Exercises/JumpPad.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // A trigger that throws any Rigidbody straight up to a chosen height, whatever its mass.
    [RequireComponent(typeof(Collider))]
    public class JumpPad : MonoBehaviour
    {
        [SerializeField] float launchHeight = 4f;   // metres above the pad

        void Reset() => GetComponent<Collider>().isTrigger = true;

        void OnTriggerEnter(Collider other)
        {
            Rigidbody body = other.attachedRigidbody;
            if (body == null || body.isKinematic) return;

            // v² = 2gh: the upward speed that just reaches the height (ignoring damping).
            float upSpeed = Mathf.Sqrt(2f * -Physics.gravity.y * launchHeight);
            Vector3 v = body.linearVelocity;
            body.AddForce(Vector3.up * (upSpeed - v.y), ForceMode.VelocityChange);
        }
    }
}

The speed to reach height h is √(2gh), 8.86 m/s for 4 m. Using VelocityChange of the difference between that and the current vertical speed means a crate dropped onto the pad from high up gets the same launch as one pushed onto it, and mass doesn't matter. It won't launch the keeper: a CharacterController has no attachedRigidbody. To support it, give KeeperController a public method that sets its vertical velocity, and call it from the pad.

5. Breakable crates

Make crates break into planks when they land hard (a drop from the supply height onto the jetty) but not when they're set down or pushed.

Solution
Exercises/BreakableCrate.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Replaces the crate with loose pieces when it's hit hard enough.
    [RequireComponent(typeof(Rigidbody))]
    public class BreakableCrate : MonoBehaviour
    {
        [SerializeField] float breakImpulse = 120f;   // newton-seconds: about a 20 kg crate landing at 6 m/s
        [SerializeField] Rigidbody piecePrefab;       // a small plank with its own Rigidbody
        [SerializeField] int pieces = 6;
        [SerializeField] float pieceLifetime = 8f;

        Rigidbody body;
        bool broken;

        void Awake() => body = GetComponent<Rigidbody>();

        void OnCollisionEnter(Collision collision)
        {
            if (broken || collision.impulse.magnitude < breakImpulse) return;
            broken = true;   // several collisions can arrive in the same step

            if (piecePrefab != null)
            {
                for (int i = 0; i < pieces; i++)
                {
                    Vector3 offset = Random.insideUnitSphere * 0.3f;
                    Rigidbody piece = Instantiate(piecePrefab, transform.position + offset, Random.rotation);
                    // Carry on with the crate's motion, plus a little scatter.
                    piece.linearVelocity = body.linearVelocity + offset * 4f;
                    Destroy(piece.gameObject, pieceLifetime);
                }
            }
            Destroy(gameObject);
        }
    }
}

The test uses collision.impulse rather than speed, so a heavy crate breaks more easily than a light one hit at the same speed. A 20 kg crate stopped from 6 m/s takes an impulse of about 120 N·s, and a 7 m drop reaches about 11.7 m/s, so the supply drop breaks crates while gentle handling doesn't. Make the piece prefab a thin box with its own Rigidbody. The broken flag matters: several collisions can arrive in one step, and without it the crate would spawn pieces more than once before it's destroyed.

6. A trajectory preview

Draw the arc a crate thrown from a point would follow, then launch one to check that it follows the line.

Solution
Exercises/TrajectoryPreview.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Draws where a body launched from here would fly, and launches one to check.
    [RequireComponent(typeof(LineRenderer))]
    public class TrajectoryPreview : MonoBehaviour
    {
        [SerializeField] float launchSpeed = 12f;
        [SerializeField] float flightTime = 3f;   // seconds of path to draw
        [SerializeField] int points = 40;
        [SerializeField] Rigidbody projectilePrefab;

        LineRenderer line;

        void Awake()
        {
            line = GetComponent<LineRenderer>();
            line.useWorldSpace = true;
        }

        void Update()
        {
            Vector3 start = transform.position;
            Vector3 velocity = transform.forward * launchSpeed;
            Vector3 gravity = Physics.gravity;

            line.positionCount = points;
            for (int i = 0; i < points; i++)
            {
                float t = flightTime * i / (points - 1);
                // Constant acceleration: p = p0 + v t + ½ g t²
                line.SetPosition(i, start + velocity * t + 0.5f * t * t * gravity);
            }
        }

        [ContextMenu("Launch")]
        public void Launch()
        {
            if (projectilePrefab == null) return;
            Rigidbody body = Instantiate(projectilePrefab, transform.position, transform.rotation);
            body.linearDamping = 0f;   // the preview ignores air resistance, so the test body must too
            body.linearVelocity = transform.forward * launchSpeed;
        }
    }
}

With constant gravity and no damping, the position after time t is p₀ + vt + ½gt² (T06 · Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation). The launched body follows the line closely; it ends slightly off because physics moves in 0.02 s steps rather than continuously, and it leaves the line where it hits something. Add damping to the body and it falls short: the formula would need damping in it too, or a physics scene simulation as described in stepping physics yourself.

7. A rope bridge

Build a bridge of planks between two posts in code, with each plank hinged to the next, so it sags and sways when the keeper or a crate crosses it.

Solution
Exercises/RopeBridge.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T08
{
    // Builds a sagging bridge of planks between two anchors, joined by hinges.
    // Put it on an empty between two posts and assign the posts' Rigidbodies (kinematic).
    public class RopeBridge : MonoBehaviour
    {
        [SerializeField] Rigidbody startAnchor;
        [SerializeField] Rigidbody endAnchor;
        [SerializeField] int planks = 10;
        [SerializeField] Vector3 plankSize = new Vector3(1.6f, 0.08f, 0.45f);
        [SerializeField] float plankMass = 4f;
        [SerializeField] Material plankMaterial;

        void Start()
        {
            if (startAnchor == null || endAnchor == null || planks < 1) return;

            Vector3 from = startAnchor.position;
            Vector3 to = endAnchor.position;
            Vector3 along = (to - from) / planks;   // one plank's share of the gap
            Quaternion facing = Quaternion.LookRotation(along);

            Rigidbody previous = startAnchor;
            for (int i = 0; i < planks; i++)
            {
                GameObject plank = GameObject.CreatePrimitive(PrimitiveType.Cube);
                plank.name = $"Plank {i + 1}";
                plank.transform.SetParent(transform, false);
                plank.transform.SetPositionAndRotation(from + along * (i + 0.5f), facing);
                plank.transform.localScale = new Vector3(plankSize.x, plankSize.y, along.magnitude * 0.95f);
                if (plankMaterial != null) plank.GetComponent<Renderer>().sharedMaterial = plankMaterial;

                Rigidbody body = plank.AddComponent<Rigidbody>();
                body.mass = plankMass;
                body.angularDamping = 0.5f;

                // Hinge at the plank's near edge, swinging around its sideways axis.
                HingeJoint hinge = plank.AddComponent<HingeJoint>();
                hinge.connectedBody = previous;
                hinge.anchor = new Vector3(0f, 0f, -0.5f);
                hinge.axis = Vector3.right;
                previous = body;
            }

            // Tie the last plank to the far anchor.
            HingeJoint last = previous.gameObject.AddComponent<HingeJoint>();
            last.connectedBody = endAnchor;
            last.anchor = new Vector3(0f, 0f, 0.5f);
            last.axis = Vector3.right;
        }
    }
}

Give the two posts kinematic Rigidbodies so planks can hinge to them. Each plank hinges at its near edge to the one before, around its sideways axis, and the last also hinges to the far post. Planks are spaced at 95% of the gap so neighbours don't overlap; joints already stop connected bodies colliding with each other. Drop crates on it: the chain sags and swings. If it stretches, raise Default Solver Iterations, lower the crate mass or add more planks. The keeper can walk on it, but a CharacterController doesn't push it down, so it only sags under crates; that's the CharacterController's "not part of physics" in action.

8. A hovercraft

Make a hovercraft that floats a metre above the ground and the sea on four invisible springs, tilts over bumps, and is driven with the Move action. Hint: one raycast per corner.

Solution
Exercises/Hovercraft.csUnity sandboxC#
using UnityEngine;
using UnityEngine.InputSystem;

namespace Handbook.T08
{
    // A hovercraft held up by four ray "springs", one at each corner, and driven by thrust and torque.
    [RequireComponent(typeof(Rigidbody))]
    public class Hovercraft : MonoBehaviour
    {
        [SerializeField] Transform[] hoverPoints = new Transform[4];   // corner empties, children of the craft
        [SerializeField] float hoverHeight = 1f;
        [SerializeField] float springStrength = 40f;   // acceleration per metre of squeeze, per corner
        [SerializeField] float springDamping = 6f;     // stops it bouncing forever
        [SerializeField] float thrust = 15f;           // metres per second, per second
        [SerializeField] float turnRate = 4f;          // radians per second, per second
        [SerializeField] float sideGrip = 2f;          // how quickly sideways sliding dies away
        [SerializeField] LayerMask groundLayers = 1;

        Rigidbody body;
        InputAction move;
        Vector2 input;

        void Awake()
        {
            body = GetComponent<Rigidbody>();
            body.angularDamping = 2f;

            InputActionAsset actions = InputSystem.actions;
            if (actions == null)
            {
                Debug.LogError("No project-wide actions. Assign them in Project Settings > Input System Package.", this);
                enabled = false;
                return;
            }
            move = actions.FindAction("Player/Move", throwIfNotFound: true);
        }

        void Update() => input = move.ReadValue<Vector2>();

        void FixedUpdate()
        {
            int touching = 0;
            foreach (Transform point in hoverPoints)
            {
                if (point == null) continue;
                if (!Physics.Raycast(point.position, -transform.up, out RaycastHit hit, hoverHeight * 2f,
                                     groundLayers, QueryTriggerInteraction.Ignore))
                    continue;
                touching++;

                // A spring that pushes harder the more it's squeezed, minus damping on how fast it's closing.
                float squeeze = hoverHeight - hit.distance;
                float closingSpeed = Vector3.Dot(body.GetPointVelocity(point.position), transform.up);
                float lift = squeeze * springStrength - closingSpeed * springDamping;
                // Mass-scaled, and shared between the corners, so the tuning doesn't depend on mass.
                body.AddForceAtPosition(transform.up * (lift * body.mass / hoverPoints.Length), point.position, ForceMode.Force);
            }

            if (touching == 0) return;   // in the air: no steering

            body.AddForce(transform.forward * (input.y * thrust), ForceMode.Acceleration);
            body.AddTorque(transform.up * (input.x * turnRate), ForceMode.Acceleration);

            // Kill some sideways slide, or it handles like a bar of soap.
            float sideways = Vector3.Dot(body.linearVelocity, transform.right);
            body.AddForce(-transform.right * (sideways * sideGrip), ForceMode.Acceleration);
        }
    }
}

Each corner casts a ray down. If it hits within twice the hover height, it pushes up at that corner with a spring force: stronger the more it's squeezed, minus damping on how fast that corner is moving up or down, measured with GetPointVelocity. Pushing at the corners with AddForceAtPosition is what tilts it over bumps. The lift is scaled by mass so the same settings work for any mass. Four springs at 40 balance gravity at 9.81 × 4 ÷ 40 ≈ 1 m of squeeze in total, about 0.25 m per corner, so it rides about 0.75 m up. For it to hover over the sea, put the Sea layer in Ground Layers: rays can hit the Water layer even though the matrix stops it colliding. The side grip turns sideways sliding into forward travel, which is the difference between a car and a puck.

Check yourself#

A Cylinder scaled (34, 0.3, 34) has a default Capsule Collider. What shape does physics see?

  1. A flat disc 34 m across and 0.6 m tall
  2. A capsule 34 m long lying on its side
  3. A sphere 34 m across
  4. Nothing: scaled colliders are disabled

The capsule's radius follows the larger of the X and Z scales (17 m) and its height follows Y (0.6 m). A capsule can't be shorter than its diameter, so it becomes a sphere of radius 17 m.

You want a jump that always adds 5 m/s of upward speed, whatever the body's mass. Which AddForce(Vector3.up * 5f, mode)?

  1. ForceMode.Force, every step
  2. ForceMode.Impulse, once
  3. ForceMode.Acceleration, once
  4. ForceMode.VelocityChange, once

VelocityChange is applied at once and ignores mass. Impulse divides by mass; Force and Acceleration are spread over a step and meant to be applied continuously.

Your game runs at 144 fps. Why should you check jump.WasPressedThisFrame() in Update rather than in FixedUpdate?

  1. FixedUpdate can't read input
  2. Most frames have no fixed step, so a press in one of those frames would never be seen
  3. Update runs before physics, so the jump is higher
  4. It's only a style rule; both work the same

At 144 fps about two frames in three have no fixed step. Read one-off input in Update, store it, and use it in the next FixedUpdate.

A kinematic platform moved by MovePosition slides into a static wall. What happens?

  1. It passes through the wall, and no collision messages are sent
  2. It stops at the wall and sends OnCollisionEnter
  3. It bounces off the wall
  4. Unity logs an error

Kinematic bodies ignore collisions, and a kinematic–static pair sends no collision messages. Only dynamic bodies are pushed back.

A static Box Collider with Is Trigger ticked sits on the ground. The keeper (a CharacterController, no Rigidbody) walks into it. Does OnTriggerEnter fire?

  1. No: at least one side needs a Rigidbody
  2. Only if the trigger is made kinematic
  3. Yes: a CharacterController counts as a body for triggers
  4. Only if the keeper's script is enabled

CharacterControllers send and receive trigger messages with static triggers. SeaRespawn relies on it.

A raycast starts at a character's centre and points forward. Its mask includes the character's own layer. Does it hit the character's capsule?

  1. Yes, at distance 0
  2. No: rays don't hit colliders they start inside
  3. Only if the capsule is a trigger
  4. Only with Queries Hit Backfaces on

Rays and shape casts ignore colliders that already contain or overlap their starting point. Convenient here, but it's also why a ray starting inside a wall reports nothing.

You pass gameObject.layer, which is 6, as the layer mask of a raycast. Which layers can it hit?

  1. Layer 6
  2. Layers 0 to 6
  3. Every layer except 6
  4. Layers 1 and 2

A mask is a set of bits. 6 is binary 110: bits 1 and 2, which are TransparentFX and Ignore Raycast. The mask for layer 6 is 1 << 6.

What does Interpolate on a Rigidbody do?

  1. Draws the body between its last two physics positions, so it moves smoothly between steps, slightly behind
  2. Runs extra physics steps for that body
  3. Stops it tunnelling through thin walls
  4. Predicts where it will be next step and draws it there

Interpolate smooths what you see and lags by up to one step. Extrapolate predicts ahead. Neither changes the physics, and neither stops tunnelling; that's Collision Detection.

You add a non-convex Mesh Collider to a crate with a dynamic Rigidbody. What should you do?

  1. Nothing: it works, just more slowly
  2. Make the crate a trigger
  3. Tick Convex, or build the shape from primitive colliders
  4. Raise the solver iterations

Dynamic bodies can't use non-convex Mesh Colliders. A convex hull (up to 255 triangles) or a compound of primitives works.

Why does SeaRespawn disable the CharacterController before moving the keeper?

  1. To stop OnTriggerEnter firing again
  2. The controller keeps its own position, and the next Move could start from the old one
  3. A disabled controller resets its velocity
  4. Transforms can't be moved while a collider is enabled

The CharacterController holds its position inside the physics engine. Disabling and enabling it, or calling Physics.SyncTransforms, makes it pick up the new Transform position.

Cheat sheet#

Bodies

  • Collider only: static. + Rigidbody: dynamic. + Is Kinematic: script-driven
  • Dynamic: forces or linearVelocity in FixedUpdate
  • Kinematic: MovePosition, MoveRotation
  • Teleport: rb.position; never transform.position each step
  • Interpolate bodies the camera follows

Force modes

  • Force: ÷ mass, over the step
  • Acceleration: over the step, ignores mass
  • Impulse: ÷ mass, at once
  • VelocityChange: at once, ignores mass
  • Continuous: every step. Instant: once

Messages

  • OnCollisionEnter(Collision c), Stay, Exit
  • OnTriggerEnter(Collider other), Stay, Exit
  • OnControllerColliderHit(hit)
  • Collisions need a dynamic body; triggers need any Rigidbody or a CharacterController
  • Use other.attachedRigidbody for the root

Queries

  • Raycast(from, dir, out hit, dist, mask), plus QueryTriggerInteraction.Ignore
  • SphereCast, CapsuleCast, BoxCast: swept shapes
  • OverlapSphereNonAlloc(pos, r, buf, mask)
  • Linecast(a, b): anything between?
  • Ignore what they start inside

Layers

  • Matrix: Project Settings > Physics
  • [SerializeField] LayerMask mask;
  • LayerMask.GetMask("Props"), 1 << layer, ~mask
  • gameObject.layer is an index, not a mask
  • Overrides: Include/Exclude Layers per collider

Defaults (new 6.3 project)

  • Gravity (0, −9.81, 0); Fixed Timestep 0.02
  • Max Allowed Timestep 0.333; solver iterations 6
  • Contact offset 0.01; bounce threshold 2
  • Friction 0.6/0.6, bounciness 0, Average
  • Queries hit triggers: on; backfaces: off

Unity 6 names

  • velocity → linearVelocity
  • drag, angularDrag → linearDamping, angularDamping
  • PhysicMaterial → PhysicsMaterial
  • autoSimulation → simulationMode

Character

  • CharacterController: Move in Update, own gravity
  • Skin Width ≈ 10% of radius; Step Offset 0.1–0.4
  • Pushes nothing unless you code it
  • Teleport: disable, move, enable
  • Rigidbody character: freeze rotation, no friction, ground check

Glossary#

Collider
A component that gives an object a shape for physics contacts and queries, separate from what's drawn.
Compound collider
Several colliders under one Rigidbody, treated as one rigid shape.
Convex hull
A shape with no dents or holes wrapped around a mesh; what a Mesh Collider uses with Convex ticked.
Rigidbody
A component that lets physics move an object with gravity, forces and collisions.
Dynamic body
A Rigidbody that physics moves. The default.
Kinematic body
A Rigidbody moved only by script, which pushes dynamic bodies but isn't pushed back.
Static collider
A collider with no Rigidbody, for scenery that never moves.
Trigger
A collider with Is Trigger ticked: not solid, and reports what enters and leaves it.
Fixed timestep
The constant amount of time physics advances in each step, 0.02 s by default.
Interpolation
Drawing a body between its last two physics positions so it moves smoothly between steps.
Force mode
How AddForce interprets its vector: with or without mass, over a step or at once.
Impulse
A sudden change in momentum, measured in newton-seconds; mass times change in velocity.
Physics Material
An asset that sets a collider's friction and bounciness and how they combine with another surface's.
Layer Collision Matrix
The table in the Physics settings that says which pairs of layers can collide.
Layer mask
A 32-bit value with one bit per layer, used to choose which layers a query sees.
Raycast
A query that follows a thin line from a point and reports what it hits.
Shape cast
A query that sweeps a sphere, capsule or box along a direction and reports the first thing it touches.
Overlap test
A query that returns every collider inside a volume.
Joint
A component that connects two bodies, or a body and the world, and limits how they move relative to each other.
Tunnelling
A fast body passing through a thin collider because it's on one side at one step and the other side at the next.
Continuous collision detection
Collision Detection modes that sweep or look ahead between steps to catch fast collisions.
Sleeping
A resting body that physics stops simulating until something disturbs it.
Contact offset
The distance before touching at which physics starts generating contacts, 0.01 m by default.