The goal of this UnrealScript tutorial is to create a small mutator that tracks and broadcasts "dying sprees", i.e. consecutive deaths of a player without any kill by that player. This tutorial is mainly targeted towards UT2004, but I hope I got the UT, UT3 and UT2003 parts right as well. (I didn't compile a single line of code for this article, so in case of errors or problems, please leave a note on the discussion page.)
Contents
Before we start
While this tutorial centers around a very simple idea, its main goal is to teach you how to modify rules of the game, not how to manage your project. Please find other "getting started" tutorials if you need help with the project setup. The legacy part of this wiki contains "Hello World" examples for UT/UT200x and for UT3. Personally I use UnCodeX and (at least for UE1/2 projects) UMake, so I simply tell UnCodeX to create a subclass in a new package (for this tutorial I will use the project name "DyingSpree"), optionally adjust the EditPackages list in UMake and my project is ready to compile. Please refer to WOTgreal or nFringe tutorials if you use one of those IDEs instead.
Getting started
Before we start hacking in some code, we first need to figure out what exactly we need. The goal is to count player deaths and detect kills that might reset the count. Fortunately there's a perfect function for this: ScoreKill. In UT (and the UDK) it is part of the Mutator class, later engine generations moved many Mutator functions related to the gameplay rules (including ScoreKill) to the GameRules class. A Mutator class is required anyway to activate our Dying Spree mod.
In other words, if you are working with UT or the UDK, only create the Mutator subclass. You can use any name you want, but for this tutorial I will use the UT200x naming convention and call it "MutDyingSpree". If you are using UT3, make sure to subclass UTMutator instead. For UT200x or UT3 you also need a GameRules subclass, which I will call "DyingSpreeRules" in this tutorial. If you compile the project at this point you should get DyingSpree.u in the System directory for UT/UT200x or UTGame\Unpublished\CookedPC\Scripts directory for UT3.
Adding the mutator
Now to tell the game about the new mutator.
For UT and UT2003 you will need to create a localization file and add your mutator information to it. Since the project is called DyingSpree (causing the package to have the name DyingSpree.u), the obvious choice for the localization file is "DyingSpree.int". As always in UE1/2, this file belongs in the System directory. It is a plain text file, so you can open it in Notepad or any other editor you like. Add the following two lines to it, save and open the game to check the mutators list:
[Public] Object=(Name=DyingSpree.MutDyingSpree,Class=Class,MetaClass=Engine.Mutator,Description="Dying Sprees,Announce when a player dies multiple times in a row without making any kills.")
UT2003 doesn't use the description part, so you can leave it out entirely. For UT the part of the Description value before the first comma is the mutator name, everything after the first comma is the description. If there's no comma, the entire value is used as mutator name.
For UT2003 and UT2004 the name and description are stored in the mutator class as default values of the FriendlyName and Description properties respectively. Create a defaultproperties section in MutDyingSpree.uc and add the two values:
class MutDyingSpree extends Mutator; defaultproperties { FriendlyName="Dying Sprees" Description="Announce when a player dies multiple times in a row without making any kills." }
Recompile to ensure everything is syntactically correct. UT2004 will create a UCL file with the name "DyingSpree.ucl" containing the mutator's class, name and description. You may have to delete it and recompile if you make changes to the FriendlyName or Description values.
UT3 and the UDK use a special INI file to store information about the mutator and a localization file for the name and description. For our simple example we will put them in the INI file as well. (I don't know about UDK here, you might have to use the localization file.) In our case the INI file should be called "UTDyingSpree.ini" and is located in the UTGame\Config directory. Make sure it contains the following lines:
[MutDyingSpree UTUIDataProvider_Mutator] ClassName=DyingSpree.MutDyingSpree FriendlyName=Dying Sprees Description=Announce when a player dies multiple times in a row without making any kills.
Keep or remove the other lines as you like, they don't really matter as they are the default values anyway.
Setting up the GameRules
You can skip this step in UT and the UDK as it doesn't apply. Instead, put any code in the MutDyingSpree class.
Ok, since ScoreKill is only called for GameRules, not for a Mutator, we need to set up our DyingSpreeRules. The ideal place for this is the MutDyingSpree's initialization, for example the PostBeginPlay function. Let's use that to spawn and register our DyingSpreeRules:
function PostBeginPlay() { // only for UT200x: Level.Game.AddGameModifier(Spawn(class'DyingSpreeRules')); // only for UT3: WorldInfo.Game.AddGameRules(class'DyingSpreeRules'); }
This not only creates our rules modifier, but also tells the game to call ScoreKill (and a few other functions) when appropriate.
This kind of manual registration is not necessary for the mutator itself because that is automatically registered when the game creates it.
Detecting player kills
Now to implement our kill/death detection. For UT and UDK this happens in the MutDyingSpree class, UT200x and UT3 do this in the DyingSpreeRules.
Rule number 1 when overriding Mutator or GameRules functions: These usually call the next rule/mutator's implementation, so make sure you don't break the chain! If you are not sure if you are overriding a such a function or a "normal" function, simply call the super class implementation. At this point you might wonder why we didn't do that for PostBeginPlay. Well, PostBeginPlay is declared in the Actor class, but neither there, nor in Info or Mutator it is overridden to contain any code. Calling the super class implementation there wouldn't have any effect so we can safely omit it.
Let's override ScoreKill:
function ScoreKill(Controller Killer, Controller Killed) { Super.ScoreKill(Killer, Killed); log(Killer@"killed"@Killed); // use `log(...) instead for UT3 and UDK }
In UT the two parameters are of type Pawn instead of Controller.
This doesn't do much yet, except write a line to the log file whenever ScoreKill is called. Compile and try playing an Instant Action or Practice Session with the Dying Sprees mutator. Switch to window mode ("endfullscreen") and open the log ("showlog") to see the live log. Whenever a player dies, the "A killed B" line should show up now. UT2004 should be started with the -makenames parameter ("D:\Path\To\UT2004.exe" -makenames) to make the output meaningful, as otherwise you will only see something like "XPawn killed XPawn". The parameter ensures the game adds numbers to the object names, like UT and UT3 do. Play around with different ways to die (e.g. suicide command, killed by weapon fire, jump into lava, add and remove bots) to get a feel for the ScoreKill function's parameter values.
You can also try starting a local dedicated server with your mutator and connect to it. This will allow you to figure out what happens when you die while becoming a spectator or because you are about to disconnect. Your ScoreKill logs will only be added to the server's log file, not the client's. This is an important little fact: Mutators and GameRules only get their functions called on the server!
Keeping track of player deaths
I hope you figured out how the Killer and Killed parameters of ScoreKill relate to the death of players. For reference: Killer is either None or the same as Killed if it was a death the player was responsible for, i.e. a suicide. Killer is something else than None or Killed if one player killed another. That means, whenever a player appears on the Killed side, we should increment that player's death spree. If a player appears on the Killer side but not on the Killed side, that player's dying spree should be reset.
Let's express that in terms of UnrealScript code:
function ScoreKill(Controller Killer, Controller Killed) { Super.ScoreKill(Killer, Killed); // Killed is never None log(Killed@"died, increment spree"); if (Killer != None && Killer != Killed) { // one player killed another log(Killer@"killed someone else, reset dying spree"); } }
So that's the basic logic. Next we need to figure out how to store each player's current dying spree. As a mutator our mod shouldn't touch the Controller or PlayerReplicationInfo, so let's just keep a list of players and their dying sprees in a variable in our Mutator/GameRules class. Each entry in this list will contain two items, the player and the current spree counter. This can be done with an array of structs. The struct could be declared as follows:
struct DyingSpreeCount { var Controller Player; // use type Pawn for UT instead var int SpreeCount; };
The variable to hold our list is simply an array of that struct type. UT doesn't have dynamic arrays, so we need to use a static array there.
// only for UT200x and later: var array<DyingSpreeCount> DyingSprees; // only for UT: var DyingSpreeCount DyingSprees[32]; // supporting up to 32 players
So we know where to store the sprees and when to increment or reset them, but how are we going to keep track of the players? Since this is different for UT, we will need to branch here. Accessing arrays and struct members to increment or reset the counter is the same in all versions, only picking the correct array index and adding players to the array will be different. Let's encapsulate that task in a function, say, GetPlayerIndex.
function ScoreKill(Controller Killer, Controller Killed) { local int PlayerIndex; Super.ScoreKill(Killer, Killed); // increment Killed's dying spree count PlayerIndex = GetPlayerIndex(Killed); DyingSprees[PlayerIndex].SpreeCount++; if (Killer != None && Killer != Killed) { // one player killed another // reset Killer's dying spree count PlayerIndex = GetPlayerIndex(Killer); DyingSprees[PlayerIndex].SpreeCount = 0; } } /** Returns an index into DyingSprees array for the specified player. The player is added to the array if encountered for the first time. */ function int GetPlayerIndex(Controller Player) // Pawn instead of Controller for UT { // TODO }
Maintaining the players list
This is probably the most complicated part of the entire mutator. As mentioned before, UT requires a different strategy, so we will focus on the other versions first. You should still read this section, as it points out problems that need to be solved in the UT version as well.
The main task of GetPlayerIndex is to return an index value. To do so, it needs to perform a linear search on the DyingSprees array to find the index with the player's entry. If no such entry exists, it can just create a new entry and add it at the end of the list.
UE3 is really easy to use here, because it provides dynamic array operations for these steps:
function int GetPlayerIndex(Controller Player) { local int PlayerIndex; // look up existing player entry PlayerIndex = DyingSprees.Find('Player', Player); if (PlayerIndex == INDEX_NONE) { // entry not found, create one PlayerIndex = DyingSprees.Length; DyingSprees.Add(1); // adds entry at end of list with zero spree count and empty player DyingSprees[PlayerIndex].Player = Player; } return PlayerIndex; }
Nice and clean, right? Ok, UT200x doesn't have those convenient array operations, so we need to do everything manually there:
function int GetPlayerIndex(Controller Player) { local int PlayerIndex; // look up existing player entry for (PlayerIndex = 0; PlayerIndex < DyingSprees.Length && DyingSprees[PlayerIndex].Player != Player; PlayerIndex++); if (PlayerIndex == DyingSprees.Length) { // entry not found, create one DyingSprees.Length = PlayerIndex + 1; // adds entry at end of list with zero spree count and empty player DyingSprees[PlayerIndex].Player = Player; } return PlayerIndex; }
In case you were wondering: That first thing is a for loop with an empty body. We don't need any body there, the condition will stop the look as soon as the index reaches the end of the list or the player is found. Unlike the array Find operation we end up with the index being the array length instead of the value of the INDEX_NONE constant, which is -1.
Now this approach will work nicely, but there's a small part we haven't considered yet. What happens if a player leaves the game? Quite simple: The player's Controller is destroyed and leaves an empty slot in our list. Our code simply skips the empty slot, but as players come and go, the list gets longer and longer, which makes our code slower and slower. As you can tell, that's a bad thing, so let's remove those empty slots while we're here. We're already iterating over the list to find the player's index, so how about we just remove any empty slots we come across? We can easily do that by extending the for loop:
for (PlayerIndex = 0; PlayerIndex < DyingSprees.Length; PlayerIndex++) { if (DyingSprees[PlayerIndex].Player == Player) break; // found the player, let's ignore further empty slots for now else if (DyingSprees[PlayerIndex].Player == None) DyingSprees.Remove(PlayerIndex--, 1); // decrement the index because otherwise we'd skip the next entry }
There, all empty slots up to the player's index are now removed. We don't care about empty slots after the player because our primary goal is to find the player. Removing empty slots is just a side effect that speeds up the next call to GetPlayerIndex.
What about UT3? It's the same problem there, so we could just copy over the UT200x code. We'd loose the quick lookup of the natively implemented Find operation, though. You know what? Let's be lazy! GetPlayerIndex is called quite often, so if we remove one empty slot at a time, that would end up having the same result at some point. Players leaving the game doesn't happen too often, so we don't need to waste time doing perfect cleanup.
function int GetPlayerIndex(Controller Player) { local int PlayerIndex; // check if any empty slots need to be cleaned up PlayerIndex = DyingSprees.Find('Player', None); if (PlayerIndex != INDEX_NONE) DyingSprees.Remove(PlayerIndex, 1); // remove this empty slot // look up existing player entry ... }
Granted, in the worst case (no empty slots and player not in the list) this implementation needs to iterate over the entire list twice (once for finding empty slots, once for finding the player), but Find is implemented in native code, which makes it much faster than a loop in UnrealScript. The number of players in the game is limited, so an extra native pass over the list really won't make much difference.
Maintaining the players list in UT
As mentioned before, UT lacks dynamic arrays, so we need to work on a static array. It's not possible to add or remove elements at runtime, so we have two options left. Either we just leave empty slots where they appear or we compact the list, moving all further elements down to close the gaps. The latter approach allows us to keep track of the highest slot in use, which can be used to optimize the linear search. On the other hand, there are only 32 elements in the array and moving elements around to close gaps is complicated. For this tutorial I'll pick the first approach (the lazy one again) and leave it to you to come up with the implementation for the optimized approach.
So, the general implementation will end up being similar to the UT2004 version above. The difference obviously is that new players may end up being added in "dirty" slots that require resetting the spree counter. Also, the search will not only need to skip empty slots, but also need to remember one of them, in case there's no room for a new player at the end of the array.
function int GetPlayerIndex(Pawn Player) { local int PlayerIndex, FirstEmptySlot; // look up existing player entry FirstEmptySlot = -1; for (PlayerIndex = 0; PlayerIndex < ArrayCount(DyingSprees); PlayerIndex++) { if (DyingSprees[PlayerIndex].Player == Player) break; // found the player, let's ignore further empty slots for now else if (DyingSprees[PlayerIndex].Player == None && FirstEmptySlot == -1) FirstEmptySlot = PlayerIndex; // if player is not in the list, add here } if (PlayerIndex == ArrayCount(DyingSprees)) { // entry not found, use the first empty slot PlayerIndex = FirstEmptySlot; DyingSprees[PlayerIndex].Player = Player; DyingSprees[PlayerIndex].SpreeCount = 0; } return PlayerIndex; }
Right, that should be about it. There's a minor problem with this approach: If there are more than 32 players, GetPlayerIndex may return 32, causing array out of bound warnings. I'll leave it to you to figure out a fix or workaround.
Broadcasting spree messages
Now that our mutator keeps track of who died how often, it's time to actually print out the corresponding messages. Because dying sprees are quite similar to killing sprees, we can just reuse the killing spree messages and add our own text and audio to them. Find your game's killing spree message (see example list below) and create a subclass called DyingSpreeMessage.
- KillingSpreeMessage (UT)
- KillingSpreeMessage (UT2003)
- KillingSpreeMessage (UT2004)
- UTKillingSpreeMessage (UT3)
The killing spree message class should already contain all the logic for creating the proper messages, so all you need to do is adjusting the messages and sounds in defaultproperties:
class DyingSpreeMessage extends KillingSpreeMessage; defaultproperties { EndSpreeNote=" ended the dying spree by killing" EndSpreeNoteTrailer="" SpreeNote(0)="is on a dying spree!" SpreeNote(1)="is being bullied!" SpreeNote(2)="is being dominated!" SpreeNote(3)="is being annihilated!" SpreeNote(4)="no longer seems to be playing!" SpreeNote(5)="is synonymous for 'being dead'!" SelfSpreeNote(0)="Dying Spree!" SelfSpreeNote(1)="Bullied!" SelfSpreeNote(2)="Dominated!" SelfSpreeNote(3)="Annihilated!" SelfSpreeNote(4)="Are you still playing?" SelfSpreeNote(5)="You like being dead, right?" // these need to be "SpreeSoundName" instead of "SpreeSound" for UT2004 SpreeSound(0)=None SpreeSound(1)=None SpreeSound(2)=None SpreeSound(3)=None SpreeSound(4)=None SpreeSound(5)=None }
Interestingly the spree text and sound variable names are identical across all UT versions. Only UT doesn't have SelfSpreeNote, so you can't use that without some sort of work-around. Note that UT and UT200x actually allow a spree index from 0 to 9, while UT3 only has room for up to index 5.
Ok, text messages are in place and the mutator tracks sprees. Time to add the broadcasting code to ScoreKill:
function ScoreKill(Controller Killer, Controller Killed) { local int PlayerIndex, SpreeLevel; Super.ScoreKill(Killer, Killed); // increment Killed's dying spree count PlayerIndex = GetPlayerIndex(Killed); DyingSprees[PlayerIndex].SpreeCount++; if (DyingSprees[PlayerIndex].SpreeCount % 5 == 0) { SpreeLevel = DyingSprees[PlayerIndex].SpreeCount / 5; if (SpreeLevel < ArrayCount(class'DyingSpreeMessage'.default.SpreeNote)) BroadcastLocalizedMessage(class'DyingSpreeMessage', SpreeLevel - 1, Killed.PlayerReplicationInfo); } if (Killer != None && Killer != Killed) { // one player killed another // reset Killer's dying spree count PlayerIndex = GetPlayerIndex(Killer); if (DyingSprees[PlayerIndex].SpreeCount >= 5) BroadcastLocalizedMessage(class'DyingSpreeMessage', 0, Killer.PlayerReplicationInfo, Killed.PlayerReplicationInfo); DyingSprees[PlayerIndex].SpreeCount = 0; } }
That's it, unless you implement this in UT. Time to compile and play-test your masterpiece.
Adding SelfSpreeNote to UT
As mentioned above, the SelfSpreeNote array isn't available in UT. Just declaring the array isn't enough, UT is actually missing the function GetRelatedString() from later engine generations that is called if the message is displayed for the player owning RelatedPRI1. From inside GetString() there is no way to know whether the string is requested for the RelatedPRI1 player. To get around this limitation we will make use of the OptionalObject parameter of the LocalMessage, which is not used for killing sprees and therefore available for our little workaround. The idea is to put the receiving player's PlayerReplicationInfo into the OptionalObject so GetString() can check whether it needs to display a different text. Of course we also need to declare the missing SelfSpreeNote array to our DyingSpreeMessage class. Variable declarations go somewhere below the class declaration, but above the first function or state declaration.
var localized string SelfSpreeNote[ArrayCount(SpreeNote)];
Using ArrayCount for the array size here ensures the array has the same size as the SpreeNote array.
So how are we going to put the receiving player's PRI into the OptionalObject? One way would be to send the message already with the proper OptionalObject value, but we'd have to replace the convenient BroadcastLocalizedMessage() call with a loop over all players and handle everything that function does. An easier way is to perform the modification when the message arrives at the client via the ClientReceive() function in the message class itself:
static function ClientReceive(PlayerPawn P, optional int MessageSwitch, optional PlayerReplicationInfo RelatedPRI1, optional PlayerReplicationInfo RelatedPRI2, optional Object OptionalObject) { // set OptionalObject to receiving player's PRI Super.ClientReceive(P, MessageSwitch, RelatedPRI1, RelatedPRI2, P.PlayerReplicationInfo); }
Yes, that's it. The ClientReceive() implementation in the LocalMessage class adds the message to the HUD, so if we call it with the modified OptionalObject parameter (the last one in the Super call), it will do exactly what we wanted. The only thing left to do is getting GetString() to actually return the SelfSpreeNote now, so we need to override that in our DyingSpreeMessage class as well. Before we do that, let's have a look at the GetRelatedString() function from UT2004, so we know what we are actually trying to achieve:
static function string GetRelatedString( optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject ) { if ( RelatedPRI_2 == None ) return Default.SelfSpreeNote[Switch]; return static.GetString(Switch,RelatedPRI_1,RelatedPRI_2,OptionalObject); }
What happens here is a check for RelatedPRI2 being None, which means it's an ongoing killing spree, not the end of a spree. Our dying spree message works the same way - RelatedPRI2 is none while the spree continues and not None when the player manages to end the spree. Note that the end of the spree uses the same string for all players.
With that knowledge, we can add special handling for the case that RelatedPRI1 belongs to the receiving player and RelatedPRI2 is empty:
static function string GetString(optional int MessageSwitch, optional PlayerReplicationInfo RelatedPRI1, optional PlayerReplicationInfo RelatedPRI2, optional Object OptionalObject) { // if displayed for receiving player and not end of spree if (RelatedPRI1 == OptionalObject && ReplatedPRI2 == None) return default.SelfSpreeNote[MessageSwitch]; // display shorter message // else display the longer text return Super.GetString(MessageSwitch, RelatedPRI1, RelatedPRI2, OptionalObject); }
Now the UT version should be ready for compiling and play-testing as well.