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

相关新闻

从零实现Transformer:深入理解注意力机制与PyTorch实战

从零实现Transformer:深入理解注意力机制与PyTorch实战

1. 项目概述:从“注意力”到“变革者” 如果你在过去几年里关注过深度学习,尤其是自然语言处理领域,那么“Transformer”这个词一定如雷贯耳。它早已不是《变形金刚》电影的专属,而是彻底重塑了我们对序列建模认知的一种神经网络架…

2026/8/2 14:42:20 阅读更多 →
如何快速清理电脑重复文件:Czkawka文件管理工具终极指南

如何快速清理电脑重复文件:Czkawka文件管理工具终极指南

如何快速清理电脑重复文件:Czkawka文件管理工具终极指南 【免费下载链接】czkawka Multi functional app to find duplicates, empty folders, similar images etc. 项目地址: https://gitcode.com/GitHub_Trending/cz/czkawka 你的电脑硬盘是不是总是不够用…

2026/8/2 14:42:20 阅读更多 →
7天快速上手:将小爱音箱改造成你的专属AI语音助手终极指南

7天快速上手:将小爱音箱改造成你的专属AI语音助手终极指南

7天快速上手:将小爱音箱改造成你的专属AI语音助手终极指南 【免费下载链接】mi-gpt 🏠 将小爱音箱接入 ChatGPT 和豆包,改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt 你是否曾经对着小爱音箱发…

2026/8/2 14:41:20 阅读更多 →

最新新闻

GitHub访问慢的终极解决方案:Fast-GitHub如何让下载速度提升100倍?

GitHub访问慢的终极解决方案:Fast-GitHub如何让下载速度提升100倍?

GitHub访问慢的终极解决方案:Fast-GitHub如何让下载速度提升100倍? 【免费下载链接】Fast-GitHub 国内Github下载很慢,用上了这个插件后,下载速度嗖嗖嗖的~! 项目地址: https://gitcode.com/gh_mirrors/fa/Fast-GitH…

2026/8/2 15:28:51 阅读更多 →
C语言volatile与extern关键字:嵌入式开发中的编译器优化与模块化编程

C语言volatile与extern关键字:嵌入式开发中的编译器优化与模块化编程

1. 项目缘起:从一次诡异的“数据消失”说起 几年前,我在调试一个嵌入式数据采集模块时,遇到了一个至今记忆犹新的问题。模块的主控芯片是STM32,通过SPI接口周期性地从外部传感器读取一组16位的温度数据,并将其存储在一…

2026/8/2 15:28:51 阅读更多 →
Arduino语音识别模块实战:从硬件连接到项目集成

Arduino语音识别模块实战:从硬件连接到项目集成

1. 项目概述:从“能听”到“会做”的硬件桥梁如果你玩过Arduino,大概率会接触过各种传感器——温湿度、光照、超声波,它们让开发板能“感知”世界。但你是否想过,让Arduino也能“听懂”你的话?这就是Grove - 语音识别器…

2026/8/2 15:28:51 阅读更多 →
Pandas数据处理实战指南:从核心概念到高效应用

Pandas数据处理实战指南:从核心概念到高效应用

1. 项目概述:为什么你需要这份pandas指南如果你刚开始用Python处理数据,或者已经用了一段时间但总觉得pandas这个库用起来有点“别扭”——比如数据合并时总对不齐,筛选条件一复杂就报错,或者处理稍大点的文件就慢得让人想砸键盘—…

2026/8/2 15:28:51 阅读更多 →
ROCm 5.7 文档坑了我三天:这份可跑清单现在开源

ROCm 5.7 文档坑了我三天:这份可跑清单现在开源

AMD ROCm 环境下 LLaMA-7B 部署全攻略:从版本冲突到生产级解决方案 为什么你的 ROCm 示例跑不起来:深度技术分析 AMD 官方 Docker 镜像 rocm/pytorch:latest 默认安装 ROCm 5.7 版本的问题远比表面看到的复杂。经过我们团队超过 200 小时的测试验证&am…

2026/8/2 15:28:50 阅读更多 →
Docker部署Jenkins全攻略:从环境搭建到CI/CD实战配置

Docker部署Jenkins全攻略:从环境搭建到CI/CD实战配置

1. 为什么选择Docker来运行Jenkins? 如果你正在搭建CI/CD流水线,或者只是想体验一下Jenkins这个老牌的自动化工具,那么“如何安装”就是你遇到的第一个问题。传统方式是在服务器上直接安装Java环境,然后下载Jenkins的WAR包或者通过…

2026/8/2 15:27:50 阅读更多 →

日新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/2 0:00:38 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/2 0:00:38 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:38 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/2 0:00:38 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/2 0:00:38 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:38 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/2 6:34:16 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/2 2:47:48 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/2 0:23:22 阅读更多 →