
Quake DeveLS - JetPack 2
Author: Attila
Difficulty: Hard
Ed's note: this new
JetPack tutorial is more than just a simple
how-to-make-a-quick-quake2-code-hack. It shows you how to make a very polished
and professional (albiet small) mod. This JetPack gives you directional control
(fly upwards wrt the way you face), pick up the jetpack instead of the quad,
get progressively less thrust as you run low on fuel, explode in fiery fury if
you're hit whilst flying and more.
Oh, and don't even try to
go swimming with this little monkey on your back ;]
[/SumFuka]
Some short notes:
· This tutorial
draws parts from the original JetPack tutorial by Muce and SumFuka, but is
significantlt different to warrant a clean slate. Start with a fresh codebase.
· Changes in
existing files begin at the bottom of these files, so the linenumbers are
always valid.
· Jetpack only
works fine on low ping connections like a LAN, at high pings the client side
prediction will make it a little jerky
· Some probs
can be solved if you change some values (rolling, friction, acceleration)
1. Changes
in local.h
Add the following 3 Jet_
variables to the gclient_s struct in file g_local.h at line 819:
float enviro_framenum; /*ATTILA begin*/ float Jet_framenum; /*burn out time when jet is activated*/ float Jet_remaining; /*remaining fuel time*/ float Jet_next_think; /*ATTILA end*/ qboolean grenade_blew_up;
They will
be used for activating/deactivating and for sync applying speed.
2. Changes
in g_items.c
First for the jetpack
itself. We use the quad damage struct and modify it, so it will be a jetpack.
Go to line 1493 and add the following modified quad damage struct. After that,
comment the original quad struct.
// // POWERUP ITEMS // /*ATTILA begin*/ /*QUAKED item_jet (.3 .3 1) (-16 -16 -16) (16 16 16) */ { "item_quad", Pickup_Powerup, Use_Jet, /*ATTILA the Use_Jet function from above*/ NULL, /*ATTILA No dropping function for jetpack*/ NULL, "items/pkup.wav", /*ATTILA this will show the monster icarus instead of quad damage in the game*/ "models/monsters/hover/tris.md2", EF_ROTATE, NULL, /* icon */ "p_quad", /*ATTILA Ok, its the quad icon on screen but who cares*/ /* pickup */ "Jetpack", /*ATTILA now we can use it with the use command*/ /* width */ 2, 60, /*ATTILA respwan after 60 secs*/ NULL, 0, NULL, 0, /* precache */ "hover/hovidle1.wav items/damage.wav items/damage2.wav items/damage3.wav" }, /*Attila end*/ /*ATTILA This is the original quad damage struct. It has to be commented.*/ /* //QUAKED item_quad (.3 .3 1) (-16 -16 -16) (16 16 16) { "item_quad", Pickup_Powerup, Use_Quad, Drop_General, NULL, "items/pkup.wav", "models/items/quaddama/tris.md2", EF_ROTATE, NULL, // icon "p_quad", // pickup "Quad Damage", // width 2, 60, NULL, 0, NULL, 0,// precache "items/damage.wav items/damage2.wav items/damage3.wav" },*/
Now add
the following lines to the Pickup_Powerup function in file g_items.c at line
140:
other->client->pers.inventory[ITEM_INDEX(ent->item)]++; /*ATTILA begin*/ if ( Q_stricmp(ent->item->pickup_name, "Jetpack") == 0 ) { other->client->pers.inventory[ITEM_INDEX(ent->item)] = 1; other->client->Jet_remaining = 700; /*if deathmatch-flag instant use is set, switch off the jetpack, the item->use function will turn it on again immediately*/ if ( (int)dmflags->value & DF_INSTANT_ITEMS ) other->client->Jet_framenum = 0; /*otherwise update the burn out time if jetpack is activated*/ else if ( Jet_Active(other) ) other->client->Jet_framenum = level.framenum + other->client->Jet_remaining; } /*ATTILA end*/ if (deathmatch->value)
If the
pickup item is a jetpack, we make sure that we never get more than 1 in our
inventory (jetpacks are big :-)) Then set the remaining fuel time to 70 secs,
at the end check some activation cases.
Now we need to add a new
function called Use_Jet for activating/deactivating. I suggest you add it at
the top of g_items.c as first function (function must be known to this module,
if you want to place it in another file, use a prototype)
/*ATTILA begin*/ void Use_Jet ( edict_t *ent, gitem_t *item ) { ValidateSelectedItem ( ent ); /*jetpack in inventory but no fuel time? must be one of the give all/give jetpack cheats, so put fuel in*/ if ( ent->client->Jet_remaining == 0 ) ent->client->Jet_remaining = 700; if ( Jet_Active(ent) ) ent->client->Jet_framenum = 0; else ent->client->Jet_framenum = level.framenum + ent->client->Jet_remaining; /*The On/Off Sound taken from the invulnerability*/ gi.sound( ent, CHAN_ITEM, gi.soundindex("items/protect.wav"), 0.8, ATTN_NORM, 0 ); /*this is the sound played when flying. To here this sound immediately we play it here the first time*/ gi.sound ( ent, CHAN_AUTO, gi.soundindex("hover/hovidle1.wav"), 0.8, ATTN_NORM, 0 ); } /*ATTILA end*/
If the jet
is already active switch it off, otherwise switch on setting the burn out time.
This function will be called if there is a jetpack in the inventory and we give
the command "use jetpack". With deathmatchflag use instant set, it
will be called automatically when the jetpack is picked up.
3. Changes
in p_view.c
So you think you can boil
some water with your jetpack? Add this to function P_WorldEffects, line 637:
if (waterlevel == 3) { /*ATTILA begin*/ if ( Jet_Active(current_player) ) /*dont jet and dive and stay alive*/ T_Damage (current_player, world, world, vec3_origin, current_player->s.origin, vec3_origin, current_player->health+1, 0, DAMAGE_NO_ARMOR); /*ATTILA end*/ // breather or envirosuit give air
Now add
the following lines to the SV_CalcBlend function in file p_view.c at line 424:
// add for powerups /*ATTILA begin*/ if ( Jet_Active(ent) ) { /*GOD -> dont burn out*/ if ( ent->flags & FL_GODMODE ) if ( (ent->client->Jet_framenum - level.framenum) <= 100 ) ent->client->Jet_framenum = level.framenum + 700; /*update the fuel time*/ ent->client->Jet_remaining = ent->client->Jet_framenum - level.framenum; /*if no fuel remaining, remove jetpack from inventory*/ if ( ent->client->Jet_remaining == 0 ) ent->client->pers.inventory[ITEM_INDEX(FindItem("Jetpack"))] = 0; /*Play jetting sound every 0.6 secs (sound of monster icarus)*/ if ( ((int)ent->client->Jet_remaining % 6) == 0 ) gi.sound (ent, CHAN_AUTO, gi.soundindex("hover/hovidle1.wav"), 0.9, ATTN_NORM, 0); /*beginning to fade if 4 secs or less*/ if (ent->client->Jet_remaining <= 40) /*play on/off sound every sec*/ if ( ((int)ent->client->Jet_remaining % 10) == 0 ) gi.sound(ent, CHAN_ITEM, gi.soundindex("items/protect.wav"), 1, ATTN_NORM, 0); if (ent->client->Jet_remaining > 40 || ( (int)ent->client->Jet_remaining & 4) ) SV_AddBlend (0, 0, 1, 0.08, ent->client->ps.blend); } else /*ATTILA end*/ if (ent->client->quad_framenum > level.framenum)
This will
play some sounds, add the Blend and remove the jetpack from the inventory when
burned out.
4. Changes
in p_hud.c
First we place the icon
and counting time on screen when the jet is activated. Add this at line 357
// // timers // /*ATTILA begin*/ if ( Jet_Active(ent) ) { ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_quad"); ent->client->ps.stats[STAT_TIMER] = ent->client->Jet_remaining/10; } else /*ATTILA end*/ if (ent->client->quad_framenum > level.framenum)
This shows
the quad damage icon on the screen and displays the remaining fuel time when
then jet is activated. Now add the following lines to the
MoveClientToIntermission function in file p_hud.c at line 26:
// clean up powerup info /*ATTILA begin*/ ent->client->Jet_framenum = 0; ent->client->Jet_remaining = 0; /*ATTILA end*/ ent->client->quad_framenum = 0;
This stops
jetting when the level is exited.
5. Changes
in p_client.c
a) function ClientThink,
line 1012
client->resp.cmd_angles[2] = SHORT2ANGLE(ucmd->angles[2]); /*ATTILA begin*/ if ( Jet_Active(ent) ) if( pm.groundentity ) /*are we on ground*/ if ( Jet_AvoidGround(ent) ) /*then lift us if possible*/ pm.groundentity = NULL; /*now we are no longer on ground*/ /*ATTILA end*/ if (ent->groundentity && !pm.groundentity ...
So we
never get on ground and avoid some odd oscillating effects.
b) function ClientThink, line
1003
ent->s.origin[i] = pm.s.origin[i]*0.125; /*ATTILA begin*/ if ( !Jet_Active(ent) || (Jet_Active(ent)&&(fabs((float)pm.s.velocity[i]*0.125) < fabs(ent->velocity[i]))) ) /*ATTILA end*/ ent->velocity[i] = pm.s.velocity[i]*0.125;
This prevents
that the "normal walk velocity" is added. Its only used, when we fly
against a wall or something else.
c) function ClientThink,
line 974
client->ps.pmove.gravity = sv_gravity->value; /*ATTILA begin*/ if ( Jet_Active(ent) ) Jet_ApplyJet( ent, ucmd ); /*ATTILA end*/ pm.s = client->ps.pmove;
This will
call the jet movement function every client-think.
d) function player_die,
line 175
memset (self->client->pers.inventory, ... } /*ATTILA begin*/ if ( Jet_Active(self) ) { Jet_BecomeExplosion( self, damage ); /*stop jetting when dead*/ self->client->Jet_framenum = 0; } else /*ATTILA end*/ if (self->health < -40)
If the jet
is activated when killed, you will be gibbed by a little explosion.
6. making
a new file jet.c
Make a new file called
jet.c and add it to the project. You can copy the following code directly into
that file.
/*begin of jet.c*/ #include "g_local.h" /*we get silly velocity-effects when we are on ground and try to accelerate, so lift us a little bit if possible*/qboolean Jet_AvoidGround( edict_t *ent ){ vec3_t new_origin; trace_t trace; qboolean success; /*Check if there is enough room above us before we change origin[2]*/ new_origin[0] = ent->s.origin[0]; new_origin[1] = ent->s.origin[1]; new_origin[2] = ent->s.origin[2] + 0.5; trace = gi.trace( ent->s.origin, ent->mins, ent->maxs, new_origin, ent, MASK_MONSTERSOLID ); if ( success=(trace.plane.normal[2]==0) ) /*no ceiling?*/ ent->s.origin[2] += 0.5; /*then make sure off ground*/ return success;} /*This function returns true if the jet is activated (surprise, surprise)*/qboolean Jet_Active( edict_t *ent ){ return ( ent->client->Jet_framenum >= level.framenum );} /*If a player dies with activated jetpack this function will be called and produces a little explosion*/void Jet_BecomeExplosion( edict_t *ent, int damage ){ int n; gi.WriteByte( svc_temp_entity ); gi.WriteByte( TE_EXPLOSION1 ); /*TE_EXPLOSION2 is possible too*/ gi.WritePosition( ent->s.origin ); gi.multicast( ent->s.origin, MULTICAST_PVS ); gi.sound( ent, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0 ); /*throw some gib*/ for ( n=0; n<4; n++ ) ThrowGib( ent, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC ); ThrowClientHead( ent, damage ); ent->takedamage = DAMAGE_NO; } /*The lifting effect is done through changing the origin, it gives the best results. Of course its a little dangerous because if we dont take care, we can move into solid*/void Jet_ApplyLifting( edict_t *ent ){ float delta; vec3_t new_origin; trace_t trace; int time = 24; /*must be >0, time/10 = time in sec for a complete cycle (up/down)*/ float amplitude = 2.0; /*calculate the z-distance to lift in this step*/ delta = sin( (float)((level.framenum%time)*(360/time))/180*M_PI ) * amplitude; delta = (float)((int)(delta*8))/8; /*round to multiples of 0.125*/ VectorCopy( ent->s.origin, new_origin ); new_origin[2] += delta; if( VectorLength(ent->velocity) == 0 ) { /*i dont know the reason yet, but there is some floating so we have to compensate that here (only if there is no velocity left)*/ new_origin[0] -= 0.125; new_origin[1] -= 0.125; new_origin[2] -= 0.125; } /*before we change origin, its important to check that we dont go into solid*/ trace = gi.trace( ent->s.origin, ent->mins, ent->maxs, new_origin, ent, MASK_MONSTERSOLID ); if ( trace.plane.normal[2] == 0 ) VectorCopy( new_origin, ent->s.origin );} /*This function applys some sparks to your jetpack, this part is exactly copied from Muce's and SumFuka's JetPack-tutorial and does a very nice effect.*/void Jet_ApplySparks ( edict_t *ent ){ vec3_t forward, right; vec3_t pack_pos, jet_vector; AngleVectors(ent->client->v_angle, forward, right, NULL); VectorScale (forward, -7, pack_pos); VectorAdd (pack_pos, ent->s.origin, pack_pos); pack_pos[2] += (ent->viewheight); VectorScale (forward, -50, jet_vector); gi.WriteByte (svc_temp_entity); gi.WriteByte (TE_SPARKS); gi.WritePosition (pack_pos); gi.WriteDir (jet_vector); gi.multicast (pack_pos, MULTICAST_PVS);} /*if the angle of the velocity vector is different to the viewing angle (flying curves or stepping left/right) we get a dotproduct which is here used for rolling*/void Jet_ApplyRolling( edict_t *ent, vec3_t right ){ float roll, value = 0.05, sign = -1; /*set this to +1 if you want to roll contrariwise*/ roll = DotProduct( ent->velocity, right ) * value * sign; ent->client->kick_angles[ROLL] = roll;} /*Now for the main movement code. The steering is a lot like in water, that means your viewing direction is your moving direction. You have three direction Boosters: the big Main Booster and the smaller up-down and left-right Boosters. There are only 2 adds to the code of the first tutorial: the Jet_next_think and the rolling. The other modifications results in the use of the built-in quake functions, there is no change in moving behavior (reinventing the wheel is a lot of "fun" and a BIG waste of time ;-))*/void Jet_ApplyJet( edict_t *ent, usercmd_t *ucmd ){ float direction; vec3_t acc; vec3_t forward, right; int i; /*clear gravity so we dont have to compensate it with the Boosters*/ ent->client->ps.pmove.gravity = 0; /*calculate the direction vectors dependent on viewing direction (length of the vectors forward/right is always 1, the coordinates of the vectors are values of how much youre looking in a specific direction [if youre looking up to the top, the x/y values are nearly 0 the z value is nearly 1])*/ AngleVectors( ent->client->v_angle, forward, right, NULL ); /*Run jet only 10 times a second so movement dont depends on fps because ClientThink is called as often as possible (fps<10 still is a problem ?)*/ if ( ent->client->Jet_next_think <= level.framenum ) { ent->client->Jet_next_think = level.framenum + 1; /*clear acceleration-vector*/ VectorClear( acc ); /*if we are moving forward or backward add MainBooster acceleration (60)*/ if ( ucmd->forwardmove ) { /*are we accelerating backward or forward?*/ direction = (ucmd->forwardmove<0) ? -1.0 : 1.0; /*add the acceleration for each direction*/ acc[0] += direction * forward[0] * 60; acc[1] += direction * forward[1] * 60; acc[2] += direction * forward[2] * 60; } /*if we sidestep add Left-Right-Booster acceleration (40)*/ if ( ucmd->sidemove ) { /*are we accelerating left or right*/ direction = (ucmd->sidemove<0) ? -1.0 : 1.0; /*add only to x and y acceleration*/ acc[0] += right[0] * direction * 40; acc[1] += right[1] * direction * 40; } /*if we crouch or jump add Up-Down-Booster acceleration (30)*/ if ( ucmd->upmove ) acc[2] += ucmd->upmove > 0 ? 30 : -30; /*now apply some friction dependent on velocity (higher velocity results in higher friction), without acceleration this will reduce the velocity to 0 in a few steps*/ ent->velocity[0] += -(ent->velocity[0]/6.0); ent->velocity[1] += -(ent->velocity[1]/6.0); ent->velocity[2] += -(ent->velocity[2]/7.0); /*then accelerate with the calculated values. If the new acceleration for a direction is smaller than an earlier, the friction will reduce the speed in that direction to the new value in a few steps, so if youre flying curves or around corners youre floating a little bit in the old direction*/ VectorAdd( ent->velocity, acc, ent->velocity ); /*round velocitys (is this necessary?)*/ ent->velocity[0] = (float)((int)(ent->velocity[0]*8))/8; ent->velocity[1] = (float)((int)(ent->velocity[1]*8))/8; ent->velocity[2] = (float)((int)(ent->velocity[2]*8))/8; /*Bound velocitys so that friction and acceleration dont need to be synced on maxvelocitys*/ for ( i=0 ; i<2 ; i++) /*allow z-velocity to be greater*/ { if (ent->velocity[i] > 300) ent->velocity[i] = 300; else if (ent->velocity[i] < -300) ent->velocity[i] = -300; } /*add some gentle up and down when idle (not accelerating)*/ if( VectorLength(acc) == 0 ) Jet_ApplyLifting( ent ); }//if ( ent->client->Jet_next_think... /*add rolling when we fly curves or boost left/right*/ Jet_ApplyRolling( ent, right ); /*last but not least add some smoke*/ Jet_ApplySparks( ent ); } /*end of jet.c*/
You can
use the jetpack like other items by selecting it in the inventory or binding it
to a key in your autoexec.cfg: bind j use Jetpack or any other key.
Happy jetting
Tutorial by Attila .
|
This site, and all
content and graphics displayed on it, |