Multiplayer - Code

[SOCL-Multiplayer-03]

Here is the High-level Sequence for Beamable Multiplayer.

See Multiplayer - Overview » Glossary for more info on terminology and parameters.

For a complete Beamable Multiplayer code example, see the Example below.

API

Create Multiplayer Session
Each game client connects this way. No one client has special status of 'host'. Depending on the needs of the game, the SimClient session may stay from the moment any one player joins until the moment the last player leaves.

_simClient = new SimClient(new SimNetworkEventStream(MatchId, BeamContext.Default.ServiceProvider), FramesPerSecond, TargetNetworkLead);

Create Custom Event
Depending on the complexity of the game play, there may be dozens of custom events.

public class MyPlayerMoveEvent
{
     public Vector3 Position;
     public MyPlayerMoveEvent(Vector3 position)
     {
          Position = position;
     }
}

Subscribe To Event
The client must repeat this for each PlayerId of interest.

_simClient.On<MyPlayerMoveEvent>(MyPlayerMoveEvent.Name, _localPlayerDbid.ToString(),
     SimClient_OnMyPlayerMoveEvent);

Send Event
Typically each player's input is converted into an event.

_simClient.SendEvent(MyPlayerMoveEvent.Name,
     new MyPlayerMoveEvent(_localPlayerDbid, new Vector3(0, 0, 0)));

Handle Event
Typically for a fair and consistent game play experience, best practices dictate that all events, even the local clients own player's events are sent to the server, later received, and then converted to rendered graphics and sounds. This way the requisite latency is the same for the local client as it is for all other clients in the same match.

private void SimClient_OnMyPlayerMoveEvent(MyPlayerMoveEvent myPlayerMoveEvent)
{
     Debug.Log($"The player moved to the position of {myPlayerMoveEvent.Position}.");
}

Example

Code

📘

Beamable SDK Examples

The following example code is available for download at GitHub.com/Beamable_SDK_Examples

Here is a complete Beamable Multiplayer example which creates a game session and sends/receives game events.

using System.Collections.Generic;
using Beamable.Experimental.Api.Sim;
using UnityEngine;
using UnityEngine.Events;

namespace Beamable.Examples.Services.Multiplayer
{
   [System.Serializable]
   public class MultiplayerExampleDataEvent : UnityEvent<MultiplayerExampleData> { }

   public enum SessionState
   {
      None,
      Initializing,
      Initialized,
      Connected,
      Disconnected
   }
   
   /// <summary>
   /// Holds data for use in the <see cref="MultiplayerExample"/>.
   /// </summary>
   [System.Serializable]
   public class MultiplayerExampleData
   {
      public string MatchId = null;
      public string SessionSeed;
      public long CurrentFrame;
      public long LocalPlayerDbid;
      public bool IsSessionConnected { get { return SessionState == SessionState.Connected; }}
      public SessionState SessionState = SessionState.None;
      public List<string> PlayerMoveLogs = new List<string>();
      public List<string> PlayerDbids = new List<string>();
   }
   
   /// <summary>
   /// Defines a simple type of in-game "move" sent by a player
   /// </summary>
   public class MyPlayerMoveEvent
   {
      public static string Name = "MyPlayerMoveEvent";
      public long PlayerDbid;
      public Vector3 Position;

      public MyPlayerMoveEvent(long playerDbid, Vector3 position)
      {
         PlayerDbid = playerDbid;
         Position = position;
      }

      public override string ToString()
      {
         return $"[MyPlayerMoveEvent({Position})]";
      }
   }
   
    /// <summary>
    /// Demonstrates <see cref="SimClient"/>.
    /// </summary>
    public class MultiplayerExample : MonoBehaviour
    {
       //  Events  ---------------------------------------
       [HideInInspector]
       public MultiplayerExampleDataEvent OnRefreshed = new MultiplayerExampleDataEvent();
       
        //  Fields  ---------------------------------------
       
        private MultiplayerExampleData _multiplayerExampleData = new MultiplayerExampleData();
        private const long FramesPerSecond = 20;
        private const long TargetNetworkLead = 4;
        private SimClient _simClient;
        private BeamContext _beamContext;
        
        //  Unity Methods  --------------------------------
        protected void Start()
        {
           Debug.Log($"Start() Instructions...\n\n" + 
                     " * Run The Scene\n" + 
                     " * Press 'Start Multiplayer'\n" + 
                     " * Press 'Send Player Move'\n" + 
                     " * See results in the in-game UI\n");

            SetupBeamable();
        }
        
        
        protected void Update()
        {
           if (_simClient != null) 
           { 
              _simClient.Update(); 
           }

           string refreshString = "";
           refreshString += $"MatchId: {_multiplayerExampleData.MatchId}\n";
           refreshString += $"Seed: {_multiplayerExampleData.SessionSeed}\n";
           refreshString += $"Frame: {_multiplayerExampleData.CurrentFrame}\n";
           refreshString += $"Dbids:";
           foreach (var dbid in _multiplayerExampleData.PlayerDbids)
           {
              refreshString += $"{dbid},";
           }

           //Debug.Log($"message:{refreshString}");
        }

        
        protected void OnDestroy()
        {
           if (_simClient != null)
           {
              StopMultiplayer();
           }
        }
        
        
        //  Methods  --------------------------------------
        private async void SetupBeamable()
        {
           _beamContext = BeamContext.Default;
           await _beamContext.OnReady;

            Debug.Log($"_beamContext.PlayerId = {_beamContext.PlayerId}");
            
            // Access Local Player Information
            _multiplayerExampleData.LocalPlayerDbid = _beamContext.PlayerId;

        }
        
        public void StartMultiplayer()
        {
           if (_simClient != null)
           {
              StopMultiplayer();
           }
           
           _multiplayerExampleData.SessionState = SessionState.Initializing;
           Refresh();
           
           // Generates a specific MatchId
           // (Otherwise use Beamable's MatchmakingService)
           _multiplayerExampleData.MatchId = "MyCustomMatchId_" + UnityEngine.Random.Range(0,99999);
           
           // Create Multiplayer Session
           _simClient = new SimClient(
              new SimNetworkEventStream(_multiplayerExampleData.MatchId, _beamContext.ServiceProvider), 
              FramesPerSecond, TargetNetworkLead);
        
           // Handle Common Events
           _simClient.OnInit(SimClient_OnInit);
           _simClient.OnConnect(SimClient_OnConnect);
           _simClient.OnDisconnect(SimClient_OnDisconnect);
           _simClient.OnTick(SimClient_OnTick);
        }

        public void StopMultiplayer()
        {
           if (_simClient != null)
           {
              // TODO: Manually Disconnect/close?
              _simClient = null;
           }

           _multiplayerExampleData.SessionState = SessionState.Disconnected;
           Refresh();
        }

        public void SendPlayerMoveButton()
        {
           if (_simClient == null)
           {
              return;
           }
           
           // Create a mock  player position
           Vector3 position = new Vector3(
              Random.Range(1, 10),
              Random.Range(1, 10),
              Random.Range(1, 10)
              );
           
           _simClient.SendEvent(MyPlayerMoveEvent.Name,
              new MyPlayerMoveEvent(_multiplayerExampleData.LocalPlayerDbid, position));
        }
        
        public void Refresh()
        {
           string refreshLog = $"Refresh() ...\n" +
                               $"\n * LocalPlayerDbid = {_multiplayerExampleData.LocalPlayerDbid}\n\n" +
                               $"\n * PlayerDbids.Count = {_multiplayerExampleData.PlayerDbids.Count}\n\n" +
                               $"\n * PlayerMoveLogs.Count = {_multiplayerExampleData.PlayerMoveLogs.Count}\n\n";
            
           //Debug.Log(refreshLog);

           OnRefreshed?.Invoke(_multiplayerExampleData);
        }

        
        //  Event Handlers  -------------------------------
        private void SimClient_OnInit(string sessionSeed)
        {
           _multiplayerExampleData.SessionState = SessionState.Initialized;
           _multiplayerExampleData.SessionSeed = sessionSeed;
           Refresh();
           
           Debug.Log($"SimClient_OnInit()...\n" + 
                     $"MatchId = {_multiplayerExampleData.MatchId}, " +
                     $"sessionSeed = {sessionSeed}");
        }

        
        private void SimClient_OnConnect(string dbid)
        {
           _multiplayerExampleData.SessionState = SessionState.Connected;
           _multiplayerExampleData.PlayerDbids.Add(dbid);
           Refresh();
        
           // Handle Custom Events for EACH dbid
           _simClient.On<MyPlayerMoveEvent>(MyPlayerMoveEvent.Name, dbid,
              SimClient_OnMyPlayerMoveEvent);
        
           Debug.Log($"SimClient_OnConnect() dbid = {dbid}");
        }

        
        private void SimClient_OnDisconnect(string dbid)
        {
           if (long.Parse(dbid) == _multiplayerExampleData.LocalPlayerDbid)
           {
              StopMultiplayer();
           }
           
           _multiplayerExampleData.PlayerDbids.Remove(dbid);
           Refresh();
           
           Debug.Log($"SimClient_OnDisconnect() dbid = {dbid}");
        }

        
        private void SimClient_OnTick(long currentFrame)
        {
           _multiplayerExampleData.CurrentFrame = currentFrame;
           Refresh();
        }

        
        private void SimClient_OnMyPlayerMoveEvent(MyPlayerMoveEvent myPlayerMoveEvent)
        {
           string playerMoveLog = $"{myPlayerMoveEvent}";
           _multiplayerExampleData.PlayerMoveLogs.Add(playerMoveLog);
           Refresh();
        }
    }
}

❗️

Experimental API

Please note that this feature currently includes an experimental API. Game makers will have to replace any experimental namespace when the finalized version is released.

Advanced

This section contains any advanced configuration options and workflows.

Handling Network Faults

It is possible that temporary network outages may occur. While these errors are occurring, player communication with the Beamable Game Relay is blocked, and no messages will be sent or received. When network connectivity resumes, the session will resume sending and receiving events.

Errors can be caught using callbacks available on the SimClient instance. When a network outage event occurs, the OnErrorStarted event will always trigger first. Later, when the outage has recovered, the OnErrorRecovered event will trigger. However, if the outage is too long, or if there is a different unrecoverable error, the OnErrorFailed event will trigger.

// client is a SimClient instance

client.OnErrorStarted += (e) =>
{
    // TODO: show "connecting to server error message"
    Debug.Log($"Sim Client Errors Starting... {r.ErrorMessage}");
};
client.OnErrorRecovered += (r) =>
{
    // TODO: remove any error UI and resume the game
    Debug.Log($"Sim Client disaster averted... ");
};
client.OnErrorFailed += r =>
{
    // TODO: force exit the match, because the network is too unstable to continue or recover.
    Debug.Log($"Sim Client wasn't able to recover. {r.ErrorMessage}" );
};

The effect of OnErrorFailed event can also be captured by using a try/catch around the SimClient.Update function, like so...

try
{
    client.Update();
}
catch (SimNetworkErrorException ex)
{
    // TODO: destroy the client- start over.
    Debug.LogException(ex);
}

In the event that a network outage is unrecoverable, the entire SimNetworkEventStream instance needs to be reconstructed. Any further calls to SimClient.Update will re-throw the SimNetworkErrorException.

Custom Fault Tolerance

It is possible to inject custom network outage fault tolerance logic. Use Custom Dependency Injection to inject a new instance of ISimFaultHandler, or pass in a custom instance of ISimFaultHandler as an optional parameter to the SimNetworkEventStream.

The default implementation of the ISimFaultHandler is the DefaultSimFaultHandler.

📘

Change Default Fault Tolerance Duration

By default, the DefaultSimFaultHandler instance will allow for 15 seconds of network outage before reporting an unrecoverable error. This duration can be adjusted by passing in a custom instance of the DefaultSimFaultHandler to the SimNetworkEventStream.

var stream = new SimNetworkEventStream(roomId, ctx.ServiceProvider, new DefaultSimFaultHandler
{
    // use a 4 second duration instead of 15.
    MaxFaultyDurationInSeconds = 4
});

Randomization and Determinism

In single-player game design, game makers may freely use randomization libraries to add variety to game play. The environment may be randomized, the enemy amount and variety can change every game session. This adds richness and replayability to the game.

In a multiplayer game its important that all players in a given game session have the SAME "random" experience. But how?

Randomization is supported by Beamable's Multiplayer with the following advice. During initialization the Beamable SimClient will pass along the same seed value to feed the random operations on each client. By using the same seed, each game client will be in-sync.

Below is a partial code snippet. Download the project to see the complete code.

private void SimClient_OnInit(string sessionSeed)
{
   int sessionSeedInt = int.Parse(sessionSeed);

   // Store this as a private variable and use it for
   // all randomization calls during game play
   System.Random random = new System.Random(sessionSeedInt);
}