Once ready with your game, head to the Build screen of the Unity Editor, under File -> Build Settings in the top menus. Make sure to select the right presets depending on your version of Unity.
Prior to version 2021.2:
Set Target Platform to Linux;
Set Architecture to x86_64;
Check the Server Build option.
Otherwise:
Set Platform to Dedicated Server;
Set Target Platform to Linux.
Then press build and select a new empty folder named linux_server as the file destination. Transfer linux_server folder to a second empty folder, which will be refered as the [SERVER BUILD] folder in this document.
Containerizing the dedicated game server
We will create a docker image containing the dedicated game server in this part. You might also be interested in reading Unity Server in Docker.
If you need more informations about Docker with Edgegap, please refer to this documentation.
Take note of the port you use for network communications, referred as the [GAME PORT]. By default, the port used is 7777. You can find this information in the Unity Editor, on the NetworkManager game object, in the Transport component.
Copy the above lines and paste them in your Dockerfile, placed inside [SERVER BUILD]. Modify the [GAME PORT] placeholders to your game port.
Having the [GAME PORT] opened on both TCP and UDP allow you to use any transport you prefer in the NetworkManager Mirror component. Finally, create a file named boot.sh at the root of the [SERVER BUILD] folder. It will be executed when starting the image in a container.
Copy the following two lines, make sure to replace the [YOUR GAME] placeholders with the name of the generated file.
Start a command prompt in the [SERVER BUILD] folder, and run the following Docker commands:
For ARM CPU (Mac M1, M2, etc.) users, see the dedicated page.
Using Linux
# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>
# 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_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
Using cmd
# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>
# 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_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
Using Powershell
# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>
# 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_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
After these commands, you should be able to see your uploaded image on the Edgegap website if you are using the Edgegap Container Registry. See this doc if you want to use the Edgegap registry. You can also use another private registry.
Deploying to Edgegap
Navigate to the Applications & Games page of the website. Click on the Create New button in the top right hand corner to access the application form. Here are the fields and how to fill them properly:
Application name : Can be any notable name you want to use to easily recognize your application among others.
Image : Can be any specific image you want to use to easily recognize your application among others.
Version name : You may want to use a version name to describe the scope of the version you are deploying. 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 :
Click the + Add port link to add a new port, and add the following entries :
[GAME PORT] - TCP/UDP - disable Verifications
3389 - TCP - disable Verifications
Once your application has been created, you can press the Deploy button to proceed with deploying your game server. Choose the region you want to deploy in, and enter the number of random players you want to generate depending on your game. Check that everything is running smoothly by verifying the following:
Latest Status should be set to Ready.
In the Port Mapping tab, you should be seeing the port you set in the application creation form:
Add sample HUD in your client application
Set the Port value of the Transport component of the NetworkManager to the external port defined in the Port Mapping tab of your deployment.
In this example, the port was set to 31887. This primarily depends on the game you are developing and will most likely be set programmatically in the game’s codebase.
Set the value of Network Address of the Network Manager to your deployment's Host. This URL can be found in the Deployment Summary on the dashboard or with the API.
In this example, the address was set to 0ace560706a5.pr.edgegap.net. Again, this value will most likely be set programmatically during the client’s communication with the master server/API responsible for the matchmaking process.
With the correct information, you should be able to connect to the game server normally and be able to start playing right away.
You now have a Mirror project available to deploy on demand!
With seat-based deployments, it's possible to use Mirror to create a system that automatically removes hanging Edgegap sessions once a player disconnects from the server, using NetworkManager callback functions, and a NetworkBehaviour script attached to the player prefab that uses a Remote Procedure Call (RPC) function and a Command function.
When the server starts, the NetworkManager retrieves the list of session IDs linked to its deployment from the Edgegap API and stores it. Afterwards, when a new player connects to the server, a client-side function is initiated via RPC that will send the player's IP address back to the server with a command. With the player's IP, the server checks for a matching IP in each session's data; The server gets the session data using its cached ID with the Edgegap API. If a match is found, the session ID is mapped to that player's NetworkConnectionToClient.
Since new sessions can be added after the server starts, the list of session IDs is updated and the new sessions are checked if a match can't be found the first time around.
Finally, once a player disconnects from the server, the server uses that player's NetworkConnectionToClient to retrieve their associated session ID, then uses the Edgegap API to delete that session. This frees a socket in the deployment for a new player to join.
PlayerNetworkBehaviour
public class PlayerNetworkBehaviour : NetworkBehaviour
{
[ClientRpc]
public void RpcSendClientIpAddress()
{
StartCoroutine(FetchPublicIpAndSendToServer());
}
private IEnumerator FetchPublicIpAndSendToServer(" alt=""><figcaption></figcaption></figure>
{
UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
string clientIp = JObject.Parse(responseText)["ip"].ToString();
CmdSendIpAddressToServer(clientIp);
}
else
{
Debug.LogError("Failed to fetch public IP address.");
}
}
[Command]
public void CmdSendIpAddressToServer(string clientIp)
{
NetworkConnectionToClient conn = connectionToClient;
if (conn != null)
{
CustomNetworkManager.Instance.StoreClientIpAddress(conn, clientIp);
}
else
{
Debug.LogError("Connection not found.");
}
}
}
CustomNetworkManager
public class CustomNetworkManager : NetworkManager
{
private static readonly string edgegapUrl = "https://api.edgegap.com/v1";
private string apiToken;
private string deploymentId;
private Dictionary<NetworkConnectionToClient, string> connectionToSessionIdMap = new Dictionary<NetworkConnectionToClient, string>();
private Dictionary<NetworkConnectionToClient, bool> mappingInProgress = new Dictionary<NetworkConnectionToClient, bool>();
private ArrayList currentDeploymentSessions = new();
private ArrayList newSessions = new();
public static CustomNetworkManager Instance;
public override void Awake()
{
base.Awake();
Instance = this;
}
public override void OnStartServer()
{
base.OnStartServer();
deploymentId = System.Environment.GetEnvironmentVariable("ARBITRIUM_REQUEST_ID");
apiToken = System.Environment.GetEnvironmentVariable("API_TOKEN");
if (string.IsNullOrEmpty(apiToken))
{
Debug.LogError("Edgegap API token not found in environment variables.");
}
StartCoroutine(GetNewSessionList());
}
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
base.OnServerAddPlayer(conn);
Debug.Log($"Adding player for connection: {conn.connectionId}");
// Get the PlayerNetworkBehaviour from the player object
PlayerNetworkBehaviour playerNetworkBehaviour = conn.identity.GetComponent<PlayerNetworkBehaviour>();
playerNetworkBehaviour.RpcSendClientIpAddress();
}
public void StoreClientIpAddress(NetworkConnectionToClient conn, string clientIp)
{
mappingInProgress[conn] = true; // Set mapping in progress to true
Debug.Log($"Stored IP address for connection {conn.connectionId}: {clientIp}");
// Start mapping the session to the connection
StartCoroutine(CheckCachedSessions(conn, clientIp));
}
private IEnumerator GetNewSessionList()
{
newSessions.Clear();
string url = $"{edgegapUrl}/status/{deploymentId}";
UnityWebRequest request = UnityWebRequest.Get(url);
request.SetRequestHeader("Authorization", apiToken);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
Debug.Log("Deployment Status Response: " + responseText);
var json = JObject.Parse(responseText);
var sessions = json["sessions"] as JArray;
foreach (var session in sessions)
{
string sessionId = session["session_id"]?.ToString();
if (!currentDeploymentSessions.Contains(sessionId))
{
newSessions.Add(sessionId);
currentDeploymentSessions.Add(sessionId);
}
}
Debug.Log("New session list initialized");
}
}
private IEnumerator CheckCachedSessions(NetworkConnectionToClient conn, string clientAddress)
{
foreach (string sessionId in currentDeploymentSessions)
{
yield return GetSessionAndMap(conn, sessionId, clientAddress);
if (connectionToSessionIdMap.ContainsKey(conn))
{
break;
}
}
if (!connectionToSessionIdMap.ContainsKey(conn))
{
Debug.Log("Could not map player with current cached sessions");
yield return GetNewSessionList();
foreach (string sessionId in newSessions)
{
yield return GetSessionAndMap(conn, sessionId, clientAddress);
if (connectionToSessionIdMap.ContainsKey(conn))
{
break;
}
}
}
}
private IEnumerator GetSessionAndMap(NetworkConnectionToClient conn, string sessionId, string clientAddress)
{
Debug.Log($"Fetching session details for session ID: {sessionId}");
string url = $"{edgegapUrl}/session/{sessionId}";
UnityWebRequest request = UnityWebRequest.Get(url);
request.SetRequestHeader("Authorization", apiToken);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("Session fetched successfully.");
string responseText = request.downloadHandler.text;
Debug.Log("Session Response: " + responseText);
var session = JObject.Parse(responseText);
var sessionUsers = session["session_users"] as JArray;
if (sessionUsers != null && sessionUsers.Count > 0)
{
Debug.Log($"Found {sessionUsers.Count} users in session.");
foreach (var user in sessionUsers)
{
string playerIp = user["ip"]?.ToString();
Debug.Log($"Player IP: {playerIp}, Connection IP: {clientAddress}");
if (playerIp == clientAddress)
{
connectionToSessionIdMap[conn] = sessionId;
Debug.Log($"Mapped session ID {sessionId} to connection {conn.connectionId} with IP {playerIp}");
// Additional confirmation logging
if (connectionToSessionIdMap.ContainsKey(conn))
{
Debug.Log($"Session ID {sessionId} successfully stored for connection {conn.connectionId} with IP {playerIp}");
}
else
{
Debug.LogError($"Failed to store session ID {sessionId} for connection {conn.connectionId} with IP {playerIp}");
}
break;
}
}
}
else
{
Debug.LogError("No users found in session.");
}
}
else
{
Debug.LogError($"Error fetching session: {request.error}");
Debug.LogError($"Response Code: {request.responseCode}");
Debug.LogError($"Response Text: {request.downloadHandler.text}");
}
mappingInProgress[conn] = false; // Set mapping in progress to false once done
}
public override void OnServerDisconnect(NetworkConnectionToClient conn)
{
base.OnServerDisconnect(conn);
Debug.Log($"Server disconnected client: {conn.connectionId}");
StartCoroutine(HandleDisconnect(conn));
}
private IEnumerator HandleDisconnect(NetworkConnectionToClient conn)
{
while (mappingInProgress.ContainsKey(conn) && mappingInProgress[conn])
{
Debug.Log($"Waiting for session mapping to complete for connection {conn.connectionId}...");
yield return new WaitForSeconds(0.1f);
}
if (connectionToSessionIdMap.TryGetValue(conn, out string sessionId))
{
Debug.Log($"Deleting session ID: {sessionId}");
StartCoroutine(DeleteSession(sessionId));
connectionToSessionIdMap.Remove(conn);
mappingInProgress.Remove(conn);
currentDeploymentSessions.Remove(sessionId);
}
else
{
Debug.LogWarning("No session ID found for this connection.");
}
}
private IEnumerator DeleteSession(string sessionId)
{
Debug.Log($"Sending request to delete session ID: {sessionId}");
string url = $"{edgegapUrl}/session/{sessionId}";
UnityWebRequest request = UnityWebRequest.Delete(url);
request.SetRequestHeader("Authorization", apiToken);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("Session deleted successfully.");
}
else
{
Debug.LogError($"Error deleting session: {request.error}");
Debug.LogError($"Response Code: {request.responseCode}");
Debug.LogError($"Response Text: {request.downloadHandler.text}");
}
}
}
Optional features
This script can be expanded upon with optional, independent features that help manage the sessions in specific cases.
For example, a timeout can be set to remove empty seat sessions after a configurable amount of time following full server initialization. This is a useful feature in the case of a player that quits before their matchmaker ticket gets resolved, which would create an empty session once a match is found. If the player never sends their IP address to be mapped to the session ID before the timeout resolves, then that session gets deleted to free up a socket.
Another feature can be to disconnect inactive players after a prolonged period of time in order to free up sockets. Once connected to the server, the client needs to send some minimalistic heartbeat message to the server every few seconds, or otherwise gets disconnected if too many heartbeats are missed in a row. Both the amount of time between heartbeats and the maximum number of messages that can be missed in a row can be configured, depending on the game's needs. In this sample, a heartbeat gets sent every few seconds if the tank is moving around the map or shooting.