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 doug@enfiniti.com.
4. Addendum by HypoThermia
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:
- The server Virtual Machine (VM)
- The server executable
- The client executable
- 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.
|