今回はUnityのアクションゲーム等で主人公が敵に囲まれた時に一斉に攻撃をされ続けるという状態を回避し、主人公を攻撃するのは一体だけにする機能を作成していきます。
敵に取り付けたスクリプトで主人公を発見次第追いかけるようにし、一定の距離になったら主人公を攻撃するようにしているとします。
同じ敵のプレハブをゲーム上に複数配置している場合は他の敵の状況にかかわらず主人公と一定の距離になったら攻撃を始めます。
このままの状態だと主人公はタコ殴りされてしまい脱出するのが不可能です。
↑のような状態で一斉に敵が攻撃をしています。
そこで他の敵が攻撃している時は攻撃をやめ、おとなしくその場で待つようにさせます!(-_-)
↑のように他の敵が攻撃している場合はおとなしく見守っています。(´Д`)
ゾンビさん礼儀正しいですね。( 一一)
一体だけが主人公を攻撃する機能
それでは機能を作成していきましょう。
敵のAnimatorController
敵のAnimatorControllerを作成していきます。
↑のような遷移を作成します。
アニメーションパラメータにはfloat型のSpeedとTrigger型のAttackを作成します。
Idle→WalkはHas Exit Timeのチェックを外し条件にはSpeedがGreaderで0.1
Walk→IdleはHas Exit Timeのチェックを外し条件にはSpeedがLessで0.1
Any State→AttackはHas Exit Timeのチェックを外しSettingsのCan Transition To Selfのチェックを外し、条件にAttackがトリガーされた時を設定します。
敵のスクリプト
まずはタコ殴りをする敵キャラクターを作成しておきます。
敵の移動スクリプト
敵はナビゲーション機能を使って地面を移動するようにします。
ナビゲーション機能については

の辺りを参照してみてください。
敵にはNavMeshAgentを取り付け、地面はNavMeshのベイクがされているとします。
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 | using UnityEngine; using System.Collections; using UnityEngine.AI; public class OnlyOneAttackEnemy : MonoBehaviour { public enum EnemyState { Wait, Chase, Attack, Freeze, }; private Animator animator; // 移動方向 private Vector3 direction; // 経過時間 private float elapsedTime; // 敵の状態 [SerializeField] private EnemyState state; // 追いかけるキャラクター private Transform playerTransform; // 攻撃した後のフリーズ時間 [SerializeField] private float freezeTime = 2f; // エージェント private NavMeshAgent agent; // Use this for initialization void Start () { animator = GetComponent <Animator> (); agent = GetComponent <NavMeshAgent> (); elapsedTime = 0f; SetState (EnemyState.Wait); } // Update is called once per frame void Update () { if (state == EnemyState.Chase) { agent.SetDestination (playerTransform.position); // エージェントの潜在的な速さを設定 animator.SetFloat ("Speed", agent.desiredVelocity.magnitude); // 攻撃する距離だったら攻撃 if (agent.remainingDistance < 1.2f) { SetState (EnemyState.Attack); } // 攻撃状態 } else if (state == EnemyState.Attack) { transform.LookAt (playerTransform); animator.SetTrigger ("Attack"); SetState (EnemyState.Freeze); // フリーズ状態 } else if (state == EnemyState.Freeze) { elapsedTime += Time.deltaTime; if (elapsedTime >= freezeTime) { SetState (EnemyState.Wait); } } } // 敵キャラクターの状態変更メソッド public void SetState(EnemyState enemyState, Transform player = null) { state = enemyState; if (state == EnemyState.Wait) { agent.isStopped = true; animator.SetFloat ("Speed", 0f); } else if (state == EnemyState.Attack) { agent.isStopped = true; animator.SetFloat ("Speed", 0f); } else if (state == EnemyState.Chase) { playerTransform = player; agent.SetDestination (playerTransform.position); agent.isStopped = false; } else if (state == EnemyState.Freeze) { elapsedTime = 0f; agent.isStopped = true; animator.SetFloat ("Speed", 0f); } else if (state == EnemyState.OtherEnemyAttackFreeze) { elapsedTime = 0f; agent.isStopped = true; animator.SetFloat ("Speed", 0f); } } public EnemyState GetState() { return state; } } |
敵は主人公を追いかけている状態の時で一定の距離に近づいたら攻撃状態へと移行します。
敵個人は攻撃した後に一定の期間フリーズ状態になった後、何もしていない状態へと遷移しています。
個人では一定のフリーズ時間は設けてありますが、他の敵の状況は把握していません。
主人公検知スクリプト
次に敵の子要素に空のゲームオブジェクトを作成し名前をSearchAreaとしSphere Colliderを取り付けIs Triggerのチェックを入れ主人公の検知を行います。
SearchAreaゲームオブジェクトにOnlyOneSearchCharaスクリプトを作成し取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OnlyOneSearchChara : MonoBehaviour { private OnlyOneAttackEnemy onlyOneAttackEnemy; void Start() { onlyOneAttackEnemy = GetComponentInParent<OnlyOneAttackEnemy> (); } void OnTriggerStay(Collider col) { if (col.tag == "Player") { if (onlyOneAttackEnemy.GetState () != OnlyOneAttackEnemy.EnemyState.Attack && onlyOneAttackEnemy.GetState () != OnlyOneAttackEnemy.EnemyState.Chase && onlyOneAttackEnemy.GetState() != OnlyOneAttackEnemy.EnemyState.Freeze ) { onlyOneAttackEnemy.SetState (OnlyOneAttackEnemy.EnemyState.Chase, col.transform); } } } void OnTriggerExit(Collider col) { if (col.tag == "Player") { onlyOneAttackEnemy.SetState (OnlyOneAttackEnemy.EnemyState.Freeze); } } } |
OnlyOneSearchCharaスクリプトでは主人公キャラクターを検知し、自身(敵)キャラクターの状態がAttack、Chase、Freeze状態でなければChase状態にして追いかけさせます。
主人公キャラクターにはPlayerタグを設定しておきます。
これで敵キャラクターが主人公を攻撃する部分が出来ました。
敵管理スクリプトの作成
他の敵が主人公を攻撃しているかどうかは別のスクリプトで他の敵が攻撃中という情報を管理しておく必要があります。
敵が攻撃をする時に他の敵が攻撃しているかを敵の管理スクリプトで調べ誰も攻撃していなければ攻撃状態へ遷移するという風にします。
ヒエラルキー上に空のゲームオブジェクトを作成し名前をEnemyManagerとします。
EnemyManagerのインスペクタのTagにEnemyManagerタグを作成し設定しておきます。
EnemyManagerにはEnemyManagerスクリプトを作成し取り付けます。
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; public class EnemyManager : MonoBehaviour { // 敵が攻撃しているかどうか private bool isOtherEnemyAttack; // 他の敵が攻撃中かどうか public bool IsOtherEnemyAttack() { return isOtherEnemyAttack; } // 攻撃フラグの設定 public void SetAttack(bool isAttack) { isOtherEnemyAttack = isAttack; } } |
↑のように敵が一体でも攻撃していればisOtherEnemyAttackにtrueを入れ、攻撃が終わればSetAttackメソッドを呼び出してisOtherEnemyAttackをfalseにします。
敵の移動スクリプトに処理を追加
敵の移動スクリプトOnlyOneAttackEnemyスクリプトに処理を追加していきます。
1 2 3 4 5 6 7 8 9 | public enum EnemyState { Wait, Chase, Attack, Freeze, OtherEnemyAttackFreeze }; |
新しい敵の状態OtherEnemyAttackFreeze状態を作成します。
この状態は他の敵が攻撃中だった時に自身は攻撃状態ではなくこの状態にし一定時間待ちます。
次はフィールドの追加です。
1 2 3 4 5 6 7 8 9 10 | // 敵が攻撃しているのでフリーズする元の時間 [SerializeField] private float defaultOtherAttackFreezeTime = 1f; // 攻撃待ちの時間とともに減らす待ち時間 [SerializeField] private float otherAttackFreezeTime; // 敵管理システム private EnemyManager enemyManager; |
defaultOtherAttackFreezeTimeは初期の攻撃待ち時間で、他の敵が攻撃している間はずっとOtherEnemyAttackFreeze状態にするのでその時にotherAttackFreezeTimeを減らして待ち時間を減らします。
Wait状態になったら待ち時間を元に戻すのでdefaultOtherAttackFreezeTimeをotherAttackFreezeTimeに入れます。
enemyManagerは先ほど作ったEnemyManagerスクリプトをStartメソッドで探して入れます。
1 2 3 4 5 6 | void Start () { enemyManager = GameObject.FindWithTag ("EnemyManager").GetComponent<EnemyManager>(); otherAttackFreezeTime = defaultOtherAttackFreezeTime; } |
FindWithTagでEnemyManagerタグが設定されたゲームオブジェクトを探しEnemyManagerスクリプトを取得しています。
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 | void Update () { if (state == EnemyState.Chase) { agent.SetDestination (playerTransform.position); // エージェントの潜在的な速さを設定 animator.SetFloat ("Speed", agent.desiredVelocity.magnitude); // 攻撃する距離だったら攻撃 if (agent.remainingDistance < 1.2f) { // 他の敵が攻撃していなければ攻撃 if (!enemyManager.IsOtherEnemyAttack ()) { enemyManager.SetAttack (isAttack: true); SetState (EnemyState.Attack); // 待ち状態にする } else { SetState (EnemyState.OtherEnemyAttackFreeze); } } // 攻撃状態 } else if (state == EnemyState.Attack) { transform.LookAt (playerTransform); animator.SetTrigger ("Attack"); SetState (EnemyState.Freeze); // フリーズ状態 } else if (state == EnemyState.Freeze) { elapsedTime += Time.deltaTime; if (elapsedTime >= freezeTime) { SetState (EnemyState.Wait); } // 他の敵が攻撃している時の待ち状態 } else if (state == EnemyState.OtherEnemyAttackFreeze) { elapsedTime += Time.deltaTime; if (elapsedTime >= otherAttackFreezeTime) { SetState (EnemyState.Chase); } } } |
EnemyState.Chase状態の時で主人公と一定距離になったらEnemyManagerのIsOtherEnemyAttackメソッドで他の敵が攻撃中かどうかを調べ、EnemyManagerのSetAttackでisOtherEnemyAttackをtrueにし、自身を攻撃状態にします。
他の敵が攻撃中であればEnemyState.OtherEnemyAttackFreeze状態にします。
otherAttackFreezeTimeの待ち時間を超えたらEnemyState.Chase状態にしキャラクターを追いかける状態へと移行します。
次に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 | // 敵キャラクターの状態変更メソッド public void SetState(EnemyState enemyState, Transform player = null) { state = enemyState; if (state == EnemyState.Wait) { agent.isStopped = true; animator.SetFloat ("Speed", 0f); } else if (state == EnemyState.Attack) { agent.isStopped = true; animator.SetFloat ("Speed", 0f); otherAttackFreezeTime = defaultOtherAttackFreezeTime; } else if (state == EnemyState.Chase) { if (player != null) { playerTransform = player; agent.SetDestination (playerTransform.position); } agent.isStopped = false; } else if (state == EnemyState.Freeze) { elapsedTime = 0f; agent.isStopped = true; animator.SetFloat ("Speed", 0f); } else if (state == EnemyState.OtherEnemyAttackFreeze) { // 待ち時間を減らす otherAttackFreezeTime -= 0.1f; elapsedTime = 0f; agent.isStopped = true; animator.SetFloat ("Speed", 0f); } } |
Attack状態の時はotherAttackFreezeTimeを初期値に戻します。
Chase状態の時はOtherEnemyAttackFreeze状態からChaseに移行させる際にキャラクターのTransformを渡さないので引数として受け取ったplayerがnullであれば主人公のTransformとエージェントの目的地の設定は行いません。
OtherEnemyAttackFreeze状態の時はotherAttackFreezeTimeから0.1減らし徐々に待ち時間を減らして同じ敵が何度も攻撃しないようにします。
主人公検知スクリプトに処理を追加
OtherEnemyAttackFreeze状態を追加したので、主人公検知スクリプトOnlyOneSearchCharaの条件に追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void OnTriggerStay(Collider col) { if (col.tag == "Player") { if (onlyOneAttackEnemy.GetState () != OnlyOneAttackEnemy.EnemyState.Attack && onlyOneAttackEnemy.GetState () != OnlyOneAttackEnemy.EnemyState.Chase && onlyOneAttackEnemy.GetState() != OnlyOneAttackEnemy.EnemyState.Freeze && onlyOneAttackEnemy.GetState() != OnlyOneAttackEnemy.EnemyState.OtherEnemyAttackFreeze ) { onlyOneAttackEnemy.SetState (OnlyOneAttackEnemy.EnemyState.Chase, col.transform); } } } |
EnemyManagerのisOtherEnemyAttackをfalseにしなければいけない
ここまでの処理でEnemyManagerのisOtherEnemyAttackをtrueにする処理は作りましたが、falseにする処理がないので敵のどれかが主人公を1回攻撃したら付いては来るものの攻撃を一切してこなくなります。
isOtherEnemyAttackをfalseにする処理をいれなければいけませんが、攻撃している敵が攻撃を終わった時にfalseにしなければいけないのでアニメーションイベントを使ってアニメーションの終了を検知すれば良さそうです。
ですがアニメーションイベントを追加するのは面倒ですし、その他諸々の理由もありまして今回はビヘイビアを使う事にします。
敵のAnimatorControllerのAttack状態を選択しインスペクタからAdd Behaviourボタンを押します。
名前をOnlyOneEndAttackBehaviourとします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OnlyOneEndAttackBehaviour : StateMachineBehaviour { private EnemyManager enemyManager; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state //override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (enemyManager == null) { enemyManager = GameObject.FindWithTag ("EnemyManager").GetComponent<EnemyManager> (); } enemyManager.SetAttack (isAttack: false); } // OnStateMove is called right after Animator.OnAnimatorMove(). Code that processes and affects root motion should be implemented here //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} // OnStateIK is called right after Animator.OnAnimatorIK(). Code that sets up animation IK (inverse kinematics) should be implemented here. //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} } |
OnStateExitはその状態を抜ける時に呼ばれるメソッドなのでそこでEnemyManagerスクリプトのSetAttackメソッドを呼び出しisOtherEnemyAttackをfalseにします。
敵のAttack状態のインスペクタは
これで敵の攻撃が終われば他の敵も攻撃が可能となります。
これで機能が完成しました。
終わりに
他の敵が攻撃中はその敵は攻撃終了待ちになりますが、通常のFreeze状態にすると先ほど攻撃した敵が再度攻撃してしまうという事も起きるので多少考慮したスクリプトにしました(同一の敵が連続で攻撃する事もある)。
他のやり方としてはEnemyManagerスクリプトで敵の攻撃する順番待ちのリストを保持しておき、時間経過で攻撃させるというのも出来るかもしれません。
やり方は色々ありそうですね。
今回の機能では敵のどれか一体が攻撃を開始するとisOtherEnemyAttackをtrueにし、どれか一体が攻撃を終了するとisOtherEnemyAttackをfalseにしています。
その為、今どの敵が攻撃中でどの敵が攻撃を終了したか?といった事までは把握させていません。
今回の仕様だと敵一体しか攻撃出来ないので、isOtherEnemyAttackがtrueの時はその敵しか攻撃していないので問題はありません。
ですが攻撃の順番をEnemyManagerスクリプトで管理していたり、同時に2体までは攻撃を可能とする場合は個々の攻撃を解除するという風に作らなくてはいけないかもしれません。