Use Followers In-Game

In this How-to, we will implement a basic Followers system in-game using LootLocker. You will learn how to show who follows a player, who they are following, allow players to follow/unfollow others, and build simple UI patterns like profile headers, lists, and follow buttons.

Prerequisites

Followers vs Friends

Followers are a one‑way relationship. Player A can follow Player B without approval (unless B has blocked A). This enables:

  • Creator / influencer style player profiles

  • Social feeds or “What your followed players did”

  • Asynchronous competitions (e.g. “Chase the ghost of players you follow”)

  • Notification or highlight surfaces (e.g. “New level published by someone you follow”)

If you need mutual, opt‑in relationships, use Friends instead (see: Use Friends in Game).

Core Concepts

  • Followers list: Players who follow the target player.

  • Following list: Players the target player has chosen to follow.

  • Pagination: Large lists return a next_cursor; request subsequent pages until empty.

  • Blocking: A blocked player cannot follow or become friends with the blocker.

  • UI Caching: Cache following IDs locally to render instant button states (“Following” vs “Follow”).

Typical UX Flow

  1. Open Profile (self or another player)

  2. Fetch counts (followers + following) for header display

  3. Lazy-load the tab the player opens first (e.g. “Following”)

  4. Infinite scroll / “Load more” using cursor

  5. Show Follow / Unfollow button with optimistic state change

  6. Update local cache + optionally refresh counts

Fetch Follower and Following Lists

Use these to populate tabs or modals.

// This example shows how to:
// 1. Fetch the first page of followers for the logged-in player
// 2. Fetch the first page of players the logged-in player is following
// 3. Request additional pages using the `pagination.next_cursor`
//
// Assumptions / Pseudo Dependencies:
// - A Game Session has already been started (player authenticated)
// - You have a simple UI layer (not included here) with methods like:
//     UIFollowersList.SetLoading(bool)
//     UIFollowingList.AppendRows(IEnumerable<LootLockerFollower>)
//     UIFollowingList.ShowEndOfList()
// - You store cursors locally for each list.

using System.Collections.Generic;
using LootLocker.Requests; // Namespace where the SDK classes live

public class FollowersExample
{
	// Local caches (only store lightweight identity info; expand as needed)
	private readonly List<LootLockerFollower> _followers = new();
	private readonly List<LootLockerFollower> _following = new();

	// Pagination cursors (empty or null means no more pages / not yet loaded)
	private string _followersNextCursor = null;
	private string _followingNextCursor = null;

	// Page size – tune for your UX & bandwidth constraints
	private const int PageSize = 25;

	// PUBLIC API you might call from UI buttons / tab switches
	public void LoadFirstFollowersPage()
	{
		_followers.Clear();
		_followersNextCursor = null; // Reset so we know this is a fresh load
		UIFollowersList.SetLoading(true);

		// First page: pass null/empty cursor, and a count
		LootLockerSDKManager.ListFollowersPaginated(Cursor: null, Count: PageSize, onComplete: (resp) =>
		{
			UIFollowersList.SetLoading(false);
			if (!resp.success)
			{
				LogFailure("Followers", resp);
				return;
			}

			if (resp.followers != null)
			{
				_followers.AddRange(resp.followers);
				UIFollowersList.ReplaceRows(resp.followers); // Replace entire list for first load
			}

			_followersNextCursor = resp.pagination?.next_cursor; // Will be null/empty if no more pages
			if (string.IsNullOrEmpty(_followersNextCursor))
			{
				UIFollowersList.ShowEndOfList();
			}
		});
	}

	public void LoadMoreFollowers()
	{
		if (string.IsNullOrEmpty(_followersNextCursor)) return; // No more pages
		UIFollowersList.SetLoading(true);
		LootLockerSDKManager.ListFollowersPaginated(Cursor: _followersNextCursor, Count: PageSize, onComplete: (resp) =>
		{
			UIFollowersList.SetLoading(false);
			if (!resp.success)
			{
				LogFailure("Followers (more)", resp);
				return;
			}

			if (resp.followers != null && resp.followers.Length > 0)
			{
				_followers.AddRange(resp.followers);
				UIFollowersList.AppendRows(resp.followers); // Append for subsequent pages
			}

			_followersNextCursor = resp.pagination?.next_cursor;
			if (string.IsNullOrEmpty(_followersNextCursor))
			{
				UIFollowersList.ShowEndOfList();
			}
		});
	}

	public void LoadFirstFollowingPage()
	{
		_following.Clear();
		_followingNextCursor = null;
		UIFollowingList.SetLoading(true);
		LootLockerSDKManager.ListFollowingPaginated(Cursor: null, Count: PageSize, onComplete: (resp) =>
		{
			UIFollowingList.SetLoading(false);
			if (!resp.success)
			{
				LogFailure("Following", resp);
				return;
			}

			if (resp.following != null)
			{
				_following.AddRange(resp.following);
				UIFollowingList.ReplaceRows(resp.following);
			}

			_followingNextCursor = resp.pagination?.next_cursor;
			if (string.IsNullOrEmpty(_followingNextCursor))
			{
				UIFollowingList.ShowEndOfList();
			}
		});
	}

	public void LoadMoreFollowing()
	{
		if (string.IsNullOrEmpty(_followingNextCursor)) return;
		UIFollowingList.SetLoading(true);
		LootLockerSDKManager.ListFollowingPaginated(Cursor: _followingNextCursor, Count: PageSize, onComplete: (resp) =>
		{
			UIFollowingList.SetLoading(false);
			if (!resp.success)
			{
				LogFailure("Following (more)", resp);
				return;
			}

			if (resp.following != null && resp.following.Length > 0)
			{
				_following.AddRange(resp.following);
				UIFollowingList.AppendRows(resp.following);
			}

			_followingNextCursor = resp.pagination?.next_cursor;
			if (string.IsNullOrEmpty(_followingNextCursor))
			{
				UIFollowingList.ShowEndOfList();
			}
		});
	}

	private void LogFailure(string context, LootLockerResponse resp)
	{
		// Centralize error logging; you might map error codes to localized user messages.
		UnityEngine.Debug.LogWarning($"[FollowersExample] {context} failed: {resp?.errorData?.message}");
	}
}

Pagination Strategy

  • Always request the first page with no cursor.

  • Store next_cursor if present.

  • Disable “Load more” when cursor is empty or null.

  • Consider prefetching the next page when the user scrolls past 70% of current content.

Show a skeleton or shimmer state while loading pages to keep the UI feeling responsive.

Displaying Counts

Instead of loading entire lists to show numbers, use the first page response (which includes total/size fields) or lazily populate counts after the first fetch. If exact counts are not required immediately, show placeholders (e.g. “—” then fade in).

// Displaying Counts Example
// There is no dedicated "counts only" endpoint, so we infer counts from:
// - Length of the page we just received
// - Presence (or absence) of pagination.next_cursor to know if more pages exist
// Optionally, you can lazily request the first page of each list only when a UI panel is opened.
//
// Assumptions:
// - UI has two text labels: UIFollowersHeader.SetCount(int? count, int? total) & UIFollowingHeader.SetCount(int? count, int? total)
// - Passing null shows a placeholder (e.g. "—") until data arrives.

public class FollowerCounts
{
	private int? _followersLoadedCount;
	private int? _followingLoadedCount;
	private int? _totalFollowersCount;
	private int? _totalFollowingCount;

	public void PrimePlaceholders()
	{
		UIFollowersHeader.SetCount(null, null);
		UIFollowingHeader.SetCount(null, null);
	}

	public void FetchInitialFollowers()
	{
		LootLockerSDKManager.ListFollowersPaginated(null, 25, (resp) =>
		{
			if (!resp.success)
			{
				// Handle error
				return;
			}
			_followersLoadedCount = resp.followers?.Length ?? 0;
			_totalFollowersCount = resp.pagination?.total);
			UIFollowersHeader.SetCount(_followersLoadedCount, _totalFollowersCount);
		});
	}

	public void FetchInitialFollowing()
	{
		LootLockerSDKManager.ListFollowingPaginated(null, 25, (resp) =>
		{
			if (!resp.success)
			{
				UIFollowingHeader.SetCount(0);
				return;
			}
			_followingLoadedCount = resp.following?.Length ?? 0;
			_totalFollowingCount = resp.pagination?.total);
			UIFollowingHeader.SetCount(_followingLoadedCount, _totalFollowingCount);
		});
	}
}

Follow a Player

Trigger from a profile card, leaderboard row, chat user badge, or “Suggested Players” carousel.

// Follow Player (Optimistic UI) Example
// Goal: Player taps a Follow button on another player's profile card.
// We immediately reflect the new state in UI, then roll back if the API fails.
//
// Assumptions:
// - UI elements: UIButton FollowButton; UILabel FollowLabel
// - Local cache: HashSet<string> FollowingIds storing public UIDs we follow
// - Method ShowToast(string msg) for transient user feedback
// - playerPublicUID is the target player's public UID

public class FollowActionController
{
	private readonly HashSet<string> _followingIds;
	public FollowActionController(HashSet<string> followingIds) => _followingIds = followingIds;

	public void OnFollowButtonClicked(string playerPublicUID)
	{
		if (string.IsNullOrEmpty(playerPublicUID)) return;
		if (_followingIds.Contains(playerPublicUID))
		{
			// Already following (button might have been double-clicked)
			ShowToast("Already following");
			return;
		}

		// Optimistic state change
		SetButtonStateLoading();
		SetVisualStateFollowing();
		_followingIds.Add(playerPublicUID);

		LootLockerSDKManager.FollowPlayer(playerPublicUID, (resp) =>
		{
			if (!resp.success)
			{
				// Rollback
				_followingIds.Remove(playerPublicUID);
				SetVisualStateFollow();
				ShowToast("Follow failed: " + resp.errorData?.message);
				return;
			}

			// Success: Optionally increment a displayed count
			PlayerProfileHeader.IncrementFollowingCount(1);
			ShowToast("Now following player: " + playerPublicUID);
		});
	}

	private void SetVisualStateFollowing()
	{
		FollowButton.interactable = false; // Optionally re-enable later as an unfollow button
		FollowLabel.text = "Following";
	}

	private void SetVisualStateFollow()
	{
		FollowButton.interactable = true;
		FollowLabel.text = "Follow";
	}

	private void SetButtonStateLoading()
	{
		FollowLabel.text = "..."; // Brief loading indicator
	}
}

Optimistic Updates

  1. Disable the button and swap label to “Following…”

  2. Send follow request

  3. On success: set to “Following” (or icon toggle)

  4. On failure: revert and show a subtle toast

Unfollow a Player

Offer this option from the “Following” list or a context / overflow menu.

// Unfollow Player Example (with optional confirm + undo)
// Assumptions:
// - _followingIds HashSet<string>
// - UI provides a confirmation popup Confirm("Unfollow X?", onYes, onNo)
// - ShowUndo(message, action) displays a snackbar with Undo button
// - PlayerProfileHeader.DecrementFollowingCount(int)

public class UnfollowActionController
{
	private readonly HashSet<string> _followingIds;
	public UnfollowActionController(HashSet<string> followingIds) => _followingIds = followingIds;

	public void RequestUnfollow(string targetPublicUID, string targetDisplayName)
	{
		if (!_followingIds.Contains(targetPublicUID)) return; // Already not following

		Confirm($"Unfollow {targetDisplayName}?", () => ExecuteUnfollow(targetPublicUID, targetDisplayName), () => { /* canceled */ });
	}

	private void ExecuteUnfollow(string targetPublicUID, string targetDisplayName)
	{
		// Optimistic removal
		_followingIds.Remove(targetPublicUID);
		PlayerProfileHeader.DecrementFollowingCount(1);
		UpdateButtonToFollow();

		LootLockerSDKManager.UnfollowPlayer(targetPublicUID, (resp) =>
		{
			if (!resp.success)
			{
				// Rollback
				_followingIds.Add(targetPublicUID);
				PlayerProfileHeader.DecrementFollowingCount(-1); // undo decrement
				UpdateButtonToFollowing();
				ShowToast("Unfollow failed: " + resp.errorData?.message);
				return;
			}

			// Provide Undo for a short window
			ShowUndo($"Unfollowed {targetDisplayName}", () => UndoUnfollow(targetPublicUID));
		});
	}

	private void UndoUnfollow(string targetPublicUID)
	{
		if (_followingIds.Contains(targetPublicUID)) return; // Already re-added somehow
		_followingIds.Add(targetPublicUID);
		PlayerProfileHeader.DecrementFollowingCount(-1);
		UpdateButtonToFollowing();
		// Optionally silently call FollowPlayer again (depends if you want immediate server re-sync)
		LootLockerSDKManager.FollowPlayer(targetPublicUID, _ => {});
	}

	private void UpdateButtonToFollow() => FollowLabel.text = "Follow"; // pseudo
	private void UpdateButtonToFollowing() => FollowLabel.text = "Following";
}

Confirmation Pattern

Use lightweight confirmations only if unfollow has downstream impact (e.g. curated feed). Otherwise allow instant toggle with an Undo snackbar.

Determining If Current Player Follows Target

To check if the logged-in player follows a specific other player you do NOT need to build or hydrate a full cache. Use the single-item pagination shortcut: call the following-list endpoint with the target player's public UID as the cursor (or path parameter depending on SDK variant) and request Count: 1. If one entry is returned, the relationship exists; if zero, it does not.

Why this works: the server returns the players the current player is following starting “from” the supplied cursor (player ULID). Asking for only one result lets the backend tell you immediately if that specific ID is in the set without scanning client‑side.

Pros:

  • O(1) network request per check (no growing local data structure)

  • Constant payload size (either 0 or 1 item)

  • Always fresh (no stale cache issues)

Consider caching only if you batch many checks in the same frame (e.g. rendering 100 profile tiles). For a single profile view, prefer this direct probe.

// Determine if we follow targetPlayerULID by requesting just one item "starting at" that player.
// If the first returned player matches the target, we are following them.

public static class FollowProbe
{
	public static void CheckFollowing(string targetPlayerULID, System.Action<bool> onResult)
	{
		if (string.IsNullOrEmpty(targetPlayerULID)) { onResult?.Invoke(false); return; }

		// Ask for a single entry starting at the target cursor
		LootLockerSDKManager.ListFollowingPaginated(targetPlayerULID, 1, (resp) =>
		{
			if (!resp.success || resp.following == null || resp.following.Length == 0)
			{
				onResult?.Invoke(false);
				return;
			}

			// Server returned at least one player; see if it is the one we asked about.
			bool isFollowing = resp.following[0].player_id == targetPlayerULID;
			onResult?.Invoke(isFollowing);
		});
	}
}

// Usage:
// FollowProbe.CheckFollowing(otherPlayerUlid, (isFollowing) => UIProfileButton.SetState(isFollowing));

Performance Tips

  • Debounce rapid follow toggles (e.g. leaderboards) to one in-flight request per target.

  • Paginate aggressively (e.g. 25–50 per page) for scrolling lists.

  • Preload the first page of “Following” at session start if many UI surfaces need that state.

  • Avoid full refresh after each follow action; surgically mutate local structures.

Example Feature Ideas

  • Activity Feed: Show recent achievements of players you follow.

  • “Players Following This Creator Also Follow…” recommendations (intersect follower sets).

  • Seasonal Follow Goals: Reward cosmetics when a player reaches follower milestones.

Conclusion

In this How-to we listed followers, listed who a player is following, implemented follow/unfollow actions, handled pagination, and optimized UI responsiveness with caching and optimistic updates. You can now extend this to social feeds, recommendations, or creator-style profile experiences. Next, explore adding Friends or managing relationships in the Web Console.

Last updated