ゲーム投稿サイト「GodotPlayer」に使われている技術
はじめに
私が個人開発しているフリーゲーム投稿サイト「GodotPlayer」で使われている技術について紹介します。
ゲーム投稿サイトとして考えなければいけないデータ転送量の課題とGodot Engine特有の仕組みより、Cloudflareが提供しているCDNとR2ストレージを組み合わせて使用しているところがポイントです。あと個人的に学習をしてみたいと思ったものを選びました。
また、先行事例としてある、ゲーム投稿サイト「unityroom」に使われている技術をとても参考にさせて頂きました。
Webサイトのシステムについて
バックエンド
PHP×Laravelを使っています。PHPについては仕事で触っている関係でなんとなく選びました。Laravelについては、私の職場でいろんなフレームワークに触れる機会がなく何か学習したいなあと思っていまして、Laravelを使いました。これまで経験してきた職場ではノンフレームワークのPHPと、Codeigniter3しか触ってきてなかったのですが、Laravel触ってみてサービスコンテナとかRefreshDatabaseTraitとかFactoryとかSeederとかとにかくいろんな便利機能があって感動しました。
DBにPostgreSQL、セッション管理にRedisを使っています。
バックエンドのソフトウェアアーキテクチャ
ソフトウェアアーキテクチャについては初心者です。DDDとかに興味があって技術書を読み込んだりしましたが挫折しました...
GodotPlayerでは当初はオニオンアーキテクチャで作ることを考えていたんですが、Laravelでリポジトリパターンを採用するとEloquentの機能を投げ捨ててしまうような部分が出てきてしまって(ページネーションとか)、それが勿体無いかなと感じ、今はmpyw様の手法に落ち着いています。
バックエンドのテストコード
PHPUnit使ってます。
単体テストについてはValidationRuleを実装しているクラスくらいしか書いていないです。機能テストについては書けそうなところは書いているんですが、特に権限周りのテスト(あるゲームを第三者が編集できないとか、ログインユーザーしか操作できないとか)や、ゲーム公開周りのテスト(非公開ゲームでも自分のゲームは閲覧できたり、限定公開のゲームはTOPページに出ないけどURL直接打てばプレイできたり)、ゲームジャムの時間周りのテスト(ゲームジャム予告期間、ゲームジャム開催期間、ゲームジャム評価期間などで処理を分岐しているところを確認)を重点的に書いています。
フロントエンド
React、Inertia.js、Tailwind CSSを使用しています。フロントエンドについても初心者でして、とりあえずなんとなく動かせるレベルになれたらいいなくらいを目指して学習しています。
バックエンドとのやり取りはInertia.jsに任せてしまっています。内部でどんなことをやっているか理解しないまま使っちゃってるんですが、Reactのpropsに値が簡単に渡ってくるのでEasyって感じで済ましてしまってます。
サーバーホスティング
Railway.appを使用しています。当初はunityroomと同じHerokuの使用を考えていましたが、無料プランが廃止されたのをきっかけに別のPaaSを探し、結果としてRailway.appを使うことにしました。
- Fly.io → コマンドベースで管理していくのが自分の肌に合わなかった
- Render.com → 無料枠のDBについて90日で期限切れになるのがネックに感じたので採用せず
- Railway.app → UIの使い勝手が良い、料金も良心的だったのでこちらを採用
Railway.appの料金ですが、Herokuなどとは異なりCPUやメモリの使用量に応じた従量課金となっています。GodotPlayerのリリース当初の費用感はStaging環境とProduction環境とで合わせて$6程度でした。
Cronやサーバーログ閲覧などの細かなところで使い勝手が微妙に感じているのと、サーバーのリージョンを選択するのに料金プランをProに上げる必要があるところが惜しいなと思っているのですが、そこら辺は今後に期待ということで全般的に満足して使用しています。
オブジェクトストレージ
ゲームファイルの保存および配信にCloudflareのR2を使用しています。ブラウザゲーム投稿サイトの挙動として、1回のゲームプレイに対しブラウザが数十MBのゲームファイルを要求します。AWSのS3とか使うとデータ転送料金のコスト的な側面で私の財布が死んでしまうため、データ転送料が格安または無料のものを選びました。こちらもunityroomと同じくConoHaのオブジェクトストレージの使用を検討しましたが、ConoHa単体ではGodot4のHTML5ゲーム特有の要件に対応できなさそうだったので別のオブジェクトストレージとしてR2を選定しました。R2についてはまだリリースされてまもなく、信頼性やセキュリティってどうなっているんだろうと疑問を感じつつ、データ転送量が無料なところに釣られてしまい使用することにしました。
ゲームファイルのアップロード周りの仕組みと運用
アップロード自体の処理についてはunityroomと同じく、PUT可能な署名付きURLをサーバーサイドから取得し、ブラウザ上でアップロードする形式をとっています。
アップローダーの形式ですが、どんなものがあるか知りたくて、まずはいろんなゲーム投稿サイトのアップローダーを動かしてみました。私の観測範囲内では大まかに2種類に分類しています。
- zip方式
- itch.ioやPLiCyが採用している
- ゲームエンジンからエクスポートしたゲームファイルをzipにまとめてアップロード
- いろんなゲームエンジンに対応可能
- zipを解凍してサーバーに配置する仕組みが必要になりそう
- 特定ゲームエンジン特化方式
- gotm.ioやunityroomが採用している
- ゲームエンジンからエクスポートしたゲームファイルの内、特定のファイルのみをアップロード
- 投稿していただくゲームを特定のゲームエンジンに絞り込むことができ、また最適化できる
- zip方式に比べアップロードするゲームファイルのファイルサイズを減らすことができる
- ゲームエンジンのバージョンに応じたテンプレートをWebサービス側で運用する必要が出てくる
GodotPlayerの場合は、
- ゲームエンジンのバージョンに応じたテンプレートの管理をしたくない
- オープンソースなのでテンプレート(wasmやjsファイル)を改変できる可能性がある
- zipを解凍してサーバーに配置するシステムを組むのが面倒
という背景で、ゲームエンジン限定方式はやりたくなくって、でもzip方式は作るの面倒くさそう、というネガティブ〜な理由で上記2つの中間に当たるようなアップロード方式を取っています。具体的には、Godotでエクスポートをすると基本的には1ディレクトリ、フラットな階層でゲームファイルが出来上がるのでそれを丸ごとアップロードする感じです。
GodotでHTML5エクスポートしてできたファイル群。1ディレクトリにまとまっている。
複数ファイルを丸ごと選択してアップロード
ファイルの選択はHTMLでmultiple属性を使って対応しています。
<input type="file" multiple>
(動作するかは別としてGodot以外のゲームエンジンのゲームファイルをアップロードできる可能性が実はあります)
SharedArrayBuffer対応
Godot4のHTML5ゲームですが、2023年11月時点ではブラウザ側でSharedArrayBufferをサポートする必要があります。
HTML5 export allows publishing games made in Godot Engine to the browser. This requires support for WebAssembly, WebGL and SharedArrayBuffer in the user's browser.
SharedArrayBufferって何者なのでしょうか。恥ずかしながら理解しておりません。
デフォルトではブラウザ上でSharedArrayBufferが無効になっているため、cross-origin isolationという状態を有効にしてSharedArrayBufferを使えるようにします。ここら辺突っ込んだ話はよく分かってなくて、こちらの記事を参考にさせていただきながら作りました。
先述のオブジェクトストレージの話に少し戻りますが、CloudflareのR2はCDNの機能を併せて使用していて、オブジェクトストレージからブラウザへのレスポンスにSharedArrayBufferに対応するためのヘッダをいくつか噛ませています。(ConoHaのオブジェクトストレージ単体だとこれが出来ない、と思うのでR2を採用した次第です)
Transform RulesのModify Response HeaderでHTTPヘッダを嚙ます
あと、リリース当初は対応していなかったのですが、利便性のためゲームをiframe読み込みでプレイして欲しかったので、Webサーバー側でもHTTPヘッダを追加しています。
public function handle(Request $request, Closure $next): Response
{
header('Cross-Origin-Opener-Policy: same-origin');
header('Cross-Origin-Embedder-Policy: require-corp');
return $next($request);
}
SharedArrayBuffer絡みでは、iframe読み込みでのゲームプレイをさせるのが実はとても悩ましく、可能にしてしまうと今度は外部リソースの読み込みがうまく出来ない状況になります。たとえばYoutubeの埋め込みなどはこんな感じになります。
Webサーバー側で何もHTTPヘッダを追加してない状態。Youtubeを埋め込み出来ている。
Webサーバー側でSharedArrayBufferに対応するためのHTTPヘッダを追加した状態。Youtubeの埋め込みが読み込めなくなった。
ここら辺各サイトどのように対応しているんだろうかと調べましたが、例えばPLiCyだと大画面で遊ぶようにしているようです。
GodotPlayerの場合は利便性を追求した結果、外部リソースがうまく読み込めない状況になっているため、まだ検証はしていませんが広告などの実装が困難になると思われます。サーバー費用くらいはサイトから賄いたいなと思っていたのですが、どうしたものか途方に暮れています。
今後のロードマップ
GodotPlayerについての個人開発は細く長く楽しんで行きたいと思っているので、機能追加のペースはけっこう緩やかになってしまうかもしれません。とあるコミュニティ様とでコラボ企画的なお話も実は頂いてたりするのですが、お待たせしてしまうかもしれません。
主にゲームジャム周りで機能追加であったり運用を容易にするための開発をしていきたいなと思っているので、ゆるりとお待ちいだだけますと幸いです。
Discussion