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


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.

      /*ATTILA begin*/
      /*QUAKED item_jet (.3 .3 1) (-16 -16 -16) (16 16 16)
        Use_Jet,        /*ATTILA the Use_Jet function from above*/
        NULL,           /*ATTILA No dropping function for jetpack*/
        /*ATTILA this will show the monster icarus instead of quad damage in 
          the game*/
        "models/monsters/hover/tris.md2", EF_ROTATE, 
      /* 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*/ 
      /* 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 
      //QUAKED item_quad (.3 .3 1) (-16 -16 -16) (16 16 16)
        "models/items/quaddama/tris.md2", EF_ROTATE,
        // icon
        // pickup
              "Quad Damage",
        // width
    // 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:
      /*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*/
          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; 
          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);
      /*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;
      /*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;
      /*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
        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,
    are ©opyrighted to the Quake DeveLS team. All rights received.
    Got a suggestion? Comment? Question? Hate mail? Send it to us!
    Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
    Thanks to Planet Quake for their great help and support with hosting.
    Best viewed with Netscape 4