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).
Architecture Overview
| Class | File | Responsibility |
|---|---|---|
ChatService | Social/SocialServices.cs | Real-time chat with 7 channel types |
GuildService | Social/SocialServices.cs | Guild CRUD, roles, member management via Nakama Groups |
FriendService | Social/SocialServices.cs | Friends list, presence tracking, blocking |
TradeService | Social/SocialServices.cs | P2P 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:
| Channel | Description |
|---|---|
Global | Server-wide public chat |
Local | Visible to nearby players (same map/area) |
Trade | Economy and trading discussions |
Guild | Guild-only messages |
Whisper | Private 1-on-1 direct messages |
System | Server announcements and notifications |
Battle | Battle-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:
| Role | Value | Permissions |
|---|---|---|
Leader | 0 | Full control (kick, promote, disband) |
Officer | 1 | Kick members, manage settings |
Member | 2 | Standard participation |
Recruit | 3 | Limited 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.
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
| OpCode | Constant | Meaning |
|---|---|---|
100 | OP_TRADE_REQUEST | Initial trade request |
101 | OP_TRADE_OFFER | Player offers a monster or item |
102 | OP_TRADE_CONFIRM | Player confirms the trade |
103 | OP_TRADE_CANCEL | Player cancels the trade |
104 | OP_TRADE_COMPLETE | Server 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 Field | Default | Description |
|---|---|---|
MaxFriends | 100 | Maximum friends per player |
MaxGuildMembers | 50 | Maximum guild members |
ChatHistoryCount | 50 | Chat messages to retain in memory |