In this How-to, we will implement a mutual Friends system in-game using LootLocker. You will learn how to send friend requests, accept or decline them, list your friends and pending requests, and handle blocking relationships. This creates opt-in, two-way connections between players.
Prerequisites
- A LootLocker account and a created game
- Multiple player profiles (e.g. create via separate sessions)
- An active Game Session
- (Optional) Some player ULIDs collected from the Player Manager
- (Optional) Familiarity with managing relationships in the Web Console
Friends vs Followers
Friends are a two-way relationship requiring mutual consent. Player A sends a friend request to Player B, and they only become friends once B accepts. This powers you to build:
- Private messaging systems
- Co-op gameplay invitations
- Trusted player groups for guilds or parties
- Mutual achievement sharing and challenges
If you need one-way relationships that don't require approval, use Followers instead (see: Use Followers in Game).
Core Concepts
- Friends list: Players who have mutually accepted each other's friend requests.
- Incoming requests: Friend requests sent to the current player by others.
- Outgoing requests: Friend requests the current player has sent but not yet accepted/declined.
- Blocked players: Players who cannot send friend requests or interact with the current player.
- Offset pagination: Lists use page numbers and per-page counts to paginate the data.
Typical UX Flow
- Open Social UI → fetch friend counts for header
- Browse friends list, incoming requests, or outgoing requests in tabs
- Send friend request from player profile → appears in target's incoming list
- Accept/decline incoming requests → moves to friends list or disappears
- Cancel outgoing requests if changed mind
- Block/unblock players to manage harassment
List Friends and Requests
The foundation of any friends system is showing existing relationships and pending requests.
// Listing Friends, Incoming/Outgoing Requests with Offset Pagination// Unity Friends API uses Page (0-indexed) and PerPage to paginate the data.// Assumptions:// - UI layer methods: UIFriendsList.SetLoading(bool), UIFriendsList.ReplaceItems(array), etc.// - Error handling via LogFailure(context, response)
using System.Collections.Generic;using LootLocker.Requests;
public class FriendsListManager{ private readonly List<LootLockerAcceptedFriend> _friends = new(); private readonly List<LootLockerFriend> _incomingRequests = new(); private readonly List<LootLockerFriend> _outgoingRequests = new(); private readonly List<LootLockerBlockedPlayer> _blockedPlayers = new();
// Page tracking (0-indexed) private int _friendsCurrentPage = 0; private int _incomingCurrentPage = 0; private int _outgoingCurrentPage = 0; private int _blockedCurrentPage = 0;
private const int PageSize = 20;
// Friends List public void LoadFriendsPage() { _friends.Clear(); _friendsCurrentPage = 0; UIFriendsList.SetLoading(true);
LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) => { UIFriendsList.SetLoading(false); if (!resp.success) { LogFailure("Friends list", resp); return; }
if (resp.friends != null) { _friends.AddRange(resp.friends); UIFriendsList.ReplaceItems(resp.friends); }
bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length; UIFriendsList.SetHasMorePages(hasMore); }); }
public void LoadMoreFriends() { _friendsCurrentPage++; UIFriendsList.SetLoading(true);
LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) => { UIFriendsList.SetLoading(false); if (!resp.success) { LogFailure("Friends list (more)", resp); _friendsCurrentPage--; // Rollback page increment return; }
if (resp.friends != null && resp.friends.Length > 0) { _friends.AddRange(resp.friends); UIFriendsList.AppendItems(resp.friends); }
bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length; UIFriendsList.SetHasMorePages(hasMore); }); }
// Incoming Friend Requests public void LoadIncomingRequestsFirstPage() { _incomingRequests.Clear(); _incomingCurrentPage = 0; UIIncomingRequestsList.SetLoading(true);
LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) => { UIIncomingRequestsList.SetLoading(false); if (!resp.success) { LogFailure("Incoming requests", resp); return; }
if (resp.incoming != null) { _incomingRequests.AddRange(resp.incoming); UIIncomingRequestsList.ReplaceItems(resp.incoming); }
bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length; UIIncomingRequestsList.SetHasMorePages(hasMore); }); }
public void LoadMoreIncomingRequests() { _incomingCurrentPage++; UIIncomingRequestsList.SetLoading(true);
LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) => { UIIncomingRequestsList.SetLoading(false); if (!resp.success) { LogFailure("Incoming requests (more)", resp); _incomingCurrentPage--; // Rollback page increment return; }
if (resp.incoming != null && resp.incoming.Length > 0) { _incomingRequests.AddRange(resp.incoming); UIIncomingRequestsList.AppendItems(resp.incoming); }
bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length; UIIncomingRequestsList.SetHasMorePages(hasMore); }); }
// Outgoing Friend Requests public void LoadOutgoingRequestsFirstPage() { _outgoingRequests.Clear(); _outgoingCurrentPage = 0; UIOutgoingRequestsList.SetLoading(true);
LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) => { UIOutgoingRequestsList.SetLoading(false); if (!resp.success) { LogFailure("Outgoing requests", resp); return; }
if (resp.outgoing != null) { _outgoingRequests.AddRange(resp.outgoing); UIOutgoingRequestsList.ReplaceItems(resp.outgoing); }
bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length; UIOutgoingRequestsList.SetHasMorePages(hasMore); }); }
public void LoadMoreOutgoingRequests() { _outgoingCurrentPage++; UIOutgoingRequestsList.SetLoading(true);
LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) => { UIOutgoingRequestsList.SetLoading(false); if (!resp.success) { LogFailure("Outgoing requests (more)", resp); _outgoingCurrentPage--; // Rollback page increment return; }
if (resp.outgoing != null && resp.outgoing.Length > 0) { _outgoingRequests.AddRange(resp.outgoing); UIOutgoingRequestsList.AppendItems(resp.outgoing); }
bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length; UIOutgoingRequestsList.SetHasMorePages(hasMore); }); }
// Blocked Players public void LoadBlockedPlayersFirstPage() { _blockedPlayers.Clear(); _blockedCurrentPage = 0; UIBlockedPlayersList.SetLoading(true);
LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) => { UIBlockedPlayersList.SetLoading(false); if (!resp.success) { LogFailure("Blocked players", resp); return; }
if (resp.blocked != null) { _blockedPlayers.AddRange(resp.blocked); UIBlockedPlayersList.ReplaceItems(resp.blocked); }
bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length; UIBlockedPlayersList.SetHasMorePages(hasMore); }); }
public void LoadMoreBlockedPlayers() { _blockedCurrentPage++; UIBlockedPlayersList.SetLoading(true);
LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) => { UIBlockedPlayersList.SetLoading(false); if (!resp.success) { LogFailure("Blocked players (more)", resp); _blockedCurrentPage--; // Rollback page increment return; }
if (resp.blocked != null && resp.blocked.Length > 0) { _blockedPlayers.AddRange(resp.blocked); UIBlockedPlayersList.AppendItems(resp.blocked); }
bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length; UIBlockedPlayersList.SetHasMorePages(hasMore); }); }
private void LogFailure(string context, LootLockerResponse resp) { UnityEngine.Debug.LogWarning($"[FriendsListManager] {context} failed: {resp?.errorData?.message}"); }}// Friends List Management - Unreal C++// Shows listing friends, incoming/outgoing requests, and blocked players using offset pagination.// Assumptions:// - UI layer methods for loading states and data display
#include "LootLockerManager.h"#include "LootLockerSDK/LLFriends.h"
class FFriendsListManager{public: TArray<FLootLockerAcceptedFriend> Friends; TArray<FLootLockerFriend> IncomingRequests; TArray<FLootLockerFriend> OutgoingRequests; TArray<FLootLockerBlockedPlayer> BlockedPlayers;
// Page tracking (0-indexed) int32 FriendsCurrentPage = 0; int32 IncomingCurrentPage = 0; int32 OutgoingCurrentPage = 0; int32 BlockedCurrentPage = 0;
static constexpr int32 PageSize = 20;
// Friends List void LoadFriendsFirstPage() { Friends.Empty(); FriendsCurrentPage = 0; UFriendsListWidget::SetLoading(true);
ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage, FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnFriendsLoaded)); }
void LoadMoreFriends() { FriendsCurrentPage++; UFriendsListWidget::SetLoading(true);
ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage, FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreFriendsLoaded)); }
// Incoming Requests void LoadIncomingRequestsFirstPage() { IncomingRequests.Empty(); IncomingCurrentPage = 0; UIncomingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage, FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnIncomingRequestsLoaded)); }
void LoadMoreIncomingRequests() { IncomingCurrentPage++; UIncomingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage, FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreIncomingRequestsLoaded)); }
// Outgoing Requests void LoadOutgoingRequestsFirstPage() { OutgoingRequests.Empty(); OutgoingCurrentPage = 0; UOutgoingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage, FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnOutgoingRequestsLoaded)); }
void LoadMoreOutgoingRequests() { OutgoingCurrentPage++; UOutgoingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage, FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreOutgoingRequestsLoaded)); }
// Blocked Players void LoadBlockedPlayersFirstPage() { BlockedPlayers.Empty(); BlockedCurrentPage = 0; UBlockedPlayersWidget::SetLoading(true);
ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage, FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnBlockedPlayersLoaded)); }
void LoadMoreBlockedPlayers() { BlockedCurrentPage++; UBlockedPlayersWidget::SetLoading(true);
ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage, FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreBlockedPlayersLoaded)); }
private: void OnFriendsLoaded(const FLootLockerListFriendsResponse& Response) { UFriendsListWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("Friends list failed: %s"), *Response.Error); return; }
Friends = Response.Friends; UFriendsListWidget::ReplaceItems(Friends);
bool bHasMore = Friends.Num < Response.Pagination.Total; UFriendsListWidget::SetHasMorePages(bHasMore); }
void OnMoreFriendsLoaded(const FLootLockerListFriendsResponse& Response) { UFriendsListWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("More friends failed: %s"), *Response.Error); FriendsCurrentPage--; // Rollback return; }
if (Response.Friends.Num() > 0) { Friends.Append(Response.Friends); UFriendsListWidget::AppendItems(Response.Friends); }
bool bHasMore = Friends.Num < Response.Pagination.Total; UFriendsListWidget::SetHasMorePages(bHasMore); }
void OnIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response) { UIncomingRequestsWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("Incoming requests failed: %s"), *Response.Error); return; }
IncomingRequests = Response.Incoming; UIncomingRequestsWidget::ReplaceItems(IncomingRequests);
bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total; UIncomingRequestsWidget::SetHasMorePages(bHasMore); }
void OnMoreIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response) { UIncomingRequestsWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("More incoming requests failed: %s"), *Response.Error); IncomingCurrentPage--; // Rollback return; }
if (Response.Incoming.Num() > 0) { IncomingRequests.Append(Response.Incoming); UIncomingRequestsWidget::AppendItems(Response.Incoming); }
bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total; UIncomingRequestsWidget::SetHasMorePages(bHasMore); }
void OnOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response) { UOutgoingRequestsWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("Outgoing requests failed: %s"), *Response.Error); return; }
OutgoingRequests = Response.Outgoing; UOutgoingRequestsWidget::ReplaceItems(OutgoingRequests);
bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total; UOutgoingRequestsWidget::SetHasMorePages(bHasMore); }
void OnMoreOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response) { UOutgoingRequestsWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("More outgoing requests failed: %s"), *Response.Error); OutgoingCurrentPage--; // Rollback return; }
if (Response.Outgoing.Num() > 0) { OutgoingRequests.Append(Response.Outgoing); UOutgoingRequestsWidget::AppendItems(Response.Outgoing); }
bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total; UOutgoingRequestsWidget::SetHasMorePages(bHasMore); }
void OnBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response) { UBlockedPlayersWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("Blocked players failed: %s"), *Response.Error); return; }
BlockedPlayers = Response.Blocked; UBlockedPlayersWidget::ReplaceItems(BlockedPlayers);
bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total; UBlockedPlayersWidget::SetHasMorePages(bHasMore); }
void OnMoreBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response) { UBlockedPlayersWidget::SetLoading(false); if (!Response.success) { UE_LOG(LogTemp, Warning, TEXT("More blocked players failed: %s"), *Response.Error); BlockedCurrentPage--; // Rollback return; }
if (Response.Blocked.Num() > 0) { BlockedPlayers.Append(Response.Blocked); UBlockedPlayersWidget::AppendItems(Response.Blocked); }
bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total; UBlockedPlayersWidget::SetHasMorePages(bHasMore); }};Coming soon...
Coming soon...
Offset Pagination Strategy
- Use page numbers (0-indexed) and
PerPagecount to paginate the data - Track current page for each list type separately
- Consider a page "complete" if returned items equal the requested
PerPage - Reset to page 0 when refreshing lists
Send and Cancel Friend Requests
Allow players to initiate and manage outgoing friend requests.
// Send and Cancel Friend Requests with Optimistic UI// Assumptions:// - _outgoingRequests list is maintained locally// - UI shows button states: Send Request / Pending / Friends// - ShowToast for user feedback
public class FriendRequestController{ private readonly List<LootLockerFriend> _outgoingRequests; private readonly HashSet<string> _friendIds; // Track current friends for button state
public FriendRequestController(List<LootLockerFriend> outgoingRequests, HashSet<string> friendIds) { _outgoingRequests = outgoingRequests; _friendIds = friendIds; }
public void SendFriendRequest(string targetPlayerULID, string displayName) { if (string.IsNullOrEmpty(targetPlayerULID)) return;
// Check if already friends or request pending if (_friendIds.Contains(targetPlayerULID)) { ShowToast("Already friends"); return; }
if (_outgoingRequests.Any(r => r.player_ulid == targetPlayerULID)) { ShowToast("Request already sent"); return; }
// Optimistic update var tempRequest = new LootLockerFriend { player_ulid = targetPlayerULID, player_name = displayName, created_at = DateTime.Now }; _outgoingRequests.Add(tempRequest); UIProfileButton.SetState(FriendButtonState.Pending); UIOutgoingRequestsList.AddItem(tempRequest);
LootLockerSDKManager.SendFriendRequest(targetPlayerULID, (resp) => { if (!resp.success) { // Rollback optimistic update _outgoingRequests.RemoveAll(r => r.player_ulid == targetPlayerULID); UIProfileButton.SetState(FriendButtonState.SendRequest); UIOutgoingRequestsList.RemoveItem(targetPlayerULID); ShowToast($"Failed to send request: {resp.errorData?.message}"); return; }
ShowToast($"Friend request sent to {displayName}"); }); }
public void CancelFriendRequest(string targetPlayerULID, string displayName) { var request = _outgoingRequests.FirstOrDefault(r => r.player_ulid == targetPlayerULID); if (request == null) return;
// Optimistic removal _outgoingRequests.Remove(request); UIProfileButton.SetState(FriendButtonState.SendRequest); UIOutgoingRequestsList.RemoveItem(targetPlayerULID);
LootLockerSDKManager.CancelFriendRequest(targetPlayerULID, (resp) => { if (!resp.success) { // Rollback _outgoingRequests.Add(request); UIProfileButton.SetState(FriendButtonState.Pending); UIOutgoingRequestsList.AddItem(request); ShowToast($"Failed to cancel request: {resp.errorData?.message}"); return; }
ShowToast($"Cancelled request to {displayName}"); }); }
private void ShowToast(string message) { // Implementation depends on your UI framework Debug.Log(message); }}
public enum FriendButtonState{ SendRequest, Pending, Friends}// Friend Request Management - Unreal C++// Handles sending and canceling friend requests with optimistic updates
class FFriendRequestController{public: TArray<FLootLockerFriend> OutgoingRequests; TSet<FString> FriendIds; // Track current friends for UI state
void SendFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName) { if (TargetPlayerULID.IsEmpty()) return;
if (FriendIds.Contains(TargetPlayerULID)) { ShowToast(TEXT("Already friends")); return; }
// Check if request already exists if (OutgoingRequests.ContainsByPredicate([&](const FLootLockerFriend& Request) { return Request.Player_ulid == TargetPlayerULID; })) { ShowToast(TEXT("Request already sent")); return; }
// Optimistic update FLootLockerFriend TempRequest; TempRequest.Player_ulid = TargetPlayerULID; TempRequest.Player_name = DisplayName; TempRequest.Created_at = FDateTime::Now().ToString();
OutgoingRequests.Add(TempRequest); UProfileButtonWidget::SetState(EFriendButtonState::Pending); UOutgoingRequestsWidget::AddItem(TempRequest);
ULootLockerSDKManager::SendFriendRequest(TargetPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback OutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) { return Request.Player_ulid == TargetPlayerULID; }); UProfileButtonWidget::SetState(EFriendButtonState::SendRequest); UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID); ShowToast(FString::Printf(TEXT("Failed to send request: %s"), *Response.Error)); return; }
ShowToast(FString::Printf(TEXT("Friend request sent to %s"), *DisplayName)); })); }
void CancelFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName) { // Find the request FLootLockerFriend* RequestPtr = OutgoingRequests.FindByPredicate([&](const FLootLockerFriend& Request) { return Request.Player_ulid == TargetPlayerULID; });
if (!RequestPtr) return; FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic removal OutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) { return Request.Player_ulid == TargetPlayerULID; }); UProfileButtonWidget::SetState(EFriendButtonState::SendRequest); UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID);
ULootLockerSDKManager::CancelFriendRequest(TargetPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, DisplayName](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback OutgoingRequests.Add(RequestCopy); UProfileButtonWidget::SetState(EFriendButtonState::Pending); UOutgoingRequestsWidget::AddItem(RequestCopy); ShowToast(FString::Printf(TEXT("Failed to cancel request: %s"), *Response.Error)); return; }
ShowToast(FString::Printf(TEXT("Cancelled request to %s"), *DisplayName)); })); }
private: void ShowToast(const FString& Message) { UE_LOG(LogTemp, Log, TEXT("%s"), *Message); // Add your toast UI implementation }};
UENUM(BlueprintType)enum class EFriendButtonState : uint8{ SendRequest, Pending, Friends};Coming soon...
Coming soon...
Accept and Decline Friend Requests
Handle incoming friend requests to build your friends network.
// Accept and Decline Incoming Friend Requests// Move requests between lists when processed
public class IncomingRequestsController{ private readonly List<LootLockerFriend> _incomingRequests; private readonly List<LootLockerAcceptedFriend> _friends;
public IncomingRequestsController(List<LootLockerFriend> incomingRequests, List<LootLockerAcceptedFriend> friends) { _incomingRequests = incomingRequests; _friends = friends; }
public void AcceptFriendRequest(string fromPlayerULID) { var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID); if (request == null) return;
// Optimistic update: move from incoming to friends _incomingRequests.Remove(request); var newFriend = new LootLockerAcceptedFriend { player_ulid = request.player_ulid, player_name = request.player_name, player_id = request.player_id, created_at = request.created_at, accepted_at = DateTime.Now }; _friends.Add(newFriend);
UIIncomingRequestsList.RemoveItem(fromPlayerULID); UIFriendsList.AddItem(newFriend); ShowToast($"Now friends with {request.player_name}");
LootLockerSDKManager.AcceptFriendRequest(fromPlayerULID, (resp) => { if (!resp.success) { // Rollback: move back to incoming _friends.Remove(newFriend); _incomingRequests.Add(request); UIFriendsList.RemoveItem(fromPlayerULID); UIIncomingRequestsList.AddItem(request); ShowToast($"Failed to accept request: {resp.errorData?.message}"); } }); }
public void DeclineFriendRequest(string fromPlayerULID) { var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID); if (request == null) return;
// Optimistic removal _incomingRequests.Remove(request); UIIncomingRequestsList.RemoveItem(fromPlayerULID); ShowToast($"Declined request from {request.player_name}");
LootLockerSDKManager.DeclineFriendRequest(fromPlayerULID, (resp) => { if (!resp.success) { // Rollback _incomingRequests.Add(request); UIIncomingRequestsList.AddItem(request); ShowToast($"Failed to decline request: {resp.errorData?.message}"); } }); }
private void ShowToast(string message) { Debug.Log(message); }}// Accept and Decline Friend Requests - Unreal C++
class FIncomingRequestsController{public: TArray<FLootLockerFriend> IncomingRequests; TArray<FLootLockerAcceptedFriend> Friends;
void AcceptFriendRequest(const FString& FromPlayerULID) { // Find the request FLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) { return Request.Player_ulid == FromPlayerULID; });
if (!RequestPtr) return; FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic update: move from incoming to friends IncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) { return Request.Player_ulid == FromPlayerULID; });
FLootLockerAcceptedFriend NewFriend; NewFriend.Player_ulid = RequestCopy.Player_ulid; NewFriend.Player_name = RequestCopy.Player_name; NewFriend.Player_id = RequestCopy.Player_id; NewFriend.Created_at = RequestCopy.Created_at; NewFriend.Accepted_at = FDateTime::Now().ToString();
Friends.Add(NewFriend); UIncomingRequestsWidget::RemoveItem(FromPlayerULID); UFriendsListWidget::AddItem(NewFriend); ShowToast(FString::Printf(TEXT("Now friends with %s"), *RequestCopy.Player_name));
ULootLockerSDKManager::AcceptFriendRequest(FromPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, NewFriend, FromPlayerULID](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback: move back to incoming Friends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) { return Friend.Player_ulid == FromPlayerULID; }); IncomingRequests.Add(RequestCopy); UFriendsListWidget::RemoveItem(FromPlayerULID); UIncomingRequestsWidget::AddItem(RequestCopy); ShowToast(FString::Printf(TEXT("Failed to accept request: %s"), *Response.Error)); } })); }
void DeclineFriendRequest(const FString& FromPlayerULID) { // Find the request FLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) { return Request.Player_ulid == FromPlayerULID; });
if (!RequestPtr) return; FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic removal IncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) { return Request.Player_ulid == FromPlayerULID; }); UIncomingRequestsWidget::RemoveItem(FromPlayerULID); ShowToast(FString::Printf(TEXT("Declined request from %s"), *RequestCopy.Player_name));
ULootLockerSDKManager::DeclineFriendRequest(FromPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback IncomingRequests.Add(RequestCopy); UIncomingRequestsWidget::AddItem(RequestCopy); ShowToast(FString::Printf(TEXT("Failed to decline request: %s"), *Response.Error)); } })); }
private: void ShowToast(const FString& Message) { UE_LOG(LogTemp, Log, TEXT("%s"), *Message); }};Coming soon...
Coming soon...
Block and Unblock Players
Manage your blocked players list to prevent unwanted interactions.
// Block and Unblock Players// Blocking removes from friends and prevents future friend requests
public class BlockingController{ private readonly List<LootLockerBlockedPlayer> _blockedPlayers; private readonly List<LootLockerAcceptedFriend> _friends;
public BlockingController(List<LootLockerBlockedPlayer> blockedPlayers, List<LootLockerAcceptedFriend> friends) { _blockedPlayers = blockedPlayers; _friends = friends; }
public void BlockPlayer(string targetPlayerULID, string displayName) { if (string.IsNullOrEmpty(targetPlayerULID)) return;
// Check if already blocked if (_blockedPlayers.Any(p => p.player_ulid == targetPlayerULID)) { ShowToast("Player already blocked"); return; }
// Optimistic update: remove from friends if present, add to blocked var existingFriend = _friends.FirstOrDefault(f => f.player_ulid == targetPlayerULID); if (existingFriend != null) { _friends.Remove(existingFriend); UIFriendsList.RemoveItem(targetPlayerULID); }
var blockedPlayer = new LootLockerBlockedPlayer { player_ulid = targetPlayerULID, player_name = displayName, blocked_at = DateTime.Now }; _blockedPlayers.Add(blockedPlayer); UIBlockedPlayersList.AddItem(blockedPlayer); UIProfileButton.SetState(FriendButtonState.Blocked);
LootLockerSDKManager.BlockPlayer(targetPlayerULID, (resp) => { if (!resp.success) { // Rollback _blockedPlayers.Remove(blockedPlayer); UIBlockedPlayersList.RemoveItem(targetPlayerULID); if (existingFriend != null) { _friends.Add(existingFriend); UIFriendsList.AddItem(existingFriend); UIProfileButton.SetState(FriendButtonState.Friends); } else { UIProfileButton.SetState(FriendButtonState.SendRequest); } ShowToast($"Failed to block player: {resp.errorData?.message}"); return; }
ShowToast($"Blocked {displayName}"); }); }
public void UnblockPlayer(string targetPlayerULID, string displayName) { var blockedPlayer = _blockedPlayers.FirstOrDefault(p => p.player_ulid == targetPlayerULID); if (blockedPlayer == null) return;
// Optimistic removal _blockedPlayers.Remove(blockedPlayer); UIBlockedPlayersList.RemoveItem(targetPlayerULID); UIProfileButton.SetState(FriendButtonState.SendRequest);
LootLockerSDKManager.UnblockPlayer(targetPlayerULID, (resp) => { if (!resp.success) { // Rollback _blockedPlayers.Add(blockedPlayer); UIBlockedPlayersList.AddItem(blockedPlayer); UIProfileButton.SetState(FriendButtonState.Blocked); ShowToast($"Failed to unblock player: {resp.errorData?.message}"); return; }
ShowToast($"Unblocked {displayName}"); }); }
private void ShowToast(string message) { Debug.Log(message); }}// Block and Unblock Players - Unreal C++
class FBlockingController{public: TArray<FLootLockerBlockedPlayer> BlockedPlayers; TArray<FLootLockerAcceptedFriend> Friends;
void BlockPlayer(const FString& TargetPlayerULID, const FString& DisplayName) { if (TargetPlayerULID.IsEmpty()) return;
// Check if already blocked if (BlockedPlayers.ContainsByPredicate([&](const FLootLockerBlockedPlayer& Player) { return Player.Player_ulid == TargetPlayerULID; })) { ShowToast(TEXT("Player already blocked")); return; }
// Remove from friends if present FLootLockerAcceptedFriend* ExistingFriend = Friends.FindByPredicate([&](const FLootLockerAcceptedFriend& Friend) { return Friend.Player_ulid == TargetPlayerULID; });
FLootLockerAcceptedFriend FriendCopy; bool bWasFriend = false; if (ExistingFriend) { FriendCopy = *ExistingFriend; bWasFriend = true; Friends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) { return Friend.Player_ulid == TargetPlayerULID; }); UFriendsListWidget::RemoveItem(TargetPlayerULID); }
// Add to blocked FLootLockerBlockedPlayer BlockedPlayer; BlockedPlayer.Player_ulid = TargetPlayerULID; BlockedPlayer.Player_name = DisplayName; BlockedPlayer.Blocked_at = FDateTime::Now().ToString();
BlockedPlayers.Add(BlockedPlayer); UBlockedPlayersWidget::AddItem(BlockedPlayer); UProfileButtonWidget::SetState(EFriendButtonState::Blocked);
ULootLockerSDKManager::BlockPlayer(TargetPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayer, FriendCopy, bWasFriend, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback BlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) { return Player.Player_ulid == TargetPlayerULID; }); UBlockedPlayersWidget::RemoveItem(TargetPlayerULID);
if (bWasFriend) { Friends.Add(FriendCopy); UFriendsListWidget::AddItem(FriendCopy); UProfileButtonWidget::SetState(EFriendButtonState::Friends); } else { UProfileButtonWidget::SetState(EFriendButtonState::SendRequest); }
ShowToast(FString::Printf(TEXT("Failed to block player: %s"), *Response.Error)); return; }
ShowToast(FString::Printf(TEXT("Blocked %s"), *DisplayName)); })); }
void UnblockPlayer(const FString& TargetPlayerULID, const FString& DisplayName) { FLootLockerBlockedPlayer* BlockedPlayerPtr = BlockedPlayers.FindByPredicate([&](const FLootLockerBlockedPlayer& Player) { return Player.Player_ulid == TargetPlayerULID; });
if (!BlockedPlayerPtr) return; FLootLockerBlockedPlayer BlockedPlayerCopy = *BlockedPlayerPtr;
// Optimistic removal BlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) { return Player.Player_ulid == TargetPlayerULID; }); UBlockedPlayersWidget::RemoveItem(TargetPlayerULID); UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
ULootLockerSDKManager::UnblockPlayer(TargetPlayerULID, FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayerCopy, DisplayName](const FLootLockerFriendActionResponse& Response) { if (!Response.success) { // Rollback BlockedPlayers.Add(BlockedPlayerCopy); UBlockedPlayersWidget::AddItem(BlockedPlayerCopy); UProfileButtonWidget::SetState(EFriendButtonState::Blocked); ShowToast(FString::Printf(TEXT("Failed to unblock player: %s"), *Response.Error)); return; }
ShowToast(FString::Printf(TEXT("Unblocked %s"), *DisplayName)); })); }
private: void ShowToast(const FString& Message) { UE_LOG(LogTemp, Log, TEXT("%s"), *Message); }};Coming soon...
Coming soon...
Check Friendship Status
Determine the current relationship between players for UI state management.
// Check Friendship Status Using GetFriend// Returns specific friend data if they are friends, null/error otherwise
public static class FriendshipProbe{ public static void CheckFriendshipStatus(string targetPlayerULID, System.Action<FriendshipStatus> onResult) { if (string.IsNullOrEmpty(targetPlayerULID)) { onResult?.Invoke(FriendshipStatus.None); return; }
LootLockerSDKManager.GetFriend(targetPlayerULID, (resp) => { if (resp.success && !string.IsNullOrEmpty(resp.player_ulid)) { // They are friends - check when friendship was established onResult?.Invoke(FriendshipStatus.Friends); } else { // Not friends - could be pending request, blocked, or no relationship // You might need additional checks to distinguish these states onResult?.Invoke(FriendshipStatus.None); } }); }}
public enum FriendshipStatus{ None, // No relationship Friends, // Mutual friends IncomingRequest, // They sent us a request OutgoingRequest, // We sent them a request Blocked // One party blocked the other}
// Usage:// FriendshipProbe.CheckFriendshipStatus(otherPlayerUid, (status) => {// switch(status) {// case FriendshipStatus.Friends:// UIProfileButton.SetState(FriendButtonState.Friends);// break;// case FriendshipStatus.None:// UIProfileButton.SetState(FriendButtonState.SendRequest);// break;// }// });// Friendship Status Check - Unreal C++
UENUM(BlueprintType)enum class EFriendshipStatus : uint8{ None, // No relationship Friends, // Mutual friends IncomingRequest, // They sent us a request OutgoingRequest, // We sent them a request Blocked // One party blocked the other};
class FFriendshipProbe{public: static void CheckFriendshipStatus(const FString& TargetPlayerULID, TFunction<void(EFriendshipStatus)> Callback) { if (TargetPlayerULID.IsEmpty()) { Callback(EFriendshipStatus::None); return; }
ULootLockerSDKManager::GetFriend(TargetPlayerULID, FLootLockerGetFriendResponseDelegate::CreateLambda([Callback](const FLootLockerGetFriendResponse& Response) { if (Response.success && !Response.Player_ulid.IsEmpty()) { // They are friends Callback(EFriendshipStatus::Friends); } else { // Not friends - could be pending, blocked, or no relationship // Additional logic could check pending requests lists Callback(EFriendshipStatus::None); } })); }};
// Usage:// FFriendshipProbe::CheckFriendshipStatus(OtherPlayerUid, [](EFriendshipStatus Status) {// switch(Status) {// case EFriendshipStatus::Friends:// UProfileButtonWidget::SetState(EFriendButtonState::Friends);// break;// case EFriendshipStatus::None:// UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);// break;// }// });Coming soon...
Coming soon...
Displaying Counts and Status
Show meaningful counts and relationship indicators in your UI.
// Friends Counts and Status Display// Use first page loads to estimate counts and provide status context
public class FriendsCountsManager{ private int _friendsCount = 0; private int _incomingRequestsCount = 0; private int _outgoingRequestsCount = 0;
public void LoadAllCounts() { // Load first pages to get count estimates LoadFriendsCount(); LoadIncomingRequestsCount(); LoadOutgoingRequestsCount(); }
private void LoadFriendsCount() { LootLockerSDKManager.ListFriendsPaginated(1, 0, (resp) => { if (resp.success) { UIFriendsHeader.SetCount(resp.pagination.total.ToString()); } }); }
private void LoadIncomingRequestsCount() { LootLockerSDKManager.ListIncomingFriendRequestsPaginated(1, 0, (resp) => { if (resp.success && resp.incoming != null) { _incomingRequestsCount = resp.pagination.total; UIIncomingRequestsHeader.SetCount(_incomingRequestsCount);
// Show notification badge if there are pending requests if (_incomingRequestsCount > 0) { UIFriendsTabButton.ShowBadge(_incomingRequestsCount); } } }); }
private void LoadOutgoingRequestsCount() { LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(1, 0, (resp) => { if (resp.success && resp.outgoing != null) { UIOutgoingRequestsHeader.SetCount(resp.pagination.total); } }); }}// Friends Counts Display - Unreal C++
class FFriendsCountsManager{public: int32 FriendsCount = 0; int32 IncomingRequestsCount = 0; int32 OutgoingRequestsCount = 0;
void LoadAllCounts() { LoadFriendsCount(); LoadIncomingRequestsCount(); LoadOutgoingRequestsCount(); }
private: void LoadFriendsCount() { ULootLockerSDKManager::ListFriendsPaginated(1, 0, FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnFriendsCountLoaded)); }
void LoadIncomingRequestsCount() { ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(1, 0, FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnIncomingCountLoaded)); }
void LoadOutgoingRequestsCount() { ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(1, 0, FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnOutgoingCountLoaded)); }
void OnFriendsCountLoaded(const FLootLockerListFriendsResponse& Response) { if (Response.success) { UFriendsHeaderWidget::SetCount(FString::FromInt(Response.Pagination.Total)); } }
void OnIncomingCountLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response) { if (Response.success) { IncomingRequestsCount = Response.Pagination.Total; UIncomingRequestsHeaderWidget::SetCount(FString::FromInt(IncomingRequestsCount));
// Show notification badge if there are pending requests if (IncomingRequestsCount > 0) { UFriendsTabButtonWidget::ShowBadge(IncomingRequestsCount); } } }
void OnOutgoingCountLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response) { if (Response.success) { OutgoingRequestsCount = Response.Pagination.Total; UOutgoingRequestsHeaderWidget::SetCount(FString::FromInt(OutgoingRequestsCount)); } }};Coming soon...
Coming soon...
Performance Tips
- Batch relationship checks: When showing many players (leaderboards for example), only fetch data for visible items
- Cache friend IDs locally: Maintain a set of friend ULIDs for instant button state rendering
- Moderate page sizes: 20-50 items per page balances responsiveness with network efficiency
- Debounce rapid requests: Prevent spam-clicking send/cancel buttons
- Lazy load secondary lists: Load friends first, then incoming/outgoing requests when tabs are opened
Example Feature Ideas
- Mutual Friends: "You have 3 mutual friends with this player"
- Friend Recommendations: Suggest friends-of-friends or players with similar interests
- Private Groups: Create invite-only spaces using your friends list
- Co-op Quick Join: Join friends' game sessions directly from the friends list
Conclusion
In this How-to we implemented friend requests, list management, acceptance/declining workflows, blocking functionality, and relationship status checking. The friends system provides the foundation for deeper social features like messaging, co-op play, and community building.
Key differences from followers include the mutual consent model, request management workflow, and offset-based pagination. Next, consider adding Followers for asymmetric relationships or explore Web Console management for administrative oversight.
