Skip to main content

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

Last updated on November 25, 2024
info

Browsing P2P sessions is not supported in WebGL builds due to the AccelByte Gaming Services (AGS) SDK for Unity lacking the P2P functionality for WebGL.

Unwrap the wrapper

After setting up a match pool, you will call the AGS Game SDK for peer-to-peer (P2P) matchmaking. This is done in the MatchmakingSessionP2PWrapper class in the StartP2PMatchmaking function to initiate the matchmaking process using P2P, since you will use the match pool name you set up previously with the P2P in its session template.

The MatchmakingSessionP2PWrapper is a subclass of MatchmakingSessionWrapper, and it uses functions that are predefined in the MatchmakingSessionWrapper parent class.

What's in the starter pack

The P2PHelper.cs class provides several functions for starting as a P2P host and joining a P2P session.

  • P2PHelper.cs: A C# script that contains most of the P2P implementation.
    • CS file: Assets/Resources/Modules/MatchmakingSessionP2P/Scripts/P2PHelper.cs

There are two variables in P2PHelper.cs: transportManager is a custom network transport from the AGS network transport, and networkManager to hold a reference to the NetworkManager class.

private static AccelByteNetworkTransportManager transportManager;
private static NetworkManager networkManager;

The Init function initializes the custom network transport.

private static void Init()
{
if (transportManager != null)
{
return;
}
networkManager = NetworkManager.Singleton;
transportManager = networkManager.gameObject.AddComponent<AccelByteNetworkTransportManager>();
ApiClient apiClient = AccelByteSDK.GetClientRegistry().GetApi();
transportManager.Initialize(apiClient);
}

To set up the network transport, the SetP2PNetworkTransport function is used. In this function, you will initialize the AccelByteNetworkTransportManager and use it as a network transport to establish a P2P connection. You need the match session ID in connection data.

private static void SetP2PNetworkTransport(InGameMode gameMode, string matchSessionId)
{
Init();
InitialConnectionData data = new InitialConnectionData() { inGameMode = gameMode, serverSessionId = matchSessionId };
networkManager.NetworkConfig.ConnectionData = GameUtility.ToByteArray(data);
networkManager.NetworkConfig.NetworkTransport = transportManager;
}

To start as a P2P host, StartAsHost is used. This function will start the network manager that has been set up with the in-game mode and match session ID, and then it starts the network manager as a host.

public static async void StartAsHost(InGameMode gameMode, string matchSessionId)
{
await GameManager.ShowTravelingLoading();

GameManager.Instance.ResetCache();
GameData.ServerType = ServerType.OnlinePeer2Peer;

SetP2PNetworkTransport(gameMode, matchSessionId);
networkManager.StartHost();
BytewarsLogger.Log($"Start P2P Host");
GameManager.StartListenNetworkSceneEvent();
}

To start as a client in a P2P network, the StartAsP2PClient function has been defined. This function needs the host user ID, which it will get from the game session.

public static async void StartAsP2PClient(string hostUserId, InGameMode gameMode, string matchSessionId)
{
await GameManager.ShowTravelingLoading();

GameManager.Instance.ResetCache();
GameData.ServerType = ServerType.OnlinePeer2Peer;

SetP2PNetworkTransport(gameMode, matchSessionId);
transportManager.SetTargetHostUserId(hostUserId);
networkManager.StartClient();
BytewarsLogger.Log($"Start P2P Client hostUserId: {hostUserId}");
}

What is in MatchmakingSessionWrapper

The MatchmakingSessionWrapper has been prepared as a parent class containing predefined code that leverages the AGS Game SDK to perform various matchmaking-related functions.

MatchmakingV2 class is from the AGS Game SDK is used to create and delete matchmaking tickets. The StartMatchmakingAsync method creates a match ticket for the player, while CheckActiveGameSessionClient ensures the player is not currently in an active session. To create a new ticket, players should refrain from joining any ongoing sessions.

public async UniTask StartMatchmakingAsync(string matchPool, bool isLocal = false)
{
CheckActiveGameSessionClient();

await WaitCheckActiveGameSessionAsync();

if (playerSessions.Count > 0)
{
BytewarsLogger.Log($"Player is already in a session: {playerSessions.Count}");
OnMatchmakingError?.Invoke($"Player is already in a session \n cannot start matchmaking");
isGetActiveSessionCompleted = false;
playerSessions.Clear();
return;
}

MatchmakingV2CreateTicketRequestOptionalParams optionalParams = new MatchmakingV2CreateTicketRequestOptionalParams();
optionalParams.attributes = new Dictionary<string, object>();

// Add local server name.
if (isLocal)
{
optionalParams.attributes.Add("server_name", ConnectionHandler.LocalServerName);
}

// Add client version
optionalParams.attributes.Add("client_version", TutorialModuleUtil.IsOverrideDedicatedServerVersion() ? Application.version : string.Empty);

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

// Play with Party additional code
if (!string.IsNullOrEmpty(PartyHelper.CurrentPartyId))
{
optionalParams.sessionId = PartyHelper.CurrentPartyId;
}

matchmakingV2.CreateMatchmakingTicket(matchPool, optionalParams, OnStartMatchmakingComplete);
}

You can only delete the match ticket if the player has not yet joined the game session created by the matchmaker.

protected internal void CancelMatchmaking(string matchTicketId)
{
matchmakingV2.DeleteMatchmakingTicket(matchTicketId, result => OnCancelMatchmakingComplete(result, matchTicketId));
}

You will observe lobby updates to check for the match status. When a match is discovered, you will be ready to join the game session.

protected void BindMatchFoundNotification()
{
lobby.MatchmakingV2MatchFound += OnMatchmakingFound;
}

Implement peer-to-peer matchmaking

You will continue the P2p matchmaking implementation in the MatchmakingSessionP2PWrapper_Starter.cs file.

  1. You need to create a matchmaking ticket based on the match pool name. Go to the StartP2PMatchmaking then update the function in the code below. In this function, you will get the match pool name from GameSessionConfig that contains the configuration of game session based on game mode and server type. This function also uses StartMatchmakingAsync from MatchmakingSessionWrapper that is responsible to create the match ticket.

    protected internal async void StartP2PMatchmaking(InGameMode inGameMode)
    {
    isMatchTicketExpired = false;
    selectedInGameMode = inGameMode;

    Dictionary<InGameMode,
    Dictionary<GameSessionServerType,
    SessionV2GameSessionCreateRequest>> sessionConfig = GameSessionConfig.SessionCreateRequest;

    if (!sessionConfig.TryGetValue(selectedInGameMode, out var matchTypeDict))
    {
    BytewarsLogger.LogWarning("Matchtype Not Found");
    return;
    }

    if (!matchTypeDict.TryGetValue(gameSessionServerType, out var request))
    {
    BytewarsLogger.LogWarning("SessionV2GameSessionCreateRequest Not Found");
    return;
    }

    ConnectionHandler.Initialization();
    await StartMatchmakingAsync(request.matchPool, ConnectionHandler.IsUsingLocalDS());
    GameManager.Instance.OnClientLeaveSession += OnClientLeave;
    }
  2. To cancel matchmaking, use this function:

    protected internal void CancelP2PMatchmaking()
    {
    if (isMatchTicketExpired)
    {
    BytewarsLogger.Log($"match ticket : {matchTicket} is expired {isMatchTicketExpired}");
    return;
    }

    CancelMatchmaking(matchTicket);
    GameManager.Instance.OnClientLeaveSession -= OnClientLeave;
    }
  3. You will bind and unbind all the events in this class:

    private void RegisterMatchmakingEventListener()
    {
    BindMatchmakingStartedNotification();
    BindMatchFoundNotification();
    BindMatchmakingExpiredNotification();
    BindOnInviteToGameSessionNotification();
    BindOnUserJoinedSessionNotification();

    OnMatchStarted += OnMatchStartedCallback;
    OnMatchFound += OnMatchFoundCallback;
    OnMatchExpired += OnMatchExpiredCallback;
    OnMatchTicketCreated += OnMatchTicketCreatedCallback;
    OnMatchTicketDeleted += OnMatchTicketDeletedCallback;
    OnInviteToGameSession += OnInviteToGameSessionCallback;
    OnJoinedGameSession += OnJoinedGameSessionCallback;
    }
    private void DeRegisterMatchmakingEventListener()
    {
    UnBindMatchmakingStartedNotification();
    UnBindMatchFoundNotification();
    UnBindMatchmakingExpiredNotification();
    UnBindOnInviteToGameSessionNotification();
    UnBindOnUserJoinedSessionNotification();

    OnMatchStarted -= OnMatchStartedCallback;
    OnMatchFound -= OnMatchFoundCallback;
    OnMatchExpired -= OnMatchExpiredCallback;
    OnMatchTicketCreated -= OnMatchTicketCreatedCallback;
    OnMatchTicketDeleted -= OnMatchTicketDeletedCallback;
    OnInviteToGameSession -= OnInviteToGameSessionCallback;
    OnJoinedGameSession -= OnJoinedGameSessionCallback;
    }
  4. Once you receive the OnMatchfound notification from the lobby, you will join the game session. Go to StartJoinToGameSession and update the function based on the code below. In this function, you will connect to the game session using JoinSession from the SessionEssentials module.

    private async void StartJoinToGameSession(string sessionId)
    {
    OnJoinSessionCompleteEvent += OnJoiningSessionCompletedAsync;
    OnMatchmakingWithP2PJoinSessionStarted?.Invoke();

    // Wait a moment to ensure the host information is received before join the game session.
    await Task.Delay(1000);

    JoinSession(sessionId);
    }
  5. You will leave the session once the game has ended or when the game client does not receive any P2P update notifications. To leave the session, you need to call LeaveSession from the session essentials and pass the session ID into it.

    public void LeaveCurrentSession()
    {
    LeaveSession(cachedSessionId);
    Reset(true);
    }
  6. To handle the player joining the game session, create a callback function. Within this function, check the P2P status. You can initiate the P2P connection once you receive the status NEED_TO_REQUEST from the backend.

    private async void OnJoiningSessionCompletedAsync(Result<SessionV2GameSession> result)
    {
    // Wait a moment to ensure the host information is received.
    await Task.Delay(1000);

    if (result.IsError)
    {
    BytewarsLogger.Log($"Error : {result.Error.Message}");
    OnMatchmakingWithP2PError?.Invoke(result.Error.Message);
    return;
    }

    OnJoinSessionCompleteEvent -= OnJoiningSessionCompletedAsync;
    OnGetSessionDetailsCompleteEvent -= OnJoiningSessionCompletedAsync;
    BytewarsLogger.Log($"Joined to session : {cachedSessionId} - Waiting DS Status");

    bool isLeader = result.Value.leaderId == GameData.CachedPlayerState.playerId;
    OnMatchmakingWithP2PJoinSessionCompleted?.Invoke(isLeader);

    GetP2PStatus(result.Value);
    }
    private async void GetP2PStatus(SessionV2GameSession session)
    {
    SessionV2DsInformation dsInfo = session.dsInformation;
    switch (dsInfo.status)
    {
    case SessionV2DsStatus.NEED_TO_REQUEST:
    BytewarsLogger.LogWarning($"DS Status: {dsInfo.status}");

    // Wait a moment to ensure the host information is received before connect to the host.
    await Task.Delay(1000);

    StartP2PConnection(GameData.CachedPlayerState.playerId, session);
    UnbindMatchmakingEvent();
    break;
    }
    }
  7. Handling the P2P connection requires distinguishing between the host and client roles. To address this, create a function called StartP2PConnection. Within this function, designate the leader as the host in the P2P connection. Substitute the function's current code with this revised version.

    private void StartP2PConnection(string currentUserId, SessionV2GameSession gameSession)
    {
    if (string.IsNullOrWhiteSpace(gameSession.leaderId))
    {
    OnMatchmakingWithP2PError?.Invoke($"GameSession.id {gameSession.id} has empty/null leader id: {gameSession.leaderId}");
    Reset(false);
    return;
    }

    if (currentUserId.Equals(gameSession.leaderId))
    {
    GameData.ServerSessionID = gameSession.id;
    P2PHelper.StartAsHost(selectedInGameMode, gameSession.id);
    }
    else
    {
    P2PHelper.StartAsP2PClient(gameSession.leaderId, selectedInGameMode, gameSession.id);
    }

    Reset(false);
    }

Resources