Unity之富文本打字机效果

前段时间需要做一个打字机效果的案子,网上也有很多UGUI的打字机插件,利用tweener动画制作的。不过后来我发现很多都不默认支持富文本,所以需要自己改写代码。这里我简化了代码,做了一个精简版的打字机效果。
必须有GIF
在这里插入图片描述
TypewriterEffect组件代码如下:

using UnityEngine;
using System.Text;
using System.Collections.Generic;
using UnityEngine.Events;
using UnityEngine.UI;
using DG.Tweening;
using System.Text.RegularExpressions;

/// <summary>
/// This script is able to fill in the text's text gradually, giving the effect of someone typing or fading in the content over time.
/// </summary>

public class TypewriterEffect : MonoBehaviour
{
   
    public string val;

    /// <summary>
    /// How many characters will be printed per second.
    /// </summary>
    public int charsPerSecond = 20;

    /// <summary>
    /// How long it takes for each character to fade in.
    /// </summary>

    public float delayOnSpace = 1f;

    /// <summary>
    /// Event delegate triggered when the typewriter effect finishes.
    /// </summary>
    public UnityEvent onFinished = new UnityEvent();

    private Tweener doFadeTweener;
    Text mLabel;
    string mFullText = "";
    int mCurrentOffset = 0;
    float mNextChar = 0f;
    bool mReset = false;
    bool mActive = false;

    /// <summary>
    /// Whether the typewriter effect is currently active or not.
    /// </summary>
    public bool isActive { get { return mActive; } }

    public bool isComplete = true;

    public void WordByWord(string content)
    {
        QuickShow();
        isComplete = false;
        val = content;
        mReset = true;
        mActive = true;
    }

    //在逐渐显示的时候点击对话,快速显示所有文字
    //重复QuickShow不会重复触发onFinish
    public void QuickShow()
    {
        //结束淡入淡出
        if (doFadeTweener != null)
            doFadeTweener.Kill(true);
        else
        {//结束WordByWord
            mReset = false;
            Finish();
        }
    }

    /// <summary>
    /// Finish the typewriter operation and show all the text right away.
    /// </summary>
    public void Finish()
    {
        if (mActive)
        {
            mActive = false;

            if (!mReset)
            {
                mCurrentOffset = mFullText.Length;
                mLabel.text = val;
            }

            onFinished.Invoke();
        }
    }

    private void Awake()
    {
        //onFinished.AddListener(() => Debug.LogError("Finish!"));

        mLabel = GetComponent<Text>();
        mLabel.text = "";
        onFinished.AddListener(() => isComplete = true);
    }

    Regex tagPattern = new Regex("<[^>]*>[^<]*</[^>]*>");
    Regex contPattern = new Regex(">.*<");
    private StringBuilder sb;
    private Dictionary<int, cont> realPosList = new Dictionary<int, cont>();
    class cont
    {
        public int realIdx;
        public bool bCont;
        public string endStr;
        public cont(int realIdx, bool bCont = false, string endStr = "</color>")
        {
            this.realIdx = realIdx;
            this.bCont = bCont;
            this.endStr = endStr;
        }
    }
    cont GetRealIndex(int index)
    {
        cont value;
        if (realPosList.TryGetValue(index, out value))
        {
            return value;
        }
        return new cont(index);
    }
    string ReplaceCharInTag(string lastText)
    {
        sb = new StringBuilder();
        realPosList.Clear();
        var group = tagPattern.Matches(lastText);
        var startIndex = 0;
        if (group.Count > 0)
        {
            for (int i = 0; i < group.Count; i++)
            {
                var m1 = group[i];

                for (int j = startIndex; j < m1.Index; j++)
                {
                    realPosList.Add(sb.Length, new cont(j));
                    sb.Append(lastText[j]);
                }
                startIndex = m1.Index + m1.Length;

                var m2 = contPattern.Match(m1.Value);
                if (m2.Success)
                {
                    for (int k = 1; k < m2.Value.Length - 1; k++)
                    {
                        realPosList.Add(sb.Length, new cont(m1.Index + m2.Index + k, true));
                        sb.Append(m2.Value[k]);
                    }
                }
                if (i == group.Count - 1)
                {
                    for (int j = startIndex; j < lastText.Length; j++)
                    {
                        realPosList.Add(sb.Length, new cont(j));
                        sb.Append(lastText[j]);
                    }
                }
            }
        }
        else
        {
            return lastText;
        }
        return sb.ToString();
    }
    void Update()
    {
        if (!mActive) return;

        if (mReset)
        {
            mNextChar = 0;
            mLabel.text = "";
            mCurrentOffset = 0;
            mReset = false;
            if (mLabel.supportRichText)
            {
                mFullText = ReplaceCharInTag(val);
            }
            else
            {
                mFullText = val;
            }
        }

        while (mCurrentOffset < mFullText.Length && mNextChar <= Time.unscaledTime)
        {
            int lastOffset = mCurrentOffset;
            charsPerSecond = Mathf.Max(1, charsPerSecond);

            // Automatically skip all symbols
            while (ParseSymbol(mFullText, ref mCurrentOffset)) { }
            ++mCurrentOffset;

            // Periods and end-of-line characters should pause for a longer time.
            float delay = 1f / charsPerSecond;
            char c = (lastOffset < mFullText.Length) ? mFullText[lastOffset] : '\n';


            if (c == ' ')
                delay += delayOnSpace;

            if (mNextChar == 0f)
            {
                mNextChar = Time.unscaledTime + delay;
            }
            else mNextChar += delay;

            if (mLabel.supportRichText)
            {
                cont ct = GetRealIndex(mCurrentOffset - 1);
                mLabel.text = val.Substring(0, ct.realIdx + 1) + (ct.bCont ? ct.endStr : "");
            }
            else
            {
                mLabel.text = mFullText.Substring(0, mCurrentOffset); ;
            }
        }

        if (mCurrentOffset == mFullText.Length)
        {
            onFinished.Invoke();
            mActive = false;
        }
    }


    /// <summary>
    /// Parse an embedded symbol, such as [FFAA00] (set color) or [-] (undo color change). Returns whether the index was adjusted.
    /// </summary>
    static public bool ParseSymbol(string text, ref int index)
    {
        int length = text.Length;
        if (index + 17 > length || text[index] != '<' || text[index + 16] != '>') return false;
        if (text[index + 1] == '/') return false;
        if (!text.Substring(index, 8).Equals("<color=#")) return false;

        string alpha = text.Substring(index + 14, 2);
        int a = (HexToDecimal(alpha[0]) << 4) | HexToDecimal(alpha[1]);
        index += 17;
        return true;
    }

    static public int HexToDecimal(char ch)
    {
        switch (ch)
        {
            case '0': return 0x0;
            case '1': return 0x1;
            case '2': return 0x2;
            case '3': return 0x3;
            case '4': return 0x4;
            case '5': return 0x5;
            case '6': return 0x6;
            case '7': return 0x7;
            case '8': return 0x8;
            case '9': return 0x9;
            case 'a':
            case 'A': return 0xA;
            case 'b':
            case 'B': return 0xB;
            case 'c':
            case 'C': return 0xC;
            case 'd':
            case 'D': return 0xD;
            case 'e':
            case 'E': return 0xE;
            case 'f':
            case 'F': return 0xF;
        }
        return 0xF;
    }
}

核心在于如何处理富文本的地方,我这里使用最简单的字符串匹配模式,将富文本的地方打标记。在Update时。获取实际打标记的index。同时判断是否在富文本的区间,自动补全结束的标签。当然,我这里给出的只支持颜色打字机效果,有需求的可以将代码中的endStr参数也赋值即可,如果后期有项目需求做以及更多的富文本的话,会继续改善代码。
下面附上demo
//download.csdn.net/download/weixin_43876382/12010790

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值