Game Maths: Vectors, Trigonometry, Matrices, Quaternions & Interpolation
The maths every Unity game uses, explained with pictures and working code: vectors, the dot and cross products, trigonometry, local and world space, matrices, Euler angles and quaternions, interpolation and smoothing that behave the same at any frame rate, randomness and noise, and the limits of floating point. The guided build gives Lighthouse Island a sweeping beam that spots ships, gulls that circle the tower, a telescope that tracks ships and a buoy that rocks on the waves.
- Study time
- 10–14 hours
- Reading time
- 123 min read · 11,531 words
- Prerequisites
- T03 · C# Fundamentals: Types, Logic, Methods & Classes, T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable
- Courses merged
- 3 course files
- Your progress
- —
- Use vectors for positions, directions and distances, and know when
sqrMagnitudeandnormalizedare the right tools - Use the dot product to test facing, cones and angles, and the cross product to tell left from right and build normals
- Place things on circles and waves with sine and cosine, and turn a direction into an angle with
Atan2 - Convert points and directions between local and world space, and read what a transformation matrix does
- Rotate objects with quaternions without gimbal lock, and predict the result of multiplying two rotations
- Smooth movement and rotation in ways that are identical at 30 and 144 frames per second, and choose between Lerp, Damp, MoveTowards and SmoothDamp
- Use random numbers, seeds and Perlin noise for natural-looking variation
- Explain why floats drift and far-away objects jitter, and write comparisons that survive it
Every moving thing in a game is maths: where an enemy is, which way it faces, whether it can see you, how a camera eases into place. You don't need a maths degree for any of it. You need about a dozen ideas, a clear picture of each, and the Unity method that does the work. This chapter gives you those, and shows what goes wrong when they're used carelessly.
Each idea comes with a picture, a small example, and the method Unity provides. Numbers quoted on this page were checked by running Unity 6.3's own maths code, and every script compiles against the 6.3 editor with no warnings. They're copied into the sandbox under Assets/Handbook/T06. You need the C# from T03 · C# Fundamentals: Types, Logic, Methods & Classes and the scripting rules from T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable, especially Time.deltaTime.
The big picture#
Unity's world is a 3D grid. Every position is three numbers: how far along X, Y and Z it is from the world origin, (0, 0, 0). The conventions are fixed, and it pays to learn them now:
- Y is up. Gravity pulls along −Y, the ground is usually the XZ plane, and "height" means Y.
- Z is forward and X is right. An object with no rotation faces +Z. The blue arrow of the Move tool is Z, red is X, green is Y.
- It's left-handed. Point your left thumb along +X, your index finger up along +Y, and your middle finger shows +Z, away from you. Many maths books and tools such as Blender use right-handed systems, which is why models sometimes import facing backwards (T31 · Blender for Unity: Modelling, UVs, Texturing & Export).
- One unit is one metre. Physics, audio falloff and the default character sizes all assume it. A 2 m door is 2 units tall.
- Angles in the Inspector are degrees; the maths functions use radians. More on that in the trigonometry section.
With those conventions, the chapter builds up in layers. Vectors describe positions and directions. Two operations on vectors, the dot and cross products, answer most "where is it relative to me?" questions. Trigonometry converts between angles and directions. Spaces and matrices explain how a child's position relates to its parent's. Quaternions store rotations. Interpolation moves any of these smoothly over time. Finally, randomness adds variety and floating point sets the limits of precision.
Vectors: points, directions and distances#
A vector is a list of numbers, written (x, y, z). Unity's Vector3 holds three floats and Vector2 holds two. The same type is used for two different things, and keeping them apart in your head prevents many bugs:
- A point (a position): "the thief is at (5, 0, 5)". It only makes sense relative to an origin.
- A direction or offset (a displacement): "three metres right and four forward", (3, 0, 4). It has a length and a direction but no fixed place; you can apply it anywhere.
The rules follow from that. Point − point = the offset from one to the other. Point + offset = a new point. Offset + offset = a combined offset. Offset × number = a longer or shorter offset. Point + point means nothing on its own (although averaging two points is fine: it's halfway between them).
From A to B is B − A#
The single most useful line of game maths is Vector3 toTarget = target - me;. It's the arrow from me to the target. Its length is the distance, and its direction is the way to go. Remember the order as "destination minus start". Getting it backwards gives the arrow pointing from the target to you, so your enemy runs away.
Length: magnitude and sqrMagnitude#
A vector's length is its magnitude, from Pythagoras: √(x² + y² + z²). Vector3.Distance(a, b) is just (a - b).magnitude.
The square root is the slow part, and you often don't need it. To ask "is the enemy within 10 m?", compare squared lengths: (enemy - me).sqrMagnitude < 10f * 10f. Both sides are squared, so the answer is the same, without a square root. In one check it hardly matters; in a loop over hundreds of objects every frame, it's a habit worth having. Don't compare sqrMagnitude with an unsquared distance, though: sqrMagnitude < 10f means "within about 3.16 m".
Direction only: normalizing#
A vector of length 1 is a unit vector, and it represents a pure direction. v.normalized returns v divided by its length; v.Normalize() changes v itself. You normalize whenever the length would get in the way: to move at a set speed in a direction (direction.normalized * speed), or to compare directions with the dot product.
Two things to know. First, a zero vector has no direction, so Unity returns (0, 0, 0) instead of dividing by zero. It does the same for any vector shorter than about 0.00001. Code that normalizes the difference between two positions that might be equal must handle a zero result. Second, the classic diagonal bug: if the input is (1, 0, 1) when two keys are held, its length is about 1.41, so diagonal movement is 41% faster. Normalizing the input, or clamping its length to 1 with Vector3.ClampMagnitude(input, 1f), fixes it. Clamping is usually better for analogue sticks, because it keeps a half-pushed stick at half speed.
Vectors are just arithmetic#
There's nothing magical inside Vector3. Here is a plain C# version with the operations above and the two products coming next, run on the guard and thief from the diagram. It's a console program, so you can run it without Unity.
using System;
// A bare-bones Vector3, to show that Unity's version is only arithmetic.
readonly struct Vec3
{
public readonly float X, Y, Z;
public Vec3(float x, float y, float z) { X = x; Y = y; Z = z; }
public static Vec3 operator +(Vec3 a, Vec3 b) => new Vec3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vec3 operator -(Vec3 a, Vec3 b) => new Vec3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vec3 operator *(Vec3 a, float k) => new Vec3(a.X * k, a.Y * k, a.Z * k);
public float Magnitude => MathF.Sqrt(X * X + Y * Y + Z * Z);
public Vec3 Normalized => this * (1f / Magnitude);
public static float Dot(Vec3 a, Vec3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;
public static Vec3 Cross(Vec3 a, Vec3 b) => new Vec3(
a.Y * b.Z - a.Z * b.Y,
a.Z * b.X - a.X * b.Z,
a.X * b.Y - a.Y * b.X);
public static float Angle(Vec3 a, Vec3 b)
{
float cos = Dot(a.Normalized, b.Normalized);
cos = MathF.Max(-1f, MathF.Min(1f, cos)); // rounding can push it past ±1
return MathF.Acos(cos) * 180f / MathF.PI;
}
public override string ToString() => $"({X:F2}, {Y:F2}, {Z:F2})";
}
static class Program
{
static void Main()
{
var guard = new Vec3(2, 0, 1);
var guardForward = new Vec3(0, 0, 1);
var thief = new Vec3(5, 0, 5);
Vec3 toThief = thief - guard;
Console.WriteLine($"Guard to thief: {toThief}, {toThief.Magnitude:F2} m");
Console.WriteLine($"Direction: {toThief.Normalized}");
float dot = Vec3.Dot(guardForward, toThief.Normalized);
Console.WriteLine($"Dot with forward: {dot:F2} ({(dot > 0 ? "in front" : "behind")})");
Console.WriteLine($"Angle off forward: {Vec3.Angle(guardForward, toThief):F1} degrees");
Vec3 side = Vec3.Cross(guardForward, toThief);
Console.WriteLine($"Cross with forward: {side} ({(side.Y > 0 ? "to the right" : "to the left")})");
Vec3 halfway = guard + toThief * 0.5f;
Console.WriteLine($"Halfway point: {halfway}");
}
}Guard to thief: (3.00, 0.00, 4.00), 5.00 m
Direction: (0.60, 0.00, 0.80)
Dot with forward: 0.80 (in front)
Angle off forward: 36.9 degrees
Cross with forward: (0.00, 3.00, 0.00) (to the right)
Halfway point: (3.50, 0.00, 3.00)The Vector3 toolbox#
| Member | What it gives you |
|---|---|
Vector3.zero, one, up, down, left, right, forward, back | Constant vectors. forward is (0, 0, 1) |
v.magnitude, v.sqrMagnitude | Length, and length squared (no square root) |
v.normalized, v.Normalize() | Unit-length copy, or normalize in place. Zero for very short vectors |
Vector3.Distance(a, b) | Distance between two points |
Vector3.Dot(a, b), Vector3.Cross(a, b) | The two products (next sections) |
Vector3.Angle(a, b), Vector3.SignedAngle(a, b, axis) | Angle between directions in degrees: 0 to 180, or −180 to 180 around an axis |
Vector3.Project, ProjectOnPlane, Reflect | Shadows of a vector on a line or plane, and bounces |
Vector3.ClampMagnitude(v, max) | Same direction, length at most max |
Vector3.Scale(a, b), Vector3.Min, Vector3.Max | Component by component: (a.x·b.x, a.y·b.y, a.z·b.z), smallest or largest of each |
Vector3.Lerp, MoveTowards, SmoothDamp, Slerp, RotateTowards | Moving from one value to another (interpolation section) |
Vector3 is a struct, so it's copied by value (T04 · C# Beyond Basics: OOP, Generics, Collections, LINQ, Delegates & Memory). That's why transform.position.x = 5; doesn't compile: position returns a copy, and changing a field of a temporary copy would do nothing. Copy it into a variable, change the copy, and assign it back.
Vector3 p = transform.position;
p.y = 2f;
transform.position = p;== on vectors is approximate: it's true when the two are less than about 0.00001 apart. (1, 0, 0) equals (1.000001, 0, 0) but not (1.001, 0, 0). That's convenient for "have I arrived?" checks after MoveTowards, which lands exactly on the target, but for anything else, compare a distance with a tolerance you choose.
The dot product: how much do two directions agree?#
The dot product multiplies two vectors component by component and adds the results: a·b = a.x·b.x + a.y·b.y + a.z·b.z. One number comes out. Its meaning is geometric: a·b = |a| × |b| × cos(angle between them). With two unit vectors the lengths are 1, so the dot product is the cosine of the angle between them:
- 1: the same direction.
- 0: at right angles.
- −1: opposite directions.
- In between: partly agreeing. Positive means less than 90° apart, negative means more.
What it's for#
- In front or behind? Only the sign matters, and the sign doesn't depend on the lengths, so you don't even need to normalize:
Dot(forward, toTarget) > 0means in front. Exercise 3 builds an alarm with this. - Inside a vision cone? Normalize the direction to the target and compare its dot product with the cosine of the cone's half-angle. A guard who sees 30° either side of straight ahead sees anything with a dot of at least cos 30° ≈ 0.866. Comparing cosines avoids computing the angle at all.
- The angle itself.
Mathf.Acos(dot)gives it in radians, which is whatVector3.Angledoes for you, returning degrees between 0 and 180. Rounding can push a dot product of unit vectors slightly past 1, andAcos(1.0000001f)is NaN, so the VectorLab program clamps it first.Vector3.Angledoes the same. - How much of a vector points along a direction. With a unit direction d,
Dot(v, d)is the length of v's shadow along d: the speed at which a car moves forward (Dot(velocity, forward)), or how directly a hit lands. - Is a surface floor, wall or ceiling? Dot the surface normal with
Vector3.up: near 1 is a floor, near 0 a wall, near −1 a ceiling. A slope limit of 45° means "walkable if the dot is at least cos 45° ≈ 0.707" (T08 · 3D Physics: Rigidbodies, Colliders, Raycasts & Character Controllers). - Lighting: a surface is lit in proportion to the dot product of its normal with the direction to the light. Every shader you write later starts with this (T34 · How GPUs Draw: The Pipeline, Coordinate Spaces & Shader Maths).
The handbook's maths helpers live in one static class. Its InCone method is the vision-cone test, written to skip the square root when the target is out of range. The guided build's lighthouse uses it to spot ships.
using UnityEngine;
namespace Handbook.T06
{
// Small maths helpers the rest of the chapter uses.
public static class MathUtil
{
// Where value sits between inMin and inMax, mapped to the same place
// between outMin and outMax. Clamped to the output range.
public static float Remap(float value, float inMin, float inMax, float outMin, float outMax)
{
return Mathf.Lerp(outMin, outMax, Mathf.InverseLerp(inMin, inMax, value));
}
// Moves current towards target by the same fraction per second at any frame rate.
// sharpness: higher is snappier (about 1 to 25 is useful).
public static float Damp(float current, float target, float sharpness, float deltaTime)
{
return Mathf.Lerp(current, target, 1f - Mathf.Exp(-sharpness * deltaTime));
}
public static Vector3 Damp(Vector3 current, Vector3 target, float sharpness, float deltaTime)
{
return Vector3.Lerp(current, target, 1f - Mathf.Exp(-sharpness * deltaTime));
}
public static Quaternion Damp(Quaternion current, Quaternion target, float sharpness, float deltaTime)
{
return Quaternion.Slerp(current, target, 1f - Mathf.Exp(-sharpness * deltaTime));
}
// True if point is inside a cone that starts at origin, points along forward
// (a unit vector), is range metres long and halfAngle degrees either side of forward.
public static bool InCone(Vector3 origin, Vector3 forward, Vector3 point, float range, float halfAngle)
{
Vector3 toPoint = point - origin;
float sqrDistance = toPoint.sqrMagnitude;
if (sqrDistance > range * range) return false; // too far: no square root needed
if (sqrDistance < 1e-6f) return true; // standing on the origin
float cosLimit = Mathf.Cos(halfAngle * Mathf.Deg2Rad);
Vector3 direction = toPoint / Mathf.Sqrt(sqrDistance);
return Vector3.Dot(forward, direction) >= cosLimit;
}
}
}The cross product: left, right and perpendicular#
The cross product of two 3D vectors is another vector, perpendicular to both. Its length is |a| × |b| × sin(angle), so it's zero when they're parallel and largest at right angles. Its direction follows Unity's left-hand rule, and order matters: Cross(a, b) is -Cross(b, a). In Unity, Cross(Vector3.forward, Vector3.up) is (−1, 0, 0), left, and Cross(Vector3.right, Vector3.up) is forward. Rather than memorising the hand rule, remember one useful result and check others in code.
Is it on my left or my right?#
Take the cross product of your forward direction with the direction to the target. With Y up, the result points straight up or down, and its sign says which side: Cross(forward, toTarget).y > 0 means the target is on your right. The VectorLab program shows it: the thief is 3 m to the right of the guard, and the cross product is (0, 3, 0). This is how a turret decides which way to turn, or a car's AI which way to steer.
Unity also does it for you: Vector3.SignedAngle(from, to, axis) returns the angle with a sign, positive for a clockwise turn seen from the axis's tip. With Vector3.up as the axis, which suits most ground-based questions, positive means a turn to the right. From forward to right is 90, and to a target ahead and to the left at (−1, 0, 1), it's −45.
Building perpendicular directions#
- A surface normal. For a triangle with corners a, b and c,
Cross(b - a, c - a)is perpendicular to it. Unity treats a triangle as front-facing when its corners go clockwise as you look at it, and for clockwise corners this cross product points out of the front, towards you. Normalize it to get the normal that lighting uses. This is how procedural meshes get their normals (T26 · Procedural Generation: Noise, Terrain, Meshes, Voxels, Dungeons & Cities). - A right direction from a forward one.
Cross(Vector3.up, forward).normalizedis the direction to your right along the ground, whatever way you face. Strafing and camera-relative movement use this (T07 · Input System, Movement & Cameras (Cinemachine)). - An axis to turn around. To rotate direction a towards direction b, turn around
Cross(a, b). (Quaternion.FromToRotation(a, b)does this for you.)
Here's a Scene-view helper that shows all three measures live. Put it on an object, drag another into Target, and move them around: the label shows the dot product, the signed angle, and which side the target is on. The Handles class is editor-only, so that part is wrapped in #if UNITY_EDITOR; without it, a player build would fail to compile.
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Handbook.T06
{
// Scene view helper: draws this object's forward and the line to a target,
// and labels the dot product, the signed angle and which side the target is on.
public class BearingGizmo : MonoBehaviour
{
[SerializeField] Transform target;
void OnDrawGizmos()
{
if (target == null) return;
Vector3 from = transform.position;
Vector3 toTarget = target.position - from;
Vector3 forward = transform.forward;
Gizmos.color = Color.cyan;
Gizmos.DrawLine(from, from + forward * 2f);
Gizmos.color = Color.magenta;
Gizmos.DrawLine(from, target.position);
#if UNITY_EDITOR
float dot = Vector3.Dot(forward, toTarget.normalized);
float angle = Vector3.SignedAngle(forward, toTarget, Vector3.up);
float side = Vector3.Cross(forward, toTarget).y; // above 0: target is to the right
string where = side > 0.001f ? "right" : side < -0.001f ? "left" : "dead ahead or behind";
Handles.Label(from + Vector3.up * 1.5f, $"dot {dot:F2}\nangle {angle:F0}°\n{where}");
#endif
}
}
}Projection, reflection and the closest point#
Projection splits a vector into the part along a direction and the part across it. Imagine a light shining straight down onto a line or a plane: the projection is the vector's shadow.
Vector3.Project(v, direction): the part of v along the direction.Project((2, 3, 0), Vector3.right)is (2, 0, 0).Vector3.ProjectOnPlane(v, normal): the part of v lying in the plane with that normal, which is v minus its projection on the normal.ProjectOnPlane((1, 1, 1), Vector3.up)is (1, 0, 1). Use it to flatten a direction onto the ground: a camera that looks down at the player still gives a level "forward" for movement, once projected and normalized. The lighthouse beam in the guided build flattens its forward direction this way.Vector3.Reflect(direction, normal): a bounce. The part along the normal flips, the rest stays.Reflect((1, −1, 0), Vector3.up)is (1, 1, 0): a ball falling to the right bounces up to the right. Pass a unit normal, such as the one a raycast returns.
The same idea finds the closest point on a line segment, which you need for "how far is the player from this laser beam?" or "where on the road is the car?". Project the point onto the segment's direction, express that as a fraction t of the segment's length, clamp t to 0–1 so you stay between the ends, and walk that far along. Exercise 6 asks you to write it; its solution is a short console program.
For a plane, the signed distance of a point p from a plane through q with unit normal n is Dot(p - q, n): positive on the side the normal points to. Unity's Plane struct wraps this (GetDistanceToPoint, GetSide, ClosestPointOnPlane), and Plane.Raycast finds where a ray, such as one from the mouse, hits a flat ground without any colliders. T07 · Input System, Movement & Cameras (Cinemachine) uses that for clicking on the ground.
Trigonometry: angles, circles and waves#
Trigonometry converts between angles and directions. You need three functions and one unit.
Degrees and radians#
A radian is the angle at which the arc around a circle is as long as its radius. A full turn is 2π radians (about 6.283), a half turn is π, a right angle is π/2. Unity's Inspector, Quaternion.Euler, Vector3.Angle and transform.Rotate use degrees; Mathf.Sin, Cos, Tan, Atan2 and Acos use radians. Convert with the constants Mathf.Deg2Rad and Mathf.Rad2Deg: Mathf.Sin(30f * Mathf.Deg2Rad) is 0.5, while Mathf.Sin(30f) is about −0.99, the sine of 30 radians. When an angle "jumps around wildly", check the units first.
Sine and cosine: a point on a circle#
Walk an angle θ round a circle of radius 1, starting from the +X axis. The point you reach is (cos θ, sin θ). That one fact gives you circles, orbits, waves, and conversions from angles to directions.
// A point on a circle round a centre, laid on the ground (angle a in radians).
Vector3 onCircle = centre + new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * radius;
// Up and down between -height and +height, frequency times a second.
float bob = Mathf.Sin(Time.time * frequency * 2f * Mathf.PI) * height;- A circle around a centre: in 3D you lay the circle on the ground, so the "up" of the circle diagram becomes Z. As a increases, the point goes anticlockwise seen from above.
- A wave: sine rises and falls once per 2π radians, so multiplying time by 2π × frequency gives that many cycles a second. Add a different phase per object (
Time.time + offset) so a row of them doesn't move in step. - A direction from an angle: (cos θ, sin θ) is a unit vector. Spread 12 bullets evenly by stepping θ by 360°/12 = 30° each.
- Tangent is sin/cos, the slope. You'll use it least of the three, mainly for field-of-view sums: half the height a camera sees at distance d is d × tan(fov/2).
This component flies an object round a centre point with a gentle bob, and turns it to face the way it's moving. The guided build puts gulls round the lighthouse with it.
using UnityEngine;
namespace Handbook.T06
{
// Flies in a circle around a centre point, bobbing up and down, facing along its path.
public class Orbiter : MonoBehaviour
{
[SerializeField] Transform centre;
[SerializeField, Min(0.1f)] float radius = 6f;
[Tooltip("Degrees per second. Positive flies anticlockwise seen from above.")]
[SerializeField] float angularSpeed = 40f;
[SerializeField] float bobHeight = 0.5f;
[SerializeField] float bobsPerSecond = 0.5f;
[SerializeField, Range(0f, 360f)] float startAngle;
float angle; // degrees around the circle
float baseHeight;
void Start()
{
angle = startAngle;
baseHeight = transform.position.y;
}
void Update()
{
if (centre == null) return;
angle = Mathf.Repeat(angle + angularSpeed * Time.deltaTime, 360f);
float radians = angle * Mathf.Deg2Rad;
// A point on a circle is (cos, sin) times the radius. Lay it on the XZ ground plane.
Vector3 offset = new Vector3(Mathf.Cos(radians), 0f, Mathf.Sin(radians)) * radius;
float bob = Mathf.Sin(Time.time * bobsPerSecond * 2f * Mathf.PI) * bobHeight;
Vector3 previous = transform.position;
Vector3 next = centre.position + offset;
next.y = baseHeight + bob;
transform.position = next;
// Face the way we moved. Skip tiny moves, whose direction is mostly rounding error.
Vector3 travel = next - previous;
if (travel.sqrMagnitude > 1e-6f)
transform.rotation = Quaternion.LookRotation(travel, Vector3.up);
}
}
}Mathf.Repeat(angle, 360f) wraps the angle back into 0–360 so it never grows huge, which matters for precision (see the floating point section). LookRotation(travel, Vector3.up) turns the travel direction into a rotation; it's covered in the rotations section.
From a direction back to an angle: Atan2#
Going the other way, "what angle is this direction?", is the job of Mathf.Atan2(y, x). It returns the angle of the point (x, y) from the +X axis in radians, between −π and π, and unlike plain arctangent it handles all four quarters of the circle and x = 0. Atan2(1, 0) is 90°, Atan2(0, −1) is 180° and Atan2(−1, 0) is −90° (once converted with Rad2Deg). Note the order: y first.
Which components you pass depends on the plane:
- 3D, turning on the ground:
Atan2(dir.x, dir.z) * Rad2Degis the Y rotation (yaw) that facesdir, measured from +Z towards +X, the same way Unity measures Y rotations. Passing x and z in this order is deliberate. - 2D, a sprite that faces +X:
Atan2(dir.y, dir.x) * Rad2Degis the Z rotation that points it atdir(T09 · 2D Development: Sprites, Tilemaps, 2D Physics, 2D Animation & Lights).
The guided build's telescope uses Atan2 twice, once for yaw and once for pitch, so that it can clamp each angle separately.
Local and world space#
T02 · The Unity Editor: Scenes, GameObjects, Components & Prefabs showed that a child's Transform values are relative to its parent. Every object therefore defines its own coordinate system, its local space: its origin is the object's pivot, its axes are the object's red, green and blue arrows, and its unit length is stretched by its scale. World space is the scene's own grid. The same point has different coordinates in each, and much of game code is converting between them.
| Method | Converts | Applies | Use for |
|---|---|---|---|
TransformPoint(p) | local → world | scale, rotation, position | Where is the muzzle of my gun, 1 m in front of me? |
InverseTransformPoint(p) | world → local | the reverse | Is that enemy in front of me, to my left, above me? (look at the local x, y, z) |
TransformDirection(d) | local → world | rotation only; length kept | Turn local input "forward" into a world direction |
InverseTransformDirection(d) | world → local | rotation only | How fast am I moving sideways, relative to myself? |
TransformVector(v) | local → world | rotation and scale | An offset that should stretch with the object |
InverseTransformVector(v) | world → local | the reverse | Rarely; the scaled counterpart of the direction version |
transform.forward, right and up are the object's local axes expressed in world space: transform.forward is exactly TransformDirection(Vector3.forward). You can also assign them: transform.forward = dir; turns the object to face dir.
Space.Self and Space.World#
transform.Translate and transform.Rotate take an optional Space. The default is Space.Self: Translate(0, 0, 1) moves 1 m along the object's own forward, and Rotate(0, 90, 0) turns it around its own up axis. With Space.World, the numbers are world directions and world axes. For an object standing upright, rotating around its own up and around the world's up are the same; for a tilted object, they're not. The lighthouse beam in the guided build spins with Space.World, so even if you tilt the lamp to look down at the sea, it sweeps horizontally round the island instead of wobbling.
Matrices: what a Transform really is#
A matrix is a grid of numbers that transforms vectors. Every Transform has a 4×4 matrix, transform.localToWorldMatrix, that does in one multiplication everything TransformPoint does: scale, then rotate, then move. Its inverse, worldToLocalMatrix, goes back. You rarely build matrices by hand in gameplay code, but you meet them in shaders, in Graphics.RenderMeshInstanced, in Gizmos and in camera maths, so it helps to read them.
Matrix4x4.TRS(position, rotation, scale)builds the matrix for a translation, rotation and scale. The name lists them in the order they're written mathematically; applied to a point, scale happens first, then rotation, then translation.m.MultiplyPoint3x4(p)transforms a point, including translation. (MultiplyPointalso handles the perspective divide that camera projection matrices need, and is slower.)m.MultiplyVector(v)transforms a direction: rotation and scale, no translation.m.inverseundoes it, anda * bcombines two matrices, applying b first.
A worked example: TRS((10, 0, 0), 90° about Y, scale 2) applied to the point (0, 0, 1). Scale doubles it to (0, 0, 2); the 90° turn takes forward to right, giving (2, 0, 0); the translation adds (10, 0, 0). The result is (12, 0, 0). Applied to the direction (0, 0, 1), there's no translation, so the result is (2, 0, 0).
void OnDrawGizmosSelected()
{
// Everything drawn after this is in the object's local space.
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(Vector3.zero, Vector3.one); // its own unit cube, rotated and scaled
}Rotations: Euler angles and quaternions#
Rotation is the hardest part of game maths, and the part where Unity's API most rewards understanding. There are two ways to describe a rotation.
Euler angles#
Euler angles are three angles in degrees, one per axis: the X, Y and Z of the Inspector's Rotation field. For an upright object, Y is yaw (turning left and right), X is pitch (nose up and down) and Z is roll (tipping sideways). They're easy for people to read and type, which is why the Inspector shows them.
Three angles applied in sequence need an order, and the order changes the result. Unity's Quaternion.Euler(x, y, z) rotates around Z first, then X, then Y, all around the world axes. The signs are worth checking once: a positive X rotation tips the nose down, a positive Y rotation turns from forward to right, and a positive Z rotation turns right towards up, which looks anticlockwise from the default camera. (The rule behind all three: look from the positive end of the axis back towards the origin, and a positive rotation is clockwise.)
Euler angles have two problems:
- Gimbal lock. Set an object's X rotation to 90 in the Inspector, then drag Y and then Z. Both now spin it around the same axis: one of the three degrees of freedom has disappeared. It happens whenever the middle rotation of the sequence (X, for Unity) reaches ±90°, because the first and last axes line up. A camera that tries to look straight up or down with Euler angles hits it.
- They don't interpolate well. Going from (0, 350, 0) to (0, 10, 0) by blending the numbers sweeps 340° the long way round instead of 20° the short way, and blending all three angles at once takes strange curved paths.
Quaternions#
So Unity stores rotations as quaternions: transform.rotation is a Quaternion, and the Euler angles in the Inspector are just a friendly display. A quaternion is four numbers (x, y, z, w) that encode "turn by this angle around this axis". The maths inside is unusual, but you never need to touch x, y, z or w. Treat a quaternion as a sealed box that means "a rotation", and use these methods:
| Method | Makes |
|---|---|
Quaternion.identity | No rotation |
Quaternion.Euler(x, y, z) | From Euler angles in degrees |
Quaternion.AngleAxis(angle, axis) | Turn angle degrees around axis |
Quaternion.LookRotation(forward, up) | Face forward, with the top towards up (default Vector3.up) |
Quaternion.FromToRotation(a, b) | The turn that takes direction a to direction b, for example aligning an object's up with a surface normal |
Quaternion.Inverse(q) | The opposite turn |
Quaternion.Slerp, Lerp, RotateTowards | Part way from one rotation to another (interpolation section) |
Quaternion.Angle(a, b) | How many degrees apart two rotations are |
q.eulerAngles | Back to Euler angles, each in 0–360 |
Quaternions don't suffer gimbal lock, and they interpolate along the shortest path at a constant speed. The price is that you can't read them directly, and a rotation of more than 360° can't be stored: a quaternion knows the final orientation, not how many times you spun to reach it. Keep a float angle if you need to count turns, like the Orbiter does.
Combining rotations: order matters#
Multiplying two quaternions combines them, and a * b means apply b first, then a, the same order as matrices. Multiplying a quaternion by a vector rotates the vector: q * Vector3.forward is the direction q faces. (The vector must be on the right; vector * q doesn't compile.)
Order matters because rotations in 3D don't commute. Take Y90 = 90° around Y and X90 = 90° around X. (Y90 * X90) * Vector3.forward is down: X90 tips forward to down, then Y90 spins around the vertical, which leaves "down" as it is. (X90 * Y90) * Vector3.forward is right: Y90 turns forward to right, then X90 spins around the X axis, which leaves "right" as it is. The two combined rotations are 120° apart.
That gives a rule you'll use constantly:
rotation = rotation * deltaappliesdeltaaround the object's own axes (likeRotatewithSpace.Self).rotation = delta * rotationappliesdeltaaround the world axes (likeSpace.World).
A first-person camera uses both: yaw around the world up (so looking left and right never tilts the horizon), pitch around its own right (so looking up and down follows the view). T07 · Input System, Movement & Cameras (Cinemachine) builds one.
Rotating around a point#
Rotations turn things around the origin. To swing a door around its hinge, or a moon around a planet, rotate the offset from the pivot and add the pivot back: pivot + turn * (position - pivot). Then turn the object itself by the same rotation so it keeps facing the right way. RotateAround(pivot, axis, angle) does both for a single step. Exercise 7 builds a door on a hinge this way.
The guided build's telescope shows the techniques together. It remembers its starting rotation, expresses the target's position in that rotation's space with Quaternion.Inverse(rest) * offset (so straight ahead is always +Z, however the telescope was placed), takes yaw and pitch from that with Atan2, clamps them, rebuilds the rotation with rest * Quaternion.Euler(pitch, yaw, 0), and turns towards it at a limited speed with RotateTowards.
Interpolation and smoothing#
Interpolation means "somewhere between A and B". Games use it for everything that should change gradually: positions, rotations, colours, volumes, camera zoom. Unity has several methods that look similar and behave very differently, and choosing the right one is most of the skill.
Lerp and InverseLerp#
Mathf.Lerp(a, b, t) is linear interpolation: a + (b − a) × t. At t = 0 you get a, at t = 1 you get b, at t = 0.5 halfway. Unity's version clamps t to 0–1, so Lerp(0, 10, 1.5f) is 10; LerpUnclamped gives 15. Vector3.Lerp and Color.Lerp do the same for each component.
Mathf.InverseLerp(a, b, value) runs it backwards: where is value between a and b, as a fraction from 0 to 1? InverseLerp(20, 5, 12.5f) is 0.5. The range can run downwards, which is handy for "closer is more". Put them together and you can map any range onto any other, a function usually called Remap. Unity doesn't have one, so MathUtil.Remap above adds it. Engine pitch from speed and danger from distance are typical uses:
using System;
static class Program
{
static float Clamp01(float t) => t < 0f ? 0f : t > 1f ? 1f : t;
static float Lerp(float a, float b, float t) => a + (b - a) * Clamp01(t);
static float InverseLerp(float a, float b, float value) => a == b ? 0f : Clamp01((value - a) / (b - a));
// Where value sits between inMin and inMax, mapped to the same place between outMin and outMax.
static float Remap(float value, float inMin, float inMax, float outMin, float outMax)
=> Lerp(outMin, outMax, InverseLerp(inMin, inMax, value));
static void Main()
{
// Engine pitch rises from 0.8 to 2.0 as speed goes from 0 to 30 m/s.
foreach (float speed in new[] { 0f, 15f, 30f, 45f })
Console.WriteLine($"Speed {speed,2} m/s -> pitch {Remap(speed, 0f, 30f, 0.8f, 2f):F2}");
// Danger grows as an enemy closes from 20 m to 5 m (note the reversed input range).
foreach (float distance in new[] { 25f, 20f, 12.5f, 5f, 2f })
Console.WriteLine($"Distance {distance,4} m -> danger {Remap(distance, 20f, 5f, 0f, 1f):F2}");
}
}Speed 0 m/s -> pitch 0.80
Speed 15 m/s -> pitch 1.40
Speed 30 m/s -> pitch 2.00
Speed 45 m/s -> pitch 2.00
Distance 25 m -> danger 0.00
Distance 20 m -> danger 0.00
Distance 12.5 m -> danger 0.50
Distance 5 m -> danger 1.00
Distance 2 m -> danger 1.00The right way to animate with Lerp is with a fixed start, a fixed end, and a t that grows with time: t += Time.deltaTime / duration, then Lerp(start, end, t). That takes exactly duration seconds at any frame rate.
The Lerp-with-deltaTime trap#
Almost every Unity tutorial has a line like this, often for a follow camera:
transform.position = Vector3.Lerp(transform.position, target.position, speed * Time.deltaTime);Each frame it moves a fraction of the remaining distance, so it eases in nicely. But multiplying by Time.deltaTime doesn't make it frame-rate independent, because the fractions compound. Covering 10% of the distance in each of two frames isn't the same as covering 20% in one: after two frames, 81% is left, not 80%. The faster the frame rate, the less it covers per second. This program follows a target 10 m away for half a second at three frame rates:
using System;
// Follows a target that sits 10 m away for half a second, at three frame rates,
// with the common "Lerp by speed * deltaTime" and with exponential damping.
static class Program
{
const float Sharpness = 5f;
static float Lerp(float a, float b, float t) => a + (b - a) * t;
static float Run(int framesPerSecond, bool damp)
{
float dt = 1f / framesPerSecond;
float position = 0f, target = 10f;
int frames = framesPerSecond / 2; // half a second
for (int i = 0; i < frames; i++)
{
float t = damp ? 1f - MathF.Exp(-Sharpness * dt) : Sharpness * dt;
position = Lerp(position, target, t);
}
return position;
}
static void Main()
{
Console.WriteLine("fps Lerp(speed*dt) Damp");
foreach (int fps in new[] { 30, 60, 144 })
Console.WriteLine($"{fps,3} {Run(fps, false),10:F3} m {Run(fps, true),6:F3} m");
Console.WriteLine($"Exact answer: {10f * (1f - MathF.Exp(-Sharpness * 0.5f)):F3} m");
}
}fps Lerp(speed*dt) Damp
30 9.351 m 9.179 m
60 9.265 m 9.179 m
144 9.215 m 9.179 m
Exact answer: 9.179 mThe Lerp version lands in a different place at each frame rate. The differences look small here, but they're the difference between a camera that feels the same on every machine and one that feels sluggish on a fast PC and floaty on a phone, and they grow with larger speed values. At very low frame rates, speed * deltaTime can exceed 1 and the object overshoots and snaps.
Damp: smoothing that's the same at any frame rate#
The fix is to compute the fraction properly. If the remaining distance should shrink by the same proportion every second, then over a frame of length dt it shrinks by e−sharpness × dt, and the fraction to move is 1 - Mathf.Exp(-sharpness * dt). That's MathUtil.Damp, and the right-hand column above shows it reaching 9.179 m at every frame rate. Use it wherever you'd have used Lerp with deltaTime. sharpness works much like the old speed: 1 is lazy, 5 is brisk, 20 is nearly instant.
Designers often find a half-life easier to think about: "close half the gap every 0.2 seconds". That's the same formula with base 2: 1 - Mathf.Pow(2, -dt / halfLife). Exercise 5 builds it.
Damping never quite arrives: it closes a fraction of the gap forever. That's usually fine for cameras and UI, but if you need to know when it's done, check whether the distance is below a small threshold and snap the rest of the way.
MoveTowards: a constant speed#
MoveTowards(current, target, maxStep) moves at most maxStep towards the target and never overshoots: MoveTowards(0, 10, 3) is 3, and once it's within 3 it lands exactly on 10. Pass speed * Time.deltaTime and you get constant-speed motion that is genuinely frame-rate independent, because the steps add up linearly. It's the right choice for lifts, conveyor belts, homing projectiles and anything mechanical. Mathf.MoveTowards does the same for a float (a health bar draining), Quaternion.RotateTowards for a rotation at a set number of degrees per step, and Vector3.RotateTowards for a direction (careful: it takes radians).
SmoothDamp: like a spring#
Vector3.SmoothDamp, called as SmoothDamp(cur, target, ref vel, time), behaves like a well-damped spring: it accelerates away, slows as it arrives and doesn't overshoot (for a target that isn't moving). smoothTime is roughly how long it takes to arrive. Because it tracks a velocity, it starts gently, unlike Damp, which moves fastest in its first frame. That makes it the classic choice for follow cameras. The velocity variable is its memory between frames, so it must be a field, not a local variable, and each smoothed value needs its own. It uses Time.deltaTime by default, and it has an optional maximum speed. Mathf.SmoothDampAngle is the version for angles.
The quickest way to feel the difference is to watch all three chase the same target. Put this on three cubes, give each a different mode, and drag the target around in the Scene view during Play.
using UnityEngine;
namespace Handbook.T06
{
// Three ways to chase a target. Put one on each of three cubes and compare.
public class SmoothFollower : MonoBehaviour
{
enum Mode { MoveTowards, Damp, SmoothDamp }
[SerializeField] Transform target;
[SerializeField] Mode mode = Mode.Damp;
[SerializeField] float speed = 4f; // MoveTowards: metres per second
[SerializeField] float sharpness = 5f; // Damp: higher is snappier
[SerializeField] float smoothTime = 0.3f; // SmoothDamp: roughly the time to arrive
Vector3 velocity; // SmoothDamp's memory between frames
void Update()
{
if (target == null) return;
Vector3 goal = target.position;
float dt = Time.deltaTime;
transform.position = mode switch
{
Mode.MoveTowards => Vector3.MoveTowards(transform.position, goal, speed * dt),
Mode.Damp => MathUtil.Damp(transform.position, goal, sharpness, dt),
_ => Vector3.SmoothDamp(transform.position, goal, ref velocity, smoothTime),
};
}
}
}Rotations: Slerp and RotateTowards#
Quaternion.Slerp(a, b, t) is spherical linear interpolation: it turns from a to b at a constant angular speed, always the short way round. Quaternion.Lerp is faster but its speed isn't constant; for rotations close together you can't see the difference. The same frame-rate rules apply: Slerp with a fixed start, end and growing t, damped Slerp (MathUtil.Damp has a quaternion version), or RotateTowards for a fixed number of degrees per second. Vector3.Slerp interpolates directions, turning one into the other along the arc instead of cutting across.
Angles wrap around#
Angles have a seam where 360 meets 0, and ordinary float maths doesn't know about it. Mathf.Lerp(350, 10, 0.5f) is 180: halfway the long way round, pointing backwards. Mathf.LerpAngle(350, 10, 0.5f) is 360, which is the same direction as 0, the true midpoint. Mathf.DeltaAngle(10, 350) is −20, the shortest signed turn, and DeltaAngle(0, 190) is −170. MoveTowardsAngle and SmoothDampAngle also go the short way. To wrap an angle into range yourself, Mathf.Repeat(−30, 360) is 330 and Repeat(370, 360) is 10. Mathf.PingPong(t, length) bounces between 0 and length instead: PingPong(5, 2) is 1.
Easing curves#
Linear motion looks mechanical: it starts and stops instantly. Easing reshapes t before you Lerp, so motion accelerates and decelerates. Mathf.SmoothStep(a, b, t) is the built-in ease-in-out: it uses 3t² − 2t³ instead of t, so it starts and ends gently. SmoothStep(0, 100, 0.25f) is 15.625, where Lerp gives 25. Simple hand-written eases are one line each: t * t eases in, 1 - (1 - t) * (1 - t) eases out.
For any shape you like, use an AnimationCurve field. The Inspector shows a curve editor with presets; in code, curve.Evaluate(t) returns the curve's height at t. A designer can then tune the feel of a door, a jump or a screen fade without touching code. AnimationCurve.EaseInOut(0, 0, 1, 1) makes a sensible default.
[SerializeField] AnimationCurve ease = AnimationCurve.EaseInOut(0, 0, 1, 1);
[SerializeField] float duration = 1.5f;
float elapsed;
void Update()
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
group.alpha = ease.Evaluate(t); // a CanvasGroup, for example
}| You want | Use |
|---|---|
| Go from A to B in exactly N seconds | Lerp or Slerp with t = elapsed / N, shaped by SmoothStep or a curve |
| Move at a constant speed, arrive exactly | MoveTowards, RotateTowards |
| Chase a moving target smoothly | MathUtil.Damp (snappy start) or SmoothDamp (gentle start) |
| Map one range onto another | InverseLerp then Lerp (Remap) |
| Interpolate angles in degrees | LerpAngle, MoveTowardsAngle, DeltaAngle |
| Never | Lerp(current, target, speed * deltaTime) |
Randomness and noise#
UnityEngine.Random is a seeded pseudo-random generator shared by the whole game:
Random.Range(0, 10)with ints returns 0 to 9: the maximum is excluded, which suits array indices (items[Random.Range(0, items.Length)]).Random.Range(0f, 10f)with floats can return anything from 0 to 10 inclusive. WritingRandom.Range(0, 1)when you meant a float always gives 0.Random.value: 0 to 1 inclusive.Random.value < 0.25fis a 25% chance.Random.insideUnitCircle(aVector2),insideUnitSphereandonUnitSpherepick points evenly inside a circle, inside a ball or on a sphere's surface. Scattering rocks around a point:centre + Random.insideUnitSphere * r, flattened if they should sit on the ground.Random.rotationgives a random orientation andRandom.ColorHSV()a random colour.Random.InitState(seed)restarts the sequence from a seed, so the same seed gives the same numbers: replayable levels, daily challenges, bug reports you can reproduce.Random.statelets you save and restore the position in the sequence.
Because UnityEngine.Random is shared, any code that draws a number changes what the next caller gets. A level generator that must produce the same island from the same seed should own its own generator: a System.Random created with the seed, or the Unity.Mathematics.Random struct, whose seed must not be zero.
Perlin noise: smooth randomness#
Random numbers jump about. Natural things such as waves, wind, terrain and a camera's handheld shake change smoothly. Perlin noise is randomness with smoothness built in: Mathf.PerlinNoise(x, y) returns a value of roughly 0 to 1 that changes gradually as x and y change, and always gives the same value for the same inputs. (It can stray slightly outside 0–1, so clamp it if you need a strict range.) To animate with it, move through the noise over time, PerlinNoise(seed, Time.time * speed), and use a different seed for each object or channel so they don't move together. Mathf.PerlinNoise1D(x) is the one-dimensional version. Scaling the input changes the feel: small steps through the noise give slow swells, big steps give jitter.
This buoy uses three channels of noise for its height, pitch and roll. Its movement never repeats and never jerks.
using UnityEngine;
namespace Handbook.T06
{
// Rocks an object on gentle, never-repeating waves using Perlin noise.
public class BuoyBob : MonoBehaviour
{
[SerializeField] float height = 0.25f; // metres up and down
[SerializeField] float tilt = 8f; // degrees of pitch and roll
[SerializeField] float speed = 0.4f; // how fast we travel through the noise
Vector3 restPosition;
Quaternion restRotation;
float seed;
void Start()
{
restPosition = transform.localPosition;
restRotation = transform.localRotation;
seed = Random.Range(0f, 1000f); // each buoy reads a different stretch of noise
}
void Update()
{
float t = Time.time * speed;
// PerlinNoise returns roughly 0 to 1. Shift it to roughly -1 to 1.
float rise = Mathf.PerlinNoise(seed, t) * 2f - 1f;
float pitch = Mathf.PerlinNoise(seed + 10f, t) * 2f - 1f;
float roll = Mathf.PerlinNoise(seed + 20f, t) * 2f - 1f;
transform.localPosition = restPosition + Vector3.up * (rise * height);
transform.localRotation = restRotation * Quaternion.Euler(pitch * tilt, 0f, roll * tilt);
}
}
}Several layers of noise at doubling frequencies and halving strength, added together, give the rough-but-natural look of coastlines and mountains. This is called fractal noise or fBm, and T26 · Procedural Generation: Noise, Terrain, Meshes, Voxels, Dungeons & Cities uses it to generate terrain.
Floating-point precision#
Unity's vectors use float, which holds about seven significant digits. Two consequences catch everybody eventually.
using System;
static class Program
{
static void Main()
{
float sum = 0f;
for (int i = 0; i < 10; i++) sum += 0.1f;
Console.WriteLine($"Ten lots of 0.1: {sum:R}");
Console.WriteLine($"Equal to 1? {sum == 1f}");
Console.WriteLine($"Close to 1? {MathF.Abs(sum - 1f) < 0.0001f}");
foreach (float x in new[] { 10f, 1_000f, 100_000f, 1_000_000f })
{
float moved = x + 0.001f; // try to move 1 mm
Console.WriteLine($"At {x,7:F0} m, adding 1 mm gives {moved - x:F6} m");
}
}
}Ten lots of 0.1: 1.0000001
Equal to 1? False
Close to 1? True
At 10 m, adding 1 mm gives 0.001000 m
At 1000 m, adding 1 mm gives 0.000977 m
At 100000 m, adding 1 mm gives 0.000000 m
At 1000000 m, adding 1 mm gives 0.000000 m- Most decimals can't be stored exactly. 0.1 is a repeating fraction in binary, so ten lots of it add up to 1.0000001. Never compare floats with
==unless one side was assigned the exact same value. UseMathf.Approximately(a, b), which allows a tiny relative difference (Approximately(1.0000001f, 1f)is true,(1.00001f, 1f)is false), or a tolerance that suits the job: "within 1 cm" isMathf.Abs(a - b) < 0.01f. Don't useMathf.Epsilonas a tolerance; it's the smallest positive float, far too small to absorb any rounding. - Precision shrinks with distance from the origin. Seven digits shared across the whole number means that at 1,000 m a float can only step in about 0.06 mm increments, so a 1 mm move becomes 0.977 mm. At 100 km the step is almost 8 mm, and a 1 mm move vanishes completely. Far from the origin, physics jitters, animations shake and cameras stutter. Unity's Transform Inspector warns about very large positions for this reason.
The practical rules: keep the playable area within a few kilometres of the origin; for larger worlds, use a floating origin, which periodically shifts the whole world back so the player stays near (0, 0, 0); and don't let ever-growing values feed into maths. Time.time is a float too, so after a game has run for many hours, Mathf.Sin(Time.time * speed) gets steppy. Keep phases wrapped with Mathf.Repeat, as the Orbiter does with its angle, or use Time.timeAsDouble for long-running clocks.
Guided build: Lighthouse Island, the lamp room#
In T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable's night shift, ships sailed in past a flashing buoy. Now the lighthouse joins in. Its lamp sweeps the sea and reports every ship its beam passes over, gulls circle the tower, a telescope on the jetty tracks the nearest ship within the limits of its mount, and the buoy rocks on noise-driven waves. Each piece uses one idea from this chapter: the dot product cone, circles with sine and cosine, Atan2 with clamped quaternions, and Perlin noise.
If you didn't build the earlier scenes, any scene with a plane for the sea, a tall object for the lighthouse and T05's Ship prefab (or any moving object) will do.
Step 1 · Scripts and the Ship tag#
- Open
_Project/Scenes/IslandNight, the scene from T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable. - Copy
MathUtil,LighthouseBeam,Orbiter,TelescopeandBuoyBobinto_Project/Scripts.MathUtilis a static class, not a component; the others need it. - The beam and the telescope find ships by tag. Open Edit → Project Settings → Tags and Layers, expand Tags, click + and add
Ship. - Double-click the Ship prefab in
_Project/Prefabsto open it. Select its root and set Tag (top of the Inspector) toShip. Leave Prefab Mode. Every ship the spawner creates is now tagged.
The tag must exist in the Tag Manager, not just be typed in the script: GameObject.FindGameObjectsWithTag throws a UnityException for a tag that isn't defined. Searching by tag is fine twice a second or ten times a second, as the scripts here do, but not for hundreds of objects every frame; T14 · Events, Messaging & Decoupled Systems shows how objects can register themselves in a list instead.
Step 2 · The sweeping beam#
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Handbook.T06
{
// Turns the lamp at a steady rate and reports ships when they enter the beam.
public class LighthouseBeam : MonoBehaviour
{
[SerializeField] float degreesPerSecond = 30f;
[SerializeField, Range(1f, 45f)] float halfAngle = 8f;
[SerializeField, Min(1f)] float range = 60f;
[SerializeField] string shipTag = "Ship";
[SerializeField, Min(0.02f)] float scanInterval = 0.1f;
[SerializeField] bool logSpots = true;
public event Action<Transform> Spotted;
readonly HashSet<Transform> lit = new HashSet<Transform>(); // ships in the beam now
float nextScan;
void Update()
{
// Spin around the world's up axis, whatever the lamp's own tilt.
transform.Rotate(0f, degreesPerSecond * Time.deltaTime, 0f, Space.World);
if (Time.time < nextScan) return;
nextScan = Time.time + scanInterval;
Scan();
}
// The beam's direction flattened onto the ground plane.
Vector3 FlatForward() => Vector3.ProjectOnPlane(transform.forward, Vector3.up).normalized;
void Scan()
{
Vector3 origin = transform.position;
Vector3 forward = FlatForward();
foreach (GameObject ship in GameObject.FindGameObjectsWithTag(shipTag))
{
// Compare at the lamp's height: the lamp is high up, the ships are at sea level.
Vector3 point = ship.transform.position;
point.y = origin.y;
bool inside = MathUtil.InCone(origin, forward, point, range, halfAngle);
if (inside && lit.Add(ship.transform)) // Add is false if it was already lit
{
if (logSpots) Debug.Log($"Beam spotted {ship.name}", ship);
Spotted?.Invoke(ship.transform);
}
else if (!inside)
{
lit.Remove(ship.transform);
}
}
lit.RemoveWhere(t => t == null); // ships destroyed while lit
}
void OnDrawGizmosSelected()
{
Vector3 origin = transform.position;
Vector3 forward = FlatForward();
if (forward == Vector3.zero) return; // pointing straight up or down
Vector3 left = Quaternion.AngleAxis(-halfAngle, Vector3.up) * forward;
Vector3 right = Quaternion.AngleAxis(halfAngle, Vector3.up) * forward;
Gizmos.color = Color.yellow;
Gizmos.DrawLine(origin, origin + left * range);
Gizmos.DrawLine(origin, origin + right * range);
Gizmos.DrawLine(origin + left * range, origin + right * range);
}
}
}- Open the Lighthouse prefab. Select
LampPivot, the object that holds the lamp head and the two spot lights. It has T02'sSpincomponent; remove it (the ⋮ menu → Remove Component), because the beam now does the turning. - Add
LighthouseBeamtoLampPivot. Leave the defaults: 30° a second, a half-angle of 8°, a range of 60 m. - Leave Prefab Mode, select the lighthouse's LampPivot in the scene, and look at the Scene view: the yellow lines are the edges of the detection cone, drawn by
OnDrawGizmosSelected. - Press Play. Each time the beam passes over a ship, the Console logs "Beam spotted Ship(Clone)". Click the message and the ship is highlighted, because the ship is passed as the context.
Two details in the code. The lamp sits 11 m up and ships are at sea level, so a ship far away is only slightly below the beam but one close by is well below it. The scan copies the lamp's height into the ship's position before testing, so it's a flat, 2D question: "is the ship within 8° of the beam's bearing?". For the same reason, the beam's forward is flattened with ProjectOnPlane; otherwise tilting the lamp would shrink the cone's footprint on the sea. And a ship is reported once each time it enters the beam, not every scan while it's lit: the HashSet remembers which ships are lit, and Add returns false for one already in it.
Step 3 · Gulls round the tower#
- Make a gull: an empty named
Gullwith two children, aBody(Sphere, scale (0.3, 0.25, 0.7)) andWings(Cube, scale (1.6, 0.05, 0.3)), both with the White material. Its beak should point along the blue Z axis, which the body's long axis does. - Put it at height 14 near the lighthouse, for example (15, 14, 7). Add
Orbiter, drag the Lighthouse into Centre, and set the radius to 7. - Duplicate it twice. Give the copies heights of 12.5 and 16, radii of 9 and 5.5, start angles of 120 and 240, and give one a negative angular speed (−30) so it circles the other way.
- Make it a prefab if you like: drag one gull into
_Project/Prefabsand then replace the others with instances.
Press Play. The gulls circle at different heights, bob gently and bank their noses up and down as they rise and fall, because LookRotation faces the full 3D direction of travel, bob included.
Step 4 · The telescope#
using UnityEngine;
namespace Handbook.T06
{
// Swivels toward the nearest ship at a limited speed, within a limited arc.
public class Telescope : MonoBehaviour
{
[SerializeField] string shipTag = "Ship";
[SerializeField, Min(1f)] float turnSpeed = 45f; // degrees per second
[SerializeField, Range(0f, 180f)] float yawLimit = 70f; // either side of rest
[SerializeField, Range(0f, 89f)] float pitchLimit = 20f; // up or down
Quaternion rest; // rotation at start: the middle of the arc
Transform target;
float nextSearch;
void Start() => rest = transform.rotation;
void Update()
{
if (Time.time >= nextSearch)
{
nextSearch = Time.time + 0.5f;
target = FindNearest();
}
Quaternion goal = target != null ? AimAt(target.position) : rest;
float step = turnSpeed * Time.deltaTime;
transform.rotation = Quaternion.RotateTowards(transform.rotation, goal, step);
}
Quaternion AimAt(Vector3 point)
{
// Express the target in the rest rotation's space, where straight ahead is +Z.
Vector3 local = Quaternion.Inverse(rest) * (point - transform.position);
float yaw = Mathf.Atan2(local.x, local.z) * Mathf.Rad2Deg;
float flat = new Vector2(local.x, local.z).magnitude;
float pitch = -Mathf.Atan2(local.y, flat) * Mathf.Rad2Deg; // positive X tips the nose down
yaw = Mathf.Clamp(yaw, -yawLimit, yawLimit);
pitch = Mathf.Clamp(pitch, -pitchLimit, pitchLimit);
return rest * Quaternion.Euler(pitch, yaw, 0f);
}
Transform FindNearest()
{
Transform nearest = null;
float bestSqr = float.PositiveInfinity;
foreach (GameObject ship in GameObject.FindGameObjectsWithTag(shipTag))
{
float sqr = (ship.transform.position - transform.position).sqrMagnitude;
if (sqr < bestSqr)
{
bestSqr = sqr;
nearest = ship.transform;
}
}
return nearest;
}
}
}- On the jetty, near its seaward end, create an empty named
Telescopestanding on the planks. Rotate it to Y 180 so its blue Z arrow points out to sea, towards where ships appear. - Give it a child
Stand: a Cylinder at local (0, 0.5, 0), scale (0.1, 0.5, 0.1), Rock material. - Add a child empty named
Headat local (0, 1.1, 0), and under it aTube: a Cylinder at local (0, 0, 0.25), rotation (90, 0, 0), scale (0.12, 0.35, 0.12). A cylinder's long axis is Y, so the 90° tilt lays it along Z: the tube points where the head faces. - Add the
Telescopecomponent toHead, not to the root, so only the head turns.
Press Play. When a ship appears, the telescope swings towards it at 45° a second, tips down slightly to look at sea level, and follows it in. When the ship docks and disappears, it swings back to rest. Move the telescope's root so a ship passes behind it and watch it stop at its 70° limit instead of spinning round. The limits work because the yaw and pitch are measured from the rest rotation: Quaternion.Inverse(rest) * offset puts the ship in a space where the telescope's resting direction is +Z.
Step 5 · The buoy rides the waves#
- Select the Buoy from T05 and add
BuoyBob. - Press Play. It rises, falls and tilts on slow waves while it flashes, and its green lamp moves with it because the lamp is a child.
- Duplicate the buoy two or three times and place the copies along the channel. Each one draws its own random seed, so they don't move in step.
Step 6 · Break it on purpose#
- Space.Self. Tilt LampPivot to X 20 and change
Space.WorldtoSpace.Selfin the beam'sRotatecall. The beam now spins around its own tilted axis, sweeping up into the sky and down into the sea. Change it back. - A cone without normalizing. In
MathUtil.InCone, replacedirectionwithtoPointin the dot product. Ships are now "spotted" only when the beam passes in their general direction, at almost any angle, because the dot product of a 40 m vector is huge. Undo it. - Degrees as radians. In
Orbiter, remove* Mathf.Deg2Rad. The gulls teleport around the circle, about 57 times too fast. Undo it. - The backward beam. The lamp has a second beam pointing the other way, but only the forward one detects ships. Make the scan check both: test the cone around
forwardand around-forward.
Step 7 · Save and commit#
Save the scene and the prefabs, check that the new scripts and their .meta files are listed in Git, and commit: git commit -m "Lamp room".
Check your work. You've finished when: the beam sweeps horizontally round the island and logs each ship once per pass; three gulls circle at different heights and speeds, facing the way they fly; the telescope follows the nearest ship within its limits and returns to rest when none is left; the buoys rock out of step with each other; and the Console shows no errors.
Going further#
Unity.Mathematics#
Unity has a second maths library, the Mathematics package (1.3.3 in this handbook's baseline), with types such as float3, float4x4 and quaternion, and functions in the math class. It's designed for the Burst compiler and the job system (T46 · DOTS: Entities (ECS), Jobs & Burst), and its syntax matches shader code (T36 · Writing Shaders by Hand: ShaderLab & HLSL in URP). Two differences matter: everything uses radians, including rotations, and functions are static (math.dot(a, b) rather than Vector3.Dot). float3 and Vector3 convert to each other automatically.
using Unity.Mathematics;
float3 a = new float3(1, 2, 0);
float3 b = new float3(3, -1, 2);
float d = math.dot(a, b); // 1
float3 up = math.normalize(math.cross(a, b));
quaternion turn = quaternion.RotateY(math.radians(90f));
float3 right = math.mul(turn, new float3(0, 0, 1)); // (1, 0, 0)For ordinary MonoBehaviour code, stay with Vector3 and Quaternion: they're what the rest of the API takes.
Curves and splines#
A Bézier curve is built from Lerps: with control points p0 to p3, lerp each neighbouring pair by t, lerp those results, and lerp once more. Exercise 8 does it, along with the hard part: moving along a curve at a constant speed, which t alone doesn't give you. For real paths, use the Splines package (2.9.2): a Spline Container component you edit with handles in the Scene view, a Spline Animate component that moves objects along it, and an API to evaluate positions and tangents. Rails, roads and camera paths are all splines.
Learning more#
Freya Holmér's lecture series (C011 · Math for Game Devs (Freya Holmer)) is the best visual treatment of these ideas, and the Catlike Coding basics tutorials (C029 · Catlike Coding: Basics, Procedural Meshes, Movement & Hex Map) apply them to graphs, meshes and movement. For rotations in depth, read about axis-angle and the quaternion "double cover" (q and −q are the same rotation). For shader maths, the same dot products, matrices and interpolations appear again in T34 · How GPUs Draw: The Pipeline, Coordinate Spaces & Shader Maths.
Common mistakes & debugging#
Vectors#
- The enemy runs away instead of towards you: the subtraction is backwards. From A to B is B − A.
- Diagonal movement is faster: the input wasn't normalized or clamped to length 1.
- NaN in a position, and the object vanishes: usually
Acosof a value just past ±1, or a division by a zero length. Clamp dot products, and check lengths before dividing. - "Look rotation viewing vector is zero" in the Console:
LookRotationwas given a zero direction, often the difference between two equal positions. Skip the rotation when the direction is tiny. - A 3D game moves on the wrong axis: a
Vector2input was converted to (x, y, 0). Build (x, 0, y) yourself. - Range checks are wrong: a
sqrMagnitudecompared with an unsquared distance.
Angles and rotations#
- Angles go haywire: degrees passed to
Mathf.SinorCos, or radians fromAtan2used as degrees. Convert withDeg2RadandRad2Deg. - A turret turns the long way round: angles were subtracted directly. Use
Mathf.DeltaAngle,LerpAngleor quaternions. - Rotation drifts or snaps: one Euler angle was read, changed and written back. Keep your own angles, or use
Rotate. - An object rotates around the wrong axes: the quaternion multiplication order is reversed, or the default
Space.Selfwas used whereSpace.Worldwas meant. - A camera flips or locks when looking straight up: gimbal lock from Euler angles, or
LookRotationwith a forward parallel to its up. Clamp the pitch below 90°. - An imported model faces backwards or lies on its back: the modelling tool's axes differ. Fix it in the import settings or the export, or put the model under an empty parent and rotate the child (T31 · Blender for Unity: Modelling, UVs, Texturing & Export).
Smoothing, randomness and floats#
- Smoothing feels different on another machine:
Lerp(current, target, speed * deltaTime). Use Damp, SmoothDamp or MoveTowards. - SmoothDamp does nothing or jumps: its velocity is a local variable, reset every frame, or one velocity is shared by two values. Make it a field per value.
- A random index sometimes throws:
Random.Range(0, list.Count + 1); the int version already excludes the maximum. - A "random" generator gives the same result every run, or a different one each run: the seed is fixed, or other code is drawing from the shared generator. Give the generator its own
System.Random. - An equality check never succeeds: floats compared with
==. Compare with a tolerance. - Things jitter far from the origin: float precision. Bring the play area closer to (0, 0, 0), or shift the world.
Exercises#
The Unity solutions are in the sandbox under Assets/Handbook/T06/Exercises, compiled against Unity 6.3. The console solutions run with .NET.
1 · Vectors by hand
With a = (1, 2, 0) and b = (3, −1, 2), work out on paper: a + b; b − a and its length; Dot(a, b); (0, 3, 4).normalized; and Dot(Vector3.forward, Vector3.back). Then check your answers with Debug.Log in a scratch script.
Solution
a + b = (4, 1, 2). b − a = (2, −3, 2), length √(4 + 9 + 4) = √17 ≈ 4.123. Dot(a, b) = 1×3 + 2×(−1) + 0×2 = 1. (0, 3, 4) has length 5, so normalized it's (0, 0.6, 0.8). Dot(forward, back) = −1: opposite unit vectors. When logging, note that Vector3.ToString() shows two decimals by default; use v.ToString("F3") for more.
2 · Remap
Write Remap(v, inMin, inMax, outMin, outMax) using only a lerp and an inverse lerp, as a console program with its own Lerp and InverseLerp (clamped). Use it to map a car's speed of 0–30 m/s to an engine pitch of 0.8–2.0, and an enemy's distance of 20 m down to 5 m to a danger level of 0–1. What happens past the ends of each range?
Solution
It's the Remap program in the Lerp section, with its output. Past the ends, the clamped inverse lerp holds the result at the nearest end: 45 m/s gives the same pitch as 30, and 2 m the same danger as 5. Reversing the input range (20 to 5) makes the value rise as the distance falls, with no extra code. For extrapolation, use unclamped versions.
3 · Behind you!
Write a component that logs a warning once when an intruder Transform comes within 5 m behind it, and a normal message once when it's no longer behind or close. Avoid square roots, and avoid logging every frame.
Solution
Range with sqrMagnitude against a squared range; "behind" with the sign of the dot product, which doesn't need normalizing. A bool remembers the last state, so the logs only happen on a change.
using UnityEngine;
namespace Handbook.T06
{
// Warns once when the intruder comes within range behind this object, and once when it leaves.
public class BehindAlarm : MonoBehaviour
{
[SerializeField] Transform intruder;
[SerializeField, Min(0.1f)] float range = 5f;
bool warned;
void Update()
{
if (intruder == null) return;
Vector3 toIntruder = intruder.position - transform.position;
bool close = toIntruder.sqrMagnitude <= range * range;
bool behind = Vector3.Dot(transform.forward, toIntruder) < 0f; // no need to normalize for a sign test
bool danger = close && behind;
if (danger == warned) return; // nothing changed since last frame
warned = danger;
if (danger) Debug.LogWarning($"{intruder.name} is behind {name}!", this);
else Debug.Log($"{name} is safe again.", this);
}
}
}4 · A clock
Build a clock face in the XY plane, facing the default camera, with hour, minute and second hands that each point straight up at rest with their pivot at the centre. Write a component that sets them from the computer's clock, with an option for a smooth or ticking second hand. Which way does a positive Z rotation turn, and what does that mean for a clockwise clock?
Solution
Each hand's angle is its fraction of a full turn × 360. The minute hand includes the seconds and the hour hand includes the minutes, so they move smoothly too. Seen from the front, a positive Z rotation is anticlockwise, so clockwise is negative. To make each hand pivot at the centre, put the visible bar as a child offset upwards, and rotate the parent.
using System;
using UnityEngine;
namespace Handbook.T06
{
// Turns three hands to the computer's clock. The face is in the XY plane, seen from -Z,
// and each hand points up (+Y) at rest with its pivot at the centre.
public class ClockHands : MonoBehaviour
{
[SerializeField] Transform hourHand;
[SerializeField] Transform minuteHand;
[SerializeField] Transform secondHand;
[SerializeField] bool smoothSeconds = true;
void Update()
{
DateTime now = DateTime.Now;
float seconds = now.Second + (smoothSeconds ? now.Millisecond / 1000f : 0f);
float minutes = now.Minute + seconds / 60f;
float hours = now.Hour % 12 + minutes / 60f;
Set(secondHand, seconds / 60f * 360f);
Set(minuteHand, minutes / 60f * 360f);
Set(hourHand, hours / 12f * 360f);
}
// Clockwise as seen from the front is a negative turn around Z.
static void Set(Transform hand, float degreesClockwise)
{
if (hand != null)
hand.localRotation = Quaternion.Euler(0f, 0f, -degreesClockwise);
}
}
}5 · Damping by half-life
Write a damping function that takes a half-life in seconds: after each half-life, the remaining distance to the target should halve. Show in a console program that after one half-life of 0.25 s, an object chasing a target 100 m away has covered 50 m at 20, 60 and 240 fps. How does it relate to MathUtil.Damp's sharpness?
Solution
The fraction to move each frame is 1 − 2−dt/halfLife. Over a whole half-life the remaining parts multiply to exactly ½ at any frame rate. Since 2−x = e−x ln 2, it's the same as Damp with sharpness = ln 2 / halfLife ≈ 0.693 / halfLife.
using System;
static class Program
{
static float Lerp(float a, float b, float t) => a + (b - a) * t;
// Each half-life, the remaining distance to the target halves.
static float DampHalfLife(float current, float target, float halfLife, float dt)
=> Lerp(current, target, 1f - MathF.Pow(2f, -dt / halfLife));
static void Main()
{
const float halfLife = 0.25f;
foreach (int fps in new[] { 20, 60, 240 })
{
float dt = 1f / fps, position = 0f;
int frames = (int)MathF.Round(halfLife * fps);
for (int i = 0; i < frames; i++)
position = DampHalfLife(position, 100f, halfLife, dt);
Console.WriteLine($"{fps,3} fps: after one half-life {position:F1} m of 100");
}
}
} 20 fps: after one half-life 50.0 m of 100
60 fps: after one half-life 50.0 m of 100
240 fps: after one half-life 50.0 m of 1006 · Closest point on a segment
Write ClosestPointOnSegment(a, b, p) for 2D points, with your own small vector struct, and test it with a segment from (0, 0) to (10, 0) and the points (4, 3), (−2, 1) and (13, −4). Print the closest point and the distance for each. What should happen if a and b are the same point?
Solution
Project p - a onto b - a with a dot product, divide by the squared length to get the fraction t along the segment, clamp t to 0–1, and return a + (b - a) * t. If a and b coincide, the squared length is zero, so return a instead of dividing by zero.
using System;
readonly struct Vec2
{
public readonly float X, Y;
public Vec2(float x, float y) { X = x; Y = y; }
public static Vec2 operator +(Vec2 a, Vec2 b) => new Vec2(a.X + b.X, a.Y + b.Y);
public static Vec2 operator -(Vec2 a, Vec2 b) => new Vec2(a.X - b.X, a.Y - b.Y);
public static Vec2 operator *(Vec2 a, float k) => new Vec2(a.X * k, a.Y * k);
public static float Dot(Vec2 a, Vec2 b) => a.X * b.X + a.Y * b.Y;
public float Magnitude => MathF.Sqrt(X * X + Y * Y);
public override string ToString() => $"({X:F1}, {Y:F1})";
}
static class Program
{
static Vec2 ClosestPointOnSegment(Vec2 a, Vec2 b, Vec2 p)
{
Vec2 ab = b - a;
float lengthSqr = Vec2.Dot(ab, ab);
if (lengthSqr < 1e-8f) return a; // a and b are the same point
float t = Vec2.Dot(p - a, ab) / lengthSqr; // how far along ab, as a fraction
t = MathF.Max(0f, MathF.Min(1f, t)); // stay between the ends
return a + ab * t;
}
static void Main()
{
var a = new Vec2(0, 0);
var b = new Vec2(10, 0);
foreach (var p in new[] { new Vec2(4, 3), new Vec2(-2, 1), new Vec2(13, -4) })
{
Vec2 c = ClosestPointOnSegment(a, b, p);
Console.WriteLine($"Point {p}: closest {c}, distance {(p - c).Magnitude:F2}");
}
}
}Point (4.0, 3.0): closest (4.0, 0.0), distance 3.00
Point (-2.0, 1.0): closest (0.0, 0.0), distance 2.24
Point (13.0, -4.0): closest (10.0, 0.0), distance 5.007 · A door on a hinge
Make a door (a Cube scaled (1, 2, 0.1)) swing open by 100° around a hinge at one edge, over 0.8 s, with an ease-in-out, and close again when toggled. Don't use a parent object as the pivot: rotate around a hinge Transform placed at the door's edge. Add a context-menu item to toggle it in Play mode.
Solution
A progress value moves between 0 and 1 with MoveTowards at 1/duration per second, so reversing halfway works smoothly. SmoothStep turns progress into an eased angle. The position is the hinge plus the rotated offset from the hinge, and the rotation is the same turn applied to the closed rotation. Right-click the component header in Play mode and choose Toggle.
using UnityEngine;
namespace Handbook.T06
{
// Swings this object around a hinge point with an eased motion.
// The hinge can be any Transform, for example an empty object at the door's edge.
public class Swing : MonoBehaviour
{
[SerializeField] Transform hinge;
[SerializeField] float openAngle = 100f; // degrees around the hinge's up axis
[SerializeField, Min(0.05f)] float duration = 0.8f;
Vector3 closedPosition;
Quaternion closedRotation;
float progress; // 0 = closed, 1 = open
bool opening;
void Start()
{
closedPosition = transform.position;
closedRotation = transform.rotation;
}
[ContextMenu("Toggle")]
public void Toggle() => opening = !opening;
void Update()
{
if (hinge == null) return;
float goal = opening ? 1f : 0f;
progress = Mathf.MoveTowards(progress, goal, Time.deltaTime / duration);
// Ease in and out, then turn the progress into an angle.
float angle = Mathf.SmoothStep(0f, openAngle, progress);
Quaternion turn = Quaternion.AngleAxis(angle, hinge.up);
// Rotating around a pivot: rotate the offset from the pivot, then add the pivot back.
transform.position = hinge.position + turn * (closedPosition - hinge.position);
transform.rotation = turn * closedRotation;
}
}
}8 · Constant speed along a Bézier curve
Write a component that moves an object along a cubic Bézier curve defined by four Transforms (start, two handles, end), facing along the curve, at a constant speed in metres per second, looping forever. Draw the curve with gizmos. Moving t at a constant rate doesn't give a constant speed. Why not, and how do you fix it?
Solution
The curve's points are spread unevenly along t: where the handles pull the curve out, equal steps of t cover more distance. The fix is an arc-length table. At start-up, walk the curve in small steps of t and record the distance travelled so far at each step. Each frame, advance a distance by speed * deltaTime, look up the two table entries it falls between, and use InverseLerp to find the t in between. Facing comes from the difference between the point and one slightly ahead.
using UnityEngine;
namespace Handbook.T06
{
// Moves along a cubic Bézier curve at a constant speed and draws the curve in the Scene view.
public class BezierPath : MonoBehaviour
{
[SerializeField] Transform p0, p1, p2, p3; // start, two handles, end
[SerializeField, Min(0.1f)] float speed = 5f; // metres per second
[SerializeField, Range(8, 256)] int samples = 64;
float[] lengths; // lengths[i] = distance along the curve at t = i / samples
float travelled;
static Vector3 Point(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
{
// Three rounds of Lerp: the de Casteljau construction.
Vector3 ab = Vector3.Lerp(a, b, t), bc = Vector3.Lerp(b, c, t), cd = Vector3.Lerp(c, d, t);
Vector3 abc = Vector3.Lerp(ab, bc, t), bcd = Vector3.Lerp(bc, cd, t);
return Vector3.Lerp(abc, bcd, t);
}
Vector3 Point(float t) => Point(p0.position, p1.position, p2.position, p3.position, t);
bool Ready => p0 != null && p1 != null && p2 != null && p3 != null;
void Start()
{
if (!Ready) { enabled = false; return; }
// Walk the curve in small steps and record the distance so far.
lengths = new float[samples + 1];
Vector3 previous = Point(0f);
for (int i = 1; i <= samples; i++)
{
Vector3 p = Point(i / (float)samples);
lengths[i] = lengths[i - 1] + Vector3.Distance(previous, p);
previous = p;
}
}
// Turns a distance along the curve into the t that reaches it.
float TAtDistance(float distance)
{
int i = 1;
while (i < samples && lengths[i] < distance) i++;
float segment = Mathf.InverseLerp(lengths[i - 1], lengths[i], distance);
return (i - 1 + segment) / samples;
}
void Update()
{
float total = lengths[samples];
travelled = Mathf.Repeat(travelled + speed * Time.deltaTime, total);
float t = TAtDistance(travelled);
Vector3 position = Point(t);
Vector3 ahead = Point(Mathf.Min(t + 0.01f, 1f)) - position;
transform.position = position;
if (ahead.sqrMagnitude > 1e-8f)
transform.rotation = Quaternion.LookRotation(ahead);
}
void OnDrawGizmos()
{
if (!Ready) return;
Gizmos.color = Color.green;
Vector3 previous = Point(0f);
for (int i = 1; i <= 32; i++)
{
Vector3 p = Point(i / 32f);
Gizmos.DrawLine(previous, p);
previous = p;
}
Gizmos.color = Color.gray;
Gizmos.DrawLine(p0.position, p1.position);
Gizmos.DrawLine(p2.position, p3.position);
}
}
}Check yourself#
You have the positions me and enemy. Which expression is the direction from you to the enemy, with length 1?
(me - enemy).normalizedenemy.normalized - me.normalized(enemy - me).normalizedenemy - me
Destination minus start gives the arrow from you to the enemy; normalizing makes its length 1. Normalizing positions first gives directions from the world origin, which mean nothing here.
forward and dir are unit vectors and Vector3.Dot(forward, dir) is −0.5. Where is dir?
- 60° to the right
- 120° away from forward, so behind
- Straight behind
- You can't tell without normalizing
For unit vectors the dot is the cosine of the angle, and cos 120° = −0.5. The dot doesn't say left or right; the cross product does.
With Y up, Cross(transform.forward, toTarget).y is positive. Where is the target?
- Above you
- Behind you
- On your left
- On your right
In Unity's left-handed system, the cross product of forward with a direction to the right points up. Its y is the sign to check.
What does Mathf.Sin(90f) return?
- About 0.89, the sine of 90 radians
- 1
- 0
- An error, because 90 is out of range
Mathf.Sin takes radians. Mathf.Sin(90f * Mathf.Deg2Rad) is 1.
Which gives the Y rotation, in degrees, that makes an object face the ground-plane direction dir?
Atan2(dir.z, dir.x) * Rad2DegAtan2(dir.x, dir.z) * Rad2DegMathf.Atan(dir.x / dir.z)Vector3.Angle(Vector3.forward, dir)
Unity measures Y rotation from +Z towards +X, so x goes in the first (y) slot of Atan2. Plain Atan loses the quarter of the circle and fails when z is 0, and Angle has no sign.
An object rotated 90° about Y is at (4, 0, 2). What is TransformDirection(Vector3.forward)?
- (4, 0, 3)
- (0, 0, 1)
- (1, 0, 0)
- (5, 0, 2)
Directions are rotated but not moved, and a 90° turn about Y takes forward to right. TransformPoint(Vector3.forward) would also add the position, giving (5, 0, 2).
transform.rotation *= delta; turns the object by delta around which axes?
- Its own local axes
- The world axes
- Its parent's axes
- It depends on the frame rate
a * b applies b first, so delta acts in the object's own frame before its existing rotation. delta * rotation turns around world axes.
Why does Lerp(pos, target, speed * deltaTime) behave differently at different frame rates?
Lerpclamps t to 1Time.deltaTimeis inaccurate- It should use
Time.fixedDeltaTime - Each frame moves a fraction of what's left, and fractions compound, so more, smaller steps cover less
The remaining distance is multiplied by (1 − speed·dt) each frame, which doesn't scale with dt. The exact fraction is 1 − e−speed·dt.
What does Random.Range(1, 6) return?
- A float from 1 to 6
- An int from 1 to 5
- An int from 1 to 6
- An int from 0 to 5
The int overload excludes the maximum. For a die, use Random.Range(1, 7).
An open-world game shows camera jitter and shaky physics far from the start point. What's the most likely cause?
- Too many Update methods
- Gimbal lock
- Float precision getting coarser far from the origin
Mathf.Approximatelybeing too strict
A float has about seven significant digits, so at large coordinates the smallest step becomes visible. A floating origin keeps the action near (0, 0, 0).
Cheat sheet#
Conventions
- Left-handed, Y up, Z forward, X right
- 1 unit = 1 m
- Inspector and Euler: degrees
Mathf.Sin,Cos,Atan2: radiansDeg2Rad,Rad2Deg
Vectors
- From A to B:
B - A - Range check:
sqrMagnitude < r * r - Direction:
v.normalized(zero if tiny) - Cap length:
ClampMagnitude(v, 1) - Flatten:
ProjectOnPlane(v, up) - Bounce:
Reflect(dir, normal)
Dot and cross
- Dot > 0: in front
- Cone:
Dot(fwd, dir) ≥ cos(half) Cross(fwd, to).y > 0: on the rightSignedAngle(fwd, to, up): + is right- Normal:
Cross(b - a, c - a)
Trig
- Circle:
(cos a, 0, sin a) * r - Wave:
Sin(t * f * 2π) * h - Yaw from dir:
Atan2(d.x, d.z) - 2D angle:
Atan2(d.y, d.x) - Wrap:
Mathf.Repeat(a, 360)
Spaces
TransformPoint: local → world pointInverseTransformPoint: world → localTransformDirection: rotation onlyTransformVector: rotation + scale- Points w = 1, directions w = 0
Rotations
a * b: b first, then arot * delta: local axesdelta * rot: world axesq * vrotates a vector- Euler order: Z, X, Y
- Pivot:
p + q * (pos - p)
Smoothing
- Timed:
Lerp(a, b, elapsed / duration) - Constant speed:
MoveTowards - Chase:
Lerp(c, t, 1 - Exp(-k * dt)) - Spring:
SmoothDamp(…, ref vel, time) - Angles:
LerpAngle,DeltaAngle - Ease:
SmoothStep,AnimationCurve
Random and floats
Range(int, int): max excludedRange(float, float): max included- Own seed:
new System.Random(seed) - Smooth:
PerlinNoise(seed, t) - Compare:
Mathf.Approximately - Stay near the origin
Courses that cover this#
Glossary#
- Vector
- A list of numbers such as (x, y, z), used for a point (a position) or a direction and length (an offset).
- Magnitude
- A vector's length, √(x² + y² + z²).
sqrMagnitudeis the length squared, which avoids the square root. - Unit vector
- A vector of length 1, representing a pure direction. Normalizing a vector divides it by its length to make one.
- Dot product
- a.x·b.x + a.y·b.y + a.z·b.z, equal to |a||b| cos θ. For unit vectors, the cosine of the angle between them: 1 same way, 0 perpendicular, −1 opposite.
- Cross product
- A vector perpendicular to two others, with length |a||b| sin θ. Used for normals and for telling left from right.
- Left-handed coordinates
- Axes arranged like a left hand's thumb (X), index finger (Y) and middle finger (Z). Unity's world uses them, with Y up.
- Radian
- The angle at which the arc equals the radius; a full turn is 2π radians, 360°.
- Atan2
- The function that turns a direction (x, y) into its angle from the +X axis, handling every quarter of the circle.
- Local space
- An object's own coordinate system: its pivot as origin, its axes and its scale.
- World space
- The scene's fixed coordinate system, measured from the world origin.
- Transformation matrix
- A 4×4 grid of numbers that scales, rotates and moves points in one multiplication. Every Transform has one.
- Euler angles
- A rotation as three angles around X, Y and Z, applied in a fixed order (Z, X, Y in Unity).
- Gimbal lock
- The loss of one rotation axis in Euler angles when the middle rotation reaches ±90°, so that two axes line up.
- Quaternion
- Four numbers that store a rotation without gimbal lock and interpolate smoothly. Unity stores every rotation as one.
- Lerp
- Linear interpolation: a + (b − a) × t. InverseLerp finds t from a value.
- Slerp
- Spherical linear interpolation: turning between two rotations or directions at a constant angular speed.
- Easing
- Reshaping the progress value t so motion speeds up or slows down instead of moving at a constant rate.
- Exponential damping
- Closing the same fraction of the remaining gap per second, written
1 - Exp(-k * dt)per frame so it's identical at any frame rate. - Perlin noise
- A smooth, repeatable random function: nearby inputs give similar outputs. Used for waves, wind, terrain and shake.
- Seed
- The starting value of a pseudo-random generator; the same seed produces the same sequence.
- Floating-point precision
- The limit of about seven significant digits in a
float, which makes small steps vanish at large values. - Floating origin
- Shifting the whole world periodically so the player stays near (0, 0, 0), where floats are most precise.