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。
