C++ Module 22 – C++ for game development
Game maths in C++: vectors and matrices
Write Vec2 and Mat3 value types in C++, use dot and cross products for view cones and bounces, and combine rotation, scale and translation in order.
What you will learn
- Implement 2D and 3D vector types with operators
- Use the dot and cross products to test angles, sides and bounces
- Apply rotation, scale and translation with 3×3 matrices, in a fixed order
- Avoid common floating-point pitfalls in game maths
Before you start
What you’ll learn · Before you start
What you will learn
- Implement 2D and 3D vector types with operators
- Use the dot and cross products to test angles, sides and bounces
- Apply rotation, scale and translation with 3×3 matrices, in a fixed order
- Avoid common floating-point pitfalls in game maths
Before you start
On this page
Is an enemy at (5, 1) inside a guard’s view of 30° to either side, and on the guard’s left or right? Game code
answers such questions with vectors, which hold positions, velocities and directions, and matrices, which
move, turn and stretch them. Written as small C++ structs with operators, a + v * dt reads like the formula it
computes.
The dot product says how much two directions agree; the cross product gives a perpendicular, whose sign tells
left from right. A 3×3 matrix holds a 2D rotation, scale and translation at once, and matrices combine in a fixed
order. Floats round at every step: positions are compared with a tolerance, never with ==, a vector turned over and
over drifts, and far from the origin small steps vanish. You will build these types and use them on a guard and a
bounce.
Vectors are small values with operators
vec.h defines Vec2 as an aggregate: A class type with no user-declared constructors, only public non-static data members, no virtual functions and no virtual, private or protected base classes, or an array. A brace list fills it member by member, in declaration order, without calling a constructor. More in “C++ structs, aggregates and designated initialisers” of two floats, half the memory of double. The
operators are free constexpr functions that take their arguments by value: an 8-byte struct is as cheap to copy as
a pointer, and constant vectors can be computed while compiling.
// A 2D vector for game maths: a small value with operators, so code reads like the formulas.
#pragma once
#include <cmath>
#include <format>
#include <string>
struct Vec2 {
float x = 0.0f;
float y = 0.0f;
};
constexpr Vec2 operator+(Vec2 a, Vec2 b) { return {a.x + b.x, a.y + b.y}; }
constexpr Vec2 operator-(Vec2 a, Vec2 b) { return {a.x - b.x, a.y - b.y}; }
constexpr Vec2 operator-(Vec2 v) { return {-v.x, -v.y}; }
constexpr Vec2 operator*(Vec2 v, float s) { return {v.x * s, v.y * s}; }
constexpr Vec2 operator*(float s, Vec2 v) { return v * s; }
constexpr Vec2 operator/(Vec2 v, float s) { return {v.x / s, v.y / s}; }
constexpr Vec2& operator+=(Vec2& a, Vec2 b) { return a = a + b; }
// |a| |b| cos(angle between a and b): positive when they point the same way, 0 when perpendicular.
constexpr float dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }
// The z part of the 3D cross product: positive when b is anticlockwise from a, with y pointing up.
constexpr float cross(Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }
inline float length(Vec2 v) { return std::sqrt(dot(v, v)); }
// The unit vector pointing like v. A zero vector has no direction, so it stays zero.
inline Vec2 normalized(Vec2 v) {
const float len = length(v);
return len > 0.0f ? v / len : Vec2{};
}
// v after bouncing off a surface whose unit normal is n.
constexpr Vec2 reflect(Vec2 v, Vec2 n) { return v - 2.0f * dot(v, n) * n; }
inline std::string str(Vec2 v) { return std::format("({}, {})", v.x, v.y); } normalized() returns the zero vector when its argument is zero: dividing by a length of 0 would produce NaN.
values.cpp checks these claims on both compilers:
// Vec2 as a value: how big it is, what the type traits say about it, a vector computed while
// compiling, and the zero vector that normalized() keeps from becoming NaN.
#include <print>
#include <type_traits>
#include "vec.h"
struct Vec2d { // the same vector with double coordinates, for comparison
double x = 0.0;
double y = 0.0;
};
// Evaluated by the compiler: the build fails if this arithmetic is wrong.
constexpr Vec2 start{0.0f, 2.0f};
constexpr Vec2 velocity{3.0f, 4.0f};
constexpr Vec2 after_half_a_second = start + velocity * 0.5f;
static_assert(after_half_a_second.x == 1.5f && after_half_a_second.y == 4.0f);
int main() {
std::println("sizeof(Vec2) = {}, sizeof(Vec2d) = {}, sizeof(void*) = {}", sizeof(Vec2), sizeof(Vec2d), sizeof(void*));
std::println("aggregate: {}, trivially copyable: {}", std::is_aggregate_v<Vec2>, std::is_trivially_copyable_v<Vec2>);
std::println("start + velocity * 0.5 = {}, checked while compiling", str(after_half_a_second));
std::println("normalized of the zero vector: {}", str(normalized(Vec2{})));
} from session import run
# values.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off values.cpp -o values && ./values
g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off values.cpp -o values-gcc && ./values-gcc
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off values.cpp -o values && ./values sizeof(Vec2) = 8, sizeof(Vec2d) = 16, sizeof(void*) = 8 aggregate: true, trivially copyable: true start + velocity * 0.5 = (1.5, 4), checked while compiling normalized of the zero vector: (0, 0) $ g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off values.cpp -o values-gcc && ./values-gcc sizeof(Vec2) = 8, sizeof(Vec2d) = 16, sizeof(void*) = 8 aggregate: true, trivially copyable: true start + velocity * 0.5 = (1.5, 4), checked while compiling normalized of the zero vector: (0, 0)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 values_session.py
Vec2 takes 8 bytes, as a pointer does, against 16 for a double version. The type traits report an aggregate that
is A type whose objects may be copied by copying their bytes, because its copy and move operations and its destructor are all the trivial ones the compiler writes.,
the static_assert passed, so the compiler itself computed (1.5, 4), and the zero vector stayed (0, 0).
Checkpoint
Check: Vectors as small values
The dot product measures agreement, the cross product picks a side
For two unit vectors, the dot product is the cosine of the angle between them: 1 when they point the same way, 0 when
they are perpendicular, −1 when they are opposite. A guard who sees 30° to either side spots an enemy when
dot(facing, normalized(to_enemy)) is at least cos 30° = 0.866, with no acos. The 2D cross product,
a.x * b.y - a.y * b.x, is the sine of the angle times the lengths, so its sign says on which side b lies:
positive means to the left when the y axis points up. With y pointing down, as in most screen coordinates, the sign
flips.
The dot product also bounces things. With n the unit normal of a surface, dot(v, n) * n is the part of a
velocity v that goes into the surface; a bounce flips that part and keeps the rest:
Reflecting a velocity off the floor with the dot product
Text description of the diagram
A horizontal line is the floor, the line y = 0. An arrow labelled v = (3, −4) comes down from the upper left and ends at a point on the floor. A dashed arrow labelled n = (0, 1) points straight up from that point: the floor's unit normal. A second arrow, labelled r = (3, 4), leaves the same point towards the upper right, at the same angle to the normal as v.
Under the drawing: v · n = −4, so the part of v along n is (0, −4), and the bounce flips that part while the rest stays: r = v − 2 (v · n) n = (3, −4) − 2 · (−4) · (0, 1) = (3, 4).
// A guard at the origin faces along +x and sees 30° to either side. Which enemies does it see,
// and on which side are they? Then a ball bounces off the floor and off a 45° wall.
#include <cmath>
#include <numbers>
#include <print>
#include "vec.h"
int main() {
const Vec2 guard{0.0f, 0.0f};
const Vec2 facing{1.0f, 0.0f}; // a unit vector
const float cos_half_view = std::cos(30.0f * std::numbers::pi_v<float> / 180.0f);
const Vec2 enemies[] = {{5.0f, 1.0f}, {2.0f, 4.0f}, {-3.0f, 0.5f}, {4.0f, -2.0f}};
std::println("cos 30° = {:.4f}", cos_half_view);
for (const Vec2 e : enemies) {
const Vec2 to_enemy = normalized(e - guard);
const float c = dot(facing, to_enemy); // the cosine of the angle off the facing direction
const char* seen = c >= cos_half_view ? "in view" : "not seen";
const char* side = cross(facing, to_enemy) > 0.0f ? "left" : "right";
std::println("enemy at {:<10} dot {:>7.4f} {:<9} {}", str(e), c, seen, side);
}
const Vec2 v{3.0f, -4.0f};
std::println("off the floor: {}", str(reflect(v, Vec2{0.0f, 1.0f})));
std::println("off the 45° wall: {}", str(reflect(v, normalized(Vec2{1.0f, 1.0f}))));
} Built with -ffp-contract=off, both compilers print the same digits:
from session import run
# dot_cross.cpp (with vec.h) built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off dot_cross.cpp -o dot_cross && ./dot_cross
g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off dot_cross.cpp -o dot_cross-gcc && ./dot_cross-gcc
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off dot_cross.cpp -o dot_cross && ./dot_cross cos 30° = 0.8660 enemy at (5, 1) dot 0.9806 in view left enemy at (2, 4) dot 0.4472 not seen left enemy at (-3, 0.5) dot -0.9864 not seen left enemy at (4, -2) dot 0.8944 in view right off the floor: (3, 4) off the 45° wall: (4, -3) $ g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off dot_cross.cpp -o dot_cross-gcc && ./dot_cross-gcc cos 30° = 0.8660 enemy at (5, 1) dot 0.9806 in view left enemy at (2, 4) dot 0.4472 not seen left enemy at (-3, 0.5) dot -0.9864 not seen left enemy at (4, -2) dot 0.8944 in view right off the floor: (3, 4) off the 45° wall: (4, -3)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dot_cross_session.py
The enemy at (5, 1) is 11° off the guard’s facing, so its dot product, 0.9806, passes the test, while (2, 4), 63° away, fails it with 0.4472. The enemy at (4, −2) is in view on the right, and the one at (−3, 0.5) is almost straight behind, with a dot product of −0.9864. The ball’s velocity (3, −4) leaves the floor as (3, 4) and the 45° wall as (4, −3).
The model below starts with the guard’s facing, a = (1, 0, 0), and the enemy at (5, 1) as b. Its angle, 11.3°, is the one the dot product tests, and the positive z of a × b says left. Set b to (4, −2, 0) and the z turns negative.
a = (1, 0, 0), b = (5, 1, 0): a · b = 5, a × b = (0, 0, 1).
Checkpoint
Check: The dot and cross products
Matrices move, turn and stretch points, and their order matters
A 3×3 matrix transforms 2D points written as (x, y, 1). The extra 1 lets one matrix also translate, so a single
matrix can hold a whole object’s placement. mat3.h builds three kinds:
- translation(t) moves every point by the vector
t; - rotation(degrees) turns points anticlockwise about the origin;
- scale(s) stretches along the x and y axes; with two different factors it is non-uniform.
// A 3x3 matrix for 2D transforms. A point (x, y) is used as the column (x, y, 1), so one matrix
// can rotate, scale and translate; m[row][column], and the default is the identity.
#pragma once
#include <cmath>
#include <numbers>
#include "vec.h"
struct Mat3 {
float m[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
};
// a * b applies b first, then a.
constexpr Mat3 operator*(const Mat3& a, const Mat3& b) {
Mat3 r;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
float sum = 0.0f;
for (int k = 0; k < 3; ++k) sum += a.m[i][k] * b.m[k][j];
r.m[i][j] = sum;
}
}
return r;
}
constexpr Mat3 translation(Vec2 t) { return {{{1, 0, t.x}, {0, 1, t.y}, {0, 0, 1}}}; }
constexpr Mat3 scale(Vec2 s) { return {{{s.x, 0, 0}, {0, s.y, 0}, {0, 0, 1}}}; }
inline Mat3 rotation(float degrees) { // anticlockwise, about the origin
const float a = degrees * std::numbers::pi_v<float> / 180.0f;
const float c = std::cos(a);
const float s = std::sin(a);
return {{{c, -s, 0}, {s, c, 0}, {0, 0, 1}}};
}
// A point moves with the translation; a direction, such as a velocity, does not.
constexpr Vec2 transform_point(const Mat3& t, Vec2 p) {
return {t.m[0][0] * p.x + t.m[0][1] * p.y + t.m[0][2], t.m[1][0] * p.x + t.m[1][1] * p.y + t.m[1][2]};
}
constexpr Vec2 transform_vector(const Mat3& t, Vec2 v) {
return {t.m[0][0] * v.x + t.m[0][1] * v.y, t.m[1][0] * v.x + t.m[1][1] * v.y};
} The product a * b applies b first, then a, so a chain of matrices reads from right to left. Swapping two
matrices usually changes the result:
// The same two transforms of the point (1, 0), in both orders: turn 30° about the origin, and
// move 5 m to the right.
#include <print>
#include "mat3.h"
int main() {
const Vec2 p{1.0f, 0.0f};
const Mat3 turn = rotation(30.0f);
const Mat3 move = translation({5.0f, 0.0f});
const Mat3 turn_then_move = move * turn; // the right-hand matrix acts first
const Mat3 move_then_turn = turn * move;
std::println("turn, then move: {}", str(transform_point(turn_then_move, p)));
std::println("move, then turn: {}", str(transform_point(move_then_turn, p)));
// A velocity is a direction: the translation does not change it.
std::println("velocity (2, 0) through turn_then_move: {}", str(transform_vector(turn_then_move, {2.0f, 0.0f})));
} from session import run
# order.cpp (with mat3.h) built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off order.cpp -o order && ./order
g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off order.cpp -o order-gcc && ./order-gcc
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off order.cpp -o order && ./order turn, then move: (5.8660254, 0.5) move, then turn: (5.196152, 3) velocity (2, 0) through turn_then_move: (1.7320508, 1) $ g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off order.cpp -o order-gcc && ./order-gcc turn, then move: (5.8660254, 0.5) move, then turn: (5.196152, 3) velocity (2, 0) through turn_then_move: (1.7320508, 1)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py
Turning (1, 0) by 30° and then moving it gives (5.866, 0.5); moving it first and then turning the moved point about the origin gives (5.196, 3). Step through both orders:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- One point, two products. Both columns start from the point (1, 0) and use the same two matrices of order.cpp, rotation(30) and translation({5, 0}). Only the order of the product differs.
- move * turn: the turn acts first. In a product a * b, b acts first. So move * turn first turns the point 30° anticlockwise about the origin, to (cos 30°, sin 30°) = (0.866, 0.5).
- then the move. The translation then adds (5, 0): (5.866, 0.5), the point order.cpp printed for turn, then move. The object turned on the spot and moved right.
- turn * move: the move acts first. turn * move first moves the point 5 m to the right, to (6, 0).
- then a turn about the origin. The turn is still about the origin, so the moved point swings along a circle of radius 6, to (5.196, 3): 2.6 m from the other result. Same matrices, different product.
A velocity goes through transform_vector, which ignores the translation, because a direction has no position: (2,
0) turned by 30° is (1.732, 1) wherever the object stands.
Three more cases need care. Rotating about a point other than the origin takes three matrices in one order. Surface normals need a different matrix from points once a scale is non-uniform. Going back from the world to an object’s own space needs the inverse of its matrix, which is cheap when the matrix only rotates and translates.
Matrix Calculator Multiply two 3×3 matrices both ways to see that the products differ.Checkpoint
Check: Matrices and the order of a product
Practise: a Vec3 for 3D
vec3.h holds a Vec3 with its operators; dot, cross, normalized and triangle_normal are yours to write.
The tests include the degenerate cases: a zero vector and a triangle whose corners lie on one line.
C header · vec3.h
// The exercise: a 3D vector with the operations a 3D game needs. Fill in the four TODOs.
#pragma once
#include <cmath>
struct Vec3 {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};
constexpr Vec3 operator+(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
constexpr Vec3 operator-(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
constexpr Vec3 operator-(Vec3 v) { return {-v.x, -v.y, -v.z}; }
constexpr Vec3 operator*(Vec3 v, float s) { return {v.x * s, v.y * s, v.z * s}; }
constexpr Vec3 operator*(float s, Vec3 v) { return v * s; }
constexpr Vec3 operator/(Vec3 v, float s) { return {v.x / s, v.y / s, v.z / s}; }
// TODO 1: the dot product of a and b.
constexpr float dot(Vec3 a, Vec3 b) {
(void)a;
(void)b;
return 0.0f;
}
// TODO 2: the cross product: perpendicular to both a and b, and as long as the area of the
// parallelogram they span. cross({1, 0, 0}, {0, 1, 0}) is {0, 0, 1}.
constexpr Vec3 cross(Vec3 a, Vec3 b) {
(void)a;
(void)b;
return {};
}
inline float length(Vec3 v) { return std::sqrt(dot(v, v)); }
// TODO 3: the unit vector pointing like v. A zero vector stays zero.
inline Vec3 normalized(Vec3 v) { return v; }
// TODO 4: the unit normal of the triangle a, b, c: it points to the side from which the corners
// run anticlockwise. Three corners on one line make no triangle: return the zero vector.
inline Vec3 triangle_normal(Vec3 a, Vec3 b, Vec3 c) {
(void)a;
(void)b;
(void)c;
return {};
} C++ · vec3_tests.cpp
// Sample tests for vec3.h. Build: clang++ -std=c++23 -Wall -Wextra vec3_tests.cpp -o vec3_tests
#include <cmath>
#include <print>
#include "vec3.h"
int passed = 0;
int failed = 0;
void expect(bool ok, const char* what) {
if (ok) {
++passed;
} else {
++failed;
std::println("FAIL {}", what);
}
}
// Floats round, so results are compared with a tolerance, never with ==.
bool near(float a, float b) { return std::abs(a - b) < 1e-6f; }
bool near(Vec3 a, Vec3 b) { return near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z); }
int main() {
const Vec3 x{1, 0, 0}, y{0, 1, 0}, z{0, 0, 1};
const Vec3 a{1, 2, 3}, b{-2, 0.5f, 4};
expect(near(dot({1, 2, 3}, {4, -5, 6}), 12.0f), "dot((1, 2, 3), (4, -5, 6)) is 12");
expect(near(cross(x, y), z) && near(cross(y, x), -z), "x cross y is z, and y cross x is -z");
expect(near(dot(cross(a, b), a), 0.0f) && near(dot(cross(a, b), b), 0.0f) && length(cross(a, b)) > 1.0f,
"a cross b is not zero, and perpendicular to a and to b");
expect(near(length(cross({3, 0, 0}, {0, 4, 0})), 12.0f) && near(cross({2, 4, 6}, {1, 2, 3}), {}),
"the cross product's length is the area: 12 for 3 by 4, 0 for parallel vectors");
expect(near(normalized({3, 4, 12}), {3.0f / 13, 4.0f / 13, 12.0f / 13}) && near(length(normalized(b)), 1.0f),
"normalized keeps the direction and makes the length 1");
const Vec3 zero = normalized({});
expect(near(normalized({0, 0, 5}), z) && !std::isnan(zero.x) && near(zero, {}),
"normalizing (0, 0, 5) gives (0, 0, 1), and the zero vector stays zero, not NaN");
expect(near(triangle_normal({0, 0, 0}, {1, 0, 0}, {0, 1, 0}), z) && near(triangle_normal({0, 0, 0}, {0, 1, 0}, {1, 0, 0}), -z),
"an anticlockwise triangle in the xy plane faces +z, a clockwise one -z");
expect(near(length(triangle_normal({0, 0, 0}, {2, 0, 0}, {0, 0, 2})), 1.0f) && near(triangle_normal({0, 0, 0}, {1, 1, 1}, {2, 2, 2}), {}),
"a normal has length 1, and three corners on one line give the zero vector");
std::println("{} of {} checks passed", passed, passed + failed);
return failed == 0 ? 0 : 1;
} from session import run
# The sample tests against the starter code, before the TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra vec3_tests.cpp -o vec3_tests && ./vec3_tests
g++-16 -std=c++23 -Wall -Wextra vec3_tests.cpp -o vec3_tests-gcc && ./vec3_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra vec3_tests.cpp -o vec3_tests && ./vec3_tests FAIL dot((1, 2, 3), (4, -5, 6)) is 12 FAIL x cross y is z, and y cross x is -z FAIL a cross b is not zero, and perpendicular to a and to b FAIL the cross product's length is the area: 12 for 3 by 4, 0 for parallel vectors FAIL normalized keeps the direction and makes the length 1 FAIL normalizing (0, 0, 5) gives (0, 0, 1), and the zero vector stays zero, not NaN FAIL an anticlockwise triangle in the xy plane faces +z, a clockwise one -z FAIL a normal has length 1, and three corners on one line give the zero vector 0 of 8 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra vec3_tests.cpp -o vec3_tests-gcc && ./vec3_tests-gcc FAIL dot((1, 2, 3), (4, -5, 6)) is 12 FAIL x cross y is z, and y cross x is -z FAIL a cross b is not zero, and perpendicular to a and to b FAIL the cross product's length is the area: 12 for 3 by 4, 0 for parallel vectors FAIL normalized keeps the direction and makes the length 1 FAIL normalizing (0, 0, 5) gives (0, 0, 1), and the zero vector stays zero, not NaN FAIL an anticlockwise triangle in the xy plane faces +z, a clockwise one -z FAIL a normal has length 1, and three corners on one line give the zero vector 0 of 8 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vec3_session.py
Edit vec3.h until both compilers print 8 of 8 checks passed. The cross product is (a.y b.z − a.z b.y, a.z b.x −
a.x b.z, a.x b.y − a.y b.x); a triangle’s normal is the normalised cross product of two of its edges, b - a and
c - a, and normalized turns a degenerate triangle’s zero cross product into the zero vector.
Practice starts with the idea you know least.
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Transforms in depth: pivots, normals and inverses, with numbers Pro: opens with a Pro, Premium or Ultimate pass
Rotating about a pivot, transforming normals and getting back from world space to an object’s own space are where matrix code most often goes wrong. Each case below runs next to a version that looks right and is not.
In this part:
- Rotating about a pivot: three matrices in one order
- Normals under a non-uniform scale, reproduced
- From world space to an object’s space: the cheap inverse
- Column vectors or row vectors: reading another library’s maths
1 diagram · 6 runnable examples (C++, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Floating-point pitfalls in game maths, reproduced and measured Pro: opens with a Pro, Premium or Ultimate pass
Floats round at every operation, and game code meets the consequences in four places: zero vectors, equality tests, values updated millions of times, and coordinates far from the origin. Each one is reproduced here with both compilers, together with the fix that holds.
In this part:
- A zero vector, a NaN that spreads, and a check that fast-math builds remove
- Comparing with a tolerance, and choosing one
- Drift: a rotation applied a million times
- Far from the origin: float spacing, measured
8 runnable examples (C++, Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What do the dot product and the 2D cross product of two vectors tell a game, and which one says whether an enemy is to the player's left?
The dot product, a.x b.x + a.y b.y, equals |a| |b| cos θ: for unit vectors it is the cosine of the angle between them, positive in front, zero at a right angle and negative behind. That makes it the test for a view cone (compare it with the cosine of the half angle) and the tool for projecting and reflecting velocities. The 2D cross product, a.x b.y − a.y b.x, equals |a| |b| sin θ, so its sign says on which side b lies: it answers the left-or-right question. Positive means left, anticlockwise, when y points up, and the sign flips in screen coordinates, where y points down. Its size is the area of the parallelogram the vectors span, zero for parallel vectors. In 3D the cross product is a vector perpendicular to both, which gives surface normals.
-
Fresher to mid
A sprite should spin about its own centre. In your code it swings in a wide circle around the corner of the screen, and when you reverse the order of the matrices it flies off the screen. Explain both bugs and write the transform that is right.
-
Mid to senior
After an artist stretches a level to twice its width, balls bounce off sloped walls at wrong angles and some pass through them. Flat floors and upright walls still behave. What is wrong in the maths, and how do you fix it in the engine?
-
Mid to senior
In an open-world game, vehicles 80 km from the world's origin shake on screen, and a door that opens 1 mm per frame never moves. Positions are floats in metres. Explain both symptoms with numbers, and compare three fixes.
Key takeaways
- Vectors are small aggregates with
constexproperators taking values;normalizedguards the zero vector. - The dot product of unit vectors is a cosine, compared with the half angle’s cosine to test a view cone.
- The sign of the 2D cross product tells left from right, and flips when y points down.
- A 3×3 matrix holds a 2D rotation, scale and translation, and in
a * bthe matrixbacts first. - Computed positions are compared with a tolerance, never with
==.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- Numerics library (cppreference.com)
- Mathematical constants (std::numbers) (cppreference.com)
- std::nextafter (cppreference.com)
- Options That Control Optimization (GCC): -ffast-math, -ffinite-math-only (GNU Project)
- Clang Compiler User's Manual: floating-point options (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress