Chapter 4. Math

Mathematics is an unavoidable part of video game development. Like it or loathe it, you need to use it, acknowledge it, and even downright embrace it to get anything done.

This chapter explores some of the most common math-related problems you’ll encounter when developing games using Unity.

Note

This isn’t all the math you’ll need for game development: far from it! But it’s enough math to help out, and we’ve done our best to structure it around the problems you might want to solve using the math, rather than the math itself.

Ever wondered what a vector is? Or how to use a matrix? What about a quaternion? We’ll cover them here, while also touching on common math problems in game development, like calculating the angles and distances to something else in your Unity-based game.

Tip

Nothing in this chapter is unique or specific to game development with Unity. We do cover things from a Unity-centric perspective when we talk about code, but everything here is just plain old math for game development.

Read on for just enough math for game development.

Tip

We can’t more highly recommend Paco Nathan’s Just Enough Math video series, also from O’Reilly. Check it out if you want to learn even more math.

4.1 Storing Coordinates of Varying Dimensions Using Vectors

Problem

Game development involves a lot of coordinates of varying dimensions:

  • One-dimensional coordinates, such as 1 or -9

  • Two-dimensional coordinates, such as 8,3 or -1,4

  • Three-dimensional coordinates, such as 3,1,-3 or -7,6,1

Realistically, when you’re building a game, as long as you’re keeping track of what you need to keep track of, it often doesn’t matter how you do it. However, one common, useful way of keeping track of coordinates is vectors.

Vectors are a set of coordinates of varying dimensions. Thus, our problem here is: how do I use vectors and Unity, and what can I use them for?

Tip

We often assign a specific meaning to vectors: in video games it’s usually coordinates in geometry, but there’s absolutely no reason you can’t store anything you want in them. They’re just a data structure.

Solution

To provide a solution here we’re going to have to unpack the problem a little. We admit, we wrote a deliberately broad problem statement, but that’s so we can show you everything you need to know about vectors without having to have one tiny recipe for each manipulation or action you might want to make with a vector.

First up, in Unity, you can define a Vector2: one with two dimensions, usually an x and a y. A Vector2 is typically used to represent a point in 2D space in a Unity game.

You can define a Vector2 with two dimensions:

Vector2 direction = new Vector2(0.0f, 2.0f);

Or use one of Unity’s built-in vectors:

var up    = Vector2.up;    // ( 0,  1)
var down  = Vector2.down;  // ( 0, -1)
var left  = Vector2.left;  // (-1,  0)
var right = Vector2.right; // ( 1,  0)
var one   = Vector2.one;   // ( 1,  1)
var zero  = Vector2.zero;  // ( 0,  0)

Unity also has a type called Vector3, which is a vector with three dimensions. There are several predefined vectors available from the Vector3 class:

Vector3 point = new Vector3(1.0f, 2f, 3.5f);

var up      = Vector3.up;      // ( 0,  1,  0)
var down    = Vector3.down;    // ( 0, -1,  0)
var left    = Vector3.left;    // (-1,  0,  0)
var right   = Vector3.right;   // ( 1,  0,  0)
var forward = Vector3.forward; // ( 0,  0,  1)
var back    = Vector3.back;    // ( 0,  0, -1)
var one     = Vector3.one;     // ( 1,  1,  1)
var zero    = Vector3.zero;    // ( 0,  0,  0)
Tip

You can learn more about Vector2 and Vector3 in Unity’s API documentation.

Every Transform component in Unity has local direction vectors defined, which are relative to their current rotation. For example, an object’s local forward direction can be accessed as:

var myForward = transform.forward;

Naturally, you can perform basic arithmetic with vectors. Vectors can be added together:

var v1 = new Vector3(1f, 2f, 3f);
var v2 = new Vector3(0f, 1f, 6f);

var v3 = v1 + v2; // (1, 3, 9)

and subtracted from each other:

var v4 = v2 - v1; // (-1, -1, 3)

You can also get the magnitude of a vector. Also known as the length of the vector, the vector’s magnitude is the straight-line distance from the origin (0,0,0) to the vector. The magnitude of a vector is the square root of the sums of the squares of the components. For example, the magnitude of the vector (0,2,0) is 2; the magnitude of the vector (0,1,1) is approximately 1.41 (that is, the square root of 2):

var forwardMagnitude = Vector3.forward.magnitude; // = 1

var vectorMagnitude =
    new Vector3(2f, 5f, 3f).magnitude; // ~= 6.16
Note

A vector whose magnitude is 1 is called a unit vector.

The magnitude can then be used to make other calculations. For example, to calculate the distance between two vectors, you can subtract one vector from another, and calculate the magnitude of the result:

var point1 = new Vector3(5f, 1f, 0f);
var point2 = new Vector3(7f, 0f, 2f);

var distance = (point2 - point1).magnitude; // = 3

The built-in method Distance performs the same calculation for you:

Vector3.Distance(point1, point2);

Calculating the magnitude of a vector requires a square root. However, there are cases where you don’t need the actual value of a vector’s magnitude, and just want to compare two lengths. In these cases, you can skip the square root, and work with the square of the magnitude. Doing this is a bit faster, and we care quite a lot about fast calculations in game development.

To get this value, use the sqrMagnitude property:

var distanceSquared = (point2 - point1).sqrMagnitude; // = 9

Lots of operations work best on vectors that have a magnitude of 1. A vector with magnitude of 1 is also called a unit vector, because its magnitude is a single unit (that is, one). You can take a vector and produce a new one that has the same direction but with magnitude of 1 by dividing it by its own magnitude. This is called normalizing a vector:

var bigVector = new Vector3(4, 7, 9); // magnitude = 12.08
var unitVector =
    bigVector / bigVector.magnitude; // magnitude = 1

This is a common operation, so you can directly access a normalized version of a vector by using the normalized property:

var unitVector2 = bigVector.normalized;

Vectors can also be scaled. When you multiply a vector by a single number (a scalar), the result is a vector in which every component of the source is multiplied by that number:

var v1 = Vector3.one * 4; // = (4, 4, 4)

You can also perform component-wise scaling by using the Scale method. This method takes two vectors, and produces a third vector in which each component of the first is multiplied by the corresponding component of the second—that is, given two vectors A and B, the result of A.Scale(B) is (A.x * B.x, A.y * B.y, A.z * B.z):

v1.Scale(new Vector3(3f, 1f, 0f)); // = (12f, 4f, 0f)

You can also get the dot product of two vectors, which tells you the difference between the directions they are pointing.

The dot product is defined as the sum of the products of the two vectors. That is, given two three-dimensional vectors A and B, A•B = sum(A.x * B.x, A.y * B.y, A.z * B.z).

You can use the dot product to determine the similarity of two vectors. The dot product between two vectors aiming in the same direction is 1:

var parallel = Vector3.Dot(Vector3.left, Vector3.left); // 1

The dot product between two vectors aiming in the opposite directions is –1:

var opposite = Vector3.Dot(Vector3.left, Vector3.right); // -1

And the dot product between two vectors at right angles to each other is 0:

var orthogonal = Vector3.Dot(Vector3.up, Vector3.forward); // 0

As a happy side effect, the dot product between two vectors is also the cosine of the angle between the two vectors. This means that, given the dot product between two vectors, you can calculate the angle between the vectors by taking its arc cosine:

var orthoAngle = Mathf.Acos(orthogonal);
var orthoAngleDegrees = orthoAngle * Mathf.Rad2Deg; // = 90
Note

The Mathf.Acos method returns a value measured in radians. To convert it to degrees, you can multiply it by the Mathf.Rad2Deg constant.

The dot product is a great way to tell if an object is in front of or behind another.

To tell if one object is in front of another, we first need to decide what “in front of” means. In Unity, the local z-axis represents the forward-facing direction, and you can access it through the forward property on an object’s Transform.

We can produce a vector representing the direction from the first object to the second by subtracting the position of the second from the position of the first. We can then take the dot product of that vector against the forward direction of the first object.

We can now use what we know about the dot product to figure out if the second object is in front of the first. Recall that the dot product of two vectors aiming in the same direction is 1. If the second object is directly in front of the first, then the direction to that object will be identical, which means that the dot product of the two vectors will be 1. If it’s 0, then the object is at a right angle to the forward direction. If it’s –1, then it’s directly behind the object, because it’s in the exact opposite direction of forward:

var directionToOtherObject =
    someOtherObjectPosition - transform.position;
var differenceFromMyForwardDirection =
    Vector3.Dot(transform.forward, directionToOtherObject);

if (differenceFromMyForwardDirection > 0) {
    // The object is in front of us
} else if (differenceFromMyForwardDirection < 0) {
    // The object is behind us
} else {
    // The object neither before or behind us; it's at a perfect right
    // angle to our forward direction.
}

The cross product, a third vector orthogonal to (at right angles to) two input vectors, is also available:

var up = Vector3.Cross(Vector3.forward, Vector3.right);
Tip

The cross product is only defined for three-dimensional vectors.

You can also get a new vector from two vectors, moving from one to the other at a certain magnitude. This is particularly useful to prevent overshooting. Here we move from (0,0,0) to (1,1,1), without moving any further than 0.5 units:

var moved =
    Vector3.MoveTowards(Vector3.zero, Vector3.one, 0.5f);
// = (0.3, 0.3, 0.3) (a vector that has a magnitude of 0.5)

Or reflect off a plane, defined by a normal:

var v =
    Vector3.Reflect(new Vector3(0.5f, -1f, 0f), Vector3.up);
// = (0.5, 1, 0)

You can also linearly interpolate, or lerp, between two input vectors, given a number between 0 and 1. If you provide 0, you’ll get the first vector; if you provide 1, you’ll get the second; and if you provide 0.5, you’ll get somewhere right in the middle of the two:

var lerped = Vector3.Lerp(Vector3.zero, Vector3.one, 0.65f);
// = (0.65, 0.65, 0.65)

If you specify a number outside the range of 0 to 1, lerp will clamp it to between 0 and 1. You can prevent this by using LerpUnclamped:

var unclamped =
    Vector3.LerpUnclamped(Vector3.zero, Vector3.right, 2.0f);
// = (2, 0, 0)

Discussion

This is just a taste of using vectors in Unity. The mathematical operations that Unity provides for you to perform on a vector can simplify a lot of things. You can use the dot product, for example, to tell if a point is in front of or behind a player character, or to create a radar to figure out where enemies are.

Vectors also make complex operations, like scaling or rotating something, very straightforward. Instead of having to calculate each object and its relation to the others manually, you can just use vector math.

Basically, vectors let you address geometry-related issues with significantly cleaner code than you would otherwise need. They’re wonderful mathematical tools in your game development toolkit!

4.2 Rotating in 3D Space

Problem

You want to rotate things in 3D space.

Solution

To rotate in 3D space, you’ll need to work with quaternions, which are a mathematical structure that’s very useful for representing rotations in 3D space. A quaternion can represent a rotation around any axis by any angle.

Tip

Quaternions can be a tricky beast, since—in pure math terms—they’re four-dimensional numbers. For the purposes of game development, though, all they are is a rotation, and it doesn’t matter if you don’t quite understand exactly why a quaternion works.

For example, you can use a quaternion to define a rotation that rotates around 90 degrees on the x-axis:

var rotation = Quaternion.Euler(90, 0, 0);

And then use this to rotate a point around the origin:

var input = new Vector3(0, 0, 1);

var result = rotation * input;
// = (0, -1, 0)

There is an identity quaternion, which represents no rotation at all:

var identity = Quaternion.identity;

You can interpolate—that is, blend—between two rotations using the Slerp method, which smoothly moves between rotations such that the change in angle is constant at every step. This is better than a linear interpolation of angles, in which the angles change at a nonconstant rate:

var rotationX = Quaternion.Euler(90, 0, 0);

var halfwayRotated =
    Quaternion.Slerp(identity, rotationX, 0.5f);
Tip

Slerp is short for spherical linear interpolation.

You can also combine quaternions together. For example, to rotate something around the y-axis, and then around the x-axis, you multiply them (they’re applied in the reverse order):

var combinedRotation =
    Quaternion.Euler(90, 0, 0) * // rotate around X
    Quaternion.Euler(0, 90, 0);  // rotate around Y
Note

This combination is not commutative: the order of multiplication matters! Rotating by X and then by Y is not the same thing as rotating by Y and then by X.

Discussion

Another method of representing rotations in 3D space is with Euler angles—that is, rotations around the x-, y-, and z-axes, stored separately. Euler angles are easy to understand, and it’s common to use them when expressing rotations in code.

However, this approach is prone to a problem called gimbal lock, which occurs when an object is rotated such that two of its rotational axes are parallel. When this happens, the object loses a degree of freedom. This problem doesn’t exist in quaternions, which can always by rotated in any direction from any other orientation.

An alternative method for avoiding gimbal lock is to use a matrix that represents a rotation (see Recipe 4.3). However, a 4×4 rotation matrix is 16 numbers, while a quaternion is just 4, which means quaternions take up less space than matrices for the same result.

4.3 Performing Transformations in 3D Space with Matrices

Problem

You want to represent an entire set of transformations—that is, movement, rotation, and scaling—in a single structure.

Solution

You can use a matrix to represent an entire transform. A matrix is just a grid of numbers (Figure 4-1):

var matrix = new Matrix4x4();
ugdc 0402
Figure 4-1. A 4×4 matrix

You can set and get values at each location in the grid:

var m00 = matrix[0, 0];

matrix[0, 1] = 2f;

You can multiply a matrix with a vector to produce a new vector. Depending on the values inside the matrix, this has the result of moving, scaling, and rotating the original vector. You can also perform more complex operations, like shearing or applying perspective projections.

You can multiply two matrices together to produce a third matrix. When you multiply this new matrix with a vector, the result is the same as if you’d separately multiplied each of the original matrices with the vector in sequence.

Tip

Computer graphics, and therefore game development, typically uses 4×4 matrices because they can be used to perform a wide range of common transformations.

Let’s create a matrix that moves (translates) a vector by 5 units, on the x-axis. First, we’ll create a new matrix, using four Vector4s (four-dimensional vectors):

var translationMatrix = new Matrix4x4(
    new Vector4(1, 0, 0, 5),
    new Vector4(0, 1, 0, 0),
    new Vector4(0, 0, 1, 0),
    new Vector4(1, 0, 0, 1)
);
Note

Each of the Vector4s that we use to create a matrix represents a column, not a row.

The matrix that this code creates is illustrated in Figure 4-1.

Note

When we multiply a three-dimensional vector by a 4×4 matrix, we add 1 to the end of the vector, forming a four-dimensional vector. The additional component is commonly referred to as the w component.

Multiplying this matrix by a four-dimensional vector, V, gives the following result:

1*Vx  +  0*Vy  +  0*Vz  +  5*Vw = resultX
0*Vx  +  1*Vy  +  0*Vz  +  0*Vw = resultY
0*Vx  +  0*Vy  +  1*Vz  +  0*Vw = resultZ
0*Vx  +  0*Vy  +  0*Vz  +  1*Vw = resultW

For example, to multiply the point (0,1,2) (a Vector3) with this matrix:

  1. We first add our w component:

    Vx = 0, Vy = 1, Vz = 2, Vw = 1
    
    1*0  +  0*1  +  0*2  +  5*1 = 5
    0*0  +  1*1  +  0*2  +  0*1 = 1
    0*0  +  0*1  +  1*2  +  0*1 = 2
    0*0  +  0*1  +  0*2  +  1*1 = 1
  2. Then we discard the fourth component, leaving our result. Our final result is therefore the vector (5,1,2).

Rather than making us do all this work ourselves, though, Unity provides a MultiplyPoint method as part of the Matrix4x4 type:

var input = new Vector3(0, 1, 2);

var result = translationMatrix.MultiplyPoint(input);
// = (5, 1, 2)
Note

You might be wondering why the matrix has the fourth row at all, since it just means we need to add and remove a useless fourth component to our vectors. It’s there to provide for operations like perspective projections. If you’re only doing transformations like translations, rotations, and scales, you can get away with only using part of the matrix, and can use Matrix4x4’s MultiplyPoint4x3 function instead. It’s a bit faster, but can be used only for translations, rotations, and scaling, and not for any of the other tasks.

Unity also provides helper methods to translate points using a matrix:

var input = new Vector3(0, 1, 2);

var translationMatrix = Matrix4x4.Translate(new Vector3(5, 1, -2));

var result = translationMatrix.MultiplyPoint(input);
// = (5, 2, 0)

You can also rotate a point around the origin using matrices and quaternions:

var rotate90DegreesAroundX = Quaternion.Euler(90, 0, 0);

var rotationMatrix = Matrix4x4.Rotate(rotate90DegreesAroundX);

var input = new Vector3(0, 0, 1);

var result = rotationMatrix.MultiplyPoint(input);

In this case, the point has moved from in front of the origin to below it, resulting in the point (0,-1,0).

If your vector represents a direction, and you want to use a matrix to rotate the vector, you can use MultiplyVector. This method uses only the parts of the matrices that are necessary to do a rotation, so it’s a bit faster:

result = rotationMatrix.MultiplyVector(input);
// = (0, -1, 0) - the same result.

You can also use a matrix scale a point away from the origin:

var scale2x2x2 = Matrix4x4.Scale(new Vector3(2f, 2f, 2f));

var input = new Vector3(1f, 2f, 3f);

var result = scale2x2x2.MultiplyPoint3x4(input);
// = (2, 4, 6)

Multiplying matrices together results in a new matrix that, when multiplied with a vector, produces the same result as if you’d multiplied the vector by each of the original matrices in order. In other words, if you think of a matrix as an instruction to modify a point, you can combine multiple matrices into a single step.

Tip

When you combine matrices like this, we call it concatenating the matrices.

In this example, we concatenate matrices:

var translation = Matrix4x4.Translate(new Vector3(5, 0, 0));
var rotation = Matrix4x4.Rotate(Quaternion.Euler(90, 0, 0));
var scale = Matrix4x4.Scale(new Vector3(1, 5, 1));

var combined = translation * rotation * scale;

var input = new Vector3(1, 1, 1);
var result = combined.MultiplyPoint(input);
Debug.Log(result);
// = (6, 1, 5)

As with quaternions, the order of multiplication matters! Matrix multiplication is not commutative, while multiplying regular numbers is. For example: 2 * 5 is the same calculation as 5 * 2: both calculations result in the number 10.

However, moving an object and then rotating it doesn’t produce the same result as rotating an object and then moving it. Likewise, combining a matrix that translates a point with one that rotates a point won’t have the same result if you combine them in the reverse order.

Combining matrices with multiplication will apply them in reverse order of multiplication. Given a point P, and matrices A, B, and C:

P * (A * B * C) == (A * (B * (C * P)))

you can create a combined translate-rotate-scale matrix using the Matrix4x4.TRS method:

var transformMatrix = Matrix4x4.TRS(
    new Vector3(5, 0, 0),
    Quaternion.Euler(90, 0, 0),
    new Vector3(1, 5, 1)
);

This new matrix will scale, rotate, and then translate any point you apply it to.

You can also get a matrix that converts a point in the component’s position in local space to world space, which means taking the local position and applying the local translation, rotation, and scaling from this object as well as those of all its parents:

var localToWorld = this.transform.localToWorldMatrix;

You can also get the matrix that does the reverse—that is, it converts from world space to local space:

var worldToLocal = this.transform.worldToLocalMatrix;

Phew. That’s a lot of things you can do with matrices!

Discussion

Internally, Unity uses matrices to represent transforms. For simplicity, you spend most of your time directly accessing the position, rotation, and scale. However, knowing how things work under the hood is always a good thing.

4.4 Working with Angles

Problem

You want to work with the angles between vectors.

Solution

In Unity, most rotations that are represented as Euler angles are given as degrees.

We can rotate things using degrees, using the Transform class’s Rotate method:

// Rotate 90 degrees - one quarter circle - around the X axis
transform.Rotate(90, 0, 0);
Tip

There are 360 degrees in a circle; there are 2π radians in a circle. They’re just different units of measurements for angles.

Degrees are much more familiar to most people, but radians are often easier to calculate with. This is why parts of Unity, particularly those related to math, expect radians. There are 2π radians in a circle:

// The sine of pi radians (one half-circle) is zero
Mathf.Sin(Mathf.PI);  // = 0

You can convert from radians to degrees, and back again, like so:

// Converting 90 degrees to radians
var radians = 90 * Mathf.Deg2Rad; // ~= 1.57 (π / 2)

// Converting 2π radians to degrees
var degrees = 2 * Mathf.PI * Mathf.Rad2Deg; // = 360

Discussion

The dot product of two unit vectors is equal to the cosine of the angle between them. If you have the cosine of a degree, you can get the original degree by taking the arc cosine of it. This means that you can find the angle between two vectors like this:

var angle = Mathf.Acos(Vector3.Dot(Vector3.up, Vector3.left));

The result of this is π radians; if you want to show it to the user, you should convert it to degrees first. There are 2π radians in a circle, while there are 360 degrees in a circle; as a result, to convert a number from radians to degrees, you multiply it by 180/π. For example, π/2 radians in degrees = (π/2) * (180/π) = 90. Converting from degrees to radians works in reverse: you multiply it by π/180. For example, 45 degrees in radians is 45 * (π/180) = π/4.

You can simplify this in your code by using the Mathf.Deg2Rad and Mathf.Rad2Deg constants. If you multiply an angle expressed in radians by Mathf.Rad2Deg, you’ll get the result in degrees; if you multiply an angle expressed in degrees by Mathf.Deg2Rad, you’ll get the result in radians.

4.5 Finding the Distance to a Target

Problem

You want to check to see if an object is within a certain range of another.

Solution

You’ll need to create and add a script to the object that needs to know when the other object is in range of it:

  1. Create a new C# script called RangeChecker.cs, and add the following code to it:

public class RangeChecker : MonoBehaviour {

    // The object we want to check the distance to
    [SerializeField] Transform target;

    // If the target is within this many units of us, it's in range
    [SerializeField] float range = 5;

    // Remembers if the target was in range on the previous frame.
    private bool targetWasInRange = false;

    void Update () {

        // Calculate the distance between the objects
        var distance = (target.position - transform.position).magnitude;

        if (distance <= range && targetWasInRange == false) {
            // If the object is now in range, and wasn't before, log it
            Debug.LogFormat("Target {0} entered range!", target.name);

            // Remember that it's in range for next frame
            targetWasInRange = true;

        } else if (distance > range && targetWasInRange == true) {
            // If the object is not in range, but was before, log it
            Debug.LogFormat("Target {0} exited range!", target.name);

            // Remember that it's no longer in range for next frame
            targetWasInRange = false;
        }

    }
}

Attach this script to any object, and attach any other object to the script’s Target field, and the script will detect when the target enters and exits the specified range.

Discussion

If you combine this recipe with Recipe 4.6, you can pretty easily put together a behavior in which an object can only “see” nearby objects that are in front of it. A more sophisticated version of this script can be seen in Recipe 10.3.

4.6 Finding the Angle to a Target

Problem

You want to find the angle between two objects.

Solution

You’ll need to create and add a script to the object that needs to know the angle between it and another object:

  1. Create a new C# script called AngleChecker.cs, and add the following code to it:

    public class AngleChecker : MonoBehaviour {
    
        // The object we want to find the angle to
        [SerializeField] Transform target;
    
        void Update () {
    
            // Get the normalized direction to the target
            var directionToTarget =
                (target.position - transform.position).normalized;
    
            // Take the dot product between that direction and our forward
            // direction
            var dotProduct = Vector3.Dot(transform.forward,
                                         directionToTarget);
    
            // Get the angle
            var angle = Mathf.Acos(dotProduct);
    
            // Log the angle, limiting it to 1 decimal place
            Debug.LogFormat(
                "The angle between my forward direction and {0} is {1:F1}°",
                target.name, angle * Mathf.Rad2Deg
            );
    
        }
    }
  2. Attach this script to any object, and attach any other object to the script’s Target field, and the script will log the angle, in degrees, between the object’s forward direction and the target object.

Discussion

The concept of “angle between two objects” depends on you choosing at least one direction. You can’t get the angle between two points in space, because there’s an infinite number of possible angles between them. Instead, you need to pick a direction relative to the first object, and compare that with the direction to the second.

Get Unity Game Development Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.