This document and the sample project allow game makers to understand and apply the benefits of Beamable Multiplayer in game development. Or watch this video:
1. Download the Multiplayer TBF Sample Project 2. Open in Unity Editor (Version 2021.3 or later) 3. Open the Beamable Toolbox 4. Sign-In / Register To Beamable. See 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: Beamable supports Unity versions 2021.3 to 2023.3
The following flowchart shows the player experience through the game:
Note: Interactive flowchart content from LucidChart is not directly convertible to static markdown. Please refer to the original documentation or recreate as a static diagram.
Game makers follow a structured process to create multiplayer experiences. There are several major parts to this game creation process.
Note: Interactive flowchart content from LucidChart is not directly convertible to static markdown. Please refer to the original documentation or recreate as a static diagram.
Multiplayer game development offers additional challenges beyond those of a typical single player game.
When designing a game with Beamable's Multiplayer feature, 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
2. Consider game input from the users
• What is each user input gesture? • What are all possible choices the player may make at that moment?
3. 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 ...
4. 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
"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."
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.
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 some common issues and solutions:
• While the name is similar, this Configuration.cs is wholly unrelated to Beamable's Configuration Manager.
The "Configuration" values are easily configurable
Optional: Game Makers may experiment with new Delay values here to allow the players' turns to occur faster or slower.
Code
The GameSceneManager is the main entry point to the Game Scene logic.
This is a partial code snippet showing the structure:
usingBeamable.Samples.TBF.Data;usingBeamable.Samples.TBF.Multiplayer;usingBeamable.Samples.TBF.Multiplayer.Events;usingBeamable.Samples.TBF.Views;usingSystem;usingBeamable.Samples.Core.Audio;usingBeamable.Samples.Core.UI;usingUnityEngine;namespaceBeamable.Samples.TBF{/// <summary>/// List of all users' moves/// </summary>publicenumGameMoveType{Null,High,// Like "Rock"Mid,// Like "Paper"Low// Like "Scissors"}/// <summary>/// Handles the main scene logic: Game/// </summary>publicclassGameSceneManager:MonoBehaviour{// Properties -----------------------------------publicGameUIViewGameUIView{get{return_gameUIView;}}publicGameProgressDataGameProgressData{get{return_gameProgressData;}set{_gameProgressData=value;}}publicConfigurationConfiguration{get{return_configuration;}}publicTBFMultiplayerSessionMultiplayerSession{get{return_multiplayerSession;}}publicRemotePlayerAIRemotePlayerAI{get{return_remotePlayerAI;}set{_remotePlayerAI=value;}}// Fields ---------------------------------------[SerializeField]privateConfiguration_configuration=null;[SerializeField]privateGameUIView_gameUIView=null;privateIBeamableAPI_beamableAPI=null;privateTBFMultiplayerSession_multiplayerSession;privateGameProgressData_gameProgressData;privateRemotePlayerAI_remotePlayerAI;privateGameStateHandler_gameStateHandler;// Unity Methods ------------------------------protectedvoidStart(){_gameUIView.BackButton.onClick.AddListener(BackButton_OnClicked);_gameUIView.MoveButton_01.onClick.AddListener(MoveButton_01_OnClicked);_gameUIView.MoveButton_02.onClick.AddListener(MoveButton_02_OnClicked);_gameUIView.MoveButton_03.onClick.AddListener(MoveButton_03_OnClicked);foreach(AvatarUIViewavatarUIViewin_gameUIView.AvatarUIViews){avatarUIView.HealthBarView.OnValueChanged+=HealthBarView_OnValueChanged;}_gameUIView.AvatarUIViews[TBFConstants.PlayerIndexLocal].HealthBarView.Value=100;_gameUIView.AvatarUIViews[TBFConstants.PlayerIndexRemote].HealthBarView.Value=100;//_gameStateHandler=newGameStateHandler(this);SetupBeamable();}protectedvoidUpdate(){_multiplayerSession?.Update();}// Other Methods -----------------------------privatevoidDebugLog(stringmessage){if(TBFConstants.IsDebugLogging){Debug.Log(message);}}privateasyncvoidSetupBeamable(){await_gameStateHandler.SetGameState(GameState.Loading);awaitBeamable.API.Instance.Then(asyncde=>{await_gameStateHandler.SetGameState(GameState.Loaded);try{_beamableAPI=de;if(!RuntimeDataStorage.Instance.IsMatchmakingComplete){DebugLog($"Scene '{gameObject.scene.name}' was loaded directly. That is ok. Setting defaults.");RuntimeDataStorage.Instance.LocalPlayerDbid=_beamableAPI.User.id;RuntimeDataStorage.Instance.TargetPlayerCount=1;RuntimeDataStorage.Instance.MatchId=TBFMatchmaking.GetRandomMatchId();}_multiplayerSession=newTBFMultiplayerSession(RuntimeDataStorage.Instance.LocalPlayerDbid,RuntimeDataStorage.Instance.TargetPlayerCount,RuntimeDataStorage.Instance.MatchId);await_gameStateHandler.SetGameState(GameState.Initializing);_multiplayerSession.OnInit+=MultiplayerSession_OnInit;_multiplayerSession.OnConnect+=MultiplayerSession_OnConnect;_multiplayerSession.OnDisconnect+=MultiplayerSession_OnDisconnect;_multiplayerSession.Initialize();}catch(Exception){SetStatusText(TBFHelper.InternetOfflineInstructionsText,TMP_BufferedText.BufferedTextMode.Immediate);}});}/// <summary>/// Render UI text/// </summary>/// <param name="message"></param>/// <param name="statusTextMode"></param>publicvoidSetStatusText(stringmessage,TMP_BufferedText.BufferedTextModestatusTextMode){_gameUIView.BufferedText.SetText(message,statusTextMode);}/// <summary>/// Render UI text/// </summary>/// <param name="message"></param>publicvoidSetRoundText(introundNumber){_gameUIView.RoundText.text=string.Format(TBFConstants.RoundText,roundNumber,_configuration.GameRoundsTotal);}privatevoidBindPlayerDbidToEvents(longplayerDbid,boolisBinding){if(isBinding){stringorigin=playerDbid.ToString();_multiplayerSession.On<GameStartEvent>(origin,MultiplayerSession_OnGameStartEvent);_multiplayerSession.On<GameMoveEvent>(origin,MultiplayerSession_OnGameMoveEvent);}else{_multiplayerSession.Remove<GameStartEvent>(MultiplayerSession_OnGameStartEvent);_multiplayerSession.Remove<GameMoveEvent>(MultiplayerSession_OnGameMoveEvent);}}privatevoidSendGameMoveEventSave(GameMoveTypegameMoveType){if(_gameStateHandler.GameState==GameState.RoundPlayerMoving){_gameUIView.MoveButtonsCanvasGroup.interactable=false;SoundManager.Instance.PlayAudioClip(SoundConstants.Click02);_multiplayerSession.SendEvent<GameMoveEvent>(newGameMoveEvent(gameMoveType));}}// Event Handlers -------------------------------privatevoidHealthBarView_OnValueChanged(intoldValue,intnewValue){if(newValue<oldValue){// Play "damage" soundSoundManager.Instance.PlayAudioClip(SoundConstants.HealthBarDecrement);}}privatevoidBackButton_OnClicked(){//Change scenesStartCoroutine(TBFHelper.LoadScene_Coroutine(_configuration.IntroSceneName,_configuration.DelayBeforeLoadScene));}privatevoidMoveButton_01_OnClicked(){SendGameMoveEventSave(GameMoveType.High);}privatevoidMoveButton_02_OnClicked(){SendGameMoveEventSave(GameMoveType.Mid);}privatevoidMoveButton_03_OnClicked(){SendGameMoveEventSave(GameMoveType.Low);}privateasyncvoidMultiplayerSession_OnInit(System.Randomrandom){await_gameStateHandler.SetGameState(GameState.Initialized);}privateasyncvoidMultiplayerSession_OnConnect(longplayerDbid){BindPlayerDbidToEvents(playerDbid,true);Debug.Log($"CHECK {_multiplayerSession.PlayerDbidsCount} < {_multiplayerSession.TargetPlayerCount}");if(_multiplayerSession.PlayerDbidsCount<_multiplayerSession.TargetPlayerCount){await_gameStateHandler.SetGameState(GameState.Connecting);}else{await_gameStateHandler.SetGameState(GameState.Connected);_multiplayerSession.SendEvent<GameStartEvent>(newGameStartEvent());}}privateasyncvoidMultiplayerSession_OnDisconnect(longplayerDbid){BindPlayerDbidToEvents(playerDbid,false);SetStatusText(string.Format(TBFConstants.StatusText_Multiplayer_OnDisconnect,_multiplayerSession.PlayerDbidsCount.ToString(),_multiplayerSession.TargetPlayerCount),TMP_BufferedText.BufferedTextMode.Immediate);await_gameStateHandler.SetGameState(GameState.GameEnded);}privateasyncvoidMultiplayerSession_OnGameStartEvent(GameStartEventgameStartEvent){DebugLog($"OnGameStartEvent() by {gameStartEvent.PlayerDbid}");if(_gameStateHandler.GameState==GameState.GameStarting){_gameProgressData.GameStartEventsBucket.Add(gameStartEvent);DebugLog($"GameStartEventBucket.Count = {_gameProgressData.GameStartEventsBucket.Count}");if(_gameProgressData.GameStartEventsBucket.Count==_multiplayerSession.TargetPlayerCount){await_gameStateHandler.SetGameState(GameState.GameStarted);}}}privateasyncvoidMultiplayerSession_OnGameMoveEvent(GameMoveEventgameMoveEvent){DebugLog($"OnGameMoveEvent() of {gameMoveEvent.GameMoveType} by {gameMoveEvent.PlayerDbid}");if(_gameStateHandler.GameState==GameState.RoundPlayerMoving){_gameProgressData.GameMoveEventsThisRoundBucket.Add(gameMoveEvent);DebugLog($"GameMoveEventsThisRoundBucket.Count = {_gameProgressData.GameMoveEventsThisRoundBucket.Count}");if(_gameProgressData.GameMoveEventsThisRoundBucket.Count==_multiplayerSession.TargetPlayerCount){await_gameStateHandler.SetGameState(GameState.RoundPlayerMoved);}}}}}
usingBeamable.Samples.TBF.Data;usingBeamable.Samples.TBF.Multiplayer;usingBeamable.Samples.TBF.Multiplayer.Events;usingBeamable.Samples.TBF.Views;usingSystem;usingSystem.Threading.Tasks;usingBeamable.Samples.Core.Audio;usingBeamable.Samples.Core.Exceptions;usingBeamable.Samples.Core.UI;usingBeamable.Samples.Core.Utilities;usingUnityAsync;usingUnityEngine;usingstaticBeamable.Samples.TBF.Data.GameProgressData;namespaceBeamable.Samples.TBF{/// <summary>/// List of all phases of the gameplay./// There are arguably more states here than are needed, /// however all are indeed used, in the order shown, for deliberate separation./// </summary>publicenumGameState{//Game loads within hereNull,Loading,Loaded,Initializing,Initialized,Connecting,Connected,GameStarting,GameStarted,//Game repeats within hereRoundStarting,RoundStarted,RoundPlayerMoving,RoundPlayerMoved,RoundEvaluating,RoundEvaluated,//Game ends hereGameEvaluating,GameEnding,GameEnded}/// <summary>/// Handles the <see cref="GameState"/> for the <see cref="GameSceneManager"/>./// </summary>publicclassGameStateHandler{// Properties -----------------------------------publicGameStateGameState{get{return_gameState;}}// Fields ---------------------------------------privateGameState_gameState=GameState.Null;privateGameSceneManager_gameSceneManager;// Other Methods -----------------------------publicGameStateHandler(GameSceneManagergameSceneManager){_gameSceneManager=gameSceneManager;}/// <summary>/// Store and handle changes to the <see cref="GameState"/>./// </summary>/// <param name="gameState"></param>/// <returns></returns>publicasyncTaskSetGameState(GameStategameState){DebugLog($"SetGameState() from {_gameState} to {gameState}");//NOTE: Do not set "_gameState" directly anywhere, except here._gameState=gameState;// SetGameState() is async...// Pros: We can use operations like "Task.Delay" to slow down execution// Cons: Error handling is tricky. // Workaround: AsyncUtility helps with its try/catch.awaitAsyncUtility.AsyncSafe(async()=>{switch(_gameState){caseGameState.Null:break;caseGameState.Loading:// **************************************// Render the scene before any latency // of multiplayer begins// **************************************_gameSceneManager.SetStatusText("",TMP_BufferedText.BufferedTextMode.Immediate);_gameSceneManager.SetRoundText(1);_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexLocal].PlayAnimationIdle();_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexRemote].PlayAnimationIdle();_gameSceneManager.GameProgressData=newGameProgressData(_gameSceneManager.Configuration);_gameSceneManager.GameUIView.MoveButtonsCanvasGroup.interactable=false;_gameSceneManager.SetStatusText(TBFConstants.StatusText_GameState_Loading,TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.Loaded:// **************************************// Update UI// // **************************************_gameSceneManager.SetStatusText(TBFConstants.StatusText_GameState_Loaded,TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.Initializing:// **************************************// Update UI// // **************************************_gameSceneManager.SetStatusText(TBFConstants.StatusText_GameState_Initializing,TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.Initialized:// **************************************// Update UI// // **************************************_gameSceneManager.SetStatusText(TBFConstants.StatusText_GameState_Initialized,TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.Connecting:// **************************************// Update UI// // **************************************_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_Connecting,_gameSceneManager.MultiplayerSession.PlayerDbidsCount.ToString(),_gameSceneManager.MultiplayerSession.TargetPlayerCount),TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.Connected:// **************************************// Advanced the state // // **************************************awaitSetGameState(GameState.GameStarting);break;caseGameState.GameStarting:// **************************************// Reset the game-specific data// // **************************************_gameSceneManager.GameProgressData.StartGame();break;caseGameState.GameStarted:// **************************************// Now that all players have connected, setup AI// // **************************************// RemotePlayerAI is always created, but enabled only sometimesboolisEnabledRemotePlayerAI=_gameSceneManager.MultiplayerSession.IsHumanVsBotMode;System.Randomrandom=_gameSceneManager.MultiplayerSession.Random;DebugLog($"[Debug] isEnabledRemotePlayerAI={isEnabledRemotePlayerAI}");_gameSceneManager.RemotePlayerAI=newRemotePlayerAI(random,isEnabledRemotePlayerAI);awaitSetGameState(GameState.RoundStarting);break;caseGameState.RoundStarting:// **************************************// Reste the round-specific data.// Advance the state. // This happens before EACH round during a game// **************************************_gameSceneManager.GameProgressData.StartNextRound();_gameSceneManager.SetRoundText(_gameSceneManager.GameProgressData.CurrentRoundNumber);awaitSetGameState(GameState.RoundStarted);break;caseGameState.RoundStarted:// **************************************// Advance the state// // **************************************while(_gameSceneManager.GameUIView.BufferedText.HasRemainingQueueText){// Wait for old messages to pass before allowing button clicksawaitAwait.NextUpdate();}_gameSceneManager.GameUIView.MoveButtonsCanvasGroup.interactable=true;awaitSetGameState(GameState.RoundPlayerMoving);break;caseGameState.RoundPlayerMoving:// **************************************// Update UI// // **************************************_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_PlayerMoving),TMP_BufferedText.BufferedTextMode.Queue);break;caseGameState.RoundPlayerMoved:// **************************************// // // **************************************longlocalPlayerDbid=_gameSceneManager.MultiplayerSession.GetPlayerDbidForIndex(TBFConstants.PlayerIndexLocal);GameMoveEventlocalGameMoveEvent=_gameSceneManager.GameProgressData.GameMoveEventsThisRoundBucket.GetByPlayerDbid(localPlayerDbid);GameMoveTypelocalGameMoveType=localGameMoveEvent.GameMoveType;longremotePlayerDbid;if(_gameSceneManager.RemotePlayerAI.IsEnabled){// HumanVSBot: Create an AI movement here...remotePlayerDbid=_gameSceneManager.RemotePlayerAI.RemotePlayerDbid;GameMoveEventgameMoveEvent=_gameSceneManager.RemotePlayerAI.GetNextRemoteGameMoveEvent(localGameMoveType);_gameSceneManager.GameProgressData.GameMoveEventsThisRoundBucket.Add(gameMoveEvent);}else{remotePlayerDbid=_gameSceneManager.MultiplayerSession.GetPlayerDbidForIndex(TBFConstants.PlayerIndexRemote);}GameMoveEventremoteGameEvent=_gameSceneManager.GameProgressData.GameMoveEventsThisRoundBucket.GetByPlayerDbid(remotePlayerDbid);GameMoveTyperemoteGameMoveType=remoteGameEvent.GameMoveType;// 1 LOCALawaitRenderPlayerMove(TBFConstants.PlayerIndexLocal,localGameMoveType);// 2 REMOTE - Always show this second, it builds dramaawaitRenderPlayerMove(TBFConstants.PlayerIndexRemote,remoteGameMoveType);// All players have moved_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_PlayersAllMoved),TMP_BufferedText.BufferedTextMode.Queue);if(_gameSceneManager.RemotePlayerAI.IsEnabled){// HumanVSBot: The human move is done. Don't wait for other moves.awaitSetGameState(GameState.RoundEvaluating);}elseif(_gameSceneManager.GameProgressData.GameMoveEventsThisRoundBucket.Count==_gameSceneManager.MultiplayerSession.TargetPlayerCount){// HumanVSHuman: All moves are complete, so evaluateawaitSetGameState(GameState.RoundEvaluating);}else{// HumanVSHuman: NOT all moves are complete, so wait...awaitSetGameState(GameState.RoundPlayerMoving);}break;caseGameState.RoundEvaluating:// **************************************// Evalute all the player moves and store result.// Advance the state// // **************************************_gameSceneManager.GameProgressData.EvaluateGameMoveEventsThisRound();awaitSetGameState(GameState.RoundEvaluated);break;caseGameState.RoundEvaluated:// **************************************// Render results onscreen (animation, sounds).// Decide: Advance round or end game// // **************************************RoundResultcurrentRoundResult=_gameSceneManager.GameProgressData.CurrentRoundResult;switch(currentRoundResult){caseRoundResult.Tie:_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_EvaluatedTie,_gameSceneManager.GameProgressData.CurrentRoundNumber),TMP_BufferedText.BufferedTextMode.Queue);while(_gameSceneManager.GameUIView.BufferedText.HasRemainingQueueText){// Wait for old messages to pass before allowing button clicksawaitAwait.NextUpdate();}awaitSetGameState(GameState.RoundStarting);return;caseRoundResult.Winner://pass through to code belowbreak;default:SwitchDefaultException.Throw(currentRoundResult);break;}boolcurrentRoundHasWinnerPlayerDbid=_gameSceneManager.GameProgressData.CurrentRoundHasWinnerPlayerDbid;if(!currentRoundHasWinnerPlayerDbid){thrownewInvalidOperationException("This is never expected. #2");}longcurrentRoundWinnerPlayerDbid=_gameSceneManager.GameProgressData.CurrentRoundWinnerPlayerDbid;stringroundWinnerName=GetPlayerNameByPlayerDbid(currentRoundWinnerPlayerDbid);_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_EvaluatedWinner,_gameSceneManager.GameProgressData.CurrentRoundNumber,roundWinnerName),TMP_BufferedText.BufferedTextMode.Queue);while(_gameSceneManager.GameUIView.BufferedText.HasRemainingQueueText){// Wait for old messages to pass before allowing button clicksawaitAwait.NextUpdate();}// Ex. Do 34 damage for each round of 3 rounds so that 3 hits = total deathintdeltaHealth=-(1+HealthBarView.MaxValue/_gameSceneManager.Configuration.GameRoundsTotal);UpdateHealth(currentRoundWinnerPlayerDbid,deltaHealth);//Wait for animations to finishawaitAsyncUtility.TaskDelaySeconds(_gameSceneManager.Configuration.DelayGameBeforeGameOver);awaitSetGameState(GameState.GameEvaluating);break;caseGameState.GameEvaluating:// **************************************// Advance the state // // **************************************if(_gameSceneManager.GameProgressData.GameHasWinnerPlayerDbid){awaitSetGameState(GameState.GameEnding);}else{awaitSetGameState(GameState.RoundStarting);}break;caseGameState.GameEnding:// **************************************// Render loss (animation and sound)// Game stays here. // **************************************// if the game loser does not have 0 health, move to 0 healthlonggameWinnerDbid=_gameSceneManager.GameProgressData.GameWinnerPlayerDbid;UpdateHealth(gameWinnerDbid,-HealthBarView.MaxValue);stringgameWinnerName;if(_gameSceneManager.MultiplayerSession.IsLocalPlayerDbid(gameWinnerDbid)){gameWinnerName=GetPlayerNameByIndex(TBFConstants.PlayerIndexLocal);//Local winnerSoundManager.Instance.PlayAudioClip(SoundConstants.GameOverWin);_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexLocal].PlayAnimationWin();_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexRemote].PlayAnimationLoss();}else{gameWinnerName=GetPlayerNameByIndex(TBFConstants.PlayerIndexRemote);//Remote winnerSoundManager.Instance.PlayAudioClip(SoundConstants.GameOverLoss);_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexLocal].PlayAnimationLoss();_gameSceneManager.GameUIView.AvatarViews[TBFConstants.PlayerIndexRemote].PlayAnimationWin();}_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_Ending,_gameSceneManager.GameProgressData.CurrentRoundNumber,gameWinnerName),TMP_BufferedText.BufferedTextMode.Queue);awaitSetGameState(GameState.GameEnded);break;caseGameState.GameEnded:// **************************************// Game stays here. // User must click "Back" buton//// NOTE: We come here from GameState.GameEnding and/or when a player disconnects//// **************************************//Turn off buttons. We may come here from any state, if a player disconnects_gameSceneManager.GameUIView.MoveButtonsCanvasGroup.interactable=false;break;default:SwitchDefaultException.Throw(_gameState);break;}},newSystem.Diagnostics.StackTrace(true));}/// <summary>/// Decrements the LOSERS health/// </summary>/// <param name="currentRoundWinnerPlayerDbid"></param>/// <param name="deltaHealth"></param>privatevoidUpdateHealth(longcurrentRoundWinnerPlayerDbid,intdeltaHealth){if(_gameSceneManager.MultiplayerSession.IsLocalPlayerDbid(currentRoundWinnerPlayerDbid)){_gameSceneManager.GameUIView.AvatarUIViews[TBFConstants.PlayerIndexRemote].HealthBarView.Value+=deltaHealth;}else{_gameSceneManager.GameUIView.AvatarUIViews[TBFConstants.PlayerIndexLocal].HealthBarView.Value+=deltaHealth;}}privateasyncTaskRenderPlayerMove(intplayerIndex,GameMoveTypegameMoveType){stringplayerName=GetPlayerNameByIndex(playerIndex);_gameSceneManager.SetStatusText(string.Format(TBFConstants.StatusText_GameState_PlayerMoved,playerName,gameMoveType),TMP_BufferedText.BufferedTextMode.Queue);AvatarViewavatarView=_gameSceneManager.GameUIView.AvatarViews[playerIndex];avatarView.PlayAnimationByGameMoveType(gameMoveType);// 1 Unity needs time to START non-IDLE animation ...awaitAwait.While(()=>{returnavatarView.IsIdleAnimation;});// 2 Unity needs time to RETURN to the IDLE animation ...awaitAwait.While(()=>{return!avatarView.IsIdleAnimation;});}privatestringGetPlayerNameByPlayerDbid(longcurrentRoundWinnerPlayerDbid){if(_gameSceneManager.MultiplayerSession.IsLocalPlayerDbid(currentRoundWinnerPlayerDbid)){returnGetPlayerNameByIndex(TBFConstants.PlayerIndexLocal);}else{returnGetPlayerNameByIndex(TBFConstants.PlayerIndexRemote);}}privatestringGetPlayerNameByIndex(intplayerIndex){return_gameSceneManager.Configuration.AvatarDatas[playerIndex].Location;}privatevoidDebugLog(stringmessage){if(TBFConstants.IsDebugLogging){Debug.Log(message);}}}}
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.
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 enemy?
4. Stop the Scene
• Unity → Edit → Stop
Code
Here are a few highlights from the project's major calls to Beamable's Multiplayer <>.
<>
Create Connection
Here the TBFMultiplayerSession.cs class creates a new connection with Beamable's Multiplayer back-end.
1 2 3 4 5 6 7 8 9101112
publicvoidInitialize(){// Create Multiplayer Session_simClient=newSimClient(newSimNetworkEventStream(_matchId),FramesPerSecond,TargetNetworkLead);// Handle Common Events_simClient.OnInit(SimClient_OnInit);_simClient.OnConnect(SimClient_OnConnect);_simClient.OnDisconnect(SimClient_OnDisconnect);_simClient.OnTick(SimClient_OnTick);}
Send Event Object
Here the GameSceneManager.cs class sends a multiplayer event object to Beamable's Multiplayer back-end.
Here the GameSceneManager.cs class receives a multiplayer event object from Beamable's Multiplayer back-end.
1 2 3 4 5 6 7 8 910
privateasyncvoidMultiplayerSession_OnGameMoveEvent(GameMoveEventgameMoveEvent){if(_gameStateHandler.GameState==GameState.RoundPlayerMoving){//Add each player event to a list_gameProgressData.GameMoveEventsThisRoundByPlayerDbid[gameMoveEvent.PlayerDbid]=gameMoveEvent;await_gameStateHandler.SetGameState(GameState.RoundPlayerMoved);}}
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
• Select the Configuration asset in the Unity Project Window • View the serialized fields in the Unity Inspector • Can you make the game play faster? Slower? Other results?
Note: Most changes can be done while the game is running. However, if a change does not appear to work, stop and play Unity.
Intermediate
Game
Add State Design Pattern
• Create a new GameStateMachine.cs class • Remove GameStateHandler references from GameSceneManager.cs • Integrate 'GameStateMachine.cs' within 'GameSceneManager.cs\`
Note: For ease of understandability and readability, the sample project uses a very light version of the State Pattern. See State Design Pattern for more info.
Advanced
Game
Add a "Dodge" move type
• Offer this as a 4th button to the user during game play • Limit it to 1 time per game • Represent it visually (create a new animation or move the avatar model in the X direction in space to mimic a side-step • Update the evaluation logic so this does no damage to either player and simply 'skips' the turn
Note: Use this as inspiration, or create your own new move type with other results. Have fun!