Unity 客戶端接入
cn.deeprespond.im.game 讓 Unity 遊戲的玩家加入你的服務端建立的遊戲房間,和其他玩家即時收發操作。支援 iOS、Android、Windows、macOS、Linux;WebGL 遊戲請用 Web 客戶端接入。房間的規則和開通條件見遊戲房間概述。
SDK 版本
當前版本 1.0.0,需要 Unity 2021.3 及以上,Mono 和 IL2CPP 都可以。沒有第三方依賴。
using DeepRespond.Im.Game;
using UnityEngine;
var im = new ImGameClient(new ImGameClientOptions {
ApiUrl = "https://api.example.com",
AppKey = "<App Key>",
DeviceId = SystemInfo.deviceUniqueIdentifier,
});
await im.Auth.LoginWithTicketAsync("alice", loginTicketFromBusinessServer);
var room = await im.Game.JoinAsync("rank-8812", new JoinOptions { Ticket = ticketFromBusinessServer });
room.OnFrame += f => game.Step(f.Frame, f.Inputs); // 严格按帧号顺序,缺帧自动补齐后再回调
room.OnMember += e => ui.UpdateSeat(e.Seat, e.Type); // Online / Offline / Left
room.OnClose += e => ui.ShowResult(e.Reason, e.ResultJson);
room.Ready(); // all_ready 房间:所有人准备后开局
// Update() 里每帧调用,SDK 按房间帧率发送
room.SendInput(game.LocalInput());
// 玩家退出对局时
await room.LeaveAsync();| 要做什麼 | 看哪裡 |
|---|---|
| 把 SDK 加進工程 | 安裝 |
| 登入 | 登入 |
| 加入房間 | 加入 |
| 每幀傳送操作、按幀推進遊戲 | 幀同步 |
| 發聊天、表情、回合制的出牌 | 自定義訊息 |
| 顯示座位、誰線上、延遲 | 房間狀態與成員 |
| 處理斷線、對局結束 | 斷線與結束 |
| 重啟遊戲後回到對局 | 回到對局 |
| 加入失敗時提示玩家 | 錯誤處理 |
| 控制回撥在哪個執行緒執行 | 回撥執行緒 |
你負責 / SDK 負責
| 你負責 | SDK 負責 |
|---|---|
| 從你的服務端拿到登入憑證和加入票據 | 登入、過期時續期;取連線資訊、連線遊戲節點、握手 |
在 Update 裡呼叫 SendInput 傳送本地操作 | 按房間幀率傳送;按幀號排序、去掉重複的幀、缺幀時向節點補幀 |
在 OnFrame 裡按幀推進遊戲邏輯 | 加密傳輸;UDP 連不上時改用 WebSocket;心跳、測延遲、估算伺服器時間 |
根據 OnClose 顯示結算或提示 | 斷線後自動重連,必要時重新取連線資訊,超過重連視窗後結束 |
安裝
方式一:npm 倉庫(推薦,之後可以在 Package Manager 裡升級)。在工程的 Packages/manifest.json 里加上 scopedRegistries 和依賴:
{
"scopedRegistries": [
{ "name": "DeepRespond", "url": "https://registry.npmjs.org", "scopes": ["cn.deeprespond"] }
],
"dependencies": {
"cn.deeprespond.im.game": "1.0.0"
}
}方式二:下載安裝包(內網、離線環境)。下載 cn.deeprespond.im.game-1.0.0.tgz(SHA256SUMS),放進工程目錄(如 Packages/),在 Package Manager 裡點「+」→ Add package from tarball 選擇它。
示例:Package Manager 裡選中本包 → Samples →「Quick Start」匯入 GameRoomQuickStart.cs,掛到場景裡任意物體上,在 Inspector 裡填好地址和票據後執行。
登入
Unity SDK 自帶登入,不需要另外接入 IM 客戶端 SDK。推薦由你的服務端簽發登入憑證(加上 auto_create 時使用者不存在會自動建立),遊戲只拿到一個 5 分鐘內有效的憑證:
await im.Auth.LoginWithTicketAsync("alice", loginTicketFromBusinessServer);選項(ImGameClientOptions) | 說明 |
|---|---|
ApiUrl | IM 的介面地址(必填) |
AppKey | 應用的 App Key,登入時需要 |
DeviceId | 裝置 ID(必填),同一臺裝置保持不變,一般用 SystemInfo.deviceUniqueIdentifier |
DeviceName | 裝置名稱,顯示在登入裝置列表裡 |
Platform | ios、android、windows、macos、linux;不填時按執行的系統判斷。手機上建議按 Application.platform 明確傳入 |
Logger | 日誌輸出,例如接到 Debug.Log。日誌裡沒有金鑰和票據 |
- 登入狀態只在記憶體裡。要下次免登入,監聽
im.Auth.SessionChanged自己儲存(建議存到系統鑰匙串,不要明文寫進PlayerPrefs),啟動時im.Auth.UseSession(saved)。 - access token 過期時,SDK 用 refresh token 自動續期後重試,你不需要處理。續期也失敗時
SessionChanged回撥null,需要重新登入。 await im.Auth.LogoutAsync()退出登入,同時斷開當前房間(OnClose的Reason為signed_out)。
加入
var room = await im.Game.JoinAsync("rank-8812", new JoinOptions { Ticket = ticketFromBusinessServer });
Debug.Log($"{room.State} 座位 {room.Seat} 传输 {room.Transport}"); // Connected、本人座位、udp 或 ws選項(JoinOptions) | 說明 |
|---|---|
Ticket | 你的服務端簽發給這個玩家的票據。open 房間不傳 |
Transport | Auto(預設)先用 UDP,3 秒內沒連上改用 WebSocket;Udp、Ws 只用一種 |
ClientSessionId | 這次加入的 ID(8–64 位可見 ASCII)。不傳時 SDK 生成;同一次加入內重新取連線資訊時保持不變 |
- 連上游戲節點、完成握手後才返回,一般在 1 秒內;UDP 被防火牆擋住時約 3 秒。
- 同一個客戶端同一時刻只能在一個房間裡;
im.Game.Current是當前的房間。對同一個房間再次呼叫JoinAsync返回同一個物件,加入另一個房間丟擲InvalidOperationException,先LeaveAsync()。 - 遊戲要能訪問節點的 UDP 埠和
wss://地址。
幀同步
sync_mode 為 frame 的房間(預設),遊戲節點每秒推進 tick_rate 幀。每一幀,節點把各座位這一幀的操作打包發給所有人;所有玩家按同樣的順序處理同樣的輸入,畫面就保持一致。
room.OnRoomState += e =>
{
if (e.State == GameRoomState.Started) Debug.Log($"开局,第一帧为 {e.StartFrame}");
};
room.OnFrame += f =>
{
// f.Inputs 只包含这一帧有操作的座位
game.Step(f.Frame, f.Inputs);
};
void Update()
{
// 每帧都可以调用(不超过 128 字节):SDK 每 1000 / tick_rate 毫秒发送一次,只发最新的一份
room.SendInput(game.LocalInput());
}OnFrame嚴格按幀號從小到大回調,不重複、不跳號。丟包時 SDK 先用後續包裡的冗餘補上,補不上時向節點請求,補齊後再按順序回撥。SendInput沒連上時返回false,不排隊——過期的操作沒有意義。不要自己按更高的頻率傳送:每秒超過tick_rate的 2 倍會被節點當作異常移出房間,SDK 的節流就是為了避免這一點。- 開局之前傳送的操作會被忽略。
room.Frame是已經回撥的最後一幀。 - 操作用你自己的二進位制格式編碼,例如方向和按鍵各佔幾位。
快照:建房時開啟 snapshot 的房間,節點會指定一名玩家定期提供遊戲狀態。room.IsSnapshotProvider 為 true 的玩家按間隔呼叫 PutSnapshot;有人斷線太久、缺的幀太多時,會先收到 OnSnapshot,再從快照之後的幀繼續:
if (room.IsSnapshotProvider) room.PutSnapshot(room.Frame, game.Serialize()); // 不超过 64KB,按建房时的间隔调用
room.OnSnapshot += s => game.ApplySnapshot(s.Data); // 之后的 OnFrame 从 s.Frame + 1 开始自定義訊息
relay 房間沒有幀,玩家之間只收發自定義訊息;frame 房間也可以用它發聊天、表情這類不進幀的資料。
room.SendCustom(Encoding.UTF8.GetBytes("gg")); // 发给除自己以外的所有人
room.SendCustom(Encoding.UTF8.GetBytes("出牌:红桃A"), to: new[] { 1 }, reliable: true); // 只发给座位 1,保证送达和顺序
room.OnCustom += m => Debug.Log($"座位 {m.From}:{Encoding.UTF8.GetString(m.Data)}");單條不超過 1KB;每個座位每秒最多 60 條、32KB,超過的被節點丟棄,連續 10 秒超限會被移出房間。
房間狀態與成員
| 屬性 | 說明 |
|---|---|
State | 連線狀態:Connecting、Connected、Reconnecting(恢復中)、Rejoining(重新取連線資訊中)、Closed、Left |
RoomState | Waiting 等待開局、Started 對局中、Closing 正在結束 |
Seat | 本人座位(0–9) |
Members | 在座的成員 Seat、Username、Online,按座位排序,離開的不在其中 |
Frame | 已回撥的最後一幀 |
Rtt | 最近 10 次往返延遲的中位數(毫秒),剛連上時為 null |
Transport | 當前使用的連線:udp 或 ws |
ServerTimeMs() | 估算的伺服器時間(Unix 毫秒),用於各端對齊倒計時 |
room.OnMember += e => ui.UpdateSeat(e.Seat, e.Type); // Online 上线、Offline 断线(可能回来)、Left 离开
room.OnState += s => ui.ShowReconnecting(s == GameConnectionState.Reconnecting || s == GameConnectionState.Rejoining);JoinAsync 返回時房間已經是 Connected,之後狀態變化才觸發 OnState。
斷線與結束
斷線時你不需要寫程式碼:SDK 在 5 秒沒收到資料或連線斷開時立即重連,之後每隔 1、2、4、4… 秒再試,回到原來的座位,缺的幀按順序補齊。重連期間 State 為 Reconnecting 或 Rejoining,SendInput 返回 false。遊戲切到後臺被系統暫停、回到前臺後,也按斷線處理並自動重連。
以下情況房間對這個玩家結束,OnClose 回撥一次,SDK 不再重連:
Reason | 什麼時候 | 建議的介面 |
|---|---|---|
room_closed | 對局結束。CloseReason 為結束原因(如 closed_by_server、expired),ResultJson 為你的服務端給的結算結果(JSON 原文) | 顯示結算 |
left | 本人呼叫了 LeaveAsync() | — |
timeout | 斷線超過房間的重連視窗 | 「網路中斷,已離開對局」 |
kicked | 被你的服務端移出 | 「你已被移出房間」 |
replaced | 同一賬號在別處加入了這個座位 | 「你已在其他裝置進入對局」 |
node_draining、room_not_found | 遊戲伺服器維護、房間已不在 | 「對局已中斷」 |
signed_out、destroyed | 退出登入、呼叫了 room.Dispose() 或 im.Dispose()(不通知其他玩家,座位按重連視窗保留) | — |
room.OnRoomState += e =>
{
if (e.State == GameRoomState.Closing) Debug.Log($"即将结束 {e.Reason} {e.ResultJson}");
};
room.OnClose += e =>
{
if (e.Reason == "room_closed") ui.ShowResult(e.CloseReason, e.ResultJson);
else ui.Toast($"离开对局:{e.Reason}");
};離開:await room.LeaveAsync() 通知遊戲節點和 IM,其他玩家收到這個座位 Left。可以重複呼叫。
回到對局
遊戲重啟後,連線資訊只在記憶體裡,需要重新加入:
var current = await im.Game.GetCurrentAsync();
if (current != null)
{
// 还在对局中的座位:不需要新票据,重新加入回到原座位
var room = await im.Game.JoinAsync(current.RoomId);
ui.Toast($"回到座位 {room.Seat}");
}GetCurrentAsync 只返回本裝置(本次登入)還沒離開的房間。超過重連視窗的座位已經釋放,需要你的服務端重新簽發票據。
錯誤處理
JoinAsync 失敗時丟擲異常。用 GameErrors.KindOf(ex) 得到分類,決定給玩家看什麼:
try
{
await im.Game.JoinAsync("rank-8812", new JoinOptions { Ticket = ticketFromBusinessServer });
}
catch (Exception ex)
{
switch (GameErrors.KindOf(ex))
{
case GameErrorKind.TicketInvalid: ui.Toast("入场凭证已失效,请重新匹配"); break;
case GameErrorKind.RoomFull: ui.Toast("房间已满"); break;
case GameErrorKind.JoinedOnOtherDevice: ui.Toast("你已在其他设备进入该对局"); break;
case GameErrorKind.RoomClosed: ui.Toast("对局已结束"); break;
default: ui.Toast("暂时无法进入对局,请稍后再试"); break;
}
}| 分類 | 原因 | 建議 |
|---|---|---|
TicketInvalid | 票據不對、過期或已經用過 | 讓你的服務端重新簽發 |
GameDisabled | 平臺沒有開放遊戲房間,或套餐不含 | 「暫時無法進入對局」 |
RoomClosed | 房間不存在或已經結束 | 「對局已結束」 |
RoomFull | 座位已滿,或指定的座位已被佔用 | 「房間已滿」 |
JoinedOnOtherDevice | 同一賬號已在其他裝置加入這個房間 | 提示玩家,或讓他在原裝置離開 |
NodeUnavailable | 房間所在的遊戲伺服器暫時不可用 | 稍後重試 |
UpgradeRequired | SDK 版本太舊 | 升級 SDK |
Network | 網路錯誤或超時 | 稍後重試 |
| 異常型別 | 什麼時候 |
|---|---|
ImApiException | 介面返回錯誤。Status、Code、Reason(details.reason)同服務端錯誤 |
GameNodeException | 遊戲節點拒絕連線。Name 為原因,如 room_full |
ImNetworkException | 網路錯誤或超時 |
ArgumentException、InvalidOperationException | 引數不對(如 SendInput 超過 128 位元組、SendCustom 超過 1KB、PutSnapshot 超過 64KB)、沒有登入、已在其他房間 |
回撥執行緒
ImGameClientOptions.EventMode | 回撥在哪裡執行 | 什麼時候用 |
|---|---|---|
SynchronizationContext(預設) | 呼叫 JoinAsync 的執行緒:在 Unity 主執行緒呼叫時就是主執行緒,可以直接改場景裡的物件 | 大多數遊戲 |
Manual | 你呼叫 room.DispatchEvents() 的地方 | 幀同步遊戲想在自己的 Update / FixedUpdate 裡固定時機處理幀 |
- 所有回撥按發生順序執行;回撥裡丟擲的異常寫進日誌,不影響 SDK。
- 網路收發在後臺執行緒,不佔主執行緒。
State、Members這些屬性在任何執行緒都可以讀。
介面參考
im.Game | 說明 |
|---|---|
Current | 當前的房間,沒有時為 null |
JoinAsync(roomId, options?) | 加入,返回 GameRoom |
GetCurrentAsync() | 本裝置當前所在的房間(RoomId、RoomStatus、Seat、MemberStatus),沒有時為 null |
LeaveAsync(roomId) | 不通過房間物件離開 |
im.Auth | 說明 |
|---|---|
LoginWithTicketAsync(username, ticket) | 用登入憑證登入 |
LoginWithPasswordAsync(username, password) | 使用者名稱密碼登入 |
UseSession(session)、Session、SessionChanged | 恢復、讀取儲存的登入狀態 |
RefreshAsync()、LogoutAsync() | 續期、退出登入 |
GameRoom 方法 | 說明 |
|---|---|
SendInput(data) | 本幀操作,≤ 128 位元組 |
SendCustom(data, to?, reliable?) | 自定義訊息,≤ 1KB;to 為座位陣列 |
Ready() | 準備 |
Start() | 房主開始。通過 IM 建立的房間暫不開放房主開始,manual 房間由你的服務端呼叫開局介面 |
PutSnapshot(frame, data) | 提供快照,≤ 64KB |
LeaveAsync() | 離開 |
Dispose() | 斷開連線但不離開(座位按重連視窗保留) |
DispatchEvents() | Manual 模式下執行積壓的回撥 |
事件 OnState、OnRoomState、OnFrame、OnCustom、OnMember、OnSnapshot、OnClose | 用 += 訂閱、-= 取消 |
平臺說明
| 平臺 | 說明 |
|---|---|
| iOS | 節點地址需要是域名:App Store 要求在純 IPv6 網路下可用,IPv4 地址在這種網路下連不上 |
| Android | Player Settings 裡 Internet Access 設為 Require,保證打包時帶上網路許可權 |
| WebGL | 不支援(瀏覽器裡沒有 UDP),用 Web 客戶端接入 |
更新記錄
| 版本 | 日期 | 內容 |
|---|---|---|
| 1.0.0 | 2026-11-10 | 首個版本:登入、加入、幀同步、自定義訊息、快照、斷線恢復、回到對局 |
許可證
MIT。
