メインコンテンツまでスキップ

OSS でフレンドを見つける - プレイヤーを検索する - (Unreal Engine モジュール)

Last updated on May 30, 2024

Unwrap the Subsystem

In this section, you will learn how to implement finding potential friends by their display name using AccelByte Gaming Services (AGS) Online Subsystem (OSS). In the Byte Wars project, there is already a Game Instance Subsystem created named FriendsSubsystem. This subsystem contains friends-related functionality, including finding potential friends. In this tutorial, you will use a starter version of that subsystem, the FriendsSubsystem_Starter, so you can implement friends-functionalities from scratch.

What's in the Starter Pack

To follow this tutorial, a starter subsystem class named FriendsSubsystem_Starter has been prepared for you. This class is available in the Resources sections and consists of the following files:

  • Header file: /Source/AccelByteWars/TutorialModules/Social/FriendsEssentials/FriendsSubsystem_Starter.h
  • CPP file: /Source/AccelByteWars/TutorialModules/Social/FriendsEssentials/FriendsSubsystem_Starter.cpp

The FriendsSubsystem_Starter class has several functionalities already included.

  • The AGS OSS interfaces declarations named FriendsInterface and UserInterface. You will use these interfaces to implement friends-related functionalities later.

    public:
    FOnlineUserAccelBytePtr UserInterface;
    FOnlineFriendsAccelBytePtr FriendsInterface;
  • A helper function to get UniqueNetId from a PlayerController. You will need these helpers for the AGS OSS interfaces mentioned above.

    FUniqueNetIdPtr UFriendsSubsystem_Starter::GetUniqueNetIdFromPlayerController(const APlayerController* PC) const
    {
    if (!PC)
    {
    return nullptr;
    }

    ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
    if (!LocalPlayer)
    {
    return nullptr;
    }

    return LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId();
    }
  • A helper function to get the LocalUserNum from a PlayerController. You will need these helpers for the AGS OSS interfaces later, too.

    int32 UFriendsSubsystem_Starter::GetLocalUserNumFromPlayerController(const APlayerController* PC) const
    {
    int32 LocalUserNum = 0;

    if (!PC)
    {
    return LocalUserNum;
    }

    const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
    if (LocalPlayer)
    {
    LocalUserNum = LocalPlayer->GetControllerId();
    }

    return LocalUserNum;
    }

In addition to the starter subsystem, there are some constants, delegates, and other helpers prepared in the /Source/AccelByteWars/TutorialModules/Social/FriendsEssentials/FriendsEssentialsModels.h file. In that file, you can find the following helpers:

  • An enum that defines a friends' status. You will need this to distinguish between friend types, such as whether the friend is already invited, already accepted as a friend, etc.

    UENUM()
    enum class EFriendStatus : uint8
    {
    Accepted = 0,
    PendingInbound,
    PendingOutbound,
    Blocked,
    Searched,
    Unknown
    };
  • A helper class named FriendData that contains the friend's information such as display name, avatar URL, and statuses. This data will be used to assign data to the entry widget, hence, we're using UObject instead of a struct or a regular CPP class.

    UCLASS()
    class ACCELBYTEWARS_API UFriendData : public UObject
    {
    GENERATED_BODY()

    public:
    UFriendData() : bIsOnline(false), bCannotBeInvited(false) {}

    FUniqueNetIdRepl UserId;
    FString DisplayName;
    FString AvatarURL;
    EFriendStatus Status = EFriendStatus::Unknown;

    bool bIsOnline;
    FDateTime LastOnline;

    bool bCannotBeInvited;
    FString ReasonCannotBeInvited;

    FString GetPresence() const
    {
    // If friend is online, simply return online.
    if (bIsOnline)
    {
    return NSLOCTEXT("AccelByteWars", "Online", "Online").ToString();
    }

    // Only check last online within a month.
    const FDateTime CurrentTime = FDateTime::UtcNow();
    if (CurrentTime.GetMonth() != LastOnline.GetMonth())
    {
    return NSLOCTEXT("AccelByteWars", "Last Online a Long Ago", "Last Online a Long Ago").ToString();
    }

    // Check last online in days.
    if (CurrentTime.GetDay() > LastOnline.GetDay())
    {
    const int32 Days = CurrentTime.GetDay() - LastOnline.GetDay();
    return FText::Format(NSLOCTEXT("AccelByteWars", "Last Online Day(s) Ago", "Last Online %d Day(s) Ago"), Days).ToString();
    }

    // Check last online in hours.
    if (CurrentTime.GetHour() > LastOnline.GetHour())
    {
    const int32 Hours = CurrentTime.GetHour() - LastOnline.GetHour();
    return FText::Format(NSLOCTEXT("AccelByteWars", "Last Online Hour(s) Ago", "Last Online %d Hour(s) Ago"), Hours).ToString();
    }

    // Check last online in minutes.
    if (CurrentTime.GetMinute() > LastOnline.GetMinute())
    {
    const int32 Minutes = CurrentTime.GetMinute() - LastOnline.GetMinute();
    return FText::Format(NSLOCTEXT("AccelByteWars", "Last Online Minute(s) Ago", "Last Online %d Minute(s) Ago"), Minutes).ToString();
    }
    else
    {
    return NSLOCTEXT("AccelByteWars", "Last Online a While Ago", "Last Online a While Ago").ToString();
    }
    }

    static UFriendData* ConvertToFriendData(TSharedRef<FOnlineUser> OnlineUser)
    {
    UFriendData* FriendData = NewObject<UFriendData>();

    FriendData->UserId = OnlineUser->GetUserId();
    FriendData->DisplayName = OnlineUser->GetDisplayName();
    OnlineUser->GetUserAttribute(ACCELBYTE_ACCOUNT_GAME_AVATAR_URL, FriendData->AvatarURL);
    FriendData->Status = EFriendStatus::Unknown;
    FriendData->bCannotBeInvited = false;

    return FriendData;
    }

    static UFriendData* ConvertToFriendData(TSharedRef<FOnlineFriend> OnlineUser)
    {
    UFriendData* FriendData = ConvertToFriendData(StaticCast<TSharedRef<FOnlineUser>>(OnlineUser));

    FriendData->bIsOnline = OnlineUser->GetPresence().bIsOnline;
    FriendData->LastOnline = OnlineUser->GetPresence().LastOnline;

    switch (OnlineUser->GetInviteStatus())
    {
    case EInviteStatus::Accepted:
    FriendData->Status = EFriendStatus::Accepted;
    FriendData->bCannotBeInvited = true;
    FriendData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "Already friend", "Already friend").ToString();
    break;
    case EInviteStatus::PendingInbound:
    FriendData->Status = EFriendStatus::PendingInbound;
    FriendData->bCannotBeInvited = true;
    FriendData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "You've been invited", "You've been invited").ToString();
    break;
    case EInviteStatus::PendingOutbound:
    FriendData->Status = EFriendStatus::PendingOutbound;
    FriendData->bCannotBeInvited = true;
    FriendData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "Already invited", "Already invited").ToString();
    break;
    case EInviteStatus::Blocked:
    FriendData->Status = EFriendStatus::Blocked;
    FriendData->bCannotBeInvited = true;
    FriendData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "Blocked", "Blocked").ToString();
    break;
    default:
    FriendData->Status = EFriendStatus::Unknown;
    FriendData->bCannotBeInvited = false;
    }

    return FriendData;
    }

    static UFriendData* ConvertToFriendData(TSharedRef<FOnlineBlockedPlayer> OnlineUser)
    {
    UFriendData* FriendData = ConvertToFriendData(StaticCast<TSharedRef<FOnlineUser>>(OnlineUser));

    FriendData->Status = EFriendStatus::Blocked;
    FriendData->bCannotBeInvited = true;
    FriendData->ReasonCannotBeInvited = NSLOCTEXT("AccelByteWars", "Blocked", "Blocked").ToString();

    return FriendData;
    }
    };
  • Delegates can be used as a callback upon caching friends' data completed, finding friends process completed, and sending friend requests completed.

    DECLARE_DELEGATE_ThreeParams(FOnGetCacheFriendListComplete, bool /*bWasSuccessful*/, TArray<TSharedRef<FOnlineFriend>>& /*CachedFriendList*/, const FString& /*ErrorMessage*/);
    DECLARE_DELEGATE_ThreeParams(FOnFindFriendComplete, bool /*bWasSuccessful*/, UFriendData* /*FriendData*/, const FString& /*ErrorMessage*/);
    DECLARE_DELEGATE_ThreeParams(FOnSendFriendRequestComplete, bool /*bWasSuccessful*/, UFriendData* /*FriendData*/, const FString& /*ErrorMessage*/);

Implement find friend

In this section, you will implement functionality to find potential friends by their display name.

  1. Before finding potential friends and sending them friend invitation requests, it is possible that the potential friends are already a friend to the player. Therefore, you need to get the player's friend list first. Then, you will use the list to check whether the found potential friends are already a friend or not. Open the FriendsSubsystem_Starter class Header file and declare the following function:

    protected:
    void GetCacheFriendList(const APlayerController* PC, const FOnGetCacheFriendListComplete& OnComplete = FOnGetCacheFriendListComplete());
  2. Create the definition for the function above. Open the FriendsSubsystem_Starter class CPP file and add the code below. The function will request to get the friend list from the backend and cache the list. Once the list is cached, you can directly access it without requesting to the backend again.

    void UFriendsSubsystem_Starter::GetCacheFriendList(const APlayerController* PC, const FOnGetCacheFriendListComplete& OnComplete)
    {
    if (!ensure(FriendsInterface))
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Cannot cache friend list. Friends Interface is not valid."));
    return;
    }

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);

    // Try to get cached friend list first.
    TArray<TSharedRef<FOnlineFriend>> CachedFriendList;
    if (FriendsInterface->GetFriendsList(LocalUserNum, TEXT(""), CachedFriendList))
    {
    // Then, update the cached friends' information by querying their user information.
    TPartyMemberArray FriendIds;
    for (const TSharedRef<FOnlineFriend>& CachedFriend : CachedFriendList)
    {
    FriendIds.Add(CachedFriend.Get().GetUserId());
    }

    // Create callback to handle queried friends' user information.
    OnQueryUserInfoCompleteDelegateHandle = UserInterface->AddOnQueryUserInfoCompleteDelegate_Handle(
    LocalUserNum,
    FOnQueryUserInfoCompleteDelegate::CreateWeakLambda(this, [this, OnComplete](int32 LocalUserNum, bool bWasSuccessful, const TArray<FUniqueNetIdRef>& UserIds, const FString& Error)
    {
    UserInterface->ClearOnQueryUserInfoCompleteDelegate_Handle(LocalUserNum, OnQueryUserInfoCompleteDelegateHandle);

    // Refresh friends data with queried friend's user information.
    TArray<TSharedRef<FOnlineFriend>> NewCachedFriendList;
    FriendsInterface->GetFriendsList(LocalUserNum, TEXT(""), NewCachedFriendList);
    for (const TSharedRef<FOnlineFriend>& NewCachedFriend : NewCachedFriendList)
    {
    // Update friend's avatar URL based on queried friend's user information.
    FString UserAvatarURL;
    TSharedPtr<FOnlineUser> UserInfo = UserInterface->GetUserInfo(LocalUserNum, NewCachedFriend.Get().GetUserId().Get());
    UserInfo->GetUserAttribute(ACCELBYTE_ACCOUNT_GAME_AVATAR_URL, UserAvatarURL);
    StaticCastSharedRef<FOnlineFriendAccelByte>(NewCachedFriend).Get().SetUserAttribute(ACCELBYTE_ACCOUNT_GAME_AVATAR_URL, UserAvatarURL);
    }

    OnComplete.ExecuteIfBound(true, NewCachedFriendList, TEXT(""));
    }
    ));

    // Query friends' user information.
    UserInterface->QueryUserInfo(LocalUserNum, FriendIds);
    }
    // If none, request to backend then get the cached the friend list.
    else
    {
    FriendsInterface->ReadFriendsList(
    LocalUserNum,
    TEXT(""),
    FOnReadFriendsListComplete::CreateWeakLambda(this, [this, OnComplete](int32 LocalUserNum, bool bWasSuccessful, const FString& ListName, const FString& Error)
    {
    TArray<TSharedRef<FOnlineFriend>> CachedFriendList;
    FriendsInterface->GetFriendsList(LocalUserNum, TEXT(""), CachedFriendList);

    OnComplete.ExecuteIfBound(bWasSuccessful, CachedFriendList, Error);
    }
    ));
    }
    }
  3. Back in the FriendsSubsystem_Starter class Header file, create a function declaration to find a potential friend.

    public:
    void FindFriend(const APlayerController* PC, const FString& InKeyword, const FOnFindFriendComplete& OnComplete = FOnFindFriendComplete());
  4. You will also need to create a callback to handle the find a potential friend process completion.

    protected:
    void OnFindFriendComplete(bool bWasSuccessful, const FUniqueNetId& UserId, const FString& DisplayName, const FUniqueNetId& FoundUserId, const FString& Error, int32 LocalUserNum, const FOnFindFriendComplete OnComplete);
  5. Create the definitions for the functions above. Open the FriendsSubsystem_Starter class CPP file and define the FindFriend() function. This function will try to get the cached friend list first by calling the GetCacheFriendList() function before performing the find a potential friend by its display name. The cached list will be used to check whether the found potential friend is already a friend or not. This check will be handled by the OnFindFriendComplete() function, which is the callback when the finding process is complete.

    void UFriendsSubsystem_Starter::FindFriend(const APlayerController* PC, const FString& InKeyword, const FOnFindFriendComplete& OnComplete)
    {
    if (!ensure(FriendsInterface) || !ensure(UserInterface))
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Cannot find a friend. Friends Interface or User Interface is not valid."));
    return;
    }

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);
    const FUniqueNetIdPtr LocalPlayerId = GetUniqueNetIdFromPlayerController(PC);
    if (!ensure(LocalPlayerId.IsValid()))
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Cannot find friends. LocalPlayer NetId is not valid."));
    return;
    }

    GetCacheFriendList(PC, FOnGetCacheFriendListComplete::CreateWeakLambda(this, [this, LocalPlayerId, LocalUserNum, InKeyword, OnComplete](bool bWasSuccessful, TArray<TSharedRef<FOnlineFriend>>& CachedFriendList, const FString& ErrorMessage)
    {
    if (bWasSuccessful)
    {
    // Find friend by exact display name.
    UserInterface->QueryUserIdMapping(LocalPlayerId.ToSharedRef().Get(), InKeyword, IOnlineUser::FOnQueryUserMappingComplete::CreateUObject(this, &ThisClass::OnFindFriendComplete, LocalUserNum, OnComplete));
    }
    else
    {
    OnComplete.ExecuteIfBound(false, nullptr, ErrorMessage);
    }
    }));
    }
  6. Define the OnFindFriendComplete() function. When successful, this function will check whether the potential friend is already a friend or not and then return the result by calling the callback delegate.

    void UFriendsSubsystem_Starter::OnFindFriendComplete(bool bWasSuccessful, const FUniqueNetId& UserId, const FString& DisplayName, const FUniqueNetId& FoundUserId, const FString& Error, int32 LocalUserNum, const FOnFindFriendComplete OnComplete)
    {
    if (bWasSuccessful)
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Success to find a friend with keyword: %s"), *DisplayName);

    // Check if the found user is the player it self.
    if (UserId == FoundUserId)
    {
    OnComplete.ExecuteIfBound(false, nullptr, CANNOT_INVITE_FRIEND_SELF.ToString());
    return;
    }

    // Check if the found user is already friend.
    TSharedPtr<FOnlineFriend> FoundFriend = FriendsInterface->GetFriend(LocalUserNum, FoundUserId, TEXT(""));
    if (FoundFriend.IsValid())
    {
    OnComplete.ExecuteIfBound(true, UFriendData::ConvertToFriendData(FoundFriend.ToSharedRef()), TEXT(""));
    return;
    }

    // Request the found user information to backend (to retrieve avatar URL, display name, etc.)
    OnQueryUserInfoCompleteDelegateHandle = UserInterface->AddOnQueryUserInfoCompleteDelegate_Handle(
    LocalUserNum,
    FOnQueryUserInfoCompleteDelegate::CreateWeakLambda(this, [this, OnComplete](int32 LocalUserNum, bool bWasSuccessful, const TArray<FUniqueNetIdRef>& UserIds, const FString& ErrorStr)
    {
    UserInterface->ClearOnQueryUserInfoCompleteDelegate_Handle(LocalUserNum, OnQueryUserInfoCompleteDelegateHandle);

    if (bWasSuccessful)
    {
    OnComplete.ExecuteIfBound(true, UFriendData::ConvertToFriendData(UserInterface->GetUserInfo(LocalUserNum, UserIds[0].Get()).ToSharedRef()), TEXT(""));
    }
    else
    {
    OnComplete.ExecuteIfBound(false, nullptr, ErrorStr);
    }
    }
    ));
    UserInterface->QueryUserInfo(LocalUserNum, TPartyMemberArray{ FoundUserId.AsShared() });
    }
    else
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Failed to find a friend with keyword: %s"), *DisplayName);
    OnComplete.ExecuteIfBound(false, nullptr, Error);
    }
    }

Implement send friend request

In this section, you will implement sending a friend invitation request.

  1. Open the FriendsSubsystem_Starter class Header file and declare the following function:

    public:
    void SendFriendRequest(const APlayerController* PC, const FUniqueNetIdRepl FriendUserId, const FOnSendFriendRequestComplete& OnComplete = FOnSendFriendRequestComplete());
  2. Create a callback function to handle when the sending friend request process is complete.

    protected:
    void OnSendFriendRequestComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& FriendId, const FString& ListName, const FString& ErrorStr, const FOnSendFriendRequestComplete OnComplete);
  3. Define the functions above. Open the FriendsSubsystem_Starter class CPP file and define the SendFriendRequest() function first. This function will send a friend request and call the OnSendFriendRequestComplete() function to handle the callback.

    void UFriendsSubsystem_Starter::SendFriendRequest(const APlayerController* PC, const FUniqueNetIdRepl FriendUserId, const FOnSendFriendRequestComplete& OnComplete)
    {
    if (!ensure(FriendsInterface) || !ensure(PromptSubsystem))
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Cannot send friend request. Friends Interface or Prompt Subsystem is not valid."));
    return;
    }

    PromptSubsystem->ShowLoading(SEND_FRIEND_REQUEST_MESSAGE);

    const int32 LocalUserNum = GetLocalUserNumFromPlayerController(PC);
    FriendsInterface->SendInvite(LocalUserNum, *FriendUserId.GetUniqueNetId().Get(), TEXT(""), FOnSendInviteComplete::CreateUObject(this, &ThisClass::OnSendFriendRequestComplete, OnComplete));
    }
  4. Define the OnSendFriendRequestComplete() function. This function will show a pop-up notification telling the player that their request successfully sent, and trigger the OnComplete delegate, so the caller can do something when this complete function triggers.

    void UFriendsSubsystem_Starter::OnSendFriendRequestComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& FriendId, const FString& ListName, const FString& ErrorStr, const FOnSendFriendRequestComplete OnComplete)
    {
    PromptSubsystem->HideLoading();

    TSharedPtr<FOnlineFriend> FoundFriend = FriendsInterface->GetFriend(LocalUserNum, FriendId, TEXT(""));
    if (bWasSuccessful && FoundFriend.IsValid())
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Success to send a friend request."));

    PromptSubsystem->ShowMessagePopUp(MESSAGE_PROMPT_TEXT, SUCCESS_SEND_FRIEND_REQUEST);
    OnComplete.ExecuteIfBound(true, UFriendData::ConvertToFriendData(FoundFriend.ToSharedRef()), TEXT(""));
    }
    else
    {
    UE_LOG_FRIENDS_ESSENTIALS(Warning, TEXT("Failed to send a friend request. Error: %s"), *ErrorStr);

    PromptSubsystem->ShowMessagePopUp(ERROR_PROMPT_TEXT, FText::FromString(ErrorStr));
    OnComplete.ExecuteIfBound(false, nullptr, ErrorStr);
    }
    }

Resources