Game Development 101: (incomplete) a. Basics of Game Development b. Designing your Video Game c. Math in Video Games d. AI Decision-making & Pathfinding e. Asset Creation & Game Art f. Physics & Collision Detection g. Shaders & VFX in Games h. Multiplayer & Networking in Games |
Reading
The purpose of this text is to familiarize you about the math in games; you should not stress on this too much, as you will eventually grasp the concepts as you progress into making stuff. Most of the things discussed here are wholly or partially well-implemented in game engines such as Unity, Godot or Unreal.
Game Math
Math lies in the depths of all computations. Video games are no exception. In games, the most common is the vector mathematics; we treat all objects as vectors in space & by manipulating vectors allows us to compute anything that can be represented as a vector.
In previous articles, you have noticed that when we moved an object, we just added a value to the position vector of the object & the object had moved to the new coordinate values.
2D & 3D Space
Position of any object on a 2D plain can be represented by how much it is away from origin in both x & y axes. In 3D, we need another axis to represent an object’s position. It is the z-axis. Thus a coordinate system established by x, y, z axes make the space; in which we can put our game objects. Everything that occurs in this space must be a vector.
What are Vectors?
Vectors are used to represent quantities that have some direction in space & some magnitude in that direction. Just like a scalar number is used to represent a quantity in 1D space, a vector is used to represent a quantity in 2D or 3D space.
A vector can be represented as:
vector A = direction A * magnitude A
If we simply need direction of a vector, we can divide the vector by its own magnitude. The resulting vector will have magnitude of 1. This is called a normalized vector. It is important in games since we often need the direction.
Vectors can be Added or Subtracted
Two vectors can be added. If we have two scalar values 5 & 10; and we wish to add them, we will get 15. Similarly if we have two vectors vec2(2, 3) and vec2(6, 5) and we wish to add them, we add the x & y components of these vectors; thus we will get vec2(2+6, 3+5) or vec2(8, 8). Vector subtraction is similar.
Vectors can be Multiplied
Vector multiplication with scalar is simple: it just scales the magnitude by some amount. However, between two vectors, multiplication can be any of the two things: the dot product or the cross product.
- Dot Product: Dot product between two vectors A & B returns the projection of A on B (a scalar value). Suppose A & B are perpendicular; their dot product will be 0 since A is not at all projecting on B. But if both vectors are parallel, their dot product will be maximum as if both vectors are multiplied like the standard scalar multiplication.
- Cross Product: Cross product is defined for 3D space. Cross product between two vectors A & B returns the area between the two vectors. The direction of the area is perpendicular to both the given vectors A & B. Thus if A(1, 0, 0) is cross multiplied with B(0, 0, 1); the result will be vec3(0, 1, 0). Cross product returns the vector unlike dot product which returns the scalar.
Interesting Uses in Games
Make the Enemy Chase the Player: Player’s position is represented by A & enemy’s own position is represented by B. If we subtract B from A, we will get a vector pointing from B to A. Now we will normalize this vector and add it to the enemy’s position to make it move towards the direction of this vector every frame.
void chase_player(player_position, enemy_position){
vector_to_player = normalize(player_position - enemy_position)
enemy_position = enemy_position + vector_to_player * speed
}
Checking if Player is in Enemy’s View: In many games, we often need to check if enemy is looking at player (thus enemy can shoot if player is in view). A simple math-based approach is to use dot product between normalized enemy’s local direction vector & normalized enemy_to_player vector.
bool is_looking_at(player_position) {
vector_to_player = normalize(player_position - enemy_position);
return dot(vector_to_player, -local_z_vector) > cos(field_of_view / 2.0);
}
The local_z_vector is the vector used to represent an object’s front direction (i.e. where the object is looking at or pointing to). In game engines such as Godot, it is already provided. But if you working from scratch, you’ll need some calculations to calculate it based on rotation of your object.
Making an Airplane Physics: For simple arcade-like airplane physics, we can calculate thrust, drag, lift & weight vectors & apply them to the center of gravity of our airplane in a game. The resulting simulation can be acceptable for simple airplane games.
Matrices & Vector Transformations
Matrix is a set of vectors. Its important property is; when multiplied by a vector, it can transform it to a new vector. A very simple example is the stretching of image by “stretching” its coordinates by multiplying it with a matrix.
Matrices can be seen as functions that transform vectors: f(V) = M x V
Where V is input vector, and f(V) is the output vector; M is the matrix.
Why We Use Matrix?
We typically use matrix transformations to switch between different spaces. For example, our screen space has origin at top-left of screen. Any vector in screen space is defined in terms of its displacement from screen’s origin. Suppose we hit a bullet at a particular point on the screen & we want to propagate the bullet in the game world; we would have to multiply the screen space vector to some matrix that would transform the screen space position to the world space position (thus, bullet’s position would then be defined in terms of world origin rather than screen origin).
The transformation matrix that converts coordinates from screen space to world space is commonly known as the “inverse projection matrix” or “view projection matrix.” This matrix is often used in computer graphics to transform 2D screen coordinates (in normalized device coordinates, NDC) back to 3D world coordinates.
There are many other such matrices, which are used to convert from model space to world space, world space to model space, and so on. But you will discover them later eventually.
Recall (obtaining local-z-vector; the front-pointing vector): Remember that we talked about local z vector above; to obtain the local z-axis vector in a game, you need to multiply the world-space z-axis vector by the inverse of the rotation matrix that represents the orientation of the object or camera. This operation transforms the global z-axis into the local coordinate system of the object.
Some Matrix Operations & Transformations
THIS SECTION IS UNFINISHED
Math Behind Basic Collision Detection
The basic approach is to check for intersections between two rectangles in case of 2D and between bounding boxes for 3D. However, it is an inferior approach. Most physics libraries (such as Bullet3D) do much more than that. However, I will not discuss details of collision detection here; but an example of 2D rectangle-based collision is sufficient for a beginner’s understanding.
// Define a struct for a Rectangle
struct Rect {
float x; // X-coordinate of the top-left corner
float y; // Y-coordinate of the top-left corner
float width; // Width of the rectangle
float height; // Height of the rectangle
};
bool is_intersecting(rect1, rect2){
// Check if rect1 is to the left of rect2
if (rect1.x + rect1.width < rect2.x or rect2.x + rect2.width < rect1.x)
return false;
// Check if rect1 is above rect2
if (rect1.y + rect1.height < rect2.y or rect2.y + rect2.height < rect1.y)
return false;
// If the above conditions are not met, the rectangles are intersecting
return true;
}
Math Behind Shading & Lighting
3D game objects must interact with light. There are many shading models for this, but here I’ll discuss simple Lambert reflection model & Blinn-Phong model for specular highlights.
Lambertian Reflection: The Lambertian reflection model is a simple model for simulating diffuse reflection of light from a surface. It assumes that light is reflected equally in all directions, regardless of the viewer’s perspective. The Lambertian model doesn’t account for specular highlights.
vec3 lambertianReflection(vec3 lightDirection, vec3 surfaceNormal, vec3 lightColor, vec3 surfaceColor) {
// Calculate the cosine of the angle between the light direction and surface normal
float cosTheta = max(0.0, dot(lightDirection, surfaceNormal));
// Ensure cosTheta is non-negative
cosTheta = max(0.0, cosTheta);
// Calculate Lambertian reflectance
vec3 lambertianReflection = (lightColor / pi) * surfaceColor * cosTheta;
return lambertianReflection;
}
Blinn-Phong Specular: Blinn-Phong model can be used for specular highlights, to add shininess.
vec3 blinnPhongSpecularReflection(vec3 lightDirection, vec3 surfaceNormal, vec3 viewDirection, vec3 lightColor, vec3 surfaceColor, float shininess) {
// Calculate the halfway vector between view direction and light direction
vec3 halfwayDir = normalize(viewDirection + lightDirection);
// Calculate the cosine of the angle between the surface normal and the halfway vector
float cosAlpha = max(0.0, dot(surfaceNormal, halfwayDir));
// Calculate the specular reflectance using the Blinn-Phong model
vec3 specularReflection = (lightColor / pi) * surfaceColor * pow(cosAlpha, shininess);
return specularReflection;
}
Limits & Derivatives
Limits
In mathematics, a limit is a fundamental concept that describes the behavior of a function as its input approaches a certain value.
Derivatives
The derivative of a function represents the rate at which the function is changing at any given point. It measures the slope of the tangent line to the graph of the function at a specific point. In other words, it provides information about how the function behaves locally.
Mathematically, it is: d(x) = (f(x + h) – f(x)) / h
where x is the function input, h is a very small value whose value approaches 0. This makes f(x + h) slightly different from f(x), thus by calculating the difference between two values of a function per their difference h, we can find the slope of the function at that point (or the degree of how much this function is changing at that point).
Derivatives Uses in Games
This property is useful for calculating the slope of the terrain surface; which can later be used for reflecting light properly in correct direction & for simulating surface erosion by guiding the rain drops to the correct direction.
Some More Math
Representing a Straight Line
Equation of straight line: y = mx + b
where y is the y-axis value, x is x-axis value, m is the slope (or call it y/x ratio) and b is the offset or displacement from the origin.
We can draw straight lines by making a function that prints different colors for all pixels (x, y), for which the equation is true.
Polygons & Geometry
Geometry is represented in the form of polygons. Polygons are shapes composed of vertices/points in space and edges connecting those points.
Calculations for simple polygons (convex shape) are often simple; but for those that have cavities (concave shape) are difficult.
Math Functions Cheat Sheet
ID | Formula | Description |
---|---|---|
1 | abs(x) | Absolute value of x |
2 | sign(x) | Sign of x (-1, 0, 1) |
3 | floor(x) | Largest integer not greater than x |
4 | ceil(x) | Smallest integer not less than x |
5 | round(x) | Nearest integer to x |
6 | trunc(x) | Integer part of x |
7 | fract(x) | Fractional part of x |
8 | mod(x, y) | Remainder of x/y |
9 | min(x, y) | Minimum of x and y |
10 | max(x, y) | Maximum of x and y |
11 | clamp(x, minVal, maxVal) | Clamps x to the range [minVal, maxVal] |
12 | mix(x, y, a) | Linear interpolation between x and y with factor a |
13 | step(edge, x) | 0.0 if x < edge, 1.0 otherwise |
14 | smoothstep(edge0, edge1, x) | Hermite interpolation between 0 and 1 based on x |
15 | length(vec) | Length of the vector |
16 | distance(vec1, vec2) | Distance between two points |
17 | normalize(vec) | Normalize the vector |
18 | dot(vec1, vec2) | Dot product of two vectors |
19 | cross(vec1, vec2) | Cross product of two vectors |
20 | reflect(I, N) | Reflection of incident vector I across normal N |
21 | refract(I, N, eta) | Refraction of incident vector I across normal N with eta |
22 | saturate(x) | Clamps x to the range [0, 1] |
23 | sin(x) | Sine of x |
24 | cos(x) | Cosine of x |
25 | tan(x) | Tangent of x |
26 | asin(x) | Arcsine of x |
27 | acos(x) | Arccosine of x |
28 | atan(x) | Arctangent of x |
29 | atan(y, x) | Arctangent of y/x |
30 | exp(x) | Exponential function |
31 | log(x) | Natural logarithm of x |
32 | exp2(x) | 2^x |
33 | log2(x) | Base-2 logarithm of x |
34 | pow(x, y) | x raised to the power y |
35 | sqrt(x) | Square root of x |
36 | inversesqrt(x) | Inverse square root of x |
37 | abs(vec) | Component-wise absolute value of a vector |
38 | sign(vec) | Component-wise sign of a vector |
39 | floor(vec) | Component-wise floor of a vector |
40 | ceil(vec) | Component-wise ceil of a vector |
41 | round(vec) | Component-wise round of a vector |
42 | trunc(vec) | Component-wise truncation of a vector |
43 | fract(vec) | Component-wise fractional part of a vector |
44 | min(vec1, vec2) | Component-wise minimum of two vectors |
45 | max(vec1, vec2) | Component-wise maximum of two vectors |
46 | clamp(vec, minVec, maxVec) | Component-wise clamp of a vector to a range |
47 | mix(vec1, vec2, a) | Component-wise linear interpolation between two vectors |
48 | step(edge, vec) | Component-wise step function |
49 | smoothstep(edge0, edge1, vec) | Component-wise smoothstep function |
50 | length(mat) | Frobenius norm of a matrix |
51 | transpose(mat) | Transpose of a matrix |
52 | inverse(mat) | Inverse of a matrix |
More Reading
A very good book on game math: https://gamemath.com/book/intro.html
Game Development 101: (incomplete) a. Basics of Game Development b. Designing your Video Game c. Math in Video Games d. AI Decision-making & Pathfinding e. Asset Creation & Game Art f. Physics & Collision Detection g. Shaders & VFX in Games h. Multiplayer & Networking in Games |