Skip to content

Unity Cluster Advanced

Architecture#

Sequence diagram#

Here's the sequence diagram for MiddleVR:

Synchronize custom data#

MiddleVR provides an easy way to synchronize any type of data across the cluster using messages.

The two methods to use are Cluster.BroadcastMessage and Cluster.AddMessageHandler.

Cluster.BroadcastMessage#

Cluster.BroadcastMessage will send a message to all cluster nodes, server and clients. The message broadcast is not immediate, it will actually be sent during the next Synchronization point.

Example:

Imagine you have a Button that calls OnButtonPress when pressed:

Without cluster, this is what your actions would look like:

With cluster, it would look like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MiddleVR;
using MiddleVR.Unity;

public class MVRClusterGUISample : MonoBehaviour
{
    void Start()
    {
        Cluster.AddMessageHandler<bool>(this, ClusterOnButtonPress, channel: 1);
    }

    public void OnButtonPress()
    {
        Debug.Log("OnButtonPress");
        Cluster.BroadcastMessage(this, true, channel: 1);
    }

    private void ClusterOnButtonPress(bool iBool)
    {
        // Actually open door on all cluster nodes

        // Note: This will also be called even if the configuration
        // is not a cluster configuration

        Debug.Log("ClusterOnButtonPress");
        Debug.Log("OpenDoor!");
    }
}

MiddleVR/Runtime/SampleScripts/MVRClusterGUISample.cs

Note: The message handlers (ClusterOnButtonPress in our example) are also be called even if the configuration is not a cluster configuration. This allows your applications to work the same if they are not in cluster.

Data types#

Cluster.BroadcastMessage can broadcast messages containing any C# unmanaged type. This means simple types like int, float, bool, enums and structs containing fields of unmanaged types only.

It can also broadcast messages containing strings and array of bytes.

private struct SynchronizedStruct
{
    public Vector3 Position;
    public Quaternion Rotation;
}

private void BroadcastStruct()
{
    if (Cluster.IsServer)
    {
        SynchronizedStruct syncStruct = new SynchronizedStruct
        {
            Position = transform.position,
            Rotation = transform.rotation
        };

        MVRTools.Log(2, $"[ ] Server setting struct to: pos={syncStruct.Position}, rot={syncStruct.Rotation}");

        Cluster.BroadcastMessage(this, syncStruct, channel: 3);
    }
}
public byte[] SyncArray = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };

private void BroadcastByteArray()
{
    if (Cluster.IsServer)
    {
        MVRTools.Log(2, $"[ ] Server setting array to: {BitConverter.ToString(SyncArray)}");
        Cluster.BroadcastMessage(this, SyncArray, channel: MessageChannel.MyByteArray);
    }
}

If you want to send more complex types, you will have to serialize/deserialize them into one of the supported basic types like string (JSon for example) or byte array.

Warning: One notable limitation is that you can't share a struct that contains a string! You need to serialize the struct manually.

Channel#

Messages are broadcast on a channel. You can only have one message handler per type per channel. This means that if you want to broadcast different messages with the same type (like a Vector3 for a position and another Vector3 for an acceleration), you must broadcast them on different channels.

The simplest way to do is simply to use one channel per message. But you could use the same channel for multiple messages as long as they are of different types.

Each game object can have its own channel list. This means that two game objects can broadcast a message of a different type on the same channel.

Message handler#

Use Cluster.AddMessageHandler to register a method to be called when the cluster node receives a message. Make sure to use the same type and channel you are using with Cluster.BroadcastMessage.

Example:

private void Start()
{
    Cluster.AddMessageHandler<float>(this, OnSynchronizedFloat, channel: 1);
}

private void OnSynchronizedFloat(float iFloat)
{
    if (Cluster.IsServer)
        return;

    MVRTools.Log(2, $"[ ] Client getting float: {iFloat}");
}

The message handler needs to be in a script that derives off of a MonoBehaviour. The message handler does not need to be in the same script.

Example: Synchronize the transform of a GameObject#

This sample can be found in the MiddleVR package: MiddleVR/Runtime/MiddleVR.Unity.Scripts/Samples/MVRShareTransform

using MiddleVR.Unity;
using UnityEngine;

public class MVRShareTransform : MonoBehaviour
{
    private struct SynchronizedState
    {
        public Vector3 Position;
        public Quaternion Rotation;
    }

    private void OnSynchronizedState(SynchronizedState state)
    {
        if (Cluster.IsServer)
            return;

        transform.SetPositionAndRotation(state.Position, state.Rotation);
    }

    private void Start()
    {
        Cluster.AddMessageHandler<SynchronizedState>(this, OnSynchronizedState);
    }

    // On the server, synchronize a SynchronizedState every update
    // On all nodes, OnSynchronizedState will be called the next time there is a synchronization update :
    // either during VRManagerScript.Update() or VRManagerPostFrame.Update() (see script ordering)
    private void Update()
    {
        if (Cluster.IsServer)
        {
            Cluster.BroadcastMessage(this,
                new SynchronizedState
                {
                    Position = transform.position,
                    Rotation = transform.rotation
                });
        }
    }
}

Synchronization point#

The message will be transmitted to all cluster nodes (server and clients) in the next network synchronization point (MVRManagerScript or MVRManagerPostFrameScript). See Sequence Diagram.

You need to make sure that all the actions that depend on a shared value are done after this synchro point or you will have different values on the server and client

This means that if your sharing script (the one with the Cluster.BroadcastMessage) has a default script ordering, it will be called after VRManagerScript. This means that the message handlers will be called when the MVRManager runs MVRManagerPostFrameScript. Your actions that depend on the correct synchronization of this value should then be called after the MVRManagerPostFrameScript.

If your script executes before MVRManagerScript, the message handlers will be called when the MVRManager runs MVRManagerScript.

If your script executes after MVRManagerPostFrameScript, the message handlers methods will be called on the next frame when the MVRManager runs MVRManagerScript.

Example#

The sample script in MiddleVR\Scripts\Samples\MVRClusterShareValuesSample demonstrates it:

using System;
using System.Collections.Generic;
using MiddleVR;
using MiddleVR.Unity;
using UnityEngine;

public class MVRClusterShareValuesSample : MonoBehaviour
{
    private void Start()
    {
        Cluster.AddMessageHandler<float>(this, OnSynchronizedFloat, channel: 1);
        Cluster.AddMessageHandler<Vector3>(this, OnSynchronizedVector3, channel: 2);
        Cluster.AddMessageHandler<SynchronizedStruct>(this, OnSynchronizedStruct, channel: 3);
        Cluster.AddMessageHandler(this, OnSynchronizedByteArray, channel: 4);
        Cluster.AddMessageHandler(this, OnSynchronizedString, channel: 5);
    }

    // On the server, synchronize a SynchronizedState every update
    // On all nodes, OnSynchronizedState will be called the next time there is a synchronization update :
    // either during VRManagerScript.Update() or VRManagerPostFrame.Update() (see script ordering)
    private void Update()
    {
        MVRTools.Log(2, $"[ ] Frame {MVR.Kernel.GetFrame()}");

        BroadcastFloat();
        BroadcastVector3();
        BroadcastStruct();
        BroadcastByteArray();
        BroadcastString();
    }

    #region SampleSynchronizeFloat
    public float SyncFloat = 0.0f;

    private void BroadcastFloat()
    {
        if (Cluster.IsServer)
        {
            SyncFloat = UnityEngine.Random.Range(0, 1000);
            MVRTools.Log(2, $"[ ] Server setting float to: {SyncFloat}");
            Cluster.BroadcastMessage(this, SyncFloat, channel: 1);
        }
    }

    private void OnSynchronizedFloat(float iFloat)
    {
        if (Cluster.IsServer)
            return;

        SyncFloat = iFloat;
        MVRTools.Log(2, $"[ ] Client getting float: {SyncFloat}");
    }
    #endregion

    #region SampleSynchronizeVector3
    public Vector3 SyncVector3 = new Vector3(0.0f, 0.0f, 0.0f);

    private void BroadcastVector3()
    {
        if (Cluster.IsServer)
        {
            SyncVector3 = UnityEngine.Random.insideUnitSphere;
            MVRTools.Log(2, $"[ ] Server setting vector3 to: {SyncVector3}");
            Cluster.BroadcastMessage(this, SyncVector3, channel: 2);
        }
    }

    private void OnSynchronizedVector3(Vector3 iVec)
    {
        if (Cluster.IsServer)
            return;

        SyncVector3 = iVec;
        MVRTools.Log(2, $"[ ] Client getting vector3: {SyncVector3}");
    }
    #endregion

    #region SampleSynchronizeStruct
    private struct SynchronizedStruct
    {
        public Vector3 Position;
        public Quaternion Rotation;
    }

    private void BroadcastStruct()
    {
        if (Cluster.IsServer)
        {
            SynchronizedStruct syncStruct = new SynchronizedStruct
            {
                Position = transform.position,
                Rotation = transform.rotation
            };

            MVRTools.Log(2, $"[ ] Server setting struct to: pos={syncStruct.Position}, rot={syncStruct.Rotation}");

            Cluster.BroadcastMessage(this, syncStruct, channel: 3);
        }
    }

    private void OnSynchronizedStruct(SynchronizedStruct iSyncStruct)
    {
        if (Cluster.IsServer)
            return;

        transform.SetPositionAndRotation(iSyncStruct.Position, iSyncStruct.Rotation);

        MVRTools.Log(2, $"[ ] Client getting struct: pos={iSyncStruct.Position}, rot={iSyncStruct.Rotation}");
    }
    #endregion

    #region SampleSynchronizeByteArray
    public byte[] SyncArray = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };

    private void BroadcastByteArray()
    {
        if (Cluster.IsServer)
        {
            for (int i = 0; i < SyncArray.Length; i++)
            {
                SyncArray[i] = (byte)UnityEngine.Random.Range(0, 0x0F);
            }

            MVRTools.Log(2, $"[ ] Server setting array to: {BitConverter.ToString(SyncArray)}");
            Cluster.BroadcastMessage(this, SyncArray, channel: 4);
        }
    }

    private void OnSynchronizedByteArray(byte[] iArray)
    {
        if (Cluster.IsServer)
            return;

        SyncArray = iArray;
        MVRTools.Log(2, $"[ ] Client getting byte array {BitConverter.ToString(SyncArray)}.");
    }
    #endregion

    #region SampleSynchronizeString
    public string SyncString;

    private void BroadcastString()
    {
        if (Cluster.IsServer)
        {
            // Randomly choose a string
            var list = new List<string> { "Buddy you're a boy", "make a big noise", "playin' in the street", "going to be a big man some day" };
            var rnd = new System.Random();

            SyncString = list[rnd.Next(0, list.Count)];
            MVRTools.Log(2, $"[ ] Server setting string to: {SyncString}");
            Cluster.BroadcastMessage(this, SyncString, channel: 5);
        }
    }

    private void OnSynchronizedString(string iString)
    {
        if (Cluster.IsServer)
            return;

        SyncString = iString;
        MVRTools.Log(2, $"[ ] Client getting string: {SyncString}");
    }
    #endregion
}

Send events#

Sending an event is exactly like broadcasting a value, but you can do it only when a particular event occurs.

The sample script in MiddleVR\Scripts\Samples\MVRClusterBroadcastEventsSample demonstrates it:

    private void Start()
    {
        Cluster.AddMessageHandler<bool>(this, OnRandomEvent, channel: 0);
        Cluster.AddMessageHandler<int>(this, OnPlayerConnected, channel: 1);
        Cluster.AddMessageHandler(this, OnDownloadCompleted, channel: 2);
    }

    private void Update()
    {
        // This is where you would test for a random event
        bool randomEvent = true;
        if( randomEvent ) { Cluster.BroadcastMessage(this, true, channel: 0); }

        // This is where you would test for a player connection
        bool playerConnected = true;
        int playerId = 1;
        if (playerConnected) { Cluster.BroadcastMessage(this, playerId, channel: 1); }

        // This is where you would test for a completed download and 
        bool downloadCompleted = true;
        byte[] downloadedData = new byte[1024];
        if (downloadCompleted) { Cluster.BroadcastMessage(this, downloadedData, channel: 2); }
    }

    private void OnRandomEvent(bool iBool)
    {
        if (Cluster.IsServer)
            return;

        MVRTools.Log(2, $"[ ] Client getting random event: {iBool}");
    }

    private void OnPlayerConnected(int iId)
    {
        if (Cluster.IsServer)
            return;

        MVRTools.Log(2, $"[ ] Client getting player connection event, id: {iId}");
    }

    private void OnDownloadCompleted(byte[] iArray)
    {
        if (Cluster.IsServer)
            return;

        MVRTools.Log(2, $"[ ] Client getting byte array {BitConverter.ToString(iArray)}.");
    }

Troubleshoot#

My sharing code is not executing in Unity Editor#

Cluster.IsServer is only true when running a cluster configuration. Make sure to use a real cluster configuration.

Data is correctly sent from the server but nothing is received on the client#

Look in the logs Unity_Player.txt. If you see:

ArgumentOutOfRangeException: Cluster: No handler registered for channel. You must register a handler before attempting to synchronize a message. Parameter name: channel

Make sure you correctly called Cluster.AddMessageHandler on the same channel and with the same type as BroadcastMessage.

Send values from a cluster client to the cluster server#

In the same way, you can send data from any cluster client to only the cluster server:

    private void Start()
    {
        // Only the server will handle those messages
        Cluster.AddClientMessageHandler(this, OnClientMessageReceived, channel: 0);
    }

    private void Update()
    {
        if (MVR.ClusterMgr.IsClient()) Cluster.SendAsyncMessageToServer(this, "Hello from '" + MVR.ClusterMgr.GetMyClusterClient().GetName() + "' !");
    }

    private void OnClientMessageReceived(int clientId, string message)
    {
        MVRTools.Log(2, $"[ ] Client message {message} from client id {clientId}, clent name = {MVR.ClusterMgr.Clients[clientId].GetName()}");
    }

When to synchronize ?#

Again the big idea is to make sure that all cluster nodes are perfectly synchronized at each frame.

The MVRManager can synchronize data at two different points in a frame:

  • On MVRManagerScript update

  • On MVRManagerPostFrameScript update

See Sequence Diagram.

The easiest way to choose is to:

  • share causes before MVRManagerScript update,

  • share consequences before MVRManagerPostFrameScript.

Note: It is important to remember that all MiddleVR's devices and nodes positions will be updated and synchronized in the MVRManagerScript update.

Share causes before MVRManagerScript#

Sharing a cause before MVRManagerScript first implies that your script executes with a script ordering lower than "-100".

Then you will probably be using the Cluster.BroadcastMessage mechanism.

If you properly execute your script before the MVRManagerScript, your message will actually be transmitted when MVRManagerScript executes.

You can then have your scripts react to those causes after the MVRManagerScript updates. By default all your scripts execute after the MVRManagerScript Update unless you change its script ordering.

Share consequences before MVRManagerPostFrameScript#

You can share consequences either with the MVRClusterObject or Cluster.BroadcastMessage.

Make sure to execute your BroadcastMessage before MVRManagerPostFrameScript. The MVRClusterObjects will automatically be synchronized during MVRManagerPostFrameScript.