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

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

Unlike cursor-based pagination, offset pagination lets you jump to specific pages without knowing the content on them

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