【自作ゲーム開発11】敵キャラクターのスタン状態【Unity】


注意

本記事で掲載されている動作の実装方法及びプログラムのソースコードは最適な方法ではない可能性があります。
今後不具合等が判明した場合には修正及び改良をおこなう可能性があります。
また今後自分で同機能を実装する場合の参考にする可能性もあるためソースコードだけではなく説明しつつ記事を進めていきます。


はじめに

Unityによる自作ゲーム開発進捗その11になります!

今回は敵キャラクターにスタン状態を追加します。

同時に味方NPCにスタン攻撃と吹き飛ばし攻撃を実装します。

既存のスクリプトを3つ更新します。

以下にソースコードを掲載します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.UI;
using TMPro;
using Febucci.UI;

public class CenterMobEnemy : MonoBehaviour{
    private const int m_const_text_array = 10;

    [SerializeField]
    private Canvas m_hitpoint_canvas;

    [SerializeField]
    private Slider m_hitpoint_bar;

    [SerializeField]
    private TextAnimatorPlayer m_damage_text;

    [SerializeField]
    private ParticleSystem m_bomb_effect;

    [SerializeField]
    private ParticleSystem m_stun_effect;

    private List<TextAnimatorPlayer> m_text_animator = new List<TextAnimatorPlayer>();

    private GameObject m_script_manager;
    private GameObject[] m_pop_point = new GameObject[5];
    private GameObject[] m_end_point = new GameObject[5];

    private ColliderManager m_collider_manager;

    private NavMeshAgent m_navmesh_agent;
 
    private Rigidbody m_rigidbody;

    private Animator m_animator;

    private BaseSystem m_base_system;

    private Vector3 m_impact_vector;
    private Vector3 m_impact_power;

    private float m_time_count = 0.0f;
    private float m_hitpoint = 0.0f;
    private float m_damagepoint = 0.0f;
    private float m_stun_set = 0.0f;
    private float m_stun_use = 0.0f;

    private int m_transition = 0;
    private int m_iskinematic_change_frame = 0;
    private int m_text_array = 0;

    enum STATE{
        WAIT,
        MOVE,
        DIE,
        BOMB,
        STUN,
        IMPACT
    }; STATE e_state;

    private void FixedUpdate(){
        m_hitpoint_canvas.transform.rotation = m_hitpoint_canvas.worldCamera.transform.rotation;

        switch(e_state){
            case STATE.WAIT:
                m_time_count += Time.deltaTime;

                if(m_time_count > 1.0f){
                    m_animator.SetBool("animation_move", true);
                    m_navmesh_agent.SetDestination(m_end_point[0].transform.position); // タスク1
                    e_state = STATE.MOVE;
                }
            break;

            case STATE.MOVE:

            break;

            case STATE.DIE:
                m_time_count += Time.deltaTime;

                if(m_time_count > 2.0f){
                    m_animator.SetBool("animation_die", false);
                    MobState(false);
                }
            break;

            case STATE.BOMB:
                m_time_count += Time.deltaTime;

                if (m_time_count > 0.3f) MobState(false);
            break;

            case STATE.STUN:
                m_time_count += Time.deltaTime;

                if (m_time_count > m_stun_use) {
                    m_time_count = 0.0f;
                    m_stun_effect.Stop();
                    m_navmesh_agent.enabled = true;
                    m_rigidbody.isKinematic = true;
                    m_navmesh_agent.SetDestination(m_end_point[0].transform.position); // タスク1
                    e_state = STATE.MOVE;
                }
            break;

            case STATE.IMPACT:
                if(m_transition == 0){
                    m_transition = 1;
                    m_iskinematic_change_frame = Time.frameCount; // 補足2
                    m_navmesh_agent.enabled = false;
                    m_rigidbody.isKinematic = false;
                    m_rigidbody.AddForce(m_impact_power, ForceMode.Impulse);
                }

                if((m_transition == 1) && (m_rigidbody.IsSleeping())){
                    m_transition = 0;
                    m_iskinematic_change_frame = Time.frameCount; // 補足2
                    m_navmesh_agent.enabled = true;
                    m_rigidbody.isKinematic = true;
                    m_navmesh_agent.SetDestination(m_end_point[0].transform.position); // タスク1
                    e_state = STATE.MOVE;
                }
            break;
        }
    }

    private void OnTriggerEnter(Collider a_collider){
        if((e_state == STATE.BOMB) || (e_state == STATE.WAIT) || (e_state == STATE.DIE) || (m_iskinematic_change_frame == Time.frameCount)) return; // 補足2

        if (a_collider.CompareTag("Finish")) { // タスク4
            m_bomb_effect.Play();
            m_base_system.BaseDamage(m_hitpoint_bar.value);
            m_navmesh_agent.enabled = true;
            m_rigidbody.isKinematic = true;
            m_navmesh_agent.ResetPath();
            m_time_count = 0.0f;
            e_state = STATE.BOMB;
            return;
        }

        m_collider_manager.ColliderDataInput(a_collider, this.gameObject, ref m_impact_vector, ref m_damagepoint, ref m_stun_set);

        if(m_damagepoint != 0){
            DamageTextShow((int)m_damagepoint);
            m_hitpoint -= (int)m_damagepoint;
            if(m_hitpoint < 0) m_hitpoint = 0;
            m_hitpoint_bar.value = m_hitpoint;
        }

        if(m_hitpoint == 0){ // タスク2
            m_navmesh_agent.enabled = true;
            m_rigidbody.isKinematic = true;
            m_navmesh_agent.ResetPath();
            m_animator.SetBool("animation_move", false);
            m_animator.SetBool("animation_die", true);
            m_time_count = 0.0f;
            e_state = STATE.DIE;
            return;
        }

        if(m_impact_vector != Vector3.zero){
            m_impact_power = m_impact_vector; // 補足3
            m_transition = 0;
            m_stun_effect.Stop();
            e_state = STATE.IMPACT;
            return;
        }

        if ((m_stun_set != 0.0f) && (e_state != STATE.IMPACT)) {
            m_stun_use = m_stun_set;
            m_navmesh_agent.enabled = true;
            m_rigidbody.isKinematic = true;
            m_navmesh_agent.ResetPath();
            m_stun_effect.Play();
            m_time_count = 0.0f;
            e_state = STATE.STUN;
            return;
        }
    }

    public void Initialize(){
        for(int l_loop = 0; l_loop < m_const_text_array; l_loop += 1){
            var l_create = m_hitpoint_canvas.transform;
            var l_object = Instantiate(m_damage_text, Vector3.zero, Quaternion.identity, l_create);
            m_text_animator.Add(l_object.GetComponent<TextAnimatorPlayer>());
        }

        m_hitpoint_canvas.worldCamera = Camera.main;
        
        m_script_manager = GameObject.Find("ScriptManager");
        m_collider_manager = m_script_manager.GetComponent<ColliderManager>();

        m_navmesh_agent = GetComponent<NavMeshAgent>();
        m_rigidbody = GetComponent<Rigidbody>();
        m_animator = GetComponent<Animator>();
        m_pop_point[0] = GameObject.Find("PopPoint"); // タスク3
        m_end_point[0] = GameObject.Find("PlayerBase"); // タスク3
        m_base_system = m_end_point[0].GetComponent<BaseSystem>();

        m_hitpoint_bar.maxValue = 1000;
    }

    public void MobState(bool a_bool){
        if(a_bool){
            m_hitpoint_bar.value = m_hitpoint = m_hitpoint_bar.maxValue;
            this.gameObject.transform.position = m_pop_point[0].transform.position; // タスク1
            m_navmesh_agent.enabled = true; // 補足1
            m_time_count = 0.0f;         
            e_state = STATE.WAIT;
        }else{
            m_navmesh_agent.enabled = false; // 補足1
            this.gameObject.SetActive(false);
        }
    }

    private void DamageTextShow(float a_damage){
        m_text_animator[m_text_array].gameObject.transform.localPosition = new Vector3(Random.Range(-0.1f, 0.1f), Random.Range(0.9f, 1.1f), 0f);
        m_text_animator[m_text_array++].ShowText(string.Format("<FADE><BOUNCE>{0}</BOUNCE></FADE>", a_damage));

        if(m_const_text_array <= m_text_array) m_text_array = 0;
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数

    [説明]
    補足1 真だと座標移動時に障害物が干渉して初期位置にズレが発生する
    補足2 IsKinematic切り替え時と同フレームのコライダー判定を無視する
    補足3 コライダー内で取得した吹き飛ばし変数をそのまま使用すると吹き飛ぶ前に0で上書きする事が有るため実行用変数を用意

    [バージョン]
    2020-12-12 プーリングとナビゲーション
    2021-01-03 アセットストアのモデル反映とアニメーション
    2021-01-28 吹き飛ばし機能の実装
    2021-02-03 体力システムの実装及びゲージとダメージの可視化
    2021-02-19 味方拠点へのダメージ及び自身の休息
    2021-03-03 スタン動作の追加

    [タスク]
    タスク1 拠点の占拠状態で分岐が必要
    (済)タスク2 プーリング動作確認用のため消去する
    タスク3 沸く場所全ての設定が必要
    タスク4 今後タグを拠点に統一させる
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColliderManager : MonoBehaviour{
    private float m_main_player_impact = 30.0f; // タスク1
    private float m_main_player_impact_damage = 100.0f; // タスク1
    private float m_main_player_skill1_damage = 50.0f; // タスク1

    public void ColliderDataInput(Collider a_collider, GameObject a_object, ref Vector3 a_vector, ref float a_damage, ref float a_stun){
        switch (a_collider.gameObject.tag) {
            case "Player":
                a_vector.Set(a_object.transform.position.x - a_collider.transform.position.x, 0f, a_object.transform.position.z - a_collider.transform.position.z);
                a_vector.Normalize();
                a_vector *= m_main_player_impact;
                a_stun = 0.0f;
                a_damage = Random.Range(m_main_player_impact_damage * 0.8f, m_main_player_impact_damage * 1.2f);
            break;

            case "Finish":
                a_vector.Set(0f, 0f, 0f);
                a_stun = 0.0f;
                a_damage = 9999.0f;
            break;

            case "PlayerSkill1":
                a_vector.Set(0f, 0f, 0f);
                a_stun = 0.0f;
                a_damage = Random.Range(m_main_player_skill1_damage * 0.8f, m_main_player_skill1_damage * 1.2f);
            break;

            case "PlayerSkill2":
                a_vector.Set(0f, 0f, 0f);
                a_stun = 3.0f;
                a_damage = 30.0f;
            break;

            default:
                a_vector.Set(0f, 0f, 0f);
                a_stun = 0.0f;
                a_damage = 0.0f;
            break;
        }
    }

    public void MainPlayerColliderSet(float a_impact, float a_impact_damage, float a_skill1_damage){
        m_main_player_impact = a_impact;
        m_main_player_impact_damage = a_impact_damage;
        m_main_player_skill1_damage = a_skill1_damage;
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数
    e_ 列挙型

    [説明]

    [バージョン]
    2021-01-28 吹き飛ばし機能の実装
    2021-02-03 体力システムの追加とそれに伴うコライダーの分岐
    2021-02-12 操作キャラクターのスキル1を追加
    2021-03-03 スタン動作の追加

    [タスク]
    タスク1 戦闘開始時にMainPlayerColliderSetでステータスを反映させる
    
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class DefensePlayer : MonoBehaviour{
    private const float m_serch_speed = 1.0f; // 補足1

    [SerializeField]
    private SerchObject m_serch_system;

    [SerializeField]
    private BoxCollider m_attack_area;

    private NavMeshAgent m_navmesh_agent;
    private Animator m_animator;
    private GameObject m_target_object;
    private GameObject m_serch_object;

    private Vector3 m_set_position;

    private float m_time_count = 0.0f;

    private int m_attack_switch = 0;

    private bool m_serch_state = false;
    private bool m_attack_state = false;

    enum STATE {
        SERCH,
        MOVE,
        ATTACK
    }; STATE e_state;

    private void Start() {
        Initialize(); // タスク1
    }

    private void FixedUpdate() {
        switch (e_state) {
            case STATE.SERCH:
                m_time_count += Time.deltaTime;

                if ((m_time_count > m_serch_speed) && (!m_serch_state)) {
                    m_navmesh_agent.ResetPath();
                    m_navmesh_agent.velocity = Vector3.zero;
                    m_animator.SetBool("animation_move", false);
                    m_serch_state = true;
                    m_serch_system.SerchStart(m_serch_object, "Enemy");
                }

                if ((m_time_count > m_serch_speed * 2.0f) && (m_serch_state)) {
                    m_serch_state = false;
                    m_time_count = 0.0f;
                    m_target_object = m_serch_system.SerchEnd(m_serch_object, 0); // タスク3

                    if (m_target_object != null) {
                        m_navmesh_agent.SetDestination(m_target_object.transform.position);
                        e_state = STATE.MOVE;
                    }
                }
            break;

            case STATE.MOVE:
                if (m_target_object.activeSelf == false) {
                    m_navmesh_agent.ResetPath();
                    m_navmesh_agent.velocity = Vector3.zero;
                    m_animator.SetBool("animation_move", false);
                    e_state = STATE.SERCH;
                } else if ((this.transform.position - m_target_object.transform.position).sqrMagnitude < 1.0f) {
                    if (((this.transform.position - m_serch_object.transform.position).sqrMagnitude) < ((m_target_object.transform.position - m_serch_object.transform.position).sqrMagnitude)) {
                        m_attack_switch = 2;
                        m_attack_area.tag = "Player"; // タスク5
                    } else {
                        m_attack_switch = 1;
                        m_attack_area.tag = "PlayerSkill2"; // タスク5
                    }

                    this.transform.LookAt(m_target_object.transform);
                    m_navmesh_agent.ResetPath();
                    m_navmesh_agent.velocity = Vector3.zero;
                    m_animator.SetInteger("animation_attack", m_attack_switch);
                    m_time_count = 0.0f;
                    e_state = STATE.ATTACK;
                } else {
                    m_navmesh_agent.SetDestination(m_target_object.transform.position);
                    m_animator.SetBool("animation_move", true);
                }
            break;

            case STATE.ATTACK:
                m_time_count += Time.deltaTime;

                if ((!m_attack_state) && m_time_count > 0.3f) {
                    m_time_count = 0.0f;
                    m_attack_state = true;
                    m_attack_area.enabled = true;
                }

                if ((m_attack_state) && m_time_count > 0.3f) {
                    m_attack_state = false;
                    m_attack_area.enabled = false;
                    m_animator.SetBool("animation_move", false);
                    m_animator.SetInteger("animation_attack", 0);

                    if (m_attack_switch == 1) {
                        m_navmesh_agent.SetDestination(m_set_position);
                        m_animator.SetBool("animation_move", true);
                    }

                    m_time_count = 0.0f;
                    e_state = STATE.SERCH;
                }
            break;
        }
    }

    public void Initialize() {
        m_navmesh_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();
        m_serch_object = GameObject.Find("PlayerBase"); // タスク4
        m_set_position = new Vector3(m_serch_object.transform.position.x, m_serch_object.transform.position.y, m_serch_object.transform.position.z + 1.0f);
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数

    [説明]
    補足1 数値を下げると行動の切り替えが早くなる(強くなる)

    [バージョン]
    2021-02-25 感知エリアから一番近いオブジェクトを追いかける
    2021-03-03 2種類の攻撃を追加しスタン時は拠点に下がる

    [タスク]
    タスク1 初期化は指揮官キャラが呼び出す
    (済)タスク2 臨時の動作(対象に向かって歩くだけ)
    タスク3 攻撃キャラは自身から近い対象にする分岐を入れる
    タスク4 臨時防衛拠点
    タスク5 臨時タグ
*/

スタン状態の追加

m_collider_manager.ColliderDataInput(a_collider, this.gameObject, ref m_impact_vector, ref m_damagepoint, ref m_stun_set);
case "PlayerSkill2":
    a_vector.Set(0f, 0f, 0f);
    a_stun = 3.0f;
    a_damage = 30.0f;
break;

ダメージを取得する関数に引数5を追加し、スタンの数値を持つ攻撃だった場合はスタンの秒数が引数5の変数に入るようにしています。

上記の例では吹き飛ばし無しスタン時間3秒ダメージ30です。

吹き飛ばしとスタンが同時に存在している場合吹き飛ばしが優先されるようにしています。


if ((m_stun_set != 0.0f) && (e_state != STATE.IMPACT)) {
        m_stun_use = m_stun_set;
        m_navmesh_agent.enabled = true;
        m_rigidbody.isKinematic = true;
        m_navmesh_agent.ResetPath();
        m_stun_effect.Play();
        m_time_count = 0.0f;
        e_state = STATE.STUN;
        return;
}

スタンの秒数が存在しており吹き飛ばされている状態では無い場合にスタン状態へ移行するようにしています。

スタン状態では勿論移動を止めるためm_navmesh_agent.ResetPath();で目的地をリセットスタン状態のエフェクトを開始します。

NavMeshAgentやIsKinematicを設定しているのは停止した状態でResetPathを実行するとエラーが発生するため念の為に入れています。


case STATE.STUN:
    m_time_count += Time.deltaTime;

    if (m_time_count > m_stun_use) {
        m_time_count = 0.0f;
        m_stun_effect.Stop();
        m_navmesh_agent.enabled = true;
        m_rigidbody.isKinematic = true;
        m_navmesh_agent.SetDestination(m_end_point[0].transform.position); // タスク1
        e_state = STATE.MOVE;    
    }
break;

スタン状態では指定された秒数の待機後にエフェクトの停止目的地のセットを実行しています。

スタン状態で吹き飛ばされた場合のために、吹き飛ばしを受けた際にもスタンのエフェクトを停止するように追加しています。


味方NPCの更新

敵をサーチする

指定オブジェクトから一番近い敵オブジェクトを取得

移動

サーチで取得したオブジェクトまで移動する

攻撃

オブジェクト接近後に吹き飛ばしかスタン攻撃を行う

次のサーチまでの行動

スタン攻撃の後にはサーチまでの間だけ防衛拠点に向かって移動する

敵をサーチする

前回の記事で作成した関数で一番近いオブジェクトを取得する。


移動

if ((m_time_count > m_serch_speed * 2.0f) && (m_serch_state)) {
    m_serch_state = false;
    m_time_count = 0.0f;
    m_target_object = m_serch_system.SerchEnd(m_serch_object, 0); // タスク3

    if (m_target_object != null) {
        m_navmesh_agent.SetDestination(m_target_object.transform.position);
        e_state = STATE.MOVE;
    }
}

オブジェクトを取得後に移動ステートに移行する。


攻撃

if (((this.transform.position - m_serch_object.transform.position).sqrMagnitude) < ((m_target_object.transform.position - m_serch_object.transform.position).sqrMagnitude)) {
    m_attack_switch = 2;
    m_attack_area.tag = "Player"; // タスク5
} else {
    m_attack_switch = 1;
    m_attack_area.tag = "PlayerSkill2"; // タスク5
}

this.transform.LookAt(m_target_object.transform);
m_navmesh_agent.ResetPath();
m_navmesh_agent.velocity = Vector3.zero;
m_animator.SetInteger("animation_attack", m_attack_switch);
m_time_count = 0.0f;
e_state = STATE.ATTACK;

攻撃の際に敵が自分より拠点側に居るか、自分が拠点側に居るかによって攻撃判定のコライダータグとアニメーションの変数を変更しています。

不自然な感じになるかもしれませんがthis.transform.LookAtにより敵の方向に強制的に向きを変更しています(滑らかな回転ではなく瞬間的に向きが変わります)。

キャラクターの移動を停止させたり、アニメーションを変数をセットして攻撃モーションを開始させたら攻撃ステートへ移行します。


次のサーチまでの行動

m_time_count += Time.deltaTime;

if ((!m_attack_state) && m_time_count > 0.3f) {
    m_time_count = 0.0f;
    m_attack_state = true;
    m_attack_area.enabled = true;
}

if ((m_attack_state) && m_time_count > 0.3f) {
    m_attack_state = false;
    m_attack_area.enabled = false;
    m_animator.SetBool("animation_move", false);
    m_animator.SetInteger("animation_attack", 0);

    if (m_attack_switch == 1) {
        m_navmesh_agent.SetDestination(m_set_position);
        m_animator.SetBool("animation_move", true);
    }

    m_time_count = 0.0f;
    e_state = STATE.SERCH;
}

攻撃ステートでコライダーの切り替え等により攻撃が終了した後はサーチステートへ戻りますが、スタン攻撃だった場合(m_attack_switch == 1)は防衛拠点への移動をセットしています。

これである程度は拠点を守りつつ行動してくれるように感じますが、攻撃モーションや当たり判定が仮の物なので今後大きく修正します

吹き飛ばしの攻撃に前進しながらのコンボ攻撃の分岐等により複数の味方が分散するようにしてから修正現在は乱数無しの攻撃のみなので味方キャラクターを増やしても時間が経過するとほぼ同じ位置で1人しか居ないような状態になる)。


動画


記事のリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です