Skip to main content

Photon Fusion 2 Netcode

Photon Fusion 2 on Arbitrium

This guide will help you create a headless server on Edgegap for a Unity project using Photon Fusion 2 as its networking solution. You will need an account with both Edgegap and Photon for this.

We will use the sample project Asteroid Simple (Host) from Photon Fusion. This guide will walk you throught the changes made to this sample to make it work with Arbitrium. You can find the final version of the source code on Edgegap Asteroids Sample GitHub.

There are two plugins included in the project to help with development: Newtonsoft Json for better Json serialization, and ParrelSync for easier testing with multiple unity editors. We also included a EdgegapHelper folder with some scripts for managing calls to the Edgegap API.

Application setup

With your Photon account, you will need to make a Fusion 2 app on the Photon dashboard. Set the app id in the PhotonAppSettings scriptable object file that can be found in the sample project under Assets/Photon/Fusion/Resources.

You will also to get an API token on the Edgegap dashboard, and later on create an application for your server there as well. Think of an application name and app version for it, and use these values along with your API token in the EdgegapConfig scriptable object file of the project, which can be found under Assets/ScriptableObjects. Make sure to keep the token keyword for the API token.

Editing the game

For this sample to work with Edgegap, some slight changes needed to be added to the base project, mainly to the StartMenu.cs script found under Assets/Asteroids-Host-Simple/Menu. This mostly involves automatically starting the match with the right mode when the game is launched as a server. We added an EdgegapManager game object to the main scene, linking the EdgegapConfig scriptable object to it.

StartMenu.cs - Autostart Server Build edit
public class StartMenu : MonoBehaviour
{
...
//You can use the value of your choice here
private ushort serverPort = 5050;
...

private void Start()
{
if (EdgegapManager.IsServer())
{
var getPortAndStartServer = EdgegapAPIInterface.GetPublicIpAndPortFromServer((ip, port) =>
{
var serverAddress = NetAddress.CreateFromIpPort(ip, port);
StartGame(GameMode.Server, EdgegapManager.EdgegapRoomCode, _gameSceneName, serverAddress);
});

StartCoroutine(getPortAndStartServer);
}
}

...

private async void StartGame(GameMode mode, string roomName, string sceneName, NetAddress? serverAddress = null)
{
...

var startGameArgs = new StartGameArgs()
{
GameMode = mode,
SessionName = roomName,
ObjectProvider = _runnerInstance.GetComponent<NetworkObjectPoolDefault>(),
};

if (mode == GameMode.Server && serverAddress != null)
{
Debug.Log("Using specific address " + serverAddress);
startGameArgs.Address = NetAddress.Any(serverPort);
startGameArgs.CustomPublicAddress = serverAddress;
}

// GameMode.Host = Start a session with a specific name
// GameMode.Client = Join a session with a specific name
var result = await _runnerInstance.StartGame(startGameArgs);

...
}
}

As for the client, we added a new button to the main menu scene that attemps to connect to a server hosted on Edgegap with a given room name; If none exists, the game will deploy a new server with the given room name, wait for it to be ready, then connect the player to it. We included a new UI text display to show the player the current state of the connection process.

For Edgegap to select the optimal server location, the player needs to provide their IP address in the deployment request. We added a new variable for it in the PlayerData class.

note

Ideally, we would include some sort of matchmaking or lobby system before sending the deployment request. This is so that we can use the IP address of every player that will participate in the match to find the best deployment location to include all of them.

As this is a simple game example, we skipped this process; Only the player that initiates the server deployment request provides their IP address. Other players can still join the game after it has been deployed, but the server's location might not be optimal for them.

Additionally, we need to edit the Onshutdown callback function of the OnServerDisconnected to not reload the menu scene when using the Edgegap connection option so that the updated StartMenu can work properly.

StartMenu.cs - Start Edgegap Client Option edit
    public class StartMenu : MonoBehaviour
{
...
private PlayerData playerData;
[SerializeField] private TextMeshProUGUI _EdgegapConnectStatus = null;

private bool tryJoinEdgegap = false;
private bool startDeploy = false;
...

private void Start()
{
_EdgegapConnectStatus.text = "";
...
}

private void Update()
{
if (EdgegapManager.EdgegapPreServerMode && EdgegapManager.TransferingToEdgegapServer)
{
// launch game again with edgegap server room code
Debug.Log("Transferring to Edgegap");
_EdgegapConnectStatus.text = "Deployment ready, connecting...";

EdgegapManager.TransferingToEdgegapServer = false;
EdgegapManager.EdgegapPreServerMode = false;
startDeploy = false;

var launchAfterDelay = RunAfterTime(0.2f, () => StartGame(GameMode.Client, EdgegapManager.EdgegapRoomCode, _gameSceneName));
StartCoroutine(launchAfterDelay);
}
else if(tryJoinEdgegap)
{
tryJoinEdgegap = false;

Debug.Log($"checking for room: {_roomName.text}");
_EdgegapConnectStatus.text = $"Attempting to connect to room {_roomName.text} with Edgegap...";

StartGame(GameMode.Client, _roomName.text, _gameSceneName);
}
else if (startDeploy && playerData.GetIpAddress() is not null)
{
startDeploy = false;

Debug.Log("Initiating deployment");
_EdgegapConnectStatus.text = $"Room {_roomName.text} not found, deploying Edgegap server...";

string[] ips = { playerData.GetIpAddress() };
StartCoroutine(EdgegapManager.Instance.Deploy(_roomName.text, ips, OnEdgegapServerReady));
}
}

...

private void SetPlayerData()
{
playerData = FindObjectOfType<PlayerData>();
if (playerData == null)
{
playerData = Instantiate(_playerDataPrefab);
}

if (string.IsNullOrWhiteSpace(_nickName.text))
{
playerData.SetNickName(_nickNamePlaceholder.text);
}
else
{
playerData.SetNickName(_nickName.text);
}

if (EdgegapManager.EdgegapPreServerMode)
{
var crtn = EdgegapManager.Instance.GetPublicIpAddress(ip => playerData.SetIpAddress(ip));
StartCoroutine(crtn);
}
}

private async void StartGame(GameMode mode, string roomName, string sceneName, NetAddress? serverAddress = null)
{
...
var result = await _runnerInstance.StartGame(startGameArgs);

if (!result.Ok)
{
startDeploy = true;
}
else
{
startDeploy = false;
EdgegapManager.EdgegapPreServerMode = false;

if (_runnerInstance.IsServer)
{
await _runnerInstance.LoadScene(sceneName);
}
}
}

public void StartEdgegap()
{
EdgegapManager.EdgegapPreServerMode = true;
SetPlayerData();
tryJoinEdgegap = true;
}

IEnumerator RunAfterTime(float timeInSeconds, Action action)
{
yield return new WaitForSeconds(timeInSeconds);
action();
}

public void OnEdgegapServerReady(string roomCode)
{
EdgegapManager.EdgegapRoomCode = roomCode;
EdgegapManager.TransferingToEdgegapServer = true;
}
}

Build the game server & Containerizing

note

To facilitate the containerization and app creation process, it's possible to use the latest version of the Edgegap Unity Plugin on our GitHub. For more information on how to use this plugin, you can check our documentation here.

If you want instead, you may also follow these step-by-step instructions.

Once the game has been edited, you are ready to create the server build. Make sure to have the Linux Dedicated Server Build Support module installed with your version of Unity, then head to the Build screen of the Unity Editor, under File -> Build Settings in the top menus. Set the Platform to Dedicated Server and the Target Platform to Linux, then press build.

Select a new empty folder named linux_server as the file destination. Transfer the linux_server folder to a second empty folder, which will be refered as the [SERVER BUILD] folder in this document. Add the following Dockerfile and boot.sh file to the [SERVER BUILD] folder:

Dockerfile

FROM ubuntu:bionic
LABEL author_detail="<author_detail>"

ARG debian_frontend=noninteractive
ARG docker_version=17.06.0-ce

RUN apt-get update && \
apt-get install -y libglu1 xvfb libxcursor1 ca-certificates && \
apt-get clean && \
update-ca-certificates

EXPOSE [SERVER_PORT]/UDP

COPY linux_server/ /root/linux_server/
COPY boot.sh /boot.sh

WORKDIR /root/
ENTRYPOINT ["/bin/bash", "/boot.sh"]

boot.sh

xvfb-run --auto-servernum --server-args='-screen 0 640X480X24:32' /root/linux_server/[GAME_NAME].x86_64 -batchmode -nographics

Make sure to replace the [SERVER_PORT] and [GAME_NAME] placeholder with the serverPort value used in StartMenu.cs and the name of the generated file respectively.

Then, start a command prompt in the [SERVER BUILD] folder; Run the following Docker commands to create an image of your build and push it to a private registry:

# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_TAG>

# login, a prompt will ask the password
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# add another tag to your image corresponding to the registry
docker image tag <IMAGE_NAME>:<IMAGE_TAG> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

Creating application on Edgegap

We will now create the previously mentionned application on the Edgegap dashboard. Use the following settings:

  • Application name : Make sure that it's the same name that you used for the App Name in the EdgegapConfig of the game sample.
  • Image : Can be any specific picture you want to use to easily recognize your application among others.
  • Version name : Make sure that it's the same name that you used for the Version in the EdgegapConfig of the game sample. Examples may be “demo”, “production”, “v1”, “v2”
  • Container :
    • Registry : [URL], where [URL] is the value from the credentials you can display on the Container Repository page.
    • Image repository : [PROJECT]/[YOUR GAME], where [PROJECT] and [YOUR GAME] are the values you used earlier when pushing the docker image.
    • Tag : [TAG], where [TAG] is the value you used earlier when pushing the docker image.
    • Tick “Using a private repository”
    • Private registry username : [USERNAME], where [USERNAME] is the value from your credentials.
    • Private registry token : [TOKEN], where [TOKEN] is the value from your credentials.
    • Requirements : Left as is.
    • Ports :
      • [SERVER_PORT] - UDP - disable Verifications, where [SERVER_PORT] is the same value as in the project and dockerfile.

Testing the client

To check that everything works properly, launch the game in the Unity editor or by creating a new client build; Enter a room name, then press on the Start Edgegap button. The game should connect to a server on Edgegap after a short while, either by automatically deploying a new one or joining an existing server if the room name matches the one entered.

You can also try joining the same room with a different client instance with no trouble by using ParrelSync.