Unityでオンラインマルチプレイなゲームを作りたい その8 プレイヤーの同期

[前回の記事](https://qiita.com/koyuki2007/items/20713d4a50ca31f7ed26)でゲーム開始前の準備を行うところまで作りました。
今回からはようやくゲーム部分に取り掛かっていきます。

# 今回からの目標
![scene_diagram＿3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/4230d096-79ba-51d8-8e0c-ed11676c7eec.png)
赤い〇で囲っている部分を実装していきます。
[その3](https://qiita.com/koyuki2007/items/247c13603244020f6bb8)の最終目標で書いてる通り、ローグライク * アクションな2Dのゲームを作っていきます。

## プレイヤーの同期
とりあえず、最初は自分が操作するプレイヤーとその同期を実装していきます。

実装内容としては、

1. プレイヤーオブジェクトの生成
2. 移動の処理の同期
3. 弾の発射と同期
4. パラメータの同期
5. プレイヤーオブジェクトの削除

の5つになります。

### 1.プレイヤーオブジェクトの生成
まず、操作するプレイヤーのプレハブを作っていきます。

Hierarchyで空のオブジェクトを生成し、わかりやすいような名前に変更します。(図ではPlayerという名前にしてます)
![01.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/6d631a5f-5f4d-476c-e0e5-34d9276d523a.jpeg)

作成した空のオブジェクトに同期を行なうための機能を追加します。
###### [MonobitViewコンポーネント](http://www.monobitengine.com/doc/mun/contents/Component/MonobitView.htm)
ネットワーク越しでオブジェクトの通信/同期を行なうために必要な機能です。

###### [MonobitTransformViewコンポーネント](http://www.monobitengine.com/doc/mun/contents/Component/MonobitTransformView.htm)
空のオブジェクトに元からついているTransform(主にPosition, Rotate, Scale)をネットワーク越しで同期を行なってくれるコンポーネントです。
これを追加することで、位置, 向き, 大きさの同期を行なってくれます。

この二つを先ほど作ったオブジェクトにAdd Componentして設定していきます。

![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/b963f3b5-6fba-9f3f-d2a1-326f1ac30a41.jpeg)
こんな感じですね。

次に、追加したMonovitViewコンポーネントの設定を行います。
MonovitViewコンポーネントのAdd Observed Component List Columnボタンを押します。
![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/0938fa5c-d5dd-7cc5-54a1-5484f11354dd.jpeg)

押した後に表示されたところにMonobitTransformViewをアタッチします。
![03.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/eb7102a1-7c73-d154-c49f-3a4dac6889a6.jpeg)
これで下準備が出来たので、このオブジェクトを操作するためのPlayer.csスクリプトを作成します。

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

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        Move();
    }

    /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }
}
```
MUNの機能を利用すために、<code>public class Player : MonobitEngine.MonoBehaviour</code>と継承元をMUNの物に変えています。
後は、とりあえずシンプルな移動操作のみ実装しています。
このスクリプトを先ほどのオブジェクトに追加し、プレハブ化しておいてください。

プレイヤーのプレハブが作成できたので、このプレハブをネットワーク越しに生成してみます。
###### [ネットワーク越しのオブジェクトの生成/MonobitEngine.MonobitNetwork.Instantiate メソッド(1)](http://www.monobitengine.com/doc/mun/contents/FeatureClient/Instantiate.htm#MonobitEngine.MonobitNetwork.Instantiate%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89(1))
この機能を使ってネットワーク越しにプレイヤーオブジェクトを生成させます。

適当に空のオブジェクトを作り、新しくSceneGameというスクリプトを作成し追加します。

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

using MonobitEngine;
public class SceneGame: MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private GameObject m_MinePLayerObj;

    // Start is called before the first frame update
    void Start()
    {
        m_MinePLayerObj = MonobitNetwork.Instantiate("Player", Vector3.zero, Quaternion.identity, 0);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
```
これでネットワーク越しにプレイヤーの生成が行えました。


### 2.移動の処理の同期
続いて、移動の同期処理を実装していきます。
とはいったものの、実はMonobitTransformViewを追加していることにより位置の同期は既に行われるようになっています。
ただ、これだけだと自分が操作しているプレイヤー以外の他のプレイヤーが操作しているはずのオブジェクトまで操作できてしまい、思ったような実装にはなりません。
ということで、自分が操作するプレイヤーのみを操作できるようにPlayer.csに処理を追加します。

###### [オブジェクト所有権チェック/MonobitEngine.MonobitView.isMine プロパティ](http://www.monobitengine.com/doc/mun/contents/FeatureClient/IsMine.htm#MonobitEngine.MonobitView.isMine%20%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)

```c#
monobitView.isMine
```
生成したオブジェクトが自身で生成したものか、他のルームメンバーがネットワーク越しに生成してきたものかを判別することができます。
MonobitViewコンポーネントを追加し、<code>MonobitEngine.MonoBehaviour</code>を継承しているもので利用できます。
これを使用して、自身のみが移動操作でオブジェクトを動かせるように変更します。

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

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();
    }

    /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }
}
```

<code>Updare()</code>内の処理をオブジェクトの所有権が自分にあるとき以外は処理させないようにしました。

これで移動の同期がきちんとできるようになりました。

### 3.弾の発射と同期
弾を発射するタイミングを同期させます。
弾の移動同期については、別途弾の記事で説明するため、今回は省きます。

ということで弾を飛ばす処理を書いていきます。

```c#Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();

        Shot();
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        Debug.Log("Shot Bullet!!!");
    }
}
```

マウスの左クリックが押されている間<code>m_ShotInterval</code>間隔で弾を発射する処理を追加しました。

では、この発射する処理をルームメンバー間で同期させる処理に変更していきます。

###### [RPC(Remote Procedure Call)/送信制御：MonobitEngine.MonobitView.RPC メソッド (1)](http://www.monobitengine.com/doc/mun/contents/FeatureClient/RPC.htm#%E9%80%81%E4%BF%A1%E5%88%B6%E5%BE%A1%EF%BC%9AMonobitEngine.MonobitView.RPC%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%20(1))
この機能を使い、発射タイミングの同期を実装します。

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

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();

        Shot();
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        /// <summary>自分も含めたルームメンバーに弾の発射を行わせる</summary>
        monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);
    }

    /// <summary>弾発射RPC</summary>
    [MunRPC]
    private void RecvShot(Vector3 position)
    {
        Debug.Log("Shot Bullet!!! Position(" + position.x + ", " + position.y + ")");
    }
}
```
<code>Shot()</code>内に記述している<code>monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);</code>でルームメンバーに対して、発射のタイミングを通知しています。
発射のタイミングを受け取ったメンバーは<code>private void RecvShot(Vector3 position)</code>が呼ぶようになっています。

これで、弾の発射を同期することが出来るようになりました。

## 4.パラメータの同期
体力やプレイヤーの状態等、ルームメンバー間で共有しておきたい数値を同期させる処理を作っていきます。
今回は、体力、自身のスコア、状態の3つを同期させるように作ります。

###### [[Tips] RPCメッセージの送信量・頻度の軽減/Tips(2) : ある特定のオブジェクト（prefab）の情報同期が目的である場合、RPCメッセージを使わず、OnMonobitSerializeView() を使う](http://www.monobitengine.com/doc/mun/contents/PerformanceTuning/TipsRPC.htm#Tips(2)%20:%20%E3%81%82%E3%82%8B%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%EF%BC%88prefab%EF%BC%89%E3%81%AE%E6%83%85%E5%A0%B1%E5%90%8C%E6%9C%9F%E3%81%8C%E7%9B%AE%E7%9A%84%E3%81%A7%E3%81%82%E3%82%8B%E5%A0%B4%E5%90%88%E3%80%81RPC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%9A%E3%80%81OnMonobitSerializeView()%20%E3%82%92%E4%BD%BF%E3%81%86)
こちらを参考に、数値の同期を実装していきます。

###### [OnMonobitSerializeView メソッド](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnMonobitSerializeView%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

```c#
public void OnMonobitSerializeView( MonobitEngine.MonobitStream stream, MonobitEngine.MonobitMessageInfo info )
```
この機能をPlayer.cs内で使用し、体力、スコア、状態をルームメンバー間で共有します。

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

using MonobitEngine;

 /// <summary>プレイヤーの状態</summary>
public enum PlayerState
{
    Active    = 0,
    Deactive  = 1,
    Unrivaled = 2,
    Dead      = 3
}

 /// <summary>プレイヤーの制御</summary>
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    /// <summary>プレイヤーの状態</summary>
    private PlayerState m_State;

    /// <summary>体力</summary>
    private int m_HealthPoint;

    /// <summary>体力の上限</summary>
    private int m_MaxHealthPoint = 10;

    /// <summary>スコア</summary>
    private int m_Score;

    // Start is called before the first frame update
    void Start()
    {
        m_State = PlayerState.Deactive;

        m_HealthPoint = m_MaxHealthPoint;
    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        switch (m_State)
       {
           case State.Active:
               Move();
               Shot();

               break;
           case State.Deactive:
               break;

           case State.Unrivaled:
               break;

           case State.Dead:
               break;
           default:
               break;
        }
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        /// <summary>自分も含めたルームメンバーに弾の発射を行わせる</summary>
        monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);
    }

    /// <summary>弾発射RPC</summary>
    [MunRPC]
    private void RecvShot(Vector3 position)
    {
        Debug.Log("Shot Bullet!!! Position(" + position.x + ", " + position.y + ")");
    }

    /// <summary></summary>
    /// <param name="stream">MonobitAnimatorViewの送信データ、または受信データのいずれかを提供するパラメータ</param>
    /// <param name="info">特定のメッセージやRPCの送受信、または更新に関する「送信者、対象オブジェクト、タイムスタンプ」などの情報を保有するパラメータ</param>
    public void OnMonobitSerializeView(MonobitEngine.MonobitStream stream, MonobitEngine.MonobitMessageInfo info)
    {
        if (stream.isWriting)
        {
            stream.Enqueue(m_HealthPoint);
            stream.Enqueue(m_MaxHealthPoint);
            stream.Enqueue(Score);
            stream.Enqueue((int)m_State);
        }
        else
        {
            m_HealthPoint = (int)stream.Dequeue();

            m_MaxHealthPoint = (int)stream.Dequeue();

            m_Score = (int)stream.Dequeue();

            m_State = (State)stream.Dequeue();
         }
     }
}
```
単に体力, スコア, 状態の変数を用意し、<code>OnMonobitSerializeView</code>内でデータの書き込み/読み込みを行っているだけです。
これで数値を変化させても常に情報を共有してくれます。

### 5.プレイヤーオブジェクトの削除
最後に、自身で生成したプレイヤーオブジェクトの後片付けを行います。
とはいえ、ルームを抜けた際に所持者が居なくなったオブジェクトは自動的に削除されるため、自分が操作するプレイヤーなど、ゲーム中に削除を行わないようなものに関しては削除の処理は実装する必要がありません。
ここでは、任意で削除したい、というときに使用する機能を記載しておこうと思います。

###### [ネットワーク越しのオブジェクトの破棄/MonobitEngine.MonobitNetwork.Destroy メソッド(1)](http://www.monobitengine.com/doc/mun/contents/FeatureClient/Destroy.htm#MonobitEngine.MonobitNetwork.Destroy%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89(1))

```c#
MonobitNetwork.Destroy();
```
この関数に削除したいオブジェクトのmonobitViewもしくはGameObjectを引数に渡してあげることで、ネットワーク越しに生成されたものを含めて削除してくれます。

これで、簡単ではありますが、プレイヤーに必要な処理の同期が実装できたかと思います。
次回はプレイヤーが発射する弾の実装を行おうと思います。