Skip to main content

Use the SDK to set up the server - Dedicated servers with AccelByte Multiplayer Servers (AMS) - (Unity module)

Last updated on November 7, 2025

Understand the AMS dedicated server flow

Take some time to look at the flow of how a game server is managed by AccelByte Multiplayer Servers (AMS). For more information, read about the AMS dedicated server states in the Integrate dedicated servers with AMS using AGS SDK guide.

Unwrap the wrapper

You are now ready to use AMS functions provided by the AccelByte Gaming Services (AGS) Game SDK.

The Byte Wars project uses a wrapper class named MultiplayerDSAMSWrapper that act as the wrapper to cache and handle AMS-related functionalities when using the AGS Game SDK.

The MultiplayerDSAMSWrapper class uses the ServerApiClient, ServerAMS, and ServerDSHub classes provided by the AccelByte Game SDK. They interact with AGS to provide the ability to register and deregister your server, receive notifications from DS Hub and AMS, and handle multiplayer features.

What's in the Starter Pack

To follow this tutorial, you use the starter wrapper class called MultiplayerDSAMSWrapper_Starter. This wrapper is defined in the file below:

  • C# file: /Assets/Resources/Modules/Play/MultiplayerDSEssentials/Scripts/MultiplayerDSAMSWrapper_Starter.cs

The MultiplayerDSAMSWrapper_Starter class has several pre-defined components below.

  • Helper variables to reference the AGS SDK interfaces. These variables are assigned when the wrapper is initialized.

    private DedicatedServer ds;
    private ServerAMS ams;
    private ServerDSHub dsHub;

    private void OnEnable()
    {
    ams = AccelByteSDK.GetServerRegistry().GetAMS(autoCreate: true, autoConnect: true);
    if (ams == null)
    {
    BytewarsLogger.LogWarning("AMS interface is invalid. This interface only support packaged server using development build.");
    return;
    }

    ds = AccelByteSDK.GetServerRegistry().GetApi().GetDedicatedServer();
    if (ds == null)
    {
    BytewarsLogger.LogWarning("Dedicated Server interface is invalid. This interface only support packaged server using development build.");
    return;
    }

    dsHub = AccelByteSDK.GetServerRegistry().GetApi().GetDsHub();
    if (dsHub == null)
    {
    BytewarsLogger.LogWarning("DSHub interface is invalid. This interface only support packaged server using development build.");
    return;
    }
    }
  • Public delegates to be invoked when receiving AMS notifications.

    public event Action OnAMSConnectionOpened = delegate { };
    public event Action OnAMSConnectionClosed = delegate { };
    public event Action OnAMSDrainSignalReceived = delegate { };
  • Helper variable and function to shutdown server.

    private readonly int ServerShutdownDelay = 10;

    private void ShutdownServer()
    {
    BytewarsLogger.Log("Shutting down server.");
    #if UNITY_EDITOR
    UnityEditor.EditorApplication.ExitPlaymode();
    #else
    Application.Quit();
    #endif
    }

Server login

Your game server needs to log in to access AMS, which you set up in the previous tutorial (Set up IAM). To perform the server login, open the MultiplayerDSAMSWrapper_Starter class and create the function below to send the request. You will use this function later during server registration.

private void LoginServer(ResultCallback onComplete)
{
BytewarsLogger.Log("Start server login.");

ds?.LoginWithClientCredentials((result) =>
{
if (result.IsError)
{
BytewarsLogger.LogWarning($"Failed to login server. Error {result.Error.Code}: {result.Error.Message}");
}
else
{
BytewarsLogger.LogWarning("Success to login server.");
}

onComplete?.Invoke(result);
});
}

Register server

You need to register it to AMS and DSHub so your server can be managed and acknowledged by AMS to serve game sessions.

  1. Open the MultiplayerDSAMSWrapper_Starter class and the create the function below to send register server request.

    private void RegisterServer()
    {
    ams.OnOpen -= RegisterServer;

    string dsId = AccelByteSDK.GetServerConfig().DsId;
    if (string.IsNullOrEmpty(dsId))
    {
    BytewarsLogger.LogWarning("Failed to register server. DSId is invalid.");
    return;
    }

    BytewarsLogger.Log("Registering server.");

    ams?.SendReadyMessage();
    dsHub?.Connect(dsId);
    }
  2. Next, create these callback function to listen when AMS and DSHub connection is established.

    private void OnAMSConnected()
    {
    BytewarsLogger.Log("Server connected to AMS.");
    OnAMSConnectionOpened?.Invoke();
    }
    private void OnDSHubConnected()
    {
    BytewarsLogger.Log($"Server connected to DSHub");
    }
  3. You can also listen for when the server is claimed. Let's create a new function to handle this by simply storing the game session ID in the game cache. This session ID will be helpful if you want to call an API from the server that requires the game session ID.

    private void OnServerClaimed(Result<ServerClaimedNotification> result)
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to claim server. Error {result.Error.Code}: {result.Error.Message}");
    return;
    }

    BytewarsLogger.Log($"Success to claim server. Session ID: {result.Value.sessionId}");
    GameData.ServerSessionID = result.Value.sessionId;
    }
  4. On the OnEnable() function, add the code below to send the login server request and then register the server when the wrapper is initialized. Also, add the code to bind the delegates to listen the AMS and DSHub events.

    private void OnEnable()
    {
    ...
    ams.OnOpen += OnAMSConnected;
    dsHub.OnConnected += OnDSHubConnected;
    dsHub.MatchmakingV2ServerClaimed += OnServerClaimed;

    LoginServer((result) =>
    {
    if (ams.IsConnected)
    {
    RegisterServer();
    }
    else
    {
    ams.OnOpen += RegisterServer;
    }
    });
    }
  5. Then, unbind the delegates when the wrapper is deinitialized in the OnDisable() function.

    private void OnDisable()
    {
    ...
    if (ams != null)
    {
    ams.OnOpen -= OnAMSConnected;
    }

    if (dsHub != null)
    {
    dsHub.OnConnected -= OnDSHubConnected;
    dsHub.MatchmakingV2ServerClaimed -= OnServerClaimed;
    }
    }

Deregister server

Before you shut down you server, such as when the game session is over, you need to deregister your server from AMS and DSHub first to avoid unused zombie servers.

  1. Open the MultiplayerDSAMSWrapper_Starter class and create the function below to send a server deregistration request.

    private void DeregisterServer()
    {
    BytewarsLogger.Log("Deregistering server.");
    ams?.Disconnect();
    dsHub?.Disconnect();
    }
  2. Next, create these callback functions to listen for when AMS and DSHub are disconnected.

    private void OnAMSDisconnected(WsCloseCode closeCode)
    {
    BytewarsLogger.Log($"Server disconnected from AMS. Close code: {closeCode}");
    OnAMSConnectionClosed?.Invoke();
    }
    private void OnDSHubDisconnected(WsCloseCode closeCode)
    {
    BytewarsLogger.Log($"Server disconnected from DSHub. Close code: {closeCode}");

    // If disconnected abnormally, try to reconnect.
    if (closeCode is WsCloseCode.Undefined or WsCloseCode.Abnormal or WsCloseCode.NoStatus)
    {
    string dsId = AccelByteSDK.GetServerConfig().DsId;
    if (string.IsNullOrEmpty(dsId))
    {
    BytewarsLogger.LogWarning("Failed to reconnecting server to DSHub. DSId is invalid.");
    }

    BytewarsLogger.Log("Reconnecting server to DSHub.");
    dsHub?.Connect(dsId);
    }
    }
  3. Create a new function to handle when the server receives game session ends notification from backend, such as when the session is time out, etc.

    private void OnGameSessionEnded(Result<SessionEndedNotification> result)
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed handle on game session ended. Error {result.Error.Code}: {result.Error.Message}");
    return;
    }

    BytewarsLogger.Log($"Recieved game session ended. Session ID: {result.Value.SessionId}. Shutting down server.");
    GameManager.Instance.StartShutdownCountdown(ServerShutdownDelay);
    }
  4. In the OnEnable() function, add the code below to call the server deregistration sequence when the game session is over (either due to game over or receiving a game session end notification), and shut down the server once the deregistration is complete.

    private void OnEnable()
    {
    ...
    ams.Disconnected += OnAMSDisconnected;
    dsHub.OnDisconnected += OnDSHubDisconnected;
    dsHub.GameSessionV2Ended += OnGameSessionEnded;

    OnAMSConnectionClosed += ShutdownServer;
    GameManager.Instance.OnDeregisterServer += DeregisterServer;
    }
  5. Then, unbind the delegates when the wrapper is deinitialized in the OnDisable() function.

    private void OnDisable()
    {
    ...
    if (ams != null)
    {
    ams.Disconnected -= OnAMSDisconnected;
    }

    if (dsHub != null)
    {
    dsHub.OnDisconnected -= OnDSHubDisconnected;
    dsHub.GameSessionV2Ended -= OnGameSessionEnded;
    }

    OnAMSConnectionClosed -= ShutdownServer;
    GameManager.Instance.OnDeregisterServer -= DeregisterServer;
    }

Handle drain state

The drain signal notifies the server that the Virtual Machine (VM) it's running on is scheduled for removal. Ideally, the server should shut down only when it's no longer serving any players. If it is still serving players, it should continue running and ignore the drain signal. You can read more about drain behavior on this page.

In Byte Wars, we handle the drain signal in this way:

The Wait for 10 seconds state accommodates potential delays in session info updates. It helps prevent cases where a server is claimed on the backend just before the update is received, which could trigger a premature claim signal.

  1. Open the MultiplayerDSAMSWrapper_Starter class and create a function to handle the drain logic. This function checks whether the server is currently hosting an active game session. If the session is active, it ignores the drain signal and keeps the server running.

    private void ExecuteDrainSignal()
    {
    if (string.IsNullOrEmpty(GameData.ServerSessionID))
    {
    BytewarsLogger.Log("ServerSessionID is empty, executing drain logic now!");
    OnAMSDrainSignalReceived?.Invoke();
    }
    else
    {
    BytewarsLogger.Log("ServerSessionID is not empty, drain ignored until session ends.");
    }
    }
  2. Create a function to be called when the dedicated server receives a drain signal from AMS. This function starts a delay and executes the ExecuteDrainSignal() function after the delay is completed.

    private void OnAMSDrainReceived()
    {
    // Get the delay config from launch param.
    const string keyword = "-DrainLogicDelayInSecs=";
    if (!float.TryParse(TutorialModuleUtil.GetLaunchParamValue(keyword), out float delay))
    {
    delay = ServerShutdownDelay;
    BytewarsLogger.Log("Could not parse drain delay launch param. Fallback to use default delay.");
    }

    // Execute drain logic after a delay to accommodate session info update delay.
    BytewarsLogger.Log($"DS received drain signal from AMS. Delaying {delay} seconds to execute drain logic.");
    Invoke(nameof(ExecuteDrainSignal), delay);
    }
  3. In the OnEnable() function, add the code below to trigger server deregistration when a drain signal is received.

    private void OnEnable()
    {
    ...
    ams.OnDrainReceived += OnAMSDrainReceived;
    OnAMSDrainSignalReceived += DeregisterServer;
    }
  4. Then, unbind the delegates when the wrapper is deinitialized in the OnDisable() function.

    private void OnDisable()
    {
    ...
    if (ams != null)
    {
    ams.OnDrainReceived -= OnAMSDrainReceived;
    }

    OnAMSDrainSignalReceived -= DeregisterServer;
    }

Resources