Megabite #12 - RPCs and Basic Network Serialization

Working with Remote Procedure Calls – “RPC”s – is a bit confusing at first.  First, let me go into a bit of applicable context and make things more understandable.

An RPC is needed when we are going to execute a function on a remote machine while connected on a network.  They use what is called a “Network View” to accomplish this, and of course your game must be network-connected.  (There are plenty of different downloads via Unity and the Asset store directly that will get you that far to begin with).

The scripting behind an RPC works similar to a regular function, except it has a tag like this:

@RPC
function MyRPCFunction() {
iDidSomething = true;
}

To execute the function, you need to start it from the Network View component, which can be easily attached to any GameObject.  Via javascript, we do something like this:

networkView.RPC(“MyRPCFunction”, RPCMode.Server);

The RPCMode requirement is tell Unity who we are communicating with.  In this case, what would happen here is that the server for the game would get the “iDidSomething” variable changed, but nobody else would – including the original sender.  This feature is invaluable for manipulating data across a network.  When choosing a mode, we can choose:

  • All
  • AllBuffered
  • Others
  • OthersBuffered
  • Server

Now the trickiness lies in the contingency that we need to be careful to make sure everything flows.  Whatever object our script is attached to must have a Network View component, if we’re trying to change variables or execute things on another connection, we need to be certain that those things already exist on their system as well (If a server has a script that clients do not, for example.)

It can be frustrating at first, but eventually writing these functions becomes second nature.  Fortunately Unity’s development console will give you a list of RPC errors just like any other scripting issue.  For the most part, it will be fairly easy to pin down problems by running your editor as a server and a build as a client, and then vice-versa with your eye on the error readout.  In this way, you can see if the problem is client-side or server-side.

There are many different ways to set up the server/client relationship within Unity.  Because of a more recent project, most of my experience is with an Authoritative server setup, meaning that the server makes all the final calls - each client sends input to the server, and the server moves the player around on their own screen.  This choice was made to ultimately make the game “fair” for multiple players in the event that someone has a sub-par connection.   While I will show how this works within this article, I wanted to first touch on another subject that boggled me as I developed:

Network.Instantiate

There was a distinct lack of information out there on this subject, so I wanted to try and explain how this worked as it applied to me.  Being that my last project was my first networked project, there were a few things I needed to wrap my head around:

  • When we just use Instantiate on a server or client, nothing is networked automatically.  Thus, it can’t be seen on another machine unless we go the extra mile to be sure.
  • If we do an RPC call and make everybody Instantiate an object, that object is seen by all, but also controlled via each machine.  It STILL isn’t truly networked unless we go that extra mile.
  • Network.Instantiate will basically make a single object across ALL machines.  It does all the extra mile work for us, and it is controlled by the machine that spawns it.  If you are using this command, make sure it only is run by one machine (like the server).  If everybody runs the code at the same time, you will have a mess.

“The extra mile”

There are situations where we want to do something similar to Network.Instantiate, but perhaps not exactly.  So, let me be clear about what it actually does when we use it.  Network.Instantiate is powerful because it automatically serializes the object.  In essence, we do have a single object as far as the network is concerned, and when it is affected, it is affected network-wide.  In truth, the command is not as powerful as what it does behind the scenes – automatic serialization.

As we move forward, we will find a great many situations in which we want serialized data.  A health bar is a good example.  Let’s say for instance that an enemy spawns with 100 health.  That enemy is hit by an existing player, and now it has 50 health.  Now, a brand-new player enters the game.  Without serialized data, that new player would perceive that the enemy has full health if the data was not serialized correctly.

So, how do we serialize?  Well, we need a Network View to send/receive the data.  Whatever script contains the health variable can be dragged onto it so it is being “watched”.  Now, we need to tell unity what needs to be transferred over the net:

function OnSerializeNetworkView(stream : BitStream, info :NetworkMessageInfo) {
if (stream.isWriting) {
var healthC : int = currentHealth;
stream.Serialize(healthC);
} else {
var healthZ : int = 0;
stream.Serialize(healthZ);
currentHealth = healthZ;
} }

This section of code came directly from the unity reference for OnSerializeNetworkView.  This is another fancy built-in Unity function that comes in very handy for this exact purpose.  It determines automatically what to send/receive based on ownership, and it will be sure your health variable is the same throughout the network.

Back to the RPCs

After all that, we now understand a bit more about the inner-workings of a network.  Now, let’s put it into a basic, understandable script.  What we’ll do here is have an object that is instantiated into an auth-networked game (Not network.instantiated, just regularly.)  As such, both the player and the server see it on their own screens, and we want to take the player input and give it to the server to make our object shoot.

function Update() {

if (Network.isClient) {
if (Input.GetKey(“space”)) {
shootButton = true;
} else {
shootButton = false;
}
if (shootButton = true && Time.time > nextFire) {
nextFire = Time.time + 1.5;
networkView.RPC(“Fire”, RPCMode.Server);
}
}
}

@RPC
function Fire() {
var fired = Network.Instantiate(bullet, transform.position, Quaternion.identity,0);
fired.GetComponent(bulletControl).enabled = true;
}

So, in this case our player’s spacebar is being monitored for input and rate of fire.  On the server side, we have the server create the bullet and enable the script to control how it moves.  (That way seemed to simplify things much more for me when dealing with eliminating the bullet later in a very clean manner.)  We’ve minimized the amount of RPC data that will be sent for this scenario, which will save a bit of bandwidth as well (instead of sending an RPC at every keypress, we do it only when something can be fired.)  In addition, because the server created the bullet, the server can easily destroy it with Network.Destroy.

Bufferiffic

Lastly, I wanted to speak about the buffer.  Network.Instnatiate automatically does it, so it’s likely that it’s something you’ll want to consider in your design.  An RPC function to “All” or “Others” can also be “AllBuffered” or “OthersBuffered”.  When you buffer, you are keeping a queue of commands that will execute for players if they connect to the server after the commands are run.

This is very useful to consider, because the last thing you want is non-synchronized objects in-game because of bad timing.  RPCs can add things to the buffer, and things can also be manually removed from the buffer in the same manner.

So, before you have a client fire out an RPC call to All or Others, be sure to ask yourself if a player joining thereafter will need to know that this thing just happened.  If so, you will need to be sure your RPCMode is buffered.