Unity在Text中添加下划线(通过添加顶点来渲染)

    xiaoxiao2025-08-08  2

    最近在准备做富文本控件,方便后期在游戏项目中使用,自己负责做一下在Text文本添加下划线功能。在NGUI的富文本中是有添加下划线功能的,但是在unity自带的UGUI的富文本控件中没有提供这个功能,需要自己手动拓展。看了下网上的博客教程大多数的解决方案是在Text下面添加Text或者通过拉伸Image的方式来解决,这种两个方式在解决普通需求时问题不到,但是在性能要求比较苛刻的手游环境来说,如果重复创建或者创建的Text,Image等太多的话性能消耗会比较大。所以采用重写Text的方式,重新渲染顶点添加下划线。我们知道unity在渲染文字时是通过两个三角形6个顶点来确定一个字符,具体的渲染原理本人也不是很懂,有空的时候再去研究。

    具体的做法是在继承Text后重写OnPopulateMesh()方法,传入VertexHelper参数用来获取相关的顶点数据。拿到顶点数据后,先通过字符串的操作读取下划线的lable标签,我这里是设置标签对<u></u>来表示下划线的开始和结束。然后记录下开始与结束的X和Y坐标,这里比较麻烦的操作是换行,我是通过检测X坐标是否在某个位置发生了剧烈变化,然后记录下换行的坐标,重新设置开始与结束的坐标。

    贴一点核心代码以便有需要的时候查看:

    首先是字符串的操作来匹配标签:这里需要注意的点是标签对的匹配通过栈这种数据可以很方便实现,遇到开始标签就入栈,直到遇到结束标签出栈,完成一对标签的匹配。觉得麻烦的可以去研究正则表达式。

    /// <summary> ///匹配标签 /// </summary> /// <param name="vh">VertexHelper</param> /// <param name="vertex">坐标数组</param> public void DueTextString(VertexHelper vh,List<UIVertex> vertex) { //string oldTextString = this.gameObject.GetComponent<Text>().text; string oldTextString = text; ; Debug.LogFormat("oldTextString的值为{0}", oldTextString); //标签的个数 int startLableCount=0;int endLableCount=0; //标签的开始下标 int startLableIndex=0;int endLableIndex = 0; Stack<int> startLableIndexStack = new Stack<int>(); List<int> startLableIndexList = new List<int>(); List<int> endLableIndexList = new List<int>(); for (int i = 0; i <= oldTextString.Length;) { startLableIndex = oldTextString.IndexOf(UnderlineStartLable, i); if (startLableIndex < 0) break; startLableCount++; Debug.LogFormat("开始标签下标-----{0}", startLableIndex); startLableIndexList.Add(startLableIndex); i += startLableIndex + 1; } for (int i = 0; i <= oldTextString.Length;) { endLableIndex = oldTextString.IndexOf(UnderlineEndLable, i); if (endLableIndex < 0) break; endLableCount++; Debug.LogFormat("结束标签下标------{0}", endLableIndex); endLableIndexList.Add(endLableIndex-UnderlineStartLable.Length); i += endLableIndex + 1; } if (startLableIndexList.Count != endLableIndexList.Count) { Debug.LogError("开始标签:"+startLableIndexList.Count.ToString()+"个 "+"结束标签:"+endLableIndexList.Count.ToString() +"个"+" 标签输入格式有误,请检查!!!"); return; } //K为结束标签的下标也用来表示当前有多少对标签 int k = 0; int currentStartIndex;int currentEndIndex; for (int m = 0; m < startLableIndexList.Count; m++) { if (startLableIndexList[m] < endLableIndexList[k]) { startLableIndexStack.Push(startLableIndexList[m]); } else { currentStartIndex = startLableIndexStack.Pop(); currentEndIndex = endLableIndexList[k]; Debug.LogFormat("开始标签{0}与结束标签{1}匹配", currentStartIndex, currentEndIndex); //先判断有没有标签嵌套,判断有没有换行 Debug.LogFormat("开始标签个数{0}", startLableCount); changelineStartIndex.Add(currentStartIndex * 4-k*28); CalculateChangeLine(vertex, currentStartIndex-k*7, currentEndIndex-k*7); changlineEndIndex.Add(currentEndIndex * 4+3-k*28); for (int i = 0; i <= changelineNum; i++) { if (changelineStartIndex[i] >= 0 && changelineStartIndex[i] <= vertex.Count && changlineEndIndex[i] >= 0 && changlineEndIndex[i] <= vertex.Count) { Vector2 Pos1 = new Vector2(vertex[changelineStartIndex[i]].position.x, GetMinY(changelineStartIndex[i], changlineEndIndex[i], vertex)); Vector2 pos2 = new Vector2(vertex[changlineEndIndex[i]].position.x, GetMinY(changelineStartIndex[i], changlineEndIndex[i], vertex)); AddUnderline(vh, Pos1, pos2); } } changelineStartIndex.Clear(); changlineEndIndex.Clear(); k++; startLableIndexStack.Push(startLableIndexList[m]); } } for(int n = 0; n <=startLableIndexStack.Count; n++) { if (startLableIndexStack.Count <= 0) return; currentStartIndex = startLableIndexStack.Pop(); currentEndIndex = endLableIndexList[k]; changelineStartIndex.Add(currentStartIndex * 4-k*28); CalculateChangeLine(vertex, currentStartIndex-k*7, currentEndIndex-k*7); changlineEndIndex.Add(currentEndIndex * 4 + 3-k*28); for (int i = 0; i <= changelineNum; i++) { if (changelineStartIndex[i] >= 0 && changelineStartIndex[i] <= vertex.Count && changlineEndIndex[i] >= 0 && changlineEndIndex[i] <= vertex.Count) { Vector2 Pos1 = new Vector2(vertex[changelineStartIndex[i]].position.x, GetMinY(changelineStartIndex[i], changlineEndIndex[i], vertex)); Vector2 pos2 = new Vector2(vertex[changlineEndIndex[i]].position.x, GetMinY(changelineStartIndex[i], changlineEndIndex[i], vertex)); AddUnderline(vh, Pos1, pos2); } } changelineStartIndex.Clear(); changlineEndIndex.Clear(); Debug.LogFormat("开始标签{0}与结束标签{1}匹配完成", currentStartIndex, currentEndIndex); k++; }

    判断是否换行:

    /// <summary> /// 计算换行的数量 /// </summary> /// <param name="uiv"></param> private void CalculateChangeLine(List<UIVertex> uiv,int startIndex,int endIndex) { changelineNum = 0; for (int i = startIndex * 4; i <= endIndex * 4; i++) { if (Mathf.Abs(uiv[i + 1].position.x - uiv[i].position.x) >= fontSize * 2) { Debug.LogFormat("划线字符串在{0}换行了", i); changelineStartIndex.Add(i + 1); changlineEndIndex.Add(i - 2); //Debug.LogFormat("第{0}个字符的X的坐标为{1}", i/4, uiv[i].position.x); changelineNum++; Debug.LogFormat("换了{0}行", changelineNum); } } }

    获取一行中最低的Y坐标,因为划线是在一行字符的最低点:

    /// <summary> /// 获取一行的最小的Y坐标的值 /// </summary> /// <param name="startIndex"></param> /// <param name="endIndex"></param> /// <param name="uiv"></param> /// <returns></returns> private float GetMinY(int startIndex,int endIndex,List<UIVertex> uiv) { if (uiv.Count >= endIndex) { float minY = uiv[startIndex].position.y; for (int i = startIndex; i < endIndex; i++) { if (uiv[i].position.y < minY) { minY = uiv[i].position.y; } } return minY; } return 0; }

    设置划线坐标,通过4个点来确定一条线:

    /// <summary> /// 设置划线的坐标 /// </summary> /// <param name="vh">VertexHelper</param> /// <param name="startPos">开始划线位置</param> /// <param name="endPos">结束划线位置</param> private void AddUnderline(VertexHelper vh, Vector2 startPos,Vector2 endPos) { Vector2 extents = rectTransform.rect.size; var setting = GetGenerationSettings(extents); TextGenerator underlineText = new TextGenerator(); underlineText.Populate("_", setting); IList<UIVertex> tut = underlineText.verts; Vector3[] ulPos = new Vector3[4]; if (Mathf.Abs( startPos.x) > 0 && Mathf.Abs( endPos.x) > 0 && Mathf.Abs( endPos.x - startPos.x) > 0&&UnderLineDistance!=UnderlineHeight) { Debug.LogFormat("下划线的高度为{0}", UnderlineHeight); Debug.LogFormat("下划线离字符的距离为{0}", UnderLineDistance); ulPos[0] = startPos+new Vector2 (0.0f, -fontSize* UnderLineDistance); ulPos[1] = endPos+new Vector2 (fontSize*0.5f, -fontSize* UnderLineDistance); ulPos[2] = endPos + new Vector2(0f, -fontSize * UnderlineHeight); ulPos[3] = startPos + new Vector2(0f, -fontSize * UnderlineHeight); UIVertex[] m_TempVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { m_TempVerts[j] = tut[j]; m_TempVerts[j].color = UnderlineColor; m_TempVerts[j].position = ulPos[j]; if (j == 3) vh.AddUIVertexQuad(m_TempVerts); } } }

    重写的绘制方法与删除标签:

    /// <summary> /// 重写绘制方法 /// </summary> /// <param name="toFill"></param> protected override void OnPopulateMesh(VertexHelper toFill) { Debug.LogFormat("m_Text的值为{0}", m_Text); var originText = m_Text; m_Text = DeleteLable(originText); base.OnPopulateMesh(toFill); m_Text = originText; UnderLineDistance = 0.1f; UnderlineHeight = 0.05f; List<UIVertex> vertexs = new List<UIVertex>(); toFill.GetUIVertexStream(vertexs); for(int i = 0; i <toFill.currentVertCount; i++) { UIVertex uiv = new UIVertex(); toFill.PopulateUIVertex(ref uiv, i); vertexs[i] = uiv; } if (vertexs.Count <= 0) return; DueTextString(toFill,vertexs); } /// <summary> /// 删除标签对 /// </summary> /// <param name="str"></param> /// <returns></returns> private string DeleteLable(string str) { if (str.Contains(UnderlineStartLable) && str.Contains(UnderlineEndLable)) { string currentStr = str.Replace(UnderlineStartLable, string.Empty); currentStr = currentStr.Replace(UnderlineEndLable, string.Empty); Debug.LogFormat("新字符串为{0}", currentStr); return currentStr; } return str; }

    研究这个东西前前后后花了一个星期,还是觉得自己太菜了,在做一个功能时对需求的理解不够到位,导致很多东西考虑不到。比如下划线会有换行的情况,比如标签嵌套等的情况。所以,在做功能前自己要先想清楚,后期就不用一直改代码去适应新的情况。最后希望自己摆正心态,不许偷懒,默默积累,厚积薄发。

    最新回复(0)