Skip to main content

Multiplayer and Networking

Massive Loop VR fundamentally is a multiplayer platform, and you as a developer are responsible to make sure that the (necessary) objects and events in your worlds are synchronized properly between all players who join the game.

Multiplayer network structure

A Multiplayer network follows a shared structure. The full control of the game is shared between the clients, and the server only provides the communication means between the clients. Players are placed in multiplayer groups called rooms. Each world can have multiple rooms as per demand, but the players from different rooms cannot interact with each other. What happens in a room, stays in a room!

Every movement of the players, or other world events, synchronized with all players. As the number of players grows, the amount of data that is interchanged also grows, therefore the rooms have a limit on how many players can join.

Always one of the players in a room is selected by the network as the master player or master client. The Master Client can be used to keep track of events happening in the world and execute scripts that are related to the room. For example, in an FPS game, the state of the game, team scores can be handled by the master client.

Master client vs non-master client.

The only difference between the master client and a non-master client is the behavior. When joining the game, they both have access to the same assets and load the same scripts for the given world. Master client is still a player itself but may execute extra code to manage the world, game or etc.

Master client migration

So what happens if the master player leaves the game. This is a process called master client migration or host migration for short. In this case, the network selects another player in the room and declares them as the master client. The newly chosen master client starts executing the master duties immediately.

State of codes

Host migration does not transfer the state of executing code! The state of LAU scripts running in the host will be lost when the host migration happens. The new host starts executing the scripts from the beginning. All the states of variables that evolved during the execution of the code will be lost.

Let's consider this scenario:

In your world, you implemented a door. This door will allow the player to enter a room. The door has a state (open or close)

Now look at this code which is running in the master client and controlling the door:

In this case we use the extension methods for MonoBehaviour to register an event handler, and invoke the event over network. By using these extension methods, we can create an local event.

using ML.SDK;
using UnityEngine;

public class Script : MonoBehaviour
{
const string EVENT_ID = "DoorEvent";
bool doorState;
EventToken token;

private void OnDoorEvent(object[] args)
{
if (args.Length > 0)
{
bool newState = (bool)args[0];

if (doorState && !newState)
{
// door is open and new state requires to be open
Close();
doorState = false;
}

if(!doorState && newState)
{
// door is close and new state requires to be close
Close();
doorState = true;
}
}
}

public void Open()
{
// invoke event over network, with true
this.InvokeNetwork(EVENT_ID, EventTarget.All, null, true);
}

public void Close()
{
// invoke event over network
this.InvokeNetwork(EVENT_ID, EventTarget.All, null, false);
}

public void Start()
{
// register an event handler for the event id
token = this.AddEventHandler(EVENT_ID, OnDoorEvent);
}
}

Here, the variable doorState defines the state of the code. The problem is that when the host migration happens, the current state of the door is lost as the script in the new host will set the door state to false or close again. If the door was open before the host migration, the state of the door in the master client conflicts with the real state of the door.

As you can see in this example, this will cause lots of issues in the games, where losing the state of something might have more important consequences than the door example.

Synchronized Variables

To remedy that, the Massive Loop incorporates Synchronized Variables. Synchronized variables stored in all clients and any change to them (from any player) will propagate to all players.

The Door example with synchronized variables would look likes this:

In C#, you can make fields with in any MonoBehaviour synchronized by attaching [MLSyncVar] attribute to it.

Script.cs
using ML.SDK;
using UnityEngine;

public class Script : MonoBehaviour
{
/// <summary>
/// synchronized door state
/// </summary>
[MLSyncVar]
bool doorState;

bool currentDoorState;


public void Open()
{
// code for opening door
}

public void Close()
{
// code for closing door
}

public void Update()
{
if(doorState != currentDoorState)
{
currentDoorState = doorState;
if(currentDoorState)
{
Open();
}
else
{
Close();
}
}
}
}

Here, we declare the doorState as a synchronized variable by adding an [MLSyncVar] attribute to the doorState field. This ensures the doorState to be synchronized between all players. Here, each player keeps a copy of the doorState and when one of the players makes changes to it, it will propagate to other players.

note

Only fields that are defined in MonoBehaviours can be made synchronized. In any other case, the attribute wont have any effect.

Local Vs Global Variables

Synchronized Variables can be local or global. A local synchronized variables scope is with in the (game object + MonoBehaviour) instance. It means that the a local synchronized variables is different with an other local synchronized variable in an other game object. You can define a global synchronized variable by making the synchronized field static.

[MLSyncVar]
public static bool doorState;

Limitations:

There are few limitation on the synchronized variables:

Type:

The synchronized variables type can change at any time during the game, however, only the value of Serializable Types gets synchronized. If a none-serializable type provided, it will synchronized as a nil.

Size

There can be any number of synchronized variables, but each frame will only synchronize 1 KB of data. It means if the total size of all changed variables exceeds the 1 KB limit, then some variables might take more than one frame to be synchronized. For the same reason, any variables that is bigger than 1 KB would not synchronize (a nil will sent instead). Note that each variable have some other overheads such as variable name and etc. So make sure that the variable sizes are less that 700 Bytes. This is of course only valid for strings and arrays. Strings are serialized with UTF-8 encoding, so one character can take between one to four bytes.

Network Lua Events

As seen in the door example above, the Events can be raised over the network.

Check out the article on ML Events System

Adding an "Event Handler" for purpose of using it as a network event is exactly the same. As a matter of fact, an event handler can be used for both purposes. Internally and over the network.

Given the handlers exist in the intended target, we can call a different Invoke method to raise the event in specified targets.

In any over the network event, we have the following actors:

  • Sender: The client which raises the event (calls the Invoke method).
  • Receiver(s): The client in which the handlers are raised (Invoked upon).
    • Master: The master client.
    • Other Players: All players, except the sender.
    • All Players : All players, including the sender.

Events can be rased using different Target modes:

  1. Invoke for All

    This mode invokes the handlers that are attached to given eventName in all players. This includes the sender as well.

    For example, if player 4 is raising the event, this is how the network would look like.

    The event initiates from player 4, goes to the cloud, and from there propagates to other players.

  2. Invoke for Others

    Similar to Invoke For All but this time the sender won't receive the event.

  3. Invoke for Master

    This mode invokes the handlers in only the Master Client.

  1. Invoke for Local Only

This mode only invokes handler in the sender client (not networked)

Synchronized Transform, Instantiation

The MLSynchronizer component is another way of synchronizing in massive loop. The MLSynchronizer works together with other components or it's Sync Modules to provide synchronization.

Sync modules are used for continuous synchronization of the object properties. Available Sync Modules are:

Synchronizing the Instantiation and Destruction.

when Synchronize Instantiate and Destroy option selected, the MLSynchronizer will attempt to synchronize the instantiation and the destruction of the game object. This works only in instances described bellow:

  • Instantiating a prefab with MLSynchronizer attached on editor.
  • Instantiating a Scene object with MLSynchronizer which added to the scene in editor.
  • Instantiating a Scene object with MLSynchronizer which itself instantiated with the conditions above.

When the above conditions met, it is sufficient to just call

Object.Instantiate(gameobject)

or any of its other arrangements from one client to get object instantiated in all clients in the multiplayer session.

Check out the Instantiate API: Object.Instantiate

Any object with conditions above also synchronously destroyed using

Object.Destroy(gameobject)

Object.Destroy

Ownership

MLSynchronizer can have a Network Ownership state and all game objects which are inherit from its game object will assume that state. The ownership state define which client in the room can write the values to the network. This is specifically important to operation of Sync Modules. The Sync Module which is in the owner client, write its current values to the network, others read and apply the value read.

For example, Lets have an object in the scene with Rigidbody component and Transform Sync Module which is synchronizing its position and rotation. This object will exists in all the user client who are in the room, but only one client can write the position and rotation value of that object to the network. That client is called the owner of that object. By default, scene objects are all owned by the Master client. However actions like grabbing that object or calling RequestOwnership can transfer the ownership of the object to other players.

In our example, this is a critical information that we need to be aware of because actions like applying force, torque or velocity to the Rigidbody, or even moving the object by script will only be effective if applied in the owner client.

There are few way to know the current owner of the object or acquire it.

  1. OnOwnerChanged messaged called when object ownership changed.
MonoBehaviour.OnOwnerChanged(int newOwnerActorNumber);
  1. OnBecomeOwner message called when current client become the object's owner.
MonoBehaviour.OnBecomeOwner();
  1. OnLostOwnership message called when current client lost the ownership of the object.
MonoBehaviour.OnLostOwnership();

here is an example on how to use these messages:

using ML.SDK;
using UnityEngine;

public class Script : MonoBehaviour
{
public void OnOwnerChanged(int newOwnerActorId)
{
MLPlayer newOwner = MassiveLoopRoom.FindPlayerByActorNumber(newOwnerActorId);
}

public void OnBecomeOwner()
{
// this client become the new owner of game object
}

public void OnLostOwnership()
{
// this client lost the owner ship of this game object
}

}

Special cases

  1. When an owner of an object leaves the room, the master client becomes the new network owner of the object.
  2. When the master client leaves the room, all of the object owner by it transfers to the new master client.

Best practices

Performance considerations.

Having a lag-free game and smooth synchronization is key for the best player experience. Every data that needs to be synchronized must be distributed through the network and to all players. When building the worlds, you must conscious of the effects of the synchronizations you incorporate into your worlds.

Here are few recommendations and best practices for managing synchronizations in your game:

Synchronized objects:

Although the easiest method for synchronizing, it is the most costly one in terms of data transfer. The theoretical limit of the number of synchronized objects in a world is 999 but practically, it is recommended to keep the number of synchronized objects as low as possible. Use synchronized objects only if absolutely necessary. You can use synchronized variables, or network events instead of synchronized objects. For example, for a swarm of enemies, you can implements a single script running in the master client and update the location of enemies using events.

Network Events

Network events can be the most efficient way to send information through the network. However, because it is possible to send any amount of data with them, network events may have the potential to be inefficient. The trick is to avoid sending large variables with events.

Here are few other considerations about the events:

  • Avoid raising too many events all at once. This will cause congestion on delivery and may even cause the server to crash. Note: Events are used internally to control other aspects of the game.

  • Send events to proper target. Send an event to the player who needs it. Check receivers on Lua events.

  • Try not to send variables with events. For instance, in the door example above, instead of using the event DoorAction with a bool variable to indicate the state of the door, like as follow

public void Open()
{
// invoke event over network, with true
this.InvokeNetwork("door-action", EventTarget.All, null, true); // one event with variable
}

public void Close()
{
// invoke event over network
this.InvokeNetwork("door-action", EventTarget.All, null, false);
}

we can have the following which each state of the door represented by a separate event:

public void Open()
{
// invoke event over network, with true
this.InvokeNetwork("door-open", EventTarget.All, null);
}

public void Close()
{
// invoke event over network
this.InvokeNetwork("door-close", EventTarget.All, null);
}

Synchronized variables

Any update of any synchronized variable will require this data to be propagated to all players in the room. To increase the efficiency and reduce the network usage, Try synchronizing basic types. Avoid synchronizing strings and arrays.