ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】敵キャラの上に体力ゲージを一定時間のみ表示する

time 2017/06/25

今回はUnity上で「体力ゲージを一定時間のみ表示する」のお話です。

普段は体力ゲージを表示せず、被ダメージ時のみ表示され、一定時間後にまた非表示になります。
自作ゲームで言えば、敵キャラクターの体力ゲージで必要になりました。敵の数は多めにする予定なので、常に表示していると邪魔でしょうがないです。ここは「ダメージ後の一定時間のみ表示」するようにしてみます。

ただし「ダメージ発生時の処理」まで書くとあれこれ長くなるので、本記事ではインスタンス化されて呼び出された後の、表示・非表示の処理のみ紹介します。
「体力ゲージ自体の増減」については以下の記事を参考にしてみてください。

【Unity】体力ゲージの実装方法の紹介 (一瞬で減る緑ゲージ+徐々に減る赤ゲージ)

ソースコード

今回はソースコードがちょっと長いので、先に全体のコードを貼っておきます。
ゲージを管理する「EnemyLifeView」クラスと個々のゲージを表す「GameEnemyLife」クラスの2つ。

using System.Collections.Generic;
using UniRx;
using UnityEngine;

/// <summary>
/// 敵キャラクターの体力ゲージを表示するViewクラス
/// </summary>
public class EnemyLifeView : MonoBehaviour
{
    [SerializeField]
    private GameEnemyLife enemyLifePrefab;

    [SerializeField]
    private Dictionary<int, GameEnemyLife> showingList = new Dictionary<int, GameEnemyLife>();

    /// <summary>
    /// 敵キャラの体力ゲージを表示
    /// </summary>
    /// <param name="enemy"></param>
    public void ShowEnemyLife(Enemy enemy)
    {
        var hashCode = enemy.GetHashCode();
        if (showingList.ContainsKey(hashCode)) {
            showingList[hashCode].Show(enemy);
            return;
        }

        var enemyLife = Instantiate(enemyLifePrefab, this.transform);
        enemyLife.Show(enemy);

        showingList.Add(hashCode, enemyLife);
    }
}
using DG.Tweening;
using UnityEngine;

public class GameEnemyLife : MonoBehaviour
{
    [SerializeField]
    private LifeGaugeController lifeGaugeController;

    private RectTransform rectTransform;
    private CanvasGroup canvasGroup;
    private Enemy enemy;
    private Tween currentShowTween;

    private void Awake()
    {
        canvasGroup = GetComponent<CanvasGroup>();
        rectTransform = GetComponent<RectTransform>();
    }

    void Update()
    {
        if (enemy != null) {
            var showPosition = enemy.BodyBounds.center + new Vector3(0, enemy.BodyBounds.extents.y, 0);
            rectTransform.position = Camera.main.WorldToScreenPoint(showPosition);
        }
    }

    public void Show(Enemy enemy)
    {
        // 既にTweenがある場合は殺しておく
        if (currentShowTween != null) {
            currentShowTween.Kill();
        }

        this.enemy = enemy;

        // ここで体力ゲージの増減処理
        lifeGaugeController.Init(enemy, true);

        // 表示・非表示
        currentShowTween = DOTween.Sequence()
            .AppendCallback(() => gameObject.SetActive(true))
            .Append(canvasGroup.DOFade(0.8f, 0.25f))
            .AppendInterval(0.5f)
            .Append(canvasGroup.DOFade(0, 0.25f))
            .AppendCallback(() => gameObject.SetActive(false))
            .SetLink(gameObject)
            .Play();
    }
}

この中でEnemyクラスは敵キャラクターに設定するコンポーネントで、LifeGaugeControllerは体力ゲージの増減を行うクラスです。
表示・非表示の処理とは関係ないため説明は割愛します。

体力ゲージのプレハブと表示用Viewの準備

まずは体力ゲージのオブジェクトを作り、プレハブ化します。

後述のフェードインアウト処理のため、親オブジェクトにCanvasGroupコンポーネントを付け、Alphaを0にしておきます。

次にCanvas下に空オブジェクトを作り、コンポーネントに先ほどの「EnemyLifeView」を設定します。
「EnemyLifePrefab」には体力ゲージのプレハブを設定します。

表示処理

生成と削除

敵キャラクターの被ダメージ時にEnemyLifeViewクラスのShowEnemyLife()を呼び出します。

public void ShowEnemyLife(Enemy enemy)
{
    var hashCode = enemy.GetHashCode();
    if (showingList.ContainsKey(hashCode)) {
        showingList[hashCode].Show(enemy);
        return;
    }

    var enemyLife = Instantiate(enemyLifePrefab, this.transform);
    enemyLife.Show(enemy);

    showingList.Add(hashCode, enemyLife);
}

ポイントはGetHashCode()で、敵キャラの体力ゲージが既に生成されているかを判定します。
既にあればGameEnemyLifeクラスのShow()を呼び出し、なければInstantiate()で生成してから呼び出します。

`Instantiate()`の第2引数は何の意味があるの?

Instantiate()の第2引数で生成時の親オブジェクトを指定できます。
UIなのでCanvas下に置きたいのはもちろん、生成時に子オブジェクトにしないとCanvasScalerの設定が適用されないようなので注意します。

フェードイン・アウト

DOTweenならCanvasGroupに対してDOFade()が使えるので、それが一番手っ取り早いです。
前述のShowEnemyLife()からGameEnemyLifeクラスのShow()が呼び出されます。

public void Show(Enemy enemy)
{
    // 既にTweenがある場合は殺しておく
    if (currentShowTween != null) {
        currentShowTween.Kill();
    }

    this.enemy = enemy;

    // ここで体力ゲージの増減処理
    lifeGaugeController.Init(enemy, true);

    // 表示・非表示
    currentShowTween = DOTween.Sequence()
        .AppendCallback(() => gameObject.SetActive(true))
        .Append(canvasGroup.DOFade(0.8f, 0.25f))
        .AppendInterval(0.5f)
        .Append(canvasGroup.DOFade(0, 0.25f))
        .AppendCallback(() => gameObject.SetActive(false))
        .SetLink(gameObject)
        .Play();
}

CanvasGroupの参照はStart()時に取っておきます。
DOTweenのSequenceでAppend()AppendInterval()を組み合わせ、「DOFadeで表示→少し待つ→DOFadeで非表示」をしています。
また非表示時はUpdate()での位置調整(後述)が要らないので、前後にSetActive()を挟んでON/OFFしています。

またSequenceの返り値のTweenインスタンスをフィールドに保持するのも大切です。
これは敵キャラが連続でダメージを受けた場合、フェード実行前にTweenを殺しておかないと一瞬だけフェードインが行われてしまうためです。

表示位置の調整

単に生成しただけだと敵が移動したときにずれるので、GameEnemyLifeUpdate()で表示位置を更新し続けます。

void Update()
{
    if (enemy != null) {
        var showPosition = enemy.BodyBounds.center + new Vector3(0, enemy.BodyBounds.extents.y, 0);
        rectTransform.position = Camera.main.WorldToScreenPoint(showPosition);
    }
}

ポイントはEnemyクラスから「体のオブジェクトのBounds」を参照可能にすることです。
私はEnemyクラスにBodyBoundsプロパティを作り、そのゲッタから取れるようにしました。Enemyクラスでの設定方法は割愛しますが、フィールド作ってインスペクター上で指定するのが楽かと。

単純に「`transform.position`+`new Vector3(0, 2f, 0)`」とかじゃダメなの?

単に「transform.positionから上に少しずらす」だと、敵キャラの大きさが異なる場合に表示位置が残念になります。
オブジェクトの原点が中心からずれている場合も同様です。

なので敵キャラの体のBoundsを見て、そのcenterからextents分だけ縦にずらします。
下の画像の枠がBoundsの大きさです。ちょっと分かりにくいですが、この中心から高さの半分だけずらすイメージ。

Boundsならキャラの大きさや原点の位置を気にしなくていいから楽だよ!

Boundsについては以下の記事で説明しているので、興味のある方はご参照ください。

【Unity】3D空間を定義する「Bounds」の基礎知識

動作テスト

実際に動かすとこんな感じ。

敵キャラの上に体力ゲージが表示されること、連続して攻撃した場合にフェードインが挟まらないことを確認します。

down

コメントする