Skip to content

Use Friends In-Game

This how-to implements a mutual Friends system in-game using LootLocker: sending friend requests, accepting or declining them, listing friends and pending requests, and handling blocking relationships. This creates opt-in, two-way connections between players.

Prerequisites

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 can't 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

  1. Open Social UI, then fetch friend counts for the header.
  2. Browse friends list, incoming requests, or outgoing requests in tabs.
  3. Send a friend request from a player profile — it appears in the target's incoming list.
  4. Accept/decline incoming requests — this moves the player to the friends list or removes the request.
  5. Cancel outgoing requests if you change your mind.
  6. 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 data.
  • Track the current page for each list type separately.
  • Consider a page "complete" if the 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 a player's 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 a player's friends list.
  • Co-op Quick Join: join friends' game sessions directly from the friends list.

Conclusion

You've implemented friend requests, list management, accept/decline 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.