using System.Collections; using UnityEngine; using UnityEngine.UI; namespace Streamingle.Contents.BossRaid { /// /// 데미지 숫자 팝업을 생성하고 관리합니다. /// WorldSpace Canvas에서 숫자가 튀어나와 중력으로 떨어집니다. /// 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 /// /// 데미지 숫자를 생성합니다. /// bounds가 있으면 바운더리 외곽 범위에서 랜덤 위치에 생성됩니다. /// public void Spawn(int damage, bool isCritical, Vector3 worldPosition, Bounds? bounds = null) { EnsureWorldCanvas(); var go = CreatePopupObject(); var text = go.GetComponent(); if (text == null) text = go.GetComponentInChildren(); 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(); _worldCanvas.renderMode = RenderMode.WorldSpace; _worldCanvas.sortingOrder = 100; var rectTransform = canvasObj.GetComponent(); 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.alignment = TextAnchor.MiddleCenter; text.horizontalOverflow = HorizontalWrapMode.Overflow; text.verticalOverflow = VerticalWrapMode.Overflow; text.raycastTarget = false; if (_font != null) text.font = _font; var rect = go.GetComponent(); rect.sizeDelta = new Vector2(400f, 200f); rect.localScale = Vector3.one; // 아웃라인 (가독성) var outline = go.AddComponent(); 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(); 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 } }