テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】ECSで子Entityの”座標”と”向き”を使いたい

f:id:tsubaki_t1:20191021213839g:plain

このサンプルは Entities.0.1.1およびUnity 2019.2f8を使用しています

子オブジェクトの座標を使いたい

 Unityでオブジェクトを構造化していると、よく親子関係を使用したくなります。

 例えば戦車砲を持つユニットの砲弾は、砲塔の先から出てほしいです。これは砲の向きを回転させたり、戦車自体が動いた場合でも同様です。これをスクリプトで制御しても良いのですが、オブジェクトで発射位置と向きを設定出来れば非常に楽といえます。

f:id:tsubaki_t1:20191021212344j:plain
戦車砲の発射位置をオブジェクトで指定する

LocalToParentを使用する

 LocalToParentは、子オブジェクトのローカル座標をワールド座標に変換したマトリクスを格納しています。このコンポーネントTranslation(ローカル座標)Rotation(ローカル回転)、`Scale(ローカル拡縮)等の情報を元に自動的に更新されるコンポーネントで、基本的には値を更新する必要はありません。

f:id:tsubaki_t1:20191021212857j:plain
LocalToWorldは他のコンポーネントがあると勝手に更新される

 このLocalToWorldに格納されている情報を使用すれば、面倒くさい計算を行わず子オブジェクトのワールド座標が取得できます。

回転しながら弾を撃つ砲台

 トップ絵の動き(回るキューブが指定方向に弾をばらまく)を作ってみます。最初に砲台と回転する物体を作ります。これはConversionWorkflowで作るのでGameObjectで配置していく感じです。線の方向がGameObjectの上方向です。

f:id:tsubaki_t1:20191021213605j:plain
砲台と砲台の向き

 コンポーネントを設定します。配置は大体こんな感じです。オーサリングのコードは割愛しますが、基本的にComponentという名前を付けています。

/// <summary> 前方に移動する </summary>
public struct BulletTag : IComponentData { }

/// <summary> 弾を発射する </summary>
public struct GunTag : IComponentData { }

/// <summary> 回転する </summary>
public struct RotationSpeed : IComponentData
{
    /// <summary> キューブの回転速度(rad)  </summary>
    public float Value;
}

f:id:tsubaki_t1:20191021214522j:plain
Entityにコンポーネントを設定

キューブを回す

 とりあえず砲台となるキューブを回します。Gunの親オブジェクトにでもRotationSpeedを登録してValueを1とかに設定(早すぎると微妙)、後はRotationSystemを記述して勝手に回します。ここはよくある記述です。ただし、親オブジェクトが移動・回転することで子オブジェクトも移動・回転していることが確認できます。

public class RotationSystem : JobComponentSystem
{
    struct RotationJob : IJobForEach<Rotation, RotationSpeed>
    {
        public void Execute(ref Rotation c0, [ReadOnly] ref RotationSpeed c1)
        {
            c0.Value = math.mul(c0.Value, quaternion.RotateZ(c1.Value));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        inputDeps = new RotationJob().Schedule(this, inputDeps);
        return inputDeps;
    }
}

f:id:tsubaki_t1:20191021215143g:plain
キューブが回ると、子オブジェクトの赤い丸も一緒に動く

弾を生成

 次にGUNの位置に弾を生成します。弾の位置と向きは親Entityによって動くのでLocalToWorldを使用することで最終的な位置と向きを取得する感じです。

 位置は LocalToWorld.Position から、向きは math.quaternion(LocalToWorld.Value)でマトリクスを回転に変更すれば良いです。このマトリクスにはスケール等の情報も格納されてるので、親オブジェクトのスケールが(1,1,1)じゃないと変な動きをするかもしれません。

using static Unity.Entities.ComponentType;

public class GunSystem : ComponentSystem
{
    // 弾の発射間隔
    private float interval;
    // PrefabとBulletを持つEntityのQuery
    private EntityQuery bulletPrefab;

    protected override void OnCreate() => bulletPrefab = GetEntityQuery(ReadOnly<Prefab>(), ReadOnly<BulletTag>());

    protected override void OnStartRunning() => interval = 0;

    protected override void OnUpdate()
    {
        var bulletEntity = bulletPrefab.GetSingletonEntity();

        // 一定間隔で射撃を行う
        interval += Time.deltaTime;
        if (interval > 0.1f)
        {
            interval -= 0.1f;

            // GUNを持つEntityから弾を発射する
            Entities.WithAllReadOnly<GunTag>().ForEach((ref LocalToWorld localToWorld) => Shot(bulletEntity, localToWorld));
        }
    }

    void Shot(Entity bulletEntity, in LocalToWorld gunTransform)
    {
        var instance = PostUpdateCommands.Instantiate(bulletEntity);

        // 新しく作成したEntityに座標と向きを登録
        PostUpdateCommands.SetComponent(instance, new Translation { Value = gunTransform.Position });
        PostUpdateCommands.SetComponent(instance, new Rotation() { Value = math.quaternion(gunTransform.Value) });
    }
}

f:id:tsubaki_t1:20191021220145g:plain
GUNの位置に弾を生成する

Entity生成は、EntityCommandBufferがBurstに対応してないのでメインスレッドでやってしまいます。

弾を上方向に飛ばす

 最後に、生成した弾を弾の向きに飛ばします。Entityの向きのfloat3は LocalToWorld.ForwardLocalToWorld.Upで取得できるので、これを足せば良いです。

 この速度もScaleの影響を受けるので注意が必要です。

public class BulletSystem : JobComponentSystem
{
    [RequireComponentTag(typeof(BulletTag))]
    struct MoveBulletJob : IJobForEach<Translation, LocalToWorld>
    {
        public float DeltaTime;
        public void Execute(ref Translation translation, [ReadOnly] ref LocalToWorld localtoworld)
        {
            translation.Value += localtoworld.Up * DeltaTime * 12;
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        inputDeps = new MoveBulletJob { DeltaTime = Time.deltaTime }.Schedule(this, inputDeps);
        return inputDeps;
    }
}

f:id:tsubaki_t1:20191021222140g:plain
生成した弾は、弾にとっての上方向に向かって進む

注意点

  • ハイブリットECS…つまりConvertToEntity.ConvertAndInjectGameObjectを使用している場合、使用できません。普通に子オブジェクトのTransformを使用してください。
  • 親子関係を作る場合、親のオブジェクトを消しても子が残る事があります。LinkedEntityGroupを使用して親を消したら子も消えるようにします。
// 何故か標準で存在しない、LinkedEntityGroupオーサリングの例
// ※複数マテリアル等、複数のEntityを生成するオブジェクトが含まれている場合は正常に動作しません
[DisallowMultipleComponent]
[RequiresEntityConversion]
public class LinkedEntityGroupComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var buffer = dstManager.AddBuffer<LinkedEntityGroup>(entity);

        var children = transform.GetComponentsInChildren<Transform>();
        foreach(var child in children)
        {
            var childEntity = conversionSystem.GetPrimaryEntity(child.gameObject);
            buffer.Add(childEntity);
        }
    }
}

f:id:tsubaki_t1:20191021223738g:plain
親のEntityを消した時に子のEntityも消えるようにする

関連

tsubakit1.hateblo.jp