Unreal Tournament 2004 Explosive Ammo Mutator

The Mutator Class

The UT class hierarchy defines the Mutator class as a plugin system for modifying game rules in an arbitrary manner. It allows a developer to muck with the game by simply extending the class and overriding specific methods. For the Explosive Ammo mutator, the important one is the CheckReplacement() function.

function bool CheckReplacement(Actor Other, out byte bSuperRelevant)

This method gets called after an Actor is spawned, but before it actually enters the game world. Returning true from this method signals the engine to keep the spawned actor, and retruning false means that the engine should throw away the Actor that was passed in. In your CheckReplacement() method, you can replace Actors in the game by calling the ReplaceWith() method.

bool ReplaceWith (Actor other, string aClassName)

Calling this method has the engine replace the Actor passed in with a newly spawned instance of the class specified by the string in the second parameter. In the Explosive Ammo mutator, we replace ammo pickup Actors with subclasses that we’ve create to explode when they take damage.

Extending Ammo Pickups

The hierarchy defines several subclasses of the UTAmmoPickup class. All of the ammo pickups in the UT2004 game extend from this base. Our mutator looks for those Actors in the CheckReplacement() method, and replaces them with our subclassed versions. The BioAmmoPickup class is replaced with ExplosiveBioAmmoPickup, the RocketAmmoPickup is replaced with the ExplosiveRocketAmmoPickup, etc.

Our subclasses override the TakeDamage() method, and immediately transition to a new Exploding state. (See my [blog post on UnrealScript])(/2004/04/unrealscript-and-domainspecific-languages.html) for an idea of what states are.) This new state spawns some explosion effects, hurts some people nearby, and then launches projectiles appropriate for the type of ammo pickup. Portions of the code for the ExplosiveRocketAmmoPickup class are shown below:

state Exploding
{
   ignores Touch, TakeDamage;

   function HurtEverything()
   {
       // Left out for brevity.
   }

Begin:
   // Start by hiding the ammo box.
   bHidden = true;
   bProjTarget = false;

   fired = Rand(AmmoAmount / 2);

   // Then spawn a nice little explosion.
   PlaySound(sound'WeaponSounds.BExplosion1', , SoundVolume * (AmmoAmount - fired) * TransientSoundVolume);
   Spawn(class'RocketExplosion', , , Location, Rotation);

   // Next, hurt anything that's nearby.
   HurtEverything();

   // Sleep for a tick so that the hurt can go off before
   // the projectiles;
   Sleep(0.0);

   for (currentShell = 0; currentShell < fired; currentShell++)
   {
      shellRotation = Rotation;
      shellRotation.Pitch += Rand(32767);
      shellRotation.Yaw   =  Rand(65535);
      shellRotation.Roll  =  Rand(65535);
      
      currentProjectile = Spawn(class'RocketProj', instigator, , location, shellRotation);
      currentProjectile.Velocity = Vector(shellRotation) * class'RocketProj'.default.Speed * RandRange(0.2, 3.0);

      if (currentShell % 5 == 0)
      {
         Sleep(FRand() * 0.02);
      }
   }

   SetRespawn();
}

Each of the other ammo classes are subclassed in a similar way. Instant hit weapons, such as the minigun or shock rifle, are different due to the nature of instant hit in the UT engine; but the general idea is the same.

Onslaught Grenades and Spider Mines

An interesting challenge was posed by grenades and spider mines in the Onslaught game type. Most other projectiles in the game automatically explode either when they hit a player, or after a certain amount of time. These two projectile types do not act like this; instead, their explosion is triggered by some other event. In the case of grenades, they must be manually triggered by alt-firing with the grenade launcher in hand; and in the case of spider mines, they explode after running after and catching an enemy player. Also, both will explode if the player who launched them dies.

The solution for both was to subclass the projectile class. For grenades, a simple timer is set for some amount of time in the future after the grenade hits its first wall. When that timer expires, the grenade explodes. This is almost identical to how assault gun grenades work.

Spider mines are slightly more tricky. I wanted the mine to retain its behavior of chasing after nearby enemies, but I didn’t want them to sit there forever. The ONSMineProjectile class defines the state OnGround. This state is active whenever a mine is sitting on the ground waiting for somebody to walk by. The BeginState() method in the base class sets a timer, and the Timer() method of the state is used to determine if anybody is nearby. If there is, the mine transitions to a Scurrying state and runs after him.

My approach was to override the BeginState() method of the OnGround state to set a point in time in the future at which the mine would explode. I then override the Timer() method to check if that point in time has past. If it has, the mine immediately explodes; otherwise, it calls the base Timer() method to do the normal ankle-check. The code ends up being really small and sexy.

Network Replication

As soon as we tried to play the mutator in a network game, we discovered a couple of problems.

Client-Side Special Effects (First Try)

The first problem we noticed was that the special effects were not showing up on the clients. Things like bullet sparks when a mini-gun shot hit the wall, or the explosion when the ammo actually blew up. Some investigation into how the engine works (mad props to the BeyondUnreal Wiki) and some reading of the game’s source code led me to realize that the reason the effects were not appearing was because these effects are normally simulated on the client, and thus are not set to replicate across the network. I played with a lot of things to try to get them to replicate. The first solution was to create a new class called ClientSideSpecialEffect.

class ClientSideSpecialEffect extends Actor;

var class SpecialEffectClass;

replication
{
   reliable if (bNetInitial && Role == ROLE_Authority && SpecialEffectClass != none)
      SpecialEffectClass;
}

simulated function PostNetBeginPlay()
{
   if (Level.NetMode != NM_DedicatedServer)
   {
      Spawn(self.SpecialEffectClass);
   }
}

defaultproperties
{
     DrawType      = DT_None;
     bNetTemporary = True;
     LifeSpan      = 5.0;
     NetPriority   = 1.5;
}

Now, for example, instead of spawning a WallSparks actor, we spawn a ClientSideSpecialEffect and set its SpecialEffectClass member to be the WallSparks class.

When this Actor is spawned, it gets replicated across the network. After spawning, the engine calls the PostNetBeginPlay() method, and if the class finds itself running on a dedicated server, it does nothing and dies after the number of seconds set in the LifeSpawn property. I tried without success to Destroy() the class at the end of PostNetBeginPlay(); but that does not work, presumably because the actor gets destroyed server-side before it ever gets replicated.

Notice the replication block. This is Unreal’s way of saying “send this variable across the network.” In my case, the SpecialEffectClass variable gets replicated only if the Role is ROLE_Authority (a.k.a. the dedicated server) and it’s the first time the class has ever been replicated, as determined by the bNetInitial flag. The bNetInitial flag is technically not needed, since the bNetTemporary flag being set will prevent the object from getting replicated more than once, but it doesn’t hurt to be safe.

Why ClientSideSpecialEffect Didn’t Work

It turns out, though, that this solution broke non-network play. That is, if you were playing a single-player game, you would no longer see the special effects. Peppering the code with log statements, I discovered that the ClientSideSpecialEffect class was inherently flawed. You see, the code to spawn a special effect looked something like this:

effect = Spawn(class'ClientSideSpecialEffect'); // Statement #1
effect.SpecialEffectClass = class'RocketExplosion'; // Statement #2

And in the PostNetBeginPlay() method, the class immediately spawned the actor class specified in the SpecialEffectClass variable. The failure here is a race condition that I failed to recognize because I wasn’t thinking multi-threaded enough. The Unreal Engine is totally multi-threaded. The ClientSideSpecialEffect class worked great in the network because the second statement would be run before the actor was ever replicated across the network. That is to say, in the amount of time between statement #1 and statement #2, it was very unlikely that the replication system would have actually gotten around to processing the object. The operation would happen sort of like this:

  1. Server: Execute statement #1
  2. Server: Enqueue the object for replication
  3. Server: Set the SpecialEffectClass variable
  4. Server: …time passes…
  5. Server: The replication system replicates the object
  6. Client: Call PostNetBeginPlay()
  7. Client: Spawn special effect stored in SpecialEffectClass variable

So why does this fail in the single-player scenario? When playing a single-player game, there is no need for replication, and thus there is no delay between the spawn call and the object’s appearance in the game. The object is created immediately, it’s PostNetBeginPlay() is called immediately, and the SpecialEffectClass variable has not been set yet. The operation happens like below. Note that there is no more distinction between client and server.

  1. Execute statement #1
  2. Call PostNetBeginPlay()
  3. The SpecialEffectClass variable has not been set, and so contains none
  4. Nothing gets spawned because you can’t spawn none 🙂
  5. Set the SpecialEffectClass variable

Sucks, don’t it?

The New Special Effect Solution (Second Try)

Further reading on the BeyondUnreal Wiki about replication led me to discover a new fact about the Actor.RemoteRole property: If the RemoteRole equals ROLE_None, then the replication system completely ignores the object with respect to replication. For the special effects we are interested in, that was just the default value of that property.

The solution was quite simple.

effect.RemoteRole = ROLE_SimulatedProxy;

This tells the replication system to simulate our desired special effect on the client-side. During a single-player game, the RemoteRole is ignored, thus making things work right. Finally!

Non-Vanishing Ammo

The other problem we noticed was that the ammo would hang around for a while after you shot it. Specifically, it would not vanish until after the explosion code had finished running. This seemed odd to me, since we were setting the very first thing the explosion code does is set the bHidden member to true. So I dove into the Pickup class to figure out why the regular pickups didn’t have this problem.

In the Sleeping state, I noticed that they were pulling a sneaky trick. In the BeginState() method, they were setting the NetUpdateTime variable to Level.TimeSeconds – 1, basically telling the engine, “This needs to be updated a second ago.” A good idea is a good idea, so I yoinked the code into the Exploding state’s BeginState():

function BeginState()
{
   NetUpdateTime = Level.TimeSeconds - 1;
   bHidden = true;
   bProjTarget = false;
}

This causes the desired effect of having the ammo dissappear as soon as it is shot.

The “Oh Shit!” Factor

Chris thought up what he termed the “Oh Shit!” factor. This parameter to the mutator scales the amount of ammo that shoots out of an exploding ammo pickup. It currently isn’t implemented, although the configuration setting is present in the UI. Chris is supposed to do it soon, though.

While he was playing around with it, he set it a bit too high, with disasterous (and humorous results). Not only did this kill is player, it also really pissed off the game engine. He estimates his framerate to be about 5 frames/sec. Chris succumbs to a tsunami of goo.

Downloads

If you want the source code for our mutator, feel free to download it from GitHub. At this point, the project is not buildable, nor is it unlikely to be of any use beyond historical curiousity. Still, if you do find some use for it, let me know. Also, please note that this source code is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License.