Mathematics and video games - Nephasto


A series of articles on the essential mathematics for developing video games.


Introduction.

Mathematics is a fundamental part of video game development. Whether you want to focus on technology or gameplay, you will need a minimum mathematical background. With this article I will try to broaden that base (mine included). All code will be C#.

In order to avoid confusion and misunderstandings, which is a bad thing in the precise mathematical world, I will use these simple typographical rules when naming the different entities that populate these worlds:

  • Scalars (numbers, constants, constants, magnitudes, variables, etc) will be italicized and lowercase, e.g.: x, y, z, w.
  • Vectors, matrices and quaternions will use bold and uppercase, such as: V, P.

These are the sections I will discuss:

  • Vectors.
  • Matrices.
  • Quaternions.
  • Basic geometry.
  • ???

Vectors.

Vectors are the mathematical basis of any video game, whether 3D or 2D. We use them to represent positions, directions, colors, geometries, etc. They are a sequence (or tuple, technically speaking) of two or more real numbers (integers, fractions, irrations and trancendents) that can represent a magnitude and a direction.

Each of these numbers can be called components of the vector. A vector of n components, being n > 1, has this form:

V = (v1, v2, …, vn).

Each v1, v2, etc are the components of the vector V. If the vector represents some spatial magnitude and/or direction (something very common in video games), we can also call them dimensions.

Thus, if a vector has two components, we can say that it is a bidimensional vector.

For example, the vector V = (4, 4) is two-dimensional. Its first component, or dimension, is worth 4. The same as its second component.

We can represent it on a two-dimensional plane, or axes, in which we measure the first component on the horizontal axis (commonly called the x axis) and the second on the vertical axis (called the y axis).

 

If we add another component, say of value 4, the result will be a three-dimensional vector of this form P = (4, 4, 4). The third component is usually measured on the z axis.

 

Don’t be afraid of the dimensions of a vector because in videogames it is normal to deal with two or three at most. Some vectors can have 4 dimensions, but they never refer to a four-dimensional space.

The simplest and simplest way in which we could encode a two-dimensional vector could be:

1
2
3
4
 public struct Vector2
 {
   public float x, y;
 }

 

And a three-dimensional one:

1
2
3
4
 public struct Vector3
 {
   public float x, y, z;
 }

 

The precision of a float in C# (4 bytes) is 7 digits, enough to cover the needs of most video games. It can be replaced by double (8 bytes) which increases the precision between 15 and 16 digits.

To make it easier to work with them, we will add this basic constructor:

1
2
3
4
5
 public Vector2(float x, float y)
 {
   this.x = x;
   this.y = y;
 }

 

To simplify and simplify the graphics and the code, we are going to use two-dimensional vectors. To operate with three-dimensional vectors, you only need to add the missing dimension.

Basic operations with vectors.

We already know what a vector is, now we want it to be useful, we want to know how to operate with them. For that we have to know first the basic operations that we can do with them and that will be the pillars of other more complex ones that we will deal with later.

These operations are: opposite, addition, subtraction, multiplication by scalar and division by scalar.

The opposite of a vector V, represented as -V, is the result of changing the sign of the components of V. If, for example, V = (4, -4), then -V = (-4, 4). If the component is positive, it becomes negative and vice versa:

 

As you can see, the result of making the opposite of a vector, is another vector facing in the opposite direction.

If in a video game, a spaceship moves because a force (a vector) is applied to it and we want it to go in the opposite direction, we just have to apply the opposite of that force.

1
2
3
4
5
6
7
 public static Vector2 operator -(Vector2 v)
 {
   v.y = -v.x;
   v.y = -v.y;
 
   return v;
 }

 

The sum of two vectors is also a very simple calculation. The result is another vector whose components are the result of adding the components of the two vectors to be added.

If we want to add the vectors P and Q, the result R = (p1 + q1, p2 + q2). If P = (3, 1) and Q = (-1, 2), then the result of adding the two is: R = (3 + -1, 1 + 2) = (2, 3)

  }

In the spacecraft in our game, if an asteroid hits the spacecraft, the result of the engine thrust and the impact would be the sum of both forces.

1
2
3
4
5
6
7
 public static Vector2 operator +(Vector2 v1, Vector2 v2)
 {
   v1.x += v2.x;
   v1.y += v2.y;
 
   return v1;
 }

 

The subtraction operation can be performed using addition with the opposite. P - Q is the same as P + (-Q): R = P - Q = P + (-Q) = (3 + 1, 1 + -2) = (4, -2)

 

The brake of our spacecraft can be the opposite of the engine force. By subtracting the brake vector from the engine vector, the spacecraft would come to a stop.

1
2
3
4
5
6
7
 public static Vector2 operator -(Vector2 v1, Vector2 v2)
 {
   v1.x -= v2.x;
   v1.y -= v2.y;
 
   return v1;
 }

 

Multiplying a vector by a scalar, is as simple as multiplying all the components of that vector by that number:

aV = Va = (a * v1, a * v2, …, a * vn).

When multiplying by a scalar, we do not vary the orientation of a vector, we only vary what it measures. The length of a vector is called magnitude or modulo and is usually written |V|.

If we multiply a vector V = (3, 1) by 2, the result is another vector, twice as long as the original:

 

As you may already be imagining, if we multiply a vector by -1, the result is its opposite. If we want our spaceship to go faster, we can multiply by a scalar the forward vector of the engine. If we multiply by a negative scalar, the result is similar to braking.

1
2
3
4
5
6
7
 public static Vector2 operator *(Vector2 v, float scalar)
 {
   v.x *= scalar;
   v.y *= scalar;
 
   return v;
 }

 

Given two scalars a and b, and three vectors P, Q and R, it is granted that:

  • P + Q = Q + P
  • (P + Q) + R = P + (Q + R)
  • (ab)P = a(bP)
  • a(P + Q) = aP + aQ
  • (a + b)P = aP + bP

Using the associative and commutative properties of the real numbers, it is very easy to verify these rules.

Dividing a vector by a scalar (nonzero), is very similar to multiplying it by a scalar, only that we divide its components by that number:

V/a = (v1 / a, v2, / a, …, vn / a)

1
2
3
4
5
6
7
8
9
 public static Vector2 operator /(Vector2 v, float scalar)
 {
   float factor = 1.0f / scalar;
  
   v.x *= factor;
   v.y *= factor;
  
   return v;
 }

 

If our spacecraft is going too fast and we want to reduce the speed, we can divide the impulse vector of the engine by 2 and we will reduce its speed by half.

Magnitude of a vector.

As we saw in the previous section, the magnitude or modulus of a vector is the length of that vector. When measuring a distance, it will never be negative (although it can be zero).

To calculate the length of a vector, we will use the well-known Pythagorean Theorem to find the hypotenuse of the triangle formed by ABC, where C is the vector we want to calculate and AB its components:

  }

Making the square root to the sum of the square of all the components of the vector, we will obtain its magnitude. In the previous case we had a vector V = (3, 3), its magnitude is:

|V| = √(32 + 32) = 4.2426…

1
2
3
4
 public float Length()
 {
   return Math.Sqrt((x * x) + (y * y));
 }

 

Vectors with magnitude one are often called unit vector. A very common operation in video games is to find the unit vector of a vector. This operation is called normalization and is very useful when we are only interested in operating with the orientation of a vector.

If we divide a vector (which has at least one non-zero component) by its magnitude (V / |V|), we will find its unit vector.

1
2
3
4
5
6
7
 public void Normalize()
 {
   float value = 1.0 / Math.Sqrt((x * x) + (y * y));
 
   x *= value;
   y *= value;
 }

 

The square root operation represents a higher computational cost than addition or multiplications. So if you want to compare the magnitude of two vectors and you are only interested in comparing them (if one is greater than, less than or equal to the other), you do not need to do the square root, the sum of the square of all its components is enough.

1
2
3
4
 public double LengthSquared()
 {
   return (x * x) + (y * y);
 }

 

To be continued with more operations with vectors ;).