Cloudflare WorkersとDurable Objectsを使えばWebSocketを使いまくってマルチプレイヤーゲームが簡単に作れる! と様々な記事で書かれているものの、誰も作っていないので作ってみました。
ソースコードはこちら。
テトリスバトルを楽しめますが、99人同時接続できるかは不明です。
今回は主に使用したパッケージの紹介と、オンラインゲームを作るにあたりWebSocket, Cloudflare Workers, Durable Objectsの何を頑張ればよかったのか紹介していきたいと思います。
まずゲームサーバーを実装するにあたり使用したPartyServerです。
このPartyServerと後述するPartySocketは、元々Cloudflareだけで完結するAIエージェントも作ろうと思い立ってCloudflare Agentsのコードリーディングした時に発見して、Honoとの相性もよかったので試しにAIエージェント以外の用途で使ってみたかったパッケージの一つです。
実際使ってみるとプリミティブなDurable ObjectsのAPIを簡単に使えるようになりました。
また、コードリーディングしていくと、ドキュメントには載っていないSQLタグ付きテンプレートリテラルも実装されていることが分かったため、Durable Objectsのストレージを使うにあたり必要なSQLクエリもきれいに書くことができるようになります。
ちなみにこのコードリーディングがきっかけでTypeScriptのSQLタグ付きテンプレートリテラルで10種類のSQLiteドライバー全部試したりするようになりました。
ゲームクライアントを実装するにあたり使用したPartySocketです。
PartyServerと異なりDurable Objectsに依存しておらず、WebSocket APIと互換性があるため、Cloudflare以外の用途でも使うことができます。
ただPartyServerとセットで使うことでクライアント・サーバー間通信を分かりやすく書くことができるので、PartyServerを使うのであれば是非使いたいパッケージです。
今回はUIがゲームなので使いませんでしたが、import usePartySocket from "partysocket/react";
でReactフックと組み合わせて使うこともできます。
皆さんご存知WebアプリケーションフレームワークのHonoです。
PartyServerでもroutePartykitRequest()
で簡単にroomベースのルーティングを書くことができますが、Honoのミドルウェアを使うとより細かいルーティングを簡単に行えるようになるので、WebSocketとHTTPリクエストを分けて考えることができるようになります。
また、Honoのミドルウェアも使えるようになるので、Durable Objectsの中身を見るための管理者ページにBasic認証をかけたり、ETagでサーバーへのアクセスを減らしたりできるようになります。
クライアントで描画するためのゲームエンジンとしてPixiJSを使いました。
ゲームエンジンは今まで様々なものを使ってきましたが、JavaScriptで書かれていて、広く使われており、パフォーマンスが高いため今回採用しました。
パッケージサイズが非常に大きいですが、地味にllms.txt
を配布しているため、コーディングエージェントの実装がうまくいかない時に役立ちます。
ただコーディングエージェントは7.xまでのAPIを完全に理解していたので、v8 Migration Guideを読んで古い書き方を修正するだけで問題なく動くことが多かったです。
以上のパッケージを駆使しながらWebSocketを用いたオンラインゲームを作っていくのですが、なかなかうまくいきません。
WebSocket自体はクライアント・サーバーそれぞれ分かりやすいAPIがあるので問題ないのですが、send()
で送るデータをどうするか最終的な決定は開発者に委ねられます。
今後アップデートで送受信するデータが増えたり減ったりするかもしれませんし、Durable Objectsのストレージと型があわなかったりすると困ります。
そこでValibotとJSON-RPC 2.0を組み合わせてWebSocket用のRPCを実装しました。
通常のJSON-RPC 2.0はid
とJSONレスポンスオブジェクトが必須ですが、id
とJSONレスポンスオブジェクトを省略できるJSON-RPC 2.0 Notificationという仕様も存在しており、双方向に送信だけを行うWebSocketにうってつけだったためこちらを採用しました。
以下はJavaScriptで書かれたクライアント側のコードです。
function createClientNotificationString(notification) {
const result = v.safeParse(ClientNotificationSchema, notification);
if (!result.success) {
console.error("FATAL: Tried to create invalid client notification:", result.issues);
return null;
}
return JSON.stringify(result.output);
}
function sendClientNotification(notification) {
const notificationString = createClientNotificationString(notification);
if (notificationString) {
socket.send(notificationString);
}
}
また、よく分からないオンラインゲームを遊ぶためにメールアドレスやパスワードを入力してサインアップやログインを行ったり、ログインに必要なパスワードを記憶させたりパスワードを忘れたケースを実装するのはばかげているので、UUIDをlocalStorageに保存するようにしています。
Durable Object Storage
SQLite-backed Durable Object Storageでは以下の5つのAPIを使うことができます。
- SQL API
- PITR API
- Synchronous KV API
- Asynchronous KV API
- Alarms API
今回は3のSynchronous KV APIと5のAlarms APIを使用しています。
当初はWebSocketのメッセージ送信時にストレージへ保存する仕組み(サーバーのonMessage()
内部処理)にしていたのですが、これだとDDoSが発生した時にあっという間にストレージの無料枠が枯渇しますし、メッセージ送信量が多いゲームでは使えなかったので、Alarms APIで1秒ごとにゲームに関する全ての処理を行うようにし、プレイヤーの情報も1秒ごとに保存することにしました。
一見富豪的な処理の仕方に見えますが、クライアント(ブラウザ)で行うチート対策にもなるのでこれでよさそうです。テトロミノの落下速度を変更できないのが少々残念ですが。
どうやらこのようなオンラインゲームの仕組みをServer Authorityと言うそうです(コーディングエージェントが教えてくれた)
たとえテトリスバトルであっても移動したり回転したりした時にブロードキャストを行うので、WebSocketの状態管理を行う必要がありますが、おそらく現時点でも最大限In-memory StateとConnection Stateを活用していると思うので、Cloudflare WorkersとDurable Objectsの無料枠は超えないはずです。たぶん。
Alarms APIで行っている1秒ごとの処理もWebSocket接続がない時は止まるようにしていますし、ゲームのような常にメッセージを送信する用途ではあまり意味がありませんが、念のためHibernation WebSocket APIも有効にしています。
static options = {
hibernate: true,
};
Cloudflare WorkersとDurable Objectsを完全に理解するのはものすごく時間がかかりますし、ドキュメントを読んでもコーディングが進むわけではないので、なるべく時短したいですよね。
そんな時はまず使いたいサービスのChangelogを読みましょう。
ただChangelogのページはCloudflare全てのサービスを取り扱っているため、使わないサービスが圧倒的に多くなります。
そこで役に立つのがカスタム検索ページです。以下のように検索すればCloudflare WorkersとDurable ObjectsのChangelogだけを見つけることができます。
最近のChangelogで紹介している内容を確認しておけば、コーディングエージェントが古い書き方をしてもすぐ気づくことができるようになります。
あとはCloudflareもllms.txt
を配布しているため、コーディングエージェントの実装がうまくいかない時に使えそうですが、これまたCloudflare全てのサービスを取り扱っているため、そのまま使うと無駄なコンテキストが含まれてしまいます。
結局のところ、llms.txt
で紹介されているCloudflare WorkersとDurable ObjectsのURLから必要な情報のあたりをつけるしかありません。
Cloudflare WorkersであればRuntime APIページにほぼすべての情報が載っているため、まずはここから始めましょう。
特にBindings (env)とHandlersの情報が役に立ちます。併せてPricingとLimits、Changelogも確認しておくと良いでしょう。
Durable ObjectsであればWorkers Binding APIページにほぼすべての情報が載っているため、まずはここから始めましょう。
特にAlarmsとSQLite-backed Durable Object Storageの情報が役に立ちます。併せてPricingとLimits、Release notesも確認しておくと良いでしょう。
Context Engineering
これらのCloudflareのMarkdownドキュメントを保存したり、使用したパッケージをそれぞれbundleして1つのファイルにまとめたものをコーディングエージェントにコンテキストファイルとして渡すとハルシネーションをおこさずに実装してくれたり、本番環境で動かすプランも考えてくれるのでとても助かりました。
PixiJSはコンテキストファイルとして渡すにはパッケージサイズが大きすぎるので、ContainersとLeaf Nodesに関するドキュメントやllms.txt
を確認したり、UIで使う正確な数値や計算を教えてあげたりしました。
最近のCloudflare WorkersやDurable Objectsにあまり慣れておらず、パッケージ含め公式ドキュメント以外の情報が非常に少なかったですが、とりあえずちゃんと遊べるものを作れて満足しました。
なお、当初はMMORPGや釣りゲームを作っていたのですが、Cloudflare WorkersとDurable Objectsの無料枠とWebSocketのメッセージ送信量について考えた結果断念することにしました(この記事のアイコンやゲーム名はその名残です)
Cloudflare WorkersとDurable ObjectsでWebTransportが使えるようになったらまた再挑戦したいですね。
Views: 0