この記事はFPSゲームの作り方講座の第9回目です。
前回はCinemachineによる移動と敵の攻撃準備のための移動処理AIを作成しました。
前回の記事:
今回は敵が自動で動いて攻撃してくる人工知能の開発を完成させていきましょう。
近距離攻撃、遠距離攻撃両方を移動処理を結合させながら行動してくるAIを作ります。
盛りだくさんの内容ですが一緒にがんばって作っていきましょう!
EnemyBase.csに死亡フラグの追記を行う
まずは、少し「EnemyBase.cs」に追記していきます。
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 |
public class EnemyBase : MonoBehaviour { ・・・ // 死亡フラグ. protected bool isDead = false; ・・・ // --------------------------------------------------------------- /// <summary> /// コライダーエンター処理. /// </summary> /// <param name="col"></param> // --------------------------------------------------------------- public virtual void OnEnemyColliderEnter( Collision col ) { if( isDead == true ) { // すでに死亡していたら破棄. Destroy( col.gameObject ); return; } ・・・ } // --------------------------------------------------------------- /// <summary> /// 死亡時処理. /// </summary> // --------------------------------------------------------------- protected virtual void OnDead() { Debug.Log( gameObject.name + "を倒しました" ); isDead = true; } ・・・ } |
追加するのは、「isDead」というフラグです。死亡した瞬間に「すでに死亡しています」という状態を表すために使用します。
「OnEnemyColliderEnter」関数の最初に「if( isDead == true ) 」のif文を追加します。
処理はまず「Destroy( col.gameObject )」ですでに死亡している場合は、矢をそのまま破棄します。そしてその後の処理を行わない様に「return」します。
「OnDead()」関数でその「isDead」を「true」にします。これで「EnemyBase」を継承している、他の敵スクリプトで死亡時に「isDead」が「true」になります。
これにより死亡が決定した後は再び攻撃処理が動かないようになります。
【学歴不問・高卒、元ニートでも挑戦できる】
EnemyCrocodileの攻撃処理を作成する
ではワニの敵「Enemy1」の攻撃処理を作成しましょう。主に近寄ってきて近距離攻撃をします。
アニメーションイベントの設定
攻撃アニメーションを実行していく前に少し必要なことがあるのでやっていきましょう。
攻撃アニメーションである「Assets/AppMain/Animator/Croc/Attack(2)」をダブルクリックしてください。すると「Animation」ウインドウが開きます。(Animatorウインドウではなく粒々がたくさんあるウインドウです)
まずは一番上の時間が書いてある部分をクリックして、カーソルを最後(一番右)に置いておいてください。
そして名前の右横にある一番右のボタン(太めの縦棒に+マークのAdd Event)を押します。するとカーソルのある部分にアニメーションイベントが作成されます。
作成されたら、その縦棒を選択してInspectorをみてください。「AnimationEvent」という項目になるので、この「Function」に「Anim_AttackEnd」と入力して下さい。
これで、「Attack(2)」アニメーションの最後のタイミングになった時に「Anim_AttackEnd」というアニメーションイベントが実行されるようになります。しかしこれだけでは「アニメーションイベントを実行するためのスクリプトがないよ」みたいなエラーになってしまうので、このイベントを受け取らないといけません。
受け取るには、このアニメーションが実行されているGameObjectと同じGameObjectに「Anim_AttackEnd」という関数をもったスクリプトを付与する必要があります。
同じ、と言うところが重要で今回アニメーションを実行している(Animatorがあるもの)のは「Monster_X_Green」です。なのでここに受け取る処理を作成します。
では新しくスクリプトを作成し「EnemyAnimationEventReceiver」と言う名前にしましょう。考え方は「ColliderCallReceiver」と同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public class EnemyAnimationEventReceiver : MonoBehaviour { // 攻撃終了時イベント. public UnityEvent AttackEndEvent = new UnityEvent(); // ------------------------------------------------------------- /// <summary> /// アニメーションイベント攻撃終了時. /// </summary> // ------------------------------------------------------------- void Anim_AttackEnd() { Debug.Log( "Anim Attack End" + gameObject.name ); AttackEndEvent?.Invoke(); } } |
まず「using UnityEngine.Events;」を忘れずに追加しましょう。
そして「public UnityEvent AttackEndEvent」を用意します。これでInspectorでイベントを設定できるようにします。
次に先ほどアニメーションイベントとして追加したものと同じ名前の関数「Anim_AttackEnd」を作成します。名前が違うと実行されないので注意してください。
処理は「AttackEndEvent?.Invoke();」でイベントを実行しているだけです。
ではこの「EnemyAnimationEventReceiver」を「Monster_X_Green」に付与してください。
FPSゲームの必須機能である敵の攻撃処理を作る
準備ができたので、「EnemyCrocodile.cs」に処理を追加していきます。
EnemyCrocodile.csに敵を攻撃するC#スクリプトを追加する
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 |
public class EnemyCrocodile : EnemyBase { ・・・ // 攻撃中フラグ. bool isAttacking = false; ・・・ // --------------------------------------------------------------------------------- /// <summary> /// 距離の計測と目的地の設定. /// </summary> // --------------------------------------------------------------------------------- void SetNext() { if( toAttack == true ) return; if( isDead == true ) return; var dis = navMeshAgent.remainingDistance; if (dis < 0.5f) { var target = GetMoveTarget(); if (target != null) navMeshAgent.SetDestination(target.position); } else if (navMeshAgent.isStopped == true) { // isStopからの復帰". navMeshAgent.isStopped = false; var target = GetMoveTarget(); if (target != null) navMeshAgent.SetDestination(target.position); } } // --------------------------------------------------------------------------------- /// <summary> /// 周辺コライダーから出て行った時のコールバック. /// </summary> /// <param name="col"> 出て行ったコライダー. </param> // --------------------------------------------------------------------------------- public void OnNeighborhoodTriggerExit( Collider col ) { if( col.gameObject.tag == "Player" ) { Debug.Log( "Neighborhood Trigger Exit Player" ); targetPlayer = null; isAttacking = false; toAttack = false; } } // --------------------------------------------------------------------------------- /// <summary> /// 攻撃しに向かう. /// </summary> // --------------------------------------------------------------------------------- void ToAttack() { if( isAttacking == true ) return; if( isDead == true ) return; toAttack = true; navMeshAgent.SetDestination( targetPlayer.transform.position ); var dis = navMeshAgent.remainingDistance; if( dis < 4f ) { navMeshAgent.isStopped = true; anim.SetTrigger( "Attack" ); var targetTransform = targetPlayer.transform.position; targetTransform.y = gameObject.transform.position.y; var dir = ( targetTransform - gameObject.transform.position ).normalized; transform.forward = dir; isAttacking = true; } else { navMeshAgent.isStopped = false; } } // --------------------------------------------------------------------------------- /// <summary> /// アニメーションエンド登録処理. /// </summary> // --------------------------------------------------------------------------------- public void OnAttackEnd() { StartCoroutine( AttackWait() ); } // --------------------------------------------------------------------------------- /// <summary> /// 攻撃間隔のためのコルーチン. /// </summary> // --------------------------------------------------------------------------------- IEnumerator AttackWait() { yield return new WaitForSeconds( 2f ); isAttacking = false; toAttack = false; } } |
変数は「isAttacking」という攻撃中のフラグを用意します。
次に「SetNext()」関数、つまり次の目的地を決める関数の最初「toAttack == true」で「return」していますが、そこにさらに「isDead == true」でも「return」するようにします。
さらに「else if( navMeshAgent.isStopped == true )」の部分。処理は「navMeshAgent.isStopped = false」以外はifの部分と同じです。これは「攻撃をする」という行動が加わることで、移動が停止のまま近くにプレイヤーもいない状態になってしまうのを防ぐための処理です。
「OnNeighborhoodTriggerExit()」関数の追加です。この関数は敵の周辺検知コライダーからプレイヤーが出て行った時の処理です。追加部分は「isAttacking = false」で攻撃中フラグを「false」にします。
続いて「ToAttack()」関数に追加します。追加部分は「if( isDead == true ) return」で死亡時には実行しない処理を追加したところと、「if( dis < 4f )」の中の処理です。
後半の追加部分だけ抜き出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if( dis < 4f ) { ・・・ anim.SetTrigger( "Attack" ); var targetTransform = targetPlayer.transform.position; targetTransform.y = gameObject.transform.position.y; var dir = ( targetTransform - gameObject.transform.position ).normalized; transform.forward = dir; isAttacking = true; } else { ・・・ } |
最初に「anim.SetTrigger( “Attack” )」でアニメーションの「Attack」トリガーを実行します。これで攻撃アニメーションを始めます。
残りはプレイヤーの方向を向く処理です。「targetTransform」に「targetPlayer.transform.position」つまりターゲットの位置を保管します。そしてそのYの値を「gameObject.transform.position.y」に合わせます。これは敵自身の高さでのプレイヤーの方向を求めています。
次に「dir」に「( targetTransform – gameObject.transform.position )」でプレイヤーの位置から自身の位置をひいて、それを「normalized」して正規化(大きさ1にする)した方向を求めます。
そしてその値を「transform.forward」にいれて正面を求めた方向に合わせます。最後に「isAttacking」を「true」にして攻撃中の状態にします。
これで、近づいたら攻撃をするようになりました。
続いて関数を新しく2つ追加します。
1 2 3 4 |
public void OnAttackEnd() { StartCoroutine( AttackWait() ); } |
まずは、「StartCoroutine( AttackWait() )」のみの「OnAttackEnd()」関数を作成します。これは、後ほど「EnemyAnimationEventReceiver」に登録するための関数です。
1 2 3 4 5 6 |
IEnumerator AttackWait() { yield return new WaitForSeconds( 2f ); isAttacking = false; toAttack = false; } |
こちらはコルーチンで最初に「yield return new WaitForSeconds( 2f )」で2秒待機します。その後「isAttacking」と「toAttack」を「false」にします。
ここの処理は一回攻撃をして、次の攻撃までの待機時間のための処理です。
InspectorでEnemyAnimationEventReceiverにイベントを登録
ではイベントを登録します。
「Monster_X_Green」の「EnemyAnimationEventReceiver」で「+」ボタンでイベントを追加して、「Enemy1」をドラック&ドロップします。そして関数は「EnemyCrocodile.OnAttackEnd」を選択します。
一旦再生して確かめよう
では一旦再生してみましょう。
敵に近づくと、寄ってきて攻撃してきます。そして離れない限りそこで攻撃を続けます。離れるとまた周回位置に戻ります。
まだ攻撃がプレイヤーに当たるようになっていませんので、次はそこを作成していきましょう。
FPSゲームにおける敵の攻撃のヒット・当たり判定を作ろう
ここからは敵の攻撃の当たり判定を実装します。
AttackPoint、AttackSphereの作成
まずは「Enemy1」の子に空オブジェクトを作成し「AttackPoint」という名前にします。
次に「AttackPoint」の子に「Sphere」を作成し「AttackSphere1」と言う名前にします。「AttackSphere1」のTransformはPosition、Rotationを「0」にしておきます。
そして、「AttackPoint」「AttackSphere1」の「Transform」を下記のように設定します。
次回に向けて
今回はここまでにしましょう。
ここまでで敵の攻撃当たり判定、攻撃アニメーション、近距離攻撃、遠距離攻撃を実装しました。
また、移動やプレイヤーの自動追尾機能との切り替えを行えるようにし、実践的な敵AIに仕上がりました。
次回は現在配置している敵を増やして自動生成していきます。
次回の記事 :
コメント