UNETメモ の続き
はじめに
Unityのマルチプレイヤー関連の機能(通称UNET)について、NetworkBehaviour
のコールバックが多すぎたので検証と整理(Unity5.6.0f3にて検証)。
処理を書いてるとローカル上のプレイヤーとサーバ上のプレイヤーについては意識するけどローカル上の他プレイヤーのことを忘れがちなので注意が必要。
Unity公式にある以下の図を常に頭に思い浮かべる必要がある。
「Client 1」の「Player 2」のことを忘れがち。コンテキストフラグ(と属性)を正しく使って処理を分ける。
また、ホストは「サーバ」と「クライアント」の両方の振る舞いを持ち、その挙動は「サーバ」と「クライアント」の和集合(×積集合)と理解する。これはフラグ使うときに大事になってくる。
コールバック一覧
公式のマニュアルから引用。サーバ用のコールバックとクライアント用のコールバックがあって、ホストではその両方が実行される。
「プレイヤー」と書いてあるがノンプレイヤーオブジェクトもあるのでその場合呼ばれるのかは要調査。
サーバー上のプレイヤーコールバック
これらのコールバックを取得するために、2 つのゲームインスタンスが必要です。1 つは Server モードで実行するもの、もう 1 つは Client モードで実行するものです。これらのコールバックは Server インスタンス上のプレイヤーオブジェクトに対してのみ呼び出されます。
コールバックを得るには、最初に Server モードのインスタンスを起動し、それから Client インスタンスを開始します。
- OnStartServer
- OnRebuildObservers
- (Start() function is called)
クライアント上のプレイヤーコールバック
これらのコールバックを取得するためには、2 つのゲームインスタンスが必要です。1 つは Server モードで実行するもの、もう 1 つは Client モードで実行するものです。これらのコールバックは Client インスタンス上のプレイヤーオブジェクトに対してのみ呼び出されます。
コールバックを得るには、最初に Server モードのインスタンスを起動し、それから Client インスタンスを開始します。
- OnStartClient
- OnStartLocalPlayer
- OnStartAuthority
- (Start() function is called)
ホスト上のプレイヤーコールバック
これらのコールバックを取得するために、エディターかスタンドアロンビルドとして Host モードで実行する 1 つのゲームインスタンスが必要です。これらのコールバックはプレイヤーオブジェクトに対してのみ呼び出されます。
- OnStartServer
- OnStartClient
- OnRebuildObservers
- OnStartAuthority
- OnStartLocalPlayer
- (Start() function is called)
- OnSetLocalVisibility
OnNetworkDestroy
OnNetworkDestroy コールバックを得るためには、1 つは Server モードで実行し、2 つは Client モードで実行する 3 つのスタンドアロンのゲームインスタンスが必要です。
OnNetworkDestroy コールバックを得る手順は以下のとおりです。
- Server インスタンスを開始します。
- Client インスタンスを開始します。
- 2 番目の Client インスタンスを開始します。
- 両方の Client インスタンスが自動的に Server インスタンスに接続したら、一方の Client インスタンスを止めます。
- OnNetworkDestroy が残りの Client インスタンスで呼び出されますが、Server インスタンス上では呼び出されません。
プレイヤーなどNetworkServer.Spawn(GameObject obj)
でスポーンしてサーバとクライアントで同期しているオブジェクトは、GameObject.Destroy()
ではなくOnNetwork.Destroy()
で削除する。OnNetworkDestroy
コールバックはそのときに呼ばれるコールバック。
マニュアルだとクライアントの接続を切ることで呼ばれるようにしている。
ホスト上でも呼び出されるし、マニュアルと違ってインスタンスを止めてなければNetworkServer.Destroy()
が呼ばれたプレイヤーのオーナーのクライアント上でも呼ばれる。
コンテキストを表すフラグの挙動
フラグ | 説明 |
---|---|
isServer | サーバ(もしくはホスト)上のオブジェクトならtrue 。ただしOnStartServer が呼ばれるまでは必ずfalse 。 |
isClient | クライアント(もしくはホスト)上のオブジェクトならtrue 。ただしOnStartClient が呼ばれるまでは必ずfalse 。 |
isLocalPlayer | クライアント(もしくはホスト)上のオブジェクトで自分が権限を持っているならtrue 。ただしOnStartLocalPlayer が呼ばれるまでは必ずfalse 。 |
「OnXXX
が呼ばれるまでは」というのは厳密に言うとそのメソッド呼び出し時点ですでにtrue
になっているので、メソッド内で判定に利用できる。逆にAwake
内だと必ず全部false
になる。
ホストだと状況的に3つすべてtrue
になる場合もある。
さらにisServer
についてはOnDestroy
が呼ばれる前に必ずfalse
になる(参考スレッド)。この場合はNetworkServer.active
を使ってサーバ上かどうか判断できるとのこと。
プレイヤーが複数人いる場合の順番
ホストとクライアントで大きな違いがある。もっと言うと実行環境でも違う。はっきり言って酷い。
以下の3人のプレイヤーがいるものとする。BとCについてはマッチングで入ってきた順にB,Cとする。
- A:ホスト
- B:クライアント
- C:クライアント
ホストAにおけるコールバックの実行順
プレイヤーごとに直列実行といった感じ。
- Aのコールバック
Awake
OnStartServer
OnStartClient
OnRebuildObservers
OnSetLocalVisibility
OnStartAuthority
OnStartLocalPlayer
Start
- Bのコールバック
Awake
OnStartServer
OnStartClient
OnRebuildObservers
OnSetLocalVisibility
- Cのコールバック
Awake
OnStartServer
OnStartClient
OnRebuildObservers
OnSetLocalVisibility
- クライアントの
Start
BのStart
CのStart
クライアントのStart
については実行する環境によって実行順が異なり、上記はエディタ上での実行順。Macのスタンドアロンビルド版だとBのStart
は「Bのコールバック」の最後に呼ばれる。なんだこれ。
iOSとAndroidは未検証だけどさらに違ってたらやばいので知ってる人いたら教えてください。
クライアントBにおけるコールバックの実行順
ホストとは異なりプレイヤーごとに順番を守った並列実行といった感じ。Bのコールバックには(★)をつけてわかりやすくしました。
- 全ての
Awake
- AのAwake
- BのAwake(★)
- CのAwake
- 全ての
StartClient
- AのOnStartClient
- BのOnStartClient(★)
0. BのOnStartLocalPlayer(★)
0. BのOnStartAuthority(★) - CのOnStartClient
- 全ての
Start
- AのStart
- BのStart(★)
- CのStart
クライアントCにおけるコールバックの実行順
Bと大差ない感じ。Cのコールバックには(★)をつけてわかりやすくしました。OnStartClient
->OnStartLocalPlayer
->OnStartAuthority
は一連の流れで呼び出されるっぽい。
- 全ての
Awake
- AのAwake
- BのAwake
- CのAwake(★)
- 全ての
StartClient
- AのOnStartClient
- BのOnStartClient
- CのOnStartClient(★)
0. CのOnStartLocalPlayer(★)
0. CのOnStartAuthority(★) - 全ての
Start
- AのStart
- BのStart
- CのStart(★)
OnNetworkDestroy
とOnDestroy
以下の順。
OnNetworkDestroy
OnDestroy
順番の保証
検証はしてみたものの、公式ではある一つのNetworkBehaviour
内の呼ばれる順番しか書いてない。なのでおそらく組み合わせについてはランダム扱いなのだと推測。
ちなみにAwake
の呼び出し順は公式でランダムとされてるが、実は指定がなければInstance ID
順に実行される。エディタの再起動で条件付きで振り直されるけど。
何度か試してもマッチングのスロット順によるコールバック呼び出し順は保証されているように見える。見えるだけ?正直よくわからん。
最後に
属性とかフラグとかわけがわからなくなるのでいっそのことサーバとクライアントでクラス分けて疑似通信したほうが良い気がしてきた。となるとHLAPIじゃなくてLLAPIに片足突っ込むことになりそう。ぐぬぬ…。
TODO
- ノンプレイヤーオブジェクトのコールバックは呼ばれるのか調査
- どのコールバックで何をすべきかまとめる
- それぞれのコールバックをUniRxで
IObservable<T>
にする