Nakama Integration Guide¶
Deep integration with the Nakama game server backend.
Overview¶
Nakama provides:
- Authentication - User accounts, social login
- Realtime - WebSockets for multiplayer
- Storage - Server-side data persistence
- Leaderboards - Global and friend rankings
- RPCs - Custom server-side logic
- Tournaments - Competition systems
Architecture¶
graph LR
A[Unity Client] -->|REST API| B[Nakama Server]
A -->|WebSocket| B
B --> C[(Database)]
B --> D[Custom RPCs]
D --> E[Go/TypeScript/Lua] Connection Management¶
Automatic Connection¶
The SDK handles connection automatically:
// Connection happens during SDK initialization
// IntelliVerseXSDK.Initialize() connects to Nakama
// Check connection status
bool isConnected = IVXNakamaManager.Instance.IsConnected;
Manual Connection¶
For advanced control:
public async Task ConnectManually()
{
// Connect to server
await IVXNakamaManager.Instance.ConnectAsync();
// Connect socket for realtime features
await IVXNakamaManager.Instance.ConnectSocketAsync();
}
Authentication¶
Link with Nakama Session¶
// After authentication, get the Nakama session
var session = IVXNakamaManager.Instance.Session;
// Session contains:
// - UserId
// - Username
// - AuthToken
// - RefreshToken
// - Expiry
Custom Authentication¶
// Authenticate with custom ID
await IVXNakamaManager.Instance.AuthenticateCustomAsync(
customId: "unique-device-id",
username: "Player123",
create: true
);
Storage¶
Store User Data¶
public async Task SavePlayerData()
{
var data = new PlayerData
{
Level = 10,
Experience = 5000,
Coins = 1500
};
// Write to Nakama storage
await IVXNakamaManager.Instance.WriteStorageObjectAsync(
collection: "player_data",
key: "profile",
value: JsonUtility.ToJson(data),
readPermission: 2, // Owner only
writePermission: 1 // Owner only
);
}
Read User Data¶
public async Task<PlayerData> LoadPlayerData()
{
var result = await IVXNakamaManager.Instance.ReadStorageObjectAsync(
collection: "player_data",
key: "profile"
);
if (result != null)
{
return JsonUtility.FromJson<PlayerData>(result.Value);
}
return new PlayerData(); // Default
}
Permission Levels¶
| Value | Read | Write |
|---|---|---|
| 0 | No one | No one |
| 1 | Owner | Owner |
| 2 | Friends | - |
| 3 | Public | - |
Leaderboards¶
Submit Score¶
public async Task SubmitHighScore(int score)
{
await IVXNakamaManager.Instance.WriteLeaderboardRecordAsync(
leaderboardId: "weekly_highscores",
score: score,
metadata: JsonUtility.ToJson(new { level = 10 })
);
}
Get Rankings¶
public async Task<List<LeaderboardEntry>> GetTopScores()
{
var records = await IVXNakamaManager.Instance.ListLeaderboardRecordsAsync(
leaderboardId: "weekly_highscores",
limit: 100
);
return records.Select(r => new LeaderboardEntry
{
Rank = r.Rank,
Username = r.Username,
Score = r.Score
}).ToList();
}
Get Around Player¶
// Get scores around the current player
var aroundMe = await IVXNakamaManager.Instance.ListLeaderboardRecordsAroundOwnerAsync(
leaderboardId: "weekly_highscores",
ownerId: userId,
limit: 10
);
RPC Calls¶
Call Server Function¶
public async Task<T> CallRpc<T>(string functionName, object payload)
{
string jsonPayload = JsonUtility.ToJson(payload);
var response = await IVXNakamaManager.Instance.RpcAsync(
id: functionName,
payload: jsonPayload
);
return JsonUtility.FromJson<T>(response.Payload);
}
// Usage
var rewards = await CallRpc<RewardResponse>("claim_daily_reward", new { userId });
Common RPCs¶
// Claim daily reward
await IVXNakamaManager.Instance.RpcAsync("claim_daily_reward", "{}");
// Get quiz questions
var quiz = await IVXNakamaManager.Instance.RpcAsync("get_daily_quiz", "{}");
// Submit quiz answers
await IVXNakamaManager.Instance.RpcAsync("submit_quiz_answers", answersJson);
Realtime Features¶
Connect Socket¶
public async Task ConnectRealtime()
{
await IVXNakamaManager.Instance.ConnectSocketAsync();
// Subscribe to events
IVXNakamaManager.Instance.Socket.ReceivedMatchState += OnMatchState;
IVXNakamaManager.Instance.Socket.ReceivedNotification += OnNotification;
}
Send Realtime Message¶
public async Task SendMatchState(long opCode, byte[] data)
{
await IVXNakamaManager.Instance.Socket.SendMatchStateAsync(
matchId: currentMatchId,
opCode: opCode,
state: data
);
}
Matchmaking¶
Find Match¶
public async Task FindMatch()
{
// Add to matchmaking queue
var ticket = await IVXNakamaManager.Instance.Socket.AddMatchmakerAsync(
query: "+properties.skill:>=80",
minCount: 2,
maxCount: 4,
stringProperties: new Dictionary<string, string>
{
{ "region", "us-east" }
},
numericProperties: new Dictionary<string, double>
{
{ "skill", 85.0 }
}
);
// Wait for match
IVXNakamaManager.Instance.Socket.ReceivedMatchmakerMatched += OnMatchFound;
}
private async void OnMatchFound(IMatchmakerMatched matched)
{
// Join the match
var match = await IVXNakamaManager.Instance.Socket.JoinMatchAsync(matched);
currentMatchId = match.Id;
}
Notifications¶
Listen for Notifications¶
public void SetupNotifications()
{
IVXNakamaManager.Instance.Socket.ReceivedNotification += (notification) =>
{
Debug.Log($"Notification: {notification.Subject}");
// Handle by type
switch (notification.Code)
{
case 1: // Friend request
HandleFriendRequest(notification);
break;
case 2: // Reward
HandleReward(notification);
break;
}
};
}
List Pending Notifications¶
var notifications = await IVXNakamaManager.Instance.ListNotificationsAsync(
limit: 50,
cacheableCursor: null
);
foreach (var n in notifications.Notifications)
{
ProcessNotification(n);
}
Error Handling¶
Handle Nakama Errors¶
try
{
await IVXNakamaManager.Instance.RpcAsync("my_function", payload);
}
catch (ApiResponseException ex)
{
// Server returned an error
Debug.LogError($"API Error {ex.StatusCode}: {ex.Message}");
switch (ex.StatusCode)
{
case 401:
// Unauthorized - re-authenticate
await RefreshAuth();
break;
case 404:
// Not found
break;
case 500:
// Server error - retry later
break;
}
}
catch (Exception ex)
{
// Network or other error
Debug.LogError($"Error: {ex.Message}");
}
Best Practices¶
1. Batch Operations¶
// Instead of multiple calls, batch when possible
var objects = new List<WriteStorageObject>
{
new WriteStorageObject { Collection = "data", Key = "profile", Value = profileJson },
new WriteStorageObject { Collection = "data", Key = "settings", Value = settingsJson }
};
await IVXNakamaManager.Instance.WriteStorageObjectsAsync(objects);
2. Cache Locally¶
// Cache frequently accessed data
private PlayerData _cachedProfile;
public async Task<PlayerData> GetProfile()
{
if (_cachedProfile == null)
{
_cachedProfile = await LoadFromServer();
}
return _cachedProfile;
}
3. Handle Reconnection¶
IVXNakamaManager.Instance.OnDisconnected += async () =>
{
Debug.Log("Disconnected, attempting reconnect...");
await Task.Delay(2000);
await IVXNakamaManager.Instance.ConnectSocketAsync();
};
Server-Side Code Examples¶
TypeScript RPC¶
// Register RPC on server
function rpcClaimDailyReward(
ctx: nkruntime.Context,
logger: nkruntime.Logger,
nk: nkruntime.Nakama,
payload: string
): string {
const userId = ctx.userId;
// Check if already claimed
const lastClaim = getLastClaimTime(nk, userId);
if (isToday(lastClaim)) {
throw Error("Already claimed today");
}
// Grant reward
const reward = calculateReward(nk, userId);
nk.walletUpdate(userId, { coins: reward });
return JSON.stringify({ success: true, reward });
}
// Register in InitModule
initializer.registerRpc("claim_daily_reward", rpcClaimDailyReward);
Troubleshooting¶
| Issue | Solution |
|---|---|
| Connection timeout | Check server URL, firewall |
| Session expired | Call refresh or re-authenticate |
| RPC not found | Verify function registered on server |
| Socket won't connect | Authenticate first, then connect socket |
See Backend Configuration for server setup.