テラシュールブログ

旧テラシュールウェアブログ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

【Unity】構造体のデータを異なる型に「再解釈」する

 DOTS…特にECSで色々とやっていると、内部のデータは同じなのに型が違うせいで変換しないといけないというケースがあります。

 例えば float のバッファを流し込んでバッチ処理を行うAPIがあった時、ローカルのデータで NativeArray<FloatData> のような構造体の配列は直接流し込めません。しかしメモリ的にはこの二つはほぼ同じものです。そこで Reinterpret<T, U>() でデータを再解釈して NativeArray<FloatData>NativeArray<float>として扱ってみます。

動かない例

 例えば下のようなコードがあったとします。 MyData という構造体が定義されていて、これを ShowFloatLog(NativeArray<float> inputs) で一気に表示したい。当然MyDataは中身がfloatであっても扱いはfloatではないので、下のコードはエラーになります。

using Unity.Collections;
using UnityEngine;

struct MyData { public float Value; }

public class Sample: MonoBehaviour
{
    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        ShowLog(input1 );

        input1.Dispose();
    }

    static void ShowFloatLog(NativeArray<float> inputs)
    {
        foreach (var data in inputs)
            Debug.Log(data);
    }
}

f:id:tsubaki_t1:20191017232117j:plain
型が違うのでエラー

Reinterpret<T, U>()で異なる型へ「再解釈」する

 Reinterpret<T, U>()を使用して、NativeArray<MyData>()をNativeArray()に解釈します。このAPICollectionパッケージに含まれているので、パッケージのインポートが必要です。

f:id:tsubaki_t1:20191017232535j:plain
Entitiesを使うと勝手に入るCollectionパッケージ

 下のように使用します。MyDataの中身がfloatしかないので、floatと解釈することが出来ています。なお、あくまで解釈してるだけなのでDisposeはしてはいけません。参照先のポインタは同じものです。

    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        // MyDataをFloatに再解釈
        var floatInput = input1.Reinterpret<MyData, float>();

        // NativeArray<float>なので動作
        ShowLog(floatInput);

        input1.Dispose();
    }

 この再解釈はポインタ的に、内部データが同じなら色々なデータに解釈できるみたいです。例えばfloat3の配列をfloatに解釈するといった事も可能です。当然、データの長さが異なるので注意が必要ですが、少し面白いと思わなくもないです。

 なお float3 -> float の場合は特に問題は無いですが、 float -> float3 の場合は3で割り切れる数の要素数でないとエラーになります。多い方がベクタライズに有利になりやすいみたいですが、そこのところは注意が必要です。

    void Start()
    {
        var input1 = new NativeArray<float3>(new[] {
            new float3(111, 222, 33),
            new float3(444, 555, 666),
            new float3(777, 888, 999),
        }, Allocator.Temp);

        // float3をfloatに再解釈
        var floatInput = input1.Reinterpret<float3, float>();

        // output : input length 3, reinterpret length 9
        // float3をfloatにするにあたり、配列の長さが変わっている
        Debug.Log($"input length {input1.Length}, reinterpret length {floatInput.Length}");

        // 111 ~ 999 までの要素を個別に出力
        ShowLog(floatInput);

        input1.Dispose();
    }

感想

 NativeArray<Vector3>を返してくる古いAPIと、NativeArray<float3>を要求する新しいAPIの間で苦しんだ時に思い出すと幸せになるかも。

【Unity】ブラシを使ってステージにGameObjectをサクサクっと配置する

f:id:tsubaki_t1:20191016203522j:plain

ステージに木や岩を配置したい

 ステージを作る際、 木や岩をランダムに配置したい という物があります。出来れば地形に沿った形で配置出来ればよいです。規模が小さかったり規則的に配置している場合はGameObjectで配置(Ctrl + Shiftで地形に沿って配置)を使用すればよいのですが、規模が少し大きめだったりランダムに配置したい場合、これを行うのは少し手間が大きいです。
 良い感じに「適当に配置」した後、「適当に向きを回転」し、場合によっては「適当に拡縮」するというのは予想以上に時間が取られます。まぁ楽しいんですが草木や岩、ゴミ等の自然に配置されないオブジェクトに関しては、出来ればブラシ等で簡単に配置したい所でしょう。

f:id:tsubaki_t1:20191016204915j:plain
規則的に並んでいたり、全てがユニークなオブジェクトならプログラムで何とか…

 ブラシで配置となるとTerrainの機能で草木を配置するのはあるんですが、これは後々の微調整が面倒くさいといった問題があります。なので今回はPolyBrushとProBuilderを使用してオブジェクトの配置を行います。

今回やること

 今回は、PolyBrushでステージ上に木や岩といったGameObjectを配置してみます。この量になるとD&Dではしんどいのですが、ブラシ単位だと割と何とかなります。

f:id:tsubaki_t1:20191016205739j:plain
ステージを岩や木で飾る

 なお注意点として、オブジェクトを配置する土台となるメッシュのスケールはscale( 1.1.1)が望ましいです。異なっている場合でも配置は可能ですが、ブラシで選択した場所と異なる場所に配置されることがあります。
 Planeを拡縮で地面を作っている場合、代りにProBuilderで地面を作ってください。

手順1:準備

 オブジェクトを配置していきます。まずはPolyBrushを現在のプロジェクトにインポートして、オブジェクトを配置する前までの操作を行います。

  1. Window>PackageManagerでパッケージマネージャーを開きPolyBrushをインポート
  2. Tools>PolyBrush>PolyBrushWindowでPolyBrushのウィンドウを開く
  3. Scatter Prefabs on Mesh(右から2番目)を選択

f:id:tsubaki_t1:20191016210722j:plain
PolyBrushのセットアップ

手順2:配置するPrefabの登録

 PolyBrushで配置するPrefabを登録します。ここではPrefabの向きやサイズ等の調整も行います。

  1. PrefabPalette-Defaultを選択して、Add Palette...を選択。新しいパレットを作成。
    • ここで指定した名前がパレット名になります。
  2. CurrentPalleteにPrefabをドラッグ&ドロップで登録。
  3. CurrentPalleteにある登録したPrefabを選択。設定を埋める
    ※この作業は登録した全てのPrefabに対して行う
  4. ブラシで配置したいPrefabにチェックを入れる

f:id:tsubaki_t1:20191016212705g:plain
パレットの作成とPrefabの登録

f:id:tsubaki_t1:20191016213415j:plain
Prefabを配置するときの設定

f:id:tsubaki_t1:20191016213618j:plain
配置するPrefabの選択

手順3:ブラシで塗る

 最後にブラシでPrefabを配置していきます。最初に塗るブラシの最大サイズや強度を設定しておくと後々楽なので、先にセットアップしておきます。

  1. Brush Radius Min/MaxでMaxを適当な大きさ(だいたい編集したい範囲の1/3くらいが楽な印象)を設定
  2. Strangthを0.8~0.9辺りに調整

あとは塗っていきます。

  • メッシュ上でクリックしながらドラッグで、ブラシの範囲内にPrefabを配置
  • Ctrlを押しながらドラッグで、範囲内のPrefabを削除(PolyBrushで配置したもののみ)
  • Ctrlを押しながらスクロールで、ブラシの大きさを変更

f:id:tsubaki_t1:20191016215104g:plain
Prefabを塗る

 なお、PrefabScatterSettingsでPrefabを配置するときの調整が出来ます。とりあえずUse PivotとHit Surface is Parentにチェックは入れて、Avoid Overlapは木を生やす時には入れて、それ以外はStrengthで調整するのが良さそうです。

  • Use Pivot:Pivotがちゃんとしてれば配置したPrefabが浮かなくなります。
  • Hit Surface is Parent:配置したオブジェクトの子オブジェクトとしてPrefabを配置します。
  • Avoid Overlap:同じ個所に何度もPrefabを配置しなくなります。例えば木が密集しすぎるのを防ぎます。

f:id:tsubaki_t1:20191016215610j:plain
Prefabを配置するときのオプション

補足

  • これ、配置するのはステージだけに限定する機能ではありません。敵とかを大量に配置する場合、コレで並べてしまうのは割と面白いと思います。その時はAvoid Overlapにチェックを。
  • 配置するのがPrefabなので、LODやスクリプト、ダメージ判定等も設定出来ます。
  • 配置する際、同じ場所に何度もメッシュが配置されるかもしれませんが(通称:ぶっ刺し)、最近の端末ならある程度は気にしなくても大丈夫です。下手にユニークなメッシュを作るより同じオブジェクトを向きを変えつつ大量に配置した方が安い時もあります。
  • 仕組み上、大量にGameObjectを配置します。ある程度は何とかなるかもしれませんが、量が桁違いになるとSubSceneや、Transformをキャッシュしておいて実行時に配置するスクリプトを用意するのが良いかもしれません。オブジェクトが4万個とか超えるなら。
  • 配置するPrefabはVariantで元のPrefabから派生しておくと、コンポーネントをつけたり調整する場合に色々と楽出来ます。
  • 配置するPrefabのマテリアルはInstancingを有効にしましょう。
  • 配置する際にブラシで配置する機能は土台となるオブジェクトにColliderは無くても動作します。移動判定をNavMeshで作る場合にはうれしい?
  • 配置するPrefabを切り替える場合、プレハブにチェックを入れたり外したりするよりPaletteを切り替える方が楽です。Paletteの複製はPaletteのアセットを複製すれば良いです
  • アセットの操作でPaletteを複製した場合、一度Paletteを変更した後に一覧表示されるようになります。
  • PaletteからPrefabを消すにはCtrl + Backspaceです。

関連

一定以上の規模になると、プロシージャルで作った方が良い

learning.unity3d.jp

規模が少ない場合に使えるTips

tsubakit1.hateblo.jp

ProBuilderで地面を作る場合

  1. Window>PackageManagerでパッケージマネージャーを開きProBulderをインポート
  2. Tools>ProBuilder>Editors>Open Shape Editor Menu Itemを選択
  3. Shape SelectorでPlaneを選択し、設定を埋めてBuildをクリック
    • Width:幅
    • Length:奥行
    • Width/length Segments:幅・奥行のメッシュ分割数

f:id:tsubaki_t1:20191016211356j:plain
Open Shape Editor Menu Itemを開く

f:id:tsubaki_t1:20191016211326j:plain
Shape Toolで作るメッシュの大きさを指定

【Unity】LODGroupの設定を一気に変更する

f:id:tsubaki_t1:20191015221051g:plain

LODGroup

 視覚上のポリゴンを削減するという用途でLODGroupという機能は非常にありがたい機能です。これは単純にローポリのモデルに差し替えるというだけではなく、遠距離に要るキャラクターのメッシュを非表示にする(≒アニメーションとスキニングのコストを削減出来る)といった点でもありがたい技術ではあります。

 このLOD Group、モデル切り替えの条件が「画面内におけるモデルの高さ」という条件で、調整には少し苦労するかもしれません。距離ベースのLODと異なり、近づいた時にいきなり巨大なオブジェクトが唐突に表れるという事は無いのですが、どの距離で表示するのかといった事は開発者のノリに依存します。

f:id:tsubaki_t1:20191015222835g:plain
元々の大きさに関係なく、画面の占有率(高さベース)でLODが切り替わる

複数のLODを編集したい

 このLODの仕組みで困るのが、複数のLODを一括変更できないという点です。複数のLODを選択すると、Multi Object Editing not supportと表示され、編集ができません。

 これは少し面倒くさいです。例えばキャラクター等をLODでカリングしようと思ったとき、キャラクター毎にLOD Groupを調整する必要が出てきます。LOD Groupは複数の設定項目を持てる上、数字入力が出来ないので本当に面倒くさいです。

 

f:id:tsubaki_t1:20191015223457j:plain
LODGroupを複数調整する場合、本当に面倒くさい

LODの設定を一気に上書きするコード

 LODの設定はスクリプトから一気に更新してしまいます。例えば下のようなコードで

using UnityEngine;

public class LODGroupSettings : MonoBehaviour
{
    [SerializeField] LODGroup src;

    void Start()
    {
        UpdateLOD();
    }

    [ContextMenu("UpdateLOD")]
    public void UpdateLOD()
    {
        var dst = GetComponent<LODGroup>();
        var srcLOD = src.GetLODs();
        var dstLOD = dst.GetLODs();

        for (int i = 0; i < srcLOD.Length; i++)
        {
            dstLOD[i].screenRelativeTransitionHeight = srcLOD[i].screenRelativeTransitionHeight;
            dstLOD[i].fadeTransitionWidth = srcLOD[i].fadeTransitionWidth;
        }
        dst.SetLODs(dstLOD);
    }
}

src にコピー元となるLODGroup(コピー先と同じLODの数)のPrefabを登録します。これで実行時にLODの設定が差し変わる他、コンテキストメニューからUpdateLODを選択すればLODが切り替わります。

 

f:id:tsubaki_t1:20191015224349j:plain
コピー元を登録して、コンテキストメニューでUpdateLODを選択する場合

注意:複数編集にPresetが使えそうに見えるが、使ってはいけない

 複数編集という点で、使えそうな設定にPresetという機能があります。この機能を使用すると、確かにLODを一気に更新することができますが、実はコレは罠です。フフフ…

 Presetの設定はコンポーネントの設定をコピーしてくれますが、実はLODGroupで表示・非表示を切り替えるメッシュもコピーしてしまいます。つまり、階層下ではない別のLODGroupに所属するRendererを対象にしてしまいます。これでLODが効かなくなったり、レンダラーの登録が全て剥がれたりして大惨事を引き起こすかもしれません。

f:id:tsubaki_t1:20191015225338g:plain
違う階層のLODを参照してしまっている図

感想

 LODの切り替え距離それ自体はProjectSettings>QualityのLOD Biasで大雑把に調整出来ますが、全体で調整されてしまうので、特定の物をテンプレートに従って一気に差し替えられるというアプローチは、便利と感じています。

 まぁ現状モバイルだとポリゴンを削減するという点でのLODは(一画面1200万ポリゴンとか行ってないなら)そこまで気にしなくても良いと思いますが。ポリゴン数以外でも、スキンメッシュやAnimatorの計算を距離で削減出来るのは、ゲームによっては結構アリかなと思います。