LoginSignup
16
6

More than 5 years have passed since last update.

【Unity】PureECSに於けるEntity同士の親子関係の構築方法について

Last updated at Posted at 2018-07-01

追記 : ECSのパッケージがバージョンアップによって破壊的な変更が入り、既存の内容が過去のものとなってしまいました。。
その為に最新のパッケージを前提とした親子関係の構築方法を前半部分に追記します。

▽ 最新(entities 0.0.12-preview.21)

最新のECSに含まれている既存のTransformSystem(EndFrameTransformSystem)を前提とした親子関係の構築方法をメモ。

プロジェクト一式についてはGitHubにアップ済みです。
最新版の方は以下のブランチを御覧ください。

mao-test-h/PureECS-TransformParentSample - entities-preview.21

※注意点として、最新のパッケージと言えどもあくまで現時点での最新の物 & entities自体もpreviewが外れていないので、再度破壊的な変更が入って情報が古くなると言った可能性も十分にあります。それらを踏まえて参考にして頂ければと思います。

■ 実装/動作環境

  • Unity version
    • Unity 2018.3.0f2
  • 依存パッケージ
    • "com.unity.entities": "0.0.12-preview.21"

▼ 実装方法

preview.21の方ではAttachと言うComponentDataが存在するので、そちらを用いることで構築することが可能みたいです。

AttachComponent.cs
namespace Unity.Transforms
{
    /// <summary>
    /// Side-Channel input for attaching child to parent transforms.
    /// To use: Create new entity with Attach component.
    /// On TransformSystem update, Attached and Parent components will
    /// be added to child.
    /// To detach: Remove Attached component from child.
    /// To change parent: Create new entity with Attach component defining new relationship.
    /// </summary>
    [Serializable]
    public struct Attach : IComponentData
    {
        public Entity Parent;
        public Entity Child;
    }

    [UnityEngine.DisallowMultipleComponent]
    public class AttachComponent : ComponentDataWrapper<Attach>
    {
    }
}

後はこちらをアーキタイプに持つEntityを作成し、フィールドのParentChildに紐付けたいEntityを登録すれ事で出来ます。

■ 実装例

今回は以下の様な簡単なサンプルを実装してみました。

test.gif

内容としては、PositionRotationを持つEntityを3つ作成し、それぞれを「親 -> 子 -> 孫」の関係で紐付けておき、Inspector上から移動/回転を出来るようにしたものです。
コードとしては以下の様になります。

Bootstrap.cs
using UnityEngine;

using Unity.Entities;
using Unity.Transforms;
using Unity.Rendering;

public struct ParentTag : IComponentData { }
public struct ChildTag : IComponentData { }
public struct GrandsonTag : IComponentData { }

public sealed class Bootstrap : MonoBehaviour
{
#pragma warning disable 0649

    // ------------------------------
    #region // Defines

    [System.Serializable]
    struct TransformStr
    {
        public Vector3 Position;
        public Vector3 Rotation;
    }

    #endregion // Defines

    // ------------------------------
    #region // Private Members(Editable)

    [Header("【MeshInstanceRenderer】")]
    [SerializeField] MeshInstanceRenderer _parentRenderer;
    [SerializeField] MeshInstanceRenderer _childRenderer;
    [SerializeField] MeshInstanceRenderer _grandsonRenderer;

    [Header("Transform】")]
    [SerializeField] TransformStr _parentTrs;
    [SerializeField] TransformStr _childTrs;
    [SerializeField] TransformStr _grandsonTrs;

    #endregion // Private Members(Editable)

    // ------------------------------
    #region // Private Members

    EntityManager _entityManager = null;
    Entity _parentEntity;
    Entity _childEntity;
    Entity _grandsonEntity;

    #endregion // Private Members

#pragma warning restore 0649


    // ----------------------------------------------------
    #region // Unity Events

    /// <summary>
    /// MonoBehaviour.Start
    /// </summary>
    void Start()
    {
        World.Active = new World("Sample World");
        this._entityManager = World.Active.CreateManager<EntityManager>();
        World.Active.CreateManager(typeof(EndFrameTransformSystem));
        World.Active.CreateManager(typeof(RenderingSystemBootstrap));
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active);

        // ------------------------------------
        // 親アーキタイプ
        var parentArchetype = this._entityManager.CreateArchetype(
            // ComponentType.Create<Attach>(),  // ここに付けたら怒られた
            ComponentType.Create<ParentTag>(),
            ComponentType.Create<Prefab>(),
            ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<LocalToWorld>(),
            ComponentType.Create<MeshInstanceRenderer>());

        // 子アーキタイプ
        var childArchetype = this._entityManager.CreateArchetype(
            // ComponentType.Create<Attach>(),  // ここに付けたら怒られた
            ComponentType.Create<ChildTag>(),
            ComponentType.Create<Prefab>(),
            ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<LocalToWorld>(),
            ComponentType.Create<MeshInstanceRenderer>());

        // 孫アーキタイプ
        var grandsonArchetype = this._entityManager.CreateArchetype(
            ComponentType.Create<GrandsonTag>(),
            ComponentType.Create<Prefab>(),
            ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<LocalToWorld>(),
            ComponentType.Create<MeshInstanceRenderer>());

        // 親子構造構築用アーキタイプ
        var attachArchetype = this._entityManager.CreateArchetype(
            ComponentType.Create<Prefab>(), ComponentType.Create<Attach>());


        // ------------------------------------
        // Create Prefabs
        var parentPrefab = this._entityManager.CreateEntity(parentArchetype);
        var childPrefab = this._entityManager.CreateEntity(childArchetype);
        var grandsonPrefab = this._entityManager.CreateEntity(grandsonArchetype);
        var attachPrefab = this._entityManager.CreateEntity(attachArchetype);
        this._entityManager.SetSharedComponentData(parentPrefab, this._parentRenderer);
        this._entityManager.SetSharedComponentData(childPrefab, this._childRenderer);
        this._entityManager.SetSharedComponentData(grandsonPrefab, this._grandsonRenderer);


        // ------------------------------------
        // Create Entities
        this._parentEntity = this._entityManager.Instantiate(parentPrefab);
        this._childEntity = this._entityManager.Instantiate(childPrefab);
        this._grandsonEntity = this._entityManager.Instantiate(grandsonPrefab);

        // 親子構造の構築(親 -> 子)
        var attachEntity = this._entityManager.Instantiate(attachPrefab);
        this._entityManager.SetComponentData(attachEntity, new Attach
        {
            Parent = this._parentEntity,
            Child = this._childEntity,
        });
        // 親子構造の構築(子 -> 孫)
        attachEntity = this._entityManager.Instantiate(attachPrefab);
        this._entityManager.SetComponentData(attachEntity, new Attach
        {
            Parent = this._childEntity,
            Child = this._grandsonEntity,
        });
    }

    /// <summary>
    /// MonoBehaviour.Update
    /// </summary>
    void Update()
    {
        // 親Entity
        this._entityManager.SetComponentData(this._parentEntity, new Position { Value = this._parentTrs.Position });
        this._entityManager.SetComponentData(this._parentEntity, new Rotation { Value = Quaternion.Euler(this._parentTrs.Rotation) });

        // 子Entity
        this._entityManager.SetComponentData(this._childEntity, new Position { Value = this._childTrs.Position });
        this._entityManager.SetComponentData(this._childEntity, new Rotation { Value = Quaternion.Euler(this._childTrs.Rotation) });

        // 孫Entity
        this._entityManager.SetComponentData(this._grandsonEntity, new Position { Value = this._grandsonTrs.Position });
        this._entityManager.SetComponentData(this._grandsonEntity, new Rotation { Value = Quaternion.Euler(this._grandsonTrs.Rotation) });
    }

    /// <summary>
    /// MonoBehaviour.OnDestroy
    /// </summary>
    void OnDestroy()
    {
        World.DisposeAllWorlds();
    }

    #endregion // Unity Events
}

■ 各種ポイント

ポイントとしては以下の様に親子構造構築用のEntityを生成し、それに対して既存のEntityを登録する所となります。

古いバージョンのやり方としては親や子となるEntityに直接TransformParentと言うComponentDataを持たせて、それに対し関係を紐付けるEntityを登録しておりましたが、それとは違って構築用の第三者となるEntityを生成し、それに登録する形になっているので注意する必要があります。

// 親子構造構築用アーキタイプ
var attachArchetype = this._entityManager.CreateArchetype(
    ComponentType.Create<Prefab>(), ComponentType.Create<Attach>());

// 親子構造構築用EntityのPrefab
var attachPrefab = this._entityManager.CreateEntity(attachArchetype);


// 親子構造の構築(親 -> 子)
var attachEntity = this._entityManager.Instantiate(attachPrefab);
this._entityManager.SetComponentData(attachEntity, new Attach
{
    Parent = this._parentEntity,
    Child = this._childEntity,
});
// 親子構造の構築(子 -> 孫)
attachEntity = this._entityManager.Instantiate(attachPrefab);
this._entityManager.SetComponentData(attachEntity, new Attach
{
    Parent = this._childEntity,
    Child = this._grandsonEntity,
});

※ちなみにコメントにも記載しておりますが、親や子となるEntityのアーキタイプに直接Attachを付けたら怒られました。(詳細までは深追いしておらず。参考までに記載。)

// 親アーキタイプ
var parentArchetype = this._entityManager.CreateArchetype(
    // ComponentType.Create<Attach>(),  // ここに付けたら怒られた
    ComponentType.Create<ParentTag>(),
    ComponentType.Create<Prefab>(),
    ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<LocalToWorld>(),
    ComponentType.Create<MeshInstanceRenderer>());

// 子アーキタイプ
var childArchetype = this._entityManager.CreateArchetype(
    // ComponentType.Create<Attach>(),  // ここに付けたら怒られた
    ComponentType.Create<ChildTag>(),
    ComponentType.Create<Prefab>(),
    ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<LocalToWorld>(),
    ComponentType.Create<MeshInstanceRenderer>());

▽ 古い内容(entities 0.0.12-preview.6)

※冒頭でも説明しておりますが、こちらの内容は過去バージョンのentitiesパッケージを対象にした説明となるので注意。(敢えて過去のパッケージで動かす場合には参考になるかもしれません。)

PureECS実装に関する小ネタです。

前提としてPureECSでTransformに関する操作を行う際には名前空間「Unity.Transfroms」にある「Position」「Rotation」と言った各種ComponentDataを使い、こちらをComponentSystemに渡す形で移動/回転制御などを行う形になるかと思われます。
ただ、こちらの「Position」「Rotation」はワールド座標系での操作自体は出来るものの既存のUnityEngine.Transformで出来たように親子関係を構築した上でローカル座標で操作できるのかと言う点が不明だったので、そこについて少し調べてみたお話となります。
結論から言えばそれと思われるやり方を見つけたので備忘録序に内容をメモ。

  • ※以下、注意点です。
    • この記事中ではECSについての基礎的な部分についての説明は致しません。ECSの基礎的と思われる部分については以前書いた記事にて検証/解説、参考リンク等を載せてありますので宜しければこちらを御覧ください。
    • ECS自体についてもまだまだPreviewな状態であり、将来的には実装が変わってくる可能性もあります。その点を踏まえて参考にして頂ければと思います。

プロジェクト一式についてはGitHubにアップしているので必要に応じて御覧ください。
mao-test-h/PureECS-TransformParentSample

■ 実装/動作環境

  • Unity version
    • Unity 2018.1.5f1
  • 依存パッケージ
    • "com.unity.entities": "0.0.12-preview.6"
      • ※この記事自体はこちらのパッケージをベースに記載しております。プロジェクトをcloneした上でパッケージのバージョンを下手にいじると動作に影響が出る可能性があるのでご了承下さい。
        • ※何故この注釈を書いたかと言うと、最近Entitiesパッケージの一部で破壊的変更が入っているのを確認しました。。変更内容としては行列(float4x4)が行優先から列優先になったり、一部フィールド名が変わると言った変更が入っているように見受けられました。(まぁpreviewなので若干仕方ない所はある...)

▼ 実装方法

実装方法としては「Unity.Transforms.TransformParent」と言うComponentDataを用いることで設定出来るみたいです。
ただ、これを入れるだけではローカル座標での移動/回転と言った制御が行えないので、必要に応じて「Unity.Transfroms.LocalPosition」「Unity.Transforms.LocalRotation」と言ったローカル座標向けのComponentDataも合わせて渡す必要があります。

要件を纏めると以下の様になります。

  • ComponentDataのTransformParentを設定
  • ComponentDataのLocalPosition/LocalRotationを設定
    • ※但しこれらを設定するとワールド座標系の制御で使っていたPosition/Rotationには制限が掛かるように思われました。詳細は後述。

■ 実装例

今回実装したサンプルとして、親子関係を持つEntityを生成した上で各Entity自体のWorld/LocalのPosition/RotationをInspector上から設定できる物を用意しました。
以下のコードは生成/制御周りの所を一部引用した物となります。

肝となる点としてはSetComponentDataでTransformParentを設定している箇所で、こちらのValueに親となるEntiyを渡すことで親子付けを行うことが出来ます。
今回の例の方では「先に生成したEntityを次に生成するEntityの親として設定する」ことで以下のような構造となるように実装しております。

[親(Root)]
    L [子1]
        L [子2]
Sample.cs
// Entity情報
[System.Serializable]
public class EntityData
{
    [System.Serializable]
    public struct TransformData
    {
        public Vector3 WorldPosition;
        public Vector3 LocalPosition;
        [SerializeField] Vector3 WorldRotationEuler;
        [SerializeField] Vector3 LocalRotationEuler;
        public Quaternion WorldRotation { get { return Quaternion.Euler(this.WorldRotationEuler); } }
        public Quaternion LocalRotation { get { return Quaternion.Euler(this.LocalRotationEuler); } }
    }
    // Transform関連のComponentDataに渡す情報
    public TransformData TransformDataInstance;
    // MeshInstanceRendererで設定するMaterial
    public Material Material;
    // Entityの実態
    public Entity Entity;
}

// 表示するMesh
[SerializeField] Mesh _mesh;

// Entity情報
[SerializeField] EntityData[] _entityData;

// EntityManagerの参照
EntityManager _entityManager;


void Start()
{
    this._entityManager = World.Active.GetOrCreateManager<EntityManager>();

    // 親(Root)Entity アーキタイプ
    var rootArchetype = this._entityManager.CreateArchetype(
        typeof(Position),
        typeof(LocalPosition),
        typeof(Rotation),
        typeof(LocalRotation),
        typeof(TransformMatrix));

    // 子Entity アーキタイプ
    var childArchetype = this._entityManager.CreateArchetype(
        typeof(Position),
        typeof(LocalPosition),
        typeof(Rotation),
        typeof(LocalRotation),
        typeof(TransformParent),
        typeof(TransformMatrix));

    Entity parent = Entity.Null;
    for (int i = 0; i < this._entityData.Length; ++i)
    {
        var data = this._entityData[i];
        bool isRoot = (i == 0);

        // Entityの生成
        var archetype = isRoot ? rootArchetype : childArchetype;
        var entity = this._entityManager.CreateEntity(archetype);
        data.Entity = entity;

        // Transformの初期化
        this._entityManager.SetComponentData(entity, new Position { Value = Vector3.zero });
        this._entityManager.SetComponentData(entity, new LocalPosition { Value = Vector3.zero });
        this._entityManager.SetComponentData(entity, new Rotation { Value = quaternion.identity });
        this._entityManager.SetComponentData(entity, new LocalRotation { Value = quaternion.identity });

        // ルート以外は以前に生成されたEntityを親として割り当てる
        if (!isRoot)
        {
            this._entityManager.SetComponentData(entity, new TransformParent { Value = parent });
        }
        parent = entity;

        // MeshInstanceRendererの設定
        var look = this.CreateMeshInstanceRenderer(data.Material);
        this._entityManager.AddSharedComponentData(entity, look);
    }
}

void Update()
{
    foreach (var data in this._entityData)
    {
        var entity = data.Entity;
        var trs = data.TransformDataInstance;
        // Inspector上から設定した座標情報をComponentDataに渡して動かす
        this._entityManager.SetComponentData(entity, new Position { Value = trs.WorldPosition });
        this._entityManager.SetComponentData(entity, new LocalPosition { Value = trs.LocalPosition });
        this._entityManager.SetComponentData(entity, new Rotation { Value = trs.WorldRotation });
        this._entityManager.SetComponentData(entity, new LocalRotation { Value = trs.LocalRotation });
    }
}

以下のGifが実行画面です。
親(Root)が赤色のCube、子1が青色のCube、子2が緑色のCubeとなっております。

sample.gif


■ 子Entityに掛かる制限について

「ワールド座標系の制御で使っていたPosition/Rotationには制限が掛かるように思われる」と上述しましたが、こちらについて補足していきます。
まず症状の方を具体的に説明すると、今回のサンプルでは「Inspector上から子EntityのPosition/Rotationを設定しても反映されない」と言う現象が確認できました。

こちらについて調べてみるためにEntityパッケージ内にあるLocalPosition/LocalRotationのソースを見た所、コメントには以下のようにありました。
※以下はLocalPositionから引用。LocalRotationの方も似たような感じだったので割愛。

User specified position in local space.
1. If a TransformParent exists, the object to world matrix will be determined
by the parent's transform. Otherwise, it will be a Unit matrix.
2. If a TransformMatrix exists, the calculated world position will be stored as the translation in that matrix.
3. If a Position exists, the calculated world position will be stored in that component data.
4. If TransformParent refers to the entity this component is associated with, the calculated world position will be used as the translation in the object to world matrix for the entity associated with this component, regardless of if it is stored in a TransformMatrix.

読み取れる意訳としては「最終的に算出されたワールド座標の位置はTransformMatrix及びPositionに格納される」と言った内容に見受けられ、Inspector上から設定した値が反映されない原因としてはまさにこちらの仕様が影響している結果になるかと思われます。
※Inspectorで値を設定してもTransform関連のComponentSystemに値を上書きされるため。逆にTransform関連のComponentSystemの実行順を上手い具合に制御してやればひょっとしたら上書きのタイミングをずらして反映する事が出来るかもしれないが...今回は未検証。

後は実際にEntity DebuggerでTransformMatrix及びPositionの内部的な値を確認した所、Inspectorから設定された値ではなくLocal座標系から変換された値の方が入っている事も確認できました。

■ 追記. 親Entityと子Entityに必要なComponentDataについて

上述しているサンプルコードのアーキタイプの定義では、以下の様に親と子ともにPosition/Rotation/LocalPosition/LocalRotationと言ったワールド座標系とローカル座標系の操作に使うComponentDataを持っておりましたが、実際には親の方はローカル座標系のComponentDataが不要であり、子の方はワールド系のComponentDataが無くても動作するみたいです。

// ルートEntity アーキタイプ
var rootArchetype = this._entityManager.CreateArchetype(
    typeof(Position),
    typeof(LocalPosition),
    typeof(Rotation),
    typeof(LocalRotation),
    typeof(TransformMatrix));

// 子Entity アーキタイプ
var childArchetype = this._entityManager.CreateArchetype(
    typeof(Position),
    typeof(LocalPosition),
    typeof(Rotation),
    typeof(LocalRotation),
    typeof(TransformParent),
    typeof(TransformMatrix));

書き直すとこんな感じです。

// 親(Root)Entity アーキタイプ
var rootArchetype = this._entityManager.CreateArchetype(
    typeof(Position),
    typeof(Rotation),
    typeof(TransformMatrix));

// 子Entity アーキタイプ
var childArchetype = this._entityManager.CreateArchetype(
    typeof(LocalPosition),
    typeof(LocalRotation),
    typeof(TransformParent),
    typeof(TransformMatrix));

子Entityに関する補足として、LocalPositionのソースのコメントを参照すると↓の様に記載されており、子にワールド座標系のComponentDataを持たせた際の違いとしては最終的な計算結果がComponentDataに入るかどうかぐらいのように思えます。(ComponentDataに入る利点としてはEntityDebuggerから現在の値を参照できる様になる)
※一応Entitiesパッケージ内にあるTransformSystem.csの該当箇所を確認してみたのですが、確かにその様な挙動になっているように見受けられました。

  1. If a Position exists, the calculated world position will be stored in that component data.
16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6