205 lines
6.6 KiB
C#
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
|
|
}
|
|
}
|