【自作ゲーム開発6】敵モブの吹き飛ばし【Unity】


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

はじめに

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

今回はモブの吹き飛ばし機能を実装していきます。

吹き飛ばす側の力や吹き飛ばされる側の重量によって吹き飛ばす量を変更したいため物理演算を使用しますがNavMeshAgentとRigidbodyの組み合わせは注意すべき箇所が多いため苦戦しました。

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class CenterMobEnemy : MonoBehaviour{
    private GameObject m_script_manager;
    private ColliderManager m_collider_manager;
    
    private GameObject[] m_pop_point = new GameObject[5];
    private GameObject[] m_end_point = new GameObject[5];

    private NavMeshAgent m_navmesh_agent;
    private Rigidbody m_rigidbody;
    private Animator m_animator;

    private Vector3 m_impact_vector;

    private float m_time_count = 0.0f;

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

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

    private void FixedUpdate(){
        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.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_vector, 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.WAIT) || (e_state == STATE.DIE) || (m_iskinematic_change_frame == Time.frameCount)) return; // 補足2

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

        if(m_impact_vector != Vector3.zero){
            m_transition = 0;
            e_state = STATE.IMPACT;
        }

        if(a_collider.gameObject.tag == "Finish"){ // タスク2
            m_navmesh_agent.enabled = true;
            m_rigidbody.isKinematic = true;
            m_animator.SetBool("animation_move", false);
            m_animator.SetBool("animation_die", true);
            m_time_count = 0.0f;
            e_state = STATE.DIE;
        }
    }

    public void Initialize(){
        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("Goal"); // タスク3
    }

    public void MobState(bool a_bool){
        if(a_bool){
            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);
        }
    }
}

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

    [説明]
    補足1 真だと座標移動時に障害物が干渉して初期位置にズレが発生する
    補足2 IsKinematic切り替え時と同フレームのコライダー判定を無視する

    [バージョン]
    2020-12-12 プーリングとナビゲーション
    2021-01-03 アセットストアのモデル反映とアニメーション
    2021-01-28 吹き飛ばし機能の実装

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

public class ColliderManager : MonoBehaviour{
    private float main_player_impact = 30.0f; // タスク1

    public void ColliderDataInput(Collider a_collider, GameObject a_object, ref Vector3 a_vector){
        if(a_collider.gameObject.tag == "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 *= main_player_impact;
        }else{
            a_vector.Set(0f, 0f, 0f);
        }
    }

    public void MainPlayerColliderSet(float a_impact){
        main_player_impact = a_impact;
    }
}

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

    [説明]

    [バージョン]
    2021-01-28 吹き飛ばし機能の実装

    [タスク]
    タスク1 戦闘開始時にMainPlayerColliderSetでステータスを反映させる
*/

吹き飛ばしについて

今回の実装した吹き飛ばしの方法は以下の流れです。

当たり判定に触れる

自位置と侵入したコライダーを持つオブジェクトの位置を取得する

計算する

お互いの位置から自位置の飛ぶ方向と力量(キャラステータス)を計算する

IsKinematicをオフにする

物理演算を使用する下準備(NavMeshAgentも停止)

吹き飛ばす

AddForceのImpulseを使用して吹き飛ばす

IsKinematicをオンにする

物理演算を使用しないように戻す(NavMeshAgentも起動)

当たり判定に触れる

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

OnTriggerEnter関数の中で計算用の関数に自位置コライダー側の位置参照型のVector3の引数を渡しています。


計算する

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 *= main_player_impact;

計算関数内では自位置(a_object)とコライダー側の位置(a_collider)の位置から角度を計算し参照型のVector3(a_vector)に吹き飛ばす力量を代入しています。

Normalizeで吹き飛ぶ方向が斜め方向であっても斜辺(距離)が1になるように底辺と高さを変更してくれています。

計算された方向(距離はNormalize で必ず1)に対して吹き飛ばし側のステータス倍率を乗算し吹き飛ばす力を変更します。

これにより吹き飛ばされる側の重さ吹き飛ばし側の力が反映された吹き飛ばしが可能となります。


IsKinematicをオフにする

m_navmesh_agent.enabled = false;
m_rigidbody.isKinematic = false;

今回吹き飛ばされる側はNavMeshAgentを使用しているためRigidbodyのIsKinematicをオンにして普段は物理演算を無効にしていますが、吹き飛ばす瞬間だけ物理演算を有効にします。

また同時にNavMeshAgentは停止しておきます。

IsKinematicの切り替えに関する問題点はこちら


吹き飛ばす

m_rigidbody.AddForce(m_impact_vector, ForceMode.Impulse);

先程の関数で計算されたVector3(m_impact_vector)の力量を使用して吹き飛ばします。


IsKinematicをオンにする

m_navmesh_agent.enabled = true;
m_rigidbody.isKinematic = true;

Rigidbodyの動きが停止したのを確認したらIsKinematicをオンにして物理演算を無効とします。

またNavMeshAgentを有効にして自動移動を再開させます。

IsKinematicの切り替えに関する問題点はこちら


IsKinematicの切り替えに関する問題点

IsKinematicは切り替え時に当たり判定を再度取得する動作をするため以下の問題点が発生します。

理想の動作
  1. ダメージと吹き飛ばし効果のある当たり判定に触れる
  2. ダメージ反映させて吹き飛ばし方向を計算する
  3. IsKinematicをオフにして物理演算有効化
  4. 吹き飛ぶ
  5. IsKinematicをオンにして物理演算無効化
問題が発生する動作
  1. ダメージと吹き飛ばし効果のある当たり判定に触れる
  2. ダメージ反映させて吹き飛ばし方向を計算する
  3. IsKinematicをオフにして物理演算有効化
  4. 再度当たり判定が発生してダメージ反映(2重)
  5. 吹き飛ぶ
  6. IsKinematicをオンにして物理演算無効化

IsKinematicをオフにする瞬間はコライダー内に居るため当たり判定を逃れる術は見つける事が出来ませんでした

しかしIsKinematicを切り替えた際に発生する当たり判定は、IsKinematicを切り替えたフレームと同じフレームで発生するようでIsKinematicを切り替えたフレームと同じフレームで発生した当たり判定を無視する事にしました。

以下が該当箇所のソースコードです。

m_iskinematic_change_frame = Time.frameCount; // 補足2
m_navmesh_agent.enabled = false;
m_rigidbody.isKinematic = false;
m_iskinematic_change_frame = Time.frameCount; // 補足2
m_navmesh_agent.enabled = true;
m_rigidbody.isKinematic = true;
if((e_state == STATE.WAIT) || (e_state == STATE.DIE) || (m_iskinematic_change_frame == Time.frameCount)) return; // 補足2

Time.frameCountが現在のフレームを取得するUnityの標準機能です。

m_iskinematic_change_frameに代入して切り替え時のフレームを保存しておき、当たり判定の1行目のif分で無視するかの判定をしています。


動画


記事のリンク

コメントを残す

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