Unity NetCode のチュートリアルを動かす

# Unity NetCode のチュートリアルを動かす

https://docs.unity3d.com/Packages/com.unity.netcode@0.1/manual/getting-started.html
こちらのチュートリアルを動かそうと思ったら大分躓いたのでメモ。上記チュートリアルページを適時参照しながらお読みください。

## NetCodeとは
https://blogs.unity3d.com/jp/2019/06/13/navigating-unitys-multiplayer-netcode-transition/
プレイヤー数が 80名以上、500 個以上のオブジェクトや AI の同期が可能
サーバーコードによるチート防止が可能
レイテンシ耐性がある

2020 年 4 月 27 日現在：Unity Transport and Reliability はプレビュー版が公開
2021 年第 2 四半期：製品品質の DOTS-Netcode
といった感じ。

## 環境
バージョン：Unity NetCode preview.6 0.1.0 
動作環境： Unity 2019.3.b11以上
適時プロジェクトorシーンを作成してください。

## プロジェクトの設定

Package ManagerからAdvanced->Show preview packagesでプレビューパッケージを表示。以下のパッケージをインストール。
- Entities(NetCodeが対応していないため0.10.0ではなく、0.9.0をインストール。[こちら](https://forum.unity.com/threads/error-after-updating-to-entities-0-1-0-m_systemstoupdate-does-not-exist.880180/#post-5789272)にあるように古いバージョンのみ対応している模様)
- Hybrid Renderer(これも最新ではなく0.4.1をインストール),
- NetCode
- Transport

## シーンを作成
空のGameObjectを作成し、名前をSharedDataに変更して、ConvertToClientServerEntityコンポーネントを追加します。

![world-game-objects.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/45c3d0f2-8ed0-f1a1-04e1-4a5d2e887334.png)

![mixed-world.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/b7af50be-b369-69a9-1cea-7d55146a8ffd.png)

そして、SharedData を選択して 3D Object > Plane を追加します。

![initial-scene.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/1ac5667e-e1d1-8088-facd-4be6cd5daff7.png)

## ghost Prefabの作成
次に、ghost Prefabを作成していきます。まず、SharedData を選択して 3D Object > Cube を追加。
作成したCubeを選択して、ProjectビューのAssetフォルダにドラッグ&ドロップしてプレハブにします。

![cube-prefab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/6ba82f2b-a3e6-8d3c-28fe-7f92dc08c840.png)

次にスプリクト`MovableCubeComponent.cs`を次のように作成して、プレハブに追加します。

```MovableCubeComponent.cs
using Unity.Entities;
using Unity.NetCode;

[GenerateAuthoringComponent]
public struct MovableCubeComponent : IComponentData
{
    [GhostDefaultField]
    public int PlayerId;
}
```

次に、Ghost Authoring Componentを同じく追加し。Update Component Listを押して、チュートリアルの通りに設定しますが、ここで説明とは異なり、Predicting player newtwork id を Predicting player network idを設定しないとエラーが出るので気をつけてください。

![ghost-config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/ab41d01e-3c2e-a5f3-c011-d6f7a9316385.png)

## Ghost Collection の作成

GhostCollectionのセットアップをします。SharedDataを選択して、空のGameObjectを作成。
名前をGhostCollectionに変更して、GhostCollectionAuthoringComponentを追加します。

Update ghost list を押すとGhostsリストに反映されます。
![ghost-collection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/475516/ebd9190a-7c79-234d-2ae3-c1f533e07e0a.png)

## 接続を確立する
次のように`Game.cs`を作成します。

```Game.cs
using Unity.Entities;
using Unity.NetCode;
using Unity.Networking.Transport;
using Unity.Burst;

// Control system updating in the default world
[UpdateInWorld(UpdateInWorld.TargetWorld.Default)]
public class Game : ComponentSystem
{
    // Singleton component to trigger connections once from a control system
    struct InitGameComponent : IComponentData
    {
    }
    protected override void OnCreate()
    {
        RequireSingletonForUpdate<InitGameComponent>();
        // Create singleton, require singleton for update so system runs once
        EntityManager.CreateEntity(typeof(InitGameComponent));
    }

    protected override void OnUpdate()
    {
        // Destroy singleton to prevent system from running again
        EntityManager.DestroyEntity(GetSingletonEntity<InitGameComponent>());
        foreach (var world in World.AllWorlds)
        {
            var network = world.GetExistingSystem<NetworkStreamReceiveSystem>();
            if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
            {
                // Client worlds automatically connect to localhost
                NetworkEndPoint ep = NetworkEndPoint.LoopbackIpv4;
                ep.Port = 7979;
                network.Connect(ep);
            }
            #if UNITY_EDITOR
            else if (world.GetExistingSystem<ServerSimulationSystemGroup>() != null)
            {
                // Server world automatically listens for connections from any host
                NetworkEndPoint ep = NetworkEndPoint.AnyIpv4;
                ep.Port = 7979;
                network.Listen(ep);
            }
            #endif
        }
    }
}
```

RpcCommandは以下のように

```Game.cs
[BurstCompile]
public struct GoInGameRequest : IRpcCommand
{
    public void Deserialize(ref DataStreamReader reader)
    {
    }

    public void Serialize(ref DataStreamWriter writer)
    {
    }
    [BurstCompile]
    private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
    {
        RpcExecutor.ExecuteCreateRequestComponent<GoInGameRequest>(ref parameters);
    }

    static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer =
        new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
    public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
    {
        return InvokeExecuteFunctionPointer;
    }
}
```

次に、RpcCommandRequestSystemを作成します。

```Game.cs
// The system that makes the RPC request component transfer
public class GoInGameRequestSystem : RpcCommandRequestSystem<GoInGameRequest>
{
}
```

次に、`CubeInput.cs`というスクリプトファイルを作成し、次のコードを記述します。ここもチュートリアルページのコードでは動作しないので注意。

```CubeInput.cs
using Unity.NetCode;
using Unity.Networking.Transport;

public struct CubeInput : ICommandData<CubeInput>
{
    public uint Tick => tick;
    public uint tick;
    public int horizontal;
    public int vertical;

    public void Deserialize(uint tick,ref DataStreamReader reader)
    {
        this.tick = tick;
        horizontal = reader.ReadInt();
        vertical = reader.ReadInt();
    }

    public void Serialize(ref DataStreamWriter writer)
    {
        writer.WriteInt(horizontal);
        writer.WriteInt(vertical);
    }

    public void Deserialize(uint tick,ref DataStreamReader reader, CubeInput baseline,
        NetworkCompressionModel compressionModel)
    {
        Deserialize(tick,ref reader);
    }

    public void Serialize(ref DataStreamWriter writer, CubeInput baseline, NetworkCompressionModel compressionModel)
    {
        Serialize(ref writer);
    }
}

public class NetCubeSendCommandSystem : CommandSendSystem<CubeInput>
{
}
public class NetCubeReceiveCommandSystem : CommandReceiveSystem<CubeInput>
{
}
```

次に、`SampleCubeInput.cs`を以下のように作成します。

```SampleCubeInput.cs
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using Unity.Transforms;

[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class SampleCubeInput : ComponentSystem
{
    protected override void OnCreate()
    {
        RequireSingletonForUpdate<NetworkIdComponent>();
        RequireSingletonForUpdate<EnableNetCubeGhostReceiveSystemComponent>();
    }

    protected override void OnUpdate()
    {
        var localInput = GetSingleton<CommandTargetComponent>().targetEntity;
        if (localInput == Entity.Null)
        {
            var localPlayerId = GetSingleton<NetworkIdComponent>().Value;
            Entities.WithNone<CubeInput>().ForEach((Entity ent, ref MovableCubeComponent cube) =>
            {
                if (cube.PlayerId == localPlayerId)
                {
                    PostUpdateCommands.AddBuffer<CubeInput>(ent);
                    PostUpdateCommands.SetComponent(GetSingletonEntity<CommandTargetComponent>(), new CommandTargetComponent {targetEntity = ent});
                }
            });
            return;
        }
        var input = default(CubeInput);
        input.tick = World.GetExistingSystem<ClientSimulationSystemGroup>().ServerTick;
        if (Input.GetKey("a"))
            input.horizontal -= 1;
        if (Input.GetKey("d"))
            input.horizontal += 1;
        if (Input.GetKey("s"))
            input.vertical -= 1;
        if (Input.GetKey("w"))
            input.vertical += 1;
        var inputBuffer = EntityManager.GetBuffer<CubeInput>(localInput);
        inputBuffer.AddCommandData(input);
    }
}

[UpdateInGroup(typeof(GhostPredictionSystemGroup))]
public class MoveCubeSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        var group = World.GetExistingSystem<GhostPredictionSystemGroup>();
        var tick = group.PredictingTick;
        var deltaTime = Time.DeltaTime;
        Entities.ForEach((DynamicBuffer<CubeInput> inputBuffer, ref Translation trans, ref PredictedGhostComponent prediction) =>
        {
            if (!GhostPredictionSystemGroup.ShouldPredict(tick, prediction))
                return;
            CubeInput input;
            inputBuffer.GetDataAtTick(tick, out input);
            if (input.horizontal > 0)
                trans.Value.x += deltaTime;
            if (input.horizontal < 0)
                trans.Value.x -= deltaTime;
            if (input.vertical > 0)
                trans.Value.z += deltaTime;
            if (input.vertical < 0)
                trans.Value.z -= deltaTime;
        });
    }
}

```

ここで、`EnableNetCubeGhostReceiveSystemComponent`は`Enable+プロジェクト名+ GhostReceiveSystemComponent`という名前になっているので注意。

## 最後のステップ

最後に、`Game.cs`に以下を追記します。

```Game.cs
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class GoInGameClientSystem : ComponentSystem
{
    protected override void OnCreate()
    {
    }

    protected override void OnUpdate()
    {
        Entities.WithNone<NetworkStreamInGame>().ForEach((Entity ent, ref NetworkIdComponent id) =>
        {
            PostUpdateCommands.AddComponent<NetworkStreamInGame>(ent);
            var req = PostUpdateCommands.CreateEntity();
            PostUpdateCommands.AddComponent<GoInGameRequest>(req);
            PostUpdateCommands.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = ent });
        });
    }
}

[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public class GoInGameServerSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.WithNone<SendRpcCommandRequestComponent>().ForEach((Entity reqEnt, ref GoInGameRequest req, ref ReceiveRpcCommandRequestComponent reqSrc) =>
        {
            PostUpdateCommands.AddComponent<NetworkStreamInGame>(reqSrc.SourceConnection);
            UnityEngine.Debug.Log(string.Format("Server setting connection {0} to in game", EntityManager.GetComponentData<NetworkIdComponent>(reqSrc.SourceConnection).Value));
            var ghostCollection = GetSingleton<GhostPrefabCollectionComponent>();
            var ghostId = 
NetCubeGhostSerializerCollection.FindGhostType<CubeSnapshotData>();
            var prefab = EntityManager.GetBuffer<GhostPrefabBuffer>(ghostCollection.serverPrefabs)[ghostId].Value;
            // プレイヤー（キューブを作成）
            var player = EntityManager.Instantiate(prefab);

            EntityManager.SetComponentData(player, new MovableCubeComponent { PlayerId = EntityManager.GetComponentData<NetworkIdComponent>(reqSrc.SourceConnection).Value});
            PostUpdateCommands.AddBuffer<CubeInput>(player);

            PostUpdateCommands.SetComponent(reqSrc.SourceConnection, new CommandTargetComponent {targetEntity = player});

            PostUpdateCommands.DestroyEntity(reqEnt);
        });
    }
}
```
ここで、NetCubeGhostSerializerCollectionも、`プロジェクト名+GhostSerializerCollection`になっているので注意。 
あとはMultiplayer > PlayMode Tools から PlayMode Type が Client & Server　になっていることを確認して、再生ボタンを押すと、W,A,S,D,で箱を移動させることが出来ているはずです。

## 参考
とても参考になった記事（中国語）：https://zhuanlan.zhihu.com/p/110986295