172 lines
6.8 KiB
C#
172 lines
6.8 KiB
C#
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; // Fallback:Inspector 直接拖入
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|