以前、敵キャラクターが最初に登場した位置からランダムな位置を歩き回る機能を作成しましたが、
CharacterControllerで敵キャラクターを動かす場合は、目的地とキャラクターとの間に壁があると壁にずっと衝突して移動出来なくなってしまいます。
そこで今回は敵キャラクターの前に壁等の特定のゲームオブジェクトがあってずっと衝突する可能性がある場合は敵キャラクターの目的地を変更するようにします。
そんなに複雑なAIというものでもなく単純に壁が前にあったら別の目的地を設定してちょっと頭良さげに見えるようにするという簡易的な方法になります。
かえって頭の悪い移動の仕方をする可能性も十分ありますけどね・・・・(^_^;)
今回の機能を作成すると、
上のような感じのものが出来上がります。
Terrainで地面を作成する
まずは敵キャラクターが移動する地面をTerrainで作り、所々盛り上げて敵キャラクターより背の高い山を作ってください。
このキャラクターより高い山が前にあったら別の目的地に変更するようにします。
Terrainの使い方は
を参照してください。
わたくしは
上のような感じの地面を作りました。
地面が出来たらインスペクタのLayerでFieldを作成し、設定しておきます。
Terrainを使って障害物を作成せずとも普通のゲームオブジェクトを設置して、そのゲームオブジェクトのLayerをFieldにしても同じように出来ます。
敵キャラクターの作成
敵キャラクターにはCharacterControllerの取り付けとコライダの調整、AnimatorControllerの作成とAnimatorへの設定を行っておきます。
ここら辺は、
を参照してください。
AnimatorControllerのアニメーションパラメータには、Float型のWalkSpeedを作成し、Idle→Walkの条件はWalkSpeedがGreaterで0.1、Walk→Idleの条件はWalkSpeedがLessで0.1を設定しておきます。
敵キャラクターの子要素にCreate Emptyで空のゲームオブジェクトを作成し、名前をRayTransformとします。
RayTransformの位置と向きは
上のようにします。
ここでRayTransformのLocalの向きが違うとうまくいきません。
敵キャラクターのスクリプト
今回作成する敵キャラクターのスクリプトでは最初に登場した位置から一定の範囲内でランダムな位置を取得し、そこを目的地に設定する事にします。
その為、最初に登場した位置から一定の距離の範囲内しか移動しません。
敵キャラクターにはTrollScriptという名前のスクリプトを作成し取り付けます。
スクリプト名は自由に変えてください。
スクリプトが長いので少しづつ解説します。
宣言部分
まずは宣言部分です。
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 System.Collections; using System.Collections.Generic; using UnityEngine; public class TrollScript : MonoBehaviour { public enum TrollState { idle, normal, chase } private CharacterController characterController; private Animator animator; // トロールが登場した最初の位置 private Vector3 defaultPos; // トロールの状態 private TrollState trollState = TrollState.idle; // 目的地 private Vector3 destination; // 移動範囲 [SerializeField] private float movementRange = 20f; // 移動速度 private Vector3 velocity = Vector3.zero; // 歩くスピード [SerializeField] private float walkSpeed = 1f; // 向きを回転する速さ [SerializeField] private float rotateSpeed = 2f; // idle状態の経過時間 private float elapsedTimeOfIdleState = 0f; // idle状態で留まる時間 [SerializeField] private float timeToStayInIdle = 3f; // 壁との接触を判定するレイを飛ばす場所 [SerializeField] private Transform rayTransform; // レイを飛ばす距離 [SerializeField] private float rayDistance = 2f; // 最初に壁に衝突してからの経過時間 private float elapsedCollisionWall = Mathf.Infinity; // 最初に壁に衝突してから次に判定するまでの時間 [SerializeField] private float avoidanceTimeCollisionWall = 5f; // Use this for initialization void Start () { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); defaultPos = transform.position; SetRandomDestinationPoint(); } |
TrollStateでは敵(トロール)の状態を宣言しています。
defaultPosはトロールが最初にいる位置を記憶しておきます。
trollStateはトロールの状態を保持します。
destinationはトロールの移動する目的地を保持します。
movementRangeはトロールが最初の位置から移動出来る距離を指定します。
walkSpeedは歩く速さ、rotateSpeedは目的地の方向に向く速さを指定します。
elapsedTimeOfIdleStateはIdle状態になってから経過した時間を入れます。
timeToStayInIdleはIdle状態になってから何秒経ったらnormal状態にするかを指定します。
rayTransformはトロールの前方に壁があるかどうかを調べる時にレイを飛ばす位置を設定します。
rayDistanceは壁を調べる時に飛ばす例の距離を指定します。
elapsedCollisionWall最初に壁を検知してからの経過時間を入れます。最初は必ず壁を検知させる為、Mathf.Infinityで最大値を入れておきます。
avoidanceTimeCollisionWallは最初に壁を検知してから次に検知出来るようにするまでの時間です。
Startメソッドではコンポーネントの取得とdefaultPosの設定、SetRandomDestinationPointメソッドを呼び出して目的地をランダムに設定します。
SetRandomDestinationPointメソッドはこの後作成します。
SetRandomDestinationPointメソッド
SetRandomDestinationPointメソッドはランダムに目的地を設定するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 目的地を設定する void SetRandomDestinationPoint() { // 最初の位置から有効範囲内のランダム位置を取得 var randomPos = defaultPos + Random.insideUnitSphere * movementRange; var ray = new Ray(randomPos + Vector3.up * 10f, Vector3.down); RaycastHit hit; // 目的地が地面になるように再設定 if (Physics.Raycast(ray, out hit, 100f, LayerMask.GetMask("Field"))) { destination = hit.point; } Debug.Log("次の目的地" + destination); } |
Random.insideUnitSphereでSphere(Vector3)のランダムな点(0~1の間)を取得出来ます。
それにmovementRangeをかけることでその範囲内のランダム値が得られます。
目的地は敵が最初にいた位置から一定の範囲内を移動出来るようにするので、最初に登場した位置 + ランダムな位置にします。
目的地が地面の位置になるように『求めた目的地の上10mの地点』から下向きのレイを求め(ray)、Fieldレイヤーが設定された地面と接触した位置(hit.point)を目的地に設定する事にします。
SetStateメソッド
SetStateメソッドでは引数で受け取った状態にします。
1 2 3 4 5 6 7 8 9 10 | void SetState(TrollState tmpState) { trollState = tmpState; if (trollState == TrollState.idle) { velocity = new Vector3(0f, velocity.y, 0f); animator.SetFloat("WalkSpeed", 0f); SetRandomDestinationPoint(); } } |
idle状態になった時は重力だけ働かせ、アニメーションパラメータのWalkSpeedを0にしてIdle状態に遷移させます。
さらに次の目的地をランダムに決定します。
Updateメソッド
Updateメソッドがメインの処理になります。
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 | void Update () { if (trollState == TrollState.idle) { elapsedTimeOfIdleState += Time.deltaTime; // 一定時間が経過したらnormal状態にする if(elapsedTimeOfIdleState >= timeToStayInIdle) { SetState(TrollState.normal); elapsedTimeOfIdleState = 0f; } } else if(trollState == TrollState.normal) { // 通常移動処理 if (characterController.isGrounded) { velocity = Vector3.zero; var direction = (destination - transform.position).normalized; animator.SetFloat("WalkSpeed", direction.magnitude); var targetRot = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(destination - transform.position), Time.deltaTime * rotateSpeed); transform.rotation = Quaternion.Euler(transform.eulerAngles.x, targetRot.eulerAngles.y, transform.eulerAngles.z); velocity = transform.forward * walkSpeed; } // 目的地に着いたらidle状態にする if (Vector3.Distance(transform.position, destination) < 0.5f) { SetState(TrollState.idle); } // レイを視覚化して表示 Debug.DrawLine(rayTransform.position, rayTransform.position + rayTransform.forward * rayDistance, Color.red); Debug.DrawLine(rayTransform.position, rayTransform.position + (rayTransform.forward + rayTransform.right).normalized * rayDistance, Color.blue); Debug.DrawLine(rayTransform.position, rayTransform.position + (rayTransform.forward - rayTransform.right).normalized * rayDistance, Color.yellow); // 敵キャラから目的地までのレイを表示 Debug.DrawLine(rayTransform.position, destination, Color.green); // 壁に突入している時 if (elapsedCollisionWall >= avoidanceTimeCollisionWall) { if (Physics.Linecast(rayTransform.position, rayTransform.position + rayTransform.forward * rayDistance, LayerMask.GetMask("Field")) || Physics.Linecast(rayTransform.position, rayTransform.position + (rayTransform.forward + rayTransform.right).normalized * rayDistance, LayerMask.GetMask("Field")) || Physics.Linecast(rayTransform.position, rayTransform.position + (rayTransform.forward - rayTransform.right).normalized * rayDistance, LayerMask.GetMask("Field")) ) { Debug.Log("壁と接触"); elapsedCollisionWall = 0f; SetState(TrollState.idle); } } // 一旦目的地を再設定したら一定の回避時間を設ける elapsedCollisionWall += Time.deltaTime; if (elapsedCollisionWall >= avoidanceTimeCollisionWall) { elapsedCollisionWall = avoidanceTimeCollisionWall; } } velocity.y = Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } |
敵の状態trollStateがTrollState.idleの時は一定時間その場で待機した後TrollState.normalに変更します。
TrollState.normal状態の時は敵キャラクターを目的地の方向に徐々に向けさせ、移動させます。
目的地に十分近づいたらidle状態にします。
その後レイを視覚化していますが、これは設定したrayTransformの位置から前方にrayDistanceの距離だけのレイ、右斜め前のレイ、左斜め前のレイを視覚化しています。
また、レイを飛ばす位置から目的地までのレイも視覚化しています。
次のif文ではelapsedCollisionWallがavoidanceTimeCollisionWallの時間を超えている時で3方向に飛ばしたレイと接触していた時は前方に壁があって進めない為、idle状態にします。
飛ばすレイは前方と右斜め前、左斜め前のレイを飛ばしある程度の角度をカバー出来るようにしています。
elapsedCollisionWallは時間をどんどん足していくので、avoidanceTimeCollisionWallの値を超えたらavoidanceTimeCollisionWallの値を設定するようにしています。
これで完成です!
終わりに
今回の機能はCharacterControllerを使って敵キャラクターを動かす場合の壁への衝突問題を簡易的に解決する為の機能です。
簡易的なものなので、狭い場所に入ってしまうと永遠にその場所から出てこられない可能性もあります。
また一度壁を認識した場合は次の移動ですぐに壁を認識しないように回避する時間を設けている為、その間は壁に衝突し続けることになります。
敵キャラクターが移動する場所をあらかじめ設定しておいたり、移動する範囲を狭めておけばこの機能を取り付ける必要はないかもしれません。
もっと頭の良い移動をさせたい!
という場合はやはりナビゲーション機能を使うと便利ですね。(^^)/