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).

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.

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