This page is a work in progress and could be pending updates.

Fusion Headless Mode

This document describes a set of Utility scripts that can be used to start Photon Fusion from a Headless Unity Application. There is nothing special when starting Fusion without a graphical interface and these script are just one way of doing so.

HeadlessController Script

The following script takes care of instantiating and starting a new Fusion Runner in Server Game Mode. It will use the HeadlessUtils script (see below) in order to check if the binary was started in "Headless Mode" and also to verify which Scene should be loaded. If anything fails while initializing the Game Server, the application will quit with a non-zero exit code, useful for checking from an external application.

using UnityEngine;
using Fusion;
using System.Collections.Generic;
using Fusion.Sockets;
using UnityEngine.SceneManagement;

public class HeadlessController : MonoBehaviour, INetworkRunnerCallbacks {

  [SerializeField]
  private NetworkRunner _runnerPrefab;

  void Awake() {
    Application.targetFrameRate = 30;
  }

  async void Start() {

    // Quit if not in Headless mode
    if (HeadlessUtils.IsHeadlessMode() == false) {
      Application.Quit(1);
    }

    // Get game scene to be loaded
    var sceneToLoad = HeadlessUtils.GetArg("-scene") ?? SceneManager.GetActiveScene().name;

    Debug.Log($"Starting Server");

    // Create a new Fusion Runner  
    var runner = Instantiate(_runnerPrefab);

    // Basic Setup
    runner.name = $"DedicatedServer";
    runner.AddCallbacks(this); // register callbacks

    // Start the Server
    var result = await runner.StartGame(new StartGameArgs() {
      GameMode = GameMode.Server, // for dedicated servers,
      Scene = SceneManager.GetSceneByName(sceneToLoad).buildIndex,
      SceneObjectProvider = gameObject.AddComponent<NetworkSceneManagerDefault>()
    });

    // Check if all went fine
    if (result.Ok) {
      Debug.Log($"Runner Start DONE");
    } else {
      // Quit the application if startup fails

      Debug.LogError($"Error while starting Server: {result.ShutdownReason}");

      // it can be used any error code that can be read by an external application
      // using 0 means all went fine
      Application.Quit(1);
    }
  }

  // Fusion INetworkRunnerCallbacks implementation

  public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) {
    Debug.LogWarning($"{nameof(OnShutdown)}: {nameof(shutdownReason)}: {shutdownReason}");

    // Quit normally
    Application.Quit(0);
  }

  public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) => Debug.LogWarning($"{nameof(OnPlayerJoined)}: {nameof(player)}: {player}");
  public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) => Debug.LogWarning($"{nameof(OnPlayerLeft)}: {nameof(player)}: {player}");
  public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) => Debug.LogWarning($"{nameof(OnConnectRequest)}: {nameof(NetworkRunnerCallbackArgs.ConnectRequest)}: {request.RemoteAddress}");
  public void OnSceneLoadDone(NetworkRunner runner) => Debug.LogWarning($"{nameof(OnSceneLoadDone)}: {nameof(runner.CurrentScene)}: {runner.CurrentScene}");
  public void OnSceneLoadStart(NetworkRunner runner) => Debug.LogWarning($"{nameof(OnSceneLoadStart)}: {nameof(runner.CurrentScene)}: {runner.CurrentScene}");
  public void OnInput(NetworkRunner runner, NetworkInput input) { }
  public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
  public void OnConnectedToServer(NetworkRunner runner) { }
  public void OnDisconnectedFromServer(NetworkRunner runner) { }
  public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
  public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
  public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }
  public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
  public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, System.ArraySegment<byte> data) { }
}

Back To Top

HeadlessUtils Script

This script only contains two methods that can be used to check if the Unity binary was started in "Headless Mode" (IsHeadlessMode) and to get the value of a specific Command Line Argument (GetArg), useful to pass custom starting settings to your headless game server.

using System;

public class HeadlessUtils {

  /// <summary>
  /// Signal if the executable was started in Headless mode by using the "-batchmode -nographics" command-line arguments
  /// <see cref="https://docs.unity3d.com/Manual/PlayerCommandLineArguments.html"/>
  /// </summary>
  /// <returns>True if in "Headless Mode", false otherwise</returns>
  public static bool IsHeadlessMode() {
    return Environment.CommandLine.Contains("-batchmode") && Environment.CommandLine.Contains("-nographics");
  }

  /// <summary>
  /// Get the value of a specific command-line argument passed when starting the executable
  /// </summary>
  /// <example>
  /// Starting the binary with: "./my-game.exe -map street -type hide-and-seek"
  /// and calling `var mapValue = HeadlessUtils.GetArg("-map", "-m")` will return the string "street"
  /// </example>
  /// <param name="keys">List of possible keys for the argument</param>
  /// <returns>The string value of the argument if the at least 1 key was found, null otherwise</returns>
  public static string GetArg(params string[] keys) {
    var args = Environment.GetCommandLineArgs();

    for (int i = 0; i < args.Length; i++) {
      foreach (var name in keys) {
        if (args[i] == name && args.Length > i + 1) {
          return args[i + 1];
        }
      }
    }

    return null;
  }
}


To Document Top