Tutorial 9 for SDK 1.1

Tutorial 9 for SDK 1.1
BFG Fun
by Trevor Hogan

Let's Have Some Fun

We've spent a lot of time talking about some pretty dry topics such as tracelines and bounds checking; however, all that stuff is useful. Let's take a break and apply some of the techniques we've learned by messing around with the BFG ball. In this tutorial we'll modify the BFG ball to explode into a dozen plasma balls which ricochet around the room causing mass havoc. Let's start with spawning the plasma balls - we'll deal with the ricochets later.

Spawning Entities

Spawning entities is easy. Just call gameLocal.SpawnEntityDef and setup any initial values on your newly created entity. Projectiles are a little more complicated and provide Create and Launch methods for automating the initial setup. Unfortunately we want to make some fairly drastic changes to the BFG ball and the plasma ball so we'll need to override the idProjectile class with our own classes.

Luckily for us the BFG ball is different enough from a regular projectile that it already has an entirely seperate class to itself (idBFGProjectile in game/Projectile.cpp). Since we want to spawn the plasma balls when the BFG ball explodes, let's take a look at idBFGProjectile :: Explode in game/Projectile.cpp. Add this code to the end of the function but before the return statement.

// get the projectile decl for the plasma ball

const idDeclEntityDef *plasmaDef = gameLocal.FindEntityDef( "projectile_plasmablast", false );

// don't try to create entities on a client, only on the server

if( !gameLocal.isClient && plasmaDef )
{
      // spawn a dozen plasma balls

      idEntity *ent;

      for( int j = 0; j < 12; j++ )
      {
            // this function spawns the entity according to the passed decl dictionary
            // the entity is created in memory but still needs some initial setup

            gameLocal.SpawnEntityDef( plasmaDef->dict, &ent, false );

            // this code assumes the "projectile_plasmablast" decl is actually an idProjectile
            // it would be a good idea to verify this in a production mod
            // e.g. if the decl is modified to spawn a different entity this code will crash

            idProjectile *proj = static_cast<idProjectile *>( ent );

            // calculate a random direction for the plasma ball
            // and use spherical polar coordinates (this math is covered in basic Calculus)

            idVec3 dir;

            float phi = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat( );
            float theta = (float)DEG2RAD( 180.0f ) * gameLocal.random.RandomFloat( );

            dir.x = idMath :: Sin( theta ) * idMath :: Sin( phi );
            dir.y = idMath :: Sin( theta ) * idMath :: Cos( phi );
            dir.z = idMath :: Cos( theta );

            // the owner is the player
            // the plasma ball starts at the BFG ball's origin
            // the plasma ball is going in a random direction

            proj->Create( player, GetPhysics( )->GetOrigin( ), dir );

            // launch the plasma ball

            proj->Launch( GetPhysics( )->GetOrigin( ), dir, vec3_zero, 0.0f, 1.0f, 1.0f );

            // and don't forget to make it ricochet

            proj->iTotalRicochets = 4;
      }
}

First we grab the plasma ball decl from the engine and then we spawn twelve plasma balls and choose a random direction for each. If you don't understand the math involved in choosing a direction then don't worry too much - you could probably get away with randomizing the x, y, and z components individually. Just make sure to normalize the vector afterwards. Next we call Create and Launch and finally we tell the entity to ricochet a few times. Note that this won't compile right now because iTotalRicochets hasn't been defined yet (we'll add it later). If you want to compile the mod now just comment out the iTotalRicochets line.

At this point you should fire up the mod and shoot off some BFG balls. Although the plasma balls don't ricochet yet it's still looking pretty cool!

Ricochets

Alright, this is where it starts getting complicated. The math behind ricochets is easy in theory but hard in practice. You should be comfortable with linear algebra concepts such as vectors, normalization, projection, and the dot product before continuing.

image1

The idea behind a ricochet is that the impact angle (x in the above diagram) is completely reflected; well, this is only true in a perfect ricochet but that's what we're modeling today. Convenient. What we need to do is reflect the projectile's velocity (coloured dark red) across the surface normal (coloured dark blue). So, to do this we'll have to project the velocity onto the surface and then project it onto the surface normal. Then we can negate the projected normal (coloured yellow) and add the two together. Let's do it step by step.

  1. Calculate the projectile's speed (the magnitude of the velocity) and store it for later.

  2. Normalize the projectile's velocity since we need to calculate the angle between the velocity and the surface normal (y in the above diagram) for use when projecting onto the surface normal. By normalizing the velocity now we can use the dot product which simplifies to the cosine of the angle because both vectors are normalized.

  3. Calculate the dot product between the negative normalized velocity (coloured light red) and the surface normal. We have to negate the normalized velocity here because we want an angle from zero to 90 degrees (i.e. a positive cosine and thus a positive dot product).

  4. Project the projectile's normalized velocity onto the surface.

  5. Set the result vector to the sum of the projected normalized velocity (coloured teal) and the projected normal (coloured yellow). This vector is normalized since we created the projected vectors from a normalized vector.

  6. Multiply the result vector by the projectile's speed calculated in the first step.

Whew. That's pretty complicated but I don't know how to explain it any better. Essentially we're just decomposing the projectile's velocity into components based on the wall's surface normal. Then we can negate the component in the direction of the surface normal and reconstruct the final velocity.

Enough math. Let's write some code. I didn't want to write an entirely new projectile class just for this example so we're going to modify idProjectile instead. If you weren't careful this would make every projectile in the game ricochet but we'll set a default of zero ricochets so everything else will behave as usual. Open game/Projectile.h and add these two new member variables to idProjectile.

      projectileState_t state;

public:
      int iTotalRicochets;          // new
      int iCurrentRicochet;         // new

Making these variables public is considered bad programming practice but I don't want to clutter up this tutorial with accessor code. We'll store the total number of ricochets that this entity should perform in iTotalRicochets (default zero) and the current ricochet count in iCurrentRicochet. Let's initialize these variables so open game/Projectile.cpp and find the idProjectile constructor. Add this code to the end of the function.

iTotalRicochets = 0;
iCurrentRicochet = 0;

Now we need to take care of saving and loading so find idProjectile :: Save and add this code to the end of the function.

savefile->WriteInt( iTotalRicochets );
savefile->WriteInt( iCurrentRicochet );

Add this code to the end of idProjectile :: Restore.

savefile->ReadInt( iTotalRicochets );
savefile->ReadInt( iCurrentRicochet );

Okay, now we need to do the actual ricochets. Find idProjectile :: Collide and add this code to the top of the function just after the EXPLODED and FIZZLED check (as seen below).

if ( state == EXPLODED || state == FIZZLED ) {
      return true;
}

if( iCurrentRicochet < iTotalRicochets && collision.c.entityNum == ENTITYNUM_WORLD )
{
      // don't perform the ricochet on the client

      if( gameLocal.isClient )
            return false;

      // read the tutorial to find out what this math does

      idVec3 bounce = velocity;
      float speed = bounce.Length( );
      bounce.Normalize( );
      float dp = -bounce * collision.c.normal;
      bounce.ProjectOntoPlane( collision.c.normal );
      bounce += collision.c.normal * dp;
      bounce *= speed;

      // reset the angular velocity because the physics code may have changed it

      GetPhysics( )->SetAngularVelocity( vec3_zero );
      GetPhysics( )->SetLinearVelocity( bounce );

      iCurrentRicochet++;

      // don't stop the physics simulation

      return false;
}

image2

If you're interested you might want to experiment with ricochets some more. The BFG ball looks very cool as it ricochets around the room - maybe you could make it ricochet a random number of times before exploding so you'll never know exactly when it'll detonate. You could also make the plasma balls leave scorch marks on the walls where they ricochet rather than only when they explode.

Finally, note that this code works just fine in multiplayer because everything is handled by the server. The client doesn't predict the new plasma balls or even the ricochets. It may be possible to predict either or both of these events but I'm still not familiar enough with Doom 3's networking code to say for sure.

December 2, 2004