> 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/ru/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](/ru/unity.md) / [Unreal Engine](/ru/unreal-engine.md) / [Godot](/ru/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 и netcode Fishnet v3.11.14.

### Облачный скрипт PlayFab

#### Создать новую функцию

Войдите в свою учетную запись PlayFab, затем перейдите на страницу обзора вашей игры на панели управления. На `Автоматизация` вкладке создайте новую функцию Cloud Script, которая отправит запрос к API Edgegap для развертывания нового сервера.

В запросе вам нужно будет передать список широт и долгот игроков, чтобы сервер можно было развернуть в наилучшем для всех месте.

Вам также нужно передать некоторые пользовательские переменные окружения в виде пар «ключ-значение», а именно:

* с `ID матча` сгенерированный матчмейкером PlayFab;
* один PlayFab `секретный ключ Title`, который можно найти в настройках вашего титула;
* список `список` идентификатора каждой `учетной записи Master Player Account Id`.

Эти переменные будут использоваться для обновления `данные игрока` PlayFab игроков информацией, необходимой для подключения к серверу. Сохраняйте ключи, которые вы используете для задания этих переменных; в этом примере это `"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 разделен запятой.
{% 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);
};
```

{% endcode %}

{% hint style="info" %}
Эта функция Cloud Script создает интерфейс между игровым клиентом и API Edgegap, что позволяет сохранять ваш `токен API Edgegap` и `секретный ключ PlayFab Title` скрытым от игроков.
{% endhint %}

### Правило автоматизации

Все еще на `Автоматизация`, перейдите к `правилам` вкладке. Создайте новое правило со следующими настройками:

* Установите `тип события` на `playfab.matchmaking.match_found`;
* Добавьте новый `действие`:
  * Установите `тип` на `Выполнить Entity Cloud Script`;
  * Установите `функция Cloud Script` на `StartEdgegapDeployment`.

<figure><img src="https://3845012722-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 учетной записи игрока титула` для последующего тестирования.

### Редактирование игры

#### Сервер

После открытия примера игры в редакторе Unity мы добавили новый пустой GameObject в `BattleScene` находящуюся в `Assets/SpaceEdge/Scenes`. Затем мы прикрепили новый C#-скрипт под названием `PlayerDataUpdateService` к нему. Он содержит функцию обратного вызова, которая будет выполнена, как только сервер будет готов.

Мы также создали второй скрипт под названием `PlayfabRestApiService`, который содержит различные запросы, которые необходимо отправлять в разные API PlayFab.

В `PlayerDataUpdateService.cs`, мы получаем несколько переменных окружения, внедренных при развертывании:

* Пользовательские переменные:
  * `PLAYFAB_MATCH_ID`;
  * `PLAYFAB_TITLE_SECRET`;
  * `PLAYFAB_MATCH_PLAYERS_ID_LIST`
* По умолчанию [Переменные Edgegap](/ru/learn/orkestraciya/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}, ошибка {(int)_request.StatusCode} с сообщением: \n{_response}");
        }
    }
}
```

{% endcode %}

#### Клиент

Настройка на стороне клиента будет выполняться в `MainMenu` сцены, находящейся в `Assets/SpaceEdge/Scenes`. Мы создали новый скрипт под названием `MainMenuService` для управления UI сцены и отображения различных сообщений игроку, а также добавили новые функции в `PlayfabRestApiService.cs`. Мы прикрепили `PlayfabRestApiService` скрипт к новому gameObject в этой сцене.

Первое, что нужно сделать игроку, — войти в PlayFab, что позволит ему отправлять дополнительные запросы к Client и Multiplayer API PlayFab.

{% hint style="info" %}
Мы используем `LoginWithCustomID` для простоты, однако в зависимости от ситуации есть более подходящие способы реализовать вход (например, в мобильных играх).
{% endhint %}

После того как игрок вошёл в систему и оба `X-Authorization` и `X-EntityToken` заголовка были корректно настроены для последующих запросов, он может создать новый билет подбора матча, используя `Начать игру` кнопку интерфейса. Это отправит запрос к многопользовательскому API PlayFab и сохранит `ID билета` для последующего использования.

После этого игровой клиент будет периодически проверять статус билета каждые несколько секунд, пока не будет найден матч или билет не будет отменён. Когда матч найден, `ID матча` сохраняется в переменную.

Затем клиент будет периодически отправлять запросы к Client API PlayFab, чтобы получить Title игрока `данные игрока`, используя `ID матча` в качестве ключа для конкретных данных, которые необходимо получить. Когда данные найдены, он устанавливает `адрес` и внешний `порт` значение в компоненте `транспорт` netcode, а затем подключается к серверу.

После того как клиент корректно подключён и запущен, отправляется запрос на удаление информации о подключении из 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);
            Вход выполнен успешно
        }
        else
        {
            Не удалось выполнить вход игрока, ошибка {(int)_request.StatusCode} с сообщением: \n{_response}
        }
    }

    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](/ru/unity.md) / [Unreal Engine](/ru/unreal-engine.md) / [Godot](/ru/godot.md).

### Тестирование

Если используете FishNet - сначала обязательно отключите `Запуск в безголовом режиме` параметр в `NetworkManager`.

Установите `PlayfabRestApiService` скрипте с `пользовательский ID` и `ID проекта` вашего первого игрока, а также вашим игровым `ID проекта` и `именем очереди`. В `Настройках сборки`, создайте новую клиентскую сборку с этими настройками.

После завершения сборки измените `пользовательский ID` и `ID проекта` значения на значения вашего второго игрока. При желании вы можете создать отдельную клиентскую сборку, хотя редактор Unity в режиме игры должен работать не хуже.

Если оба клиента корректно входят в систему при запуске, нажмите на кнопку в обоих экземплярах `Начать игру` кнопку. Через некоторое время они будут сопоставлены через матчмейкер PlayFab, а затем подключатся к одному и тому же развертыванию в Edgegap после того, как сервер завершит настройку.
