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;}}
}
效果