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 theDefaultSimFaultHandler
to theSimNetworkEventStream
.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);
}
Updated about 1 year ago