This document is about: QUANTUM 2
SWITCH TO

Example using pre-built UI


Available in the Gaming Circle and Industries Circle
Circle

このページでは、ゲームにコアトーナメントツールを実装する方法を説明した簡単なチュートリアルをご紹介します。
トーナメントSDKUnityプラグイン内にあるプレビルドのUIスクリーンを使用しています。

デモシーンを実装する

UnityプラグインパッケージからTournamentSDK_Demoフォルダーを実装します。
このフォルダにはトーナメントメタデータの可視化に便利な、関連するUIスクリプト、プレハブ、シーンが含まれています。

import demo scene image

正常にインポートしたら、TournamentSDK_Demo/Scenes/Demo.unityにあるデモシーンを開きます。
シーンを実行します。
ゲームの クライアントID を入力してトーナメントSDKクライアントを開始します。
Fill in your ニックネーム を入力して 無名のプロバイダ を使用してログインします。
(無名のログインプロバイダがダッシュボードで 有効 になっていることを確認してください。)
この段階で、最近のトーナメントをブラウズ氏、連動することができます。

マニュアルクライアントの初期化

この例では、ユーザーがニックネームを選択し、Photonが正常にサーバーに接続したの後でのみトーナメントSDKを初期化します。
シーンオブジェクトにBackboneManagerスクリプトを追加します。
開始時に初期化する ボックスの チェックを外 します。

Add backbone manager image
Set backbone manager image

また、同じシーンオブジェクトにResourceCacheスクリプトを追加します。

Add resource cache image

BackboneIntegrationという新しいスクリプトを作成します。

Add backbone integration image

スクリプトを開いてクライアント初期化フローを実装します。

C#

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

public class BackboneIntegration : MonoBehaviour {

    private WaitForSeconds waitOneSecond = new WaitForSeconds(1);

    private IEnumerator Start()
    {
        // wait until player nick was set (this happens on initial screen)
        while (string.IsNullOrEmpty(PhotonNetwork.player.NickName))
        {
            yield return this.waitOneSecond;
        }
        // keep trying to initialize client
        while (!BackboneManager.IsInitialized)
        {
            yield return BackboneManager.Initialize();
            yield return this.waitOneSecond;
        }
        // create arbitrary user id (minimum 64 chars) based on nickname
        string arbitraryId = "1000000000000000000000000000000000000000000000000000000000000001" + PhotonNetwork.player.NickName;
        // log out user if ids do not match
        if (BackboneManager.IsUserLoggedIn &&
            BackboneManager.Client.User.GetLoginId(LoginProvider.Platform.Anonym) != arbitraryId)
        {
            Debug.LogFormat("Backbone user({0}) logged out.", BackboneManager.Client.User.UserId);
            yield return BackboneManager.Client.Logout();
        }
        // log in user
        if (!BackboneManager.IsUserLoggedIn)
        {
            yield return BackboneManager.Client.Login(LoginProvider.Anonym(true, PhotonNetwork.player.NickName, arbitraryId));
            if (BackboneManager.IsUserLoggedIn)
            {
                Debug.LogFormat("Backbone user({0}) logged in.", BackboneManager.Client.User.UserId);
            }
            else
            {
                Debug.LogFormat("Backbone user failed to log in.");
            }
        }
    }
}

次のトーナメントを表示する

ユーザーが正常にログインしてから、トーナメントリストの更新を行います。

C#

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

BackboneManager.Client.Tournaments.UpcomingTournamentプロパティを使用してユーザーに対し次のトーナメントを取得します。

Upcoming tournament image

C#

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

トーナメントハブスクリーンをコピーする

この例ではトーナメントSDKで提供されている デフォルトのUIスクリーン を使用します。
実装すべき一番重要なスクリーンはTournamentListScreenTournamentHubScreenです。

UIヒエラルキーにTournamentHubScreenをコピーします。
このスクリーンはユーザーにすべてのトーナメントの詳細を表示し、トーナメンプレイハブとして動作します。

Import tournament hub image

TournamentHubScreenを初期化するには、UIオブジェクトを有効にする前に正しい トーナメントID を設定する必要があります。
TournamentHubScreen内のGUITournamentHubScreenへのリファレンスを作成し Initialize(long tournamentId)を呼び出します。

C#

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

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

前のステップで作成した次のトーナメントウィジェットから、メソッドを呼び出せるようになりました。

C#

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

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

注意: TournamentListScreenが表示するのが次のトーナメントのみであるため、簡略化のためこの例では使用していません。

トーナメントマッチハンドラーを実装する

トーナメントハブとゲームルーム・ロビー作成API間のインターフェースを作成しましょう。
TournamentMatchHandlerという新しいスクリプトを作成します。
スクリプトを開いてTournamentMatchCallbackHandlerから派生させます。
簡潔な一連のメソッドを提供してロビー・ルームのステートをトーナメントハブに伝達します。

C#

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

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

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

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

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

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

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

上記のメソッドは以下の順番で実行します:

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

OnJoinTournamentMatch

トーナメント、マッチ、コントローラオブジェクトをパスするトーナメントハブからのコールバック
マッチデータを使用して正しいロビー・ルームに参加します。
コントローラを使用してトーナメントハブにロビー・ルームでの変更を通知します。

C#

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

private IEnumerator JoinRoomRoutine()
{
    while (this.tournamentMatch != null)
    {
        // If you require specific region for tournament, you can use
        // tournament custom properties providing the info about required region.
        // string cloudRegion = this.tournament.CustomProperties.Properties["cloud-region"];
        // ...
        // PhotonNetwork.ConnectToRegion(region, gameVersion);
        // ...
        // wait until connected to proper region
        // ...
        // continue connecting to room

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

        yield return this.waitOneSec;
    }
}

このメソッドはTournamentMatchHandlerを初期化し、適切なロビー・ルームへの接続を開始します。
これが呼び出されるのは、ユーザーが次のマッチへの準備でいていると確認を行ったあとのみです。

IsConnectedToGameServerNetwork

クライアントが正常にネットワークバックエンドに接続したか確認するトーナメントハブからのコールバック
ユーザーが接続しており、与えられたマッチに参加する準備ができていればtrueを返します。

C#

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

IsUserConnectedToMatch

特定のユーザーがすでにロビー・ルーム接続したかどうか確認するトーナメントハブからのコールバック。
ユーザーが接続していればtrue。
プレイヤープロパティが、Photonルームに参加する前に ユーザーID で設定されていることにご注意ください。
このプレイヤープロパティは接続してたPhotonプレイヤーを認識するのに使用します。

C#

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

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

接続済のトーナメントマッチにいるべきすべてのユーザーに対してメソッドが呼び出されます。

IsGameSessionInProgress

既定のトーナメントマッチでゲームセッションが既に進行しているか確認するトーナメントハブからのコールバック。
ゲームセッションが進行していればtrue。

C#

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

この例ではBackboneManager.Client.CreateGameSession()への正常な呼び出しを行った後、sessionStartedをtrueに設定しています。

IsUserReadyForMatch

特定のユーザーが準備完了しているか確認するトーナメントハブからのコールバック(例:ただしいスロットに移動)。
ユーザーの開始準備が整っていればtrueを返します。
マッチにまだチェックインしていないローカルユーザーは、trueを返した後にのみチェックインされます。

C#

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

この例では、ルームへの接続後ユーザーに設定する必要のあるものはありません。ですので、デフォルトでtrueを返します。

StartGameSession

ゲームセッションの即時開始をリクエストするトーナメントハブからのコールバック
現在のマッチに正常にチェックインしたユーザーをパスします。
トーナメントゲームセッションを作成しゲームを開始します。
これはIsGameSessionInProgressがtrueを返すまで数回呼び出される可能性があります。

C#

public override void StartGameSession(IEnumerable<TournamentMatch.User> checkedInUsers)
{
    // Start tournament game session with users that checked in.
    // Be aware that this callback can be called multiple times until
    // IsGameSessionInProgress returns true.

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

    // Check if Photon is still connected to room and ready
    if (!PhotonNetwork.connectedAndReady ||
        !PhotonNetwork.inRoom)
    {
        return;
    }

    // Check if session is not being requested
    if (!this.creatingSession)
    {
        this.creatingSession = true;
        // Create tournament game session
        BackboneManager.Client.CreateGameSession(
            checkedInUsers,
            this.tournamentMatch.Id,
            0)
            .ResultCallback((gameSession) =>
                {
                    this.creatingSession = false;
                    // Check if game session was created
                    if (gameSession != null)
                    {
                        // Indicate that session has started
                        this.sessionStarted = true;
                        // Set room properties
                        var ht = new ExitGames.Client.Photon.Hashtable();
                        ht.Add("SESSIONID", gameSession.Id);
                        ht.Add("TOURNAMENTID", this.tournament.Id);
                        ht.Add("TOURNAMENTMATCHID", this.tournamentMatch.Id);
                        PhotonNetwork.room.SetCustomProperties(ht);
                        // At this point you can also initiate scene loading
                        // and game session start
                    }
                })
            .Run(this);
    }
}

備考: OnJoinTournamentMatchでパスされたtournamentMatchControllerレポートメソッド を使用することも 重要 です。
特定のイベントが接続されたロビーまたはルームで発生した場合にこれらのコントローラメソッドを呼び出します。
トーナメントハブはこれらを使用してメタデータを更新するタイミングを決定します。
これをうまく行えないと、1人のクライアントがマッチを開始し手もほかのクライアントが開始していない、などの不一致を引き起こすことになります。 (例:他のクライアントにとってはユーザーがチェックインしていない)。

tournamentMatchControllerへ変更を報告するPhotonルームコールバックの使用例:

C#

//Photon callback when new player joined room
public void OnPlayerEnteredRoom(Player newPlayer)
{
    long userId;
    //extract user id from player custom properties
    if (this.tournamentMatchController != null &&
        TryGetPlayerBackboneUserId(newPlayer, out userId))
    {
        //report user who joined room
        this.tournamentMatchController.ReportJoinedUser(userId);
    }
}
//Photon callback when player disconnected from room
public void OnPlayerLeftRoom(Player otherPlayer)
{
    long userId;
    //extract user id from player custom properties
    if (this.tournamentMatchController != null &&
        TryGetPlayerBackboneUserId(otherPlayer, out userId))
    {
        //report user who disconnected from room
        this.tournamentMatchController.ReportDisconnectedUser(userId);
    }
}
//Photon callback when room properties are updated
public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
{
    if (this.tournamentMatchController != null)
    {
        //reporting status change will refresh match metadata
        this.tournamentMatchController.ReportStatusChange();
    }
}
//Photon callback when player properties are updated
public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
    if (this.tournamentMatchController != null)
    {
        //reporting status change will refresh match metadata
        this.tournamentMatchController.ReportStatusChange();
    }
}

トーナメントマッチハンドラーをアタッチする

シーンオブジェクトに作成したTournamentMatchHandlerを追加します。 (例: TournamentHubScreenのとなり)
TournamentMatchHandlerのオブジェクトリファレンスを MatchHandler フィールドのGUITournamentActiveMatchスクリプトに追加します。
このスクリプトはTournamentHubScreen/Canvas/ActiveMatchContainerオブジェクトにあります。

Add match handler image

結果サブミッションを実装する

ゲームセッションが終了したら最終順位を設定する必要があります。
ゲームセッションの開始前に新しいゲームセッションオブジェクトを作成するかBackboneManager.Client.CreateGameSession()で取得したものを使用します。
カスタムステータスをゲームセッション結果と一緒に送信します。

C#

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

ゲームセッションオブジェクトを入力したらBackboneManager.Client.SubmitGameSession(gameSession);で送信します。

C#

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

結果送信の後トーナメントデータを再読み込みしてUserActiveMatchがクローズしているか終了しているか確認します。
ユーザーアクティブマッチが終了していない場合は、他のゲームセッションが続きます。(例:三番勝負)
新しいゲームセッションをBackboneManager.Client.CreateGameSession() で作成し、サイクルを繰り返します。

トーナメントコアループを仕上げる

最後の結果送信の後でUserActiveMatchが終了している、またはクローズしている場合、ユーザーはUserActiveMatchに戻ります。
There he can see current stats and progress chage after finished match.
最新のステータスを確認して変更を
ユーザーは明示的に「次のマッチへの準備完了」承認のアクションを経ず即座に他のマッチに移動されるわけではありません。
ユーザーが次のマッチへ準備完了の承認をおこなうと、システムが別のUserActiveMatchを割り当てトーナメントが狩猟するまでサイクルを繰り返します。

Back to top