# 多人游戏快速开始

SDK 0.3 使用 game-owned backend。平台不再替游戏维护房间、匹配、排行榜或比赛状态。

## 1. 定义协议和状态机

先写清客户端命令、服务端事件、恢复快照和四组状态：连接、业务、消息、版本。实时输入包含 `sequence`，业务命令包含稳定的 `requestId`，服务端事件包含 `eventId` 与 `stateVersion`。

## 2. 创建 backend

```ts
// backend/index.ts
import { defineGameBackend } from '@qingzhenghust/game-sdk/server';

export default defineGameBackend({
  async http(request, ctx) {
    const url = new URL(request.url);
    if (request.method === 'GET' && url.pathname === '/state') {
      const result = await ctx.sql.query(
        'select state, version from matches where player_id = $1',
        [ctx.identity.userId],
      );
      return Response.json(result.rows[0] ?? null);
    }
    return Response.json({ code: 'NOT_FOUND' }, { status: 404 });
  },
  async websocket(socket, ctx) {
    socket.send({ type: 'ready', protocol: 1, userId: ctx.identity.userId });
  },
});
```

持久表通过 `backend/migrations/` 创建。backend 只使用注入的 `ctx.sql`、`ctx.redis` 和 `ctx.media`，不得读取数据库、Redis、TURN 或 SFU 凭据。

## 3. 连接客户端

```ts
const sdk = createGameSdkFromHost();
await sdk.ready();

const state = await sdk.backend().get('/state');
const socket = await sdk.backend().connect('/events');
socket.onMessage(handleServerEvent);
socket.send({ type: 'subscribe', protocol: 1, requestId: crypto.randomUUID() });
```

恢复顺序固定为：进入 `recovering`、读取完整快照、校验版本、建立新订阅、恢复用户意图、进入 `active`。旧 socket 的消息必须由 generation 或 connection ID 丢弃。

## 4. 可选房间和媒体

游戏需要成员房间时，backend 使用 `@qingzhenghust/game-backend-kit`；客户端使用 `sdk.gameRooms()`。语音与视频通过 `sdk.media()` 获取短期授权，游戏只编排 UI，不实现 SDP、ICE、TURN 或 SFU token。

```ts
const room = await sdk.gameRooms().create({ maxPlayers: 4 });
await sdk.gameRooms().join(room.id);
const call = await sdk.media().video(room.id);
await call.join();
```

相机和麦克风必须由用户点击触发。页面退出时调用 `leave()` 并清理所有 observer。

## 5. 验证

至少验证两个独立身份、重复命令、乱序输入、响应丢失、Runner 重启、WS 重连、完整快照恢复和旧连接回调。媒体还需验证 TURN relay、SFU 多人发布/订阅、真实解码、权限拒绝和资源释放。构建成功或 socket open 不能替代端到端验收。
