UNet Pitfalls: OnStartClient Execution Order

Unity’s UNet multiplayer networking layer is easy to use and a good solution for any project without special demands in this regard. However, doing my second project now with UNet, I repeatedly find some pitfalls which temporarily take the fun out of programming. This time, I wondered about some SyncVars not synchronizing correctly, until I found out that my problems had nothing to do with them. Instead, I discovered that the OnStartClient method of NetworkBehaviours is not always called in the same order with other callbacks such as Awake or Start.

The setup of my project is rather simple. I started out with Unity’s Network Lobby example from the asset store. Besides the lobby and prior to version 1.5 it also contained a simple Pong example as a two-player networked game. My goal was to replace the Pong game with my own without having to bother a lobby implmentation myself. The UNet high-level API provides all you need for this.

An example of a typical UNet lobby layout.
Before starting the game players meet in the lobby.

The game always starts with the lobby scene. Players connect to each other via the lobby. Once a match has been found the game is started by switching all clients to the game scene. The lobby takes care of instantiating one player object for each player on each client.

Event Execution Order and OnStartClient

Each MonoBehaviour in Unity may contain Event Functions such as Awake, Start, and Update. These are called in a certain order as described in the Unity manual. Awake is called as soon as an object is instantiated. For those GameObjects which are part of a scene, Awake is called before Start for all of them. Classes derived from NetworkBehaviour may implement another callback, OnStartLocalClient. It is called “on every NetworkBehaviour when it is activated on a client”.

The Pong example contains a “PongManager” class which is part of the game scene. It is instantiated when the game scene is loaded by the lobby scene. A “PongPaddle” (the player object) is added for each player via networking. The player classes register with the PongManager in the following way:

public override void OnStartClient()
{
  [..]
  //sometime instance isn't yet define at that point (network 
  //synchronisation) so we check its existence first
  if (PongManager.instance != null)
    PongManager.instance.PlayerNameText[number].text = playerName;
}

As you can see, the comment already warns about the fact that, although the PongManager is part of the scene, it may not yet be instantiated because it is a NetworkBehaviour. This code, however, will fail anyway in case the PongManager does not exist yet. It will never register the player object with the manager because OnStartClient is only called once for each NetworkBehaviour.

Undefined Execution Order

I ran head-on into the problem above, without even noticing it for a while. The reason was that when running the game in the editor, no problem arises. All objects which are part of the scene are instantiated first, such as the PongManager. After that, any networking objects are created which are instantiated at runtime. The execution order of events is as expected.

However, building the game and running it in a standalone Windows client, this execution order may be, and usually is, different. Some or all of the player objects are instantiated before the PongManager, their OnStartClient method being called before the PongManager exists. Unfortunately, this goes unnoticed unless you compile your game as a development build, where eventually you may see error messages popping up. Not in case of the PongManager which only shows the player’s name. In my case I had to register the player objects with my game manager, which led to a number of errors.

Workarounds

Once you realize this issue, there are several simple ways to avoid it. One solution is to wait in Update until the GameManager exists.

private bool isRegistered = false;

void Update()
{
  if ((!isRegistered) && (GameManager.instance != null))
  {
    GameManager.instance.RegisterPlayer(this);
    isRegistered = true;
  }
}

A more elegant way is the use of a Coroutine. It is started in OnStartClient and only exists until the GameManager is instantiated.

public override void OnStartClient()
{
  StartCoroutine(DelayedRegistration());
}

private IEnumerator DelayedRegistration()
{
  while (GameManager.instance == null)
  {
    yield return null;
  }
  GameManager.instance.RegisterPlayer(this);
}

This solution requires no instance variable (isRegistered) and generates no overhead inside Update.

Summary

The normal event execution order of MonoBehaviour startup methods is “Awake” being called for all GameObject components in the scene, followed by “Start” for all scripts. All scripts attached to GameObjects being instantiated at runtime follow at least after the first round of Awake calls. Not so for UNet’s NetworkBehaviours. While the execution order within one NetworkBehaviour is always Awake, OnStartClient, Start, the order between NetworkBehaviours is not deterministic. Moreover, while it seems to be well defined in editor mode, it may be quite different in a standalone client.

Prevent OnStartClient pitfall by using a Development Build
Development Build is selected in your player settings.

While knowing about this little pitfall helps, my advice is to always build your clients as a development build. Do not trust that your code behaves in a build as it did in the editor. A development build shows any exceptions and errors inside your game window as soon as they occur.

Update in 2022, six years later: fell into the same trap again, without even needing NetworkBehaviours. Continue reading in my new post on my return to the execution order pit!

0 0 votes
Article Rating
Abonnieren
Benachrichtige mich bei
0 Comments
Inline Feedbacks
View all comments
0
Was denkst Du? Bitte hinterlasse einen Kommentar!x