Unity Text打字机效果,支持富文本

将之前的代码给deepseek跑了一下,感觉优化的很不错。效果与预期相符。
能正确显示和解析富文本
TypewriterEffect.cs

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

namespace MYTOOL.UI
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(UnityEngine.UI.Text))]
    public class TypewriterEffect : MonoBehaviour
    {
        [SerializeField, Tooltip("默认打字速度(每个字符的间隔时间)")]
        private float defaultTypeInterval = 0.05f;

        private UnityEngine.UI.Text targetTextComponent;
        private readonly StringBuilder typewriterBuilder = new StringBuilder();
        private string processedText;
        private Coroutine typingCoroutine;
        private Action onCompleteCallback;

        private static readonly string[] uguiRichTextTags = { "b", "i", "size", "color" };
        private readonly Stack<string> activeTags = new Stack<string>();

        private void Awake()
        {
            ValidateTextComponent();
        }

        public void StartTyping(string content, float speed = -1)
        {
            ValidateTextComponent();
            PrepareForNewTyping(content, speed);
            typingCoroutine = StartCoroutine(TypingRoutine(content));
        }

        public void SkipTyping()
        {
            if (!IsTyping) return;

            StopCoroutine(typingCoroutine);
            typingCoroutine = null;

            targetTextComponent.text = processedText;
            activeTags.Clear();

            onCompleteCallback?.Invoke();
        }

        public bool IsTyping => typingCoroutine != null;

        public void SetCompleteCallback(Action callback)
        {
            onCompleteCallback = callback;
        }

        private void ValidateTextComponent()
        {
            if (targetTextComponent == null)
            {
                targetTextComponent = GetComponent<UnityEngine.UI.Text>();
                if (targetTextComponent == null)
                {
                    throw new MissingComponentException("Text component is required for TypewriterEffect");
                }
            }
        }

        private void PrepareForNewTyping(string content, float speed)
        {
            defaultTypeInterval = speed > 0 ? speed : defaultTypeInterval;
            processedText = ProcessSpeedTags(content);
            targetTextComponent.text = string.Empty;
            activeTags.Clear();

            if (typingCoroutine != null)
            {
                StopCoroutine(typingCoroutine);
            }
        }

        private IEnumerator TypingRoutine(string originalText)
        {
            typewriterBuilder.Clear();
            float currentSpeed = defaultTypeInterval;

            for (int index = 0; index < originalText.Length; index++)
            {
                if (TryProcessSpeedTag(originalText, ref index, out float newSpeed))
                {
                    currentSpeed = newSpeed;
                    continue;
                }

                if (TryProcessRichTextTag(originalText, ref index))
                {
                    UpdateTextDisplay();
                    continue;
                }

                typewriterBuilder.Append(originalText[index]);
                UpdateTextDisplay();
                yield return new WaitForSeconds(currentSpeed);
            }

            FinalizeTyping();
        }

        private bool TryProcessSpeedTag(string text, ref int index, out float speed)
        {
            speed = defaultTypeInterval;
            const string speedTagOpen = "[speed=";
            const string speedTagClose = "[/speed]";

            // 处理开标签
            if (text[index] == '[' && text.Length > index + speedTagOpen.Length && text.Substring(index, speedTagOpen.Length) == speedTagOpen)
            {
                int closingBracketIndex = text.IndexOf(']', index);
                if (closingBracketIndex == -1) return false;

                string speedValue = text.Substring(index + speedTagOpen.Length, closingBracketIndex - index - speedTagOpen.Length);
                if (float.TryParse(speedValue, out speed))
                {
                    index = closingBracketIndex;
                    return true;
                }
            }

            // 处理闭标签
            if (text.Length > index + speedTagClose.Length && text.Substring(index, speedTagClose.Length) == speedTagClose)
            {
                speed = defaultTypeInterval;
                index += speedTagClose.Length - 1;
                return true;
            }

            return false;
        }

        private bool TryProcessRichTextTag(string text, ref int index)
        {
            if (text[index] != '<') return false;

            // 处理开标签
            foreach (var tag in uguiRichTextTags)
            {
                string tagStart = $"<{tag}";
                if (text.Length > index + tagStart.Length && text.Substring(index, tagStart.Length) == tagStart)
                {
                    int closingIndex = text.IndexOf('>', index);
                    if (closingIndex == -1) return false;

                    string fullTag = text.Substring(index, closingIndex - index + 1);
                    typewriterBuilder.Append(fullTag);
                    activeTags.Push(tag);
                    index = closingIndex; // 跳过整个标签内容
                    return true;
                }
            }

            // 处理闭标签
            foreach (var tag in uguiRichTextTags)
            {
                string tagEnd = $"</{tag}>";
                if (text.Length >= index + tagEnd.Length && text.Substring(index, tagEnd.Length) == tagEnd)
                {
                    if (activeTags.Count > 0 && activeTags.Peek() == tag)
                    {
                        activeTags.Pop();
                    }
                    typewriterBuilder.Append(tagEnd);
                    index += tagEnd.Length - 1; // 跳过整个闭合标签
                    return true;
                }
            }

            return false;
        }

        private void UpdateTextDisplay()
        {
            targetTextComponent.text = typewriterBuilder.ToString() + GenerateActiveTagsClosure();
        }

        private string GenerateActiveTagsClosure()
        {
            StringBuilder closure = new StringBuilder();
            foreach (var tag in activeTags)
            {
                closure.Append($"</{tag}>");
            }
            return closure.ToString();
        }

        private void FinalizeTyping()
        {
            typingCoroutine = null;
            onCompleteCallback?.Invoke();
        }

        private static string ProcessSpeedTags(string input)
        {
            return System.Text.RegularExpressions.Regex.Replace(input, @"\[speed=[^\]]+\]", string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
        }
    }

    public static class TypewriterExtensions
    {
        public static void StartTypewriter(this UnityEngine.UI.Text textComponent, string content, float speed = 0.05f, Action onComplete = null)
        {
            TypewriterEffect effect = GetOrAddTypewriterComponent(textComponent);
            effect.StartTyping(content, speed);
            effect.SetCompleteCallback(onComplete);
        }

        public static void SkipTypewriter(this UnityEngine.UI.Text textComponent)
        {
            if (textComponent.TryGetComponent<TypewriterEffect>(out var effect)) effect.SkipTyping();
        }

        public static bool IsTyping(this UnityEngine.UI.Text textComponent)
        {
            TypewriterEffect effect = textComponent.GetComponent<TypewriterEffect>();
            return effect != null && effect.IsTyping;
        }

        private static TypewriterEffect GetOrAddTypewriterComponent(UnityEngine.UI.Text textComponent)
        {
            if (!textComponent.TryGetComponent<TypewriterEffect>(out var effect))
            {
                effect = textComponent.gameObject.AddComponent<TypewriterEffect>();
            }

            return effect;
        }
    }
}

效果
测试代码
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值