概要
スマホゲームはオンラインマルチプレイ(オンラインで他のユーザーとリアルタイムな協力・対戦をする)アプリがとても人気。そういったゲームには大体以下のような機能が実装されていると思われる。
- ユーザーマッチング
- チャット
- ボイスチャット
- 同期通信、レンダリング
これらを実装する上でどのような便利なライブラリ(と呼べばよいのかな?)が世の中に転がっているのか、それを使ってどう実装するのかがとても気になるところ。この記事ではその辺をまとめていきたい。
便利なライブラリ
Tech AcademyのUnityの学習でメンターさんから例えばPUN2というのがありますよ、と聞いていた。
また、"Unity リアルタイム通信"でググると、"MagicOnion"というワードがよく引っかかった。
この2つについて調べてみる
その他Tech Academyのメンターさんに聞いたモノビットというものもあるらしい。
1. PUN2(Photon Unity Network2)
そもそもPhotonとは
- リアルアイムネットワーク通信フレームワークの現主流がPhoton。Photon製品は複数あるが、ここを起点にまとめると以下のようになる。
- Photon:マルチプレイを簡単に実現するためのネットワークエンジン。ExitGames社が開発している。マッチング、メッセージの同期送信などができる。
- Photon Server:Photonをオンプレのサーバーで利用するプラン
- Photon Cloud:PhotonをSaasで利用するプラン
- Photon Unity Network2(PUN2):PhotonをUnityで利用するためのパッケージ。サーバーはPhoton Cloudが裏で使われ、フリープランは20人同時接続可能、有料プランは100人同時接続可能らしい。
概要
- リアルタイムネットワーク通信フレームワークの王様PhotonをUnityで扱えるようにしたもので、サーバー側はPhoton Cloudを利用するものらしい。
- 最大20人まで同時接続できるサーバーが無料で利用可能らしい
- プレイヤーは同一ルーム上で遊び、ルーム内に生成したオブジェクトは各端末間で同期することができるらしい
2. MagicOnion
概要
- リアルタイムネットワーク通信のフレームワークらしい(Photonと同じ位置付けのものらしい)
- MagicOnionはクライアント側もサーバー側もC#で統一したいという思想のもと作られているらしい
- クライアントおよびサーバーでC#でインターフェースを定義したファイルを設置、サーバー側でインターフェースを実装したクラスのファイルを設置するとさほどクライアント側で通信処理を意識せずにサーバー側の処理を呼び出せる、という感じらしい
- MagicOnion自体はOSSだが、別途サーバーが必要になるため、個人開発の場合はPhoton Cloudを使うのもありらしい
3. モノビット
http://izm-11.hatenablog.com/entry/2019/05/09/120458
の記事で見た感じだと、
- 技術情報はPhotonが多い。
- オンプレでゲームサーバーを立てる場合、Photon(Photon Server)Windowsサーバーしか使えない
という感じらしい。
個人でオンラインマルチプレイの実装を学ぶという段階では、オンプレでサーバー立てずにPhoton Cloudつかってなおかつ無料プラン範囲内でやりたいため、PUN2の方に分があるかな、と考える。
3. まとめ
オンラインマルチプレイゲームを開発するためのリアルタイムネットワーク通信フレームワークとして、PhotonとMagicOnionというものがある。PUN2はPhotonをUnityで利用するためのパッケージ。PUN2と似たものとして、日本製のモノビットというものもある。Unityでオンラインマルチプレイゲーム開発をするならばPUN2かモノビットかの選択になるかと思うが、個人開発する上では、技術情報が多くクラウドのゲームサーバを利用して無料プランで20人まで同時接続できるPUN2を選択するのがよさげ。
というわけで、ここからはPUN2について学んでいく。
PUN2の学習
まずはPUN2の学習のとっかかりになるような情報がほしいところ。
"PUN2 学習"とか"Photon 学習"とか"Photon 入門"でググったり、ヒットした記事の中をたどったりして見つかっためぼしい記事は以下。
まずは1. の公式チュートリアルで学んでいこうと思ったが、非常に難解で挫折。意味がわからなすぎる。3. で学習することにした。
Photonを使ったマルチプレイ入門その1での学習
以下、実際にやってみての自身のメモ、コメント。
初期設定をしよう
- アプリケーションIDって公式サイトからも発行できたんだ・・・インポートした後のウィザードで発行するという方法しかないのかと思ってました。
- そして発行した後このサイトから確認できたんだ・・・便利ですね。
- PUN2のセットアップが完了するとAssets > Photon > PhotonUnityNetworking > ResourcesにPhotonServerSettingsという設定ファイルができて、AppVersion(これから作成するアプリのバージョン?)、Fixed Region(PUN2として接続するサーバーを固定する場合のリージョン設定)、Enable Support Logger(ログ出力の設定?)など色々設定できるみたい。あと、App Id RealTimeっていう項目で、ウィザードで割り当てたAppIdが入ってることにきづいた。ここにセットされるんだね。
とりあえず覚えておきたい重要クラス
- PUN2に備わっている機能を調べたい時にはPhotonNetworkクラスから調べ始めるといいらしい。オンラインゲームを開発するための主要操作のほとんどがこのクラスから使用できるらしい。
- ネットワーク上で同期させるオブジェクトはNetworked Objectと呼んでいて、通常のオブジェクトをNetworked Object化できるみたい。そうするには以下の条件を満たさなければならないらしい。
- PrefabにPhotonViewコンポーネント追加
- PrefabをResourcesフォルダへ格納
- PhotonNetwork.Instantiate()でインスタンス生成
他プレイヤーの画面にオブジェクトを表示させよう
- 他プレイヤーと通信するまでの流れが以下のようになることがわかった。
- マスターサーバーへ接続
- マスターサーバーへ接続後、ルームの作成、ある場合はルームへ参加(=マッチング)する
- マッチング成功後、ネットワークオブジェクトを生成する。
- なんとなく以下の流れだと思っていた(PUN2サーバー側へゲーム参加要求出したら後はPUN2サーバー側でいい感じに処理してくれるイメージ).
- 各プレイヤーがゲームへ参加する要求を出す
- プレイヤーからの要求を受けてPUN2サーバーがプレイヤーをマッチング
- マッチング完了後にPUN2サーバーはルーム作成し各プレイヤーをルームへ入れる→ゲーム開始
- そうではなくて、本当は各プレイヤーが能動的に動く形だった。
- 各プレイヤーはマスターサーバーへ能動的に接続する
- 各プレイヤーは能動的にゲームサーバ上にある作成済みのルームに入ろうとする。なければ自身でゲームサーバ上へルームを作って入る。
ネットワークオブジェクトを作成しよう
- 上記1., 2.をやるには具体的には以下の操作をした。
- ヒエラルキービューに空のオブジェクト追加(Create > Create Empty)
- プロジェクトビューでAssets配下にResourcesフォルダを作成(Create > Folder)
- Resourcesフォルダ配下にPrefabを作成(Create > Prefab)
- ヒエラルキービューにある空のオブジェクトをResurces配下のPrefabにアタッチ
- Prefabに対してAdd ComponentからPhoton Viewスクリプトを追加(Add Component > Photon Networking > Photon View)
- ヒエラルキービューにあるオブジェクトを削除(もう不要)
ネットワークオブジェクトを表示するための最小スクリプト
- 空のゲームオブジェクトを作成して以下のスクリプトを追加してください・・・については、そもそもどこにスクリプトおいたらいいんだろう?ということで調べてみると、Scriptsフォルダを掘っておくといいということでそうした。
- 追加するサンプルのScript内のコメントを見て、PhotonNetwork.ConnectUsingSettings()メソッドは、Assets > Photon > PhotonUnityNetworking > ResourcesにPhotonServerSettingsの設定を使ってマスターサーバー(マッチングを担当するサーバ)へ接続するものだということがわかった。
- MonoBehaviourPunCallbacksクラスを継承したSampleScene内によくわかっていないコールバックメソッドが登場するので調べてみたココで。
- Start():MonoBehaviourPunCallBacksクラスは、MonoBehaviorクラスを継承しているので、どうやらMonoBehaviorクラスのよく知ってるStart()らしい。
- OnConnectedToMaster():クライアントがマスターサーバーへ接続され、マッチメイキングやその他のタスクの準備ができているときに呼び出されるものらしい。
- OnJoinedRoom():ロードバランシングクライアント(なにそれ?)が部屋に入ったときに呼び出されるものらしい。このクライアントが部屋を作成した、単に参加したによらず。SampleSceneクラス内のコメントでは「マッチングが成功したときに」と書いてあるけどリファレンスの説明の方がしっくりくるかも。
-
PhotonNetworkクラスのメソッドも各コールバックメソッド内で呼ばれてるけどよくわかっていないので調べる
- PhotonNetwork.JoinOnCreateRoom():
- 概要
- 名前で指定したルームへ参加する。ルームが存在しなければそのルームを作成する。実行された後、コールバックメソッドOnJoinedRoom()(ルームへの参加成功時)かOnJoinRoomFailed()(ルームへの参加失敗時)を呼ぶ。特に、ルームを作成したクライアントはOnCreatedRoom()というコールバックメソッドも呼ばれる。このメソッドはコールバックメソッドOnConnectedToMaster()を実装してこの中で呼び出す必要がある。なぜならクライアントがマスターサーバーに接続しているときのみ、呼び出すことができるメソッドだから。
- 引数
- string roomName:ルーム名
- RoomOptions roomOptions:ルーム作成時(すなわちまだそのルームが存在していない場合)のルームのオプション。RoomOptionsは、「ルーム内プレイヤーの最大数(初期値0で、これは無制限の意味になる)」、「プレイヤーのネットワークが切れたときにルーム内からプレイヤーが削除されるまでの時間」、「ルーム内からプレイヤーがだれもいなくなってから、ルームが削除されるまでの時間」などが設定できる。ルームがすでに存在している場合、このRoomOption自体が無視される。これは、ルームのオプションが遅れて参加してきたプレイヤーによって変更されるのを防ぐためとのこと。
- TypedLobby typedLobby:ルーム作成時(すなわちまだそのルームが存在していない場合)に所属させたいロビー。ルームがすでに存在している場合は無視される。(※そもそもPUN2では必要がない場合ロビーの使用は非推奨らしい。)
- String[] expectedUsers=null:ルームへの参加見込みプレイヤーのUserIdを指定する。指定した場合、ルームへの参加枠がこのユーザーのために確保される模様。
- 概要
- PhotonNetwork.Instatiate()
- 概要
- 公式リファレンス、まさかの説明放棄wなんも書いてない。入門記事には、ネットワーク上で同期させるオブジェクトをインスタンス化するメソッドと書いてあるからそうなんだろう。
- 引数
- string prefabName:ResourcesフォルダにPhoton Viewというスクリプトがアタッチされたプレハブを置いた状態で、そのプレハブの名前を指定してやる。
- Vector3 position:プレハブの位置
- Quqternion rotation:プレハブの回転
- byte group=0:???
- object[] data=0:???
- 概要
- PhotonNetwork.JoinOnCreateRoom():
- なお、PUN2で始めるオンラインゲーム開発入門【その1】時点での自プロジェクトの状態は以下のような感じ。
- 「本当に他プレイヤー側でも自動的にネットワークオブジェクトが生成されているのか確認したい場合は、ビルドして複数起動してみると良いでしょう。」については色々調べて試して以下の確認手段を整えた
PUN2で始めるオンラインゲーム開発入門【その2】での学習
自分のオブジェクトだけを操作しよう
- ネットワークオブジェクトも普通のオブジェクトと同じようにスクリプトで操作できるらしい。逆に言えば、共有しているオブジェクトに対する操作権限をしっかりしておかないと、意図しない動作をしてしまうということ・・・かな。
- オブジェクトが自分が生成したものか?はphotonView.IsMineでチェックできる
- なお、photonView.IsMineを使うにはMonoBehaviourPunCallbacksを継承したクラスのスクリプトが対象のオブジェクト(プレハブ)にアタッチされていないとダメ。
- 「うーんなんかシーンを再生しても記事のGifみたいな動きしてくれないなぁ」と思っていたら、矢印キーの入力に応じて動くんだったw
ちなみにif(photonView.IsMine)を外したとき、あるときで以下のような動きになり、IsMineが存在するときは確かに自身が生成したオブジェクトだけ操作対象になっていることがわかる。
if(photonView.IsMine)内で操作(自分のだけ動く)
if(photonView.IsMine)なしで操作(両方動く)
入力周りを改善しよう
- 上の移動処理において、以下の問題を修正する
- 斜め移動時に移動量が大きい(maxで√2倍)
->ベクトルを正規化(絶対値1。new Vector2(valueX, valueY).normalizedとするとできる) - 移動量がフレームレート依存
->1フレームあたりの移動量をTime.deltaTime(前フレーム開始~次フレーム開始までの時間(秒))に比例するような処理にする
- 斜め移動時に移動量が大きい(maxで√2倍)
座標を同期させる最も簡単な方法
- ここまでの段階ではまだ自身の端末で自身のオブジェクトのみ移動するだけ
- ネットワークオブジェクト化するPrefabにはPhotonViewスクリプトをアタッチする必要があった
- 座標/回転/大きさを同期するには、PhotonTransformViewというスクリプトも同Prefabにアタッチしてやりつつ、PhotonViewが監視する対象のスクリプトとして、PhotonTransformViewを設定してやれば勝手に端末間で同期される仕組みになっている。
任意の値を定期的に同期させる方法
- 座標/回転/大きさの同期はPhotonTransformViewを使えばよかった
- オブジェクトの値などその他の値を端末間で同期させるには以下のようにすればよい
- 同期対象のオブジェクト自体のクラスをIPunObservableインターフェースも継承したものにする
- IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)メソッドをオーバーライドする
- Update()とOnPhotonSerializeView()を使って全体として以下のようにかけばOK
using Photon.Pun;
using UnityEngine;
public Class <同期対象オブジェクトのクラス名> : MonoBehaviorPunCallback, IPunObservable{
// オブジェクトの状態を表すメンバ
同期対象のメンバ1;
同期対象のメンバ2;
// Update
private void Update(){
// 処理対象が自身のオブジェクトでないならば打ち切り
if(!photonView.IsMine) return;
// 自身のオブジェクトに対しての処理を記述
// 1. 位置/回転/大きさの変化を発生させる記述(PhotonTransformViewによっていい感じに同期される)
// 2. オブジェクトの状態を管理するメンバ値の変化を発生させる処理を記述(OnPhotonSerializeVie()メソッドで同期させる)
// 3. オブジェクトの状態を管理するメンバからオブジェクトの状態を実際に変化させる
}
// データの送受信を行うメソッド
// SendNext()とReceiveNext()の引数となる同期対象のデータは順番を揃える必要がある。
// SendNext()はストリームブロックに同期対象データを書き込む
// ReceiveNext()はストリームブロックに書き込まれているデータを読み取り、次のブロックに進む
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
// 処理対象が自身のオブジェクトである場合
if (stream.IsWriting) {
// オブジェクトの状態を管理するデータをストリームに送る
stream.SendNext(同期対象のメンバ1);
stream.SendNext(同期対象のメンバ2);
}
// 処理対象が自身のオブジェクトでない場合
else {
// 1. オブジェクトの状態を管理するデータをストリームから受け取る
同期対象のメンバ1 = (同期対象のメンバ1のデータ型)stream.ReceiveNext();
同期対象のメンバ2 = (同期対象のメンバ1のデータ型)stream.ReceiveNext();
// 2. オブジェクトの状態を管理するメンバからオブジェクトの状態を実際に変化させる
}
}
}
- 例として挙げられているスクリプトについて
- Vector2.magnitude: ベクトルの距離
-
Color HSVToRGB(hue, saturation, value):
- 概要:HSVで表された色をRGBに変換する
- パラメータ:
- H(hue):色相(色自体。値の範囲は0〜360度が一般的。0→360で赤橙黄緑青藍紫と変化しいき、360でまた赤に戻る。このメソッドでは0~1で一周する設定になってる)
- S(saturation):彩度(小さいほど透明、大きいほど色づくもの。値の範囲は0〜100%が一般的。このメソッドでは0~1の範囲になってる)
- V(value):明度(小さいほど黒く、大きいほど色づくもの。値の範囲は0〜100%が一般的。このメソッドでは0~1の範囲になってる)
- 返り値:
- Color型。Color型はRGBAで色を表現する。Color.rとかで色の値を取得できる。なお、Min:0,Max:1である。
-
OnPhotonSerializeView()
- 概要:定期的にデータを送受信するためのメソッド
- 1秒間に数回呼び出される。更新頻度に関係する設定は後述のPhotonNetworkメンバでできる
- PhotonViewのObservedコンポーネントとして割り当てられているスクリプトで呼び出される
- 概要:定期的にデータを送受信するためのメソッド
- 挙動を確認すると以下の感じ。
メッセージの送信頻度を調整しよう
- OnPhotonSerializeView()に関係して、同期頻度ぼ設定は以下のPhotonNetWorkメンバを設定することでできる。
- PhotonNetwork.SendRate:一秒に何度送信するか
- PhotonNetwork.SerializationRate:OnPhotonSerialize()を一秒に何度呼ぶか
- パッと意味がわからなかったが、動作と各設定値がどの処理に影響するのかを図示(以下)するとイメージがつかめた。(デフォルトではSendRate:20, SerializationRate:10になっているが、少なくともSendRateがSerializationRateより小さいと明らかに円滑に同期できないであろうこことがわかる。しかし、SendRate = SerializationRateとしても同期に影響はでなさそうだがダメなんだろうか?)
★必要な場合のみ送信してメッセージ数を節約しよう
Streamにデータが書き込まれている場合のみStreamが相手方の端末に送信されるので、余計な通信を行わないためには、同期したいときのみStreamNext()を実行するようにすればよい、という話。
UnityエディタのPhotonViewのObserve Optionでも監視対象の値が更新された時に同期という設定ができるが、データ型ごとにしきい値を設定するもので変数単位ではないため実用的ではないとのこと。
★座標の同期を独自実装する方法
座標の同期はPhotonTransformViewを使うのが最も簡単だが、同期仕様が微妙だな、と思ったら独自に座標の同期処理を実装することもできる。
例として以下の方法が紹介されていた
- 線形補間
- 推測航法
- PhotonTransformViewClassic
- 3次スプライン補間
PUN2で始めるオンラインゲーム開発入門【その3】での学習
とりあえず弾を発射できるようにしよう
-
やること
- 弾のプレハブ作成
- 弾オブジェクトのスクリプト作成
- Init():弾オブジェクトの位置、速度を設定して初期化
- Update(): 経過時間から弾オブジェクトの位置を更新
- OnBecameInvisible():画面外になったときに自身を削除する
- 弾オブジェクトのスクリプトを弾プレハブへアタッチ
- プレイヤーオブジェクトのスクリプトに弾オブジェクトを発射する処理を追加(以下)
- Update():自身の移動に加え、自身とクリックされた地点の角度から弾を発射
- FireProjectile():弾を発射(位置と速度を与えた弾オブジェクトを生成)
-
つまったところ
-
以下のコードをGamePlayerのスクリプトに追加すると「defaultとかC#7.1以降じゃないとつかえまへん!」というエラーが発生。
=default
を削除することにより対応。
[SerializeField]
private Projectile projectilePrefab = default; // Projectileプレハブの参照- シーンを再生してクリックしても一定の方向へしか弾が発射されない。これはメインカメラの投影方法がデフォルトで[Perspectiveになっているため、``Camera.main.ScreenToWorldPoint()``メソッドがうまく働かない](https://ghoul-life.hatenablog.com/entry/2017/05/16/001743)から。OrthographicにしてやればOK。
-
弾を発射する処理を同期させよう
- 要点
- 弾を端末間で同期するにあたって、これまで学習してきたネットワークオブジェクト化してしまうのも一つの手だが、弾は発射直後の初期位置と方向さえわかっていれば以降の挙動が決まるもの。すなわち定期的に同期を取る必要がないオブジェクトである。
- 無駄な通信をさけるために、ネットワークオブジェクトでなく通常のオブジェクトとして他端末上で生成してもらうのが適当。
RPCを使おう
- RPC(リモートプロシージャコール)という仕組みで他端末上でメソッドを実行できるらしい。
- オプションでRPCを送信する相手を指定できるらしい。
- RpcTarget.All: 全員(自分だけ通信せずソッコー実行)
- RpcTarget.Others: 自分以外
- RpcTarget.AllViaServer: 全員
- 疑問
- どういう通信経路で実行されるの?★
- どういうタイミングで実行されるの?★
- 補間的な処理はされるの?
- サンプルスクリプトで詰まったところ
- nameofでC#6以上じゃないと使えませんって出てきた
→""でメソッド名をくくって対応できたが・・・nameofってそもそも何なのか。
- nameofでC#6以上じゃないと使えませんって出てきた
オブジェクトプールで弾を管理しよう
被弾処理を同期させよう
弾を受ける側が当たり判定を行う
弾を当てる側が当たり判定を行う
サーバー時刻を活用しよう
弾を発射した時刻を送ろう
PUN2で始めるオンラインゲーム開発入門【その4】での学習
PUN2で始めるオンラインゲーム開発入門【その5】での学習
参考
- PUN2で始めるオンラインゲーム開発入門【その1】
- https://connect.unity.com/p/pun2deshi-meruonraingemukai-fa-ru-men-sono4
- 【PUN2】Unityでオンラインマルチプレイを爆速で実装する
- MagicOnion入門
- Photon Cloud or Photon Server?
- CEDEC2019レポート:Unity C# × gRPC × サーバーサイドKotlinによる次世代のサーバー/クライアント通信 ?ハイパフォーマンスな通信基盤の開発とMagicOnionによるリアルタイム通信の実現?
- UnityのPhotonの勉強メモ~オンラインゲームを作りたい~
- Photonを使ったマルチプレイ入門その1
- Unity猫本のサンプルゲームをPhotonでオンライン対戦ゲーム化してみた
- Demos And Tutorials(Photon公式)
- Photon(PUN2)でサーバーに接続してルームを作成する際のメモ
- Unityフォルダ構成のルールについて