ネットワーク対戦を実装してみる
こんにちは😊
だんだん冬の寒さが和らいで、過ごしやすくなってきていますが、みなさまいかがお過ごしでしょうか
さて、今回はネット対戦の実装の仕方をご紹介したいと思います。
私は趣味で自作ロックマンエグゼを作っているのですが、その際に実装したネット対戦機能の概略を紹介します。
※この機能実装は私の完全に独自で実装したコードになるため、効率が悪かったりそもそもアーキテクチャの質が悪かったりするかもしれません。こういう方法でも動くんだくらいの感じで読んでいただけると幸いです🙏
全体概要
今回はRouter側でバトルに必要な情報を持ち、各Client(アプリ)から非同期で必要な情報を送信、バトルの状態は一定期間ごとにRouterからClientに向かって発信するといった設計にしました。
1 対 1 なので互いのアプリがやり取りするような方針でもよかったのですが、以下の理由からこのアーキテクチャを選択しました
1. 対戦相手のIPアドレスを知る必要があり、Firewallの設定でそのIPアドレスからの通信を許可してやる必要がある
一般の人がwindowsでこの操作をやるのは非常に大変でめんどくさいです
2. 将来的に n 対 n 対戦や試合観戦者のライブビューイングみたいな1 対 1 対戦以上に拡張しようと思った際に、拡張しやすい
RouterとMatcherという感じであえて分けているのは、もしアクセス者数が増えたときにRouterを増やして負荷分散できるようにするためですね。(今のところ杞憂で終わりそうですが)
あとは、Ruby on Railsとgolangという全然別のフレームワークを使っているので分けとくかっていうのもあります
ちなみに後々調べたらこの方式はゲーム業界で非同期式と呼ばれる方法だったみたいですね
構成技術
まず一番下の通信をする土管の部分ですが、今回はgRPCを使うことにしました。
一般のゲームおけるネット対戦ではUDPをベースとしたプロトコルを使用するのですが、今回は趣味のアプリで同時接続数もそんなに多くないことと、PCからの通信なのでそこまでインターネット環境も悪くならんだろうってことでgRPCを選択しました。
私がgRPCに手慣れていたことも理由の一つです。
正直ここはなんでもいいと思います。それこそビジネスでない趣味のアプリならWeb socketでもgRPCでも(UDPでも)そこまで差は出ないと思います。
protocol bufferはこんな感じですね。
syntax = "proto3";
package router;
service Router {
rpc SendAction(Action) returns (Result) {}
rpc PublishData(AuthRequest) returns (stream Data) {}
}
・・・略
SendActionはClientからAction(移動したやダメージくらったなど)をRouterに送信し、その動作が成功したか失敗したかを受け取るメソッドです。
PublishDataは最初のClientの認証情報を送り、うまく認証されたらRouterから定期的に情報を受け取るためのstreamを受け取ります。
Routerの情報が絶対正義なので各ClientはこのPublishDataで流れてきたデータを自身のアプリに反映するといった処理を行ないます。
そして何か自分からアクションしたい場合はSendActionでデータを送信し、Routerで処理してもらいます
ちょっと工夫している点
単にネットワーク越しにデータをやりとりするだけでなくちょっと工夫している点もあるのでここで紹介します
Action時にローカルのカウントも使う
Client(アプリ)側は定期的にRouterから送られてくるデータを表示しているだけなのですが、それだけだと送られてくる頻度が少なすぎてカクカクして表示されてしまいます。
Routerから送られてくるデータ間隔はサーバーの負荷やネットワーク遅延などを考えると100 ~ 150msに1回くらいが限度です
それに対して、アプリでは一般に60FPS(1秒間に60回の画面更新)くらいが必要とされてます。このFPSを実現するためには16ms(1秒/60回)ごとに画面更新が必要となります。
このギャップを埋めるために、Routerからの情報をスナップショットとして扱い、そこから多分こう動くだろうという仮説のものローカルでアニメーションさせ、次の情報がRouterからデータが送られてきたらローカルアニメーションをその情報に合わせるということをやっています。
イメージとしてはこんな感じですね
こうすることで定期的なRouter情報を反映しつつぬるぬると動くアニメーションも表現してやることができます
ClientからSendActionするタイミングをまとめる
ClientからActionしたい場合はSendActionメソッドを呼び出せばいいのですが、現在私のアプリでは一度にまとめて送信しています
これはSendActionするとその結果(成功したか失敗したか)を同期的に待たないといけません。(それによってアクションが変わるため)
そうすると、待っている間にユーザーが次のアクションを取れずにボタンを押しても反応しないといった不快感を感じるようになってしまいました。
そのため、送信したいActionを一旦まとめてちょうど良いタイミングでまとめて処理しています。
将来的には通信の回数自体を減らすため、BulkSendActionみたいなメソッドを作って1回の通信で全部送れるようにしたいですが、それは今後の課題です(多分そこまで難しくはなさそう)
インフラ情報
最後にちょろっとインフラ情報も載せておきます
今回は使用されてない時はできるだけ無料で抑えられるようにしたかったので基本無料で組んでいます
さいごに
一応素人でもネット対戦を形にすることはできました💪
ただ、実際にビジネスとしてやるなら全然考慮は足りませんし、趣味でゲーム制作する際でもライブラリが使えるならそっち使ったほうが有用だと思います。
ライブラリがないような環境でネット対戦的なことをやりたい人にこの記事が届けば幸いです。
あと、もし自作ロックマンエグゼに興味がある人いればぜひ遊んでみてください😀
https://github.com/sh-miyoshi/go-rockmanexe