Version: 2022.3
言語: 日本語
エージェントに一連の地点を巡回させる
用語集

アニメーションとナビゲーションの組み合わせ

このページでは、ナビゲーションシステムを使ってヒューマノイド型キャラクターを移動する方法について説明します。

それを行うために、カスタムのスクリプトと共に、Unity のビルトインのアニメーションシステムとナビゲーションシステムを使います。

ここでの説明は、Unity と Mecanim アニメーションシステムの基本操作に慣れていることを前提としています。

サンプルプロジェクトを利用できるので、スクリプトを加えたり、アニメーションやアニメーションコントローラを設定することを 0 から行う必要はありません。

アニメーションコントローラーの作成

応答性と汎用性の高いアニメーションコントローラーを取得し広い範囲を動けるように、様々な方向に移動するアニメーションのセットが必要です。これは「Strafe」セットと呼ばれることもあります。

移動するアニメーションに加えて、立っているキャラクターのアニメーションも必要です。

まず、2D ブレンドツリーで Strafe セットを配置します。Blend Type で 2D Simple Directional を選択し Compute Positions > Velocity XZ を選択し、アニメーションを配置してください。

ブレンディングの制御を行うために、2 つの Float パラメーター velxvely を加え、それらをブレンドツリーに割り当てます。

ここでは、それぞれ異なる速度を持っている 7 つの Run (走行) アニメーションクリップを配置します。前方 (正面と左/右前方) と後方 (後方と左/右後方) に加えて、ある一点上で走るアニメーションクリップを使用します。下の 2D ブレンドマップでは、一点上で走るアニメーションクリップ (Run_Spot アニメーションクリップ) が中央でハイライトされています。一点上でアニメーションを走らせる理由は 2 つあります。1 つは他のアニメーションとブレンドする時に走行するスタイルを維持するためです。2 つ目はブレンドするときにアニメーションで足が滑っているように見えるのを防ぐためです。

次に、休止 (Idle) のアニメーションクリップをそのノード (Idle) に加えます。今度は、2 つの分離したアニメーションステートを2 つの遷移と組み合わせます。

Move (移動) と Idle (休止) 状態の切り替えを制御するために、boolean 型制御パラメーター move を加えます。その後、Transitions の Has Exit Time プロパティを無効にします。これにより、アニメーション中にいつでも遷移の発生が可能です。遷移の反応を良くするために、遷移時間を約 0.10 秒あたりに設定します。

新しく作成したアニメーションコントローラーを動かしたいキャラクターに配置します。

再生ボタンを押してキャラクターを Hierarchy ウィンドウ で選んでください。これで、Animator ウィンドウ のアニメーションの値を手動で制御することができ、動きの状態と速度を変更ができます。

次のステップは、アニメーションのパラメーターを制御する他の手段を作成についてです。

ナビゲーション コントロール

キャラクターに NavMesh Agent コンポーネントを配置します。キャラクターに合わせて半径、高さを調整します。さらにアニメーションブレンドツリーの最高速度に合わせて Speed プロパティを変更します。

キャラクターを配置したシーンの NavMesh を作成します。

次に、どこに移動するかをキャラクターに伝える必要があります。これは通常、アプリケーションごとに特有なものです。ここでは、クリックすると移動する (click to move) 挙動、つまり、ユーザーが画面上でクリックしたワールドの位置にキャラクターが移動する挙動を選択します。

// ClickToMove.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent (typeof (NavMeshAgent))]
public class ClickToMove : MonoBehaviour {
    RaycastHit hitInfo = new RaycastHit();
    NavMeshAgent agent;

    void Start () {
        agent = GetComponent<NavMeshAgent> ();
    }
    void Update () {
        if(Input.GetMouseButtonDown(0)) {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray.origin, ray.direction, out hitInfo))
                agent.destination = hitInfo.point;
        }
    }
}

再生ボタンを押し、シーンのあちこちをクリックすると、キャラクターがシーン内を移動するのが見られます。ただし、アニメーションはその動きとまったく一致しません。エージェントの状態と速度をアニメーションコントローラーへ伝えなければなりません。

エージェントから状態と速度の情報をアニメーションコントローラーに伝えるためにもう 1 つスクリプトを加えます。

// LocomotionSimpleAgent.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent (typeof (NavMeshAgent))]
[RequireComponent (typeof (Animator))]
public class LocomotionSimpleAgent : MonoBehaviour {
    Animator anim;
    NavMeshAgent agent;
    Vector2 smoothDeltaPosition = Vector2.zero;
    Vector2 velocity = Vector2.zero;

    void Start ()
    {
        anim = GetComponent<Animator> ();
        agent = GetComponent<NavMeshAgent> ();
        // 位置を自動的に更新しません
        agent.updatePosition = false;
    }
    
    void Update ()
    {
        Vector3 worldDeltaPosition = agent.nextPosition - transform.position;

        // worldDeltaPosition をローカル空間にマップします
        float dx = Vector3.Dot (transform.right, worldDeltaPosition);
        float dy = Vector3.Dot (transform.forward, worldDeltaPosition);
        Vector2 deltaPosition = new Vector2 (dx, dy);

        // deltaMove にローパスフィルターを適用します
        float smooth = Mathf.Min(1.0f, Time.deltaTime/0.15f);
        smoothDeltaPosition = Vector2.Lerp (smoothDeltaPosition, deltaPosition, smooth);

        // 時間が進んだら、velocity (速度) を更新します
        if (Time.deltaTime > 1e-5f)
            velocity = smoothDeltaPosition / Time.deltaTime;

        bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius;

        // アニメーションのパラメーターを更新します
        anim.SetBool("move", shouldMove);
        anim.SetFloat ("velx", velocity.x);
        anim.SetFloat ("vely", velocity.y);

        GetComponent<LookAt>().lookAtTargetPosition = agent.steeringTarget + transform.forward;
    }

    void OnAnimatorMove ()
    {
        // position (位置) を agent (エージェント) の位置に更新します
        transform.position = agent.nextPosition;
    }
}

このスクリプトについて少し説明しましょう。このスクリプトは、前出の ClickToMove スクリプト同様、 AnimatorNavMeshAgent コンポーネントがアタッチされたキャラクターに設定されます。

まず、このスクリプトはキャラクターの位置を自動更新しないようにエージェントに指示します。スクリプトの最後で位置の更新を処理します。向き (方向) はエージェントによって更新されます。

アニメーションのブレンドはエージェントの速度を読み込むことで制御されます。速度は、キャラクターの向きに基づいた相対速度に変換され、平滑化されます。変換された水平方向速度のコンポーネントは、Animator に渡されます。さらに Idle と Moving 間の状態の切り替えは、速度 (すなわち velocity.magnitude) によって制御されます。

OnAnimatorMove() コールバックで、キャラクターの位置を NavMeshAgent と一致するように更新します。

シーンをもう 1 度再生すると、アニメーションと動きがほぼ完全に合っています。

移動するキャラクターの品質の向上

アニメーションされたキャラクターや移動するキャラクターの品質を向上させるため、いくつかの方法をみてみましょう。

注視

キャラクターに一点を注視させ、その方向を向かせることは、注目と期待を集めるために重要です。アニメーションシステムの LookAt API を使います。これはもうひとつのスクリプトを呼び出します。

// LookAt.cs
using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Animator))]
public class LookAt : MonoBehaviour {
    public Transform head = null;
    public Vector3 lookAtTargetPosition;
    public float lookAtCoolTime = 0.2f;
    public float lookAtHeatTime = 0.2f;
    public bool looking = true;

    private Vector3 lookAtPosition;
    private Animator animator;
    private float lookAtWeight = 0.0f;

    void Start ()
    {
        if (!head)
        {
            Debug.LogError("No head transform - LookAt disabled");
            enabled = false;
            return;
        }
        animator = GetComponent&lt;Animator&gt; ();
        lookAtTargetPosition = head.position + transform.forward;
        lookAtPosition = lookAtTargetPosition;
    }

    void OnAnimatorIK ()
    {
        lookAtTargetPosition.y = head.position.y;
        float lookAtTargetWeight = looking ? 1.0f : 0.0f;

        Vector3 curDir = lookAtPosition - head.position;
        Vector3 futDir = lookAtTargetPosition - head.position;

        curDir = Vector3.RotateTowards(curDir, futDir, 6.28f*Time.deltaTime, float.PositiveInfinity);
        lookAtPosition = head.position + curDir;

        float blendTime = lookAtTargetWeight &gt; lookAtWeight ? lookAtHeatTime : lookAtCoolTime;
        lookAtWeight = Mathf.MoveTowards (lookAtWeight, lookAtTargetWeight, Time.deltaTime/blendTime);
        animator.SetLookAtWeight (lookAtWeight, 0.2f, 0.5f, 0.7f, 0.5f);
        animator.SetLookAtPosition (lookAtPosition);
    }
}

キャラクターにこのスクリプトを加え、キャラクターをヒエラルキーで表示し Head の Transform に head のプロパティを割り当てます。LookAt スクリプトにはナビゲーションコントロールの概念がありません。したがって、どこを見るかをコントロールするために、LocomotionSimpleAgent.cs スクリプトに戻り、注視の挙動を制御するために、2、3行追加します。Update() の最終行に以下の行を追加してください。

        LookAt lookAt = GetComponent<LookAt> ();
          if (lookAt)
              lookAt.lookAtTargetPosition = agent.steeringTarget + transform.forward;

これは、注視する点をほぼ経路の次の曲がり角に、または曲がり角がない場合は経路の終わりに設定するように LookAt スクリプトに伝えます。

試してみてください。

ナビゲーションを使用したアニメーション重視のキャラクター

これまでキャラクターはエージェントが指示した位置によって完全に制御されていました。この方法では、他のキャラクターや障害物を確実に回避することが、直接キャラクターの位置に変換されます。ただし、アニメーションが指定された速度に満たない場合、足が滑っているように見える場合があります。ここでは、キャラクターの制約を少し緩めます。基本的に、障害物回避の品質を犠牲にしてアニメーションの品質を向上させます。

LocomotionSimpleAgent.cs スクリプトの OnAnimatorMove() コールバックを以下の行と置き換えてください。

    void OnAnimatorMove ()
        {
            // ナビゲーションサーフェスの高さを使って、アニメーションの動きに基づいて位置を更新します
            Vector3 position = anim.rootPosition;
            position.y = agent.nextPosition.y;
            transform.position = position;
        }

これを使うと、キャラクターがエージェント (緑のワイヤーフレームのシリンダー) の位置から徐々に離れることに気がつきます。キャラクターがアニメーションで離れるのを制限する必要があります。これはキャラクターに向かってエージェントを引っぱるか、または、エージェントの位置に向かってキャラクターを引っぱるかどちらでも行うことができます。LocomotionSimpleAgent.cs スクリプトの Update() メソッドの最後に以下を追加してください。

        // キャラクターをエージェントの方に引っ張ります
                if (worldDeltaPosition.magnitude &gt; agent.radius)
                    transform.position = agent.nextPosition - 0.9f*worldDeltaPosition;

または、エージェントをキャラクターについていくようにする場合は以下を追加します。

        // エージェントをキャラクターの方に引っ張ります
                if (worldDeltaPosition.magnitude &gt; agent.radius)
                    agent.nextPosition = transform.position + 0.9f*worldDeltaPosition;

どの方法が一番よく動作するかは、それぞれのケースによります。

終わりに

ここでは、ナビゲーションシステムを使用して移動し、それに応じたアニメーションを行うキャラクターを設定しました。ブレンドの回数や LookAt の重み付けなどで見た目を向上できます。これらを調整してみることは、この設定をさらに学習するのによいでしょう。

エージェントに一連の地点を巡回させる
用語集