Article 2 - Vectors
Maths - Urgh, Eccch, YUCK ! But, like it or not, vectors are crucial to quake coding. Every time you fire a rocket, you need vectors. In fact, you could say that "if you don't wanna go down, you betta get up with da vecta".
What is a vector?
In a 3-dimensional world such as quake, a vector is simply three numbers - an x component, a y component and a z component. In q_shared.h at line 258 we can see the C code definition of vec3_t:
typedef float vec_t; typedef vec_t vec2_t; typedef vec_t vec3_t; typedef vec_t vec4_t; typedef vec_t vec5_t;
If you're new to C, a typedef associates a complicated datatype with another name. Above, the first typedef states that a single dimensional vector type, vec_t, is a floating point number (e.g. 23.4567, as opposed to integers like 23). The next typedef's define 2,3,4 and 5 dimensional vector types. The most common vector type is the 3-dimenstional one, vec3_t - it is simply an array of three vec_t's (in other words, an array of three float's). These three floats represent what we call the x, y and z components.
Ok, the most basic vector is the origin vector. Let's write it down like this : (0.0, 0.0, 0.0). The x, y and z components are all zero. Let's create an origin vector in C code.
vec3_t origin; origin = 0.0; origin = 0.0; origin = 0.0;
Now let's create a vector to represent the position of player0, at (55.0, -23.0, 12.5).
vec3_t player0pos; player0pos = 55.0; player0pos = -23.0; player0pos = 12.5;
Now, where is player0 standing ? Well, that depends on something called...
The Coordinate system
Let's assume that our quake map is three dimensional (and multi-levelled). A space map, for example. Let's also assume that the location (0.0, 0.0, 0.0) is right on top of a platform somewhere in the middle of the map, with bits of the map extending in directions both left and right, forwards and back, and up and down. But, what defines 'left-right', 'forwards-backwards' or 'up-down'?
Quake uses what we call a 'left handed 3d co-ordinate system'. This means, take your left hand and point your arm forwards. Point your thumb upwards, your index finger forwards and your middle finger directly to the right. Let's assume that we are standing at the origin in our space map, facing northwards.
Now, your middle finger is pointing in the direction of positive x co-ordinates, your index finger is pointing to positive y co-ordinates, and your thumb is pointing to positive z co-ordinates.
So, by holding out our 2 fingers and thumb (remember, LEFT hand!) imagine that we are sitting at the origin looking north. We can see that player0pos is a position to the right of us (x=55.0 is positive), behind us (y=-23.0 is negative) and above us (z=12.5 is positive).
If we wanted to suddenly teleport player0 64 units eastwards and 64 units up into the air, how would we do it ? Maybe :
player0pos += 64.0; player0pos += 64.0;
There is a better way to do this, however...
Ok, all the vectors we've been working with so far have been positional vectors - an (x,y,z) position in the game world. There is also another use for vectors - to represent velocity (Note: velocity is different to speed, more later). How do we represent the velocity of rocket that is moving eastwards, and 45 degrees upwards, at 900 units/second ? A vector.
This may be a little confusing, since the rocket will have a position and a velocity, and both are represented by vec3_t.
vec3_t rocketpos; vec3_t rocketvel; rocketpos = 0.0; rocketpos = 0.0; rocketpos = 0.0; rocketvel = 636.39; rocketvel = 0.0; rocketvel = 636.39; // now let's 'move' the rocket, assume one second has gone by. rocketpos += rocketvel; rocketpos += rocketvel; rocketpos += rocketvel; // another second goes by... etc rocketpos += rocketvel; rocketpos += rocketvel; rocketpos += rocketvel;
The result: after 1 second our rocket is located at (636.39, 0, 636.39). But why did we use 636.39, not 900 ? Well the answer to that lies in the fact that if you move in two dimensions (in our case, eastwards and up) the total distance traveled is measured along the long side of a right-angled triangle, where the short sides of the triangle measure the distances moved in those two dimensions. And we know that the long side of a right-angled triangle is calculated from the good ol' c = sqrt (a squared + b squared). So if a and b are 636.39, c works out to be... 900.
In three dimensions the maths gets even stickier.
Rather than worry about getting 636.39 from 900, there's a better way to work with a velocity of 900 units/second eastwards and upwards at 45 degrees. We simply break it into two components - a direction and a speed.
Now for some definitions, a direction vector is a vector in any direction where the length is precisely 1.0. For example, (1.0, 0.0, 0.0) or (0.707, 0.0, -0.707) etc. We call any vector with a length of precisely 1.0 a normalized vector . A speed is a linear quantity (NOT a vector!!). For example, 900.0, 0.0 or -450.0. A speed of 0.0 represents standing still whilst negative speeds represent moving backwards.
Why is this easier ? Well, there is a simple equation that allows us to 'move' an entity through space - endpos = startpos + speed * direction. For example, let's model our rocket :
vec3_t rocketpos; vec3_t rocketdir; float rocketspeed; rocketpos = 0.0; rocketpos = 0.0; rocketpos = 0.0; rocketdir = 0.707; rocketdir = 0.0; rocketdir = 0.707; rocketspeed = 900.0; // now let's 'move' the rocket, assume one second has gone by. rocketpos += rocketspeed * rocketdir; rocketpos += rocketspeed * rocketdir; rocketpos += rocketspeed * rocketdir;
But again, why is this easier ? Well, firstly - quake provides us with several direction vectors at many points in the code. For example, we quite often see the forward vector. This vector is always pointing in the direction we are aiming. To fire a rocket? Give it a velocity of forward multiplied by 900. To fire a railgun, trace along the forward vector for 8192 units. Etc. As luck would have it, Carmack has given us a mini-library of...
Since vectors are used so much in quake, it makes sense to define functions that take care of all the common vector manipulation tasks (copying, adding, multiplying, etc). Not only is it good practice to make use of these functions (since they're already written, DUH!), it's important to realize that the operators +, * etc CANNOT be used - this is because vec3_t is an array, and the common mathematical operators cannot be used on arrays in C (in the way we expect them too, anyway). For example,
// the stuff below is WRONG. vector_a *= 100.0; // WRONG! vector_c = vector_a + vector_b; // WRONG!
// we use functions to do addition, multiplication, etc. VectorScale (vector_a, 100.0, vector_a); // CORRECT! VectorAdd (vector_a, vector_b, vector_c); // CORRECT!
<Here's a complete list (from q_shared.h line 357):
- VectorSubtract(a,b,c) - subtract b from a, result is c
- VectorAdd(a,b,c) - add a to b, result is c
- VectorCopy(a,b) - copy a to b
- VectorScale(v,s,o) - make v s units long, result in o
- VectorMA(v,s,b,o) - make b s units long, add to v, result in o
- VectorClear(a) - too easy
- VectorNegate(a,b) - flip a, result in b
- VectorSet(v,x,y,z) - another easy one
- Vector4Copy(a,b) - used for 4 dimensional vectors
- SnapVector(v) - round a vector to integer values
Got it all ? Notice how most of these functions take one, two or three parameters and save the output in a second, third or forth output parameter (the exceptions are VectorClear() and VectorSet()). For example, calling VectorSubtract(a,b,c) will not modify a or b - the result will be places in vector c. (Unless of course, we say VectorSubtract(a,b,a), which is quite ok to do).
You might be wondering what this function does. It's a useful function that combines a vector multiply and a vector addition. It's used all over the place so it's important to understand what it does. Let's go back to our "rocket moving eastwards and upwards at 900 units/second" example, using the vector functions...
vec3_t rocketpos; vec3_t rocketdir; float rocketspeed; VectorClear(rocketpos); VectorSet(rocketdir, 0.707, 0.0, 0.707); rocketspeed = 900.0; // now let's 'move' the rocket, assume one second has gone by. VectorMA(rocketpos, rocketspeed, rocketdir, rocketpos);
Notice that we did *exactly* the same thing as before, but in half the amount of code. And it's much neater because we've used the vector functions. VectorMA is particularly useful - get to know it like an old girlfriend. Innit.
Putting it all together
Ok, let's throw all this into action. Open up g_missile.c and look at lines 390-393 :
VectorCopy( start, bolt->s.pos.trBase ); VectorScale( dir, 900, bolt->s.pos.trDelta ); SnapVector( bolt->s.pos.trDelta ); // save net bandwidth VectorCopy (start, bolt->r.currentOrigin);
First, we assume that start is where the rocket is firing from (just in front of the player, at waist height). And dir is the direction that the player is facing (a normalized vector, right ?). And a rocket moves at 900 units/second.
So the code simply does this :
- Copy the start vector to the bolt's trBase
- Multiply the dir vecotr by 900 and leave the result in trDelta
- Snap trDelta (convert from floats to integers)
- Copy the start vector to the bolt's currentOrigin
That's all there is to it. Have a look through some other functions like fire_plasma and weapon_railgun_fire - see if you can work out (for example) how fast does plasma move ? What is the range of the railgun ? Or the lightning gun ?
If you can answer all those questions - congratulations, you're a vector geek!