PlayScreen SDK 仕様
内部ドキュメント
このドキュメントは内部実装の詳細です。ゲーム開発者向けの情報は docs.code.uzu-app.com を参照してください。
ソースコード: javascript/uzu_code/sdk/
Web ゲームが Flutter ホストと通信するための TypeScript SDK。 GitHub Packages npm レジストリに @uzuhq/code-sdk として公開されている。
パッケージ情報
| 項目 | 値 |
|---|---|
| パッケージ名 | @uzuhq/code-sdk |
| モジュール形式 | ES Modules |
| ビルドステップ | TypeScript → dist/ にコンパイル |
| 依存関係 | なし (devDependencies: typescript ^5.7.0 のみ) |
ディレクトリ構造
javascript/uzu_code/sdk/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # メインエントリ (init, run, on, send 等)
├── types.ts # 型定義 (GameConfig, SendAction, SeatKind 等)
├── reconnectable-ws.ts # ReconnectableWebSocket (自動再接続・keepalive)
├── dev-hooks.ts # window.__uzu_dev (外部 automation 用フック)
├── scenario-callback.ts # シナリオ呼び出しの例外境界
├── host-bridge.ts # ホストへの送信口 (transport と presence から送るため)
├── platform.ts # アプリとサーバーのあいだの伝言 (bridge の platform ⇄ WebSocket の __platform)
├── presence.ts # 接続と通話の状態 (getPresence / onPresenceChange / getPause / onPauseChange)
├── presence-types.ts # Presence と bridge の voiceChanged / connectionChanged の型
├── pause-state.ts # サーバーが時計を止めているか (wire の frozen)
└── run/ # run() API 実装
├── local-server-action.ts # ソロモード (サーバーなし)
├── server-action.ts # オンラインモード (GameHost に接続)
└── optimistic-action-client.ts # クライアント予測 + reconciliationTypeScript 設定
| 設定 | 値 |
|---|---|
| target | ES2020 |
| module | ESNext |
| moduleResolution | bundler |
| strict | true |
| noEmit | true |
| declaration | true |
ビルド出力なし。Vite 等のバンドラーが TypeScript ソースを直接消費する。
エクスポート
主要 API
import {
init,
run,
on,
send,
playSound,
playBgm,
getPresence,
onPresenceChange,
getPause,
onPauseChange,
setListeningRoom,
muteMic,
} from '@uzuhq/code-sdk';| API | 説明 |
|---|---|
init() | SDK 初期化 + Flutter/iframe ブリッジ設定 |
run<S>(config) | サーバー権威のゲームループ実行 |
on(type, handler) | Flutter/ホストからのメッセージリスナー |
send(message) | Flutter/ホストへメッセージ送信 |
getPresence() / onPresenceChange(listener) | 接続と通話の状態(接続と通話の状態)。onPresenceChange は購読を解除する関数を返す |
getPause() / onPauseChange(listener) | 一時停止とその理由(緊急一時停止)。onPauseChange は購読を解除する関数を返す |
setListeningRoom(room) | 観測席が聴く部屋を選ぶ。プレイヤーの部屋はロジックの voicePlan が決める |
muteMic() | 本人のマイクをミュートする。作品からマイクをオンにする手段は無い(オンにできるのは本人だけ) |
型定義
import type {
PlayScreenMessage,
Seat,
SeatKind,
GameLogic,
GameConfig,
Operation,
ConnectionState,
SeededRandom,
Presence,
PresenceChange,
Pause,
VoicePlan,
MicPolicy,
Connections,
} from '@uzuhq/code-sdk';| 型 | 説明 |
|---|---|
PlayScreenMessage | Flutter ホストとのメッセージ共通型 |
Seat | roster に載る席 { id: string; nickname: string; iconUrl: string; characterId?: string }。roster は配役を受け取る参加者だけで、観測者は載らない |
SeatKind | 自分の席種別 'player' | 'spectator' | 'admin'。?seatKind= から読み onState の第 3 引数で渡る |
GameLogic<S> | ゲームロジック定義 (setup, actions, update) |
GameConfig<S> | run() に渡す設定オブジェクト |
Operation | JSON Patch 操作 (replace, add, remove) |
ConnectionState | ReconnectableWebSocket の接続状態 (connecting, connected, reconnecting, disconnected)。シナリオが見る自分の接続は getPresence().me.connection |
SeededRandom | seed 付き乱数生成器インターフェース |
Presence | 自分の接続と、全員の通話の状態(me / others / observers)。他の人は通話だけ。部品の型 Me / Other / Mic / Quality / ObserverRef / ConnectionStatus / VoiceStatus も出す |
PresenceChange | onPresenceChange の第 2 引数(変わったところ) |
Pause | 一時停止。null / { reason: 'emergency'; by } / { reason: 'waiting'; for; since } |
VoicePlan / MicPolicy | ロジックの voicePlan が返す部屋とマイクの方針(free / muted / locked) |
API リファレンス
公開 API の仕様は docs.code.uzu-app.com/reference/api が正。 このページには公開ドキュメントに書かない内部実装 (ディレクトリ構成・グローバルオブジェクト・ 初期化シーケンス・設計方針) だけを残す。
グローバルオブジェクト
SDK は以下の window プロパティを使用する。型定義は declare global で宣言済み。
| オブジェクト | 方向 | 説明 |
|---|---|---|
window.__ps.onMessage | Flutter → JS | Flutter が JS を注入してメッセージを配信する受信口 |
window.FlutterHost.postMessage | JS → Flutter | WebView が提供する JavaScriptChannel |
window.parent.postMessage | JS → 親ウィンドウ | iframe 環境での通信 |
window.addEventListener('message') | 親ウィンドウ → JS | iframe 環境でのメッセージ受信 |
初期化シーケンス
Flutter WebView 環境
Game (main.ts) SDK Flutter (BridgeService)
│ │ │
│ import │ │
│──────────────────────→│ isHosted = true │
│ │ (FlutterHost 検出) │
│ │ │
│ init() │ │
│──────────────────────→│ │
│ │ window.__ps 登録 │
│ │ │
│ │ send({type:'__ps_ready'})│
│ │─────────────────────────→│
│ │ │ _sdkReady = true
│ │ │ _flushPendingMessages()
│ │ │ onReady?.call()
│ │ │
│ │ (保留メッセージがあれば) │
│ │←─────────────────────────│
│ │ window.__ps.onMessage() │
│ │ │
│ drawSplash() │ │
│ requestAnimationFrame(│ │
│ firstFrameReady) │ │
│──────────────────────→│ │
│ │ send({type:'firstFrame │
│ │ Ready'}) │
│ │─────────────────────────→│
│ │ │ onFirstFrameReady?.call()
│ │ │ → splash overlay 非表示
│ │ │
│ await loadAssets() │ │
│ gameReady() │ │
│──────────────────────→│ │
│ │ send({type:'gameReady'})│
│ │─────────────────────────→│
│ │ │ onGameReady?.call()
│ │ │ → 入力受付 / TTI 計測終了iframe 環境
Game (iframe) SDK 親ウィンドウ
│ │ │
│ init() │ │
│──────────────────────→│ isHosted = true │
│ │ (parent !== window) │
│ │ │
│ │ addEventListener │
│ │ ('message') │
│ │ │
│ │ parent.postMessage │
│ │ ({type:'__ps_ready'}) │
│ │─────────────────────────→│__ps_ready ハンドシェイクにより、SDK 初期化前に Flutter がメッセージを送信してもロストしない。Flutter 側の BridgeService は ready 受信までメッセージをキューに保持する。
URL パラメータ
init() / run() は以下の URL パラメータを読み取る:
| パラメータ | 説明 |
|---|---|
?server= | WebSocket サーバーの URL (例: wss://uzu-code play-server-dev.uzu-account.workers.dev / uzu dev では ws://localhost:<port>) |
?roomId= | ルーム ID |
?seatId= | 自分の席 ID |
?players= | JSON エンコードされた roster (配役を受け取る参加者のみ) |
?seatKind= | 自分の席種別 (player / spectator / admin)。省略時は player。iframe URL 限定で WebSocket には出さない |
?revisionId= | GameHost endpoint 用 revision ID (?server= 指定時に必須、 uzu dev では dev 固定) |
?monitor= | 1 なら監視で開いた画面。WebSocket にも monitor=1 を付け、play-server はこの接続を presence に数えず、止める要求も受けない |
ゲームからの利用例
import { run } from '@uzuhq/code-sdk';
import logic, { type MyState, type MyActions } from './logic';
run<MyState, MyActions>({
logic,
playerCount: 2,
onState(state, myPlayerId) {
currentState = state;
myId = myPlayerId;
},
inputs(sendAction) {
canvas.addEventListener('click', () => {
sendAction('choose', { hand: 'rock' });
});
},
});state 同期を使わない 1 人用シナリオ (サウンドやボイスだけを使うもの) は init() だけを呼ぶ。
import { init, onPresenceChange, playBgm } from '@uzuhq/code-sdk';
init();
playBgm('bgm_main');
onPresenceChange(({ me }) => {
drawMicIndicator(me.voice.status === 'here' ? me.voice.mic : null);
});init() だけではサーバーにつながらないので、getPresence() で意味を持つのはアプリが渡す自分の通話(me.voice)だけになる。others は空、me.connection は connecting のまま、getPause() は null のまま。部屋割り・他の人の接続・一時停止には run() が要る。
設計方針
| 方針 | 説明 |
|---|---|
| シングルトン | グローバル1インスタンスで状態管理を簡潔に |
| ゼロビルド | TypeScript ソースを直接エクスポート、バンドラーに委任 |
| 環境非依存 | Flutter 外でもエラーにならず console.log にフォールバック |
| 型安全 | strict モード + PlayScreenMessage 型で通信を型付け |
| 最小 API | 必要最小限のメソッドのみ公開 |
| 例外境界 | シナリオのコールバックの例外は SDK 内で止める (scenario-callback.ts) |
例外境界
onState / event ハンドラ / bridge の購読 (on()) はシナリオが書く コードで、state 配信やメッセージ受信のたびに同じ経路を通る。例外をそのまま上へ通すと state 適用 (confirmedState 更新 / ack 処理 / seq 前進) が途中で止まり、以降の配信でも同じ経路で 投げ続ける。WebSocket もホストアプリも生きたまま画面だけが永久に更新されなくなるので、 シナリオを呼ぶ箇所はすべて callScenario() を通す。
- 例外は握り潰さず
console.errorに出す - state 適用は描画の成否と無関係に完了させる。
onStateは毎回フル state から再実行される ので、次の配信で描画がやり直される - 購読が複数あるところでは 1 件の失敗を他の購読へ波及させない
onPresenceChange/onPauseChangeのリスナーはpresence.tsがリスナーごとに try/catch で囲む(console.warnに出す)- WebSocket の message ハンドラにも最終防衛の catch を置き、message 1 件の処理失敗を 接続と後続 message に波及させない