# Playfab 브리지

이 가이드는 PlayFab의 매치메이커를 통해 플레이어들이 매칭된 후 Edgegap에 서버를 자동으로 배포하도록 게임을 설정하는 방법을 보여줍니다. 클라이언트는 서버가 준비되면 서버가 업데이트하는 PlayFab 플레이어 데이터를 통해 서버에 연결하는 정보를 액세스합니다. 시작하기 전에 다음을 전제로 합니다:

* 현재 PlayFab을 사용하고 있습니다;
* 이미 [생성했습니다](https://docs.edgegap.com/docs.edgegap.com-ko/learn/orchestration/application-and-versions) 및 [배포했습니다](https://docs.edgegap.com/docs.edgegap.com-ko/learn) 이전에 Edgegap로 애플리케이션을 배포한 적이 있습니다.

최종 샘플 프로젝트는 우리의 [GitHub](https://github.com/edgegap/sample-playfab-matchmaker-fishnet)에서 찾을 수 있습니다. 이 샘플은 Unity v2021.3.10 및 네트코드로 Fishnet v3.11.14를 사용하여 테스트되었습니다.

### PlayFab Cloud Script 설정

#### 새 함수 생성

PlayFab 계정에 로그인한 다음 대시보드에서 게임의 개요 페이지로 이동합니다.  `Automation` 탭에서 Edgegap API로 요청을 보내 새 서버를 배포할 Cloud Script 함수를 새로 만듭니다.

요청에 플레이어들의 위도/경도 위치 목록을 전달해야 하며, 그래야 서버를 모두에게 가장 적절한 위치에 배포할 수 있습니다.

또한 키-값 쌍으로 몇 가지 사용자 지정 환경 변수를 전달해야 합니다. 즉:

* 다음 `Match ID` 는 PlayFab의 매치메이커에서 생성됩니다;
* PlayFab `Title Secret Key`는 타이틀 설정에서 찾을 수 있습니다;
* 다음의 `목록` 각 플레이어의 `Master Player Account Id`.

이 변수들은 플레이어들의 PlayFab `Player Data` 를 서버에 연결하는 데 필요한 정보로 업데이트하는 데 사용됩니다. 이러한 변수를 설정할 때 사용하는 키를 기록해 두십시오. 이 샘플에서는 다음과 같습니다. `"PLAYFAB_MATCH_ID"`, `"PLAYFAB_TITLE_SECRET"`및 `"PLAYFAB_MATCH_PLAYERS_ID_LIST"`.

{% hint style="info" %}
이 함수를 만드는 가장 간단한 방법은 `Revisions (Legacy)` 탭에서 새 리비전을 배포하고 다음 코드를 추가하는 것입니다. 먼저 최상단의 `Edgegap_AppName`, `Edgegap_AppVersion`, `Edgegap_ApiToken` 및 `PlayFab_TitleSecret` 변수를 자신의 값으로 업데이트했는지 확인하십시오.

앱 이름과 버전은 Edgegap에서 게임 서버를 컨테이너화할 때 나중에 사용됩니다.

플레이어 ID 목록은 각 ID가 쉼표로 구분된 하나의 문자열로 여기 저장됩니다.
{% endhint %}

```javascript
var Edgegap_AppName = "APP_NAME";
var Edgegap_AppVersion = "APP_VERSION";
var Edgegap_ApiToken = "token XXXX"; // "token" 키워드를 포함하세요
var PlayFab_TitleSecret = "TITLE_SECRET";

handlers.StartEdgegapDeployment = function (args, context) {
  var QueueName = context.playStreamEvent.Payload.QueueName;
  var MatchId = context.playStreamEvent.Payload.MatchId;

  var GeoIpList = [];
  var PlayerIds = "";

  var MatchMember = multiplayer.GetMatch({
    QueueName: QueueName,
    MatchId: MatchId,
    EscapeObject: false,
    ReturnMemberAttributes: false,
  }).Members;

  MatchMember.forEach((Member) => {
    var Profile = entity.GetProfile({
      Entity: Member.Entity,
    });

    var Location = server.GetPlayerProfile({
      PlayFabId: Profile.Profile.Lineage.MasterPlayerAccountId,
      ProfileConstraints: {
        ShowLocations: true,
      },
    });

    GeoIpList.push({
      ip: "1.2.3.4",
      latitude: Location.PlayerProfile.Locations[0].Latitude,
      longitude: Location.PlayerProfile.Locations[0].Longitude,
    });

    PlayerIds += Profile.Profile.Lineage.MasterPlayerAccountId + ",";
  });

  PlayerIds = PlayerIds.slice(0, -1);

  var response = JSON.parse(
    http.request(
      "https://api.edgegap.com/v1/deploy",
      "post",
      JSON.stringify({
        app_name: Edgegap_AppName,
        version_name: Edgegap_AppVersion,
        geo_ip_list: [...GeoIpList],
        tags: [MatchId.slice(0, 20)],
        env_vars: [
          {
            key: "PLAYFAB_MATCH_ID",
            value: MatchId,
          },
          {
            key: "PLAYFAB_TITLE_SECRET",
            value: TitleSecret,
          },
          {
            key: "PLAYFAB_MATCH_PLAYERS_ID_LIST",
            value: PlayerIds,
          },
        ],
      }),
      "application/json",
      { authorization: Edgegap_ApiToken }
    )
  );

  log.info("response", response);
};
```

{% hint style="info" %}
이 Cloud Script 함수는 게임 클라이언트와 Edgegap API 사이의 인터페이스를 생성하여 귀하의 `Edgegap API 토큰` 및 `PlayFab Title Secret` 을(를) 플레이어로부터 숨겨 줍니다.
{% endhint %}

### 자동화 규칙

아직 `Automation`에서, `Rules` 탭으로 이동합니다. 다음 설정으로 새 규칙을 만드십시오:

* 다음을 설정하십시오: `Event Type` 에서 `playfab.matchmaking.match_found`;
* 새 `Action`:
  * 다음을 설정하십시오: `Type` 에서 `Execute Entity Cloud Script`;
  * 다음을 설정하십시오: `Cloud Script Function` 에서 `StartEdgegapDeployment`.

<figure><img src="https://1562312210-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsR0dHSFv9ymoC0DO5G8J%2Fuploads%2Fgit-blob-558d1a8ab45a1545c4c7d64d1963a0f1e682645c%2Fautomation-rule.png?alt=media" alt=""><figcaption></figcaption></figure>

### 매치메이커 큐 및 플레이어

&#x20; `Multiplayer` 탭에서, `Matchmaking`아래에 큐가 사용 가능하도록 설정되어 있는지 확인하거나, 그렇지 않다면 새 큐를 생성하십시오. 빠른 테스트를 위해 단순히 임의의 두 플레이어를 매칭하도록 설정하십시오.

매치메이커 규칙을 만족시킬 만큼 충분한 플레이어가 PlayFab 타이틀에 등록되어 있어야 하므로, 이 경우 두 명이면 충분합니다. 필요한 경우 `Players` 탭에서 새 플레이어를 생성할 수 있습니다. 이 샘플 프로젝트의 경우 나중에 테스트를 위해 해당 플레이어들의 `Custom ID` 및 `Title Player Account ID` 를 기록해 두십시오.

### 게임 편집

#### 서버

Unity 에디터에서 게임 샘플을 연 후, 우리는 `BattleScene` 에 새 빈 gameObject를 추가했습니다. 해당 씬은 `Assets/SpaceEdge/Scenes`에 있습니다. 그런 다음 `PlayerDataUpdateService` 라는 새 C# 스크립트를 이것에 연결했습니다. 이 스크립트에는 서버가 준비되면 실행될 콜백 함수가 포함되어 있습니다.

또한 PlayFab의 다양한 API에 전송해야 하는 여러 요청을 포함하는 `PlayfabRestApiService`라는 두 번째 스크립트도 만들었습니다.

파일 `PlayerDataUpdateService.cs`에서 배포 시 주입되는 몇 가지 환경 변수를 가져옵니다:

* 사용자 지정 변수:
  * `PLAYFAB_MATCH_ID`;
  * `PLAYFAB_TITLE_SECRET`;
  * `PLAYFAB_MATCH_PLAYERS_ID_LIST`
* 기본 Edgegap 변수:
  * `ARBITRIUM_PUBLIC_IP`;
  * `ARBITRIUM_PORTS_MAPPING`;

파일 `ARBITRIUM_PORTS_MAPPING`에서, 클라이언트가 서버에 연결할 수 있도록 하는 특정 외부 `포트` 값을 가져와야 합니다. 이 샘플에서는 이름이 `"Game Port"`.

인 포트입니다. `PLAYFAB_TITLE_SECRET` 인증으로 `Player Data` 를 사용하여 PlayFab 서버 API에 요청을 보내 해당 ID에 대한 `PLAYFAB_MATCH_PLAYERS_ID_LIST` 를 다음 키-값 쌍으로 업데이트합니다:

* 키: `PLAYFAB_MATCH_ID`;
* 값: `"{ARBITRIUM_PUBLIC_IP}:{port}"`;

#### PlayerDataUpdateService

```cs
public class PlayerDataUpdateService : NetworkBehaviour
{
    private PlayfabRestApiService _playfabService;

    [SerializeField] private string KeyMatchId = "PLAYFAB_MATCH_ID";
    [SerializeField] private string KeyTitleSecret = "PLAYFAB_TITLE_SECRET";
    [SerializeField] private string KeyPlayerIds = "PLAYFAB_MATCH_PLAYERS_ID_LIST";
    [SerializeField] private string ServerPortName = "Game Port";

    private string matchID;
    private string titleSecretKey;
    private string[] playerIdList;

    public override void OnStartServer()
    {
        _playfabService = FindObjectOfType<PlayfabRestApiService>();

        matchID = Environment.GetEnvironmentVariable(KeyMatchId);
        titleSecretKey = Environment.GetEnvironmentVariable(KeyTitleSecret);
        playerIdList = Environment.GetEnvironmentVariable(KeyPlayerIds)?.Split(',');

        string address = Environment.GetEnvironmentVariable("ARBITRIUM_PUBLIC_IP");
        string portsMapping = Environment.GetEnvironmentVariable("ARBITRIUM_PORTS_MAPPING");

        if (!string.IsNullOrEmpty(portsMapping))
        {
            int port = JSON.ParseString(portsMapping).GetJSON("ports").GetJSON(ServerPortName).GetInt("external");

            foreach (string playerId in playerIdList)
            {
                if (!string.IsNullOrEmpty(playerId))
                {
                    _playfabService.UpdatePlayerData(titleSecretKey, playerId, matchID, $"{address}:{port}");
                }
            }
        }
        else
        {
            Debug.Log("Port Mapping Unavailable");
        }
    }

    ...
}
```

#### PlayfabRestApiService

```cs
public class PlayfabRestApiService : MonoBehaviour
{
    ...

    [SerializeField] private string TitleID = "TITLE_ID";

    private const string TypeHeaderValue = "application/json";
    private readonly HttpClient _playfabHttpClient = new();
    private HttpResponseMessage _request;
    private StringContent _requestData;
    private string _response;

    ...

    public async void UpdatePlayerData(string secretKey, string playerID, string key, string value)
    {
        if (!_playfabHttpClient.DefaultRequestHeaders.Contains("X-SecretKey"))
        {
            _playfabHttpClient.DefaultRequestHeaders.Add("X-SecretKey", secretKey);
        }

        var dataProperty = new JSON();
        dataProperty.Add(key, value);

        var requestJson = new JSON();
        requestJson.Add("PlayFabId", playerID);
        requestJson.Add("Data", dataProperty);

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);
        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Server/UpdateUserData", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (!_request.IsSuccessStatusCode)
        {
            Debug.Log($"Could Not Update PlayerData of ID {playerID}, Error {(int)_request.StatusCode} With Message: \n{_response}");
        }
    }
}
```

#### 클라이언트

클라이언트 측 설정은 `MainMenu` 씬에서 관리되며, 해당 씬은 `Assets/SpaceEdge/Scenes`에 있습니다. 우리는 `MainMenuService` 씬의 UI를 관리하고 플레이어에게 다양한 메시지를 표시하기 위해, 그리고 다음에 새로운 기능을 추가했습니다 `PlayfabRestApiService.cs`. 우리는 다음을 부착했습니다 `PlayfabRestApiService` 스크립트를 이 씬의 새 게임 오브젝트에도 붙였습니다.

플레이어가 가장 먼저 해야 할 일은 PlayFab으로 로그인하는 것입니다. 그러면 PlayFab의 Client 및 Multiplayer API에 추가 요청을 보낼 수 있습니다.

{% hint style="info" %}
우리는 다음을 사용합니다 `LoginWithCustomID` 엔드포인트를 단순화를 위해 사용하지만, 상황에 따라(예: 모바일 게임) 로그인 구현에 더 적합한 방법이 있습니다.
{% endhint %}

플레이어가 로그인하고 `X-Authorization` 및 `X-EntityToken` 헤더가 이후 요청을 위해 올바르게 설정되면, 다음을 사용하여 새로운 매치메이킹 티켓을 생성할 수 있습니다 `Start Game` UI 버튼. 그러면 PlayFab의 멀티플레이어 API에 요청을 보내고 `티켓 ID` 를 추후 사용을 위해 저장합니다.

그 후, 게임 클라이언트는 몇 초마다 티켓의 상태를 주기적으로 확인하여 매치가 발견되거나 티켓이 취소될 때까지 계속합니다. 매치가 발견되면 `매치 ID` 가 변수로 저장됩니다.

그 다음 클라이언트는 플레이어의 Title `Player Data`를 가져오기 위해 PlayFab의 Client API에 주기적으로 요청을 보냅니다. 이때 가져와야 하는 특정 데이터의 키로 `매치 ID` 를 사용합니다. 데이터가 발견되면 서버의 `주소` 와 외부 `포트` 값을 넷코드의 `트랜스포트` 컴포넌트에 설정한 다음 서버에 연결합니다.

클라이언트가 정상적으로 연결되고 시작되면, 해당 플레이어의 PlayFab에서 연결 정보를 제거하기 위한 요청이 전송됩니다 `Player Data` 콜백 함수 덕분에.

{% hint style="info" %}
이러한 키-값 쌍이 삭제되었는지 확인하는 것이 중요하며, 가능하면 배포가 종료되기 전에 삭제하는 것이 좋습니다. 이는 다음을 어질러 놓는 것을 방지하기 위함입니다 `Player Data` 에 불필요한 항목이 너무 많아지는 것을.
{% endhint %}

#### MainMenuService

```cs
public class MainMenuService : MonoBehaviour
{
    [SerializeField] private Button startGameButton;
    [SerializeField] private TMP_InputField playerNameInput;
    [SerializeField] private TMP_Text serverStatus;

    private PlayfabRestApiService _playfabService;

    private void Start()
    {
        if (!InstanceFinder.ServerManager.GetStartOnHeadless())
        {
            _playfabService = FindObjectOfType<PlayfabRestApiService>();
            _playfabService.OnStatusUpdate += ServerStatusUpdate;
            _playfabService.OnUpdateButton += UpdateButtonState;

            startGameButton.onClick.AddListener(StartGame);
            playerNameInput.onValueChanged.AddListener(NameChanged);
            if (PlayerPrefs.HasKey("PlayerName"))
                playerNameInput.text = PlayerPrefs.GetString("PlayerName");

            _playfabService.LoginPlayer();
        }
    }

    private void OnDestroy()
    {
        if (!InstanceFinder.ServerManager.GetStartOnHeadless())
        {
            _playfabService.OnStatusUpdate -= ServerStatusUpdate;
            _playfabService.OnUpdateButton -= UpdateButtonState;
        }
    }

    private void StartGame()
    {
        startGameButton.interactable = false;
        serverStatus.text = "";
        _playfabService.CreateTicket();
    }

    private void ServerStatusUpdate(string status, bool isError)
    {
        serverStatus.text += "\n" + status;
        serverStatus.color = isError ? Color.red : Color.green;
        Debug.Log(status);
    }

    private void UpdateButtonState(bool state)
    {
        startGameButton.interactable = state;
        serverStatus.text = "";
    }

    private void NameChanged(string text) => PlayerPrefs.SetString("PlayerName", text);
}
```

#### PlayfabRestApiService

```cs
public class PlayfabRestApiService : MonoBehaviour
{
    [SerializeField] private string PlayerCustomID = "PLAYER_CUSTOM_ID";
    [SerializeField] private string PlayerTitleID = "PLAYER_TITLE_ID";
    [SerializeField] private string TitleID = "TITLE_ID";
    [SerializeField] private string QueueName = "QUEUE_NAME";

    private const string TypeHeaderValue = "application/json";
    private readonly HttpClient _playfabHttpClient = new();
    private HttpResponseMessage _request;
    private StringContent _requestData;
    private string _response;
    private string _ticketID = "";
    private string _matchID = "";

    public Action<string, bool> OnStatusUpdate;
    public Action<bool> OnUpdateButton;

    private static PlayfabRestApiService _instance = null;

    private void Awake()
    {
        _playfabHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(TypeHeaderValue));

        if (_instance is null)
        {
            DontDestroyOnLoad(this);
            _instance = this;
        }
        else
        {
            Destroy(this);
        }
    }

    public async void LoginPlayer()
    {
        var requestJson = new JSON();
        requestJson.Add("CustomID", PlayerCustomID);
        requestJson.Add("CreateAccount", false);
        requestJson.Add("TitleID", TitleID);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/LoginWithCustomID", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (_request.IsSuccessStatusCode)
        {
            var responseJson = JSON.ParseString(_response);

            string token = responseJson.GetJSON("data").GetJSON("EntityToken").GetString("EntityToken");
            _playfabHttpClient.DefaultRequestHeaders.Add("X-EntityToken", token);

            string sessionTicket = responseJson.GetJSON("data").GetString("SessionTicket");
            _playfabHttpClient.DefaultRequestHeaders.Add("X-Authorization", sessionTicket);

            OnUpdateButton?.Invoke(true);
            OnStatusUpdate?.Invoke("로그인 성공", false);
        }
        else
        {
            OnStatusUpdate?.Invoke($"플레이어를 로그인할 수 없습니다. 오류 {(int)_request.StatusCode} 메시지: \n{_response}", true);
        }
    }

    public async void CreateTicket()
    {
        var requestJson = new JSON();

        var entityProperty = new JSON();
        entityProperty.Add("Id", PlayerTitleID);
        entityProperty.Add("Type", "title_player_account");

        var creatorProperty = new JSON();
        creatorProperty.Add("Entity", entityProperty);

        requestJson.Add("Creator", creatorProperty);
        requestJson.Add("GiveUpAfterSeconds", 90);
        requestJson.Add("QueueName", QueueName);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Match/CreateMatchmakingTicket", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (_request.IsSuccessStatusCode)
        {
            var responseJson = JSON.ParseString(_response);
            _ticketID = responseJson.GetJSON("data").GetString("TicketId");

            OnStatusUpdate?.Invoke($"티켓이 생성되었습니다, ID #{_ticketID}", false);

            GetMatchIdAttempt();
        }
        else
        {
            OnUpdateButton?.Invoke(true);
            OnStatusUpdate?.Invoke($"티켓을 생성할 수 없습니다. 오류 {(int)_request.StatusCode} 메시지: \n{_response}", true);
        }
    }

    private async void GetMatchIdAttempt()
    {
        bool isMatchFound = false;
        var requestJson = new JSON();
        requestJson.Add("EscapeObject", true);
        requestJson.Add("QueueName", QueueName);
        requestJson.Add("TicketId", _ticketID);
        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        while (!isMatchFound)
        {
            _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Match/GetMatchmakingTicket", _requestData);
            _response = await _request.Content.ReadAsStringAsync();

            if (_request.IsSuccessStatusCode)
            {
                var responseJson = JSON.ParseString(_response);
                string ticketStatus = responseJson.GetJSON("data").GetString("Status");
                isMatchFound = ticketStatus == "Matched";

                if (isMatchFound)
                {
                    _matchID = responseJson.GetJSON("data").GetString("MatchId");
                    OnStatusUpdate?.Invoke($"티켓이 매칭되었습니다, ID #{_matchID}", false);

                    StartConnectionAttempt();
                }
                else if (ticketStatus == "Canceled")
                {
                    isMatchFound = true;
                    string reason = responseJson.GetJSON("data").GetString("CancellationReason");

                    OnUpdateButton?.Invoke(true);
                    OnStatusUpdate?.Invoke($"{reason} 때문에 티켓이 취소되었습니다", false);
                }
                else
                {
                    OnStatusUpdate?.Invoke("매치를 찾지 못했습니다. 10초 후 다시 시도합니다...", false);
                    await Task.Delay(10000);
                }

            }
            else
            {
                isMatchFound = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"티켓 정보를 가져올 수 없습니다. 오류 {(int)_request.StatusCode} 메시지: \n{_response}", true);
            }
        }
    }

    private async void StartConnectionAttempt()
    {
        bool gotConnectionInfo = false;

        var requestJson = new JSON();
        requestJson.Add("Keys", new List<string> { _matchID });

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);

        while (!gotConnectionInfo)
        {
            _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/GetUserData", _requestData);
            _response = await _request.Content.ReadAsStringAsync();

            if (_request.IsSuccessStatusCode)
            {
                var responseJson = JSON.ParseString(_response);
                var dataObject = responseJson.GetJSON("data").GetJSON("Data");

                if (dataObject.Count > 0)
                {
                    gotConnectionInfo = true;
                    string connectionInfo = dataObject.GetJSON(_matchID).GetString("Value");
                    string serverAddress = connectionInfo.Split(':')[0];

                    if (ushort.TryParse(connectionInfo.Split(':')[1], out ushort serverPort))
                    {
                        InstanceFinder.TransportManager.Transport.SetPort(serverPort);
                        InstanceFinder.TransportManager.Transport.SetClientAddress(serverAddress);
                        Debug.Log($"포트 {serverPort} 및 IP {serverAddress}를 사용하여 서버에 연결 중");
                        InstanceFinder.ClientManager.StartConnection();
                    }
                    else
                    {
                        OnUpdateButton?.Invoke(true);
                        OnStatusUpdate?.Invoke($"서버 포트 값을 파싱하는 중 오류", true);
                    }
                }
                else
                {
                    OnStatusUpdate?.Invoke("연결 데이터가 없습니다. 10초 후 다시 시도합니다...", false);
                    await Task.Delay(10000);
                }
            }
            else
            {
                gotConnectionInfo = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"플레이어 데이터를 가져올 수 없습니다. 오류 {(int)_request.StatusCode} 메시지: \n{_response}", true);
            }
        }
    }

    public async void RemovePlayerData()
    {
        var requestJson = new JSON();
        requestJson.Add("KeysToRemove", new string[] { _matchID });

        _requestData = new StringContent(requestJson.CreateString(), Encoding.UTF8, TypeHeaderValue);
        _request = await _playfabHttpClient.PostAsync($"https://{TitleID}.playfabapi.com/Client/UpdateUserData", _requestData);
        _response = await _request.Content.ReadAsStringAsync();

        if (!_request.IsSuccessStatusCode)
        {
            Debug.Log($"플레이어 데이터를 업데이트할 수 없습니다. 오류 {(int)_request.StatusCode} 메시지: \n{_response}");
        }
    }

    ...
}
```

#### PlayerDataUpdateService

```cs
public class PlayerDataUpdateService : NetworkBehaviour
{
    private PlayfabRestApiService _playfabService;

    ...

    public override void OnStartClient()
    {
        _playfabService = FindObjectOfType<PlayfabRestApiService>();
        _playfabService.RemovePlayerData();
    }
}
```

### 게임 서버 빌드 및 컨테이너화

서버 빌드를 컨테이너화하기 전에, 다음을 활성화했는지 확인하세요 `Start On Headless` 옵션을 `을(를) 선택하고` 게임 오브젝트에서.

다음으로 Edgegap 플러그인을 엽니다 `Tools/Edgegap Hosting` 툴바 메뉴. 다음을 확인하세요 `Edgegap API Token` 그리고 게임을 위한 애플리케이션을 생성하거나 로드하세요. 다음이 일치하는지 확인하세요 `포트` 값이 다음의 Tugboat 컴포넌트와 일치하는지 `을(를) 선택하고` 게임 오브젝트. 다음을 선택하세요 `UDP` 프로토콜, 그런 다음 `New Version Tag`을 입력합니다. 앱 이름과 버전이 Cloud Script를 설정할 때 사용한 값과 모두 일치하는지 확인하세요.

모든 설정이 올바르게 완료되면, 다음을 클릭하세요 `Build and Push`를 클릭하면, 짧은 대기 시간 후 게임 서버가 자동으로 컨테이너화되고 Edgegap 대시보드에 새로운 애플리케이션 버전이 생성됩니다.

{% hint style="info" %}
앱 버전을 생성할 때, 플러그인은 기본적으로 포트 이름을 \`"Game Port"\`로 지정합니다.

플러그인을 사용하지 않고 수동으로 컨테이너화 및 앱 버전을 생성하는 경우, 사용할 포트를 선택할 때 해당 이름을 사용해야 합니다.
{% endhint %}

### 테스트

먼저 다음을 비활성화했는지 확인하세요 `Start On Headless` 옵션을 `을(를) 선택하고` 게임 오브젝트. 다음을 설정하세요 `PlayfabRestApiService` 스크립트를 다음과 함께 `Custom ID` 및 `첫 번째 플레이어의 Title ID` 와 게임의 `첫 번째 플레이어의 Title ID` 및 `Queue Name`과 함께. 다음에서 `Build Settings`에서, 이러한 설정으로 새로운 클라이언트 빌드를 생성합니다.

빌드가 완료되면, 다음을 변경하세요 `Custom ID` 및 `첫 번째 플레이어의 Title ID` 값을 두 번째 플레이어의 값으로 변경하세요. 원하는 경우 별도의 클라이언트 빌드를 생성할 수 있지만, 플레이 모드의 Unity 에디터도 동일하게 잘 작동합니다.

두 클라이언트가 실행 시 정상적으로 로그인하면, 두 인스턴스의 `Start Game` 버튼을 클릭하세요. 잠시 후 PlayFab의 매치메이커를 통해 매치된 다음, 서버 설정이 완료되면 Edgegap의 동일한 배포에 연결됩니다.
