Godot から Supabase を使う

2021/10/06に公開

作ったもの

https://youtu.be/rskUSMZSXrQ

Supabase の Database機能 と Realtime機能 でチャットを作りました
詳しくは以下のソース

https://github.com/harumaxy/godot-supabase-chat-demo

Supabase とは

Open Source Project を使って作られたFirebaseの代わり
NoSQLではなくRDB(PostgreSQL)を使う

PostgRESTとかPhoenixChannel(WebSocket)とかが使われてるらしい
詳しい構成は以下参照。
https://supabase.io/docs/architecture

ローカルエミュレーターも用意されており、今回はこれを使った

npm install -g supabase
mkdir project && cd project
supabase init

supabase init コマンドを実行するとAPI rootとAPI Key(public, private)が表示される。
後で使うのでメモっておく。

Godot Engine Supabase Addon

https://github.com/fenix-hub/godot-engine.supabase

今回使った Supabase クライアントの Godot addon

AssetLib からインストールして、アドオンを有効にしておく

addons/supabase/.envファイルに、 supabase initコマンド実行時に表示されたAPI root と API Key(public)を記入しておく。

[supabase/config]

supabaseUrl="http://localhost:8000"
supabaseKey="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJyb2xlIjoiYW5vbiJ9.36fUebxgx1mcBo4s19v0SzqmzunP--hm_hep0uLX0ew"

Godotで使えるSupabase UIライブラリもあるらしい(まだ触ってない)

https://github.com/fenix-hub/godot-engine.supabase-ui

Auth

Godot Supabase addon で Database や Realtime の機能を使うには、まずログインが必要。
ログイン時に取得できる AccessToken をアドオンモジュールに保存して使いまわしているっぽい。(Supabase.auth._auth)

onready var email_edit = $VBoxContainer/Email/LineEdit
onready var password_edit = $VBoxContainer/Password/LineEdit

func try_login() -> SupabaseUser:
  if GlobalState.user != null:
    print("already login")
    return null
  var result: AuthTask = yield(Supabase.auth.sign_in(email_edit.text, password_edit.text), "completed")
  if result.user != null:
    GlobalState.user = result.user
    return result.user
  return null

func try_sign_up() -> SupabaseUser:
  var result: AuthTask = yield(Supabase.auth.sign_up(email_edit.text, password_edit.text), "completed")
  if result.user != null:
    GlobalState.user = result.user
    return result.user
  return null

ログイン時に取得できるユーザー情報は後で使うので、自分で用意した GlobalState Signleton に保存する。

Database

まず、チャットルームとチャットメッセージのテーブルをDBに作成する(SQLで)

supabase init したときに PostgreSQL に接続できるURLが表示されるので psql なり何なりお好きなDBクライアントを使って以下のSQLを実行。

CREATE TABLE public.rooms (
  id serial NOT NULL PRIMARY KEY,
  name text NOT NULL,
  created_at timestamp without time zone NOT NULL DEFAULT now()
);


CREATE TABLE public.chat_messages (
  id serial NOT NULL PRIMARY KEY,
  room_id integer NOT NULL REFERENCES public.rooms(id),
  user_id uuid NOT NULL,
  message text NOT NULL,
  created_at timestamp without time zone NOT NULL DEFAULT now()
);

PostgREST により、テーブルを作成すると自動でCRUD APIが作成される。
HTTP Requestでも利用できるが、クライアントライブラリを使うと更に簡単。

func async_list_rooms() -> DatabaseTask:
  var query = SupabaseQuery.new().from("rooms").select()
  return Supabase.database.query(query)

func _ready():
  var result: DatabaseTask = yield(async_list_rooms(), "completed")
  if !result.error:
    print(result.data)
    rooms = result.data
    update_rooms_ui(rooms)
  else:
    print("not login yet")

Godot Supabase addon に含まれる、SupabaseQuery class はクエリビルダーとして使える。

Supabase.database.query() メソッドの実行結果はTaskオブジェクトで、completedシグナルを connect()メソッドで登録したコールバックもしくはyieldで待ち受ける。

Realtime

Realtime機能は、Supabase.realtimeから利用できる。

  # Listen Realtime Message Post
  var client = Supabase.realtime.client()
  var connect_result = client.connect_client()
  if connect_result != OK:
    print("realtime client error")
  else:
    # Websocket Client 接続に成功してからじゃないと channel を取得しようとしてもうまく行かない
    client.connect("error", self, "_on_realtime_client_error")
    channel = client.channel("public", "chat_messages", "room_id=eq.{room_id}".format({room_id=GlobalState.chat_room_id}))
    channel.connect("insert", self, "_on_message")
    channel.subscribe()
  1. RealtimeClientオブジェクトを取得
  2. client.connect_client()メソッドを実行 (signalにコールバックを登録するNode.connect()メソッドと似てて紛らわしい)
  3. client.channel()メソッドで変更を購読するチャンネルを作成・取得する
  4. channel.connect()でsignalにコールバックを登録
  5. channel.subscribe()で購読を開始

この手順通りにやらないとうまく行かないっぽい?ので注意

RealtimeChannel の topic

Supabase の Realtime機能は、内部的にはWebsocketの上にElixirPhoenixChannelを使って実装されている(らしい)。

以下のようなtopicのフォーマットを指定する。

schema:table_name:column_name=op.val

  • public:rooms
    • public.rooms テーブルの変更を購読する(INSERT, UPDATE, DELETE)
  • public:chat_messages:room_id=eq.8
    • public.chat_messages テーブルの room_id カラムの値が 8 の列の変更だけ購読する

RealtimeClient.channel()メソッドの3つの引数で指定してチャンネルを作成できる。
カラムの部分は省略できる。 *ワイルドカードも使える。

まだ試していないこと

  • Database機能のRow Level Security
  • Storage機能

まだ調べてないこと

知ってる人とかいたらぜひコメントで教えて下さい

  • Firebase とのパフォーマンスの差
    • Realtime Database と PostgreSQL の書き込み性能差
    • Supabaseマネージドサービスだと、どのくらい自動スケールするのか?その場合の料金は?
  • Supabase Realtime API の最大同時接続数とか応答速度パフォーマンスとか
    • バックエンドがElixirだから並列性とか高そうな気がする
    • WebSocketだから頻繁なレベルのリアルタイム性は厳しい気がする(UDPの用途とかのやつ)

感想

  • すごく簡単だった
  • PostgREST により、DBのテーブルがそのままAPIになるのが楽
    • NoSQLと違って、RDBのリレーションや制約を生かしたカッチリしたデータ構造を書ける
    • SQL使える人はそのまま知識を活かせると思う
  • Realtime API でDBの変更を監視できるのが便利
  • Firebaseよりカスタマイズ性が高そう

ローカル開発ツールはdocker-composeを利用してるっぽく、簡単に起動・停止できるし、supabase ejectコマンドで Dockerfile を吐き出せるっぽい(まだ試してない)

カスタム構成にしたりセルフホスティングもできそう?

余談1

今まで、存在は知りつつも触ってなかったけど、PostgRESTみたいなDBのテーブルがそのままAPIになるというシステムは神だと思った。

普段の仕事でもバックエンドサーバー書いてて、単なるDB操作のためのAPIになってるってのはよくあるから、PostgREST使えたら仕事なくなっちゃうかもなぁ笑(DB操作じゃないAPIもたまにはあるけど)
セキュリティ関連(Row Level Security)とかロール権限とかもうちょっと勉強して、仕事でも活かせたらなあと思うのであった

余談2

Firebase SDK の Godot addon もあった

https://github.com/GodotNuts/GodotFirebase

Supabase は実績がちょっとね...という人は Firebase 使えばいいと思う

(そもそも Godot が Unity や Unreal Engine に比べて実績がないよね、というツッコミは置いといて)

Discussion