核心組件:
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可以直接制作,非常方便。