Web 客戶端接入
im.game 讓已登入的玩家加入你的服務端建立的遊戲房間,和其他玩家即時收發操作。適用於網頁遊戲和 Cocos Creator 的 Web 平臺。房間的規則和開通條件見遊戲房間概述。
SDK 版本
遊戲房間需要 @deeprespond/im-web 1.1.0 及以上;React、Vue 的 hooks 需要 @deeprespond/im-web-react、@deeprespond/im-web-vue 1.1.1 及以上(1.1.0 的 React useGameRoom() 在加入房間後會報錯)。
const room = await im.game.join('rank-8812', { 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.result)); // 对局结束、被移出等,之后不再重连
room.ready(); // all_ready 房间:所有人准备后开局
const timer = setInterval(() => room.sendInput(game.localInput()), 33);
// 玩家退出对局时
clearInterval(timer);
await room.leave();| 要做什麼 | 看哪裡 |
|---|---|
| 加入房間 | 加入 |
| 每幀傳送操作、按幀推進遊戲 | 幀同步 |
| 發聊天、表情、回合制的出牌 | 自定義訊息 |
| 顯示座位、誰線上、延遲 | 房間狀態與成員 |
| 處理斷線、對局結束 | 斷線與結束 |
| 重新整理頁面後回到對局 | 回到對局 |
| 加入失敗時提示玩家 | 錯誤處理 |
| 在 React、Vue 中使用 | React 和 Vue |
你負責 / SDK 負責
| 你負責 | SDK 負責 |
|---|---|
| 從你的服務端拿到加入票據 | 取連線資訊、連線遊戲節點、握手 |
每幀呼叫 sendInput 傳送本地操作 | 按幀號排序、去掉重複的幀、缺幀時向節點補幀 |
在 onFrame 裡按幀推進遊戲邏輯 | 每秒心跳、測延遲、估算伺服器時間 |
根據 onClose 顯示結算或提示 | 斷線後自動重連,必要時重新取連線資訊,超過重連視窗後結束 |
加入
if (!im.game.supported) throw new Error('当前浏览器不支持');
const room = await im.game.join('rank-8812', { ticket: ticketFromBusinessServer });
console.log(room.state, room.seat, room.members); // 'connected'、本人座位、在座的成员| 選項 | 說明 |
|---|---|
ticket | 你的服務端簽發給這個玩家的票據。open 房間不傳 |
client_session_id | 這次加入的 ID(8–64 位可見 ASCII)。不傳時 SDK 生成;同一次加入內重新取連線資訊時保持不變 |
signal | AbortSignal,取消還沒完成的加入 |
- 連上游戲節點、完成握手後才返回,一般在 1 秒內。
- 同一個客戶端同一時刻只能在一個房間裡;
im.game.current是當前的房間。對同一個房間再次呼叫join返回同一個物件,加入另一個房間以invalid_state拒絕,先leave()。 - 瀏覽器使用 WebSocket 連線遊戲節點,頁面需要能訪問節點的
wss://地址。
幀同步
sync_mode 為 frame 的房間(預設),遊戲節點每秒推進 tick_rate 幀。每一幀,節點把各座位這一幀的操作打包發給所有人;所有玩家按同樣的順序處理同樣的輸入,畫面就保持一致。
room.onRoomState((e) => {
if (e.state === 'started') console.log('开局,第一帧为', e.startFrame);
});
room.onFrame((f) => {
// f.inputs 只包含这一帧有操作的座位
game.step(f.frame, f.inputs);
});
// 每帧发送一次本地操作(不超过 128 字节)
setInterval(() => room.sendInput(game.localInput()), 1000 / 30);onFrame嚴格按幀號從小到大回調,不重複、不跳號。丟包時 SDK 先用後續包裡的冗餘補上,補不上時向節點請求,補齊後再按順序回撥。sendInput在同一輪事件迴圈裡多次呼叫只發最後一次。沒連上時返回false,不排隊——過期的操作沒有意義。- 開局之前傳送的操作會被忽略。
room.frame是已經回撥的最後一幀。 - 操作用你自己的二進位制格式編碼,例如方向和按鍵各佔幾位。
快照:建房時開啟 snapshot 的房間,節點會指定一名玩家定期提供遊戲狀態。room.getSnapshot().snapshotProvider 為 true 的玩家按間隔呼叫 putSnapshot;有人斷線太久、缺的幀太多時,會先收到 onSnapshot,再從快照之後的幀繼續:
setInterval(() => {
if (room.getSnapshot().snapshotProvider) room.putSnapshot(game.frame, game.serialize()); // 不超过 64KB
}, 30_000);
room.onSnapshot((s) => game.applySnapshot(s.data)); // 之后的 onFrame 从 s.frame + 1 开始自定義訊息
relay 房間沒有幀,玩家之間只收發自定義訊息;frame 房間也可以用它發聊天、表情這類不進幀的資料。
const text = new TextEncoder();
room.sendCustom(text.encode('gg')); // 发给除自己以外的所有人
room.sendCustom(text.encode('出牌:红桃A'), { to: [1], reliable: true }); // 只发给座位 1
room.onCustom((m) => console.log(`座位 ${m.from}:`, new TextDecoder().decode(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 |
serverTime() | 估算的伺服器時間(毫秒),用於各端對齊倒計時 |
room.onMember((e) => {
// e.type:online 上线、offline 断线(可能回来)、left 离开
ui.updateSeat(e.seat, e.type);
});
room.onState((s) => ui.showReconnecting(s === 'reconnecting' || s === 'rejoining'));room.getSnapshot() 返回包含以上資訊的只讀物件,內容變化時換成新物件;room.subscribe(fn) 在變化時通知。幀號不在快照裡(每幀都變),幀用 onFrame 接收。
所有 onXxx 方法和 room.on(event, fn) 都返回取消訂閱的函式。
斷線與結束
斷線時你不需要寫程式碼:SDK 在 5 秒沒收到資料或連線斷開時立即重連,之後每隔 1、2、4、4… 秒再試,回到原來的座位,缺的幀按順序補齊。重連期間 state 為 reconnecting 或 rejoining,sendInput 返回 false。
以下情況房間對這個玩家結束,onClose 回撥一次,SDK 不再重連:
reason | 什麼時候 | 建議的介面 |
|---|---|---|
room_closed | 對局結束。closeReason 為結束原因(如 closed_by_server、expired),result 為你的服務端給的結算結果 | 顯示結算 |
left | 本人呼叫了 leave() | — |
timeout | 斷線超過房間的重連視窗 | 「網路中斷,已離開對局」 |
kicked | 被你的服務端移出 | 「你已被移出房間」 |
replaced | 同一賬號在別處加入了這個座位 | 「你已在其他裝置進入對局」 |
node_draining、room_not_found | 遊戲伺服器維護、房間已不在 | 「對局已中斷」 |
signed_out、destroyed | 退出登入、銷燬了客戶端(不通知其他玩家,座位按重連視窗保留) | — |
room.onRoomState((e) => {
if (e.state === 'closing') console.log('即将结束', e.reason, e.result);
});
room.onClose((e) => {
if (e.reason === 'room_closed') ui.showResult(e.closeReason ?? '', e.result);
else ui.toast(`离开对局:${e.reason}`);
});離開:await room.leave() 通知遊戲節點和 IM,其他玩家收到這個座位 left。可以重複呼叫。
回到對局
重新整理頁面或重新開啟後,連線資訊只在記憶體裡,需要重新加入:
const { room: current, member } = await im.game.getCurrent();
if (current && member) {
// 还在对局中的座位:不需要新票据,重新加入回到原座位
const room = await im.game.join(current.room_id);
ui.toast(`回到座位 ${room.seat}`);
}getCurrent 只返回本裝置(本次登入)還沒離開的房間。超過重連視窗的座位已經釋放,需要你的服務端重新簽發票據。
錯誤處理
join 失敗時丟擲 DRError。用 im.game.errorKind(err) 得到分類,決定給玩家看什麼:
try {
await im.game.join('rank-8812', { ticket: ticketFromBusinessServer });
} catch (err) {
switch (im.game.errorKind(err)) {
case 'ticket_invalid': ui.toast('入场凭证已失效,请重新匹配'); break;
case 'room_full': ui.toast('房间已满'); break;
case 'joined_on_other_device': ui.toast('你已在其他设备进入该对局'); break;
case 'room_closed': ui.toast('对局已结束'); break;
default: ui.toast('暂时无法进入对局,请稍后再试');
}
}| 分類 | 原因 | 建議 |
|---|---|---|
ticket_invalid | 票據不對、過期或已經用過 | 讓你的服務端重新簽發 |
game_disabled | 平臺沒有開放遊戲房間,或套餐不含 | 「暫時無法進入對局」 |
room_closed | 房間不存在或已經結束 | 「對局已結束」 |
room_full | 座位已滿,或指定的座位已被佔用 | 「房間已滿」 |
joined_on_other_device | 同一賬號已在其他裝置加入這個房間 | 提示玩家,或讓他在原裝置離開 |
node_unavailable | 房間所在的遊戲伺服器暫時不可用 | 稍後重試 |
upgrade_required | SDK 版本太舊 | 升級 SDK |
network | 網路錯誤或超時 | 稍後重試 |
原始的錯誤碼在 err.code、err.reason:加入介面的錯誤同服務端錯誤;遊戲節點拒絕連線時 code 為 game_node_error。
本地檢查:sendInput 超過 128 位元組、sendCustom 超過 1KB、putSnapshot 超過 64KB 時丟擲 local_validation。
React 和 Vue
useGameRoom() 返回當前房間的快照(加入、結束時自動更新),useGameEvent() 訂閱房間的事件、元件解除安裝時自動取消。加入房間仍然由你在使用者操作時呼叫 im.game.join()。
import { useDRClient, useGameEvent, useGameRoom } from '@deeprespond/im-web-react';
function Match({ ticket }: { ticket: string }) {
const im = useDRClient();
const room = useGameRoom(); // null:还没加入
useGameEvent(undefined, 'frame', (f) => game.step(f.frame, f.inputs));
if (!room) return <button onClick={() => im?.game.join('rank-8812', { ticket })}>进入对局</button>;
return <div>座位 {room.seat} · {room.state} · 延迟 {room.rtt ?? '-'} ms · 在座 {room.members.length} 人</div>;
}Vue 的用法相同,返回只讀的 ShallowRef:
import { useGameEvent, useGameRoom } from '@deeprespond/im-web-vue';
const room = useGameRoom(); // room.value?.state
useGameEvent(undefined, 'member', (e) => ui.updateSeat(e.seat, e.type));| API | 說明 |
|---|---|
useGameRoom(room?) | 不傳時跟隨 im.game.current;傳入房間物件時用它;傳 null 返回 null |
useGameEvent(room, event, listener) | room 為 undefined 時用當前房間;event 為 frame、custom、member、roomState、snapshot、state、close |
其他 hooks 見在 React 和 Vue 中使用。
介面參考
im.game | 說明 |
|---|---|
supported | 當前環境能否加入遊戲房間 |
current | 當前的房間,沒有時為 null |
join(roomId, options?) | 加入,返回 GameRoom |
getCurrent() | 本裝置當前所在的房間 { room, member } |
leave(roomId) | 不通過房間物件離開 |
errorKind(err) | 錯誤的分類 |
GameRoom 方法 | 說明 |
|---|---|
sendInput(data) | 本幀操作,≤ 128 位元組 |
sendCustom(data, { to?, reliable? }) | 自定義訊息,≤ 1KB;to 為座位陣列 |
ready() | 準備 |
start() | 房主開始。通過 IM 建立的房間暫不開放房主開始,manual 房間由你的服務端呼叫開局介面 |
putSnapshot(frame, data) | 提供快照,≤ 64KB |
leave() | 離開 |
getSnapshot()、subscribe(fn) | 狀態快照和變化通知 |
on(event, fn)、onState、onRoomState、onFrame、onCustom、onMember、onSnapshot、onClose | 事件,返回取消函式 |
頁面切到後臺時,瀏覽器可能降低計時器頻率,幀和心跳會變慢;長時間在後臺可能斷線,回到前臺後 SDK 自動重連。
