【自作ゲーム開発7】体力ゲージ(HPバー)とダメージを可視化【Unity】


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

はじめに

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

今回はモブの体力とダメージを可視化してみます。

UI要素はキャンバスを分けて負荷を下げたり出来る可能性もありそうですが現状は同じキャンパス上で体力ゲージとダメージを表示します。

今回は既存のスクリプト2つを更新します。

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

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;

    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 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 int m_transition = 0;
    private int m_iskinematic_change_frame = 0;
    private int m_text_array = 0;

    enum STATE{
        WAIT,
        MOVE,
        DIE,
        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.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.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, ref m_damagepoint);

        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;
            e_state = STATE.IMPACT;
        }
    }

    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("Goal"); // タスク3

        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 体力システムの実装及びゲージとダメージの可視化

    [タスク]
    タスク1 拠点の占拠状態で分岐が必要
    タスク2 プーリング動作確認用のため消去する
    タスク3 沸く場所全ての設定が必要
*/
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 = 200.0f; // タスク1

    public void ColliderDataInput(Collider a_collider, GameObject a_object, ref Vector3 a_vector, ref float a_damage){
        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 *= m_main_player_impact;
            a_damage = Random.Range(m_main_player_impact_damage * 0.8f, m_main_player_impact_damage * 1.2f);
        }else if(a_collider.gameObject.tag == "Finish"){
            a_vector.Set(0f, 0f, 0f);
            a_damage = 9999.0f;
        }else{
            a_vector.Set(0f, 0f, 0f);
            a_damage = 0.0f;
        }
    }

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

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

    [説明]

    [バージョン]
    2021-01-28 吹き飛ばし機能の実装
    2021-02-03 体力システムの追加とそれに伴うコライダーの分岐

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

体力の可視化

体力の可視化はUIのスライダーを使用しました。

モブにCanvasを追加する

CanvasのRender ModeをWorld Spaceに変更する。

つまみを削除する

体力を表示するだけなのでHandle Slide Areaは削除する。

その他の設定をおこなう

ゲージを両端まで反映させたりゲージの色やサイズ等。

スクリプトで操作

Max ValueやValueを操作して体力をゲージに反映させる。

モブにCanvasを追加する

Render ModeWorld Spaceに変更する。


つまみを削除する

削除前

削除後


その他の設定をおこなう

Fill AreaとFillどちらも0詰めにする。

またスライダーが大きすぎるためScaleもお好みで調整(0.005, 0.01, 0.05)

ついでにBackgroundのColorを赤に変更しFillのColorを緑に変更しました。


スクリプトで操作

m_hitpoint_bar.maxValue = 1000;

maxValue = 数値で最大値の設定(m_hitpoint_barはSlider)。


m_hitpoint_bar.value = m_hitpoint;

value = 数値で体力を反映させる(m_hitpointは現在体力)。


m_hitpoint_canvas.worldCamera = Camera.main;

Render ModeをWorldSpaceとした場合のEvent CameraはNoneとなっていますがNoneのままでは負荷が大きいと公式のマニュアルに記載されているためオブジェクト生成時にメインカメラを割り当てています。


m_hitpoint_canvas.transform.rotation = m_hitpoint_canvas.worldCamera.transform.rotation;

FixedUpdate内で作成したCanvasを正面に向かせ続ける。


ダメージの可視化

今回はダメージの可視化に加えてテキストを少しアニメーションさせています。

アニメーションさせるために「Text Animator for Unity」を使用しています。

作成したTextMeshProに以下2つのコンポーネントを追加します。

  • TextAnimator
  • TextAnimatorPlayer

TextAnimatorでは使用するアニメーションの1つであるBounceのAmplitubeを0.4に変更し文字が跳ねる強さを初期より強めました。


TextAnimatorPlayerは追加した状態のままです。


m_text_animator[m_text_array++].ShowText(string.Format("<FADE><BOUNCE>{0}</BOUNCE></FADE>", a_damage));

アニメーションさせたい文字を指定のキーワードで囲みます

今回はFadeBounceを使用。


using Febucci.UI;

参照を宣言しておき

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));

1行目はテキストの表示位置を多少ずらしています。

2行目のShowTextでアニメーション付きの文字を表示しています。


今回使用するアセット


動画


記事のリンク

コメントを残す

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