news  about  forums  links  tools  tutorials  contact 

Sniper Mod - author: ca
 

Sniper Mod - download


This is a modification that takes the InstaGib mutator from Unreal Tournament and adds in a unique scoring system.  The idea is fairly simple - 2pts for head shots, 1pt for a body/torso shot, and 0pts for a leg shot.  Then on top of that you can get special points bonuses like point blank, aerial kills, etc.  (It also does a couple other things, which will be explained later in the tutorial.)  This could of course be managed by a mutator, but I opted for the simple and straightforward approach of making my own game type.

Making your own game type is a simple process, but you end up trading off the benefits of mutators (plugin with almost any gametype) for the flexibility of having complete access to the game state.  In this mod I also opted to make a new Scoreboard to display player accuracies, so I will explain briefly the basics of adding your own custom changes to the scoreboard.

Note that there are some places where I had to 'hack' some code, and this doesn't mean hack in the same sense of say a CD crack or something, but rather in the sense of that in good design this code would be located elsewhere.  But in all projects there comes a point where "making it work now" becomes more important than "making it work the correct way".  And sometimes I'm just lazy...

SniperModRifle

The first step is to create our Sniper Rifle, since this is a Sniper Mod after all.  I opted to subclass the Enhanced Shockrifle (SuperShockRifle) since I like the feel of one-shot kills.

class SniperModRifle extends SuperShockRifle;

The following line imports a package so that we can reference it's contents without UCC complaining about missing dependencies.  In this particular case we need to access a font later on in the RenderOverlays() function.

#exec OBJ LOAD FILE=..\Textures\LadderFonts.UTX Package=LadderFonts
var float lAccuracy;  // This is used for an text effect

I wanted this mod to keep track of a player's accuracy, basically just total shots fired and how many shots hit.  I've used a modified PRI (PlayerReplicationInfo) to keep track of those values, and everytime a player fires we increment the total shots fired variable.

function Fire(float Accuracy)
{
  // If it's a player then update the player PRI
  if (SniperModPRI(Pawn(Owner).PlayerReplicationInfo) != None)
    SniperModPRI(Pawn(Owner).PlayerReplicationInfo).TotalFired++;
  // Otherwise if it's a bot then update the bot PRI
  else if (SniperModBotPRI(Pawn(Owner).PlayerReplicationInfo) != None)
    SniperModBotPRI(Pawn(Owner).PlayerReplicationInfo).TotalFired++;
    
  Super.Fire(Accuracy);  // Call the Parent Fire() to fire normally
}

I stole the zoom ability from UT's SniperRifle, so I can't take credit for that code - not that it's complicated or anything, just why re-write code that works?

function AltFire(float Accuracy)
{
  ClientAltFire(Accuracy);
}
simulated function bool ClientAltFire( float Value )
{
  GotoState('Zooming');
  return true;
}
state Zooming
{
  simulated function Tick(float DeltaTime)
  {
    if ( Pawn(Owner).bAltFire == 0 )
    {
      if ( (PlayerPawn(Owner) != None) && PlayerPawn(Owner).Player.IsA('ViewPort') )
        PlayerPawn(Owner).StopZoom();
      SetTimer(0.0,False);
      GoToState('Idle');
    }
  }
  simulated function BeginState()
  {
    if ( Owner.IsA('PlayerPawn') )
    {
      if ( PlayerPawn(Owner).Player.IsA('ViewPort') )
        PlayerPawn(Owner).ToggleZoom();
      SetTimer(0.2,True);
    }
    else
    {
      Pawn(Owner).bFire = 1;
      Pawn(Owner).bAltFire = 0;
      Global.Fire(0);
    }
  }
}

ProcessTraceHit() is called when the weapon has hit another Actor, and here we use it to check if we've hit a player so that we can update the total shots hit variable in the PRI for our accuracy calculation.  We also do the locational damage check here as well so that the Killed() function in our GameInfo can determine how many points to award.

function ProcessTraceHit(Actor Other, Vector HitLocation, Vector HitNormal, Vector X, Vector Y, Vector Z)
{
  if (Other.IsA('PlayerPawn') || Other.IsA('Bot'))
  {
    if (SniperModPRI(Pawn(Owner).PlayerReplicationInfo) != None)
      SniperModPRI(Pawn(Owner).PlayerReplicationInfo).TotalHit++;
    else if (SniperModBotPRI(Pawn(Owner).PlayerReplicationInfo) != None)
      SniperModBotPRI(Pawn(Owner).PlayerReplicationInfo).TotalHit++;
  }
  if (Other == None)
  {
    HitNormal = -X;
    HitLocation = Owner.Location + X*10000.0;
  }
  SpawnEffect(HitLocation, Owner.Location + CalcDrawOffset() + (FireOffset.X + 30) * X + 6 * Y + -8 * Z);
  Spawn(class'ut_SuperRing2',,, HitLocation+HitNormal*8,rotator(HitNormal));
  if ((Other != Self) && (Other != Owner) && (Other != None))
  {
    // Hit location checks
    if (HitLocation.Z - Other.Location.Z > 0.62 * Other.CollisionHeight)
      Other.TakeDamage(HitDamage, Pawn(Owner), HitLocation, 60000.0*X, 'headshot');
    else if (HitLocation.Z - Other.Location.Z > 0.23 * Other.CollisionHeight)
      Other.TakeDamage(HitDamage, Pawn(Owner), HitLocation, 60000.0*X, 'torsoshot');
    else Other.TakeDamage(HitDamage, Pawn(Owner), HitLocation, 60000.0*X, 'legshot');
  }
}

By overriding the DropFrom() function we stop the player from dropping his/her weapon when he/she dies.  Since the rifle is given to the player at spawn by the GameInfo it doesn't make sense to have rifles laying around as pickups.

function DropFrom(vector StartLocation)
{
  Destroy();
}

RenderOverlays() is called every frame and it allows individual weapons to draw on the Canvas.  The Canvas is basically a polygon covering the screen that is very close to the camera, and it is what the interface (HUD/UWindows) is drawn upon.  I used it here to draw the player's accuracy, total shots fired, and total shots hit in the lower left corner.  Ideally you would probably want to subclass UT's HUD class and add the extra drawing code there so that you could take advantage of the complex HUD configuration options UT has, but I didn't feel like going to all that work for a little gain.

A little word about the simulated keyword - basically it tells the server that this function should be executed on the client as well.  This is necessary for any client-side effects, as the server shouldn't have to bother drawing the client's interface - that's what a client is for.  So if you ever see a function with this keyword in front of it, make sure you put it in front of your version, otherwise you'll end up with some annoying bugs in multiplayer games.

simulated function RenderOverlays(Canvas C)  // Always remember the simulated keyword!
{
  local string sAccuracy;
  local float fAccuracy;
  local SniperModPRI smPRI;
  
  Super.RenderOverlays(C);  // Let the parent render normally first
  smPRI = SniperModPRI(Pawn(Owner).PlayerReplicationInfo);
  if (smPRI == None || PlayerPawn(Owner).bShowScores)
    return;
  
  // Calculate the current accuracy and then convert it into a string
  sAccuracy = "-";
  if (smPRI.TotalFired != 0)
  {
    fAccuracy = 100.0 * (float(smPRI.TotalHit)/float(smPRI.TotalFired));
 
    if (Abs(lAccuracy - fAccuracy) < 1.0)
      lAccuracy = fAccuracy;
    else if (Abs(lAccuracy - fAccuracy) > 30.0)
      lAccuracy = fAccuracy;
    else if (lAccuracy > fAccuracy)
      lAccuracy -= 0.2;
    else if (lAccuracy < fAccuracy)
      lAccuracy += 0.2;
      
    sAccuracy = string(lAccuracy);
    sAccuracy = Left(sAccuracy, InStr(sAccuracy, ".") + 2) $ "%";
  }
  
  C.Font = Font'LadderFonts.UTLadder18';
  
  if (fAccuracy > 35.0)
  {
    C.DrawColor.R = 0;
    C.DrawColor.G = 255;
    C.DrawColor.B = 0;
    C.SetPos(0, C.ClipY - 12 - 16);
    C.DrawText("ROF+", false);
  }
  else
  {
    C.DrawColor.R = 255;
    C.DrawColor.G = 255;
    C.DrawColor.B = 255;
  }
  
  // And finally draw all the information
  C.SetPos(0, C.ClipY - 120);
  C.DrawText("Accuracy: "$sAccuracy, false);
  C.DrawColor.R = 255;
  C.DrawColor.G = 255;
  C.DrawColor.B = 255;
  C.SetPos(0, C.ClipY - 120 + 16);
  C.DrawText("Shots Fired: "$smPRI.TotalFired, false);
  C.SetPos(0, C.ClipY - 120 + 32);
  C.DrawText("Shots Hit: "$smPRI.TotalHit, false);
}

In the midst of playing this mod I decided it would be cool to gain bonuses if you kept your accuracy above a certain percentage.  The only bonus I've created so far increases the rate of fire if your accuracy is above 35%, and this is achieved by the PlayFiring() function.  In UT all of the weapons rely upon the animations to determine when they can fire again (at least for humans, bots cheat), so by increasing the rate that the fire animation is played, we can increase the rate of fire - simple, no?

simulated function PlayFiring()
{
  local float fAccuracy;
  local SniperModPRI smPRI;
  
  smPRI = SniperModPRI(Pawn(Owner).PlayerReplicationInfo);
  fAccuracy = 100.0 * (float(smPRI.TotalHit)/float(smPRI.TotalFired));
  
  PlayOwnedSound(FireSound, SLOT_None, Pawn(Owner).SoundDampening*4.0);
  
  if (fAccuracy > 35.0)
    LoopAnim('Fire1', 0.30 + 0.30 * FireAdjust + 0.4,0.05);  // 0.4 faster if we have the bonus
  else LoopAnim('Fire1', 0.30 + 0.30 * FireAdjust,0.05);  // otherwise normal speed
}

 SniperModGI

Next I made my new game info class, in this case SniperModGI and it extends/subclasses DeathMatchPlus.  This class is responsible for making sure the players start with our modified rifle, awarding any bonuses for kills, and notifying the player about said bonuses.

class SniperModGI extends DeathMatchPlus;

Since we're using custom PRI's to keep track of some extra data, we need to make sure the players (and bots) create the new class instead of their normal default one.  Note that with a .Default you can set the default value for a variable for a class for the current game, and we use/abuse it here so that when a player looks to see what kind of PRI it should create when it spawns, it will create our new custom class.  Nifty, huh?

function PreBeginPlay()
{
  Super.PreBeginPlay();
  
  // Hack to change bot PRI to our own custom class
  class'Bot'.Default.PlayerReplicationInfoClass = class'SniperModBotPRI';
}
event playerpawn Login(string Portal, string Options, out string Error, class<playerpawn> SpawnClass)
{
  // Hack to change player PRI to our own custom class
  SpawnClass.Default.PlayerReplicationInfoClass = class'SniperMod.SniperModPRI';
  
  return Super.Login(Portal, Options, Error, SpawnClass);
}

IsRelevant() is called whenever an actor is spawned, and it allows the gameinfo to get rid of it before it comes into being, so to speak.  Here we override it to disallow any other Inventory subclasses except for our rifle, that means no other weapons, no health, no nothing.  Return true from this function and the actor is good to go, return false and it's off to the garbage heap for it.

function bool IsRelevant(actor Other)
{
  if (!Other.IsA('SniperModRifle') && (Other.IsA('Weapon') || Other.IsA('Inventory')))
    return false;
  else return Super.IsRelevant(Other);
}

Killed is where all the points are awarded for special kills.  Currently the following bonuses are awarded: aerial - victim was in the air, flying - killer is in the air, point blank - killer was less than 200uu's away, super distance - killer was more than 2000uu's away, and distance - killer was more than 1250uu's away.  Also it checks the damageType that was passed from the rifle to award the proper points for head/torso/leg shots.  After awarding extra points it sends a message to the killer notifying them of the bonuses they've received via UT's message classes.

function Killed(pawn Killer, pawn Other, name damageType)
{
  local vector hitloc, hitnormal;
  local bool bKillerInAir, bOtherInAir;
  
  // Er, no bonuses for killing yourself...
  if (Killer == Other)
  {
    Super.Killed(Killer, Other, damageType);
    return;
  }
  bKillerInAir = Killer.FastTrace(Killer.Location + (vect(0, 0, -1) * 75));
  bOtherInAir = Other.FastTrace(Other.Location + (vect(0, 0, -1) * 75));
  // Do a Trace to check for an flying/aerial kills
  if (bKillerInAir)
  {
    if (bOtherInAir)
    {
      // We have a godly flying aerial killer
      Killer.PlayerReplicationInfo.Score += 5;
      PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModFlyingAerialKillMessage');
    }
    else
    {
      Killer.PlayerReplicationInfo.Score += 2;
      PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModFlyingKillMessage');
    }
  }
  else if (bOtherInAir)
  {
    Killer.PlayerReplicationInfo.Score += 2;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModAerialKillMessage');
  }
  // Check for Point Blank
  else if (VSize(Killer.Location - Other.Location) < 200)
  {
    Killer.PlayerReplicationInfo.Score += 1;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModPointBlankMessage');
  }
  // Check for Super Distance Kill
  else if (VSize(Killer.Location - Other.Location) > 2000)
  {
    Killer.PlayerReplicationInfo.Score += 2;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModSuperDistanceKillMessage');
  }
  // Check for Distance Kill
  else if (VSize(Killer.Location - Other.Location) > 1250)
  {
    Killer.PlayerReplicationInfo.Score += 1;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModDistanceKillMessage');
  }
  
  // award points and send appropriate message for locational kills
  if (damageType == 'headshot')
  {
    Killer.PlayerReplicationInfo.Score += 2;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModHeadShotMessage');
  }
  else if (damageType == 'torsoshot')
  {
    Killer.PlayerReplicationInfo.Score += 1;
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModTorsoShotMessage');
  }
  else if (damageType == 'legshot')
    PlayerPawn(Killer).ReceiveLocalizedMessage(class'SniperModLegShotMessage');
  
  if (PlayerPawn(Other) != None && PlayerPawn(Other).DesiredFOV != 90)
    PlayerPawn(Other).SetDesiredFOV(90);
  Super.Killed(Killer, Other, damageType);
}

ScoreKill() normally awards 1 frag for a kill, and since we've already awarded points in Killed() we need to 'castrate' ScoreKill().

function ScoreKill(pawn Killer, pawn Other)
{
  Other.DieCount++;
  if((Killer == Other) || (Killer == None))
    Other.PlayerReplicationInfo.Score -= 1;
  BaseMutator.ScoreKill(Killer, Other);
}

The following function is called everytime a player is respawned, and normally is used to give the enforcer, impact hammer, and translocator.  Instead we override it to spawn our modified rifle, and rely upon the IsRelevant() function to worry about eliminating the other default weapons.

function AddDefaultInventory(pawn PlayerPawn)
{
  local Weapon NewWeapon;
  local Bot B;
  if ( PlayerPawn.IsA('Spectator') || (bRequireReady && (CountDown > 0)) )
    return;
  GiveWeapon(PlayerPawn, "SniperMod.SniperModRifle");
  Super.AddDefaultInventory(PlayerPawn);
}  

And finally we give a name to the game type, and specify our new scoreboard class to be used.  There are a lot of variables that are worth investigating if you decide to make your own game type, just look at Engine.GameInfo, and DeathMatchPlus for more information.

defaultproperties
{
     GameName="Sniper DeathMatch"
     ScoreBoardType=Class'SniperMod.SniperModScoreBoard'
}

SniperModPRI

PlayerReplicationInfo's are classes used to store player data that is persistent between spawns, i.e. stuff like the name, team, and score.  This makes it an excellent place to keep track of the player's accuracy, and so we make a new version and add a couple variables to it.  Note that only the SniperModPRI is shown here, the bot version subclasses BotReplicationInfo, but contains the same new variables.

class SniperModPRI extends PlayerReplicationInfo;
// Keep track of shots fired and hit
var int TotalFired, TotalHit;
replication
{
  // Replicate from server to client
  reliable if ( Role == ROLE_Authority )
    TotalFired, TotalHit;
}

SniperModHeadShotMessage

If you remember, our modified game type sends a message to the player whenever they get a bonus, and this message is of the same type of UT's kill messages ("X killed Y", "X is Godlike!").  Each bonus has it's own message class, which is somewhat cumbersome, but since you only have to modify one or two lines to change the message it's really not that big of a deal.

class SniperModHeadShotMessage extends CriticalEventPlus;

GetString() is called when it comes time to display the actual message, so we override it to return our custom string, in this case the head shot bonus message.

static function string GetString(
  optional int Switch,
  optional PlayerReplicationInfo RelatedPRI_1,
  optional PlayerReplicationInfo RelatedPRI_2,
  optional Object OptionalObject
  )
{
  return "Head Shot +2pts";
}

All of the string formatting and placement is handled by a couple variables, which we set in the default properties.  Check out LocalMessage, LocalMessagePlus, and CriticalEventPlus for all of the various settings.

defaultproperties
{
  FontSize=1
  bIsSpecial=True
  bIsUnique=True
  bFadeMessage=True
  bBeep=True
  DrawColor=(R=0,G=128)
  YPos=126.000
  bCenter=False
}

SniperModScoreBoard

Creating a new scoreboard is simple, just subclass the UT scoreboard, add in your changes, and then change the scoreboard class in your new game type.  All I've done here is add in a new header that displays player accuracies, and changes the "Frags" header to "Points".

class SniperModScoreBoard extends TournamentScoreBoard;

Override DrawCategoryHeaders() to add in our new "Accuracy" header.

function DrawCategoryHeaders(Canvas Canvas)
{
  local float Offset, XL, YL;
  Offset = Canvas.CurY;
  Canvas.DrawColor = WhiteColor;
  // Draw the normal headers
  Super.DrawCategoryHeaders(Canvas);
  // And then draw a new accuracy header
  Canvas.StrLen("Accuracy", XL, YL);
  Canvas.SetPos((Canvas.ClipX / 8)*4 - XL/2, Offset);
  Canvas.DrawText("Accuracy");
}

And then override the DrawNameAndPing() function to draw the actual accuracy statistic for all the players.

function DrawNameAndPing(Canvas Canvas, PlayerReplicationInfo PRI, float XOffset, float YOffset, bool bCompressed)
{
  local float XL, YL, XL2, YL2, XL3, YL3;
  local string Accuracy, Points;
  // Draw the name and ping
  Super.DrawNameAndPing(Canvas, PRI, XOffset, YOffset, bCompressed);
  
  Canvas.Font = MyFonts.GetBigFont(Canvas.ClipX);
  Canvas.StrLen( "0000", XL, YL );
  // Figure out the accuracy and convert it to a string
  Accuracy = "-";
  if (SniperModPRI(PRI) != None && SniperModPRI(PRI).TotalFired != 0)
  {
    Accuracy = string(100.0 * (float(SniperModPRI(PRI).TotalHit)/float(SniperModPRI(PRI).TotalFired)));
    Accuracy = Left(Accuracy, InStr(Accuracy, ".") + 2) $ "%";
  }
  else if (SniperModBotPRI(PRI) != None && SniperModBotPRI(PRI).TotalFired != 0)
  {
    Accuracy = string(100.0 * (float(SniperModBotPRI(PRI).TotalHit)/float(SniperModBotPRI(PRI).TotalFired)));
    Accuracy = Left(Accuracy, InStr(Accuracy, ".") + 2) $ "%";
  }
  // And draw the accuracy
  Canvas.StrLen(Accuracy, XL2, YL);
  Canvas.SetPos(Canvas.ClipX * 0.5 + XL * 0.5 - XL2, YOffset);
  Canvas.DrawText(Accuracy, false);
}
defaultproperties
{
     FragsString="Points"
}

SniperMod.upkg

Whenever you compile a new package there are some flags that can be toggled to specify whether this package is server side only (ServerSideOnly), client optional (ClientOptional), and downloadable (AllowDownload).  Depending on your package the serverside/clientoptional variables will change, but most likely you'll want to enable AllowDownload so that players can connect to a server and automatically download your mod.  (This is disabled for UT's packages so that someone with the demo couldn't just download the full game.)  Just create a file in your mod's Classes directory named <PackageName>.upkg - in SniperMod's case it is SniperMod.upkg, and then add the following:

[Flags]
ClientOptional=False
ServerSideOnly=False
AllowDownload=True

SniperMod.int

When you make a new game type, you need to make a .int file so that the UT menus can find it to add it in.  You can accomplish this by creating a .int file, similar to a .upkg file except that it goes in the System directory along with the compiled package.

[Public]
Object=(Name=SniperMod.SniperModGI,Class=Class,MetaClass=Botpack.TournamentGameInfo)
Preferences=(Caption="Sniper DeathMatch",Parent="Game Types",Class=SniperMod.SniperModGI,Immediate=True)

And that about covers it.  If you have any questions, I suggest you download the mod and look at the source first, and then if you still need help with something jump on the forums.  The best way to learn sometimes is just to get in and try, and to see how other people have accomplished similar things.