🚀

SupabaseのBroadcastとPresenceを使ってオンライン対戦ゲームを実装してみる

2023/09/05に公開

はじめに

Supabaseのリアルタイム機能と言えばPostgresのデータ変更を監視する「Postgres Changes」が多いですが、「Broadcast」と「Presence」を用いてリアルタイム機能を実現することも可能です。本記事では、当該の二つの機能を使ってオンライン対戦ゲームを実装した際の覚書を記述します。

成果物

今回作成したのはマインスイーパーの対戦ゲームです。個人で遊ぶモードと対戦モードの二種類があります。対戦モードではリンクをコピーして対戦相手にURLを共有することで対戦が開始できます。

https://github.com/esttom/vs-minesweeper

Broadcast・Presenceとは

Supabaseにはリアルタイムに処理を行うための機能として以下の3つの機能が存在しています。

  • Postgres Changes
    Postgresデータベースの変更をリッスンして承認したクライアントに送信する機能。行レベルのセキュリティ(RLS)がセキュリティの基本。(※行ごとにポリシーを設定して権限を設定する方法)
  • Broadcast
    クライアントからクライアントにメッセージを低遅延で送信する機能。チャンネルのトピックを作成し、同一のトピックを持つクライアントにメッセージを送信。
  • Presence
    接続されたクライアント間でユーザの状態を共有する機能。クライアント切断の検知が可能。

Postgres ChangesとBroadcast・Presenceのどちらを用いてもある程度同じようなリアルタイムの機能は実現可能ですが、Postgres ChangesはRLSに基づいているためログインが前提になる(執筆時点では匿名ログインがない)ことと切断の検知ができない点においてBroadCast・Presenceの方が優位性があります。逆にデータを残しておきたい、より厳密なセキュリティが必要などの場合においてはPostgres Changesの方が良いかもしれません。

BroadcastとPresenceの使い分けとしてはPresenceがユーザ(あるいはオンラインのルーム)の状態(接続・切断)の管理、Broadcastがメッセージの送信を担当するという形になります。

実装方法

公式のガイドにまとまっているため、詳細はそちらをご参照ください。また、ライブラリのインストールなどは別途行ってください。

クライアントの作成

機能を利用するためにはクライアントの作成が必要です。SupabaseのプロジェクトのページでURLとanon-keyが記載してあるので値を設定してください。(環境変数として.envに記述することを推奨します)
クライアントのインスタンスは都度作成するのではなく、1アプリケーションに1つのインスタンスとなるように作成します。

import { createClient } from '@supabase/supabase-js'

const clientA = createClient(
  'https://<project>.supabase.co',
  '<your-anon-key>'
)

チャンネルの作成

トピック、つまりデータを共有したい単位にチャンネルをそれぞれ作成します。下記の例ではroom-1のチャンネルのユーザはroom1のデータを取得することができ、room-2のデータは取得できません。ちなみにrealtimeというチャンネル名は予約語なため利用できません。

const channelA = clientA.channel('room-1')
const channelB = clientA.channel('room-2')

Broadcast

登録・送信

作成したチャンネルに登録するにはsubscribeを行います。登録後には同一チャンネルにsendを用いてメッセージを送信が可能です。sendのパラメータであるtypeは固定、eventは後述する受信処理と同じイベント名とします。payloadは送信パラメータになり受信側にデータが渡されます。

channelA.subscribe((status) => {
  if (status === 'SUBSCRIBED') {
    channelA.send({
      type: 'broadcast',
      event: 'test',
      payload: {
        message: 'hello, world'
      },
    })
  }
})

受信

送信したデータを受信する場合はonでイベントハンドラーを記述します。イベントは送信時と同一のものにする必要があり、複数のイベントを登録可能です。(送受信でそれぞれチャンネルをsubscribeする必要はなく一度のみです)

channelA
  .on(
    'broadcast',
    { event: 'test' },
    (payload) => console.log(payload)
  )
  .subscribe()

Presence

Broadcastと同様にチャンネルにsubscribeしてtrackを行うことで状態の追跡・同期が可能になります。追跡可能なイベントは参加を表すjoin、退出を表すleave、そして状態が変更された際に発火するsyncの三種類となります。trackの際には任意のデータを詰めることが出来るため管理したいデータがあれば別途詰め込みましょう。

channelA
  .on('presence',
    { event: 'sync' },
    () => {
      const newState = channelA.presenceState()
      console.log('sync', newState)
    }
  )
  .on('presence',
    { event: 'join' },
    ({ key, newPresences }) => {
      console.log('join', key, newPresences)
    }
  )
  .on('presence',
    { event: 'leave' },
    ({ key, leftPresences }) => {
      console.log('leave', key, leftPresences)
    }
  )
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      const presenceTrackStatus = await channelA.track({
        user: 'user-1',
        online_at: new Date().toISOString(),
      })
      console.log(presenceTrackStatus)
    }
  })

上記によりブラウザを閉じた場合などの切断を検知可能ですが、明示的にチャンネルから退出する際はuntrackを実行します。これによりleaveと関連してsyncイベントが他のユーザにおいて発火します。

const presenceUntrackStatus = await channelB.untrack()
console.log(presenceUntrackStatus)

実装時に困ったこと

Broadcastでメッセージを連続送信できない

Broadcast、Presenceにはデフォルトで送信制限がかかっており、100msに1メッセージのみとなっています。連続で送信する際には間隔を空けるか、クライアント作成時に以下のパラメータを変更しましょう。(下記では秒間20メッセージ、50msに1メッセージ)

const clientA = createClient('https://<project>.supabase.co', '<your-anon-key>', {
  realtime: {
    params: {
      eventsPerSecond: 20,
    },
  },
})

Presenceの入室制限

サンプルで作成したアプリケーションは一対一のマインスイーパーであるため二人に制限する必要があるのですが、Presenceでは入室制限は設定できません。入室した際にはsyncが呼び出され現在の人数を確認可能なため、指定の人数以上いる場合はチャンネルから退出させる、エラーメッセージを表示するなどの対応は可能です。

雑感

Postgresのデータ監視ではテーブル構成やRLSに悩まされることが多かったのですが、BroadcastやPresenceはデータの送受信の対象が直観的でかつ、切断検知などの機能もあり、リッチなアプリケーション実現に利用できそうだと感じました。

参考

Supabase realtime
https://supabase.com/docs/guides/realtime

Discussion