Unityでオンラインマルチプレイなゲームを作りたい その12 ダンジョンの生成同期

[前回の記事](https://qiita.com/koyuki2007/items/09e7342badff41fd7b24)で敵の弾の実装が出来ました。
今回はステージである、ランダムダンジョンの生成の同期について書いていこうと思います。

# 今回の目標
ダンジョンの生成の同期を実装する。

## ダンジョンの作り方について
ダンジョン自体の実装については、本記事では省こうと思います。(長くなるのと、ダンジョンの同期の仕方についてを書きたいので...。)
穴掘り法、分割法、等で調べて作成して頂けたらと...。

## ランダムダンジョンの同期の方法
ランダムダンジョンは階層を進めるごとにランダムでマップを形成するため、その都度同期を取ってあげる必要があります。
ですが、マップの縦 * 横分のデータを毎回同期のデータとして送るのはとても非効率です。

### ランダムのシード値を渡す
ゲーム開始前に、ランダムでダンジョンを形成するために使うRandam生成機能のシード値を共有することで、同期時に必要なデータをごっそり削減することが出来ます。
(具体的に言うと<code>UnityEngine.Random.seed</code>に相当するものを共有します。)
上記の点を踏まえてダンジョンの同期を実装していこうかと思います。

### ランダムのシード値を扱う際の注意
[【Unity道場スペシャル 2017京都】乱数完全マスター 京都編](https://www.slideshare.net/UnityTechnologiesJapan/ss-82219762)
上記のサイトのスライドで分かりやすく解説されていますのでこちらを見ていただけたらと思います。
簡単に説明すると、Seed値を共有しても、Random.Range()を呼ぶ回数がバラバラであれば違った結果になってしまう、ということです。
対処法もコード付きで説明してくれているので、参考にしていただければと思います。

## 実装
では実装していきます。
まずは、ゲーム開始前に各クライアントにシード値を共有してあげる処理を作ります。
3つほど実装方法があります。

### 1.RPCによる実装(MonobitTargets.Others)
今まで利用してきたRPCと同様の使い方での実装になります。
ホスト側でルームに入室者があるたびにシード値を乗せたRPCを飛ばす方法です。

### 2.RPCによる実装(MonobitTargets.AllBuffered)
1.と同じくRPCを利用しますが、ターゲットの設定が異なります。
MonobitTargets.AllBufferedは、サーバーにRPCをストックしておく方法で、ストックされている間は、クライアントの入室があった際にストックされているRPCを送信してくれます。
要するに他クライアントの入室時にホストで毎回RPCを呼ぶ必要が無く、一度だけ読んであげるだけでよくなります。

### 3.ルームカスタムパラメータに入れてしまう。
もはやRPCを実装しない方法です。
ルームカスタムパラメータに入れてしまえば、以降ルームに入室してきたクライアントに簡単にシード値を共有できるようになります。

正直どれで実装してもあんまり変わりはないと思うんですが、RPCの検索を減らせるかなと思い、3.の方法で実装しようかと思います。

```c#:SceneGame.cs
using UnityEngine;

using MonobitEngine;

/// <summary>ゲームシーン管理</summary>
public class SceneGame : MonobitEngine.MonoBehaviour
{
    /// <summary>ダンジョンのオブジェクト</summary>
    GameObject m_DungeonGeneratorObj;

    /// <summary>ダンジョン制御</summary>
    DungeonGenerator m_DungeonGenerator ;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inRoom) { return; }

        m_DungeonGeneratorObj = Instantiate(Resources.Load("DungeonGenerator")) as GameObject;

        m_DungeonGenerator = m_DungeonGeneratorObj.GetComponent<DungeonGenerator>();

        // ホストだった場合、ルームカスタムパラメータにランダムのシード値を入れる
        // ホスト以外はルームカスタムパラメータに入っているランダムのシード値を取得する
        if (MonobitNetwork.isHost)
        {
            Hashtable roomCustomParams = MonobitNetwork.room.customParameters;
            roomCustomParams["dungeonSeed"] = DungeonGenerator.Randomize.GetSeed();
            MonobitNetwork.room.SetCustomParameters(roomCustomParams);
        }
        else
        {
            DungeonGenerator.Randomize.SetSeed((int)MonobitNetwork.room.customParameters["dungeonSeed"]);
        }

        m_DungeonGenerator.Generate();            
    }
}
```
これでランダムのシード値を共有できるようにしました。

次にダンジョンの進行時(階段で居りたり上がったりした時)にダンジョンをランダム形成の同期を行なう処理を追加します。

```c#:SceneGame.cs
using UnityEngine;

using MonobitEngine;

/// <summary>ゲームシーン管理</summary>
public class SceneGame : MonobitEngine.MonoBehaviour
{
    /// <summary>ダンジョンのオブジェクト</summary>
    GameObject m_DungeonGeneratorObj;

    /// <summary>ダンジョン制御</summary>
    DungeonGenerator m_DungeonGenerator ;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inRoom) { return; }

        m_DungeonGeneratorObj = Instantiate(Resources.Load("DungeonGenerator")) as GameObject;

        m_DungeonGenerator = m_DungeonGeneratorObj.GetComponent<DungeonGenerator>();

        // ホストだった場合、ルームカスタムパラメータにランダムのシード値を入れる
        // ホスト以外はルームカスタムパラメータに入っているランダムのシード値を取得する
        if (MonobitNetwork.isHost)
        {
            Hashtable roomCustomParams = MonobitNetwork.room.customParameters;
            roomCustomParams["dungeonSeed"] = DungeonGenerator.Randomize.GetSeed();
            MonobitNetwork.room.SetCustomParameters(roomCustomParams);
        }
        else
        {
            m_DungeonGenerator.Randomize.SetSeed((int)MonobitNetwork.room.customParameters["dungeonSeed"]);
        }

        m_DungeonGenerator.Generate();            
    }

    /// <summary>次の階層へ進んだことを通知する</summary>
    public void EnterNextHierarchy()
    {
        monobitView.RPC("RecvEnterNextHierarchy", MonobitTargets.All);
    }

    /// <summary>次の階層を作成する</summary>
    [MunRPC]
    void RecvEnterNextHierarchy()
    {
        // 現在のダンジョンをクリア
        m_DungeonGenerator.Clear();

        // 新しくダンジョンを形成
        m_DungeonGenerator.Generate();
    }
}
```
予めシード値を共有しているので、ダンジョンの形成を各クライアントで呼ばせても同じダンジョンが出来上がります。
<code>EnterNextHierarchy()</code>を階層が進んだタイミングで呼んで使います。

これでダンジョンの形成の同期が実装できました。

次回はプレイヤーや敵の当たり判定について書いていこうかと思います。

# 参考
【Unity道場スペシャル 2017京都】乱数完全マスター 京都編
https://www.slideshare.net/UnityTechnologiesJapan/ss-82219762

[Tips] カスタムパラメータによる送信量・頻度の軽減/Tips(1) : シーン静的オブジェクトで常時共有される情報は、ルームカスタムパラメータで送信した方が速い
http://www.monobitengine.com/doc/mun/contents/PerformanceTuning/TipsCustomParameter.htm#Tips(1)%20:%20%E3%82%B7%E3%83%BC%E3%83%B3%E9%9D%99%E7%9A%84%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A7%E5%B8%B8%E6%99%82%E5%85%B1%E6%9C%89%E3%81%95%E3%82%8C%E3%82%8B%E6%83%85%E5%A0%B1%E3%81%AF%E3%80%81%E3%83%AB%E3%83%BC%E3%83%A0%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7%E9%80%81%E4%BF%A1%E3%81%97%E3%81%9F%E6%96%B9%E3%81%8C%E9%80%9F%E3%81%84