
核心组件:
Dotween TextMeshPro
过程轨迹如下图:

代码如下:
cs
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.Pool;
public class DamageTextController : MonoBehaviour
{
[Header("配置参数")]
public GameObject textPrefab;
public int poolSize = 20;
public float floatHeight = 2f;
public float duration = 1f;
public GameObject bossGame;
[Header("颜色配置")]
public Color[] damageColors = { Color.black, Color.red, Color.green,
Color.blue, Color.yellow, Color.magenta };
public int maxDamageThreshold = 1000;
[Header("字体大小")]
public float minFontSize = 20f;
public float maxFontSize = 40f;
[Header("偏移配置")]
// 垂直偏移
public float maxVerticalOffset = 20f;
private ObjectPool<TextMeshProUGUI> pool; // 替换原有队列
private Camera mainCamera;
private void Awake()
{
// 安全获取相机引用
if (!mainCamera) mainCamera = Camera.main;
// 初始化对象池
pool = new ObjectPool<TextMeshProUGUI>(
createFunc: () =>
{
var obj = Instantiate(textPrefab, transform);
return obj.GetComponent<TextMeshProUGUI>();
},
actionOnGet: (text) =>
{
text.gameObject.SetActive(true);
text.transform.localPosition = Vector3.zero;
},
actionOnRelease: (text) => text.gameObject.SetActive(false),
actionOnDestroy: (text) => Destroy(text.gameObject),
defaultCapacity: poolSize
);
// 预创建对象
var preload = new List<TextMeshProUGUI>();
for (int i = 0; i < poolSize; i++)
{
preload.Add(pool.Get());
}
foreach (var item in preload)
{
pool.Release(item);
}
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
ShowDamage(bossGame.transform.position + Vector3.up, Random.Range(100, maxDamageThreshold));
}
}
// 获取可用文字对象
private TextMeshProUGUI GetTextObject()
{
return pool.Get(); // 简化获取逻辑
}
// 显示伤害文字(世界坐标版本)
public void ShowDamage(Vector3 worldPosition, int damage)
{
var text = GetTextObject();
// 添加空引用保护
if (!mainCamera) return;
text.transform.position = mainCamera.WorldToScreenPoint(worldPosition);
// 提取颜色计算逻辑到独立方法
text.color = CalculateDamageColor(damage);
// 提取字体大小计算到独立方法
text.fontSize = CalculateFontSize(damage);
text.text = damage.ToString();
StartCoroutine(PlayAnimation(text));
}
private Color CalculateDamageColor(int damage)
{
float ratio = Mathf.Clamp01((float)damage / maxDamageThreshold);
// 修改索引计算方式,使最后一个颜色可以被访问到
int index = Mathf.FloorToInt(ratio * damageColors.Length);
index = Mathf.Clamp(index, 0, damageColors.Length - 1);
return new Color(damageColors[index].r, damageColors[index].g, damageColors[index].b, 1f);
}
private float CalculateFontSize(int damage)
{
float ratio = Mathf.Clamp01((float)damage / maxDamageThreshold);
return Mathf.Lerp(minFontSize, maxFontSize, ratio);
}
// 动画协程
private IEnumerator PlayAnimation(TextMeshProUGUI text)
{
// 重置文本状态
text.alpha = 1f;
text.transform.localScale = Vector3.one;
text.gameObject.SetActive(true);
// ==== 出现阶段 (0.2秒) ====
text.color = new Color(text.color.r, text.color.g, text.color.b, 0); // 初始透明
Vector3 originalPos = text.transform.position;
// 初始状态设置
text.transform.localScale = Vector3.one * 0.2f;
text.transform.position += Vector3.up * 50f; // 初始位置上方50像素
// 第一阶段动画:淡入 + 放大 + 下落准备
var phase1 = DOTween.Sequence()
.Join(text.DOFade(1, 0.2f).SetEase(Ease.OutQuad))
.Join(text.transform.DOScale(1f, 0.2f).SetEase(Ease.OutBack))
.Join(text.transform.DOMoveY(originalPos.y + 30f, 0.2f));
// ==== 显示阶段 (0.3秒) ====
var phase2 = text.transform.DOMoveY(originalPos.y - 30f, 0.3f)
.SetEase(Ease.Linear);
// ==== 结束阶段 (0.3秒) ====
var phase3 = DOTween.Sequence()
.Append(text.transform.DOMoveY(originalPos.y - maxVerticalOffset, 0.3f).SetEase(Ease.InQuad))
.Join(text.DOFade(0, 0.3f))
.Join(text.transform.DOScale(0.5f, 0.3f));
// 组合完整动画
var fullSequence = DOTween.Sequence()
.Append(phase1)
.Append(phase2)
.Append(phase3);
yield return fullSequence.WaitForCompletion();
// 在回收前重置属性
text.alpha = 1f;
text.transform.localScale = Vector3.one;
// 修改回收部分
text.gameObject.SetActive(false);
pool.Release(text); // 使用对象池的Release方法
}
private void OnDestroy()
{
pool.Clear(); // 确保销毁时清理对象池
}
}
使用TMP后期可以无缝切换位图字体,TMP可以直接制作,非常方便。