今回は、Unityで目的地に移動する敵キャラを作ってみようと思います。
まずは敵キャラをAsset Storeで探してインポートしてください。
3Dモデル→人型→その他→Zombie
をインポートしてみました。
インポートしたZombieのプレハブをRig→Humanoidで人型にしてシーンに設置します。
上のようにAnimatorのApply Root Motionのチェックを外しアニメーションの動きを移動値に反映させないようにします。
敵キャラクターにコンポーネントを追加する
現時点ではAnimator Controllerの設定をしていなかったり、ゾンビを動かすCharacter Controllerを追加していないので、石造のように動きません。
まずはCharacter Controllerを設定して、当たり判定の範囲を調整しましょう。
これでスクリプトからCharacter Controllerの機能を使ってゾンビを操作する準備が出来ました。
次はアニメーションの状態と遷移を作成していきます。
Zombieフォルダ内にZombie Controllerという名前のAnimator Controllerを作成します。
Zombie Controllerをダブルクリックしアニメーション遷移画面を表示させます。
上の画像の+を押して、Float型のアニメーションパラメータSpeedを追加し、右クリック→Create State→EmptyでIdleとWalkの二つの状態を作ります。
IdleからWalk、WalkからIdleへ状態を選択して右クリック→Make Transitionしアニメーション遷移を作成します。
それぞれの遷移条件をSpeedが1の時を境に遷移するように設定します。
これでアニメーション遷移とその条件の設定が終わりました。この辺りは
の記事でやったのでそちらを参考にしてください。
敵キャラを動かす方法を考える
それではスクリプトを作成し敵キャラを移動させます。
敵キャラの場合は主人公キャラと違ってキーを押したら動くわけではないので、目的地を設定しそこに移動するようにします。
移動が終わったら次の目的地を設定し、また移動する。
という事を繰り返します。
キャラクターの移動はCharacterControllerの機能を使い目的地に移動させます。
アニメーションの切り替えはAnimatorControllerのアニメーションパラメータを操作し切り替えるようにします。
アニメーションは移動している時は歩くアニメーション、目的地についたらその場でただ立っているアニメーションにします。
今回はゾンビの初期位置をx:17、y:0、z:18にして、ゾンビの目的地をx:25、y:0、z:25にします。
敵キャラを動かすスクリプトを作成する
AssetsフォルダのscriptフォルダにMoveEnemyという名前のスクリプトを作成します。
このMoveEnemyに敵キャラが動くスクリプトを書いていきます。
MoveEnemyスクリプトを敵キャラにドラッグ&ドロップするか、敵キャラのインスペクタ上でAdd Componentからスクリプトの追加をします。
MoveEnemyスクリプトに処理を書いていきます。
まずはフィールド宣言です。
1 2 3 4 5 6 7 8 9 10 11 12 | private CharacterController enemyController; private Animator animator; // 目的地 private Vector3 destination; [SerializeField] private float walkSpeed = 1.0f; // 速度 private Vector3 velocity; // 移動方向 private Vector3 direction; |
enemyControllerはゾンビに取り付けたCharacterControllerを入れます。
animatorはゾンビのAnimatorを入れます。
destinationはゾンビの目的地を入れます。
walkSpeedはゾンビの歩行スピードを入れます。
velocityはゾンビの速度を入れます。
directionは目的地の方向を設定します。
1 2 3 4 5 6 7 8 | void Start () { enemyController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); destination = new Vector3 (25f, 0f, 25f); velocity = Vector3.zero; } |
ゲームオブジェクト開始時に様々な初期処理を実行しておきます。
最初に決めた通り、目的地destinationにnew Vector3 (25f, 0f, 25f)を設定します。
次は移動の処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void Update () { if(enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat("Speed", 2.0f); direction = (destination - transform.position).normalized; transform.LookAt(new Vector3(destination.x, transform.position.y, destination.z)); velocity = direction * walkSpeed; Debug.Log (destination); } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move(velocity * Time.deltaTime); } |
敵キャラが接地(地面の上にいる)していたらアニメーションパラメータのSpeedの値を2.0にしWalkアニメーションに切り替えます。
1.0より大きい値であればなんでもかまわないです。
ただ1.0だとWalk→Idleへの条件とも合致するせいか何も反応しなくなりました。
directionは移動する方向を入れるフィールドで、
(目的地 - 現在のキャラの位置).normalized
で目的地の方向ベクトルが計算出来ます。
.normalizedは正規化した値を出すので距離は最短で方向だけ(単位ベクトル)を取り出す事が出来ます。
この方向に歩くスピード値をかけて移動させていきます。
次にキャラクターを目的地の方向に向かせたいので、
1 2 3 | transform.LookAt(new Vector3(destination.x, transform.position.y, destination.z)); |
とします。
transform.LookAtで指定した位置を向かせる事が出来るので、引数にVector3で向かせたい位置を指定しています。
xとzの値は目的地の値を指定し、yの値だけはキャラクターのy値を指定します。
yは上下方向なので目的地のy値(transform.position.y)を指定すると目的地が山の上にあると、キャラクターが上を向いて進んでいくということになってしまう為にY座標だけはゾンビと同じにしておきます。
これでキャラクターの方向を目的地の方向に向ける事が出来ました。
ちなみにDebug.LogはUnityのConsoleに変数の値がちゃんと入っているかどうか?
の確認に使う事が出来ます。
あとは重力分をVelocityに換算し、CharacterControllerの機能を使って移動させるだけです。
ここまでのソースコードは以下の通り
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 | using UnityEngine; using System.Collections; public class MoveEnemy : MonoBehaviour { private CharacterController enemyController; private Animator animator; // 目的地 private Vector3 destination; // 歩くスピード [SerializeField] private float walkSpeed = 1.0f; // 速度 private Vector3 velocity; // 移動方向 private Vector3 direction; // Use this for initialization void Start () { enemyController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); destination = new Vector3 (25f, 0f, 25f); velocity = Vector3.zero; } // Update is called once per frame void Update () { if(enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat("Speed", 2.0f); direction = (destination - transform.position).normalized; transform.LookAt(new Vector3(destination.x, transform.position.y, destination.z)); velocity = direction * walkSpeed; Debug.Log (destination); } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move(velocity * Time.deltaTime); } } |
敵キャラ移動スクリプトの問題点
ではUnityの実行ボタンを押して確認してみます。
移動は出来ましたが、目的地に到着しても目的地あたりでうごめくのがわかるかと思います。
到着しても知らせる処理を設けておらず、永遠に目的地に移動しようとしてしまいます。
本来であれば到着したらその場でIdleのアニメーションになりただ立っている状態にしたいところです。
敵キャラが目的地についたかどうかの情報を持たせる
それでは目的地に到着した場合、到着した!というフラグ(旗)を立て、その後は移動しないようにします。
新しいフィールドを宣言します。
1 2 3 4 | // 到着フラグ private bool arrived; |
到着したかどうかの判定に使うbool型のフィールドarrivedを宣言します。
bool型は真か偽かの判定に使うものです。
Startメソッド内で初期値としてfalseを入れておきます。最初はもちろん到着してませんからね。
1 2 3 | arrived = false; |
さて次は到着したかどうかの判定部分です。到着したかどうかの判定は目的地と現在地の距離がかなり狭まった時に到着と判定する事にします。
2地点の距離はVector3.Distanceで求められるので、
1 2 3 4 5 6 | if(Vector3.Distance(transform.position, destination) < 0.5f) { arrived = true; animator.SetFloat("Speed", 0.0f); } |
距離が0.5未満になったら到着判定変数にtrueを入れ、アニメーターのSpeed値に0を入れます。
これで到着した場合の処理が完成しました。
高低差を考慮しない場合は
1 2 3 4 5 6 | if(Vector3.Distance(transform.position, new Vector3(destination.x, transform.position.y, destination.z)) < 0.5f) { arrived = true; animator.SetFloat("Speed", 0.0f); } |
のようにY軸をゾンビと合わせます。
到着していた場合はキャラクターの移動はさせないように、到着していない場合のみ移動処理を実行させます。
ここまでの処理を追加したソースコードは以下の通りです。
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 53 | using UnityEngine; using System.Collections; public class MoveEnemy : MonoBehaviour { private CharacterController enemyController; private Animator animator; // 目的地 private Vector3 destination; // 歩くスピード [SerializeField] private float walkSpeed = 1.0f; // 速度 private Vector3 velocity; // 移動方向 private Vector3 direction; // 到着フラグ private bool arrived; // Use this for initialization void Start () { enemyController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); destination = new Vector3 (25f, 0f, 25f); velocity = Vector3.zero; arrived = false; } // Update is called once per frame void Update () { if (!arrived) { if (enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat ("Speed", 2.0f); direction = (destination - transform.position).normalized; transform.LookAt (new Vector3 (destination.x, transform.position.y, destination.z)); velocity = direction * walkSpeed; Debug.Log (destination); } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move (velocity * Time.deltaTime); // 目的地に到着したかどうかの判定 if (Vector3.Distance (transform.position, destination) < 0.5f) { arrived = true; animator.SetFloat ("Speed", 0.0f); } } } } |
arrivedがtrueになっていない時だけ処理をします。
これで指定した位置に敵キャラが移動するようになりました。
しかし、このスクリプトではいちいち目的地を設定し直さないといけなくなってしまい使い勝手がよくありません。
なのでnew Vector3(25f, 0f, 25f)としている場所をランダムに設定出来るようにしてみます。
目的地をランダムに設定出来るようにする
スタート位置(敵キャラを最初に置いた場所)から一定の範囲内にランダムに目的地を設定する事が出来るようにします。
最初にスタート位置を記憶しておきます。
スタート位置フィールドはstartPositionとし、Startメソッド内で初期位置を入れておきます。
1 2 3 | startPosition = transform.position; |
Startメソッドはオブジェクトが現れた時に1回(厳密にはアクティブになった時1回)実行されるので、その時のオブジェクトの位置、つまりシーンに配置した位置(transform.position)を入れておきます。
次にランダム値を作ります。
Random.insideUnitCircleで半径1以内のVector2のランダムな点を作成する事が出来るので、半径8以内であれば、
1 2 3 | Random.insideUnitCircle * 8; |
となります。スタートポイントにランダムな点を足せば決められた範囲内で目的地を作る事が出来ます。
1 2 3 4 | var randDestination = Random.insideUnitCircle * 8; destination = startPosition + new Vector3(randDestination.x, 0, randDestination.y); |
ランダムな点はVector2の点で得られるので、それをVector3に適用します。
Unityではyは上下、zは前後の動きなので、Vector2のy値をzに指定します。
Random.insideUnitCircleを使わずともランダム値を得るRandom.range(0.0f, 8.0f)を使って二つの値を作り、それを設定する事でも同じような結果が得られます。
以下全文です。
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 53 54 55 56 57 | using UnityEngine; using System.Collections; public class MoveEnemy : MonoBehaviour { private CharacterController enemyController; private Animator animator; // 目的地 private Vector3 destination; // 歩くスピード [SerializeField] private float walkSpeed = 1.0f; // 速度 private Vector3 velocity; // 移動方向 private Vector3 direction; // 到着フラグ private bool arrived; // スタート位置 private Vector3 startPosition; // Use this for initialization void Start () { enemyController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); var randDestination = Random.insideUnitCircle * 8; destination = startPosition + new Vector3(randDestination.x, 0, randDestination.y); velocity = Vector3.zero; arrived = false; startPosition = transform.position; } // Update is called once per frame void Update () { if (!arrived) { if (enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat ("Speed", 2.0f); direction = (destination - transform.position).normalized; transform.LookAt (new Vector3 (destination.x, transform.position.y, destination.z)); velocity = direction * walkSpeed; Debug.Log (destination); } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move (velocity * Time.deltaTime); // 目的地に到着したかどうかの判定 if (Vector3.Distance (transform.position, destination) < 0.5f) { arrived = true; animator.SetFloat ("Speed", 0.0f); } } } } |
出来上がったので、Unityの実行ボタンを押して確認してみましょう。
ゾンビが目的地に移動し、目的地近くになったらその場でただ立っている状態になります。
終わりに
敵キャラが登場したら登場した位置を基点にランダムな目的地に移動するキャラクターが出来ました。
次回は目的地に移動したら数秒待機し別の目的地を設定し移動を開始するスクリプトを組みます。
諸事情によりスクリプトを分ける作業が入ります。