
類幸存者項(xiàng)目做到第 041 步戰(zhàn)斗循環(huán)基本能跑通之后最先暴露出來的通常不是玩法問題而是“反饋跟不上”。攻擊打中敵人傷害只體現(xiàn)在血條上數(shù)字沒有飄出來玩家根本感受不到 build 變化。文字飄動支持要解決的就是這類高頻即時(shí)反饋。這篇文章會在 Unity 開發(fā)環(huán)境下用 QFramework 框架給類幸存者項(xiàng)目加一個(gè)穩(wěn)定的文字飄動模塊讓傷害、暴擊、治療、經(jīng)驗(yàn)和升級提示都能按統(tǒng)一規(guī)則生成、移動、淡出和回收。適合用 QFramework 做類幸存者 Demo、正打算把戰(zhàn)斗反饋補(bǔ)完整的人看。這里最值得關(guān)注的不是某個(gè) UI 特效而是把“事件觸發(fā)—文字生成—?jiǎng)赢嬺?qū)動—對象池回收”這一整條鏈路打通。1. 文字飄動在類幸存者項(xiàng)目里屬于“戰(zhàn)斗反饋層”類幸存者玩法的核心是大量敵人、自動攻擊和成長 build。玩家大部分時(shí)間只負(fù)責(zé)移動和選擇強(qiáng)化方向戰(zhàn)斗過程會非常高頻地觸發(fā)命中、擊殺、掉落、升級。如果這些信息全部靠血條和彈窗展示反饋會慢半拍。文字飄動是成本最低、反饋?zhàn)钪苯拥姆桨浮?.1 文字反饋承擔(dān)了哪幾類信息先盤一下類幸存者項(xiàng)目里常見的飄字場景普通傷害數(shù)字每一次攻擊命中敵人時(shí)出現(xiàn)顏色建議偏白或淺黃字號適中出現(xiàn)頻率最高。暴擊數(shù)字觸發(fā)暴擊時(shí)出現(xiàn)字號要明顯大一些顏色用橙黃色最好再帶一點(diǎn)縮放效果讓玩家一眼就能看到關(guān)鍵輸出。治療數(shù)字角色回血時(shí)出現(xiàn)綠色通常出現(xiàn)在角色附近。經(jīng)驗(yàn)拾取提示拾取經(jīng)驗(yàn)碎片時(shí)出現(xiàn)的小字?jǐn)?shù)字小、存在時(shí)間短不需要太搶眼。升級提示升級時(shí)出現(xiàn)的核心反饋文本更大位置可以放在角色上方或屏幕中央偏上承擔(dān)一定的成就感表達(dá)。這些文字類型有一個(gè)共同點(diǎn)它們都是戰(zhàn)斗邏輯的結(jié)果。傷害由攻擊系統(tǒng)計(jì)算暴擊由暴擊系統(tǒng)決定治療由治療邏輯觸發(fā)。把它們的展示方式統(tǒng)一到一個(gè)模塊里是最合理的做法。1.2 為什么不能每個(gè)系統(tǒng)各顯示各的類幸存者項(xiàng)目如果開發(fā)得比較快很容易出現(xiàn)一種情況技能系統(tǒng)自己生成一個(gè) Text敵人受傷邏輯里又生成一個(gè) Text拾取系統(tǒng)再寫一份。結(jié)果就是一個(gè)場景里出現(xiàn)好幾種字號、好幾種字體層級還經(jīng)常亂掉有的被 UI 面板擋住有的在暫停時(shí)消失。獨(dú)立游戲開發(fā)成本有限飄字這種“所有系統(tǒng)都會用到”的表現(xiàn)層功能必須在早期抽出來統(tǒng)一管理。統(tǒng)一管理之后至少能獲得這幾個(gè)好處字體和字號只有一處配置不會出現(xiàn)表現(xiàn)不一致。所有飄字走同一個(gè)層級不會被戰(zhàn)斗 UI 或血條 UI 遮住。對象池只需要維護(hù)一種預(yù)制體復(fù)用邏輯清晰。后續(xù)如果要升級成 TextMeshPro、支持本地化數(shù)字格式、增加音效只需要改飄字模塊本身。QFramework 在這里的角色是提供事件、對象池、資源管理這類基礎(chǔ)設(shè)施。飄字模塊本身屬于應(yīng)用層不要求在框架內(nèi)部寫死而是建立在框架能力之上。這樣既有框架的解耦能力又不會把飄字邏輯塞進(jìn)框架代碼里。2. 動手前先把坐標(biāo)空間、生命周期和歸屬關(guān)系定清楚寫飄字模塊之前最應(yīng)該先解決的不是“怎么寫動畫”而是“這個(gè)字到底顯示在哪個(gè)坐標(biāo)系里”。很多飄字問題比如文字出現(xiàn)在了錯(cuò)誤位置、被 UI 遮住、移動時(shí)抖動大多是因?yàn)樽鴺?biāo)空間沒有定清楚。2.1 屏幕空間還是世界空間幸存者類游戲里戰(zhàn)斗傷害數(shù)字應(yīng)該出現(xiàn)在敵人頭頂或受擊點(diǎn)附近。這樣一來數(shù)字在玩家視角里是“貼”在敵人身上的反饋?zhàn)钭匀?。這種需求應(yīng)該使用世界空間。世界空間飄字常見有兩種實(shí)現(xiàn)方式World Space Canvas用 UI Text 或 TextMeshPro 掛在 Canvas 下面把 Canvas 設(shè)為 World Space指定 camera。優(yōu)點(diǎn)是可以繼續(xù)用 UI 排版和字體資源樣式維護(hù)方便。3D TextMeshPro / TextMesh直接用場景中的文字物體適合海量飄字但樣式編輯不如 UI 直觀。我的建議是如果項(xiàng)目里已經(jīng)用了 TextMeshPro可以直接用它如果團(tuán)隊(duì)更熟悉 UGUI就使用 World Space Canvas。兩種方式在最終表現(xiàn)上沒有太大差異關(guān)鍵是把坐標(biāo)轉(zhuǎn)換統(tǒng)一處理。屏幕空間飄字則用來處理升級提示、獲得新技能、系統(tǒng)公告這類和世界坐標(biāo)無關(guān)的反饋。它們可以放到一個(gè) Screen Space Overlay Canvas 下位置固定或按屏幕比例定位。這里很容易踩一個(gè)坑傷害數(shù)字如果放在屏幕空間就必須把世界坐標(biāo)轉(zhuǎn)成屏幕坐標(biāo)并且在攝像機(jī)移動時(shí)重新計(jì)算。類幸存者雖然攝像機(jī)相對固定但角色和敵人都會動轉(zhuǎn)來轉(zhuǎn)去很容易出問題。所以戰(zhàn)斗數(shù)字優(yōu)先放世界空間系統(tǒng)提示放屏幕空間兩條鏈路分開最省事。2.2 一條飄字從生成到回收的生命周期一條普通飄字的生命周期可以拆成五個(gè)階段準(zhǔn)備從對象池取出對象設(shè)置文字內(nèi)容、顏色、字號、起始位置。顯示對象激活可以播放一個(gè)輕微放大的入場動畫。上升文字沿 Y 軸慢慢向上漂這是“飄”字的核心。淡出到達(dá)指定時(shí)間后透明度逐漸降為 0?;厥胀该鞫葹?0 后把對象還回對象池等待下一次使用。這個(gè)流程看起來簡單但要注意一個(gè)關(guān)鍵問題飄字使用的時(shí)間基準(zhǔn)是什么。類幸存者游戲里如果玩家升級后觸發(fā)了短暫暫停或慢動作效果飄字如果跟隨 Time.deltaTime可能會瞬間跳過淡出過程或者暫停時(shí)字幕直接停下。項(xiàng)目一旦涉及時(shí)間縮放飄字動畫最好獨(dú)立使用一個(gè)時(shí)間源比如游戲內(nèi)單獨(dú)維護(hù)的 unscaledDeltaTime或者把飄字也歸入暫停層來管理。2.3 歸屬關(guān)系文字歸誰管飄字對象不應(yīng)該屬于某個(gè)技能、某個(gè)敵人或某個(gè)面板。它們應(yīng)該統(tǒng)一歸 FloatingTextManager 管理。FloatingTextManager 通常是一個(gè)場景中的 MonoBehaviour持有兩個(gè)關(guān)鍵對象World Space Canvas用來掛戰(zhàn)斗飄字位置由世界坐標(biāo)決定。Screen Space Canvas用來掛升級提示和系統(tǒng)提示。FloatingTextManager 負(fù)責(zé)對象池、預(yù)制體實(shí)例化、掛載位置和回收回調(diào)。戰(zhàn)斗系統(tǒng)不直接訪問任何飄字對象只發(fā)送“這里有傷害數(shù)字”的事件。飄字模塊接收到事件后自己去查樣式、取對象、設(shè)置位置。這樣職責(zé)清楚后續(xù)替換文字樣式時(shí)不會影響戰(zhàn)斗邏輯。3. 用 QFramework 事件系統(tǒng)搭一條最小飄字鏈路3.1 為什么用事件而不是直接調(diào)用在類幸存者項(xiàng)目里戰(zhàn)斗系統(tǒng)會調(diào)用飄字模塊嗎不一定。比如傷害系統(tǒng)在計(jì)算傷害后需要告訴“飄字模塊”顯示一個(gè)數(shù)字。如果直接在傷害代碼里調(diào)用FloatingTextManager.Instance.ShowDamage(...)看起來也沒問題但傷害代碼就永久依賴了 FloaingTextManager。以后如果要在把飄字模塊換成傷害數(shù)字特效或者想在多人同步時(shí)把飄字做成服務(wù)器驅(qū)動改動范圍會更大。使用事件之后方向就反過來了傷害系統(tǒng)只負(fù)責(zé)發(fā)送一個(gè)“FloatingTextEvent”。飄字模塊負(fù)責(zé)監(jiān)聽這個(gè)事件并決定如何顯示。傷害系統(tǒng)完全不知道飄字模塊存在。這就是 QFramework 事件系統(tǒng)的價(jià)值。它不會強(qiáng)制你使用哪種架構(gòu)但能把單向依賴切斷。在類幸存者這種多系統(tǒng)高頻交互的場景里事件驅(qū)動尤其適合。3.2 定義事件數(shù)據(jù)和發(fā)送端先定義一個(gè)飄字消息結(jié)構(gòu)字段不需要太多public struct FloatingTextEvent { public Vector3 worldPos; public string content; public Color color; public float fontSize; public string styleId; // 可選預(yù)留擴(kuò)展 public static FloatingTextEvent Create( Vector3 worldPos, string content, Color color, float fontSize, string styleId ) { return new FloatingTextEvent { worldPos worldPos, content content, color color, fontSize fontSize, styleId styleId }; } }發(fā)送端就非常簡潔。假設(shè)敵人受傷邏輯在 EnemyDamageHandler 里發(fā)一條普通傷害TypeEventSystem.Global.Send(FloatingTextEvent.Create( hitPoint, damage.ToString(), Color.white, 32f ));暴擊數(shù)字則可以在相同位置發(fā)送更大字號的橙色文字TypeEventSystem.Global.Send(FloatingTextEvent.Create( hitPoint, damage.ToString(), new Color(1f, 0.6f, 0f), 46f ));這里要注意QFramework 的事件系統(tǒng)在不同版本里的 API 可能略有差異比如Global.Send(...)和注冊方式要以項(xiàng)目里實(shí)際安裝的 QFramework 版本為準(zhǔn)。整體思路是不變的。3.3 監(jiān)聽端處理并生成數(shù)字FloatingTextManager 需要注冊監(jiān)聽private void OnEnable() { TypeEventSystem.Global.RegisterFloatingTextEvent(OnFloatingTextEvent); } private void OnDisable() { TypeEventSystem.Global.UnRegisterFloatingTextEvent(OnFloatingTextEvent); } private void OnFloatingTextEvent(FloatingTextEvent e) { // 從池中取出一個(gè)飄字對象 var item floatingTextPool.Get(); // 顯示并設(shè)置位置、內(nèi)容、顏色、字號 item.Show(e.worldPos, e.content, e.color, e.fontSize); }對應(yīng)的飄字對象腳本我一般會設(shè)計(jì)成這樣public class FloatingTextItem : MonoBehaviour { public TextMeshPro text; public CanvasGroup canvasGroup; public void Show(Vector3 pos, string content, Color color, float fontSize) { transform.position pos; text.text content; text.color color; text.fontSize fontSize; canvasGroup.alpha 1f; gameObject.SetActive(true); } }先做到這里飄字已經(jīng)能“出現(xiàn)”了。后面再補(bǔ)動畫和回收。3.4 最小鏈路測試寫完監(jiān)聽和發(fā)送先不要急著做復(fù)雜樣式做一個(gè)最小鏈路驗(yàn)證。我在測試時(shí)會這樣做在場景里創(chuàng)建一個(gè) World Space Canvas指定 Event Camera 為主相機(jī)。在 Canvas 下放一個(gè) TextMeshPro 預(yù)制體并把它設(shè)為 FloatingTextManager 的飄字預(yù)制體。掛 FloatingTextManager預(yù)載 10 個(gè)對象。寫一個(gè) TestTrigger 腳本按空格鍵發(fā)送一條普通傷害事件。運(yùn)行游戲按空格確認(rèn)在指定位置出現(xiàn)數(shù)字。這一步能通過說明事件通知、對象池取出、文字顯示、坐標(biāo)定位都正常。之后再去做上升和淡出動畫問題定位會更清晰。4. 用對象池接管文字的生成、運(yùn)動與回收4.1 高頻生成場景下Instantiate 和 Destroy 不是一個(gè)好選擇類幸存者和普通 RPG 的區(qū)別在于戰(zhàn)斗頻率特別高。一個(gè)敵人可能會同時(shí)挨到多個(gè)彈道的傷害瞬間就可能生成五六個(gè)飄字。如果這時(shí)候每個(gè)飄字都走Instantiate和Destroy場景里很快會出現(xiàn)大量創(chuàng)建和銷毀操作產(chǎn)生不必要的 GC還會帶來短暫的幀率抖動。獨(dú)立游戲開發(fā)時(shí)這個(gè)坑尤其隱蔽。低配機(jī)器上可能不會立刻卡死但連續(xù)戰(zhàn)斗幾分鐘后內(nèi)存碎片和 GC 會導(dǎo)致明顯掉幀。正確做法是使用對象池。預(yù)先創(chuàng)建一批飄字對象使用完成后再放回去不銷毀。這樣生成的只是“從隊(duì)列里取出一個(gè)對象”成本很低。4.2 一個(gè)簡單的飄字對象池實(shí)現(xiàn)FloatingTextManager 可以自己維護(hù)一個(gè)隊(duì)列不需要額外引入復(fù)雜容器public class FloatingTextManager : MonoBehaviour { public GameObject floatingTextPrefab; public int prewarmCount 30; public Transform worldCanvas; private QueueFloatingTextItem pool new QueueFloatingTextItem(); private void Awake() { for (int i 0; i prewarmCount; i) { var item CreateItem(); item.gameObject.SetActive(false); pool.Enqueue(item); } } private FloatingTextItem CreateItem() { var go Instantiate(floatingTextPrefab, worldCanvas); return go.GetComponentFloatingTextItem(); } public FloatingTextItem Get() { if (pool.Count 0) { return pool.Dequeue(); } // 池不足時(shí)擴(kuò)容不需要每次都創(chuàng)建 return CreateItem(); } public void Release(FloatingTextItem item) { item.gameObject.SetActive(false); pool.Enqueue(item); } }注意幾個(gè)細(xì)節(jié)預(yù)載數(shù)量不要一開始就設(shè)成很大。30 個(gè)已經(jīng)能在默認(rèn)條件下覆蓋大多數(shù)短暫飄字需求。如果同屏飄字經(jīng)常超過這個(gè)數(shù)再提高預(yù)載量。池不足時(shí)仍然擴(kuò)容創(chuàng)建不會因?yàn)槌貪M導(dǎo)致飄字缺失?;厥諘r(shí)必須把對象 SetActive(false)否則動畫播放完畢的舊文字會留在場景里。FloatingTextItem 自身也要在動畫結(jié)束后調(diào)用 Manager 的 Releaseprivate void Finish() { floatingTextManager.Release(this); }這樣對象池的取和還就閉環(huán)了。4.3 預(yù)制體、Canvas 和字體配置細(xì)節(jié)飄字預(yù)制體雖然小但配置不對會產(chǎn)生很多問題。使用 World Space Canvas 時(shí)Canvas 的縮放決定了文字大小。如果文字出現(xiàn)在敵人頭頂?shù)翘蠡蛱?yōu)先檢查 Canvas 的 Scale 和 Text 的 font size。預(yù)制體上的 Text 類型建議直接用 TextMeshPro因?yàn)樗钱?dāng)前 Unity 里更穩(wěn)定、性能更好的文字方案。傳統(tǒng) UGUI Text 也能用但大量動態(tài)文字時(shí)表現(xiàn)力和字體渲染效果都不如 TextMeshPro。每條飄字掛一個(gè) CanvasGroup方便控制淡出。直接改 text.color.a 也可以但對多個(gè)子元素時(shí) CanvasGroup 更統(tǒng)一。如果 World Space Canvas 使用了 Screen Space Camera 選項(xiàng)之外的模式還要確認(rèn) Canvas 的 worldCamera 字段是否指向主相機(jī)。還有一個(gè)容易忽略的問題飄字對象的 Root 不應(yīng)該帶多余的 Image 或 Raycast Target 組件。UI 射線檢測會在鼠標(biāo)或觸摸操作時(shí)產(chǎn)生額外開銷。飄字只是純展示應(yīng)該把 Raycast Target 關(guān)閉。5. 把樣式和動畫參數(shù)抽成配置方便后續(xù)調(diào)表現(xiàn)5.1 一份可擴(kuò)展的飄字樣式當(dāng)飄字類型多起來后每個(gè)事件都手動指定顏色、字號很容易出錯(cuò)。更穩(wěn)妥的方式是使用樣式配置。先定義一個(gè) Serializable 的樣式類[System.Serializable] public class FloatingTextStyle { public string styleId; public Color color Color.white; public float fontSize 32f; public float riseSpeed 1.5f; public float lifeTime 0.8f; public float fadeDuration 0.4f; public Vector2 randomOffset Vector2.zero; public bool scaleOnEnable false; }然后在 FloatingTextManager 里放一個(gè)樣式數(shù)組在 Inspector 里配置public FloatingTextStyle[] styles;寫一個(gè)根據(jù) styleId 查找樣式的方法。找不到時(shí)使用第一個(gè)樣式作為默認(rèn)值。事件發(fā)送端不再需要手動指定顏色和字號只指定樣式 idTypeEventSystem.Global.Send(FloatingTextEvent.Create( hitPoint, damage.ToString(), damage_normal ));這樣傷害系統(tǒng)就不關(guān)心顏色、字號、飄動速度這些表現(xiàn)層參數(shù)了。美術(shù)或策劃想調(diào)整表現(xiàn)只需要改 Editor 配置不用找開發(fā)改代碼。5.2 不同來源的飄字如何區(qū)分常見樣式可以這樣規(guī)劃樣式 id用途顏色字號上升速度生命周期淡出damage_normal普通傷害白色321.50.80.4damage_crit暴擊橙黃461.80.90.5heal治療綠色321.20.80.4exp經(jīng)驗(yàn)拾取淺藍(lán)220.80.60.3levelup升級金色600.51.50.8pickup拾取提示淺綠241.00.70.4這些參數(shù)不是固定標(biāo)準(zhǔn)具體數(shù)值要根據(jù)游戲美術(shù)風(fēng)格和手感調(diào)。我的建議是先按這個(gè)表跑起來然后在實(shí)際戰(zhàn)斗中看觀感再逐個(gè)調(diào)。暴擊數(shù)字比普通傷害大一號通常就夠醒目。如果希望更有沖擊力可以在暴擊樣式上開啟scaleOnEnable播放一個(gè)從 1.2 倍縮小到 1 倍的入場動畫。經(jīng)驗(yàn)拾取數(shù)字不需要太顯眼因?yàn)槭叭☆l率高、信息價(jià)值低做得太大會干擾戰(zhàn)斗畫面。5.3 動畫用 DoTween 還是手寫飄字動畫可以直接使用 DoTween這個(gè)插件在 Unity 項(xiàng)目里非常常見。DoTween 的好處是代碼簡潔而且 DOMove、DOFade、DOScale 都有現(xiàn)成接口transform.DOMove(transform.position Vector3.up * riseDistance, lifeTime).SetEase(Ease.OutCubic); canvasGroup.DOFade(0f, fadeDuration).SetDelay(lifeTime - fadeDuration);但如果項(xiàng)目不想引入額外插件手寫 Update 也完全夠private float elapsed; private float fadeStartTime; private bool isPlaying; public override void Play(FloatingTextData data) { elapsed 0f; fadeStartTime data.lifeTime - data.fadeDuration; isPlaying true; } private void Update() { if (!isPlaying) return; elapsed Time.deltaTime; var t elapsed / data.lifeTime; // 上升 transform.position Vector3.up * data.riseSpeed * Time.deltaTime; // 縮放入場 if (t 0.2f) transform.localScale Vector3.Lerp(startScale, Vector3.one, t / 0.2f); // 淡出 if (elapsed fadeStartTime fadeStartTime 0) { canvasGroup.alpha Mathf.Lerp(1f, 0f, (elapsed - fadeStartTime) / data.fadeDuration); } // 回收 if (elapsed data.lifeTime) { isPlaying false; floatingTextManager.Release(this); } }手寫的優(yōu)勢是可控性強(qiáng)依賴少DoTween 的優(yōu)勢是代碼短、緩動函數(shù)豐富。使用 DoTween 時(shí)對象池回收前必須調(diào)用Kill或者使用帶onKill回調(diào)的方式否則 Tween 回調(diào)會在對象復(fù)用后觸發(fā)產(chǎn)生奇怪的表現(xiàn)。如果同一屏飄字?jǐn)?shù)量很大建議優(yōu)先考慮手寫。不是 DoTween 本身性能有問題而是 Tween 對象在大量并行時(shí)也需要維護(hù)和銷毀會增加管理成本。飄字這種生命周期非常簡單的動畫手寫并不麻煩。6. 接入傷害、拾取和升級反饋并建立排查鏈路6.1 接進(jìn)傷害結(jié)算、拾取和升級現(xiàn)在飄字模塊已經(jīng)具備基本能力接下來要把它接入類幸存者的業(yè)務(wù)邏輯。傷害結(jié)算處在計(jì)算出傷害數(shù)值之后發(fā)送事件var damage player.GetDamage(); TypeEventSystem.Global.Send(FloatingTextEvent.Create( enemy.HitPoint, damage.ToString(), damage_normal )); if (isCrit) { TypeEventSystem.Global.Send(FloatingTextEvent.Create( enemy.HitPoint, damage.ToString(), damage_crit )); }暴擊這里有個(gè)細(xì)節(jié)暴擊通常不應(yīng)該同時(shí)顯示普通傷害和暴擊傷害。如果傷害系統(tǒng)也發(fā)了普通傷害暴擊也發(fā)一條玩家會看到兩個(gè)數(shù)字疊在一起。正確做法是傷害系統(tǒng)里先判斷是否暴擊只發(fā)一條事件用不同的樣式 id。拾取經(jīng)驗(yàn)時(shí)在玩家角色附近發(fā)一條樣式為 exp 的飄字TypeEventSystem.Global.Send(FloatingTextEvent.Create( playerTransform.position Vector3.up * 0.5f, expValue.ToString(), exp ));升級提示可以不走世界空間改成屏幕空間 UITypeEventSystem.Global.Send(FloatingTextEvent.Create( levelUpTitlePos, LEVEL UP, levelup ));如果升級提示使用了屏幕空間FloatingTextManager 需要額外判斷樣式對應(yīng)的 Canvas 類型??梢栽跇邮奖砝锛右粋€(gè)isScreenSpace字段然后再選擇使用哪個(gè) Canvas。6.2 驗(yàn)證標(biāo)準(zhǔn)接入之后不要只看“有沒有數(shù)字飄出來”。我會按以下幾個(gè)標(biāo)準(zhǔn)驗(yàn)證啟動不報(bào)錯(cuò)事件注冊沒有異常。單條飄字出現(xiàn)位置正確內(nèi)容正確。連續(xù)發(fā)送多條飄字所有飄字都能正常播放并回收。暴擊樣式和普通傷害樣式能區(qū)分開。動畫播放期間如果打開對象池查看對象數(shù)量不會持續(xù)增加。連續(xù)戰(zhàn)斗五分鐘后沒有明顯 GC 增長幀率穩(wěn)定。游戲暫?;驎r(shí)間縮放下飄字表現(xiàn)符合預(yù)期。這些標(biāo)準(zhǔn)里第 5 條和第 6 條最容易出問題。很多模塊在小規(guī)模測試時(shí)看起來正常一旦進(jìn)入連續(xù)戰(zhàn)斗就暴露出頻繁創(chuàng)建和銷毀的問題。6.3 常見問題與排查順序飄字模塊的排錯(cuò)順序我一般會這樣走先看現(xiàn)象再查事件鏈路最后查配置和動畫。如果飄字完全沒出現(xiàn)優(yōu)先確認(rèn)事件是否發(fā)送成功??梢栽诎l(fā)送端和監(jiān)聽端各放一條 Debug.Log跑一次看輸出。如果事件沒發(fā)問題在傷害系統(tǒng)如果事件發(fā)了但沒有飄字問題在飄字模塊。如果飄字出現(xiàn)了但位置不對優(yōu)先檢查坐標(biāo)空間。世界空間飄字需要看世界坐標(biāo)和 Canvas 是否匹配屏幕空間飄字要看坐標(biāo)轉(zhuǎn)換是否正確。還有一個(gè)常見原因是 Canvas 的縮放和相機(jī)參數(shù)沒配對導(dǎo)致數(shù)字跑到屏幕邊緣。如果飄字出現(xiàn)后沒有飄優(yōu)先檢查 Update 是否還在執(zhí)行。比如對象被 SetActive(false) 后 Update 不會執(zhí)行事件里傳入的 lifeTime 為 0 也會導(dǎo)致立即回收。如果飄字出現(xiàn)一小會后突然消失優(yōu)先檢查對象池回收邏輯。很可能是生命周期太短或者淡出時(shí)間大于生命周期導(dǎo)致計(jì)算異常。如果飄字動畫卡頓先檢查預(yù)載數(shù)量是否不足。池不足時(shí)每次擴(kuò)容都會 Instantiate即使邏輯正確首次擴(kuò)容的一瞬間也會有性能峰值??梢园杨A(yù)載數(shù)量提高或者提前在場景加載時(shí)預(yù)熱。還有一個(gè)高頻問題就是回收時(shí)沒有重置文字狀態(tài)。比如上次回收前文字顏色是橙色下次復(fù)用時(shí)如果沒重置TextMeshPro 會保留舊顏色。建議在 Show 方法里把所有狀態(tài)統(tǒng)一重置包括位置、縮放、旋轉(zhuǎn)、顏色、透明度、默認(rèn)內(nèi)容。6.4 性能邊界和后續(xù)優(yōu)化類幸存者項(xiàng)目里飄字?jǐn)?shù)量可能非??鋸?。滿屏敵人同時(shí)受傷時(shí)每秒可能出現(xiàn)幾十個(gè)飄字。這個(gè)數(shù)量級下需要做一些保護(hù)策略。首先是限制同屏飄字總數(shù)。FloatingTextManager 可以維護(hù)一個(gè)當(dāng)前活躍數(shù)量超過上限時(shí)不再生成新的飄字而是丟棄或者合并。普通傷害數(shù)字丟失幾個(gè)影響不大但暴擊數(shù)字要保證一定顯示優(yōu)先級。其次是控制字符處理。傷害數(shù)字的ToString()在低頻率下沒有問題但高頻飄字時(shí)字符串的分配也會產(chǎn)生 GC。可以提前做簡單的字符串緩存或者把damage.ToString()的結(jié)果先存起來避免每次傷害都新建字符串。再就是 Canvas 性能。飄字在移動時(shí)會改 transform這會讓 Unity UI 重新計(jì)算合批。同屏飄字越多這個(gè)成本越高。解決思路有幾個(gè)使用 TextMeshPro 而不是 UGUI Text。降低飄字?jǐn)?shù)量上限不要無限制顯示。調(diào)整飄字動畫盡量使用局部坐標(biāo)移動避免頻繁修改 Canvas 根節(jié)點(diǎn)。最后是合理的展示策略。普通傷害數(shù)字可以只保留最近幾次命中暴擊數(shù)字絕對保留拾取經(jīng)驗(yàn)小字在數(shù)量超過三個(gè)時(shí)直接合并顯示一個(gè)總數(shù)。這些策略都能減少畫面噪點(diǎn)同時(shí)減少性能開銷。如果你也在做類幸存者項(xiàng)目我的建議是先把普通傷害和暴擊這一條鏈路跑穩(wěn)再往上升級提示、拾取提示這些額外樣式。文字飄動本身不復(fù)雜復(fù)雜的是讓它在長時(shí)間、高頻率、多敵人的戰(zhàn)斗中不卡、不亂、不錯(cuò)位。這幾條處理好戰(zhàn)斗手感會明顯提升一個(gè)檔次。