> 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/docs/tools-and-integrations/playfab-bridge.md).

# Deploy from Playfab

Keep your Playfab matchmaker and deploy your game servers to Edgegap, combining both platforms.

Learn how to deploy dedicated servers on Edgegap easily and **keep using Playfab matchmaker**.

## 🚀 Getting Started

Before we dive in, make sure you have:

* Familiarized yourself with the [Playfab Console](https://developer.playfab.com/en-us/r/sign-in), specifically [Player Data](https://learn.microsoft.com/en-us/xbox/playfab/player-progression/player-data/) and [Matchmaking](https://learn.microsoft.com/en-us/xbox/playfab/multiplayer/matchmaking/).
* Completed Getting Started steps for your project - [Unity](/unity.md) / [Unreal Engine](/unreal-engine.md) / [Godot](/godot.md).
* Deployed your server on Edgegap using plugin or [dashboard](https://app.edgegap.com/deployment-management/deployments/create), connecting with IP & port.

You can find the final sample project on our [GitHub](https://github.com/edgegap/sample-playfab-matchmaker-fishnet). This sample was tested using Unity v2021.3.10 and Fishnet v3.11.14 netcode.

### PlayFab Cloud Script

#### Create New Function

Log in to your PlayFab account, then navigate to your game's overview page on the dashboard. In the `Automation` tab, create a new Cloud Script function that will send a request to the Edgegap API to deploy a new server.

You will need to pass a list of the players' latitude/longitude locations in the request, so the the server can be deployed in the best possible location for everyone.

You also need to pass some custom environment variables as key-value pairs, namely:

* the `Match ID` generated by PlayFab's matchmaker;
* a PlayFab `Title Secret Key`, which can be found in your title settings;
* a `list` of each player's `Master Player Account Id`.

These variables will be used to update the players' PlayFab `Player Data` with the information needed to connect to the server. Keep track of the keys you use to set these variables; in this sample, they are `"PLAYFAB_MATCH_ID"`, `"PLAYFAB_TITLE_SECRET"`, and `"PLAYFAB_MATCH_PLAYERS_ID_LIST"`.

{% hint style="info" %}
The simplest way to create this function is to deploy a new revision in the `Revisions (Legacy)` tab, adding the following code to it. Make sure to update the `Edgegap_AppName`, `Edgegap_AppVersion`, `Edgegap_ApiToken` and `PlayFab_TitleSecret` variables at the top with your own values first.

The app name and version will be used later on when containerizing the game server on Edgegap.

The list of player IDs is stored here as a single string, with each ID separated by a comma.
{% endhint %}

{% code expandable="true" %}

```javascript
var Edgegap_AppName = "APP_NAME";
var Edgegap_AppVersion = "APP_VERSION";
var Edgegap_ApiToken = "token XXXX"; //include the "token" keyword
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" %}
This Cloud Script function creates an interface between the game client and the Edgegap API, which keeps your `Edgegap API token` and `PlayFab Title Secret` hidden from the players.
{% endhint %}

### Automation Rule

Still under `Automation`, navigate to the `Rules` tab. Create a new rule with the following settings:

* Set the `Event Type` to `playfab.matchmaking.match_found`;
* Add a new `Action`:
  * Set the `Type` to `Execute Entity Cloud Script`;
  * Set the `Cloud Script Function` to `StartEdgegapDeployment`.

<figure><img src="https://3027732442-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>

### Matchmaker Queue & Players

In the `Multiplayer` tab, under `Matchmaking`, make sure to have a queue available, otherwise create a new one. For quick testing, set it up to simply match any two players together.

You will need enough players registered to your Playfab title to satisfy the matchmaker's rules, so in this case only two will do. You can create new ones under the `Players` tab if needed. For this sample project, make sure to keep track of those players' `Custom ID` and `Title Player Account ID` for testing later.

### Editing The Game

#### Server

After opening the game sample in the Unity editor, we added a new empty gameObject to the `BattleScene` found under `Assets/SpaceEdge/Scenes`. We then attached a new C# script called `PlayerDataUpdateService` to it. It contains a callback function to be executed once the server is ready.

We also created a second script called `PlayfabRestApiService`, which contains the various requests that need to be sent to PlayFab's different APIs.

In `PlayerDataUpdateService.cs`, we retrieve a few of the environment variables injected in the deployment:

* Custom variables:
  * `PLAYFAB_MATCH_ID`;
  * `PLAYFAB_TITLE_SECRET`;
  * `PLAYFAB_MATCH_PLAYERS_ID_LIST`
* Default [Edgegap variables](/learn/orchestration/deployments.md#injected-environment-variables):
  * `ARBITRIUM_PUBLIC_IP`;
  * `ARBITRIUM_PORT_GAMEPORT_EXTERNAL`;

Using the `PLAYFAB_TITLE_SECRET` as authentification, we send a request to the PlayFab Server API to update the `Player Data` of each corresponding ID in `PLAYFAB_MATCH_PLAYERS_ID_LIST` with the following key-value pair:

* Key: `PLAYFAB_MATCH_ID`;
* Value: `"{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("Port Mapping Unavailable");
        }
    }

    ...
}
```

{% 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($"Could Not Update PlayerData of ID {playerID}, Error {(int)_request.StatusCode} With Message: \n{_response}");
        }
    }
}
```

{% endcode %}

#### Client

The client-side setup will be managed in the `MainMenu` scene found under `Assets/SpaceEdge/Scenes`. We created a new script called `MainMenuService` to manage the UI of the scene and display various messages to the player, and added new functions to `PlayfabRestApiService.cs`. We attached the `PlayfabRestApiService` script to a new gameObject in this scene as well.

The first thing the player needs to do is login with PlayFab, which will allow them to send additional requests to PlayFab's Client and Multiplayer APIs.

{% hint style="info" %}
We use the `LoginWithCustomID` endpoint for simplicity, however there are more suitable ways to implement the login depending on the situation (e.g.: with mobile games).
{% endhint %}

One the player has logged in and both the `X-Authorization` and `X-EntityToken` headers have been properly set up for later requests, they can create a new matchmaking ticket using the `Start Game` UI button. This will send a request to PlayFab's multiplayer API and store the `ticket ID` for later use.

Afterwards, the game client will periodically check the ticket's status every few seconds until a match has been found or the ticket gets cancelled. Once a match is found, the `match ID` gets stored as a variable.

The client will then periodically send requests to PlayFab's Client API in order to get the player's Title `Player Data`, using the `match ID` as the key for the specific data that needs to be retrieved. Once the data is found, it sets the server's `address` and external `port` value in the netcode's `transport` component, then connects to the server.

Once the client gets connected and started properly, a request gets sent to remove the connection information from that player's PlayFab `Player Data` thanks to a callback function.

{% hint style="info" %}
It is important to make sure these key-value pairs get deleted, preferably before the deployment gets terminated. This is to avoid cluttering the `Player Data` with too many unnecessary entries.
{% 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("Login Successful", false);
        }
        else
        {
            OnStatusUpdate?.Invoke($"Unable To Login Player, Error {(int)_request.StatusCode} With Message: \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($"Ticket Created, ID #{_ticketID}", false);

            GetMatchIdAttempt();
        }
        else
        {
            OnUpdateButton?.Invoke(true);
            OnStatusUpdate?.Invoke($"Unable To Create Ticket, Error {(int)_request.StatusCode} With Message: \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($"Ticket Has Been Matched, ID #{_matchID}", false);

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

                    OnUpdateButton?.Invoke(true);
                    OnStatusUpdate?.Invoke($"Ticket Has Been Cancelled Due To {reason}", false);
                }
                else
                {
                    OnStatusUpdate?.Invoke("Match Not Found, Retrying In 10 Seconds...", false);
                    await Task.Delay(10000);
                }

            }
            else
            {
                isMatchFound = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"Could Not Get Ticket Info, Error {(int)_request.StatusCode} With Message: \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($"Connecting To Server Using Port {serverPort} And IP {serverAddress}");
                        InstanceFinder.ClientManager.StartConnection();
                    }
                    else
                    {
                        OnUpdateButton?.Invoke(true);
                        OnStatusUpdate?.Invoke($"Error While Parsing Server Port Value", true);
                    }
                }
                else
                {
                    OnStatusUpdate?.Invoke("No Connection Data Found, Retrying In 10 Seconds...", false);
                    await Task.Delay(10000);
                }
            }
            else
            {
                gotConnectionInfo = true;
                OnUpdateButton?.Invoke(true);
                OnStatusUpdate?.Invoke($"Could Not Get Player Data, Error {(int)_request.StatusCode} With Message: \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($"Could Not Update PlayerData, Error {(int)_request.StatusCode} With Message: \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 %}

### Building Game Server & Containerizing

Repeat the build steps now with the added functionality - [Unity](/unity.md) / [Unreal Engine](/unreal-engine.md) / [Godot](/godot.md).

### Testing

If using FishNet - Make sure to first disable the `Start On Headless` option in the `NetworkManager`.

Set the `PlayfabRestApiService` script with the `Custom ID` and `Title ID` of your first player, along with your game's `Title ID` and `Queue Name`. In the `Build Settings`, create a new client build with these settings.

Once the build is complete, change the `Custom ID` and `Title ID` values to that of your second player. You can create a separate client build if you want, although the Unity editor in play mode should work just as well.

If both clients login properly on launch, click on both instance's `Start Game` button. After a short while, they will be matched through PlayFab's matchmaker, then connect to the same deployment on Edgegap once the server has finished setting up.
