徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】NavMeshを学ぶ OffMeshLink編

 最近僕はNavMeshの勉強に力を注いでいます。
 今回勉強するオフメッシュリンクでNavMesh基本的なことは一通り使えるようになるはずです。
 これで僕だけ遠足のお昼ご飯の時に仲間外れにされることがなくなるはずだ!
 さあ、頑張るぞー。


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

前回を学んだことを信じる

 エリアとコストを扱えるようになった。
 
www.urablog.xyz

オフメッシュリンクの力を信じる

 オフメッシュリンクを学ぶ前に、前々回から使っている巡回用のスクリプトを組み込もう。

using UnityEngine;
using UnityEngine.AI;
using System.Collections;

[RequireComponent( typeof( NavMeshAgent ) )]
public class ObjectController : MonoBehaviour
{
    [SerializeField]
    private Transform[]     m_targets   = null;
    [SerializeField]
    private float   m_destinationThreshold  = 0.0f;

    protected NavMeshAgent  m_navAgent  = null;

    private int m_targetIndex   = 0;

    private Vector3 CurretTargetPosition
    {
        get
        {
            if( m_targets == null || m_targets.Length <= m_targetIndex )
            {
                return Vector3.zero;
            }

            return m_targets[ m_targetIndex ].position;
        }
    }

    protected virtual void Start()
    {
        m_navAgent = GetComponent<NavMeshAgent>();
        m_navAgent.destination  = CurretTargetPosition;
    }

    private void Update()
    {
        if( m_navAgent.remainingDistance <= m_destinationThreshold )
        {
            m_targetIndex   = ( m_targetIndex + 1 ) % m_targets.Length;

            m_navAgent.destination = CurretTargetPosition;
        }
    }

} // class ObjectController

f:id:urahimono:20171015215129p:plain

 このスクリプトで四隅の目標地点を巡回していくようになるのだが、今回のステージは壁で左右が分割されているため、左のエリアから右のエリアに移動することは出来ないはずだ。
 試してみよう。

f:id:urahimono:20171015215147g:plain
f:id:urahimono:20171015215241p:plain

 案の定、左のエリアを当てもなくうろうろする子犬のようになってしまっている。

 こんな時こそオフメッシュリンクを使う時だ。

docs.unity3d.com

 どうやらOffMeshLinkコンポーネントを使えば、二つのTransformを繋げて移動できるようになるらしい。
 早速、使ってみよう。
f:id:urahimono:20171015215256p:plain

 簡単に設定できた。
 これを実行すれば、左のエリアから右のエリアに移動できるようになるはずだ。

f:id:urahimono:20171015215335g:plain
f:id:urahimono:20171015215457p:plain

 ああ、うん。
 確かに右のエリアに移動できたようだ。
 まさか壁を突っ切るとは思わなかったが。

自動生成できるという噂を信じる

 先ほどは手動でOffMeshLinkを設定したが、風の噂では自動生成することができるとのことだ。
docs.unity3d.com

 ほうー、便利な世の中になったものだ。
 では早速試してみようかね。

Jump Distance

 こんな感じのステージを新たに作ってみた。

f:id:urahimono:20171015215515p:plain

 これでNavMeshを焼いてあげればいいのかな。

f:id:urahimono:20171015215528p:plain

 設定されてないじゃないかOffMeshLinkが!
 自動で設定されるなんてうまい話にまんまと騙されちまったぜ。

 えっ、焼く際に追加設定がいるって。
 飛び越えられる距離をJump Distanceに設定する必要があるのかぃ?

f:id:urahimono:20171015215541p:plain
f:id:urahimono:20171015215551p:plain

 おお、今度はちゃんと自動でOffMeshLinkが追加されたぜ。
 実行してみよう。

f:id:urahimono:20171015215608g:plain

 ああ、うん。繫がっているようだね。
 相変わらず突っ切って進んでいくようだが。

Drop Height

 先ほどは同じ高さの地面同士の割れ目を飛び越えるOffMeshLinkを自動で生成できた。
 ではこのような高低差のある場所のOffMeshLinkは自動で生成できないものか。

f:id:urahimono:20171015215728p:plain

 考えるまでもなく、Jump Distanceの上にあったDrop Heightがそれっぽい。

f:id:urahimono:20171015215743p:plain
f:id:urahimono:20171015215752p:plain

 ふふふ、予想通りOffMeshLinkが自動生成されたぜ。
 ……ただ気になるところは、Jump Distanceの時と違って降りる方向にしかOffMeshLinkが設定されていない気がする。
 まあ、最近俺は老眼がひどくなっているから、見間違いであろう。
 とりあえず実行してみよう。

f:id:urahimono:20171015215807g:plain

 降りることは出来るが、登ることが出来ない。
 老眼のせいではなかったか。
 Drop Heightはその名の通り、降りる用のOffMeshLinkしか作られないようだね。
 気を付けねば。

俺の方がうまく移動処理が作れることを信じる

 これでオフメッシュリンクの設定は終わったのだが……、このままではあまり使い物にならないなぁ。
 プログラム的には離れたNavMesh同士が繋がって移動できていることには間違いないのだが、壁などの障害物を無視して進んでいる。
 見た目的にはただのバグにしか見えない!

 どうやら少し手を入れる必要がありそうだな!

ワープ

 最初に作ったステージに戻ろう。
f:id:urahimono:20171015215934p:plain

 UnityバイブルのNavMeshの記事を参考に、少しObjectController.csを改良してみようか。
tsubakit1.hateblo.jp

ObjectController.cs

protected virtual void Start()
{
    m_navAgent = GetComponent<NavMeshAgent>();
    m_navAgent.destination  = CurretTargetPosition;

    StartCoroutine( UpdateOffMeshLink() );
}

private IEnumerator UpdateOffMeshLink()
{
    // オフメッシュリンクの挙動が自動モードの場合は、この処理は行わない。
    if( m_navAgent.autoTraverseOffMeshLink )
    {
        yield break;
    }

    while( true )
    {
        // オフメッシュリンクに乗るまで待機。
        while( !m_navAgent.isOnOffMeshLink )
        {
            yield return null;
        }

        // NavMeshの挙動を止めます。
        // Stop()はObsoleteになったようなので使わないよ。
        m_navAgent.isStopped = true;

        // オフメッシュリンクと高さに差があるので、ちょっと微調整。
        OffMeshLinkData offMeshLinkData = m_navAgent.currentOffMeshLinkData;
        Vector3 targetPos   = offMeshLinkData.endPos;
        targetPos.y        += transform.position.y - offMeshLinkData.startPos.y;

        yield return OffMeshLinkProcess( targetPos );

        // オフメッシュリンクの計算を完了する。
        m_navAgent.CompleteOffMeshLink();

        // NavMeshの挙動を再開する。
        // Resume()はObsoleteになったようなので使わないよ。
        m_navAgent.isStopped = false;
    }
}

protected virtual IEnumerator OffMeshLinkProcess( Vector3 i_targetPos )
{
    yield return null;
}

 NavMeshAgentAuto Traverse OffMesh Linkを有効にしていると、先ほどの壁を突っ切る動きをまたし始めるので、無効にして俺が作った処理で移動処理を行うようにする。
f:id:urahimono:20171015215950p:plain

 さあ、実行だ。
f:id:urahimono:20171015220006g:plain

 瞬間移動するようになった。
 壁を突っ切るよりましだが、面白みがない。
 ObjectControllerを継承した新しいコントローラーが必要だな。

ObjectWarpController.cs

using UnityEngine;
using UnityEngine.AI;
using System.Collections;

[RequireComponent( typeof( NavMeshAgent ) )]
public class ObjectWarpController : ObjectController
{
    [SerializeField]
    private float   m_scaleTime = 0.0f;
    [SerializeField]
    private float   m_waitTime  = 0.0f;

    protected override IEnumerator OffMeshLinkProcess( Vector3 i_targetPos )
    {
        Vector3 defaultScale    = transform.localScale;

        yield return new WaitForSeconds( m_waitTime );

        yield return ScaleProcess( Vector3.zero, m_scaleTime );

        yield return new WaitForSeconds( m_waitTime );

        transform.position  = i_targetPos;

        yield return ScaleProcess( defaultScale, m_scaleTime );

        yield return new WaitForSeconds( m_waitTime );
    }

    private IEnumerator ScaleProcess( Vector3 i_toScale, float i_time )
    {
        Vector3 fromScale = transform.localScale;

        float startTime = Time.time;
        float endTime   = startTime + i_time;

        while( Time.time < endTime )
        {
            float elapsedTime = Time.time - startTime;
            float elapsedRate = elapsedTime / i_time;

            Vector3 scale           = Vector3.Lerp( fromScale, i_toScale, elapsedRate );
            transform.localScale    = scale;

            yield return null;
        }

        transform.localScale    = i_toScale;
    }
    
} // class ObjectWarpController

f:id:urahimono:20171015220131p:plain

 さあ、新しいコントローラーObjectWarpControllerの力を見せてくれ!

f:id:urahimono:20171015220148g:plain
f:id:urahimono:20171015220229p:plain

 ワープっぽくなった。
 もう少し演出を強化すればより良くなりそうだが、今回はここまででいいや。

ジャンプ

 では次にこのステージの挙動を改良しよう。

f:id:urahimono:20171015220243p:plain

 今回は飛び越えるようにしたいので、放物線の動きで移動するようにしよう。
 放物線の動きについては以前勉強したので、同じようにRigidbodyを使って放物線の動きを再現してみよう。
www.urablog.xyz

 NavMeshAgentRigidbodyの挙動を同時に扱うことはできないようなので、このオフメッシュリンクの時だけRigidbodyを使うように工夫しよう。

ObjectJumpController.cs

using UnityEngine;
using UnityEngine.AI;
using System.Collections;

[RequireComponent( typeof( NavMeshAgent ) )]
[RequireComponent( typeof( Rigidbody ) )]
public class ObjectJumpController : ObjectController
{
    [SerializeField]
    private float   m_jumpTime  = 0.0f;

    private Rigidbody   m_rigidbody = null;

    protected override void Start()
    {
        base.Start();

        // 放物線の挙動のためにRigidbodyを使う。
        // NavMeshAgentを使っているときは使いたくないので、
        // isKinematicを有効にしてRigidbodyの無効化する。
        m_rigidbody = GetComponent<Rigidbody>();
        m_rigidbody.isKinematic = true;
    }

    protected override IEnumerator OffMeshLinkProcess( Vector3 i_targetPos )
    {
        m_rigidbody.isKinematic = false;
        m_rigidbody.velocity    = Vector3.zero;

        ShootFixedTime( i_targetPos, m_jumpTime );

        yield return new WaitForSeconds( m_jumpTime );

        transform.position      = i_targetPos;

        m_rigidbody.isKinematic = true;
        m_rigidbody.velocity    = Vector3.zero;
    }

    private void ShootFixedTime( Vector3 i_targetPosition, float i_time )
    {
        float speedVec  = ComputeVectorFromTime( i_targetPosition, i_time );
        float angle     = ComputeAngleFromTime( i_targetPosition, i_time );

        if( speedVec <= 0.0f )
        {
            // その位置に着地させることは不可能のようだ!
            Debug.LogWarning( "!!" );
            return;
        }

        Vector3 vec = ConvertVectorToVector3( speedVec, angle, i_targetPosition );
        SetRigidbody( vec );
    }

    private Vector3 ConvertVectorToVector3( float i_v0, float i_angle, Vector3 i_targetPosition )
    {
        Vector3     startPos    = transform.position;
        Vector3     targetPos   = i_targetPosition;
        startPos.y  = 0.0f;
        targetPos.y = 0.0f;

        Vector3     dir     = ( targetPos - startPos ).normalized;
        Quaternion yawRot   = Quaternion.FromToRotation( Vector3.right, dir );
        Vector3     vec     = i_v0 * Vector3.right;
    
        vec     = yawRot * Quaternion.AngleAxis( i_angle, Vector3.forward ) * vec;

        return vec;
    }

    private float ComputeVectorFromTime( Vector3 i_targetPosition, float i_time )
    {
        Vector2 vec = ComputeVectorXYFromTime( i_targetPosition, i_time );

        float v_x   = vec.x;
        float v_y   = vec.y;

        float v0Square  = v_x * v_x + v_y * v_y;
        // 負数を平方根計算すると虚数になってしまう。
        // 虚数はfloatでは表現できない。
        // こういう場合はこれ以上の計算は打ち切ろう。
        if( v0Square <= 0.0f )
        {
            return 0.0f;
        }

        float v0        = Mathf.Sqrt( v0Square );

        return v0;
    }

    private float ComputeAngleFromTime( Vector3 i_targetPosition, float i_time )
    {
        Vector2 vec = ComputeVectorXYFromTime( i_targetPosition, i_time );

        float v_x   = vec.x;
        float v_y   = vec.y;

        float rad   = Mathf.Atan2( v_y, v_x );
        float angle = rad * Mathf.Rad2Deg;

        return angle;
    }

    private Vector2 ComputeVectorXYFromTime( Vector3 i_targetPosition, float i_time )
    {
        // 瞬間移動はちょっと……。
        if( i_time <= 0.0f )
        {
            return Vector2.zero;
        }


        // xz平面の距離を計算。
        Vector2 startPos    = new Vector2( transform.position.x, transform.position.z );
        Vector2 targetPos   = new Vector2( i_targetPosition.x, i_targetPosition.z );
        float   distance    = Vector2.Distance( targetPos, startPos );

        float x     = distance;
        // な、なぜ重力を反転せねばならないのだ...
        float g     = -Physics.gravity.y;
        float y0    = transform.position.y;
        float y     = i_targetPosition.y;
        float t     = i_time;

        float v_x   = x / t;
        float v_y   = ( y - y0 ) / t + ( g * t ) / 2;

        return new Vector2( v_x, v_y );
    }

    private void SetRigidbody( Vector3 i_shootVector )
    {
        // 速さベクトルのままAddForce()を渡してはいけないぞ。力(速さ×重さ)に変換するんだ
        Vector3 force = i_shootVector * m_rigidbody.mass;

        m_rigidbody.AddForce( force, ForceMode.Impulse );
    }

} // class ObjectJumpController

f:id:urahimono:20171015220256p:plain

 ObjectJumpController、お前の力を見せてくれ!

f:id:urahimono:20171015220307g:plain
f:id:urahimono:20171015220417p:plain

 うんうん、これで飛び越えているように見えるようになった。
 えかったえかった。

そして更なる高みへ

 NavMeshの基本的なことは大体わかるようになった。
 ただNavMeshにはまだまだ出来ることが多いようだ。

 Uniteの発表では、GitHubに更なるNavMeshの機能が公開されているらしい。
github.com

 ただもう俺の頭は既に限界だ。
 俺の頭の容量は大体4キロバイトなので、これ以上勉強すると地下鉄の乗り方を忘れてしまう。
 今回はここまでとしよう。

【Unity】NavMeshを学ぶ