Skip to content

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 # クライアント予測 + reconciliation

TypeScript 設定 ​

設定値
targetES2020
moduleESNext
moduleResolutionbundler
stricttrue
noEmittrue
declarationtrue

ビルド出力なし。Vite 等のバンドラーが TypeScript ソースを直接消費する。


エクスポート ​

主要 API ​

typescript
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()本人のマイクをミュートする。作品からマイクをオンにする手段は無い(オンにできるのは本人だけ)

型定義 ​

typescript
import type {
  PlayScreenMessage,
  Seat,
  SeatKind,
  GameLogic,
  GameConfig,
  Operation,
  ConnectionState,
  SeededRandom,
  Presence,
  PresenceChange,
  Pause,
  VoicePlan,
  MicPolicy,
  Connections,
} from '@uzuhq/code-sdk';
型説明
PlayScreenMessageFlutter ホストとのメッセージ共通型
Seatroster に載る席 { id: string; nickname: string; iconUrl: string; characterId?: string }。roster は配役を受け取る参加者だけで、観測者は載らない
SeatKind自分の席種別 'player' | 'spectator' | 'admin'。?seatKind= から読み onState の第 3 引数で渡る
GameLogic<S>ゲームロジック定義 (setup, actions, update)
GameConfig<S>run() に渡す設定オブジェクト
OperationJSON Patch 操作 (replace, add, remove)
ConnectionStateReconnectableWebSocket の接続状態 (connecting, connected, reconnecting, disconnected)。シナリオが見る自分の接続は getPresence().me.connection
SeededRandomseed 付き乱数生成器インターフェース
Presence自分の接続と、全員の通話の状態(me / others / observers)。他の人は通話だけ。部品の型 Me / Other / Mic / Quality / ObserverRef / ConnectionStatus / VoiceStatus も出す
PresenceChangeonPresenceChange の第 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.onMessageFlutter → JSFlutter が JS を注入してメッセージを配信する受信口
window.FlutterHost.postMessageJS → FlutterWebView が提供する JavaScriptChannel
window.parent.postMessageJS → 親ウィンドウiframe 環境での通信
window.addEventListener('message')親ウィンドウ → JSiframe 環境でのメッセージ受信

初期化シーケンス ​

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 に数えず、止める要求も受けない

ゲームからの利用例 ​

typescript
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() だけを呼ぶ。

typescript
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 に波及させない