Skip to main content

Use the SDK for peer-to-peer matchmaking - Quick match with peer-to-peer - (Unity module)

Last updated on July 28, 2025
info

AccelByte Gaming Services (AGS) SDK for Unity does not support P2P on WebGL. This module cannot be used in WebGL builds.

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 MatchmakingP2PWrapper class. This wrapper manages matchmaking-related actions such as starting and canceling matchmaking. For this tutorial, you will use the MatchmakingP2PWrapper_Starter class, which is starter version of the MatchmakingP2PWrapper class.

What's in the starter pack

To follow this tutorial, you will use the starter wrapper class called MatchmakingP2PWrapper_Starter. This class inherits from the MatchmakingEssentialsWrapper class, which is defined in the following file:

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

The MatchmakingP2PWrapper_Starter class is defined in the files below:

  • C# file: Assets/Resources/Modules/Play/MatchmakingP2P/Scripts/MatchmakingP2PWrapper_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;

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

    Matchmaking ??= AccelByteSDK.GetClientRegistry().GetApi().GetMatchmakingV2();
    }
  • 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 { };

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

  • A helper method to start peer-to-peer host and connect game client to peer-to-peer host. You need to use the AccelByteNetworkTransportManager plugin as your network transport to integrate P2P networking using AccelByte SDK.

    public virtual void TravelToP2PHost(SessionV2GameSession session, InGameMode gameMode)
    {
    AccelByteNetworkTransportManager transportManager = NetworkManager.Singleton.GetComponent<AccelByteNetworkTransportManager>();
    if (transportManager == null)
    {
    transportManager = NetworkManager.Singleton.gameObject.AddComponent<AccelByteNetworkTransportManager>();
    transportManager.Initialize(AccelByteSDK.GetClientRegistry().GetApi());
    transportManager.OnTransportEvent += GameManager.Instance.OnTransportEvent;
    }

    InitialConnectionData initialData = new InitialConnectionData()
    {
    inGameMode = gameMode,
    serverSessionId = session.id,
    userId = GameData.CachedPlayerState.PlayerId
    };
    NetworkManager.Singleton.NetworkConfig.ConnectionData = GameUtility.ToByteArray(initialData);
    NetworkManager.Singleton.NetworkConfig.NetworkTransport = transportManager;

    bool isHost = session.leaderId == GameData.CachedPlayerState.PlayerId;
    GameManager.Instance.ShowTravelingLoading(() =>
    {
    GameManager.Instance.ResetCache();
    GameData.ServerType = ServerType.OnlinePeer2Peer;

    if (isHost)
    {
    BytewarsLogger.Log("Start as P2P host");
    GameData.ServerSessionID = session.id;
    NetworkManager.Singleton.StartHost();
    }
    else
    {
    BytewarsLogger.Log($"Start as P2P client. Target host: {session.leaderId}");
    transportManager.SetTargetHostUserId(session.leaderId);
    NetworkManager.Singleton.StartClient();
    }
    },
    isHost ? StartingAsHostMessage : WaitingHostMessage);
    }

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 EliminationP2PSessionTemplateName = "unity-elimination-p2p";
    public const string TeamDeathmatchP2PSessionTemplateName = "unity-teamdeathmatch-p2p";
  • 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.PeerToPeer,
    new SessionV2GameSessionCreateRequest()
    {
    type = SessionConfigurationTemplateType.P2P,
    joinability = SessionV2Joinability.OPEN,
    configurationName = EliminationP2PSessionTemplateName,
    matchPool = EliminationP2PSessionTemplateName,
    }
    },
    ...
    }
    },
    {
    InGameMode.MatchmakingTeamDeathmatch, new()
    {
    ...
    {
    GameSessionServerType.PeerToPeer,
    new SessionV2GameSessionCreateRequest()
    {
    type = SessionConfigurationTemplateType.P2P,
    joinability = SessionV2Joinability.OPEN,
    configurationName = TeamDeathmatchP2PSessionTemplateName,
    matchPool = TeamDeathmatchP2PSessionTemplateName,
    }
    },
    ...
    }
    },
    ...
    };
  • 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;
    }

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 MatchmakingP2PWrapper_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 and caches the session locally. 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:

    • sessionId: Adds the party session ID so that party members can join the matchmaking.
    public override void StartMatchmaking(
    string matchPool,
    ResultCallback<MatchmakingV2CreateTicketResponse> onComplete)
    {
    // If there is an existing session, leave it first.
    if (CachedSession != null)
    {
    LeaveGameSession(CachedSession.id, (leaveResult) =>
    {
    if (leaveResult.IsError)
    {
    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 party session id for 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($"Success to start 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 variable is null.
    string messageVariablesJson = startResult.Error.messageVariables?.ToJsonString();
    if (string.IsNullOrEmpty(messageVariablesJson))
    {
    OnMatchmakingStarted?.Invoke(startResult);
    onComplete?.Invoke(startResult);
    return;
    }

    // Abort if the message variable does not contain 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 MatchmakingP2PWrapper_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($"Success to cancel 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 MatchmakingP2PWrapper_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, and when a session invitation is received after a match is found.

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

    Lobby.MatchmakingV2TicketExpired += (result) => OnMatchmakingExpired?.Invoke(result);
    Lobby.MatchmakingV2MatchFound += (result) => OnMatchFound?.Invoke(result);
    Lobby.SessionV2InvitedUserToGameSession += (result) => OnSessionInviteReceived?.Invoke(result);
    }
  2. Finally, create the following function to send 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 peer-to-peer host is ready to connect.

    public override void JoinGameSession(
    string sessionId,
    ResultCallback<SessionV2GameSession> onComplete)
    {
    base.JoinGameSession(sessionId, (result) =>
    {
    if (!result.IsError)
    {
    InGameMode requestedGameMode = GetGameSessionGameMode(result.Value);
    if (requestedGameMode == InGameMode.None)
    {
    BytewarsLogger.LogWarning($"Failed to travel to the P2P host. Session's game mode is not supported by the game.");
    onComplete?.Invoke(Result<SessionV2GameSession>.CreateError(ErrorCode.NotAcceptable, InvalidSessionTypeMessage));
    return;
    }
    TravelToP2PHost(result.Value, requestedGameMode);
    }

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

Resources