Unity Entitiesが正式リリースされたのでマルチプレイチュートリアルを弄ってみる

Unity 2022 LTSのリリースにあわせて、長らく開発中だったECSがEntitiesパッケージとしてついに正式リリースされました。

正式版のEntitiesに対応したNetCode for Entitiesもあわせてリリースされており、今回はそちらが本題です。

公開されているチュートリアルを触って理解を深めてみました。

Entitiesの基礎

リリースに併せて公開された以下の動画が、基本的な概念を非常に分かり易く説明してくれています。

Entity, Component, Systemが何を指しているのか理解出来るので、ゼロから触ってみる人は視聴をお勧めします。

www.youtube.com

以前からECSではPure ECSとHybrid ECSという枠組みが用意されていたのですが、現在ではGameObjectベースで作成したオーサリングデータを、C#のソースジェネレーター機能及びBake処理によってコンパイル時にPure ECS相当の構造に変換する仕組みが実装されているようです。自分でPure ECSを書くこともできますが、以前はあった実行時パフォーマンスの差も無くなっているようなので、普段はオーサリングデータを作り、どうしても痒いところに手が届かないときに手を出すのがよさそうな印象です。

NetCode for Entitiesのサンプルを実装してみる(チュートリアル)

まずは公式マニュアルに載っているNetworked Cubeサンプルを実装してみました。

記事作成時点でのバージョンは以下の通りです。

  • Unity 2022.3.6f1
  • Entities 1.0.11
  • Entities Graphics 1.0.11
  • Netcode for Entities 1.0.12

チュートリアルの範囲については基本的に手順通り実装しただけなので、詳細は省きます。

ただし、Bakeメソッドを実装する際、AddComponentの引数にEntityが必要になっているので、そこだけ修正を行いました。

  • CubeAuthoring.cs
        public override void Bake(CubeAuthoring authoring)
        {
            Cube component = default(Cube);
-            AddComponent(component);
+            var entity = GetEntity(TransformUsageFlags.None);
+            AddComponent(entity, component);
        }
  • CubeSpawnerAuthoring.cs
        public override void Bake(CubeSpawnerAuthoring authoring)
        {
            CubeSpawner component = default(CubeSpawner);
            component.Cube = GetEntity(authoring.Cube);
-            AddComponent(component);
+            var entity = GetEntity(TransformUsageFlags.None);
+            AddComponent(entity, component);
        }

動作確認

サンプルを実装後、Playボタンを押すと、サーバーに接続してキューブが生成されます。このキューブは矢印キーで前後左右に移動させることができます。

また、UnityのメニューよりMultiplayer/Window: PlayMode Toolsを選び、開いたウィンドウで'Num Thin Clients'の値を弄ると、他のクライアントの接続がシミュレートされてキューブが生成されます。

スポーン位置を動かしてみる

このままではキューブのスポーン位置が被っているので、ずらしてみます。

GoInGameRequest.cs内のOnUpdateに以降のコードを追加します。

まずは生成位置をランダムにずらすため、乱数生成オブジェクトを作成します。

var random = new Unity.Mathematics.Random((uint)(SystemAPI.Time.ElapsedTime * 1000));

次に、foreach内で生成したEntitiyに、以下のようにランダムな初期座標を与えます。

commandBuffer.SetComponent(player,
    new LocalTransform
    {
        Position = random.NextFloat3(new float3(-5, 0, -5), new float3(5, 0, 5)),
        Rotation = quaternion.identity,
        Scale = 1
    });

これでキューブが程々の範囲に散らばって生成されるようになりました。

シンクライアントのダミー入力を追加してみる

※シミュレートされた他のクライアントの事をシンクライアントと呼びます。

生成されたキューブ達が動かないのは寂しいので、シンクライアントからダミーの入力を与えて、キューブを動かしてみます。

シンクライアントで生成された入力情報を他のクライアントに渡すためには、コード生成で自動生成されたInputBufferDataコンポーネントをダミー入力用のエンティティに追加する必要があります。

自動生成されたコードはメニューよりMultiplayer/Open Source Generated Folderを選択することで開くフォルダ内に格納されているので、それを参考にして以下のようにダミー入力を与えるシステムを作成します。

using Unity.Entities;
using Unity.NetCode;

[WorldSystemFilter(WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct ThinClientInputSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<NetworkId>();
    }

    public void OnUpdate(ref SystemState state)
    {
        if (SystemAPI.TryGetSingleton<CommandTarget>(out var commandTarget) &&
            commandTarget.targetEntity == Entity.Null)
            CreateThinClientPlayer(ref state);

        foreach (var input in SystemAPI.Query<RefRW<CubeInput>>())
        {
            input.ValueRW.Horizontal = SystemAPI.Time.ElapsedTime % 2 > 1 ? 1 : -1;
            input.ValueRW.Vertical = (SystemAPI.Time.ElapsedTime + 0.5d) % 2 > 1 ? 1 : -1;
        }
    }

    private void CreateThinClientPlayer(ref SystemState state)
    {
        var ent = state.EntityManager.CreateEntity();
        state.EntityManager.AddComponent<CubeInput>(ent);

        var connectionId = SystemAPI.GetSingleton<NetworkId>().Value;
        state.EntityManager.AddComponentData(ent, new GhostOwner { NetworkId = connectionId });

        state.EntityManager.AddComponent<Assembly_CSharp.Generated.CubeInputInputBufferData>(ent);

        SystemAPI.SetSingleton(new CommandTarget { targetEntity = ent });
    }
}

上記のサンプルコードでは生成された型はAssembly_CSharp.Generated.CubeInputInputBufferDataですが、これは元となるCubeInput型の名前空間の定義やアセンブリ定義によって変化します。

また、自動生成されたコードはコードエディタ上(VisualStudio, Rider等)では認識されず、エラーが表示されますが、Unity Editor上ではエラーは発生しません。以下のように表示されますが正常です。

上記のシステムを作成後、生成したキューブの方にCommandTargetを付けてやることで、シンクライアント内でシミュレートされた入力がクライアントに同期されます。

commandBuffer.SetComponent(reqSrc.ValueRO.SourceConnection, new CommandTarget { targetEntity = player });

まとめ

もっと色々サンプルを改造したかったのですが、シンクライアント周りの調査、実装に予想以上に手間取ってしまったので、次の機会に移そうと思います……。

Entities及びNetCode for Entitiesは、以前のバージョンに比べるとかなり機能としては拡充されていると感じる一方で、いまだドキュメントが充実しているとは言い難いです。今回のマルチプレイサンプルの改造も、GitHub上のサンプルリポジトリやフォーラムを漁ってようやく実装ができました。最初に紹介した動画のように、Unity公式も情報を出していく姿勢は見せているので、これからの充実に期待しつつ、この記事もNetCode for Entitiesに興味を持った方への一助となれば幸いです。

参考リンク

シンクライアントのダミー入力の実装はフォーラム上のスレッドを参考にしました。

forum.unity.com

また、Networked Cubeを含めて、Entitiesの実装サンプルは以下のリポジトリにまとまっています。

github.com