Skip to content

Multiplayer and Networking⚓︎

Check out quick reference here

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.

flowchart LR;
subgraph Room;
C(("Cloud Server"));
P1[["Player1\nMaster"]];
P2["Player2"];
P3["Player3"];
P4["Player4"];
P5["Player5"];
P6["Player6"];
P7["Player7"];

P1 <--> C;
P2 <--> C;
P3 <--> C;
C <--> P4;
C <--> P5;
C <--> P6;
C <--> P7;
end

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 global state (open or close)

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

do  -- Door 
    local Door = LUA.script;

    -- The state of door. true for open and false for close. 
    local doorState = false;

    -- define that handler function when door state change requested 
    local function onDoorEvent()
        if doorState then
            close();
            doorState = false;
        else 
            open();
            doorState = true;
        end
    end


    function Door.Start()
        -- attach the handler to the event. 
        eventId = LuaEvents.add("DoorEvent", onDoorEvent);  
    end

    function open()
        -- code for openning the door  
        LuaEvents.invokeForAll("DoorAction", true);
    end

    function close()
        -- code for closing the door
        LuaEvents.invokeForAll("DoorAction", false);
    end
end

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:

do  -- Door 
    local Door = LUA.script; 
    -- Make Door state a synchronized variable
    local doorState = SyncVar(Door, "DoorState");

    local function open()
        -- code for openning the door  
    end

    local function close()
        -- code for closing the door
    end

    -- define that handler function when door state change requested by button 
    local function onDoorEvent()
        if doorState.SyncGet() then
            close();
            -- change the sync variable state 
            doorState.SyncSet(false);
        else 
            open();
            -- change the sync variable state
            doorState.SyncSet(true);
        end
    end

    -- to act when the value of the sync variable changed by other clients
    local function OnVarChanged(val)
        if val then
            Open();
        else
            Close();
        end
    end

    function Door.Start()

        -- Check if this is the first client
        if doorState.SyncGet() == nil then
            doorState.SyncSet(false);
        else 
            -- handle the initial state of the door
            if doorState.SyncGet() then
                Open();
            else
                Close();
            end
        end

        -- attach the handler to variable change 
        doorState.OnVariableChange.Add(OnVarChanged);

        -- attach the handler to the event. Will invoked using a UI button. 
        eventId = LuaEvents.Add("DoorEvent", onDoorEvent);  
    end
end

Here, we declare the doorState as a synchronized variable by calling doorState = SyncVar(Door, "DoorState") function. 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.

Local Vs Global Variables⚓︎

Synchronized Variables can be local or global. A local synchronized variables scope is with in the (game object + Lua Script) instance. It means that the a local synchronized variables is different with an other local synchronized variable in an other game object, or script with the same name.

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. The usage of network Lua events is very similar to normal Lua events.

Check out the article on Lua Events

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.

From Lua Events API Reference, we have following four invoke method:

  1. InvokeForAll(eventName)

This method 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.

flowchart LR;
subgraph Room;
C(("Cloud Server"));
P1[["Player1\nMaster"]];
P2["Player2"];
P3["Player3"];
P4["Player4"];
P5["Player5"];
P6["Player6"];
P7["Player7"];

C --> P1;
C --> P2;
C --> P3;
P4 --> C;
P4 --> P4
C --> P5;
C --> P6;
C --> P7;
end
  1. invokeForOthers(eventName)

Similar to invokeForAll() but this time the sender won't receive the event.

flowchart LR;
subgraph Room;
C(("Cloud Server"));
P1[["Player1\nMaster"]];
P2["Player2"];
P3["Player3"];
P4["Player4"];
P5["Player5"];
P6["Player6"];
P7["Player7"];

C --> P1;
C --> P2;
C --> P3;
P4 --> C;
C --> P5;
C --> P6;
C --> P7;
end
  1. invokeForMaster(eventName)

This method invokes the handlers in Master Client and the sender.

flowchart LR;
subgraph Room;
C(("Cloud Server"));
P1[["Player1\nMaster"]];
P2["Player2"];
P3["Player3"];
P4["Player4"];
P5["Player5"];
P6["Player6"];
P7["Player7"];
C --> P1;
C -.- P2;
C -.- P3;
P4 --> C;
P4 --> P4;
C -.- P5;
C -.- P6;
C -.- P7;
end
  1. invokeForMasterOnly(eventName)

This method invokes the handler only in the master client. Excluding the sender.

flowchart LR;
subgraph Room;
C(("Cloud Server"));
P1[["Player1\nMaster"]];
P2["Player2"];
P3["Player3"];
P4["Player4"];
P5["Player5"];
P6["Player6"];
P7["Player7"];
C --> P1;
C -.- P2;
C -.- P3;
P4 --> C;
C -.- P5;
C -.- P6;
C -.- P7;
end

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. The Gameobject.owner property. This property will return a MLPlayer owner of the object if the game object or its parents have an MLSynchronizer with a Network Ownership state.
  2. The Gameobject.RequestOwnership() method. You can call this method to transfer the ownership of the object to current player.
  3. The OnOwnerChanged callback. This callback called when the owner of the object is changed.
  4. The OnBecomeOwner callback. This callback called when this client becomes the network owner of the object.
  5. The OnLostOwnership callback. This callback called when this client loses the ownership status of the 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

function open()
    -- code for openning the door  
    LuaEvents.InvokeForAll("DoorAction", true); -- one event with variable 
end

function close()
    -- code for closing the door
    LuaEvents.InvokeForAll("DoorAction", false);
end

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

function open()
    -- code for opening the door  
    LuaEvents.InvokeForAll("DoorActionOpen"); -- event which opens the door
end

function close()
    -- code for closing the door
    LuaEvents.InvokeForAll("DoorActionClose"); -- event which closes the door
end    

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.