Unity Networking Basics and Beginner Issues

While working on my multiplayer game project, I ran into some awkward issues with the Unity's networking system (UNET). I had some experience with the Photon Unity Networking from before, thus wrapping my head around and adapting my mind to Unity's newer native networking system took some time.

Basics of UNET

If you (like me) try to go over the application tutorials about the networking system such as this, you will probably have a hard time understanding the basic consepts of the system, which is required to apply the things you learned to your own project that is different than a generic multiplayer shooter. Instead of going over Unity's tutorial, I highly recommend reading the concepts documentation which introduces the most basic logic behind the system. 

Basically, you have a host and several guests who connect to the host. Host embodies both the server and its client, while guests are just clients. This structure can be interpreted as that the server is not on a remote machine but on the host itself. Nevertheless, since the host also is a client, you need to be aware of this seperation of concern on the host side. For instance, when you send a "ClientRpc" call (which runs only on the clients), this call will be triggered on the host as well as the guest clients. Conversely, the "Command" calls are only run on the host machine. The downside of this system is that, depending on your code structure, you may end up with a lot of "isServer" and "isLocalPlayer" checks. All things aside, what I am trying to say is don't be like me and be puzzled about why the rpc calls are run on the host machine.

When you wrap your mind around the basics and go into the more advanced stuff, you will come across some bumps and hills. There are some parts where Unity's documentation lacks or some unfixed bug emerges which you need to track its workaround from the internet. I will try to reference to a few of such situations I encountered, which made me lose a lot of time during my lovely sessions with the UNET. Bear in mind that the version of Unity I was using when I encountered these issues was 5.6.1.

Unity's Editor Behaviour is Different Than Its Standalone Build

The first problem for me was that, when using network behaviours, the order of calling of the Start() and Awake() methods are different on standalone builds compared to when you run the game inside the editor. This runtime flow difference caused me to waste a lot of time. In the article "Base and Singleton Monobehaviour Classes in Unity", I wrote about how I use base classes for singleton game object components. I extended this method to use the same approach with the NetworkBehaviour classes and similarly created "BaseNetworkBehaviour" and "SingletonNetworkBehaviour":

public class BaseNetworkBehaviour : NetworkBehaviour
{
    ...
	public static T GetNw<T>() where T : SingletonNetworkBehaviour
	{
		return SingletonNetworkBehaviour.get<T>();
	}
	...
	void Awake()
	{
		WakeUp();
		PostWakeUp();
	}
	public virtual void WakeUp()
	{
	}
	...
}
	
public class SingletonNetworkBehaviour : BaseNetworkBehaviour
{

	public static List<SingletonNetworkBehaviour> Instance = new List<SingletonNetworkBehaviour>();
	
	public static T get<T>() where T : SingletonNetworkBehaviour
	{
		var instance = Instance.OfType<T>().FirstOrDefault();
		if (instance == null)
		{
			instance = GameObject.FindObjectOfType<T>();
			if (instance != null)
				Instance.Add(instance);
		}
		return instance;
	}
}

The main idea for the singleton class is that I add the singleton instance to a list when the "Awake()" function is called. If I designate the singleton game object to be a network behaviour, then the Awake() function seems to be only triggered when the object is fully initialized over the network. Therefore, I cannot pull the instance of my singleton behaviour from my non-networked components. This actually sounds like the logical outcome to expect, you can't be sure when the networked component will be ready for use; but this flow is different when you run the code on the editor, regardless of the editor instance being designated as the host or the guest client. The build on the editor always runs the Awake() methods of the NetworkBehaviour classes before the Start() methods of the MonoBehaviour classes, while the standalone compiled build runs Awake() functions after the game object is initialized on the network. This inconsistency distracted me from seeing that the situation might be caused by the Unity's own behaviour.

Secretly Dictated Way of Initializing The SyncListStruct

Although I did not dig its details, another weirdness I saw is that I needed to leave the initialization of the SyncListStruct variables on the variable decleration like this:

public class Player : NetworkBehaviour 
{
    ...
    public CardSyncList Deck = new CardSyncList();
    public CardSyncList Hand = new CardSyncList();
    ...
}

If I do the initialization on a command, I get weird errors (invalid IL code errors that I refer to in the next paragraphs):

...
[Command]
public void InitializeDeck() {
	Deck = new CardSyncList(); // This causes error
	...
}
...

As far as I have seen, this is not mentioned on the SyncListStruct documentations, which is very disappointing since it looks like this usage dictation is a very fundamential part of the whole concept. The documentation page only has a working example where initialization is done the way it should be and a word about how it is the only allowed way to do it is nowhere to be found.

Deep Into Unity with Invalid IL Code Errors

Third and maybe the most annoying issue with the system is that it sometimes gives very obscure low level errors, where you have no idea what it is talking about. One example is the following:

InvalidProgramException: Invalid IL code in Bs.TowerAttack.Game.CardSyncList:SerializeItem (UnityEngine.Networking.NetworkWriter,int): IL_0002: ldfld     0x0a0000bc


UnityEngine.Networking.SyncList`1[System.Int32].SendMsg (Operation op, Int32 itemIndex, Int32 item) (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/SyncList.cs:319)
UnityEngine.Networking.SyncList`1[System.Int32].Add (Int32 item) (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/SyncList.cs:389)
Bs.TowerAttack.Game.PlayerIdentity.CmdSetPlayerDataToServer (System.Int32[] deck) (at Assets/Bs TowerAttack/Scripts/Bs/TowerAttack/Game/PlayerIdentity.cs:71)
Bs.TowerAttack.Game.PlayerIdentity.CallCmdSetPlayerDataToServer (System.Int32[] deck)
Bs.TowerAttack.Game.PlayerIdentity.RpcGetPlayerDataFromClient () (at Assets/Bs TowerAttack/Scripts/Bs/TowerAttack/Game/PlayerIdentity.cs:63)
Bs.TowerAttack.Game.PlayerIdentity.InvokeRpcRpcGetPlayerDataFromClient (UnityEngine.Networking.NetworkBehaviour obj, UnityEngine.Networking.NetworkReader reader)
UnityEngine.Networking.NetworkIdentity.HandleRPC (Int32 cmdHash, UnityEngine.Networking.NetworkReader reader) (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:660)
UnityEngine.Networking.ClientScene.OnRPCMessage (UnityEngine.Networking.NetworkMessage netMsg) (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/ClientScene.cs:739)
UnityEngine.Networking.NetworkConnection.InvokeHandler (UnityEngine.Networking.NetworkMessage netMsg) (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:231)
UnityEngine.Networking.LocalClient.ProcessInternalMessages () (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/LocalClient.cs:141)
UnityEngine.Networking.LocalClient.Update () (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/LocalClient.cs:69)
UnityEngine.Networking.NetworkClient.UpdateClients () (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/NetworkClient.cs:947)
UnityEngine.Networking.NetworkIdentity.UNetStaticUpdate () (at /Users/builduser/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:1091)

Apparently, this error is given when you use SyncListStruct directly with a basic type such as float or int like this:

public class SyncListFloat : SyncListStruct<float> { }

What you need to do is create a struct with a variable of the basic type that you want to hold:

public struct FloatStruct
{
	public float Value;
}
public class SyncListFloat : SyncListStruct<FloatStruct> { }

The correct way of usage is actually the one mentioned in the documentation, but the fact that it only works with structs is not stressed enough. The documentation says "We allow the following types to be used in your struct" but it is left to the reader as an excersize to understand that the struct mentioned is the one you construct and not the SyncListStruct. That aside, if you understand it wrong (which is easy to do) and just use it with base types, you get the "invalid IL code" error and look at the screen confusedly and helplessly.

It is very easy to encounter these IL code errors while working on Unity. It seems like the error handling on the UNET system is poor, which results in seeing unmeaningful and nondirective error messages. At least, when you get these errors, you know that you are doing something wrong. Nonetheless, it is hard to guess where the error is and the investigation process is very time consuming.

All my rants aside, I am actually enjoying my time with UNET. It is quite a native-feeling and flexible system if you know what you are doing and plan your code accordingly. I am yet to dive into the potential nightmare of converting a multiplayer game to singleplayer, which I am planning to do. Since the ease of creating a singleplayer experience from a multiplayer game is one of the advertised features of UNET, I have been thinking about this, but I can not wrap my head around how the system will work easily, maybe by initializing a local client where the gameplay of that client is simulated by an AI. I am planning on writing more articles about the features and problems I come across in UNET, since I am working on a game project using it.