Use Friend In-Game
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
Multiple player profiles (e.g. create via separate sessions)
(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}");
}
}
Offset Pagination Strategy
Use page numbers (0-indexed) and
PerPage
count to paginate the dataTrack 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
}
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);
}
}
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);
}
}
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;
// }
// });
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);
}
});
}
}
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.
Last updated