Unity FPS Networking Sample

fps_sample_screenshot

This Unity Package can be downloaded here (updated):  fps_sample_2013_02_21.unitypackage

Unity is a great tool for Indie developers, but there are parts of it that can really make you spin around in circles.  Networking is definitely one of those parts.  I set out to make a simple peer-to-peer FPS sample project with two goals in mind:

  1. Make it as simple as possible
  2. Avoid (as much as possible) the “exotic” Unity networking components

The components I decided to avoid were NetworkViews, Network.Instantiate, and RemoveRPCs.  I picked these three because NetworkViews (even when set to “Unreliable”) can create a lot of unnecessary network traffic if they are not managed very carefully.  Network.Instantiate also generates so much network traffic that the game can pause and skip every time a new player joins.  And finally, RemoveRPCs (RPC = “Remote Procedure Call”) only works when you are using Network.Instantiate, so it goes out the window as well.  I really wanted to do this as a true peer-to-peer, but Unity’s built-in networking is geared strictly for client-server networking.

Update:  I recently found a good way to do Server Discovery for the LAN, which I should really post soon.

So, what does that leave us with?  It leaves us with…

FPS Sample Using a Single World NetworkView

If you start digging through LOTS of other Unity Networking tutorials, they’ll follow the “standard route” of adding a NetworkView to your player prefab and then relying on Unity to decide when to send updates, and how much data will be sent.  Using this setup, EACH player will have its own chatty NetworkView sending out information to everyone else more or less continuously.

We’re not going to take the standard route.

However, we’re still going to need a single NetworkView because that’s the only way Unity can communicate over the network (since we don’t want to re-create the wheel and build our own Socket manager in .NET).  However, a single NetworkView component attached to an empty GameObject can be set to State Synchronization=OFF, and Observed=NONE.  This will allow us to send RPCs back and forth between client and server without all the overhead and without all the unnecessary complexity.

Which would leave us with something like this:

fps_sample_overview

At the same time, we still want a way to guarantee a unique ID is assigned to each player.  To do this, we could use any of .NET’s functions specifically designed for this task.  But if we did that, then we’d have to import at least another package — which isn’t really worth it just for one function.  A better option is to just use AllocateViewID() to get a unique object on the server even though our actual player object won’t have their own unique network views.  Yes, that’s totally cheating :)

Having just a single NetworkView over which RPCs can be sent means there are a few extra responsibilities we have to take on:

  • Maintaining a dictionary between GameObjects and NetworkPlayer objects so that we can easily lookup one from the other.
  • Managing the creating and destruction of player objects on all clients (from the server) as players join or disconnect.
  • Devising a way for clients to be able to distinguish between messages that are meant for them and messages that are meant for other players

To help explain how this is accomplished, let’s take a look at a process flow diagram.  Did I mention that I <3 process flow diagrams.  I should probably mention that before we get to far into things and PFDs start flying all over the place.

simple_fps

If you haven’t done so already, grab the Unity package and open up the networkController script so you can follow along.

OK, let’s look at the important parts, starting with our first RPC in OnPlayerConnected()

networkView.RPC("JoinPlayer", RPCMode.All, newViewID, Vector3.zero, p);

In OnPlayerConnected(), we execute this RPC call to all connected clients using the server’s world networkView.  Notice that we are using RPCMode.All – which will send the JoinPlayer() RPC to all players and the server.  The player object must be created on all clients and the server, and this is the simplest way of accomplishing that.

Also, notice that the server AND the client is maintaining the HashTable of all players.  This is done in the JoinPlayer() RPC with this simple command:

players.Add(p,newPlayer);

…which is removed when a player disconnects with something very similar in the DisconnectPlayer() RPC:

players.Remove(player);

Now, in the JoinPlayer() RPC, we have this critical comparison:

if(p.ipAddress==LocalAddress)

In Start(), we determine the computer’s Local IP Address using Dns.GetHostEntry(Dns.GetHostName()) from .NET’s System.Net.Socket package and then use this to compare it against the NetworkPlayer structure’s “ipAddress” attribute that is being sent from the server.

Now, at this point, you’re probably asking, “well what about everyone else?”  After all, it’s all well and good that the player and server know about each  other now, but what if there are already players in the game?  How does the server tell the new player about existing players.  Before that can be done, the client must wait until it is fully connected to the server, which is why we wait until the client determines that it is connected before requesting the complete player list from the server.

We use the built-in function OnConnectedToServer() to get our timing just right, and then request the player list:

void OnConnectedToServer()
{ 
   networkView.RPC("SendAllPlayers", RPCMode.Server);
}

Because we use RPCMode.All in the original JoinPlayer() RPC, the server has instantiated a GameObject for every client that is joined.  To get a list of all players, the server only needs to run this code once every time a new player joins in the SendAllPlayers() function:

GameObject[] goPlayers = GameObject.FindGameObjectsWithTag("Player");

For each member of goPlayers, the server extracts the NetworkPlayer and the allocated NetworkViewID, and sends those on using more RPC calls to the JoinPlayer() function.  However, we make sure that we are NOT sending the requestor’s data back to them by doing this comparison:

if(gonp.ToString() != info.sender.ToString())

Because the allocated NetworkViewID is guaranteed unique within the server session, this is far more reliable than using RPCMode.Others (which, despite Unity’s claims to the contrary, actually DOES send a message back to the original sender).

The client ALSO checks the incoming NetworkPlayer data structure’s ipAddress against its own LocalAddress — but this is really just a cross-check to insure that no client can get a doppelganger :)

…because no one like doppelgangers.

So, that’s pretty much it, a complete FPS Networking Sample that only uses a single world NetworkView that generates minimal network traffic.

So is this the end of our peer-to-peer networking dream?  Hmmm, you’ll notice that the Hashtable of all players is being maintained on all clients.  So, in reality, each client already knows everything the server knows.

If you are guessing that I’ve already thought of a way that any given client can:

  • detect when the server disconnects,
  • using ipAddresses, determine if it should take the place of the server,
  • turn itself into the server,
  • and finally tell all the other clients that it is now the server…

…well, if you’ve guessed all that, then you should really consider getting checked out to see if you are psychic or something :D

To see what I’ve got so far, grab the Unity Package here:  fps_sample_2013_02_21.unitypackage

As always, thanks for reading!

9 thoughts on “Unity FPS Networking Sample

  1. I’m currently in school trying to get the network for our game set up, ‘nd this sure beats the standard method >.< Thankyou so much! ( I <3 PFDs 2) :3

  2. hello, can you help me please, can its solution use (server-client) mode. i create level, and i want to add my friends, but i want play with they. Now, if i start server, i can`t connect to self.

  3. Correct if I am wrong (surely)

    When you say you get rid off the NetworkViews, and let just one per client-world, do you mean “I still use the networkviews, but are all managed and centralized in the world’s networkview”?

    I see you centralize the common operations on the world’s network view, and you avoid using the Network.Instantiate. But the prefab you are instantiating (with the normal Instantiate, not the network one) HAS a network view component, it’s true that viewID is assigned by the World NWV, but they have that networkview observing the player’s transform (in your sample package).

    I am a bit confused at this point, any help will be thanked.
    Thanks in advance.

    • Correct if I am wrong (surely)

      do you mean… “I still use the networkviews, but are all managed and centralized in the world’s networkview”?

      It means I’m using just one networkView — just like the title of the blog post says: “FPS Sample Using a Single World NetworkView”

      “…but the prefab you are instantiating (with the normal Instantiate, not the network one) HAS a network view component”

      The network view on the player instance that is being instantiated is turned off — the player instances do not send out any information to the server — all information transfer is done via RPCs, which is much more efficient than an active NetworkView. The NetworkView is only used to provide a convenient globally unique ID (well, as close to a GUID as Unity can offer with its built-in networking) that the server can use as a reference to the player that owns the networkView on each client session — that’s how we’re still able to move around other players without their network views actually being turned on.

  4. Hi, I was wondering how you would go about sending a RPC from the client to the NetworkController. I’m attempting to add a simple ‘Press ESC to Disconnect” feature. With this information I should be able to do what ever else I wish, such as implementing weapons. Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s