Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 40 | 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 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:

    1. 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.
    2. You've got to set bot_grapple 1 for the bots to actually use the grapple.
    3. 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.

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