Example using pre-built UI
このページでは、ゲームにコアトーナメントツールを実装する方法を説明した簡単なチュートリアルをご紹介します。
トーナメントSDKUnityプラグイン内にあるプレビルドのUIスクリーンを使用しています。
デモシーンを実装する
UnityプラグインパッケージからTournamentSDK_Demo
フォルダーを実装します。
このフォルダにはトーナメントメタデータの可視化に便利な、関連するUIスクリプト、プレハブ、シーンが含まれています。
正常にインポートしたら、TournamentSDK_Demo/Scenes/Demo.unity
にあるデモ
シーンを開きます。
シーンを実行します。
ゲームの クライアントID を入力してトーナメントSDKクライアントを開始します。
Fill in your ニックネーム を入力して 無名のプロバイダ を使用してログインします。
(無名のログインプロバイダがダッシュボードで 有効 になっていることを確認してください。)
この段階で、最近のトーナメントをブラウズ氏、連動することができます。
マニュアルクライアントの初期化
この例では、ユーザーがニックネームを選択し、Photonが正常にサーバーに接続したの後でのみトーナメントSDKを初期化します。
シーンオブジェクトにBackboneManager
スクリプトを追加します。
開始時に初期化する ボックスの チェックを外 します。
また、同じシーンオブジェクトにResourceCache
スクリプトを追加します。
BackboneIntegration
という新しいスクリプトを作成します。
スクリプトを開いてクライアント初期化フローを実装します。
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
プロパティを使用してユーザーに対し次のトーナメントを取得します。
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スクリーン を使用します。
実装すべき一番重要なスクリーンはTournamentListScreen
とTournamentHubScreen
です。
UIヒエラルキーにTournamentHubScreen
をコピーします。
このスクリーンはユーザーにすべてのトーナメントの詳細を表示し、トーナメンプレイハブとして動作します。
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.
}
上記のメソッドは以下の順番で実行します:
- OnJoinTournamentMatch()
- IsConnectedToGameServerNetwork()
- IsUserConnectedToMatch()
- IsGameSessionInProgress()
- IsUserReadyForMatch()
- 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
オブジェクトにあります。
結果サブミッションを実装する
ゲームセッションが終了したら最終順位を設定する必要があります。
ゲームセッションの開始前に新しいゲームセッションオブジェクトを作成するか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
を割り当てトーナメントが狩猟するまでサイクルを繰り返します。