Article

+ Updated

Game Math Basics - What Dot and Cross Products Do in Games

Is the target in front or behind? On the left or the right? Inside the field of view? None of these questions need trigonometry. A single dot or cross product answers each one.

The problem: did the guard see you?

Say you're making a stealth game. A guard stands in a corridor, looking in some direction. Every frame you need to answer:

  • Is the player in front of the guard or behind them? (backstab check)

  • Is the player to the guard's left or right? (which way the guard should turn their head)

  • Is the player inside the guard's 90° vision cone?

A beginner's first instinct is often to compute two angles with atan2, subtract them, and then deal with the wraparound between 359° and 1°. These questions actually have simpler, faster answers: the dot product and the cross product.


Coordinate convention

All code in this series uses screen coordinates: x points right, y points down. This affects what the cross product tells you about left and right, which gets its own explanation below.

A 2D vector needs just two floats, plus the usual operators:

struct Vec2 {
    float x = 0.0f;
    float y = 0.0f;

    constexpr Vec2() = default;
    constexpr Vec2(float x_, float y_) : x(x_), y(y_) {}

    constexpr Vec2 operator+(Vec2 o) const { return {x + o.x, y + o.y}; }
    constexpr Vec2 operator-(Vec2 o) const { return {x - o.x, y - o.y}; }
    constexpr Vec2 operator*(float s) const { return {x * s, y * s}; }
    constexpr Vec2 operator/(float s) const { return {x / s, y / s}; }
    constexpr Vec2 operator-() const { return {-x, -y}; }
    constexpr Vec2& operator+=(Vec2 o) { x += o.x; y += o.y; return *this; }
    constexpr Vec2& operator-=(Vec2 o) { x -= o.x; y -= o.y; return *this; }
    constexpr Vec2& operator*=(float s) { x *= s; y *= s; return *this; }
    friend constexpr Vec2 operator*(float s, Vec2 v) { return v * s; }
    friend constexpr bool operator==(Vec2 a, Vec2 b) = default;
};

Dot product: front/back and projection

// Dot product: |a||b|cosθ. Positive = roughly the same direction, 0 = perpendicular, negative = roughly opposite
constexpr float dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }

// 2D cross product (also called perp-dot): |a||b|sinθ. Its sign tells you which side of a b is on
constexpr float cross(Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }

Geometrically, dot(a, b) = |a|·|b|·cosθ, where θ is the angle between the two vectors. When we don't care about the exact value, the sign alone is enough:

DOT(A, B)

Angle θ

Meaning

Greater than 0

Less than 90°

Roughly the same direction: target is in front

Equal to 0

Exactly 90°

Perpendicular: target is directly to the side

Less than 0

Greater than 90°

Roughly opposite: target is behind

// Dot > 0: the angle between facing and target is under 90°, so the target is "in front"
inline bool isInFront(Vec2 facing, Vec2 toTarget) { return dot(facing, toTarget) > 0.0f; }

If a is a unit vector, dot(a, b) also happens to equal the length of b's projection onto a, i.e. "how far away the target is along the direction I'm facing". In the demo, the length of the ochre line segment is exactly this dot product.


Cross product: left or right

The 2D cross product cross(a, b) = |a|·|b|·sinθ is a signed number, and its sign tells you which side of a b is on:

enum class Side { Left, Right, Straight };

// The sign of the cross product decides left vs. right. Note: screen y points down, so the result is the opposite of a math textbook (y up)
inline Side sideOf(Vec2 facing, Vec2 toTarget, float epsilon = 1e-4f) {
    float c = cross(facing, toTarget);
    if (c > epsilon) return Side::Right;
    if (c < -epsilon) return Side::Left;
    return Side::Straight;
}

With y pointing down, left and right are flipped

In a math textbook the y axis points up, and cross > 0 means b is to the left of a (counterclockwise). In screen coordinates y points down, the whole picture is flipped vertically, and cross > 0 becomes right. When you switch engines or coordinate systems, this is the easiest thing to get wrong.


Try it yourself

Drag the red dot (player), the black dot (guard), or the arrow in front of the guard. While playing, the guard sweeps left and right. The bottom-left corner shows the dot product, cross product and local coordinates live.

A few experiments worth trying:

  1. Drag the player directly beside the guard and dot approaches 0; drag them behind and dot goes negative, with the ochre segment pointing backward.

  2. Move the player back and forth across the guard's line of sight and watch the sign of cross flip, along with the switch between left and right.

  3. Lower fov, then put the player near the edge of the vision cone and see when can see changes.

  4. Compare the two components of local with dot and cross. You'll find they're the same pair of numbers (explained below).


Vision cones without acos

To check "is the angle smaller than half the field of view", the intuitive version is acos(dot(f, normalize(to))) < halfAngle. But acos is slow, and you have to normalize first. Take the cosine of both sides and multiply through by |to|, and it becomes a single multiply-and-compare:

struct VisionCone {
    Vec2 position;
    Vec2 facing;      // unit vector
    float halfAngle;  // half of the field of view (radians)
    float range;
};

// No acos, and no need to normalize toTarget first:
//   dot(f, to) >= |to| · cos(halfAngle)  ⇔  angle <= halfAngle
inline bool canSee(const VisionCone& cone, Vec2 target) {
    Vec2 to = target - cone.position;
    float d2 = lengthSq(to);
    if (d2 > cone.range * cone.range) return false;  // do the cheap distance check first
    if (d2 < 1e-8f) return true;
    return dot(cone.facing, to) >= std::cos(cone.halfAngle) * std::sqrt(d2);
}

In a real project, cos(halfAngle) can be precomputed whenever the vision parameters change, and distance comparisons use squared length to skip the square root:

constexpr float lengthSq(Vec2 v) { return dot(v, v); }  // use this when comparing distances to skip the sqrt
inline float length(Vec2 v) { return std::sqrt(dot(v, v)); }
inline float distance(Vec2 a, Vec2 b) { return length(b - a); }

inline Vec2 normalized(Vec2 v) {
    float len = length(v);
    return len > 1e-6f ? v / len : Vec2{};  // a zero vector has no direction; return zero instead of NaN
}

Matrices: seeing the world from another point of view

You can rotate a vector directly with trig functions:

// Perpendicular vector: in y-down screen coordinates this is "rotate 90° clockwise", i.e. the right-hand direction when facing v
constexpr Vec2 perp(Vec2 v) { return {-v.y, v.x}; }

inline Vec2 fromAngle(float radians) { return {std::cos(radians), std::sin(radians)}; }
inline float angleOf(Vec2 v) { return std::atan2(v.y, v.x); }

inline Vec2 rotate(Vec2 v, float radians) {
    float c = std::cos(radians), s = std::sin(radians);
    return {v.x * c - v.y * s, v.x * s + v.y * c};
}

// Signed angle from a to b, in the range (-π, π]
inline float signedAngle(Vec2 a, Vec2 b) { return std::atan2(cross(a, b), dot(a, b)); }

But when we need to convert back and forth between "world coordinates" and "an object's local coordinates", a matrix is the better tool. Understanding a 2×2 rotation matrix takes one sentence: the two columns of the matrix are the x axis and y axis after rotation.

// Stored as two columns: col0 is the transformed x axis, col1 is the transformed y axis
struct Mat2 {
    Vec2 col0{1, 0};
    Vec2 col1{0, 1};

    static Mat2 rotation(float radians) {
        float c = std::cos(radians), s = std::sin(radians);
        return {{c, s}, {-s, c}};
    }

    // Matrix × vector = weighted sum of the columns by component
    Vec2 operator*(Vec2 v) const { return col0 * v.x + col1 * v.y; }

    Mat2 operator*(const Mat2& o) const { return {(*this) * o.col0, (*this) * o.col1}; }

    Mat2 transposed() const { return {{col0.x, col1.x}, {col0.y, col1.y}}; }
};

An object's transform = rotation + translation. Going from local to world coordinates means rotate first, then translate; going the other way means translate back first, then rotate in reverse. The columns of a rotation matrix are perpendicular to each other and have length 1, so its inverse is just its transpose:

// Position + orientation: the most common transform for a game object (scale omitted here)
struct Transform2D {
    Vec2 position;
    float rotation = 0.0f;

    Vec2 forward() const { return fromAngle(rotation); }
    Vec2 right() const { return perp(forward()); }

    // Local -> world: rotate first, then translate
    Vec2 toWorld(Vec2 local) const { return position + Mat2::rotation(rotation) * local; }

    // World -> local: translate back first, then rotate in reverse. The inverse of a rotation matrix is its transpose
    Vec2 toLocal(Vec2 world) const { return Mat2::rotation(rotation).transposed() * (world - position); }
};

Expand the transposed matrix times a vector and it turns out to be one dot product along the "forward" axis and one along the "right" axis. The x component of the local coordinates is dot(to, forward), and the y component is dot(to, right). Dot products and matrices are really the same thing:

// Equivalent version: local coordinates are just one dot product each along the "forward" and "right" axes
inline Vec2 toLocalByDot(const Transform2D& t, Vec2 world) {
    Vec2 d = world - t.position;
    return {dot(d, t.forward()), dot(d, t.right())};
}

Projection and reflection

Two more operations that come up constantly: projecting a vector onto a direction (for example, splitting a velocity into the part along a wall and the part into it), and reflecting across a normal (bullets bouncing off walls, billiard balls hitting the cushion):

// Projection of v onto the direction of onto (onto doesn't need to be a unit vector)
inline Vec2 project(Vec2 v, Vec2 onto) {
    float d = lengthSq(onto);
    return d > 1e-12f ? onto * (dot(v, onto) / d) : Vec2{};
}

// Reflect v across the unit normal n: bullets bouncing off walls and light reflecting both use this
constexpr Vec2 reflect(Vec2 v, Vec2 n) { return v - n * (2.0f * dot(v, n)); }

Common pitfalls

Problem

Cause

Fix

NaN appears and objects teleport or vanish

Normalizing a zero vector (dividing by 0)

normalized returns a zero vector for very short vectors; let the caller check

Vision check works only some of the time

The facing vector isn't normalized, so its length inflates the dot product

Guarantee facing is a unit vector when you store it

Left and right come out exactly reversed

The coordinate system's y direction doesn't match the formula's assumption

Write a unit test that checks one known example

Angle interpolation or comparison goes wrong

Mixing degrees and radians, or not handling the ±180° wraparound

Use radians everywhere internally; prefer vectors over angles when you can

Distance comparisons slow things down

Calling sqrt a lot

Use lengthSq when comparing distances


In real games

  • Backstab checks: if the dot product of the attacker's facing and the victim's facing is above some threshold (roughly the same direction), the attack came from behind.

  • Steering decisions: when an AI needs to turn toward a target, the sign of the cross product decides whether to turn left or right, with no angle math.

  • Lighting: diffuse brightness is proportional to dot(normal, light direction). It's the first line of nearly every shader.

  • Which side of a line a point is on: the sign of cross(b - a, p - a). Point-in-triangle tests, polygon convexity checks and collision detection all use it (see Volume 2).

  • In 3D: the dot product means exactly the same thing; the cross product instead gives a new vector perpendicular to both inputs, which is the basis for computing normals and building coordinate frames.


Further reading

  • 3Blue1Brown, Essence of Linear Algebra: builds intuition for matrices and dot products with animation

  • Fletcher Dunn and Ian Parberry, 3D Math Primer for Graphics and Game Development

Copyright

Copyright of this article belongs to its author or rights holder. When reposting or quoting, please follow the applicable license terms and keep the link to this article.

XuYi, “Game Math Basics - What Dot and Cross Products Do in Games”, https://xuyi.dev/en/2026-11-06-bgcegv

Comments

Comments

Loading comments…