Quantum
Bomberman (ARENA)
Step by step instruction of recreation of the project
Set up the project
Set up Quantum
To set up the Quantum App-ID follow the instructions on following link: https://doc.photonengine.com/quantum/current/quantum-100/quantum-101#step_3_create_and_link_a_quantum_appid
May be that instead of App Settings > App Id Realtime you will have to go App Settings > App Id.

Tournament-SDK UI implementation
Start by importing Tournament-SDK to your Unity project.
To set up UI to direct player among MainMenu, TournamentListScreen and TournamentHubScreen, first we are going to add necessary changes/additions to the game code.
Prepare Scripts to implement Tournament-SDK UI
UITournamentList
-
Create new script called
UITournamentList. -
Open new script and copy paste following code that is necessary to direct player from
TournamentListScreentoMainMenuorTournamentHubScreen:using Quantum.Demo; public class UITournamentList : UIScreen<UITournamentList> { public void OnGetBackToMainMenu() { HideScreen(); // Hide Tournament List Screen UIConnect.ShowScreen(); // Show Main Menu screen } public void OnPlayOpenTournamentHub() { HideScreen(); // Hide Tournament List Screen UITournamentHub.ShowScreen(); // Show Tournament Hub Screen } }
UITournamentHub
-
Create new script called
UITournamentHub. -
Open new script and copy paste following code that is necessary to direct player from
TournamentHubScreentoTournamentListScreen:using Quantum.Demo; public class UITournamentHub : UIScreen<UITournamentHub> { public void OnGetBackToTournamentList() { HideScreen(); // Hide Tournament Hub Screen UITournamentList.ShowScreen(); // Show Tournament List Screen } }
Prepare code to implement Tournament-SDK UI
-
Open script called
UIConnect. -
Add variable to store
GUITournamentHubScreencrucial for tournament initialization:[SerializeField] private GUITournamentHubScreen TournamentHubScreen; -
Add two following methods:
// Directs player from MainMenu to TournamentListScreen public void OnTournamentClicked() { HideScreen(); // Hide MainMenu UITournamentList.ShowScreen(); // Show Tournament List Screen } // Method that will redirect player back to the TournamentHubScreen after the tournament match has been finished public void ReturnToTournament(long tournamentId) { HideScreen(); // Hide MainMenu TournamentHubScreen.Initialize(tournamentId); // Initialize tournament using tournamentId saved from last played tournament UITournamentHub.ShowScreen(); // Show Tournament Hub Screen }
Add UI
Tournament Button
-
In
Hierarchygo toUICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel. -
Duplicate last element called
ReconnectButton. -
Rename it to
TournamentButton.
-
Go
TournamentButton > ButtonTextand changeTextcomponent toTOURNAMENTS.
Tournament List & Tournament Hub
-
Add two new objects to
UICanvasscene. -
Rename them to
TournamentListUIandTournamentHubUI. -
Drag&Drop
TournamentListScreenandTournamentHubScreenrespectively. -
Make sure
TournamentListUI > TournamentListScreen > SubScreenis Enabled.
-
Find
Rect transformcomponent and resetLeft/Right/Bottom/Topto 0. -
Set scale for
X/Y/Zto 1.
Set Up UI
-
Add newly created scripts
UITournamentListandUITournamentHubtoMenuobject inHierarchyto make them accessible. -
Drag&Drop
TournamentListUIintoUITournamentList > Panelcomponent. -
Drag&Drop
TournamentHubUIintoUITournamentHub > Panelcomponent.
TournamentButton
To make TournamentButton direct player from MainMenu to TournamentListScreen:
-
Go to
UICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel > TournamentButton. -
In
Buttoncomponent change firstOnClickaction fromUIConnect.OnReconnectClickedtoUIConnect.OnTournamentClicked.
TournamentList to TournamentHub
To allow TournamentList direct player to TournamentHub:
-
Drag&Drop
TournamentHubUI > TournamentHubScreen > SubScreentoTournamentListUI > TournamentListScreen > SubScreeninGUITournamentListScreen > TournamentHubScreencomponent.
-
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Scroll View > Viewport > Content > TournamentListItem > ButtonUnderline(Open)findButtoncomponent. -
Add new
OnClick()action, drag&dropMenuinto empty field underRuntime Only, changeNo FunctiontoUITournamentList.OnPlayOpenTournamentHub
-
Repeat this process for
TournamentHubUIas well.
TournamentList to MainMenu
To allow TournamentList direct player back to MainMenu:
-
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Title > ButtonUnderline(Back), findButtoncomponent. -
Remove existing action and add three new actions.
-
For first action, drag&drop
Menuinto empty field underRuntime Onlyand set it toUITournamentList.OnGetBackToMainMenu. -
For second action, drag&drop
Directional Lightinto empty field underRuntime Onlyand set it toGameObject.SetActiveand make sure to enable it. -
For third action, drag&drop
UICanvasCustominto empty field underRuntime Onlyand set it toGameObject.SetActiveand make sure to enable it.
TournamentHub to TournamentList
-
Drag&Drop
TournamentListUI > TournamentListScreen > SubScreentoTournamentHubUI > TournamentHubScreen > SubScreeninGUISubScreen > ReturnScreencomponent.
-
Go to
UICanvas > Menu > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > Title > ButtonUnderline(Back), findButtoncomponent. -
Add new action, drag&drop
Menuinto empty field underRuntime Onlyand set it toUITournamentHub.OnGetBackToTournamentList.
Backbone Integration
To initialize TournamentList we need to create an object that will do it on every launch of the game.
-
Add new
GameObjecttoUICanvasand rename it toBackboneIntegration. -
Add
BackboneManagerandResource Cachecomponents to it. -
Create new script called
BacboneIntegrationand copy paste following code into it.using Gimmebreak.Backbone.User; using Quantum.Demo; using System.Collections; using UnityEngine; using UnityEngine.UI; public class BackboneIntegration : MonoBehaviour { private WaitForSeconds waitOneSecond = new WaitForSeconds(1); [SerializeField] private Button tournamentButton = default; private IEnumerator Start() { // Disable tournament button until user is logged in tournamentButton.interactable = false; // wait until player nick was set (this happens on initial screen) while (string.IsNullOrEmpty(UIConnect.Instance.Username.text)) { yield return this.waitOneSecond; } // keep trying to initialize client while (!BackboneManager.IsInitialized) { yield return BackboneManager.Initialize(); yield return this.waitOneSecond; } // create arbitrary user id (minimum 64 chars) based on nickname // ClientInfo.Username is the nickname you set on the first launch of the game string arbitraryId = "1000000000000000000000000000000000000000000000000000000000000001" + UIConnect.Instance.Username.text; // log out user if ids do not match if (BackboneManager.IsUserLoggedIn && BackboneManager.Client.User.GetLoginId(LoginProvider.Platform.Anonym) != arbitraryId) { Debug.LogFormat("Backbone user({0}) logged out.", BackboneManager.Client.User.UserId); yield return BackboneManager.Client.Logout(); } // log in user if (!BackboneManager.IsUserLoggedIn) { yield return BackboneManager.Client.Login(LoginProvider.Anonym(true, UIConnect.Instance.Username.text, arbitraryId)); if (BackboneManager.IsUserLoggedIn) { Debug.LogFormat("Backbone user({0}) logged in.", BackboneManager.Client.User.UserId); } else { Debug.LogFormat("Backbone user failed to log in."); } } if (BackboneManager.IsUserLoggedIn) { // Enable tournament button, because if user is logged in, // then all the information needed is already available tournamentButton.interactable = true; } } } -
Get back to the scene and find
BackboneIntegrationcomponent. AddTournamentButtonto the missing field in component.
Implementing Tournament logic into the Quantum game
Quantum code changes/additions
To succesfully merge Tournament logic into Quantum game, let's first prepare game to handle/carry/mantain tournament information inside the game.
-
Open Quantum part of the project. Go to project location, there must be folder called
quantum_codeopen it as a project in an editor. -
Open
RuntimePlayer.Userand copy paste following code into it:using Photon.Deterministic; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Quantum { partial class RuntimePlayer { public ColorRGBA Color; // Variables to store user's identificators for tournament public long TournamentUserId; public byte TournamentTeamId; partial void SerializeUserData(BitStream stream) { // implementation stream.Serialize(ref Color.R); stream.Serialize(ref Color.G); stream.Serialize(ref Color.B); stream.Serialize(ref Color.A); stream.Serialize(ref TournamentUserId); stream.Serialize(ref TournamentTeamId); } } } -
Go to
gameSession.qtnand add following variable to component to store list of players who have been destroyed by bomb explosion during the match. -
Set list as
[AllocateOnComponentAdded]which will allocate it on every simulation.[AllocateOnComponentAdded] list<PlayerRef> Placements; -
Find
BomberSystemand copy paste following code into it:using System.Collections.Generic; namespace Quantum { public unsafe class BomberSystem : SystemMainThreadFilter<BomberSystem.BomberFilter> { public struct BomberFilter { public EntityRef Entity; public Bomber* Bomber; public Transform2D* Transform; } public override void Update(Frame f, ref BomberFilter filter) { var gridPosition = filter.Transform->Position.RoundToInt(Axis.Both); var isInvincible = false; #if DEBUG // Used for debugging purposes isInvincible = f.RuntimeConfig.IsInvincible; #endif if (isInvincible == false && f.Grid.GetCellPtr(gridPosition)->IsBurning) { // Death animation is triggered from OnEntityDestroyed // Add to list before destroy in order to get access to players info after match has finished to process tournament results foreach (var (entity, component) in f.GetComponentIterator<GameSession>()) { f.ResolveList(component.Placements).Add(f.Get<PlayerLink>(filter.Entity).Id); } f.Destroy(filter.Entity); } } } } -
Now to apply added changes re-build the quantum project.
Game code changes/additions
UIConnecting contains method OnConnectedToMaster() that starts the game, in order to start a tournament game all players must have same room paramenters.
-
Find
OnConnectedToMaster()method and change it as shown below, so on every launch it will check if Room name has been set and connect to specific Room.public void OnConnectedToMaster() { if else ... if else if ... // Above we have unchanged part of the code var joinRandomParams = new OpJoinRandomRoomParams(); _enterRoomParams = new EnterRoomParams(); _enterRoomParams.RoomOptions = new RoomOptions(); _enterRoomParams.RoomOptions.IsVisible = true; _enterRoomParams.RoomOptions.MaxPlayers = (byte)(TournamentMatchHandler.MaxUserTournament != 0 ? TournamentMatchHandler.MaxUserTournament : Input.MAX_COUNT); _enterRoomParams.RoomOptions.Plugins = new string[] { "QuantumPlugin" }; _enterRoomParams.RoomOptions.CustomRoomProperties = new Hashtable { { "HIDE-ROOM", false }, { "MAP-GUID", defaultMapGuid }, }; _enterRoomParams.RoomOptions.PlayerTtl = PhotonServerSettings.Instance.PlayerTtlInSeconds * 1000; _enterRoomParams.RoomOptions.EmptyRoomTtl = PhotonServerSettings.Instance.EmptyRoomTtlInSeconds * 1000; Debug.Log("Starting random matchmaking"); _enterRoomParams.RoomName = string.IsNullOrEmpty(TournamentMatchHandler.RoomName) ? null : TournamentMatchHandler.RoomName; // Below we have unchanged part of the code if }UIGameis responsible to track game state. -
Add
Disconnectbutton to it, to disable it, so players couldn't spoil the process of the tournament game:// Variable that represents Disconnect button [SerializeField] private UnityEngine.UI.Button OnLeaveButton; -
Change
Update()method to:public void Update() { if (QuantumRunner.Default != null && QuantumRunner.Default.HasGameStartTimedOut) { UIDialog.Show("Error", "Game start timed out", () => { UIMain.Client.Disconnect(); }); } // Disable Disconnect button if tournament game if (TournamentMatchHandler.TournamentGame) OnLeaveButton.interactable = false; }UIGameStatsis responsible for the end of the game -
Add variable to track if result procession started:
private bool resultProcessingStarted = false; -
Then in
UpdateUImethod changeEndingcase to start result processing, right before finishing the game:case GameSessionState.Ending: if (TournamentMatchHandler.TournamentGame && !this.resultProcessingStarted) { StartCoroutine(ProcessResult(frame, gameSession.Winner)); } else if (!TournamentMatchHandler.TournamentGame) { _gameStateMessageTMP.text = gameSession.Winner.IsValid ? $"Player {gameSession.Winner._index} won!" : "DRAW!"; var timeUntilDisconnection = timer.GetRemainingTime(frame).AsFloat; // If more than 60 seconds are left until disconnection, write out counter in min + sec _timerTMP.text = timeUntilDisconnection > 60 ? $"Disconnection in {(int)timeUntilDisconnection / 60} min {(int)timeUntilDisconnection % 60} seconds" : $"Disconnection in {(int)timeUntilDisconnection} seconds"; _gameStateTMP.text = "Game Over"; if (timer.HasExpired(frame)) UIMain.Client.Disconnect(); } break; -
Add new method called
ProcessResult:private IEnumerator ProcessResult(Frame frame, PlayerRef winner) { this.resultProcessingStarted = true; List<Gimmebreak.Backbone.GameSessions.GameSession.User> users = new List<Gimmebreak.Backbone.GameSessions.GameSession.User>(); Dictionary<long, byte> results = new Dictionary<long, byte>(); Gimmebreak.Backbone.GameSessions.GameSession gameSession; List<PlayerRef> prs = new List<PlayerRef>(); foreach (var (entity1, component) in frame.GetComponentIterator<GameSession>()) { foreach (PlayerRef pr in frame.ResolveList(component.Placements)) { prs.Add(pr); } } prs.Add(winner); foreach (PlayerRef pr in prs) { RuntimePlayer runtimePlayer = frame.GetPlayerData(pr); users.Add(new Gimmebreak.Backbone.GameSessions.GameSession.User( runtimePlayer.TournamentUserId, runtimePlayer.TournamentTeamId) { Place = runtimePlayer.TournamentUserId == frame.GetPlayerData(winner).TournamentUserId ? 1 : 2 }); if (runtimePlayer.TournamentUserId == frame.GetPlayerData(winner).TournamentUserId) results.Add(runtimePlayer.TournamentUserId, 1); else results.Add(runtimePlayer.TournamentUserId, 0); } // Create new GameSession gameSession = new Gimmebreak.Backbone.GameSessions.GameSession( (long)UIMain.Client.CurrentRoom.CustomProperties["GameSessionId"], 0, users, (long)UIMain.Client.CurrentRoom.CustomProperties["TournamentMatchId"]); // Attach users and their results to current GameSession gameSession.Users.ForEach(user => { gameSession.AddStat(1, user.UserId, results[user.UserId]); }); //report game session yield return BackboneManager.Client.SubmitGameSession(gameSession); //refresh tournament data yield return BackboneManager.Client.LoadTournament((long)UIMain.Client.CurrentRoom.CustomProperties["TournamentId"]); // Leave session after result was submited TournamentMatchHandler.TournamentGame = false; UIConnect.Instance.ReturnToTournament((long)UIMain.Client.CurrentRoom.CustomProperties["TournamentId"]); UIMain.Client.Disconnect(); }As we want to disable button in
UIGame, we need to attach button to it. -
Go to
UICanvas > Gamethere is empty fieldOnLeaveButton, drag&dropUICanvas > Game > Panel > DisconnectButtoninto empty field.
Implementing Tournament Match Handler
-
In
Hierarchygo toUICanvas > TournamentHubUI > TournamentHubScreen > Canvasat the very bottom of it, create a new game object and rename it toTournamentMatchHandler. -
Create new script called
TournamentMatchHandlerand add this script as component toTournamentMatchHandlerobject. -
Copy paste following code into it:
using ExitGames.Client.Photon; using Gimmebreak.Backbone.Core; using Gimmebreak.Backbone.Tournaments; using Photon.Realtime; using Quantum; using Quantum.Demo; using System.Collections; using System.Collections.Generic; using System.Data; using System.Linq; using UnityEngine; public class TournamentMatchHandler : TournamentMatchCallbackHandler, IInRoomCallbacks { Tournament tournament; TournamentMatch tournamentMatch; ITournamentMatchController tournamentMatchController; bool sessionStarted; bool creatingSession; private WaitForSeconds waitOneSec = new WaitForSeconds(1); private string tournamentSessionName; public static byte MaxUserTournament = 0; public static string RoomName = ""; public static bool TournamentGame = false; public override void OnJoinTournamentMatch(Tournament tournament, TournamentMatch match, ITournamentMatchController controller) { // User is requesting to join a tournament match, create or join appropriate session this.tournament = tournament; this.tournamentMatch = match; this.tournamentMatchController = controller; this.sessionStarted = false; this.creatingSession = false; this.tournamentSessionName = $"{this.tournamentMatch.Secret}_{this.tournamentMatch.CurrentGameCount}"; // Join Photon session StartCoroutine(JoinRoomRoutine()); } public void OnDisable() { if (UIMain.Client != null) { UIMain.Client.RemoveCallbackTarget(this); } } private IEnumerator JoinRoomRoutine() { while (this.tournamentMatch != null) { // Use ConnectedStatus instead of UIMain.Client.State, because Client is only created after UIConnect.Instance.OnConnectClicked(); // If you require specific region for tournament, you can use // tournament custom properties providing the info about required region. // string cloudRegion = this.tournament.CustomProperties.Properties["cloud-region"]; // If tournament match is finished then leave if (this.tournamentMatch.Status == TournamentMatchStatus.MatchFinished || this.tournamentMatch.Status == TournamentMatchStatus.Closed) { // Check if connected session is for finished match if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined && UIMain.Client.CurrentRoom.Name == this.tournamentSessionName) { UIMain.Client.Disconnect(); } } // Try to connect to tournament match session else if (!(UIMain.Client != null && UIMain.Client.State == ClientState.Joined)) { // Set player propery with UserId so we can identify users in session // Set max players for session based on tournament phase setting MaxUserTournament = (byte)(this.tournament.GetTournamentPhaseById(this.tournamentMatch.PhaseId).MaxTeamsPerMatch * this.tournament.PartySize); // Join or create Photon session with tournamemnt match secret as session id RoomName = this.tournamentSessionName; PlayerDataContainer.Instance.RuntimePlayer.TournamentUserId = BackboneManager.Client.User.UserId; PlayerDataContainer.Instance.RuntimePlayer.TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId; UIConnect.Instance.OnConnectClicked(); ExitGames.Client.Photon.Hashtable customProperties = new ExitGames.Client.Photon.Hashtable() { { "TournamentUserId", BackboneManager.Client.User.UserId}, { "TournamentTeamId", this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId} }; UIMain.Client.AddCallbackTarget(this); TournamentGame = true; UIMain.Client.LocalPlayer.SetCustomProperties(customProperties); } // If we are in wrong session then leave else if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined && this.tournamentSessionName != UIMain.Client.CurrentRoom.Name) { UIMain.Client.Disconnect(); } yield return this.waitOneSec; } } public override bool IsConnectedToGameServerNetwork() { // Check if user is connected to photon and ready to join a session if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined) { return true; } return false; } public override bool IsGameSessionInProgress() { // Check if game session has started return sessionStarted; } public override bool IsUserConnectedToMatch(long userId) { // Check if tournament match user is connected to session if (UIMain.Client.CurrentRoom == null) return false; foreach (var player in UIMain.Client.CurrentRoom.Players.Values) { if (player.CustomProperties.ContainsValue(userId)) { return true; } } return false; } public override bool IsUserReadyForMatch(long userId) { // In particular case if player is connected to the match it's considered to be ready return IsUserConnectedToMatch(userId); } public override void OnLeaveTournamentMatch() { this.tournament = null; this.tournamentMatch = null; this.tournamentMatchController = null; TournamentGame = false; UIMain.Client?.RemoveCallbackTarget(this); UIMain.Client?.Disconnect(); } public override void StartGameSession(IEnumerable<TournamentMatch.User> checkedInUsers) { // Start tournament game session with users that checked in. // Be aware that this callback can be called multiple times until // sessionStarted returns true. // Check if session has started if (sessionStarted) { return; } // Check if session is not being requested if (!this.creatingSession) { this.creatingSession = true; // Create tournament game session BackboneManager.Client.CreateGameSession( checkedInUsers, this.tournamentMatch.Id, 0) .ResultCallback((gameSession) => { this.creatingSession = false; // Check if game session was created if (gameSession != null) { // Indicate that session has started this.sessionStarted = true; if (UIMain.Client.LocalPlayer.IsMasterClient) { ExitGames.Client.Photon.Hashtable hashtable = new ExitGames.Client.Photon.Hashtable(); hashtable.Add("GameSessionId", gameSession.Id); hashtable.Add("TournamentMatchId", this.tournamentMatch.Id); hashtable.Add("TournamentId", this.tournament.Id); UIMain.Client.CurrentRoom.SetCustomProperties(hashtable); UIRoom.Instance.OnStartClicked(); } UITournamentHub.HideScreen(); } }) .Run(this); } } public void OnPlayerEnteredRoom(Player newPlayer) { if (this.tournamentMatchController != null) { // Report user who joined room this.tournamentMatchController.ReportJoinedUser((long)newPlayer.CustomProperties["TournamentUserId"]); } } public void OnPlayerLeftRoom(Player otherPlayer) { { // Report user who disconnected from room this.tournamentMatchController.ReportDisconnectedUser((long)otherPlayer.CustomProperties["TournamentUserId"]); } } public void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps) { if (this.tournamentMatchController != null) { // Reporting status change will refresh match metadata this.tournamentMatchController.ReportStatusChange(); } } public void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged) { if (this.tournamentMatchController != null) { // Reporting status change will refresh match metadata this.tournamentMatchController.ReportStatusChange(); } } public void OnMasterClientSwitched(Player newMasterClient) { } } -
Get back to Unity project, find
UICanvas > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > ActiveMatchContainer, find componentGUITournamentActiveMatchthere will be empty fieldMatchHandlerdrag&dropTournamentMatchHandlerinto it.
Tournament creation & Final test
Build project
We need at least 2 players to be sure that project is working fine, so build project to create 2nd player.
Don't start it immediately, wait till we create a tournament, otherwise tournament won't be visible on TournamentListScreen.
Creating tournament template
Creating the template
-
There you will see
No tournament templates .Create your first template to get started. -
Press
Create your first template.
Edit template
-
Tournament template will appear. Select
Edit template.
-
Go to
Descriptionand setTournament Name.
-
Go to
Registrationset:
-
Maximum players- 2 -
Party(team) size- 1 -
Registration rules->Open to everyone
- Go to
Format/Add Phase
In Format set:
Teams- 2Min teams per match- 2Max Teams per match- 2
Leave field Max loses in Scores empty
In Rounds set:
Type- BO3Minimum game time (minutes)- 2Maximum round time (minutes)- 8
-
On the bottom of the screen you should see
Careful - you have unsaved changes!, pressSave Changes.
Start tournament
-
Get back to
Tournament templatespage. -
Press
Schedule, setTimetoYour current time + 5 minutesand pressStart tournament.
Final test
-
Now get back to Unity and start the project.
-
Press
Tournamentsbutton that was added earlier. You should seeTournamentListScreenand the tournament that you just added.
Tournament may be unavailable to register for some time, wait until
Sign upbutton is available to register to tournament.
-
Repeat the process in build version
-
When tournament will start, button
Ready to playwill become available.
-
Press on both Unity and Build
Ready to play.

-
Finish the match on both players.
-
Get back to: https://www.tournament-sdk.com/schedule
-
You should see tournament.

-
Click on it, to see more information about the tournament and played matches.
-
To check more detailed information about every match played during the tournament, go to
Phase 1. -
Press
Show matchesto the right from any participant. -
Click
Show detailsfor more information.
-
Click
Game #IDandStatsfor more information.