Tuesday, May 5, 2015

[IndieDev] Unity3D's Networking - An In-Depth Technical Discussion

It's been a couple of weeks since I wrote a blog post. Turns out shipping a game involves some crunch time. Who knew? In all seriousness, it's been stupid busy. Partly because of some insanity that I wasn't originally privy to and/or did not understand the full implications of until recently. Let's talk about the practicalities of networking in Unity, because not many people do, and I'm honestly not sure what kind of game Unity has built their Networking APIs for. Note: This article assumes some familiarity with how Unity works with respect to scenes, components, and prefabs.


Networking in Unity3D

Here's the basics of how Unity does Networking out of the box. You create a Server, and then once your Clients find you, they can connect. Once connected, Unity does all the things under the covers to keep them connected. So far so awesome. To perform networking calls you need to have NetworkView components in both the server and the clients that have the same NetworkViewID and Type. For example: NetworkViewID 12 and Type Scene.

When making a Remote-Procedure Call (RPC) using a GameObject's NetworkView, the NetworkViewID/Type pair basically tell Unity, "Hey, I'm sending a message to the client. Route it to the object with this ViewID/Type." That's all well and dandy, but how do you get there to begin with?

You've basically two options to get yourself bootstrapped: a scene shared between the client and the server, or Network.Instantiate.


NetworkView component in your scene, with a ViewID already allocated.
The easiest--and possibly the most fragile--method is just using the same scene with objects in the scene that already have NetworkView components. Unity will pre-allocate an ID and mark it as type Scene. Once you've connected to the server, your objects will communicate naturally from there. As you can see in the screenshot above, ViewIDs allocated thusly are marked as Type Scene, and you can see an ID already set aside.

This is fragile because once you ship, if you have clients who are out of date with the server, you might not be able to have them communicate correctly. If IDs don't line up, or don't exist, your communications will fail. But it's super easy to set up.

The other solution is to get bootstrapped with a Network.Instantiate. Network.Instantiate creates an object on whoever calls it and all other machines connected to your game. So if you call it from the server, it'll create the object on the server, and all of the clients. If you call it from a client, it'll create the object on that client as well as the server and other clients. Whoever calls Network.Instantiate is the owner of that object. If that object has a NetworkView on it (or any of it's children), Unity will allocate a NetworkViewID marked as Type Allocated such that they can communicate immediately after instantiation.


This is what you've wrought when you use Network.Instantiate, The same object, on all game instances, capable of communication to any or all other instances.
The issue, however, is that by using Network.Instantiate, you've created a Many-to-Many connection, which is effectively peer-to-peer. It's convenient in terms of not having to think too hard about the networking, up until you start running into some gnarly issues around patterns that Unity pushes you to use.

When calling RPCs, you can say, "Send this message to everyone (including myself)," "Send this message to everyone (not including myself)," or "Send this message to the server only." One thing a lot of folks miss, because it's buried at the bottom of the RPC page, is you can direct them to a specific client by passing the NetworkPlayer struct instead of the RPC.Mode. This allows you to communicate specifically with one client directly rather than being stuck talking to everyone, using the same NetworkView.

That being said, though, Peer-to-peer isn't a very common networking pattern for gaming these days. Authoritative servers that sync state and prevent cheating are generally the norm, though I imagine a game like Spaceteam might actually prefer a peer-to-peer model. But for a game like Eon Altar, we need an authoritative server. Since players are each on their own device, and we have a central server which is controlling (and displaying) the state of the game, players shouldn't need to communicate with each other directly.

Why The Distinction Can Be Important

A side note, this whole conversation was precipitated by something that I noticed around how Unity handles objects and prefabs. Turns out that when you have a prefab loaded up, either by linking it in your scene, or using Resources.Load, it loads everything about that prefab and what that prefab links into memory. Which makes sense in hindsight, but judging by the Internet, we're not the only ones to make that mistake.

The issue we hit was the fact that we used Network.Instantiate on our player objects, since the handsets need information like powers, attributes, health, and so on. However, as mentioned above, Network.Instantiate creates the objects on all connected machines, so we have wasted RAM on loading up stuff about the other players on any given player's handset.


In a 4-player game, our liberal use of Network.Instantiate and RPC.All led to a conceptual model that looks a lot like the pentagram spaghetti above.

Note that it's plausible Unity still routes the changes through the server rather than client to client, but conceptually it's still effectively a direct peer-to-peer connection.
To make matters worse, because our "server" is doing the displaying of visual effects, sound effects, and the player model, and those are linked to the player prefab, we're loading up an immense amount of extraneous data. The handsets don't need the sound or visual effects, or the player model at all! This ended up chewing a good 90 - 120 MBs of RAM for no easily discernible reason on our handsets, which when you want to run on a 512 MB RAM iPhone isn't exactly desirable.


Possible Solutions

A solution to the previous issue might've been to create a separate network object for Server<->Player connections, but Unity isn't really built around that. Rather, Unity really likes you to perform your network operations on the objects doing the work. That's not to say it's impossible, but Unity's natural patterns discourage that line of thinking. Fighting your platform isn't generally a good idea if you can avoid it.

Another solution would be to have entirely different objects hooked up with the same NetworkViewID so they can talk to each other. This would require us to abandon using Network.Instantiate, and ensure that we made special code to handle that case.

The solution we're going with is a variation on the previous: abandon Network.Instantiate for the player objects, and basically make our own Network.InstantiateOnSpecificClient code. This allows us to use the same player objects we were before, but reduces the memory required as we're not loading up everyone's player object on every device. It also prevents us from accidentally sending commands to other clients when we really shouldn't be by conceptually enforcing Authoritative server model. Ideally it should also just reduce networking overhead in general because we'd always be sending to specific clients rather than everybody.


Conceptually more sane, and actually mimics an actual Authoritative server model. Also not loading prefabs on every single client is nice.
This of course required us to build our own Network.Instantiate replacement. And it turns out that Unity does a lot of work for you in Network.Instantiate. Without it, you need to make sure you have some object with RPC capability already set up (either via a shared scene, or via Network.Instantiate), then you make a method that:
  • Instantiates your prefab locally
  • Sets your instance's position/rotation
  • Allocates a Network View ID and assign it and a group to your NetworkView
  • Figures out your unique identifier for your prefab (we used the Resource path)
  • Make a RPC call to the client in question which:
    • Loads the prefab from Resources using the unique identifier
    • Instantiates an instance of said prefab locally
    • Sets the instance's position/rotation
    • Assigns the allocated NetworkViewID/Group from the server
Once you have that, you have the ability to Network.InstantiateOnSpecificClient, effectively. In theory they could be the same object, or you could use it to hook up two different objects (something I've done when the server and client need substantially different functionality that it didn't make sense to put it in the same class). 

Note that if you have nested objects with NetworkView components, you'll need to manually allocate those, too. Also note that you'll need to ensure all your RPC communication goes to a specific client (or RPCMode.Server), or Unity will give you errors on the other clients that no NetworkViewID of number blah exists.

You'll likely want to parent that object to something, which requires you to write some RPC code to set the parent. Here's a hint: you can find objects in your scene by NetworkViewID calling NetworkView.Find. If you know the NetworkViewID of the parent and the child, the rest of the exercise should be trivial.


Conclusion

I haven't even touched on buffered RPC calls, or the state synchronization convenience fields on the NetworkView component. But overall, I've found that Unity's convenience methods induce creating bad architecture. When building a network architecture, I highly suggest ensuring that you think about the model you want to follow and ensure the platform you're building on can support that model out of the box. If not, you might have some extra code you need to write. #IndieDev, #EonAltar, #Unity

4 comments:

  1. I don't know anything about Unity. But to me it seems that your "authoritative server" model is *normal*, and what you would expect when writing networking code. I find it really surprising that Unity does not default to that.

    I wonder if you are looking at a layer higher than you should be. I.e. Unity has the regular networking model on layer 1, and above it they built a couple of conceptual APIs on layer 2. But everyone serious writes against the layer 1 API.

    ReplyDelete
    Replies
    1. Right?! That's why their networking model is so confusing, it's pretty much the opposite of normal.

      The only layer below is .NET sockets. Which to be fair, we are using to get the initial connection set up. Unity's APIs and RPC model are super handy though, and honestly I think folks mostly use a combination of the two. Depends on when/what you want to do.

      Delete
  2. I don't know anything about Unity, but I have to agree with you two for the most part. I mean, I can see situations where Unity's Network.Instantiate model might be useful (maybe something like dynamically setting up Etherpads, or other situations in which close interaction between peers is required, without the need for a coordinating instance), but it definitely feels like the classic Client-Server setup is much more prevalent and useful in most cases.

    And about your "logical vs. physical" routing note: I would very much hope that Unity routes all traffic through the server! That's not only important for security reasons (the clients don't need to know the network addresses of the other clients). It also should significantly reduce network traffic on the clients: Since I assume you'd send the same data to every client, you might as well just sent it once, to the server, and let it take care of the propagation. People who use a phone client on a mobile data plan will be much happier!

    ReplyDelete
    Replies
    1. Re: Security standpoint
      Excellent point! Absolutely agreed.

      And yeah. In terms of your client's traffic, direct communication with the server only would/should reduce the overall network usage from the client's perspective

      Delete