1、問題背景
??今天接到一個表現(xiàn)上的需求:在有淡出動畫的獎勵提示上,異色標記稀有道具的名稱。
本來是一個挺簡單的功能,在提示文字中找出道具名的位置,然后在兩端插入UGUI的<color>標簽。測試的時候卻發(fā)現(xiàn),淡出過程中異色部分的透明度沒有發(fā)生變化。

Alpha不變的情況
在項目組詢問一番,有大佬已經(jīng)寫腳本處理了這個問題。懷著不斷造輪子的心態(tài),對同事的腳本進行了改寫(當然改寫的腳本沒有放入項目),改寫的目的有兩個:
- 用自己的命名習(xí)慣書寫
- 改善下性能

改進后的效果
2、淺析
??簡單分析(cai)下問題的原因。
首先,UGUI改變color屬性,是修改頂點色,而不是修改材質(zhì)的屬性。因此可以出現(xiàn)同一段文字,顏色(包含Alpha)不同的情況。
然后,文字異色是通過<color>標簽進行標記的,它使用的是一個8位十六進制數(shù)表示,順序分別是RGBA。第一張圖中雖然我用的是6位的#00ff00,但Unity內(nèi)部應(yīng)該會把它補成8位的#00ff00ff。(具體實現(xiàn)沒細究,大概是取不到Alpha位就默認不透明吧)
那么問題就能猜到了,淡出動畫僅僅是修改了color屬性,文本中的<color>標簽沒有任何變化,Unity依舊使用標簽的信息去填充頂點色。解決方案也是針對<color>標簽進行處理的。
3、完整代碼
[ExecuteInEditMode]
public class RichTextAlphaUpdater : MonoBehaviour
{
public Text Txt;
/// <summary>
/// 匹配顏色值
/// </summary>
public static readonly Regex RichColorReg = new Regex("<color=#([a-f0-9]{8})>", RegexOptions.IgnoreCase);
public const int ColorMax = 255;
private UnityAction _vertDirtyAction;
private UnityAction VertDirtyAction
{
get
{
if (null == _vertDirtyAction)
{
_vertDirtyAction = _OnVertDirty;
}
return _vertDirtyAction;
}
}
/// <summary>
/// 文字頂點變化的事件
/// </summary>
private void _OnVertDirty()
{
string alpha = _GetHexAlpha();
string txt = Txt.text;
Match match = RichColorReg.Match(txt);
Group group = null;
while (match.Success)
{
group = match.Groups[1];
_ReplaceAlpha(txt, group.Index, alpha);
match = match.NextMatch();
}
}
/// <summary>
/// 緩存數(shù)據(jù),降低處理頻率
/// </summary>
private int _prevAlpha = 0;
private string _hexAlpha = null;
/// <summary>
/// 獲取當前Alpha的Hex值
/// </summary>
private string _GetHexAlpha()
{
int alpha = Mathf.Clamp((int) (Txt.color.a * ColorMax), 0, ColorMax);
if (null != _hexAlpha && alpha == _prevAlpha)
{
return _hexAlpha;
}
string hexAlpha = Convert.ToString(alpha, 16);
if (hexAlpha.Length == 1)
{
return "0" + hexAlpha;
}
return hexAlpha;
}
private void _ReplaceAlpha(string txt, int colorIdx, string alpha)
{
unsafe
{
fixed (char* hexPtr = txt)
{
hexPtr[colorIdx + 6] = alpha[0];
hexPtr[colorIdx + 7] = alpha[1];
}
}
}
void OnEnable()
{
if (null == Txt)
{
Txt = GetComponent<Text>();
}
if (null != Txt)
{
Txt.RegisterDirtyVerticesCallback(VertDirtyAction);
}
}
void OnDisable()
{
if(null == Txt) return;
Txt.UnregisterDirtyVerticesCallback(VertDirtyAction);
}
}
- 使用:
1)把腳本掛到要控制的Text組件上
2)腳本掛到任意激活的GameObject上,自己關(guān)聯(lián)Text組件
4、知識點
代碼雖然簡單,但也有幾個小點值得記錄備忘。
1)Text重建回調(diào)
- Text提供了
RegisterDirtyVerticesCallback、RegisterDirtyMaterialCallback、RegisterDirtyLayoutCallback等幾個回調(diào),讓開發(fā)者可以在重建的時候做些事情 - 回調(diào)執(zhí)行后重建不是馬上(同一幀)進行的,這里只是通知開發(fā)者,組件被加入了相應(yīng)的Change List
-
在回調(diào)中做引發(fā)重建的處理,會陷入死循環(huán)
同事的方案中,是通過【取消 - 再注冊】的方式避免死循環(huán)的,針對類似的情況應(yīng)該是挺好的處理方法。
我的方案可以不考慮死循環(huán),因為是直接修改的string對象,不會觸發(fā)重建。
2)Unity中使用指針
??為了減少字符串操作(減少GC),我嘗試使用指針進行字符替換,然后得到了喜人的結(jié)果,性能和GC都有所提高~
- 獲取指針需要用
fixed域固定內(nèi)存的位置,僅使用unsafe是不夠的 - 為了讓Unity能夠編譯unsafe代碼,要在工程中加入一個smcs.rsp文件,里面僅寫入
-unsafe,并重啟Unity?。?/strong>
??這里有個小抉擇,本來為了使用的時候方便,想支持6位色值的。寫完指針方案后,我放棄了6位色值。因為它無法通過一對一的char替換完成,需要插入內(nèi)容,那么GC就無法避免了。
3)正則表達式
- 這套方案并不是無GC的,我在Editor中測試,一段簡單的文字(十來個有用字符)動畫過程中每幀也有1.4K左右的GC產(chǎn)生。雖沒細測,但基本可以確定這部分開銷是正則產(chǎn)生的。好吃易上火啊Orm
- 遍歷正則的匹配結(jié)果,可以用Matches()+Index或Match()+NextMatch(),測試發(fā)現(xiàn),后者比前者產(chǎn)生的GC少0.1K。