Files
zeling_v2/Assets/_Game/Scripts/UI/FloatingDamageText.cs
2026-05-25 13:21:41 +08:00

172 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using BaseGames.Core.Events;
using BaseGames.Combat;
namespace BaseGames.UI
{
/// <summary>
/// 伤害飘字(架构 10_UIModule §10
/// 从对象池取出后由 FloatingDamageSpawner 调用 Show()
/// 动画推进由 <see cref="FloatingDamageTickSystem"/> 统一调度,避免大量独立协程。
/// </summary>
public class FloatingDamageText : MonoBehaviour
{
[SerializeField] private TMP_Text _text;
[SerializeField] private float _floatDistance = 1.5f;
[SerializeField] private float _duration = 0.8f;
/// <summary>
/// 父级 Canvas用于 RectTransformUtility 坐标转换)。
/// 适配所有 Canvas 渲染模式Overlay / Camera / World Space
/// Screen Space - Overlay 时可传 null会自动 fallback 到 null camera
/// </summary>
[SerializeField] private Canvas _parentCanvas;
private RectTransform _rectTransform;
// 每次 Show() 解析一次,动画期间(< 1s复用避免每帧走 FindObjectByTag
private Camera _cachedCamera;
private void Awake()
{
_rectTransform = (RectTransform)transform;
}
/// <summary>
/// 在世界坐标位置显示伤害数字并启动飘动动画。
/// 动画结束后由 OnTickComplete 回调列表 SetActive(false)。
/// </summary>
public void Show(Vector2 worldPosition, int damage, DamageType type)
{
// 取消上一条动画(如果被预期复用)
FloatingDamageTickSystem.Instance.Unregister(this);
// 每次 Show 解析一次摄像机
_cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
? _parentCanvas.worldCamera
: UnityEngine.Camera.main;
_text.text = damage.ToString();
_text.color = GetColorForType(type);
SetAnchoredPosition(worldPosition);
FloatingDamageTickSystem.Instance.Register(this, worldPosition,
_duration, _floatDistance, _text.color.a);
}
/// <summary>由 <see cref="FloatingDamageTickSystem"/> 每帧调用t 为归一化进度 [0,1]。</summary>
internal void TickAnimation(Vector2 startWorld, float t, float startAlpha, float floatDistance)
{
SetAnchoredPosition(startWorld + new Vector2(0, floatDistance * t));
var color = _text.color;
color.a = Mathf.Lerp(startAlpha, 0f, Mathf.Clamp01((t - 0.5f) / 0.5f));
_text.color = color;
}
/// <summary>动画结束回调:反激活 GameObject 归还池。</summary>
internal void OnTickComplete() => gameObject.SetActive(false);
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)
{
// 保留作为 fallback若业务方选择继续使用独立协程可直接调用本方法。
float elapsed = 0f;
var color = _text.color;
var startAlpha = color.a;
while (elapsed < _duration)
{
float t = elapsed / _duration;
TickAnimation(startWorld, t, startAlpha, _floatDistance);
elapsed += Time.deltaTime;
yield return null;
}
OnTickComplete();
}
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
};
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>
/// 伤害飘字生成器(挂在 Canvas_HUD 上)。
/// 订阅 EVT_DamageDealt 事件频道,从 Addressables 池取出 FloatingDamageText 显示。
/// </summary>
public class FloatingDamageSpawner : MonoBehaviour
{
[Header("事件频道")]
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
[Header("预制体(对象池 key = AddressKeys.PrefabUIFloatingDmgText")]
[SerializeField] private GameObject _floatingDmgPrefab; // FallbackInspector 直接拖入
private readonly List<FloatingDamageText> _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<FloatingDamageText>();
if (comp != null) _pool.Add(comp);
return comp;
}
}
}