Creating Mutators
Welcome! This is the first thematic article series from Team 2/5.
Series Topic: Creating Mutators
- Article 1: Minimal mutator template and GroupName, FriendlyName, Description variables
- Article 2: Mutator types
- Article 3: Combining mutators
Article 1: Minimal Mutator Template
Let’s consider the minimal template of any mutator:
unrealscript
Class TestMut extends Mutator;
defaultproperties
{
GroupName="KF-TestMut"
FriendlyName="TestMut"
Description="Test mutator. Basic template"
}
I’ve always been too lazy to check what GroupName and FriendlyName are. Here’s a reason to study them.
GroupName
So, from the comment in the variable description in the Engine.Mutator class
unrealscript
var() cache string GroupName; // Will only allow one mutator with this tag to be selected.
it becomes clear that this variable is used to allow blocking the addition of mutators from the same group. If desired, you can look at the implementation of this mechanism in Engine.GameInfo.AddMutator — I won’t copy the code here, the curious will look themselves, others don’t need it.
Usage:
- Protection against adding mutators that implement the same thing (or ideologically similar things)
- Protection against adding conflicting mutators
- I’ve never really used this — waiting for your ideas on how this can be used
Important Note:
In KF there’s a page for adding mutators to the game — KFGui.KFMutatorPage. And if we carefully look at the SetCurrentGame function, we’ll see this code:
unrealscript
...
for (i=0;i<MutatorList.Length;i++)
{
if ( Left(MutatorList[i].GroupName,2)== "KF")
lb_Avail.List.Add(MutatorList[i].FriendlyName,,MutatorList[i].Description);
}
...
Which means that only mutators whose GroupName starts with “KF” will be displayed in the list of available mutators. So don’t forget to write GroupName=“KFsomething” if you want your mutator to be available in this menu.
Common approach: GroupName = "KF-MutatorName"
FriendlyName
Well, here it’s pretty simple — this is the mutator name that’s displayed in the mutator selection window. The variable name hints at friendliness. There’s a concept of a user-friendly interface. Same here — the mutator class itself can be named however inconveniently, but in this variable you should write a concise and clear name, ideally so that even the Description isn’t needed for the average player.
You can write the mutator name in Cyrillic too (just don’t forget to switch to Little Endian encoding), for example:
unrealscript
FriendlyName="Test Mutator"
Moreover, you can color the mutator name if you really have nothing better to do. The common Unreal way of “coloring” is described in various Unreal Engine tutorials.
Let’s make the word “Test” red and “Mutator” green:
You can use the ServerColor program, or use a HEX editor (like Hex Editor Neo). I’ll take not pure colors, but just those that I liked. The prefix for red color will be 1B D1 60 60. The prefix for green color will be 1B 3D B7 74.
Enter these values into a HEX editor and you’ll get special color codes for red and green colors.
You can also make it blue.
Description
Well, everything is generally clear here. This is the mutator description that’s displayed in the mutator selection window. Again, you can make it in Russian and colored.
Let’s make it blue. The prefix for blue color can be added using a HEX editor.
Final Mutator Example
In the end, we got this mutator:
unrealscript
Class TestMut extends Mutator;
defaultproperties
{
GroupName="KF-TestMut"
FriendlyName="TestMut"
Description="Test mutator. Basic template"
}
Article 2: Mutator Types
- Mutator that works only on the server (will add a link to the article “client-server model for unreal engine” later).
- Mutator that works on both server and client (while actions may be performed only for clients, no useful code is executed for the server).
- Mutator that works on the server, but requires some resources to be downloaded to the client (because implicit replication goes there).
TYPE #1: Server-Only Mutator
Most mutators are examples of this type. A characteristic feature is complete independence from what happens on the client.
Since such a mutator runs only on the server, we don’t need to download it to the client, and there’s no need to add bAddToServerPackages=true in defaultproperties, call the AddToPackageMap() function somewhere, or write ServerPackages=MutatorPackage in KillingFloor.ini.
Example:
unrealscript
class ServerOnlyMut extends Mutator;
function PostBeginPlay()
{
// Your code here
}
defaultproperties
{
GroupName="KF-ServerOnlyMut"
FriendlyName="Server Only Mutator"
Description="Mutator that works only on the server"
}
Note:
You can create a “PackageName.upkg” file in the mutator folder. And write flags AllowDownload, ClientOptional, ServerSideOnly in it. This text file is used by the compiler.
By writing AllowDownload=false, we prohibit downloading our package to the client, regardless of whether the admin wrote this package in ServerPackages or not.
Marco has this set of flags in ServerPerksMut:
ini
[Flags]
AllowDownload=False
ClientOptional=False
ServerSideOnly=True
I used to not worry about this and added bAddToServerPackages=true to all mutators. Beginners should probably do the same so as not to break their heads unnecessarily.
And another note:
This type of mutator can be launched not as a mutator, but as a ServerActor. I’ll dig into this topic later, as I don’t quite understand the pros and cons of this step. Reminder: ServerActor is an object that is created on the server when loading a map.
TYPE #2: Mutator Used on Client and Possibly on Server
Below are some of the cases when they are needed:
- HUD modification (usually using Interaction)
- handling keyboard/mouse button presses
- calling special effects on client
- need to change properties of objects used only on client. For example: displaying weapon weight in shop
Client mutators are generally a complex topic. It requires understanding how objects are created on server and client. This will be gradually revealed in other articles. For now, I’ll just describe the main characteristic features of the code.
1. For the mutator to work on the client, you need to add 2 lines in defaultproperties:
unrealscript
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
2. The mutator must be downloaded to the client, so you need either the line:
unrealscript
bAddToServerPackages=True
or another alternative way to add the mutator to the package list.
3. Functions used on the client must have the simulated prefix
4. To check whether this mutator instance is running on the client or server, you can do this:
Option 1:
unrealscript
if(Role == ROLE_Authority)
{
//CodeForServer
}
else
{
//CodeForClient
}
Option 2:
unrealscript
local PlayerController PC;
PC=Level.GetLocalPlayerController();
if(PC!=none)
{
CodeForClient;
}
else
{
CodeForServer;
}
Option 3:
unrealscript
if(Level.NetMode==NM_DedicatedServer)
{
//CodeForDedicatedServer
}
else
{
//CodeForClient (provided the mutator is installed on a dedicated server)
}
5. The mutator runs on the client side, and there’s an opportunity to get PlayerController:
unrealscript
Level.GetLocalPlayerController()
Accordingly, for each mutator instance there will be a PlayerController of its owner. For the server, Level.GetLocalPlayerController() will return none. This is the basis for the check in the previous point (option 2). This Level.GetLocalPlayerController() is a very useful thing.
6. On the client you cannot use Level.Game
What does this mean? This means you cannot check if a wave is in progress on the client like this:
unrealscript
if(KFGameType(Level.Game).bWaveInProgress)
{
...
}
You need to use the KFGameReplicationInfo object:
unrealscript
local KFGameReplicationInfo KFGRI;
KFGRI = KFGameReplicationInfo(Level.GetLocalPlayerController().GameReplicationInfo);
if(KFGRI.bWaveInProgress)
{
...
}
7. You need to clearly understand that if you create a client mutator that reads information from an ini file, then client versions of the mutator will look for ini on the client, and the server one - on the server. However, this is a topic for a more complex article.
Example of a client mutator:
Let the mutator write a greeting to the player at the start of the wave (the first wave they play):
unrealscript
class TestClientMut extends Mutator;
//Function runs on client and server thanks to points 1 and 2
//For server Disable('Tick') is called immediately and the function is no longer called for server
//For client we wait for wave start, post message and disable function
//In principle, we could delete the mutator for server and client, not just disable Tick, but it's not that important
simulated function Tick(float dt)
{
local PlayerController PC;
local KFGameReplicationInfo KFGRI;
PC=Level.GetLocalPlayerController();
if(PC!=none)
{
KFGRI = KFGameReplicationInfo(PC.GameReplicationInfo);
if(KFGRI.bWaveInProgress)
{
PC.ClientMessage("Greetings Mortal, are you ready to die?");
Disable('Tick');
}
}
else
Disable('Tick');
}
defaultproperties
{
bAddToServerPackages=True
GroupName="KF-TestClientMut"
FriendlyName="TestClient"
Description="Testing base client mutator"
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
}
Example of a server mutator with similar functionality:
unrealscript
class GreetingsMut extends Mutator;
function Tick(float dt)
{
local Controller C;
if(KFGameType(Level.Game).bWaveInProgress)
{
for( C = Level.ControllerList; C != None; C = C.nextController )
{
if(C.IsA('PlayerController') && C.PlayerReplicationInfo.PlayerID>0)
PlayerController(C).ClientMessage("Greetings Mortal, are you ready to die?");
}
Disable('Tick');
}
}
defaultproperties
{
GroupName="KF-GreetingsMut"
FriendlyName="Greetings"
Description="GreetingsMut"
}
In the first case, each client has a mutator that writes a message personally for them. In the second case, we simply go through all players on the server and write a message to each of them. It’s useful to compare these two mutators and clearly understand how each of them works.
TYPE #3: Mutator That Works on Server, But Requires Some Resources to Be Downloaded to Client
It’s like a server mutator, but the problem is - if you don’t download it to the client, it won’t work fully.
For example, a mutator that on the server calls a function that plays a sound ClientPlaySound. If you don’t specify downloading resources to the client, the sound won’t play.
Therefore, if the mutator needs resources from some package to work on the client, you need to download this mutator to the client. That is, add bAddToServerPackages=True in defaultproperties.
Article 3: Combining Mutators
Today we’ll fight against mutator overload. Have you noticed server crashes when changing maps from web admin? Or even when starting the server? All this is a consequence of mutator overload.
The Problem
When you have many mutators, each of them creates its own object, which can lead to:
- Server crashes
- Problems when changing maps
- Increased loading time
The Idea
Use one common mutator, convert old mutators into Actor objects and delegate tasks to them.
Example
Let’s combine three mutators: AutoSpawner, AllTraderMut and KFNoDramaMut into one.
1. Move all mutators into one folder, for example, “ServerMutators”.
2. Create a “ServerAggregatorMut” mutator:
unrealscript
class ServerAggregatorMut extends Mutator;
var AutoSpawner autoSpawner;
var AllTraderMut allTraderMut;
var KFNoDramaMut kfNoDramaMut;
function PostBeginPlay()
{
autoSpawner = Spawn(class'AutoSpawner');
allTraderMut = Spawn(class'AllTraderMut');
kfNoDramaMut = Spawn(class'KFNoDramaMut');
}
defaultproperties
{
GroupName="KF-Aggregator"
FriendlyName="AggregatorMut"
Description="Combined mutator"
}
Since AllTraderMut and KFNoDramaMut don’t contain any functions from the Mutator class, combining them is very simple.
Example code for AutoSpawner:
unrealscript
class AutoSpawner extends Actor config (Mutators);
struct PlayerData
{
var PlayerController PC;
var int WaveNum;
var bool bRespawned;
};
var array<PlayerData> PlayerDB;
var int WaveCounter;
function PostBeginPlay()
{
SetTimer(0.1, true);
}
function Timer()
{
local int ID;
local PlayerController PC;
if (Invasion(Level.Game).bWaveInProgress)
{
for (ID = 0; ID < PlayerDB.Length; ID++)
{
if (PlayerDB[ID].PC == None)
continue;
PC = PlayerDB[ID].PC;
if (PC.PlayerReplicationInfo != None && !PC.PlayerReplicationInfo.bOnlySpectator && PC.PlayerReplicationInfo.bOutOfLives)
{
Level.Game.Disable('Timer');
PC.PlayerReplicationInfo.bOutOfLives = false;
PC.PlayerReplicationInfo.NumLives = 0;
PC.PlayerReplicationInfo.Score = Max(KFGameType(Level.Game).MinRespawnCash, int(PC.PlayerReplicationInfo.Score));
PC.GotoState('PlayerWaiting');
PC.SetViewTarget(PC);
PC.ClientSetBehindView(false);
PC.bBehindView = False;
PC.ClientSetViewTarget(PC.Pawn);
Invasion(Level.Game).bWaveInProgress = false;
PC.ServerReStartPlayer();
Invasion(Level.Game).bWaveInProgress = true;
Level.Game.Enable('Timer');
}
}
}
if (Invasion(Level.Game).WaveNum != WaveCounter)
{
for (ID = PlayerDB.Length - 1; ID > -1; ID--)
{
if (PlayerDB[ID].PC == None)
PlayerDB.Remove(ID, 1);
}
WaveCounter = Invasion(Level.Game).WaveNum;
}
}
defaultproperties
{
VersionString="0.0.8"
DateString="10/July/10"
WaveCounter=-1
}
This way we can “pack” an unlimited number of mutators.
Client-Side Mutators
All mutators considered above run only on the server and are not downloaded to the client. Let’s briefly consider packaging a mutator that’s downloaded and runs on the client.
It’s better to pack such mutators in a separate package, it’s at least logical.
Let’s pack the BrutusPatHPLeft mutator:
unrealscript
class AggregatorMut extends Mutator;
var BrutusPatHPLeft brutusPatHPLeft;
simulated function PostBeginPlay()
{
brutusPatHPLeft = Spawn(class'BrutusPatHPLeft');
}
defaultproperties
{
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
GroupName="KF-Aggregator"
FriendlyName="AggregatorMut"
Description="AggregatorMut"
}
unrealscript
class BrutusPatHPLeft extends Actor config (Mutators);
...
defaultproperties
{
bAffectSpectators=True
bAffectPlayers=True
DisplayString="Health: %h/%m (%p%)"
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
}
The only difference is in 3 added variables in defaultproperties:
unrealscript
bAddToServerPackages=True
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
And in the simulated keyword before functions in the mutator (in this case before PostBeginPlay). Everything else is no different from the server mutator variant.
Notes
Note 1: This is especially relevant for Windows users.
Note 2: The number of packages doesn’t matter, the problem persists even if all mutators are in one package.
Note 3: You can add config variables to the general mutator so you can enable/disable this or that “packed” mutator in settings.
Note 4: Initially the article was much larger, in particular due to “packing” a larger number of mutators. But then we decided — no need. If you have specific questions on how to pack this or that mutator — write.
Note 5: Perhaps it makes sense to use not Actor classes, but Info. This would be more correct in some way, but that’s a topic for a separate article.
Note 6: In particular, it makes sense to add the bHidden=true property to all our Actor objects.
Note 7: Similarly, you can do this when creating additional objects in PlayerReplicationInfo, so as not to execute all code directly in the base replication class.
Note 8: Link to the packaging result: ServerMutators.ServerAggregatorMut, Mutators.AggregatorMut
Articles by Team 2/5