最近僕は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
このスクリプトで四隅の目標地点を巡回していくようになるのだが、今回のステージは壁で左右が分割されているため、左のエリアから右のエリアに移動することは出来ないはずだ。
試してみよう。
案の定、左のエリアを当てもなくうろうろする子犬のようになってしまっている。
こんな時こそオフメッシュリンクを使う時だ。
どうやらOffMeshLink
コンポーネントを使えば、二つのTransformを繋げて移動できるようになるらしい。
早速、使ってみよう。
簡単に設定できた。
これを実行すれば、左のエリアから右のエリアに移動できるようになるはずだ。
ああ、うん。
確かに右のエリアに移動できたようだ。
まさか壁を突っ切るとは思わなかったが。
自動生成できるという噂を信じる
先ほどは手動でOffMeshLink
を設定したが、風の噂では自動生成することができるとのことだ。
docs.unity3d.com
ほうー、便利な世の中になったものだ。
では早速試してみようかね。
Jump Distance
こんな感じのステージを新たに作ってみた。
これでNavMeshを焼いてあげればいいのかな。
設定されてないじゃないかOffMeshLink
が!
自動で設定されるなんてうまい話にまんまと騙されちまったぜ。
えっ、焼く際に追加設定がいるって。
飛び越えられる距離をJump Distance
に設定する必要があるのかぃ?
おお、今度はちゃんと自動でOffMeshLink
が追加されたぜ。
実行してみよう。
ああ、うん。繫がっているようだね。
相変わらず突っ切って進んでいくようだが。
Drop Height
先ほどは同じ高さの地面同士の割れ目を飛び越えるOffMeshLink
を自動で生成できた。
ではこのような高低差のある場所のOffMeshLink
は自動で生成できないものか。
考えるまでもなく、Jump Distance
の上にあったDrop Height
がそれっぽい。
ふふふ、予想通りOffMeshLink
が自動生成されたぜ。
……ただ気になるところは、Jump Distance
の時と違って降りる方向にしかOffMeshLink
が設定されていない気がする。
まあ、最近俺は老眼がひどくなっているから、見間違いであろう。
とりあえず実行してみよう。
降りることは出来るが、登ることが出来ない。
老眼のせいではなかったか。
Drop Height
はその名の通り、降りる用のOffMeshLink
しか作られないようだね。
気を付けねば。
俺の方がうまく移動処理が作れることを信じる
これでオフメッシュリンクの設定は終わったのだが……、このままではあまり使い物にならないなぁ。
プログラム的には離れたNavMesh同士が繋がって移動できていることには間違いないのだが、壁などの障害物を無視して進んでいる。
見た目的にはただのバグにしか見えない!
どうやら少し手を入れる必要がありそうだな!
ワープ
最初に作ったステージに戻ろう。
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; }
NavMeshAgent
のAuto Traverse OffMesh Link
を有効にしていると、先ほどの壁を突っ切る動きをまたし始めるので、無効にして俺が作った処理で移動処理を行うようにする。
さあ、実行だ。
瞬間移動するようになった。
壁を突っ切るよりましだが、面白みがない。
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
さあ、新しいコントローラーObjectWarpController
の力を見せてくれ!
ワープっぽくなった。
もう少し演出を強化すればより良くなりそうだが、今回はここまででいいや。
ジャンプ
では次にこのステージの挙動を改良しよう。
今回は飛び越えるようにしたいので、放物線の動きで移動するようにしよう。
放物線の動きについては以前勉強したので、同じようにRigidbody
を使って放物線の動きを再現してみよう。
www.urablog.xyz
NavMeshAgent
とRigidbody
の挙動を同時に扱うことはできないようなので、このオフメッシュリンクの時だけ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
ObjectJumpController
、お前の力を見せてくれ!
うんうん、これで飛び越えているように見えるようになった。
えかったえかった。
そして更なる高みへ
NavMeshの基本的なことは大体わかるようになった。
ただNavMeshにはまだまだ出来ることが多いようだ。
Uniteの発表では、GitHubに更なるNavMeshの機能が公開されているらしい。
github.com
ただもう俺の頭は既に限界だ。
俺の頭の容量は大体4キロバイトなので、これ以上勉強すると地下鉄の乗り方を忘れてしまう。
今回はここまでとしよう。