236 lines
8.3 KiB
C#
236 lines
8.3 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Streamingle.Contents.BossRaid
|
|
{
|
|
/// <summary>
|
|
/// 콤보 카운터 UI. 컴팩트 디자인, 오른쪽 수직 중앙.
|
|
/// 히트 시 슬라이드 인 + 펀치 스케일, 타임아웃 시 슬라이드 아웃.
|
|
/// </summary>
|
|
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>();
|
|
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
_canvas.sortingOrder = 101;
|
|
_canvasGroup = canvasObj.AddComponent<CanvasGroup>();
|
|
_canvasGroup.interactable = false;
|
|
_canvasGroup.blocksRaycasts = false;
|
|
var scaler = canvasObj.AddComponent<CanvasScaler>();
|
|
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<RectTransform>();
|
|
_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<RectTransform>();
|
|
comboRect.anchorMin = new Vector2(0f, 0.25f);
|
|
comboRect.anchorMax = Vector2.one;
|
|
comboRect.offsetMin = Vector2.zero;
|
|
comboRect.offsetMax = Vector2.zero;
|
|
_comboText = comboObj.AddComponent<Text>();
|
|
_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>();
|
|
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<RectTransform>();
|
|
labelRect.anchorMin = Vector2.zero;
|
|
labelRect.anchorMax = new Vector2(1f, 0.3f);
|
|
labelRect.offsetMin = Vector2.zero;
|
|
labelRect.offsetMax = Vector2.zero;
|
|
_hitLabel = labelObj.AddComponent<Text>();
|
|
_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<Outline>();
|
|
labelOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
|
|
labelOutline.effectDistance = new Vector2(1f, -1f);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|