This document is about: QUANTUM 2
SWITCH TO

Example using pre-built UI


Available in the Gaming Circle and Industries Circle
Circle

This is simple tutorial guide to demonstrate how to implement core tournament loop into your game. It uses pre-built UI screen available in tournament-sdk unity plugin.

Importing demo scene

Import folder TournamentSDK_Demo from unity plugin package. This folder contains relevant UI scripts, prefabs and scene that will help you to visualize tournament metadata.

import demo scene image

After successful import open Demo scene located in TournamentSDK_Demo/Scenes/Demo.unity. You can run the scene. Fill in your game client id and initialize tournament-sdk client. Fill in your nickname and login using anonymous provider (make sure that anonymous login provider is enabled in your dashboard). You should be able at this point to browse recent tournaments and interact with them.

Manual client initialization

In this example we want to initialize tournament-sdk only after user chose nickname and Photon has successfully connected to servers.

Add the BackboneManager script into your scene object. Untick the box which says Initialize on start.

add backbone manager image
set backbone manager image
Also add `ResourceCache` script into same scene object.
add resource cache image

Create new script called BackboneIntegration.

add backbone integration image

Open this script and implement client initialization flow.

C#

using Gimmebreak.Backbone.User;
using System.Collections;
using UnityEngine;

public class BackboneIntegration : MonoBehaviour {

    private WaitForSeconds waitOneSecond = new WaitForSeconds(1);

    private IEnumerator Start()
    {
        // wait until player nick was set (this happens on initial screen)
        while (string.IsNullOrEmpty(PhotonNetwork.player.NickName))
        {
            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
        string arbitraryId = "1000000000000000000000000000000000000000000000000000000000000001" + PhotonNetwork.player.NickName;
        // 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, PhotonNetwork.player.NickName, arbitraryId));
            if (BackboneManager.IsUserLoggedIn)
            {
                Debug.LogFormat("Backbone user({0}) logged in.", BackboneManager.Client.User.UserId);
            }
            else
            {
                Debug.LogFormat("Backbone user failed to log in.");
            }
        }
    }
}

Show upcoming tournament

Once user is successfully logged in you can refresh tournament list.

C#

// refresh tournament list
BackboneManager.Client.LoadTournamentList()
    // add finish callback
    .FinishCallback(() => { /* List is loaded */ })
    // run on 'this' MonoBehaviour
    .Run(this);

You can use BackboneManager.Client.Tournaments.UpcomingTournament property to get next tournament for user.

upcoming tournament image

C#

var tournament = BackboneManager.Client.Tournaments.UpcomingTournament;
this.tournamentName.text = tournament.TournamentName;
this.tournamentDate.text = tournament.Time.ToLocalTime().ToString("'<b>'dd. MMM'</b>' HH:mm");
this.tournamentTicket.text = string.Format("{0}/{1} | {2}",
                                           tournament.CurrentInvites,
                                           tournament.MaxInvites,
                                           GetSignupStatus());

Copying tournament hub screen

In this example we want to use some of the default UI screens provided in tournament-sdk. The most essential screens to implement are TournamentListScreen and TournamentHubScreen.

Copy TournamentHubScreen into your UI hierarchy. This screen shows all tournament details to user and acts as a tournament play hub.

import tournament hub image

To initialize TournamentHubScreen a correct tournament id has to be set before the UI object is enabled. Create a reference to GUITournamentHubScreen that can be found on TournamentHubScreen object and call Initialize(long tournamentId).

C#

// This is inside UI container script that controls and shows UI panel (not part of tournament-sdk)

// Reference to imported tournament hub screen script
[SerializeField]
private GUITournamentHubScreen tournamentHubScreen;
//...
// Show tournament hub for specific tournament id
public static void ShowScreen(long tournamentId)
{
    // Check if tournament is present
    var tournament = BackboneManager.Client.Tournaments.GetTournamentById(tournamentId);
    if (tournament != null)
    {
        initializedTournamentId = tournamentId;
        // Initialize tournament hub screen for correct tournament id
        Instance.tournamentHubScreen.Initialize(tournamentId);
        // Enable UI object
        ShowScreen();
    }
}

This method can be now called from upcoming tournament widget we created in previous step.

C#

// This is inside UI container script that controls and shows UI panel (not part of tournament-sdk)

// Open tournament hub for upcoming tournament
public void OpenTournamentHub()
{
    if (BackboneManager.IsUserLoggedIn)
    {
        // Check if upcomning tournament is present
        var upcomingTournament = BackboneManager.Client.Tournaments.UpcomingTournament;
        if (upcomingTournament != null)
        {
            // Hide main screen
            LobbyMain.HideScreen();
            // Show tournament hub
            // Note: UITournamentHub is not part of tournament-sdk, it is a wrapper
            // around TournamentHubScreen
            UITournamentHub.ShowScreen(upcomingTournament.Id);
            LobbyAudio.Instance.OnClick();
        } 
    }
}

Note: This example will not use TournamentListScreen as it shows only upcoming tournament for simplicity.

Implementing tournament match handler

Now an interface between tournament hub and games lobby/room creation api has to be made. Create new script called TournamentMatchHandler. Open this script and derive it from TournamentMatchCallbackHandler. It provides simple set of methods to communicate your lobby/room state to tournament hub.

C#

public bool IsConnectedToGameServerNetwork()
{
    //Check if client is successfully connected to your networking backend.
    //Return true if user is connected and ready to join given match.
}

public bool IsUserConnectedToMatch(long userId)
{
    //Check if specific user is already connected to lobby/room.
    //Return true if user is connected.
}

public void OnJoinTournamentMatch(Tournament tournament, TournamentMatch match, ITournamentMatchController controller)
{
    //Callback from tournament hub passing tournament, match and controller object.
    //Use match data to join correct lobby/room.
    //Use controller to inform tournament hub about changes in your lobby/room.
}

public bool IsUserReadyForMatch(long userId)
{
    //Check if specific user is ready (e.g. moved to correct slot)
    //Return true if user is ready to start.
}

public bool IsGameSessionInProgress()
{
    //Check if game session is already in progress for given tournament match.
    //Return true if game session is in progress.
}

public void OnLeaveTournamentMatch()
{
    //Callback from tournament hub informing user should leave joined lobby/room.
}

public void StartGameSession(IEnumerable<TournamentMatch.User> checkedInUsers)
{
    //Callback from tournament hub requesting game session to start immediately. Also
    //passing users that successfully checked in for current match.
    //Create tournament game session, and start your game.
    //This might be called multiple times until IsGameSessionInProgress returns true.
}

Methods above are executed in following order:

  1. OnJoinTournamentMatch()
  2. IsConnectedToGameServerNetwork()
  3. IsUserConnectedToMatch()
  4. IsGameSessionInProgress()
  5. IsUserReadyForMatch()
  6. StartGameSession()

OnJoinTournamentMatch

Callback from tournament hub passing tournament, match and controller object. Use match data to join correct lobby/room. Use controller to inform tournament hub about changes in your lobby/room.

C#

public override void OnJoinTournamentMatch(Tournament tournament, TournamentMatch match, ITournamentMatchController controller)
{
    // User is requesting to join a tournament match, create or join appropriate room.
    // You can use match.Secret as room id.
    this.tournament = tournament;
    this.tournamentMatch = match;
    this.tournamentMatchController = controller;
    this.sessionStarted = false;
    this.creatingSession = false;
    // Forward UserId & TeamId to Quantum player
    PlayerData.Instance.BackboneUserId = BackboneManager.Client.User.UserId;
    PlayerData.Instance.BackboneTeamId = match.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId;
    // Join Photon room
    StartCoroutine(JoinRoomRoutine());
}

private IEnumerator JoinRoomRoutine()
{
    while (this.tournamentMatch != null)
    {
        // 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"];
        // ...
        // PhotonNetwork.ConnectToRegion(region, gameVersion);
        // ...
        // wait until connected to proper region
        // ...
        // continue connecting to room

        // If tournament match is finished then leave.
        if (this.tournamentMatch.Status == TournamentMatchStatus.MatchFinished ||
            this.tournamentMatch.Status == TournamentMatchStatus.Closed)
        {
            // Check if connected room is for finished match
            if (PhotonNetwork.inRoom &&
                PhotonNetwork.room.Name == this.tournamentMatch.Secret)
            {
                PhotonNetwork.LeaveRoom(false);
            }
        }
        // Try to connect to tournament match room
        else if (PhotonNetwork.connectedAndReady &&
                 !PhotonNetwork.inRoom &&
                 !this.connectingToRoom)
        {
            this.connectingToRoom = true;
            // Set player propery with UserId so we can identify users in room
            SetPlayerProperty("BBUID", BackboneManager.Client.User.UserId);
            RoomOptions roomOptions = new RoomOptions();
            roomOptions.IsVisible = false;
            // Set max players for room based on tournament phase setting
            roomOptions.MaxPlayers = (byte)(this.tournament.GetTournamentPhaseById(this.tournamentMatch.PhaseId).MaxTeamsPerMatch * this.tournament.PartySize);
            // Join or create Photon room with tournamemnt match secret as room id
            PhotonNetwork.JoinOrCreateRoom(this.tournamentMatch.Secret, roomOptions, TypedLobby.Default);
        }
        // If we are in wrong room then leave
        else if (PhotonNetwork.inRoom &&
                 PhotonNetwork.room.Name != this.tournamentMatch.Secret)
        {
            PhotonNetwork.LeaveRoom(false);
        }

        yield return this.waitOneSec;
    }
}

This method should initialize your TournamentMatchHandler and start connection to appropriate lobby/room. This is only called once after user makes confirmation that he is ready for next match.

IsConnectedToGameServerNetwork

Callback from tournament hub to check if client is successfully connected to your networking backend. Return true if user is connected and ready to join given match.

C#

public override bool IsConnectedToGameServerNetwork()
{
    // Check if user is connected to photon and ready to join a room.
     return PhotonNetwork.connectedAndReady;
}

IsUserConnectedToMatch

Callback from tournament hub to check if specific user is already connected to lobby/room. Return true if user is connected. Notice that player property was set with UserId before joining photon room. This player property is used for identification of connected photon players.

C#

public override bool IsUserConnectedToMatch(long userId)
{
    // Check if tournament match user is connected to room.
    // Before user joined room, photon player property BBUID was set with users id.
    var photonPlayer = GetPhotonPlayerByBackboneUserId(userId);
    return photonPlayer != null;
}

private PhotonPlayer GetPhotonPlayerByBackboneUserId(long userId)
{
    if (PhotonNetwork.inRoom)
    {
        for (int i = 0; i < PhotonNetwork.playerList.Length; i++)
        {
            long playerUserId;
            if (TryGetPlayerProperty(PhotonNetwork.playerList[i], "BBUID", out playerUserId) &&
                userId == playerUserId)
            {
                return PhotonNetwork.playerList[i];
            }
        }
    }
    return null;
}

This method is called for every user that is expected to be in connected tournament match.

IsGameSessionInProgress

Callback from tournament hub to check if game session is already in progress for given tournament match. Return true if game session is in progress.

C#

public override bool IsGameSessionInProgress()
{
    // Determine if tournament match session has started.
    return sessionStarted;
}

In this example we set sessionStarted to true after successful call to BackboneManager.Client.CreateGameSession().

IsUserReadyForMatch

Callback from tournament hub to check if specific user is ready (E.g. moved to correct slot). Return true if user is ready to start. Local user that is not checked in for the match yet, will be checked in only after returning true.

C#

public override bool IsUserReadyForMatch(long userId)
{
    // Return true when user loaded/set everything neccsary for
    // match to start (if user input is required this should be time limited).
    return true;
}

In this example we don't need anything to be set for user once it's connected to the room, so we return true by default.

StartGameSession

Callback from tournament hub requesting game session to start immediately. Also passing users that successfully checked in for current match. Create tournament game session, and start your game. This might be called multiple times until IsGameSessionInProgress returns true.

C#

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
    // IsGameSessionInProgress returns true.

    // Check if session has started
    if (sessionStarted)
    {
        return;
    }

    // Check if Photon is still connected to room and ready
    if (!PhotonNetwork.connectedAndReady ||
        !PhotonNetwork.inRoom)
    {
        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;
                        // Set room properties
                        var ht = new ExitGames.Client.Photon.Hashtable();
                        ht.Add("SESSIONID", gameSession.Id);
                        ht.Add("TOURNAMENTID", this.tournament.Id);
                        ht.Add("TOURNAMENTMATCHID", this.tournamentMatch.Id);
                        PhotonNetwork.room.SetCustomProperties(ht);
                        // At this point you can also initiate scene loading
                        // and game session start
                    }
                })
            .Run(this);
    }
}

NOTE: it is also important to use reporting methods on tournamentMatchController that is passed in OnJoinTournamentMatch. Call these controller methods when specific events occur in connected lobby/room. Tournament hub is using these to determine when to refresh metadata. Failing to do so can lead into inconsistencies where one client start match but others do not (e.g. other client thinks user did not check in yet).

Example of using Photon room callbacks to report changes to tournamentMatchController:

C#

//Photon callback when new player joined room
public void OnPlayerEnteredRoom(Player newPlayer)
{
    long userId;
    //extract user id from player custom properties
    if (this.tournamentMatchController != null &&
        TryGetPlayerBackboneUserId(newPlayer, out userId))
    {
        //report user who joined room
        this.tournamentMatchController.ReportJoinedUser(userId);
    }
}

//Photon callback when player disconnected from room
public void OnPlayerLeftRoom(Player otherPlayer)
{
    long userId;
    //extract user id from player custom properties
    if (this.tournamentMatchController != null &&
        TryGetPlayerBackboneUserId(otherPlayer, out userId))
    {
        //report user who disconnected from room
        this.tournamentMatchController.ReportDisconnectedUser(userId);
    }
}

//Photon callback when room properties are updated
public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
{
    if (this.tournamentMatchController != null)
    {
        //reporting status change will refresh match metadata
        this.tournamentMatchController.ReportStatusChange();
    }
}

//Photon callback when player properties are updated
public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
    if (this.tournamentMatchController != null)
    {
        //reporting status change will refresh match metadata
        this.tournamentMatchController.ReportStatusChange();
    }
}

Attach tournament match handler

Add created TournamentMatchHandler to the scene object. (E.g. next to TournamentHubScreen) Then add object reference of TournamentMatchHandler to GUITournamentActiveMatch script, field MatchHandler. This script can be found on object TournamentHubScreen/Canvas/ActiveMatchContainer.

add match handler image

Implementing result submission

When a game session is finished final placements has to be set. Create a new game session object or use the one obtained with BackboneManager.Client.CreateGameSession() before game session started. You can also submit a custom stats with game session result.

C#

List<GameSession.User> users = new List<GameSession.User>();
Dictionary<long, int> kills = new Dictionary<long, int>();
Dictionary<long, int> deaths = new Dictionary<long, int>();
// Iterate through game players(robots) and gather placements and stats
for (int i = 0; i < sortedRobots.Count; i++)
{
    // Get quantum runtime player
    var runtimePlayer = QuantumGame.Instance.Frames.Current.GetPlayerData(((Robot*)sortedRobots[i])->Player);
    if (runtimePlayer != null)
    {
        // Get players BackboneUserId & match TeamId
        long userId = (long)runtimePlayer.BackboneUserId;
        byte teamId = runtimePlayer.BackboneTeamId;
        // Create a new game session user and assign a final placement 
        // (1-X, one being the best)
        users.Add(new Gimmebreak.Backbone.GameSessions.GameSession.User(userId, teamId) { Place = (i + 1) });
        // Get players kills stat
        kills.Add(userId, ((Robot*)sortedRobots[i])->Score.Kills);
        // Get players death stat
        deaths.Add(userId, ((Robot*)sortedRobots[i])->Score.Deaths);
    }
}
// Create a game session object to be submitted
Gimmebreak.Backbone.GameSessions.GameSession gameSession = new Gimmebreak.Backbone.GameSessions.GameSession(gameSessionId, 0, users, tournamentMatchId);
// Set a play date & session time
gameSession.PlayDate = ServerTime.UtcNow;
gameSession.PlayTime = gameTime;
// Add game session stats
gameSession.Users.ForEach(user =>
                          {
                              gameSession.AddStat(1, user.UserId, kills[user.UserId]);
                              gameSession.AddStat(2, user.UserId, deaths[user.UserId]);
                          });

Once game session object is populated you can submit it with BackboneManager.Client.SubmitGameSession(gameSession);

C#

 private IEnumerator ProcessResult(long tournamentId, GameSession finishedGameSession)
 {       
     var tournament = BackboneManager.Client.Tournaments.GetTournamentById(tournamentId);
     var tournamentMatch = tournament.UserActiveMatch;
     //report game session
     yield return BackboneManager.Client.SubmitGameSession(finishedGameSession);
     //refresh tournament data
     yield return BackboneManager.Client.LoadTournament(tournamentId);
     //check if tournament match was not finished (if not another game should be played)
     bool initializeNextGame = tournamentMatch != null &&
         tournamentMatch.Status != TournamentMatchStatus.MatchFinished &&
         tournamentMatch.Status != TournamentMatchStatus.Closed;
 }

You can reload tournament data after result submission to see if UserActiveMatch is closed or finished. If user active match is not finished it means another game session should follow. (E.g. best of three match) Create a new game session with BackboneManager.Client.CreateGameSession() and repeat the cycle.

Finalizing tournament core loop

When UserActiveMatch is finished or closed after last result submission it means user should return back to TournamentHubScreen. There he can see current stats and progress chage after finished match. User will not be immediately moved to another match without explicit confirmation action "ready for next match". When user confirms he is ready for next match system assigns another UserActiveMatch and cycle repeats until tournament is finished.

Back to top