Version: 2017.1
エージェントに設定した地点を巡回させる
Unity サービス

アニメーションとナビゲーションの利用

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

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

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

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

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

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

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

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

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

ここでは、それぞれ異なる速度を持っている 7 つの「 run 」アニメーションクリップを配置しています。前方(と、左/右)と後方(と左後45度/右後45度)に加えて、Spot 上で「 Run 」するアニメーションクリップを使用してます。後者( Run_Spot アニメーションクリップ )は、「 Blend Tree 」の中でハイライトされています。Spot 上でアニメーション「 Run 」をさせる理由は2つあります。ひとつは他のアニメーションとブレンドされる時に「 Run 」しているスタイルを保持します。二つ目はブレンドするときアニメーションで足がすべるのを防ぎます。

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

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

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

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

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

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

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

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

次に、どこに移動するかをキャラクターに伝える必要があります。これは通常、アプリケーションに特有なものになります。ここで「 Move 」をクリックして選択し、キャラクターをユーザーが画面上でクリックした点に移動させます。

// ClickToMove.cs
using UnityEngine;

[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;
        }
    }
}

今、「 Play 」を押し、シーンの周辺をクリックすると、シーン内を移動するキャラクターが表示されます。ただし、アニメーションは動きとまったく一致しません。エージェントの状態と速度をアニメーションコントローラーへ伝えなければなりません。

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

// LocomotionSimpleAgent.cs
using UnityEngine;

[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> ();
        //  position を自動的に更新してはいけません
        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 を Low-pass フィルターにかけます
        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;

        //  animation のパラメーターを更新します
        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 position に更新します
        transform.position = agent.nextPosition;
    }
}

このスクリプトについて少し説明するだけの価値はあります。クリックすると上記のスクリプトに移動するだけでなく、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<Animator> ();
        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 > 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 の重み付けなどで見た目を向上できます。これらを調整してみることは、この設定をさらに学習するのによいでしょう。

エージェントに設定した地点を巡回させる
Unity サービス