> For the complete documentation index, see [llms.txt](https://docs.edgegap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.edgegap.com/ko/docs/tools-and-integrations/playfab-bridge.md).

# Playfab에서 배포

Playfab 매치메이커는 그대로 유지하고 게임 서버는 Edgegap에 배포하여 두 플랫폼을 함께 사용하세요.

Edgegap에서 전용 서버를 쉽게 배포하고 **PlayFab 매치메이커를 계속 사용하는 방법**.

## 🚀 시작하기

시작하기 전에 다음 사항을 준비했는지 확인하세요:

* 다음에 익숙해졌는지 확인하세요 [PlayFab 콘솔](https://developer.playfab.com/en-us/r/sign-in), 특히 [플레이어 데이터](https://learn.microsoft.com/en-us/xbox/playfab/player-progression/player-data/) 및 [매치메이킹](https://learn.microsoft.com/en-us/xbox/playfab/multiplayer/matchmaking/).
* 프로젝트의 시작하기 단계를 완료했습니다 - [Unity](/ko/unity.md) / [Unreal Engine](/ko/unreal-engine.md) / [Godot](/ko/godot.md).
* 플러그인 또는 다음을 사용하여 Edgegap에 서버를 배포했습니다 [대시보드](https://app.edgegap.com/deployment-management/deployments/create), IP 및 포트로 연결합니다.

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

### PlayFab 클라우드 스크립트

#### 새 함수 만들기

PlayFab 계정에 로그인한 후 대시보드에서 게임의 개요 페이지로 이동합니다.  `자동화` 탭에서 새 서버를 배포하기 위해 Edgegap API에 요청을 전송할 새 Cloud Script 함수를 만드세요.

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

또한 다음과 같은 일부 사용자 지정 환경 변수를 키-값 쌍으로 전달해야 합니다:

* 다음 `매치 ID` PlayFab 매치메이커에서 생성됩니다;
* PlayFab `타이틀 비밀 키`, 타이틀 설정에서 찾을 수 있습니다;
* 각 `목록` 플레이어의 `마스터 플레이어 계정 ID`.

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

{% hint style="info" %}
이 함수를 만드는 가장 간단한 방법은 다음에서 새 리비전을 배포하는 것입니다 `리비전(레거시)` 탭에 다음 코드를 추가합니다. 먼저 상단의 다음 변수를 업데이트해야 합니다 `Edgegap_AppName`, `Edgegap_AppVersion`, `Edgegap_ApiToken` 및 `PlayFab_TitleSecret` 변수를 자신의 값으로 설정하세요.

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

플레이어 ID 목록은 여기에서 단일 문자열로 저장되며, 각 ID는 쉼표로 구분됩니다.
{% endhint %}

{% code expandable="true" %}

```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 GeoUserList = [];
  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,
      },
    });

    GeoUserList .push({
      user_type: "geo_coordinates",
      user_data: {
        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/v2/deployments",
      "post",
      JSON.stringify({
        application: Edgegap_AppName,
        version: Edgegap_AppVersion,
        users: [...GeoUserList],
        tags: [MatchId.slice(0, 20)],
        environment_variables: [
          {
            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);
};
```

{% endcode %}

{% hint style="info" %}
이 Cloud Script 함수는 게임 클라이언트와 Edgegap API 사이의 인터페이스를 생성하여 `Edgegap API 토큰` 및 `PlayFab 타이틀 비밀 키` 가 플레이어에게 노출되지 않도록 합니다.
{% endhint %}

### 자동화 규칙

계속해서 `자동화`에서 다음으로 이동합니다 `규칙` 탭입니다. 다음 설정으로 새 규칙을 만드세요:

* 다음을 설정합니다 `이벤트 유형` 을 `playfab.matchmaking.match_found`;
* 새 `작업`:
  * 다음을 설정합니다 `유형` 을 `엔터티 Cloud Script 실행`;
  * 다음을 설정합니다 `Cloud Script 함수` 을 `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>

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

다음 `멀티플레이어` 탭의 다음 항목 아래에서 `매치메이킹`, 사용할 수 있는 큐가 있는지 확인하고, 없으면 새로 만드세요. 빠른 테스트를 위해 임의의 두 플레이어를 매칭하도록 설정하세요.

매치메이커 규칙을 충족하려면 PlayFab 타이틀에 충분한 플레이어가 등록되어 있어야 하므로, 이 경우에는 두 명이면 됩니다. 필요한 경우 다음에서 새 플레이어를 만들 수 있습니다 `플레이어` 탭입니다. 이 샘플 프로젝트에서는 해당 플레이어의 다음 ID를 기록해 두세요 `사용자 지정 ID` 및 `타이틀 플레이어 계정 ID` 는 나중에 테스트할 때 사용됩니다.

### 게임 편집

#### 서버

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

또한 다음 이름의 두 번째 스크립트를 만들었습니다 `PlayfabRestApiService`, 여기에는 PlayFab의 여러 API로 전송해야 하는 다양한 요청이 포함되어 있습니다.

다음에서 `PlayerDataUpdateService.cs`, 배포에 주입된 몇 가지 환경 변수를 가져옵니다:

* 사용자 지정 변수:
  * `PLAYFAB_MATCH_ID`;
  * `PLAYFAB_TITLE_SECRET`;
  * `PLAYFAB_MATCH_PLAYERS_ID_LIST`
* 기본 [Edgegap 변수](/ko/learn/orchestration/deployments.md#injected-environment-variables):
  * `ARBITRIUM_PUBLIC_IP`;
  * `ARBITRIUM_PORT_GAMEPORT_EXTERNAL`;

다음을 `PLAYFAB_TITLE_SECRET` 인증으로 사용하여 PlayFab Server API에 요청을 보내 다음을 업데이트합니다 `플레이어 데이터` 의 각 해당 ID `PLAYFAB_MATCH_PLAYERS_ID_LIST` 에 다음 키-값 쌍을 사용합니다:

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

#### PlayerDataUpdateService

{% code expandable="true" %}

```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";

    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 port = Environment.GetEnvironmentVariable("ARBITRIUM_PORT_GAMEPORT_EXTERNAL");

        if (!string.IsNullOrEmpty(portsMapping))
        {
            foreach (string playerId in playerIdList)
            {
                if (!string.IsNullOrEmpty(playerId))
                {
                    _playfabService.UpdatePlayerData(titleSecretKey, playerId, matchID, $"{address}:{port}");
                }
            }
        }
        else
        {
            Debug.Log("포트 매핑을 사용할 수 없습니다");
        }
    }

    ...
}
```

{% endcode %}

#### PlayfabRestApiService

{% code expandable="true" %}

```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($"ID {playerID}의 PlayerData를 업데이트할 수 없습니다. 오류 {(int)_request.StatusCode}, 메시지: \n{_response}");
        }
    }
}
```

{% endcode %}

#### 클라이언트

클라이언트 측 설정은 다음에서 관리됩니다 `MainMenu` 다음 경로에 있는 씬 `Assets/SpaceEdge/Scenes`. 다음 이름의 새 스크립트를 만들었습니다 `MainMenuService` 을 만들어 씬의 UI를 관리하고 플레이어에게 다양한 메시지를 표시했으며, 다음에 새 함수를 추가했습니다 `PlayfabRestApiService.cs`. 또한 이 씬의 새 gameObject에 다음을 연결했습니다 `PlayfabRestApiService` 스크립트도 연결했습니다.

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

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

플레이어가 로그인하고 다음 두 항목이 모두 `X-Authorization` 및 `X-EntityToken` 헤더가 이후 요청을 위해 올바르게 설정되면, 다음을 사용하여 새 매치메이킹 티켓을 만들 수 있습니다 `게임 시작` UI 버튼입니다. 그러면 PlayFab의 멀티플레이어 API에 요청을 보내고 다음을 저장합니다 `티켓 ID` 를 나중에 사용합니다.

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

그런 다음 클라이언트는 플레이어의 타이틀을 가져오기 위해 PlayFab의 Client API에 주기적으로 요청을 보냅니다 `플레이어 데이터`, 다음을 사용하여 `매치 ID` 을 검색해야 하는 특정 데이터의 키로 사용합니다. 데이터를 찾으면 서버의 `주소` 및 외부 `포트` 값을 넷코드의 `전송` 컴포넌트에 설정한 후 서버에 연결합니다.

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

{% hint style="info" %}
이러한 키-값 쌍은 가급적 배포가 종료되기 전에 삭제되도록 하는 것이 중요합니다. 이는 다음이 불필요한 항목으로 너무 많이 쌓이는 것을 방지하기 위함입니다 `플레이어 데이터` 에 너무 많은 불필요한 항목이 쌓이는 것을 방지하기 위함입니다.
{% endhint %}

#### MainMenuService

{% code expandable="true" %}

```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);
}
```

{% endcode %}

#### PlayfabRestApiService

{% code expandable="true" %}

```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}");
        }
    }

    ...
}
```

{% endcode %}

#### PlayerDataUpdateService

{% code expandable="true" %}

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

    ...

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

{% endcode %}

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

추가된 기능으로 이제 빌드 단계를 다시 수행하세요 - [Unity](/ko/unity.md) / [Unreal Engine](/ko/unreal-engine.md) / [Godot](/ko/godot.md).

### 테스트

FishNet을 사용하는 경우 - 먼저 다음을 비활성화하세요: `헤드리스에서 시작` 옵션을 `NetworkManager`.

다음을 설정합니다 `PlayfabRestApiService` 스크립트에서 `사용자 지정 ID` 및 `Title ID` 첫 번째 플레이어의 값과 게임의 `Title ID` 및 `대기열 이름`. 다음에서 `빌드 설정`, 이러한 설정으로 새 클라이언트 빌드를 생성하세요.

빌드가 완료되면 다음을 변경하세요: `사용자 지정 ID` 및 `Title ID` 값을 두 번째 플레이어의 값으로 바꾸세요. 원하면 별도의 클라이언트 빌드를 만들 수 있지만, 플레이 모드의 Unity 에디터도 충분히 잘 작동할 것입니다.

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