@qingzhenghust/game-sdk
v0.4.0
Published
Riffle game backend HTTP/WebSocket and host capability SDK
Maintainers
Readme
@qingzhenghust/game-sdk
Riffle 游戏客户端 SDK。0.3 起,多人的业务规则和状态由每个游戏自己的 backend/ 实现;平台负责身份、HTTP/WS 路由、隔离的 SQL/Redis,以及语音视频的短期 TURN/SFU 授权。
初始化
import { createGameSdkFromHost } from '@qingzhenghust/game-sdk';
const sdk = createGameSdkFromHost({ app: riffleBridge.app });
await sdk.ready();宿主注入 Runtime URL、Launch Token、平台用户和 game ID。SDK 自动刷新临近过期的 Launch Token,并为 HTTP 和 WebSocket 请求附加同一个可信身份。游戏不得自己保存平台 token、拼接 Runtime backend URL 或传入用户 ID 冒充其他玩家。
SDK 0.3 的默认入口只有这些能力:
sdk.backend():游戏 backend 的 HTTP/WS 客户端;sdk.gameRooms():基于游戏 backend 约定的可选房间组件;sdk.media():语音、视频、TURN/SFU 和网络探针;sdk.llm()、sdk.uploads()和宿主sdk.app能力。
旧 Runtime 的 room()、rooms()、matchmaking()、leaderboard()、db() 和实时模拟 capability 不在 0.3 的公开 API 中。匹配、排行榜、状态存储和服务端模拟应在游戏 backend 中实现。
HTTP 和 WebSocket
const backend = sdk.backend();
const snapshot = await backend.get('/matches/m1');
await backend.post('/matches/m1/input', {
requestId: crypto.randomUUID(),
sequence: 18,
action: 'jump',
});
const socket = await backend.connect('/matches/m1/events');
const unsubscribe = socket.onMessage(event => applyServerEvent(event));
socket.send({ type: 'subscribe', protocol: 1, requestId: crypto.randomUUID() });HTTP 适合命令、CRUD、权限检查和恢复快照。WebSocket 适合订阅、服务端推送和短小的实时输入。业务 ACK 必须说明请求是已接收、已持久化还是已应用;socket open 和 HTTP 200 本身不代表比赛动作已经执行。
游戏 backend 使用 @qingzhenghust/game-sdk/server 声明 HTTP/WS 入口:
import { defineGameBackend } from '@qingzhenghust/game-sdk/server';
export default defineGameBackend({
async http(request, ctx) {
// ctx.identity 是可信平台身份;ctx.sql/ctx.redis 已按部署隔离。
return Response.json({ userId: ctx.identity.userId });
},
async websocket(socket, ctx) {
socket.send({ type: 'ready', protocol: 1 });
},
});服务端入口放在 backend/index.ts,迁移放在 backend/migrations/。部署平台以稳定的 backendDeploymentId 更新该游戏唯一 Runner;发布新 artifact 不会多建一套长期 Runner。完整约定见 Backend API 和 多人快速开始。
房间组件
房间不是平台隐式状态。游戏需要房间时,由自己的 backend 使用 @qingzhenghust/game-backend-kit 定义规则,客户端再调用:
const rooms = sdk.gameRooms();
const room = await rooms.create({ maxPlayers: 4, metadata: { mode: 'casual' } });
await rooms.join(room.id, { team: 'blue' });
const current = await rooms.get(room.id);
await rooms.leave(room.id);不需要房间的留言板、排行榜和异步协作可直接设计 HTTP/WS API。SDK 不会强迫这些玩法创建虚构房间。
语音和视频
先由游戏 backend 确认当前用户属于该房间。SDK 随后通过 backend 获取短期媒体授权,并管理采集、P2P/SFU、信令订阅、重连和资源释放。
const voice = await sdk.media().voice(room.id);
voice.onTrack((_peerId, stream) => attachAudio(stream));
await voice.join(); // 必须由用户操作触发权限请求
voice.setMuted(true);
await voice.leave();
const video = await sdk.media().video(room.id);
video.onLocalStream(stream => showPreview(stream));
video.onTrack((peerId, stream) => attachRemoteVideo(peerId, stream));
await video.join();
await video.setCameraEnabled(false);
await video.switchCamera();
await video.leave();双人房默认可用 P2P + TURN,多人房使用 SFU。选择结果来自服务端授权,游戏不接触 TURN 密钥或 SFU 签名密钥。媒体帧不会进入 Runner、SQL 或 Redis;backend 只处理成员权限、房间业务和信令路由。
语音和视频都提供延迟与丢包探针:
const stop = video.observeNetwork(snapshot => {
renderNetwork({
rttMs: snapshot.summary.rttMs,
loss: snapshot.summary.packetLossRate,
quality: snapshot.summary.quality,
});
}, { intervalMs: 1000 });
const diagnostic = await video.probeNetwork({ sampleWindowMs: 1000 });
stop();探针读取浏览器 WebRTC 统计,不上传媒体内容、SDP、ICE 地址或凭据。
可靠性边界
- 客户端必须显式区分连接状态、业务状态、消息状态和状态版本。
- 写命令携带稳定
requestId,连续输入携带递增sequence,持久状态使用版本或事务。 - 普通位置帧允许丢弃和覆盖,不能等待 ACK 后才继续模拟。
- 开局、计分和结算使用去重、有限重试和快照恢复。
- 断线恢复先读取服务端快照,再恢复订阅;旧连接回调不能覆盖新一代状态。
- SQL 保存持久事实;Redis 保存短期租约、限流和在线协调;高频帧不写入数据库。
