Skip to main content

Social Features

The social system provides real-time chat, guilds, friends with presence tracking, and peer-to-peer monster/item trading. All services use Nakama's built-in social APIs (Groups, Friends, Chat channels) and realtime matches (for trading).

The chat panel over a live world

The tabs across the top of that panel are the channel types in the table below — the player picks a scope, not a server. Local and Global are different channels of the same connection, so switching between them costs nothing and loses nothing.

The guild panel with no guild joined

The guild panel is worth showing in its empty state, because that is the state every new account starts in: the sections exist (members, guild chat, guild wars, treasury, settings) and the panel says plainly that you are not in a guild yet, with the two ways out of it right there. Nothing is hidden until you qualify for it.

Architecture Overview​

ClassFileResponsibility
ChatServiceSocial/SocialServices.csReal-time chat with 7 channel types
GuildServiceSocial/SocialServices.csGuild CRUD, roles, member management via Nakama Groups
FriendServiceSocial/SocialServices.csFriends list, presence tracking, blocking
TradeServiceSocial/SocialServices.csP2P trading with server-validated state machine

Access via NakamaManager.Instance:

var chat = NakamaManager.Instance.Chat;
var guilds = NakamaManager.Instance.Guilds;
var friends = NakamaManager.Instance.Friends;
var trade = NakamaManager.Instance.Trade;

Chat System​

ChatService implements a multi-channel real-time chat system using Nakama's channel messaging.

Channel Types​

The ChatChannel enum defines 7 channel types:

ChannelDescription
GlobalServer-wide public chat
LocalVisible to nearby players (same map/area)
TradeEconomy and trading discussions
GuildGuild-only messages
WhisperPrivate 1-on-1 direct messages
SystemServer announcements and notifications
BattleBattle-related communication

Joining Channels​

The global channel is joined automatically during connection via JoinGlobalChannelAsync(). It uses Nakama's Room-type channel with persistence enabled:

// Automatic on connect -- but can be called manually
await chat.JoinGlobalChannelAsync();

Sending Messages​

// Send to a public channel
await chat.SendMessageAsync(ChatChannel.Global, "Hello everyone!");
await chat.SendMessageAsync(ChatChannel.Trade, "WTB: Rare Candy x10");

// Whisper (direct message)
await chat.SendMessageAsync(ChatChannel.Whisper, "Hey!", targetId: recipientUserId);

Whispers use Nakama's DirectMessage channel type. The service automatically joins a DM channel with the target user before sending.

Receiving Messages​

chat.OnMessageReceived += (ChatMessage msg) =>
{
// msg.SenderId, msg.SenderName, msg.Content
// msg.Channel (ChatChannel enum), msg.Timestamp
};

// Access message history (capped at ChatHistoryCount)
IReadOnlyList<ChatMessage> history = chat.History;

Message history is capped at OnlineConfig.ChatHistoryCount (default: 50). Oldest messages are removed when the cap is exceeded.

Channel Events​

chat.OnChannelJoined += (ChatChannel channel) =>
{
Debug.Log($"Joined {channel} channel");
};

Cleanup​

All channels are left during disconnect:

await chat.LeaveAllChannelsAsync();

Guild System​

GuildService manages guilds (teams/clubs) using Nakama's Groups API.

Creating a Guild​

GuildInfo guild = await guilds.CreateGuildAsync(
name: "Team Rocket",
tag: "TR",
description: "Prepare for trouble!");

// guild.Id, guild.Name, guild.Tag, guild.MemberCount, guild.MaxMembers

Maximum members per guild: OnlineConfig.MaxGuildMembers (default: 50).

Joining and Leaving​

bool joined = await guilds.JoinGuildAsync(guildId);
bool left = await guilds.LeaveGuildAsync();

Guild Roles​

The GuildRole enum defines a permission hierarchy:

RoleValuePermissions
Leader0Full control (kick, promote, disband)
Officer1Kick members, manage settings
Member2Standard participation
Recruit3Limited access (probation)

Member Management​

// Promote a member
await guilds.PromoteMemberAsync(memberId, GuildRole.Officer);

// Kick a member
await guilds.KickMemberAsync(memberId);

Searching Guilds​

List<GuildInfo> results = await guilds.SearchGuildsAsync("Rocket");

foreach (var g in results)
{
// g.Id, g.Name, g.Description
// g.MemberCount, g.MaxMembers
}

Guild State​

bool isInGuild = guilds.IsInGuild;
GuildInfo current = guilds.CurrentGuild;

guilds.OnGuildUpdated += (GuildInfo info) => { };
guilds.OnMemberJoined += (string userId) => { };
guilds.OnMemberLeft += (string userId) => { };

Friends System​

FriendService uses Nakama's Friends API with real-time presence tracking via status events.

The friends panel

The 0 online counter is presence, not a roster size — it updates from status events as people connect and disconnect, without the panel asking again.

Loading Friends​

Friends are loaded automatically during connection:

await friends.RefreshFriendsListAsync(userId);

This calls Client.ListFriendsAsync (limited to OnlineConfig.MaxFriends, default: 100) and then follows all friends' statuses via Socket.FollowUsersAsync() for real-time presence updates.

Friend Requests​

// Send a friend request
bool sent = await friends.SendFriendRequestAsync(targetUserId);

// Accept a friend request
bool accepted = await friends.AcceptFriendRequestAsync(fromUserId);

// Remove a friend
bool removed = await friends.RemoveFriendAsync(friendId);

Presence Tracking​

Real-time presence updates arrive via ReceivedStatusPresence on the socket:

friends.OnFriendOnline += (FriendInfo info) =>
{
Debug.Log($"{info.DisplayName} is online!");
};

friends.OnFriendOffline += (string userId) =>
{
Debug.Log($"Friend {userId} went offline");
};

friends.OnFriendRequestReceived += (FriendInfo info) =>
{
// Show friend request notification
};

Friends Data​

IReadOnlyList<FriendInfo> list = friends.FriendsList;

foreach (var f in list)
{
// f.UserId, f.DisplayName, f.IsOnline
// f.CurrentMap, f.Level
}

Blocking​

bool blocked = await friends.BlockUserAsync(userId);

Uses Nakama's BlockFriendsAsync which prevents all interaction.

Player-to-Player Trading​

TradeService implements a complete P2P trading system using Nakama realtime matches with a state machine protocol.

Trade Protocol OpCodes​

OpCodeConstantMeaning
100OP_TRADE_REQUESTInitial trade request
101OP_TRADE_OFFERPlayer offers a monster or item
102OP_TRADE_CONFIRMPlayer confirms the trade
103OP_TRADE_CANCELPlayer cancels the trade
104OP_TRADE_COMPLETEServer confirms trade completed

Trade States​

The TradeState enum tracks the trade lifecycle:

Pending -> Offering -> Confirming -> Completed (or Cancelled at any point)

Initiating a Trade​

string tradeId = await trade.RequestTradeAsync(targetUserId);

This creates a Nakama match for the trade session and notifies the target via the trade_request RPC.

Offering Items​

// Offer a monster
await trade.OfferMonsterAsync(tradeId, monsterId);

// Offer an item
await trade.OfferItemAsync(tradeId, itemId: "rare_candy", quantity: 5);

Offers are sent as match state messages with OP_TRADE_OFFER.

Confirming and Completing​

// Both players must confirm
await trade.ConfirmTradeAsync(tradeId);

When both players confirm, the server executes the trade atomically and sends OP_TRADE_COMPLETE.

Cancelling​

await trade.CancelTradeAsync(tradeId);

Sends OP_TRADE_CANCEL, leaves the match, and clears the active trade.

Trade Events​

trade.OnTradeRequestReceived += (string fromUserId, string fromName) => { };
trade.OnTradeStateChanged += (TradeState state) => { };
trade.OnTradeCompleted += (bool success) =>
{
if (success) Debug.Log("Trade completed!");
else Debug.Log("Trade was cancelled.");
};

Trade State Properties​

bool isTrading = trade.IsTrading;
string activeTradeId = trade.ActiveTradeId;
string activeMatchId = trade.ActiveMatchId;

Trade match state is also routed through NakamaManager's ReceivedMatchState handler, which forwards to Trade.HandleMatchState().

Social Notifications​

NakamaManager surfaces all Nakama notifications through a unified event:

NakamaManager.Instance.OnNotification += (string subject, string content) =>
{
Debug.Log($"Notification: {subject} - {content}");
};

Additionally, dedicated events exist for common social actions:

NakamaManager.Instance.OnBattleInvite += (string fromUserId) => { };
NakamaManager.Instance.OnTradeRequest += (string fromUserId) => { };
NakamaManager.Instance.OnFriendRequest += (string fromUserId) => { };

Configuration Reference​

Config FieldDefaultDescription
MaxFriends100Maximum friends per player
MaxGuildMembers50Maximum guild members
ChatHistoryCount50Chat messages to retain in memory