Unity的UGUI避免行的开头出现符号

一、遇到问题

大家好,我是阿赵。最近在游戏过版署的时候,修改意见里面有一条,游戏内部分文本内容中有标点符号出现在行首的问题。
一般来说,我们编辑文本的时候,是会注意不要把标点符号在换行的时候刚好出现的在行首的,但这个问题,似乎不是策划编辑文本时的问题。这里我会分析问题产生的原因,并附上解决问题的源码。代码写得有点仓促,不确定有没有隐含的问题。如果各位使用的时候发现问题,可以一起交流一下。

二、分析问题出现的原因

下面来看看具体出现的原因:
1、在Canvas Scaler里面选择UI Scale Model 为Scale With Screen Size
在这里插入图片描述

2、在Text里面设置文本是自动换行的
在这里插入图片描述

3、在一个分辨率的情况下看,文字并没有出现行首是标点符号的情况。
在这里插入图片描述

4、换一种分辨率之后,发现某个行首出现了标点符号。
在这里插入图片描述

通过上面的操作步骤,基本上已经能知道这个问题的出现原因了。
首先,是Scale With Screen Size的缩放方式才会出现问题的,如果使用Constant Pixel Size的缩放方式,是不会有这种问题。
然后,一定是要自动换行才会有这种问题,如果不是自动换行,每行出现的内容不会发生变化
那么我们是不是可以把缩放方式用Constant Pixel Size,并且不用自动换行来解决这个问题呢?
答案明显是否定的。对于一个大型游戏来说,首先适配各种设备分辨率是必须的。如果用Constant Pixel Size,是不会出现换行错乱的问题,但对于不同分辨率的设备来说,固定显示一定分辨率的做法明显是不符合适配的要求的。
然后自动换行的问题也是一样。游戏里面出现的带有文本的UI非常多,同一个UI里面显示的内容,根据游戏进度不一样,也是不一样的,所以很难做到每个界面不自动换行,而逐行Text指定游戏内容的。一般都是设定一个文本框的显示范围,然后自动换行来填充文字。
我个人感觉,这个问题的确是Unity底层自动换行造成的问题,因为Unity的自动换行规则导致的。

说个题外话,如果没兴趣听的可以直接跳到下一节。
这个时候,估计有些朋友已经开始痛骂Unity引擎是垃圾了。我个人认为,问题的确是出在Unity引擎的身世,但垃圾的不是Unity引擎,而是我们国内的技术人员。
为什么这么说呢?这是因为,Unity引擎,是使用英语为母语的人制作出来的,他们考虑问题的出发点,都是以英语为基础的。
比如这个换行的问题,如果输入的不是中文,而是英文。英文单词之间是有空格分隔的,而Unity对于自动换行的规则,是遇到不足以在同一行显示的文本时,会以空格作为切割文本的依据。比如说句末有一个单词连着句号,并且发现这个单词不能完整的显示在同一行,那么Unity会把整个单词加上句号一起换行了。所以,这种问题如果在纯英文的项目里面,基本上是不存在的。
我们可以说Unity没有做好各种语言的本地化适配,但我们更应该意识到,为什么我们要使用一个由英语作为母语并且以英语作为思考问题出发点的引擎呢?为什么我们不直接使用一个由中文作为基础的引擎呢?答案很简单,我们做不出来这种基本的引擎。既然自己做不出来,就不能怪别人垃圾了,只能怪自己没有能力。

言归正传,既然出现问题,我们也只能去解决这个问题。

三、解决问题的

我遇到这个问题的时候,在网上学习了很多前辈的处理方法,总结一下,处理方法基本上是这样的:
1.修改Text的底层,或者继承Text写一个新的Text类
2.监听Text文本变化的方法
3.当文本有变化时,先等当前帧渲染完
4.然后逐行去判断,是否有行首符号,如果有,往前再找一个字符,给他前面强制插入换行符
归纳了别人的解决办法之后,我自己的解决办法稍微不同
1.我并没有修改Text的底层或者继承Text写新的类,而是新建了一个继承MonoBehaviour的类,用于动态挂在有Text的物体上,需要的时候才挂
2.我没有监听Text文本变化的方法,而是自己存储了从外部主动设置到Text的文本,然后通过Update去判断当前的文本是否有变化。这是因为,我不但要关心Text的文本变化,还要关心游戏设备的分辨率变化。当游戏分辨率变化的时候,我不能直接拿Text当前的文本去重新计算,而是要拿最原始的文本去重新计算
3.会出现一种情况,之前没有符号在行首,但由于上面的行插入了新的回车,导致下面的行出现了行首符号的。所以并不能一次性就计算完,需要多次计算直到没有行首符号为止。
4.由于多次计算每次都需要等待当前帧渲染完毕,所以从肉眼可以看出,Text的文本出现闪一下的情况。

四、完整代码

using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;

public class TextSymbolWrap : MonoBehaviour
{
    // Start is called before the first frame update
    public Text textCom;
    private string origStr;
    private string replaceStr;
    private string finalReplaceStr;

    /// 标记不换行的空格(换行空格Unicode编码为/u0020,不换行的/u00A0)
	public static readonly string Non_breaking_space = "\u00A0";

    /// 用于匹配标点符号,为了不破坏富文本标签,所以只匹配指定的符号
    private readonly string strPunctuation = @"[,。??!!…]";

    /// 用于存储text组件中的内容
    private System.Text.StringBuilder TempText = null;

    /// 用于存储text生成器中的内容
    private IList<UILineInfo> TextLine;

    private int screenWidth = 0;
    private int screenHeight = 0;
    //在替换后的文本最后面添加一个看不到的字符,以用于辨别当前输入的文本是不是原始文本
    private string endString = " ";

    private bool isReplacing = false;

    void Start()
    {
        
    }
    private void OnEnable()
    {
        CheckTextComponent();
        CheckScreenSizeChange();
        ReplaceTextFun();
    }
    // Update is called once per frame
    void Update()
    {
        if(CheckScreenSizeChange() == true)
        {
            if(textCom!=null&&string.IsNullOrEmpty(origStr)==false)
            {
                if(textCom!=null)
                {
                    textCom.text = origStr;
                }                
                replaceStr = "";
                finalReplaceStr = "";
            }
        }
        CheckReplaceText();
    }

    private bool CheckScreenSizeChange()
    {
        if(Screen.width!=screenWidth||Screen.height!=screenHeight)
        {
            screenWidth = Screen.width;
            screenHeight = Screen.height;
            return true;
        }
        else
        {
            return false;
        }
    }

    private void CheckTextComponent()
    {
        if(textCom!= null)
        {
            return;
        }
        textCom = this.gameObject.GetComponent<Text>();
    }

    private void CheckReplaceText()
    {
        if (textCom == null)
        {
            return;
        }
        if (CheckTextIsChange() == false)
        {
            return;
        }
        ReplaceTextFun();
    }

    private void ReplaceTextFun()
    {
        if(isReplacing == true)
        {
            return;
        }

        replaceStr = "";
        finalReplaceStr = "";
        StartCoroutine("ClearUpPunctuationMode", textCom); 
    }

    private bool CheckTextIsChange()
    {
        if(textCom == null)
        {
            return false;
        }
        string txt = textCom.text;
        if(string.Equals(txt, finalReplaceStr) ==true)
        {
            return false;
        }
        return true;
    }

    IEnumerator ClearUpPunctuationMode(Text _component)
    {
        isReplacing = true;
        //不能立刻就进行计算,要等起码渲染完上一帧才计算,所以延迟了60毫秒
        yield return new WaitForSeconds(0.06f);

        //清除上一次添加的换行符号
        //_component.text = _component.text.Replace(" ", string.Empty);
        string tempTxt = _component.text;
        bool isOrigStr = false;
        if(tempTxt[tempTxt.Length-1].ToString() != endString)
        {
            //如果结尾没有空白字符,就认为是原始的字符串,记录下来用于分辨率改变时再次计算
            origStr = tempTxt;
            isOrigStr = true;
        }
        TextLine = _component.cachedTextGenerator.lines;
        //需要改变的字符序号
        int ChangeIndex = -1;
        TempText = new System.Text.StringBuilder(_component.text);
        for (int i = 1; i < TextLine.Count; i++)
        {
            //首位是否有标点
            bool IsPunctuation = Regex.IsMatch(TempText[TextLine[i].startCharIdx].ToString(), strPunctuation);
            //因为将换行空格都改成不换行空格后需要另外判断下如果首字符是不换行空格那么还是需要调整换行字符的下标
            if (TempText[TextLine[i].startCharIdx].ToString() == Non_breaking_space)
            {
                IsPunctuation = true;
            }

            //没有标点就跳过本次循环
            if (!IsPunctuation)
            {
                continue;
            }
            else
            {
                //有标点时保存当前下标
                ChangeIndex = TextLine[i].startCharIdx;
                //下面这个循环是为了判断当已经提前一个字符后当前这个的首字符还是标点时做的继续提前字符的处理
                while (IsPunctuation)
                {
                    ChangeIndex = ChangeIndex - 1;
                    if (ChangeIndex < 0) break;

                    IsPunctuation = Regex.IsMatch(TempText[ChangeIndex].ToString(), strPunctuation);
                    //因为将换行空格都改成不换行空格后需要另外判断下如果首字符是不换行空格那么还是需要调整换行字符的下标
                    if (TempText[ChangeIndex].ToString() == Non_breaking_space)
                    {
                        IsPunctuation = true; 
                    }

                }
                if (ChangeIndex < 0) continue;

                if (TempText[ChangeIndex - 1] != '\n')
                    TempText.Insert(ChangeIndex, "\n");
            }

        }
        
        replaceStr = TempText.ToString();
        if(string.Equals(tempTxt, replaceStr)==false)
        {
            //如果计算出来的最后结果和text组件当前的字符串不一致,证明有改动,改动后还需要继续判断
            //因为有可能在插入换行后,其他的地方会出现问题
            if(isOrigStr)
            {
                replaceStr += endString;
            }            
            _component.text = replaceStr;

        }
        else
        {
            //计算后的结果和当前text组件的字符串一致,证明当前text组件的字符串已经没有问题
            //记录下来,用于判断当前的字符串是否有改变
            finalReplaceStr = replaceStr;
        }
        isReplacing = false;
    }
}

猜你喜欢

转载自blog.csdn.net/liweizhao/article/details/130656581
今日推荐