The goal of this tutorial is to create a rapid-fire instant-hit weapon for UT2004. Primary fire will be a standard Minigun-style mode with high fire rate and quite some spread, while the secondary mode will fire explosive rounds with lower fire rate and spread.
Unlike the UT2004 Minigun, this weapon will immediately start firing while winding up. The dynamic nature of the wind-up/down behavior will require an unusual handling of fire modes.
The Vauss Cannon will use the standard UT2004 Minigun visuals with custom firing sounds. Feel free to customize the visuals as well, but this tutorial makes some assumptions based on details of the Minigun's 1st person mesh.
Contents
Project setup
First you need to set up your UnrealScript project. Personally I prefer just creating classes with UnCodeX, which automatically creates the project directory if you specify a new package name. To compile my projects I use UMake, which stores its settings in the file make.ini in the project directory and can be configured as the default compiler in UnCodeX. You can use any UnrealScript programming environment you want, but I strongly recommend against using UnrealEd because this tutorial is going to work with defaultproperties which are not editable in UnrealEd.
For this tutorial, the project directory (and thus the package to compile) is assumed to be called VaussCannonTutorial and since this is supposed to become a simple weapon mutator, it resides directly in the UT2004 directory. The custom sounds we will use in this tutorial are already set up in the sound package VaussFireSounds.uax, which should be placed in the project directory as it will be imported into our project package later.
Getting started
UT2004 weapons consist of different parts. The most important part is the Weapon subclass, which often implements the overall behavior of the weapon and also acts as mediator between the player and other parts of the weapon. Let's start by creating the new Weapon subclass VaussCannon and filling in some basic information about the weapon we are going to create.
Any potential comments aside, the only line of code in the new class should be:
class VaussCannon extends Weapon;
We will use custom sounds, so let's load those first:
#exec obj load file=VaussFireSounds.uax package=VaussCannonTutorial
This loads our custom sound package into the package we will be compiling. The part after #exec is actually a console command that will be executed when the VaussCannon.uc file is parsed for the first time. Any resources in VaussFireSounds.uax are loaded and will be available during the entire compiler run. Any other classes we compile will be able to reference these resources in code and defaultproperties as well.
Speaking of defaultproperties, let's fill in some default values as well:
defaultproperties { ItemName = "Vauss Cannon" Description = "..." // be creative here, the Vauss Cannon's basic functionality was explained at the start of the tutorial // the following properties were all copied from the Minigun class: HighDetailOverlay=Material'UT2004Weapons.WeaponSpecMap2' IconMaterial=Material'HudContent.Generic.HUD' IconCoords=(X1=246,Y1=80,X2=332,Y2=106) InventoryGroup=6 Mesh=mesh'Weapons.Minigun_1st' BobDamping=2.25 EffectOffset=(X=100.0,Y=18.0,Z=-16.0) DisplayFOV=60 PutDownAnim=PutDown DrawScale=0.4 PlayerViewOffset=(X=2,Y=-1,Z=0) SmallViewOffset=(X=8,Y=1,Z=-2) PlayerViewPivot=(Pitch=0,Roll=0,Yaw=500) AttachmentClass=class'MinigunAttachment' SoundRadius=400.0 SelectSound=Sound'WeaponSounds.Minigun.SwitchToMinigun' SelectForce="SwitchToMiniGun" AIRating=+0.71 CurrentRating=+0.71 HudColor=(r=255,g=255,b=255,a=255) CustomCrosshair=12 CustomCrosshairTextureName="Crosshairs.Hud.Crosshair_Circle1" CustomCrosshairColor=(r=255,g=255,b=255,a=255) Priority=9 CenteredOffsetY=-6.0 CenteredYaw=-500 CenteredRoll=0 }
These properties already mentions another part of all UT2004 weapons, the WeaponAttachment. That's the mesh attached to the player model. We will just reuse the Minigun attachment here as it already does what we want. It is responsible for spawning tracer effects, ejected shells and impact sparks. For the alternate fire mode it even spawns a small explosion effect.
Implementing fire modes
Right now there's not much we can do with our weapon. We could get it into the game via the Arena mutator, but it wouldn't do anything yet.
In order to make our weapon shoot, we need to add fire mode classes. More precisely, we need to create one or two subclasses of WeaponFire and use it/them as the fire mode class(es) of the VaussCannon class. The Vauss Cannon is an instant-hit weapon with two similar firing modes. After a look at the WeaponFire class hierarchy, the most obvious choice is subclassing InstantFire. We could also subclass MinigunFire, but we will see later that the Vauss Cannon fire modes are more than a bit different from those of the Minigun.
Let's call our primary fire mode class VaussCannonFire:
class VaussCannonFire extends InstantFire;
...and our secondary fire mode class VaussCannonAltFire:
class VaussCannonAltFire extends VaussCannonFire;
Since the fire modes will be very similar, it makes sense to extend one from the other. And since the secondary mode has splash damage, we'll extend it from the non-splash damage primary mode and add the splash damage handling here later.
Very important, to tell our weapon to use these new fire mode classes, we need to add the relevant values to the VaussCannon's defaultproperties block:
FireModeClass[0] = class'VaussCannonFire' FireModeClass[1] = class'VaussCannonAltFire'
Primary fire mode
For now we will only work on the primary fire mode. Changes we do here will be inherited by the secondary fire mode class because it extends the primary fire mode class.
Before we start adding default values and code, let's see what InstantFire and its parent class WeaponFire already provide. InstantFire defines a trace range of 10000UU and all the logic required to perform a hit trace and apply damage. The WeaponFire class defines the standard logic for dealing with relevant player input (pressing fire(alt fire button), with some of the logic residing in native code. There are also AI recommendations and a few predefined animation names. Neither of these classes define a default type or amount of damage, so let's do that first.
defaultproperties { DamageMin = 5 DamageMax = 6 DamageType = class'DamTypeMinigunBullet' Momentum = 1000.0 }
Another thing we need to define is the amount and type of ammo our weapon uses. This is also done in the defaultproperties block of the fire mode classes, so add the following lines to it:
AmmoClass = class'MinigunAmmo' AmmoPerFire = 1
These two lines mean the Vauss Cannon uses the same ammo type as the Minigun and requires one ammo unit per shot.
At this point you can compile and test the current state of the Vauss Cannon using the Arena mutator. You will notice that it will shoot and do damage, but it's almost completely silent and the fire rate is far too low. It does spawn tracers and impact effects, though. These are created by the MinigunAttachment class we defined as the VaussCannon's AttachmentClass.
To add a firing sound, simply add the following line to the VuassCannonFire defaults:
FireSound = Sound'VaussFireSound'
The firing rate is defined by the time between shots, so a rate of e.g. 20 shots per second would require the following default value:
FireRate = 0.05
If you test again, you'll hear a nice machinegun sound and see the corresponding fire effects, including the damage opponents take. However, the shots are still spot-on. A machine gun with this firing rate would be expected to be quite inaccurate, and if we look at the MinigunFire class, we can see a default value for the property Spread, which defines a relative spread angle for instant-hit modes. A spread of 0.08 is used by the Minigun primary fire and that value should be good for the Vauss Cannon primary fire as well.
Spread = 0.08
That should be enough for the first implementation pass on the primary fire mode, so let's move on to the secondary mode.
Secondary fire mode
The secondary fire mode of the Vauss Cannon is supposed to have a lower firing rate (and thus less spread), but do more damage, including some splash damage. To make it more obvious, we will also assign a slightly different firing sound. Five shots per second should be ok for this mode, so add the following lines to the VaussCannonAltFire class:
defaultproperties { FireSound = Sound'VaussAltFireSound' FireRate = 0.2 Spread = 0.03 }
At the lower firing rate and with the inherited damage values, the secondary mode is seriously underpowered. Let's override the inherited values with the following new defaults:
DamageMin = 13 DamageMax = 15 DamageType = class'DamTypeMinigunAlt' Momentum = 10000.0
Now the secondary mode fires 5 shots per second, each doing 13 to 15 damage. That's an average damage output of about 5*(13+15)/2=70. The primary mode does about 20*(5+6)/2=110 damage, but only at closer range, because at greater distance not all shots will hit the target. The damage output per second may be a bit lower for secondary mode, but the damage output per ammo is more than twice as high as the primary mode's. That means, if the player only used the secondary mode, he would be able to save a lot of ammo at the expense of some damage per second. To counter this, we should increase the ammo usage of the secondary mode a bit:
AmmoPerFire = 2
Now the secondary mode will consume 2/0.2=10 ammo units per second, while the primary mode consumes 1/0.5=20 ammo units per second.
One problem caused by an ammo amount greater than 1 is that the weapon may stop firing in this mode if less than the required ammo amount is left, but it won't switch away automatically. This also happened with the Linkgun secondary mode before Epic worked around it in a patch by adding the following code to the alt fire mode, which we will also do in our VaussCannonAltFire class:
function bool AllowFire() { return Weapon.AmmoAmount(ThisModeNum) >= 1; }
ThisModeNum is a variable defined in the WeaponFire class that is initialized with the actual mode number for this fire mode by the weapon when creating the fire mode instance. Weapon also is a variable defined by the WeaponFire and initialized by the weapon. The AmmoAmount function of the weapon returns the current ammo amount for the specified fire mode. Usually this will be the same value for both fire modes, but some weapons like the Assault Rifle keep two different ammo values for primary and secondary mode.
So now the secondary Vauss Cannon fire mode already does more damage at a lower fire rate and uses more ammo per shot, but it doesn't have splash damage yet. We will have to override the logic that finds hit targets and applies damage. The function responsible for this part is InstantFire.DoTrace(). If you look at its source code (and I strongly recommend doing so!), you will see it was written to handle reflecting off the Shieldgun shield if the fire mode supports it.
We only need to replace the call to Other.TakeDamage(...)
, but it's burried deep inside this function. There's no way to replace only that single line, so we need to override and reimplement the entire DoTrace() function. Our weapon does not support reflection, so we can leave out the corresponding code. Of course you can also copy/paste the InstantFire.DoTrace() function entirely and only replace the TakeDamage line in a similar way as the following code does. First we will introduce a new property for our fire mode class. Add the following variable declaration below the class declaration, but above the first function:
var float DamageRadius;
...and give it a meaningful value in the defaultproperties block:
DamageDarius = 10.0
Now we can replace the simple TakeDamage() call with something that causes radius damage. The most obvious candidate here is the Actor.HurtRadius() function, which already implements radius damage with linear falloff from the center. In UT2004 the WeaponFire classes do not extend Actor, so we need to get hold of an actor to call HurtRadius(). This actor should be owned by the player as otherwise the player might not be credited for kills. We are lucky here, as the weapon itself extends Actor >> Inventory >> Weapon and therefore inherits the HurtRadius() function:
function DoTrace(vector Start, rotator Dir) { local vector X, End, HitLocation, HitNormal; local Actor Other; local float DamageAmount; X = vector(Dir); End = Start + MaxRange() * X; Other = Weapon.Trace(HitLocation, HitNormal, End, Start, true); if (Other != None && Other != Instigator) { DamageAmount = RandRange(DamageMin, DamageMax) * DamageAtten; Weapon.HurtRadius(DamageAmount, DamageRadius, DamageType, Momentum, HitLocation); WeaponAttachment(Weapon.ThirdPersonActor).UpdateHit(Other, HitLocation, HitNormal); } else { HitNormal = vect(0,0,0); HitLocation = End; WeaponAttachment(Weapon.ThirdPersonActor).UpdateHit(Other, HitLocation, HitNormal); } SpawnBeamEffect(Start, Dir, HitLocation, HitNormal, 0); }
So far so good, but when testing the secondary mode, you'll notice it neither damages enemies nor yourself if you shoot at some nearby object. This is not really a problem with our code, but one with the HurtRadius() function. It uses VisibleCollidingActors and passes the DamageRadius directly. That iterator function, however, only looks for actors whose center is within the specified radius. At only 10UU radius, this will work for almost no actor in the game.
Fixing the radius damage
We will have to write a custom radius damage function that finds all actors overlapping the damage radius, even if their center is outside the radius. Also, the actor actually hit should take full damage. This is important for vehicles, which use complex collision. Also, unlike the HurtRadius() function that bases the damage amount on the distance from the actor center minus the actor's CollisionRadius, our function should take the actual distance from the collision cylinder. With a radius as small as the one we use and a CollisionHeight much greater than the CollisionRadius, hitting players at the feed or the head would otherwise do no damage.
What we need is a function that calculates a point's distance from a cylinder's surface. Player collision cylinders are always upright, so the problem can be split into calculating a height difference and finding the distance of a point from a circle. The following function will first find the horizontal and vertical distances and then calculate the actual distance from these components:
function float DistToCylinder(vector CenterDist, float HalfHeight, float Radius) { CenterDist.X = VSize(vect(1,1,0) * CenterDist) - Radius; if (CenterDist.X < 0) CenterDist.X = 0; CenterDist.Y = 0; if (CenterDist.Z < 0) CenterDist.Z *= -1; CenterDist.Z -= HalfHeight; if (CenterDist.Z < 0) CenterDist.Z = 0; return VSize(CenterDist); }
We will use this in our custom radius damage function to calculate the distance of players from the hit location.
The new radius damage function needs to know three things:
- who/what was hit,
- where was the hit and
- what's the shot direction.
These need to be passed as parameters, everything else is available as class-global variables.