LogoLogo
edgegap.comDashboard
  • 📘Learning Center
    • Getting Started
    • Unity Games
      • Getting Started - Servers
      • Developer Tools
    • Unreal Engine Games
      • Getting Started - Servers
      • Developer Tools
    • Matchmaking
      • Getting Started
      • Matchmaker In-Depth
      • Ping Beacons
    • Advanced Features
      • Apps and Versions
      • Deployments
      • Managed Clusters
  • API Reference
    • Dedicated Servers
    • Integration
    • Matchmaking
    • Peer to Peer
  • Release Notes
  • 📚Documentation
    • Sample Projects
      • Unity Netcodes
        • Unity NGO
        • Photon Fusion 1
        • Photon Fusion 2
        • Mirror
        • Mirror WebGL
        • Fishnet
        • Fishnet WebGL
        • Photon Bolt
      • Unreal Top-Down Sample
      • NuxtJS
      • Ruby On Rails
      • Unity Lobbies
      • Unity Matchmaker
    • Tools & Integrations
      • Container
        • What is Docker
        • Your First Docker
        • The Good Practices
        • SSH in Your Container
        • External Registries
          • Docker Hub
          • AWS ECR
          • GCP GCR
          • Gitlab registry
      • Deploy from Nakama
      • EOS Lobby Integration
      • Switch From Gamelift
      • Switch From Multiplay
      • Playfab Bridge
    • Deployment
      • Endpoint Storage
        • How to Save Logs
        • Upload File to Deployment
      • Webhooks
    • Application
      • Command and Arguments
      • 1:1 Port Mapping
    • Session
      • How They Work
      • Application Version Configuration
      • Manage Request
      • Selectors Filtering
    • Fleet
      • Fleet Policy
      • Policy Filter
      • Linking Version
      • Fleet's Deployment
    • Container Registry
    • Distributed Relay
      • Matchmaker/Lobby
      • Relay Edgegap API
      • Transport Samples
    • Lobby
      • Lobby Service
      • Functions
    • Glossary
    • SLA Terms
Powered by GitBook
LogoLogo

Connect with Community

  • Discord
  • Linkedin
  • X

Read More

  • Release Notes
  • Blog
  • Enterprise
  • Legal
  • edgegap.com

© 2025 Edgegap

On this page
  • Setting Up PlayFab Cloud Script
  • Automation Rule
  • Matchmaker Queue & Players
  • Editing The Game
  • Building Game Server & Containerizing
  • Testing

Was this helpful?

  1. Documentation
  2. Tools & Integrations

Playfab Bridge

PreviousSwitch From MultiplayNextDeployment

Last updated 2 months ago

Was this helpful?

This guide will show you how to set up a game with PlayFab so as to automatically deploy a server on Edgegap after players have been matched through the PlayFab matchmaker. The clients will access the information to connect to the server via the PlayFab Player Data, which will be updated by the server once it's ready. Before getting started, we expect that:

  • You currently use Playfab;

  • You have already and an application with Edgegap before.

You can find the final sample project on our . This sample was tested using Unity v2021.3.10 and Fishnet v3.11.14 as its netcode.

Setting Up PlayFab Cloud Script

Create New Function

Log on 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".

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.

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

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.

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.

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:

    • ARBITRIUM_PUBLIC_IP;

    • ARBITRIUM_PORTS_MAPPING;

In ARBITRIUM_PORTS_MAPPING, we need to retrieve the specific external port value that will allow the client to connect to the server; in this sample, it is the one named "Game Port".

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}:{port}";

PlayerDataUpdateService

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

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

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.

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).

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.

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.

MainMenuService

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

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

    ...
}

PlayerDataUpdateService

public class PlayerDataUpdateService : NetworkBehaviour
{
    private PlayfabRestApiService _playfabService;

    ...

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

Building Game Server & Containerizing

Before containerizing the server build, make sure to enable the Start On Headless option in the NetworkManager gameObject.

Open the Edgegap plugin with the Tools/Edgegap Hosting toolbar menu. Verify your Edgegap API Token and either create or load an application for the game. Make sure the port value matches that of the Tugboat component of the NetworkManager gameObject. Select the UDP protocol, then enter a New Version Tag. Make sure that both the app name and version match the values that were used when setting up the Cloud Script.

Once this is properly set up, click on Build and Push, which will automatically containerize your game server and create a new application version on the Edgegap dashboard after a short waiting period.

When creating the app version, the plugin will name the port `"Game Port"` by default.

If you choose to manually containerize and create your app version instead of using the plugin, make sure to use that name when selecting which port to use.

Testing

Make sure to first disable the Start On Headless option in the NetworkManager gameObject. 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.

📚
created
deployed
GitHub