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.
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.
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.
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
- PlayerData.cs
- OnServerDisconnected.cs
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;
}
}
}
public class PlayerData : MonoBehaviour
{
private string _nickName = null;
private string ipAddress = null;
...
public static string GetRandomNickName()
{
var rngPlayerNumber = Random.Range(0, 9999);
return $"player{rngPlayerNumber.ToString("0000")}";
}
...
public void SetIpAddress(string ip)
{
ipAddress = ip;
}
public string GetIpAddress()
{
return ipAddress;
}
}
public class OnServerDisconnected : MonoBehaviour, INetworkRunnerCallbacks
{
[SerializeField] private string _menuSceneName = String.Empty;
public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason)
{
// When the local NetworkRunner has shut down, the menu scene is loaded.
if (!EdgegapManager.EdgegapPreServerMode || SceneManager.GetActiveScene().name != _menuSceneName)
{
SceneManager.LoadScene(_menuSceneName);
}
}
...
}
These changes allow us to use Edgegap to host game servers made with Photon Fusion 2.