Unity进阶:基于TextMeshPro实现带淡入效果的打字机脚本

📅 2026/8/2 14:42:13
Unity进阶:基于TextMeshPro实现带淡入效果的打字机脚本
1. 项目概述与核心价值在Unity里做对话系统新手和老手之间往往就差那么几个“感觉”。一个最直观的“感觉”就是文字呈现方式。直接把一整段话“啪”地一下怼到屏幕上和让文字像老式打字机一样一个字符一个字符地、带着呼吸感地浮现出来玩家的沉浸体验是天差地别的。这个“打字机效果”Typewriter Effect几乎是现代叙事驱动型游戏的标配。但很多教程止步于用Unity自带的UI Text组件实现基础打字机效果生硬功能单一。今天要聊的是它的“进阶版”——基于TextMeshProTMP实现带淡入效果的打字机。为什么非得是TMP因为Unity原生的UI Text在字体渲染、排版丰富度尤其是混排图文、超链接和性能上已经被TMP全面超越。对于追求文本表现力的游戏TMP是事实上的标准选择。这个项目要解决的不仅仅是“让字蹦出来”。它要解决的是如何让每个字在“蹦出来”的瞬间不是生硬地出现而是像从背景中逐渐显现淡入并且整个过程是可控的速度、暂停、跳过、事件回调。最终我会附上一个经过实战打磨的、功能完整的C#脚本你可以直接拿去用在你的项目里或者以此为骨架进行二次开发。2. 核心思路与方案选型实现一个打字机效果最核心的思路就是分帧渲染。我们不能在一帧内把整段文本设置好而是需要在一段连续的时间内每帧增加一个或几个字符的显示。2.1 基础方案对比协程 vs. Update最常见的两种实现载体是协程Coroutine和Update方法。协程方案利用yield return new WaitForSeconds(interval)来等待每个字符的显示间隔。代码结构清晰时序控制简单直观非常适合这种“等待-执行”的循环任务。它天然地将逻辑与帧率解耦你设定每0.05秒显示一个字那无论帧率是30还是60显示速度都是恒定的。Update方案在每帧中累积时间根据累积时间计算当前应显示的字符数。这种方式更贴近引擎的底层循环但需要自己处理时间计算代码稍显繁琐且在时间缩放Time.timeScale为0时也会停止。我们的选择是协程。理由很充分对于打字机这种强时序、离散事件驱动的功能协程的写法更符合直觉易于阅读和维护。我们可以轻松地在字符间插入等待也可以方便地挂起和继续。2.2 淡入效果的关键逐字顶点动画基础打字机只控制文本的“可见字符数”。而淡入效果需要控制每个字符的透明度Alpha。TMP的强大之处在于它允许我们访问和修改文本网格中每个字符的顶点信息。核心思路如下我们通过一个协程逐步增加TMP_Text.maxVisibleCharacters属性来控制显示多少个字。在每次增加显示字符后立即遍历当前所有可见字符。对于每个字符获取它的顶点数据一个包含4个顶点的数组代表一个字符的四边形。修改这些顶点颜色Color32的Alpha值根据该字符“已显示的时间”计算出一个从0到255的过渡值从而实现淡入。最后调用TMP_Text.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32)来强制更新网格颜色数据使修改生效。这里的关键在于淡入动画是逐字独立的。每个字符从完全透明Alpha0开始在它“出生”后的一小段时间内例如0.2秒过渡到完全不透明Alpha255。这样当一段文字在打印时你会看到最前面的字已经清晰中间的字半透明而最新的字刚刚开始显现形成一种平滑的波浪式推进效果视觉上非常舒服。2.3 功能完备性设计一个工业级的打字机脚本不能只有显示功能。我们必须考虑实际游戏中的交互需求速度控制支持全局字符间隔时间设置。加速与跳过玩家按住某个键如空格键或鼠标左键时打字速度应倍增玩家单击该键时应瞬间完成当前段落的显示。音效支持每个字符出现时可以触发一个打字音效对于不同角色或情绪可能还需要不同的音效。事件回调当开始显示、每个字符显示、完成显示时应能触发UnityEvent或C# Action方便其他系统如音频、任务日志、角色表情动画进行联动。富文本兼容必须正确处理TMP的富文本标签如color#FF0000红色/color、b粗体/b确保标签本身不被当作字符显示出来且不影响其包裹内容的样式。我们的脚本将围绕这些需求进行构建。3. 核心脚本拆解与实现下面我将逐步拆解这个名为AdvancedTypewriterEffect的核心脚本。我会先给出关键代码片段并解释最后提供完整脚本。3.1 脚本结构与属性定义首先我们创建一个继承自MonoBehaviour的类并声明所需的公共变量和私有字段。using UnityEngine; using UnityEngine.Events; using TMPro; using System.Collections; using System.Text.RegularExpressions; [RequireComponent(typeof(TMP_Text))] public class AdvancedTypewriterEffect : MonoBehaviour { [Header(基础设置)] public float charactersPerSecond 30f; // 打字速度字符/秒 [Tooltip(启用后每个字符的淡入时间固定。禁用则淡入时间与字符间隔关联。)] public bool useFixedFadeTime true; [SerializeField] private float fadeDuration 0.15f; // 每个字符的淡入时长秒 [Header(交互控制)] public KeyCode speedUpKey KeyCode.Space; public float speedUpMultiplier 3.0f; // 加速时的倍率 public bool canSkip true; [Header(音效)] public AudioClip typeSound; public AudioSource audioSource; // 可指定独立AudioSource为空则尝试获取组件上的 [Header(事件)] public UnityEvent onTypingStarted; public UnityEvent onCharacterPrinted; public UnityEvent onTypingCompleted; // 私有字段 private TMP_Text m_TextComponent; private string m_OriginalText; private Coroutine m_TypewriterCoroutine; private bool m_IsTyping false; private float m_CurrentInterval; // 计算出的实际字符间隔 private int m_TotalVisibleCharacters; // 去除富文本标签后的实际字符数 // 用于跳过富文本标签的正则表达式简化版 private static readonly Regex s_RichTextRegex new Regex([^]*); }属性解析charactersPerSecond 这是更直观的速度设置。字符间隔m_CurrentInterval会根据它计算间隔 1.0f / 速度。useFixedFadeTime和fadeDuration 这是淡入效果的控制核心。如果启用固定淡入时间无论打字快慢每个字的淡入动画时长都是fadeDuration。如果禁用淡入时间会自动与字符间隔时间挂钩例如设为间隔时间的80%确保动画节奏与打字节奏同步。事件UnityEvent 提供了无需硬编码的脚本通信方式在Inspector面板里就能拖拽关联其他游戏对象的方法非常灵活。3.2 初始化与启动打字在Start或Awake中获取组件引用并提供一个公共方法来启动打字过程。void Awake() { m_TextComponent GetComponentTMP_Text(); if (audioSource null) audioSource GetComponentAudioSource(); } /// summary /// 开始播放打字机效果。 /// /summary /// param nametextToType要打印的文本。如果为null或空则使用TMP组件当前的文本。/param public void StartTyping(string textToType null) { // 如果正在打字先停止之前的协程 if (m_IsTyping m_TypewriterCoroutine ! null) { StopCoroutine(m_TypewriterCoroutine); } // 重置文本状态 m_TextComponent.maxVisibleCharacters 0; m_TextComponent.ForceMeshUpdate(); // 强制更新网格以获取正确的文本信息 // 设置要打印的文本 if (!string.IsNullOrEmpty(textToType)) { m_TextComponent.text textToType; m_TextComponent.ForceMeshUpdate(); } m_OriginalText m_TextComponent.text; // 计算去除富文本标签后的实际字符数用于控制循环 m_TotalVisibleCharacters s_RichTextRegex.Replace(m_OriginalText, ).Length; // 计算基础间隔 m_CurrentInterval 1.0f / charactersPerSecond; // 触发开始事件 onTypingStarted?.Invoke(); m_IsTyping true; // 启动打字协程 m_TypewriterCoroutine StartCoroutine(TypewriterCoroutine()); }关键点ForceMeshUpdate() 在修改TMP文本或开始处理前调用此方法确保文本信息网格是最新的这对于后续获取正确的字符信息至关重要。s_RichTextRegex.Replace 这是一个简单的预处理。我们用一个正则表达式匹配所有...格式的标签并将其移除得到纯文本的长度。这样在协程循环时我们递增的“可见字符数”是针对TMP渲染的而循环终止条件判断的是“实际显示的字符数”避免了因标签计数导致提前结束或延迟结束的问题。这是一个常见的坑。3.3 打字协程的核心逻辑这是脚本的“心脏”。协程将逐步增加可见字符并处理淡入效果。private IEnumerator TypewriterCoroutine() { TMP_TextInfo textInfo m_TextComponent.textInfo; int totalCharacters textInfo.characterCount; // TMP解析出的字符总数含不可见字符 int visibleCount 0; int printedVisibleCount 0; // 已打印的“实际字符”数 // 预先将所有字符设置为完全透明 SetAllCharactersAlpha(0); m_TextComponent.maxVisibleCharacters 0; // 存储每个字符的“出生时间” float[] characterBirthTime new float[totalCharacters]; for (int i 0; i totalCharacters; i) characterBirthTime[i] -1f; float currentTime Time.time; while (printedVisibleCount m_TotalVisibleCharacters) { // 处理加速与跳过输入 float interval m_CurrentInterval; if (Input.GetKey(speedUpKey)) { interval / speedUpMultiplier; } if (canSkip Input.GetKeyDown(speedUpKey)) { // 立即显示所有文本 m_TextComponent.maxVisibleCharacters totalCharacters; SetAllCharactersAlpha(255); m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); printedVisibleCount m_TotalVisibleCharacters; break; } // 等待下一个字符的间隔时间 yield return new WaitForSeconds(interval); // 增加一个可见字符从TMP的角度 visibleCount; m_TextComponent.maxVisibleCharacters visibleCount; // 记录新显示字符的“出生时间” int currentCharIndex visibleCount - 1; if (currentCharIndex totalCharacters) { // 只有非空白、非标签的字符才算作“打印”了一个实际字符 TMP_CharacterInfo charInfo textInfo.characterInfo[currentCharIndex]; if (charInfo.character ! 0 !charInfo.isVisible) // 0是空字符通常由标签产生 { // 如果是不可见字符如富文本标签则不计入打印计数也不记录出生时间 // 但需要继续循环尝试显示下一个字符 continue; } characterBirthTime[currentCharIndex] Time.time; printedVisibleCount; // 实际打印字符数1 // 播放音效 PlayTypeSound(); // 触发字符打印事件 onCharacterPrinted?.Invoke(); } // 更新所有已出生字符的淡入状态 UpdateCharactersAlpha(characterBirthTime, visibleCount); currentTime Time.time; } // 循环结束打字完成 OnTypingFinished(); }逻辑详解双计数器visibleCount对应TMP的maxVisibleCharacters每次循环1。printedVisibleCount对应过滤掉富文本标签后的“实际字符”只有遇到真正渲染的字符时才1。这确保了即使文本中有很多样式标签打字节奏也不会被打乱。字符出生时间表characterBirthTime数组记录了每个TMP字符索引开始显示的时间点初始为-1。当某个字符变为可见时我们将其时间戳设为当前Time.time。输入处理 在循环开始时检查加速键。GetKey用于持续加速GetKeyDown用于触发跳过。跳过时我们直接将可见字符设为最大并将所有字符Alpha值设为255完全不透明然后跳出循环。淡入更新 每次显示一个新字符后调用UpdateCharactersAlpha根据每个字符的“已存活时间”当前时间 - 出生时间来更新其透明度。3.4 淡入效果的具体实现现在来看最关键的淡入动画部分。/// summary /// 根据字符的“出生时间”更新其透明度。 /// /summary private void UpdateCharactersAlpha(float[] birthTimes, int upToIndex) { float fadeTime useFixedFadeTime ? fadeDuration : (m_CurrentInterval * 0.8f); float currentTime Time.time; TMP_TextInfo textInfo m_TextComponent.textInfo; Color32[] newVertexColors; Color32 c0; // 遍历当前所有可见的字符 for (int i 0; i upToIndex i textInfo.characterCount; i) { TMP_CharacterInfo charInfo textInfo.characterInfo[i]; if (!charInfo.isVisible || birthTimes[i] 0) continue; // 跳过不可见或未出生的字符 int materialIndex charInfo.materialReferenceIndex; int vertexIndex charInfo.vertexIndex; newVertexColors textInfo.meshInfo[materialIndex].colors32; // 计算该字符的淡入进度 (0 到 1) float timeSinceBirth currentTime - birthTimes[i]; float alphaProgress Mathf.Clamp01(timeSinceBirth / fadeTime); byte targetAlpha (byte)(Mathf.Lerp(0, 255, alphaProgress)); // 更新该字符四个顶点的颜色Alpha值 c0 newVertexColors[vertexIndex]; c0.a targetAlpha; newVertexColors[vertexIndex] c0; c0 newVertexColors[vertexIndex 1]; c0.a targetAlpha; newVertexColors[vertexIndex 1] c0; c0 newVertexColors[vertexIndex 2]; c0.a targetAlpha; newVertexColors[vertexIndex 2] c0; c0 newVertexColors[vertexIndex 3]; c0.a targetAlpha; newVertexColors[vertexIndex 3] c0; } // 告诉TMP更新顶点颜色数据 m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); } /// summary /// 将所有字符的透明度设置为指定值。 /// /summary private void SetAllCharactersAlpha(byte alpha) { TMP_TextInfo textInfo m_TextComponent.textInfo; for (int i 0; i textInfo.characterCount; i) { TMP_CharacterInfo charInfo textInfo.characterInfo[i]; if (!charInfo.isVisible) continue; int materialIndex charInfo.materialReferenceIndex; int vertexIndex charInfo.vertexIndex; Color32[] newVertexColors textInfo.meshInfo[materialIndex].colors32; Color32 c0 new Color32(newVertexColors[vertexIndex].r, newVertexColors[vertexIndex].g, newVertexColors[vertexIndex].b, alpha); newVertexColors[vertexIndex] c0; newVertexColors[vertexIndex 1] c0; newVertexColors[vertexIndex 2] c0; newVertexColors[vertexIndex 3] c0; } }技术细节顶点操作 TMP中每个字符由4个顶点构成一个四边形。charInfo.vertexIndex指向这个四边形第一个顶点的索引。我们需要同时修改这4个顶点的颜色。材质引用索引 如果文本使用了多个材质比如自定义字体图集materialReferenceIndex指示当前字符属于哪个材质/网格。我们必须从对应的meshInfo数组中获取颜色数据。UpdateVertexData 这是点睛之笔。修改了顶点颜色数组后必须调用此方法并传入TMP_VertexDataUpdateFlags.Colors32标志才能将更改同步到实际渲染的网格上。忘记调用这一步屏幕上什么都不会改变。淡入计算Mathf.Clamp01确保进度在0到1之间。使用Mathf.Lerp在0和255之间进行线性插值得到平滑的Alpha过渡。3.5 收尾与音效打字完成或跳过时需要清理状态并触发完成事件。private void OnTypingFinished() { m_IsTyping false; m_TypewriterCoroutine null; onTypingCompleted?.Invoke(); } private void PlayTypeSound() { if (typeSound ! null audioSource ! null) { // 避免音效重叠太密集可以加一些随机音调或音量微调增加真实感 // audioSource.pitch Random.Range(0.95f, 1.05f); audioSource.PlayOneShot(typeSound); } } /// summary /// 立即停止打字并显示全部文本。 /// /summary public void FinishTypingImmediately() { if (m_TypewriterCoroutine ! null) { StopCoroutine(m_TypewriterCoroutine); } m_TextComponent.maxVisibleCharacters m_TextComponent.textInfo.characterCount; SetAllCharactersAlpha(255); m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); OnTypingFinished(); } /// summary /// 判断当前是否正在打字。 /// /summary public bool IsTyping() { return m_IsTyping; }FinishTypingImmediately方法提供了另一种从外部强制完成打字的方式比如在对话选择枝出现时。4. 使用示例与场景配置将脚本挂载到带有TMP_Text组件的GameObject上RequireComponent属性会自动添加。在Inspector中配置参数基础设置Characters Per Second设为30-50比较舒适。Fade Duration设为0.1-0.2秒淡入效果会比较明显。交互 通常将Speed Up Key设为Space。Speed Up Multiplier设为3.0到5.0。音效 将打字音效剪辑拖入Type Sound。如果对象上没有AudioSource脚本会尝试获取你也可以手动指定一个。事件 点击事件下方的号可以将其他对象的方法拖入。例如On Character Printed可以关联到一个随机播放多个打字音效的脚本On Typing Completed可以激活一个“继续”提示图标。在代码中调用// 获取引用 AdvancedTypewriterEffect typewriter dialogueText.GetComponentAdvancedTypewriterEffect(); // 开始打印一段新对话 typewriter.StartTyping(你好旅行者。color#FFAA00这个世界/color正在等待你的探索...);5. 常见问题、优化与避坑指南在实际使用中你可能会遇到以下问题这里给出解决方案和优化思路。5.1 富文本标签导致计时不准这是最常见的问题。我们的脚本通过正则表达式预处理文本计算纯文本长度作为终止条件并在协程中跳过不可见字符基本解决了这个问题。但需要注意非常复杂的标签嵌套如colorredb文本/b/color可能被TMP解析为多个字符索引我们的charInfo.isVisible判断通常能正确处理。如果遇到极端情况可以尝试使用TMP_Text.GetParsedText()来获取处理后的文本进行分析。5.2 性能考量与顶点更新优化在每一帧协程的每次循环后都遍历所有可见字符并更新顶点颜色如果文本很长超过200字可能会有性能压力。优化方法增量更新 在UpdateCharactersAlpha中只更新“出生”时间在最近一两秒内的字符更早的字符透明度早已是255无需重复计算和设置。分帧更新 如果单次更新顶点数过多可以考虑将顶点更新操作分散到多帧完成但这会增加代码复杂度可能影响视觉效果的一致性。对于大多数对话场景一次更新的字符数有限直接更新的开销是可以接受的。5.3 与对话系统集成这个打字机脚本是一个“表现层”组件。一个完整的对话系统通常有数据层对话树、JSON配置、逻辑层对话流转、条件分支和表现层UI、打字机、角色立绘动画。建议的集成方式创建一个DialogueManager单例或管理器类负责加载对话数据、管理当前对话状态。当需要显示一句话时DialogueManager调用AdvancedTypewriterEffect.StartTyping(text)。监听打字机的onTypingCompleted事件。当一句话打完时DialogueManager显示“继续”按钮等待玩家输入。玩家按下继续键后DialogueManager处理下一句对话或弹出选项。5.4 扩展功能思路情绪化打字 可以为不同的角色或情绪预设不同的打字速度、淡入时间和音效。在StartTyping方法中增加参数或通过配置文件读取。震动效果 在字符出现时给其顶点位置添加微小的随机偏移模拟老旧打字机的震动感。这需要在修改顶点颜色时同步修改顶点位置 (meshInfo.vertices)。光标闪烁 在打字过程中在文本末尾显示一个闪烁的光标。可以在协程中在每次更新文本后动态地在文本末尾附加一个光标字符如“_”或“|”并单独控制其闪烁动画。打字完成后移除或停止闪烁。多语言与字体回退 TMP本身支持字体回退。确保你的TMP字体资源包含了所需语言的字符集。对于像中文、日文这样字符集庞大的语言要特别注意字体文件的大小和内存占用。5.5 一个关键的“坑”TextMeshProUGUI 与 TextMeshProUnity的TMP有两个主要组件TextMeshProUGUI用于UI系统和TextMeshPro用于3D世界空间或2D SpriteRenderer。我们的脚本通过TMP_Text这个基类来引用两者都兼容。但要注意如果你的文本是UI元素请使用TextMeshProUGUI。如果你需要在3D世界中显示漂浮文字则使用TextMeshPro。两者在顶点数据的获取和更新上API一致所以我们的脚本无需修改即可通用。最后附上完整的脚本代码。你可以直接复制到一个新的C#文件中命名为AdvancedTypewriterEffect.cs然后拖到你的TMP文本对象上开始使用。记住好的工具是打磨出来的根据你的项目需求调整参数和扩展功能让它成为你对话系统里最得力的那一环。// AdvancedTypewriterEffect.cs 完整脚本 using UnityEngine; using UnityEngine.Events; using TMPro; using System.Collections; using System.Text.RegularExpressions; [RequireComponent(typeof(TMP_Text))] public class AdvancedTypewriterEffect : MonoBehaviour { [Header(基础设置)] public float charactersPerSecond 30f; [Tooltip(启用后每个字符的淡入时间固定。禁用则淡入时间与字符间隔关联。)] public bool useFixedFadeTime true; [SerializeField] private float fadeDuration 0.15f; [Header(交互控制)] public KeyCode speedUpKey KeyCode.Space; public float speedUpMultiplier 3.0f; public bool canSkip true; [Header(音效)] public AudioClip typeSound; public AudioSource audioSource; [Header(事件)] public UnityEvent onTypingStarted; public UnityEvent onCharacterPrinted; public UnityEvent onTypingCompleted; private TMP_Text m_TextComponent; private string m_OriginalText; private Coroutine m_TypewriterCoroutine; private bool m_IsTyping false; private float m_CurrentInterval; private int m_TotalVisibleCharacters; private static readonly Regex s_RichTextRegex new Regex([^]*); void Awake() { m_TextComponent GetComponentTMP_Text(); if (audioSource null) audioSource GetComponentAudioSource(); } public void StartTyping(string textToType null) { if (m_IsTyping m_TypewriterCoroutine ! null) { StopCoroutine(m_TypewriterCoroutine); } m_TextComponent.maxVisibleCharacters 0; m_TextComponent.ForceMeshUpdate(); if (!string.IsNullOrEmpty(textToType)) { m_TextComponent.text textToType; m_TextComponent.ForceMeshUpdate(); } m_OriginalText m_TextComponent.text; m_TotalVisibleCharacters s_RichTextRegex.Replace(m_OriginalText, ).Length; m_CurrentInterval 1.0f / charactersPerSecond; onTypingStarted?.Invoke(); m_IsTyping true; m_TypewriterCoroutine StartCoroutine(TypewriterCoroutine()); } private IEnumerator TypewriterCoroutine() { TMP_TextInfo textInfo m_TextComponent.textInfo; int totalCharacters textInfo.characterCount; int visibleCount 0; int printedVisibleCount 0; SetAllCharactersAlpha(0); m_TextComponent.maxVisibleCharacters 0; float[] characterBirthTime new float[totalCharacters]; for (int i 0; i totalCharacters; i) characterBirthTime[i] -1f; float currentTime Time.time; while (printedVisibleCount m_TotalVisibleCharacters) { float interval m_CurrentInterval; if (Input.GetKey(speedUpKey)) { interval / speedUpMultiplier; } if (canSkip Input.GetKeyDown(speedUpKey)) { m_TextComponent.maxVisibleCharacters totalCharacters; SetAllCharactersAlpha(255); m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); printedVisibleCount m_TotalVisibleCharacters; break; } yield return new WaitForSeconds(interval); visibleCount; m_TextComponent.maxVisibleCharacters visibleCount; int currentCharIndex visibleCount - 1; if (currentCharIndex totalCharacters) { TMP_CharacterInfo charInfo textInfo.characterInfo[currentCharIndex]; if (charInfo.character 0 || !charInfo.isVisible) { continue; } characterBirthTime[currentCharIndex] Time.time; printedVisibleCount; PlayTypeSound(); onCharacterPrinted?.Invoke(); } UpdateCharactersAlpha(characterBirthTime, visibleCount); currentTime Time.time; } OnTypingFinished(); } private void UpdateCharactersAlpha(float[] birthTimes, int upToIndex) { float fadeTime useFixedFadeTime ? fadeDuration : (m_CurrentInterval * 0.8f); float currentTime Time.time; TMP_TextInfo textInfo m_TextComponent.textInfo; Color32[] newVertexColors; Color32 c0; for (int i 0; i upToIndex i textInfo.characterCount; i) { TMP_CharacterInfo charInfo textInfo.characterInfo[i]; if (!charInfo.isVisible || birthTimes[i] 0) continue; int materialIndex charInfo.materialReferenceIndex; int vertexIndex charInfo.vertexIndex; newVertexColors textInfo.meshInfo[materialIndex].colors32; float timeSinceBirth currentTime - birthTimes[i]; float alphaProgress Mathf.Clamp01(timeSinceBirth / fadeTime); byte targetAlpha (byte)(Mathf.Lerp(0, 255, alphaProgress)); c0 newVertexColors[vertexIndex]; c0.a targetAlpha; newVertexColors[vertexIndex] c0; c0 newVertexColors[vertexIndex 1]; c0.a targetAlpha; newVertexColors[vertexIndex 1] c0; c0 newVertexColors[vertexIndex 2]; c0.a targetAlpha; newVertexColors[vertexIndex 2] c0; c0 newVertexColors[vertexIndex 3]; c0.a targetAlpha; newVertexColors[vertexIndex 3] c0; } m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); } private void SetAllCharactersAlpha(byte alpha) { TMP_TextInfo textInfo m_TextComponent.textInfo; for (int i 0; i textInfo.characterCount; i) { TMP_CharacterInfo charInfo textInfo.characterInfo[i]; if (!charInfo.isVisible) continue; int materialIndex charInfo.materialReferenceIndex; int vertexIndex charInfo.vertexIndex; Color32[] newVertexColors textInfo.meshInfo[materialIndex].colors32; Color32 c0 new Color32(newVertexColors[vertexIndex].r, newVertexColors[vertexIndex].g, newVertexColors[vertexIndex].b, alpha); newVertexColors[vertexIndex] c0; newVertexColors[vertexIndex 1] c0; newVertexColors[vertexIndex 2] c0; newVertexColors[vertexIndex 3] c0; } } private void OnTypingFinished() { m_IsTyping false; m_TypewriterCoroutine null; onTypingCompleted?.Invoke(); } private void PlayTypeSound() { if (typeSound ! null audioSource ! null) { audioSource.PlayOneShot(typeSound); } } public void FinishTypingImmediately() { if (m_TypewriterCoroutine ! null) { StopCoroutine(m_TypewriterCoroutine); } m_TextComponent.maxVisibleCharacters m_TextComponent.textInfo.characterCount; SetAllCharactersAlpha(255); m_TextComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); OnTypingFinished(); } public bool IsTyping() { return m_IsTyping; } }