IntroIf there was one field of mathematics that has helped me the most in game development and many other areas of computer programming, it is linear algebra. Calculus and discrete math are all fine and good, but there are so many things that can be represented easily and beautifully as vectors in game programming, that knowledge of linear algebra has been completely invaluable to me.
So, I thought I would make this tutorial to help complete math novices with the basic concepts of vector arithmetic and linear algebra.
This tutorial will only assume that you know something about programming, and a little bit about physics.
Part I: PongLet's say that you're writing a clone of pong. Pong is a very simple game, and it's often one of the first that game programmers write. It was my first game, as well.
For those not familiar, pong is a game in which a ball bounces around the screen. Two players with paddles face off on opposite sides of the screen, and try to knock the ball onto the opposite side of the screen before the other player can stop it.
It looks kind of like this:
How would we go about coding this sort of game? Assuming that we have a way of drawing circles and rectangles, a good idea would first be to store the position of the ball on the screen.
Screen coordinates are typically represented as a pair of numbers called the row and column. The row is the number of pixels down from the top of the screen, and the column is the number of pixels to the left from the left side of the screen. Typically, the column is referred to as the "x" coordinate of the ball, and the row is referred to as the "y" coordinate of the ball.
These are known as the "Cartesian coordinates" of the ball, named after the famous mathematician, Renes Descartes. In the Cartesian coordinate system, every point on a plane can be represented as two numbers, x and y. In this case, the plane is the computer screen, and the x and y coordinates refer to the number of pixels from the left and top of the screen.
So, let's define our ball's position like this (here I'm assuming the language is based on C):
// The ball's Cartesian coordinates.
float ballPositionX;
float ballPositionY;
Okay, so now we know where the ball is, and know where to draw it. How do we make it move? To move the ball, we need to change its x and y coordinates, and re-draw it. The first thing we'll try to do is make it move in a straight line.
When I first tried to code Pong (over a decade ago, now!) this part confused me. I had learned from algebra that I could represent straight lines as functions where the y coordinate depended on the x coordinate, so the first thing I tried was to get the ball to move on a straight line by following the equation:
// This is BAD, don't do it!
ballPositionY = (ballPositionX)*m + b;
Where m is the slope of the line, and b is the Y offset from the origin. This worked fine for some lines, but it quickly became apparent to me that the number of possible lines the ball could travel along was very large! And, what if I wanted the ball to change speed, or direction in mid-flight?
What I hadn't realized at the time was that what I needed was a concept of time. I didn't need to be
modeling the ball's motion as a line, but instead I needed to
simulate the motion of a ball through space.
The best way to get the ball to move along a straight line is to introduce a
velocity for the ball. What do I mean by this?
Imagine you have the ball at position (50,100) at time t (say, 1 second after starting your program). Now, imagine that at time t2 (say, 2 seconds after starting your program), the ball has traveled to position (55, 101). Then, between t and t2, the ball has moved five pixels to the right, and one pixel down. So, the ball's
velocity at this time can be represented as another pair of numbers : (5, 1) pixels per second.
So, we can represent the ball's motion over time without any special equations or modeling. We can simply add two more quantities, the ball's
x velocity and
y velocity.
// The ball's Cartesian coordinates.
float ballPositionX;
float ballPositionY;
// The ball's Cartesian velocity.
float ballVelocityX;
float ballVelocityY;
Then, in our method where we're updating the ball's position, we simply do this:
void update( float dt)
{
// Add x velocity to x position.
ballPositionX+=(ballVelocityX/dt);
// Add y velocity to y position.
ballPositionY+=(ballVelocityY/dt);
}
Where dt is the time in seconds passed since the last update. (For now you can ignore this. The important part is that the ball's velocity is
ADDED to its position with each update loop!)
Now, you can get the ball to move in whatever way you want by changing its velocity! There is no need for complicated equations to model the movement of the ball. All of this is done automatically. If the ball's velocity remains constant, it will move in a straight line. If the ball's velocity changes over time, it will change direction! For instance, if you negated (multiplied by -1) the ball's y velocity, it would turn upwards, as if it were bouncing off of the floor. In fact, this is how you get the ball to bounce off of the floor and walls in a pong game. If it's y position is greater than the screen height,or less than 0, you multiply its y velocity by -1. If it's x position is greater than the screen width, or less than 0, you multiply its x velocity by -1 also.
So what have we just done here?
We've simplified the representation of a ball moving on the screen down to just four numbers, its x position, its y position, and its x and y velocities. Manipulating the ball's position and velocity is as simple as changing these four values. Easy, right?
Part II: VectorsUsing groups of numbers in this way is called vector arithmetic. A
Vector is simply an ordered set of numbers. In the previous example, the ball's position, a pair of numbers, was a vector. The first number in the pair always corresponded to the x-coordinate of the ball, and the second number always corresponded to the y-coordinate of the ball. It's velocity was also a vector in which the first number corresponded to a quantity in the x direction, and the second number corresponded to a quantity in the y direction.
We can modify our previous example by making a simple Vector structure that contains two numbers.
// A simple structure containing two floats.
struct Vector2f
{
float x;
float y;
};
// Now the ball's position and velocity
// are in structures.
Vector2f ballPosition;
Vector2f ballVelocity;
The numbers inside a vector are called its
components. The ball's position and velocity each have two components, x, and y. People will often call the components of a vector
scalars, which is another name for ordinary numbers. Scalars can interact with vectors to produce interesting results.
Vectors don't have to have just two components. A Vector that has two components is called a 2-dimensional vector, a vector that has
k components is called a k-dimensional vector. For instance, 3-dimensional vectors with quantities called (x,y,z) are often used to represent positions and directions in 3D games. 4-dimensional vectors called quaternions are often used to represent orientation in 3D games by adding a fourth quantity, (x,y,z,w) corresponding to rotation around a line.
Vector AdditionA key property of vectors is that they can be
added together. As we saw with the pong example, we could add the ball's velocity to its position by adding the components together. This property is called "linearity," and it is why doing math with vectors is often called "linear algebra."
Let's make things easier by using the plus sign (+) to define addition of vectors, implying that we are adding together their components. This will be especially useful for you if you are using a language with operator overloading, like C++ or C#.
// Adding the ball's velocity to its position is done like this:
// NOTE: + sign is vector addition!
ballPosition = ballPosition + ballVelocity;
In this sense, you can add and subtract vectors exactly as you would ordinary numbers. If you have vectors A, B, C, and D, then if (A = B+C), and (D = C+B), then (A = D).
However, you can only add two vectors together if they have the same number of components, and never if they have more or less! For instance, this is not possible:
// It's impossible to add scalars to vectors!
// A scalar is a one-dimensional vector.
ballPosition = ballPosition + 5;
Intuitively, we can look at 2-dimensional vectors as arrows with a direction and a length. The two components of the vector form the legs of a right triange, and the hypotenuse of the triangle is the arrow we can draw to represent it.
What, then, happens to two vectors when we add them together in this intiutive sense? If we represent both vectors as arrows, their sum can be found by taking one of arrows, and placing its "tail" on the other's "tip," as in this diagram:
If you try this out yourself, you can get an intuitive understanding of what happens to vectors as you add them together. You can also confirm that A + B = B + A for vectors A and B, though this should be obvious from the definition of vector addition.
It's important, though, not to get caught up in all of this arrow stuff. It can be somewhat appealing to see vectors as little directional arrows, but at heart vectors are simply lists of numbers with very simple properties.
Subtraction is exactly the same as you would expect. Subtracting vectors means subtracting their x and y components. In the intuitive arrow representation, (A-B) is done the same way as (A+B), except B's direction is reversed before adding it to A.
Some applications of vector addition include our use of it to add the ball's velocity to its position.
Scaling Remember when I said that scalars couldn't be added to vectors? Although they cannot be added to vectors, they can be
multiplied with vectors. This is called "scalar multiplication," or sometimes "scaling."
When you multiply a scalar with a vector, you simply multiply each of the vector's componenents by the scalar.
For instance:
// Create a vector called A.
// A = (1,3)
Vector2f A = new Vector2f(1,3);
// Create a scalar called s.
float s = 2;
//Multiply A by s
// A = (1*2, 3*2) = (2, 6)
A = A*s;
// This is equivalent to:
A.x = A.x*s;
A.y = A.y*s;
Using our intuitive arrow representation, scaling a vector means changing the length of the arrow. Scaling a vector by 0.5, for instance, would make the arrow half as long. Scaling it by -1 would reverse its direction.
Scaling a vector can be incredibly useful for a variety of purposes, from reversing its direction to finding points along a line, etc.
Part III: Cool Stuff You Can Do With Just This Usually, vector tutorials will diverge at this point into discussions of things like vector multiplication and matrix-vector transforms; but before we do that, there is a lot of cool stuff you can do by just using vector addition and multiplication!
Let's see what fun we can have with our simple pong game.
Reflecting a Vector As mentioned before, we can reflect a vector by multiplying either its x or y components by -1. The following code will cause the ball to bounce around on the walls:
void bounceBall()
{
if(ballPosition.x > SCREEN_WIDTH
|| ballPosition.x < 0)
{
ballVelocity.x *= -1;
}
if(ballPosition.y > SCREEN_HEIGHT
|| ballPosition.y < 0)
{
ballVelocity.y *= -1;
}
}
Adding Forces And Gravity So, we have the ball's position and velocity as vectors. What if we wanted to change the ball's velocity without accessing it directly? Well, we could add another vector, the acceleration vector, to the ball. This acceleration vector will be added to the ball's velocity with each frame, and then cleared.
We can also define a scalar for the ball's mass.
// New Vector for acceleration
Vector2f ballPosition;
Vector2f ballVelocity;
Vector2f ballAcceleration;
float mass;
void update(float dt)
{
// What to do in the update loop:
// Get the change in position over time
// by scalar multiplying it by 1 over the
// change in time.
Vector2f changeInPosition = ballVelocity * (1/dt);
// Bouncing method we implemented earlier
bounceBall();
// Move the ball.
ballPosition += changeInPosition;
// Compute the amount the velocity has
// changed over time.
Vector2f changeInVelocity = ballAcceleration * (1/dt);
// Adjust the ball's velocity.
ballVelocity += changeInVelocity;
// Clear the ball's acceleration
// (in newtonian physics, acceleration is
// not preserved, unlike velocity and position)
ballAcceleration = ballAcceleration * (0);
}
Now, we can add a force to the ball with the following function:
// Changes the ball's acceleration
// by a force.
void addForce(Vector2f force)
{
// Newton's third law F = ma
Vector2f dv = force*(1/mass);
// Add to the ball's acceleration.
ballAcceleration += dv;
}
By adding force to the pong game, we can create a whole variety of interesting effects. For instance, we can add simple downward gravity by doing the following in each update loop:
// Gravity is one pixel per second squared
// downwards in our simulation!
Vector2f gravity = (0, 1);
// Make the ball accelerate downwards.
addForce(gravity * mass);
This will make the ball move in a parabolic arc, and if you've already implemented ball bouncing on the walls, it will cause it to bounce around the screen!
Add Friction We can use scalar multiplication to modify our bounceBall method and add friciton to the ball's motion. Each time the ball hits a wall, it will lose some energy, and its velocity will be reduced rather than perfectly reflected. Typically, we'll want to reduce its velocity by a tiny amount.
// Modified bounceBall function that
// will cause the ball to lose energy
// with subsequent bounces.
void bounceBall(float wallFriction)
{
if(ballPosition.x > SCREEN_WIDTH
|| ballPosition.x < 0)
{
ballVelocity.x *= -(wallFriciton);
}
if(ballPosition.y > SCREEN_HEIGHT
|| ballPosition.y < 0)
{
ballVelocity.y *= -(wallFriction);
}
}
With this implemented, and using a reasonable number like 0.999, the ball will slowly lose energy, bouncing lower and lower until it eventually stops. Interestingly, if you use a number greater than 1, the ball will gain energy and go out of control!
Next TimeThis is about all the time I have for today, but I hope this has been enlightening for beginning game programmers! Using vectors can make things very simple in game programming, especially for simulation-based games.
Next time, we'll go over some common vector problems you will encounter in games, such as finding a vector from one position to another, finding the angle of a vector, and other stuff!