The creator-centric platform for building live games in Unity

Easily add social, commerce, and content management features to your live game with low-code, drag-and-drop prefabs fully integrated with your Unity workflow.

Multiplayer (KOR) - Sample

Allow game makers to create multi-user experiences

šŸš§

Beamable Labs

This Beamable functionality is currently in-development and subject to change. Game makers are invited and encouraged to try it out, send feedback, and help shape the future of the product.

Overview

Welcome to "King of the Ring" (KOR). This downloadable sample game project showcases the Beamable Multiplayer FeatureFeature - An individual aspect of the Beamable product used to create a great user experience.

Attack with power & dodge with speed! Last player alive, wins.

This document and the sample project allow game makers to understand and apply the benefits of Multiplayer in game development. Or watch this video.

Download

These learning resources provide a better way to build live games in Unity.

SourceDetail
1. Download the Multiplayer KOR Sample Project
2. Open in Unity Editor ( Version 2020.3.11f1 )
3. Open the Beamable Toolbox
4. Sign-In / Register To Beamable. See Step 1 - Getting Started for more info
5. Open the 1.Intro Scene
6. Play The Scene: Unity ā†’ Edit ā†’ Play
7. Click "Start Game: Human vs Bot" for an easy start. Or do a standalone build of the game and run the build. Then run the Unity Editor. In both running games, choose "Start Game: Human vs Human" to play against yourself
8. Enjoy!

Note: Supports Mac & Windows and includes the Beamable SDK

Rules of the Game

  • 2-6 players enter the battle "ring"
  • Tap and hold anywhere in the ring to move your player
  • Collide with other players to bump them
  • An player who falls out of the ring, loses shield points and ranking.
  • The last player alive, wins!

Pro Tip: Earn coins by playing the game. Spend coins in the store to improve your avatar.

Screenshots

The player navigates from the Intro Scene to the Game Scene, where all the action takes place.

Intro Scene
Lobby Scene
Game Scene
Store Scene
Leaderboard Scene
Project Window

Player Experience Flowchart

Here is the high level execution flow of user input and system interactions.

Game Maker User Experience

During development, the game maker's user experience is as follows. There are several major parts to this game creation process.

Steps

Follow these steps to get started.

These steps are already complete in the sample project. The instructions here explain the process.

šŸ“˜

Related Guides

A common use case for the feature is covered in the guides. It is recommended to read the guide before continuing with the sample steps below.

ā€¢ Matchmaking - Connect remote players in a room
ā€¢ Multiplayer - Communicate messages between players in a room

Step 1. Setup Project

Here are instructions to setup the Beamable SDK and "GameType" content.

StepDetail
1. Install the Beamable SDK and Register/Loginā€¢ See Step 1 - Getting Started for more info.
2. Open the Content Manager Windowā€¢ Unity ā†’ Window ā†’ Beamable ā†’ Open Content Manager
3. Create the "GameType" content




ā€¢ Select the content type in the list
ā€¢ Press the "Create" button
ā€¢ Populate the content name
4. Configure "GameType" content

ā€¢ Populate the Max Players field

Note: The other fields are optional and may be needed for advanced use cases

5. Save the Unity Projectā€¢ Unity ā†’ File ā†’ Save Project

Best Practice: If you are working on a team, commit to version control in this step
6. Publish the contentā€¢ Press the "Publish" button in the Content Manager Window

Step 2. Plan the Multiplayer Game Design

See Multiplayer (Plan the Multiplayer Game Design) for more info.

Step 3. Create the Game Code

This step includes the bulk of time and effort the project.

StepDetail
1. Create C# game-specific logicā€¢ Implement game logic
ā€¢ Handle player input
ā€¢ Render graphics & sounds

Note: This represents the bulk of the development effort. The details depend on the specifics of the game project.

Inspector

Here is the GameSceneManager.cs main entry point for the Game Scene interactivity.

The "Configuration" and "GameUIView" are passed as referencesThe "Configuration" and "GameUIView" are passed as references

The "Configuration" and "GameUIView" are passed as references

Here is the Configuration.cs holding high-level, easily-configurable values used by various areas on the game code. Several game classes reference this data.

šŸš§

Gotchas

Here are hints to help explain some of the trickier concepts.

ā€¢ While the name is similar, this Configuration.cs is wholly unrelated to Beamable's Configuration Manager.

The "Configuration" values are easily configurableThe "Configuration" values are easily configurable

The "Configuration" values are easily configurable

Optional: Game Makers may experiment with new Delay values here to allow animations to occur faster or slower.

Code

The GameSceneManager is the main entry point to the Game Scene logic.

Here a a few highlights.

Send Game Event

NetworkController.Instance.SendNetworkMessage(new ReadyEvent(_ownAttributes, alias));

Receive Game Event

private void OnPlayerReady (ReadyEvent readyEvent)
{
    // Handle consequences...
}

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

namespace Beamable.Samples.KOR
{
    /// <summary>
    /// Handles the main game scene logic
    /// </summary>
    public partial class GameSceneManager : SingletonMonobehavior<GameSceneManager>
    {
        //  Properties -----------------------------------
        public GameUIView GameUIView { get { return _gameUIView; } }

        public Configuration Configuration { get { return _configuration; } }

        public List<SpawnPointBehaviour> AvailableSpawnPoints;

        //  Fields ---------------------------------------
        private IBeamableAPI _beamableAPI = null;

        [SerializeField]
        private Configuration _configuration = null;

        [SerializeField]
        private GameUIView _gameUIView = null;

        private Attributes _ownAttributes = null;

        private List<SpawnablePlayer> _spawnablePlayers = new List<SpawnablePlayer>();
        private List<SpawnPointBehaviour> _unusedSpawnPoints = new List<SpawnPointBehaviour>();
        private HashSet<long> _dbidReadyReceived = new HashSet<long>();
        private bool _hasSpawned = false;

        private ConcurrentQueue<Action> _concurrentQueue = new ConcurrentQueue<Action>();

        public void EnqueueConcurrent(Action action)
        {
            _concurrentQueue.Enqueue(action);
        }

        private void FixedUpdate()
        {
            Action newAction;
            if (_concurrentQueue.TryDequeue(out newAction))
                newAction();
        }

        //  Unity Methods   ------------------------------
        protected void Start()
        {
            for (int i = 0; i < 6; i++)
                _gameUIView.AvatarUIViews[i].GetComponent<CanvasGroup>().alpha = 0.0f;

            _gameUIView.BackButton.onClick.AddListener(BackButton_OnClicked);
            SetupBeamable();
        }

        //  Other Methods   ------------------------------

        private async void SetupBeamable()
        {
            _beamableAPI = await Beamable.API.Instance;
            await RuntimeDataStorage.Instance.CharacterManager.BootstrapTask;
            _ownAttributes = await RuntimeDataStorage.Instance.CharacterManager.GetChosenPlayerAttributes();

            // Do this after calling "Beamable.API.Instance" for smoother UI
            _gameUIView.CanvasGroupsDoFadeIn();

            // Set defaults if scene was loaded directly
            if (RuntimeDataStorage.Instance.TargetPlayerCount == KORConstants.UnsetValue)
            {
                RuntimeDataStorage.Instance.TargetPlayerCount = 1;
                RuntimeDataStorage.Instance.CurrentPlayerCount = 1;
                RuntimeDataStorage.Instance.LocalPlayerDbid = _beamableAPI.User.id;
                RuntimeDataStorage.Instance.RoomId = KORMatchmaking.GetRandomRoomId();
            }

            // Set the ActiveSimGameType. This happens in 2+ spots to handle direct scene loading
            if (RuntimeDataStorage.Instance.IsSinglePlayerMode)
                RuntimeDataStorage.Instance.ActiveSimGameType = await _configuration.SimGameType01Ref.Resolve();
            else
                RuntimeDataStorage.Instance.ActiveSimGameType = await _configuration.SimGameType02Ref.Resolve();

            // Initialize ECS
            SystemManager.StartGameSystems();

            // Show the player's attributes in the UI of this scene

            _gameUIView.AttributesPanelUI.Attributes = _ownAttributes;

            // Initialize Networking
            await NetworkController.Instance.Init();

            // Set Available Spawns
            _unusedSpawnPoints = AvailableSpawnPoints.ToList();

            NetworkController.Instance.Log.CreateNewConsumer(HandleNetworkUpdate);

            // Optional: Show queueable status text onscreen
            SetStatusText(KORConstants.GameUIView_Playing, TMP_BufferedText.BufferedTextMode.Immediate);

            // Optional: Add easily configurable delays
            await Task.Delay(TimeSpan.FromSeconds(_configuration.DelayGameBeforeMove));

        }

        public async void OnPlayerJoined(PlayerJoinedEvent joinEvent)
        {
            if (_spawnablePlayers.Find(i => i.DBID == joinEvent.PlayerDbid) != null)
                return;

            var spawnIndex = NetworkController.Instance.rand.Next(0, _unusedSpawnPoints.Count);
            var spawnPoint = _unusedSpawnPoints[spawnIndex];
            _unusedSpawnPoints.Remove(spawnPoint);

            SpawnablePlayer newPlayer = new SpawnablePlayer(joinEvent.PlayerDbid, spawnPoint);
            _spawnablePlayers.Add(newPlayer);
            await RuntimeDataStorage.Instance.CharacterManager.BootstrapTask;
            newPlayer.ChosenCharacter = await RuntimeDataStorage.Instance.CharacterManager.GetChosenCharacterByDBID(joinEvent.PlayerDbid);
            string alias = await RuntimeDataStorage.Instance.CharacterManager.GetPlayerAliasByDBID(joinEvent.PlayerDbid);

            if (joinEvent.PlayerDbid == NetworkController.Instance.LocalDbid)
                NetworkController.Instance.SendNetworkMessage(new ReadyEvent(_ownAttributes, alias));
        }

        private void OnPlayerReady(ReadyEvent readyEvt)
        {
            _dbidReadyReceived.Add(readyEvt.PlayerDbid);

            SpawnablePlayer sp = _spawnablePlayers.Find(i => i.DBID == readyEvt.PlayerDbid);
            sp.Attributes = new Attributes(readyEvt.aggregateChargeSpeed, readyEvt.aggregateMovementSpeed);
            sp.PlayerAlias = readyEvt.playerAlias;

            if (!_hasSpawned && _dbidReadyReceived.Count == RuntimeDataStorage.Instance.CurrentPlayerCount)
            {
                _hasSpawned = true;
                SpawnAllPlayersAtOnce();
                StartGameTimer();
            }
        }

        private void StartGameTimer()
        {
            GameUIView.GameTimerBehaviour.StartMatch();
            GameUIView.GameTimerBehaviour.OnGameOver += async () =>
            {
                var uis = FindObjectsOfType<AvatarUIView>();
                var validUis = uis.Where(ui => ui.Player).ToList();
                validUis.Sort((a, b) => a.SpawnablePlayer.DBID > b.SpawnablePlayer.DBID ? 1 : -1);
                validUis.Sort((a, b) => a.Player.HealthBehaviour.Health > b.Player.HealthBehaviour.Health ? -1 : 1);

                var scores = validUis.Select(ui => new PlayerResult
                {
                    playerId = ui.SpawnablePlayer.DBID,
                    score = ui.Player.HealthBehaviour.Health,
                }).ToArray();
                var selfRank = 0;
                var selfScore = scores[0];
                for (var i = 0; i < scores.Length; i++)
                {
                    scores[i].rank = i;
                    if (scores[i].playerId == NetworkController.Instance.LocalDbid)
                    {
                        selfRank = i;
                        selfScore = scores[i];
                    }
                }

                foreach (var motionBehaviour in FindObjectsOfType<AvatarMotionBehaviour>())
                {
                    motionBehaviour.Stop();
                    motionBehaviour.enabled = false;
                }

                foreach (var inputBehaviour in FindObjectsOfType<PlayerInputBehaviour>())
                {
                    inputBehaviour.enabled = false;
                }

                var results = await NetworkController.Instance.ReportResults(scores);

                var isWinner = selfRank == 0;
                var earnings = string.Join(",", 
                    results.currenciesGranted.Select(grant => $"{grant.amount}x{grant.symbol}"));
                
                var earningsBody = string.IsNullOrWhiteSpace(earnings)
                    ? "nothing"
                    : earnings;
                var body = "You came in place: " + (selfRank + 1) + ". You earned " + earningsBody;
                
                _gameUIView.DialogSystem.ShowDialogBox<DialogUI>(

                    // Renders this prefab. DUPLICATE this prefab and drag
                    // into _storeUIView to change layout
                    _gameUIView.DialogSystem.DialogUIPrefab,

                    // Set Text
                    isWinner
                        ? KORConstants.Dialog_GameOver_Victory
                        : KORConstants.Dialog_GameOver_Defeat,
                    body,

                    // Create zero or more buttons
                    new List<DialogButtonData>
                    {
                        new DialogButtonData(KORConstants.Dialog_Ok, () =>
                        {
                            KORHelper.PlayAudioForUIClickPrimary();
                            _gameUIView.DialogSystem.HideDialogBox();

                            // Clean up manager
                            _spawnablePlayers.Clear();
                            _unusedSpawnPoints.Clear();
                            _dbidReadyReceived.Clear();
                            _hasSpawned = false;
                            NetworkController.Instance.Cleanup();

                            // Destroy ECS
                            SystemManager.DestroyGameSystems();

                            // Change scenes
                            StartCoroutine(KORHelper.LoadScene_Coroutine(_configuration.IntroSceneName,
                                _configuration.DelayBeforeLoadScene));
                        }),
                        new DialogButtonData(KORConstants.Dialog_Cancel, () =>
                        {
                            KORHelper.PlayAudioForUIClickSecondary();
                            _gameUIView.DialogSystem.HideDialogBox();
                        })
                    });

            };
        }

        private void SpawnAllPlayersAtOnce()
        {
            List<CanvasGroup> avatarUiCanvasGroups = new List<CanvasGroup>();

            for (int p = 0; p < _spawnablePlayers.Count; p++)
            {
                SpawnablePlayer sp = _spawnablePlayers[p];

                AvatarView avatarView = GameObject.Instantiate<AvatarView>(sp.ChosenCharacter.AvatarViewPrefab);
                avatarView.transform.SetPhysicsPosition(sp.SpawnPointBehaviour.transform.position);

                Player player = avatarView.gameObject.GetComponent<Player>();
                player.SetAlias(sp.PlayerAlias);

                avatarView.SetForPlayer(sp.DBID);
                _gameUIView.AvatarViews.Add(avatarView);

                if (sp.DBID == NetworkController.Instance.LocalDbid)
                    avatarView.gameObject.GetComponent<AvatarMotionBehaviour>().PreviewBehaviour = null;
                else
                    avatarView.gameObject.GetComponent<PlayerInputBehaviour>().enabled = false;

                AvatarMotionBehaviour amb = avatarView.gameObject.GetComponent<AvatarMotionBehaviour>();
                amb.Attributes = sp.Attributes; 

                _gameUIView.AvatarUIViews[p].Set(player, sp);
                _gameUIView.AvatarUIViews[p].Render();
                avatarUiCanvasGroups.Add(_gameUIView.AvatarUIViews[p].GetComponent<CanvasGroup>());
            }

            TweenHelper.CanvasGroupsDoFade(avatarUiCanvasGroups, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f);
        }

        public void HandleNetworkUpdate(TimeUpdate update)
        {
            foreach (var evt in update.Events)
            {
                HandleNetworkEvent(evt);
            }
        }

        public void ShakeCamera(float force=1)
        {
            _gameUIView.CinemachineImpulseSource.GenerateImpulse();
        }

        public void HandleNetworkEvent(KOREvent korEvent)
        {
            switch (korEvent)
            {
                case ReadyEvent readyEvt:
                    OnPlayerReady(readyEvt);
                    break;

                case PlayerJoinedEvent joinEvt:
                    OnPlayerJoined(joinEvt);
                    break;
            }
        }

        /// <summary>
        /// Render UI text
        /// </summary>
        /// <param name="message"></param>
        /// <param name="statusTextMode"></param>
        public void SetStatusText(string message, TMP_BufferedText.BufferedTextMode statusTextMode)
        {
            _gameUIView.BufferedText.SetText(message, statusTextMode);
        }

        //  Event Handlers -------------------------------
        private void BackButton_OnClicked()
        {
            KORHelper.PlayAudioForUIClickBack();

            _gameUIView.DialogSystem.ShowDialogBox<DialogUI>(

                // Renders this prefab. DUPLICATE this prefab and drag
                // into _storeUIView to change layout
                _gameUIView.DialogSystem.DialogUIPrefab,

                // Set Text
                KORConstants.Dialog_AreYouSure,
                "This well end your game.",

                // Create zero or more buttons
                new List<DialogButtonData>
                {
                    new DialogButtonData(KORConstants.Dialog_Ok, () =>
                    {
                        KORHelper.PlayAudioForUIClickPrimary();
                        _gameUIView.DialogSystem.HideDialogBox();

                        // Clean up manager
                        _spawnablePlayers.Clear();
                        _unusedSpawnPoints.Clear();
                        _dbidReadyReceived.Clear();
                        _hasSpawned = false;
                        NetworkController.Instance.Cleanup();

                        // Destroy ECS
                        SystemManager.DestroyGameSystems();

                        // Change scenes
                        StartCoroutine(KORHelper.LoadScene_Coroutine(_configuration.IntroSceneName,
                            _configuration.DelayBeforeLoadScene));
                    }),
                    new DialogButtonData(KORConstants.Dialog_Cancel, () =>
                    {
                        KORHelper.PlayAudioForUIClickSecondary();
                        _gameUIView.DialogSystem.HideDialogBox();
                    })
                });
        }
    }
}

Step 4. Create the Multiplayer Code

Now that the core game logic is setup, use Beamable to connect 2 (or more) players together. Create the Multiplayer event objects, send outgoing events, and handle incoming events.

StepDetail
1. Create C# Multiplayer-specific logicā€¢ Create event objects
ā€¢ Send outgoing event
ā€¢ Handle incoming events

Note: Its likely that game makers will add multiplayer functionality throughout development including during step #3. For sake of clarity, it is described here as a separate, final step #4.
2. Play the 1.Intro Sceneā€¢ Unity ā†’ Edit ā†’ Play
3. Enjoy the game!ā€¢ Can you beat the opponents?
4. Stop the Sceneā€¢ Unity ā†’ Edit ā†’ Stop

Additional Experiments

Here some optional experiments game makers can perform with the sample project.

Did you complete all the experiments with success? We'd love to hear about it. Contact us.

DifficultySceneNameDetail
BeginnerGameTweak Configurationā€¢ Update the Configuration.asset values in the Unity Inspector Window

Note: Experiment and have fun!
IntermediateLobbyAdd Lobby Graphicsā€¢ The lobby shows text indicating "Player 1/2 joined"
ā€¢ As each player joins the multiplayer matchmaking session, show the 2D asset onscreen and player's name
IntermediateGameAdd a new characterā€¢ The game includes a character selector and several characters
ā€¢ Add 2D/3D assets for a new character
ā€¢ Update Beamable content to define the new character

Note: No 3D skills? An alternative is to duplicate an existing 3D character prefab and recolor its texture
IntermediateGameAdd "Jump" Inputā€¢ The game includes 'tap and hold' input to move the character
ā€¢ Add a 'Jump' button in the bottom menu
ā€¢ Apply a physics force upwards on the local player

Note: Send a new multiplayer game event to all players to keep the game in sync
AdvancedGameAdd a collectible pickupā€¢ Spawn an item in to the game world
ā€¢ A character collides with the item to collect the item
ā€¢ Collecting the item rewards the player (Shield, Speed, etc...)
AdvancedGameAdd a bombā€¢ Spawn a bomb in to the game world
ā€¢ After 3 seconds the bomb explodes and disappears
ā€¢ The explosion causes a physics force to push away players and items

Advanced

This section contains any advanced configuration options and workflows.

Matchmaking

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

See Matchmaking for more info.

Game Security

See Multiplayer (Game Security) for more info.

Playing "Against Yourself"

See Multiplayer (Playing Against Yourself) for more info.

Randomization and Determinism

See Multiplayer (Randomization and Determinism) for more info.

Learning Resources

These learning resources provide a better way to build live games in Unity.

SourceDetail
1. Download the Multiplayer KOR Sample Project
2. Open in Unity Editor ( Version 2020.3.11f1 )
3. Open the Beamable Toolbox
4. Sign-In / Register To Beamable. See Step 1 - Getting Started for more info
5. Open the 1.Intro Scene
6. Play The Scene: Unity ā†’ Edit ā†’ Play
7. Click "Start Game: Human vs Bot" for an easy start. Or do a standalone build of the game and run the build. Then run the Unity Editor. In both running games, choose "Start Game: Human vs Human" to play against yourself
8. Enjoy!

Note: Supports Mac & Windows and includes the Beamable SDK

Updated about a month ago


Multiplayer (KOR) - Sample


Allow game makers to create multi-user experiences

Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.