This document is about: QUANTUM 2
SWITCH TO

사전 구축 UI를 활용한 예제


Available in the Gaming Circle and Industries Circle
Circle

이 샘플은 게임에 핵심 토너먼트 루프를 구현하는 방법을 보여주는 간단한 튜토리얼 가이드입니다. tournament-sdk 유니티 플러그인에서 사용할 수 있는 사전 구축된 UI 화면을 사용합니다.

데모 씬 불러오기

유니티 플러그인 패키지에서 TournamentSDK_Demo 폴더를 임포트 합니다. 이 폴더에는 토너먼트 메타데이터를 시각화하는 데 도움이 되는 관련 UI 스크립트, 프리팹 및 씬이 포함되어 있습니다.

import demo scene image

성공적으로 임포트 한 후 TournamentSDK_Demo/Scenes/Demo.unity에 있는 Demo씬을 오픈합니다. 씬을 실행할 수 있습니다. 클라이언트 id를 입력하고 tournament-sdk 클라이언트를 초기화합니다. 닉네임을 입력하고 anonymous provider(관리 화면에서 익명 로그인이 사용으로 되어있는지를 확인)를 사용하여 로그인합니다. 이 시점에서는 최근 토너먼트와 토너먼트에서 했던 일들이 보여야 합니다.

수동 클라이언트 초기화

이 예에서는 사용자가 닉네임을 선택하고 Photon이 서버에 성공적으로 연결한 후에만 토너먼트-sdk를 초기화하려고 합니다. 씬 객체에 BackboneManager 스크립트를 추가합니다. Initialize on start가 나오면 박스를 Untick합니다.

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를 초기화하려면 tournament id를 UI 객체가 사용 가능으로 되기 전에 설정되어야 합니다. 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 룸에 참여하기 전에 UserId로 설정되었습니다. 이 플레이어 속성은 연결된 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);
    }
}

토너먼트 매치 핸들러 붙이기

씬 객체에 생성해놓은 TournamentMatchHandler를 추가합니다.(예, TournamentHubScreen 옆) 그리고 GUITournamentActiveMatch 스크립트 에 TournamentMatchHandler 의 객체 참조, MatchHandler 필드를 추가합니다. 이 스크립트는 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가 종료되었거나 끝났는지를 보기 위해 토너먼트 데이터를 다시 로드할 수 있습니다. 사용자 활성 매치가 아직 끝나지 않았으면, 다른 게임 세션이 이어진다는 의미입니다(예, 3강 대결). BackboneManager.Client.CreateGameSession()로 새로운 게임 세션을 생성하고 사이클을 반복합니다.

토너먼트 코어 루프 종료

마지막 결과 제출 후 UserActiveMatch가 완료되거나 닫히면 사용자가 TournamentHubScreen로 돌아가야 합니다. 거기서 사용자는 현재 통계와 경기가 끝난 후의 진행 상황을 볼 수 있습니다. "다음 경기 준비"라는 명시적 확인 작업이 없으면 사용자가 즉시 다른 경기로 이동되지 않습니다. 사용자가 다음 경기를 치를 준비가 되었다고 확인되면 시스템이 또 다른 UserActiveMatch를 할당하고 토너먼트가 끝날 때까지 순환합니다.

Back to top