このページでは、ナビゲーションシステムを使ってヒューマノイド型キャラクターを移動する方法について説明します。
それを行うために、カスタムのスクリプトと共に、Unity のビルトインのアニメーションシステムとナビゲーションシステムを使います。
ここでの説明は、Unity と Mecanim アニメーションシステムの基本操作に慣れていることを前提としています。
サンプルプロジェクトを利用できるので、スクリプトを加えたり、アニメーションやアニメーションコントローラを設定することを 0 から行う必要はありません。
応答性と汎用性の高いアニメーションコントローラーを取得し広い範囲を動けるように、様々な方向に移動するアニメーションのセットが必要です。これは「Strafe」セットと呼ばれることもあります。
移動するアニメーションに加えて、立っているキャラクターのアニメーションも必要です。
まず、2D ブレンドツリーで Strafe セットを配置します。Blend Type で 2D Simple Directional を選択し Compute Positions > Velocity XZ を選択し、アニメーションを配置してください。
ブレンディングの制御を行うために、2 つの Float パラメーター velx と vely を加え、それらをブレンドツリーに割り当てます。
ここでは、それぞれ異なる速度を持っている 7 つの Run (走行) アニメーションクリップを配置します。前方 (正面と左/右前方) と後方 (後方と左/右後方) に加えて、ある一点上で走るアニメーションクリップを使用します。下の 2D ブレンドマップでは、一点上で走るアニメーションクリップ (Run_Spot アニメーションクリップ) が中央でハイライトされています。一点上でアニメーションを走らせる理由は 2 つあります。1 つは他のアニメーションとブレンドする時に走行するスタイルを維持するためです。2 つ目はブレンドするときにアニメーションで足が滑っているように見えるのを防ぐためです。
次に、休止 (Idle) のアニメーションクリップをそのノード (Idle) に加えます。今度は、2 つの分離したアニメーションステートを2 つの遷移と組み合わせます。
Move (移動) と Idle (休止) 状態の切り替えを制御するために、boolean 型制御パラメーター move を加えます。その後、Transitions の Has Exit Time プロパティを無効にします。これにより、アニメーション中にいつでも遷移の発生が可能です。遷移の反応を良くするために、遷移時間を約 0.10 秒あたりに設定します。
新しく作成したアニメーションコントローラーを動かしたいキャラクターに配置します。
再生ボタンを押してキャラクターを Hierarchy ウィンドウ で選んでください。これで、Animator ウィンドウ のアニメーションの値を手動で制御することができ、動きの状態と速度を変更ができます。
次のステップは、アニメーションのパラメーターを制御する他の手段を作成についてです。
Place a NavMesh Agent component on the character and adjust the radius, height and to match the character - additionally change the speed property to match the maximum speed in the animation blend tree.
キャラクターを配置したシーンの 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 スクリプト同様、 Animator と NavMeshAgent コンポーネントがアタッチされたキャラクターに設定されます。
まず、このスクリプトはキャラクターの位置を自動更新しないようにエージェントに指示します。スクリプトの最後で位置の更新を処理します。向き (方向) はエージェントによって更新されます。
アニメーションのブレンドはエージェントの速度を読み込むことで制御されます。速度は、キャラクターの向きに基づいた相対速度に変換され、平滑化されます。変換された水平方向速度のコンポーネントは、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 > agent.radius)
transform.position = agent.nextPosition - 0.9f*worldDeltaPosition;
または、エージェントをキャラクターについていくようにする場合は以下を追加します。
// エージェントをキャラクターの方に引っ張ります
if (worldDeltaPosition.magnitude > agent.radius)
agent.nextPosition = transform.position + 0.9f*worldDeltaPosition;
どの方法が一番よく動作するかは、それぞれのケースによります。
ここでは、ナビゲーションシステムを使用して移動し、それに応じたアニメーションを行うキャラクターを設定しました。ブレンドの回数や LookAt の重み付けなどで見た目を向上できます。これらを調整してみることは、この設定をさらに学習するのによいでしょう。