Overview

What is Tournament-SDK

Tournament SDK is solution for orchestrating real-time tournaments for your game. It provides a Unity plugin that allows your playerbase to interact, enter and compete in tournaments. All within your game client witout a players need to leave the game.

It also allows you to schedule and reccure specific tournaments, hook onto your backed for deducting entry fees and delivering prizes, and much more.

Implementation examples

Implementation example image
Implementation example image
Implementation example image
Implementation example image

Solution parts

Dashboard

Web based dashboard allows you to handle all neccesary live-ops tasks around tournaments. Creating tournament templates, scheduling tournaments, analyzing and optimizing tournament performance.

Dashboard image

Unity plugin

Easy to use Unity plug-in gives you access to all tournament metadata and neccesary orchestration tools for executing tournaments within your game client. By default it comes with example UI. These can be rebuild or not used at all in favour of custom UX/UI integration.

Code example of loading tournament data:

private IEnumerator LoadCoroutine()
{
    //load/refresh tournament list
    yield return BackboneManager.Client.LoadTournamentList();
    //get first tournament in the list
    var tournament = BackboneManager.Client.Tournaments.TournamentList[0];
    //load/refresh all tournament data
    yield return BackboneManager.Client.LoadTournament(tournament);
    //access and visualize tournament metadata
    var name = tournament.Name;
    var startTime = tournament.Time;
    var status = tournament.Status;
    var userInvite = tournament.Invite;
    //...
}

Best with Quantum

Quatum makes it easier to achieve best tournament experience for players as good implementaion of tournaments in your game should include:

  • Reconnect - players do not want to lose important matches just because they had unexpected disconnection from game server.
  • Replays - let players rewatch competitive matches so they can analyze where they made a mistake that lead to loss.
  • Spectator - give tournament admins ability to late join any match and spectate allowing to create stream coverages of the most premium tournaments in your game.

SDK & Release Notes

v1.3.x

Latest release date: 17.12.2022

Latest version v1.3.1 - Download SDK

1.3.1

Release date: 1.12.2022

-Added new merging tags for tournament webhooks.
-Added option to submit results from game server.
-Added option to search user by external id in dashboard.
-Added option to create SOLO tournament matches (solo leaderboards).
-Added option to query match data from game server.
-Added option to modify match points from predefined stats.
-Added ability to query tournament data for custom analytics.
-Added new invite option for tournament via private CODE.
-Added various QoL improvements in dashboard.
-Added webGL support.
-Added "public website" with customizable player leaderboards. 

-Fixed issue where phase would get stuck in certain situations.
-Fixed issue where optimized SE bracket would not propagate corret round settings to clients after tournament start.
-Fixed issue when calling game session create while results are being processed.
-Fixed incorrect final placement for 'dynamic brackets'.
-Fixed dashboard user search failing in certain situations.
-Fixed issue when inviting from previous tournaments where no rewards were given.
-Fixed issue when inciting by template would include canceled tournaments.
-Fixed issue caused by negative tiebreaker stats.
-Fixed issue with brackets where checked in user would not be moved forward.
-Fixed issue with brackets where user would not get points for autowin in certain situations.

-Changed sorting order on tournament list.

1.3.0

Release date: 1.6.2021

-Added new phase type 'Dynamic brackets'.
-Added default UIs for dynamic brackets.
-Added seed value from previous phase to score.
-Added methods to get phase score by party id or user id.
-Added methods to help calculate phase start and finish timestamps.
-Added methods to help calculate round start and finish timestamps.
-Added method to get start and finish deadline of next game in active match.
-Added option to vertically fill groups in round robin phase.
-Added new dashboard tournament detail view with option to search for specific user. Improved performance and usability for BIG tournaments.
-Added option to batch prize delivery into multiple server calls.
-Added merging tag for server callbacks that provide user names on specific placement in tournament (e.g. #TOURNAMENT_PARTY_1_USERNAMES# for 1st placement).
-Added option to accept replay when specific amount of replay submissions are received.
-Added dashboard callback option for discord.

-Fixed warning messages not correctly triggered on default UIs in certain situations.
-Fixed issue where in some rare cases tournament hub could get stuck in WaitingForUserReadyConfirmation state.
-Fixed issue where in some rare cases submitted results did not close match immediately.
-Fixed issue in arena matchmaking where players were not correctly matched based on their intermediate score.
-Fixed issue in arena matchmaking causing autowin streak in certain situations.
-Fixed sponsor name and image fields returning incorrect values.

-Changed 'GetMatchNextGameDeadline'. This method is now obsolete. Use 'GetMatchNextGameStartDeadline' or 'GetMatchNextGameFinishDeadline' instead.
-Changed tournament player count to be recalculated when tournament starts to reflect valid users only.
-Changed seeding for single and double elimination brackets making it more fair.
-Changed tiebreaker resolution in phase/match to use seed value from previous phase.
-Changed dashboard round time settings making it easier to edit and understand.

v1.2.x

SDK download no longer available for v1.2.x. Please update to newer version.

1.2.3

Release date: 11.11.2020

-Added setting to allow skipping phase if less players sign up.
-Added API for browsing tournament phase score.
-Added party code to be returned with tournament data.
-Added automatic round optimization for SE bracket (skipping rounds in case of less players).
-Added automatic captain flag passing in case of inactivity.
-Added setting for custom phase tiebreaker stats.
-Added game and match point distribution data on round object.
-Added default UI for browsing phase score table.
-Added dashboard time localization.
-Added dahsboard analytics view for total daily unique attendees. 
-Added dashboard tournament filter on schedule tab.

-Fixed late result submission reopening match in certain situations.
-Fixed "create game session" api causing race condition issues under high load.
-Fixed dashboard analytics overview, it no loger shows deleted templates.
-Fixed Playfab integration version setting for cloud scripts. Can be now set to "0" or "Live" to point to latest deployment.
-Fixed generating invalid JSON payload for Playfab if user id contains only numbers.
-Fixed race condition that updated player score to invalid values in certain situations.
-Fixed sign up issue via external providers where last player would be rejected.
-Fixed issue where auto wins and loses would be given earlier than round deadline in some cases.
-Fixed issue where spectators would not refresh match data and not trigger match start in certain situations.
-Fixed issue allowing to create tournament without any phase.
-Fixed issue allowing misconfiguration of tournament time deadlines.

-Changed tournament list default update interval to be less agressive.
-Changed tournament list default window to be +- 3 days.

1.2.2

Release date: 7.4.2020

-Added API for creating/joing tournament party with sharable code.
-Added default UI scripts to handle parties with sharable code.
-Added support for partially checked in team to start a match.
-Added property 'PartiallyCheckedInTeamCount' on tournament match.
-Added phase timer on default UI screens.

-Fixed misspelled text in default UI spcripts.
-Fixed stack overlow error when calling ReinitializeComponent method.
-Fixed tournament position being incorrectly shown in parties bigger than 1.
-Fixed tournament hub not setting correct state when user is knocked out early.

-Changed property 'CheckedInTeamCount' to 'FullyCheckedInTeamCount' on match.

1.2.1

Release date: 24.3.2020

-Added get season stats method accepting any platform user id (E.g. PlayFab user id).
-Added API to load specific tournament matches by their ids.
-Added reinitialization method for default UI scripts.
-Added tournament match detail dialog in default UI screens.

-Fixed single elimination bracket being incorrectly rendered with default UI screens.
-Fixed tournament hub not informing correctly about state in certain bracket situations.
-Fixed default UI scripts throwing error when reactivated without reinitialization.
-Fixed testing tournaments being included in template statistics.
-Fixed Playfab prize delivery payload to no longer use arbitrary user id.
-Fixed Playfab store import to fail when some item fields were missing/empty.

-Changed default UI script to no longer show user specific points when no party is set.

1.2.0

Release date: 12.12.2019

-Added new phase type 'Round robin'.
-Added new phase type 'Double elimination bracket'.
-Added pre-build default UI screens for new formats.
-Added new status to indicate if tournament is in progress.
-Added party invite method accepting any platform user id (E.g. PlayFab user id).
-Added API to browse all past or future tournaments on the client.

-Changed pre-build default UI screen prefabs for easier integration.

v1.1.x

SDK download no longer available for v1.1.x. Please update to newer version.

v1.1.2

Release date: 31.8.2019

-Added pre-build default UI screens.
-Added PlayFab integration.
-Added change nickname option for user.
-Added new tournament type for testing.
-Added API to browse all tournament matches on the client.
-Added 'UserMatches' property in tournament.
-Added 'AllMatches' property in tournament.
-Added decline party invite option for user.
-Added tournament hub match interface for easier hookup to room/lobby API.
-Added property to see match points rewarded for each user after match finished.

-Fixed error when parsing decimal numbers on certain localizations.
-Fixed and improved re-throwing of exceptions in nested AsyncOperation API calls.
-Fixed updating of 'CurrentGameCount' property in tournament match.
-Fixed issue where 'UserActiveMatch' had different object reference.
-Fixed tournament match having not set values of 'WinScore' and 'MaxGameCount' in certain situations. 

-Changed plugin folder structure.

-Removed 'BracketMatches' property in tournament which was replaced with new API for browsing tournament matches that covers this functionality.

v1.1.1

Release date: 17.5.2019

-Added compression option to resource cache.
-Added new tournament hub status 'KickedOutByAdmin'.
-Added interface to inject different file storage APIs for caching (E.g. Nintendo Switch).

-Fixed incorrect serialization of certain tournament data on the client.
-Fixed various small non critical issues.

v1.1.0

Release date: 29.3.2019

-Added optional resource cache script for loading url images for tournamens.
-Added checkin team count in tournament match.
-Added checkin user count in tournament match.
-Added user season profile with tournament statistics.
-Added methods to more easily get stats from game session.
-Added global custom properties for game title.
-Added language setting for user.
-Added Steam login provider.

-Fixed 'GetMatchStartDeadline' returning invalid time in certain cases.
-Fixed exception thrown sometimes during compression and decompression of replays.
-Fixed client not reporting errors during login.
-Fixed icorrect sorting of users in tournament match.

v1.0.x

SDK download no longer available for v1.0.x. Please update to newer version.

v1.0.1

Release date: 16.2.2019

-Added user reporting from the client (E.g. for suspected cheating).
-Added replay submission.
-Added custom game and match point system.
-Added FFA format for phases.
-Added max game count cap for round match.
-Added 'UserPlayedRoundCount' property informing how many rounds user already passed.
-Added 'UserIsParticipating' property for detection if user has moved to next phase.
-Added new tournament hub status 'ResolvingPartiallyFilledMatch'.
-Added idication of silent login on subsequent client initializations.

-Fixed user active match not refreshing after result submission.
-Fixed user not automatically moved to next round after overdue match.
-Fixed tournament data not being sometimes automatically refreshed after finished match.
-Fixed incorrect indication of user being knocked out in certain situations.
-Fixed pre-start check in not being applied.
-Fixed party invite receiving invalid party id.

-Changed accepting tournament party invite, it now requires tournament id parameter.

v1.0.0

Release date: 21.1.2019

-Initial release.

Example using pre-built UI

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.

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.

// 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

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).

// 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.

// 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.

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.

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.

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.

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.

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.

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.

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:

//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.

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);

 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.

QuantumBlueless

Step by step instruction of Tournament-SDK implementation

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

Set up Tournament-SDK

Import Tournament-SDK.F

To set up the Tournament-SDK Client Game ID follow the steps of Create new game and setup gameId in instruction on following link: https://www.tournament-sdk.com/docs#Create+new+game+and+setup+gameId

Tournament-SDK UI implementation

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. Drag&drop Menu into field under Runtime Only and set it to UITournamentList.OnGetBackToMainMenu.

    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()
        {
            // First way as in previous game would be to use UIConnect.Instance.Username.text
    
            // Second option is to check if PlayerPref contains saved name and use it, because what if player didn't input any text as name
            var localName = string.IsNullOrEmpty(PlayerPrefs.GetString("Quantum.Demo.UIConnect.LastUsername")) ? UIConnect.Instance.Username.text : PlayerPrefs.GetString("Quantum.Demo.UIConnect.LastUsername");
    
            // 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(localName))
            {
                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" + localName;
            // 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, localName, 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 AssetRefEntityPrototype PrototypeRef;
            public string PlayerName;
            public string BodyId;
            public long TournamentUserId;
            public byte TournamentTeamId;
    
            partial void SerializeUserData(BitStream stream)
            {
                stream.Serialize(ref PrototypeRef.Id.Value);
                stream.Serialize(ref PlayerName);
                stream.Serialize(ref BodyId);
                stream.Serialize(ref TournamentUserId);
                stream.Serialize(ref TournamentTeamId);
            }
        }
    }
  3. Now to apply added changes save them and re-build the quantum project.

Game code changes/additions

UIConnecting is script that responsible to set up room properties and start lobby, that's why it's important to set custom info about tournament room here.

Find method OnConnectedToMaster() and change as shown below:

public void OnConnectedToMaster()
{
    if
    else

    if
    else

    if

    // Above is unchanged code

    var joinRandomParams = new OpJoinRandomRoomParams();
    _enterRoomParams = new EnterRoomParams();
    _enterRoomParams.RoomOptions = new RoomOptions();
    _enterRoomParams.RoomOptions.IsVisible = true;
    _enterRoomParams.RoomOptions.MaxPlayers = 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;
    _enterRoomParams.RoomName = string.IsNullOrEmpty(TournamentMatchHandler.RoomName) ? null : TournamentMatchHandler.RoomName;

    // Below is unchanged code

    if
}

Result processing

To start result processing we have to find place where game end is being tracked.

  1. Go to ScoreboardHud script.

  2. Add variable to track if result processing has started.

    private bool processingStarted = false;
  3. Find method OnGameEnded() and change as follows:

    private unsafe void OnGameEnded(EventOnGameEnded onGameEnded)
    {
        _alwaysShow = true;
    
        if (TournamentMatchHandler.TournamentGame && !this.processingStarted) StartCoroutine(ProcessResult(QuantumRunner.Default.Game.Frames.Verified));
    }
  4. Now copy paste method ProcessResult():

    private IEnumerator ProcessResult(Frame f)
    {
        this.processingStarted = true;
    
        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 and gather placements and stats
    
        for (int i = 0; i < _sortedRobots.Count; i++)
        {
            // Using EntityRef get PlayerID which contains PlayerRef to obtain RuntimePlayer
            RuntimePlayer rp = f.GetPlayerData(f.Get<PlayerID>(_sortedRobots[i]).PlayerRef);
            // Using EntityRef of particular player get Score construct that contains Kills and Deaths
            Score score = f.Get<Score>(_sortedRobots[i]);
            long userId = rp.TournamentUserId;
            byte teamId = rp.TournamentTeamId;
    
            Debug.LogWarning($"User with ID -> {userId} # Place -> {i+1} # Kills -> {score.Kills} # Deaths -> {score.Deaths}");
            users.Add(new Gimmebreak.Backbone.GameSessions.GameSession.User(userId, teamId) { Place = (i + 1) });
            // Get players kills stat
            kills.Add(userId, score.Kills);
            // Get players death stat
            deaths.Add(userId, score.Deaths);
        }
        // Create a game session object to be submitted
        GameSession gameSession = new Gimmebreak.Backbone.GameSessions.GameSession(
            (long)UIMain.Client.CurrentRoom.CustomProperties["GameSessionId"],
            0,
            users,
            (long)UIMain.Client.CurrentRoom.CustomProperties["TournamentMatchId"]);
    
        // Add game session stats
        gameSession.Users.ForEach(user =>
        {
            gameSession.AddStat(1, user.UserId, kills[user.UserId]);
            gameSession.AddStat(2, user.UserId, deaths[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"]);
    
        //check if tournament match was not finished (if not another game should be played)
        TournamentMatchHandler.TournamentGame = false;
        UIConnect.Instance.ReturnToTournament((long)UIMain.Client.CurrentRoom.CustomProperties["TournamentId"]);
        UIMain.Client.Disconnect();
    }

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 long TournamentUserId;
        public static byte TournamentTeamId;
        public static byte MaxUserTournament;
        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)
            {
                // 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 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;
    
                    TournamentUserId = BackboneManager.Client.User.UserId;
                    TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId;
    
                    UIConnect.Instance.OnConnectClicked();
    
                    // Set player propery with UserId so we can identify users in session
                    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

QuantumBomber

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

Set up Tournament-SDK

Import Tournament-SDK.

To set up the Tournament-SDK Client Game ID follow the steps of Create new game and setup gameId in instruction on following link: https://www.tournament-sdk.com/docs#Create+new+game+and+setup+gameId

Tournament-SDK UI implementation

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

FusionKarts

Implementing Tournament-SDK into Fusion based game

This tutorial is based on:

Karts game sample available on https://doc.photonengine.com/fusion/current/game-samples/fusion-karts#download

To run Karts project you need Editor Version 2020.3.47f1 or any 2020 LTS version.

Tournament-SDK 1.3.1 is available on https://www.tournament-sdk.com/docs#-sdk-release-notes

Set up the Fussion

Open downloaded Karts project in unity.

Follow instruction in given link to set up Fussion AppId: https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/1-getting-started#step_6___create_an_app_id

Set up Tournament-SDK

Import Tournament-SDK.

To set up the Tournament-SDK Client Game ID follow the steps of Create new game and setup gameId in instruction on following link: https://www.tournament-sdk.com/docs#Create+new+game+and+setup+gameId

Setting up the scene

Add necessary objects

  1. In Project section, open Assets/Scenes -> double-click Launch.

    Launch

  2. In Hierarchy add 2 new GameObjects above Prompts, rename them to TournamentListUI and TournamentHubUI.

    SetUp

Adjust resolution

  1. Select TournamentListUI.

  2. In Rect Transform component in left top corner change adjustement to stretched.

  3. Set Left,Top,Right,Bottom parameters to 0.

    SetUp

  4. Do the same for TournamentHubUI.

Setting up the objects

  1. Add UIScreen as component to both new objects.

    SetUp

  2. Add TounamentHubScreen and TounamentListScreen to TounamentHubUI and TounamentHubUI respectively. Tournament(Hub/List)Screen can be found in Assets/TournamentSDK_Demo/Prefabs/DefaultUIScreens.

    SetUp

    Make sure SubScreen inside both TounamentHubScreen and TounamentListScreen is enabled!

    SetUp

  3. Disable TounamentListUI and TounamentHubUI.

Setting up necessary layout

  1. In Hierarchy go to -> Main Canvas/Main Menu Screen/LayoutGroup/Footer/LayoutGroup, select Exit Button and press Ctrl+D to duplicate the object.

    TButton

  2. Inspector will open right after object duplication, change button's name to Tournaments, also find Rect Transform change Width to 470.

    TButton

  3. Open newly created object in Hierarchy, select Text and change Text component to Tournaments.

    TButton

Setting up the buttons

Moving from Main Menu Screen to Tournament List UI

  1. Select Tournaments object created previosly in Hierarchy.
  2. Find Button component in Inspector.
  3. In On Click() field, drag ang drop Main Menu Screen from Hierarchy into field under Runtime Only.
  4. Change No Function to UIScreen/FocuScreen.
  5. In Hierarchy find TournamentListUI, drag and drop it into On Click() field under UIScreen.FocusScreen where None (UI Screen) is written.

    TButton

Moving from Tournament List UI to Main Menu Screen

  1. In Hierarchy go to TournamentListUI/TournamentListScreen/SubScreen/Canvas/Title.
  2. Select ButtonUnderline(Back), in it's Button component drag and drop TournamentListUI into field under Runtime Only.
  3. then change No Function to UIScreen/Back.

    TButton

Moving from Tournament List UI to Tournament Hub UI

  1. Go to TournamentListUI/TournamentListScreen/SubScreen/Canvas/Scroll View/ViewPort/Content/TournamentListItem/ButtonUnderline(Open)/(Play).

  2. Add new action to On Click().

  3. Drag TournamentListUI into field under Runtime Only.

  4. Change No Function to UIScreen/FocusScreen.

  5. Drag TournamentHubUI into field under UIScreen.FocusScreen.

    TButton

    Due to we used prefabs of TournamentListScreen and TournamentHubScreen, GUITournamentListScreen script has unset variable TournamentHubScreen.

  6. Go to TournamentListUI/TournamentListScreen/SubScreen there will be unset Tournament Hub Screen.

  7. From TournamentHubUI/TournamentHubScreen drag SubScreen into unset Tournament Hub Screen.

    TButton

Moving from Tournament Hub UI to Tournament List UI

Go to ButtonUnderline(Back) in TournamentHubUI same way as for TournamentListUI.

  1. Add another action on button by pressing +.

  2. Drag and drop TournamentHubUI into field under Runtime Only.

  3. Change No Function to UIScreen/Back.

    TButton

  4. Go to TournamentHubUI/TournamentHubScreen/SubScreen in GUISubScreen component will be unset Return Screen.

  5. From TournamentListUI/TournamentListScreen drag SubScreen into unset Return Screen field.

    TButton

Implementing Tournament-SDK

Create/Set up BackboneManager

  1. Create new GameObject on the very bottom of Main Canvas, rename it to BackboneManager.

  2. Press Add Component -> start typing Backbone Manager, it will appear in search tab, click on it.

  3. Backbone Manager component will appear in Inspector. Make sure Initialize On Start box is UN-ticked.

    BBManager

  4. Add Resource Cache component to Backbone Manager object.

Implementing client initialization flow into BackboneIntegration

  1. In Inspector of BackboneManager object select Add Component -> type BackboneIntegration -> select New Script/Create and Add.

    BBManager

  2. Add following code into BackboneIntegration script.

    using Gimmebreak.Backbone.User;
    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(ClientInfo.Username))
            {
                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" + ClientInfo.Username;
            // 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, ClientInfo.Username, 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;
            }
        }
    }

    BackboneIntegration component now misses the TournamentButton

  3. Drag Tournaments button from MainMenuScreen/LayoutGroup/Footer/LayoutGroup, into TournamentButton field in BackboneIntegration component.

    BBManager

Preparation to implement TournamentMatchHandler

Further changes are done in order to prevent any errors while implementing TournamentMatchHandler. Changes are done in game logic itself so that tournament match would have access to necessary data.

GameLauncher

Go to GameLauncher script add/change variables/methods as follows:

  1. Add SessionName variable to store name of the session, so we could gain access to it from other Game classes:

    public string SessionName
    {
        get
        {
            if (_runner != null) { return _runner.SessionInfo.Name; }
            return null;
        }
    }
  2. Add SetTournamentLobby() to set gameMode suitable for tournaments.

    // First player tries to connect to session as a Host,
    // if session is already created, then user tries to reconnect as a Client
    public void SetTournamentLobby() => _gameMode = GameMode.AutoHostOrClient;
  3. Go to JoinOrCreateLobby() method, in _runner.StartGame({...}) change:

    DisableClientSessionCreation = true

TO

```csharp
// Allows game to attempt to create a session as a Client
// Condition is false only when tournament starts the session
// False because we don't want to disable that option
DisableClientSessionCreation = _gameMode != GameMode.AutoHostOrClient
```
  1. Change SetConnectionStatus() to:

    private void SetConnectionStatus(ConnectionStatus status)
    {
        Debug.Log($"Setting connection status to {status}");
    
        ConnectionStatus = status;
    
        if (!Application.isPlaying)
            return;
    
        if (status == ConnectionStatus.Disconnected || status == ConnectionStatus.Failed)
        {
            SceneManager.LoadScene(LevelManager.LOBBY_SCENE);
            UIScreen.BackToInitial();
            // We know that GameMode.AutoHostOrClient is set only when tournament is starting the session
            // If it is tournament session, then by the end of it we want player to be directed to TournamentHubUI
            if (_gameMode == GameMode.AutoHostOrClient)
            {
                LevelManager.LoadTournamentHub();
            }
        }
    }
  2. Change OnPlayerJoined() to:

    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        if (runner.IsServer)
        {
            // GameMode.AutoHostOrClient allows tournament session Host to spawn a GameManager
            if (_gameMode == GameMode.Host || _gameMode == GameMode.AutoHostOrClient)
                runner.Spawn(_gameManagerPrefab, Vector3.zero, Quaternion.identity);
            var roomPlayer = runner.Spawn(_roomPlayerPrefab, Vector3.zero, Quaternion.identity, player);
            roomPlayer.GameState = RoomPlayer.EGameState.Lobby;
        }
        SetConnectionStatus(ConnectionStatus.Connected);
    }

LevelManager

Go to LevelManager script add/change variables/methods as follows:

  1. Add variables:

    // LevelManager will use these variables to redirect player back to TournamentHubScreen after tournament match
    [SerializeField] private UIScreen tournamentHubScreenUI;
    [SerializeField] private GUITournamentHubScreen tournamentHubScreenGUI;
    [SerializeField] private UIScreen tournamentListScreenUI;
  2. Add method LoadTournamentHub():

    public static void LoadTournamentHub() 
    {
        // Redirect player to TournamentListUI and then to TournamentHubUI to keep correct chronology
        UIScreen.Focus(Instance.tournamentListScreenUI);
        Instance.tournamentHubScreenGUI.Initialize(GameManager.Instance.TournamentId);
        UIScreen.Focus(Instance.tournamentHubScreenUI);
    }

GameManager

Go to GameManager script add/change variables/methods as follows:

  1. Add variables:

    // Values to personalize tournament match and later proceed it's statistics  
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long GameSessionId { get; set; }
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long TournamentId { get; set; } = 0;
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long TournamentMatchId { get; set; }
  2. Add method:

    // Allows us to chech if game is a tournament game
    public bool IsTournamentGame() => this.isActiveAndEnabled && this.TournamentId != 0;

ClientInfo

Go to ClientInfo script add/change variables/methods as follows:

  1. Add static variables to get information about Tournament(User/Team)Id when match result is being proceed:

    public static long TournamentUserId
    {
        get => long.Parse(PlayerPrefs.GetString("C_TournamentUserId", "0"));
        set => PlayerPrefs.SetString("C_TournamentUserId", value.ToString());
    }
    
    public static byte TournamentTeamId
    {
        get => byte.Parse(PlayerPrefs.GetString("C_TournamentTeamId", "0"));
        set => PlayerPrefs.SetString("C_TournamentTeamId", value.ToString());
    }

RoomPlayer

Go to RoomPlayer script add/change variables/methods as follows:

  1. Add variables:

    // These variables allow us to identify player and to proceed players statistics after match
    [Networked] public long TournamentUserId { get; set; }
    [Networked] public byte TournamentTeamId { get; set; }
  2. In Spawned() method find RPC_SetPlayerStats() and change it to:

    RPC_SetPlayerStats(ClientInfo.Username, ClientInfo.KartId, ClientInfo.TournamentUserId, ClientInfo.TournamentTeamId);
  3. Change RPC_SetPlayerStats() method to:

    private void RPC_SetPlayerStats(NetworkString<_32> username, int kartId, long tournamentUserId, byte tournamentTeamId)
    {
        // Allows game to store and update information about Tournament(Team/User)Id on in game object
        Username = username;
        KartId = kartId;
        TournamentUserId = tournamentUserId;
        TournamentTeamId = tournamentTeamId;
    }

Add new GUI/UIs to LevelManager

Level Manager is responsible for returning player back to TournamentHubUI after match is finished.

  1. Go to Main Canvas.
  2. Add TournamentHubScreenUI, TournamentHubScreenGUI and TournamentListUI as shown:

    TMH

Implementing TournamentMatchHandler

  1. Go to TournamentHubUI/TournamentHubScreen/SubScreen/Canvas.

  2. Add empty object to Canvas and rename it to TournamentMatchHandler.

  3. Add new script to it called TournamentMatchHandler.

    TMH

  4. Add following code into it.

using Fusion;
using Gimmebreak.Backbone.Core;
using Gimmebreak.Backbone.Tournaments;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TournamentMatchHandler : TournamentMatchCallbackHandler
{
    Tournament tournament;
    TournamentMatch tournamentMatch;
    ITournamentMatchController tournamentMatchController;
    bool sessionStarted;
    bool creatingSession;
    private GameLauncher _launcher;
    private WaitForSeconds waitOneSec = new WaitForSeconds(1);
    private string tournamentSessionName;

    void Awake()
    {
        _launcher = FindObjectOfType<GameLauncher>();
    }

    public void OnDisable()
    {
        // Unattach Actions from RoomPlayer Actions
        RoomPlayer.PlayerJoined -= OnPlayerEnteredRoom;
        RoomPlayer.PlayerLeft -= OnPlayerLeftRoom;
        RoomPlayer.PlayerChanged -= OnPlayerPropertiesUpdate;
    }

    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}";

        // Attach actions to RoomPlayer Actions
        RoomPlayer.PlayerJoined += OnPlayerEnteredRoom;
        RoomPlayer.PlayerLeft += OnPlayerLeftRoom;
        RoomPlayer.PlayerChanged += OnPlayerPropertiesUpdate;

        // Join Photon session
        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"];

            // 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 (GameLauncher.ConnectionStatus == ConnectionStatus.Connected &&
                    _launcher.SessionName == this.tournamentSessionName)
                {
                    _launcher.LeaveSession();
                }
            }
            // Try to connect to tournament match session
            else if (GameLauncher.ConnectionStatus == ConnectionStatus.Disconnected)
            {
                // Set player propery with UserId so we can identify users in session
                // Set max players for session based on tournament phase setting
                ServerInfo.MaxUsers = (byte)(this.tournament.GetTournamentPhaseById(this.tournamentMatch.PhaseId).MaxTeamsPerMatch * this.tournament.PartySize);
                // Join or create Photon session with tournamemnt match secret as session id
                ServerInfo.LobbyName = this.tournamentSessionName;
                ClientInfo.LobbyName = this.tournamentSessionName;
                ClientInfo.TournamentUserId = BackboneManager.Client.User.UserId;
                ClientInfo.TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId;
                _launcher.SetTournamentLobby();
                _launcher.JoinOrCreateLobby();
            }
            // If we are in wrong session then leave
            else if (GameLauncher.ConnectionStatus == ConnectionStatus.Connected &&
                     this.tournamentSessionName != _launcher.SessionName)
            {
                _launcher.LeaveSession();
            }

            yield return this.waitOneSec;
        }
    }

    public override bool IsConnectedToGameServerNetwork()
    {
        // Check if user is connected to photon and ready to join a session
        if (GameLauncher.ConnectionStatus == ConnectionStatus.Connected)
        {
            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
        foreach (RoomPlayer rp in RoomPlayer.Players)
        {
            if (rp.TournamentUserId == 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;
        _launcher.LeaveSession();
    }

    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;
                        RoomPlayer.Local.RPC_ChangeReadyState(true);
                        RoomPlayer.Local.KartId = 1;
                        // Set session properties
                        GameManager.Instance.GameSessionId = gameSession.Id;
                        GameManager.Instance.TournamentId = this.tournament.Id;
                        GameManager.Instance.TournamentMatchId = this.tournamentMatch.Id;
                    }
                })
                .Run(this);
        }
    }

    // Photon callback when player entered the session
    public void OnPlayerEnteredRoom(RoomPlayer player)
    {
        long userId = player.TournamentUserId;
        if (this.tournamentMatchController != null)
        {
            // Report user who joined room
            this.tournamentMatchController.ReportJoinedUser(userId);
        }
    }

    // Photon callback when player disconnected from session
    public void OnPlayerLeftRoom(RoomPlayer player)
    {
        if (this.tournamentMatchController != null)
        {
            long userId = player.TournamentUserId;
            // Report user who disconnected from room
            this.tournamentMatchController.ReportDisconnectedUser(userId);
        }
    }

    // Photon callback when player properties are updated
    public void OnPlayerPropertiesUpdate(RoomPlayer player)
    {
        if (this.tournamentMatchController != null)
        {
            // Reporting status change will refresh match metadata
            this.tournamentMatchController.ReportStatusChange();
        }
    }
}

Implementing TournamentMatchHandler into ActiveMatchContainer

After all necessary changes are done, add TournamentMatchHandler to ActiveMatchContainer as MatchHandler. ActiveMatchContainer can be found in TournamentHubUI/TournamentHubScreen/SubScreen/Canvas/ActiveMatchContainer.

TMH

Implementing Result submission

  1. Go to EndRaceUI script.
  2. Add variable that will keep track if result processing started.
private bool processingStarted = false;
  1. Find RedrawResultsList() method and edit it as follows:
public void RedrawResultsList(KartComponent updated)
{
    var parent = resultsContainer.transform;
    ClearParent(parent);

    var karts = GetFinishedKarts();
    for (var i = 0; i < karts.Count; i++)
    {
        var kart = karts[i];

        // As we disconnect player from session before dispawned is called,
        // this method will be called when kart.Controller.RoomUser is already destroyed.
        // Doesn't happen in ordinary match.
        if (kart.Controller.RoomUser != null)
        {
            Instantiate(resultItemPrefab, parent)
                .SetResult(kart.Controller.RoomUser.Username.Value, kart.LapController.GetTotalRaceTime(), i + 1);
        }
    }

    EnsureContinueButton(karts);
}
  1. Now edit EnsureContinueButton() method and add ProcessResult() method:
private void EnsureContinueButton(List<KartEntity> karts)
{
    var allFinished = karts.Count == KartEntity.Karts.Count;

    // Remove submission button and proceed result without user interaction to prevent any errors connected to that
    if (!this.processingStarted && this.isActiveAndEnabled && allFinished && GameManager.Instance.IsTournamentGame())
    {
        StartCoroutine(ProcessResult(karts));
    }
}

private IEnumerator ProcessResult(List<KartEntity> karts)
{
    // Notify that result processing has started
    this.processingStarted = true;

    List<GameSession.User> users = new List<GameSession.User>();
    Dictionary<long, float> results = new Dictionary<long, float>();
    GameSession gameSession;

    for (int i = 0; i < karts.Count; i++)
    {
        // Loop through each kart to obtain necessary information for result submition
        var tempUserId = tempSorted[i].Controller.RoomUser.TournamentUserId;
        var tempTeamId = tempSorted[i].Controller.RoomUser.TournamentTeamId;
        var tempUserValue = tempSorted[i].LapController.GetTotalRaceTime();

        // All karts are already sorted by time
        users.Add(new GameSession.User(tempUserId, tempTeamId) { Place = i + 1 });
        results.Add(tempUserId, tempUserValue);
    }

    // Create new GameSession
    gameSession = new GameSession(
        GameManager.Instance.GameSessionId,
        0,
        users,
        GameManager.Instance.TournamentMatchId);

    // Attach users and their results to current GameSession
    gameSession.Users.ForEach(user =>
    {
        gameSession.AddStat(1, user.UserId, (decimal)results[user.UserId]);
    });

    //report game session
    yield return BackboneManager.Client.SubmitGameSession(gameSession);
    //refresh tournament data
    yield return BackboneManager.Client.LoadTournament(GameManager.Instance.TournamentId);

    // Leave session after result was submited
    FindObjectOfType<GameLauncher>().LeaveSession();
}

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

FusionTanknarok

Implementing Tournament-SDK into Fusion based game

This tutorial is based on:

Tanknarok game sample available on https://doc.photonengine.com/fusion/current/game-samples/fusion-tanknarok#download

To run Karts project you need Editor Version 2020.3.47f1 or any 2020 LTS version.

Tournament-SDK 1.3.1 is available on https://www.tournament-sdk.com/docs#-sdk-release-notes

Set up the Fussion

Open downloaded Karts project in unity.

Follow instruction in given link to set up Fussion AppId: https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/1-getting-started#step_6___create_an_app_id

Set up Tournament-SDK

Import Tournament-SDK.

To set up the Tournament-SDK Client Game ID follow the steps of Create new game and setup gameId in instruction on following link: https://www.tournament-sdk.com/docs#Create+new+game+and+setup+gameId

Setting up the scene

Adding/adjusting necessary prefabs/objects/components

  1. To open MainScene go to Assets/Scenes/MainScene

    MainScene

  2. Create two new objects in MainScene/App and rename them to TournamentListPanel and TournamentHubPanel.

    SetUp

  3. For both TournamentListPanel and TournamentHubPanel set Rect Transform to stretch and re-set Left/Right/Top/Bottom to 0.

    SetUp

  4. Add Panel Component to both TournamentListPanel and TournamentHubPanel.

    SetUp

  5. Go to Assets/TournamentSDK_Demo/Prefabs/DefaultUIScreens.

  6. Drag&Drop TournamentHubScreen and TournamentListScreen prefabs to TournamentHubPanel and TournamentListPanel respectively.

    SetUp

  7. Find SharedMode in App/StartUI/SharedMode, duplicate it and rename to TournamentMode and change it's PosX param in Rect Transform to 541.

    SetUp

  8. Go to TournamentMode/Text(TMP)(1) change Text component to Tournament Mode.

  9. Go to TournamentMode/BtnShared rename it to BtnTournaments, in BtnTournament/Text (TMP) change Text component to Tournaments.

Before setting up the UI buttons, let's adjust game code to it.

GameLauncher code update for UI usage

Open GameLauncher script:

  1. Add necessary variables to store information about tournament player/match:

    // Panels to control UI to:
    // Open TournamentListPanel from MainMenu
    // Open TournamentHubPanel from TournamentListPanel
    // Get back from TournamentHubPanel to TournamentListPanel
    // Get back from TournamentListPanel to MainMenu
    [SerializeField] private Panel _tournamentListPanel;
    [SerializeField] private Panel _tournamentHubPanel;
    
    // Store TournamentHubGUI to enable it after tournament game has ended
    [SerializeField] private GUITournamentHubScreen _tournamentHubGUI;
    
    // Variables to store players tournament info
    public static long TournamentUserId { get;set;}
    public static byte TournamentTeamId { get;set;}
    public static string PlayerName { get; set; }
    
    // Variables to store information about tournament to process results after match finishes
    public static long TournamentId { get; set; }
    public static long GameSessionId { get; set; }
    public static long TournamentMatchId { get; set; }
    
    // Boolean to identify if tournament game has been started/finished (Not ordinary game!)
    public static bool TournamentGame = false;
  2. Add necessary methods to enable switching among MainMenu/TournamentListScreen/TournamentHubScreen.

    Allows to redirect player from MainMenu to TournamentListScreen.

    public void OnTournamentOptions()
    {
        if (GateUI(_uiStart)) _tournamentListPanel.SetVisible(true);
    }

    Getter and Setter for tournament session name which is _room.text in this case.

    public string GetSessionName()
    {
        return _room.text;
    }
    
    public void SetSessionName(string name)
    {
        _room.text = name;
    }

    Allows us to manipulate Panels to enable switching between TournamentListScreen/TournamentHubScreen.

    public void ForceVisible(Panel panel)
    {
        panel.SetVisible(true);
    }
    
    public void ForceInvisible(Panel panel)
    {
        panel.SetVisible(false);
    }

    Allows to get players connection status to game session.

    public ConnectionStatus GetConnectionStatus()
    {
        return _status;
    }
  3. After all addition necessary for UI manipulation, let's change game code to provide smooth tournament game start and result processing:

    Change method Start() to:

    private void Start()
    {
        // Add or create name to register player for tournament, add to PlayerPrefs not to get random name on every launch
        if (PlayerPrefs.GetString("login").IsNullOrEmpty())
        {
            PlayerName = $"Player{Random.Range(1, 10)}{Random.Range(1, 10)}{Random.Range(1, 10)}";
            PlayerPrefs.SetString("login", PlayerName);
        }
        else
        {
            PlayerName = PlayerPrefs.GetString("login");
        }
        OnConnectionStatusUpdate(null, FusionLauncher.ConnectionStatus.Disconnected, "");
    }

    Change method OnEnterRoom() to:

    public void OnEnterRoom()
    {
        if (TournamentGame)
        {
            _gameMode = GameMode.Shared;
            // If starting tournament game, _uiRoom must be set visible immediately(second "True" param) to prevent animation from breaking
            _uiRoom.SetVisible(true, true);
        }
        if (GateUI(_uiRoom))
        {
            FusionLauncher launcher = FindObjectOfType<FusionLauncher>();
            if (launcher == null)
                launcher = new GameObject("Launcher").AddComponent<FusionLauncher>();
    
            LevelManager lm = FindObjectOfType<LevelManager>();
            lm.launcher = launcher;
            launcher.Launch(_gameMode, _room.text, lm, OnConnectionStatusUpdate, OnSpawnWorld, OnSpawnPlayer, OnDespawnPlayer);
        }
    }

    Change method OnConnetionStatusUpdate() to:

    private void OnConnectionStatusUpdate(NetworkRunner runner, FusionLauncher.ConnectionStatus status, string reason)
    {
        if (!this)
            return;
    
        Debug.Log(status);
    
        if (status != _status)
        {
            switch (status)
            {
                case FusionLauncher.ConnectionStatus.Disconnected:
                    // No need to show message if we forced disconnect, after tournament game has been finished!
                    if (!TournamentGame) ErrorBox.Show("Disconnected!", reason, () => { });
                    break;
                case FusionLauncher.ConnectionStatus.Failed:
                    ErrorBox.Show("Error!", reason, () => { });
                    break;
            }
        }
    
        _status = status;
    
        UpdateUI();
    }

    Change method UpdateUI() to:

    private void UpdateUI()
    {
        bool intro = false;
        bool progress = false;
        bool running = false;
    
        switch (_status)
        {
            case FusionLauncher.ConnectionStatus.Disconnected:
                _progress.text = "Disconnected!";
                intro = true;
                break;
            case FusionLauncher.ConnectionStatus.Failed:
                _progress.text = "Failed!";
                intro = true;
                break;
            case FusionLauncher.ConnectionStatus.Connecting:
                _progress.text = "Connecting";
                progress = true;
                break;
            case FusionLauncher.ConnectionStatus.Connected:
                _progress.text = "Connected";
                progress = true;
                break;
            case FusionLauncher.ConnectionStatus.Loading:
                _progress.text = "Loading";
                progress = true;
                break;
            case FusionLauncher.ConnectionStatus.Loaded:
                running = true;
                break;
        }
    
        _uiCurtain.SetVisible(!running);
        _uiProgress.SetVisible(progress);
        // If player was disconnected from Tournament game, redirect to TournamentHubPanel
        if (_status == FusionLauncher.ConnectionStatus.Disconnected && TournamentGame)
        {
            // Disable Main Menu
            _uiStart.SetVisible(false);
            TournamentGame = false;
            // Initialize recent tournament
            _tournamentHubGUI.Initialize(TournamentId);
            // redirect to TournamentHubPanel
            _tournamentHubPanel.SetVisible(true);
        }
        else _uiStart.SetVisible(intro);
    
        _uiGame.SetActive(running);
    
        if (intro)
            MusicPlayer.instance.SetLowPassTranstionDirection(-1f);
    }
  4. Get back to App in Hierarchy find GameLauncher component and add missing Panels and GUI

    SetUp

Setting up UI

Tournament button

Direct player from MainMenu to TournamentListScreen.

Go to App/StartUI/TournamentMode/BtnTournaments, find Button component and set OnClick() to GameLauncher.OnTournamentOptions

UI

TournamentListScreen to MainMenu

To allow player to return back from TournamentListPanel to MainMenu:

  1. Go to TournamentListPanel/TournamentListScreen/SubScreen/Canvas/Title/ButtonUnderline(Back).

  2. Remove GUISubScreen.Back, by pressing - under OnClick().

    UI

  3. Add two new actions, by pressing + under OnClick().

  4. Drag&drop App from Hierarchy to empty field under Runtime Only for both.

  5. Set them from No Function to GameLauncher.ForceInvisible and GameLauncher.ForceVisible.

  6. Drag&drop TournamentListPanel and StartUI respectively.

    UI

TournamentListScreen to TournamentHubScreen

  1. Go to TournamentListPanel/TournamentListScreen/SubScreen.

  2. Drag&drop TournamentHubPanel/TournamentHubScreen/SubScreen into field TournamentHubScreen in GUITournamentListScreen.

    UI

  3. Go to TournamentListPanel/TournamentListScreen/SubScreen/Canvas/ScrollView/Viewport/Content/ButtonUnderline(Open).

  4. Add two new OnClick() actions.

  5. Drag&drop App from Hierarchy to empty field under Runtime Only for both.

  6. Set them to GameLauncher.ForceInvisible and GameLauncher.ForceVisible.

  7. Drag&drop TournamentListPaenl and TournamentHubPanel respectively.

    UI

  8. Repeat for TournamentListPanel/TournamentListScreen/SubScreen/Canvas/ScrollView/Viewport/Content/ButtonUnderline(Play).

TournamentHubScreen to TournamentListScreen

To allow player to return back from TournamentHubPanel to TournamentListPanel:

  1. Go to TournamentHubPanel/TournamentHubScreen/SubScreen.

  2. In GUISubScreen component fill empty field Return Screen with TournamentListPanel/TournamentListScreen/SubScreen.

    UI

  3. Go to TournamentHubPanel/TournamentHubScreen/SubScreen/Canvas/Title/ButtonUnderline(Back)

  4. Add two more actions.

  5. Set them to GameLauncher.ForceInvisible and GameLauncher.ForceVisible as previously.

  6. Drag&drop TournamentHubPanel and TournamentListPanel respectively.

    UI

Backbone integration

To gather information about available tournaments we first need to log in player.

  1. Create new empty object at the end of MainScene.

  2. Rename it to BackboneManager.

  3. Add BackboneManager and ResourceCache components to it.

  4. Create new script called BackboneIntegration and add it to BackboneManager.

    BBM

    Make sure Initialize On Start field is Disabled

  5. Open BackboneIntegration script and add following code:

    using FusionExamples.Tanknarok;
    using Gimmebreak.Backbone.User;
    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()
        {
            tournamentButton.interactable = false;
    
            // wait until player nick was set (this happens on initial screen)
            while (string.IsNullOrEmpty(GameLauncher.PlayerName))
            {
                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" + GameLauncher.PlayerName;
            Debug.Log($"arbitraryId -> {arbitraryId}");
            // 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, GameLauncher.PlayerName, 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)
            {
                tournamentButton.interactable = true;
            }
        }
    }

    BackboneIntegration component in BackboneManager now misses TournamentButton, go ahead and add it.

    BBM

    BackboneIntegration starts BackboneManager initialization and logs player in, which allows game gather information about available tournaments.

Tournament Match Handler

Escential changes to implement TournamentMatchHandler

To successfully start a tournament match, before adding TournamentMatchHandler let's first prepare game to work smoothly with it.

We already added necessary variables and methods to GameLauncher earlier, now using them let's update code to support tournament games.

  1. Open Player script:

    1. Add variables to store user's info connected to tournament:

      // Assign call back to receive notification when player has assigned it's unique tourament id
      [Networked(OnChanged = nameof(TournamentUserIDChanged))] public long TournamentUserId { get; set; } = 0;
      [Networked] public byte TournamentTeamId { get; set; }
      public static Action<long> OnPlayerTIDChange;
      public static Action<long> OnPlayerDespawned;
    2. Add method responsible for call back:

      public static void TournamentUserIDChanged(Changed<Player> changed)
      {
          // Notify subscribers to this call that TournamentUserId has changed
          OnPlayerTIDChange(changed.Behaviour.TournamentUserId);
      }
    3. Change method Spawned() to:

      public override void Spawned()
      {
          if (Object.HasInputAuthority)
          {
              // Store player variables escential to process its results.
              local = this;
              if (GameLauncher.TournamentGame)
              {
                  playerName = GameLauncher.PlayerName;
                  TournamentUserId = GameLauncher.TournamentUserId;
                  TournamentTeamId = GameLauncher.TournamentTeamId;
              }
          }
      
          // Getting this here because it will revert to -1 if the player disconnects, but we still want to remember the Id we were assigned for clean-up purposes
          playerID = Object.InputAuthority;
      
          ready = false;
      
          SetMaterial();
          SetupDeathExplosion();
      
          _teleportIn.Initialize(this);
          _teleportOut.Initialize(this);
      
          _damageVisuals = GetComponent<TankDamageVisual>();
          _damageVisuals.Initialize(playerMaterial);
      
          PlayerManager.AddPlayer(this);
          // Auto will set proxies to InterpolationDataSources.Snapshots and State/Input authority to InterpolationDataSources.Predicted
          // The NCC must use snapshots on proxies for lag compensated raycasts to work properly against them.
          // The benefit of "Auto" is that it will update automatically if InputAuthority is changed (this is not relevant in this game, but worth keeping in mind)
          GetComponent<NetworkCharacterControllerPrototype>().InterpolationDataSource = InterpolationDataSources.Auto;
      }
    4. Change method Despawned() to:

      public override void Despawned(NetworkRunner runner, bool hasState)
      {
          Destroy(_deathExplosionInstance);
          PlayerManager.RemovePlayer(this);
          OnPlayerDespawned(this.TournamentUserId);
      }
  2. Go to PlayerManager script:

    Add new method GetPlayerFromTID()

    // Allows us to get spesific user using its TournamentUserId
    public static Player GetPlayerFromTID(long id)
    {
        foreach (Player player in _allPlayers)
        {
            if (player.TournamentUserId == id)
                return player;
        }
    
        return null;
    }
  3. Go to GameManager script:

    1. Add variable to track if result processing has started:

      public bool processingStarted = false;
    2. Change method OnTankDeath() to:

      public void OnTankDeath()
      {
          if (playState != PlayState.LOBBY)
          {
              int playersleft = PlayerManager.PlayersAlive();
              Debug.Log($"Someone died - {playersleft} left");
              if (playersleft <= 1)
              {
                  Player lastPlayerStanding = playersleft == 0 ? null : PlayerManager.GetFirstAlivePlayer();
                  // if there is only one player, who died from a laser (e.g.) we don't award scores. 
                  if (lastPlayerStanding != null)
                  {
                      int winningPlayerIndex = lastPlayerStanding.playerID;
                      int nextLevelIndex = _levelManager.GetRandomLevelIndex();
                      int winningPlayerScore = lastPlayerStanding.score + 1;
                      if (winningPlayerIndex >= 0)
                      {
                          Player winner = PlayerManager.GetPlayerFromID(winningPlayerIndex);
                          if (winner.Object.HasStateAuthority)
                              winner.score = winningPlayerScore;
                          if (winningPlayerScore >= MAX_SCORE)
                              nextLevelIndex = -1;
                      }
                      if (GameLauncher.TournamentGame && nextLevelIndex == -1)
                      {
                          StartCoroutine(ProcessResult());
                      }
                      else
                      {
                          LoadLevel(nextLevelIndex, winningPlayerIndex);
                      }
                  }
              }
          }
      }
    3. Add method ProcessResult():

      private IEnumerator ProcessResult()
      {
          // Wait for one sec, which is enough to propogate winner score to other players
          yield return new WaitForSeconds(1);
      
          this.processingStarted = true;
      
          List<GameSession.User> users = new List<GameSession.User>();
          Dictionary<long, float> usersScore = new Dictionary<long, float>();
          GameSession gameSession;
      
          List<Player> players = PlayerManager.allPlayers.OrderBy(x => x.score).Reverse().ToList();
      
          for (int i = 0; i < players.Count; i++)
          {
              var TournamentUserId = players[i].TournamentUserId;
              var TournamentTeamId = players[i].TournamentTeamId;
              var UserScore = players[i].score;
      
              users.Add(new GameSession.User(TournamentUserId, TournamentTeamId) { Place = i + 1 });
              usersScore.Add(TournamentUserId, UserScore);
          }
      
          // Create gameSession using previously saved info about tournament in GameLauncher to process the match result
          gameSession = new GameSession(
              GameLauncher.GameSessionId,
              0,
              users,
              GameLauncher.TournamentMatchId);
      
          gameSession.Users.ForEach(user =>
          {
              gameSession.AddStat(1, user.UserId, (decimal)usersScore[user.UserId]);
          });
      
          //report game session
          yield return BackboneManager.Client.SubmitGameSession(gameSession);
          //refresh tournament data
          yield return BackboneManager.Client.LoadTournament(GameLauncher.TournamentId);
      
          Restart(ShutdownReason.Ok); // <- Disconnect from the finished match
      }

Implementing TournamentMatchHandler

  1. Go to App/TournamentHubPanel/TournamentHubScreen/SubScreen/Canvas.

  2. Add new game object, rename it to TournamentMatchHandler.

    TMH

  3. Create new script called TournamentMatchHandler, add it as component to TournamentMatchHandler game object.

  4. Add following code into it:

    using Fusion;
    using Fusion.Sockets;
    using FusionExamples.FusionHelpers;
    using FusionExamples.Tanknarok;
    using FusionExamples.UIHelpers;
    using Gimmebreak.Backbone.Core;
    using Gimmebreak.Backbone.Tournaments;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class TournamentMatchHandler : TournamentMatchCallbackHandler
    {
        Tournament tournament;
        TournamentMatch tournamentMatch;
        ITournamentMatchController tournamentMatchController;
        bool sessionStarted;
        bool creatingSession;
        private GameLauncher launcher;
        private WaitForSeconds waitOneSec = new WaitForSeconds(1);
        private string tournamentSessionName;
        [SerializeField] Panel tournamentHubPanel;
    
        void Awake()
        {
            launcher = FindObjectOfType<GameLauncher>();
            // Assign call back to receive notification when player has set unique TournamentUserId
            Player.OnPlayerTIDChange += OnPlayerIdChanged;
            // Assign call back to receive notification when player is being despawned
            Player.OnPlayerDespawned += OnPlayerLeftRoom;
        }
    
        private void LeaveSession()
        {
            // To leave game session we call runner.Shutdown which will disconnect player from game session
            NetworkRunner runner = FindObjectOfType<NetworkRunner>();
            if (runner != null && !runner.IsShutdown)
            {
                // Calling with destroyGameObject false because we do this in the OnShutdown callback on FusionLauncher
                runner.Shutdown(false, ShutdownReason.Ok);
            }
        }
    
        public void OnPlayerIdChanged(long userId)
        {
            // Call back that will be fired as soon as new player assings it's tournamentUserId
            if (PlayerManager.GetPlayerFromTID(userId))
                OnPlayerEnteredRoom(userId);
        }
    
        public override void OnJoinTournamentMatch(Tournament tournament, TournamentMatch match, ITournamentMatchController controller)
        {
            // User is gathering information about tournament match that player want's to join
            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 according to gathered information
            StartCoroutine(JoinRoomRoutine());
        }
    
        private IEnumerator JoinRoomRoutine()
        {
            while (this.tournamentMatch != null)
            {
                // If tournament match is finished then leave
                if (this.tournamentMatch.Status == TournamentMatchStatus.MatchFinished ||
                    this.tournamentMatch.Status == TournamentMatchStatus.Closed)
                {
                    if (launcher.GetConnectionStatus() == FusionLauncher.ConnectionStatus.Loaded &&
                        launcher.GetSessionName() == this.tournamentSessionName)
                    {
                        LeaveSession();
                    }
                }
                // Try to connect to tournament match session if player is not in any other game session
                else if (launcher.GetConnectionStatus() == FusionLauncher.ConnectionStatus.Disconnected)
                {
                    // Set users tournament info for results processing, that will hapen after the match will be finished
                    GameLauncher.TournamentUserId = BackboneManager.Client.User.UserId;
                    GameLauncher.TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId;
    
                    // Allows to differ tournament game from ordinary one
                    // Used to redirect player back to tournamentHub after match is finished
                    // Triggers result processing start after match is finished
                    GameLauncher.TournamentGame = true;
    
                    // Set session id as Photon's tournamemnt session match secret
                    launcher.SetSessionName(this.tournamentSessionName);
                    // Create/Join session
                    // GameMode is set to Shared in OnEnterRoom if it's tournament game
                    launcher.OnEnterRoom();
                }
                // If user connected to wrong session -> then leave
                else if (launcher.GetConnectionStatus() == FusionLauncher.ConnectionStatus.Loaded &&
                        this.tournamentSessionName != launcher.GetSessionName())
                {
                    LeaveSession();
                }
    
                yield return this.waitOneSec;
            }
        }
    
        public override bool IsConnectedToGameServerNetwork()
        {
            // Check if user created/joined a lobby
            // We know that it's correct lobby, due to checks in JoinRoomRoutine
            return launcher.GetConnectionStatus() == FusionLauncher.ConnectionStatus.Loaded;
        }
    
        public override bool IsUserConnectedToMatch(long userId)
        {
            // Check if tournament match user is connected to game session
            // User is considered connected when entered lobby with own unique TournamentUserId
            return (PlayerManager.GetPlayerFromTID(userId) != null);
        }
    
        public override bool IsUserReadyForMatch(long userId)
        {
            // Check if user is ready to start tournament match
            // Player is considered as ready to play tournament match when it has connected to game session and has unique TournamentUserId assigned to it
            return IsUserConnectedToMatch(userId);
        }
    
        public override void OnLeaveTournamentMatch()
        {
            // If user leaves tournament match, need to remove all information about the match that user previously connected to
            this.tournament = null;
            this.tournamentMatch = null;
            this.tournamentMatchController = null;
    
            // And disconnect player from game session
            LeaveSession();
            GameLauncher.TournamentGame = false;
        }
    
        public override bool IsGameSessionInProgress()
        {
            // Check if tournament game session has been started
            return sessionStarted;
        }
    
        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 tournament game session creation has started, If not then start tournament game session creation
            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 session properties to GameLauncher to be able to access them even after the TournamentMatchHandler will be disabled
                            // So later when game will be finished we could use these properties to process session results
                            GameLauncher.GameSessionId = gameSession.Id;
                            GameLauncher.TournamentId = this.tournament.Id;
                            GameLauncher.TournamentMatchId = this.tournamentMatch.Id;
    
                            // Start game immediately.
                            GameManager.instance.OnAllPlayersReady();
    
                            // Disable THPanel because by this moment lobby is already in the back ground, but TournamentHubPanel is on top of it.
                            launcher.ForceInvisible(tournamentHubPanel);
                        }
                    })
                    .Run(this);
            }
        }
    
        // Photon callback when player entered the session
        public void OnPlayerEnteredRoom(long userId)
        {
            // Photon session informing TSDK
            if (this.tournamentMatchController != null)
            {
                // Report TournamentMatchController about user who joined session
                this.tournamentMatchController.ReportJoinedUser(userId);
            }
        }
    
        // Photon callback when player disconnected from session
        public void OnPlayerLeftRoom(long userId)
        {
            if (this.tournamentMatchController != null)
            {
                // Report TournamentMatchController about user who disconnected from session
                this.tournamentMatchController.ReportDisconnectedUser(userId);
            }
        }
    }
  5. Go to App/TournamentHubPanel/TournamentHubScreen/SubScreen/Canvas/TournamentMatchHandler, in TournamentMatchHandler component will be unset TournamentHubPanel, drag&drop TournamentHubPanel from Hierarchy into empty field.

    TMH

  6. Go to App/TournamentHubPanel/TournamentHubScreen/SubScreen/Canvas/ActiveMatchContainer, drag&drop TournamentMatchHandler into MatchHandler for GUITournamentActiveMatch component.

    TMH

    Before running the project, disable both TournamentListPanel and TournamentHubPanel.

    TMH

    Make sure TournamentListPanel/TournamentListScreeb/SubScreen is enabled.

    TMH

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

Starting With Dashboard

Login

You will receive an email from the Tournament Dashboard, from the email address: no-reply@tournament-sdk.com - Make sure to check your junk/spam mail folder. It will look like this:

Dashboard email image

The email contains a link for you to click on to validate your account and allow you to choose a password. When you have completed this process, you will be able to log in to the Tournament Dashboard. Bookmark the link and don't forget your password!

Create Game

To set up your game on the platform, click on the icon in the top left hand corner of the screen and select "New game" option:

Dashboard new game image

This will step you through the process of creating your new game in the Tournament Dashboard.

Step 1 (Info)

Enter the following details, then click the "Continue" button:

Game name

The name of your game as it will be displayed in the Tournament dashboard.

Game icon URL

The URL of your game's icon so it can be displayed in the Tournament dashboard.

Version Number

The version number of your game in the format n.n.n.

Dashboard new game image

Step 2 (Seasons)

Unless you know what you're doing at this stage (i.e. you've set up a game in the Tournament Dashboard previously), select "Default" then click the "Continue" button (you can change these settings later):

Dashboard new game season image

Step 3 (Confirmation)

Click on the "Create game" button:

Dashboard new game summary image

Congratulations, you've successfully created your game in the Tournament Dashboard.

The Tournament Dashboard is navigated using the icons shown in the bar on the left hand side of the screen. As you click on the icons, the content for that icon will be displayed in the main part of the screen - the left side-bar will always be visible.

As you go through the set up process, the system will attempt to help you make sure that you're completing all the relevant steps by:

  • showing indicators for mandatory data

  • showing specific messages at the bottom of the page if:

    • you attempt to save your configuration/set up while there are still tasks that you need to complete
    • you've made a mistake

    See below:

Dashboard template errors image

It will also make sure that, if you make changes, you don't forget to save them by showing the below message at the bottom of the screen when you start working:

Dashboard unsaved changes image

And by showing the below message if you attempt to navigate away from the screen before trying to save your changes:

Dashboard abandon changes image

There is also text on the screens that describes the purpose of each feature so, in a lot of cases, what you're attempting to do will be self-explanatory. This document contains a complete glossary of all of the settings and features in the Tournament Dashboard. You can also call or email our team if you get stuck or need help or advice.

First Tournament

This guide explains how to use the Tournament Dashboard to set up and run automated tournaments within your game.

As a prerequisite to using this guide, your development team (or you!) should have already implemented the Tournament SDK into a build of your game.

Step 1: Game settings

The first thing that you'll need to do is configure the Game Settings for your specific game. This is a one-off exercise which involves some, limited technical activity.

You access the Game settings by clicking on the Game Controller icon in the left hand bar as shown in the picture below. All of the settings that you need to complete are described in the Glossary of Settings - "Section 5: Game Settings (Game Controller icon)" so refer to that section and go and complete this activity now.

Dashboard game general settings image

Step 2: Tournament Settings

Now that your General Settings are complete, you have to create your first tournament template. You access the Tournament templates by clicking on the Trophy icon in the left hand bar as shown in the picture below. All of the settings that you need to complete are described in the Glossary of Settings - "Section 3: Tournament Templates (Trophy icon)" so refer to that section and go and complete this activity now. To get started you will need to click on the "New template" box as seen in the picture below.

When you've been using the Tournament Dashboard for a while, this screen will be full up with templates (each one laying out the plan for a specific tournament series) and you'll be able to create a new template by simply duplicating an existing one.

Dahsboard new template image

Step 3: Start Tournament

When you've completed your first template, click on the template's green "Start tournament" button then enter a date and time that you'd like the tournament to be run (this should be a future date/time). The tournament will be started automatically at this time.

Dashboard create tournament image

When the tournament has been run successfully (you can initially set it up as a "testing" type tournament and then change this later) and you're happy with the way it went, you can then set recurrency settings so that it will automatically be scheduled and run at pre-defined intervals.

To do this, click on the "(change)" Recurrency link against the template:

Dashboard template recurrency image

You can use this to set multiple recurrency rules per tournament template defining (for each):

  • Starting from date: The date on which you want the first tournament to run (this should be in the future)
  • Tournament Time (GMT): The time at which you want the tournament to start (if you want to run more than one a day, simply add a new recurrency rule by clicking the "+" button)
  • Repeat: Choose from options (Every day, Every 2 days, Every 3 days, Every 5 days, Every week, Every 2 weeks, Every 4 weeks)
  • Tournaments pre-generated: The number of tournaments you want to be generated from this template. N.B.: Once tournaments have been generated, changes made to the tournament template will not change/update existing tournaments, they will only affect tournaments that you subsequently generate.

Your pre-generated tournaments will now automatically be run. Invites will be sent (if any), players will sign up, entry fees will be collected (if any), requirements will be checked (if any), players will play, lose, draw and win games and matches, some players will be knocked out while others advance, you will have a champion and a number of runners up, their prizes will be calculated and awarded, and invites will be sent to players for subsequent tournaments. All of this will happen without any manual input whatsoever. The players just play and you just chill.

Step 4: Review and refinement

When you have either a) clicked the green button (against the tournament template) to manually start/schedule tournament or b) pre-generated multiple tournaments using the tournament schedule (against the tournament template), the actual tournaments that you've created/scheduled will be shown in the "Tournament schedule" (the calendar icon, second from the top in the left hand menu bar):

Dashboard tournament list image

Once a tournament has been set up, changes to template settings won't affect individual instances of tournaments shown in this screen. To make changes to a tournament shown here, simply click on the tournament and you will be able to check on progress and also make changes to tournaments that are due to happen at a future date/time (i.e. change the format, extend the invitee list, change entry requirements etc.).

To review your overall tournament performance for a season, you can visit the dashboard screen (graph icon at the top of the left hand menu bar).

You'll quickly get a sense of how popular different tournament formats are and you can use this information to further refine the tournaments and rewards that you offer to your players.

Dashboard Glossary

Dashboard (Line Chart icon)

Dashboard navigation stats image

The Dashboard screen shows you all the relevant metrics and key performance indicators for the tournaments that you've run. Initially, as you haven't used the platform to run any tournaments yet, these screens will be empty - as follows:

Dashboard performance nodata image

Tournament Schedule (Calendar Icon)

Dashboard navigation schedule image

The Tournament Schedule screen will show you details of all tournaments that you have run or that are scheduled to run. Initially, as you haven't used the platform to run any tournaments yet, these screens will be empty - as follows:

Dashboard schedule selector image

When you've created tournaments, you will be able to click on the dates to set the date range (as shown above). When individual tournaments are shown here, you'll be able to review historical tournaments and also make changes to individual instances of tournaments that are yet to happen.

Tournament Templates (Trophy icon)

Dashboard navigation templates image

This section allows you to create tournament templates for a tournament series specifying all the relevant rules, formats, entry criteria and prizes. A schedule can then be built based on a template which will automatically create and run the required tournaments. The set up options are described below:

General

This section allows you to decide how many and which players will be invited to this type of tournament.

Dashboard template general image

Generals

Template Name

Give this tournament template a name - this can be anything.

Type

Select a type for the tournament. This will help you to organize your templates - you will be able to see an indicator for the type of tournament when viewing your list of tournament templates. There are four types:

  • Public - For when you want anyone to be able to enter the tournament
  • Premium - For when you want players to attain/own something before entering
  • Private - For when you want participation to be by invite only
  • Testing - For when you want to try out new things (it is recommended that you create a test tournament initially. You can always change/update the type late once you're happy with what you've set up)

N.B.: The rules for tournament availability are configured separately, this is just an indicator that you/others will be able to see whilst maintaining your templates.

Registration

Registration opens

Select how long before a tournament you would like players to be able to register for it. There are eight, self-explanatory options:

  • immediately
  • 2 weeks before start
  • 1 week before start
  • 2 days before start
  • 12 hours before start
  • 2 hours before start
  • 1 hour before start

You will want to allow longer periods of time for invitation-only or special/important/high value tournaments. Public tournaments should have shorter periods of time as players are more likely to participate on a casual/ad hoc basis and will be registering right up until the tournament starts.

Registration closes

Select how long before a tournament you would like to close registration (i.e. stop new players from registering). There are six, self-explanatory options:

  • 2 minutes before start
  • 2 hours before start
  • 12 hours before start
  • 1 days before start
  • 2 days before start
  • 1 week before start

You will want to allow longer periods of time invitation-only or special/important/high value tournaments. Public tournaments should have shorter periods of time as players are more likely to participate on a casual/ad hoc basis and will be registering right up until the tournament starts.

N.B.: You will need to make sure your registration open and close settings work together (i.e. don't set registration to close before it opens).

Maximum players

Specify the maximum number of players that you want to participate in this type of tournament. You would tend to specify lower numbers (i.e. 32, 64, 128, 256) for special/important/high value tournaments or tournaments that you plan to stream as this keeps them elite and also ensures that they'll be finished in a manageable time.

Don't worry if you don't meet your maximum number of players or if some players don't turn up to play, the Tournament SDK will deal with this for you.

The maximum number of players may also be influenced by your chosen tournament format.

Party (Team) size

Specify how many players will be on each team. This will be determined by the type of game you're creating a tournament for.

N.B.: The maximum number of players in the tournament has to be divisible by the party size (i.e. you can't have teams of two players and then only allow an odd number of players to register for the tournament).

Registration rules

Specify who will be invited to this type of tournament. You can specify multiple registration rules with different registration timelines so you can do things like: initially inviting a select group of players and then opening up a tournament to general player registration at a later date/time.

The options are as follows:

Open to everyone

Make the tournament available to all your players with no restrictions.

When: You must specify when players of this type will be able to see the tournament to register. There are seven, self-explanatory options:

  • Immediately
  • 1 hour after registration opens
  • 2 hours after registration opens
  • 12 hours after registration opens
  • 1 day after registration opens
  • 2 days after registration opens
  • 1 week after registration opens
Direct Invite

Invite a specific list of players.

When: You must specify when players of this type will be able to see the tournament to register. The selectable options are the same as those listed above.

Player list: You will add a list of players to send the invite to by clicking the "add user" button then searching for the players that you want to add.

Private Code

Invite only players who knows private code.

When: You must specify when players of this type will be able to see the tournament to register. The selectable options are the same as those listed above.

Private Code: Will be generated when tournament is scheduled. If tournament is public then player has to register with private code. This can be distributed e.g. via social platforms for specific campaign or via influencer.

In case tournament is private and player does not see the tournament in his game client then player still can register with private code. Tournament will become available to him. This can be used when organizing private competition. NOTE: When combined with direct invite option player who receives the invite will have also private code available in there game client. This way they can share code further with desired audience.

The best from tournaments

Make the tournament available to players who have been successful in previous tournaments (i.e. encourage/incent loyal players to keep playing by giving them early entry or just create a champion of champions series).

When: You must specify when players of this type will be able to see the tournament to register. The selectable options are the same as those listed above.

Number of Invites: How many invites of this type will be sent (i.e. the maximum number of invites of this type that will be sent)

Invite based on item [ID]: You can send invites to players who have won a particular item (i.e. something (items, currency, points etc.) won through success in previous tournaments). When the system is deciding which players to send invites to it will look for players with this item - this does not have to be something that is awarded in your game, it can be something that is only set up in the tournament platform (see "Store" settings), for example a virtual tournament chip/coin/ticket.

Add Tournaments: Search for tournaments within a date range and add the specific tournaments that you want to invite players from.

The best score from [Number] of results: This will prioritize invites to players who have the best scores across the given number of tournaments (i.e. if this is set to 2, it will take each players top 2 results, regardless of how many tournaments they have played in). This should incent players to play in more tournaments to improve their top n. scores.

The best from series

Make the tournament available to players who have been successful in particular tournament series (i.e. tournaments that were created from the same tournament template).

When: You must specify when players of this type will be able to see the tournament to register. The selectable options are the same as those listed above.

Number of Invites: How many invites of this type will be sent (i.e. the maximum number of invites of this type that will be sent)

Invite based on item [ID]: You can send invites to players who have won a particular item (i.e. something (items, currency, points etc.) won through success in previous tournaments). When the system is deciding which players to send invites to it will look for players with this item - this does not have to be something that is awarded in your game, it can be something that is only set up in the tournament platform (see "Store" settings), for example a virtual tournament chip/coin/ticket.

Add Tournaments: Search for tournaments within a date range and add the specific tournaments that you want to invite players from.

The best score from [Number] of results: This will prioritize invites to players who have the best scores across the given number of tournaments (i.e. if this is set to 2, it will take each players top 2 results, regardless of how many tournaments they have played in). This is particularly relevant across a tournament series (i.e. a recurring tournament that you might run at the same time every day). This should incent players to play in more tournaments to improve their top n. scores.

Enabled/Disable

As you enable each of these registration rule options, you will see the text "enabled" appear in green. To disable any of the registration rules, move your mouse pointer over the text "enabled" and you will see it change to red text that says "disable". Click on this to disable/remove a registration rule.

Actions - Delete

Don't press this unless you have a really good reason to as it will permanently delete your template!

Description

This section allows you specify information about your tournament that will be displayed to players/invitees.

Dashboard template description image

Language

Select the language from the available list and click the + button. Do this for each language that you want to display tournament information in. All of this will be available to your game through the SDK - remember that you will probably have multiple tournaments available to players at any one time so these settings will help to differentiate/advertise the tournament.

The following settings will be available for each language:

Language

Even though you've already selected this, it can be edited here as well.

Tournament Name

Specify the name of the tournament. This should be something cool.

Image URL

Provide the URL of the image that will be shown to players to represent this tournament

Icon URL

Provide the URL of the icon that will be displayed to players for this tournament.

Theme color

Specify a theme color that will be used. This color can be picked up through the in game SDK and used to change the visuals/highlight items in your game UI for this tournament.

Description

Provide a brief description of the tournament that will be shown to players.

Description 2

Provide some additional information about the tournament.

Enabled/Disable

As you enable each required language, you will see the text "enabled" appear in green. To disable any of the languages, move your mouse pointer over the text "enabled" and you will see it change to red text that says "disable". Click on this to disable/remove language information.

Format

This section allows you to define the format of the tournament. Tournaments will consist of one or more phases, each phase will have multiple rounds (each round will have one match for each player/team), each match can be made up of multiple games (i.e. best of 3). The platform will narrow the field of players/teams down until there is a winner/champion. The possible types of phase are as follows:

  • Arena - It is best to start your tournament with an arena phase if you:

    • have a large number of players competing in a tournament
    • want to ensure that the best players make it through to the final stages
    • want to guarantee that players get to play a certain number of matches rather than being knocked out immediately

    In an arena phase players/teams will be automatically matched against each other over a fixed number of rounds during which no players/teams will be eliminated (all teams are guaranteed to play in each round). Players/teams are matched against players of a similar skill level and will accumulate a score based on their performances and strength of competition. At the end of an arena you can either declare a winner/winners, or allow the best players to advance to another phase (either arena or bracket)

  • Bracket - A bracket phase matches players/teams in a single elimination (i.e. lose and you're out) knockout format, building every participant a clear/visible route to the final (through quarter finals, semi finals etc.). You can't have any further phases after a bracket (either bracket or arena) as a bracket phase will always give you a final match and a single winner. As per the above section on Registration rules, you can automatically invite winners to future tournaments.

Dashboard template format image

Phase Type

Click on "+Add Phase" and select either Arena or Bracket.

Teams

Teams

Specify the number of teams that will be in this phase.

Min teams per match

Specify the minimum number of teams that will play in a match. You'll need to set this (as appropriate) for your type of game.

Max teams per match

Specify the maximum number of teams that can play in a match. You'll need to set this (as appropriate) for your type of game (and may be the same as the minimum number)

Scores

Max Losses (arena only)

Specify the maximum number of losses a player/team can have (i.e. if you lose more than this number of games then you're automatically out). In team games, it is possible to have multiple winners so a "loss" is any game when a player doesn't score any match points. In a bracket phase "max losses" its pre-set to "1" (i.e. lose and you're out) so this setting is not shown.

Advanced settings

Click on the "show" advanced settings" link to show these. By default, these settings are automatically set.

The settings define how to award points to players who finish in certain positions within a game and/or match, you might want to alter these if each game/match has large numbers of players.

Game point distribution:

This is specified as a comma delimited list. In a head to head game, this value will be set to "1" i.e 1 point is awarded to the winner of a game. In a more complex scenario (i,.e. battle royale type game) you may choose to give 5 points to the winner of a game, 2 points to the runner up, and 1 point each to the 3rd-5th placed finishers. This would be specified as follows: 5,2,1,1,1

Match point distribution:

When all the games in a match have been played, the platform will rank players based on the number of points awarded across all games. Match points will then be awarded according to the ranked order (i.e. anyone who is categorized as a "winner" will get at least one match point so, if your top 3 players are "winners", you might specify the match point distribution as: 3,2,1). The awarding of match points will determine if players progress to future rounds and, in an arena, who they are matched against.

Format

Add in as many rounds as you require for this phase of the tournament. If this is a Bracket phase, the correct number of rounds will be added for you based on the number of teams/players and the team size. For each round you can then specify additional characteristics.

Type

You must select how each round (match) will be decided. There are options for Best Of (BO) 1-5 (i.e. how many games should the players/teams play against each other to determine the winner of their match). There is also an option for "Custom". If you select this item you must also enter:

  • Score to Win - the score that a player/team requires to win the match
  • Maximum number of games - the maximum number of games that the teams can play
Minimum game time (minutes)

Enter the minimum amount of time that a game will take to be played. This setting helps to ensure that players will get to play all of the games in an individual round. I.e. if the first game in a best of 3 lasts for a long time, messages can be sent through SDK to kill/end the first game to allow time for subsequent, minimum length, games to be played.

Maximum round time (minutes)

To ensure the tournament is over in a manageable time, specify the maximum duration for each round. This must be more than the sum of the minimum times of the games in the round. When you're setting this you should think about the number of games in a round, the minimum (and average) length of a game and then think about (potentially) adding an appropriate safety margin (i.e. in your game, a typical game play session might last between 1 and 3(ish) minutes). You would specify the minimum game time as 1. Then, if your round/match was the best of 3 games, you might specify the maximum round time as 9 minutes (2 mins average game time * 3 games + 3 minutes buffer/safety margin).

Remember that all matches in a round must be complete before the next round can begin so you may have some players who complete their match early and then have time when they are waiting for their next match to begin. Whilst there is no exact science, it's important to consider this carefully and set an appropriate limit.

Results

Resolve phase tiebreakers

Phase tiebreakers are always resolved. At the end of each phase players are sorted according to these stats:

  • Points - higher is better - accumulated from winning match points (rounds)
  • Match wins - higher is better - accumulated from winning matches (rounds)
  • Match loses - lower is better - accumulated from losing matches (rounds)
  • Game wins - higher is better - accumulated from winning games (in each match)
  • Game loses - lower is better - accumulated from losing games (in each match)
  • Tiebreaker stat 1 (optional) - accumulated from played games
  • Tiebreaker stat 2 (optional) - accumulated from played games
  • Phase seed - lower is better - final position from previous phase
  • Lose weight - lower is better - accumulated from losing matches and weight increases with each round

You may specify two custom tiebreaker stats to be used to break the tie. For example (depending on your game): most kills, furthest distance travelled, total goals scored etc. This must be passed by the game through the SDK, statistics will be set up in "Game settings, Statistics" section (see below).

Resolve match tiebreakers

Tick this box if you want to resolve match tiebreakers. This is only used in scenarios where you set up a round to be the "Best of" an even number of games - in this scenario, you may specify another statistic to be used to break the tie. For example (depending on your game): most kills, furthest distance travelled, total goals scored etc. This must be passed by the game through the SDK, statistics will be set up in "Game settings, Statistics" section (see below).

In case of a tiebreaker the match points are split between all teams that occupy tiebreaker positions. For example if 2 teams finish with same amount of game points in a match and they tiebreak for 2 match points then the points are split equally between them so each gets 1 match point.

N.B.: You should always resolve tiebreakers in a bracket phase or you run the risk of a random player being put through to the next round.

Entry Fee

This section allows you to set tournament entry fees (if any). These can be in-game currency or owned items. You an also choose whether, or not, you want these to be added to a prize pool to be shared amongst the winners.

Drag and drop the items/currencies from the right hand menu. These items will be deducted from each players account when they register for the tournament.

Rewards

This section allows you to set up rewards for the tournament winners. The rewards will be automatically distributed when the tournament ends.

Click on the "Add rule" button (do this as many times as you need to set up rewards for different placed players)

Place

Specify the place that this reward applies to (i.e. first place only, 2-4th place, 5th-10th place etc)

Shared pot

Add the % of the shared pot that this place finisher will win

Drag and drop items here

Items and currencies can be dragged from the right hand menu. You can drag as many as you want for each place.

Add description
  • Describe - add a text description of your rewards
  • Image - add an image that shows the rewards will/have been

The rewards can be shown to players registering or reviewing the results of past tournaments.

Requirements

This section allows you to specify entry requirements for the tournament (i.e. a player must've attained a certain rank or must possess a certain item to participate). You can enter your own custom requirements (i.e. ones that are relevant to your game) as key-value pairs. When players are signing up to the tournament, these values will be passed to server and to check if the player has met the requirements to register or not.

Add requirement

Click on the "+Add requirement" button and add as many requirements as necessary. These are configured in the format:

Requirement

The name of the requirement (i.e. Player rank).

Requirement Value

The vale of the requirement (i.e. 10).

Administrators

This section allows you to add players as tournament administrators. An administrator is a tournament player who has access to all the matches in a tournament whereas any normal player only has access to the matches that they are participating in. If your game has a spectator mode, this would allow an administrator to join any match. This is might be especially useful for streaming the tournaments as an administrator can join and view any match within the game client.

Custom properties

This section allows you to specify custom properties for the tournament. These can be anything relevant to your game and related to a tournament and will be passed to the game through the SDK (i.e. what map(s) will be used, what rules will be used, what spells/weapons will be used/banned etc.). This can also be used to add a stream link for a particular tournament so you can have a "watch now on twitch" button inside your game.

Add custom property

Click on the "+Add custom property" button and add as many custom properties as necessary. These are configured as key-value pairs.

Webhooks

This section allows you to set up webhooks to automatically send information/notifications about key tournament-related events to other platforms (i.e. Discord etc.). There are several events that can each be configured to make multiple calls:

Tournament registration opens

Executed when registrations are opened

Tournament registration closes

Executed when registrations are closed

Tournament start

Executed when tournament is ready to start.

Tournament end

Executed after tournament is finished (prizes are distributed)

Tournament is next

Executed when tournament is next, will be triggered multiple times (Does not trigger on private tournaments)

Callback

For each of the above events, multiple callbacks can be configured by selecting the event and clicking the "+New callback" button. The following parameters will be specified for each callback - if you're not sure about how to set this up, ask your dev team who will be able to help.

Callback Name

Give this webhook call a name.

Retries

Specify the number of times that this call should be re-attempted if it fails.

Method

Select one of: POST, GET, PUT, DELETE - depending on which option is required.

URL

Specify the URL that will be called.

Headers

Click on the "+Add Header" button to enter additional headers as required. These will be specified as Key-Value pairs.

Parameters

Click on the "+Add Parameter" button to enter additional parameters as required. These will be specified as Key-Value pairs.

Store (Shopping bag icon)

Dashboard navigation store image

The Store section allows you to add currencies and items that can be used in the Rewards and Entry fee sections of tournament template setup. You will typically import game items and currencies from your servers (by setting up integration in game settings). These settings will allow you to add items and currencies manually if you don't import from your server. The "currencies" here can also be used as a custom, in-tournament points system so you can award points that are not related to your game currency.

Game Settings (Game Controller icon)

Dashboard navigation settings image

General settings

This section contains general settings related to your game.

Dashboard game settings image

Game Name

The name of the game for which you are creating tournaments.

Game Icon URL

URL to render your game's icon in the tournament dashboard.

Version Number

The version of the tournament dashboard that you're currently running.

Client Game ID

Unique, system generated ID for your game within the tournament system.

Seasons

This section allows you to set up tournament seasons. You can either set up season recurrence or create new seasons manually. Seasons (and parts within seasons) will help you to manage the tournaments that you run, creating different themes etc. Game/player stats are also aggregated by season.

Options in a drop down 1 month to 24 months.

Part start day

Options in drop down for days of the week (and default).

Part Length (Days)

Options in a drop down: 1 week, 2 weeks, 1 month

Button to "Create Season".

Tournament settings integration

This section allows you to set up webhooks for:

External signup

Method (post), URL, optional Headers, optional Parameters.

External prize delivery

Method (post), URL, optional Headers, optional Parameters.

Theses are for notifying other servers (e.g. your game servers) of these events.

Authentication providers

This section will allow you to set up authentication to allow players to be logged in to tournaments.

Dashboard game settings image

The options available are: Custom, Email, Anonymous, Steam, Apple, Google Play. Click on the appropriate icon and enter the required details as appropriate to your game to allow players to be authenticated.

Store integration

This section allows you to set up a webhook to pull in items from your in-game store/servers. This can then be used to set tournament requirements and award prizes etc.

Dashboard game settings image

To create the "External Store Hookup" enter the required parameter and the click the "Sync data now" button.

N.B.: you will need to press this button again if you add new items to your game and they're not showing up in the tournament dashboard.

Statistics

This section allows you to set up statistics that will be passed from your game to the tournament platform. As well as being used for potential tie-break situations, these are also, automatically, aggregated for each player for each season and (through the SDK) can be pulled back into the game and shown to players, or on leader boards, or against tournament results etc. (i.e. depending on your game type you could show: what abilities/weapons the top players used to win tournaments, how many kills they had in tournaments, how many goals they scored etc.).

Dashboard game settings image

Game statistics aggregation

To create the season profile for a player specify:

Game session

What game types do you want to pull down stats for (array of ids for your game types (comma separated e.g. 1v1, 2v2)).

Last matches

What game types do you want to pull down stats for (array of ids for game types (comma separated e.g. 1v1, 2v2)).

Value aggregation

Specify which stats that will be aggregated as values. You will automatically get; sum, average, minimum, maximum for the season (returned to the client through the SDK, i.e. game time/length).

Count aggregation

Specify which stats that will be aggregated as a "count of" items. This will count the occurrences of an item (rather than summing an ID) and also how many times you win/lose etc. with this item (i.e. a weapon was used n. times; n. wins, n. losses).

Custom properties

This section allows you to define other, global custom parameters (if any) that needs to be passed to the game through the SDK e.g. on which servers can the tournaments be played etc.

These are added by clicking the "Add custom properties" button. These are added as key-value pairs.

Cog icon (left, bottom of screen)

Dashboard settings image

Roles

Clicking on "Roles" allows you to add new users to the tournament dashboard. You do this by clicking the "+ Add user" button and entering the user's; First name, Last name, E-mail, and User type (either User or Admin).

Once a user is created, you can give them permissions (per game that you have set up in the Tournament dashboard) to be able to edit/not edit; Tournaments, Game Settings, Manage Store, Manage Players.

The user will be sent an email with an authentication link. They will need to click on this link and create a password to login.

My Account

Clicking on "My account" allows you to edit user profile details (i.e. change your name or email address or, if your're an administrator, your own system access rights).

Logout

This allows you to sign out of the Tournament dashboard.

ON THIS PAGE

Backend Integration

Custom asset server integration

It is possible to load items/currencies from a custom asset server and use them as tournament rewards or entry fees. Also, any sign up can be routed through a custom server that can deduct those items or reject tournament signup (e.g. user does not have enough currency).

Login with custom server

In order to authenticate login via custom server an HTTP endpoint has to be created that returns user basic info in a specific, JSON format. You have to do this if you want to have your user IDs associated with tournament account. Associated user IDs will then be provided in other custom server integration calls (e.g. reporting tournament results to custom server).

Data passed from the client for user authetication will be attached as application/x-www-form-urlencoded. Send data from client that are needed for user authentication (e.g. clinet token).

// coroutine
// ...
// use custom external login provider, pass custom data from your backend to authenticate user
var loginOperation = BackboneManager.Client.Login(LoginProvider.CustomExternal(
    true,
    MyBackend.Username,
    new KeyValuePair<string, object>("clientToken", MyBackend.Token),
    new KeyValuePair<string, object>("userId", MyBackend.Id)));
yield return loginOperation;

Check passed data from client and determine if user is valid. You have to respond with JSON format that contains your user ID and user nick.

 { 
    "id": "123", 
    "nickName": "nick" 
 }

The endpoint also has to respond with status code 200.

External store image

Once there is a endpoint that returns the required format, navigate to Game Settings/Authentication providers and enable custom authentication. Insert a valid HTTP path and add any necessary headers/parameters for authentication required by your server.

Loading store items from server

In order to load items and currencies from the custom server an HTTP endpoint has to be created that returns items in a specific, JSON format. You have to do this to make sure that the server loads the items for internal use and marks those items/currencies as "loaded" from the external resource.

JSON format:

{
   items:[
       {
           id:"itemId1", 
           name:"itemName1",
           image:"imageUrl (optional)"
       },
       {
           id:"itemId2", 
           name:"itemName2",
           image:"imageUrl (optional)"
       }],   
   currencies:[
       {
           id:"currencyId1", 
           name:"currencyName1"
       },
       {
           id:"currencyId2", 
           name:"currencyName2"
       }]
}

The endpoint also has to respond with status code 200. Always return all items you wish to import. Items/currencies that already exist will be updated.

External store image

Once there is a endpoint that returns the required format, navigate to Game Settings/Store Integration and enable "external store hookup". Select Custom URL provider. Insert a valid HTTP path and add any necessary headers/parameters for authentication required by your server.

Click on the Sync data now button which will import your items/currencies. You should now be able to access imported items when you are editing your tournament template rewards and entry fees.

If there are any updates to your assets just import them again by clicking on the Sync data now button.

Signup via custom server

In order to route tournament signups through a custom server, an HTTP endpoint has to be created that accepts a specific, JSON format. On every user signup, a call will be made to a custom server with a payload containing information about the tournament, user, entry fees, etc.

The HTTP call is POST with a parameter called userTicket containing the JSON payload.

JSON format:

{
   isSignup:true,
   isSignout:false,
   userId:"64bit number as string",
   userExternalId:"string",
   ticketId:"64bit number as string",
   tournamentId:"64bit number as string",
   customRequirements:[{
    name:"string",
    value:"string"
   }],
   fees:{
    items:[{
       id:"64bit number as string",
       externalId:"string",
       amount:1
    }],
    currencies:[{
       id:"64bit number as string",
       externalId:"string",
       amount:1
    }]
   }
}

A successful response is then awaited with HTTP status code 200 (or another code in case of rejection).

External signup image

Once there is a endpoint that accepts the required JSON format navigate to: Game Settings/Tournament settings Integration and enable "external signup". Insert a valid HTTP path and add any necessary headers/parameters for authentication required by your server.

After this setup, any signup from a client should be routed through the custom server before it's accepted and confirmed.

Tournament result report to custom server

To receive tournament results via a custom server, an HTTP endpoint has to be created that accepts a specific, JSON format. After every tournament finishes, a call will be made to a custom server with a payload containing information about the tournament, users and their prizes, etc.

The HTTP call is POST with a parameter called jsonPayload containing JSON payload.

JSON format:

{
   "tournamentId":"64bit number as string",   
   "prizes":[{
       "ticketId":"64bit number as string",
       "userId":"64bit number as string",
       "userExternalId":"string",
       "place":1,
       "items":[{
           "id":"64bit number as string",
           "amount":1,
           "externalId":"string"}],
       "currencies":[{
           "id":"64bit number as string",
           "amount":1,
           "externalId":"string"}]
    }]
}

A successful response is then awaited with HTTP status code 200. In case of failure, it will be repeated several times.

External prize delivery image

Once there is an endpoint that accepts the required JSON format, navigate to: Game Settings/Tournament settings Integration and enable "external prize delivery". Insert a valid HTTP path and add any necessary headers/parameters for authentication required by your server.

PlayFab asset server integration

It is possible to load items/currencies from a PlayFab backend and use them as tournament rewards or entry fees. Also, any sign up can be routed through a custom cloud script that can deduct those items or reject tournament signup (e.g. user does not have enough currency).

Create PlayFab secret key

For PlayFab integration you will need TitleId and SecretKey. Login to your playfab dashboard, select your desired game and open settings screen.

PlayFab game settings

You can find TitleId in API Features tab. You can create a new SecretKey in Secret Key tab. Do not set expiration date for secret key as that could cause outtage for your game in case you forget to set new one in tournament dashboard.

Login with PlayFab provider

If you integrating with PlayFab services we strongly recommend enabling only PlayFab login provider in tournament dashboard. Also NOTE that if you will not use PlayFab login provider other integration features such as tournament signup or delivering prizes via cloudscript fail to work.

Dashboard playfab login provider image

Enter here TitleId & SecretKey and save changes.

In your game client, after you initialize and login with playfab you will get a session ticket. This session ticket is then passed to playfab login provider to authenticate user and login to tournaments. This process will ensure that system has correct and valid playfab id associated with user account.

string playfabId;
string playfabSessionTicket;
bool isPlayfabLoginFinished;

// coroutine
// ...
// login playfab client
PlayFab.PlayFabClientAPI.LoginWithCustomID(
    new PlayFab.ClientModels.LoginWithCustomIDRequest() { 
        CreateAccount = true, 
        CustomId = "customPlayfabId" 
    },
    (result) =>
    {
        isPlayfabLoginFinished = true;
        // get playfab user id and session ticket (we will use it for authentication)
        playfabId = result.PlayFabId;
        playfabSessionTicket = result.SessionTicket;
    },
    (error) =>
    {
        isPlayfabLoginFinished = true;
    });
// wait until playfab login process finishes
yield return new WaitUntil(() => isPlayfabLoginFinished);
// use playfab login provider, pass playfab id and session ticket
yield return BackboneManager.Client.Login(
    Gimmebreak.Backbone.User.LoginProvider.Playfab(
        true, 
        "NickName-" + playfabId, 
        playfabSessionTicket, 
        playfabId));

To get associated playfab id with user account call BackboneManager.Client.User.GetLoginId(LoginProvider.Platform.Playfab). This can be used to compare if logged playfab client matches with logged tournament client.

Loading store items from Playfab

Navigate to Game Settings/Store Integration and enable "external store hookup". Select Playfab provider. Insert a valid title id & secret key and save chages.

External store playfab image

Click on the Sync data now button which will import your items/currencies (From primary catalogue). You should now be able to access imported items when you are editing your tournament template rewards and entry fees.

If there are any updates to your assets just import them again by clicking on the Sync data now button.

Signup via Playfab CloudScript

In order to route tournament signups through a Playfab CloudScript, an function has to be created that accepts a specific, JSON format. On every user signup, a call will be made to this cloudscript funtion with a payload containing information about the tournament, user, entry fees, etc.

The CloudScript function parameter args will contain the JSON payload.

JSON format:

{
   serverSecret:"string",
   isSignup:true,
   isSignout:false,
   userId:"64bit number as string",
   userPlayfablId:"string",
   ticketId:"64bit number as string",
   tournamentId:"64bit number as string",
   customRequirements:[{
    name:"string",
    value:"string"
   }],
   fees:{
    items:[{
       id:"64bit number as string",
       externalId:"string",
       amount:1
    }],
    currencies:[{
       id:"64bit number as string",
       externalId:"string",
       amount:1
    }]
   }
}

Here is an example CloudScript function that handles signup/signout:

// This is a example tournament signup/signout handler. If signup is requested 
// check if user can sign up and deduct any required fees. If signout is requested
// return any deducted fees back to users account.
handlers.tournamentSignup = function(args, context){
    if (args.serverSecret == "secret") {
        if (args.isSignup) {
            // Check any custom requirement for the tournament. This can be 
            // e.g. having required minimum rank. If player does not meet
            // specified criteria, the signup should be rejected.
            for (var i = 0; i < args.customRequirements.length; i++) {
                var name = args.customRequirements[i].name;
                var value = args.customRequirements[i].value;
                if (name == "testRequirement" &&
                    value == "reject") {
                    return { success: false };
                }
            }
            // Check if user has required signup fees.
            if (args.fees.items.length > 0 ||
                args.fees.currencies.length > 0) {
                // Get user inventory
                var userInvetoryResult = server.GetUserInventory({PlayFabId: currentPlayerId});
                // Check if user has enough currency
                for (var i = 0; i < args.fees.currencies.length; i++) {
                    var currencyFee = args.fees.currencies[i];
                    var userCurrency = userInvetoryResult.VirtualCurrency[currencyFee.externalId];
                    if (!userCurrency ||
                        userCurrency < currencyFee.amount) {
                        // User does not have required currency or amount
                        return { success: false };
                    }
                }
                // Sort user invetory items by id
                var userInventory = {};
                for (var i = 0; i < userInvetoryResult.Inventory.length; i++) {
                    var item = userInvetoryResult.Inventory[i];
                    if (!userInventory[item.ItemId]) {
                        userInventory[item.ItemId] = [];
                    }
                    userInventory[item.ItemId].push(item);
                }
                // Check if user has enough items
                for (var i = 0; i < args.fees.items.length; i++) {
                    var itemFee = args.fees.items[i];
                    var userItems = userInventory[itemFee.externalId];
                    if (!userItems ||
                        userItems.length < itemFee.amount) {
                        // User does not have required item or amount
                        return { success: false };
                    }
                }

                // Substract user's currencies
                for (var i = 0; i < args.fees.currencies.length; i++) {
                    var currencyFee = args.fees.currencies[i];
                    server.SubtractUserVirtualCurrency({PlayFabId: currentPlayerId, VirtualCurrency: currencyFee.externalId, Amount: currencyFee.amount });
                }

                // Revoke user's items
                var revokedItems = { Items: [] };
                for (var i = 0; i < args.fees.items.length; i++) {
                    var itemFee = args.fees.items[i];
                    for (var p = 0; p < itemFee.amount; p++) {
                        revokedItems.Items.push({PlayFabId: currentPlayerId, ItemInstanceId: userInventory[itemFee.externalId][p].ItemInstanceId});
                        // Maximum 25 items can be removed at once (Playfab documentation)
                        // If container is filled with 25 items, revoke and continue
                        if (revokedItems.Items.length == 25) {
                            server.RevokeInventoryItems(revokedItems);
                            revokedItems.Items = [];
                        }
                    }
                }
                // Check if any items should be revoked (last 25 items)
                if (revokedItems.Items.length > 0) {
                    server.RevokeInventoryItems(revokedItems);
                }

                // All fees deducted, confirm tournament signup
                return { success: true };
            }
            else {
                // No fees to deduct, confirm tournament signup
                return { success: true };
            }
        }
        if (args.isSignout) {
            // Return user's currencies
            for (var i = 0; i < args.fees.currencies.length; i++) {
                var currencyFee = args.fees.currencies[i];
                server.AddUserVirtualCurrency({PlayFabId: currentPlayerId, VirtualCurrency: currencyFee.externalId, Amount: currencyFee.amount });
            }
            // Return user's items
            var returnItems = { PlayFabId: currentPlayerId, ItemIds: [], Annotation: "Returned tournament fee items. TournamentId: " + args.tournamentId };
            for (var i = 0; i < args.fees.items.length; i++) {
                var itemFee = args.fees.items[i];
                for (var p = 0; p < itemFee.amount; p++) {
                    returnItems.ItemIds.push(itemFee.externalId);
                }
            }
            // Check if any items should be returned
            if (returnItems.ItemIds.length > 0) {
                server.GrantItemsToUser(returnItems);
            }
            // User fees has been returned
            return { success: true };
        }
    }
    return { success: false };
};

A successful response is then awaited in form { success: true } (or anything else in case of rejection).

External signup playfab image

Once there is a CloudScript function that accepts the required JSON format navigate to: Game Settings/Tournament settings Integration and enable "external signup". Select Playfab provider. Insert a valid title id, secret key, CloudScript function name, function version (revision number) and save changes.

There is pre-generated server secret that you can change if you want to. Use this secret in cloud script to prevent playfab user clients to execute it.

After this setup, any signup from a client should be routed through the cloud script before it's accepted and confirmed.

Tournament result via Playfab CloudScript

To receive tournament results via a Playfab CloudScript, an function has to be created that accepts a specific, JSON format. After every tournament finishes, a call will be made to this cloudscript funtion with a payload containing information about the tournament, users and their prizes, etc.

The CloudScript function parameter args will contain the JSON payload.

JSON format:

{
   "serverSecret":"string",
   "tournamentId":"64bit number as string",   
   "prizes":[{
       "ticketId":"64bit number as string",
       "userId":"64bit number as string",
       "userPlayfabId":"string",
       "place":1,
       "items":[{
           "id":"64bit number as string",
           "amount":1,
           "externalId":"string"}],
       "currencies":[{
           "id":"64bit number as string",
           "amount":1,
           "externalId":"string"}]
    }]
}

Here is an example CloudScript function that handles prize delivery:

// This is example tournament prize delivery handler. Once tournament is finished
// all results will be provided togather with information which items and currencies
// should be granted to user.
handlers.tournamentPrizeDelivery = function(args, context){
    if (args.serverSecret == "secret" &&
        args.prizes) {
        for (var i = 0; i < args.prizes.length; i++) {
            var prize  = args.prizes[i];
            // Grant user currency prizes
            for (var p = 0; p < prize.currencies.length; p++) {
                var wonCurrency = {
                    PlayFabId: prize.userPlayfabId, 
                    VirtualCurrency: prize.currencies[p].externalId, 
                    Amount: prize.currencies[p].amount 

                };
                server.AddUserVirtualCurrency(wonCurrency);
            }
            // Grant user item prizes
            var wonItems = { 
                PlayFabId: prize.userPlayfabId, 
                ItemIds: [], 
                Annotation: "Won items in tournament. Place: " + prize.place + " TournamentId: " + args.tournamentId
            };
            for (var p = 0; p < prize.items.length; p++) {
                var itemPrize = prize.items[p];
                for (var c = 0; c < itemPrize.amount; c++) {
                    wonItems.ItemIds.push(itemPrize.externalId);
                }
            }
            if (wonItems.ItemIds.length > 0) {
                server.GrantItemsToUser(wonItems);
            }
        }
    }
};

A successful response is then awaited with HTTP status code 200. In case of failure, it will be repeated several times.

External prize delivery playfab image

Once there is an endpoint that accepts the required JSON format, navigate to: Game Settings/Tournament settings Integration and enable "external prize delivery". Select Playfab provider. Insert a valid title id, secret key, CloudScript function name, function version (revision number) and save changes.

There is pre-generated server secret that you can change if you want to. Use this secret in cloud script to prevent playfab user clients to execute it.

Game server integration

Submit game session result from custom server

In order to process results from external server first navigate to: Game Settings/Tournament settings Integration and enable "external result submission".

To submit game session result from dedicated server or custom backed a specific HTTP call has to be made. The HTTP call is POST and the format has to be application/x-www-form-urlencoded.

WARNING You have to request dedicated SERVER ACCESS TOKEN that is meant only for your servers and must not be included in you game clients

Endpoint URL:

https://backbone-client-api.azurewebsites.net/api/v1/gameSessionSetResultFromServer

Headers:

BACKBONE_APP_ID: YOUR-TSDK-GAME-CLIENT-ID
ACCESS_TOKEN: YOU-WILL-BE-GIVEN-SERVER-ACCESS-TOKEN
Content-Type: application/x-www-form-urlencoded

application/x-www-form-urlencoded parameters:

gameSessionData

gameSessionData value has to be JSON string (do not include comments):

{
   // this is 64 bit number as string, id is provided from create game session API call, 
  "gameSessionId":"491197014055328536",
   // type of game session is anything you want between 0-127, default is 0
  // e.g.    0 - 1v1    1 - 2v2   etc you can give certain type to your games
  "type":"0",
  // this is 64 bit number as string, id is provided from match object e.g. match.Id
   // NB: NOT match.MatchId that is something different
  "tournamentMatchId":"491196930559318805",
   // time in seconds your game session lasted
  "time":"100",
   // list of users and their respective placements
  "users":[
    {
      "teamId":"1",
      "userId":"491174601636715209",
      "place":"2"
    },    
    {
      "teamId":"2",
      "userId":"491174351375178434",
      "place":"1"
    }
  ],
   // game session stats e.g. what type of map was played (optional)
  "gameStats":[
    {
      "statId":"2",
      "textValue":"test"
    }
  ],
  // user game session stats, e.g. how much damage specific user did (optional)
  "userStats":[
    {
      "statId":"1",
      "userId":"491174351375178434",
      "floatValue":"134.234"
    },
     {
      "statId":"1",
      "userId":"491174601636715209",
      "floatValue":"12.234"
    }
  ]
}

Get match data from server

Query tournament match data from game server.

Endpoint URL:

https://backbone-client-api.azurewebsites.net/api/v1/tournamentGetMatchDataForServer

Headers:

BACKBONE_APP_ID: YOUR-TSDK-GAME-CLIENT-ID
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded

application/x-www-form-urlencoded parameters:

accessToken
tournamentId
matchId
  • accessToken value has to be SERVER ACCESS TOKEN
  • tournamentId value has to be id of tournament match belongs to
  • matchId value has to be id of specific tournament match

WARNING When match is still searching for oppenents not all users are present in the results. Once match starts then returned users can be considered as immutable and final.

Response:

{
    "id": "620604471341234",
    "secret": "lz59JQiTfdgsdfg",
    "deadline": "2022-09-09T13:06:33.797Z",
    "matchId": 1,
    "phaseId": 1,
    "groupId": 1,
    "roundId": 1,
    "playedGameCount": 1,
    "maxGameCount": 1,
    "status": 8,
    "users": [
        {
            "userId": "6156293913241234",
            "userExternalId": "XXXX",
            "userPlayfabId": "XXXX",
            "teamId": 1,
            "checkedIn": false,
            "userScore": 0,
            "teamScore": 0,
            "userPoints": 0,
            "teamPoints": 0,
            "matchPoints": 0,
            "matchWinner": false
        },
        {
            "userId": "6203549123412344",
            "userExternalId": "XXXX",
            "userPlayfabId": "XXXX",
            "teamId": 2,
            "checkedIn": true,
            "userScore": 0,
            "teamScore": 0,
            "userPoints": 0,
            "teamPoints": 0,
            "matchPoints": 0,
            "matchWinner": false
        }
    ],
    "roundSettings": [
        {
            "partySize": 1,
            "maxTeamsPerMatch": 16,
            "minGameTime": 3,
            "maxRoundTime": 6
        }
    ],
    "propertySettings": [
        {
            "name": "property1",
            "value": "value1"
        },
        {
            "name": "property2",
            "value": "value2"
        }
    ],
    "gameSessions": [
        {
            "gameSessionId": "6206051532412344"
        }
    ]
}

Data reporting

Query tournament data reports

You can query past tournament data for custom analytics.

The HTTP calls are POST and the format has to be application/x-www-form-urlencoded.

WARNING You have to request dedicated SERVER ACCESS TOKEN that is meant only for your servers and must not be included in you game clients

Tournaments

Query finished tournaments from specific date range.

Endpoint URL:

https://backbone-client-api.azurewebsites.net/api/v1/dataReportGetTournaments

Headers:

BACKBONE_APP_ID: YOUR-TSDK-GAME-CLIENT-ID
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded

application/x-www-form-urlencoded parameters:

accessToken
sinceDate
untilDate
  • accessToken value has to be SERVER ACCESS TOKEN
  • sinceDate value has to be UTC date and time in format 2022-10-29T00:00:00 (inclusive)
  • untilDate value has to be UTC date and time in format 2022-10-30T00:00:00 (inclusive)

Response:

[
    {
        // 64 bit number as string
        "tournamentId": "63809769643125123",
        // 64 bit number as string
        "templateId": "63009307313241234",
        // UTC datetime when tournament was created
        "createdAt": "2022-10-27T19:32:26.063Z",
        // Default name of the tournament
        "name": "NameOfTheTournament 2v2",
        // Type of the tournament 
        // 0 - public
        // 1 - premium
        // 2 - private
        // 3 - testing
        "type": 1,
        // UTC datetime of tournament start
        "startedAt": "2022-10-29T23:00:00.000Z",
        // UTC datetime of tournament end
        "finishedAt": "2022-10-30T04:15:49.420Z",
        // User sign up count
        // NOTE: excludes players who fail to form a team
        "signedUpCount": 24563,
        // Maximum number of users
        "maxSignedUpCount": 30000,
        // Required size of team/party
        "partySize": 1,
        // Count of tournament phases
        "phaseCount": 4,
        // Count of all tournament rounds across all phases
        "roundCount": 34
    }
]

Tournament participants

Query participants of specific tournament.

Endpoint URL:

https://backbone-client-api.azurewebsites.net/api/v1/dataReportGetTournamentUsers

Headers:

BACKBONE_APP_ID: YOUR-TSDK-GAME-CLIENT-ID
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded

application/x-www-form-urlencoded parameters:

accessToken
tournamentId
  • accessToken value has to be SERVER ACCESS TOKEN
  • tournamentId value has to be ID of specific tournament

Response:

[
    {
        // UTC datetime when user was added (ticket created)
        "createdAt": "2022-10-25T15:00:35.447Z",
        // 64 bit number as string
        "tournamentId": "63592122754314523",
        // 64 bit number as string
        "ticketId": "63730450823452345234",
        // 64 bit number as string
        "userId": "410158954303241234",
        // External system ID as string
        "userExternalId": "XXXXXX",
        // 64 bit number as string
        "partyId": "6373045088123412432",
        // Status of user's ticket
        // 0 - invited
        // 1 - confirmed (can play)
        // 2 - declined
        // 3 - incomplete party
        // 4 - processing signup
        // 5 - signup fail
        // 6 - processign signout
        // 7 - signout fail
        "status": 1,
        // Flag indicating user's attendance
        "checkIn": false,
        // User's final placement
        "userPlace": 0,
        // User's total playtime (in minutes)
        "totalPlayTime": null,
        // User's total match count
        "matchCount": null,
        // User's total game count
        "gameCount": null,
        // User's reached phase (>=1)
        "maxPhase": null
    }
]

Tournament template stats

Query daily performance stats of templates from specific date range.

Endpoint URL:

https://backbone-client-api.azurewebsites.net/api/v1/dataReportGetTournamentTemplates

Headers:

BACKBONE_APP_ID: YOUR-TSDK-GAME-CLIENT-ID
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded

application/x-www-form-urlencoded parameters:

accessToken
sinceDate
untilDate
  • accessToken value has to be SERVER ACCESS TOKEN
  • sinceDate value has to be UTC date and time in format 2022-10-29T00:00:00 (inclusive)
  • untilDate value has to be UTC date and time in format 2022-10-30T00:00:00 (inclusive)

Response:

[
    {
        // UTC datetime of day stats account for
        "recordedAt": "2022-10-26T00:00:00.000Z",
        // 64 bit number as string
        "templateId": "60208317421341234",
        // Name of tournament template
        "name": "Battlecup",
        // Type of the tournament 
        // 0 - public
        // 1 - premium
        // 2 - private
        // 3 - testing
        "type": 2,
        // Count of tournaments for a day
        "tournamentCount": 1,
        // Daily tournament attendance.
        // Average percentage of users who checked in and played the tournament. 
        // Example 100 successful signups, 75 users played would result in 75% attendance. 
        "dtaAvg": 88.89,
        // Sum of checked in users who played all template tournaments this day.
        "dtaSum": 8,
        // Sum of unique checked in users who played all template tournament this day.
        "dtaUSum": 8,
        // Monthly tournament attendance.
        // Same as daily stat above but aggregated over past 30 days.
        "mtaAvg": 79.46,
        // Same as daily stat above but aggregated over past 30 days.
        "mtaSum": 465,
        // Same as daily stat above but aggregated over past 30 days.
        "mtaUSum": 47,
        // Weekly retention over past month (week 1)
        "wtr1": 47,
        // Weekly retention over past month (week 2)
        "wtr2": 31,
        // Weekly retention over past month (week 3)
        "wtr3": 16,
        // Weekly retention over past month (week 4)
        "wtr4": 14
    }
]

Unity Plugin

How to start

Install the unity plugin

Download and install the unity package (You will get download link once given access to SDK). Import the package as you normally would in unity with Assets/Import package/Custom Package.

This will include the dlls that are required to run tournaments within your game client.

Create new game and setup gameId

Once all the files are imported in to your game/project, you will need to create a game id. To do this, log in to the tournament dashboard, click on the add new game button and follow wizard steps until you have created your new game.

Add game image

Then open Game settings/General where you can find your game id.

Get gameid image

Next, in your unity project, find imported file named: BackboneClientSetting.asset which should be located in the Plugins/Gimmebreak.Backbone/Resources/ folder. Open the file and copy in your game id as shown below.

Set gameid image

Initialize SDK client

Add the BackboneManager script into your scene object. Tick the box which says Initialize on start if you want the client to initialize when Unity's Start() is called.

Add backbone manager image

Set backbone manager image

If you would like to initialize the client manually (e.g. once you check internet connectivity) you can do it with an explicit call later.

public class MyMonoBehaviour : MonoBehaviour
{
    //EXAMPLE 1, using callback
    public void ExplicitInitializeCall()
    {
        //initializing Backbone (TournamentSDK) client
        BackboneManager.Initialize()
            .ResultCallback((result) => {
                if(result)
                {
                    //success
                }
                else
                {
                    //fail
                }
            })
            .Run(this);
    }

    //EXAMPLE 2, using Unity coroutine
    public IEnumerator ExplicitInitializeCallCoroutine()
    {
        //some other code initialization
        //...
        //waiting for internet connectivity
        //...
        //initializing Backbone (TournamentSDK) client
        AsyncOperation<bool> asyncOperation = BackboneManager.Initialize();
        //wait until initialization is done
        yield return asyncOperation;
        //check result
        if(asyncOperation.ReturnValue)        
        {
            //success
        }
        else
        {
            //fail
        }
    }
}

You can check if the client is initialized by using BackboneManager.IsInitialized.

Login user

Enable login providers in dashboard

Open the tournament dashboard and navigate to: Game Settings/Authentication providers. Enable the providers that are relevant for your game. Fill in the required settings for each enabled provider and save.

Login providers image

Login user

If your login providers are set up you can proceed to log a user in. After client initialization, you can check if a user is logged in or not. If not you can proceed with the login operation.

In this example we are using Steam authentication:

private IEnumerator Start()
{
    //wait until backbone client is initialized
    while (!BackboneManager.IsInitialized)
    {
        yield return null;
    }
    //check if user is logged in
    if (!BackboneManager.IsUserLoggedIn)
    {
        //Obtain steam user name, auth session ticket, steam id from prefered steam
        //api library
        //...
        //login user using steam authentication provider
        var steamLogin = LoginProvider.Steam(true, userName, steamAuthCode, steamUserId);
        yield return BackboneManager.Client.Login(steamLogin);
    }
}

This will log the user in using the Steam login provider. After the user is successfully logged in, you can interact with the client API.

Basic tournament operations

Get list of tournaments

A list of all tournaments can be found in:

var allTournaments = BackboneManager.Client.Tournaments.TournamentList;

In order to load or refresh the list (it can be empty after login) we have to call BackboneManager.Client.LoadTournamentList() operation.

 //load/refresh tournament list
 BackboneManager.Client.LoadTournamentList()
     //set finish callback
     .FinishCallback(() =>
     {
         //bind data after operation finishes
         BindData();
     })
     //run async operation on this MonoBehaviour
     .Run(this);

NOTE: This operation returns bool result which indicates if the list was successfully refreshed. If this operation is called too often it will return false. The current "allowed refresh limit" is set to 1 minute. You can get the operation result by registering.

ResultCallback((result) => {}) as follows:

 //load/refresh tournament list
 BackboneManager.Client.LoadTournamentList()
     //set finish callback
     .FinishCallback(() =>
     {
         //bind data after operation finishes
         BindData();
     })
     .ResultCallback((result) => {
         if(result)
         {
             //successful refresh/load
         }
         else
         {
             //not refreshed/loaded
         }
     })
     //run async operation on this MonoBehaviour
     .Run(this);

After the tournament list is loaded, not all properties of the tournament class are populated. To load all tournament data,BackboneManager.Client.LoadTournament(tournamentId) has to be called. The tournament class contains a flag: HasAllDataLoaded that can be checked if all data has been loaded.

Get tournament

To load/refresh all tournament data call: BackboneManager.Client.LoadTournament(tournament). This operation returns bool which indicates if the tournament was successfully loaded/refreshed. Also there is a property on tournament class: tournament.HasAllDataLoaded indicating if all data has already been loaded previously.

//get first tournament in the list
var tournament = BackboneManager.Client.Tournaments.TournamentList[0];
//load/refresh all tournament data
BackboneManager.Client.LoadTournament(tournament)
    //set finish callback
    .FinishCallback(() =>
    {
        if(tournament.HasAllDataLoaded)
        {
            //all data has been loaded/refreshed
        }
    })
    //run async operation on this MonoBehaviour
    .Run(this);

Sign up for tournament

To check if a user is already signed up, look at the tournament property: tournament.Invite. If invite is null, the user does not have an invite and it's also not confirmed. To check if an invite is available, check: tournament.Invite.Status which holds the user's tournament invite status.

//get tournament
var tournament = BackboneManager.Client.Tournaments.GetTournamentById(tournamentId);
//check if user is signed up
if (tournament.Invite == null ||
    tournament.Invite.Status != TournamentUserStatus.Confirmed)
{
    //user is not signed up for tournament
}

To sign user up for a tournament call: SignupForTournament(tournamentId).

//sign up user for tournament
BackboneManager.Client.SignupForTournament(tournamentId)
    //set result callback
    .ResultCallback((result) =>
    {
        //check sign up result
        if (result.ProcessStatus != TournamentSignUpStatus.Ok)
        {
            LobbyAlertDialog.Show("Sign up process failed with status: " + result.ProcessStatus.ToString());
        }
    })
    //run async operation on this MonoBehaviour
    .Run(this);

This operation returns an InviteResult object that contains information about the signup result. If the external server for the signup process was set up in dashboard (e.g. to deduct currency, items, etc.) inviteResult.IsExternalSignupError will indicate if there was a problem during this operation. Also, any error message thrown by custom server can be found in inviteResult.ErrorMessage.

Tournament Hub

Initialize Tournament Hub

The Tournament Hub acts as a custom lobby for the tournament. Once connected, it will provide information about matches and the progress/status of the tournament. To receive tournament hub callback's, implement the ITournamentHubCallbackHandler interface.

public void OnInitialized(ITournamentHubController controller)
{
    //tournament hub was initialized and provides controller
}

public void OnHubStatusChanged(TournamentHubStatus newStatus)
{
    //tournament hub status has changed
}

public void OnTournamentUpdate()
{
    //tournament data has been updated
}

public void OnHubMatchStatusChanged(TournamentHubMatchStatus newStatus)
{
    //joined tournament match status has changed
}

public void OnHubMatchUpdate()
{
    //joined tournament match data has been updated
}

To initialize the Tournament Hub call: ConnectTournamentHub(callbackHandler, tournament); where callback handler is the object that implements ITournamentHubCallbackHandler.

var tournament = BackboneManager.Client.Tournaments.GetTournamentById(tournamentId);
BackboneManager.Client.ConnectTournamentHub(this, tournament);

Once the Tournament Hub is initialized it will call: OnInitialized(ITournamentHubController controller) and return a controller that is used to control certain tournament flows from user perspective (e.g. indicating that a user is ready for the next match).

Tournament hub statuses

OnHubStatusChanged(TournamentHubStatus newStatus) will indicate the current status of the tournament from a user's perspective. Your UI should react to any changes accordingly.

public void OnHubStatusChanged(TournamentHubStatus newStatus)
{       
    switch (newStatus)
    {
        case TournamentHubStatus.RegistrationClosed:
            //Registration is closed and has not been open yet. You can check
            //open time in tournament property 'RegistrationOpenTime'.
            break;
        case TournamentHubStatus.RegistrationOpening:
            //Registration is opening as 'RegistrationOpenTime' was reached but
            //confirmation from server is awaited.
            break;
        case TournamentHubStatus.RegistrationOpened:
            //Registration is opened and users can sign up for tournament.
            break;
        case TournamentHubStatus.RegistrationClosing:
            //Registration/Inivitation is closing as 'InvitationCloseTime' was reached
            //but confirmation from server is awaited.
            break;
        case TournamentHubStatus.WaitingForTournamentStart:
            //Registration is closed and tournament start confirmation from 
            //server is awaited.
            break;
        case TournamentHubStatus.Starting:
            //Tournament is starting as tournament 'Time' was reached but
            //confirmation from server is awaited.
            break;
        case TournamentHubStatus.Started:
            //Tournament has started. Get current phase from
            //'tournament.GetCurrentTournamentPhase()' containing user standings.
            break;
        case TournamentHubStatus.MatchInProgress:
            //User has match in progress he should be part of.
            //Get all match metadata from 'tournament.UserActiveMatch'. 
            break;
        case TournamentHubStatus.ResolvingPartiallyFilledMatch:
            //User active match was not filled in time. Awaiting confirmation from
            //server if match should be played.
            break;
        case TournamentHubStatus.ClosingOverdueMatch:
            //User active match reached a deadline and its due for closure.
            //Awaiting confirmation from server.
            break;
        case TournamentHubStatus.WaitingForUserReadyConfirmation:
            //User can proceed to next round of the tournament. Explicit confirmation
            //is requested by calling 'tournamentHubController.SetUserReady()'.
            break;
        case TournamentHubStatus.WaitingForNextPhase:
            //Tournament current phase is about to finish. Waiting for next phase to
            //start.
            break;
        case TournamentHubStatus.WaitingForTournamentToFinish:
            //User has finished all rounds in current phase or was already knocked
            //out of the tournament and waiting for tournament to finish.
            break;
        case TournamentHubStatus.Finishing:
            //Last round of last phase has reached deadline and all matches should
            //be finilized. Awaiting confirmation from server.
            break;
        case TournamentHubStatus.Finished:
            //Tournament has finished. Found out who won by looking at 
            //'tournament.Winner.Users'
            break;
    }
}

User's active match

When a tournament is running and a user has signed up, they will automatically be given an Active match that contains all metadata and statuses. A user's active match can be accessed as follows from the Tournament Hub: tournamentHub.Tournament.UserActiveMatch.

UserActiveMatch.Secret can be used as a network room/lobby password or name. This is distributed only to users that are allowed to join a specific match.

UserActiveMatch.Status should be used to determine if a user; is waiting for other users/opponents, must connect to a specific room/lobby when the game is in progress or, if the user should proceed to another match.

switch (tournamentHubController.Tournament.UserActiveMatch.Status)
{
    case TournamentMatchStatus.Created:
        //Match was successfuly created.
        break;
    case TournamentMatchStatus.WaitingForOpponent:
        //Match is not filled and opponents are still awaited to join. 
        break;
    case TournamentMatchStatus.GameReady:
        //Match game is ready to be played. Proceed to create game session that will
        //change status to 'GameInProgress'.
        //NB: users might still be checking in and connecting to room/lobby.
        //Client should wait until all parties are successfully connected and ready.
        //Only then procced to create game session.
        break;
    case TournamentMatchStatus.GameInProgress:
        //Match game is in progress, game session was created and it's in progress.
        //NB: user should be able to reconnect to ongoing game session.
        break;
    case TournamentMatchStatus.GameFinished:
        //Match game has finished as results were reported. If match requires more games
        //to be played per series (e.g. best of 3) proceed to create another game session
        //that will change status to 'GameInProgress' again.
        break;
    case TournamentMatchStatus.MatchFinished:
        //Match has finished as all games has been played (or deadline was reached).
        //It will be closed soon, user can proceed to another match.
        break;
    case TournamentMatchStatus.Closed:
        //Match was finalized and closed by server.
        break;
}

You can determine if opponents are ready by checking: UserActiveMatch.Users[i].IsCheckedIn. You can refresh: UserActiveMatch data by calling: tournamentHubController.RefreshActiveMatch().

Note that the UserActiveMatch can be null or it can be in finished state. In order to request another match and proceed in tournament user has to explicitly call tournamentHubController.SetUserReady(). User is expected to do this when tournament hub will enter a state TournamentHubStatus.WaitingForUserReadyConfirmation. This action can be represented with UI (e.g. Ready for next match) or it can be done automatically without user interaction moving him immediately to next match when tournament hub enters the state.

Joined match interface

If your game has defined methods for private game/lobby creation, implementing match interface makes easier to manage user joining and starting tournament matches.

Implement ITournamentMatchCallbackHandler interface to your lobby script or create a new scrip implementing this. It provides simple set of methods to communicate your lobby state to tournament hub.

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.
    //User controller to inform tournament hub about changes in your lobby/room.
}

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

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

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.
    //NB: local user that is not checked in for the match yet, will be checked in
    //only after returning true
}

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

Object implementing this interface can then passed to tournament hub controller method when joining specific match: tournamentHubController.JoinTournamentMatch().

Code flow example:

//Initialize tournament hub
BackboneManager.Client.ConnectTournamentHub(hubCallbackHandler, tournament);
//...
//Get hub controller 
public void OnInitialized(ITournamentHubController controller)
{
    hubController = controller
}
//...
//Check tournament hub is in "MatchInProgress" status
public void OnHubStatusChanged(TournamentHubStatus newStatus)
{       
    switch (newStatus)
    {
        case TournamentHubStatus.MatchInProgress:
        case TournamentHubStatus.ResolvingPartiallyFilledMatch:
            //...
            //Join users active match
            var match = hubController.Tournament.UserActiveMatch;
            hubController.JoinTournamentMatch(match, matchCallbackHandler);
            //...
            break;        
    }
}

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:

//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();
    }
}

Tournament game session & result submission

Based on tournament settings, a match can contain multiple game sessions. For example if a 1v1 match is set up as a "best of 3" series, it will require at least 2 game sessions to be played. A "game session" represents a single game in match series.

Create game session

In order to create a game session, call: CreateGameSession(users, userActiveMatchId, sessionType). The returned game session id can be then distributed to other clients. If a another subsequent call is made, the same game session id is returned.

//create game session only for checked in users
var checkedInUsers = userActiveMatch.Users.Where(user => user.IsCheckedIn);
//game session type can be used to identify specific game modes
//e.g. 0-default, 1-4player mode, 2-8player mode
var sessionType = 0;
//create game session
BackboneManager.Client.CreateGameSession(checkedInUsers, userActiveMatch.Id, sessionType)
    //set result callback
    .ResultCallback((gameSession) =>
    {
        //get game session id and distribute it to other clients
        //NB: this is up to developer, e.g. use custom room/lobby properties
        //or broadcast message
        var gameSessionId = gameSession.Id;
    })
    //run async operation on this MonoBehaviour
    .Run(this);

Submit results

After a game session is finished, results have to be reported before the match deadline is reached. If a match is set up to have more than one game session (e.g. best of 3 series) then GetMatchNextGameDeadline(match) can be used to determine the ideal deadline for the current game session so that the subsequent game session(s) still has time to be played.

//get user active match
var userActiveMatch = tournament.UserActiveMatch;
//get ideal deadline for next game session
var deadline = tournament.GetMatchNextGameDeadline(userActiveMatch);

To submit the game session result use the SubmitGameSession(gameSession) call. A game session object has to be created on all clients using the distributed game session id returned from the CreateGameSession call. It is important to set the users place which will determine the point distribution for a given game session.

//create game session only for checked in users
var matchUsers = userActiveMatch.Users.Where(user => user.IsCheckedIn).ToList();
//sort users based on your game session results, e.g. kills, deaths etc.
matchUsers.Sort((user1, user2) =>{
    //sort users from best to worst based on specific game rules
});
//create list for game session users
List<GameSession.User> gameSessionUsers = new List<GameSession.User>();
//loop through sorted users from best to worst
for(var i = 0; i < matchUsers.Count; i++)
{
    var userId = matchUsers[i].UserId;
    var teamId = matchUsers[i].TeamId;
    //add game session user with set final place in game session (more users
    //can have same placement if required)
    gameSessionUsers.Add(new GameSession.User(userId, teamId) { Place = (i + 1) });
}
//get user active match id
var matchId = userActiveMatch.Id;
//create game session using id that was obtained from 'CreateGameSession' call as
//well as passing tournament match id
GameSession gameSession = new GameSession(gameSessionId, 0, gameSessionUsers, matchId);
//set played date and game session duration
gameSession.PlayDate = DateTime.UtcNow;
gameSession.PlayTime = gameTime;
//submit game session to server
BackboneManager.Client.SubmitGameSession(gameSession)
    .ResultCallback((result) => {
        if (result)            
        {
            //game session was successfuly submitted
        }
    })
    .Run(this);

Miscellaneous

Setting client language

Client can define preffered language. When tournament has defined content in multiple languages preffered language will be served to the client. In case preffered laguage does not exist then english is given as default.

To set user language assign ISO639‑1 string to UserLanguage property.

BackboneManager.Client.User.UserLanguage = "en";
BackboneManager.Client.User.SetDataAsDirty();
BackboneManager.Client.SynchUser();

Tournament list caching and date range

Api call LoadTournamentList() has default caching set to 15 minutes. This can be changed by setting TournamentData.tournamentListUpdateLimit. Same way also default date range can be set wtih TournamentData.tournamentListSinceOffset and TournamentData.tournamentListUntilOffset.

// refresh tournament list cache every 5 minutes
TournamentData.tournamentListUpdateLimit = 5;
// load tournaments from 3 days ago
TournamentData.tournamentListSinceOffset = System.TimeSpan.FromDays(-3);
// load tournaments for next 14 days
TournamentData.tournamentListUntilOffset = System.TimeSpan.FromDays(14);

NOTE: caching does not apply when calling LoadTournaments() or LoadTournamentsAll().

API-BackboneClient

Properties

Game

Get games global data. (E.g. game id, global properties)

public GameData Game
{
    get;
}

IsInitialized

Determine if client is successfully initialized.

public bool IsInitialized
{
    get;
}

IsUserLoggedIn

Determine if user is logged in and has a valid session to do API calls.

public bool IsUserLoggedIn
{
    get;
}

Notifications

Get users notification data. (E.g. tournament party invitations)

public NotificationData Notifications
{
    get;
}

Season

Get current season data. (E.g. start date, end date, user season stats)

public SeasonData Season
{
    get;
}

Tournaments

Get users tournaments data. (E.g. list of tournaments)

public TournamentData Tournaments
{
    get;
}

User

Get users data. (E.g. nickname, platform ids, user properties)

public UserData User
{
    get;
}

Methods

AcceptPartyInvite(long, long)

Accepts tournament party invite. User has to sign up for tournament first before he can accept any party invite.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.TournamentAcceptPartyStatus> AcceptPartyInvite(long tournamentId, long partyInviteId)

Parameters

  • tournamentId: Tournament id.
  • partyInviteId: Tournament party invite id.

Returns

Accept party invite status.

Remarks

Party invites are received as notifications (TournamentPartyInviteNotification) and they contain tournamentId as well as partyInviteId.

AcceptPartyInvite(long, string)

Accepts tournament party invite by providing shared party code. User has to sign up for tournament first before he can accept any party invite.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.TournamentAcceptPartyStatus> AcceptPartyInvite(long tournamentId, string partyCode)

Parameters

  • tournamentId: Tournament id.
  • partyCode: Tournament party code.

Returns

Accept party invite status.

Remarks

Party codes are created on demand by calling CreatePartyCodeForTournament method. Once a party code exits, it can be shared with other users (e.g. chat) who can use it to join the party.

ChangeNickname(string)

Change user nick name. Unique hash number will be given on success.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> ChangeNickname(string nickName)

Parameters

  • nickName: New nick name (max 100 characters).

Returns

True if operation was successfull.

Remarks

If you want to force specific hash number (E.g. to match your own system) use ChangeNickname(string nickName, int preferredNickHash) instead.

ChangeNickname(string, int)

Change user nick name. Desired/preferred hash number can be provided but if combination is already taken then unique hash number will be given on success.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> ChangeNickname(string nickName, int preferredNickHash)

Parameters

  • nickName: New nick name (max 100 characters).
  • preferredNickHash: Desired/preferred hash number.

Returns

True if operation was successfull.

ConnectTournamentHub(ITournamentHubCallbackHandler, Tournament)

Connect and initialize tournament hub for specific tournament. Passed tournament hub (ITournamentHubCallbackHandler) will start receiveing callbacks until it is disconnected. This is mainly used for ongoing tournament to propagate tournament state changes via tournament hub.

public void ConnectTournamentHub(Gimmebreak.Backbone.Tournaments.ITournamentHubCallbackHandler tournamentHub, Gimmebreak.Backbone.Tournaments.Tournament tournament)

Parameters

  • tournamentHub: Tournament hub implementing ITournamentHubCallbackHandler.
  • tournament: Specific tournament to initialize tournament hub for.

Remarks

Only one tournament can be connected to tournament hub at a time. Even if different tournament hub object is passed connecting to other tournament it will stop callbacks on previousely connected tournament hub.

CreateGameSession(IEnumerable<TournamentMatch.User>, long, byte)

Creates game session for specific tournament match. Users passed must be a subset of users specified in tournament match.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.GameSessions.GameSession> CreateGameSession(System.Collections.Generic.IEnumerable<Gimmebreak.Backbone.Tournaments.TournamentMatch.User> users, long tournamentMatchId, byte gameSessionType = 0)

Parameters

  • users: Users participating in tournament match.
  • tournamentMatchId: Tournament match id.
  • gameSessionType: Type of game session (defined in dashboard, e.g. team match, deadmatch, capture flag, etc.)

Returns

Game session with unique Id, returns null if operation failed. Will be needed for reporting results.

Remarks

Same game session is returned for specific tournament match id until results are submitted and game session is closed.

CreateGameSession(IEnumerable<GameSession.User>, long, byte)

Creates game session for specific tournament match. Users passed must be a subset of users specified in tournament match.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.GameSessions.GameSession> CreateGameSession(System.Collections.Generic.IEnumerable<Gimmebreak.Backbone.GameSessions.GameSession.User> users, long tournamentMatchId, byte gameSessionType = 0)

Parameters

  • users: Users participating in tournament match.
  • tournamentMatchId: Tournament match id.
  • gameSessionType: Type of game session (defined in dashboard, e.g. team match, deadmatch, capture flag, etc.)

Returns

Game session with unique Id, returns null if operation failed. Will be needed for reporting results.

Remarks

Same game session is returned for specific tournament match id until results are submitted and game session is closed.

CreatePartyCodeForTournament(long, bool)

Create party code for tournament that can be shared and used to join the party.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.PartyCodeResult> CreatePartyCodeForTournament(long tournamentId, bool recreate = False)

Parameters

  • tournamentId: Tournament id.
  • recreate: If set to true existing party code will be replaced with new one.

Returns

Result of party create code process.

Remarks

When party code is recreated by setting "recreate" parameter to true then old party code will be rendered invalid and can no longer be used for joining the party.

CreatePartyInviteForTournament(long, long)

Create party invite for tournament by providing user id.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.PartyInviteResult> CreatePartyInviteForTournament(long tournamentId, long inviteUserId)

Parameters

  • tournamentId: Tournament id.
  • inviteUserId: Id of user that will receive party invite.

Returns

Result of party invite process.

CreatePartyInviteForTournament(long, LoginProvider.Platform, string)

Create party invite for tournament by providing type and user id of specific platform.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.PartyInviteResult> CreatePartyInviteForTournament(long tournamentId, Gimmebreak.Backbone.User.LoginProvider.Platform inviteUserPlatformType, string inviteUserPlatformId)

Parameters

  • tournamentId: Tournament id.
  • inviteUserPlatformType: Specific platform type.
  • inviteUserPlatformId: Specific platform id.

Returns

Result of party invite process.

CreatePartyInviteForTournament(long, string, int)

Create party invite for tournament by providing users nick name and hash number.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.PartyInviteResult> CreatePartyInviteForTournament(long tournamentId, string inviteUserNick, int inviteUserHash)

Parameters

  • tournamentId: Tournament id.
  • inviteUserNick: Users nick name.
  • inviteUserHash: Users hash number.

Returns

Result of party invite process.

DeclinePartyInvite(long)

Decline tournament party invite.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> DeclinePartyInvite(long partyInviteId)

Parameters

  • partyInviteId: Party invite id.

Returns

True if operation was successfull.

Remarks

Declining an invite will not prevent other user to send another one again.

DisconnectTournamentHub()

Disconnects tournament hub and stops running callbacks.

public void DisconnectTournamentHub()

DismissNotification(Notification)

Dismiss users notification and remove it from active list. Notification is still available until next list refresh. Check IsDismissed property to determine if notification was already dismissed.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> DismissNotification(Gimmebreak.Backbone.Notifications.Notification notification)

Parameters

  • notification: Notification to dismiss.

Returns

Ture if notification was dismissed.

DownloadGameSessionReplay(GameSession)

Downloads game session replay if available and populates list of submissions on game session replay object (GameSessions.GameSession.Replay).

public Gimmebreak.Backbone.Core.AsyncOperation<bool> DownloadGameSessionReplay(Gimmebreak.Backbone.GameSessions.GameSession gameSession)

Parameters

  • gameSession: Valid game session.

Returns

True if operation was successful.

Initialize(IBackboneClientCallbackHandler)

Initialize Backbone client using setting from Default BackboneClientSetting asset.

public static Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Core.BackboneClient> Initialize(Gimmebreak.Backbone.Core.IBackboneClientCallbackHandler callbackHandler = null)

Parameters

  • callbackHandler: Pass handler that will listen to client callbacks.

Returns

Initialized Backbone client ready for use.

Initialize(BackboneClientSetting, IBackboneClientCallbackHandler)

Initialize Backbone client using setting from BackboneClientSetting asset.

public static Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Core.BackboneClient> Initialize(Gimmebreak.Backbone.Core.BackboneClientSetting settings, Gimmebreak.Backbone.Core.IBackboneClientCallbackHandler callbackHandler = null)

Parameters

  • settings: BackboneClientSetting asset.
  • callbackHandler: Pass handler that will listen to client callbacks.

Returns

Initialized Backbone client ready for use.

Initialize(Uri, string, IBackboneClientCallbackHandler)

Initialize Backbone client for specific server and game.

public static Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Core.BackboneClient> Initialize(System.Uri server, string gameId, Gimmebreak.Backbone.Core.IBackboneClientCallbackHandler callbackHandler = null)

Parameters

  • server: Server url or ip.
  • gameId: Game id obtained from Backbone dashboard.
  • callbackHandler: Pass handler that will listen to client callbacks.

Returns

Initialized Backbone client ready for use.

LoadNotifications()

Load users active notifications (e.g. news, party invites, etc.)

public Gimmebreak.Backbone.Core.AsyncOperation<bool> LoadNotifications()

Returns

True if notifications were loaded

LoadTournament(Tournament)

Load all tournament data. (Party users, matches, details, etc.)

public Gimmebreak.Backbone.Core.AsyncOperation<bool> LoadTournament(Gimmebreak.Backbone.Tournaments.Tournament tournament)

Parameters

  • tournament: Tournament to load data for.

Returns

True if data were loaded.

LoadTournament(long)

Load all tournament data. (Party users, matches, details, etc.)

public Gimmebreak.Backbone.Core.AsyncOperation<bool> LoadTournament(long tournamentId)

Parameters

  • tournamentId: Tournament id to load data for.

Returns

True if data were loaded.

LoadTournamentList()

Load tournament list containing past, upcoming and current tournaments. Loaded tournaments do not contain all metadata. You can check property HasAllDataLoaded. To load all data you have to subsequently call LoadTournament().

public Gimmebreak.Backbone.Core.AsyncOperation<bool> LoadTournamentList()

Returns

True if list was refreshed successfully.

LoadTournamentMatches(long, IEnumerable<long>)

Load tournament matches for specific match ids. The return limit is 25 matches per call.

public Gimmebreak.Backbone.Core.AsyncOperation<System.Collections.Generic.List<Gimmebreak.Backbone.Tournaments.TournamentMatch>> LoadTournamentMatches(long tournamentId, System.Collections.Generic.IEnumerable<long> matchIds)

Parameters

  • tournamentId: Tournament id.
  • matchIds: Specific tournament match ids (max 25).

Returns

List of tournament matches on success, otherwise null.

LoadTournamentMatches(long, int, int, int, int, int, bool, int)

Load tournament matches for specific phase. This method allows to browse all matches in tournament.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.MatchesPaginatedResult> LoadTournamentMatches(long tournamentId, int phaseId, int fromRoundId, int toRoundId, int maxResults, int page, bool onlyInProgress, int groupId = 0)

Parameters

  • tournamentId: Tournament id.
  • phaseId: Specific tournament phase to load matches for.
  • fromRoundId: Phase round id to load matches from, inclusive.
  • toRoundId: Phase round id to load matche to, inclusive.
  • maxResults: Max results to be returned per page.
  • page: Requested page number.
  • onlyInProgress: Loads only matches that are in progress.
  • groupId: Specific phase group to load matches for.

Returns

One page of tournament matches.

LoadTournamentMatchesAll(long, int, int)

Load all tournament matches for specific phase. If phase contains large number of matches the operation will be split into more than one call and can take same time to finish. If you wish to load/refresh only specific matches use LoadTournamentMatches() method. (E.g. to load only matches from first round.)

public Gimmebreak.Backbone.Core.AsyncOperation<System.Collections.Generic.List<Gimmebreak.Backbone.Tournaments.TournamentMatch>> LoadTournamentMatchesAll(long tournamentId, int phaseId, int groupId = 0)

Parameters

  • tournamentId: Tournament id.
  • phaseId: Specific tournament phase to load matches for.
  • groupId: Specific phase group to load matches for.

Returns

List of all phase matches, otherwise null.

LoadTournamentMatchGameSessions(TournamentMatch)

Loads finished game sessions of tournament match. Only tournament match that is ongoing or finished will be processed.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> LoadTournamentMatchGameSessions(Gimmebreak.Backbone.Tournaments.TournamentMatch tournamentMatch)

Parameters

  • tournamentMatch: Tournament match that is ongoing or finished.

Returns

True if operation was successfull.

LoadTournaments(DateTime, DateTime, int, int)

Loads tournaments between specific dates. Loaded tournaments do not contain all metadata. You can check property HasAllDataLoaded. To load all data you have to subsequently call LoadTournament().

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.TournamentsPaginatedResult> LoadTournaments(System.DateTime sinceDate, System.DateTime untilDate, int maxResults = 25, int page = 1)

Parameters

  • sinceDate: UTC date time (smaller value then until date).
  • untilDate: UTC date time (bigger value than since date).
  • maxResults: Max results to be returned per page.
  • page: Requested page number.

Returns

One page of tournament.

LoadTournamentsAll(DateTime, DateTime)

Load all tournaments between specific dates. Loaded tournaments do not contain all metadata. You can check property HasAllDataLoaded. To load all data you have to subsequently call LoadTournament(). If time frame contains large number of tournaments the operation will be split into more than one call and can take same time to finish. If you wish to load/refresh only specific tournaments use LoadTournaments() method.

public Gimmebreak.Backbone.Core.AsyncOperation<System.Collections.Generic.List<Gimmebreak.Backbone.Tournaments.Tournament>> LoadTournamentsAll(System.DateTime sinceDate, System.DateTime untilDate)

Parameters

  • sinceDate: UTC date time (smaller value then until date).
  • untilDate: UTC date time (bigger value than since date).

Returns

List of all tournaments between dates, otherwise null.

LoadTournamentScores(long, int, int, int, int)

Load tournament scores for specific phase. This method allows to browse all party scores in tournament.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.ScoresPaginatedResult> LoadTournamentScores(long tournamentId, int phaseId, int maxResults, int page, int groupId = 0)

Parameters

  • tournamentId: Tournament id.
  • phaseId: Specific tournament phase to load scores for.
  • maxResults: Max results to be returned per page.
  • page: Requested page number.
  • groupId: Specific phase group to load scores for.

Returns

One page of tournament scores.

LoadTournamentScoresAll(long, int, int)

Load all tournament scores for specific phase. If phase contains large number of parties the operation will be split into more than one call and can take same time to finish. If you wish to load/refresh only specific scores use LoadTournamentScores() method. (E.g. to load only scores from top 10 places.)

public Gimmebreak.Backbone.Core.AsyncOperation<System.Collections.Generic.List<Gimmebreak.Backbone.Tournaments.TournamentScore>> LoadTournamentScoresAll(long tournamentId, int phaseId, int groupId = 0)

Parameters

  • tournamentId: Tournament id.
  • phaseId: Specific tournament phase to load scores for.
  • groupId: Specific phase group to load scores for.

Returns

List of all phase scores, otherwise null.

LoadUserSeasonProfile(long, int)

Loads user profile for specific season. This contains stats such as played games, tournaments, etc., as well as last played game sessions.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Season.UserSeasonProfile> LoadUserSeasonProfile(long userId, int season)

Parameters

  • userId: User id
  • season: Season number, starting with 1.

Returns

User season profile if operation was successfull.

LoadUserSeasonProfile(LoginProvider.Platform, string, int)

Loads user profile for specific season. This contains stats such as played games, tournaments, etc., as well as last played game sessions.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Season.UserSeasonProfile> LoadUserSeasonProfile(Gimmebreak.Backbone.User.LoginProvider.Platform userPlatformType, string userPlatformId, int season)

Parameters

  • userPlatformType: Specific platform type.
  • userPlatformId: Specific platform id.
  • season: Season number, starting with 1.

Returns

User season profile if operation was successfull.

Login(LoginProvider)

Login user via specific login provider. If user does not exists it fails or creates a new one based on provider setting.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.User.LoginResult> Login(Gimmebreak.Backbone.User.LoginProvider loginProvider)

Parameters

  • loginProvider: Login provider used for authentication.

Returns

Access and refresh token pair.

Logout()

Logout user and remove all cached data

public Gimmebreak.Backbone.Core.AsyncOperation<bool> Logout()

Returns

True if user was logged out

RemovePartyUser(long, long)

Remove user from tournament party. Other users in party can be only removed by party leader. You can pass your own id to leave party.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.TournamentRemoveUserStatus> RemovePartyUser(long tournamentId, long userId)

Parameters

  • tournamentId: Tournament id of party to remove user from.
  • userId: Id of user to be removed.

Returns

Remove user status.

Remarks

If party leader leaves party (By removing himself) new party leader is automatically set from remaining party users.

ReportUser(long, ReportReason, long, long, long)

Report user for suspected cheating or other reason. This action can be initiated by user or automatically by any anticheat. Use correct report reason indicating who initiated the report (User or System).

public Gimmebreak.Backbone.Core.AsyncOperation<bool> ReportUser(long userId, Gimmebreak.Backbone.User.ReportReason reportReason, long gameSessionId = 0, long tournamentMatchId = 0, long tournamentId = 0)

Parameters

  • userId: Id of user to be reported.
  • reportReason: Reason of report.
  • gameSessionId: Optional game session id (usefull to track down game session replay).
  • tournamentMatchId: Optional tournament match id.
  • tournamentId: Optional tournament id.

Returns

True if operation was successful.

Remarks

Reports are immediately visible in tournament dashboard. In case of ongoing tournament, admin can kick or ban excessively reported users.

SaveSession()

Snapshots client state and save data to disk. When client initializes it loads latest snapshot. This way game can access user data in offline mode.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> SaveSession()

Returns

True if snapshot was saved.

SignoutFromTournament(long)

Sign out from tournament if registration is open.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.InviteResult> SignoutFromTournament(long tournamentId)

Parameters

  • tournamentId: Id of tournament to sign out from.

Returns

Reuslt of signout process. Can return null if tournament id is invalid.

SignupForTournament(long)

Sign up user for tournament if registration is open.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.InviteResult> SignupForTournament(long tournamentId)

Parameters

  • tournamentId: Id of tournament to sign up for.

Returns

Result of signup process. Can return null if tournament id is invalid.

Remarks

If there are custom requirements set for tournament that can be verified locally check them before proceeding to user signup that would fail. Use server requirement verification as sanity check rather than directing user experience flow. The same applies for any entry fees for tournament. Check if user has all entry fees (E.g. currency, items, points) before proceeding to user signup that would fail.

SignupForTournament(string)

Sign up user for tournament if registration is open.

public Gimmebreak.Backbone.Core.AsyncOperation<Gimmebreak.Backbone.Tournaments.InviteResult> SignupForTournament(string privateCode)

Parameters

  • privateCode: Private code of tournament to sign up for.

Returns

Result of signup process.

Remarks

If there are custom requirements set for tournament that can be verified locally check them before proceeding to user signup that would fail. Use server requirement verification as sanity check rather than directing user experience flow. The same applies for any entry fees for tournament. Check if user has all entry fees (E.g. currency, items, points) before proceeding to user signup that would fail.

SubmitGameSession(GameSession, byte[])

Submits game session result to server. All users have to have final place set in order to assign points. You can provide game stats set up in dahboard (e.g. number of shots fired, kills, picked abilities, etc.). All match participants have to submit results and they have to be same. If someone reports different outcome such match will be marked and users will receive warning report to further investigate potential cheating.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> SubmitGameSession(Gimmebreak.Backbone.GameSessions.GameSession gameSession, byte[] replayData = null)

Parameters

  • gameSession: Game session with final results and stats.
  • replayData: Optional replay data of game session.

Returns

True if submission was succesfull.

SubmitGameSessionReplay(long, byte[])

Submits user game session replay to server.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> SubmitGameSessionReplay(long gameSessionId, byte[] replayData = null)

Parameters

  • gameSessionId: Valid game session id.
  • replayData: Uncompressed raw replay data. (Data will be compressed before sending)

Returns

True if operation is successful.

Remarks

All users have to submit game session replay successfully otherwise it will not indicate that replay is available.

SynchUser(bool)

Get the latest user data from server.

public Gimmebreak.Backbone.Core.AsyncOperation<bool> SynchUser(bool fullSynch = False)

Parameters

  • fullSynch: Some parts are updated incrementally, set true if full sych should be done.

Returns

True if operation was successfull.

ON THIS PAGE

API-ITournamentHubCallbackHandler

Methods

OnHubMatchStatusChanged(TournamentHubMatchStatus)

Callback informing that joined tournament match has changed status for user. If tournament hub status indicates that user should be in ongoing match then UserActiveMatch on tournament object will contain metadata for specific match. To receive match statuses use tournament hub controller and call ITournamentHubController.JoinTournamentMatch to join tournament match.

public abstract virtual void OnHubMatchStatusChanged(Gimmebreak.Backbone.Tournaments.TournamentHubMatchStatus newStatus)

Parameters

  • newStatus: New tournament match status for user.

OnHubMatchUpdate()

Callback informing that user active match metadata has been updated.

public abstract virtual void OnHubMatchUpdate()

Remarks

This callback does not necessary mean there have been any changes to user active match. It indicates that user active match was refreshed with the most recent data. This callback can be used to trigger UI updates.

OnHubStatusChanged(TournamentHubStatus)

Callback informing that tournament has changed status for user. Tournament hub undergoes many status changes during ongoing tournament. The status is informing of exact state of tournament for logged user.

public abstract virtual void OnHubStatusChanged(Gimmebreak.Backbone.Tournaments.TournamentHubStatus newStatus)

Parameters

  • newStatus: New tournament hub status for user.

Remarks

User that is signed up for tournament will receive different statuses than user who is not. Use statuses to inform user what is happeing, if they should wait, join match, sign up etc.

OnInitialized(ITournamentHubController)

Callback informing that tournament hub was initialized and providing coresponding controller to interact with tournament. This is result of connecting tournament hub with ConnectTournamentHub() call on backbone client.

public abstract virtual void OnInitialized(Gimmebreak.Backbone.Tournaments.ITournamentHubController controller)

Parameters

  • controller: Tournament hub controller for interaction with related tournament.

Remarks

This callback is not executed immediately after ConnectTournamentHub() is called. The initialization process takes few seconds after which OnInitialized is executed.

OnTournamentUpdate()

Callback informaing that tournament metadata has been updated.

public abstract virtual void OnTournamentUpdate()

Remarks

This callback does not necessary mean there have been any changes to tournament metadata. It indicates that tournament was refreshed with the most recent data. This callback can be used to trigger UI updates.

API-ITournamentHubController

Properties

ActiveMatchFinishDeadlineCountdown

Determine remaining time until users active match has to be finished.

public abstract virtual TimeSpan ActiveMatchFinishDeadlineCountdown
{
    get;
}

Remarks

Finish deadline can be obtained for any match from match property Deadline.

ActiveMatchStartDeadlineCountdown

Determine remaining time until users active match should start.

public abstract virtual TimeSpan ActiveMatchStartDeadlineCountdown
{
    get;
}

Remarks

Start deadline can be obtained for any match by calling tournament.GetMatchStartDeadline(TournamentMatch).

IsConnectedToGameServerNetwork

Determine if tournament match handler is connected to game servers.

public abstract virtual bool IsConnectedToGameServerNetwork
{
    get;
}

Remarks

This can be used to inform UI (E.g. tournament handler is switching to preferred tournament server region).

IsGameSessionInProgress

Determine if game session is in progress.

public abstract virtual bool IsGameSessionInProgress
{
    get;
}

IsMatchHandlerInitialized

Determine if tournament match handler is initialized.

public abstract virtual bool IsMatchHandlerInitialized
{
    get;
}

Remarks

This is set to true after successful call by JoinTournamentMatch(TournamentMatch, ITournamentMatchCallbackHandler). Also set back to false after LeaveTournamentMatch() call.

IsTournamentRunning

Determine if tournament is running (has started).

public abstract virtual bool IsTournamentRunning
{
    get;
}

IsUserKnockedOut

Determine if user has been knocked out of the tournament.

public abstract virtual bool IsUserKnockedOut
{
    get;
}

NextEventCountdown

Determine remaining time until next deciding event for user. (E.g. remaining time until tournament starts.)

public abstract virtual TimeSpan NextEventCountdown
{
    get;
}

PartyInvites

Get all party invites user received for this tournament. This contains also dismissed invites.

public abstract virtual IEnumerable<TournamentPartyInviteNotification> PartyInvites
{
    get;
}

Remarks

To refresh user invites (E.g. to get recently received ones) call LoadNotifications() on backbone client.

Status

Get current tournament hub status.

public abstract virtual TournamentHubStatus Status
{
    get;
}

Tournament

Get tournament that this controller was initialized for.

public abstract virtual Tournament Tournament
{
    get;
}

Methods

IsUserConnectedToMatch(long)

Determine if specific user is connected to tournament match room/lobby.

public abstract virtual bool IsUserConnectedToMatch(long userId)

Parameters

  • userId: Backbone user id.

Returns

True if user is connected.

Remarks

This can be used to inform UI (E.g. tournament user is connected in room/lobby).

IsUserReadyForMatch(long)

Determine if specific user is ready for tournament match.

public abstract virtual bool IsUserReadyForMatch(long userId)

Parameters

  • userId: Backbone user id.

Returns

True if user is ready.

Remarks

This can be used to inform UI (E.g. user is connected in room/lobby, is set on desired room slot, etc.).

JoinTournamentMatch(TournamentMatch, ITournamentMatchCallbackHandler)

Informs tournament hub that user wants to join a specific match and initiate connection to games room/lobby. It also requires to pass tournament match callback handler (ITournamentMatchCallbackHandler) that is an interface to games room/lobby handling API (E.g. set up a private game room with specific parameters).

public abstract virtual void JoinTournamentMatch(Gimmebreak.Backbone.Tournaments.TournamentMatch match, Gimmebreak.Backbone.Tournaments.ITournamentMatchCallbackHandler tournamentMatchHandler)

Parameters

  • match: Tournament match user wants to join.
  • tournamentMatchHandler: Tournament match handler implementing ITournamentMatchCallbackHandler interfacing room/lobby handling API.

Remarks

After requesting to join a match tournament hub will start receiving tournament match status via callback OnHubMatchStatusChanged(TournamentHubMatchStatus).

LeaveTournamentMatch()

Leave joined tournament match.

public abstract virtual void LeaveTournamentMatch()

Remarks

After requesting to leave a match tournament hub will stop receiving tournament match status via callback OnHubMatchStatusChanged(TournamentHubMatchStatus). This will also prompt tournament match handler to leave joined room/lobby.

RefreshActiveMatch()

User request to refresh UserActiveMatch data.

public abstract virtual void RefreshActiveMatch()

SetUserReady()

User informs tournament that he is ready for next match. This is meant to be used in case when tournament hub status is set to TournamentHubStatus.WaitingForUserReadyConfirmation.

public abstract virtual void SetUserReady()

Remarks

Almost every time when user finishes a match then system does not immediately move users to next round. It also depends on specific phase format if user will be given autolose or moved to next match when round finishes. As an example, in bracket setup, winning users are moved forward when round is finished. As an example, in arena setup, users will be given autolose for failing to call SetUserReady().

API-ITournamentMatchCallbackHandler

Methods

IsConnectedToGameServerNetwork()

Callback from tournament hub to check if client is successfully connected to your networking backend.

public abstract virtual bool IsConnectedToGameServerNetwork()

Returns

True if user is connected and ready to join given match.

IsGameSessionInProgress()

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

public abstract virtual bool IsGameSessionInProgress()

Returns

True if game session is in progress.

IsUserConnectedToMatch(long)

Callback from tournament hub to check if specific user is already connected to lobby/room. This method is called for every user that is expected to be in connected tournament match.

public abstract virtual bool IsUserConnectedToMatch(long userId)

Parameters

  • userId: Backbone user id.

Returns

True if user is connected.

IsUserReadyForMatch(long)

Callback from tournament hub to check if specific user is ready (E.g. moved to correct slot).

public abstract virtual bool IsUserReadyForMatch(long userId)

Parameters

  • userId: Backbone user id.

Returns

True if user is ready to start.

OnJoinTournamentMatch(Tournament, TournamentMatch, ITournamentMatchController)

Callback from tournament hub passing tournament, match and match controller object. Use match data to join correct lobby/room (E.g. using match Secret as room id). Use match controller to inform tournament hub about changes in your lobby/room (E.g. new player connected, player disconnected, etc.).

public abstract virtual void OnJoinTournamentMatch(Gimmebreak.Backbone.Tournaments.Tournament tournament, Gimmebreak.Backbone.Tournaments.TournamentMatch match, Gimmebreak.Backbone.Tournaments.ITournamentMatchController controller)

Parameters

  • tournament: Tournament which initiate this match.
  • match: Tournament match requested to be joined.
  • controller: Tournament match controller for interaction with related tournament hub.

Remarks

If you require specific parameters for room/lobby API (E.g. map, modes, allowed abilities, etc.), you can use tournament custom properties providing the info. This method is also called only once therefore any room/lobby joining procedure should repeatedly try to connect until OnLeaveTournamentMatch() is called.

OnLeaveTournamentMatch()

Callback from tournament hub informing user should leave joined lobby/room.

public abstract virtual void OnLeaveTournamentMatch()

StartGameSession(IEnumerable<TournamentMatch.User>)

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.

public abstract virtual void StartGameSession(System.Collections.Generic.IEnumerable<Gimmebreak.Backbone.Tournaments.TournamentMatch.User> checkedInUsers)

Parameters

  • checkedInUsers: Users that checked in for match. (In case of parties, only users from fully checked in parties)

Remarks

When this method is called, user should initiate CreateGameSession(TournamentMatch.User[], long, byte) on backbone client. After user receives a valid tournament game session it should proceed to start a game.

API-ITournamentMatchController

Methods

ReportDisconnectedUser(long)

Report user id who just disconnected room/lobby.

public abstract virtual void ReportDisconnectedUser(long userId)

Parameters

  • userId: Backbone user id.

ReportJoinedUser(long)

Report user id who just joined room/lobby.

public abstract virtual void ReportJoinedUser(long userId)

Parameters

  • userId: Backbone user id.

ReportStatusChange()

Report any chage in room/lobby that could affect any user "ready for match" status.

public abstract virtual void ReportStatusChange()

API-TournamentMatch

Constructors

TournamentMatch()

Create an instance of tournament match.

public TournamentMatch()

Properties

CheckedInUserCount

Current number of checked in users.

public int CheckedInUserCount
{
    get;
    set;
}

CurrentGameCount

Current number of games that have been already played (this includes also auto win games).

public int CurrentGameCount
{
    get;
    set;
}

Deadline

This is total series deadline after which winner has to be determined.

public DateTime Deadline
{
    get;
    set;
}

FullyCheckedInTeamCount

Current number of fully checked in teams (all party memebers have checked in for match).

public int FullyCheckedInTeamCount
{
    get;
    set;
}

GameSessions

List of finished game sessions. List has to be loaded on demand. To populate game sessions use LoadTournamentMatchGameSessions(TournamentMatch) on backbone client.

public List<GameSession> GameSessions
{
    get;
    set;
}

GroupId

Determine match group id (Only used in tournament phases with groups).

public int GroupId
{
    get;
    set;
}

Id

Unique match id.

public long Id
{
    get;
    set;
}

MatchId

Match id is used for brackets, Id is only unique for each round.

public int MatchId
{
    get;
    set;
}

MaxGameCount

Maximum number of games that can be played for this match.

public int MaxGameCount
{
    get;
    set;
}

MinCheckinsPerTeam

Determine minimum number of checkins per team to consider team as partially checked in.

public int MinCheckinsPerTeam
{
    get;
    set;
}

PartiallyCheckedInTeamCount

Current number of partially checked in teams (minimum number of party memebers have checked in for match).

public int PartiallyCheckedInTeamCount
{
    get;
    set;
}

PhaseId

Determine phase id of this match.

public int PhaseId
{
    get;
    set;
}

RoundId

Determine round id of this match.

public int RoundId
{
    get;
    set;
}

Secret

Random 16 char value, only users allowed to join match (players/admins) get this value otherwise null (Can be used as a password for match room).

public string Secret
{
    get;
    set;
}

Status

Determine current match status.

public TournamentMatchStatus Status
{
    get;
    set;
}

Users

List of match users.

public List<TournamentMatch.User> Users
{
    get;
    set;
}

WinScore

Required win score for this match.

public int WinScore
{
    get;
    set;
}

Methods

GetCheckInTeamUsers()

Get only users of teams who at least partially checked in.

public System.Collections.Generic.IEnumerable<Gimmebreak.Backbone.Tournaments.TournamentMatch.User> GetCheckInTeamUsers()

Returns

Checked in users.

GetMatchUserById(long)

Get match user by id.

public Gimmebreak.Backbone.Tournaments.TournamentMatch.User GetMatchUserById(long userId)

Parameters

  • userId: User id.

Returns

Match user.

IsTeamFullyCheckedIn(TournamentMatch.User)

Check if whole team is checked in.

public bool IsTeamFullyCheckedIn(Gimmebreak.Backbone.Tournaments.TournamentMatch.User teamMember)

Parameters

  • teamMember: Team member.

Returns

True if whole team is checked in, othwrwise false.

IsTeamFullyCheckedIn(byte)

Check if whole team is checked in.

public bool IsTeamFullyCheckedIn(byte teamId)

Parameters

  • teamId: Team id

Returns

True if whole team is checked in, othwrwise false.

IsTeamPartiallyCheckedIn(TournamentMatch.User)

Check if team is partially checked in.

public bool IsTeamPartiallyCheckedIn(Gimmebreak.Backbone.Tournaments.TournamentMatch.User teamMember)

Parameters

  • teamMember: Team member.

Returns

True if team is partially checked in, othwrwise false.

IsTeamPartiallyCheckedIn(byte)

Check if team is partially checked in.

public bool IsTeamPartiallyCheckedIn(byte teamId)

Parameters

  • teamId: Team id

Returns

True if team is partially checked in, othwrwise false.

API-TournamentPhase

Constructors

TournamentPhase()

Creates a tournament phase.

public TournamentPhase()

Properties

GroupCount

Determine how many groups are in current phase.

public int GroupCount
{
    get;
    set;
}

GroupSize

Determine maximum number of teams/parties per single group.

public int GroupSize
{
    get;
}

HasGroups

Determine if there are groups in current phase.

public bool HasGroups
{
    get;
}

Id

Phase id (starts with 1).

public int Id
{
    get;
    set;
}

IsLoserBracketSeeded

Determine if loser bracket is seeded in double elimination bracket phase.

public bool IsLoserBracketSeeded
{
    get;
    set;
}

IsSkipAllowed

Determine if phase is allowed to be skipped in case there are less or equal teams in current phase then expected in next phase.

public bool IsSkipAllowed
{
    get;
    set;
}

MaxLoses

Max loses in this phase (E.g. arena can define that if you lose 3 times you are out).

public int MaxLoses
{
    get;
    set;
}

MaxPlayers

Max players entering this phase.

public int MaxPlayers
{
    get;
    set;
}

MaxTeams

Max teams/parties entering this phase.

public int MaxTeams
{
    get;
    set;
}

MaxTeamsPerMatch

Maximum allowed teams per match.

public int MaxTeamsPerMatch
{
    get;
    set;
}

MinCheckinsPerTeam

Determine how many players in team needs to checkin in order to be allowed to start a tournament match.

public int MinCheckinsPerTeam
{
    get;
    set;
}

MinTeamsPerMatch

Minimum required teams per match. This indicates if match should be played or not. Only fully checked in teams/parties counts.

public int MinTeamsPerMatch
{
    get;
    set;
}

Remarks

In certain phase formats if match does not contain MinTeamsPerMatch until start deadline then all checked in parties will get autowin and are moved to the next round.

Rounds

Phase rounds. Each round can have a different time and win conditions (E.g. each round is best of three, but final round is best of five).

public List<TournamentRound> Rounds
{
    get;
    set;
}

Scores

Determine all party scores in this phase. It holds summary of points, wins, loses, etc. each party gathered. List has to be loaded on demand. To populate scores use LoadTournamentPhaseScores() on backbone client.

public List<TournamentScore> Scores
{
    get;
    set;
}

Type

Phase type (E.g. Arena or Bracket).

public TournamentPhaseType Type
{
    get;
    set;
}

UserGroupId

Determine user group id in current phase.

public int UserGroupId
{
    get;
    set;
}

UserIsGroupAssigned

Determine if user group id has been assigned. In case there are groups in first phase of a tournament users has to check in first. For a team only party leader will trigger group id assigning process on checkin.

public bool UserIsGroupAssigned
{
    get;
}

UserIsParticipating

Determine if user is participating in phase. It can be false if user has been knocked out in previous phase.

public bool UserIsParticipating
{
    get;
    set;
}

UserLoses

Number of user loses in current phase.

public int UserLoses
{
    get;
    set;
}

UserPlayedRoundCount

Count of already finished rounds for user in given phase. This increases despite if user did or did not really play/participate in previous rounds.

public int UserPlayedRoundCount
{
    get;
    set;
}

UserPoints

Number of user points in current phase. This represents total points for match results (not total points scored in game sessions).

public int UserPoints
{
    get;
    set;
}

UserPositionBottom

Users current bottom position in this phase (E.g.(1-3/8) => 3).

public int UserPositionBottom
{
    get;
    set;
}

UserPositionTop

Users current top position is this phase (E.g. (1-3/8) => 1).

public int UserPositionTop
{
    get;
    set;
}

Methods

GetRoundById(int)

Gets phase round by specific id.

public Gimmebreak.Backbone.Tournaments.TournamentRound GetRoundById(int roundId)

Parameters

  • roundId: Round id (first round always starts with id=1).

Returns

Tournament round or null if round does not exists.

GetScoreByPartyId(long)

Get phase score record by party id. Note that scores has to be loaded prior using this method otherwise the return value will be NULL. Use LoadTournamentPhaseScores() on backbone client.

public Gimmebreak.Backbone.Tournaments.TournamentScore GetScoreByPartyId(long partyId)

Parameters

  • partyId: Party id for which we want score record

Returns

Score record or null if it does not exist (or not loaded)

GetScoreByUserId(long)

Get phase score record by user id. Note that scores has to be loaded prior using this method otherwise the return value will be NULL. Use LoadTournamentPhaseScores() on backbone client.

public Gimmebreak.Backbone.Tournaments.TournamentScore GetScoreByUserId(long userId)

Parameters

  • userId: User id for which we want score record

Returns

Score record or null if it does not exist (or not loaded)

API-TournamentRound

Constructors

TournamentRound()

Creates an instance of tournament round.

public TournamentRound()

Properties

GamePointDistribution

Determine point distribution for specific position for each game in this round.

public List<TournamentRound.GamePositionPoints> GamePointDistribution
{
    get;
    set;
}

Id

Round id (starting from 1).

public int Id
{
    get;
    set;
}

MatchPointDistribution

Determine point distribution for specific position for each match in this round.

public List<TournamentRound.MatchPositionPoints> MatchPointDistribution
{
    get;
    set;
}

MaxGameCount

Maximum allowed games to be played in match (E.g. if we set this to 2 then we effectively created BO2, this would not work if we set WinScore to 1 in 1v1 scenario).

public int MaxGameCount
{
    get;
    set;
}

MaxLength

Maximulm length for this round, all games should end within this window. This should be greater than MinGameLength * MAXPOSSIBLEGAMECOUNT plus some margin.

public int MaxLength
{
    get;
    set;
}

MinGameLength

Minimum game length, all games should try to fit into this window (E.g. round is best of 3 (max 3 games) and minimum game length is 2 minutes then Max length should be at least 6 minutes to allow all games to be played if necessary).

public int MinGameLength
{
    get;
    set;
}

WinScore

Winning score required for match to be closed (E.g. if we give any points for game win then best of 3 would be represented by WinScore=2 (WinScore = how many times user places on scored position)).

public int WinScore
{
    get;
    set;
}

API-Tournament

Constructors

Tournament()

Create an instance of tournament.

public Tournament()

Properties

AdditionalDescription

Get additional description of this tournament if any.

public string AdditionalDescription
{
    get;
    set;
}

AllMatches

All tournament matches from all phases. This field can be populated with LoadTournamentMatches() or LoadTournamentMatchesAll() call on backbone client.

public List<TournamentMatch> AllMatches
{
    get;
    set;
}

CurrentInvites

Determine current active invites/registrations for tournament. This always represents a single user even if tournament is set to have parties/teams.

public int CurrentInvites
{
    get;
    set;
}

CurrentPhaseId

Determine current active phase Id (If id=0 then tournament was not initiated yet, first phase always starts with id=1).

public int CurrentPhaseId
{
    get;
    set;
}

CurrentPhaseStarted

Determine when current phase started. This is UTC datetime.

public DateTime CurrentPhaseStarted
{
    get;
    set;
}

CustomProperties

Get tournament custom properties. (e.g. game mode, allowed abilities, allowed maps, etc.)

public TournamentCustomProperties CustomProperties
{
    get;
    set;
}

Description

Get description of this tournament if any.

public string Description
{
    get;
    set;
}

EntryFee

Determine entry fee of this tournament. This will not be null even if tournament does not have any entry fees set up, however EntryFee.Items will be empty.

public TournamentEntryFee EntryFee
{
    get;
    set;
}

HasAllDataLoaded

Determine if all tournament data were loaded.

public bool HasAllDataLoaded
{
    get;
    set;
}

Remarks

This is set to false if tournament was loaded only with LoadTournamentList() call on backbone client. This is set to true if tournament was loaded with LoadTournament() call on backbone client.

HighlightsUrl

Get highlights url of tournament if any.

public string HighlightsUrl
{
    get;
    set;
}

IconUrl

Get icon url of this tournament if any.

public string IconUrl
{
    get;
    set;
}

Id

Tournament unique id.

public long Id
{
    get;
    set;
}

ImageUrl

Get image url of this tournament if any.

public string ImageUrl
{
    get;
    set;
}

InvitationCloseTime

Determine when invitation closes. After this point nobody can sign up or sign out. This is UTC datetime.

public DateTime InvitationCloseTime
{
    get;
    set;
}

InvitationOpenTime

Determine when invitation opens. This is UTC datetime.

public DateTime InvitationOpenTime
{
    get;
    set;
}

Remarks

When invitation opens then only user who has a direct invite can sign up. Direct invites can represent winners from previous tournaments or specific users set up in dashboard. This means you can give time priority to those users for sign up before public registration opens.

Invite

Get tournament invite/ticket. If user was invited to the tournament, ticket is available but not confirmed. This property is null if user was not automatically invited or signed up for the tournament.

public TournamentInvite Invite
{
    get;
    set;
}

IsAdministrator

Determine if user is administrator of this tournament. Administrator can access match secrets when browsing matches and therefore join as spectator.

public bool IsAdministrator
{
    get;
    set;
}

IsInvitationOnly

Determine if tournament is only for invited users. If set to true then RegistrationOpenTime should not be used as it does not contain valid value.

public bool IsInvitationOnly
{
    get;
    set;
}

LastMatchRoundId

Determine last round id of match that user played.

public int LastMatchRoundId
{
    get;
    set;
}

LastUpdate

Determine last tournament data update.

public DateTime LastUpdate
{
    get;
    set;
}

MaxInvites

Determine max invites/registrations for tournament. This always represents a single user even if tournament is set to have parties/teams.

public int MaxInvites
{
    get;
    set;
}

NextPhase

Determine time when next phase will start. This is UTC datetime.

public DateTime NextPhase
{
    get;
    set;
}

Remarks

If all matches have been played in current phase then tournament might progress to next phase sooner to prevent unnecessary wating for users.

Party

Get user party (signup user is always in a party, even in 1v1). This property is null until user signs up for the tournament.

public TournamentParty Party
{
    get;
    set;
}

Remarks

Signed up user is always in a party. If user leaves party then he is automatically assigned a new one.

PartySize

Determine requried party size for tournament (E.g. 1v1, 2v2, 3v3, etc).

public int PartySize
{
    get;
    set;
}

PhaseCount

Determine how many phases are in tournament (always at least one).

public int PhaseCount
{
    get;
    set;
}

Phases

List of all tournament phases. Tournament always has at least one phase (PhaseId = 1).

public List<TournamentPhase> Phases
{
    get;
    set;
}

PolicyURL

Get policy url of this tournament if any.

public string PolicyURL
{
    get;
    set;
}

PrivateCode

Get private code of this tournament. Can be shared with other users to sign up for private tournaments.

public string PrivateCode
{
    get;
    set;
}

Remarks

Private code is generated only if this type of invite is enabled in dashboard.

Prizes

List of tournament prizes.

public List<TournamentPrize> Prizes
{
    get;
    set;
}

RegistrationOpenTime

Determine when registration opens. This property is set only if IsInvitationOnly is set to false. This is UTC datetime.

public DateTime RegistrationOpenTime
{
    get;
    set;
}

Remarks

When registration opens then any user can sign up.

ReplayExpires

Determine when replay url expires (e.g. twitch after 14 days) This is UTC datetime.

public DateTime ReplayExpires
{
    get;
    set;
}

ReplayUrl

Get replay url of tournament if any. (E.g. twitch VOD)

public string ReplayUrl
{
    get;
    set;
}

Requirements

Determine tournament requiremts for signup. This will not be null even if tournament does not have any custom requirements set up, however Requirements.CustomRequirements will be empty.

public TournamentRequirements Requirements
{
    get;
    set;
}

RoundCount

Determine how many rounds in total are in tournament (always at least one).

public int RoundCount
{
    get;
    set;
}

Season

Determine which season tournament is created in.

public int Season
{
    get;
    set;
}

SeasonPart

Determine which season part tournament is created in.

public int SeasonPart
{
    get;
    set;
}

SponsorImageUrl

Get sponsor image.

public string SponsorImageUrl
{
    get;
    set;
}

SponsorName

Get sponsor name.

public string SponsorName
{
    get;
    set;
}

Status

Current status of tournament. (E.g. Invitation Open, Finished, etc.) To get more detailed statuses you have to connect tournament with tournament hub.

public TournamentStatus Status
{
    get;
    set;
}

StreamUrl

Get tournament stream url if any.

public string StreamUrl
{
    get;
    set;
}

ThemeColor

Get theme color of this tournament if any.

public string ThemeColor
{
    get;
    set;
}

Time

Determine start time of tournament. This is UTC datetime.

public DateTime Time
{
    get;
    set;
}

TournamentName

Tournament official name.

public string TournamentName
{
    get;
    set;
}

Type

Determine type of tournament. (E.g. public, private, testing) Some tournament types are only visible to certain users.

public TournamentType Type
{
    get;
    set;
}

UserActiveMatch

Currently active match for the user which he should join. This is only applicable for users that are signed up for the tournament.

public TournamentMatch UserActiveMatch
{
    get;
    set;
}

Remarks

This property is automatically set when user connects to tournament hub. In some situations user will be asked by tournament hub to explicitly confirm that he is ready before tournament match is assigned.

UserMatches

All user matches from all phases.

public List<TournamentMatch> UserMatches
{
    get;
    set;
}

Remarks

If user failed to start certain rounds on time and totally skiped some round matches (autolose) then those matches will not be present in this list. Use UserPlayedRoundCount property on tournament phase object to determine if user actually missed a match.

Winner

Get basic tournament winner data. This contains list of all users in all parties that finished on first position.

public TournamentWinner Winner
{
    get;
    set;
}

Remarks

If last phase of tournament has groups then tournament can have multiple winners as final position is calculated within a group.

Methods

GetCurrentTournamentPhase()

Get current tournament phase.

public Gimmebreak.Backbone.Tournaments.TournamentPhase GetCurrentTournamentPhase()

Returns

Tournament phase or null if tournament has not started yet. Finished tournament always returns last phase.

GetMatchMinGameLength(TournamentMatch)

Get minimum lenght (in minutes) required for a game to be played.

public int GetMatchMinGameLength(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Minimum lenght (in minutes).

GetMatchNextGameDeadline(TournamentMatch)

WARNING This item is deprecated. Use GetMatchNextGameFinishDeadline() instead.

Calculate a finish deadline for next game within a match. This deadline is calculated in a way there is still at least minimum game time for each game that has to be played.

public System.DateTime GetMatchNextGameDeadline(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Maximum possible finish deadline for a next game.

GetMatchNextGameFinishDeadline(TournamentMatch)

Calculate a finish deadline for next game within a match. This deadline is calculated in a way there is still at least minimum game time for each game that has to be played.

public System.DateTime GetMatchNextGameFinishDeadline(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Maximum possible finish deadline for a next game.

GetMatchNextGameStartDeadline(TournamentMatch)

Calculate a start deadline for next game within a match. This deadline is calculated in a way there is still at least minimum game time for each game that has to be played.

public System.DateTime GetMatchNextGameStartDeadline(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Maximum possible start deadline for a next game.

GetMatchStartDeadline(TournamentMatch)

Determine deadline after which match has to start in order to be able accommodate all required games each to be played with at least minimum game time.

public System.DateTime GetMatchStartDeadline(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Deadline after which match has to start.

GetThemeColor()

Get tournament theme color.

public UnityEngine.Color GetThemeColor()

Returns

Unity color.

GetTournamentMatchById(long)

Get tournament match by its unique id.

public Gimmebreak.Backbone.Tournaments.TournamentMatch GetTournamentMatchById(long tournamentMatchId)

Parameters

  • tournamentMatchId: Tournament match id.

Returns

Tournament match or null if match cannot be found.

Remarks

This method searches for the match locally and does not access server data. If match id is valid but method return null populate local data with LoadTournamentMatches() on backbone client.

GetTournamentMatchMaxGameCount(TournamentMatch)

Determine maximum allowed number of games to be played in specific match. Once this value is reached then system will determine winners by scored points from all played games.

public int GetTournamentMatchMaxGameCount(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Maximum allowed number of games.

GetTournamentMatchMinCheckinsPerTeam(TournamentMatch)

Determine required win score for specific match. Win score determines how many times user has to place on scored position in order to win the match.

public int GetTournamentMatchMinCheckinsPerTeam(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Win score for specific match.

GetTournamentMatchWinScore(TournamentMatch)

Determine required win score for specific match. Win score determines how many times user has to place on scored position in order to win the match.

public int GetTournamentMatchWinScore(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

Win score for specific match.

GetTournamentPhaseById(int)

Get specific tournament phase by its id.

public Gimmebreak.Backbone.Tournaments.TournamentPhase GetTournamentPhaseById(int phaseId)

Parameters

  • phaseId: Tournament phase id (first phase always starts with id=1).

Returns

Tournament phase or null if phase does not exists.

GetTournamentPhaseFinishTime(int)

Get finish timestamp of specific tournament phase.

public System.DateTime GetTournamentPhaseFinishTime(int phaseId)

Parameters

  • phaseId: Phase id.

Returns

Finish timestamp of requested phase.

GetTournamentPhaseMaxLength(int)

Get tournament phase maximum time lenght in minutes. This is a sum of all round MaxLength values.

public int GetTournamentPhaseMaxLength(int phaseId)

Parameters

  • phaseId: Phase id.

Returns

Length of tournament phase in minutes.

GetTournamentPhaseRoundFinishTime(int, int)

Get round finish timestamp in currently active tournament phase.

public System.DateTime GetTournamentPhaseRoundFinishTime(int phaseId, int roundId)

Parameters

  • phaseId: Phase id.
  • roundId: Round id.

Returns

Finish timestamp of requested round.

GetTournamentPhaseRoundStartTime(int, int)

Get round start timestamp in currently active tournament phase.

public System.DateTime GetTournamentPhaseRoundStartTime(int phaseId, int roundId)

Parameters

  • phaseId: Phase id.
  • roundId: Round id.

Returns

Start timestamp of requested round.

GetTournamentPhaseStartTime(int)

Get start timestamp of specific tournament phase.

public System.DateTime GetTournamentPhaseStartTime(int phaseId)

Parameters

  • phaseId: Phase id.

Returns

Start timestamp of requested phase.

IsMatchInLoserBracket(TournamentMatch)

Determine if match is in loser bracket. This is used for double elimination braket format.

public bool IsMatchInLoserBracket(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

True if match is in loser bracket.

IsMatchInWinnerBracket(TournamentMatch)

Determine if match is in winner bracket. This is used for double elimination braket format.

public bool IsMatchInWinnerBracket(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

True if match is in winners bracket.

IsMatchMergingInLoserBracket(TournamentMatch)

Determine if match is merging within loser bracket. This is used for double elimination braket format.

public bool IsMatchMergingInLoserBracket(Gimmebreak.Backbone.Tournaments.TournamentMatch match)

Parameters

  • match: Tournament match.

Returns

True if match is merging in loser bracket.

Remarks

Merging in loser bracket meant that winner of loser bracket match will face another winner of loser bracket match. In opposite case, if match is not merging then winner of loser bracket will face loser of winner bracket therefore number of loser bracket matches will be same in next round. This method helps with UI visualisation of double elimination brackets.

AdvancedFeatures

Setting up advanced features on dashboard

How to set up 1v1, 2v2 etc.

1v1

Go to Registration and set Party(team) size to 1, so we could only have 1 player per team.

1v1

Depending on amount of player you expecting to participate, set Maximum players to N, where N can be any whole number greater than 1.

After setting up Registration go to Format and add a new Phase.

1v1

In Phase find Teams and fill the field with N set previously for Maximum players.

1v1

2v2

Go to Registration and set Party(team) size to 2, so we could only have 2 players per team.

Depending on amount of player you expecting to participate, set Maximum players to N, where N can be any whole even number greater than 2, like 4, 6, 8, etc.

After setting up Registration go to Format and add a new Phase.

In Phase find Teams and fill the field with N/2 set previously for Maximum players. So if N is equal to 8, Teams should be 4.

NvN

Go to Registration and set Party(team) size to N, so we could only have N players per team.

Depending on amount of player you expecting to participate, set Maximum players to N*T, where T is number of teams you expect to participate.

After setting up Registration go to Format and add a new Phase.

In Phase find Teams and fill the field with T used previously for Maximum players.

How to set up tournament for BO1 - BO5

Go to Format, find field Rounds and there in field Type you can choose options from BO1 to BO5 or Custom.

With rounds from BO1 to BO5 you can set only Minimum game time (minutes) and Maximum round time (minutes).

Set timing for BO1-BO5

To set time for rounds, go to Format/Rounds and assuming we have a BO3 game where each match is about 10 minutes long, Maximum round time (minutes) would be 45 minutes and Minimum game time (minutes) would be 15 minues. Which means that round can be finished after at least 15 minutes after it's beginning and can last up to 45 minutes.

Correct? -> If Maximum round time (minutes) has expired and last game wasn't finished, round will be considered as finished anyways, so it's better to have some additional time in case of unexpected changes or issues.

If Maximum round time (minutes) has expired before last game has started, round will be considered as finished anyways, so it's better to have some additional time in case of unexpected changes or issues.

Custom

If Custom option is chosen, then you can set amount of rounds need to be won for victory in field Score to win.

Restricting number of games with field Maximum number of games defines what is the limit of the games that can be played in order to define winner.

Maximum number of games must be greater than Score to win, as Score to win defines amount of games that must be won out of Maximum number of games in order to define winner.

How to set up fees and prize pool

Entry Fees

In order to set up Entry Fee you first have to go to Store, then add there Currencies and Items.

EntryFees

EntryFees

Get back to Template settings and go to Entry fee. If Currency or Item were added, then they can be seen in right part of the screen.

EntryFees

Now just drag&drop any of them into Drag and drop items here field. If you don't have predefined amount of Currency or Item that will be a reward for participants.

EntryFees

You can tick the box on the right side of the added Currency or Item under Add to pot column, then Currency and Item will be summed up to present prize pool. Save changes in the popping up, on the bottom of the page, notification.

EntryFees

Go to Rewards, there you will have to add rewards rule. In appeared form, there is Place field where you can define which places will be rewarded. If you want to have a dynamic prize pool find Shared Pot field and enter the percentage from overall prize pool to be devided among places defined in Place field.

EntryFees

NB! IF Shared Pot DIDN'T APPEAR ON THE Rewards PAGE, TRY TO RELOAD IT AND MAKE SURE YOU SAVED CHANGES AFTER Add to pot IS TICKED

If you want to have both dynamic prize pool and additional reward, you can drag&drop from the right table Currency or Item that will be a reward, to Drag and drop items here field.

Now let's assume only first four places will have a reward, that will consist from fifty percent of prize pool and 5 additional items, then in Place we enter 4, in Shared pot field enter 50, drag&drop item from right column and set amount to 5. Notice that on the right part of the row for Shared pot there is Each place field that shows how many percent from overall prize pool each player from first to fourth place will receive.

If you want to split reward between 32 player so that first 8 places would receive 50% from overall prize pool, then from 8th to 16th would receive 25% and from 16th to 32nd would receive 15%, you have to add 3 rewards rule forms, and enter percentage and places accordingly.

EntryFees