Skip to content

Use Friends 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

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

  1. Open Social UI → fetch friend counts for header
  2. Browse friends list, incoming requests, or outgoing requests in tabs
  3. Send friend request from player profile → appears in target's incoming list
  4. Accept/decline incoming requests → moves to friends list or disappears
  5. Cancel outgoing requests if changed 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 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.