Web client
im.game lets a signed-in player join a game room your server created and exchange inputs with other players in real time. It works for web games and for the web platform of Cocos Creator. Room rules and requirements are in the game rooms overview.
SDK version
Game rooms need @deeprespond/im-web 1.1.0 or later; the React and Vue hooks need @deeprespond/im-web-react and @deeprespond/im-web-vue 1.1.1 or later (in 1.1.0, React's useGameRoom() throws once a room is joined).
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();| To do this | See |
|---|---|
| Join a room | Join |
| Send an input every frame and advance the game frame by frame | Frame sync |
| Send chat, emotes, or a card play in a turn-based game | Custom messages |
| Show seats, who is online, latency | Room state and members |
| Handle disconnects and the end of a match | Disconnects and the end |
| Return to the match after reloading the page | Return to a match |
| Tell the player why joining failed | Error handling |
| Use it in React or Vue | React and Vue |
You do / the SDK does
| You do | The SDK does |
|---|---|
| Get a join ticket from your server | Fetches connection details, connects to the game node, handshakes |
Call sendInput every frame with the local input | Orders frames by number, drops duplicates, asks the node for missing frames |
Advance your game logic in onFrame | Sends heartbeats every second, measures latency, estimates server time |
Show the result or a notice from onClose | Reconnects automatically after drops, fetches new connection details when needed, ends after the reconnect window |
Join
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'、本人座位、在座的成员| Option | Description |
|---|---|
ticket | The ticket your server issued to this player. Omit it for open rooms |
client_session_id | ID of this join (8–64 visible ASCII characters). Generated by the SDK when omitted; it stays the same when fetching new connection details within the same join |
signal | An AbortSignal to cancel a join that has not finished |
- It returns after connecting to the game node and completing the handshake, usually within a second.
- A client can be in only one room at a time;
im.game.currentis the current room. Callingjoinagain for the same room returns the same object; joining another room is rejected withinvalid_state— callleave()first. - Browsers connect to game nodes over WebSocket, so the page must be able to reach the node's
wss://address.
Frame sync
In rooms with sync_mode frame (the default), the game node runs tick_rate frames per second. Every frame it bundles all seats' inputs for that frame and sends them to everyone; every player processes the same inputs in the same order, so everyone sees the same game.
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);onFrameis called strictly in ascending frame order, never repeated, never skipped. When packets are lost the SDK first fills gaps with the redundancy in later packets; otherwise it asks the node and, once complete, calls back in order.- Multiple
sendInputcalls in the same event-loop turn send only the last one. When not connected it returnsfalseand does not queue — stale inputs are useless. - Inputs sent before the match starts are ignored.
room.frameis the last frame delivered. - Encode inputs in your own binary format, for example a few bits each for direction and buttons.
Snapshots: in rooms created with snapshot enabled, the node picks one player to provide the game state periodically. The player whose room.getSnapshot().snapshotProvider is true calls putSnapshot at the interval; a player who was away too long, with too many frames missing, first receives onSnapshot and then continues from the frames after the snapshot:
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 开始Custom messages
relay rooms have no frames; players only exchange custom messages. frame rooms can also use them for chat, emotes and other data that is not part of a 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)));Each message is at most 1KB; each seat can send at most 60 messages and 32KB per second. Anything beyond is dropped by the node, and staying over the limit for 10 seconds in a row removes the player from the room.
Room state and members
| Property | Description |
|---|---|
state | Connection state: connecting, connected, reconnecting (resuming), rejoining (fetching new connection details), closed, left |
roomState | waiting for the start, started in a match, closing ending |
seat | Your seat (0–9) |
members | Seated members { seat, username, online }; those who left are not included |
frame | The last frame delivered |
rtt | Median of the last 10 round-trip times (ms); null right after connecting |
serverTime() | Estimated server time (ms), for aligning countdowns across players |
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() returns a read-only object with the information above and is replaced by a new object when anything changes; room.subscribe(fn) notifies you of changes. The frame number is not in the snapshot (it changes every frame); receive frames with onFrame.
Every onXxx method and room.on(event, fn) returns a function that unsubscribes.
Disconnects and the end
You write no code for disconnects: when no data arrives for 5 seconds or the connection closes, the SDK reconnects immediately, then retries after 1, 2, 4, 4… seconds, returns to the same seat and fills in missing frames in order. While reconnecting, state is reconnecting or rejoining and sendInput returns false.
In the following cases the room ends for this player; onClose is called once and the SDK stops reconnecting:
reason | When | Suggested UI |
|---|---|---|
room_closed | The match ended. closeReason is why (such as closed_by_server, expired) and result is the settlement result from your server | Show the result |
left | You called leave() | — |
timeout | Disconnected for longer than the room's reconnect window | "Connection lost, you left the match" |
kicked | Removed by your server | "You were removed from the room" |
replaced | The same account joined this seat elsewhere | "You entered the match on another device" |
node_draining, room_not_found | Game server maintenance, the room is gone | "The match was interrupted" |
signed_out, destroyed | Signed out, or the client was destroyed (other players are not told; the seat is kept for the reconnect window) | — |
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}`);
});Leaving: await room.leave() tells the game node and IM; other players see this seat as left. It can be called more than once.
Return to a match
After reloading or reopening the page you need to join again, because connection details live only in memory:
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 returns only rooms on this device (this sign-in) that you have not left. Seats past the reconnect window are released, and your server needs to issue a new ticket.
Error handling
When join fails it throws a DRError. Use im.game.errorKind(err) to get a category and decide what to show the player:
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('暂时无法进入对局,请稍后再试');
}
}| Category | Cause | Suggestion |
|---|---|---|
ticket_invalid | The ticket is wrong, expired or already used | Have your server issue a new one |
game_disabled | The platform has not opened game rooms, or the plan does not include them | "The match is not available right now" |
room_closed | The room does not exist or has ended | "The match has ended" |
room_full | All seats are taken, or the pinned seat is occupied | "The room is full" |
joined_on_other_device | The same account already joined this room on another device | Tell the player, or have them leave on the original device |
node_unavailable | The game server hosting the room is temporarily unavailable | Retry later |
upgrade_required | The SDK version is too old | Upgrade the SDK |
network | Network error or timeout | Retry later |
The original error code is in err.code and err.reason: errors from the join API are the same as the server errors; when a game node rejects the connection, code is game_node_error.
Local checks: sendInput over 128 bytes, sendCustom over 1KB and putSnapshot over 64KB throw local_validation.
React and Vue
useGameRoom() returns a snapshot of the current room (updated on join and end), and useGameEvent() subscribes to room events and unsubscribes when the component unmounts. Joining is still done by you, calling im.game.join() on a user action.
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 works the same way and returns a read-only 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 | Description |
|---|---|
useGameRoom(room?) | Follows im.game.current when omitted; uses the given room object otherwise; returns null for null |
useGameEvent(room, event, listener) | Uses the current room when room is undefined; event is frame, custom, member, roomState, snapshot, state or close |
Other hooks are in using React and Vue.
API reference
im.game | Description |
|---|---|
supported | Whether this environment can join game rooms |
current | The current room, null when none |
join(roomId, options?) | Joins and returns a GameRoom |
getCurrent() | The room this device is in, { room, member } |
leave(roomId) | Leaves without a room object |
errorKind(err) | The error's category |
GameRoom method | Description |
|---|---|
sendInput(data) | This frame's input, ≤ 128 bytes |
sendCustom(data, { to?, reliable? }) | Custom message, ≤ 1KB; to is an array of seats |
ready() | Gets ready |
start() | Host start. Rooms created through IM do not allow host start yet; for manual rooms your server calls the start API |
putSnapshot(frame, data) | Provides a snapshot, ≤ 64KB |
leave() | Leaves |
getSnapshot(), subscribe(fn) | State snapshot and change notification |
on(event, fn), onState, onRoomState, onFrame, onCustom, onMember, onSnapshot, onClose | Events; each returns an unsubscribe function |
When the page goes to the background, the browser may slow down timers, so frames and heartbeats slow down; after a long time in the background the connection may drop, and the SDK reconnects when the page returns to the foreground.
