核心组件:
Dotween TextMeshPro
过程轨迹如下图:
代码如下:
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可以直接制作,非常方便。