---
title: "マルチプレイのルーム"
description: "ゲーム向け。 ルームは1本のWebSocketで動くリアルタイムのセッションです。クイックマッチ、6文字の部屋コードで入るプライベートルーム、公開ロビー、メッセージの中継、ルームの共有状態、プレイヤーごとの状態、ホストの引き継ぎ、再接続がそろっています。"
url: "/ja/docs/guides/multiplayer/"
lang: ja
index: "/ja/llms.txt"
---
# マルチプレイのルーム

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

## ルームに入る

```js
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つのブラウザタブで対戦できます。

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

## やり取りする

```js
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`オブジェクトは消えますが、席は残っています。

```js
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が引き受けます。コードは変わりません。

```json
{ "key": "coop", "minPlayers": 2, "maxPlayers": 4, "transport": "p2p", "p2p": { "maxPlayers": 4, "relay": "fallback" } }
```

```js
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回あたりの料金は[友だちとオンラインで遊べるようにする](/ja/docs/guides/play-online/index.md)にあります。`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](/ja/docs/guides/functions/index.md)で動かしてください（Durable Objectsで状態を持つWebSocketのルームを作れます）。
