feature image

2024年9月16日 | ブログ記事

ゲームのバックエンドを作ってみたってだけの話

夏のブログリレー 29 日目の記事です!

たけのひとです。今日は去年1年間制作してたポーカーのバックエンド制作の話をします。ほぼ備忘録です。ゲーム自体は完成しなかったけど……。

色々話したいことはあるのですが、今回は"バックエンド"(技術的な話)のみに焦点を当てて色々話します!

先にお断りするのですが、プロジェクトの方針でソースコードは公開しないことにしました。なのでコードは一切出てこないです。若干わかりづらいかもですがご了承ください…!

何作ってたの?

ポーカー(テキサスホールデム)の 1vs1 の対戦アプリです。レート戦とフレンドバトルがあります。

テキサスホールデムについては、↓の動画とか見ると大体わかるはず!これの 1 対 1 版です。

ただこの記事を読む上では、"ターン制のオンライン対戦ゲーム" ということさえ頭の中に入っていれば問題ないと思います!

技術スタック

通信について

まず、フロント・バックの通信方法についてですが、ゲーム外では HTTP / websocket, ゲーム中は Websocket を使って行いました。

ゲーム外に関しては、以下のような機能を実装していました:

マッチングはおいておいて、他は大体ふつーの Web バックエンドと同じことやればよさそうなので、使い慣れている技術を持ってきました(それで Go を持ってきた)。

問題はリアルタイム通信の方で、Websocket と gRPC の 2 択で検討していたのですが、gRPC が C# でうまく使えないという問題にあたってしまって、結局 Websocket を使うことにしました。

Websocket、 openAPI みたいないい感じに通信仕様を書ける場所がなくてちょっと困ってたり…。これを解決せずに開発終盤まで持っていった結果どうにもならなくなってしまったので、最後は git 上で md 形式で仕様書をまとめることにしました(この点をどうにかできそうだというところで、 gRPC は気になっていました)。

ちなみに今回 Pub/Sub の仕組みは実装してません。だって対戦相手1人しかいないんだもん。不特定多数にイベントを送信する、ということがないので、Client struct にメッセージを送信するメソッドが生えていて、それを直接叩く処理になっています。Player A にも B にも送信する、という仕組みがいろんなところで生えていて少し面倒だった。

バックエンドの構成

バックエンドはだいたいこんな感じのアーキテクチャ・実装になりました:

traP の人(別に traP じゃなくてもそうなんですけど)みんなクリーンアーキテクチャを採用しがち。ただ自分は昔 Laravel で書いてたというのもあって、この書き方にまだ慣れないんですよね…(最近になって慣れてきました)。なのでこういう感じになっています。

パッケージ間の依存関係を図にすると、大体こういう感じ (破線になってるのは interface 使って DI してるよ〜、という意味。使い方あってる?)

ちなみに、これパッケージ構成を整理するリファクタリング途中のときの図なので、もしかしたら破綻があるかもしれないです。あったらごめん。

大変だったことピックアップ

大変だったこと、学んだことを3つだけピックアップしておきます。

poker Master とタイマーについて

前述の通り、 poker.Master は仮想のゲームサーバーを模しています。 Master には主に以下のような機能があります:

  1. アクション(コマンド)を実行する
  2. Master からクライアントにメッセージを発出する
  3. ゲームの盤面を動かす(手番など)
  4. 排他ロック処理
  5. 各種タイマーの管理

(他にもゲーム終了時のクリーンアップとか、再接続処理とか色々…)

ここではゲームタイマーの管理についてピックアップしていきます。これは明らかに普通の Web サーバーにはない機能です。

ターン制のゲームなので "いつアクションが届いたか" を細かく管理する必要はないのですが、持ち時間が存在するのでこれを管理する必要があります。これが結構面倒でした。

Go 標準の time.Timer は一定時間後に channel に通知が来るだけなので、消費時間を管理したりできません。ということで、自分で消費時間を管理できて、残り時間を確認できる timer を自作して管理することにしました。(ついでに context を渡して外から強制終了できるようにしたり…。)

これを使って、時間切れになったときは新しく時間切れのメソッドが叩かれる、みたいな実装にしました。排他ロックを実装してたので、デッドロックとかが怖かった。

あとは "timer の goroutine がずっと残る(消えない)" とか "ゲーム終了後に timer の時間切れイベントが発火してゲームを進行しようとし、色々あって最後 panic する" とか timer 周りのバグが沢山あって大変でした。考慮しきれんて。

あとは、ラグを計算してそこから逆算して消費時間を計算〜、とかできると格好良かったなとか思ってます。

エラーの取り扱い

インゲームでエラーが発生したときのエラーの取り扱いに結構悩みました。外に開示するべきエラーなのか、ゲームを中断するべきエラーなのか、、などなど…。

あとは結構複雑な構造になっていたのにもかかわらずメソッドで出たエラーはラップせずにそのまま return していたので、どこで出たエラーなのか分からないということもしばしば起きてしまいました。

この辺を解決しようとして、pokererrors pkg を自分で作ることにしました。こういう感じになったらしい。なんかもうちょっと上手く作れた気がする…。

type Error interface {
	error
	Unwrap() error
	Type() ErrorType
	IsInternalError() bool
}

type ErrorType struct {
	IsInternalErr   bool   // whether to show the error message to the user
	Message         string // message to be displayed to log / should be in English
	Code            string // code to be used in client
	ExternalMessage string // message to be displayed to users / message in Japanese
}

大量に発生する panic

しばしば panic が起きてアプリケーションが落ちる、ということがありました。原因は色々あったのですが

バグは発生し次第都度対処はしてたのですが、本番で落ちてしまうと問題なので結局自分で recover できる機構を組むことになりました。panic が発生したら panic が発生した当該ゲームを強制終了してログを残し、各 Client に強制終了を通知して close する、というロジックです。普通の Web サーバーだったら別に panic が起きても最悪 recover するだけで良い(なんなら echo の middleware 使えば基本問題にならない)ので、それと比べるとちょっと面倒だなぁ、と思った瞬間でした。

感想

goroutine を使いだす(非同期処理を使いだす)と、一気に考慮事項が増えて難しかったです。あと想像できないバグも沢山出てきて特定がすごく大変でした。このあたりを気にせずに使えるようになっている Echo って実はすごく優秀なのでは…?


他にも語れることは沢山あるのですが、これ以上記事が長くなったら誰も読まなさそうなのでこの辺にします!めっちゃ雑な記事になっちゃってごめんなさい。読んで頂きありがとうございました〜!

明日は @komichi さんと @ramdos くんの記事です!お楽しみに〜

Takeno_hito icon
この記事を書いた人
Takeno_hito

22B 「たけ」 基本的にゲームばっかりやってる怠け者です。SysAd班とゲーム制作が主

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年9月17日
1か月でゲームを作った #BlueLINE
Komichi icon Komichi
2024年8月21日
【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう!!!!【WebView UI】
kashiwade icon kashiwade
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2022年9月16日
5日でゲームを作った #tararira
Komichi icon Komichi
2024年8月29日
クロスコンパイルRust
H1rono_K icon H1rono_K
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記