Fusion
Karts (Racing)
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.
Set up the Fusion
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
Start by importing Tournament-SDK to your Unity project.
Setting up the scene
Add necessary objects
-
In
Projectsection, openAssets/Scenes-> double-clickLaunch.
-
In
Hierarchyadd 2 new GameObjects abovePrompts, rename them toTournamentListUIandTournamentHubUI.
Adjust resolution
-
Select
TournamentListUI. -
In
Rect Transformcomponent in left top corner change adjustement to stretched. -
Set
Left,Top,Right,Bottomparameters to 0.
-
Do the same for
TournamentHubUI.
Setting up the objects
-
Add
UIScreenas component to both new objects.
-
Add
TounamentHubScreenandTounamentListScreentoTounamentHubUIandTounamentHubUIrespectively.Tournament(Hub/List)Screencan be found inAssets/TournamentSDK_Demo/Prefabs/DefaultUIScreens.
Make sure
SubScreeninside bothTounamentHubScreenandTounamentListScreenis enabled!
-
Disable
TounamentListUIandTounamentHubUI.
Setting up necessary layout
-
In
Hierarchygo to ->Main Canvas/Main Menu Screen/LayoutGroup/Footer/LayoutGroup, selectExit Buttonand press Ctrl+D to duplicate the object.
-
Inspectorwill open right after object duplication, change button's name toTournaments, also findRect TransformchangeWidthto 470.
-
Open newly created object in
Hierarchy, selectTextand changeTextcomponent toTournaments.
Setting up the buttons
Moving from Main Menu Screen to Tournament List UI
-
Select
Tournamentsobject created previosly inHierarchy. -
Find
Buttoncomponent inInspector. -
In
On Click()field, drag ang dropMain Menu ScreenfromHierarchyinto field underRuntime Only. -
Change
No FunctiontoUIScreen/FocuScreen. -
In
HierarchyfindTournamentListUI, drag and drop it intoOn Click()field underUIScreen.FocusScreenwhereNone (UI Screen)is written.
Moving from Tournament List UI to Main Menu Screen
-
In
Hierarchygo toTournamentListUI/TournamentListScreen/SubScreen/Canvas/Title. -
Select
ButtonUnderline(Back), in it'sButtoncomponent drag and dropTournamentListUIinto field underRuntime Only. -
then change
No FunctiontoUIScreen/Back.
Moving from Tournament List UI to Tournament Hub UI
-
Go to
TournamentListUI/TournamentListScreen/SubScreen/Canvas/Scroll View/ViewPort/Content/TournamentListItem/ButtonUnderline(Open)/(Play). -
Add new action to
On Click(). -
Drag
TournamentListUIinto field underRuntime Only. -
Change
No FunctiontoUIScreen/FocusScreen. -
Drag
TournamentHubUIinto field underUIScreen.FocusScreen.
Due to we used prefabs of
TournamentListScreenandTournamentHubScreen,GUITournamentListScreenscript has unset variableTournamentHubScreen. -
Go to
TournamentListUI/TournamentListScreen/SubScreenthere will be unsetTournament Hub Screen. -
From
TournamentHubUI/TournamentHubScreendragSubScreeninto unsetTournament Hub Screen.
Moving from Tournament Hub UI to Tournament List UI
Go to ButtonUnderline(Back) in TournamentHubUI same way as for TournamentListUI.
-
Add another action on button by pressing
+. -
Drag and drop
TournamentHubUIinto field underRuntime Only. -
Change
No FunctiontoUIScreen/Back.
-
Go to
TournamentHubUI/TournamentHubScreen/SubScreeninGUISubScreencomponent will be unsetReturn Screen. -
From
TournamentListUI/TournamentListScreendragSubScreeninto unsetReturn Screenfield.
Implementing Tournament-SDK
Create/Set up BackboneManager
-
Create new
GameObjecton the very bottom ofMain Canvas, rename it toBackboneManager. -
Press
Add Component-> start typingBackbone Manager, it will appear in search tab, click on it. -
Backbone Managercomponent will appear inInspector. Make sureInitialize On Startbox is UN-ticked.
-
Add
Resource Cachecomponent toBackbone Managerobject.
Implementing client initialization flow into BackboneIntegration
-
In
InspectorofBackboneManagerobject selectAdd Component-> typeBackboneIntegration-> selectNew Script/Create and Add.
-
Add following code into
BackboneIntegrationscript.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; } } }BackboneIntegrationcomponent now misses theTournamentButton -
Drag
Tournamentsbutton fromMainMenuScreen/LayoutGroup/Footer/LayoutGroup, intoTournamentButtonfield inBackboneIntegrationcomponent.
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:
-
Add
SessionNamevariable 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; } } -
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; -
Go to
JoinOrCreateLobby()method, in_runner.StartGame({...})change:DisableClientSessionCreation = true
TO
// 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
-
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(); } } } -
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:
-
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; -
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:
-
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; } -
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:
-
Add static variables to get information about
Tournament(User/Team)Idwhen 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:
-
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; } -
In
Spawned()method findRPC_SetPlayerStats()and change it to:RPC_SetPlayerStats(ClientInfo.Username, ClientInfo.KartId, ClientInfo.TournamentUserId, ClientInfo.TournamentTeamId); -
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.
-
Go to
Main Canvas. -
Add
TournamentHubScreenUI,TournamentHubScreenGUIandTournamentListUIas shown:
Implementing TournamentMatchHandler
-
Go to
TournamentHubUI/TournamentHubScreen/SubScreen/Canvas. -
Add empty object to
Canvasand rename it toTournamentMatchHandler. -
Add new script to it called
TournamentMatchHandler.
-
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.

Implementing Result submission
- Go to
EndRaceUIscript. - Add variable that will keep track if result processing started.
private bool processingStarted = false;
- 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);
}
- Now edit
EnsureContinueButton()method and addProcessResult()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
-
There you will see
No tournament templates .Create your first template to get started. -
Press
Create your first template.
Edit template
-
Tournament template will appear. Select
Edit template.
-
Go to
Descriptionand setTournament Name.
-
Go to
Registrationset:
-
Maximum players- 2 -
Party(team) size- 1 -
Registration rules->Open to everyone
- Go to
Format/Add Phase
In Format set:
Teams- 2Min teams per match- 2Max Teams per match- 2
Leave field Max loses in Scores empty
In Rounds set:
Type- BO3Minimum game time (minutes)- 2Maximum round time (minutes)- 8
-
On the bottom of the screen you should see
Careful - you have unsaved changes!, pressSave Changes.
Start tournament
-
Get back to
Tournament templatespage. -
Press
Schedule, setTimetoYour current time + 5 minutesand pressStart tournament.
Final test
-
Now get back to Unity and start the project.
-
Press
Tournamentsbutton that was added earlier. You should seeTournamentListScreenand the tournament that you just added.
Tournament may be unavailable to register for some time, wait until
Sign upbutton is available to register to tournament.
-
Repeat the process in build version
-
When tournament will start, button
Ready to playwill become available.
-
Press on both Unity and Build
Ready to play.

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

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