ゼルダの伝説風の4分割ハートアイコンでのHP表現を完全再現!
Animationではなく、コードのみでソレっぽいアニメーションも付けました。
ポップな感じも含めつつ多量のHPを表現可能なので、ACTやARPGに最適。
- 2022/06/21: changeHeartIndexの計算で何故かPiecesPerHeart”Float”になっていた&コード以外の文章を修正。
- 2022/06/26: 一部コメントを修正&StartChangeHearts多重実行時の、「前回の変更を適用」を「前回の適用を戻す」処理に変更。
(同じ値のダメージ&回復が、同時に発生した場合にバグるのを防ぐ)
サンプル動画
見ての通り疑似アニメーションしつつ、スムーズに遷移しております。
(左右の奴はHP増減管理のボタン)
仕組み
ざっくり言うと、HP値をいじくり回して、対象のハートや満ち欠け具合のパラメーターを弾き出している。
- (ハート1つ = 4HP相当)
- (UIのハート数 + HPの増減値) / ハートの構成数で、HPの変更対象のハートのIndexを計算。
- ↑の計算の余り / ハートの構成数でハートの欠片の数を計算。
- 回復時のみ、1と2の値をちょっと補正。
- ImageコンポーネントのfillAmountの値で満ち欠けを表現。
白ハートの画像素材
- 白ハート
- ライセンス: CC0
(一応デカめで作っているので、小さくしたい場合はUnity上で設定してください)
手順
コードを導入&各オブジェクトを作成
- 既存の物に追加 or 新規スクリプトに、コードを全文コピー。
- 空のオブジェクトで、「UIManager」と「Player」を作成し、各スクリプトをアタッチ。
- 任意のCanvas下に空のオブジェクト「Hearts」を作成。
(ハートの親オブジェクト)
白ハートのSpriteを導入&設定
- 白ハートの画像を用意し、Unityへインポート。
- Projectウィンドウで選択。
- インスペクター -> Texture Type -> Sprite (2D and UI)に変更。
- Apply
ハートの設置&設定
- ヒエラルキーウィンドウ -> 右クリック -> UI -> Image
- Imageコンポーネント -> Source Image -> (先程インポートした)white-heartを指定。
- 同オブジェクトにHeartUIをアタッチ。
- 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;
}
}
コレを使って伝説級のゲームを作ろう!