If you learn one operation in game math, learn the
dot product.
It is a single multiply-and-add that secretly measures the angle between two
directions, and that one number answers a startling range of in-game questions: is the enemy
in front of me? how long is this vector's shadow on that one? how bright is this surface? Its
whole power flows from one identity,
\vec{a} \cdot \vec{b} = \lVert \vec{a}\rVert\,\lVert \vec{b}\rVert\,\cos\theta,
where \theta is the angle between
\vec{a} and \vec{b}
(see dot product and angle).
Because \cos rides along, the dot product is really an
angle-meter in disguise. Here are its three workhorse jobs.
Three jobs, line by line
Step 1 — (i) Facing: the sign tells you in front or behind. The lengths in
\lVert\vec{a}\rVert\lVert\vec{b}\rVert are always positive, so the
sign of \vec{a}\cdot\vec{b} is exactly the sign of
\cos\theta:
\operatorname{sign}(\vec{a}\cdot\vec{b}) = \operatorname{sign}(\cos\theta) = \begin{cases} +\ & \theta < 90^\circ \ \ (\vec{b} \text{ is in front of } \vec{a}) \\ 0\ & \theta = 90^\circ \ \ (\text{exactly beside}) \\ -\ & \theta > 90^\circ \ \ (\vec{b} \text{ is behind } \vec{a}). \end{cases}
Point \vec{a} the way the guard faces and let
\vec{b} be the direction to the player: one dot product, read its
sign, and you know whether the player is ahead, beside, or behind.
Step 2 — (ii) Projection: the shadow length. Take
\vec{b} and a unit direction
\hat{a} (so \lVert\hat{a}\rVert = 1).
Then the identity collapses to the length of \vec{b}'s shadow
cast along \hat{a} — its
projection:
\vec{b}\cdot\hat{a} = \lVert\vec{b}\rVert\cos\theta = (\text{how far } \vec{b} \text{ reaches in the } \hat{a} \text{ direction}).
This is "how much of my speed is forward?", "how far up the ramp did I climb?", "what's my
component toward the goal?" — all one dot with a unit vector.
Step 3 — (iii) Lighting: brightness is a cosine. A matte (diffuse) surface
with unit normal \hat{N} lit from unit direction
\hat{L} is brightest face-on and dims as it tilts away — exactly
\cos\theta. Clamp the back side to dark (no negative light):
\text{brightness} = \max\!\big(0,\; \hat{N}\cdot\hat{L}\big).
This single line — Lambert's cosine law — runs for every lit pixel in the
frame. Face the light: \hat{N}\cdot\hat{L} = 1, full bright. Turn
edge-on: 0. Turn away: clamped to 0,
dark.
For vectors \vec{a}, \vec{b} at angle
\theta,
\vec{a}\cdot\vec{b} = \lVert\vec{a}\rVert\lVert\vec{b}\rVert\cos\theta,
and three jobs follow:
-
Facing — the sign of \vec{a}\cdot\vec{b}
tells you whether \vec{b} is in front of
(>0), behind (<0), or beside
(=0) \vec{a}.
-
Projection — with \hat{a} a unit vector,
\vec{b}\cdot\hat{a} = \lVert\vec{b}\rVert\cos\theta is the
length of \vec{b}'s shadow along
\hat{a}.
-
Diffuse lighting — a surface's brightness is
\max(0,\ \hat{N}\cdot\hat{L}), the (clamped) cosine of the
angle to the light.
"Can the guard see the player?" is a dot product. Let
\hat{f} be the unit direction the guard faces and
\hat{d} the unit direction to the player. The player is inside a
vision cone of total angle \text{fov} exactly when the angle
between them is at most half the cone — i.e. when the cosine is large enough:
\hat{f}\cdot\hat{d} \;>\; \cos\!\Big(\frac{\text{fov}}{2}\Big).
One dot, one precomputed constant \cos(\text{fov}/2), one
comparison — no inverse cosine, no square roots, no branches. That is why stealth games can
afford to ask it for hundreds of enemies every single frame. Narrow the cone and the
threshold rises toward 1; widen it toward
180^\circ and the threshold falls toward
0, "anything not behind me".
It's tempting to treat the dot product's raw value as the trustworthy number — but
in game code the sign is usually the safer thing to lean on. Checking whether the
player is in front of or behind a guard only needs
\operatorname{sign}(\hat f\cdot\vec d): scaling either vector by
any positive number never flips that sign, so the raw (non-unit) vector to the player works
just as well as a unit one for this check.
Comparing magnitudes is a different story. Say two enemies stand at different
distances and you want to know which is more directly ahead by comparing
\hat f\cdot\vec d_1 against
\hat f\cdot\vec d_2. If \vec d_1 and
\vec d_2 aren't both normalised to unit length first, the farther
(longer) direction vector can win the comparison simply because it is longer — nothing to do
with which enemy is actually more "ahead". Forgetting to normalise before comparing
dot-product sizes across different pairs of vectors is one of the most common silent
bugs in gameplay code: it never crashes, it just quietly picks the wrong target.