テラシュールブログ

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

【Unity】ECSでもNavMeshを使って移動範囲を限定したい

f:id:tsubaki_t1:20191026222901g:plain

ECSでNavMeshを使いたい

 先日行われた1WeekGameJamの実況にてユニティちゃんが走り回るゲームがあり、それを見て何となくECS(SubScene)でもNavMeshによる移動制限を使用したいと思ったので、やり方を考えてみました。

 考え的には大昔に書いたマップから落ちないようにステージ上を歩かせる楽な方法 - テラシュールブログと同じ考えです。このアプローチはそうそうコリジョン抜けによる落下や貫通が無く、またPhysicsを使用しなくても良いので計算数が少ないとかなり良いです(ジャンプが無ければ)

tsubakit1.hateblo.jp

SubSceneでNavMeshをロードできるようにする

 最初にNavMeshをSubSceneでもロード出来るようにすることを考えてみます。

 NavMeshのベイクはNavMeshSurfaceで行います。これはエディターの機能でベイク(シーンに紐づく)場合、データの更新が面倒くさいという理由があります。さてNavMeshSurfaceはECSへの変換コードを持っていないので、ECSからNavMeshをロードできるようにConversionSystemを用意してやります。

f:id:tsubaki_t1:20191116105908j:plain
NavMeshのベイク

 NavMeshSurfaceを変換し、SharedComponentDataとして保存するコードです。SharedComponentDataProxyを使用しているので、ファイル名はNavMeshSurfaceComponent.csとする必要があります。内容はNavMeshDataをNavMeshSurfaceData にコピーしているだけです。

using UnityEngine.AI;
using Unity.Entities;
using System;
[assembly: RegisterGenericComponentType(typeof(NavMeshSurfaceData))]

// Proxy
[System.Serializable]
public class NavMeshSurfaceComponent : SharedComponentDataProxy<NavMeshSurfaceData> { }

// データを保持する実体
[System.Serializable]
public struct NavMeshSurfaceData : ISharedComponentData, IEquatable<NavMeshSurfaceData>
{
    public NavMeshData navmeshData;
    public bool Equals(NavMeshSurfaceData other) => navmeshData == other.navmeshData;
    public override int GetHashCode() => navmeshData.GetHashCode();
}

// NavMeshSurfaceから変換するコンバージョンシステム
public class NavMeshSurfaceConversion : GameObjectConversionSystem
{
    protected override void OnUpdate()
    {
        Entities.ForEach((NavMeshSurface navmesh) =>
        {
            var entity = GetPrimaryEntity(navmesh.gameObject);
            DstEntityManager.AddSharedComponentData(entity, new NavMeshSurfaceData {
                navmeshData = navmesh.navMeshData
            });
        });
    }
}

 データをロードします。このロード処理で少し気をつけなければいけないのが「誰かが意図せずNavMeshInstance*1を持つEntityを破棄する可能性がある」という点です。例えばSubScene内をロード・アンロードだけ考えているとSubScene内に含まれているNavMeshInstanceが破棄されてリークすることが考えられます。

 そのため、ロード処理に付随して破棄の処理も組み込みます。この「削除されたら動作する」仕組みはISystemStateComponentData を使用します。

using Unity.Entities;
using UnityEngine.AI;

// NavMeshDataをロードしたりアンロードしたりする
public class LoadNavmeshSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.WithNone<LoadedNavMesh>().ForEach((Entity entity, NavMeshSurfaceData data) => 
        {
            var instance = NavMesh.AddNavMeshData(data.navmeshData); // NavMeshをゲームに登録
            PostUpdateCommands.AddComponent(entity, new LoadedNavMesh { Value = instance});
        });

        Entities.WithNone<NavMeshSurfaceData>().ForEach((Entity entity, ref LoadedNavMesh data) => {
            data.Value.Remove(); // ロードしたNavMeshInstanceを破棄
            PostUpdateCommands.RemoveComponent<LoadedNavMesh>(entity);
        });
    }
}

// インスタンスを保持する。ISystemStateComponentData なのでEntityが破棄されても破棄されない
public struct LoadedNavMesh : ISystemStateComponentData {
    public NavMeshDataInstance Value;
}

f:id:tsubaki_t1:20191116113344g:plain
SubSceneに配置したステージでNavMeshを作る

EntityがNavMeshの外に移動したら補正する

 NavMeshの範囲外に移動するのを防ぐコードを追加します。プレイヤーやAI、その他何らかのシステムにより移動するEntityが存在している状況で、Entityがステージ外に出てしまうのを防ぎます。これをJobSystem上で行う事が目標です。これは下のコードで実現しています。

using static Unity.Entities.ComponentType;

public class AgentLimitSystem : JobComponentSystem
{
    NavMeshQuery query;

    protected override void OnCreate()
    {
        RequireForUpdate(GetEntityQuery(ReadOnly<LoadedNavMesh>(), ReadOnly<NavMeshSurfaceData>()));
    }

    protected override void OnStartRunning()=> query = new NavMeshQuery(NavMeshWorld.GetDefaultWorld(), Allocator.Persistent);

    protected override void OnStopRunning() => query.Dispose();

    protected override JobHandle OnUpdate(JobHandle inputDeps) => new LimitJob { query = query }.Schedule(this, inputDeps);

    [BurstCompile]
    [RequireComponentTag(typeof(LimitByNavmeshTag))]
    struct LimitJob : IJobForEach<Translation>
    {
        [ReadOnly] public NavMeshQuery query;

        public void Execute(ref Translation position)
        {
            var location = query.MapLocation(position.Value, math.float3(1, 1, 1), -1);
            position.Value = location.position;
        }
    }
}

public struct LimitByNavmeshTag : IComponentData { }

 少し解説すると、NavMeshを誰もロードしていない状態でこの機能を動かす訳にはいかないので、RequireForUpdate(GetEntityQuery(ReadOnly<LoadedNavMesh>(), ReadOnly<NavMeshSurfaceData>())); で誰かがロード完了するまで待ちます。ロードが完了したら RequireForUpdate の条件が揃うので NavMeshQuery を生成、LimitJob を実行します。

 LimitJobの中身は、NavMeshの情報を元に「最も近いNavMeshの範囲内に収まるように補正」しています。本当はパス計算を使いたかったのですが、保持するパス情報やハンドルが面倒くさいので今回は範囲内に収める処理だけにしました。

 あとはオーサリングコードを用意してGameObjectに追加するなり、Entityにコードで追加するなりすれば、該当のEntityはNavMeshに沿ってしか移動できなくなります。

// オーサリングコード
[DisallowMultipleComponent]
[RequiresEntityConversion]
public class LimitByNavmeshComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new LimitByNavmeshTag());
    }
}

f:id:tsubaki_t1:20191116112033j:plain
コンポーネントに追加すれば動くのはGameObject的で楽でいい

関連

NavMeshData(アセット)を保存するために使用

tsubakit1.hateblo.jp

破棄を検出して云々する為に使用

tsubakit1.hateblo.jp

*1:NavMeshをゲーム内でハンドリングする情報

【Unity】スプライトアニメーションの再生速度がやたらと早い時の対処

f:id:tsubaki_t1:20191112185918g:plain

 スプライトアニメーションを今まで通りドラッグ&ドロップで作成した所、やたらとアニメーション再生が早いという事になりました。例えばトップ絵の左のように、速歩きのような速度でアニメーションを再生してしまいます。出来れば右のような速度に設定したい所です。

サンプルレート

 これは単純にアニメーションのサンプルレートが想定より早い事が原因です。Unityの初期設定でスプライトアニメーションを作成する場合サンプルレートは12となっています*1。これは 1秒間に12回キーを打てる という事です。

 例えば2Dスプライトを登録した場合、毎フレーム変化するスプライトが登録されるので、4スプライトで構成されるアニメーションの場合は1スプライト0.3秒で、1秒に3回サイクルが回るアニメーションが再生されます。

f:id:tsubaki_t1:20191112190708j:plain

 これでは速度が早すぎるので、1サイクル1秒で再生したいというのが、今回の趣旨です。

微妙な解決策

 最初に出てくるアイディアは1秒に合わせてアニメーションするようにAnimationClipを調整するというものです。これはサンプルレート的に少しもったいないような気がしなくもないです。またアニメーションをループさせる場合、「最終に登録したスプライトは1サンプル分しか表示されない」問題があるので、秒数側を操作すると少し面倒くさい話になります。

f:id:tsubaki_t1:20191112191314p:plain

 またAnimator側でアニメーション速度を調整するというアイディアがあるかもしれません。速度的に0.3をかければ1秒間にだいたい1回の再生です。これは他のアニメーションとの折り合いを考えたときに少しだけ面倒くさくなります。

f:id:tsubaki_t1:20191112191736j:plain

そうだ、サンプルレートを変えよう

 この問題をスマートに解決するのは、サンプルレートを変更することです。たぶんコレが一番はやいとおもいます。

 問題は、以前にあったサンプルレートの項目が表示されないことです。以前は現在のフレーム数の下あたりにありましたが、現在は表示されなくなっています。サンプルレートは何故か現在は非表示になっており、オプションから表示に切り替える必要があります。

 コンテキストメニュー > Show Sample rate でサンプルレートが表示されます。

f:id:tsubaki_t1:20191112192000j:plain

f:id:tsubaki_t1:20191112192141j:plain

f:id:tsubaki_t1:20191112192407g:plain

~ HAPPY END ~

感想

 なぜ非表示にしたし。コレ見つける前に一瞬Debugモードで編集を考えました。

関連

ぴぽや https://pipoya.net/

*1:普通に作ると60

【Unity】NavMeshComponentsに2D対応ブランチが追加されていたので試してみた

https://user-images.githubusercontent.com/1644563/68397953-e4cd2700-016b-11ea-913b-f062f47bc505.gif

NavMeshComponents

 NavMeshComponentsはNavMeshの機能を拡張するクラス郡です。NavMeshを動的に構築したりアセットに書き出したりといった事を非常に簡単に実現してくれます。エディターからリンクを参照される程度には重要な機能の割にパッケージ化されない謎機能でもあります。

2D ブランチ

 なんとなくブランチ一覧を見た時「2019.3-2D」という妙なブランチを見かけたので早速入れて試した所、Tilemapで作った地形をNavMeshで走らせることが出来ました。追加されているのはNavMeshBuilder2DNavMeshSourceTag2D というクラスで、この機能でナビゲーションを実現します。

f:id:tsubaki_t1:20191107234235j:plain
ブランチ

f:id:tsubaki_t1:20191107234203j:plain
含まれるスクリプト

タイルマップでNavMeshを使用しよう

 タイルマップベースでパスの取得を試してみました。

f:id:tsubaki_t1:20191110171319j:plain

ステージを作成しよう

 まずはタイルマップベースのステージを作成します。使用したのは2D UFO Tutorialです。この中のBackGround.pngに用が合ったので使用しています。現実的な話で言えば、タイルマップなら別に何でも良いです。取得した画像ファイルはタイルのグリッドに合わせて分割します。今回の場合は3x3で9分割です。
 なおスプライトのサイズが大きすぎるかもしれないので、Pixel Per Unitを1000辺りにしました(1000ピクセルが1m)

assetstore.unity.com

f:id:tsubaki_t1:20191110172017j:plain
Background.pngを9分割

 次にCustom Physics Shapeを選択して、タイルのコライダーの形状を変更します。このコライダーは通れる場所に設定するという点に注意する必要があります。通常だと逆(コライダーが無い場所を通れる)ですが、今回の場合はそうなっています。

f:id:tsubaki_t1:20191110172449g:plain
コライダーの設定

 あとはタイルマップを作成していきます。

  1. パレットを作成
  2. パレットにタイルを登録
  3. タイルを作成していく

あとは作成したタイルマップにはTilemap Collider2Dを設定します。これで事前準備は完了です。

https://user-images.githubusercontent.com/1644563/68541238-d6158880-0394-11ea-838f-cc700df166c2.gif

f:id:tsubaki_t1:20191110173535j:plain

NavMeshをベイクしよう!

 NavMeshをベイクします。

 最初に先程作成したTilemap Collider 2Dが付いているグリッドに NavMesh Surface Tag 2D を設定します。 GameObjectを新しく作成し、NavMeshBuilder2Dコンポーネントを追加します。 NavMeshBuilder2DのBakeボタンを押します。

f:id:tsubaki_t1:20191110174103j:plain

f:id:tsubaki_t1:20191110174143j:plain

 実際にベイクできたかは Window > AI > Navigation ウィンドウを開いて確認します。ベイクで来ている場合は移動範囲を青く表現されます。
 ベイクで来たらBake On Enableを設定しておきます。

f:id:tsubaki_t1:20191110174210j:plain

移動範囲を、もっとタイルに沿って配置したい場合は、Agent TypeのHumanoidのRadiusを小さな値にします。

NavMeshによる移動経路を取得しよう

 ナビゲーションの移動経路を取得してみます。これは特に特殊なことはせず、今まで通りの形で取得できます。例えばパスを LineRenderer で表現するコードを考えてみます。

using UnityEngine;
using UnityEngine.AI;

[DefaultExecutionOrder(10)]
public class DrawPath : MonoBehaviour
{
    [SerializeField] LineRenderer line;

    [SerializeField] Transform startPos, endPos;

    private NavMeshPath path;

    void Awake()
    {
        path = new NavMeshPath();
    }

    void OnEnable()
    {
        var result = NavMesh.CalculatePath(startPos.position, endPos.position, NavMesh.AllAreas, path);
        enabled = line.enabled = result;

        if( result )
        {
            var corners = path.corners;
            line.positionCount = corners.Length;
            line.SetPositions(corners);
        }
    }
}

f:id:tsubaki_t1:20191110180406j:plain

f:id:tsubaki_t1:20191110180417j:plain

NavMeshAgentを使用する場合

 NavMeshAgentはパスを3Dの物と認識しているので注意が必要です。NavMeshAgentを使用したい場合、座標だけ同期して実際の描画は別のGameObjectで行う等が楽で良いです。

関連

 トップ絵のキャラクターの表現はコレを使用。移動方向を渡せば良いだけの簡単仕様

tsubakit1.hateblo.jp

 この機能はすごく単純に、NavMeshをベイクする際のポリゴン情報をNavMeshから取得しているだけです。

tsubakit1.hateblo.jp

【Unity】Unityエディターを拡大表示する

f:id:tsubaki_t1:20191107235040j:plain

文字が小さくて読めない

 Unityで文字が小さくて読めんという話を稀に聞きます。実際文字が小さい事もありますし、Pro Skinは色的にサクッと認識出来ない事も多いです。また、異様に小さいボタンのせいで押せないというケースもあります。Timelineのロックボタンとか(2019.3のUIだと多少見やすくなっています)

f:id:tsubaki_t1:20191108000355j:plain
ロックボタン

UIのサイズを変更する

 たぶんUnity 2019.3からUIをスケールする機能が追加されたみたいです。縮小は出来ませんが拡大は出来るので、非常に高い解像度で作業する場合には重宝するかもしれません。下の図では大雑把に100%と150%の拡大率で画面をキャプチャしてみました。メニューバーの大きさを基準に考えると結構大きさが変わっているのがわかります。実際、めっちゃでかくなります。

f:id:tsubaki_t1:20191107235611j:plain
100%

f:id:tsubaki_t1:20191107235638j:plain
150%

変更方法

 Preference > UI ScalingUse Default desktop setting を外し、好きなスケールを設定します。unityエディター再起動後にスケールしたサイズでUIが表示されるようになります。 f:id:tsubaki_t1:20191107235414j:plain

感想

 正直、これよりアセット名の改行表示がほしい。割と切実に