A blatant "copy" from http://unreal.jall.org/tutorials/replication.html, since that link seems to be down (Web Archive copy is available)
Contents
- 1 Introduction
- 2 Replication Overview
- 3 Relevant Actors
- 4 NetPriorities
- 5 Roles and RemoteRoles Introduction
- 6 General Replication
- 7 Variable Replication
- 8 Variable Replication Examples
- 9 Role and RemoteRole Summary
- 10 Function Replication, and Simulation
- 11 Level NetModes and their Implications
- 12 Special Properties about Special Stuff
- 13 When Things Go Awry
- 14 Figure Out What's Going On
- 15 Testing Real-World Conditions
- 16 Conclusion
- 17 Special Networking Tricks (Case Studies)
Introduction
Ask any halfway decent UnrealScript programmer what the most confusing aspect of Unreal coding, and he'll be sure to tell you replication. (or perhaps PlayerMovement or pawn/bot AI :) I've been there myself, and I still have trouble with it sometimes, but I think I know enough to write some documentation. Lots of trial and error helped me get far, along with various insights from people that had the source code to the engine. I've tried to piece it all together to create this document. I have however, recently gotten a lot more information about exactly how replication works, which has helped me immensely in understanding replication. Since I can't post that information directly because of NDA, I'll try my best to explain it to you through this networking documentation. Hopefully I'll be able to expand upon what was described in Epic's documents at: http://unreal.epicgames.com/Network.htm and http://unreal.epicgames.com/UTMods.html. Please read it first, as it will help a lot with background and context. In writing this document, I'm going to assume you've programmed in UnrealScript before, you have at the very least considered the problems of replication, you are reasonably intelligent, you know how to read, and you think my jokes are funny.
First, Unreal Tournament uses a client-server architecture. The client is basically a dumb terminal, that sends data to the server about where the player moved, if they're jumping, if they're shooting, etc. It displays to the user what is given to it by the server. The client's information is necessarily out of date with what exists on the server, because of lag, ping time, and other factors. So in order to present a believable world, the client has logic to simulate what it thinks the server will be sending it. It knows the laws of physics, and that if something is travelling in a straight line, then it will contine to travel with that same velocity. Likewise, if it's falling through the air, it's likely to continue to be accelerated under the force of gravity. The dumb terminal predicts where how the game state will be progressing, and so can provide the user with a more believable game. On the other end of the connection is the server, which holds the authoritative state of what's going on in the game. If the server thinks you are dead, then you are dead. Like your teachers, they're never wrong, no matter how much evidence you've seen to the contrary (like how you *thought* you dodged that rocket). The client gets information sent to it by the server (like where other people are, what things have been shot, etc) , and sends other information back to the server (like where the client wants to move, shoot, etc). In the client-server architecture, the client only communicates with the server, and the server communicates with all of the clients (people connected to the server).
Server as Authority
With the recent brew-ha-ha regarding the ZeroPing mod, which aims to give the client more control, there have been two sides to the issue of why the server as authority is the best model.
A good description of the history of the networking architectures in games can be found at the top of the tech page's networking document. Go read the "History" section now, and then come back. The current model is that of a client-server architecture, where the client has prediction to minimize the effects of lag. In current versions of UT, the client tells the server all about it's input. Where is the user looking, in which direction are they moving, and are they firing primary or secondary fires.. The server then uses that data to calculate information about the player, and to tell other clients about that player. For the most part, that is all there is to it. ZeroPing pioneered an idea of giving the client more power. It allowed the client to decide whether they hit the other player. In theory, this sounds great. The user gets to decide what's happening, and if they hit the player, then they hit the player. Unfortunately, there are problems with this idea.
In a large-scale game like UT or Q3A, many people try to cheat. The people that make the game have to prevent that from happening. With these games, the client does so little logic, that there is little room to cheat. The client sends its data to the server, and the trusted server decides whether they hit the player or not. Cheats are extremely limited in what they can do, as they can only modify the direction the player is going, and whether to cheat or not. It's not something that is really worthwhile. However, when you have something like ZeroPing, you are letting the client decide whether or not you hit the player. A cheat can pretend that it hit every player, and can wreck havoc in such a game, where it is killing everyone the moment they enter view, and sometimes even getting repeated kills ten times in a row on a single player as they respawn and then are 'hit' again. Mods like ZeroPing are mods, and so do not ahve the same public appeal that a full game does. As such, mods are less likely to be hacked for cheats. But it does not make them immune. If game makers were to use the approach that ZeroPing uses, there would be a flood of cheats within weeks of the game's release. You can understand the resistance that the game companies have for such an approach. It is not because it's the 'not-invented-here' syndrome as many have suggested, but rather that it really is much more open to cheating.
As a side note, I guess the ZeroPing author does realize this. He has hidden his code to make it impossible to cheat with. He took the security through obfuscation approach. This will work for awhile, until code decompilers become available, or the code gets out. While I am not working on any of these myself, I do know of some UnrealScript code decompilers in the works. So this approach really will not work in the long run.
Replication Overview
Replication deals with how information is sent from the server to the client and vice versa. Because of bandwidth limitations for modems, not everything can be sent, and because of the unpredictability of the Internet, not everything can be ensured of arriving on time. Only a subset of all the information is sent to the client, and replication deals with that subset that travels from the server to the client, and deals with how the client sends its relevant information back to the server.
While the definition of replication may sound simple, there is a lot of underlying assumptions and quirks that you must be aware of, which UT uses in the interest of optimum bandwidth and speed. It's an unmapped jungle out there, and many an explorer has been lost to the dangerous native inhabitants. This guide you are reading is your map and survival guide.
First, the Unreal server does some logic like the following for each client connected:
- Run all events (like tick, timer, hitwall, etc)
- Go through each client connected to the server
- Get the list of relevant actors for this client.
- For each actor, replicate variables that need to be replicated.
- Replicate any function calls that should be replicated to that particular client, if they were called during the previous server-side events run above.
The client does the following logic:
- Receive data from the server, and update its local copies of the actors
- Run any replicated calls that were called on the server
- Run any simulated events (like tick, timer, hitwall, etc), and do the local playerpawn handling
- Find out what replicated were called on the client and need to be replicated to the server, and send them
If that didn't make any sense, don't worry about it. They just outline the main parts of replication, and they will all be explained later.
If you did understand that, there's still plenty to learn. UT has a lot of logic going on under the hood in regards to replication, most of which is not immediately apparent. That was the purpose of this document, to explain these mysteries to you, the reader, so that your networking development will go that much more smoothly. Again, all this will be explained in due time, assuming you read the whole document. :)
Relevant Actors
First, we need to define what a relevant actor is. A client, as the dumb terminal, needs to know about everything that it will need to display on screen. It needs to know about the other players, and weapons in the level, if any projectiles are visible, if the flag is visible, health packs, etc. All of these actors constitute the relevant set of actors. how is the relevant set of actors determined? If an actor is not relevant, no amount of replication statements or voodoo magic will cause its information to be sent to the client. It must be relevant to that particular client in order for data to be sent to that client. For Unreal / Unreal Tournament, the method of determining relevant actors is as follows (ripped from the tech page's networking document):
The following logic is performed on each actor, and you should be able to tell whether it is relevant. The actor must pass two discrete sets of steps in order to be considered relevant. One it passes the first test, it must then pass the second. The rules higher in the list have precedence over rules that follow them:
- If it is bNetTemporary, then it does not pass the first stage.
- If it is not bStatic and not bNoDelete, then it passes the first stage.
- If it is a LevelInfo, it passes the first stage.
- If it is a temporary network actor, (as defined by bNetTemporary), and it's been sent to this client already, then it does not pass the first stage. These types of actors will be decribed in more detail later.
- If its Role is ROLE_None, then does not pass the first stage.
- If it is bAlwaysRelevant, and time since the last relevancy is less than 1 / NetUpdateFrequency (described farther below) seconds, then it does not pass the first stage. This basically means an actor will only update it's variables to the client NetUpdateFrequency times per second. (NetUpdateFrequency values: Actor: 100, ZoneInfo: 4, GameReplicationInfo: 4, PlayerReplicationInfo: 2, or Inventory: 8).
- If it is bNetOptional, and it has a non-zero LifeSpan (meaning it does not exist forever), and it is not in the first 150 ms of its lifetime, then it passes the first stage. Any attempt at replicating a bNetOptional actor after the first 150 ms, (because it was pushed off the replication list due to bandwidth contraints for that client, etc,) will fail.
- If the time since the last relevancy is greater than 1 / ~NetUpdateFrequency seconds, it passes the first stage. (This is a more finely tuned optimization of NetUpdateFrequency, tuned to the type of actor. It will only perform the calculations needed if it has passed through the weeding techniques above.)
- It does not pass the first stage.
If it passed all of those steps, it's not through and clear to be relevant yet. It has another step to go through. If it is deemed relevant during the following steps, then it is relevant to this client.
- If it is bAlwaysRelevant, then it is relevant.
- If it is owned by the client or owned by the actor we are viewing through (as returned by PlayerCalcView), then it is relevant.
- If it is the client or the actor we are viewing through (as returned by PlayerCalcView), then it is relevant.
- If its Instigator is the client, then it is relevant.
- If it has an ambient sound and is within 'audible distance', then it is relevant.
- If it is a weapon that is owned by a visible Pawn owner, then it is relevant.
- If it is bHidden or and not bBlockPlayers and the AmbientSound is none, then the actor is not relevant.
- If it is not relevant (per the above tests), but if any of the above conditions made it relevant within the last RelevantTimeout seconds, (by default, five seconds, but is defined in UnrealTournament.ini:[IpDrv.TcpNetDriver],) then it is relevant.
- It is not relevant.
Those rules, while seeming complicated, are exactly how UT determines what is relevant to each client. Many times such checks are not related to what you are doing, but they are always good to know. Thinking about it, it makes sense. If something is not visible, then the client probably does not need to know about it. If it's a player on the other side of CTF-LavaGiant, what use does this client have for it? Or if it's an invisible pathnode used for Bot AI, the client also has no use for it. Sending data and variables for such actors would be wasteful on a tiny little 28.8 kbps modem.
Working this discussion into a guideline for your own code, you should set your actors' bAlwaysRelevant to true if they are data storage units that the client needs to know about, like PlayerReplicationInfo or GameReplicationInfo. Things like PlayerReplicationInfo, which store information about the player, are always relevant. This allows a client to view the Scoreboard and see the information (name, score, ping, packetloss, etc) for everyone in the level, regardless of whether that player actor is relevant to the client. PlayerReplicationInfo's allow precious information that's small, bandwidth wise, to be relevant and replicated to each client, yet have information like the location, velocity, weapon, animations, etc of the player to not be replicated when it's not necessary (because it's in the Pawn/PlayerPawn, which is not always relevant.) Hopefully I've not lost you here. It's an important thing to know and remember about Unreal replication. Where to put your variables, and how to make them relevant to the client. If you needed some important game information to be seen by all clients, your best bet would be to subclass ReplicationInfo and utilize that for your data variables. Information that is only needed when it's seen is best put in regular actors, that will go in and out of relevancy as time passes.
In a nutshell, if the client needs to know about it, they will. All inventory is relevant to the client, as is all nearby players. All projectiles and weapon effects that are visible are relelvant. Anything that affects the player's world on the client, will be replicated. This includes things that block the player's movement, or actors that play ambient music. There is an overhead to spawning a new actor in netplay, and so UT does not immediately remove an actor from the relevancy list when it goes out of view. It gives it a few seconds before it is really considered not relevant. Oh, and UT takes into account the fact that a player may be viewing through another actor's eyes, as in the case of the GuidedWarhead, or the teammate views when you hit the number keys on the right side of your keyboard.
NetPriorities
While that's the "simple" description, figuring out why things happen can get a bit more complicated. Unreal Tournament gives different priorities for different types of actors. NetPriority, which should typically be between 1 and 3, determines how to prioritize a given actor's bandwidth "allocation" in the stream being sent to each client. Actors are first sorted by a calculation based upon their priority (adjusted to take into account how close the client is looking at them, etc.) Actors marked bNetOptional are sent to the bottom of the list. So....actors with a higher priority are sent first in the bandwidth stream, and when the connection becomes saturated, any remaining actors are starved of bandwidth until later. (UT remembers which actors have not been replicated, and so it can give bNetOptional actors 150 ms to get replicated before they are discarded. See the above relevancy list for more information.)
Here's a wide and almost complete sampling of UT's NetPriorities:
Bots | 3.0 |
PlayerPawns | 3.0 |
GuidedRedeemer | 3.0 |
Movers | 2.7 |
Projectiles | 2.5 |
Carcasses | 2.5 |
Other Pawns (Spectators, Titans, Bunny Rabbits, etc) | 2.0 |
Effects (sparks, blood, smoke) | 2.0 |
Simpler effects (shell casings) | 1.4 |
Inventory | 1.4 or 2.5 |
Everything else | 1.0 |
This shows that the server considers a Player's (be it bot or playerpawn) information to be much more important than something like bullet casings from a gun. And it makes sense. A guided redeemer also has unpredictability to it, since it is controlled by a player, and thus needs a higher NetPriority. A projectile follows the simple Physics given to it, eg: PHYS_Falling, PHYS_Projectile, and so they are not very unpredictable. Thus, its NetPriority is not as high as a player's. However, projectiles are still quite important to gameplay (doesn't it suck to get hit by that rocket you never saw? :), and they get a high NetPriority. It's the same with the carcasses, since their animations as they fly through the air or fall to the ground are quite important (not to gameplay, but to that touchy-feel-good feeling when you kill someone.) You should use these examples as a guide when creating your own actors that need a different priority.
One additional thing to note is that you can change the NetPriority on the fly. This is done with Inventory, for example. It exists as 1.4 for most of its life, but when it is thrown from a player's inventory, it now is a moving actor with more importance attached to it. It is then given a higher NetPriority. Once it hits the ground, it gets a NetPriority of 1.4 again, since it does not need to be updated as often when it is sitting stationary on the ground.
In general, all of the relevant actors for a particular client are put in an array each tick, and then sorted by their priorities. The server then starts replication the actors, starting with the highest NetPrioritized actor, working it's way down. UT then stops replicating actors when the bandwidth becomes saturated. That's why it's important to prioritize your actors correctly, so that when a lot of actors are on screen, or on the lowly 28.8K connections, UT will know how to perform triage, and who to starve of bandwidth first. If an actor is not replicated, the variables it was supposed to tell the client about are not forgotten. They will be put on the list during the next tick, to have another go at getting replicated. Note that doubling the size of the actor's NetPriority does not actually change anything in regards to giving it more bandwidth, since a NetPriority only has meaning in relation to other NetPriorities. Since 3.0 is the highest on the list, there is no real difference between giving your actor a NetPriority of 4.0 and 4000.0, since they will both be placed at the top of the list.
Roles and RemoteRoles Introduction
Now is a good a time as any to explain the purposes of the various roles in Unreal. Roles simply guide UT in determining what role this particular actor plays in the game of replication. For example, a player needs to be replicated quite differently from an ammo box sitting on the ground, or a projectile moving according to the rules of physics. On a server, the Role will be ROLE_Authority, which makes sense since that actor is currently being evaluated on the authoritative server. The RemoteRole of that actor will be set to any of a variety of roles, defined below. On the client, the Role and RemoteRole will be reversed. On the client, the RemoteRole will be ROLE_Authority, and the Role will be one of the other role types. Let's go through the various roles (which will be set in the RemoteRole default properties), in order of increasing 'control.'
- ROLE_None
- This role is probably the simplest. It simply tells the server not to make this actor relevant to the client. No information will be sent about it, and it will exist where it was created only. If it was created on both the client and the server (via a simulated function, described later), and given a Role of ROLE_None, then it would exist both on the server and on the client, and they would both operate independently of each other.
- ROLE_DumbProxy
- This role is used for objects that don't move much, yet still need information about them updated to the client. The server does this by sending periodic updates about this actor's location and velocity to the client. If the actor has any sort of logic to it, with Physics, for example, it's much better to use the following role.
- ROLE_SimulatedProxy
- This is used for anything that should be simulated over the network, via prediction. Take a rocket, for example. It always travels in a straight line, and is an ideal candidate for SimulatedProxy. By the same token, UT can predict the falling physics clientside, and so grenades, and falling people are also good candidates for this role. Anything that can be predicted clientside because of Physics or clientside physics should use this Role. All Bots and PlayerPawns use this role as well, since when a player is running, he's more than likely to continue to run. It's the best prediction method that can be used for unpredictable players. The only exception to this is the playerpawn you yourself are playing as.
- ROLE_AutonomousProxy
- This is used for you. When you play online, you yourself should be treated differently from all other playerpawns. You don't want your own self being predicted based upon the server's expectations. Rather, you want to control yourself and tell the server where you are moving. With this Role, that's easily possible. Many things that should only be done for your own playerpawn, like server-corrections of your location when you get lagged out, are only done with this role.
- ROLE_Authority
- This role is different from the others in that it is always the Role on the server. On the server, all Roles will be ROLE_Authority, and the RemoteRole will be one of the above. On the client, the reverse will be true. This role doesn't have any real significance, beyond it's use in replication statements, described below.
General Replication
For each actor that is deemed to be relevant to a particular client, the server needs to determine what subset of its variables will be sent to the client. The most obvious rule is to only send variables if they change. Unfortunately, that rule alone is not enough to fit data over a modem. Instead, the server has to further whittle down its list of variables, and it does this through replication statements. These replication statements are executed for each client, for each relevant actor to that client. When a subset of the variables of a subset of the actors are sent over the network, only then can it be fit on a regular modem. But how can a client operate without all of the information in the game? Once you stop and think about it, it's not quite that unthinkable. Do all of a Bot's AI variables need to be replicated to the client? Only the result of the AI needs to be sent, which basically consists of the same thing for any other Pawn in the game. Also, depending upon the RemoteRole, different things are replicated. Some cause the Location to be updated periodically as a poor man's simulation of its motion, while another role causes the physics to be replicated, and the location not to be, creating true client-side simulation of the actor's motion.
No Recursive Relevancy
Please note one important point that can easily be overlooked. Relevancy is not recursive with replicated references to variables. Let's say that Actor A contains a reference to Actor B in Property C, and Actor A replicates Property C (currently set to Actor B). Let's also say that Actor A is relevant, and Actor B is not. (Don't worry, there's only three letters in this example.) In this example, Actor A is relevant, and so it's Property C variable will be replicated to the client. Now what does the client know? The client knows about Actor B through Actor A's reference, but Actor B is not relevant. All of Actor B's variables are not checked for replication. The client only knows about Actor B, which really is of no use whatsoever, since the variables are not replicated. I initially thought that replicated variables to actors caused those actors to be relevant and parsed themselves for replicated variables, but that view is false. An actor can only be relevant according to the rules stated above, which make no provision for a reliable reference to them from a relevant actor.
Replication Checks
When you think about the number of clients in a server, and the number of relevant actors for each client, and the number of replication checks on variables for each actor, there's a large number of such checks. Special care should be taken when implementing replication statements. As each conditional is evaluated, you should be considerate of the server's limited processing ability. Don't call functions from within replication statements, as function calls are much too slow for replication statements. Along the same lines, you should try to keep your checks as simple as possible, only evaluating a condition if its necessary. Finally, note that Epic had a problem with replication checks taking too long for some important actors like Actor or Inventory, both of which replicate many variables. To alleviate the situation on servers, Epic moved the checks to native code. That's why you will see the 'nativereplication' on a few of the Engine classes. Any variables being evaluated in those classes will be covered by the nativereplication code, making any UnrealScript replication statements useless in that class, (except as a form of documentation for UnrealScript programmers.) Any variables that are not defined in a nativereplication class, (defined in a non-nativereplication subclass or superclass,) will be replicated via the unrealscript definitions. Despite the fact that the nativereplication checks are in C++, the UnrealScript checks that remain in those classes are still accurate, and a good guide to go by in determining problems with replication, and ways around it.
Special Replication Variables
When writing replication conditionals, there are a few things you should be aware of. During replication, a few helpful variables are set for your use in evaluating whether it should be replicated. These variables are defined in actor, and are:
- bNetInitial
- true if this is the first time this actor is being replicated across the network. Useful for variables that differ from the defaultproperties, yet will not change over the life of the actor.
- bNetOwner
- true if the player we are replicating to owns this actor directly.
- bSimulatedPawn
- true if the actor is a pawn (or a pawn subclass) with Role == ROLE_SimulatedProxy. In other words, true if it's a pawn that's not the client.
- bDemoRecording
- bClientDemoRecording
- bClientDemoNetFunc
- used for demo recording purposes.
- bNetRelevant
- bNetSee
- bNetHear
- bNetFeel
- Not used anymore, never even set.
NetUpdateFrequency
NetUpdateFrequency is another variable useful in replication. It effectively causes an otherwise relevant (not useful when the actor is not relevant) actor to be relevant NetUpdateFrequency times per second. This is useful when the data changes frequently but you only want periodic updates. For example, PlayerReplicationInfo contains a lot of data about the client. Stuff like the various player's pings may change quite frequently, and having the client keep track of such information would be quite a network hog. But since PlayerReplicationInfo has a NetUpdateFrequency of 2, it is only updated twice a second, which is much nicer for that player's bandwidth. The list of the current NetUpdateFrequency's for all UT classes are listed below:
Actor | 100 Hz |
ZoneInfo | 4 Hz |
GameReplicationInfo | 4 Hz |
PlayerReplicationInfo | 2 Hz |
Inventory | 8 Hz |
Reliable versus Unreliable
Another optimization used in networking is the difference between Reliable and Unreliable data. In the case of variables, all data is reliable. It is all guaranteed to reach the client, even if it arrives in the wrong order. (Pawns however, have code that prevent them from appearing jumpy, although the effects of this can still be seen at times when the player bounces back and forth in a somewhat laggy game.....location updates are being received out of order.) Functions however, exist in both reliable and unreliable forms. A reliable function, like reliable data, is guaranteed to reach the client. An unreliable function however, is sent to the client, but it is not guaranteed of reaching the client. Something that sends messages to the client, like BroadcastMessage or BroadcastLocalizedMessage, are reliable to ensure that the client gets the message. Something like a client's update to the server about its position need not be reliable, however. Since the client sends these update requests to the server every single tick (in ServerMove, if you're wondering), it does not matter if they all reach the server. And since each update is timestamped, the server knows which ones to regard because they are out of order or lagged. And so, the server can be assured of getting updates from the client, while not requiring that they ALL be sent. A reliable function is repeatedly sent over the network until the client acknowledges it. This can lead to a one second delay or more if it has to try to send the message once or twice. Since you don't want to bog down the network connection with reliable updates about where the client is looking or moving, unreliable is best in this situation since the server only needs to get "most" of them to work great.
Writing Replication Statements
There are a few useful things to know about when writing your own replication statements. First, you cannot override replication statements. If a replication statement exists in a parent in regards to Location, Velocity, some function, or another variable you want to change the conditions for...you can't. Sorry. Instead, you have to mold your actor into fitting the requirements so that it's property or function will be replicated. Usually you can accomplish this by setting a variable or two (you can't set the "const" ones which are set natively of course), or by fiddling with the RemoteRole until it matches up. However, don't go around doing that blindly, or you're sure never to succeed. Instead, keep reading, and you will discover (hopefully, if I do it right :), exactly what each of the Roles are for, and what side effects they cause. Next, keep them simple. We already discussed the importance of not doing anything overly complicated, and that rule should be adhered to. Second, use the bNet* variables if possible. These variables, like bNetInitial, bNetOwner, bSimulatedPawn are quite useful, and are gratis. There is no additional CPU requirement to use these variables, as they are set for you automatically. Another very important thing to hinge your variables on is the Role and RemoteRole. "if (Role == ROLE_Authority)" would cause that variable/function to be replicated from the server (where the Role equals ROLE_Authority) to the client. Something like: "if (Role < ROLE_Authority)" would cause the variable to be replicated from the client (where the Role is less powerful than ROLE_Authority) to the server. Checks can also be performed to do various things depending upon if the Role/RemoteRole is a SimulatedProxy or a DumbProxy. The best place to find examples of these more intricate checks is in the Engine classes, particularly Actor, Pawn, and PlayerPawn. It should also go without saying that you should not change anything in a replication statement. Doing i++ in a replication statement causes i to change unpredictably as it is being replicated, which is not something you really want to do. And finally, as the last word advice, the server checks all incoming data to be sure it is legitimate. If the client replicates something (a variable or a function) to the server, the server will exchange the Role and RemoteRole variables temporarily, and evaluate the replication condition to see if the other end had a legitimate reason for sending that data. If the check fails, then the data (variable or function call) is discarded. That means that when you are writing replication statements, make sure that the server also has all the data needed to perform the replication check. Otherwise, it will never be processed or accepted by the server.
Variable Replication
Variables are one type of data that can be sent through replication statements. The other is function calls (RPC, if that helps). Here, we'll be discussing how data is replicated from the server to the client, and vice versa. There's a lot of little things that server to complicate it more than the replication described above. As I already stated above, all variables are considered reliable, so the unreliable versus reliable discussion is irrelevant here.
Newly Relevant Actors (Spawning)
When an actor becomes relevant to a client, it is spawned on the client, with all the defaultproperties of the actor. If the server set variables immediately after the spawning of the actor, (before the relevancy checks), then those variables will be sent to the client as an 'addendum' of sorts, assuming those variables meet the replication statements. The server will send any variables that differ from the defaultproperties. Things can get hairy though, when the server has a different idea of an actor's defaultproperties than the client does. Initially, the defaultproperties on the server should match up with what the client has. If a defaultproperty is changed on the server, that change is not replicated to the client. You must change it in a simulated function in order for that to occur. However, what happens if the actor becomes irrelevant? In that case, bad things happen. When the actor becomes relevant again, the default spawned actor on the client will have different defaultproperties from the server. When the server then proceeds to perform the checks on what to replicate, it sees that the default var is the same as that var, and so that var is not replicated. The client then never gets data, and so has the REAL default variable, of a fresh actor. That variable can be incorrect, and cause strange behavior to occur.
As an example, for a particular class-based mod, a programmer (not me, this time ;-) had set both the Mesh and Default.Mesh to the playerclass that the player had chosen. Players however, go in and out of scope, to save on network bandwidth. Normally they are only relevant when you can see them, and for about three seconds once them disappear from view. Let's say the player changed their playerclass at that point. Or say they changed their class at the start of the game while the round has not started and they are invisible, and thus not relevant. When they did become relevant to the client, because they turned a corner, or because the round just started and they are spawned and made visible, the Mesh == Default.Mesh, and so the Mesh is never replicated. The skin however, was still replicated normally. To clients in the game, they saw the player as the original mesh, with a inappropriate skin that was intended for another model. Like I said, strange behavior, that can be hard to trace down. The way to handle this, is to not set the Default variables unless you are really know what you are doing. Just setting the Default.Mesh because it feels right will only cause problems. In this case, the Default.Mesh was not needed for everything, and removing that assignment caused everything to work fine.
A Time for Replication
Replicated variables are only replicated at the end of each tick. That means that if you change the variable repeatedly during a loop, or some other block of code, only the final value will be replicated. If you change a variable many times during a tick, and change it back to its original value when the tick completes, then no variable change will have been seen, and no network bandwidth will have been spent.
Client-Server Replication
Another quirk to be aware of is that client-server replication exists ONLY for your own playerpawn. That means that if you want a variable replicated from the client to the server, it needs to be done in your current PlayerPawn (or one of its parent classes). (You can use function replication to get around this limitation however.) Other playerpawns in the level are not the one that you currently 'possess', so their variables will not be replicated to the server. Just be aware of that when trying to replicate your variables and they aren't working. :) Thinking about it, it makes sense. What right does the client have to send any information to the server about another playerpawn, or some inventory item? The client is only concerned with variable changes that the player himself (or herself) makes, and thus those are the only ones that are sent to the server.
Replication Only When Necessary
Further optimizations exist for variable replications as well. A variable will only be replicated if the server thinks the client has the incorrect version compared to the server's latest version. This usually happens when the server changes a variable, and it knows the clients do not know about such a change. (They would if the function that changed them was simulated and was called clientside, meaning that you technically would not need to replicate that variable. This is explained later.. :) This means your code can change stuff clientside, and it will not be written over by server-side data until the server's data changes. As an example, I used this technique to implement a simple little static LOD back in the Unreal days before Epic introduced their realtime LOD. The client determined how far away the player was, and switched in variously detailed models depending on the distance. This allowed more models onscreen at once. And in multiplayer games, the client could change the mesh to the various LOD meshes, and it would work fine. Since the server never saw a change in the server's mesh (assuming it was a dedicated server), the client could change the LOD mesh without any problems. When the server did change the mesh however, the client would get a new mesh variable sent to it, and so it would have to then change it again. As a bragging aside, it even modified the intensity at which is applied LOD depending upon the framerate. ;)
Replication Data Approximation
Another optimization is that of how Unreal sends vectors, rotators, and arrays across the internet. The rules for this can best be described from the Unreal Tech page's Networking document, and I've copied it here to keep it in context:
- Vectors and Rotators: To improve bandwidth efficiency, Unreal reduces the accuracy of vectors and rotators slightly, a process known as quantizing. The X, Y, Z components of vectors are converted to 16-bit signed integers before being sent, thus any fractional values or values out of the range -32768..32767 are lost. The Pitch, Yaw, Roll components of vectors are converted to bytes, of the form (Pitch8)&255. So, you need to be careful with vectors and rotators. If you absolutely must have full precision, then use int or float variables for the individual components; all other data types are sent with their complete precision.
- General structs are replicated by sending all of their components. A struct is sent as an "all or nothing" type of thing.
- Arrays of variables can be replicated, but only of the size of the array (in bytes) is less than 448 bytes.
- Arrays are replicated efficiently; if a single element of a large array changes, only that element is sent.
Variable Replication Examples
Now would probably be a good time to go through some important examples of replication and how it is used with variables, to give you a better idea of how things work, and to help give examples for your own adventures in coding.
Let's take a few examples, and describe exactly what they do. We'll start off with the easy ones, and gradually work our way up in complexity. The following examples are all taken from Actor, unless specified otherwise.
In PlayerPawn:
reliable if ( Role < ROLE_Authority ) Password, bReadyToPlay;
This here shows where your Password is sent to the server, when trying to join passworded servers, or to log in as an admin to that server. The server needs your password in order to validate it, and this code sends it to the server. Let's evaluate this check both on the server and on the client. Server: On the server, Role == ROLE_Authority. Looking at the conditional, it can easily be shown to be false, and so the Password is not replicated server-side. This means that the server does not send the Password to the other end of the connection. Client: Since on the client, the Role is not ROLE_Authority (remember it's only Authority on the server itself), this replication check evaluations to true on the client. And since variable replication only works in your current PlayerPawn, it will be sent to the server for yourself alone. (It makes sense not to send others' passwords, doesn't it? ;)
This also shows how bReadyToPlay, which is used in Tournament style games, where everyone has to click to start the game. Note that this is only sent for yourself, since the client can only replicate variables to the server if they are part of it's own playerpawn actor.
unreliable if( Role == ROLE_Authority ) Owner, Role, RemoteRole;
This ensures that the owner of an object is always replicated to the client. Remember that the owner's variables themselves are not replicated, (because there is no recursive replication,) but the actor itself is. This means that simple comparisons "if (PlayerPawn == Owner)" will work fine because the reference to the Owner is replicated to the client. Remember that the unreliable versus reliable makes no difference in UT, as they are both treated equivilently. They may have had an effect at sone point during the Unreal 1 days, but they no longer play a factor in development. Don't let that confuse you.
Replicating Role and RemoteRole may seem strange at first, since they should be reversed on the client. However, there's native code that causes that switch to be made. On the client, it will ensure that those two variables are reversed, so that RemoteRole is ROLE_Authority. You may wonder why they are even replicated in the first place. While the client never has any direct use for these variables in the code, there is one easy-to-overlook place where they are vitally important: Replication Statements. When the client is evaluating whether it needs to replicate a certain function call to the server, or a playerpawn's variable to the server, the client needs accurate copies of what Role and RemoteRole are. Without those, it would be unable to make the necessary decisions on what to send to the server. And since the server checks the validity of the replication data sent to the server with Role/RemoteRole, the client needs the latest copy in order to successfully replicate that data.
unreliable if( bNetOwner && Role == ROLE_Authority ) bNetOwner, Inventory;
Inventory is the head of the linked list that lists the entire Inventory for the Actor (usually only utilized with Pawn and PlayerPawn). We want the inventory to be replicated to the client, so that he knows what Inventory he has, to display on his HUD, and stuff (makes sense, right?). Simply replicating the head of a linked list (where each object points to the next in the list) is not enough however. Each actor must itself be relevant (satisfied by the criterion that it must be owned by the current playerpawn), and each link itself must be replicated. And since Inventory subclass Actor somewhere in the chain, Inventory also has a replicated Inventory variable. And that's how you are able to see your Inventory client-side. And since I don't need to know the full Inventory list of what every other player in the game has (the current weapon and the shieldbelt effect are transferred via other means), the Inventory will only be replicated if the client is the owner.
The other variable in the above replication statement is bNetOwner. Since this is set natively on both the client and server, this variable does not need to be replicated. It could be argued that it adds a tiny amount to bandwidth, but its effect is probably negligible. Perhaps it will be removed in a future patch.
unreliable if( DrawType == DT_Mesh && Role == ROLE_Authority ) Mesh, PrePivot, bMeshEnviroMap, Skin, MultiSkins, Fatness, AmbientGlow, ScaleGlow, bUnlit;
Here we see that all these mesh-specific variables are ONLY replicated if this actor is currently being displayed as a mesh. If it's a sprite, or a brush, or even no drawtype at all, there is no reason to send these variables.
Now let's get into some of the more complex variable replication statements...
unreliable if( RemoteRole == ROLE_SimulatedProxy ) Base;
Here we see a slightly more complex replication statement, that does not involve Role == ROLE_Authority. Here we see that the current Base (set via SetBase) is only replicated if the actor is set to be a simulated proxy. If you set the actor to a DumbProxy, you'll see no Base change clientside, and instead will get the jerky Location updates that the server sends to you, (more on this later). So if you are using SetBase, make sure it's a SimulatedProxy, or if it's not a SimulatedProxy, use something other than SetBase. :) Or if you need both SetBase and a SimulatedProxy, the Base will not be replicated for you. You'll have to work out some other mechanism to do that, probably a simulated function that sets the base, so it is run on the client, (more on this later, too.) You might still have troubles when the actor gets its simulated function called while the actor was not relevant. This results in the simulated function never being called clientside, and then when the actor does finally become relevant, it's as if that function was never called (and the base isn't set), which will result in those things affectionately referred to as 'bugs.' You can make the actor being attached (and what it's being attached to) bAlwaysRelevant so that the simulated functions will always be called, but that might be a bit too harsh on network bandwidth. :)
unreliable if( RemoteRole == ROLE_SimulatedProxy && Physics == PHYS_Rotating && bNetInitial ) bFixedRotationDir, bRotateToDesired, RotationRate, DesiredRotation;
This one is still relatively easy, but it's getting more complex. Here we have similar logic to what was seen above, with the server only sending these variables to the client if the client is a SimulatedProxy. However, since all these variables apply ONLY to PHYS_Rotating, there is no need to replicate these variables to the client if the client is not using PHYS_Rotating. And finally, a very important clause at the end is the bNetInitial one. This states that these variables will only be replicated to the client when it is being replicated for the first time.
unreliable if( bSimFall || (RemoteRole == ROLE_SimulatedProxy && bNetInitial && !bSimulatedPawn) ) Physics, Acceleration, bBounce;
Here we have some of the interesting variables replicated. We see that if the bSimFall is set to true, it will replicate the Physics to the client. This is used when tossing weapons from your inventory. When they are tossed, they require a Physics change from PHYS_None while sitting in your Inventory to PHYS_Falling as they fly through the air. The then get set back to PHYS_None when they land on the ground. All these Physics changes are accomplished by setting bSimFall at the key points during the TournamentWeapon's life. It is set to true when it is thrown from the inventory, to capture the change, and left on until it hits the ground, where it changes Physics again. After it finally hits and has its physics reset, bSimFall is set to false so no further Physics changes occur. Looking at the second half of the statement, we see that the Physics is replicated ONLY if it's a SimulatedProxy, it's the first time this actor is being replicated over the internet, AND it's not a simulated pawn. The bSimulatedPawn variable is set to true if it's a Pawn with a RemoteRole of ROLE_SimulatedProxy (who would have thought? :) This means any changes to the Physics of a SimulatedProxy non-pawn actor before it is replicated will be replicated to the client. The Physics are never replicated for pawns. Rather, the code for their physics is hard-wired into the code to cause them to be pushed to the ground. Basically, when a pawn jumps, the server sets his vertical velocity so that he travels upwards. That Velocity is then replicated to the client (described below). The client then checks if the pawn is a player (eg: a bot or a playerpawn), that they are currently unable to fly (set via the Pawn's bCanFly variable), and that they are not in a water zone (since that involves different gravities and physics). So in fact, the Physics for a pawn is never replicated to the client, it only appears to do so. If you are attempting to create alternate physics with the playerpawn, you will need to ensure that you set bCanFly to true so that the native code does not enforce the falling Physics upon the player when he's on the wall or in the air. You must then implement the alternate means of transportation yourself.
unreliable if( !bCarriedItem && (bNetInitial || bSimulatedPawn || RemoteRole < ROLE_SimulatedProxy) && Role == ROLE_Authority ) Location;
Here we see another important variable, Location, and it's replication. We see that it is not replicated for carried items. This is useful in the case of Inventory. When a player is carrying it, there's no reason its location should be replicated, since it isn't used for anything. replicating a player's entire inventory would be quite the strain on network bandwidth. Let's look at the next section of the statement. The location is replicated if this is the first time the actor is being replicated (assuming the other parts of the conditional are true). This is very useful, since all pawns will be spawned in different locations when you enter the game, and rockets will need their start locations set when they spawn out of a rocket launcher, etc. The location is also sent if this is a bSimulatedPawn. This helps correct any errors that may occur in replicating pawns. Since a pawn can change direction, and a client may not always know about it (due to lag, etc), this location resetting is the only way in which the locational updates can be 'corrected'. Note that this is not used to correct your own location when you experience lag. When that happens, you are an AutonomousProxy, and functions specific to PlayerPawn handle that (namely ClientAdjustPosition). For example, PHYS_Walking is not like PHYS_Projectile, where the position can be predicted with great accuracy. PHYS_Walking just means "keep them attached to the ground", basically, The client's only updates (without location) would have been their velocity, which can easily lead to errors in the player's movements over any significant amount of time. This location is used as a correcting factor, making sure the client's view does not straw too far from where he expects the player to be, while at the same time keeping the velocity's replication so he can be predicted inbetween server updates. The other option for allowing the Location to replicate is that of where the RemoteRole is less than a ROLE_SimulatedProxy, namely, a ROLE_DumbProxy, since the ROLE_None prevents relevancy in the first place. DumbProxies get periodic updates from the server every few ticks, and would create a jumpy effect in netplay. It was the cause of a jerky puck in the hockey mod I attempted awhile ago, although I knew nowhere near enough to fix it at the time. :) A SimulatedProxy would not get sent Location updates, since it would be predicted through the use of the Velocity and the current Physics. Since DumbProxies aren't simulated client-side to achieve smoothness, they only get the Location updates. I'm going to clear this all up after the examples of the various replicated variables.
unreliable if( !bCarriedItem && (DrawType == DT_Mesh || DrawType == DT_Brush) && (bNetInitial || bSimulatedPawn || RemoteRole < ROLE_SimulatedProxy) && Role == ROLE_Authority ) Rotation;
Here we see another important variable, Rotation. This also has the same !bCarriedItem clause, for the same reasons as described above. The rotation is also only replicated if this actor is a mesh or a brush. Since sprites always face the player, replicating their rotation is useless. The bNetInitial also causes the rotation to be replicated the first time through, (again, assuming if the other conditions are true,) since rockets need to be facing in the right direction when it has them flying through the air. Their velocity determines their direction of movement, but the direction they are facing which is equally important, is determined by their rotation. The rotation is replicated if they are a simulated pawn (every playerpawn and bot in the level except your own self). This is so you can see what direction the other people are facing. The ViewRotation (which determines where the client is looking), is sent to the server via ServerMove, where it is translated into a rotation. it is this rotation that is then replicated to the clients. And finally, if it is a DumbProxy (the only valid one less than SimulatedProxy), it also gets the rotation updates. Rotation updates do not need any interpolating between updates like location does. Location lag is much more noticeable when the player is moving quickly, but you never really notice Rotation lag. Besides, there is no real way to predict or forecast rotation. It depends entirely upon the other user's mouse.
unreliable if( bSimFall || ((RemoteRole == ROLE_SimulatedProxy && (bNetInitial || bSimulatedPawn)) || bIsMover) ) Velocity;
Here's one of the last important variables with a complex replication statement. The velocity is replicated if it has bSimFall set, (used when throwing weapons from your inventory). The PHYS_Falling alone isn't enough. It needs to know exactly what it's initial velocity is when it is launched from the pawn. And no, bNetInitial will not work here, since it's not the first time it's being replicated. It's existed as a weapon for quite some time. It's just for the initial velocity of when it's being thrown from the player that we want updates. Moving along, we see that SimulatedProxies get their Velocities replicated if it the first time on the replication channel, for newly spawned rockets, or for grenades, or shock projectiles, etc. SimulatedProxies also get their velocities replicated if they are a simulated pawn, as all the various pawns need their velocities replicated so that they can be predicated locally. And finally, movers get their velocities replicated, so that the client can accurately predict the mover's movement locally. In the early Unreal 1 days, mover's velocity was not replicated, and so the client got periodic update locations because it was a DumbProxy. This resulted in very jumpy movers in netplay, something that was fixed for Unreal 224+.
unreliable if( DrawType == DT_Mesh && ((RemoteRole <= ROLE_SimulatedProxy && (!bNetOwner || !bClientAnim)) || bDemoRecording) ) AnimSequence, SimAnim, AnimMinRate, bAnimNotify;
There are a few more variables here, all of which relate to animation. These variables are only replicated if it is currently being drawn as a Mesh. If we are recording a demo, then the animations are always sent. Otherwise, it checks the somewhat confusing conditional. If the actor is a DumbProxy, and the player is not the owner of this object, and bClientAnim is not set, then it replicates the animation variables. In the case of weapons, you see those animations clientside, and so the animation variables do not need to be replicated from the server. That is because all TournamentWeapons have the bClientAnim set to true, indicating that their animations are handled clientside. If the animations are not handled clientside, and the actor is a dumb proxy, then it will send them from the server. SimulatedProxies, which include pawns and rockets and other projectiles, are all simulated proxies, and so they do not receive animations from the server. Instead, the client animates them with its client-side prediction. In the case of pawns, this is handled internally in the engine (for another tutorial ;), and you need not worry about it.
Role and RemoteRole Summary
Hopefully this intricate look into some of the more complex and confusing variables has helped you to grasp the concepts of replication some more, and realize many of the differences between the various roles. However, with the above knowledge in mind, I'd like to do a little re-cap, emphasizing the use of dumb, simulated, and autonomous proxies, and help give you a clearer idea of when each should be used.
- ROLE_Authority
- Again, this is the Role for all actors on the server, and is the RemoteRole for all actors on the client. It basically means that I'm the authority on this actor. This is the only one variable that Role is set to, server-side. The remaining ones are all used for the RemoteRole when on the server.
- ROLE_None
- This basically says: Never make this actor relevant. This prevents replication conditions from even being run, and so does not factor into any of the replication statements.
- ROLE_DumbProxy
- This role is used for simple actors that require no real client-side prediction. They don't need any Physics prediction, but they only need to be relevant to the client to get their variables replicated. This is used for all Inventory actors when they reside on the ground, which prevents any higher-bandwidth SimulatedProxy stuff, but yet allows you to see all the stuff you need for the inventory actor. If you do try to move this actor, the client will get periodic updates, usually around every half-second or so on a regular connection. This creates quite a jumpy effect if you are using DumbProxy with a particular Physics type, since it really isn't intended for that. Rather, the Location updates are useful for when moving the actor across the level with a flag respawn, etc. The same is done with rotation updates, with periodic changes coming in to the client every so often. This role also causes animation updates to be sent from the server to the client, unless the client specifically tells it not to. In summary, an actor with this role is force-fed data from the server. It's intended for actor's that don't move (except in jumps like transportation, respawn, etc), yet are still relevant.
- Other quirks: There's a few quirks that can have a significant effect on your netplay development. DumbProxy actors do not have a variety of events run clientside. Simulated Tick(), Timer(), and state code, along with the physics calculations, are not performed on the client of a DumbProxy actor. They are supposed to be a dumb proxy actor that gets data sent over the net, and so these events are not called. One exception to the above rule is that physics are performed clientside for DumbProxy actors if the actor has a falling physics, which allows tossed weapons to fall correctly in netplay. In an early attempt at the weapon in the tutorial at the bottom of this document, I attempted to create a client-side physics that would combine with the location and rotation updates that were inherently sent via the DumbProxy role, but the client would not run my Tick()-based physics calculations, and so that idea had to be thrown out the window. :)
- ROLE_AutonomousProxy
- This is used for things that the client directly controls. In most cases, this will only be used by the playerpawn. This RemoteRole value should only be set for the one particular player, and it should be a SimulatedProxy for every other client in the game. The player for which it is AutonomousProxy is the player possessing that particular PlayerPawn. When it is set to AutonomousProxy, it appears to be a SimulatedProxy for everyone BUT the owner of the proxy or the instigator. This ensures that your PlayerPawn looks like a SimulatedProxy to everyone but yourself. Note that this technique is also used with the guided redeemer, to ensure that it works the same way. Having the server tell you where your guided redeemer is flying would not be appropriate. Rather, you want to tell the server where your guided redeemer is. While AutonomousProxy doesn't make this just 'work' for you, it does differentiate you from the other people viewing your redeemer, which is what is needed to have it act differently for you, compared to everyone else.
- ROLE_SimulatedProxy
- There are two different things that can happen with this particular type. It depends on whether the actor with this role is a pawn or not a pawn. I'll discuss the latter first.
- Non-Pawn ROLE_SimulatedProxy
- This one is basically the opposite of DumbProxy. It expects full client-side prediction of the actor, given initial values. It's like extrapolating out a curve given some data, where the current Physics is the curve fit, and you have initial data available to extrapolate from. ROLE_SimulatedProxy gives you an initial Location, Rotation, and Physics. It then keeps you updated with the Velocities, should they change. These variables should enable the client to perform full client-side prediction on this object, assuming it continues along the physics prediction. In the case of PHYS_Falling and PHYS_Projectile, this is the case. There can be no aberration form these behaviors, (unless you play around with the Physics variables bBounce and such, which are very useful in prediction.) Perhaps I'll explain Physics in more detail in another tutorial. Perhaps. ;) Finally, for this role, you also get animation updates, assuming the client is not the owner and it's not set for client-side animation. In summary, this Role is for actors that smoothly predict their data on the client.
- Pawn ROLE_SimulatedProxy
- When ROLE_SimulatedProxy is combined with Pawns, a different sort of simulated proxy is created, that act differently in netplay. Pawns are one of the most complex actors in terms of replication, because of their need for clientside prediction, yet inherent unpredictability in their movement. There is no way to duplicate this behavior except by subclassing pawn, so don't expect to achieve it in your projectile subclass. A SimulatedPawn gets the best of dumbproxy and simulatedproxy. It gets velocity updates for the clientside prediction, while at the same time getting location updates from the server to 'readjust' the player's locations. You might then wonder why the pawn does not jump when a new location update comes in. Again, there is native code that causes the actor to smoothly head towards the location given by the server if the location position is too far off from the player's real position. This again ensures seamless movement of players while keeping them accurate to the server's version. All non-players will get by with velocity-only client-side prediction. No physics will be taken into account for these pawns. This is because some non-players can be PHYS_spider, or a variety of other physics that don't work with the following always-walking/falling prediction. Only players (bots and playerpawns) get the special client-side prediction to have falling/walking physics. This physics is an assumed physics, which is accomplished by applying the zone's gravity if the player is not on the ground. This logic will be bypassed if the pawn's bCanFly variable is set, which frees it of the clientside physics, but must be accounted for in your code, since there is no physics replication in pawns. This logic is also bypassed if the pawn is in a water zone, which also differs from the default walking/falling physics. In water zones, actors are predicted using velocity alone, which is sufficient with the low amount of gravity that exists in those zones. The actor's get their locations 'adjusted' anyway, so it doesn't make any real difference in the long run. Finally, pawns get rotation updates. Since rotation (which is directly based off the ViewRotation, or the direction the player is looking) can change quite radically, no prediction is done with it. Instead, the player's rotation is adjusted as new player rotation information comes in from the client. Animation is handled the same as any other non-pawn simulated proxy.
Function Replication, and Simulation
In a network game, functions can be called on the server, on the client, or on both. Sometimes you will want one situation, and sometimes you will want another. Function replication and simulation both achieve different pieces of these goals. Despite what may at first seem like two different topics of discussion altogether, they are actually quite related at their root. I will discuss them together, and then finish with a clarification of their differences so that each can be understood clearly.
Internally, the Unreal Engine uses a very simple system for its function stuff. It calls a function to determine if it can call an UnrealScript function on that machine. if it can, it does, if it cannot, then it does not. That's all that simulation and replication entails, and the only additions to this system are the origin of UnrealScript simulation, events, (which will be discussed later), and the function replication itself (much of which you know already.)
Rep/Sim Function Rules
I will first present the basic simple rules to determine if a client or server is able to execute the function locally. The following conditions will determine whether that happens. Again, if a condition matches, then the checking stops there, and it executes locally depending upon the resulting clause of that condition.
- If it is a static function, it can be called on that machine.
- If we're on a standalone server, it can be called on that machine.
- If it's not a replicated function, then go to step 10.
(The rest apply ONLY to replicated functions, per condition three.)
- If we are the server, and there is no parent PlayerPawn to this actor, (at any point in the parent hierarchy,) or if the player does not have a connection (listen servers for the player on the server,) then it is executed locally.
- If the replication condition is not met for the current machine, (meaning that it was replicated to us, or that it is not a valid replication statement), then go to step 10.
(The rest apply ONLY to replicated functions where the replication condition is met, and it is a valid function to replicate.)
- If it is an unreliable function, and the connection (server-client or client-server) is saturated, then 'pretend' it was executed remotely, (eg: do not execute it locally.)
- Replicate the function, and then do not execute it locally.
This last step is only used if it is called upon by the above steps.
- If we are on the server, then it can be called.
- If we are on the client as an AutonomousProxy, it can be called locally.
- If we are on the client as a DumbProxy or SimulatedProxy (NOT an AutonomousProxy,) and we are a simulated function, then it can be called locally.
While this may seem complex, it accurately describes in every detail how Unreal does it's function handling, and RPC (remote procedure call) nature. Hopefully, in the next paragraph or two, I'll be able to simplify it into a few simple rules.
Rep/Sim Function Rules Restatement
In order for a function to be replicated to the other end of the connection, it must be all of the following:
- Owned by a network PlayerPawn at some point it it's parent hierarchy.
- It must be a reliable function, or an unreliable function with available bandwidth.
In order for that replicated function to actually be executed on the other end of the machine, it must pass rule number 10. Additionally, if we are on the client, it must meet any of the following rules.
- A static function.
- A simulated function.
- In an actor with an AutonomousProxy Role (a local PlayerPawn or local GuidedWarhead on the client.)
The above rules need to be passed for ANY function that is executed on the client, whether it is through simulation or replication.
Rep/Sim Function Summary
With those rules in mind, I'll try to summarize the whole situation below.
On a server, every function is executed, unless it is a (client-server) replicated function and meets the checks above. So you can be assured of all your functions running on the server, unless you specifically tell them not to.
On the client, every function has to originate from a 'source.' A source can be a server-client replicated function, an exec function, or an event. Events, while not covered previously, are quite simple. An event is any function that is called from native code. Events happen both on the server and on the client. The server does not directly tell the client to execute the event, but the client knows to execute the event based upon the game state (colliding actors, hitting a wall, a timer event, ticking of the game state, etc). If that event is simulated, then it can also serve as a 'source' of clientside function calls. A source can call any function it wishes, assuming it meets the client-side execution conditions, which can then call any...and so on.
What happens if a function is called, but doesn't meet the conditions for execution, either because it was replicated to the other end, or because it does not meet the conditions for running client-side. In that case, it is as if the function never ran in the first place. The return value of such a function is 0, "", or None, depending upon the context. Any out parameters defined by the function are passed back exactly as they were entered. Relying on the result of a function that is never executed is the source of many accessed nones that appear only on the client. Exec functions (functions that can be bound to keystrokes) naturally evaluate on the client. Again, remember to make it simulated, (unless you are in an AutonomousProxy, like a PlayerPawn subclass.) In some cases, you may want to have it replicated to the server, as in the case of a Server-Administrator command (there are plenty of these defined in PlayerPawn.) Exec functions can be replicated to the server, the same as any other function can.
Spawning Within Simulated Functions
There are other interesting hidden behaviors that may not be immediately apparent. For example, because of the requirements that an actor be owned by a playerpawn at some point in its parent hierarchy, a function can only be replicated to a single player in the game. (No, making a player owned by another player will not work. ;) Multicast function replication does not exist. You can try to get this behavior using simulated functions, but no data can be passed between the server and the client, (since they both originate on their local machine, and exist only on that local machine.) Or you can use replicated variables, which can and are replicated to all players in the game, assuming they are relevant to them.
If a spawn is performed in a simulated function (simulated functions are explained later), then multiple copies of the actor are spawned: the server, and each of the clients that the simulated function was run on. The server's version is normally then replicated to the client, so the client would see two versions. This would create strange behavior as the two became out of sync, and the client-spawned one could easily be mistaken for the 'official one' that for some reason, isn't having it's variables replicated.
If you need to do a spawn in simulated code (there are many valid reasons, like spawning effects clientside, etc), then you must account for this. If you are attempting to make clientside effects, then make sure you set the RemoteRole to ROLE_None immediately afterwards. Since relevancy is only checked between ticks, this will ensure that when that check does come around, it will not be relevant, and thus not replicated. That way, the server and the client will each have their local copy of the effects that run their course and then destroy themself. This is not useful for permanent effects, since simulated functions are not run on ALL clients, only clients where the actor containing the simulated function is relevant. That means that if the simulated function didn't run (because it's actor was not relevant at the time), then the effect will not be visible when you come into view of that effect. That is why it should only be used for temporary effects that run their course and then destroy themselves.
If however, you are spawning something that is intended to be permanent, or needs data replicated from the server, (like a crosshair dot,) then you SHOULD NOT spawn it on the client. Usually, you can accomplish this with a 'If (Level.NetMode != NM_Client)' check before you perform the spawn. This way, the spawned actor will then become relevant (assuming the conditions match), and it's variables will be replicated to the clients. If a client-side actor had been spawned, then there would be two actors, one of which would just sit there without information from the server
Networked-Function Paramater Optimizations
And finally, when replicating functions, Unreal optimizes the parameters of a function call through a few techniques. When sending a vector, (which is composed of three floats,) the individual components are rounded to integers, and thus lose their fractional values, and any values outside of -32768 to 32767. A common technique is to multiply the values before they are sent to make them scaled integers, and then dividing them on the other end back into their appropriate values, so it is almost completely accurate. With rotators, the values are scaled between 0 and 255, where the 128+ range corresponds to the normally negative range of rotator component values. Then they are rounded to ints, and sent over the network. They are then fixed back up again to their original values (except they are left as positive values, since they are equivalent to their negative counterpart that existed on the other end of the connection. One approach that applies to both vectors and rotators is to send the X/Y/Z or Pitch/Yaw/Roll values of the struct as individual floats so that complete accuracy can be maintained. And finally, for completeness, planes (which are not used much at all,) replicate their components as rounded integers.
Simulated States
Another, many times overlooked feature is that of simulated states. You probably have already seen state code, code that exists outside of functions, but in states, usually prefixed with labels. This code, when in a regular state, is executed on the server alone. But by prefixing the state statename with simulated, you can make the state code itself simulated. So the revised state declaration would be simulated state statename. Just beware that state changes are NOT replicated or sent to the client by default. However, if a gotostate is executed in a function that is run on the client (either through replication or simulation), then the client will know of the new state. Playerpawn's inherent simulation allows all it's functions to be able to control the state, but other classes would need to have simulated functions that are executed from a simulated source and chain of function calls in order to know of the state change. The current UT code makes no use of simulated states, but I thought I would include this section for the sake of completeness, in case you ever needed it.
Level NetModes and their Implications
In the Level variable, which references a LevelInfo actor and is accessible from every object, is a NetMode variable. The NetMode variable is used to describe what role or position the current instance of UT plays in the overall scheme. The possible values, and their uses are:
- NM_Standalone
- This is for when the game is played alone, like botmatches, the single player tournament ladder, etc
- NM_DedicatedServer
- This is for when the server is running alone, with no one playing directly in that instance of UT. This is the netmode for the majority of servers that are online. It can be started with 'ucc server', or by clicking 'Dedicated' when you select 'Start New Multiplayer Game.'
- NM_ListenServer
- When you click 'Start' in the 'Start New Multiplayer Game' window, you are starting a listen server, which is when you are playing and hosting at the same time. This causes CPU to be spent on rendering the world for you, as well as hosting the game for any connecting players. Thus, dedicated servers are preferred to listen servers.
- NM_Client
- When you are connected to a running UT server, no matter what type of server it is, you are this netmode.
These netmodes are usually not important as Roles and RemoteRoles can handle everything. However, when you want to execute a block of code on the client, but don't want to replicate it, these netmodes can help. For example, code is run on the client through a chain of simulation. If the chain of simulation is upheld, then you can do a 'if (Level.Netmode == NM_Client) {', then that code will be executed on the client only, or on many clients, if that event exists in many places. By combining that with a check on Owner, it is possible to ensure that it is only run on one single client. One thing that makes the whole deal more complex is that you want to ensure your code will run in standalone games, and for the guy running and playing in listen servers. For this reason, you will often see code that does 'if (Level.NetMode != NM_DedicatedServer) {', which probably does exactly what you want. Other examples include only spawning non-relevant gibs if you aren't on a dedicated server (because of the increase in CPU time). Another example is the use of spawning a gibbed head. The gibbed head needs to travel in the same, but random, direction as the server. Giving it a random velocity on the client would cause it to readjust midflight when the network-replicated data came through the pipe. Instead, the carcass head is not spawned clientside, but rather it lets the network data create the head, and give it the server's random velocity, so that it will be correctly in sync. Further uses are left to your imagination. :)
Special Properties about Special Stuff
bNetOptional Usage
You've seen bNetOptional talked about a few times. Now's a good a time as any disucss what it was intended for, and when you should use it. As was discussed earlier, any actors that have bNetOptional set get put at the bottom of the list of actors. The effect of this is that they get starved of bandwidth when the connection becomes full. If there's bandwidth available however, then they'll be replicated. They have 150 ms to be replicated, before they are forgotten about completely, and the client never sees them. When should you use these? In most cases, you'll want to use them for temporary effects that can be replicated to the client, and then allowed to complete on their own. For example, a little puff of smoke coming from a wall after you shoot it would be a good example. If your bandwidth is overloaded, either from a big firefight, or from excessive lag, then the little puff of smoke will not be seen. You probably won't notice it in either case. bNetOptional actors will not be replicated after their initial replication, either. They're conditions are evaluated only once, the first time they are executed. They are expected to complete their actions without any interaction of the server. This also saves a lot on bandwith. It wouldn't make sense to have the server continually evaluate replication checks for these actors, since that would only waste bandwidth. bNetOptional is not for everything though. An explosion sprite, like a rocket's animated explosion, is necessary for everyone in view. It's easy to miss a sprite puff of smoke in the heat of battle, but a large explosion is not something easily missed. You'll want to use the bNetOptional only for small, non-vital actors that aren't necessary. In Unreal Tournament, they are used for decals, fragments (those little chunks of 'wall' that fly off), shell casings, smoke puffs, the wall hits (the little yellow 'spark' when you hit the wall), and the respawn effects. Note that if the default lifespan is set to 0, then a bNetOptional actor will be treated just like any other actor. If the bNetOptional actor does have a default lifespan set, and it extends beyond the first 150 ms of it's lifetime, then it will never be replicated. Any request to destroy an actor will be replicated to the client regardless of at what point it exists in it's lifespan, as UT will always make sure the client destroys what the server destroys.
bNetTemporary Usage
While we're at it, we might as well cover bNetTemporary here. bNetTemporary actors are for 'tear-off replication', as someone once put it. They are replicated when they are first spawned, and that is it. There is never any further connection between the server and client in regards to that bNetTemporary actor. A bNetTemporary actor is not necessarily temporary, just that the replication of it's variables is temporary, (only once.) This is used in many places. In UT, all decals and projectiles are bNetTemporary by default. A decal is replicated to every client that it is relevant to, and then the connection is broken. this saves significantly on bandwidth. This means that if you walk into view of a scene where there was a battle that you did not witness, then you will not see any of the decals in that room. They were temporarily replicated, and so you will never see them if you weren't present for it's one-time replication. In Half-Life, decals were not 'bNetTemporary', and so were sent to every client when they came into view. This contributed to part of the lag that many experienced because of the many decals and spraypaint decals that were spawned in a game. bNetTemporary actors need to be destroyed in clientside simulated code, since a server destruction of the actor will not replicate to the client, because of it's bNetTemporary status. This is done when decals grow stale and need to delete themselves based upon client information, since the server cannot tell the client about that. Most projectiles utilize bNetTemporary. Take a razorblade, for example. It gives the client the initial velocity, and the client can work everything out from there. Since the razorblade is entirely predictable and deterministic, it can be predicted on the client with 100% accuracy. The wall it bounces off on the server.....it also bounces off the equivalent wall on the client. Because it is entirely predictable, the server only needs to send it once, and the client can figure it out. For that reason, projectiles are bNetTemporary. Problems can happen when you enter a big room that has razorblades flying around. Because the razorblades are bNetTemporary, you will not know about them. So you can walk into what seems like a perfectly empty room, and then get your head chopped off with a razorblade. However, you'll since most people do not use 360 FOV, you'll probably attribute it to just not seeing it coming. So no one ever notices that. Other projectiles like the the guided redeemer missles, or even regular redeemer missles (which can be shot down midflight) cannot be bNetTemporary because the server needs to send status updates to the client. The green globs from the goop gun need to be visible on the floor when you run into a room, (because they are sorta-long-lasting land mines,) and so they too cannot be bNetTemporary. There are other examples of where projectiles are not bNetTemporary, but I'm sure you get the idea.
PlaySound's and PlayOwnedSound's Special Behaviors
This one issue was brought to my attention when someone was trying to figure out how come a sound wasn't playing correctly in his mod, even though it worked fine in UT. After looking into it, I couldn't see how the sound was working in UT. The replication and simulation behavior was not set up for it. Turns out that it had hidden behavior, depending upon where and how it was called. In theory, PlaySound is used to have an actor play a sound. The game will determine how loud to play it, depending upon occlusion and distance, etc. UT tries to determine the 'right' and intelligent way to propogate the sound, depending upon how it was called. First, we'll cover PlaySound. It can be called in two contexts:
From a simulated function, or from a replicated-to-client function
- In this scenario, the sound is being played in many places. The simulated nature means that any player that can see the actor, is getting the simulation events. So each client will play the sound on it's own, due to the simulation. In the case of a replicated function, (where it's being run on the client,) UT assumes you know what you're doing, and that you want the sound to be played on that client only. You may wonder why there is a check for a replicated-to-client function when there's already a simulated function check, (since all replicated-to-client functions need to be simulated,) but if you remember, a PlayerPawn that is operating under ROLE_AutonomousProxy can execute replicated functions that do not have the simulated prefix. This case covers those. In general, this play-locally behavior is what you want, when you call PlaySound from within a simulated function.
From a non-simulated, non-replicated-to-client function
- When you call PlaySound in this context, UT does more work for you. In this scenario, the PlaySound is being called server-side. That does absolutely no good in a real game, since that sound would then not be heard by anyone. You might think it only has to be replicated, but if you'll remember, function replication is not multicast, so the sound would only be heard by the owner of the actor PlaySound is being called upon. Simulation doesn't help, since that would only happen if the sound was being called on the client. Simulation does not actually send anything over the network. So, UT performs some special magic instead. In Pawn, there is a ClientHearSound, whose definition is:
native simulated event ClientHearSound ( actor Actor, int Id, sound S, vector SoundLocation, vector Parameters );
This ClientHearSound is replicated from the server to the client, per the replication definition in Pawn. Remembering that it only goes to it's owner, this works fine, since calling Pawn.ClientHearSound(someactor...) on the server will cause that one pawn to hear the sound on the client, as if the sound came from someactor's direction. The special magic performed, is that UT effectively turns the call to PlaySound into a multicast function replication on ClientHearSound. When PlaySound is called, it iterates through all nearby players that should hear the sound, and replicates a call to ClientHearSound to each player. In this fashion, each player that should hear it, does hear it. And the original identity of the actor playing the sound is maintained through the first parameter to ClientHearSound.
This is actually quite the nifty behavior for PlaySound, as it causes the function to work the way it 'should,' in most cases. In the cases where it doesn't, you'll need to figure in it's dual nature, and figure out how to call it, (or call some other home-grown sound replication feature,) to make it work.
The other interesting behavior to talk about is that of PlayOwnedSound. The purpose of this function is to play the sound for everyone but the owner. This may seem strange at first, but there are a few uses for it. In UT, it used in most of the weapons. When a client fires a gun, they should get immediate feedback about the shot. That is why you see the weapon animations plya clientside, and hear the sounds clientside. However, the client cannot force sounds to be played elsewhere. What is done, is that the weapon plays a sound clientside immediately when the shot is fired. The client then tells the server it fired the gun, which then calls PlayOwnedSound with the sound, so that the sound can be heard by all the other clients. You wouldn't want the sound to be played on the player firing the gun, since they already played the sound. Hopefully that example is clear, but you can check the examples provided in UT's weapons if it does not satisfy you. One final note: when UT determines who is the owner, it only checks the actor itself (to see if it is the pawn or the playerpawn), and the immediate owner of the actor. It does not follow the hierarchy up like some other functions do.
ClientPlaySound and ClientReliablePlaySound
Here's a simple example, but still noteworthy nonetheless. ClientPlaySound is an unreliable function that plays a sound on the client. Calling PlayerPawn.ClientPlaySound(...) causes that player to play the sound locally on their machine. It is a location-less sound, and the sound is played as if it came from within their head, (as opposed to next to them, or in front of them, etc.) When called on the client, it is guaranteed to run, since no replication is involved, (it's a simulated function being called from a simulated function...) When it is called from the server, it is not guaranteed to reach the other end, however. What happens if you want to ensure that the sound is played? You call ClientReliablePlaySound, of course. This is a reliable function, which ensures that the sound reaches the client and is played. I'll just rip the whole ClientReliablePlaySound function, so you can see what is going on:
simulated function ClientReliablePlaySound(sound ASound, optional bool bInterrupt, optional bool bVolumeControl ) { ClientPlaySound(ASound, bInterrupt, bVolumeControl); }
You can see that the reliable definition which is farther above in PlayerPawn ensures that ClientReliablePlaySound is run. Once that happens, the body of ClientReliablePlaySound is run on the client. There, it calls ClientPlaySound which ensures that the sound is played.
When Things Go Awry
What should you do when things go awry? There's two main techniques that I use, when trying to figure out what's going on. Technically, everything can be figured out with the first technique alone, but the second helps a lot in figuring out the applying technique.
Figure Out What's Going On
You need to isolate your problem. Know exactly what's happening. What stuff is being replicated, and what isn't. Then realize that those are only your assumptions, and that no matter what, you cannot be positive about them. With all the native stuff happening with replication, it's so easy to blame your problems on what's going on under the hood. In fact, after readinng this guide, you should be able to solve your problems yourself. I think I've covered just about every native replication aspect there is in this document. It's why it's so big. When I was having replication troubles, I remember that I wished there was documentation. I'd (and I'm sure others) would rather have too much than too little. Anyways, now that I've had access to the source, and have been able to know exactly what's been going on, I've been able to figure out why problems are happening in netplay, even ones that aren't my own. I say this only to point out that just knowing and understanding what's going on under the hood can make any problem solvable. You don't need extensivle trial and error to figure out where things are screwing up. Just thinking the problem through, and checking the unrealscript code, can provide you with the answer.
First, figure out what you believe is not replicating. Is it a function? A variable? What's going on exactly? Sometimes, because of intricate relationships in your code, you'll be unable to tell exactly what is 'missing' on the client. In that case, you'll need to use logging to figure out what's there and what's not. Logging is the second technique I mentioned. I'll discuss more on how to use it properly in debugging networking problems in a bit.
Check the Replication Statements
Anyways, once you figure out what's not being replicated, check the replication statement. 98% of the time, you can figure out why something is not replicating, by evaluating the replication statement yourself. I went through a great many replication statements in actor up above in the variable replication section. You might want to check there for some of the more common ones. But thinking about each part of the statement in turn will help you figure it out. Many times, stuff has a bNetInitial clause, which only happens the first time the actor is sent over the network when becoming relevant. This causes problems when your data isn't getting replicated midstream, or is at odd points, like when it goes out of and then comes back into relevancy. You can follow the function replication and simulation guidelines listed above to see if the function meets the criteria for being sent accross the network, or evaluated client-side.
Another programmer I've talked to had problems with his velocity not replicating when the grenades were thrown, but it was because they were only replicated on bNetInitial, and so that's why he never saw his velocity updates. Fiddling with the variables, he found that a RemoteRole of ROLE_DumbProxy seemed to work, as the grenades did seem to be moving. It may be hard to tell that's what is going on when you're on a LAN or even connecting to your own server, because of the small ping, and high bandwidth. But you can be sure that you're players playing over their modems will notice it. :) Remember the various properties of ROLE_DumbProxy, and ROLE_SimulatedProxy, and what side effects each causes, in terms of what is replicated. I've seen a few cases where that is the reason for their strange behavior. The problem with his approach was that he was getting the spoon-fed location updates from the server, and so in a real environment online, that would never work, since players cannot get location updates over the net and expect it to work right. In that case, the grenades should never be thrown from the inventory. ROLE_SimulatedProxy was a perfect fit for what he was trying to do, except for the fact that when he threw it, it was not bNetInitial. Instead, he just had to spawn a new actor, and give it the 'thrown' properties. That satisfied the criteria needed for the variable replication for ROLE_SimulatedProxy, and allowed the grenades to be thrown correctly, be simulated correctly, and it to all just work.
Logging is Your Friend
If that doesn't help you, try sticking logging statements in there. When you have two copies of UT running, a server and a client, two different logs are generated. One is a server log, and one is a client log. (doh :) If you aren't sure if a function is executing clientside, place a log in it. Then, check to see if that log appears in the clientside log. If it does, then the function is running on the client, and your strange behavior must be coming from elsewhere. If there is no log happening, then the function is not being executed. You may think it must be, because it is the only explanation for behavior you are seeing. But the logs do not lie. What is more often the case is that the function is being run on the server, and modifying variables, which are being replicated to the client, giving the appearance that the function ran on the client. As i talked about before, it can be easy to overlook this in LAN settings, but it will surely be found in real world conditions. I'll discuss how to test things in 'real-world' conditions in just a bit. Logging is your friend, and you should become quite familiar with it. Most Unreal Tournament programmers are already familiar with it, and if they've gotten bogged down in replication problems already, they're just about gunaranteed to have logged stuff in trying to figure things out. However, combine logging with a knowledge of what's actually going on, and you increase your chances of success manifold. I'll be covering a few 'case studies' of sorts at the end of the document. You'll be able to see exactly what's going on as each part is done, and hopefully the real-world examples of replication will make things clearer.
Testing Real-World Conditions
Testing a Mod in Netplay without a Network
It's always important to test your mod in netplay. Sometimes, it can be hard, when you don't have a bunch of people available to test with. However, there is nothing preventing you from running a server and a client on the same machine. This allows you to test netplay on a single machine.
Windows 9x
On Windows 9x, the UT client will consume 100% of the CPU by default. This is so that extraneous programs like ICQ, mirc, or whatever else you might have open do not steal CPU resources from your UT client, which should get full priority. This means that if you run a client and server on the same machine, the server will be starved of CPU cycles, and your UT client will experience extreme lag and packet loss. While you may want to simulate these at some point, this is not the way to go about doing it. Luckily, someone wrote a simple program that helps solve this dilemma of testing a UT server and client together. It was originally intended to solve this same problem for QuakeWorld servers, but it is perfectly applicable to our situation.
Let's disucss how to use it. First, download priority and extract it somewhere safe. I have mine in my UnrealTournament\System directory. Next start your server. Your best bet is to do this with the 'ucc server' commandlet. The general format for testing your mods should be:
ucc server mapname.unr?game=packagename.classname?packagename1.classname1,packagename2.classname2
Some example commandlines follow:
- ucc server DM-Morbias][
- ucc server DM-Morpheus?game=RocketArena.RocketArenaGame
- ucc server CTF-Face?mutators=Botpack.InstaGibDM,Botpack.LowGrav
Next, you have to tell priority to give more priority to our UT server. You can read the readme associated with priority if you're interested in exactly what commandline parameters do what. But for our UT client/server testing purposes, just type the following in another window or command prompt:
ucc -title ucc
Now, our UT server will have the necessary CPU time it needs to run, even if a UT client is running. Now run the UT client, connecting to your local machine, with:
unrealtournament 127.0.0.1
Test your mod, and make sure it all works as you have planned. Of course, some things will require the use of more than one person to test, but at least you should be able to test and fix a large portion of your problems yourself, without requiring someone's valuable time, or requiring you to waste your own organizing a beta team and stuff.
Windows NT/2000
On Windows NT/2000, this is not a problem, since NT supports true multi-tasking, and so the use of priority.exe is not an issue. ////////////////////////////
Simulating Lag and PacketLoss
Testing multiplayer as described above only solves one problem. It helps you see if your code will execute in multiplayer correctly. However, the successful completion of that test does not mean that your mod will work in netplay. When testing netplay on your machine, your computer connects directly to itself, accross an incredibly fast and large connection. Testing on a LAN has the same 'problem', in that you will not be simulating the effect of people playing over modems on servers halfway around the world. Luckily, UT includes commandline options to help you simulate this type of behavior. They are: PktLoss, PktOrder, PktLag, and PktDup. These variables only affect data as it is being sent, so you will need to place them on the server and client to accomplish true lag. But first, if you check your UnrealTournament.ini, you'll notice references to SimPacketLoss and SimLatency. These worked once, in a land far, far away in a time long, long ago. They do absolutely nothing now, so you might as well remove them from your ini.
The true way to simulate lag is to use the commandline parameters when you start UT. We'll cover and document all of these variables here. To use these variables, you need to start UT like this: unrealtournament.exe server.ip.addr?PktLoss=4?PktLag=400. This will cause your UT client to experience this amount of lag when sending data to the server. Data sent from the server to the client till still travel at full speed, however. By the same token, you can also pass these variables to ucc with ucc server mapname.unr?PktLoss=10?PktLag=600. This will create a very 'bad' server with terrible conditions...probably too extreme even for real-life conditions. (If you play under 600 ping and 10% packetloss, then I feel bad for you. :) Let's get into the documentation of the individual variables. PktLoss will work all the time. The remaining three, PktOrder, PktLag, and PktDup are mutually exclusive, and if multiple options are passed on the commandline, the order just stated is the order they are checked. For example, if you pass PktLoss, PktLag, and PktOrder, then only PktOrder and PktLoss will be used. PktOrder has a higher priority than PktLag, and PktLoss always works.
PktLoss
The first, PktLoss. is set a percentage of packets sent. It determines the percentage of packets lost over the connection. Passing 10 will mean that 10% of the packets sent to the client will be lost to the bandwidth graveyard. Any packetloss usually will hurt a connection in real world connection, as many people complain about anything over 0% packetloss. So when testing, this value doesn't need to be that high, and probably should only range between 0 and 10. PktLoss set with: PktLoss=5
PktOrder
The next, PktOrder, determines whether packets will be sent out of order. This is simply a boolean flag, meaning that the only option for it is on/off, not a variable as in PktLoss. If this flag is set to on, it preclues the use of PktLag and PktDup, described below. PktOrder works like this: Every time a packet is going to be sent, it gets put at the end of a 'to-send' list. Then UT goes through the 'to-send' list, and sends half of the variables over the connection. While this is not truly random, it doesn't matter. It succeeds in it's goal in sending packets out of order, since they can be kept on the list for an indefinite length of time. PktOrder set with: PktOrder=1
PktLag
If you want to delay packets and simulate lag, then PktLag is the variable you want. Precluding the use of PktDup, and only working if PktOrder is not set, PktLag is set as a value of milliseconds. All packets being sent over the connection will be delayed by PktLag milliseconds. PktLag set with: PktLag=450
PktDup
If you want to simulate the effects of sending packets twice over the network, PktDup fits the bill. Only working if both PktOrder and PktLag are not set, PktDup is set as a percentage of packets that are duplicated. Every packet that is sent has a PktDup chance of being sent twice. All packets will be sent once. PktDup set with PktDup=20
Using the Variables
Now that you know how the variables work, here's a little lesson in how to use them. Remember that they only work for packets being sent. So if you set PktLag to 500 on the server, the client will experience 500 ms lag between what the server sent and what the client sees. Client packets sent to the server will go at their regular speed, however, which is usually around 40 ms each way on my machine. To truly test lag, you will need to use PktLag on both the client and the server. Ping, if you remember, is the time it takes for a packet to go round trip from client to server and back to client. To simulate a 500 ping, the best way would be to give a 250 PktLag on both the server and the client. All clients to connect to the server would then experience 250 ms additional lag, regardless of their local settings. The client would then add 250 ms additional lag itself through PktLag, resulting in a 500 ms effective ping.
Of the variables above, you will find yourself using PktLoss and PktLag most often. The others were more intended for Epic to test the results of it's own network code, and test it under extreme conditions that it could not duplicate. I doubt you will have need of PktDup or PktOrder unless you are unable to reproduce the player-reported problems about your mod being unresponsive or ineffective.
Conclusion
That's it, folks. Hopefully you now understand replication a lot more than you did before. Granted, it all may seem COMPLETELY overwhelming and confusing at first, and it is. But through application and testing of these principles, you will become more familiar with what is going on, and will better grasp network coding as time passes. You may need to read this document through a few times, before it all sinks in. If you find a section that is confusing even after you've read it through, send me an email at mongo-replication@jall.org. Don't expect me to help you out personally however, since I imagine I will probably get countless emails from new people that don't meet the basic assumptions I made and stated at the beginning of the document. It is always inevitable. Of course, those people probably haven't even read this far, so they'll be emailing me anyway, but I'm just letting you faithful readers know. :) I'm planning to read all your emails, and if they have good or valid suggestions for things to cover, or describe ambiguous or confusing aspects of my tutorial, then I'll make corrections and updates to this document in the future.
When something doesn't work, don't assume it's UT's native code in regards to replication. I believe I have successfully delineated all such hidden tricks in this document. The error has to be in your code, however much you don't like hearing that. Trust me, this is from experience. ;) Check your replication statements to make sure their logic is correct, and run through your code as both the server and the client, following the flow of logic to figure out what's going on. Make sure what you're expecting to be replicated or simulated IS ACTUALLY replicated or simulated, by following the checklists I've given in the document above. And as I stated before, LOG LOG LOG LOG. Fill your code chock-full of logs, to make sure that things are happening the way you want. After following all the guidelines, the only true test to make sure things are happening the way you expect is to put logs in, and see what is happening where. With all this in mind, I'm sure you will be successful in your future networking endeavors.
Good luck!
Mongo
Still to cover:
Special Networking Tricks (Case Studies)
Let's take a look at a few examples and problems that people have had, and an approach to solving them. Hopefully, this will help you solve your own problem, or at the very least, help you see how others have approached their problems, and fixed them.
thoroughly read through above sections on special properties of special stuff, realworld conditions, things go awry serverpackages packageflags native final functions are simulated automatically (?) state changes only seen for playerpawns, unless changed in simulated functions (then they cause problems for newly relevant actors) window stuff on client functions usually replicated before variables, cannot be sure Case Studies:
- HUDMutators (everyone wants them :/ )
- DrSiN's Window Replication Info
- How to get custom pris: RAUT's PRI extra stuff (avoid spawnnotifies)
- Laser-Guided-Ripper Tutorial get a better laserdot
- UT's method for sending data to the server for playerpawns
- How they did the GuidedWarhead in netplay
Weapon Replication/Networking overview (where?)
Other Neat Tricks
- Faking replication of parent-variables that wouldn't normally replicate
- Faking multicast replication with specially-owned objects by each player, that get functions called on them from a general function.
- Use physics to your advantage Use of PHYS_Trailer for Halo, Shieldbelt
- Use of Simulated ticks to accomplish clientside manual physics just a discussion, as complex code: Taliesin's code for inverse IK (hey, it's cool, too!)
- Other ways of doing non-standard variable replication with functions, like Taliesin's code for seekers