Unityアセット AnySync を使ってネットワークを介したゲームオブジェクトの位置のシンクロを簡単に

#はじめに

ネットワークゲームでは、ネットワーク上の各マシン上で共有されているゲームオブジェクトの位置が常にシンクロ（同期）していないと困りますが、それをきっちり作るのに意外と苦労しています、、、。最初はシンクロしているようでも、時間がたつと段々ズレてしまったりして。

そこで便利なのがAnySyncというUnity用アセットです（残念ながら有料。15ドル）

https://assetstore.unity.com/packages/tools/network/anysync-114580

これは、ネットワーク上のマシン間でゲームオブジェクトのPosition, Rotationなどを共有するためのアセットです。UNET、Photonなど幅広い種類のライブラリと一緒に使うことができます。ただ、使用の際には少しコーディングが必要なので、その方法について解説します。なお、今回はLAN内P2P(peer to peer)に限定してお話するのでご注意ください。

#準備

UnityのネットワークライブラリはUNET([2018.4LTSを最後に提供終了予定](https://blogs.unity3d.com/jp/2018/08/02/evolving-multiplayer-games-beyond-unet/))が
最も身近ですが、Izmさんの[こちらの記事](https://speakerdeck.com/izm/unity-networking-libraries-overview2018)を参考にした結果、UNETと近い感覚で使えて動作が安定している [Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321) という無料アセットを使うことにしました。

Unityプロジェクトを作ったら、MirrorとAnySyncをダウンロード、インポートしてください。

#シンクロさせるゲームオブジェクトの設定

今回は、2つのPCで一方にHost,もう一方にClientを実行し、Host側でスペースキーを押すとBallというプレハブがインスタンス化され、それをHost側で動かすとClient側でも動くようにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/63c5b7d6-07fa-3cc0-10b4-f1cfeda06552.png)


まず、メニューからGameObject/3D Object/Sphereを選んでゲームオブジェクトを作り、Ballという名前を付けてください。

次に、BallにNetwork Identityコンポーネントをアタッチします。Server Only,Local Player Authorityのチェックは不要です。更に、Sync Bufferもアタッチします。これには、シンクロさせたい位置情報などが保存されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/96235578-8bbe-b423-e3a7-148dd9f876f7.png)

それでは、シンクロに関する処理のためのスクリプトBallSync.csを準備しましょう。AnySyncのサンプルプロジェクトに付属のものを基にしています。準備できたらBallにアタッチしてください。

```BallSync.cs

using UnityEngine;
using Mirror;

public class BallSync : NetworkBehaviour
{

    private const float MinimumSendInterval = 0.05f; 

    private float _timeSinceLastSync;
    private Vector3 _lastSentPosition;
    private bool _idle;

    private SyncBuffer BallSyncBuffer;

    private void Awake()
    {
        BallSyncBuffer = GetComponent<SyncBuffer>();
    }

    private void Update()
    {
        if (hasAuthority)
        {
            _timeSinceLastSync += Time.deltaTime;
            if (_timeSinceLastSync >= MinimumSendInterval)
            {
                if (_lastSentPosition != transform.position)
                    _idle = false;

                if (!_idle)
                {
                    CmdSync(_timeSinceLastSync, transform.position);
                    if (_lastSentPosition == transform.position)
                        _idle = true;

                    _lastSentPosition = transform.position;
                    _timeSinceLastSync = 0f;
                }
            }
        }
        else
        {
            if (BallSyncBuffer.HasKeyframes)
            {
                BallSyncBuffer.UpdatePlayback(Time.deltaTime);
                transform.position = BallSyncBuffer.Position;
            }
        }
    }

    [Command]
    private void CmdSync(float interpolationTime, Vector2 position)
    {
        BallSyncBuffer.AddKeyframe(interpolationTime, position);
        RpcSync(interpolationTime, position);
    }

    [ClientRpc]
    private void RpcSync(float interpolationTime, Vector2 position)
    {
        if (isLocalPlayer || isServer)
            return;

        BallSyncBuffer.AddKeyframe(interpolationTime, position);

    }
}

```
この中で、[Command]アトリビュートのついているCmdSyncはホスト側で、[ClientRpc]アトリビュートのついているRpcSyncはクライアント側で実行されます。

Update関数内はhasAuthorityによって分岐しています。hasAuthorityがTrue、つまりAuthorityを持っている場合（今回はホスト側）はCmdSyncを実行して現在の位置情報をSync Bufferに送ります。更にその中でRpcSyncを実行して、クライアント側のSyncBufferにも位置情報を送ります。これでホスト、クライアント両方で同じ位置情報がバッファーに保存されます。hasAuthorityがFalseの場合（今回はクライアント側）はSync Bufferに保存されているデータを読み取ってクライアント側のBallの位置として代入しています。

これでBallの準備はできたので、Ballをプロジェクトウィンドウにドラッグして、プレハブ化してください。ヒエラルキー内のBallは消しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/7460bf97-789e-9013-bffe-1f750c41a335.png)


#他のゲームオブジェクトの設定

次に、BallSpawnerという空のゲームオブジェクトを作ってください。Network Identityコンポーネントをアタッチして、Server Onlyにチェックをいれます。これにより、BallSpawnerはホスト側だけで動作するようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/e9bf9a4b-b301-1d59-44bc-090a0c2f70ef.png)


次に、スクリプト BallSpawner.csを作ってBallSpawnerオブジェクトにアタッチしてください。

```BallSpawner.cs
using UnityEngine;
using Mirror;

public class BallSpawner : NetworkBehaviour {

    public GameObject Ball;
	
	void Update () {
		if (Input.GetKeyDown(KeyCode.Space))
        {
                Fire();
        }
	}

    void Fire()
    {
        var go = Instantiate(Ball);
        NetworkServer.Spawn(go);
    }
}
```
インスペクタ上で変数BallにBallプレハブをアサインするのを忘れずに。

これで、スペースキーを押すとホスト側でBallがインスタンス化されて、さらに、NetworkServer.Spawn(go)によりクライアント側でもBallオブジェクトが生成されます。

最後に、NetworkManagerという空のゲームオブジェクトを作ってください。そちらに、Network Managerコンポーネント、Network Manager HUDコンポーネントをアタッチしてください。さらに、Network Managerコンポーネントの中のSpawn Info中のRegistered Spawnable PrefabsにBallプレハブをアサインしてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/63dcb054-b968-0aff-8ba0-00e688a35ec5.png)


#完成

これで完成です。スタンドアロンとしてビルドして、エディターと一緒に実行してみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58503/85813567-3c90-e526-b7ee-888429045b74.png)

この画面になったら、Editor側ではLAN Host, ビルドしたアプリ側ではLAN Clientをクリックしてください。次に、スペースバーを押せばBallが出てきて、シーンビューか何かでBallを移動させれば、Client（スタンドアロンアプリ）側でも同じように移動するはずです。

![AnySync.gif](https://qiita-image-store.s3.amazonaws.com/0/58503/d76947fb-3c66-73eb-ecd0-796e6e0407d9.gif)