Skip to main content

Use the SDK for dedicated server matchmaking - Quick match with dedicated servers - (Unity module)

Last updated on July 28, 2025

Unwrap the wrapper

In this tutorial, you will learn how to implement AccelByte Gaming Services (AGS) Matchmaking using the AGS Software Development Kit (SDK). In Byte Wars, a game instance wrapper is defined in the MatchmakingDSWrapper and the MatchmakingDSServerWrapper classes. These wrappers manage matchmaking-related actions such as starting and canceling matchmaking. For this tutorial, you will use the MatchmakingDSWrapper_Starter and the MatchmakingDSServerWrapper_Starter classes, which are starter versions of the MatchmakingDSWrapper and MatchmakingDSServerWrapper classes.

What's in the starter pack

To follow this tutorial, you will use the starter wrapper class called MatchmakingDSWrapper_Starter for the game client and the MatchmakingDSServerWrapper_Starter class for the server. These classes inherit from the MatchmakingEssentialsWrapper class, which is defined in the following file:

  • C# file: Assets/Resources/Modules/Play/MatchmakingEssentials/Scripts/MatchmakingEssentialsWrapper.cs

The MatchmakingDSWrapper_Starter and MatchmakingDSServerWrapper_Starter classes are defined in the files below:

  • C# file: Assets/Resources/Modules/Play/MatchmakingDS/Scripts/MatchmakingDSWrapper_Starter.cs
  • C# file: Assets/Resources/Modules/Play/MatchmakingDS/Scripts/MatchmakingDSServerWrapper_Starter.cs

Additionally, there are model classes containing helper functions and variables, defined in the following files:

  • C# file: Assets/Resources/Modules/Play/OnlineSessionUtils/Scripts/AccelByteWarsOnlineSessionModels.cs
  • C# file: Assets/Resources/Modules/Play/MatchmakingEssentials/Scripts/MatchmakingEssentialsModels.cs

The MatchmakingEssentialsWrapper itself inherits from the SessionEssentialsWrapper class, which contains basic session management functionality such as joining, leaving, and rejecting session invitations. You learned how to implement this functionality in the previous Introduction to Session module. The MatchmakingEssentialsWrapper contains several predefined helper components, including:

  • Helper variables for referencing AGS SDK interfaces. These variables are assigned during initialization:

    protected static MatchmakingV2 Matchmaking;

    #if UNITY_SERVER
    protected static ServerDSHub ServerDSHub;
    protected static ServerMatchmakingV2 ServerMatchmaking;
    #endif

    protected override void Awake()
    {
    base.Awake();

    Matchmaking ??= AccelByteSDK.GetClientRegistry().GetApi().GetMatchmakingV2();

    #if UNITY_SERVER
    ServerDSHub ??= AccelByteSDK.GetServerRegistry().GetApi().GetDsHub();
    ServerMatchmaking ??= AccelByteSDK.GetServerRegistry().GetApi().GetMatchmakingV2();
    #endif
    }
  • Delegates to handle matchmaking states:

    public static ResultCallback<MatchmakingV2CreateTicketResponse> OnMatchmakingStarted = delegate { };
    public static ResultCallback<MatchmakingV2MatchFoundNotification> OnMatchFound = delegate { };
    public static ResultCallback OnMatchmakingCanceled = delegate { };
    public static ResultCallback<MatchmakingV2TicketExpiredNotification> OnMatchmakingExpired = delegate { };
    public static ResultCallback<SessionV2GameInvitationNotification> OnSessionInviteReceived = delegate { };
    public static ResultCallback<SessionV2DsStatusUpdatedNotification> OnDSStatusChanged = delegate { };

The root parent class of MatchmakingEssentialsWrapper named AccelByteWarsOnlineSession class also contains several predefined helper components, including:

  • A helper method to connect the game client to the dedicated server.

    public virtual void TravelToDS(SessionV2GameSession session, InGameMode gameMode)
    {
    SessionV2DsInformation dsInfo = session.dsInformation;
    if (dsInfo == null)
    {
    BytewarsLogger.LogWarning("Failed to travel to dedicated server. Dedicated server information not found.");
    return;
    }

    if (NetworkManager.Singleton.IsListening)
    {
    BytewarsLogger.LogWarning("Failed to travel to dedicated server. The instance is running as listen server.");
    return;
    }

    string ip = dsInfo.server.ip;
    ushort port = (ushort)dsInfo.server.port;
    InitialConnectionData initialData = new InitialConnectionData()
    {
    sessionId = string.Empty,
    inGameMode = gameMode,
    serverSessionId = session.id,
    userId = GameData.CachedPlayerState.PlayerId
    };

    GameManager.Instance.ShowTravelingLoading(() =>
    {
    BytewarsLogger.Log("Travel to dedicated server as client.");
    GameManager.Instance.StartAsClient(ip, port, initialData);
    });
    }

The AccelByteWarsOnlineSessionModels file contains several predefined helper components, including:

  • A string constant to store the session template name, based on the Session Template and Match Pool configured in the Admin Portal:

    public const string EliminationDSAMSSessionTemplateName = "unity-elimination-ds-ams";
    public const string TeamDeathmatchDSAMSSessionTemplateName = "unity-teamdeathmatch-ds-ams";
  • A helper dictionary that maps in-game modes to session request models based on the session template name:

    public static readonly Dictionary<InGameMode, Dictionary<GameSessionServerType, SessionV2GameSessionCreateRequest>> SessionCreateRequestModels = new()
    {
    ...
    {
    InGameMode.MatchmakingElimination, new()
    {
    ...
    {
    GameSessionServerType.DedicatedServerAMS,
    new SessionV2GameSessionCreateRequest()
    {
    type = SessionConfigurationTemplateType.DS,
    joinability = SessionV2Joinability.OPEN,
    configurationName = EliminationDSAMSSessionTemplateName,
    matchPool = EliminationDSAMSSessionTemplateName,
    clientVersion = ClientVersion
    }
    }
    }
    },
    {
    InGameMode.MatchmakingTeamDeathmatch, new()
    {
    ...
    {
    GameSessionServerType.DedicatedServerAMS,
    new SessionV2GameSessionCreateRequest()
    {
    type = SessionConfigurationTemplateType.DS,
    joinability = SessionV2Joinability.OPEN,
    configurationName = TeamDeathmatchDSAMSSessionTemplateName,
    matchPool = TeamDeathmatchDSAMSSessionTemplateName,
    clientVersion = ClientVersion
    }
    }
    }
    },
    ...
    };
  • A helper function to retrieve the session request model by in-game mode:

    public static SessionV2GameSessionCreateRequest GetGameSessionRequestModel(
    InGameMode gameMode,
    GameSessionServerType serverType)
    {
    if (!SessionCreateRequestModels.TryGetValue(gameMode, out var matchTypeDict))
    {
    return null;
    }

    matchTypeDict.TryGetValue(serverType, out var request);
    return request;
    }
  • String constants to add additional attributes to the match ticket parameter.

    public static readonly string ServerNameAttributeKey = "server_name";
    public static readonly string ClientVersionAttributeKey = "client_version";

The MatchmakingEssentialsModels file contains several predefined helper components, including:

  • A string constant to parse the match ticket result upon matchmaking start. You will use this to decide whether to retry starting matchmaking or not.

    public static readonly string MatchTicketIdAttributeKey = "ticketID";

Start matchmaking

This section will guide you on how to start matchmaking using the AGS SDK.

  1. Open the MatchmakingDSWrapper_Starter class and create the following function. This function first leaves the active session (if one exists) before starting matchmaking. When the matchmaking starts successfully, the backend creates a new match ticket and match ticket ID, which you can use to cancel matchmaking. A player can only have one match ticket ID, so if starting matchmaking fails due to conflicting match ticket IDs, this function will try to cancel the existing ticket first and retry starting matchmaking. Once the request is complete, it invokes the assigned delegate. From the code below, you may notice that it also enriches the request by adding several values to the optional parameters of MatchmakingV2CreateTicketRequestOptionalParams. Here's what those parameters do:

    • latencies: Sets the requested regions so the session will use the AMS dedicated server in a specific region and select the nearest one based on latency.
    • sessionId: Adds the party session ID so that party members can join the matchmaking.
    • attributes: Additional attributes you can add to the match ticket. In this case, the following values are added:
      • server_name: The value from the ServerNameAttributeKey variable. This attribute sets the requested local server name for testing the game with a local dedicated server.
      • client_version: The value from the ClientVersionAttributeKey variable. This attribute selects the build config name for testing using the AMS development fleet.
    public override void StartMatchmaking(
    string matchPool,
    ResultCallback<MatchmakingV2CreateTicketResponse> onComplete)
    {
    // Leave the existing session before starting matchmaking.
    if (CachedSession != null)
    {
    LeaveGameSession(CachedSession.id, (leaveResult) =>
    {
    // Abort only if there's an error and it's not due to a missing session.
    if (leaveResult.IsError && leaveResult.Error.Code != ErrorCode.SessionIdNotFound)
    {
    BytewarsLogger.LogWarning($"Failed to start matchmaking. Error {leaveResult.Error.Code}: {leaveResult.Error.Message}");
    Result<MatchmakingV2CreateTicketResponse> errorResult = Result<MatchmakingV2CreateTicketResponse>.CreateError(leaveResult.Error);
    OnMatchmakingStarted?.Invoke(errorResult);
    onComplete?.Invoke(errorResult);
    return;
    }

    StartMatchmaking(matchPool, onComplete);
    });
    return;
    }

    MatchmakingV2CreateTicketRequestOptionalParams optionalParams = new() { attributes = new() };

    // Add local server name.
    if (!string.IsNullOrEmpty(ConnectionHandler.LocalServerName))
    {
    optionalParams.attributes.Add(ServerNameAttributeKey, ConnectionHandler.LocalServerName);
    }

    // Add client version.
    optionalParams.attributes.Add(ClientVersionAttributeKey, ClientVersion);

    // Add preferred regions.
    Dictionary<string, int> preferredRegions =
    RegionPreferencesModels.GetEnabledRegions().ToDictionary(x => x.RegionCode, y => (int)y.Latency);
    if (preferredRegions.Count > 0)
    {
    optionalParams.latencies = preferredRegions;
    }

    // Add party session ID for the playing-with-party feature.
    SessionV2PartySession partySession = PartyEssentialsModels.PartyHelper.CurrentPartySession;
    if (partySession != null && !string.IsNullOrEmpty(partySession.id))
    {
    optionalParams.sessionId = partySession.id;
    }

    Matchmaking.CreateMatchmakingTicket(matchPool, optionalParams, (startResult) =>
    {
    // Matchmaking started successfully.
    if (!startResult.IsError)
    {
    BytewarsLogger.Log($"Successfully started matchmaking. Match ticket ID: {startResult.Value.matchTicketId}");
    OnMatchmakingStarted?.Invoke(startResult);
    onComplete?.Invoke(startResult);
    return;
    }

    BytewarsLogger.LogWarning($"Failed to start matchmaking. Error {startResult.Error.Code}: {startResult.Error.Message}");

    // Attempt recovery if conflict due to existing ticket. Otherwise, return the result.
    if (startResult.Error.Code != ErrorCode.MatchmakingV2CreateMatchTicketConflict)
    {
    OnMatchmakingStarted?.Invoke(startResult);
    onComplete?.Invoke(startResult);
    return;
    }

    // Abort if the message variables are null.
    string messageVariablesJson = startResult.Error.messageVariables?.ToJsonString();
    if (string.IsNullOrEmpty(messageVariablesJson))
    {
    OnMatchmakingStarted?.Invoke(startResult);
    onComplete?.Invoke(startResult);
    return;
    }

    // Abort if the message variables do not contain the conflicted match ticket attribute.
    Dictionary<string, object> messageVariables = JsonConvert.DeserializeObject<Dictionary<string, object>>(messageVariablesJson);
    if (messageVariables == null ||
    !messageVariables.TryGetValue(MatchTicketIdAttributeKey, out var existingMatchTicketIdObj) ||
    existingMatchTicketIdObj is not string existingMatchTicketId)
    {
    OnMatchmakingStarted?.Invoke(startResult);
    onComplete?.Invoke(startResult);
    return;
    }

    // Cancel existing ticket and retry.
    CancelMatchmaking(existingMatchTicketId, cancelResult =>
    {
    if (cancelResult.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to start matchmaking. Error {cancelResult.Error.Code}: {cancelResult.Error.Message}");
    Result<MatchmakingV2CreateTicketResponse> errorResult = Result<MatchmakingV2CreateTicketResponse>.CreateError(cancelResult.Error);
    OnMatchmakingStarted?.Invoke(errorResult);
    onComplete?.Invoke(errorResult);
    return;
    }

    StartMatchmaking(matchPool, onComplete);
    });
    });
    }

Cancel matchmaking

This section will guide you on how to cancel matchmaking using the AGS SDK.

  1. Open the MatchmakingDSWrapper_Starter class and create the following function. This function sends a request to delete the match ticket ID to cancel the matchmaking request. Once the request is complete, it invokes the assigned delegate.

    public override void CancelMatchmaking(
    string matchTicketId,
    ResultCallback onComplete)
    {
    Matchmaking.DeleteMatchmakingTicket(matchTicketId, (result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning(
    $"Failed to cancel matchmaking with ticket ID: {matchTicketId}. " +
    $"Error {result.Error.Code}: {result.Error.Message}");
    }
    else
    {
    BytewarsLogger.Log($"Successfully canceled matchmaking. Match ticket ID: {matchTicketId}");
    }

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

Listen to matchmaking events

This section will guide you on how to listen on the matchmaking events.

  1. Open the MatchmakingDSWrapper_Starter class and replace the predefined Awake() function with the code below. This code binds delegates to listen for events, such as when matchmaking expires, when matchmaking finds players to match, when a session invitation is received after a match is found, and when the backend finds a dedicated server to serve the session.

    protected override void Awake()
    {
    base.Awake();

    Lobby.MatchmakingV2TicketExpired += (result) => OnMatchmakingExpired?.Invoke(result);
    Lobby.MatchmakingV2MatchFound += (result) => OnMatchFound?.Invoke(result);
    Lobby.SessionV2InvitedUserToGameSession += (result) => OnSessionInviteReceived?.Invoke(result);
    Lobby.SessionV2DsStatusChanged += (result) => OnDSStatusChanged?.Invoke(result);
    }
  2. Create the following function. This function sends the join session request by calling the parent class function, which is what you learned from the Introduction to Session module. Once the request is completed, it checks whether the dedicated server is ready to travel.

    public override void JoinGameSession(
    string sessionId,
    ResultCallback<SessionV2GameSession> onComplete)
    {
    // Reregister delegate to listen for dedicated server status changed event.
    OnDSStatusChanged -= OnDSStatusChangedReceived;
    OnDSStatusChanged += OnDSStatusChangedReceived;

    base.JoinGameSession(sessionId, (result) =>
    {
    // If dedicated server is ready, broadcast the dedicated server status changed event.
    if (!result.IsError && result.Value.dsInformation.StatusV2 == SessionV2DsStatus.AVAILABLE)
    {
    OnDSStatusChanged?.Invoke(Result<SessionV2DsStatusUpdatedNotification>.CreateOk(new()
    {
    session = result.Value,
    sessionId = result.Value.id,
    error = string.Empty
    }));
    }

    onComplete?.Invoke(result);
    });
    }
  3. Then, create a new function to handle when the dedicated server status is received. If the dedicated server is ready, the game client will travel to it. Otherwise, it will throw errors based on the status types.

    private void OnDSStatusChangedReceived(Result<SessionV2DsStatusUpdatedNotification> result)
    {
    if (result.IsError)
    {
    OnDSStatusChanged -= OnDSStatusChangedReceived;
    BytewarsLogger.LogWarning(
    $"Failed to handle dedicated server status changed event. " +
    $"Error {result.Error.Code}: {result.Error.Message}");
    return;
    }

    SessionV2GameSession session = result.Value.session;
    SessionV2DsInformation dsInfo = session.dsInformation;

    // Check if the requested game mode is supported.
    InGameMode requestedGameMode = GetGameSessionGameMode(session);
    if (requestedGameMode == InGameMode.None)
    {
    BytewarsLogger.LogWarning(
    $"Failed to handle dedicated server status changed event. " +
    $"Session's game mode is not supported by the game.");
    OnDSStatusChanged -= OnDSStatusChangedReceived;
    OnDSStatusChanged?.Invoke(
    Result<SessionV2DsStatusUpdatedNotification>.
    CreateError(ErrorCode.NotAcceptable, InvalidSessionTypeMessage));
    return;
    }

    // Check the dedicated server status.
    switch (dsInfo.StatusV2)
    {
    case SessionV2DsStatus.AVAILABLE:
    OnDSStatusChanged -= OnDSStatusChangedReceived;
    TravelToDS(session, requestedGameMode);
    break;
    case SessionV2DsStatus.FAILED_TO_REQUEST:
    case SessionV2DsStatus.ENDED:
    case SessionV2DsStatus.UNKNOWN:
    OnDSStatusChanged -= OnDSStatusChangedReceived;
    BytewarsLogger.LogWarning(
    $"Failed to handle dedicated server status changed event. " +
    $"Session failed to request for dedicated server due to unknown reason.");
    break;
    default:
    BytewarsLogger.Log($"Received dedicated server status change. Status: {dsInfo.StatusV2}");
    break;
    }
    }

Handle backfill

When matchmaking, your game server might have been claimed by the session, but it might still have room for other players to join based on the maximum players you've configured in the Session Template. To fill these empty slots, you can implement backfill for your game server. This section will guide you on how to implement backfill for your game server.

  1. Open the MatchmakingDSServerWrapper_Starter class and create a function to accept backfill by adding the code below.

    private void AcceptBackfillProposal(MatchmakingV2BackfillProposalNotification proposal)
    {
    ServerMatchmaking.AcceptBackfillProposal(proposal, (result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to accept backfill proposal. Error {result.Error.Code}: {result.Error.Message}");
    }
    else
    {
    BytewarsLogger.Log($"Successfully accepted backfill proposal. Session ID: {result.Value.id}. Backfill Ticket ID: {result.Value.backfillTicketId}");
    }
    });
    }
  2. Next, create a function to reject backfill by adding the code below.

    private void RejectBackfillProposal(MatchmakingV2BackfillProposalNotification proposal, bool stopBackfilling)
    {
    ServerMatchmaking.RejectBackfillProposal(proposal, stopBackfilling, (result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to reject backfill proposal. Error {result.Error.Code}: {result.Error.Message}");
    }
    else
    {
    BytewarsLogger.Log($"Successfully rejected backfill proposal. Stop backfilling: {stopBackfilling}");
    }
    });
    }
  3. When your server still has empty slots for players to join, the backend will send a backfill proposal to your game server. Create a function to handle this proposal by adding the code below. Specifically for Byte Wars, we accept the backfill if the game has not yet started; otherwise, we reject the proposal and request the backend to stop backfilling the game server.

    private void OnBackfillProposalReceived(Result<MatchmakingV2BackfillProposalNotification> result)
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to handle backfill proposal. Error {result.Error.Code}: {result.Error.Message}");
    return;
    }

    // If gameplay has not yet started, accept the backfill proposal.
    if (GameManager.Instance.InGameState == InGameState.None)
    {
    AcceptBackfillProposal(proposal: result.Value);
    }
    // Otherwise, reject the proposal.
    else
    {
    RejectBackfillProposal(proposal: result.Value, stopBackfilling: true);
    }
    }
  4. Finally, replace the predefined Awake() function with the code below to bind the function above to listen to the backfill proposal event.

    protected override void Awake()
    {
    base.Awake();

    ServerDSHub.MatchmakingV2BackfillProposalReceived += OnBackfillProposalReceived;
    }

Resources