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 how to test this sample, as well as the general changes made to it to make it work with Edgegap.

You can find the final version of the source code on the 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.

Testing the sample

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 need to get an API token on the Edgegap dashboard, and later on create an application for your server there as well. In the EdgegapConfig scriptable object file of the project, which can be found under Assets/ScriptableObjects, enter the App Name and Version that you will be using when creating your Edgegap application, as well as your API token. Make sure to include the token keyword.

Optionally, you can update the serverPort variable in the StartMenu.cs script with the value of your choice. This script can be found under Assets/Asteroids-Host-Simple/Menu. The default value is 5050.

Build the game server & Containerizing

The game server app version can be created quickly using the Edgegap Plugin included in the project. For more information on how to use this plugin, you can check our documentation here.

First, make sure that Docker is running on your computer and that the Linux Build Support (Mono) and Linux Dedicated Server Build Support modules are installed with your Unity version.

In the project, make sure that both scenes in the Assets/Asteroids-Host-Simple/Scenes folder are included in the build settings.

Then, open the Edgegap Plugin with the Edgegap/Edgegap Hosting toolbar option. Verify your API token. Using the same App Name as in the EdgegapConfig, click on Create Application or Load Existing App if it already exists on the Edgegap dashboard. Enter the same Version Tag as before, and the port value used in the StartMenu.cs script. Select the UDP protocol type, then click on Build and Push.

Launching the game

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 Photon Fusion room name matches the one entered.

You can have multiple players can join the same room with no trouble afterwards, using the ParrelSync plugin or a different instance of the build.

warning

Trying to have 2 game clients attempt to join a room that doesnt exists with the same name at the same time will create 2 separate deployments

This is because of the fact that this sample does not include a matchmaking system for simplicity, as well as the short delay it takes for the server to set up the Photon Fusion room with the specified name during the deployment.

Ideally, we would include some sort of matchmaker or lobby system before sending the deployment request to prevent this. It would also let us use the IP address of every player that will participate in the match to find the best deployment location to include all of them.

How it works

For this sample to work with Edgegap, some slight changes were added to the base Photon AsteroidsSimple project, mainly to the StartMenu.cs script as well as by adding an EdgegapManager game object to the main scene and linking the EdgegapConfig scriptable object to it.

Autostart Server Mode

One of the first changes is to automatically start the Photon Fusion room in server mode when the game is launched as a deployment, using the deployment's IP address and a port value.

note

The room is created with the correct name because it was passed as an environment variable to the server when the client requested a new deployment. It is then read and stored in the EdgegapManager on launch.

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

private NetworkRunner _runnerInstance = null;

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)
{
_runnerInstance = FindObjectOfType<NetworkRunner>();
if (_runnerInstance == null)
{
_runnerInstance = Instantiate(_networkRunnerPrefab);
}

// Let the Fusion Runner know that we will be providing user input
_runnerInstance.ProvideInput = true;

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

if (!result.Ok && EdgegapManager.EdgegapPreServerMode)
{
...
}
else
{
...

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

Client in Edgegap Mode

For the client build, we added a new button in the menu scene to join a Photon Fusion room that is hosted on Edgegap, which can be clicked once a valid room name has been entered. If no room is found with that name, the client will request a new server to be deployed, passing the room's name as an environment variable and the player's IP address for optimal location. The client then waits for the server to ready the room, then joins it.

This logic is implemented in the StartMenu.cs script. We also included a UI text display in the scene to show the player the current state of the connection process.

We also added a new property to the PlayerData class to store the player's IP address, and edited the random nickname so as to not include spaces.

Lastly, we edited the OnShutdown callback function of the OnServerDisconnected.cs script to not reload the menu scene when using the Edgegap connection option so that the updated StartMenu.cs script can work properly.

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

[SerializeField] private Button _EdgegapStartBtn = null;

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

private void Start()
{
_roomName.onValueChanged.AddListener(ValidateRoomName);
_EdgegapStartBtn.interactable = false;
_nickName.onValueChanged.AddListener(value => CheckForSpecialcharacters(value, _nickName));
_EdgegapConnectStatus.text = "Please enter a room name to test with Edgegap.";
EdgegapManager.EdgegapPreServerMode = false;
waiting = false;

...
}

private void Update()
{
if (EdgegapManager.EdgegapPreServerMode && EdgegapManager.TransferingToEdgegapServer)
{
// launch game again with edgegap server room code
_EdgegapConnectStatus.text = "Deployment ready, attempting to connect...";
startDeploy = false;

var launchAfterDelay = RunAfterTime(0.5f, () => TryConnectDeployment(EdgegapManager.EdgegapRoomCode, _gameSceneName));
StartCoroutine(launchAfterDelay);
}
else if(tryJoinEdgegap)
{
tryJoinEdgegap = false;

_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;

_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 && EdgegapManager.EdgegapPreServerMode)
{
startDeploy = true;
}
else
{
startDeploy = false;

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

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

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

IEnumerator RunAfterTime(float timeInSeconds, Action action)
{
if (!waiting)
{
waiting = true;
yield return new WaitForSeconds(timeInSeconds);
action();
}
}

private async void TryConnectDeployment(string roomName, string sceneName)
{
Debug.Log("Attempting to connect...");

_runnerInstance = FindObjectOfType<NetworkRunner>();
if (_runnerInstance == null)
{
_runnerInstance = Instantiate(_networkRunnerPrefab);
}
_runnerInstance.ProvideInput = true;

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

var result = await _runnerInstance.StartGame(startGameArgs);

if (!result.Ok)
{
waiting = false;
return;
}
else
{
_EdgegapConnectStatus.text = "Game starting...";
EdgegapManager.TransferingToEdgegapServer = false;
}

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

private void ValidateRoomName(string value)
{
if (string.IsNullOrEmpty(value))
{
_EdgegapStartBtn.interactable = false;
_EdgegapConnectStatus.text = "Please enter a room name to test with Edgegap.";
}
else
{
_EdgegapStartBtn.interactable = true;
_EdgegapConnectStatus.text = "";

CheckForSpecialcharacters(value, _roomName);
}
}

private void CheckForSpecialcharacters(string value, TMP_InputField textfield)
{
string newValue = Regex.Replace(value, @"[^0-9a-zA-Z]", string.Empty);
if (value != newValue)
{
Debug.Log("Please do not use special characters in room name or player name.");
textfield.text = newValue;
}
}
}

These changes allow us to use Edgegap to host game servers made with Photon Fusion 2.