KUMODeck
EN

マルチプレイのルーム

ゲーム向け。 ルームは1本のWebSocketで動くリアルタイムのセッションです。クイックマッチ、6文字の部屋コードで入るプライベートルーム、公開ロビー、メッセージの中継、ルームの共有状態、プレイヤーごとの状態、ホストの引き継ぎ、再接続がそろっています。サーバーのコードを書く必要はありません。

ルームに入る#

const room = await kumo.rooms.quickMatch('duel');          // ルームの準備ができたら resolve
// または
const room = await kumo.rooms.create('duel', { private: true });
showCode(room.code);                                       // 例: "K7QM3X"
// 友だちの側:
const room = await kumo.rooms.join('k7qm3x');              // ルームIDか部屋コード。大文字小文字は区別しない
メソッド補足
rooms.quickMatch(mode)そのモードで最も人が多い空きのある公開ルームに入る。無ければminPlayers人がそろう(またはfillTimeoutSecondsが過ぎる)までキューで待つ
rooms.cancelMatch()待つのをやめる(待っていたquickMatchはmatch_cancelledでrejectされる)
rooms.create(mode, opts)private、maxPlayers、metadata(2 KB以下。ロビーに表示)、任意のcode、hostOnlyState
rooms.join(idOrCode)room_not_found、room_full、room_locked
rooms.list(mode?)ロビー用の空きのある公開ルーム。人数の多い順

モードは設定で決めます。モードの無いプロジェクトには組み込みのdefaultモード(2〜8人)があるので、設定を書く前から2つのブラウザタブで対戦できます。

{ "multiplayer": { "modes": [
  { "key": "duel", "minPlayers": 2, "maxPlayers": 2, "fillTimeoutSeconds": 20 },
  { "key": "party", "minPlayers": 1, "maxPlayers": 8, "fillTimeoutSeconds": 0 }
] } }

やり取りする#

room.send('move', { x, y });                     // 自分以外の全員へ(送りっぱなし、順序は保証)
room.send('hit', { dmg: 3 }, { to: [targetId] }); // 特定のプレイヤーへ
room.on('message', ({ from, type, data }) => { … });

await room.setState({ round: 2 });               // 共有状態: 全員(自分も)が 'stateChanged' を受け取る
await room.setMyState({ ready: true, skin: 'red' }); // 自分のプレイヤー状態: 'playerStateChanged'
room.state;          // 現在の共有状態
room.players;        // [{ id, displayName, connected, joinedAt, state }]

状態の差分はどのクライアントでも同じ順序で適用される(値がnullならキーを削除)ので、全クライアントの状態が一致します。位置のような頻度の高いデータにはsendを、途中参加の人にも見えるべきもの(スコア、席、設定)には状態を使います。

ホスト#

room.hostId / room.isHostは、最も早く入った接続中のプレイヤーです。ホストが抜けたり切断されたりすると、すぐに次のプレイヤーがホストになり(hostChanged)、戻ってきたプレイヤーがホストを取り返すことはありません。権限はホストに持たせます。ホストでシミュレーションし、スナップショットを配信します。hostOnlyState: trueにすると、setStateできるのはホストだけになります。room.lock()(ホストのみ)を使うと、進行中の試合に新しいプレイヤーが入れなくなります。

イベント#

イベントペイロード
message{ from, type, data }
playerJoined / playerLeftプレイヤー / { playerId, reason: 'left' | 'timeout' }
playerDisconnected / playerReconnected{ playerId } — 再接続するまで席は確保される
stateChanged{ patch, state, by, version }
playerStateChanged{ playerId, patch, state }
hostChanged{ hostId, previousHostId }
reconnecting / resumed自分の接続が切れた / 戻った(状態はスナップショットから更新)
closed{ reason } — left、seat_expired、connection_lost、replaced、shutdown、unauthorized

kumo.rooms.on('connection', state => …)はconnecting、open、reconnecting、closedを通知します。

再接続とページの再読み込み#

SDKはバックオフしながら自動で再接続し、サーバーは席を20秒確保します。ページを再読み込みするとRoomオブジェクトは消えますが、席は残っています。

const held = await kumo.rooms.fetchHeldRoom();   // { roomId, mode, code, expiresInMs } または null
if (held) room = await kumo.rooms.rejoin();       // 戻れる。状態はスナップショットから復元

あるいは、そのまま新しく始めてもかまいません。create、join、quickMatchは、古い席を通常の退出として解放します。

P2Pモード#

既定では、すべてのメッセージがKUMODeckのサーバーを通ります。モードに"transport": "p2p"を指定すると、ゲームのメッセージはプレイヤー同士がWebRTCで直接やり取りします。面倒な部分(マッチング、部屋コード、入退室、ホストの決定、WebRTCの接続手続きの中継、TURN中継の短期の資格情報の発行)は引き続きKUMODeckが引き受けます。コードは変わりません。

{ "key": "coop", "minPlayers": 2, "maxPlayers": 4, "transport": "p2p", "p2p": { "maxPlayers": 4, "relay": "fallback" } }
const room = await kumo.rooms.quickMatch('coop');
room.transport;                                   // 'p2p'
room.send('pos', { x, y }, { reliable: false });  // 順序なし・再送なし。座標などに向く
room.on('peer', ({ playerId, state, relayed }) => { … }); // connecting | connected | failed | closed
room.peers;                                       // [{ playerId, state, relayed }]

SDKは全員を全員とつなぎ(メッシュ)、TURNの中継をp2p.relayの指定どおりに使い(下の表)、ホストが抜けても共有状態が続くようにします。次のホストはサーバーが決め(上と同じ規則)、そのホストが手元の状態の写しから続けます。確定していないsetStateは新しいホストへ送り直されます。

注意 — 審判がいません。 P2Pのルームでは共有状態をホストのブラウザが決めるため、改造したクライアントは何でも送れます。スコアを競う対戦、賞品や課金が絡む対戦には、既定のサーバー方式を使ってください。

注意 — IPアドレス。 WebRTCで直接つなぐと、各プレイヤーのIPアドレス(住んでいる地域の目安)が相手に見えます。p2p.relayで選びます。

p2p.relayどうなるかあきらめること
alwaysすべてTURNの中継を通る。相手に見えるのは中継のアドレスだけ。中継に届けばつながる中継の通信量を送った量で請求
fallbackまず直接。直接つながらないときだけ中継直接のときは互いのIPアドレスが見える
never直接だけ。中継の料金は一切かからない直接つながらない組み合わせの人どうしはつながらない(peerイベントのstate: 'failed')

どれを選ぶか、やり方ごとの1回あたりの料金は友だちとオンラインで遊べるようにするにあります。multiplayerのSkillがクリエイターに1回だけ聞いて選びます。

人数メッシュあたり2〜8人(p2p.maxPlayers、既定4。各プレイヤーが相手全員へ送るため)。満員のメッシュへの参加はp2p_room_full(details.max)
費用中継の通信量を原価どおりturn_egress_bytesとして請求(上乗せなし)。接続手続きの中継はごくわずか
エラーwebrtc_unavailable(この環境にWebRTCが無い)、turn_unavailable(中継が必要なのに使えない)、ホストに届かないときのsetStateはtimeoutでreject
状態サーバー方式と同じ上限(共有64 KB、プレイヤーごと8 KB)。全員が抜けると消える

試すには、2つの別のブラウザ(または通常のウィンドウとプライベートウィンドウ)でゲームを開き、同じp2pモードでクイックマッチして、両方でroom.peersがconnectedになることを確かめます。

上限#

上限値
メッセージ接続あたり30件/秒(バースト60件)。超えた分は捨てられ、warningイベントが届く
メッセージのサイズ16 KB(payload_too_large)
共有状態合計64 KB。プレイヤー状態はそれぞれ8 KB
ルームあたりの人数最大64人(モードごと)
アイドル状態のソケットどのルームにも入っていない状態が10分続くと閉じる(次の呼び出しで再接続)

1つのブラウザプロファイル = 1人のプレイヤーです。2つ目のタブは1つ目のタブの接続を置き換えます。1台のマシンでマルチプレイを試すには、タブごとに別のストレージを使います(テンプレートは?player=2を使っています)。

補足 ルームは中継と同期を行うもので、ゲームのロジックをサーバーで動かすわけではありません。対戦ゲームはホストに権限を持たせる(examples/sky-duelを参照)か、試合のロジックを自分のFunctionsで動かしてください(Durable Objectsで状態を持つWebSocketのルームを作れます)。