Quantum
Blueless (Shooter)
Integrate tournaments to a fast and action-packed online shooter. Follow this example to learn any action packed game can benefit for the tournament integration.
Unity: 2021.3.30f1. - PC (Windows / Mac), WebGL and Mobile (Android)
The sample game developed by BitCake Studio for Photon Engine.
Want to skip to the end?
We would really recommend to follow the sample and integrate the tournament system yourself. This way you will understand what the system can do and how to integrate it to your game.
However, you can also download a fully funtional demo that was created by following exactly these steps.
Download DemoYou still need to Create a new game in the dashboard and connect it with the project.
1. Set up the project
Set up Quantum
To run the sample in online multiplayer mode, first create a Quantum AppId in the PhotonEngine Dashboard and paste it into the AppId
field in PhotonServerSettings
asset.
May be that instead of App Settings > App Id Realtime
you will have to go App Settings > App Id
.
2. Tournament-SDK UI
Start by importing Tournament-SDK to your Unity project.
To set up UI to direct player among MainMenu
, TournamentListScreen
and TournamentHubScreen
, first we are going to add necessary changes/additions to the game code.
Prepare Scripts to implement Tournament-SDK UI
-
Create new script called
UITournamentList
. -
Open new script and copy paste following code that is necessary to direct player from
TournamentListScreen
toMainMenu
orTournamentHubScreen
:
UITournamentList
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
}
}
-
Create new script called
UITournamentHub
. -
Open new script and copy paste following code that is necessary to direct player from
TournamentHubScreen
toTournamentListScreen
:
UITournamentHub
using Quantum.Demo;
public class UITournamentHub : UIScreen<UITournamentHub>
{
public void OnGetBackToTournamentList()
{
HideScreen(); // Hide Tournament Hub Screen
UITournamentList.ShowScreen(); // Show Tournament List Screen
}
}
Prepare code to implement Tournament-SDK UI
-
Open script called
UIConnect
. -
Add variable to store
GUITournamentHubScreen
crucial for tournament initialization:[SerializeField] private GUITournamentHubScreen TournamentHubScreen;
-
Add two following methods:
// Directs player from MainMenu to TournamentListScreen public void OnTournamentClicked() { HideScreen(); // Hide MainMenu UITournamentList.ShowScreen(); // Show Tournament List Screen } // Method that will redirect player back to the TournamentHubScreen after the tournament match has been finished public void ReturnToTournament(long tournamentId) { HideScreen(); // Hide MainMenu TournamentHubScreen.Initialize(tournamentId); // Initialize tournament using tournamentId saved from last played tournament UITournamentHub.ShowScreen(); // Show Tournament Hub Screen }
Add UI
Tournament Button
-
In
Hierarchy
go toUICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel
. -
Duplicate last element called
ReconnectButton
. -
Rename it to
TournamentButton
. -
Go
TournamentButton > ButtonText
and changeText
component toTOURNAMENTS
.
Tournament List & Tournament Hub
-
Add two new objects to
UICanvas
scene. -
Rename them to
TournamentListUI
andTournamentHubUI
. -
Drag&Drop
TournamentListScreen
andTournamentHubScreen
respectively. -
Make sure
TournamentListUI > TournamentListScreen > SubScreen
is Enabled. -
Find
Rect transform
component and resetLeft
/Right
/Bottom
/Top
to 0. -
Set scale for
X
/Y
/Z
to 1.
Set Up UI
-
Add newly created scripts
UITournamentList
andUITournamentHub
toMenu
object inHierarchy
to make them accessible. -
Drag&Drop
TournamentListUI
intoUITournamentList > Panel
component. -
Drag&Drop
TournamentHubUI
intoUITournamentHub > Panel
component.
TournamentButton
To make TournamentButton
direct player from MainMenu
to TournamentListScreen
:
-
Go to
UICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel > TournamentButton
. -
In
Button
component change firstOnClick
action fromUIConnect.OnReconnectClicked
toUIConnect.OnTournamentClicked
.
TournamentList to TournamentHub
To allow TournamentList
direct player to TournamentHub
:
-
Drag&Drop
TournamentHubUI > TournamentHubScreen > SubScreen
toTournamentListUI > TournamentListScreen > SubScreen
inGUITournamentListScreen > TournamentHubScreen
component. -
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Scroll View > Viewport > Content > TournamentListItem > ButtonUnderline(Open)
findButton
component. -
Add new
OnClick()
action, drag&dropMenu
into empty field underRuntime Only
, changeNo Function
toUITournamentList.OnPlayOpenTournamentHub
-
Repeat this process for
TournamentHubUI
as well.
TournamentList to MainMenu
To allow TournamentList
direct player back to MainMenu
:
-
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Title > ButtonUnderline(Back)
, findButton
component. -
Drag&drop
Menu
into field underRuntime Only
and set it toUITournamentList.OnGetBackToMainMenu
.
TournamentHub to TournamentList
-
Drag&Drop
TournamentListUI > TournamentListScreen > SubScreen
toTournamentHubUI > TournamentHubScreen > SubScreen
inGUISubScreen > ReturnScreen
component. -
Go to
UICanvas > Menu > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > Title > ButtonUnderline(Back)
, findButton
component. -
Add new action, drag&drop
Menu
into empty field underRuntime Only
and set it toUITournamentHub.OnGetBackToTournamentList
.
Backbone Integration
To initialize TournamentList
we need to create an object that will do it on every launch of the game.
-
Add new
GameObject
toUICanvas
and rename it toBackboneIntegration
. -
Add
BackboneManager
andResource Cache
components to it. -
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; } } }
-
Get back to the scene and find
BackboneIntegration
component. AddTournamentButton
to the missing field in component.
3. Implementing Tournament logic into the Quantum game
Quantum code changes/additions
To succesfully merge Tournament logic into Quantum game, let's first prepare game to handle/carry/mantain tournament information inside the game.
-
Open Quantum part of the project. Go to project location, there must be folder called
quantum_code
open it as a project in an editor. -
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); } } }
-
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.
-
Go to
ScoreboardHud
script. -
Add variable to track if result processing has started.
private bool processingStarted = false;
-
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)); }
-
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
-
In
Hierarchy
go toUICanvas > TournamentHubUI > TournamentHubScreen > Canvas
at the very bottom of it, create a new game object and rename it toTournamentMatchHandler
. -
Create new script called
TournamentMatchHandler
and add this script as component toTournamentMatchHandler
object. -
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) { } }
-
Get back to Unity project, find
UICanvas > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > ActiveMatchContainer
, find componentGUITournamentActiveMatch
there will be empty fieldMatchHandler
drag&dropTournamentMatchHandler
into it.
4. Schedule a tournament
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
Description
and setTournament Name
. -
Go to
Registration
set:
-
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 templates
page. -
Press
Schedule
, setTime
toYour current time + 5 minutes
and pressStart tournament
.
5. Final test
-
Now get back to Unity and start the project.
-
Press
Tournaments
button that was added earlier. You should seeTournamentListScreen
and the tournament that you just added.Tournament may be unavailable to register for some time, wait until
Sign up
button is available to register to tournament. -
Repeat the process in build version
-
When tournament will start, button
Ready to play
will 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 matches
to the right from any participant. -
Click
Show details
for more information. -
Click
Game #ID
andStats
for more information.