Multiplayer Mutators
This guide should provide the basic foundation for creating fully-functioning multiplayer mutators, and multiplayer code in general. It is taken from the tutorial of the same name, written for the official Tripwire Interactive forums by Benjamin.
It is geared towards Killing Floor which uses UE 2.5, however it should be compatible with other games and newer versions of the engine.
It does not go into great deal about the inner workings of UE's networking, since there are too many intricate details to cover, so those who are curious can get in-depth information from the offical networking overview over on UDN.
Server-side mutators
A server-side mutator is one that executes exclusively on the server, where it has control over the major elements of the game. Since the server is the authority on everything important to the game (such as spawning enemies, deciding who takes damage) a server-side mutator can alter these elements to spawn more enemies, alter weapon damage, and more. However, elements that are local the client (such as the HUD and local effects) cannot be altered by the server since they are managed exclusively by the client. If you want to write a mutator that (for example) adds nightvision or makes certain objects stand out it must execute on the client.
Client-side mutators
Strictly speaking there is no such thing as a mutator that is only client-side, since a mutator is part of the game, which of course is managed by the server. However, you can create a mutator that executes on client machines and then use some logic to prevent the server from running the same code. For example, you may wish to spawn a local effect such as a muzzle flash - the server doesn't need to spawn such superficial actors, so you can add some logic to prevent the server from executing this code.
Theory
Actor roles
Every actor has a pair of role attributes associated with it, which describe who owns the actor and how the other side should manage it. When writing a mutator that needs to execute client-side you must use the simulated proxy role which allows functions marked with the simulated keyword to be executed. Simply, the idea behind this concept is that the client can perform simulation of certain actors (for instance, physics simulation for projectiles) by calling the actor's functions locally. If the client couldn't do simulation the movement of such actors would be very jagged and laggy since you'd be simply viewing them at each set of coordinates that the server sends you, instead of being able to smoothly interpolate between them.
These are the default role attributes for a mutator:
defaultattributes { Role=ROLE_Authority RemoteRole=ROLE_None }
And here are the roles you want if your mutator needs to execute client-side:
defaultattributes { Role=ROLE_Authority RemoteRole=ROLE_SimulatedProxy }
Note that it's not necessary to specify the Role attribute since this is the default.
When you set role attributes for an actor you set them from the perspective of whoever spawns that actor. For example, if the server spawns this actor they will be the authority, and the client will have no role (meaning it doesn't interact with the mutator at all). See here for more details on actor roles.
Actor relevance
For the sake of bandwidth the client doesn't work with any actors that aren't relevant to it. For example, if another player is on the other side of the map and you can't seem them, there's no need to know where they are and perform simulation on their movement. As a mutator is never 'seen' in the world it will never be considered relevant to other players, which is why you must force it to be relevant by setting this in the mutator's default attributes:
bAlwaysRelevant=true
Otherwise the mutator will be never considered relevant to clients, and they won't be able to execute any of its functions. See here for more details on actor relevancy.
Variable replication
Replication is where the server (or client) gives a copy of a variable to a client (or server) in order to match the state of the actor on both machines. For example, player health is calculated and managed by the server, so when the health variable changes the server will send an updated copy of this variable to the client. By default a variable isn't replicated - you have to explicitely write some logic to show which are, and which direction they are replicated in (either from client to server, or server to client, but never both for the same variable). It's very important to remember this, since if you want to write a mutator that changes (for example) the maximum health of a player you must change it on the server. Conversely, if you want to change a client-local actor such as a HUD element it must be done on the client. It's important to note whether a variable is replicated or not when working with them. To see if a variable of a particular class is replicated, look for the replication statements in the source file where the variable is declared.
Writing a client-executable mutator
Example
Rather than giving a long-winded explanation on all the details of writing a mutator that may execute client-side, I'm going to show you an example and give a brief explanation on how it works. The following example writes a string to both the server's log and the client's log.
class ExampleMutator extends Mutator; simulated function PostBeginPlay() { Log("PostBeginPlay() was called"); } defaultproperties { GroupName="KFExampleMutator" FriendlyName="Example Mutator" Description="Mutator description here" RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=true bAddToServerPackages=true }
Explanation from top to bottom:
simulated function PostBeginPlay()
The simulated keyword indicates that the function should be allowed to execute on the client. It's often used for actors that require local simulation (such as interpolation of other players on the screen). It can be applied to existing functions and your own functions. If a simulated function calls a non-simulated function, the latter function won't execute on the client. Additionally, functions declared with the keywords final or native will execute on the client.
{ Log("PostBeginPlay() was called"); }
Calling the log function writes a string to the log (either killingfloor.log or server.log depending on whether the client or server executes it). It's very useful for debugging.
defaultproperties { GroupName="KFExampleMutator" FriendlyName="Example Mutator" Description="Mutator description here"
Standard mutator attributes.
RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=true bAddToServerPackages=true }
These three lines are very important if you want to execute functions client-side.
The first line indicates that the actor's role on the client is that of a "simulated proxy", meaning we can call functions on it to handle local simulation. This is used for anything that the client needs to create a smooth simulation of - projectiles, the movement of other players, etc.
The second line indicates that the actor is always relevant, meaning its functions will always be called by every client, and any replicated variables will always be transmitted to all clients. When an actor (such as a projectile) is in view of a player it is considered relevant since the player needs information on the actor's coordinates and it may need to handle local simulation of its movement. As a mutator isn't 'viewed' by a player it'll never be considered relevant, so we use the bAlwaysRelevant attribute to force it to always be relevant to all clients.
The third line indicates that the mutator package should be included as a standard server package, meaning clients will download it when they connect.
Writing a client-only section of code
If you are writing a piece of client-specific code that you don't want the server to execute, you can simply check the NetMode variable:
if (Level.NetMode != NM_DedicatedServer) { // Execute client-only code here }
Examples
Client-side functions
class ExampleMutator extends Mutator; simulated function Tick(float dt) { local RedWhisp RW; foreach DynamicActors(class'KFMod.RedWhisp', RW) { RW.mColorRange[0] = class'Canvas'.static.MakeColor(rand(255),rand(255),rand(255),255); RW.mColorRange[1] = class'Canvas'.static.MakeColor(rand(255),rand(255),rand(255),255); } } defaultproperties { GroupName="KFExampleMutator" FriendlyName="Example Mutator" Description="Mutator description here" RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=true bAddToServerPackages=true }
The RedWhisp effect is spawned locally on the client, meaning if we want to change any of its values we must do so on the client.
Client-side functions II
class ExampleMutator extends Mutator; simulated function PostBeginPlay() { if (Level.NetMode != NM_DedicatedServer) class'KFChar.ZombieClot'.default.AmbientGlow = 255; } defaultproperties { GroupName="KFExampleMutator" FriendlyName="Example Mutator" Description="Mutator description here" RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=true bAddToServerPackages=true }
Since the AmbientGlow variable isn't replicated from the server unless the server changes it, we can change it locally and it won't be overwritten by the server's copy.
Variable replication
class ExampleMutator extends Mutator; var color TrailColor; function PostBeginPlay() { SetTimer(1, true); } function Timer() { TrailColor = class'Canvas'.static.MakeColor(rand(255),rand(255),rand(255),255); } simulated function PostNetReceive() { local RedWhisp RW; foreach DynamicActors(class'KFMod.RedWhisp', RW) { RW.mColorRange[0] = TrailColor; RW.mColorRange[1] = TrailColor; } } replication { unreliable if (Role == ROLE_Authority) TrailColor; } defaultproperties { GroupName="KFExampleMutator" FriendlyName="Example Mutator" Description="Mutator description here" RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=true bAddToServerPackages=true bNetNotify=true }
Here we enable bNetNotify and handle client-side updating of the effect by using the PostNetReceive function which is called after variables have been received.