Click for more information!

Code3Arena

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

menu

  • News
  • Staff
  • Contributors
  • Compiling
  • Help!!!
  • Submission
  • 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


      Articles
      < Index >
      1. Entities
      2. Vectors
      3. Good Coding
      4. Compilers I
      5. Compilers II


      Links

    1. Quake3 Files
    2. Quake3 Forums
    3. Q3A Editing Message Board
    4. Quake3 Editing


      Feedback

    5. SumFuka
    6. Calrathan
    7. HypoThermia
    8. AssKicka
    9. WarZone





      Site Design by:
      ICEmosis Design


    10.  
      TUTORIAL 18 - Vortex Grenades II
      by TeknoDragon

      This is a follow-up to AssKicka's Vortex Grenades Tutorial, that beautifies the grenade's effects and may speed up the algorithm in high load situations with a lot of players tossing grenades. This tutorial assumes that you have just stepped out of the Vortex Grenades Tutorial, so if you haven't done that get over there!

      I'll also go over a few things that make good development practice, they are a composite of personal experience and hours in a few Comp. Sci. classes at WSU.
       

      1. Simple Tweaks

      Now that you have vortex grenades there are a few issues to consider if you want to optimize this mod. First you can change the nextthink of the grenades so that they aren't calling G_Suck 50 times a second, instead maybe 10 or 20.

      Go to the G_Suck function in g_missile.c and scroll down to this line:

      self->nextthink = level.time + 20;
      
      You can change the 20 to something larger (say 100 -- that's 10 times per second) to make G_Suck suck up less process. This doesn't change the rate that players are sucked towards the grenade because G_Suck doesn't add the grenade's velocity to yours, it replaces it.

      Looking at how vortex grenades move the player (through modifying target->client->ps.velocity), you can find what else modifies the player motion and what it does to beautify the movement. If you use MS Visual Studio to search though the source files you'll find client->ps.velocity in several functions, including G_Damage in g_combat.c seen here:

      VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity);
      
      // set the timer so that the other client can't cancel
      // out the movement immediately
      
      if ( !targ->client->ps.pm_time ) {
      	int		t;
      	t = knockback * 2;
      	if ( t < 50 ) {
      		t = 50;
      	}
      
      	if ( t > 200 ) {
      		t = 200;
      	}
      
      	targ->client->ps.pm_time = t;
      	targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
      }
      
      You'll notice that immediately after the kick velocity is added to the client a special timer is set so that the movement isn't immediately negated. We'll add something like this into our G_Suck function to beautify the effect that 2 grenades have on each other. In the original Vortex Grenades tutorial says that VectorScale adds the scaled vector to the result, but if you look at the code of _VectorScale (the unoptomized version of the VectorScale #define macro -- both in g_math.c) you'll see that it doesn't! We can change this so that it does add the velocity, but in affect that means that we're accelerating towards the grenade! This feels more natural to a player since acceleration is a natural affect of forces and it will jerk them around less.

      First we need to add a new variable: vec3_t kvel, since dir is reused at the end of the function we shouldn't mess with it. We should also make a few #defines so that we can change the G_Suck effects more easily.

      At the beginning of G_Suck change this:

      vec3_t start,dir,end,kvel;
      
      Then add this before G_Suck:
      #define GSUCK_TIMING	50			// the think time interval of G_Suck
      #define GSUCK_VELOCITY	200			// the amount of kick each second gets
      
      Now you can change GSUCK_TIMING to affect the times per second that G_Suck pulls a player back. Finally to correct this little bug and add in the kick timer let's change the G_Suck code:
      // scale directional vector by the kick factor and add to the targets velocity
      VectorScale(dir,GSUCK_VELOCITY / GSUCK_TIMING, kvel);
      
      // add the kick velocity to the player's velocity
      VectorAdd (target->client->ps.velocity,kvel, target->client->ps.velocity);
      
      // set the timer so that the other client can't cancel
      // out the movement immediately
      if ( !target->client->ps.pm_time ) {
      	targ->client->ps.pm_time = GSUCK_TIMING - 1;
      
      	/* the next G_Suck that works here will
      	probably be the one that worked before */
      
      	targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
      }
      
      Now compile and play around with it a bit, you'll notice a few changes.
       

      2. Fine Tuning It

      The grenades might not seem so effective, so turn up the GSUCK_VELOCITY. I found that 2000 is just enough so that if there's only one grenade there you can still get away, but 2 will almost certainly kill you (remember they really add forces now).

      The GSUCK_TIMING value is pretty important to balancing how good it looks vs. how much process it uses. If GSUCK_VELOCITY is very large and GSUCK_TIMING is very small players will be tossed back and forth across the grenade, so be careful with this extreme.

      Next, you might even up the stakes and comment out these lines:

      //	if (target == self->parent)
      //		continue;
      Now you are affected too and can better tune the grenade's strength!

      We see the VectorCopy call at the end of the function, but we don't see it in the G_Damage function, that's strange. I suppose you could comment out this last thing and then remove kvel from the beginning saving you about 12 bytes on G_Suck's stack, but that's not too bad in the great big scheme of things (and no, it doesn't unequivocally add up to be a per grenade penalty... only per G_Suck simultaneous calls, which don't happen that much unless you have a lot of grenades out there).
       

      3. The Big Fix

      There is one big optimization that can be done. If you poke around in many of the core functions of the code you'll see stuff from syscalls.c a lot, particularly trap_Trace. This function serves to calculate impacts. There is another function that can serve us well: trap_EntitiesInBox. This function takes two vectors that define a bounding box and returns a list of entity numbers that are inside this region! We'll take the function G_KillBox as our example.
      void G_KillBox (gentity_t *ent) {
      	int			i, num;
      	int			touch[MAX_GENTITIES];
      	gentity_t	*hit;
      	vec3_t		mins, maxs;
      
      	VectorAdd( ent->client->ps.origin, ent->r.mins, mins );
      	VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs );
      	num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES );
      
      	for (i=0 ; iclient ) {
      			continue;
      		}
      
      		// nail it
      		G_Damage ( hit, ent, ent, NULL, NULL,
      			100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG);
      	}
      }
      
      100k damage, Ouch! You see the exact behavior or trap_EntitiesInBox here, so although there is little to no documentation on the syscalls you can still use them just like id does!

      However we've got a circle, not a box! So we fudge the radius a bit and then double check to make sure everyone's inside. First we need some kind of a list like touch[MAX_GENTITIES] and a number. Add this line at the beginning of G_Suck:

      vec3_t start,dir,end,kvel,mins,maxs;
      int targNum[MAX_GENTITIES],num;
      
      Next add this define near the others:
      #define GSUCK_RADIUS 500
      
      Now take out this line:
      while ((target = findradius(target, self->r.currentOrigin, 500)) != NULL) {
      
      In it's place, put this:
      mins[0] = -GSUCK_RADIUS * 1.42;
      mins[1] = -GSUCK_RADIUS * 1.42;
      mins[2] = -GSUCK_RADIUS * 1.42;
      maxs[0] = GSUCK_RADIUS * 1.42;
      maxs[1] = GSUCK_RADIUS * 1.42;
      maxs[2] = GSUCK_RADIUS * 1.42;
      
      VectorAdd( self->r.currentOrigin, mins, mins );
      VectorAdd( self->r.currentOrigin, maxs, maxs );
      
      num = trap_EntitiesInBox(mins,maxs,targNum,MAX_GENTITIES);
      for(num--; num > 0; num--) {    // count from num-1 down to 0
      	target = &g_entities[targNum[num]];
      
      Finally add these 3 lines:
      // target must be able to take damage
      if (!target->takedamage) 
      	continue;
      
      // target must actually be in GSUCK_RADIUS
      if ( Distance(self->r.currentOrigin,targ->r.currentOrigin) > GSUCK_RADIUS )
      	continue;
      
      We multiply GSUCK_RADIUS by 1.42 because it describes the far corner, not just the closest edge of the square. Since distance is = sqrt( height^2 + width^2 ) the distance between a unit square's corners as well as the ratio of its width to its diagonal distance is sqrt( 2 ) or about 1.42!

      Compile away! If you have less than the minimal requirements for Quake 3 then you will probably see some performance increase when there are a lot of people and a lot of vortex grenades.
       

      4. A Nasty Surprize!

      One last thing that we can do is do another trap_EntitiesInBox to see if someone is right over the top of our grenade and then explode it on them! We'll reuse targNum and make another define:

      #define GSUCK_TRIGGER	32
      
      At the very end of the function add these lines:
      mins[0] = -GSUCK_TRIGGER * 1.42;
      mins[1] = -GSUCK_TRIGGER * 1.42;
      mins[2] = -GSUCK_TRIGGER * 1.42;
      maxs[0] = GSUCK_TRIGGER * 1.42;
      maxs[1] = GSUCK_TRIGGER * 1.42;
      maxs[2] = GSUCK_TRIGGER * 1.42;
      
      VectorAdd( self->r.currentOrigin, mins, mins );
      VectorAdd( self->r.currentOrigin, maxs, maxs );
      
      num = trap_EntitiesInBox(mins,maxs,targNum,MAX_GENTITIES);
      for(num--; num > 0; num--) {    // count from num-1 down to 0
      	target = &g_entities[targNum[num]];
      
      	// target must be a client
      	if (!target->client) 
      		continue;
      
      	// target must not be the player who fired the vortex grenade 
      	if (target == self->parent)		// makes sense here
      		continue;
      
      	// target must be able to take damage
      	if (!target->takedamage)
      		continue;
      
      	G_ExplodeMissile( self)			// EXPLODE goes the weasel!
      }
      
      I don't check the radius again here since it's so small, but you certainly can! However, it involves some heavy multiplication that's not necessarily optimized.

      Now that we've done the same thing twice you could take the code that seems repeated and put it in another function. This will reduce average memory usage since you can reduce the time large variables like targNum are in memory. I'll leave the implementation up to you.

      One final thing to consider: MAX_GENTITIES is 1<<10, i.e. 1024, that's 4k on the stack each time though the function. I changed mine to 64 instead, which is only 256. The tradeoff here is that if you get more than 64 entities (including rockets, players, plasma, and just about everything that moves) within GSUCK_RADIUS some of them will be tagged for suckage and the younger entities probably won't AFAIK (entity management is a bit of a mystery to me yet).
       

      5. Testing and Balancing

      The absolutely best way to test and balance your mod is to get together with some Quake 3 fans on a LAN and get everyone's opinions on your work. These opinions are in fact more valuable than your own in the development process, because they hopefully represent the kinds of people that will spend time playing your mod.

      You can go back and change the four #defines: GSUCK_TIMING, GSUCK_VELOCITY, GSUCK_RADIUS, and GSUCK_TRIGGER to tune up your mod in-between games when you're testing it. You'll also want to add another #define to easily change grenade damage. Add this line with your other #defines:

      #define GRENADE_DAMAGE	100		// bolt->damage for grenade
      #define GRENADE_RADIUS	150		// bolt->splashRadius for grenade
      
      Next be sure to replace the appropriate lines in fire_grenade:
      bolt->damage = GRENADE_DAMAGE;
      bolt->splashDamage = GRENADE_DAMAGE;
      bolt->splashRadius = GRENADE_RADIUS;
      

      This way you can have the file open in MS Dev Studio and tweak a few values and compile. Most modern compilers including MS Dev Studio will only recompile the changed files. Next copy the dll's or qvm's over your lan and start the next game in a few seconds.

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