シンプルなアクションゲームを作ってみようの第14回です。
今回はプレイヤーキャラクターを追いかける敵キャラクターを作成していきます。
前回は経過時間を表示する機能を作成しました。
シンプルなアクションゲームを作ってみようの他の記事は
シンプルなアクションゲームを作るのを通してUnityの使い方を学ぶカテゴリです。
から参照出来ます。
敵キャラクターモデルの配置
最初に敵キャラクターモデルをStage1に配置します。
敵キャラクターはにはプレイヤーキャラクターと同じEthanを配置し、モデルのマテリアルを変更することで違いを出します。
Assets/Standard Assets/Characters/ThirdPersonCharacter/ModelsのEthan.fbxをヒエラルキーにドラッグ&ドロップして配置します。
プレイヤーキャラクターの名前もEthanで敵キャラクターもEthanでわかりにくいので、以前から配置していたプレイヤーキャラクターのEthanの名前をPlayer、今配置した敵キャラクターのEthanの名前をEnemyと変更します。
敵キャラクターの概要
今回、敵キャラクターにはプレイヤーキャラクターと同じようにRigidbodyとCapsule Colliderコンポーネントを取り付けますが、プレイヤーキャラクターを追いかけるのにナビゲーションシステムを使って動かす事にします。
ナビゲーションシステムはあらかじめ決められた場所(ゲームオブジェクト)をベイクし、その場所(ナビメッシュ)をナビメッシュエージェント(NavMeshAgentを持ったゲームオブジェクト)が移動出来る機能です。
敵キャラクターにナビメッシュエージェントを取り付けて、ナビメッシュした場所に限定して動かす事で、敵キャラクターが床から落下したりといったことも起きません。
敵キャラクターの設定
敵キャラクターの設定をしていきます。
敵キャラクター用のマテリアルを作成
敵キャラクターモデルの見た目がプレイヤーキャラクターと同じなので、敵キャラクターモデル用のマテリアルを作成し、設定することにします。
Assets/Materialsフォルダで右クリックからCreate→Materialを選択し、名前をEnemyとします。
Enemyマテリアルを選択し、インスペクタでAlbedoの横の白色部分を押し、RGを0、Bを255にして青色にします。
次にNormal Mapの横の丸を押し、EthanNormalsを設定しOcclusionの横の丸を押し、EthanOcclusionを設定します。
ヒエラルキーのEnemyの子要素のEthanBodyとEthanGlassesをCtrlキーを押しながら選択し、インスペクタのAdd Componentの下の方にEnemyマテリアルをドラッグ&ドロップして設定します。
コンポーネントの追加と設定
敵キャラクターのEnemyにコンポーネントを追加して設定していきます。
ヒエラルキーのEnemyを選択し、インスペクタのAdd ComponentからPhysics→RigidbodyとPhysics→Capsule Colliderを取り付けます。
TransformのPositionを変更し、スタート時はプレイヤーキャラクターと少し離します。
RigidbodyのMassを60にし、Is Kinematicにチェックを入れスクリプトから位置等を操作するようにします。
Capsule Colliderはキャラクターの衝突範囲なので、キャラクターモデルに合わせて調整します。
次にナビメッシュエージェントコンポーネントを取り付けます。
Add ComponentボタンからNavigation→Nav Mesh Agentを選択します。
Speedは最高速度なので2にします。
Radiusは0.3、Heightは1.6にします。
これはナビメッシュ上を歩く時の半径と高さで、障害物があったり幅によっては通れなくなります。
こちらの設定はCapsule Colliderのコライダのサイズと同じにしました。
敵キャラクター用のAnimatorControllerの作成
敵のアニメーション管理用のAnimator Controllerを作成していきます。
敵キャラクターはプレイヤーキャラクターと同じキャラクターモデルですし、歩いている時は同じアニメーションを使う事にします。
プレイヤーキャラクターの時と同じように敵キャラクター用のAnimator Controllerを作ってもいいんですが、状態や遷移条件が同じなので、プレイヤーキャラクターのAnimator Controllerをオーバーライドして敵キャラクターのAnimator Controllerを作成したいと思います。
Assets/Animatorsで右クリックからCreate→Animator Override Controllerを選択し、名前をEnemyとします。
Enemyアニメーターオーバーライドコントローラーを選択し、インスペクタでControllerにPlayerアニメーターコントローラーをドラッグ&ドロップをして設定します。
これでEnemyアニメーターオーバーライドコントローラーはPlayerアニメーターコントローラーをオーバーライド(上書き)したものになります。
Enemyは状態と遷移がPlayerと同じになり、インスペクタで再生するアニメーションはオーバーライド出来ます。
今回はPlayerアニメーターコントローラーで使っているアニメーションをそのまま使うので特にアニメーションクリップをオーバーライドしません。
次にヒエラルキーのEnemyを選択し、AnimatorのControllerにEnemyアニメーターオーバーライドコントローラーを設定し、Apply Root Motionのチェックを外します。
敵キャラクターの移動スクリプトの作成
敵キャラクターの移動スクリプトを作成していきます。
Assets/Scriptsフォルダに新しくEnemyControllerスクリプトを作成し、Enemyに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyController : MonoBehaviour { private GameManager gameManager; private NavMeshAgent navMeshAgent; private Transform player; private Animator animator; // キャラクターを操作可能かどうか private bool canControl; // Start is called before the first frame update void Start() { gameManager = GameObject.Find("GameManager").GetComponent<GameManager>(); navMeshAgent = GetComponent<NavMeshAgent>(); player = GameObject.Find("Player").transform; animator = GetComponent<Animator>(); canControl = true; } // Update is called once per frame void Update() { if(!canControl) { return; } // ゲームオーバー時は停止してこれ以降何もしない if (gameManager.GameOver) { navMeshAgent.isStopped = true; animator.SetFloat("Speed", 0f); canControl = false; return; } // 目的地の再設定 navMeshAgent.SetDestination(player.position); // プレイヤーキャラクターと敵キャラクターの距離 var characterDistance = Vector2.Distance(new Vector2(player.position.x, player.position.z), new Vector2(transform.position.x, transform.position.z)); // 止まっている時 if (navMeshAgent.isStopped) { // 距離が開いたら再度追いかける(Y軸は無視) if (characterDistance > 2f) { navMeshAgent.isStopped = false; } } else { // 目的地との距離が開いている場合 if (characterDistance > 0.8f) { navMeshAgent.isStopped = false; animator.SetFloat("Speed", navMeshAgent.speed); } else { navMeshAgent.isStopped = true; animator.SetFloat("Speed", 0f); } } } } |
gameManagerはGameManagerを入れます。
navMeshAgentにはNavMeshAgentを入れます。
playerには追いかける対象であるプレイヤーキャラクターのTransformを入れます。
animatorはAnimatorを入れます。
canControlはキャラクターを操作可能かどうかのフラグです。
Startメソッドの中を見ていきます。
GameManagerゲームオブジェクトを探して、取り付けてあるGameManagerスクリプトを取得します。
NavMeshAgentはEnemyゲームオブジェクトに取り付けたので、GetComponentで自身のゲームオブジェクトから取得します。
playerもPlayerゲームオブジェクトを探して、そのプロパティのtransformを取得し設定しています。
transformはゲームオブジェクトが必ず持つので、GetComponentでなくtransformプロパティでそのまま取得出来るようになっています。
canControlにtrueを入れて敵キャラクターが操作可能であると設定します。
Updateメソッドを見ていきます。
!canControlである時、つまり操作出来ない時はreturnでそれ以降の処理をしません。
ゲームオーバーだった時はnavMeshAgent.isStoppedにtrueを入れナビメッシュエージェントの動きを止めます。
アニメーションパラメータ―のSpeedも0にし、アニメーションをIdle状態にします。
canControlをfalseにし、操作不可能に設定します。
returnでそれ以降の処理をしません。
1 2 3 | navMeshAgent.SetDestination(player.position); |
でnavMeshAgent.SetDestinationメソッドでナビメッシュエージェントの目的地を設定します。
敵キャラクターの目的地はプレイヤーキャラクターなので、引数にプレイヤーキャラクターの位置を渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // プレイヤーキャラクターと敵キャラクターの距離 var characterDistance = Vector2.Distance(new Vector2(player.position.x, player.position.z), new Vector2(transform.position.x, transform.position.z)); // 止まっている時 if (navMeshAgent.isStopped) { // 距離が開いたら再度追いかける(Y軸は無視) if (characterDistance > 2f) { navMeshAgent.isStopped = false; } } else { // 目的地との距離が開いている場合 if (characterDistance > 0.8f) { navMeshAgent.isStopped = false; animator.SetFloat("Speed", navMeshAgent.speed); } else { navMeshAgent.isStopped = true; animator.SetFloat("Speed", 0f); } } |
Vector2.Distanceメソッドを使って第1引数(プレイヤーキャラクターの位置)と第2引数(敵キャラクターの位置)の距離を計算します。
キャラクターの位置はVector3型で表しますが、ここで比較したいのは高さを無視した距離なのでVector2にプレイヤーキャラクターと敵キャラクターのXとZを当てはめて計算しています。
Vector3にしてYのところに0を入れたりしても出来ます。
navMeshAgent.isStoppedは先ほども出てきましたが、ナビメッシュエージェントを停止しているかどうかのプロパティです。
停止していたら、それが2より大きければnavMeshAgent.isStoppedをfalseにして再びナビメッシュエージェントが動き出します。
ナビメッシュエージェントが動いている場合でキャラクターの距離が0.8より大きければナビメッシュエージェントを動かします。
アニメーションパラメータ―のSpeedにnavMeshAgentのspeedプロパティを渡します。
距離が0.8以下であればナビメッシュエージェントを停止し、アニメーションパラメータ―のSpeedに0を渡します。
これでスクリプトが出来ました。
敵のレイヤーの設定とPlayerControllerスクリプトの変更
プレイヤーが敵に乗った時にも接地と判定する為、Enemyレイヤーを新しく作成し、Enemyゲームオブジェクトに設定します。
また、PlayerControllerスクリプトで接地判定をしているレイヤーにEnemyレイヤーを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 地面のチェック private void CheckGround() { if (isGrounded) { return; } // アニメーションパラメータのRigidbodyのYが0.1以下でGroundまたはEnemyレイヤーと球のコライダがぶつかった場合は地面に接地 if (Physics.CheckSphere(rigidBody.position, myCollider.radius - 0.1f, LayerMask.GetMask("Ground", "Enemy")) ) { isGrounded = true; velocity.y = 0f; } else { isGrounded = false; } } |
LayerMask.GetMaskにEnemyレイヤーも追加します。
ナビメッシュのベイク
ナビメッシュエージェントの設定とスクリプトが出来たので、後はナビメッシュエージェントが移動するナビメッシュをベイクします。
ナビメッシュにするゲームオブジェクトはインスペクタでNavigation Staticにチェックを入れる必要がありますが、今回は他のStatic設定をまとめてチェックを入れてしまいます。
敵キャラクターが移動出来るようにするゲームオブジェクトはWholeFloorとFloor、Floor(1)、Floor(2)、Slope、Slope(1)なのでCtrlキーを押しながらヒエラルキーの該当するゲームオブジェクトを選択します。
インスペクタで名前の横のStaticのチェックを入れます。
チェックを入れると新しいウインドウが出てきて、選択したゲームオブジェクトの子要素もStaticにするのか?という選択肢が出るので、No,this object onlyボタンを押して子要素はチェックを入れないようにします。
選択したゲームオブジェクトのStatic全般にチェックがされました(当然のことながらNavigation Staticもチェックされました)。
次はNavigation Staticされたゲームオブジェクトをベイクします。
UnityメニューのWindowからAI→Navigationを選択してNavigationウインドウを開きます。
Navigationウインドウで設定をします。
ナビメッシュエージェントの半径を0.3、高さを1.6にして敵キャラクターと合わせます。
Drop Heightは3にして、3mの高さから落下するオフメッシュリンクを作成します。
オフメッシュリンクは地続きでない場所を行き来する為の機能で、Drop Heightは落下のオフメッシュリンク、Jump Distanceはジャンプ可能なオフメッシュリンクの距離を設定します。
ここまで設定したらBakeボタンを押してナビメッシュをベイクします。
シーンビューを確認するとベイクされたナビメッシュを確認出来ます。
ナビメッシュエージェントが歩ける部分は青っぽい色で塗られている所です。
坂等も同じですが、坂は黄色なので青色と重なって緑っぽい色になっていますが・・・。
この色はNavigationウインドウのAreasタブを押した時に表示されているエリアの名前のWalkableと同じ色が塗られています。
このAreasで指定した名前の横で、ナビメッシュエージェントが移動するのにかかるコストを設定することが出来ます。
例えばUser 3にSlopeという名前のエリアを作成し、かかるコストを5とします。
次にヒエラルキーのSlopeとSlope(1)を選択し、NavigationウインドウのObjectタブを押します。
さらにNavigation AreaをSlopeに設定するとSlopeゲームオブジェクトとSlope(1)ゲームオブジェクトのエリアがSlopeになります。
Bakeタブを押し、Bakeボタンを押すと坂を上る時のエリアがSlopeになったので移動出来るエリアの色が変わります。
実行して確認してみる
機能が出来たので実行して確認してみましょう。
上のようになりました。
終わりに
今回はナビゲーションシステムを使って敵キャラクターがプレイヤーキャラクターを追いかけるようにしました。
次回はゲームにBGMを付けていきます。