The Dot Product in Games

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:

"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.

See it explained