PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 38 | Next >>

TUTORIAL 38 - Accelerating Rockets
by Doug Hammond

HypoThermia tells me that there have been a few requests for accelerating rockets. This helpful tutorial will show you how to make them. While not actually too useful in a deathmatch, the accelerating rocket is a good example of how to make your own movement types, which I'll explain a bit more about later. Using the basics of this tutorial, it's possible to make rockets that corksrew, plasma shots that do figure eights, and pretty much anything else you can think of, as long as you have the mathematical skills.

Why is it important? Providing a working movement type allows client prediction to be used. This makes the client game much smoother, and allows a "fire and forget" type system for projectile entities. Updates to the position of an entity no longer depend on the quality of connection between the client and server (once the client knows the entity has been created).

#### 1. A bit more introduction

This tutorial is a look into one of the most central functions in the game, BG_EvaluateTrajectory, and it's sister function, BG_EvaluateTrajectoryDelta. These functions predict the position and velocity of objects respectively in the game world. Every moving object in the game refers to these functions every program cycle, sometimes several times.

This tutorial tinkers with BG_EvaluateTrajectory to allow a new kind of object movement, which can then be implemented into Accelerating Rockets. BG_EvaluateTrajectory has a fairly simple structure to it. It first checks what trajectory type (trType) the object is using (TR_LINEAR for rockets, TR_GRAVITY for grenades, TR_SINE for moving platforms, and so on...) and then predicts the object's position using an equation specific to that type.

While the coding modifications in this tutorial are reasonably light, the mathematics used are really quite complicated. To be able to make your own movement type, you will need a decent grasp of calculus, particularly derivitives. If you don't have a clue, you may just have to find someone who does.

The bulk of modifications are made in bg_misc.c with one addition in q_shared.h, and a couple of tweaks are made to the rocket launcher in g_missile.c.

#### 2. The Code

First of all, let's have a look at the variables used in the trajectory code. Open up q_shared.h and have a look at line 867. You should see something like this:
```typedef enum {
TR_STATIONARY,
TR_INTERPOLATE,
TR_LINEAR,
TR_LINEAR_STOP,
TR_SINE,
TR_GRAVITY
} trType_t;
```
These are all the avaliable trTypes in the game. TR_STATIONARY just sits there, TR_INTERPOLATE I'm not too sure on, TR_LINEAR moves in a straight line, TR_LINEAR_STOP moves in a straight line for a specified length of time and then stops, TR_SINE moves backwards and forwards in a sine wave pattern and TR_GRAVITY moves in a gravity affected arc.

Before we make our first modification, I'd like to distract your attention to the next typedef in q_shared.h. It looks something like this:

```typedef struct {
trType_t	trType;
int		trTime;
int		trDuration;
vec3_t	trBase;
vec3_t	trDelta;
} trajectory_t;
```
These are all the object-specific variables we have to work with in BG_EvaluateTrajectory. If you add any extra variables it causes serious errors in the running of the game, so we will have to re-use one of the values. But more on that later. Most of these are reasonably self evident, exceptions being trBase which is the starting point of the object and trDuration being an extra variable used only in TR_SINE and TR_LINEAR_STOP.

If any of this isn't making sense, don't worry. You'll get a better idea of what's going on once we take a look at BG_EvaluateTrajectory. But first, we need to add our own trType to the list. Add this line to the typedef trType_t:

```	TR_GRAVITY,
TR_ACCEL
} trType_t;
```
Don't forget to add in the comma after TR_GRAVITY.

Now for the guts of our modification. Open up bg_misc.c, and find the function BG_EvaluateTrajectory. Look closely. Basically, the function is structured so that it checks what trType the object is, runs the appropriate equation and then returns the predicted position of the object. We want to add a little bit of code for the TR_ACCEL trType. The equation we're looking for should take the object's initial velocity, multiply it by the time since the object started moving, then add the half the acceleration times the change in time squared. Sound complicated? This is actually a simple physics equation, normally written like so:

s = u*t + .5*a*t^2

Unfortunately, because of the combination of simple numbers (time and acceleration) and vector numbers (velocity and the final result), we can't just put this equation into the code without getting serious errors. This means we'll need to use some of quake 3's in built vector math equations. First we need an extra variable in the function for the missile's direction. Go to the start of the function and put this in:

```	float		deltaTime;
float		phase;
vec3_t		dir;

switch( tr->trType ) {
```
Now we can start with our equation. At near the end of the function, put this in:
```case TR_GRAVITY:
deltaTime = ( atTime - tr->trTime ) * 0.001;
VectorMA( tr->trBase, deltaTime, tr->trDelta, result );
result[2] -= 0.5 * DEFAULT_GRAVITY * deltaTime * deltaTime;
break;
case TR_ACCEL:
// time since missile fired in seconds
deltaTime = ( atTime - tr->trTime ) * 0.001;

// the .5*a*t^2 part. trDuration = acceleration,
// phase gives the magnitude of the distance
// we need to move
phase = (tr->trDuration / 2) * (deltaTime * deltaTime);

// Make dir equal to the velocity of the object
VectorCopy (tr->trDelta, dir);

// Sets the magnitude of vector dir to 1
VectorNormalize (dir);

// Move a distance "phase" in the direction "dir"
// from our starting point
VectorMA (tr->trBase, phase, dir, result);

// The u*t part. Adds the velocity of the object
// multiplied by the time to the last result.
VectorMA (result, deltaTime, tr->trDelta, result);
break;
default:
Com_Error( ERR_DROP,
"BG_EvaluateTrajectory: unknown trType: %i",
tr->trTime );
```
Quake also needs to be able to find the velocity of the object at any given time, and so uses the aforementioned sister function BG_EvaluateTrajectoryDelta. If you take a look at it, you'll find it works in much the same way as BG_EvaluateTrajectory. The equation for TR_ACCEL's velocity is the initial velocity of the object plus the acceleration multiplied by the time, or:

v = u + a*t

To cut a long story short, the end result of the function should look something like this:

```/*
================
BG_EvaluateTrajectoryDelta

For determining velocity at a given time
================
*/
void BG_EvaluateTrajectoryDelta
(const trajectory_t *tr,
int atTime,vec3_t result ){
float	deltaTime;
float	phase;
vec3_t	dir;

switch( tr->trType ) {
case TR_STATIONARY:
case TR_INTERPOLATE:
VectorClear( result );
break;
case TR_LINEAR:
VectorCopy( tr->trDelta, result );
break;
case TR_SINE:
deltaTime = ( atTime - tr->trTime ) /
(float) tr->trDuration;
phase = cos( deltaTime * M_PI * 2 );
phase *= 0.5;
VectorScale( tr->trDelta, phase, result );
break;
case TR_LINEAR_STOP:
if ( atTime > tr->trTime + tr->trDuration ) {
VectorClear( result );
return;
}
VectorCopy( tr->trDelta, result );
break;
case TR_GRAVITY:
deltaTime = ( atTime - tr->trTime ) * 0.001;
VectorCopy( tr->trDelta, result );
result[2] -= DEFAULT_GRAVITY * deltaTime;
break;
case TR_ACCEL:
// time since missile fired in seconds
deltaTime = ( atTime - tr->trTime ) * 0.001;

// Turn magnitude of acceleration into a vector
VectorCopy(tr->trDelta,dir);
VectorNormalize (dir);
VectorScale (dir, tr->trDuration, dir);

// u + t * a = v
VectorMA (tr->trDelta, deltaTime, dir, result);
break;
default:
Com_Error( ERR_DROP,
"BG_EvaluateTrajectoryDelta:
unknown trType: %i",
tr->trTime );
break;
}
}
```
And that's pretty much everything you need to know to make a new trType. Now all we need to do is implement it into the rocket launcher. Open g_missile.c and look for the function fire_rocket(). Towards the bottom you should see something like:
```	bolt->clipmask = MASK_SHOT;

bolt->s.pos.trType = TR_LINEAR;
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;
VectorCopy( start, bolt->s.pos.trBase );
VectorScale( dir, 800, bolt->s.pos.trDelta );
SnapVector( bolt->s.pos.trDelta );
VectorCopy (start, bolt->r.currentOrigin);

return bolt;
```
We need to change the trType so Quake knows to use our equation, then we need to add an accaleration value (a.k.a. trDuration) and then lower the rocket's initial velocity. And the end result is:
```	bolt->clipmask = MASK_SHOT;

bolt->s.pos.trType = TR_ACCEL;
bolt->s.pos.trDuration = 500;
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;
VectorCopy( start, bolt->s.pos.trBase );
VectorScale( dir, 50, bolt->s.pos.trDelta );
SnapVector( bolt->s.pos.trDelta );
VectorCopy (start, bolt->r.currentOrigin);

return bolt;
```

You can test this by starting a map and firing a rocket while running forward. At first you'll run faster than the rocket, then it'll overtake you with a doppler whoosh!

#### 3. Problems and warnings

I encountered a few problems while making this tutorial that you all might be interested in. Firstly and most importantly, the cgame code uses the BG_EvaluateTrajectory function heavily. Even though we didn't change anything, if you're using .qvm files you'll need to compile cgame.qvm after making the above modifications otherwise the game will hang every time someone fires a rocket.

Another one, as I mentioned earlier, is that adding a variable to the trajectory_t typedef causes some wierd things to happen in the game. Try it. You'll be able to walk through doors without them opening, weapons won't work properly, all the items on the map will be missing and the jump pads all get mixed up and launch you in the wrong directions. This happens because trajectory_t is used in the executable, and even though we modify it in q_shared.h, we never recompile the executable so it can see the changes.

Finally, be careful what equations you use in BG_EvaluateTrajectory. For example, a variation on the code we used is as follows:

```case TR_ACCEL:
deltaTime = ( atTime - tr->trTime ) * 0.001;
speed = tr->trDelta[0] * tr->trDelta[0]
+ tr->trDelta[1] * tr->trDelta[1]
+ tr->trDelta[2] * tr->trDelta [2];
speed = sqrt(speed);
//Don't forget to add speed variable at
//the start (as float)
phase = ((tr->trDuration / 2) * (deltaTime*deltaTime))
+ (speed * deltaTime);
VectorCopy (tr->trDelta, dir);
VectorNormalize (dir);
VectorMA (tr->trBase, phase, dir, result);
break;
```
Try this. Everything on the server side works fine, but something happens with the client game. The client side code predicts the rocket as floating in mid air while the server game predicts the rocket as it should. Try it for yourself to see what I mean. I think the problem is the sqrt() function.

If anyone can find solutions to these problems, drop me a line 'cause I'd be interested to know. Also, I was thinking of making a more complicated trType for use in cluster rockets. It's also not too hard to make a bfg that bobs up and down and sprays plasma everywhere.

Any questions, comments, email me at [email protected].

The problem with the sqrt() method not working (described in section 3 above) is more subtle than it first appears. It turns out that it isn't the sqrt() function that's causing the problem.

It's very important to have a clear idea in your mind of where data is stored and how it's transmitted between the client and server. There are four places where game play data can be stored:

1. The server Virtual Machine (VM)
2. The server executable
3. The client executable
4. The client VM

Take the creation of a rocket as an example. It first exists within the server VM (1) as a data structure initialized by fire_rocket(). It's not until trap_LinkEntity() is called that the server executable (2) is aware of the entity.

It then becomes the responsibility of the server executable to transmit information about the entity to the client executable (3). The client VM (4) then picks up a copy of the information about the entity. Repeatedly calling trap_LinkEntity() in the server will only cause updated information to be sent to each client if the data structure itself changes.

Ideally we would only like to do the following with our entity: create it, and then destroy it only when it next interacts with the world. This is what allows the client to "move/predict" the entity for us.

Why does the code in section 3 not work? The data in the trajectory_t structure is only linked into the server executable (2) once. The client only sees the initial value of trDelta (magnitude 50, set in fire_rocket()). For an accelerating rocket, trDelta actually represents the direction it was fired in.

So how does the proper code run? It takes the time of creation for the rocket trTime (which is fixed and reliable), and calculates where the rocket should be based on the elapsed time. trDelta is scaled to the correct time elapsed magnitude before use. Because this happens in the bg_* code, both client and server will agree on the movement and prediction of the rocket (without transmitting any data across the network).

Should the rocket entity be re-linked with an updated trDelta? DEFINITELY NOT! This would take up extra bandwidth, would arrive at the client too late to be reliable, and destroy the "fire and forget" method we're trying to preserve.

It's up to you to decide which parts of the trajectory_t need to be updated - the less frequently the better. Idealy it should only be done when there is a *drastic* and sharp change in a value (like an accelerating rocket bouncing off of a wall).

Server movement and client prediction will only agree when the changes are localized within the bg_* set of files. Any server side modification only will cause some degree of prediction error, but remember that the server has the definitive view of the world. Prediction is a gameplay assistance, not a cast iron view of the world. The server can make changes like that.

One other interesting side effect: slow moving objects have a granularity in the direction they can be fired. This is because trDelta gets rounded to integer values. Using the zoom view you can see that these accelerating rockets might be travelling slightly off centre. A magnitude of 50 is about the smallest you can get away with in small arenas. If you want a slow moving and accurate entity, then you'll have to create a special movement type for the job.

 PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 38 | Next >>