Skip to main content

Implement SDK - Playing with Friends - (Unity module)

Last updated on July 28, 2025
note

Make sure you have completed the Session Essentials tutorial module before proceeding, as this page depends on functions you implemented in that module.

Unwrap the wrapper

Byte Wars uses a wrapper class called PlayingWithFriendsWrapper to integrate with the AccelByte Gaming Services (AGS) Software Development Kit (SDK). This wrapper leverages the User, Session, and Lobby interfaces provided by the SDK. In this tutorial, you'll work with a starter version of the wrapper that allows you to implement the required functionality from scratch.

What's in the Starter Pack

To follow along with this tutorial, a starter subsystem class named PlayingWithFriendsWrapper_Starter is provided. You can find it in the Resources section. It includes the following files:

  • CS file: Assets/Resources/Modules/Play/PlayingWithFriends/Scripts/PlayingWithFriendsWrapper_Starter.cs

The PlayingWithFriendsWrapper_Starter class is derived from SessionEssentialsWrapper_Starter that you have set up in Session Essentials module, which is derived from AccelByteWarsOnlineSession class. This wrapper uses functions and variables from its inherited class.

The AccelByteWarsOnlineSession class includes several helpful components:

  • Cached current session data.
public static SessionV2GameSession CachedSession { get; protected set; }
  • Static references of the AGS SDK interfaces that you'll use in this page.
protected static User User;
protected static Lobby Lobby;
protected static Session Session;
  • A function to handle client travel to Dedicated Server based on the session info.
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);
});
}
  • A function to handle client travel to P2P host based on the session info. This function uses AccelByte’s networking plugin, AccelByteNetworkTransportManager.
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 SessionEssentialsWrapper_Starter class includes several helpful components:

  • A function to send game session invitation.
public override void SendGameSessionInvite(
string sessionId,
string inviteeUserId,
ResultCallback onComplete)
{
Session.InviteUserToGameSession(
sessionId,
inviteeUserId,
(result) =>
{
if (result.IsError)
{
BytewarsLogger.LogWarning(
$"Failed to send game session invite to {inviteeUserId}. " +
$"Error {result.Error.Code}: {result.Error.Message}");
}
else
{
BytewarsLogger.Log($"Success to send game session invite to {inviteeUserId}");
}

onComplete?.Invoke(result);
});
}
  • A function to join a game session.
public override void JoinGameSession(
string sessionId,
ResultCallback<SessionV2GameSession> onComplete)
{
// Leave the existing session before joining a new session.
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 join game session. Error {leaveResult.Error.Code}: {leaveResult.Error.Message}");
onComplete?.Invoke(Result<SessionV2GameSession>.CreateError(leaveResult.Error));
return;
}

JoinGameSession(sessionId, onComplete);
});
return;
}

// Join a new session.
Session.JoinGameSession(sessionId, (result) =>
{
if (result.IsError)
{
BytewarsLogger.LogWarning($"Failed to join game session. Error {result.Error.Code}: {result.Error.Message}");
}
else
{
BytewarsLogger.Log($"Success to join game session. Session id: {result.Value.id}");
}

CachedSession = result.Value;
onComplete?.Invoke(result);
});
}
  • A function to reject game session invite.
public override void RejectGameSessionInvite(
string sessionId,
ResultCallback onComplete)
{
Session.RejectGameSessionInvitation(sessionId, (result) =>
{
if (result.IsError)
{
BytewarsLogger.LogWarning(
$"Failed to reject game session with ID: {sessionId}. " +
$"Error {result.Error.Code}: {result.Error.Message}");
}
else
{
BytewarsLogger.Log($"Success to reject game session. Session ID: {sessionId}");
}

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

This wrapper uses a model class located at Assets/Resources/Modules/Play/OnlineSessionUtils/AccelByteWarsOnlineSessionModels.cs to determine the game mode of the current session. Byte Wars uses the session name to determine what game mode a given session is.

public static InGameMode GetGameSessionGameMode(SessionV2GameSession session)
{
if (session == null)
{
return InGameMode.None;
}

bool isMatchmaking = string.IsNullOrEmpty(session.matchPool);
switch (session.configuration.name)
{
case EliminationDSSessionTemplateName:
case EliminationDSAMSSessionTemplateName:
case EliminationP2PSessionTemplateName:
return isMatchmaking ? InGameMode.MatchmakingElimination : InGameMode.CreateMatchElimination;
case TeamDeathmatchDSSessionTemplateName:
case TeamDeathmatchDSAMSSessionTemplateName:
case TeamDeathmatchP2PSessionTemplateName:
return isMatchmaking ? InGameMode.MatchmakingTeamDeathmatch : InGameMode.CreateMatchTeamDeathmatch;
default:
return InGameMode.None;
}
}

There is also a model class located at Assets/Resources/Modules/Play/PlayingWithFriends/Scripts/PlayingWithFriendsModels.cs that stores the text used in notifications.

public static readonly string SendGameSessionInviteErrorNotInSession = "Not currently in any game session.";
public static readonly string SendGameSessionInviteSuccess = "Invitation sent.";
public static readonly string SendGameSessionInviteError = "Fail to send invite.";
public static readonly string InviteReceived = " invites to join game session.";
public static readonly string InviteRejected = " rejected invitation.";
public static readonly string InviteAccept = "Accept";
public static readonly string InviteReject = "Reject";

Implement sending invitation

Open the PlayingWithFriendsWrapper_Starter class and create a new function using the code below. This function sends an invitation request to a given user ID, displays a notification, and triggers the onComplete delegate. It uses the cached session set by other session modules. You can obtain this session ID from the response of session creation or join session.

public void SendInviteToCurrentGameSession(string userId, ResultCallback onComplete)
{
SendGameSessionInvite(CachedSession.id, userId, (Result result) =>
{
// Display notification.
MenuManager.Instance.PushNotification(new PushNotificationModel
{
Message = result.IsError ? PlayingWithFriendsModels.SendGameSessionInviteError : PlayingWithFriendsModels.SendGameSessionInviteSuccess,
UseDefaultIconOnEmpty = true
});
onComplete?.Invoke(result);
});
}

Implement receiving invitation

  1. Create a new function using the code below. This function handles incoming game session invitations. The invitation payload only includes the session ID, but Byte Wars also needs to show the sender's display name in the popup. To achieve this, two requests must be made in sequence: retrieve session details to get the sender’s user ID; and retrieve the user's public info to get the sender’s display name. Then, it will show the popup to either accept or reject invitation. Note that the accepting invitation is done by calling join session.

    private void OnGameSessionInviteReceived(Result<SessionV2GameInvitationNotification> notif)
    {
    if (notif.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to handle received game session invitation. Error {notif.Error.Code}: {notif.Error.Message}");
    return;
    }

    // Construct local function to display push notification.
    void OnGetSenderInfoCompleted(Result<AccountUserPlatformInfosResponse> result)
    {
    AccountUserPlatformData senderInfo = result.IsError ? null : result.Value.Data[0];
    if (senderInfo == null)
    {
    BytewarsLogger.LogWarning($"Failed to get sender info. Error {result.Error.Code}: {result.Error.Message}");

    return;
    }

    MenuManager.Instance.PushNotification(new PushNotificationModel
    {
    Message = senderInfo.DisplayName + PlayingWithFriendsModels.InviteReceived,
    IconUrl = senderInfo.AvatarUrl,
    UseDefaultIconOnEmpty = true,
    ActionButtonTexts = new string[]
    {
    PlayingWithFriendsModels.InviteAccept,
    PlayingWithFriendsModels.InviteReject
    },
    ActionButtonCallback = (PushNotificationActionResult actionResult) =>
    {
    switch (actionResult)
    {
    // Show accept party invitation confirmation.
    case PushNotificationActionResult.Button1:
    JoinGameSession(notif.Value.sessionId, (Result<SessionV2GameSession> result) =>
    {
    OnJoinGameSessionCompleted(result, null);
    });
    break;

    // Reject party invitation.
    case PushNotificationActionResult.Button2:
    RejectGameSessionInvite(notif.Value.sessionId, null);
    break;
    }
    }
    });
    }

    // Construct local function to get sender info.
    void OnGetGameSessionDetailsCompleted(Result<SessionV2GameSession> result)
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to get game session details. Error {result.Error.Code}: {result.Error.Message}");

    return;
    }

    User.GetUserOtherPlatformBasicPublicInfo("ACCELBYTE", new string[] { result.Value.leaderId }, OnGetSenderInfoCompleted);
    }

    // Get session info.
    Session.GetGameSessionDetailsBySessionId(notif.Value.sessionId, OnGetGameSessionDetailsCompleted);
    }
  2. Create a handler function for the join session request using the code below. This function connects the player to the Dedicated Server (DS) or P2P host. For DS sessions, the game attempts to connect immediately. However, if the DS is not yet ready, this may fail. In such cases, the game listens to the SessionV2DsStatusChanged event to try again when the DS becomes available. For P2P, the game connects to the host immediately.

    private void OnJoinGameSessionCompleted(Result<SessionV2GameSession> result, ResultCallback<SessionV2GameSession> onComplete = null)
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to received game session user joined notification. Error {result.Error.Code}: {result.Error.Message}");
    onComplete?.Invoke(result);
    return;
    }

    // Update cached session ID.
    GameData.ServerSessionID = result.Value.id;

    SessionV2GameSession gameSession = result.Value;
    switch (gameSession.configuration.type)
    {
    case SessionConfigurationTemplateType.DS:
    if (
    gameSession.dsInformation == null ||
    gameSession.dsInformation.status != SessionV2DsStatus.AVAILABLE
    )
    {
    // Server is not ready, listen to DS event.
    Lobby.SessionV2DsStatusChanged += OnDsStatusChanged;
    }
    else
    {
    TravelToDS(gameSession, AccelByteWarsOnlineSessionModels.GetGameSessionGameMode(gameSession));
    }
    break;
    case SessionConfigurationTemplateType.P2P:
    TravelToP2PHost(gameSession, AccelByteWarsOnlineSessionModels.GetGameSessionGameMode(gameSession));
    break;
    default:
    break;
    }

    onComplete?.Invoke(result);
    }
  3. Create a function to retry connecting to the DS using the code below. As explained above, if the DS isn’t ready yet, the game will keep trying to connect on each DS update.

    private void OnDsStatusChanged(Result<SessionV2DsStatusUpdatedNotification> result)
    {
    Lobby.SessionV2DsStatusChanged -= OnDsStatusChanged;

    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Dedicated server information received with error. Error {result.Error.Code}: {result.Error.Message}");
    return;
    }
    if (
    result.Value.session.dsInformation == null ||
    result.Value.session.dsInformation.status != SessionV2DsStatus.AVAILABLE
    )
    {
    TravelToDS(result.Value.session, AccelByteWarsOnlineSessionModels.GetGameSessionGameMode(result.Value.session));
    }
    else
    {
    BytewarsLogger.LogWarning("Failed to travel to dedicated server. Dedicated server information not found.");
    }
    }
  4. Locate the OnEnable() function and replace the existing function with the code below to bind the invitation handler to the SDK's event.

    private void OnEnable()
    {
    Lobby.SessionV2InvitedUserToGameSession += OnGameSessionInviteReceived;
    }
  5. Locate the OnDisable() function and replace the existing function with the code below to unbind the handler, ensuring clean shutdown behavior.

    private void OnDisable()
    {
    Lobby.SessionV2InvitedUserToGameSession -= OnGameSessionInviteReceived;
    }

Implement invitation rejection notification

  1. Create a handler function for the invitation rejection notification using the code below. This displays a notification to the inviter that their invite was rejected.

    private void OnGameSessionInviteRejected(Result<SessionV2GameInvitationRejectedNotification> notif)
    {
    if (notif.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to received game session user joined notification. Error {notif.Error.Code}: {notif.Error.Message}");
    return;
    }

    // Construct local function to display push notification.
    void OnGetSenderInfoCompleted(Result<AccountUserPlatformInfosResponse> result)
    {
    AccountUserPlatformData receiverInfo = result.IsError ? null : result.Value.Data[0];
    if (receiverInfo == null)
    {
    BytewarsLogger.LogWarning($"Failed to get sender info. Error {result.Error.Code}: {result.Error.Message}");
    return;
    }

    MenuManager.Instance.PushNotification(new PushNotificationModel
    {
    Message = receiverInfo.UniqueDisplayName + PlayingWithFriendsModels.InviteRejected,
    IconUrl = receiverInfo.AvatarUrl,
    UseDefaultIconOnEmpty = true
    });
    }

    // Construct local function to get sender info.
    User.GetUserOtherPlatformBasicPublicInfo("ACCELBYTE", new string[] { notif.Value.rejectedId }, OnGetSenderInfoCompleted);
    }
  2. Locate the OnEnable() function and replace the existing function with the code below to bind the rejection handler to the SDK’s event.

    private void OnEnable()
    {
    Lobby.SessionV2InvitedUserToGameSession += OnGameSessionInviteReceived;
    Lobby.SessionV2UserRejectedGameSessionInvitation += OnGameSessionInviteRejected;
    }
  3. Locate the OnDisable() function and replace the existing function with the code below to unbind the handler, ensuring clean shutdown behavior.

    private void OnDisable()
    {
    Lobby.SessionV2InvitedUserToGameSession -= OnGameSessionInviteReceived;
    Lobby.SessionV2UserRejectedGameSessionInvitation -= OnGameSessionInviteRejected;
    }

Resources