UNETのNetworkBehaviourクラスのコールバックとコンテキストフラグの罠

[UNETメモ](http://qiita.com/su10/items/516ddd4a08429be9c42b) の続き

# はじめに
Unityのマルチプレイヤー関連の機能（通称UNET）について、`NetworkBehaviour`のコールバックが多すぎたので検証と整理（Unity5.6.0f3にて検証）。

処理を書いてるとローカル上のプレイヤーとサーバ上のプレイヤーについては意識するけどローカル上の他プレイヤーのことを忘れがちなので注意が必要。

Unity公式にある以下の図を常に頭に思い浮かべる必要がある。

![](https://docs.unity3d.com/jp/540/uploads/Main/NetworkLocalPlayers.png)

「Client 1」の「Player 2」のことを忘れがち。コンテキストフラグ（と属性）を正しく使って処理を分ける。

また、ホストは「サーバ」と「クライアント」の両方の振る舞いを持ち、その挙動は「サーバ」と「クライアント」の和集合（×積集合）と理解する。これはフラグ使うときに大事になってくる。

# コールバック一覧
[公式のマニュアル](https://docs.unity3d.com/jp/540/Manual/NetworkBehaviourCallbacks.html)から引用。サーバ用のコールバックとクライアント用のコールバックがあって、ホストではその両方が実行される。

「プレイヤー」と書いてあるがノンプレイヤーオブジェクトもあるのでその場合呼ばれるのかは要調査。

## サーバー上のプレイヤーコールバック
>
これらのコールバックを取得するために、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 コールバックを得る手順は以下のとおりです。
>
0. Server インスタンスを開始します。
0. Client インスタンスを開始します。
0. 2 番目の Client インスタンスを開始します。
0. 両方の Client インスタンスが自動的に Server インスタンスに接続したら、一方の Client インスタンスを止めます。
0. 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`になる（[参考スレッド](https://forum.unity3d.com/threads/ondestroy-onnetworkdestroy-isserver.335620/)）。この場合は`NetworkServer.active`を使ってサーバ上かどうか判断できるとのこと。

# プレイヤーが複数人いる場合の順番
ホストとクライアントで大きな違いがある。もっと言うと実行環境でも違う。はっきり言って酷い。

以下の３人のプレイヤーがいるものとする。BとCについてはマッチングで入ってきた順にB,Cとする。

* A：ホスト
* B：クライアント
* C：クライアント

## ホストAにおけるコールバックの実行順
プレイヤーごとに直列実行といった感じ。

0. Aのコールバック
  0. `Awake`
  0. `OnStartServer`
  0. `OnStartClient`
  0. `OnRebuildObservers`
  0. `OnSetLocalVisibility`
  0. `OnStartAuthority`
  0. `OnStartLocalPlayer`
  0. `Start`
0. Bのコールバック
  0. `Awake`
  0. `OnStartServer`
  0. `OnStartClient`
  0. `OnRebuildObservers`
  0. `OnSetLocalVisibility`
0. Cのコールバック
  0. `Awake`
  0. `OnStartServer`
  0. `OnStartClient`
  0. `OnRebuildObservers`
  0. `OnSetLocalVisibility`
0. クライアントの`Start`
  0. `BのStart`
  0. `CのStart`

クライアントの`Start`については実行する環境によって実行順が異なり、上記はエディタ上での実行順。Macのスタンドアロンビルド版だとBの`Start`は「Bのコールバック」の最後に呼ばれる。なんだこれ。

iOSとAndroidは未検証だけどさらに違ってたらやばいので知ってる人いたら教えてください。

## クライアントBにおけるコールバックの実行順
ホストとは異なりプレイヤーごとに順番を守った並列実行といった感じ。Bのコールバックには（★）をつけてわかりやすくしました。

0. 全ての`Awake`
  0. AのAwake
  0. BのAwake（★）
  0. CのAwake
0. 全ての`StartClient`
  0. AのOnStartClient
  0. BのOnStartClient（★）
     0. BのOnStartLocalPlayer（★）
     0. BのOnStartAuthority（★）
  0. CのOnStartClient
0. 全ての`Start`
  0. AのStart
  0. BのStart（★）
  0. CのStart

## クライアントCにおけるコールバックの実行順
Bと大差ない感じ。Cのコールバックには（★）をつけてわかりやすくしました。`OnStartClient`->`OnStartLocalPlayer`->`OnStartAuthority`は一連の流れで呼び出されるっぽい。

0. 全ての`Awake`
  0. AのAwake
  0. BのAwake
  0. CのAwake（★）
0. 全ての`StartClient`
  0. AのOnStartClient
  0. BのOnStartClient
  0. CのOnStartClient（★）
     0. CのOnStartLocalPlayer（★）
     0. CのOnStartAuthority（★）
0. 全ての`Start`
  0. AのStart
  0. BのStart
  0. CのStart（★）

# `OnNetworkDestroy`と`OnDestroy`
以下の順。

0. `OnNetworkDestroy`
0. `OnDestroy`

# 順番の保証
検証はしてみたものの、公式ではある一つの`NetworkBehaviour`内の呼ばれる順番しか書いてない。なのでおそらく組み合わせについてはランダム扱いなのだと推測。

ちなみに`Awake`の呼び出し順は公式でランダムとされてるが、実は指定がなければ`Instance ID`順に実行される。エディタの再起動で条件付きで振り直されるけど。

何度か試してもマッチングのスロット順によるコールバック呼び出し順は保証されているように見える。見えるだけ？正直よくわからん。

# 最後に
属性とかフラグとかわけがわからなくなるのでいっそのことサーバとクライアントでクラス分けて疑似通信したほうが良い気がしてきた。となるとHLAPIじゃなくてLLAPIに片足突っ込むことになりそう。ぐぬぬ…。

## TODO
* ノンプレイヤーオブジェクトのコールバックは呼ばれるのか調査
* どのコールバックで何をすべきかまとめる
* それぞれのコールバックをUniRxで`IObservable<T>`にする