今回はUnityのナビゲーション機能を使って敵キャラクターを移動させてみます。
今回の機能は以前作成した記事に処理を追加・修正していますので、この機能は何?と思ったら以下の記事で機能作成の流れを確認してみてください。
スクリプトで目的地を設定し移動させる事は可能ですが、大草原のように周りに何もない時はいいですが、木や建物があった場合、壁などにぶつかりながら無理やり移動せざるを得ない、もしくは壁に突き進むという状態になりました。
そこでナビゲーションの機能を使って敵キャラクターをスムーズに移動出来るようにします。
移動出来る場所をNavMeshして作成し、移動するキャラクターにNavMeshAgentというコンポーネントを追加します。
目的地を設定すれば、壁にぶつかりながら移動せず、最適な移動をしてくれます。
↑のような感じです。
CharacterControllerを使った移動とほとんど変わらないように見えますが、途中に壁等の障害物がある時はそれを回避し進めるようになります。
NavMeshを作成しキャラクターが移動出来る場所を作成
まずはNavMeshを作成し、移動できる場所を作成します。Terrainで作ったフィールドを選択しインスペクタでStaticにチェックを入れます。
Terrainに関しては
を参照してください。
Navigationウインドウ
UnityメニューからWindow→AI→Navigationを選択します。
Navigationの使い方について少し見ていきます。
Agentタブ
NavigationウインドウのAgentタブを選択するとエージェントの設定をすることが出来ます。
デフォルトではHumanoidが用意されており新しくAgent Typesで+を押せばエージェントの設定を追加することが出来ます。
Radiusはエージェントの半径
Heightはエージェントの高さ
Step Heightは飛び越えられる高さ
Slopeは登れる坂の角度
です。
ここで注意が必要なのがデフォルトのHumanoid以外にエージェントタイプを追加したとしてもNavigationウインドウでBakeしたフィールドには適用されないようです(2019/06/21時点)。
Humanoid以外で作成したエージェントタイプが適用されるのは別途インポートする必要があるランタイムにNavMeshベイクが出来るコンポーネントを使った時だけのようです。
Areasタブ
Areasタブはナビゲーションのコストを設定することが出来ます。
コストを上げるとそのエリアを設定したゲームオブジェクトを移動する時はコストがかかる為、別のルートを使った方が最適なルートとなります。
Bakeタブ
BakeタブでNavigation Staticにチェックされたゲームオブジェクトをベイクすることが出来ます。
NavigationウインドウのBakeでパラメータを設定し、下のBakeを押します。
Bakeのパラメータには移動できる範囲を設定します。
Agent RadiusはNavMeshしたフィールドを移動するエージェントの半径
Agent Heightはエージェントの高さ
Max Slopeは移動出来る角度
Step Heightは超えられる段差
CharacterControllerで設定するパラメータと似たようなものですね。
ここで設定するAgent RadiusとAgent Heightは後ほどキャラクターに設定するNavMeshAgentコンポーネントの
サイズと合わせた方がいいかもしれません。
ここでの設定がNavigation Staticオブジェクトのベイクに反映されAgentタブの設定は先ほど紹介したNavMeshComponentsの設定で使われるのかもしれません。ちょっとややこしいですね・・・(^_^;)
Generated Off Mesh Linksの
Drop Heightはこの値以下であれば飛び降りる為のオフメッシュリンクを作成します。
Jump Distanceはこの値以下であれば飛び越えるオフメッシュリンクを作成します。
オフメッシュリンクはナビゲーションエリアが切れている部分を移動する為のリンク地点です。
Bakeを押すとフィールドに青い部分が表示されます。
この青い部分がNavMeshAgentを設定したキャラクターが移動出来る個所になります。
(もちろんキャラクターの移動できる角度や段差が成立しないと高い所には移動できませんが)
青い部分はNavigation AreaでWalkableが設定されているエリアで、NavigationウインドウのObjectタブでゲームオブジェクトのNavigation Areaを変更する事が出来、そこで、別のエリアを指定してからBakeすると違う色が表示されます。
Objectタブ
Objectタブではヒエラルキー上で選択しているゲームオブジェクトのAreaの設定が出来ます。
Navigation Staticにチェックを入れると選択しているゲームオブジェクトのNavigation Staticにチェックを入れます。
Generate OffmeshLinksにチェックを入れるとベイクした時にオフメッシュリンクを作成します。オフメッシュリンクは連続していない地点を繋ぐリンクのようなものです。
Navigation Areaでは選択しているゲームオブジェクトのエリアを指定します。
これでNavigationウインドウの使い方が分かったので実際にエリアを設定してNavMeshをベイクしてみます。
NavigationウインドウのAreasタブで新たにHard Walkを作成しCostを50にします。
Terrainで作ったフィールドは既にインスペクタでStaticにチェックを入れているので、ヒエラルキー上のTerrainを選択した状態でNavigationウインドウのObjectタブを選択し、AreaにWalkableを指定します。
次にいくつかヒエラルキー上にCubeを配置しインスペクタのStaticにチェックを入れ、選択した状態でObjectタブを押しAreaにHard Walkを指定します。
NavigationウインドウのBakeタブでBakeボタンを押してNavMeshをベイクします。
これでキャラクターが移動出来る場所を設定出来ました。
Terrainの地面はWalkableエリアなので青色、Cubeの地面はHard Costエリアなので紫色に色分けされています。
上の画像ではCubeを3つ作成し、2つはベイクした時のエージェントの設定でそのまま移動出来る場所に配置し、1つはエージェントが地続きで移動出来ない少し高めに起きました。
高めに置いたCubeが一番左にあるものでオフメッシュリンクが自動で作成されています。
NavMeshした場所を移動するキャラクターの作成
次は敵キャラクターにNavMeshAgentコンポーネントを追加します。
上のように敵キャラクターにNavMeshAgentを追加します。
Agent Typeでエージェントのタイプ
Base Offsetでエージェントの位置のオフセット
Speedでエージェントの最高速度
Angular Speedで最高回転速度
Accelerationは最大加速度
Stopping Distanceは目的地とどれだけ近づいたら止めるかの距離
Auto Brakingにチェックを入れると目的地に近づいたら減速させます。
Radiusは障害物を回避する半径
Heightは障害物を回避する高さ
Qualityは回避する品質の高さ
Priorityは指定した数値より低いエージェントの回避を無視します。
Auto Traverse Off MeshLinkにチェックを入れるとオフメッシュリンクのポイントに来たら自動でオフメッシュリンクを移動します。
Auto Repathにチェックを入れると部分的な移動経路の目的地についたら自動で次の経路を探索します。
Area Maskはこのエージェントがどのエリアを移動可能にするかを選択出来ます。
敵には新しくCapsule Colliderを取り付けIs Triggerのチェックを外し物理的に衝突するようにします。
コライダのサイズはCharacterControllerのコライダのサイズと同じにします。
さらにRigidbodyコンポーネントも取り付けIs Kinematicにチェックを入れます。
今まで敵に取り付けていたCharacterControllerの機能は使わなくなりますので削除します。
主人公が敵キャラクターを武器で攻撃した時のOnTriggerEnterイベントを発生させるには、
CharacterControllerがついているか、何らかのコライダ+Rigidbodyがついているキャラクターでないとダメっぽいです。
キャラクターを移動させるスクリプトを修正する
敵キャラクターを操作していたEnemyスクリプトの中身を変更します。
CharacterControllerを使ったキャラクターの移動は
を参照してください。
今まではCharacterControllerのMove機能を使ってキャラクターを動かしていましたが、そこをNavMeshAgentの機能を使って移動するように修正していきます。
フィールドとStartメソッド
JavaScriptでスクリプトを組んでいる場合は問題ないですが、C#で記述している場合にUnityが5.5バージョン?以降のNavMesh系のクラスはUnityEngine.AIパッケージ以下にあるので、
using UnityEngine.AI;
というusingディレクティブを入れておくとスクリプト中で
UnityEngine.AI.NavMeshAgent → NavMeshAgent
という簡単な記述が出来ます。
それ以前のバージョンであればusingディレクティブを追加する必要はありません。
移動スクリプトはそれほど変わらず、目的地を設定しそこに移動するという機能をそのままNavMeshAgentの機能へと移行させるだけです。
1 2 3 4 5 6 7 | // エージェント private NavMeshAgent navMeshAgent; // 回転スピード [SerializeField] private float rotateSpeed = 45f; |
NavMeshAgentを入れておくフィールドを宣言します。
攻撃時に主人公キャラクターの方向を徐々に向かせる為、回転スピードのフィールドを宣言します。
Startメソッド内ではNavMeshAgentコンポーネントの取得の処理を追加します。
Startメソッド内でSetStateメソッドを呼び出している場合はSetStateメソッドの内でNavMeshAgentコンポーネントを使用している為、SetStateメソッドの呼び出しの前に処理を入れる必要があります。
1 2 3 4 | navMeshAgent = GetComponent <NavMeshAgent> (); SetState(EnemyState.Wait); |
敵キャラ移動処理
次は移動処理部分の変更です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | void Update() { if(state == EnemyState.Dead) { return; } // 見回りまたはキャラクターを追いかける状態 if (state == EnemyState.Walk || state == EnemyState.Chase) { // キャラクターを追いかける状態であればキャラクターの目的地を再設定 if (state == EnemyState.Chase) { setPosition.SetDestination(playerTransform.position); navMeshAgent.SetDestination(setPosition.GetDestination()); } // エージェントの潜在的な速さを設定 animator.SetFloat("Speed", navMeshAgent.desiredVelocity.magnitude); if (state == EnemyState.Walk) { // 目的地に到着したかどうかの判定 if (navMeshAgent.remainingDistance < 0.1f) { SetState(EnemyState.Wait); animator.SetFloat("Speed", 0f); } } else if (state == EnemyState.Chase) { // 攻撃する距離だったら攻撃 if (navMeshAgent.remainingDistance < 1.2f) { SetState(EnemyState.Attack); } } // 到着していたら一定時間待つ } else if (state == EnemyState.Wait) { elapsedTime += Time.deltaTime; // 待ち時間を越えたら次の目的地を設定 if (elapsedTime > waitTime) { SetState(EnemyState.Walk); } // 攻撃後のフリーズ状態 } else if (state == EnemyState.Freeze) { elapsedTime += Time.deltaTime; if (elapsedTime > freezeTimeAfterAttack) { SetState(EnemyState.Walk); } } else if (state == EnemyState.Attack) { // プレイヤーの方向を取得 var playerDirection = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z) - transform.position; // 敵の向きをプレイヤーの方向に少しづつ変える var dir = Vector3.RotateTowards(transform.forward, playerDirection, rotateSpeed * Time.deltaTime, 0f); // 算出した方向の角度を敵の角度に設定 transform.rotation = Quaternion.LookRotation(dir); } } |
移動処理では今までの複雑な処理がいらなくなり、状態がMyState.Chaseだった時に主人公キャラを目的地に設定します。
1 2 3 | navMeshAgent.SetDestination(目的地のVector3); |
でナビゲーションエージェントの目的地を設定する事が出来ます。
Agentは目的地が設定されると移動を開始するので複雑な処理が必要なくなります。
アニメーションパラメータのSpeedにはagent.disiredVelocity.magnitudeを渡す事にします。
エージェントの現在の速度はagent.velocityでも得られますが今回はagent.disiredVelocityで回避行動による潜在的な速度を考慮したものを使う事にします。
Speedにはfloat値を渡す必要があるのでmagnitudeを使って速度を速さに変換して渡しています。
敵が目的地に到着したり、主人公キャラを攻撃する距離はnavMeshAgent.remainingDistanceを使用するよう変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (state == EnemyState.Walk) { // 目的地に到着したかどうかの判定 if (navMeshAgent.remainingDistance < 0.1f) { SetState(EnemyState.Wait); animator.SetFloat("Speed", 0f); } } else if (state == EnemyState.Chase) { // 攻撃する距離だったら攻撃 if (navMeshAgent.remainingDistance < 1.2f) { SetState(EnemyState.Attack); } } |
agent.remainingDistanceでエージェントの目的地との距離を取得出来ます。
キャラクターが攻撃状態になったら主人公の方向を徐々に向かせるようにします。
1 2 3 4 5 6 7 8 9 10 | } else if (state == EnemyState.Attack) { // プレイヤーの方向を取得 var playerDirection = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z) - transform.position; // 敵の向きをプレイヤーの方向に少しづつ変える var dir = Vector3.RotateTowards (transform.forward, playerDirection, rotateSpeed * Time.deltaTime, 0f); // 算出した方向の角度を敵の角度に設定 transform.rotation = Quaternion.LookRotation (dir); } |
まずは相手の位置から敵キャラの位置を引いて主人公キャラクターの方向を取得します。
ここで相手方の位置のY軸だけ自身(敵)のY軸の位置にします。
こうすることでY軸以外の回転を行わないように出来ます。
プレイヤーの方向を取得出来たらVector3.RotateTowardsを使って敵の方向をプレイヤーの方向へと徐々に向かせた方向dirを取得します。
最後にQuaternion.LookRotationを使って方向の角度を計算し、それを敵の角度に代入しています。
敵の攻撃時に主人公がその場所から逃げようとするとその方向に向きながら攻撃するのでより敵の攻撃が当たりやすくなります。
敵キャラ状態変更メソッドSetStateの修正
次に状態変更メソッドSetStateを変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // 敵キャラクターの状態変更メソッド public void SetState(EnemyState tempState, Transform targetObj = null) { if(state == EnemyState.Dead) { return; } state = tempState; velocity = Vector3.zero; if (tempState == EnemyState.Walk) { arrived = false; elapsedTime = 0f; setPosition.CreateRandomPosition(); navMeshAgent.SetDestination(setPosition.GetDestination()); navMeshAgent.isStopped = false; } else if (tempState == EnemyState.Chase) { // 待機状態から追いかける場合もあるのでOff arrived = false; // 追いかける対象をセット playerTransform = targetObj; navMeshAgent.SetDestination(playerTransform.position); navMeshAgent.isStopped = false; } else if (tempState == EnemyState.Wait) { elapsedTime = 0f; arrived = true; animator.SetFloat("Speed", 0f); } else if (tempState == EnemyState.Attack) { animator.SetFloat("Speed", 0f); animator.SetBool("Attack", true); audioSource.PlayOneShot(attackSound); navMeshAgent.isStopped = true; } else if (tempState == EnemyState.Freeze) { elapsedTime = 0f; animator.SetFloat("Speed", 0f); animator.SetBool("Attack", false); } else if (tempState == EnemyState.Damage) { animator.ResetTrigger("Attack"); animator.SetTrigger("Damage"); navMeshAgent.isStopped = true; } else if (tempState == EnemyState.Dead) { animator.SetTrigger("Dead"); Destroy(this.gameObject, 3f); navMeshAgent.isStopped = true; } } |
Unity5.6以前のバージョンではそれぞれの状態へと変更した時にエージェントの移動を止める必要がある場合は
1 2 3 | navMeshAgent.Stop(); |
を使用してエージェントを止めます。
移動を再開させる為に、
1 2 3 | navMeshAgent.Resume(); |
を使って移動の再開をしています。
Unity5.6以降のバージョンではStopとResumeではなく
1 2 3 | agent.isStopped = true; |
↑のようにisStoppedプロパティをオン・オフする事でエージェントの停止・再開をします。
Stop(停止)に対応する場合はisStoppedにtrueを入れ、Resume(再開)に対応する場合はisStoppedにfalseを入れます。
それでは敵キャラの修正が終わったのでUnityの実行ボタンを押して試してみましょう。
敵が主人公をすり抜けるようになった?
今までちゃんとしていたのに、敵キャラが急に主人公キャラにめり込むようになってしまった。という方もいるかもしれません。
この原因は敵の移動機能をCharacterControllerからNavMeshAgentにしたことと敵の衝突判定のコライダをCharacterControllerを使用している場合に起こります。
敵キャラクターの当たり判定に使っていたCharacterControllerコンポーネントを削除し、Capsule Colliderで作成しIs Triggerのチェックを外し、Rigidbodyコンポーネントを取り付けてIs Kinematicにチェックを入れれば主人公に衝突するようになります。
NavMeshAgentが障害物として認識するようになる機能を追加する
Agentに設定したキャラクターが通れる場所はNavMeshがベイクされている所だけです。
ですがNavMeshはあらかじめStaticにした動かないゲームオブジェクトを対象にしているので動くゲームオブジェクトの場合はNavMeshをベイクしても元の位置でしか反映されません。
なのでゲームオブジェクトが動いてもあらかじめベイクされた部分をエージェントが動くことが出来て動くオブジェクトと重なってしまいます(そもそもStaticにしたゲームオブジェクトは動かさない)。
そんな動くゲームオブジェクトをリアルタイムに障害物として認識させる機能がNavMeshObstacleコンポーネントです。
これを動くゲームオブジェクトに取り付けて試してみましょう。
ヒエラルキー上にCubeを作成し、新しくSimpleCubeMoveスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class SimpleCubeMove : MonoBehaviour { private Vector3 basePos; // Start is called before the first frame update void Start() { basePos = transform.position; } // Update is called once per frame void Update() { transform.position = basePos + new Vector3(Mathf.Sin(Time.time) * 3f, 0f, 0f); } } |
これでCubeがX軸をいったりきたりします。
CubeのインスペクタのAdd ComponentからNavigation→Nav Mesh Obstacleを取り付けます。
Carveの設定をチェックするとスムーズに避けるようになります。
Carve Only Stationaryのチェックを外すとゲームオブジェクトが動いていても有効になります。
これでObstacleの設定が終わったので、実行してみましょう。
Navigationウインドウを開いた状態でUnityを実行し、シーンビューでCubeを見てみます。
上のようにCubeにはNavMeshObstacleを取り付けているので動いた部分のNavMeshのベイクされた部分が排除されています。
ナビゲーション機能を使ってみての感想
これでナビゲーションを使って敵キャラクターを移動させる事が出来るようになりました。
注意点としては、NavMeshで指定するオブジェクトをStaticにしてBakeをする事です。
建物等がStaticされていないとすり抜けていってしまいます。
動かない障害物を作成する場合は必ずStaticにしてBakeします。
BakeされたNavMeshファイルはそのシーンと同じ階層にシーン名のフォルダが作成されその中に入るようです。
いやぁ、ナビゲーション機能は本当に便利ですね!
設定すれば自然なルートで移動してくれますので、敵が巡回しているようなAIが簡単に作成出来ます。
ナビゲーション機能では他にも使いたい機能があるので、そちらも使えるようになったら記事にしたいと思います。
もナビゲーション機能を扱った記事になります。