Vercel + Ably でリアルタイム 2P Co-op ブラウザゲームをデプロイしてみた

Vercel + Ably でリアルタイム 2P Co-op ブラウザゲームをデプロイしてみた

Vercel (Next.js) と Ably (リアルタイム Pub/Sub) を組み合わせて、サーバー管理なしで 2 人協力プレイのブラウザゲームをデプロイしました。アーキテクチャの全体像や実装面の工夫について解説します。
2026.02.23

はじめに

Vercel (Next.js) と Ably (リアルタイム Pub/Sub) だけで、2 人協力プレイのブラウザゲームをデプロイしました。

以前 Vercel + Supabase の構成で対戦ゲームの実装を試したことがあり、今回はそれと同じ感覚で、Ably との組み合わせでリアルタイム Co-op がどこまで手軽に作れるか試してみました。制作したのは、大量の敵を倒しながらレベルアップし、3 分後に出現するボスの撃破を目指す 2P Co-op のアクションゲームです。

vercel-ably-gameplay-gif

Vercel とは

Vercel は、Next.js のホスティング基盤です。静的ファイルの配信に加えて、API Routes をサーバーレス関数として実行できます。今回のプロジェクトでは、フロントエンドの配信と、後述する Ably Token の発行およびルーム管理を API Routes で処理しています。

Ably とは

Ably はリアルタイム Pub/Sub メッセージングサービスです。クライアント間でメッセージを双方向にやり取りするための基盤を SaaS として提供しています。WebSocket の接続管理や再接続処理は SDK が吸収してくれるため、開発者はメッセージの送受信だけに集中できます。Co-op ゲームのように双方向かつ低レイテンシな通信が求められるユースケースと相性がよく、今回のプロジェクトに採用しました。

対象読者

  • ブラウザで動くリアルタイム Co-op ゲームを手軽にデプロイしたい方
  • Ably を使ったことがなく、Next.js との組み合わせ方を知りたい方
  • サーバーを自前で管理せずにリアルタイム通信アプリを構築したい方

参考

アーキテクチャの全体像

システムの構成は次の通りです。

特徴的なのは、ゲームロジックがすべてブラウザ上で動作する点です。Vercel の API Routes が担うのは Ably Token の発行と 4 桁ルームコードの管理だけで、ゲームの状態をサーバー側で保持しません。プレイヤー間のリアルタイム同期は Ably の Pub/Sub チャンネルを通じて直接行います。

敵のスポーンや移動にはネットワーク通信を使いません。 両クライアントが同じシード値で初期化した決定論的な擬似乱数生成器 (PRNG) を使い、敵のスポーン位置やタイミングをローカルで計算します。 この仕組みにより、画面上に 1,000 体の敵が存在しても敵自体の座標同期で帯域を消費しません。なお、敵の撃破に伴う経験値取得などの結果は game-event として通信しますが、これは離散イベントのため帯域への影響は軽微です。

vercel-ably-1000-enemies-gif

Ably によるプレイヤー同期

Token 認証

Ably への接続には Token 認証を採用しています。API Key をブラウザに露出させないため、Next.js の API Route で Token を発行し、クライアントに渡します。

src/app/api/auth/token/route.ts
const ably = new Ably.Rest({ key: apiKey });
const tokenRequest = await ably.auth.createTokenRequest({
  clientId,
  ttl: 3600 * 1000,
});
return NextResponse.json(tokenRequest);

クライアント側では authCallback を指定して接続します。Ably SDK が Token の期限切れ時に自動で再取得してくれるため、再認証のハンドリングは不要です。

src/lib/ably/NetworkManager.ts
this.ably = new Ably.Realtime({
  authCallback: async (_tokenParams, callback) => {
    const res = await fetch('/api/auth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ clientId: this._clientId }),
    });
    const tokenRequest = await res.json();
    callback(null, tokenRequest);
  },
  clientId: this._clientId,
  echoMessages: false,
});

チャンネル設計とメッセージ

ルームごとに game-room-{code} という 1 つのチャンネルを使い、2 種類のメッセージを流しています。

player-state は 50ms 間隔 (約 20fps) で送信するプレイヤーの位置や状態です。JSON ペイロードは約 100 bytes ですが、Ably のプロトコルオーバーヘッドを含めた実測値は 1 メッセージあたり平均 204 bytes でした。受信側は線形補間 (Lerp) で 60fps の描画にスムーズに反映します。

{
  "id": "p1",
  "pos": [120.5, 340.2],
  "vel": [1.0, 0.0],
  "hp": 85,
  "dead": false,
  "lv": 5,
  "seq": 142,
  "ts": 1740200000000
}

game-event はゲーム開始、プレイヤー死亡、経験値取得といった離散的なイベントです。発生した時点でのみ送信するため、頻度は低く帯域への影響は軽微です。

決定論的 PRNG による敵の同期

Co-op ゲームでは敵の状態を両クライアント間で一致させる必要があります。しかし、1,000 体の敵の座標を毎フレーム送信するのは現実的ではありません。 この問題を解決するために、決定論的な擬似乱数生成器 (PRNG) を使いました。Mulberry32 というアルゴリズムで、同じシード値を与えれば常に同じ乱数列を生成します。

src/lib/game/utils/RNG.ts
export class Mulberry32 {
  private state: number;

  constructor(seed: number) {
    this.state = seed;
  }

  next(): number {
    let t = (this.state += 0x6d2b79f5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  }
}

ゲーム開始時に GAME_START イベントでシード値を共有し、両クライアントで同じ Mulberry32 インスタンスを初期化します。以降、毎フレームの敵スポーンは同じ乱数列に基づいてローカルで計算されるため、敵のスポーンと移動に関するネットワーク通信は発生しません。

実際の通信量

Ably CLI (ably apps stats) で取得した検証期間中の実測値を紹介します。十数回のテストプレイを行った結果です。

項目
メッセージ数 (検証期間累計) 94,560
データ量 (検証期間累計) 約 19.3 MB
平均メッセージサイズ 約 204 bytes
ピーク同時接続数 4

十数回のプレイでメッセージ数は約 9.5 万件でした。1 プレイあたり数千件のメッセージが発生する計算になるため、Ably Free Tier (月間 600 万メッセージ) で本番運用するのは現実的ではありません。ただし、プロトタイプの検証用途であれば Free Tier で十分にまかなえます。

Ably のコンソールには通信量を確認できる Global usage 画面やリアルタイムに接続状態を確認できる Live stats 画面があり、現状確認や見積もりに便利でした。

ably console global usage

体感のレイテンシについて

実際に 2 人で Co-op プレイしたところ、ゲーム全体を通してラグはほとんど感じませんでした。 敵のスポーンや経験値の反映は前述の通りローカル計算または離散イベントで処理しているため、遅延の影響を受けません。

一方で、相手プレイヤーの移動が一瞬ワープして見える場面がありました。受信側では線形補間 (Lerp) で座標間をなめらかにつないでいますが、通信の揺らぎで次の座標の到着が遅れると、到着時に補間先が飛んで瞬間的に速く動いて見えます。これは Ably に限らずどのリアルタイム通信基盤でも起こりうる現象です。

リアルタイム通信では、ネットワークの不安定さをユーザーに感じさせないためのクライアント側の補間処理が重要になります。 今回は線形補間のみですが、速度ベクトルを使った予測移動や、到着タイミングのジッタを吸収するバッファリングなど、改善の余地はあります。通信基盤の選定と同じくらい、ローカル側でいかに見た目を破綻させないかが体験の質を左右します。

まとめ

Vercel + Ably の構成で、サーバー管理なしで 2P Co-op ブラウザゲームのリアルタイム同期とデプロイができました。Ably が Pub/Sub の基盤を提供してくれるため、WebSocket サーバーの構築や運用を気にせずゲームの開発に集中できます。

リアルタイム通信の選択肢は Ably だけではありません。Vercel + Momento、Vercel + Supabase Realtime など、それぞれ異なる特性を持った組み合わせがあります。キャッシュ用途が主であれば Momento、データベースとの統合が必要であれば Supabase、双方向のリアルタイムメッセージングが中心であれば Ably というように、要件に応じて選択するのがよいでしょう。

この記事をシェアする

FacebookHatena blogX

関連記事