今回は主人公キャラが敵キャラに近づいたら敵キャラが主人公に近づいてくるようにしたいと思います。
前回、敵キャラクターが見回りをするというスクリプトを組みました。
まずはスクリプトをどうやって組むか考えてみます。
敵キャラクターに『主人公キャラクターが近づいてきた』、というのがわからなければいけません。
これにはコライダ(接触判定に使う)を使います。敵が持つコライダと主人公キャラのコライダが接触していたら近づいているとします。
コライダの範囲が主人公キャラを検知できる範囲になりますね。
次に敵キャラが主人公キャラクターを追いかける。
というのは敵キャラが移動する目的地を主人公キャラクターの位置に変更すればいいような気がします。
ただ目的地(主人公キャラ)は動くので常に監視し変更しなければいけません。
また敵キャラの検知範囲から主人公が外れた場合は敵キャラはまた見回りに戻る為、新しい目的地を設定するようにします。
敵キャラが主人公キャラを検知する範囲を作成する
まずは敵キャラにコライダを設定します。
敵キャラのボーンの階層の下にSearch Areaという空のオブジェクトを作ります。(右クリック→Create Empty)
Add Component→Physics→Box Colliderを選択しIs Triggerにチェックを入れてください。
(ここではBox Colliderを選択していますが、Sphere Colliderの方がいいです)。
コライダの範囲を修正します。
Is Triggerにチェックを入れるとスクリプト上で接触の検知は出来ますが、物体としては当たらないようになります。
例えば木などにコライダを設定すると、その部分に侵入は出来ませんが、Is Triggerにチェックを入れると侵入可能となり、木をすり抜けていきます。
上のような検知範囲を設定してみました。
広く設定されているように見えますが、実際は足音が確実に聞こえる距離までいかないと相手は気づきません。
ゲームオブジェクトにタグを設定する
次に検知範囲に入って来たものが主人公キャラクターであるという判別が必要です。
その為に主人公キャラクターにタグを設定します。
タグはそのオブジェクトがどういう種類に属するかを判別する為のもので、自分で作成する事も出来ます。
主人公キャラにPlayerという元々あるタグを設定してみます。なければAdd Tagで追加してください。
タグを追加したので、SearchAreaオブジェクトにSearchCharacterスクリプトを追加し「masasi」君を検知出来るようにします。
サーチエリア(コライダ)に主人公が進入したか調べるスクリプト
SearchCharacterスクリプトはその名の通り検知するスクリプトです。
OnTriggerStayメソッドはスクリプトが設定されているオブジェクトのコライダが他のコライダと接触中に実行されるメソッドです。
そこで接触したコライダが何だったかをタグを使って判定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using UnityEngine; using System.Collections; public class SearchCharacter : MonoBehaviour { private MoveEnemy moveEnemy; void Start() { moveEnemy = GetComponentInParent<MoveEnemy>(); } void OnTriggerStay(Collider col) { // プレイヤーキャラクターを発見 if (col.tag == "Player") { } } } |
Startメソッドで自身の親に取り付けているMoveEnemyスクリプトを取得します。
OnTriggerStayメソッドでは引数colに接触した他のコライダが設定されるようになります。
接触した他のコライダのタグがPlayerの時に敵キャラが主人公を追いかけるようにすればいいという事になります。
接触したのがPlayerタグが設定されたゲームオブジェクトのコライダだった時で、敵キャラであるゾンビの状態が追いかける状態でなければ追いかける状態にします。
1 2 3 4 5 6 7 8 9 10 11 12 | // プレイヤーキャラクターを発見 if (col.tag == "Player") { // 敵キャラクターの状態を取得 MoveEnemy.EnemyState state = moveEnemy.GetState(); // 敵キャラクターが追いかける状態でなければ追いかける設定に変更 if (state != MoveEnemy.EnemyState.Chase) { Debug.Log("プレイヤー発見"); moveEnemy.SetState(MoveEnemy.EnemyState.Chase, col.transform); } } |
とします。
MoveEnemyスクリプトのGetStateメソッドとSetStateメソッドは後で作るのでここでは置いておいて、
SetStateでは第1引数にEnemyState.Chaseという列挙型を渡し、第2引数にcol.transformで接触したオブジェクトのTransformを渡しています。
特定の値だけ入れる事が出来る列挙型という変数
敵キャラが今どの状態にあるのか?という状態変数stateを宣言し、その状態を表すのに列挙型を使いますが、列挙型は普通の変数とは少し扱いが変わります。
宣言した項目以外の選択をしないようにする事が出来ます。
1 2 3 4 5 6 7 | public enum EnemyState { Walk, Wait, Chase }; |
と宣言し、これを変数などに入れて使います。
C#だと整数型を指定する事が出来るみたいなんですが、JavaScriptだとどうやればいいかわかりませんでした・・。
型を割り当てない時はint型の数値が割り当てられwalkには0、waitには1と順番に数値が割り当てられます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public enum State { Walk, Wait, Chase }; void Start () { if(State.Walk == 0) { Debug.Log("walk"); } if(State.Wait == 1) { Debug.Log("wait"); } if(State.Chase == 2) { Debug.Log("chase"); } } |
↑のようにすると
walk
wait
chase
という文字が表示されます。
State内の状態にそれぞれ値を設定する事も出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public enum State { Walk = 3, Wait = 7, Chase = 5 }; void Start () { if(State.Walk == 3) { Debug.Log("walk"); } if(State.Wait == 7) { Debug.Log("wait"); } if(State.Chase == 5) { Debug.Log("chase"); } } |
↑も同じように
walk
wait
chase
という文字が表示されます。
あらかじめ宣言された値以外をstateに入れるとエラーになるので、特定の値しか入れたくない場合は列挙型が活躍します。
敵キャラクターに状態を表す列挙型と状態変更メソッドの定義
敵キャラクター操作スクリプトのMoveEnemyに状態を表す列挙型と敵キャラクターの状態を取得するGetStateメソッドと状態変更をする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 | using UnityEngine; using System.Collections; public class MoveEnemy : MonoBehaviour { public enum EnemyState { Walk, Wait, Chase }; /* その他処理 ・ ・ ・ */ // 敵キャラクターの状態変更メソッド public void SetState(EnemyState tempState, Transform targetObj = null) { if (tempState == EnemyState.Walk) { arrived = false; elapsedTime = 0f; state = tempState; setPosition.CreateRandomPosition(); } else if (tempState == EnemyState.Chase) { state = tempState; // 待機状態から追いかける場合もあるのでOff arrived = false; // 追いかける対象をセット playerTransform = targetObj; } else if (tempState == EnemyState.Wait) { elapsedTime = 0f; state = tempState; arrived = true; velocity = Vector3.zero; animator.SetFloat("Speed", 0f); } } // 敵キャラクターの状態取得メソッド public EnemyState GetState() { return state; } } |
第1引数で受け取った文字列で分岐させています。それぞれの分岐処理で状態の変化等をしています。
stateに入れている「EnemyState.名前」は列挙型のEnemyStateで宣言されていなければエラーになります。
フィールド宣言とStartメソッド内の追加と変更
フィールド宣言にEnemyStateフィールドとStartメソッド内でSetStateメソッドを呼び出し状態を初期化する処理を入れます。
1 2 3 4 5 6 | // 敵の状態 private EnemyState state; // 追いかけるキャラクター private Transform playerTransform; |
敵の状態を表すEnemyState型のstateと追いかける状態になった時に追いかけるキャラクターのTransformを入れておくplayerTransformを宣言します。
1 2 3 4 5 6 7 8 9 10 11 12 | void Start () { enemyController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); setPosition = GetComponent <SetPosition> (); setPosition.CreateRandomPosition (); velocity = Vector3.zero; arrived = false; elapsedTime = 0f; SetState (EnemyState.Walk); } |
状態によって処理を分岐させる
次にMoveEnemyスクリプトの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 | void Update () { // 見回りまたはキャラクターを追いかける状態 if(state == EnemyState.Walk || state == EnemyState.Chase) { // キャラクターを追いかける状態であればキャラクターの目的地を再設定 if (state == EnemyState.Chase) { setPosition.SetDestination (playerTransform.position); } if (enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat ("Speed", 2.0f); direction = (setPosition.GetDestination () - transform.position).normalized; transform.LookAt (new Vector3 (setPosition.GetDestination ().x, transform.position.y, setPosition.GetDestination ().z)); velocity = direction * walkSpeed; } // 目的地に到着したかどうかの判定 if (Vector3.Distance (transform.position, setPosition.GetDestination ()) < 0.7f) { SetState ("wait"); animator.SetFloat ("Speed", 0.0f); } // 到着していたら一定時間待つ } else if(state == EnemyState.Wait) { elapsedTime += Time.deltaTime; // 待ち時間を越えたら次の目的地を設定 if(elapsedTime > waitTime) { SetState (EnemyState.Walk); } } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move (velocity * Time.deltaTime); } |
目的地まで歩く処理とキャラクターを追いかける処理は目的地を指定すれば同じ処理を記述する事になるので、追いかける状態である時だけ目的地を再設定し、実際に移動させる処理は同じ処理をしています。
目的地に着いたら、待ち状態に変更します。
待ち状態である時は一定時間その場で待ち、指定時間を超えたら巡回状態(EnemyState.Walk)に変更します。
完成したMoveEnemyスクリプト
ここまで作成したMoveEnemyスクリプトは
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | using UnityEngine; using System.Collections; public class MoveEnemy : MonoBehaviour { public enum EnemyState { Walk, Wait, Chase }; private CharacterController enemyController; private Animator animator; // 目的地 private Vector3 destination; // 歩くスピード [SerializeField] private float walkSpeed = 1.0f; // 速度 private Vector3 velocity; // 移動方向 private Vector3 direction; // 到着フラグ private bool arrived; // SetPositionスクリプト private SetPosition setPosition; // 待ち時間 [SerializeField] private float waitTime = 5f; // 経過時間 private float elapsedTime; // 敵の状態 private EnemyState state; // プレイヤーTransform private Transform playerTransform; // Use this for initialization void Start() { enemyController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); setPosition = GetComponent<SetPosition>(); setPosition.CreateRandomPosition(); velocity = Vector3.zero; arrived = false; elapsedTime = 0f; SetState(EnemyState.Walk); } // Update is called once per frame void Update() { // 見回りまたはキャラクターを追いかける状態 if (state == EnemyState.Walk || state == EnemyState.Chase) { // キャラクターを追いかける状態であればキャラクターの目的地を再設定 if (state == EnemyState.Chase) { setPosition.SetDestination(playerTransform.position); } if (enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat("Speed", 2.0f); direction = (setPosition.GetDestination() - transform.position).normalized; transform.LookAt(new Vector3(setPosition.GetDestination().x, transform.position.y, setPosition.GetDestination().z)); velocity = direction * walkSpeed; } // 目的地に到着したかどうかの判定 if (Vector3.Distance(transform.position, setPosition.GetDestination()) < 0.5f) { SetState(EnemyState.Wait); animator.SetFloat("Speed", 0.0f); } // 到着していたら一定時間待つ } else if (state == EnemyState.Wait) { elapsedTime += Time.deltaTime; // 待ち時間を越えたら次の目的地を設定 if (elapsedTime > waitTime) { SetState(EnemyState.Walk); } } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move(velocity * Time.deltaTime); } // 敵キャラクターの状態変更メソッド public void SetState(EnemyState tempState, Transform targetObj = null) { if (tempState == EnemyState.Walk) { arrived = false; elapsedTime = 0f; state = tempState; setPosition.CreateRandomPosition(); } else if (tempState == EnemyState.Chase) { state = tempState; // 待機状態から追いかける場合もあるのでOff arrived = false; // 追いかける対象をセット playerTransform = targetObj; } else if (tempState == EnemyState.Wait) { elapsedTime = 0f; state = tempState; arrived = true; velocity = Vector3.zero; animator.SetFloat("Speed", 0f); } } // 敵キャラクターの状態取得メソッド public EnemyState GetState() { return state; } } |
となります。
主人公を検知するスクリプトSearchCharacterに追記
MoveEnemy内のスクリプトの記述が終わったので、SearchCharacterに戻ります。
現時点では敵がキャラクターを追いかける状態になったらそのまま一生追い続けます。(^_^;)
そこでSearchAreaに設定したコライダからキャラクターが出ていったら敵キャラクターを待ち状態に変更するようにします。
OnTriggerExitメソッドを追記します。
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 | using UnityEngine; using System.Collections; public class SearchCharacter : MonoBehaviour { private MoveEnemy moveEnemy; void Start() { moveEnemy = GetComponentInParent<MoveEnemy>(); } void OnTriggerStay(Collider col) { // プレイヤーキャラクターを発見 if (col.tag == "Player") { // 敵キャラクターの状態を取得 MoveEnemy.EnemyState state = moveEnemy.GetState(); // 敵キャラクターが追いかける状態でなければ追いかける設定に変更 if (state != MoveEnemy.EnemyState.Chase) { Debug.Log("プレイヤー発見"); moveEnemy.SetState(MoveEnemy.EnemyState.Chase, col.transform); } } } void OnTriggerExit(Collider col) { if (col.tag == "Player") { Debug.Log("見失う"); moveEnemy.SetState(MoveEnemy.EnemyState.Wait); } } } |
OnTriggerExitはオブジェクトが接触状態から抜けた時に呼ばれるメソッドなので、その時のオブジェクトのタグがPlayerであったら、敵キャラの状態を待機状態に変更しています。
これで追いかける処理と検知範囲外に出たら見失って一定時間待機し、再度見回りに戻るというスクリプトが出来ました。
簡単なAIではありますが、結構使えると思います。
もっと簡単に敵キャラの移動処理を作るなら
を使うと移動処理が簡単になります。
また、ナビゲーション機能を追加した後に
の機能を追加するとよりリアルな敵の動きが表現出来ます。
スクリプトがバラバラで解り辛かったので、修正を入れました。
オーバーロード機能は使う必要がなくなったので削除しました。(^_^;)
次回は敵キャラを最初から配置しておくのではなく自動で増やせるようにしたいと思います。