205 lines
6.6 KiB
C#

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 데미지 숫자 팝업을 생성하고 관리합니다.
/// WorldSpace Canvas에서 숫자가 튀어나와 중력으로 떨어집니다.
/// </summary>
public class DamagePopup : MonoBehaviour
{
#region Fields
[Header("프리팹")]
[SerializeField]
[Tooltip("데미지 숫자 프리팹. 없으면 자동 생성")]
private GameObject popupPrefab;
[Header("일반 히트")]
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private int normalFontSize = 32;
[Header("크리티컬")]
[SerializeField] private Color criticalColor = new Color(1f, 0.2f, 0.2f);
[SerializeField] private int criticalFontSize = 48;
[Header("애니메이션")]
[SerializeField] private float popupDuration = 1.2f;
[SerializeField] private float upwardForce = 3f;
[SerializeField] private float gravity = 8f;
[Header("위치")]
[SerializeField]
[Tooltip("바운더리 외곽 여유 (콜라이더 경계 밖으로 얼마나 더 퍼질지)")]
private float spreadMargin = 0.5f;
[SerializeField]
[Tooltip("바운더리가 없을 때 기본 X 퍼짐")]
private float fallbackSpreadX = 1f;
[SerializeField]
[Tooltip("바운더리가 없을 때 기본 높이 오프셋")]
private float fallbackHeightOffset = 1.5f;
private Canvas _worldCanvas;
private Font _font;
#endregion
#region Unity Messages
private void Awake()
{
_font = BossRaidFontLoader.Load();
EnsureWorldCanvas();
}
#endregion
#region Public Methods
/// <summary>
/// 데미지 숫자를 생성합니다.
/// bounds가 있으면 바운더리 외곽 범위에서 랜덤 위치에 생성됩니다.
/// </summary>
public void Spawn(int damage, bool isCritical, Vector3 worldPosition, Bounds? bounds = null)
{
EnsureWorldCanvas();
var go = CreatePopupObject();
var text = go.GetComponent<Text>();
if (text == null) text = go.GetComponentInChildren<Text>();
if (text != null)
{
text.text = damage.ToString();
text.color = isCritical ? criticalColor : normalColor;
text.fontSize = isCritical ? criticalFontSize : normalFontSize;
if (isCritical) text.fontStyle = FontStyle.Bold;
}
Vector3 spawnPos;
if (bounds.HasValue)
{
var b = bounds.Value;
// 바운더리 외곽에서 랜덤 위치
float halfX = b.extents.x + spreadMargin;
float topY = b.max.y + spreadMargin;
float bottomY = b.center.y;
spawnPos = new Vector3(
b.center.x + Random.Range(-halfX, halfX),
Random.Range(bottomY, topY),
b.center.z
);
}
else
{
spawnPos = worldPosition + new Vector3(
Random.Range(-fallbackSpreadX, fallbackSpreadX),
fallbackHeightOffset + Random.Range(0f, 1f),
0f
);
}
go.transform.position = spawnPos;
StartCoroutine(AnimatePopup(go, isCritical));
}
#endregion
#region Private Methods
private void EnsureWorldCanvas()
{
if (_worldCanvas != null) return;
var canvasObj = new GameObject("BossRaid_DamagePopupCanvas");
canvasObj.transform.SetParent(transform);
_worldCanvas = canvasObj.AddComponent<Canvas>();
_worldCanvas.renderMode = RenderMode.WorldSpace;
_worldCanvas.sortingOrder = 100;
var rectTransform = canvasObj.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(10f, 10f);
rectTransform.localScale = Vector3.one * 0.01f;
}
private GameObject CreatePopupObject()
{
if (popupPrefab != null)
return Instantiate(popupPrefab, _worldCanvas.transform);
var go = new GameObject("DamageNumber");
go.transform.SetParent(_worldCanvas.transform);
var text = go.AddComponent<Text>();
text.alignment = TextAnchor.MiddleCenter;
text.horizontalOverflow = HorizontalWrapMode.Overflow;
text.verticalOverflow = VerticalWrapMode.Overflow;
text.raycastTarget = false;
if (_font != null) text.font = _font;
var rect = go.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(400f, 200f);
rect.localScale = Vector3.one;
// 아웃라인 (가독성)
var outline = go.AddComponent<Outline>();
outline.effectColor = Color.black;
outline.effectDistance = new Vector2(2f, -2f);
return go;
}
private IEnumerator AnimatePopup(GameObject go, bool isCritical)
{
if (go == null) yield break;
Vector3 startPos = go.transform.position;
float velocityY = upwardForce * (isCritical ? 1.3f : 1f);
float velocityX = Random.Range(-0.5f, 0.5f);
float elapsed = 0f;
Vector3 startScale = go.transform.localScale;
Vector3 peakScale = startScale * (isCritical ? 1.5f : 1.2f);
var text = go.GetComponent<Text>();
while (elapsed < popupDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / popupDuration;
velocityY -= gravity * Time.deltaTime;
startPos.x += velocityX * Time.deltaTime;
startPos.y += velocityY * Time.deltaTime;
go.transform.position = startPos;
float scaleT = t < 0.2f ? t / 0.2f : 1f;
go.transform.localScale = Vector3.Lerp(peakScale, startScale, scaleT);
float alpha = t < 0.6f ? 1f : 1f - ((t - 0.6f) / 0.4f);
if (text != null)
{
var c = text.color;
c.a = alpha;
text.color = c;
}
if (Camera.main != null)
go.transform.rotation = Camera.main.transform.rotation;
yield return null;
}
Destroy(go);
}
#endregion
}
}