うら干物書き

ゲームを作っています。

【Unity】NavMesh×段差×Spawn=面倒なことに

 今回はNavMeshを使っていたら発生した謎の現象についてのお話です。
 最終的には力技を使って、現象を発生しないようにしています。
 一体なぜこんなことが……。


この記事にはUnity2017.2.0f3を使用しています。

現象の発生手順

 では現象が発生した手順をみていきましょう。

 まず段差のあるフィールドを作成しましょう。
 二つのCubeメッシュを使用して簡単に作ります。

f:id:urahimono:20171125082258p:plain

 次にNavMeshを焼いて作成しましょう。
 上の段と下の段が分かれるように設定する必要があります。

f:id:urahimono:20171125082324p:plain

 次にオブジェクトを生成する場所であるSpawnポイントを作成しましょう。
 見た目で位置分かりやすいように、今回はSphereメッシュを付けていますが、Transformがあれば大丈夫です。
 ここで重要な点として、Spawnポイントはフィールドの上の段と下の段に分けて設定する必要があります。

f:id:urahimono:20171125082342p:plain

 では次にSpawnさせるオブジェクトを作成しましょう。
 オブジェクトには、NavMeshAgentNavMeshAgentを制御するコンポーネントがついています。

NavController.cs

using UnityEngine;
using UnityEngine.AI;

[RequireComponent( typeof( NavMeshAgent ) )]
public class NavController : MonoBehaviour
{
    private NavMeshAgent m_navMeshAgent = null;

    private void Awake()
    {
        m_navMeshAgent  = GetComponent<NavMeshAgent>();
    }

} // class NavController

 NavControllerNavMeshAgentを制御用のコンポーネントですが、現時点は特に制御はしていません。
 簡単な作りです。

f:id:urahimono:20171125082403p:plain

 最後に、先ほど作成したオブジェクトをSpawnするコントローラーを作成しましょう。

GameController.cs

using UnityEngine;
using System.Collections;

public class GameController : MonoBehaviour
{
    [SerializeField]
    private Transform[] m_points        = null;
    [SerializeField]
    private float       m_spawnTime     = 1.0f;
    [SerializeField]
    private GameObject  m_spawnObject   = null;

    private int m_spawnIndex    = 0;


    private void Start()
    {
        StartCoroutine( SpawnProcess() );
    }

    private IEnumerator SpawnProcess()
    {
        while( true )
        {
            yield return new WaitForSeconds( m_spawnTime );

            SpawnObject();
        }
    }

    private void SpawnObject()
    {
        if( m_spawnObject == null )
        {
            return;
        }

        if( m_points == null || m_points.Length == 0 )
        {
            return;
        }

        Transform point = m_points[ m_spawnIndex ];
        m_spawnIndex = ( m_spawnIndex + 1 ) % m_points.Length;

        Instantiate( m_spawnObject, point.position, point.rotation );
    }

} // class GameController

 コントローラーは一定時間ごとに、指定したポイントに順番にオブジェクトをSpawnさせる機能だけがあります。

 準備が整ったので、実行してみましょう。

f:id:urahimono:20171125082422g:plain

 はーい、不具合が発生しましたよー。
 下の段のSpawnポイントに、オブジェクトを生成しようとすると、Spawnポイントの場所ではないところに配置されてしまっています。

f:id:urahimono:20171125082454p:plain

 ちなみにこの現象、作成したNavMeshによっては、上の段に配置しようとしたオブジェクトが下の段に配置される場合もあります。
 この現象の肝は、高さの異なるNavMeshに、ランタイムでオブジェクトを生成して配置しようとすると発生するみたいです。

 というわけで担当プログラマーの方ー、このバグをアサインしますよー。

現象が発生しない方法A

 残念ながらぼっちプロジェクトのため、報告担当も修正担当も僕がやる必要があるため原因を調査いてみましょう。
 以下の場合にはこの不具合は発生しないことを確認しました。

 まずNavMeshの焼き方を変えてみましょう。
 フィールドの段差がNavMesh的に普通に登れる値AgentHeightStepHeightを調整しましょう。
 段差の高さよりStepHeightの値が大きいことが特徴です。

f:id:urahimono:20171125082506p:plain

 さあ、実行しましょう。

f:id:urahimono:20171125082517g:plain

 これなら問題なく指定したSpawnポイントにオブジェクトを配置することができます。

f:id:urahimono:20171125082534p:plain

 ただやり方には問題しかありません。
「段差があると、なんか知らんけどオブジェクトが配置できないから、段差がある仕様やめようぜ!」
 と言っているようなものです。

現象が発生しない方法B

 NavMeshを元に戻しました。
 次の方法は、SpawnするオブジェクトのNavMeshAgentを無効化するやり方です。
 自作コンポーネントがほとんど何もしていない以上、NavMeshAgentが何かしら悪さをしているのは明白です。

f:id:urahimono:20171125082546p:plain

 NavMeshAgentを無効化しました。
 この状態で再度実行してみます。

f:id:urahimono:20171125082600g:plain

 うまく配置できました。
 Spawnポイントは地面より若干上の方にあるので、オブジェクトは空中に配置されています。

f:id:urahimono:20171125082627p:plain

 ちなみにSpawnポイントを地面に同じ高さに設定しても現象は発生したので原因ではなさそうです。
 もちろん、このままではダメです。
 NavMeshAgentを付けている理由は、NavMeshAgentを使って移動処理を行うためです。
 NavMeshAgentを無効化してしまっては、そもそもNavMesh焼く必要すらなくなってしまいます。

現象が発生しない方法C

 先ほどの方法に少し手を加えましょう。
 オブジェクトのNavMeshAgentを有効状態に戻します。
 NavMeshAgentの有効無効をコンポーネントで制御しましょう。
 何もしていなかったNavController.csに仕事をさせます。

NavController.cs

private void Awake()
{
    m_navMeshAgent  = GetComponent<NavMeshAgent>();
    m_navMeshAgent.enabled  = false;
}

private void Start()
{
    m_navMeshAgent.enabled  = true;
}

 Awake()NavMeshAgentenabledfalseにして、Start()NavMeshAgentenabledtrueにして元に戻します。

 では実行しましょう。

f:id:urahimono:20171125082643g:plain

 不具合の現象が発生しなくなりました!

f:id:urahimono:20171125082709p:plain

 今回、コンポーネントの処理順は設定していないため、状況によってはStart()NavMeshAgentenabledを元に戻すタイミングが間に合わない可能性もありますが、少なくとも生成時にNavMeshAgentを無効化し、のちに元に戻せば現象が発生しなくなることがわかりました。

Warp()を使ってみようよ

2017.12.14 追記

 このようなご連絡をいただきましたよー。
 ありがとうございます。

 そうか、Warp()関数がありましたよ!
 確かにNavMeshAgentが実行中にtransformを直接いじるのはよくないですな。
docs.unity3d.com

 試してみましょう。

NavController.cs

private void Awake()
{
    m_navMeshAgent  = GetComponent<NavMeshAgent>();        
}

private void Start()
{
    m_navMeshAgent.Warp( transform.position );
}

f:id:urahimono:20171214234024g:plain

 やった、現象が発生しなくなりましたよ。
 enabledを切り替えるより処理がシンプルでいいですね。
 ちなみにAwake()Warp()を呼んだ場合はダメでした。お気を付けを。