A short view on how we implemented our networking solution using the tools available from UNET while maintaining a rather transparent architecture for designers to work with network components.
Rock & Rails is a arcade shooter loaded with heavy metal released for GearVR that can partner you up with a friend in a local CO-OP jam session to blow monsters with the power of rock!
In this article we will explore how the multiplayer was achieved inside Unity in a way that favored the simplicity of content creation by the game designers.
Rock & Rails' local CO-OP multiplayer option is based of the new UNet structure from Unity, but using the bare bones of the native network structure of synced lists to follow a host-client model.
All the decision-making is left for the host to process, while clients only run intents and inputs. The sole exception are the local players that are allowed decisions to their local character and those decisions are replicated to their avatar on other clients.
This article will be divided in some sections as to help the understanding of the reasons and the architecture used to achieve this:
Why using synced lists only and why not using cmds, rpcs, etc;
The multiplayer architecture;
Integration with plugins;
Why using synced lists and not the rest of the UNet?
UNet is currently on the beginning of its life cycle. The module was introduced in Unity 5.?. Like any new API introduced to a team, a learning curve is expected to impact its productivity.
We also wanted to have the networking as transparent as possible to designers. They don't need to worry about authority, commands, rpcs, clients/hosts or any other technical networking aspect. Their work should be focused only on gameplay only.
We wanted the team to develop the game as they would in a single player game and the networking just would work naturally.
For that end we decided to only use sync lists to send messages between host and clients, and have the host decide all the outcomes.
Cmds, rpcs and synced vars were used in very specific situations that are not worth mentioning on this article as it will subtract from the content rather than give insights.
This decision simplified the networking architecture to a level that each network object on the scene only needed a network ID to be functional in a multiplayer environment.
Message routers in the code were programmed to allow the designers to fully use any plugins or tools they wanted to achieve their results.
The Multiplayer Architecture
Unet has a multiplayer architecture where one of the players act as both host and client and the aditional players are clients of that host.
Sync list are a type of UNet variable that keeps it's content synced across the clients, and whenever an item is added to it, the hook method on the clients are called with that information being transfered across the clients.
Sync lists are the core feature of our multiplayer solution, abstracting the concept of authority and replacing it with a message router that is able to find a game object in the scene and send a message to it, regardless of wich client or host that sent the message.
To handle the problem of multiple clients manipulating the same object, we created a concept of brain and body in the game.
The brains are scripts and codes that are able to make decisions about where to go, who to shoot, AI, etc, while the body carries out those decisions.
The brain runs only on the host, and each body on each client replicates this decision when a brain message is updated on the sync list of the clients.
The multiplayer relies mainly in three synced lists and the concept of brain and body on the objects that will have interaction on the scene.
The synced lists are:
-Generic Network Messages;
A lock that prevents brain scripts to run on client was created, including modifications on the plugin Behavior Designer so all behavior trees only ran in said host.
This architecture remained true to the host device that also acts as a client.
A network message would contain the Network ID of the brain that took the decision, a Network ID of the target object involved on the decision (if any) and an action to be executed by both parties.
As network IDs cannot exist in children objects of a GameObject that contains a network id, the message would also carry the path relative to the object linked to the network ID so the router could call the right method on the right GameObject. This message would be executed in all the clients when the synced list received a new add operation.
To improve performance, we replaced the spawn function of UNet (see how to do it here ) with our own poolable spawn system.
Since the spawn on the host/client is instantaneous while in other clients it might take a couple more frames, there was the possibility of some decisions being taken by the brain in host spawned object while that object was not yet spawned in other clients.
To solve that, we created the synced list of spawn messages that would map all new objects to its network IDs, and while the network and playmaker messages had new incoming messages without receiver, we buffered them until the new object presented itself on the current client, so then those messages could be flushed and executed.
Integration with Plugins
The main plugins used by designers were the Behavior Designer and Playmaker.
Our main goal as developers was to enable them to work with those plugins regardless of multiplayer, so the only restriction they had to work with was to reserve any decision-making to the behavior designer, and any execution to playmaker.
With custom actions for both, they could just drag and drop a single box that would allow communication between both while in the background those actions would map the object and the target into network messages, send to the host and get distributed throughout the clients, providing transparency to the network layer when building a new enemy or player behavior.
The worst bug we found was with the type of Qos Channel used. For some reason, the only one that will not mess up the synchronization of the synced lists between all devices is “Reliable Sequenced” (tested up till Unity 5.4.5).
What would happen is that after a couple of operations with the sync list, a message would be lost and while one client would have...lets say, 15 itens on the list, the other would have 14.
This syncronization was crucial to the game to correctly route packages to the right destination.
All other channels would eventually mess up the messages and have different synced list items in them and progressively bug the game to an unplayable state.
Although the team is eager to implement the full features of UNet, this architecture provided a very transparent and fluid work flow between engineers and designers, removing most of the complexity when thinking about authority, commands and rpc’s and providing a great network synchronization result.
Untill the UNet gets more iterations and maturity as a tool in the Unity arsenal, we shall keep using synced lists as our bread and butter during multiplayer matches.