Multiplayer (KOR) - Guide
[SMPL-KOR-02]
Overview
This downloadable sample game project showcases the Multiplayer feature in game development. Or watch this video.
Download
These learning resources provide a better way to build live games in Unity.
Source | Detail |
---|---|
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 Installing Beamable for more info 5. Rebuild the Unity Addressables : Unity → Window → Asset Management → Groups, then Build → Update a Previous Build 6. Open the 1.Intro Scene7. Play The Scene: Unity → Edit → Play 8. 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 9. 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 Features
More details are covered in related feature page(s).
• Matchmaking - Connect remote players in a room
• Multiplayer - Allow game makers to create multi-user experiences
Step 1. Setup Project
Here are instructions to setup the Beamable SDK and "GameType" content.
Step | Detail |
---|---|
1. Install the Beamable SDK and Register/Login | • See Installing Beamable 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 fieldNote: 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 (Planning) for more info.
Step 3. Create the Game Code
This step includes the bulk of time and effort the project.
Step | Detail |
---|---|
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.
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.
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...
}
Below is a partial code snippet. Download the project to see the complete code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Beamable.Examples.Features.Multiplayer.Core;
using Beamable.Samples.KOR.Behaviours;
using Beamable.Samples.KOR.Data;
using Beamable.Samples.KOR.Multiplayer;
using Beamable.Samples.KOR.Multiplayer.Events;
using Beamable.Samples.KOR.Views;
using UnityEngine;
using Beamable.Experimental.Api.Sim;
using Beamable.Samples.Core.Debugging;
using Beamable.Samples.Core.UI;
using Beamable.Samples.Core.UI.DialogSystem;
using Beamable.Samples.Core.Utilities;
namespace Beamable.Samples.KOR
{
/// <summary>
/// Handles the main scene logic: Game
/// </summary>
public class GameSceneManager : MonoBehaviour
{
// 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;
// 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 void DebugLog(string message)
{
// Respects Configuration.IsDebugLog Checkbox
Configuration.Debugger.Log(message);
}
private async void SetupBeamable()
{
_beamableAPI = await Beamable.API.Instance;
await RuntimeDataStorage.Instance.CharacterManager.Initialize();
_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)
{
DebugLog(KORHelper.GetSceneLoadingMessage(gameObject.scene.name, true));
RuntimeDataStorage.Instance.TargetPlayerCount = 1;
RuntimeDataStorage.Instance.CurrentPlayerCount = 1;
RuntimeDataStorage.Instance.LocalPlayerDbid = _beamableAPI.User.id;
RuntimeDataStorage.Instance.MatchId = KORMatchmaking.GetRandomMatchId();
}
else
{
DebugLog(KORHelper.GetSceneLoadingMessage(gameObject.scene.name, false));
}
// 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: Stuff to use later when player moves are incoming
long tbdIncomingPlayerDbid = _beamableAPI.User.id; // test value;
DebugLog($"MinPlayerCount = {RuntimeDataStorage.Instance.MinPlayerCount}");
DebugLog($"MaxPlayerCount = {RuntimeDataStorage.Instance.MaxPlayerCount}");
DebugLog($"CurrentPlayerCount = {RuntimeDataStorage.Instance.CurrentPlayerCount}");
DebugLog($"LocalPlayerDbid = {RuntimeDataStorage.Instance.LocalPlayerDbid}");
DebugLog($"IsLocalPlayerDbid = {RuntimeDataStorage.Instance.IsLocalPlayerDbid(tbdIncomingPlayerDbid)}");
DebugLog($"IsSinglePlayerMode = {RuntimeDataStorage.Instance.IsSinglePlayerMode}");
// 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));
// Optional: Play sound
//SoundManager.Instance.PlayAudioClip(SoundConstants.Click01);
// Optional: Render color and text of avatar ui
_gameUIView.AvatarViews.Clear();
}
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.Initialize();
newPlayer.ChosenCharacter = await RuntimeDataStorage.Instance.CharacterManager.GetChosenCharacterByDBID(joinEvent.PlayerDbid);
string alias = await RuntimeDataStorage.Instance.CharacterManager.GetPlayerAliasByDBID(joinEvent.PlayerDbid);
DebugLog($"alias from joinEvent dbid={joinEvent.PlayerDbid} alias={alias}");
if (joinEvent.PlayerDbid == NetworkController.Instance.LocalDbid)
NetworkController.Instance.SendNetworkMessage(new ReadyEvent(_ownAttributes, alias));
}
private void OnPlayerReady(ReadyEvent readyEvt)
{
Configuration.Debugger.Log($"Getting ready for dbid={readyEvt.PlayerDbid}"
+ $" attributes move/charge={readyEvt.aggregateMovementSpeed}/{readyEvt.aggregateChargeSpeed}", DebugLogLevel.Verbose);
_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;
Configuration.Debugger.Log($"alias from readyEvt dbid={readyEvt.PlayerDbid} alias={sp.PlayerAlias}");
Configuration.Debugger.Log($"OnPlayerReady Players={_dbidReadyReceived.Count}/{RuntimeDataStorage.Instance.CurrentPlayerCount}", DebugLogLevel.Verbose);
if (!_hasSpawned && _dbidReadyReceived.Count == RuntimeDataStorage.Instance.CurrentPlayerCount)
{
_hasSpawned = true;
SpawnAllPlayersAtOnce();
StartGameTimer();
}
}
private void StartGameTimer()
{
GameUIView.GameTimerBehaviour.StartMatch();
GameUIView.GameTimerBehaviour.OnGameOver += async () =>
{
// TODO: score the players, and end the game.
Debug.Log("Game over!");
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));
})
});
};
}
private void SpawnAllPlayersAtOnce()
{
List<CanvasGroup> avatarUiCanvasGroups = new List<CanvasGroup>();
for (int p = 0; p < _spawnablePlayers.Count; p++)
{
SpawnablePlayer sp = _spawnablePlayers[p];
Configuration.Debugger.Log($"DBID={sp.DBID} Spawning character={sp.ChosenCharacter.CharacterContentObject.ContentName}"
+ $" attributes move/charge={sp.Attributes.MovementSpeed}/{sp.Attributes.ChargeSpeed}", DebugLogLevel.Verbose);
DebugLog($"playerAlias={sp.PlayerAlias}");
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 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 will 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.
Step | Detail |
---|---|
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 are some optional experiments game makers can complete in the sample project.
Did you complete all the experiments with success? We'd love to hear about it. Contact us.
Difficulty | Scene | Name | Detail |
---|---|---|---|
Beginner | Game | Tweak Configuration | • Update the Configuration.asset values in the Unity Inspector Window Note: Experiment and have fun! |
Intermediate | Lobby | Add 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 |
Intermediate | Game | Add 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 |
Intermediate | Game | Add "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 |
Advanced | Game | Add 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...) |
Advanced | Game | Add 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.
Source | Detail |
---|---|
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 Installing Beamable for more info 5. Rebuild the Unity Addressables : Unity → Window → Asset Management → Groups, then Build → Update a Previous Build 6. Open the 1.Intro Scene7. Play The Scene: Unity → Edit → Play 8. 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 9. Enjoy! Note: Supports Mac & Windows and includes the Beamable SDK |
Updated about 1 year ago