using System.Collections; using UnityEngine; using UnityEngine.UI; namespace Streamingle.Contents.BossRaid { /// /// 콤보 카운터 UI. 컴팩트 디자인, 오른쪽 수직 중앙. /// 히트 시 슬라이드 인 + 펀치 스케일, 타임아웃 시 슬라이드 아웃. /// public class ComboCounter : MonoBehaviour { #region Fields [Header("설정")] [SerializeField] private float comboTimeout = 2f; [Header("비주얼")] [SerializeField] private Color comboColor = new Color(1f, 0.85f, 0.15f); [SerializeField] private Color labelColor = new Color(0.85f, 0.85f, 0.9f); [SerializeField] private int baseFontSize = 80; [SerializeField] private int maxFontSize = 120; [SerializeField] private int fontSizeGrowPerHit = 1; private Canvas _canvas; private CanvasGroup _canvasGroup; private Text _comboText; private Text _hitLabel; private RectTransform _rootRect; private int _currentCombo; private float _lastHitTime; private Coroutine _punchCoroutine; private Coroutine _slideCoroutine; private Font _font; private bool _isShowing; // 슬라이드 위치 (화면 오른쪽 밖 → 안) private const float OffScreenX = 120f; private const float OnScreenX = -40f; #endregion #region Properties public int CurrentCombo => _currentCombo; #endregion #region Unity Messages private void Awake() { _font = BossRaidFontLoader.Load(); CreateUI(); _canvasGroup.alpha = 0f; _isShowing = false; } private void Update() { if (_currentCombo <= 0) return; if (Time.time - _lastHitTime > comboTimeout) SlideOut(); } #endregion #region Public Methods public void RegisterHit() { _currentCombo++; _lastHitTime = Time.time; UpdateDisplay(); // 슬라이드 인 (처음이거나 숨겨진 상태일 때) if (!_isShowing) SlideIn(); // 펀치 스케일 if (_punchCoroutine != null) StopCoroutine(_punchCoroutine); _punchCoroutine = StartCoroutine(PunchCoroutine()); } public void ResetCombo() { _currentCombo = 0; _isShowing = false; if (_canvasGroup != null) _canvasGroup.alpha = 0f; } public void Hide() { ResetCombo(); } #endregion #region Private Methods private void UpdateDisplay() { _comboText.text = _currentCombo.ToString(); _comboText.fontSize = Mathf.Min(baseFontSize + _currentCombo * fontSizeGrowPerHit, maxFontSize); } private void SlideIn() { if (_slideCoroutine != null) StopCoroutine(_slideCoroutine); _slideCoroutine = StartCoroutine(SlideCoroutine(OffScreenX, OnScreenX, 0f, 1f, 0.2f)); _isShowing = true; } private void SlideOut() { if (!_isShowing) return; if (_slideCoroutine != null) StopCoroutine(_slideCoroutine); _slideCoroutine = StartCoroutine(SlideOutAndReset()); } private IEnumerator SlideCoroutine(float fromX, float toX, float fromAlpha, float toAlpha, float duration) { float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = elapsed / duration; float ease = 1f - (1f - t) * (1f - t); // EaseOutQuad _rootRect.anchoredPosition = new Vector2(Mathf.Lerp(fromX, toX, ease), _rootRect.anchoredPosition.y); _canvasGroup.alpha = Mathf.Lerp(fromAlpha, toAlpha, ease); yield return null; } _rootRect.anchoredPosition = new Vector2(toX, _rootRect.anchoredPosition.y); _canvasGroup.alpha = toAlpha; _slideCoroutine = null; } private IEnumerator SlideOutAndReset() { yield return SlideCoroutine(OnScreenX, OffScreenX, 1f, 0f, 0.3f); _currentCombo = 0; _isShowing = false; } private IEnumerator PunchCoroutine() { var rect = _comboText.rectTransform; float punchSize = 1.4f; float duration = 0.12f; float elapsed = 0f; rect.localScale = Vector3.one * punchSize; while (elapsed < duration) { elapsed += Time.deltaTime; float t = elapsed / duration; // EaseOutBack float ease = 1f + 2.7f * Mathf.Pow(t - 1f, 3f) + 1.7f * Mathf.Pow(t - 1f, 2f); rect.localScale = Vector3.one * Mathf.LerpUnclamped(punchSize, 1f, ease); yield return null; } rect.localScale = Vector3.one; _punchCoroutine = null; } private void CreateUI() { var canvasObj = new GameObject("BossRaid_ComboCounter"); canvasObj.transform.SetParent(transform); _canvas = canvasObj.AddComponent(); _canvas.renderMode = RenderMode.ScreenSpaceOverlay; _canvas.sortingOrder = 101; _canvasGroup = canvasObj.AddComponent(); _canvasGroup.interactable = false; _canvasGroup.blocksRaycasts = false; var scaler = canvasObj.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); // 루트 (오른쪽 수직 중앙, 피벗 오른쪽) var rootObj = new GameObject("ComboRoot"); rootObj.transform.SetParent(canvasObj.transform, false); _rootRect = rootObj.AddComponent(); _rootRect.anchorMin = new Vector2(1f, 0.5f); _rootRect.anchorMax = new Vector2(1f, 0.5f); _rootRect.pivot = new Vector2(1f, 0.5f); _rootRect.sizeDelta = new Vector2(160f, 110f); _rootRect.anchoredPosition = new Vector2(OffScreenX, 0f); // 콤보 숫자 var comboObj = new GameObject("ComboNumber"); comboObj.transform.SetParent(rootObj.transform, false); var comboRect = comboObj.AddComponent(); comboRect.anchorMin = new Vector2(0f, 0.25f); comboRect.anchorMax = Vector2.one; comboRect.offsetMin = Vector2.zero; comboRect.offsetMax = Vector2.zero; _comboText = comboObj.AddComponent(); _comboText.text = "0"; _comboText.fontSize = baseFontSize; _comboText.color = comboColor; _comboText.alignment = TextAnchor.MiddleCenter; _comboText.fontStyle = FontStyle.Bold; _comboText.horizontalOverflow = HorizontalWrapMode.Overflow; _comboText.raycastTarget = false; if (_font != null) _comboText.font = _font; var outline = comboObj.AddComponent(); outline.effectColor = new Color(0.15f, 0.1f, 0f, 1f); outline.effectDistance = new Vector2(2f, -2f); // "COMBO" 라벨 var labelObj = new GameObject("ComboLabel"); labelObj.transform.SetParent(rootObj.transform, false); var labelRect = labelObj.AddComponent(); labelRect.anchorMin = Vector2.zero; labelRect.anchorMax = new Vector2(1f, 0.3f); labelRect.offsetMin = Vector2.zero; labelRect.offsetMax = Vector2.zero; _hitLabel = labelObj.AddComponent(); _hitLabel.text = "COMBO"; _hitLabel.fontSize = 16; _hitLabel.color = labelColor; _hitLabel.alignment = TextAnchor.MiddleCenter; _hitLabel.raycastTarget = false; if (_font != null) _hitLabel.font = _font; var labelOutline = labelObj.AddComponent(); labelOutline.effectColor = new Color(0f, 0f, 0f, 0.9f); labelOutline.effectDistance = new Vector2(1f, -1f); } #endregion } }