← ユースケース

マルチプレイヤーゲームのルームとロビー

パーティー、クイズ、ボード、カードゲーム:コードで入るルーム、席、ホスト、準備状態、チャットを、ゲームサーバーなしでプレゼンスチャンネルとクライアントイベントの上に作ります。

パーティーゲーム、クイズ、ボードゲームやカードゲーム、一緒に観るルーム。どれも始まりは同じです:ルームコード、参加者の一覧、準備ボタン、そしてスタートを押すホスト。この部分にゲームサーバーは要りません。ルームはプレゼンスチャンネルで、オープンソースの @hibira-io/lobby パッケージが、それをブラウザ上の席、ホスト、準備状態、キック、スタート、ルームチャットに変えます。

プレイヤーにとっての価値

  • コードかリンクで入れるルーム。席が埋まっていき、満員ならはっきり「満員」と表示されます。
  • 準備状態、ホストのバッジ、スタートボタンが全員に同時に見えます。
  • 「入力中…」つきのルームチャット。ホストが抜けたら次のホストに引き継がれます。

使う機能

  • プレゼンスチャンネル(presence-)。各ルームにいる人の一覧です。
  • クライアントイベント(client-*)。準備状態、設定、スタート、チャット、ゲーム中の操作をブラウザ間で送ります。
  • HTTP API(GET /channels?filter_by_prefix=presence-room-&info=user_count)。データベースなしでルーム一覧を作れます。
  • @hibira-io/lobby。これらを組み合わせてルームの仕組みにまとめます。

ステップ 1:サーバーでルームを認可する

認可エンドポイントは、誰が入れるかを決め、各プレイヤーが入った時刻に署名し、満員のルームは断ります。全員がその時刻でルームの順番を決めるので、席とホストについて全員の認識が一致します:

import Pusher from 'pusher';
import { authorizeRoom, listRooms, RoomFullError } from '@hibira-io/lobby/server';

const hibira = new Pusher({ appId, key, secret, host: 'ws.hibira.io', useTLS: true });

app.post('/rooms/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  try {
    res.json(await authorizeRoom(hibira, {
      socketId: req.body.socket_id,
      channel: req.body.channel_name,
      userId: req.user.id,
      userInfo: { name: req.user.name },
      capacity: 4,
    }));
  } catch (err) {
    res.status(err instanceof RoomFullError ? 403 : 500).end();
  }
});

// Room browser: [{ id: 'KQTZ', channel: 'presence-room-KQTZ', members: 3 }]
app.get('/rooms', async (req, res) => res.json(await listRooms(hibira)));

ステップ 2:ブラウザでルームに入る

アプリの設定で クライアントイベントを許可 をオンにしてから:

import { Room } from '@hibira-io/lobby';

const room = Room.join(pusher, 'presence-room-KQTZ', { capacity: 4 });

room.on('members', ({ players, host }) => renderSeats(players, host));
room.on('message', (m) => chat.append(m.info.name, m.text));
room.on('full', () => show('This room is full'));

readyButton.onclick = () => room.setReady(!room.me.ready);
chatForm.onsubmit = () => room.send(input.value);
input.oninput = () => room.sendTyping();

room.players は席順の一覧で、各プレイヤーの ready と host を持っています。自分の操作はすぐに反映され、ホストだけが送れるイベントをほかの人が送っても無視されます。

ステップ 3:ゲームを始める

全員が準備できたらホストがスタートします。start() に渡したものは全員に届きます。たとえばラウンドのタイミング、乱数のシード、ゲームサーバーのアドレスなど:

if (room.isHost && room.allReady) room.start({ seed: 1234, rounds: 5 });

room.on('start', ({ data }) => game.begin(data));

ゲーム中の操作は同じチャンネルのクライアントイベント(room.channel.trigger('client-move', move))で送れます。ゲームサーバーが必要なゲームなら、そちらを通しても構いません。

Laravel の場合

プレゼンスチャンネルのコールバックで参加時刻を返し、Echo.join() のチャンネルを使います:

// routes/channels.php
Broadcast::channel('room.{code}', function ($user, $code) {
    return ['id' => $user->id, 'name' => $user->name, 'joined_at' => (int) floor(microtime(true) * 1000)];
});
const room = new Room(Echo.join(`room.${code}`).subscription, { capacity: 4 });

試してみる

Reaction rooms のサンプル は Next.js で作った完全なゲームです。ルーム一覧、4 つの席、準備、チャット、5 ラウンドの反応速度ゲームがあり、無料のアプリで動きます。

ヒント

  • 1 つのルームに入れるのは最大 100 人(プレゼンスチャンネルの上限)です。
  • クライアントイベントは、受け取るプレイヤーが何人でも 1 件のメッセージとして数えます。4 人で短いゲームをしても数十件程度です。
  • ゲームが始まった後はクライアントを信頼する作りです。不正が問題になるゲームでは、結果をサーバーで確認するか、ゲームの状態をゲームサーバーに置き、ルームはロビーだけに使ってください。
  • Unity で作るなら Unity のロビー、マッチング、ランキング もどうぞ。