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.

QSetUp

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

  1. Create new script called UITournamentList.

  2. Open new script and copy paste following code that is necessary to direct player from TournamentListScreen to MainMenu or TournamentHubScreen:

    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

  1. Create new script called UITournamentHub.

  2. Open new script and copy paste following code that is necessary to direct player from TournamentHubScreen to TournamentListScreen:

    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

  1. Open script called UIConnect.

  2. Add variable to store GUITournamentHubScreen crucial for tournament initialization:

    [SerializeField] private GUITournamentHubScreen TournamentHubScreen;
    
  3. 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

  1. In Hierarchy go to UICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel.

  2. Duplicate last element called ReconnectButton.

  3. Rename it to TournamentButton.

    TSDKUI

  4. Go TournamentButton > ButtonText and change Text component to TOURNAMENTS.

Tournament List & Tournament Hub

  1. Add two new objects to UICanvas scene.

  2. Rename them to TournamentListUI and TournamentHubUI.

  3. Drag&Drop TournamentListScreen and TournamentHubScreen respectively.

  4. Make sure TournamentListUI > TournamentListScreen > SubScreen is Enabled.

    TSDKUI

  5. Find Rect transform component and reset Left/Right/Bottom/Top to 0.

  6. Set scale for X/Y/Z to 1.

    TSDKUI

Set Up UI

  1. Add newly created scripts UITournamentList and UITournamentHub to Menu object in Hierarchy to make them accessible.

  2. Drag&Drop TournamentListUI into UITournamentList > Panel component.

  3. Drag&Drop TournamentHubUI into UITournamentHub > Panel component.

    TSDKUI

TournamentButton

To make TournamentButton direct player from MainMenu to TournamentListScreen:

  1. Go to UICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel > TournamentButton.

  2. In Button component change first OnClick action from UIConnect.OnReconnectClicked to UIConnect.OnTournamentClicked.

    TSDKUI

TournamentList to TournamentHub

To allow TournamentList direct player to TournamentHub:

  1. Drag&Drop TournamentHubUI > TournamentHubScreen > SubScreen to TournamentListUI > TournamentListScreen > SubScreen in GUITournamentListScreen > TournamentHubScreen component.

    TSDKUI

  2. Go to UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Scroll View > Viewport > Content > TournamentListItem > ButtonUnderline(Open) find Button component.

  3. Add new OnClick() action, drag&drop Menu into empty field under Runtime Only, change No Function to UITournamentList.OnPlayOpenTournamentHub

    TSDKUI

  4. Repeat this process for TournamentHubUI as well.

TournamentList to MainMenu

To allow TournamentList direct player back to MainMenu:

  1. Go to UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Title > ButtonUnderline(Back), find Button component.

  2. Remove existing action and add three new actions.

  3. For first action, drag&drop Menu into empty field under Runtime Only and set it to UITournamentList.OnGetBackToMainMenu.

  4. For second action, drag&drop Directional Light into empty field under Runtime Only and set it to GameObject.SetActive and make sure to enable it.

  5. For third action, drag&drop UICanvasCustom into empty field under Runtime Only and set it to GameObject.SetActive and make sure to enable it.

    TSDKUI

TournamentHub to TournamentList

  1. Drag&Drop TournamentListUI > TournamentListScreen > SubScreen to TournamentHubUI > TournamentHubScreen > SubScreen in GUISubScreen > ReturnScreen component.

    TSDKUI

  2. Go to UICanvas > Menu > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > Title > ButtonUnderline(Back), find Button component.

  3. Add new action, drag&drop Menu into empty field under Runtime Only and set it to UITournamentHub.OnGetBackToTournamentList.

    TSDKUI

Backbone Integration

To initialize TournamentList we need to create an object that will do it on every launch of the game.

  1. Add new GameObject to UICanvas and rename it to BackboneIntegration.

  2. Add BackboneManager and Resource Cache components to it.

  3. Create new script called BacboneIntegration and 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;
            }
        }
    }
    
  4. Get back to the scene and find BackboneIntegration component. Add TournamentButton to the missing field in component.

    TSDKUI

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.

  1. Open Quantum part of the project. Go to project location, there must be folder called quantum_code open it as a project in an editor.

  2. Open RuntimePlayer.User and 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);
            }
        }
    }
    
  3. Go to gameSession.qtn and add following variable to component to store list of players who have been destroyed by bomb explosion during the match.

  4. Set list as [AllocateOnComponentAdded] which will allocate it on every simulation.

    [AllocateOnComponentAdded] list<PlayerRef> Placements;
    
  5. Find BomberSystem and 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);
                }
            }
        }
    }
    
  6. 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.

  1. 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
    }
    

    UIGame is responsible to track game state.

  2. Add Disconnect button 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;
    
  3. 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;
    }
    

    UIGameStats is responsible for the end of the game

  4. Add variable to track if result procession started:

    private bool resultProcessingStarted = false;
    
  5. Then in UpdateUI method change Ending case 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;
    
  6. 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.

  7. Go to UICanvas > Game there is empty field OnLeaveButton, drag&drop UICanvas > Game > Panel > DisconnectButton into empty field.

    TSDKUI

Implementing Tournament Match Handler

  1. In Hierarchy go to UICanvas > TournamentHubUI > TournamentHubScreen > Canvas at the very bottom of it, create a new game object and rename it to TournamentMatchHandler.

  2. Create new script called TournamentMatchHandler and add this script as component to TournamentMatchHandler object.

  3. 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)
        {
        }
    }
    
  4. Get back to Unity project, find UICanvas > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > ActiveMatchContainer, find component GUITournamentActiveMatch there will be empty field MatchHandler drag&drop TournamentMatchHandler into it.

    TSDKUI

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

  1. Go to https://www.tournament-sdk.com/tournaments

  2. There you will see No tournament templates .Create your first template to get started.

  3. Press Create your first template.

    TTemplate

Edit template

  1. Tournament template will appear. Select Edit template.

    TTemplate

  2. Go to Description and set Tournament Name.

    TTemplate

  3. Go to Registration set:

  • Maximum players - 2

  • Party(team) size - 1

  • Registration rules -> Open to everyone

    TTemplate

  1. Go to Format/Add Phase

In Format set:

  • Teams - 2
  • Min teams per match - 2
  • Max Teams per match - 2

Leave field Max loses in Scores empty

In Rounds set:

  • Type - BO3
  • Minimum game time (minutes) - 2
  • Maximum round time (minutes) - 8
  1. On the bottom of the screen you should see Careful - you have unsaved changes!, press Save Changes.

    TTemplate

Start tournament

  1. Get back to Tournament templates page.

  2. Press Schedule, set Time to Your current time + 5 minutes and press Start tournament.

    TTemplate

Final test

  1. Now get back to Unity and start the project.

  2. Press Tournaments button that was added earlier. You should see TournamentListScreen and the tournament that you just added.

    FinalTest

    Tournament may be unavailable to register for some time, wait until Sign up button is available to register to tournament.

    FinalTest

  3. Repeat the process in build version

  4. When tournament will start, button Ready to play will become available.

    FinalTest

  5. Press on both Unity and Build Ready to play.

    FinalTest

    FinalTest

  6. Finish the match on both players.

  7. Get back to: https://www.tournament-sdk.com/schedule

  8. You should see tournament.

    FinalTest

  9. Click on it, to see more information about the tournament and played matches.

  10. To check more detailed information about every match played during the tournament, go to Phase 1.

  11. Press Show matches to the right from any participant.

  12. Click Show details for more information.

    FinalTest

  13. Click Game #ID and Stats for more information.

    FinalTest