Code3Arena

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

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    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:

    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 >>