Chapter 5: Math for games
Practical applications for sine waves, pseudo-randomness, angles, and more.
Diagonal movement
If you're making a game with 4-way directional movement, it's easy to compute velocity. If the player is pressing → then have them go +SPEED units on the x axis. If they're pressing ↑ then have them go +SPEED units on the y axis. Flip the pluses for minuses for the other two directions. Easy!
function fixedUpdate(dt: number) {
const speed = 80;
if (keysDown.has("ArrowUp")) {
player.y -= speed * dt;
} else if (keysDown.has("ArrowDown")) {
player.y += speed * dt;
} else if (keysDown.has("ArrowLeft")) {
player.x -= speed * dt;
} else if (keysDown.has("ArrowRight")) {
player.x += speed * dt;
}
}Now we want to support 8-way directional movement, so the player can press → and ↑ at the same time to go diagonally. Easy, too, right?
Click to focus. WASD or arrow keys to move.
Inconsistent diagonal speed
function fixedUpdate(dt: number) {
const speed = 80;
if (keysDown.has("ArrowUp")) player.y -= speed * dt;
if (keysDown.has("ArrowDown")) player.y += speed * dt;
if (keysDown.has("ArrowLeft")) player.x -= speed * dt;
if (keysDown.has("ArrowRight")) player.x += speed * dt;
}But now when → and ↑ are both held, the player begins moving faster on the diagonal. Why?
Well, if each direction key adds full speed on its own axis, holding two keys adds two full speeds at a right angle.
Put another way, think of a right triangle where the base is your x velocity and the height is your y velocity. When you move diagonally, you're traveling along its hypotenuse.
So to make the speed consistent, when travelling diagonally you need to divide both the x and y velocities by that right triangle's hypotenuse.
Click to focus. WASD or arrow keys to move.
function fixedUpdate(dt: number) {
const speed = 80;
let vx = 0;
let vy = 0;
if (keysDown.has("ArrowUp")) vy -= speed * dt;
if (keysDown.has("ArrowDown")) vy += speed * dt;
if (keysDown.has("ArrowLeft")) vx -= speed * dt;
if (keysDown.has("ArrowRight")) vx += speed * dt;
const hypot = Math.hypot(vx, vy) || 1;
player.x += vx / hypot;
player.y += vy / hypot;
}We'll revisit this problem in Chapter 8 → on controllers where we look into handling movement using analog sticks.
Modulo
In languages like Ruby and Python, the % symbol is a modulo operator. It gives you the remainder when dividing two numbers, and wraps negative results back into a positive range. But in JavaScript and most other C-like languages, % is the remainder operator [MDN] and it does not wrap negative numbers.
items = ["Sword", "Bow", "Staff", "Shield"]
index % length = -1 // undefined
mod(index, length) = 3 // "Shield"
Things like clocks which are always increasing can be implemented using the % operator directly since the numbers are all positive. But games often also need a modulo operator that handles negative number wrapping, for things like world wrapping or item selection in hotbars. Since one isn't built into the language, we have to use our own:
function (: number, : number) {
return (( % ) + ) % ;
}Seeded random numbers
Before computers were effective at generating psuedorandom numbers, people relied on books filled with random digits [Wikipedia] in order to pull random numbers for work in fields that required actual randomness, such as cryptography, nuclear physics, or statistics.
With a book of random numbers, you could pick a page and line number, and although the digits you would find at that location would be completely random, you could return to that place any number of times in the future and get the exact same digits.
We get the same guarantee when we use a seeded random number generator. A seed is like the page and line numbers. It is a way to get back to the same set of random digits.
Why is that useful?
For procedurally generated games like Minecraft or games with deterministic randomness like Balatro, you can enter a specific seed to reproduce an exact (but still randomly-generated) game state. Sometimes this is just for fun--players can share interesting seeds with one another. But seeded randomness is also practical. You might have a bug that only occurs one in a thousand times, and in a truly random game that might be impossible to debug. If a tester shares the seed with the broken state, you can reliably test and reproduce the issue as many times as you need.
Seeded random
—
Math.random()
—
The seeded random pattern repeats after every reset.
Although under the hood, the built-in Math.random() and crypto.getRandomValues() are also seeded random number generators, they're pre-seeded and we don't have an API to reset that seed. So we'll need to find our own seeded random algorithms that allow us to set the seed directly.
A popular PRNG that's nice and short is splitmix32:
function splitmix32(seed: number) {
return function () {
seed |= 0;
seed = (seed + 0x9e3779b9) | 0;
let t = seed ^ (seed >>> 16);
t = Math.imul(t, 0x21f0aaad);
t = t ^ (t >>> 15);
t = Math.imul(t, 0x735a2d97);
return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
};
}
const random = splitmix32(123);
// if you use seed 123 you'll also get these first 3 random numbers:
// random(); => 0.4575126694981009
// random(); => 0.21505506429821253
// random(); => 0.7675276368390769Is splitmix32 any good? How does it compare to other PRNGs?
It's hard to tell based on a series of numbers like 0.4575126694981009, 0.21505506429821253, 0.7675276368390769, ... if our random number generator is actually doing a good job. Instead, we can do a quick visual spot check for patterns by filling a canvas with sequential random() values from our PRNG.
A good PRNG will look like pure static with no visible patterns or streaks.
Let's see what the noise pattern looks like for an absolutely terrible seeded "random" function:
function random(seed: number) {
let t = seed;
return () => {
t += 0.1; // the slider changes this value
return (Math.sin(t * t) + 1) / 2;
};
}While they make for interesting generative art, these values are not random.
Other algorithms result in noise values that seem random at a glance, but still form patterns. Since our canvas width determines where the wrapping occurs, you may have to fiddle a bit with the width to get a pattern to emerge.
So these approaches clearly don't pass the visual spot check. But they also won't pass empirical analyses based on statistical test suites.
Rather than roll our own PRNG, this is a good place to use a tried and true algorithm. Splitmix32 is fast, easy to embed, and random-enough for all practical applications for games. Just note that it isn't cryptographically secure, so it should not be used in sensitive contexts. It's a solid choice overall to use in JavaScript runtimes which use 32-bit floating point numbers.
See also:
- A brief history of random numbers
- Mulberry32: A Tiny, Fast, Deterministic RNG, a similar algorithm to Splitmix32. (Mulberry's creator now recommends using Splitmix32.)
Utility functions
Once you've created your seeded random number generator, here's how you can use it to pick a random item from an array:
// an arbitrary seed #
const = 123;
// our seeded random number generator
const = ();
// selects a random element from an array
function <>(: [], = .) {
return [.(() * .)];
}
// pass in the seeded PRNG
([5, 2, 7, 9, 1], ); // => 7A handful of utilities that work with seeded random number generators:
// pick a random floating point number between min and max
function (: number, : number, = .) {
return + () * ( - );
}
// pick a random integer between min and max
function (: number, : number, = .) {
return .(() * ( - + 1)) + ;
}
// select a random element from an array
function <>(: [], = .) {
return [.(() * .)];
}
// shuffles an array in-place
function <>(: [], = .) {
for (let = . - 1; > 0; --) {
const = .(() * ( + 1));
[[], []] = [[], []];
}
return ;
}Sine waves
A sine wave gives us a number that oscillates between -1 and +1. We can pass it the current time to get a value in [-1, 1] that changes over time.
y = Math.sin(time);To change the speed of the oscillation, include a multiplier:
y = Math.sin(time * speed);Most of the time it's easier to work with values scaled between 0 and 1, instead of the -1 to +1 range we get with Math.sin.
y = (Math.sin(time) + 1) / 2;And if you want on/off logic, you can either round the value to give you 0 or 1, or do a range check.
// rounded number (0 or 1)
y = Math.round((Math.sin(time) + 1) / 2);
// or a boolean value (true 50% of the time)
isOn = Math.sin(time) > 0;Don't pollute your code with more state than you need
const state = {
isOn: true,
timeSinceToggled: 0,
};
function update(dt: number) {
state.timeSinceToggled += dt;
if (state.timeSinceToggled > 1) {
state.timeSinceToggled = 0;
state.isOn = !state.isOn;
}
}Sine waves to the rescue
Remember, your simulation might not need state.
const state = {
time: 0;
}
function update(dt: number) {
state.time += dt;
}
function isOn() {
return Math.sin(state.time * Math.PI) > 0;
}Where did π come from?
Trigonometry. If you draw a dot moving around the unit circle, computing Math.sin of the angle from the origin to that dot gives us the dot's y position.
One full trip around the circle is 2π radians, so a sine wave also repeats every 2π radians. Since state.time is measured in seconds, passing it directly into Math.sin means the wave takes 2π seconds--about 6.28s--to complete one full cycle.
We need the result of Math.sin(...) > 0 to change every second to match the stateful isOn toggle timing. Since sine crosses zero every π radians, we want each second to advance the angle by π.
Try increasing the scale until the positive interval lines up with 1s:
At scale = π, one second of time becomes π radians. So after 1s, the wave reaches π and crosses from positive to negative. After 2s, it reaches 2π and crosses back again.
If you want the same kind of repeating up-and-down change, but with linear movement instead of smooth easing at the top and bottom, you can use a triangle wave like we did for the DVD bounce in Chapter 4:
const period = 2; // seconds for one full up/down cycle
const t = (time % period) / period; // 0..1
const y = t < 0.5 ? t * 2 : 2 - t * 2;Here's an assortment of practical applications of sine waves for games.
Oscillating movement (rotation)
Oscillating movement (position)
Oscillating opacity
Blinking
The only change from the previous example is the inclusion of Math.round.
const state = { time: 0 };function update(dt: number) {state.time += dt;}function draw(ctx: CanvasRenderingContext2D) {const { width, height } = bounds;ctx.clearRect(0, 0, width, height);ctx.fillStyle = "deepskyblue";const size = 50;const speed = 3;const wobble = Math.sin(state.time * speed);ctx.save();ctx.translate(100, 100);ctx.rotate(wobble);ctx.fillRect(-size / 2, -size / 2, size, size);ctx.restore();}
Projectiles
To get the angle between two points, we can use a math trick that involves Math.atan2(y, x), which only takes in one point for its arguments. The trick is to translate so that one of the points is at (0, 0), then make the second point's coordinates relative to that new origin. From there, atan2 will give you the angle from the origin point to the other point (in radians).
To pull out the x and y components from this angle, use the cosine and sine functions.
const = 100;
const = .() * ; // x velocity
const = .() * ; // y velocitySlightly simpler option if you don't need the angle
If all you need are the x and y velocities without first knowing the angle, you can avoid some trigonometry.
const = { : 10, : 20 };
const = { : 30, : 40 };
const = {
: . - .,
: . - .,
};
const = .(
., // targetX - originX
., // targetY - originY
);
const = 100;
const = (. / ) * ;
const = (. / ) * ;From here you can add gravity, explosions, or anything else to the projectiles.
Utility functions
// how long is the line from the origin (0, 0) to the point at (x, y)
function (: number, : number) {
return .(, );
}
// how long is the line between (x1, y1) and (x2, y2)
function (: number, : number, : number, : number) {
return .( - , - );
}
// what is the angle from the origin (0, 0) to the point at (x, y)
function (: number, : number) {
return .(, ); // in radians
}
// what is the angle from (x1, y1) to (x2, y2)
function (: number, : number, : number, : number) {
return .( - , - ); // in radians
}
// if you need to convert the radians from `angle(x, y)` to degrees:
function (: number) {
return ( * 180) / .;
}
// or convert back from degrees to radians:
function (: number) {
return ( * .) / 180;
}
// pull out the x and y components of the unit vector pointing
// in the direction of the `angle` in radians
function (: number) {
return {
: .(),
: .(),
};
}Collisions & intersections
Detecting when in-game objects overlap is essential not just for things like didProjectileHitPlayer, but also to keep the player from falling through platforms or even just to make it possible to click on stuff.
This section is less a guide and more a collection of common code snippets for collision detection algorithms between various shapes.
The functions in the next sections will tell you when two objects collide, but you'll need to compare the current and past collision state if you want to act on the instant the collision occurred:
const = {
: false, // stale state from previous frame
: { : 20, : 50 },
: { : 30, : 40, : 50 },
};
function (: number) {
const = (., .);
const = && !.;
if () {
("whoosh");
();
}
. = ;
}Point in shape
Code snippets for point-in-shape checks
type = { : number; : number };
type = [, ];
type = { : number; : number; : number };
type = { : number; : number; : number; : number }; // aka an axis-aligned bounding box (AABB)
type = { : number; : number; : number; : number; : number }; // aka an oriented bounding box (OBB)
type = [];
function (: , : ): boolean {
const = 0.000001; // avoid floating point inaccuracy near the segment
if (.(([0], [1], )) > ) {
return false;
}
const [, ] = ;
return (
.(., .) - . <= &&
. - .(., .) <= &&
.(., .) - . <= &&
. - .(., .) <=
);
}
function (: , : ): boolean {
const { } = ;
const = . - .; // (x – h)
const = . - .; // (y – k)
return ** 2 + ** 2 <= ** 2;
}
function (: , : ): boolean {
return (
. >= . &&
. <= . + . &&
. >= . &&
. <= . + .
);
}
function (: , : ): boolean {
const = (, );
return (, { : -. / 2, : -. / 2, : ., : . });
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
// helpers
function (: , : , : ): number {
return (. - .) * (. - .) - (. - .) * (. - .);
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : ): {
const = ();
const = (, , -.);
return {
: . - .,
: . - .,
};
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}Segment collisions
Code snippets for segment collision checks
type = { : number; : number };
type = [, ];
type = { : number; : number; : number };
type = { : number; : number; : number; : number }; // aka an axis-aligned bounding box (AABB)
type = { : number; : number; : number; : number; : number }; // aka an oriented bounding box (OBB)
type = [];
function (: , : ): boolean {
const [, ] = ;
const [, ] = ;
const = (, , );
const = (, , );
const = (, , );
const = (, , );
if ((, ) && (, )) {
return true;
}
return (
(, ) ||
(, ) ||
(, ) ||
(, )
);
}
function (: , : ): boolean {
return (, ) <= . ** 2;
}
function (: , : ): boolean {
const [, ] = ;
if ((, ) || (, )) {
return true;
}
return ().(() => (, ));
}
function (: , : ): boolean {
const : = [
([0], ),
([1], ),
];
const = { : -. / 2, : -. / 2, : ., : . };
return (, );
}
function (
: CanvasRenderingContext2D,
: ,
: ,
): boolean {
const [, ] = ;
if ((, , ) || (, , )) {
return true;
}
return ().(() => (, ));
}
// helpers
function (: number, : number, : number): number {
return .(.(, ), );
}
function (: , : ): number {
const = . - .;
const = . - .;
return * + * ;
}
function (: , [, ]: ): {
const = { : . - ., : . - . };
const = . * . + . * .;
if ( === 0) {
return ;
}
const = ((. - .) * . + (. - .) * .) / ;
const = (, 0, 1);
return {
: . + * .,
: . + * .,
};
}
function (: , : ): number {
return (, (, ));
}
function (: , : ): boolean {
return (
. >= . &&
. <= . + . &&
. >= . &&
. <= . + .
);
}
function (: , : , : ): number {
return (. - .) * (. - .) - (. - .) * (. - .);
}
function (: , [, ]: ): boolean {
const = 0.000001; // epsilon to avoid floating point inaccuracy
if (.((, , )) > ) {
return false;
}
return (
. >= .(., .) - &&
. <= .(., .) + &&
. >= .(., .) - &&
. <= .(., .) +
);
}
function (: number, : number): boolean {
return ( < 0 && > 0) || ( > 0 && < 0);
}
function (: ): [] {
const = { : ., : . };
const = { : . + ., : . };
const = { : . + ., : . + . };
const = { : ., : . + . };
return [
[, ],
[, ],
[, ],
[, ],
];
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : ): {
const = ();
const = (, , -.);
return {
: . - .,
: . - .,
};
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}Ray collisions
Code snippets for ray intersection checks
For rays it's often useful to know where the collision occurred, too--not just that there was a collision. So all the rayIntersects* functions return a number, not a boolean, with the distance along the ray to the detected collision (or Infinity if no collision).
type = { : number; : number };
type = [, ];
type = { : ; : };
type = { : number; : number; : number };
type = { : number; : number; : number; : number }; // aka an axis-aligned bounding box (AABB)
type = { : number; : number; : number; : number; : number }; // aka an oriented bounding box (OBB)
type = [];
function (: , : ): number {
const [, ] = ;
const = { : . - ., : . - . };
const = { : . - .., : . - .. };
const = (., );
const = 0.000001;
if (.() < ) {
if (.((, .)) > ) {
return ;
}
const = (., .);
const = (, .) / ;
const =
({ : . - .., : . - .. }, .) / ;
if ( < 0 && < 0) {
return ;
}
return .(0, .(, ));
}
const = (, ) / ;
const = (, .) / ;
return >= 0 && >= 0 && <= 1 ? : ;
}
function (: , : ): number {
const = { : .. - ., : .. - . };
const = (., .);
const = 2 * (, .);
const = (, ) - . ** 2;
const = * - 4 * * ;
if ( < 0) {
return ;
}
const = .();
const = (- - ) / (2 * );
const = (- + ) / (2 * );
if ( >= 0) return ;
if ( >= 0) return 0;
return ;
}
function (: , : ): number {
let = 0;
let = ;
const = [
{ : .., : .., : ., : . + . },
{ : .., : .., : ., : . + . },
];
for (const of ) {
if (. === 0) {
if (. < . || . > .) {
return ;
}
continue;
}
const = (. - .) / .;
const = (. - .) / .;
= .(, .(, ));
= .(, .(, ));
if ( > ) {
return ;
}
}
return ;
}
function (: , : ): number {
const = ();
const = (., );
const = (
{ : . + .., : . + .. },
,
-.,
);
const = { : . - ., : . - . };
return (
{ : , : },
{ : -. / 2, : -. / 2, : ., : . },
);
}
function (: CanvasRenderingContext2D, : , : ): number {
if ((, ., )) {
return 0;
}
return .(...().(() => (, )));
}
// helpers
function (: , : ): number {
return . * . + . * .;
}
function (: , : ): number {
return . * . - . * .;
}
function (: , : number): {
return {
: .. + .. * ,
: .. + .. * ,
};
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : ): {
const = ();
const = (, , -.);
return {
: . - .,
: . - .,
};
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}Circle collisions
Code snippets for circle collision checks
type = { : number; : number };
type = [, ];
type = { : number; : number; : number };
type = { : number; : number; : number; : number };
type = { : number; : number; : number; : number; : number };
type = [];
function (: , : ): boolean {
return (, ) <= (. + .) ** 2;
}
// circleOverlapsSegment: see segmentOverlapsCircle
function (: , : ): boolean {
return (., ., ., ., ., ., .);
}
function (: , : ): boolean {
const = (, );
return (
.,
.,
.,
-. / 2,
-. / 2,
.,
.,
);
}
function (
: CanvasRenderingContext2D,
: ,
: ,
): boolean {
if ((, , )) {
return true;
}
return ().(
() => (, ) <= . ** 2,
);
}
// helpers
function (: number, : number, : number): number {
return .(.(, ), );
}
function (: , : ): number {
const = . - .;
const = . - .;
return * + * ;
}
function (: , : ): number {
return (, (, ));
}
function (
: number,
: number,
: number,
: number,
: number,
: number,
: number,
): boolean {
const = (, , + );
const = (, , + );
const = - ;
const = - ;
return * + * <= ** 2;
}
function (: , [, ]: ): {
const = { : . - ., : . - . };
const = . * . + . * .;
if ( === 0) {
return ;
}
const = ((. - .) * . + (. - .) * .) / ;
const = (, 0, 1);
return {
: . + * .,
: . + * .,
};
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : ): {
const = ();
const = (, , -.);
return {
: . - .,
: . - .,
};
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}Rect collisions (AABB)
When a rectangle is in the same orientation as the game canvas itself it's considered to be "axis-aligned". Collision detection is simpler with axis-aligned rectangles than rotated ones. A common abbreviation you'll see in many game engines for this is AABB (axis-aligned bounding box). The alternative, for rotated rectangles, is an "oriented bounding box", which we'll show in the next section.
Code snippets for rect collision checks
type = { : number; : number };
type = [, ];
type = { : number; : number; : number; : number };
type = { : number; : number; : number; : number; : number };
type = [];
function (: , : ): boolean {
return (
. <= . + . &&
. + . >= . && // X-axis overlap
. <= . + . &&
. + . >= . // Y-axis overlap
);
}
// rectOverlapsSegment: see segmentOverlapsRect
// rectOverlapsCircle: see circleOverlapsRect
function (: , : ): boolean {
return ((), ());
}
function (: CanvasRenderingContext2D, : , : ): boolean {
const = ();
return (
.(() => (, , )) ||
.(() => (, )) ||
().(() =>
().(() => (, )),
)
);
}
// helpers
function (: , : ): boolean {
return (
. >= . &&
. <= . + . &&
. >= . &&
. <= . + .
);
}
function (: ): [] {
return [
{ : ., : . },
{ : . + ., : . },
{ : . + ., : . + . },
{ : ., : . + . },
];
}
function (: ): [] {
return (());
}
function (: ): [] {
const = ();
const = . / 2;
const = . / 2;
const = [
{ : -, : - },
{ : , : - },
{ : , : },
{ : -, : },
];
return .(() =>
({ : . + ., : . + . }, , .),
);
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: , : ): boolean {
const = [...(), ...()];
for (const of ) {
if (!((, ), (, ))) {
return false;
}
}
return true;
}
function (: ): [] {
return ().(([, ]) => (, ));
}
function (: , : ): {
return { : . - ., : . - . };
}
function (: [], : ): { : number; : number } {
let = ;
let = -;
for (const of ) {
const = . * . + . * .;
= .(, );
= .(, );
}
return { , };
}
function (
: { : number; : number },
: { : number; : number },
): boolean {
return . <= . && . >= .;
}
function (: , : ): boolean {
const [, ] = ;
const [, ] = ;
const = (, , );
const = (, , );
const = (, , );
const = (, , );
if ((, ) && (, )) {
return true;
}
return (
(, ) ||
(, ) ||
(, ) ||
(, )
);
}
function (: , [, ]: ): boolean {
const = 0.000001;
if (.((, , )) > ) {
return false;
}
return (
.(., .) - . <= &&
. - .(., .) <= &&
.(., .) - . <= &&
. - .(., .) <=
);
}
function (: number, : number): boolean {
return ( < 0 && > 0) || ( > 0 && < 0);
}
function (: , : , : ): number {
return (. - .) * (. - .) - (. - .) * (. - .);
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}Rotated rect collisions (OBB)
Code snippets for rotated rect collision checks
type = { : number; : number };
type = [, ];
type = { : number; : number; : number; : number; : number };
type = [];
// rotatedRectOverlapsSegment: see segmentOverlapsRotatedRect
// rotatedRectOverlapsCircle: see circleOverlapsRotatedRect
// rotatedRectOverlapsRect: see rectOverlapsRotatedRect
function (: , : ): boolean {
return ((), ());
}
function (
: CanvasRenderingContext2D,
: ,
: ,
): boolean {
const = ();
return (
.(() => (, , )) ||
.(() => (, )) ||
().(() =>
().(() => (, )),
)
);
}
// helpers
function (: , : ): boolean {
const = (, );
return (
. >= -. / 2 &&
. <= . / 2 &&
. >= -. / 2 &&
. <= . / 2
);
}
function (: ): [] {
const = ();
const = . / 2;
const = . / 2;
const = [
{ : -, : - },
{ : , : - },
{ : , : },
{ : -, : },
];
return .(() =>
({ : . + ., : . + . }, , .),
);
}
function (: ): {
return { : . + . / 2, : . + . / 2 };
}
function (: , : ): {
const = ();
const = (, , -.);
return {
: . - .,
: . - .,
};
}
function (: , : , : number): {
const = .();
const = .();
const = . - .;
const = . - .;
return {
: . + * - * ,
: . + * + * ,
};
}
function (: , : ): boolean {
const = [...(), ...()];
for (const of ) {
if (!((, ), (, ))) {
return false;
}
}
return true;
}
function (: ): [] {
return ().(([, ]) => (, ));
}
function (: , : ): {
return { : . - ., : . - . };
}
function (: [], : ): { : number; : number } {
let = ;
let = -;
for (const of ) {
const = . * . + . * .;
= .(, );
= .(, );
}
return { , };
}
function (
: { : number; : number },
: { : number; : number },
): boolean {
return . <= . && . >= .;
}
function (: , : ): boolean {
const [, ] = ;
const [, ] = ;
const = (, , );
const = (, , );
const = (, , );
const = (, , );
if ((, ) && (, )) {
return true;
}
return (
(, ) ||
(, ) ||
(, ) ||
(, )
);
}
function (: , [, ]: ): boolean {
const = 0.000001;
if (.((, , )) > ) {
return false;
}
return (
.(., .) - . <= &&
. - .(., .) <= &&
.(., .) - . <= &&
. - .(., .) <=
);
}
function (: number, : number): boolean {
return ( < 0 && > 0) || ( > 0 && < 0);
}
function (: , : , : ): number {
return (. - .) * (. - .) - (. - .) * (. - .);
}
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}Polygon collisions
Polygon collisions are expensive to compute and often not actually necessary. Instead, see if you can get away with simplifying your hit boxes to simpler shapes (rectangles, points, and circles).
Code snippets for polygon collision checks
type = { : number; : number };
type = [, ];
type = [];
// polygonOverlapsSegment: see segmentOverlapsPolygon
// polygonOverlapsCircle: see circleOverlapsPolygon
// polygonOverlapsRect: see rectOverlapsPolygon
// polygonOverlapsRotatedRect: see rotatedRectOverlapsPolygon
function (: CanvasRenderingContext2D, : , : ): boolean {
return (
.(() => (, , )) ||
.(() => (, , )) ||
().(() => ().(() => (, )))
);
}
// helpers
function (: CanvasRenderingContext2D, : , : ): boolean {
return .((), ., .);
}
function (: ): Path2D {
const = new ();
const [, ...] = ;
.(., .);
for (const of ) {
.(., .);
}
.();
return ;
}
function (: ): [] {
return .((, ) => [, [( + 1) % .]]);
}
function (: , : ): boolean {
const [, ] = ;
const [, ] = ;
const = (, , );
const = (, , );
const = (, , );
const = (, , );
if ((, ) && (, )) {
return true;
}
return (
(, ) ||
(, ) ||
(, ) ||
(, )
);
}
function (: , [, ]: ): boolean {
const = 0.000001;
if (.((, , )) > ) {
return false;
}
return (
.(., .) - . <= &&
. - .(., .) <= &&
.(., .) - . <= &&
. - .(., .) <=
);
}
function (: number, : number): boolean {
return ( < 0 && > 0) || ( > 0 && < 0);
}
function (: , : , : ): number {
return (. - .) * (. - .) - (. - .) * (. - .);
}Distance checks
Code snippets for distance checks
type = { : number; : number };
type = [, ];
function (: , : ): number {
return .((, ));
}
// runs faster than Math.sqrt, so if you're doing a ton of distance
// calculations in a hot loop, use squared values
function (: , : ): number {
const = . - .;
const = . - .;
return * + * ;
}
function (: , : ): number {
return (, (, ));
}
// returns where segment a first hits segment b (0..1), or Infinity if they don't overlap
function (: , : ): number {
const [, ] = ;
const [, ] = ;
const = . - .;
const = . - .;
const = . - .;
const = . - .;
const = * - * ;
if ( === 0) {
return ;
}
const = ((. - .) * - (. - .) * ) / ;
const = ((. - .) * - (. - .) * ) / ;
if ( < 0 || > 1 || < 0 || > 1) {
return ;
}
return ;
}
// helpers
function (: number, : number, : number): number {
return .(.(, ), );
}
function (: , [, ]: ): {
const = { : . - ., : . - . };
const = . * . + . * .;
if ( === 0) {
return ;
}
const = ((. - .) * . + (. - .) * .) / ;
const = (, 0, 1);
return {
: . + * .,
: . + * .,
};
}Dot product
(how much are we pointing towards or away from a thing (todo))
calculate angle from player to cursor
project (todo):
- moving player
- diagonal movement isn't faster
- launching projectiles toward the cursor
- with gravity
- and ground collisions
- (then reuse this project in the juice section by adding anticipation animation, explosion on collision, haptics, sfx, screen shake, etc.)
const state = {player: {x: 50,y: 100,size: 20,},cursor: {x: 100,y: 150,},keysDown: new Set<string>(),bullets: Array.from({ length: 30 }, () => ({ isAlive: false, x: 0, y: 0, vx: 0, vy: 0 })),nextBulletIndex: 0,timeSinceBullet: 0,};addEventListener("pointermove",(event) => {const { left, top } = bounds;state.cursor.x = event.x - left;state.cursor.y = event.y - top;},{ signal },);addEventListener("keydown",(event) => {state.keysDown.add(event.key);},{ signal },);addEventListener("keyup",(event) => {state.keysDown.delete(event.key);},{ signal },);function shoot() {const speed = 160;const yDiff = state.cursor.y - state.player.y;const xDiff = state.cursor.x - state.player.x;const angle = Math.atan2(yDiff, xDiff);const bullet = state.bullets[state.nextBulletIndex];bullet.x = state.player.x;bullet.y = state.player.y;bullet.isAlive = true;bullet.vx = Math.cos(angle) * speed;bullet.vy = Math.sin(angle) * speed;state.nextBulletIndex = (state.nextBulletIndex + 1) % state.bullets.length;}function updateBullets(dt: number) {for (const bullet of state.bullets) {bullet.x += bullet.vx * dt;bullet.y += bullet.vy * dt;}}function fixedUpdate(dt: number) {state.timeSinceBullet += dt;while (state.timeSinceBullet > 0.2) {shoot();state.timeSinceBullet -= 0.2;}updateBullets(dt);movePlayer(dt);}function movePlayer(dt: number) {const speed = 80;let vx = 0;let vy = 0;if (state.keysDown.has("w")) vy -= speed * dt;if (state.keysDown.has("a")) vx -= speed * dt;if (state.keysDown.has("s")) vy += speed * dt;if (state.keysDown.has("d")) vx += speed * dt;const hypot = Math.hypot(vx, vy) || 1;state.player.x += (vx * 1) / hypot;state.player.y += (vy * 1) / hypot;}function draw(ctx: CanvasRenderingContext2D) {const { width, height } = bounds;ctx.clearRect(0, 0, width, height);ctx.fillStyle = "deepskyblue";ctx.fillRect(state.player.x - state.player.size / 2,state.player.y - state.player.size / 2,state.player.size,state.player.size,);for (const bullet of state.bullets) {if (!bullet.isAlive) continue;ctx.fillStyle = "red";ctx.beginPath();ctx.arc(bullet.x, bullet.y, 2, 0, Math.PI * 2);ctx.fill();}}
See also
- How to Turn a Few Numbers into Worlds [YouTube] — fractal Perlin noise explained, by The Taylor Series
- Making Randomness [YouTube] — pseudorandom number generators explained, by Jorge Rodriguez
- Math for Game Devs [YouTube] — four-part lecture series by Freya Holmér (+ Part 2, Part 3, Part 4)
- Math Visualizations — interactive visual math references by Freya Holmér
- The Ultimate Guide to Cross Product and Dot Product — a post by Moonvane on the Roblox developer forums
- Trigonometry [YouTube] — from Sebastian Lague's Introduction to Game Development series
- What Kind of Math Should Game Developers Know? [YouTube] — overview of common math concepts for game development, by SimonDev