using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; using BaseGames.Core.Events; using BaseGames.Combat; namespace BaseGames.UI { /// /// 伤害飘字(架构 10_UIModule §10)。 /// 从对象池取出后由 FloatingDamageSpawner 调用 Show();协程完成后归还。 /// 挂在预制体根 GameObject 上(Canvas_HUD 子节点,World Space Canvas 或 Screen Space)。 /// public class FloatingDamageText : MonoBehaviour { [SerializeField] private TMP_Text _text; [SerializeField] private float _floatDistance = 1.5f; [SerializeField] private float _duration = 0.8f; /// /// 父级 Canvas(用于 RectTransformUtility 坐标转换)。 /// 适配所有 Canvas 渲染模式(Overlay / Camera / World Space), /// Screen Space - Overlay 时可传 null(会自动 fallback 到 null camera)。 /// [SerializeField] private Canvas _parentCanvas; private RectTransform _rectTransform; private Coroutine _animCoroutine; // 每次 Show() 解析一次,协程期间(< 1s)复用,避免每帧走 FindObjectByTag private Camera _cachedCamera; private void Awake() { _rectTransform = (RectTransform)transform; } /// /// 在世界坐标位置显示伤害数字并开始飘动动画。 /// 动画结束后由对象池回收(Deactivate)。 /// public void Show(Vector2 worldPosition, int damage, DamageType type) { if (_animCoroutine != null) StopCoroutine(_animCoroutine); // 每次 Show 解析一次摄像机:动画时长 < 1s,期间不会切换主摄像机; // 若 Boss 过场后再次 Show,会自动获取新的主摄像机。 _cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera) ? _parentCanvas.worldCamera : UnityEngine.Camera.main; _text.text = damage.ToString(); _text.color = GetColorForType(type); SetAnchoredPosition(worldPosition); _animCoroutine = StartCoroutine(FloatAndFade(worldPosition)); } private void SetAnchoredPosition(Vector2 worldPosition) { var cam = _cachedCamera; var screenPoint = cam != null ? (Vector2)cam.WorldToScreenPoint(worldPosition) : Vector2.zero; var canvasRect = _parentCanvas != null ? (RectTransform)_parentCanvas.transform : null; if (canvasRect != null) { RectTransformUtility.ScreenPointToLocalPointInRectangle( canvasRect, screenPoint, cam, out var localPoint); _rectTransform.anchoredPosition = localPoint; } else { _rectTransform.anchoredPosition = screenPoint; } } private IEnumerator FloatAndFade(Vector2 startWorld) { float elapsed = 0f; var color = _text.color; var startAlpha = color.a; while (elapsed < _duration) { float t = elapsed / _duration; SetAnchoredPosition(startWorld + new Vector2(0, _floatDistance * t)); // alpha 淡出(后半段开始)—— 修改 struct 的 a 分量并回写,避免每帧 new Color 堆分配 color.a = Mathf.Lerp(startAlpha, 0f, Mathf.Clamp01((t - 0.5f) / 0.5f)); _text.color = color; elapsed += Time.deltaTime; yield return null; } gameObject.SetActive(false); // 归还对象池 } private static Color GetColorForType(DamageType type) => type switch { DamageType.Fire => new Color(1f, 0.5f, 0f), // 橙 DamageType.Poison => new Color(0.3f,0.9f, 0.3f), // 绿 DamageType.True => new Color(1f, 0.95f,0.4f), // 黄 DamageType.Ice => new Color(0.5f,0.85f,1f), // 冰蓝 DamageType.Lightning=>new Color(0.9f,0.9f, 0.2f), // 闪黄 _ => Color.white }; } // ───────────────────────────────────────────────────────────────────────── /// /// 伤害飘字生成器(挂在 Canvas_HUD 上)。 /// 订阅 EVT_DamageDealt 事件频道,从 Addressables 池取出 FloatingDamageText 显示。 /// public class FloatingDamageSpawner : MonoBehaviour { [Header("事件频道")] [SerializeField] private DamageInfoEventChannelSO _onDamageDealt; [Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText)")] [SerializeField] private GameObject _floatingDmgPrefab; // Fallback:Inspector 直接拖入 private readonly List _pool = new(); private readonly CompositeDisposable _subs = new(); private void OnEnable() => _onDamageDealt?.Subscribe(OnDamageDealt).AddTo(_subs); private void OnDisable() => _subs.Clear(); private void OnDamageDealt(DamageInfo info) { if (info.FinalDamage <= 0) return; var text = GetOrCreate(); if (text != null) text.Show(info.SourcePosition, info.FinalDamage, info.Type); } private FloatingDamageText GetOrCreate() { // 线性扫描全部已创建实例,找首个未激活的复用 for (int i = 0; i < _pool.Count; i++) { var pooled = _pool[i]; if (pooled != null && !pooled.gameObject.activeSelf) { pooled.gameObject.SetActive(true); return pooled; } } // 没有可用实例则实例化新的,加入列表供下次复用 if (_floatingDmgPrefab == null) return null; var go = Instantiate(_floatingDmgPrefab, transform); var comp = go.GetComponent(); if (comp != null) _pool.Add(comp); return comp; } } }