Unity + PhotonRealtime でオンラインゲームを作る

# 概要

Unity のオンラインゲームで使われるクラウドエンジンである PhotonRealtime を使ってみる。
ちょっと使ってみた感じは結構良さそうな印象を受けた。

詳しくは[公式ドキュメント](https://doc.photonengine.com/ja-jp/pun/current/demos-and-tutorials/pun-basics-tutorial/intro)を参照。
ただ、公式はかなりふわふわしてるので QiitaやTwitter、英語ブログ等を中心に検索すると良さそう。

Photon でモデルを共有する際、クライアント実行ファイルに予め該当のモデルが存在しないと表示されないので、アバターアップロード等の機能を実装するときは外部からアバターをダウンロードしてくる処理を実装する必要がありそう。

# Photonファミリー

|名称|機能|
|---|---|
|PhotonRealtime| MO とか MMO 向け (VRなら多分これ)|
|PhotonTurnbased|ターン制ゲームの特化版|
|PhotonChat|チャット機能に特化していて、料金も安い|
|PhotonVoice|ボイスチャット機能を提供してくれる|

# サンプル

https://github.com/kawashi/PhotonExample

![サンプルgif](https://i.gyazo.com/93b7e61bcea4379bcc676b687dfacfb6.gif)

https://github.com/kawashi/PhotonAnimatorSyncExample

[![Image from Gyazo](https://i.gyazo.com/1c9d57ee3071f029b44227d9ea2056b3.gif)](https://gyazo.com/1c9d57ee3071f029b44227d9ea2056b3)

# 開発の流れ

1. [PhotonRealtime](https://www.photonengine.com/) の登録
2. SDK ( Photon Unity Networking, 2じゃない方 ) を AssetStore から導入 (Demoはチェックを外す)
3. SDK のセットアップ
4. AppID の入力
5. リージョンの設定 (JP で良い)
6. Auto-Join Lobby と Enable Lobby Stats のチェックを入れる
7. 同期する GameObject に Photon Transform View と Photon View のコンポーネント導入
8. Photon Transform View を設定
9. Photon View の Observed Components に Photon Transform View をセット
10. 同期するオブジェクトをプレハブにする
11. サーバやルームへ接続するスクリプトを記述して、Empty GameObject に追加


基本的には上記の流れになる。
SDK の設定は `Photon Unity Networking/Resources/PhotonServerSettings.asset` から出来る。

同期するオブジェクトの扱いについてはもうちょっと実験が必要そう。
PhotonRealtime は多分 GameObject の座標や回転をオンラインで共有出来るってことだと思う。

# サーバやルームへ接続する流れについて

[ソースコード](https://github.com/kawashi/PhotonExample/blob/master/Assets/Resources/manage.cs) を眺めながら詳しく見ていく。

基本的に `PhotonNetwork` クラスに必要なメソッドが静的に定義されている。
ゲーム時のフローは多分下記の通り。

1. サーバへ接続
2. ロビーへ入出
3. ルームへ入出 (ルームが無ければ作成して入出)
4. 同期オブジェクトの読み込み (同期オブジェクトの操作?)

1 - 3 までのフローは共通で、4 のフローが重要になりそう。
各フローの詳細はソースコード参照。

# アニメーションを同期させるには

Photon でのアニメーション同期は Photon Animator View という Component で実装可能。
Photon では Animator の Parameter を共有出来る仕組みを用意してくれている。
書籍にはコードで実装する方法があるが、こっちの方がコードを Component で隠蔽してて楽。

使い方は下記の通り。

1. 予め Animator を設定しておく
2. 対象の GameObject に Photon Animator View を追加
3. Photon View の Observed Component に Photon Animator View を D&D
4. Photon Animator View の Synchronize Parameters を設定する

また、対象の GameObject には下記のコードを Update メソッドの最初に追加しておくこと。
これを行うことで自分のキャラクターのみ `Update()` の内容を反映出来る。
また、これを行わないとアニメーションが同期されないことがあるので注意。

```csharp
void Update() {
    if (!(photonView.isMine)) return;
    ...
}
```

アニメーションに関するその他の情報は下記リンクや書籍を参照。

- [【PhotonCloud】 PhotonAnimatorViewでAnimatorの同期を行う](https://qiita.com/toRisouP/items/f3811c221a0c2ad4546a)
- [Photon CloudでUnityちゃんを動かそう](https://ameblo.jp/chicktack123/entry-11879894730.html)
- [Photon Unity Networkingを試してみる(Animation)](http://ninataka-japan.hatenablog.com/entry/2015/07/17/200158)
- [PhotonAnimatorView Class Reference](https://doc-api.photonengine.com/ja-jp/pun/current/class_photon_animator_view.html#details)

# ボイスチャットを実装するには

サンプル: https://github.com/kawashi/PhotonVoiceExample

[これ](https://qiita.com/kina-churapps/items/ea45e483f2367c69c006) 通りにやればすぐに出来る。
手順は下記の通り。

1. PhotonVoiceアプリケーションの登録
2. PUN と Photon Voice の導入
3. 音を出す GameObject に Photon Voice Recoder と Photon View を追加

# クライアントにメッセージを送信する

PhotonView を持っている GameObject にメッセージを送信する時は RPC というものを使うと良い。
例えば、当たり判定等はラグで位置情報だけだと上手く同期出来ないので、RPC を使って当たったことを通知する。
RPC は PhotonView を持っている全ての GameObject のスクリプトで送受信可能。

RPC を扱う際、ViewID という物を使うと PhotonView を持っている GameObject を一意に識別出来る。
( ViewID は PhotonNetwork.Instantiate で生成された GameObject に一意に割り振られる。 )

例: 

```csharp
void OnCollisionEnter(Collision other) {
  photonView.RPC("OnCollisionRPCFunction", PhotonTargets.All, photonView.viewID);
}

[PunRPC]
void OnCollisionRPCFunction(int viewID) {
  if ( photoView.viewID == viewID ) {
    Debug.Log("自分と同一のviewIDからRPCメソッドが呼ばれた！");
  }
}
```

PhotonTargets は `Enums.cs` で下記のように定義されてる。

```Enum.cs
/// <summary>Enum of "target" options for RPCs. These define which remote clients get your RPC call. </summary>
/// \ingroup publicApi
public enum PhotonTargets
{
    /// <summary>Sends the RPC to everyone else and executes it immediately on this client. Player who join later will not execute this RPC.</summary>
    All,
    /// <summary>Sends the RPC to everyone else. This client does not execute the RPC. Player who join later will not execute this RPC.</summary>
    Others,
    /// <summary>Sends the RPC to MasterClient only. Careful: The MasterClient might disconnect before it executes the RPC and that might cause dropped RPCs.</summary>
    MasterClient,
    /// <summary>Sends the RPC to everyone else and executes it immediately on this client. New players get the RPC when they join as it's buffered (until this client leaves).</summary>
    AllBuffered,
    /// <summary>Sends the RPC to everyone. This client does not execute the RPC. New players get the RPC when they join as it's buffered (until this client leaves).</summary>
    OthersBuffered,
    /// <summary>Sends the RPC to everyone (including this client) through the server.</summary>
    /// <remarks>
    /// This client executes the RPC like any other when it received it from the server.
    /// Benefit: The server's order of sending the RPCs is the same on all clients.
    /// </remarks>
    AllViaServer,
    /// <summary>Sends the RPC to everyone (including this client) through the server and buffers it for players joining later.</summary>
    /// <remarks>
    /// This client executes the RPC like any other when it received it from the server.
    /// Benefit: The server's order of sending the RPCs is the same on all clients.
    /// </remarks>
    AllBufferedViaServer
}
```

余談だが、恐らく cluster ではこのメッセージ同期に [MQTT](https://ja.wikipedia.org/wiki/MQ_Telemetry_Transport) を利用していると考えられる。

参考URL
- [RPCとRaiseEvent | Photon Engine](https://doc.photonengine.com/ja-jp/pun/current/gameplay/rpcsandraiseevent)
- [Unityでゲームを作っている方は必見！クラスターのUnityエンジニアが語る「クラスターの魅力とVR×Unityのお仕事」 | cluster's Blog](https://www.wantedly.com/companies/cluster/post_articles/132336)

# プロパティを共有する

Photon では全クライアントでプロパティを共有することが出来る。
プロパティの設定は下記の通り。

```csharp
var properties = new ExitGames.Client.Photon.Hashtable();
properties.Add ("プロパティ名", 値);
PhotonNetwork.room.SetCustomProperties (properties);
```

また、プロパティの取得は下記の通り。
(プロパティが存在しない時は `NullReferenceException` が発生する)

```csharp
(型名)PhotonNetwork.room.CustomProperties ["プロパティ名"]
```

参考URL: https://qiita.com/mo4_9/items/f43606875c8b198fb145

# 他プレイヤーの新規接続を検知する

下記メソッドを定義すると、新規プレイヤーを検知出来る。

```csharp
void OnPhotonPlayerConnected(PhotonPlayer player) {
    // ...
}
```


# 注意点

- ログイン直後は全 Object が初期位置に一瞬配置される為、当たり判定が発火する可能性がある
- 相手が生成した Object のスクリプトも動いてるのを忘れてバグを生む可能性がある
- ラグで当たり判定がズレてバグることがある、当たり判定での処理はRPCを使ったほうが良い
- ログイン直後は相手の Object が存在していないので、Object を参照したい時は Invoke で数秒待つ
- マスタークライアントという概念を取り入れる場合、マスターが切断した時の引き継ぎ処理が必要 (公式で仕組みが用意されているっぽい...?)
- `PhotonNetwork.Instantiate` はキャッシュが残り、`PhotonNetwork.Destroy` で削除しないとキャッシュが残る可能性がある ( ただし、削除は所有者が行う必要がある、`MonoBehaviour.Destroy` じゃ駄目 )

# Photon関連URL

- [協力・対戦ゲームを支える Photoの最新情報、 Mecanimにネイティブ対応した PUNの実力](https://japan.unity3d.com/unite/unite2015/files/DAY1_1800_room2_Namiki.pdf)
- [PUNで動的にAnimator込みの同期オブジェクトを同期する - Qiita](https://qiita.com/nekomimi-daimao/items/379e5a35e3049c3a1736)
- [Photonでオブジェクトをスポーンする方法の調査およびIPunPrefabPoolとUniRx.Toolkit.ObjectPoolを使ったオブジェクトプールのサンプル - Qiita](https://qiita.com/su10/items/4bae56bc814c4e6a2bc0)
- [【WIP】これだけでPUNにおける通信は9割わかる - Qiita](https://qiita.com/k0mach1/items/ecfe30165653969a408d)
- [【Unity】僕もPhotonを使いたい #05 ルームからの流れ](https://www.urablog.xyz/entry/2016/09/17/234255#%E3%83%AB%E3%83%BC%E3%83%A0%E3%81%AB%E3%81%84%E3%82%8B%E7%8A%B6%E6%85%8B%E3%81%A7%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E9%83%A1)
- [【PhotonCloud】 PhotonTransformViewでTransformの同期を行う - Qiita](https://qiita.com/toRisouP/items/10af1e0edbfc171e2f62)