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:
- Server: Execute statement #1
- Server: Enqueue the object for replication
- Server: Set the
SpecialEffectClass
variable - Server: …time passes…
- Server: The replication system replicates the object
- Client: Call
PostNetBeginPlay()
- 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.
- Execute statement #1
- Call
PostNetBeginPlay()
- The
SpecialEffectClass
variable has not been set, and so containsnone
- Nothing gets spawned because you can’t spawn
none
🙂 - 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.
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.