TUTORIAL 40 - Advanced Grapple Hook
by Kilderean
After Matt Ownby's tutorial for activating
the grapple hook, we're going to add more functionality in three
ways. First we'll give an option of turning off the grapple, then a
faster movement version, fix some bounce pad problems, and finally we'll
give the bots a chance to use the grapple!
This tutorial has been taken from the source code for PureCTF, here in
the modsource section of Code3Arena. With some of the classic Q2 CTF
maps converted to Q3, and a return to the old grapple form of CTF,
you'll want to visit the PureCTF
mod home page for some pure fun.
1. Enabling the hook, PureCTF style
Matt's grapple tutorial is in two
parts, enabling the grapple in g_client.c, and giving a key bind
in the ui. The key bind code in the user interface doesn't need to be
changed. We'll make an addition to the server code, adding a cvar that
controls whether the grapple is enabled.
The cvar addition is, as usual, quite simple. In g_main.c we
add a variable for storing cvar info:
vmCvar_t g_enableBreath;
vmCvar_t g_proxMineTimeout;
#endif
// KILDEREAN
vmCvar_t g_PureAllowHook;
// END KILDEREAN
And within the gameCvarTable[] array we add the default initialization:
{ &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse},
{ &g_rankings, "g_rankings", "0", 0, 0, qfalse},
// KILDEREAN
{ &g_PureAllowHook, "g_PureAllowHook", "1", CVAR_SERVERINFO | CVAR_LATCH, 0, qfalse }
// END KILDEREAN
};
The use of CVAR_LATCH defers the change of the server cvar
until the next map is loaded. On general principle you must latch
a cvar that will only take effect on a map change. This allows the
current value to still be used, and prevents something inconsistant from
happening.
And CVAR_SERVERINFO means that this information will be
available for all clients to act upon. More on this later.
This cvar should also be available to the rest of the server code,
so we add an extern declaration to g_local.h at around line 747:
extern vmCvar_t g_singlePlayer;
extern vmCvar_t g_proxMineTimeout;
#endif
// KILDEREAN
extern vmCvar_t g_PureAllowHook;
// END KILDEREANM
Then in g_client.c we need to change how the grappling hook is
given to the player. This replaces the first part of how Matt forced the
grapple hook onto a player. Function ClientSpawn(), around line
1160:
if ( g_gametype.integer == GT_TEAM ) {
client->ps.ammo[WP_MACHINEGUN] = 50;
} else {
client->ps.ammo[WP_MACHINEGUN] = 100;
}
//KILDEREAN
if(g_PureAllowHook.integer){
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GRAPPLING_HOOK);
client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
}
//END KILDEREAN
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GAUNTLET );
client->ps.ammo[WP_GAUNTLET] = -1;
// client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
// health will count down towards max_health
ent->health = client->ps.stats[STAT_HEALTH] =
client->ps.stats[STAT_MAX_HEALTH] + 25;
You should also make the second change by Matt that prevents the
grapple being selected as the default weapon.
The hook should also be loaded on map startup, so we add the following into
ClearRegisteredItems() in g_items.c, around line 785:
RegisterItem( BG_FindItemForWeapon( WP_MACHINEGUN ) );
RegisterItem( BG_FindItemForWeapon( WP_GAUNTLET ) );
//KILDEREAN
if(g_PureAllowHook.integer)
RegisterItem( BG_FindItemForWeapon( WP_GRAPPLING_HOOK ) );
// END KILDEREAN
#ifdef MISSIONPACK
Finally, there are some details within the client that need
attention.
The grapple can cause an "Out of Ammo" message. The
simplest thing to do is just remove it when allowing the hook, though a
more refined check for actually using the hook is also possible.
To do this we need to grab the hook state from the server status,
only sent to the client because the CVAR_SERVERINFO flag was set.
Add this variable to the client game state cgs_t struct in
cg_local.h, about line 1035:
int acceptTask;
int acceptLeader;
char acceptVoice[MAX_NAME_LENGTH];
// media
cgMedia_t media;
// KILDEREAN
int allowhook;
// END KILDEREAN
We grab this value in CG_ParseServerinfo(), which is called
when the gamestate is first sent, and when any CVAR_SERVERINFO
cvars are changed. Open cg_servercmds.c at about line 123:
cgs.capturelimit = atoi( Info_ValueForKey( info, "capturelimit" ) );
cgs.timelimit = atoi( Info_ValueForKey( info, "timelimit" ) );
cgs.maxclients = atoi( Info_ValueForKey( info, "sv_maxclients" ) );
// KILDEREAN: g_PureAllowHook
cgs.allowhook = atoi( Info_ValueForKey( info, "g_PureAllowHook" ) );
// END KILDEREAN
Note that we can only access the g_PureAllowHook in the client
because it has the CVAR_SERVERINFO flag set in the server.
When we do the out of ammo display check, we don't print a message if
the hook is enabled. In cg_draw.c, change CG_Draw2D() at
about line 2503:
#else
CG_DrawStatusBar();
#endif
// KILDEREAN: screw ammo warning when g_PureAllowHook=1
if (!cgs.allowhook)
CG_DrawAmmoWarning();
// END KILDEREAN
The mod source code also
shows how to display the server gameplay options during the level
startup splash screen. You'll find this code in the
CG_DrawInformation() function in cg_info.c. If your mod
has the option of major gameplay changes, then announcing it to
connecting players like this is a Good Thing(tm).
Finally, since the default grapple uses the lightning shaft shader,
we need to initialize this shader. For maps that don't have a lightning
gun, we won't otherwise get this set up. These changes go into
cg_weapons.c in CG_RegisterWeapon() at about line 535:
case WP_GRAPPLING_HOOK:
cgs.media.lightningShader = trap_R_RegisterShader( "lightningBolt");
MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f );
weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/rocket/rocket.md3" );
weaponInfo->missileTrailFunc = CG_GrappleTrail;
The Pure CTF grapple actually replaces this "lightning"
shaft with a proper rope and hook. There's a correspondingly different
setup for the code, a model change for the grapple weapon, and drawing
method for the grapple itself.
If you want to use the rope version then you'll have to
contact Kilderean for
permission to include the media in your mod.
|
2. Fast hook
Having two speeds of hook certainly makes for more interesting play,
so we'll take a look on how to implement that. Along the way we're going
to find out that the grapple speed will be used in the movemement code
common to the server and the client (bg_*.c files).
These changes assume that you've already done the work in section 1 above.
As before, we need a cvar that flags whether the fast hook is enabled.
In g_main.c:
vmCvar_t g_enableBreath;
vmCvar_t g_proxMineTimeout;
#endif
// KILDEREAN
vmCvar_t g_PureAllowHook;
vmCvar_t g_PureFastHook;
// END KILDEREAN
and in the gameCvarTable[] array:
{ &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse},
{ &g_rankings, "g_rankings", "0", 0, 0, qfalse},
// KILDEREAN
{ &g_PureFastHook, "g_PureFastHook", "1", CVAR_SERVERINFO, 0, qfalse },
{ &g_PureAllowHook, "g_PureAllowHook", "1", CVAR_SERVERINFO | CVAR_LATCH, 0, qfalse }
// END KILDEREAN
};
and finally an extern in the g_local.h:
extern vmCvar_t g_singlePlayer;
extern vmCvar_t g_proxMineTimeout;
#endif
// KILDEREAN
extern vmCvar_t g_PureFastHook;
extern vmCvar_t g_PureAllowHook;
// END KILDEREANM
Adding this cvar has complicated matters. We need to make sure that
client prediction knows how fast the grapple should move. This means
working in bg_pmove.c where we don't have direct access to the
cvars.
The way to tackle this problem is to make use of the pmove_t
structure that stores the actual or predicted movement. In fact, any cvar
value (that doesn't change very often) can be put into this structure,
just so long as you remember to do so from both the server and the
client end of the call to Pmove().
First we define the slow and fast speeds in bg_public.h at
about line 31:
#define CROUCH_VIEWHEIGHT 12
#define DEAD_VIEWHEIGHT -16
// KILDEREAN : grapple pull speed
#define PURE_FASTGRAPPLE 1000
#define PURE_NORMALGRAPPLE 700
// END KILDEREAN
Then we put the flag into the pmove_t struct in
bg_public.h, about line 170:
// callbacks to test the world
// these will be different functions during game and cgame
void (*trace)( trace_t *results, const vec3_t start,
const vec3_t mins, const vec3_t maxs, const vec3_t end,
int passEntityNum, int contentMask );
int (*pointcontents)( const vec3_t point, int passEntityNum );
// KILDEREAN : so we can implement fast grappling
qboolean fastGrapple;
// END KILDEREAN
} pmove_t;
Then we need to set the flag in the server just before Pmove()
is called. Easily done because we have direct access to the cvar. The
modification goes into g_active.c, in ClientThink_real()
at about line 862.
pm.pointcontents = trap_PointContents;
pm.debugLevel = g_debugMove.integer;
pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0;
// KILDEREAN
if(g_PureFastHook.integer == 1)
pm.fastGrapple = qtrue;
else
pm.fastGrapple = qfalse;
// END KILDEREAN
The modification in the client is a little more challenging, but not
too difficult. Again we need to add a state to the client global
structure data, in cg_local.h at about line 1035:
int acceptTask;
int acceptLeader;
char acceptVoice[MAX_NAME_LENGTH];
// media
cgMedia_t media;
// KILDEREAN
int fastGrapple;
int allowhook;
// END KILDEREAN
Then set this new flag within CG_ParseServerinfo() in
cg_servercmds.c at about line 123:
cgs.capturelimit = atoi( Info_ValueForKey( info, "capturelimit" ) );
cgs.timelimit = atoi( Info_ValueForKey( info, "timelimit" ) );
cgs.maxclients = atoi( Info_ValueForKey( info, "sv_maxclients" ) );
// KILDEREAN: g_PureAllowHook
cgs.fastGrapple = atoi( Info_ValueForKey( info, "g_PureFastHook" ) );
cgs.allowhook = atoi( Info_ValueForKey( info, "g_PureAllowHook" ) );
// END KILDEREAN
And finally copy the value into the pmove_t structure for use
within Pmove(). The changes go into
CG_PredictPlayerState() in cg_predict.c at around line 437:
cg_pmove.noFootsteps = ( cgs.dmflags & DF_NO_FOOTSTEPS ) > 0;
// KILDEREAN
if(cgs.fastGrapple == 1)
cg_pmove.fastGrapple = qtrue;
else
cg_pmove.fastGrapple = qfalse;
// END KILDEREAN
// save the state before the pmove so we can detect transitions
oldPlayerState = cg.predictedPlayerState;
With fastGrapple set up, all that remains is to use it within
PM_GrappleMove() in bg_pmove.c (line 656):
if (vlen <= 100)
VectorScale(vel, 10 * vlen, vel);
// KILDEREAN
else{
if(pm->fastGrapple)
VectorScale(vel,PURE_FASTGRAPPLE, vel);
else
VectorScale(vel,PURE_NORMALGRAPPLE, vel);
}
// END KILDEREAN
VectorCopy(vel, pm->ps->velocity);
3. Curing bounce pads
If you've implemented the grapple code so and given it a whirl, then
you'll soon find there's a problem with bounce pads. Sound effects are
often played again and again, while jump pads that kick you high enough
to give falling damage will actually damage you repeatedly.
The server side must be fixed first, this is the definitive check
that we've actually hit the bouncepad. In g_trigger.c we update
trigger_push_touch, at about line 123:
void trigger_push_touch (gentity_t *self,
gentity_t *other, trace_t *trace ) {
if ( !other->client ) {
return;
}
// KILDEREAN
if (other->client && other->client->hook)
return;
// END KILDEREAN
BG_TouchJumpPad( &other->client->ps, &self->s );
}
While this corrects the server side of the problem, there's still the
client side prediction of a bounce pad hit that needs to be fixed too.
We need to add a variable that is true when we're firing the
grapple. In cg_local.h in the structure playerEntity_t, line 140:
// machinegun spinning
float barrelAngle;
int barrelTime;
qboolean barrelSpinning;
// KILDEREAN
int grappleFiring;
// END KILDEREAN
} playerEntity_t;
Then we have to set or clear this flag based on predicted movement in
CG_AddPlayerWeapon() in cg_weapons.c, about line 1004:
if ( !ps ) {
// add weapon ready sound
cent->pe.lightningFiring = qfalse;
// KILDEREAN
cent->pe.grappleFiring = qfalse;
// END KILDEREAN
if ( ( cent->currentState.eFlags & EF_FIRING ) && weapon->firingSound) {
// lightning gun and guantlet make a different sound when fire is held down
trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
vec3_origin, weapon->firingSound );
cent->pe.lightningFiring = qtrue;
// KILDEREAN + HYPOTHERMIA
if (cg.predictedPlayerState.weapon == WP_GRAPPLING_HOOK)
cent->pe.grappleFiring = qtrue;
// END KILDEREAN
} else if ( weapon->readySound ) {
trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
vec3_origin, weapon->readySound );
}
}
The check for the weapon type (above) fixes a small bug in PureCTF
that disables jump pad prediction when firing the lightning gun or
gauntlet.
|
The last part is in cg_predict.c in
CG_TouchTriggerPrediction(), where we make the following addition
at about line 354:
if ( ent->eType == ET_TELEPORT_TRIGGER ) {
cg.hyperspace = qtrue;
} else if ( ent->eType == ET_PUSH_TRIGGER ) {
// KILDEREAN : grappling disables jumpad predict
// FIXME : maybe because this function differs from 1.17
if ( cg.predictedPlayerEntity.pe.grappleFiring ) {
continue;
}
// END KILDEREAN
BG_TouchJumpPad( &cg.predictedPlayerState, ent );
}
4. Making the bots use the grapple
Now that you've added your grapple into the code, you'll want the
bots to use this weapon properly as well!
PureCTF includes a bug fix for the bot grapple provided by Mr
Elusive, author of the bot code in in Q3. The fix correctly puts the
view offset relative to the bot. The change needs to be made to
ai_dmq3.c in BotSetupForMovement() at around line 1574:
void BotSetupForMovement(bot_state_t *bs) {
bot_initmove_t initmove;
memset(&initmove, 0, sizeof(bot_initmove_t));
VectorCopy(bs->cur_ps.origin, initmove.origin);
VectorCopy(bs->cur_ps.velocity, initmove.velocity);
// KILDEREAN : changed by Mr E to fix bot grapple
// VectorCopy(bs->cur_ps.origin, initmove.viewoffset);
VectorClear(initmove.viewoffset);
// END KILDEREAN
initmove.viewoffset[2] += bs->cur_ps.viewheight;
The second change we need to make is in the update of the bot
library. The change is to BotAIStartFrame() in ai_main.c,
about line 1460.
// do not update missiles
// KILDEREAN : added grapple so bots are fixed, by Mr E
if (ent->s.eType == ET_MISSILE && ent->s.weapon != WP_GRAPPLING_HOOK) {
// END KILDEREAN
trap_BotLibUpdateEntity(i, NULL);
continue;
}
Next we have to tell the bot library that the grapple isn't offhand, as well as
the weapon index for the grapple. We add this to the BotInitLibrary()
in ai_main.c, about line 1616.
if (strlen(buf)) trap_BotLibVarSet("cddir", buf);
//
#ifdef MISSIONPACK
trap_BotLibDefine("MISSIONPACK");
#endif
// KILDEREAN
trap_BotLibVarSet("offhandgrapple", "0");
trap_BotLibVarSet("weapindex_grapple", "10");
// END KILDEREAN
//setup the bot library
return trap_BotLibSetup();
Finally we enable the grapple for use by the bots. This is back in
ai_dmq3.c, in BotSetupDeathmatchAI() at about line 5358.
maxclients = trap_Cvar_VariableIntegerValue("sv_maxclients");
trap_Cvar_Register(&bot_rocketjump, "bot_rocketjump", "1", 0);
// KILDEREAN : changed to default to 1
trap_Cvar_Register(&bot_grapple, "bot_grapple", "1", 0);
// END KILDEREAN
trap_Cvar_Register(&bot_fastchat, "bot_fastchat", "0", 0);
Three things that you should know:
- A map must be BSP'd with the -grapplereach option when using bspc.exe.
The bots can then calculate routes to places while using the grapple.
- You've got to set bot_grapple 1 for the bots to actually use
the grapple.
- Bots don't often use the grapple. Even when you've installed this
code, you won't see them use it very often. When they're engaged in
combat, the grapple definitely won't be used. The best map that I've
found for seeing the use of the grapple is puremap6.
|
5. A loose end...
You've now built a more detailed grapple implementation into your
mod... you can now have some fun trying to turn it into an offhand
version!
One other issue that needs attention is when the grapple hits a
mover. Unfortunately it stays in a fixed position while the mover
doesn't stop. I'll leave you to have a bit of fun with this loose end.
|