Skip to main content

Implementing Online Subsystem - Play with friends - (Unreal Engine module)

Last updated on October 24, 2024

Game session invitation flow

Here's how the invite friends flow works. Do note that functionality mentioned in the sub-graph Handled by Match Session module has been implemented by you when you completed the Joinable sessions (dedicated server or peer-to-peer) module, and the code that isn't specific to Byte Wars is not necessary for it to function. You don't need to take additional steps for those two sub-graphs.

Unwrap the subsystem

A subsystem starter class has been provided so you can get started implementing right away. You can find the PlayingWithFriendsSubsystem_Starter class files at the following locations:

  • Header file: /Source/AccelByteWars/TutorialModules/Play/PlayingWithFriends/PlayingWithFriendsSubsystem_Starter.h
  • CPP file: /Source/AccelByteWars/TutorialModules/Play/PlayingWithFriends/PlayingWithFriendsSubsystem_Starter.cpp

Take a look at what has been provided in the class.

  • In the PlayingWithFriendsSubsystem_Starter Header file, you will see some functions encased in a region called Helper.

    public:
    // ...
    bool IsInMatchSessionGameSession() const;
    private:
    // ...
    bool IsMatchSessionGameSessionReceivedServer() const;

    FUniqueNetIdRef GetSessionOwnerUniqueNetId(const FName SessionName) const;
    UPromptSubsystem* GetPromptSubsystem() const;

    FOnlineSessionV2AccelBytePtr GetSessionInterface() const;
    FOnlineIdentityAccelBytePtr GetIdentityInterface() const;

    void JoinGameSessionConfirmation(const int32 LocalUserNum, const FOnlineSessionInviteAccelByte& Invite);
    void OnQueryUserInfoOnGameSessionParticipantChange(
    const FOnlineError& Error,
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo,
    FName SessionName,
    const bool bJoined);
    • IsInMatchSessionGameSession is a function to make sure that the current game session is a Match Session, which was covered in the Joinable sessions (dedicated server or peer-to-peer) module.
    • GetSessionOwnerUniqueNetId is a simple function to retrieve the session's owner unique net ID from the session name.
    • GetPromptSubsystem is a Byte Wars-specific function to retrieve a subsystem that handles global prompt UI, such as popup notifications, popup loading screens, popup confirmation screens, and push notifications.
    • OnQueryUserInfoOnGameSessionParticipantChange is also a Byte Wars-specific function and is needed to retrieve the username from the unique net ID contained within the OnGameSessionParticipantChange notification.
  • Still in the Header file, you will see these variable declarations:

    private:
    // ...
    UPROPERTY()
    UAccelByteWarsOnlineSessionBase* OnlineSession;

    FUniqueNetIdPtr LeaderId;
    bool bLeaderChanged = false;
    • OnlineSession is a pointer to the Online Session class you set up in the Joinable sessions (dedicated server or peer-to-peer) module.
    • LeaderId, as the name suggest, is the cached current leader of the game session's ID. Since the OnGameSessionParticipantChange does not have a way to tell whether the leader has changed or not, we need to store the current leader ID and compare it manually against the one session info stored in the Online Subsystem (OSS).
    • bLeaderChanged is a flag to indicate whether the leader has changed or not.

Send session invite

  1. Open the PlayingWithFriendsSubsystem_Starter Header file and add the following declarations:

    public:
    void SendGameSessionInvite(const APlayerController* Owner, const FUniqueNetIdPtr Invitee) const;
  2. Open the PlayingWithFriendsSubsystem_Starter CPP file and add the implementations below. Here, you simply call the SendSessionInvite immediately.

    void UPlayingWithFriendsSubsystem_Starter::SendGameSessionInvite(const APlayerController* Owner, const FUniqueNetIdPtr Invitee) const
    {
    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Called"));

    OnlineSession->SendSessionInvite(
    OnlineSession->GetLocalUserNumFromPlayerController(Owner),
    OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
    Invitee);
    }
  3. Go back to the Header file and add a public delegate as a way for the user interface (UI) to trigger something when the response is received.

    public:
    // ...
    FOnSendSessionInviteComplete OnSendGameSessionInviteCompleteDelegates;
  4. Still in the Header file, add this function declaration that will be called when the response to SendSessionInvite is received.

    private:
    void OnSendGameSessionInviteComplete(
    const FUniqueNetId& LocalSenderId,
    FName SessionName,
    bool bSucceeded,
    const FUniqueNetId& InviteeId) const;
  5. Open the CPP file and add the implementations below. This makes sure that the current session is a game session first, shows a push notification indicating that the request was successfully sent, and triggers the delegate that you just added.

    void UPlayingWithFriendsSubsystem_Starter::OnSendGameSessionInviteComplete(
    const FUniqueNetId& LocalSenderId,
    FName SessionName,
    bool bSucceeded,
    const FUniqueNetId& InviteeId) const
    {
    // Abort if not a game session.
    if (!OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
    {
    return;
    }

    // Only handle the event if the game session is in the game server.
    if (!IsMatchSessionGameSessionReceivedServer())
    {
    return;
    }

    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Succeedded: %s"), *FString(bSucceeded ? TEXT("TRUE") : TEXT("FALSE")));

    if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    return;
    }

    // show sent push notification
    if (UPromptSubsystem* PromptSubsystem = GetPromptSubsystem())
    {
    PromptSubsystem->PushNotification(TEXT_INVITE_SENT);
    }

    OnSendGameSessionInviteCompleteDelegates.Broadcast(LocalSenderId, SessionName, bSucceeded, InviteeId);
    }
  6. Bind the response function to the delegate from Online Session. In the CPP file, navigate to Initialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    OnlineSession->GetOnSendSessionInviteCompleteDelegates()->AddUObject(
    this, &ThisClass::OnSendGameSessionInviteComplete);
    // ...
    }
  7. Unbind them when they're not needed. Still in the CPP file, navigate to Deinitialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Deinitialize()
    {
    // ...
    OnlineSession->GetOnSendSessionInviteCompleteDelegates()->RemoveAll(this);
    // ...
    }

Accept session invitation

  1. Open PlayingWithFriendsSubsystem_Starter Header file and add the following function declaration:

    private:
    // ...
    void JoinGameSession(const int32 LocalUserNum, const FOnlineSessionSearchResult& Session) const;
  2. Move on to the CPP file and add the implementations below. Here, call the JoinSession function from Online Session with a predefined parameter to indicate we want to join a game session.

    void UPlayingWithFriendsSubsystem_Starter::JoinGameSession(const int32 LocalUserNum, const FOnlineSessionSearchResult& Session) const
    {
    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Called"));

    OnlineSession->JoinSession(
    LocalUserNum,
    OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession),
    Session);
    }
  3. Return to the Header file and add this callback function declaration.

    private:
    // ...
    void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type CompletionType) const;
  4. Open the CPP file and add the following implementations. Here, you want to show an error popup screen if the joining failed.

    void UPlayingWithFriendsSubsystem_Starter::OnJoinSessionComplete(
    FName SessionName,
    EOnJoinSessionCompleteResult::Type CompletionType) const
    {
    // Abort if not a game session.
    if (!OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
    {
    return;
    }

    // Only handle the event if the game session is in the game server.
    if (!IsMatchSessionGameSessionReceivedServer())
    {
    return;
    }

    const bool bSucceeded = CompletionType == EOnJoinSessionCompleteResult::Success;
    FText ErrorMessage;

    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Succeedded: %s"), *FString(bSucceeded ? TEXT("TRUE") : TEXT("FALSE")));

    switch (CompletionType)
    {
    case EOnJoinSessionCompleteResult::Success:
    ErrorMessage = FText();
    break;
    case EOnJoinSessionCompleteResult::SessionIsFull:
    ErrorMessage = TEXT_FAILED_SESSION_FULL_PLAYING_WITH_FRIENDS;
    break;
    case EOnJoinSessionCompleteResult::SessionDoesNotExist:
    ErrorMessage = TEXT_FAILED_SESSION_NULL_PLAYING_WITH_FRIENDS;
    break;
    case EOnJoinSessionCompleteResult::CouldNotRetrieveAddress:
    ErrorMessage = TEXT_FAILED_TO_JOIN_SESSION_PLAYING_WITH_FRIENDS;
    break;
    case EOnJoinSessionCompleteResult::AlreadyInSession:
    ErrorMessage = TEXT_FAILED_ALREADY_IN_SESSION_PLAYING_WITH_FRIENDS;
    break;
    case EOnJoinSessionCompleteResult::UnknownError:
    ErrorMessage = TEXT_FAILED_TO_JOIN_SESSION_PLAYING_WITH_FRIENDS;
    break;
    default:
    ErrorMessage = FText();
    }

    if (UPromptSubsystem* PromptSubsystem = GetPromptSubsystem(); !bSucceeded && PromptSubsystem)
    {
    PromptSubsystem->PushNotification(ErrorMessage);
    }
    }
  5. Bind the response function to the delegate from Online Session. In the CPP file, navigate to Initialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    OnlineSession->GetOnJoinSessionCompleteDelegates()->AddUObject(
    this, &ThisClass::OnJoinSessionComplete);
    // ...
    }
  6. Unbind them when they're not needed. Still in the CPP file, navigate to Deinitialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Deinitialize()
    {
    // ...
    OnlineSession->GetOnJoinSessionCompleteDelegates()->RemoveAll(this);
    // ...
    }

Reject session invitation

  1. Open the PlayingWithFriendsSubsystem_Starter Header file and add the following function declaration:

    public:
    // ...
    void RejectGameSessionInvite(const APlayerController* Owner, const FOnlineSessionInviteAccelByte& Invite) const;
  2. Open the CPP file and add the following implementations:

    void UPlayingWithFriendsSubsystem_Starter::RejectGameSessionInvite(
    const APlayerController* Owner,
    const FOnlineSessionInviteAccelByte& Invite) const
    {
    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Called"));

    OnlineSession->RejectSessionInvite(
    OnlineSession->GetLocalUserNumFromPlayerController(Owner),
    Invite);
    }
  3. Go back to the Header file and add a public delegate as a way for the UI to trigger something when the response is received.

    public:
    // ...
    FOnRejectSessionInviteCompleteMulticast OnRejectGameSessionInviteCompleteDelegates;
  4. Still in the Header file and add this callback function declaration:

    private:
    // ...
    void OnRejectGameSessionInviteComplete(bool bSucceeded) const;
  5. Open the CPP file and add the following implementations:

    void UPlayingWithFriendsSubsystem_Starter::OnRejectGameSessionInviteComplete(bool bSucceeded) const
    {
    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Succeedded: %s"), *FString(bSucceeded ? TEXT("TRUE") : TEXT("FALSE")));

    OnRejectGameSessionInviteCompleteDelegates.Broadcast(bSucceeded);
    }
  6. Bind the response function to the delegate from Online Session. In the CPP file, navigate to Initialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    OnlineSession->GetOnRejectSessionInviteCompleteDelegate()->AddUObject(
    this, &ThisClass::OnRejectGameSessionInviteComplete);
    // ...
    }
  7. Unbind them when they're not needed. Still in the CPP file, navigate to Deinitialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Deinitialize()
    {
    // ...
    OnlineSession->GetOnRejectSessionInviteCompleteDelegate()->RemoveAll(this);
    // ...
    }

Receive session invitation

Byte Wars needs to show the invitee's username when receiving an invitation, which requires QueryUserInfo since the invitation notification only contains the unique net ID of the invitee. This section walks you through this implementation the way Byte Wars requires.

  1. Open PlayingWithFriendsSubsystem_Starter the Header file and add the below function declaration. This function will act as the entry function that will be called when the player is invited.

    private:
    // ...
    void OnGameSessionInviteReceived(
    const FUniqueNetId& UserId,
    const FUniqueNetId& FromId,
    const FOnlineSessionInviteAccelByte& Invite);
  2. Open the CPP file and add the implementations below. Here, most of the implementation is to accommodate the QueryUserInfo. What you need to pay attention to is the call to ShowInviteReceivedPopup, which at this point will show an error for you. You will declare and implement that in the next step.

    void UPlayingWithFriendsSubsystem_Starter::OnGameSessionInviteReceived(
    const FUniqueNetId& UserId,
    const FUniqueNetId& FromId,
    const FOnlineSessionInviteAccelByte& Invite)
    {
    /* Make sure it is a game session.
    * Also check if the invite is not from party leader.
    * Since the party members will automatically join, there is no need to show game session invitation notification.*/
    if (UserId == FromId ||
    Invite.SessionType != EAccelByteV2SessionType::GameSession ||
    OnlineSession->IsPartyLeader(FromId.AsShared()))
    {
    return;
    }

    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Invite received from: %s"), *FromId.ToDebugString());

    const APlayerController* PlayerController = OnlineSession->GetPlayerControllerByUniqueNetId(UserId.AsShared());
    if (!PlayerController)
    {
    return;
    }

    const int32 LocalUserNum = OnlineSession->GetLocalUserNumFromPlayerController(PlayerController);
    if (LocalUserNum == INDEX_NONE)
    {
    return;
    }

    const FUniqueNetIdAccelByteUserRef FromABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(FromId.AsShared());
    if (UStartupSubsystem* StartupSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UStartupSubsystem>())
    {
    StartupSubsystem->QueryUserInfo(
    0,
    TPartyMemberArray{FromABId},
    FOnQueryUsersInfoCompleteDelegate::CreateWeakLambda(this, [this, Invite, LocalUserNum](
    const FOnlineError& Error,
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo)
    {
    /**
    * For some reason, calling ShowInviteReceivedPopup through CreateUObject crashes the game.
    * WeakLambda used in place of that.
    */
    if (Error.bSucceeded)
    {
    ShowInviteReceivedPopup(UsersInfo, LocalUserNum, Invite);
    }
    }));
    }
    }
  3. Go back to the Header file and add the following function declaration:

    private:
    // ...
    void ShowInviteReceivedPopup(
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo,
    const int32 LocalUserNum, const FOnlineSessionInviteAccelByte Invite);
  4. Move to the CPP file and add the implementation below. Do remember that the PromptSubsystem is a Byte Wars system, not an Unreal Engine system. The implementation will show a popup prompt screen with two buttons: one for accept and one for reject. Pay attention to the highlighted lines below. Those bind the accept and reject buttons to the corresponding functionality. Also, notice that it calls JoinGameSessionConfirmation in this implementation, which is a part of the helper function.

    void UPlayingWithFriendsSubsystem_Starter::ShowInviteReceivedPopup(
    const TArray<TSharedPtr<FUserOnlineAccountAccelByte>>& UsersInfo,
    const int32 LocalUserNum,
    const FOnlineSessionInviteAccelByte Invite)
    {
    UPromptSubsystem* PromptSubsystem = GetPromptSubsystem();
    if (UsersInfo.IsEmpty() || !PromptSubsystem)
    {
    return;
    }

    const TSharedPtr<FUserOnlineAccountAccelByte> User = UsersInfo[0];
    const FText Message = FText::Format(
    TEXT_FORMAT_INVITED,
    FText::FromString(User->GetDisplayName().IsEmpty() ?
    UTutorialModuleOnlineUtility::GetUserDefaultDisplayName(User->GetUserId().Get()) :
    User->GetDisplayName()));

    FString AvatarURL;
    User->GetUserAttribute(ACCELBYTE_ACCOUNT_GAME_AVATAR_URL, AvatarURL);

    PromptSubsystem->PushNotification(
    Message,
    AvatarURL,
    true,
    TEXT_ACCEPT_INVITE,
    TEXT_REJECT_INVITE,
    FText(),
    FPushNotificationDelegate::CreateWeakLambda(
    this,
    [this, Invite, LocalUserNum](EPushNotificationActionResult ActionButtonResult)
    {
    switch (ActionButtonResult)
    {
    case EPushNotificationActionResult::Button1:
    JoinGameSessionConfirmation(LocalUserNum, Invite);
    break;
    case EPushNotificationActionResult::Button2:
    OnlineSession->RejectSessionInvite(LocalUserNum, Invite);
    break;
    default: /* Do nothing */;
    }
    }));
    }
  5. Navigate to the JoinGameSessionConfirmation function and add the highlighted lines from the code below. This will complete the functionality of showing a confirmation popup screen if the player is already in a session before joining another session.

    void UPlayingWithFriendsSubsystem_Starter::JoinGameSessionConfirmation(
    const int32 LocalUserNum,
    const FOnlineSessionInviteAccelByte& Invite)
    {
    if (OnlineSession->GetSession(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    UPromptSubsystem* PromptSubsystem = GetPromptSubsystem();
    if (!PromptSubsystem)
    {
    return;
    }

    PromptSubsystem->ShowDialoguePopUp(
    TEXT_LEAVING_SESSION,
    TEXT_JOIN_NEW_SESSION_CONFIRMATION,
    EPopUpType::ConfirmationConfirmCancel,
    FPopUpResultDelegate::CreateWeakLambda(this, [this, LocalUserNum, Invite](EPopUpResult Result)
    {
    switch (Result)
    {
    case Confirmed:
    JoinGameSession(LocalUserNum, Invite.Session);
    break;
    case Declined:
    OnlineSession->RejectSessionInvite(LocalUserNum, Invite);
    break;
    }
    }));
    }
    else
    {
    UPromptSubsystem* PromptSubsystem = GetPromptSubsystem();
    if (!PromptSubsystem)
    {
    return;
    }

    PromptSubsystem->ShowLoading(TEXT_JOINING_SESSION);
    JoinGameSession(LocalUserNum, Invite.Session);
    }
    }
  6. Bind the function to the delegate from Online Session. In the CPP file, navigate to Initialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    OnlineSession->GetOnSessionInviteReceivedDelegates()->AddUObject(
    this, &ThisClass::OnGameSessionInviteReceived);
    // ...
    }
  7. Unbind them when they're not needed. Still in the CPP file, navigate to Deinitialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Deinitialize()
    {
    // ...
    OnlineSession->GetOnSessionInviteReceivedDelegates()->RemoveAll(this);
    // ...
    }

Game session member changed notification

All the session members need a way to know when another player joins or leaves the session. That is where this implementation comes in. Remember, however, that Byte Wars requires the call QueryUserInfo to retrieve the player's username.

  1. Open PlayingWithFriendsSubsystem_Starter Header file and add the following function declaration:

    private:
    // ...
    void OnGameSessionParticipantsChange(FName SessionName, const FUniqueNetId& Member, bool bJoined);
  2. Open PlayingWithFriendsSubsystem_Starter CPP file and add the implementation below. In this implementation, you will query the user info to retrieve their username, then you will show a push notification of whether the user has left or joined the session. There's also logic to determine whether the leader has changed. You can learn more about that implementation in the OnQueryUserInfoOnGameSessionParticipantChange function.

    void UPlayingWithFriendsSubsystem_Starter::OnGameSessionParticipantsChange(
    FName SessionName,
    const FUniqueNetId& Member,
    bool bJoined)
    {
    // Abort if not a game session.
    if (!OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession).IsEqual(SessionName))
    {
    return;
    }

    // Only handle the event if the game session is in the game server.
    if (!IsMatchSessionGameSessionReceivedServer())
    {
    return;
    }

    UE_LOG_PLAYINGWITHFRIENDS(Verbose, TEXT("Member %s: %s"), *FString(bJoined ? "JOINED" : "LEFT"), *Member.ToDebugString());

    const int32 LocalUserNum = OnlineSession->GetLocalUserNumFromPlayerController(
    OnlineSession->GetPlayerControllerByUniqueNetId(GetSessionOwnerUniqueNetId(SessionName)));
    if (LocalUserNum == INDEX_NONE)
    {
    return;
    }

    TArray<FUniqueNetIdRef> UsersToQuery = {Member.AsShared()};

    // check if leader changed
    if (const FNamedOnlineSession* Session = OnlineSession->GetSession(SessionName))
    {
    if (const TSharedPtr<FOnlineSessionInfoAccelByteV2> AbSessionInfo =
    StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(Session->SessionInfo))
    {
    const FUniqueNetIdPtr NewLeaderId = AbSessionInfo->GetLeaderId();
    if (LeaderId.IsValid())
    {
    if (!OnlineSession->CompareAccelByteUniqueId(
    FUniqueNetIdRepl(LeaderId),
    FUniqueNetIdRepl(NewLeaderId)))
    {
    UE_LOG_PLAYINGWITHFRIENDS(VeryVerbose, TEXT("Leader changed to: %s"), *LeaderId->ToDebugString());

    UsersToQuery.Add(NewLeaderId->AsShared());
    bLeaderChanged = true;
    }
    }
    LeaderId = NewLeaderId;
    }
    }

    if (UStartupSubsystem* StartupSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UStartupSubsystem>())
    {
    StartupSubsystem->QueryUserInfo(
    0,
    UsersToQuery,
    FOnQueryUsersInfoCompleteDelegate::CreateUObject(
    this,
    &ThisClass::OnQueryUserInfoOnGameSessionParticipantChange,
    SessionName,
    bJoined));
    }
    }
  3. Bind the function to the delegate from Online Session. Navigate to Initialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    OnlineSession->GetOnSessionParticipantsChange()->AddUObject(
    this, &ThisClass::OnGameSessionParticipantsChange);
    }
  4. Unbind that delegate. Still in the CPP file, navigate to Deinitialize and add the following code:

    void UPlayingWithFriendsSubsystem_Starter::Deinitialize()
    {
    // ...
    OnlineSession->GetOnSessionParticipantsChange()->RemoveAll(this);
    // ...
    }

Resources