Multiplayer

Allow game makers to create multi-user experiences

Overview

The Beamable Multiplayer feature allows game makers to create real-time and turn-based multi-user game experiences.

Support turn-based gameplay with seamless integration with rewards, matchmaking, and leaderboards without the need to build, run, and scale a game server.

What is a game server? A game server is the source of event coordination in a multiplayer video game. The server transmits enough events about its internal state to allow each connected client to maintain their own accurate version of the game. Events may contain various types of information including; properties about the game world, the players, and player input. See Wikipedia for more info.

Here are leading types of game servers;

Relay Server

The server sends and receives events between all clients in a match. The match is the "Room" in which all the game players interact. Each client is the game.

Ideal For: Real-time strategy, tower defense, MOBAs, card battlers, auto chess, and more...

Dedicated Server

The server processes game-specific logic. The server is the game.

Ideal For: Persistent world games, MMOs, and first-person shooters.

Peer-To-Peer Server

The server introduces client to client. Key communication passes directly from client to client.

Ideal For: First-person shooters and games that can tolerate cheating.

Here is a comparison of various game servers;

Default Benefits

Relay Server

Dedicated Server

Peer-To-Peer Server

Lower Bandwidth

✔️

Simpler Server Infrastructure & Simpler Setup

✔️

Client-Authoritative

✔️

✔️

Lower Cost

✔️

Higher Security

✔️ (Determinism)

✔️ (Server-Authoritative)

Lower Latency

✔️

✔️

✔️

What is Beamable Multiplayer?

The Beamable Multiplayer API is lean and straightforward to setup. The pipeline and workflow for development are ready out-of-the-box on day 1 of development, allowing game makers to focus on game-specific client logic.

Beamable Multiplayer uses relay server technology for rapid game development.

Beamable's Multiplayer is a relay server. What exactly is a relay server? Essentially, Beamable keeps a list of all events sent from all clients in a given match. Then, via push or pull, each client can ask "for all events since last time I asked". The clients then MUST advance the game play deterministically, replaying each player’s input, so each client is in sync.

Plus, with Beamable Multiplayer, the game logic can access the full suite of Beamable functionality including Admin Flow, Cloud Save, Inventory Flow, and more!

API

High-level Sequence for Beamable Multiplayer

For more information on relevant parameters, see the Glossary below.

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

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), 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 DBIDDBID - The database identification. Beamable generates an anonymous account for the player when the project first runs 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}.");
}

Content

The content type related to Matchmaking is the SimGameType. It defines the parameters for the match to be created.

See Matchmaking (Content) for more info.

Steps

Follow these steps to get started.

Note: Step 2 and step 3 are the bulk of the development effort. The details depend on the specifics of the game project.

Step

Detail

  1. Setup Beamable

• See Step 1 - Getting Started

  1. Create C# multiplayer-specific logic

• Access Local Player Information
• Create Multiplayer Session
• Handle Events
• Send Events

  1. Create C# game-specific logic

• Convert input to events
• Gracefully handle network latency
• Convert events into graphics and audio rendering
• Handle game logic (e.g. win/loss conditions)

Code

📘

Beamable SDK Examples

This and all examples are 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;
        
        //  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()
        {
            var beamableAPI = await Beamable.API.Instance;

            Debug.Log($"beamableAPI.User.id = {beamableAPI.User.id}");
            
            // Access Local Player Information
            _multiplayerExampleData.LocalPlayerDbid = beamableAPI.User.id;

        }
        
        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), 
              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();
        }
    }
}

Game Maker User Experience

When setup properly, the user experience in the game project will be as follows.

A common workflow during development of multiplayer games is to "Play Against Yourself" by spawning multiple instances of the same game while in active development. With Beamable Multiplayer this process is straightforward.

Advanced

This section contains any advanced configuration options and workflows.

Glossary

Here are some common terms.

Name

Detail

Client

This refers to the instance of the game code that is running on each player's local device (e.g. Mobile Phone)

Event

The data packet that represents a player's turn in the game. This may be any serializable C# structure.

Latency

This measurement indicates the average total time needed for the game client to send an event to the Multiplayer server and receive the reply. Latency is measured in milliseconds (e.g. 200ms)

Matchmaking

This is the process of choosing a MatchId based on criteria. E.g. "Give me a match to play in with 3 total players with beginner skill level". Beamable includes an optional, light-weight Matchmaking service.

SimClient

This class is the main entry point for Beamable's Multiplayer feature.

SimClient's
TargetNetworkLead

This is network buffer. It represents how long the Multiplayer server holds game events before sending them back to clients. E.g. With a value of 5, the Multiplayer server will hold 5 events before sending those 5 to the game client's. A higher value provides more consistency to the clients' rendering experience, but at the cost of higher latency.

SimClient's
Match

This represents a set of players playing a specific instance of the game together. Only players in the same match may collaborate, compete, and communicate.

SimClient's
FramesPerSecond

This is the rate (in times per second) at which the client receives event updates from the Multiplayer server. As a data optimization, typically this is set lower than the rendering frame rate of Unity.

Server

This refers to the instance of the server code that sits between the game's clients and manages distribution of the game events.

Plan the Multiplayer Game Design

Multiplayer game development offers additional challenges beyond those of a typical single player game.

When designing a game with Beamable’s Multiplayer FeatureFeature - An individual aspect of the Beamable product used to create a great user experience, it is important to plan which user interactions will be sent to the servers.

Step

Detail

  1. Define the game's high level goals

• What are player motivations?
• How is the 'story' of the player told through audio, graphics, animations, etc ... ?

Note: If you are new to Multiplayer game design, this is a good place to start. Think about the game as if it was indeed a single-player experience. Then iterate on the design in the following steps

  1. Consider game input from the users

• What is each user input gesture?
• What are all possible choices the player may make at that moment?

  1. Consider game input from other sources

• What else impacts the user experience?
• Are any elements 'random'? How will that be determined, sent, received, and processed by each client?
• Where will delays be required? The game need to wait at key moments for animations to finish, sounds to finish, etc ...

  1. Design the structure of the Multiplayer event objects

• How many event objects are needed?
• What info is needed in each object?

Note: A solid, purposeful design in this step is important to create a pleasing multiplayer game experience. See The Balancing Act below for more info

These user interactions are relayed as event objects to all connected clients. Each client will deterministically simulate those events to ensure a consistent user experience for all players.

Designing an event-driven, deterministic simulation is vital.

The Balancing Act

Deciding which user interactions require events and how to design the event payloads, striking a balance is recommended.

  • Too Few Events: Each game requires a certain amount of events to faithfully and deterministically keep all players in sync. Sending too few will cause the game to fall out of sync or lack the polish expected by the game community. The same is true if each event contain too little data to represent the needs of the game design.
  • Too Many Events: Each event requires reasonable overhead for serializing, sending, receiving, and processing data. Sending too many can cause latency and the players will notice lag in the user experience. The same is true if the events contain unnecessarily heavy data within each event.

📘

Expert Advice

Here is more info from Beamable's development team.

"Determinism of the game simulation is important due to the nature of our server implementation. Because Beamable's relay servers are game state agnostic, any sort of state checkpointing or CPU architectural differences are not corrected on the server side so in order to keep the game state in sync on all clients, it is imperative that the game be made deterministic. "

• See Ruoyu Sun's Game Networking Article for more info

Game Security

To be more secure against any malicious hackers in your multiplayer game’s community, consider never sending any game object state (player’s heath, power of weapon, etc) as part of the messages to be synchronized over the network.

Any enterprising hacker can “easily” hack a game client to send inflated values which will only make the game worse for everyone. If all a player can do is send an “action”. i.e. “What I did” (“I fired my equipped gun” rather than “I hit player 2 for 100 damage”), it becomes quite a bit harder for someone to cheat.

A hacker may also attempt to replay actions AGAIN for a particular turn or to send malformed game event object messages that can lead to a worst experience for other players.

Matchmaking

In multiplayer gaming, matchmaking is the process of choosing a MatchId based on criteria (e.g. "Give me a match to play in with 2 total players of any skill level"). Beamable supports matchmaking through its matchmaking service.

See Matchmaking for more info.

Playing "Against Yourself"

One of the challenges of developing a multiplayer game is testing it frequently as an individual developer. Finding a friend to play against you every time you run the game in development is prohibitive.

Here are 2 strategies to help the development process.

1. Add a Bot

The sample game includes an optional bot opponent. This is an AI that plays against you. Simply click "Start Game: Human vs Bot" from the Menu Scene to activate it. Architecturally this is similar to the full game and it fully uses the Beamable Multiplayer event objects for each player move. However, it requires only one human player.

2. Play Against Yourself

The sample game allows a workflow to test the full 2 player experience with relative ease. Build the Unity sample project as a standalone game for either Mac or PC. Then run the game in the Unity Editor also. Simply click "Start Game: Human vs Human" from the Menu Scene in both game clients to activate it.

Doing a build takes a few minutes. Depending on the specifics of your game, it may not be required to rebuild the standalone after every code change. See Game Maker User Experience for more info.

This is the process of choosing a MatchId based on criteria. E.g. "Give me a match to play in with 3 total players with beginner skill level". Beamable includes an optional, light-weight Matchmaking service.

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.

Here 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);
}

Did this page help you?