【Unity】ゼルダの伝説風HPシステムを完全再現

【Unity】ゼルダの伝説風HPシステムを完全再現

ゼルダの伝説風の4分割ハートアイコンでのHP表現を完全再現!
Animationではなく、コードのみでソレっぽいアニメーションも付けました。

ポップな感じも含めつつ多量のHPを表現可能なので、ACTやARPGに最適。

  • 2022/06/21: changeHeartIndexの計算で何故かPiecesPerHeart”Float”になっていた&コード以外の文章を修正。
  • 2022/06/26: 一部コメントを修正&StartChangeHearts多重実行時の、「前回の変更を適用」を「前回の適用を戻す」処理に変更。
    (同じ値のダメージ&回復が、同時に発生した場合にバグるのを防ぐ)

サンプル動画

見ての通り疑似アニメーションしつつ、スムーズに遷移しております。
(左右の奴はHP増減管理のボタン)

仕組み

ざっくり言うと、HP値をいじくり回して、対象のハートや満ち欠け具合のパラメーターを弾き出している。

  • (ハート1つ = 4HP相当)

  1. (UIのハート数 + HPの増減値) / ハートの構成数で、HPの変更対象のハートのIndexを計算。
  2. ↑の計算の余り / ハートの構成数でハートの欠片の数を計算。
  3. 回復時のみ、1と2の値をちょっと補正。
  4. ImageコンポーネントのfillAmountの値で満ち欠けを表現。

白ハートの画像素材

(一応デカめで作っているので、小さくしたい場合はUnity上で設定してください)

手順

コードを導入&各オブジェクトを作成

  1. 既存の物に追加 or 新規スクリプトに、コードを全文コピー。
  2. 空のオブジェクトで、「UIManager」と「Player」を作成し、各スクリプトをアタッチ。
  3. 任意のCanvas下に空のオブジェクト「Hearts」を作成。
    (ハートの親オブジェクト)

白ハートのSpriteを導入&設定

  1. 白ハートの画像を用意し、Unityへインポート。
  2. Projectウィンドウで選択。
  3. インスペクター -> Texture Type -> Sprite (2D and UI)に変更。
  4. Apply

ハートの設置&設定

  1. ヒエラルキーウィンドウ -> 右クリック -> UI -> Image
  2. Imageコンポーネント -> Source Image -> (先程インポートした)white-heartを指定。
  3. 同オブジェクトにHeartUIをアタッチ。
  4. Projectウィンドウにドラッグ&ドロップして、プレハブ化しておく。

各オブジェクトの紐付け

  • UIManagerのインスペクターから各オブジェクトを紐付けしておく。
    • player: プレーヤー
    • heartPrefab: ハートUIのプレハブ
    • heartsParentTf: 任意のCanvas下に設置したHearts (ハートの親オブジェクト)

コード

UIManager


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIManager : MonoBehaviour
{

	public static UIManager Instance = null;



	[Header("Playerを紐付けしておく")]
	[SerializeField]
	Player player;

	[Header("ハートUIのプレハブを紐付けしておく")]
	[SerializeField]
	GameObject heartPrefab;

	[Header("Hearts(Canvasに設置したハートの親オブジェクト)を紐付けしておく")]
	[SerializeField]
	Transform heartsParentTf;


//【任意の値】UIのハート数の上限。
	static readonly int HeartUIMax = 3;

	List<HeartUI> heartUIList = new List<HeartUI>(HeartUIMax);

//1ハートの構成ピース数。
	static readonly int PiecesPerHeart = 4;

//1ハートの構成ピース数(float版)。
	static readonly float PiecesPerHeartFloat = 4.0f;

//ハートUI設置時の基本位置。
	Vector2 heartsBasePosition;

//【任意の値】ハートの増減アニメーションのトータルの長さ。
	static readonly float ChangeHeartsDuration = 0.25f;

//ハートの一欠片(1/4)の増減アニメーションの長さ(ChangeHeartsDurationと増減数で計算される)。
	float changeHeartPieceDuration;

//表示上のハート数。
	int displayedHp;

	Coroutine changeHearts;
	float changeHeartsElapsedTime;
	Coroutine changeHeartsEffect;
	float changeHeartsEffectElapsedTime;

//HPの増減方向を示す。回復時には1が、ダメージ時には-1が入る。
	int changeHeartsSign;

	float changeHeartFillAmount;
	float changeHeartPreviousFillAmount;
	int changeHeartIndex;



//ハート変更エフェクト関係。

//ハートの基本色。
	public static readonly Color32 HeartDefaultColor = new Color32(255, 96, 43, 255);
//ハートの点滅エフェクト時の色。
	static readonly Color32 HeartFlickerColor = new Color32(255, 255, 255, 128);
//ハートの基本スケール。
	static readonly Vector3 HeartDefaultScale = new Vector3(1.0f, 1.0f, 1.0f);
//ハートの点滅エフェクト時のスケール。
	static readonly Vector3 HeartFlickerScale = new Vector3(1.3f, 1.3f, 1.3f);

	float changeHeartsEffectPhase;

//これらで補正してDurationの間に0~1.0f~0を取得している。
	static readonly float PingPongThreshold = 1.0f;
	static readonly float ChangeHeartsEffectPhaseRate = 2.0f;



//適当にシングルトン化。
	void Awake()
	{
		if (Instance == null)
			Instance = this;
		else if (Instance != this)
			Destroy(gameObject);    

		DontDestroyOnLoad(gameObject);



//初期化&設置(アクティブ化)。
		InitializeHeartUIs();
		InitializeHpMax();
	}


//ハートUIを非アクティブ状態で待機させている。
	void InitializeHeartUIs()
	{
		for (int i = 0; i < HeartUIMax; i++) {
			heartUIList.Add(Instantiate(heartPrefab, heartsParentTf).GetComponent<HeartUI>());
			heartUIList[i].SetActive(false);
		}
	}

//メインゲーム開始時に呼ぶ。HPの分ハートをアクティブ化している。
	void InitializeHpMax()
	{
		displayedHp = player.hpMax;


//左揃えVer
//        heartsBasePosition.x = 0;
//中央揃えVer
		heartsBasePosition.x = (displayedHp / PiecesPerHeart - 1) * HeartUI.Size * -0.5f;


		for (int i = 0; i < displayedHp; i += PiecesPerHeart) {
			heartUIList[i / PiecesPerHeart].rt.anchoredPosition = heartsBasePosition;
			heartsBasePosition.x += HeartUI.Size;

			heartUIList[i / PiecesPerHeart].SetActive(true);
		}
	}



//HP増減時にコレを呼ぶ。
	public void UpdateHearts()
	{
		StartChangeHearts();
		StartChangeHeartsEffect();
	}



//HP変動時のハート充填量遷移エフェクト。
	void StartChangeHearts()
	{
//重複実行チェック。
		if (changeHearts != null) {
			StopCoroutine(changeHearts);
//多重実行された場合は前回の変更を戻しておく。
			heartUIList[changeHeartIndex].img.fillAmount = changeHeartPreviousFillAmount;
		}

		changeHearts = StartCoroutine(ChangeHearts());
	}


	IEnumerator ChangeHearts()
	{
//回復時
		if (displayedHp < player.hp) {
			changeHeartsSign = 1;
		} else {
//ダメージ時
			changeHeartsSign = -1;
		}

//トータルのDurationから1ピースのDurationを計算。
		changeHeartPieceDuration = ChangeHeartsDuration / Mathf.Abs(displayedHp - player.hp);

//表示上のハート数とHP値が一致しない場合はループ。
		while (displayedHp != player.hp) {

//変更対象のハートIndexを計算。
			changeHeartIndex = (displayedHp + changeHeartsSign) / PiecesPerHeart;
//変更対象のハートのfillAmountを計算。
			changeHeartFillAmount = (displayedHp + changeHeartsSign) % PiecesPerHeart / PiecesPerHeartFloat;

//回復時にHPの端数がハート構成数で割り切れる(8等)場合に、通常の計算だと対象のハートが別(対象が3番目でfillAmountが0)になるので、一つ前にズラし満タンに補正する。
			if (0 < changeHeartsSign && changeHeartFillAmount == 0) {
				changeHeartFillAmount = 1.0f;
				changeHeartIndex--;
			}


//fillAmountの値を変更するループ。
			changeHeartsElapsedTime = 0;
			changeHeartPreviousFillAmount = heartUIList[changeHeartIndex].img.fillAmount;
			while (changeHeartsElapsedTime < changeHeartPieceDuration) {
				changeHeartsElapsedTime += Time.deltaTime;

				heartUIList[changeHeartIndex].img.fillAmount = Mathf.Lerp(changeHeartPreviousFillAmount, changeHeartFillAmount, changeHeartsElapsedTime / changeHeartPieceDuration);

				yield return null;
			}
			displayedHp += changeHeartsSign;
		}

		changeHearts = null;
	}


//HP変動時のハートの点滅エフェクト。
	void StartChangeHeartsEffect()
	{
//重複実行チェック。
		if (changeHeartsEffect != null) {
			StopCoroutine(changeHeartsEffect);
		}

		changeHeartsEffect = StartCoroutine(ChangeHeartsEffect());
	}


//表示されているハートのみに良い感じにエフェクトを掛けている。
	IEnumerator ChangeHeartsEffect()
	{
		changeHeartsEffectElapsedTime = 0;

		while (changeHeartsEffectElapsedTime < ChangeHeartsDuration) {
			changeHeartsEffectElapsedTime += Time.deltaTime;

//Lerpの方は0~1.0fの範囲をはみ出しても対応してくれると思うが、PingPongでちょっとズレるのでClamp01で補正している。
			changeHeartsEffectPhase = Mathf.PingPong(Mathf.Clamp01(changeHeartsEffectElapsedTime / ChangeHeartsDuration) * ChangeHeartsEffectPhaseRate, PingPongThreshold);

			for (int i = 0; i < HeartUIMax; i++) {
				if (heartUIList[i].isActive && heartUIList[i].img.fillAmount != 0) {
					heartUIList[i].img.color = Color.Lerp(HeartDefaultColor, HeartFlickerColor, changeHeartsEffectPhase);
					heartUIList[i].tf.localScale = Vector3.Lerp(HeartDefaultScale, HeartFlickerScale, changeHeartsEffectPhase);
				}
			}
			yield return null;
		}

		changeHeartsEffect = null;
	}

}

Player


//使わなかった為、一応コメントアウトしているので、使う時は戻してください。
//using System.Collections;
//using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

	[System.NonSerialized]
	public int hp = 12;

//ハート1つで4HP相当なので、ハート3つだと12。
	[System.NonSerialized]
	public int hpMax = 12;


//被ダメージ時に呼ぶ。
	public void TakeDamage(int damage)
	{
		hp -= damage;

		if (hp < 0)
			hp = 0;

		UIManager.Instance.UpdateHearts();
	}

//回復時に呼ぶ。
	public void Heal(int healAmount)
	{
		hp += healAmount;

		if (hpMax < hp)
			hp = hpMax;

		UIManager.Instance.UpdateHearts();
	}

}

HeartUI


using UnityEngine;
using UnityEngine.UI;

public class HeartUI : MonoBehaviour
{

//Resetの方で紐付けされるので基本いじらなくて良い筈。外れていた場合は手動で紐付けしてください。
	public GameObject go;
	public Transform tf;
	public RectTransform rt;
	public Image img;

//アクティブ状態のフラグ。
	public bool isActive;

//【任意の値】ハートの大きさ。設置時にズラす距離を計算するのに使用。
	public static readonly float Size = 100.0f;


//非実行時のスクリプトアタッチ時に呼ばれる奴。各コンポーネントを紐付けしたり、諸々設定を変更している。
	void Reset()
	{
		if (go == null) {
			go = gameObject;
			tf = transform;
			rt = GetComponent<RectTransform>();
			rt.sizeDelta = Vector2.one * Size;
			img = GetComponent<Image>();

			img.type = Image.Type.Filled;
			img.color = UIManager.HeartDefaultColor;
			img.raycastTarget = false;

			go.name = "HeartUI";
		}
	}

	public void SetActive(bool b)
	{
		go.SetActive(b);
		isActive = b;
	}

}

コレを使って伝説級のゲームを作ろう!

タイトルとURLをコピーしました