【Unity】带有字符淡入效果的TextMeshPro打字机效果组件
在TextMeshPro中,可以通过 TMP_Text.maxVisibleCharacters 属性控制可见字符的个数,实现简单的打字机效果。如果要为打字机效果增加字符淡入效果,可以通过调整字符Mesh的顶点颜色来实现。下面的代码实现了一个基础的带有字符淡入效果的TextMeshPro打字机效果组件,主要实现步骤已在代码注释中进行了说明。
已知问题:
- FadeRange大于0时,会强制将可见字符的透明度设为完全不透明。
要修复此问题,需要在开始输出字符前记录所有字符的原始透明度,并在执行字符淡化时代入记录的原始透明度进行计算。 - 带有删除线、下划线、背景色等效果的文本不能正常显示。
- 输出字符的过程中改变TextMeshPro组件的RectTransform参数,会导致文本显示异常。

源代码:
using System;
using System.Collections;
using TMPro;
using UnityEngine;
public enum TypewriterState
{
Completed,
Outputting,
Interrupted
}
[RequireComponent(typeof(TMP_Text))]
public class Typewriter : MonoBehaviour
{
public byte OutputSpeed
{
get { return _outputSpeed; }
set
{
_outputSpeed = value;
CompleteOutput();
}
}
public byte FadeRange
{
get { return _fadeRange; }
set
{
_fadeRange = value;
CompleteOutput();
}
}
public TypewriterState State { get; private set; } = TypewriterState.Completed;
[Tooltip("字符输出速度(字数/秒)。")]
[Range(1, 255)]
[SerializeField]
private byte _outputSpeed = 20;
[Tooltip("字符淡化范围(字数)。")]
[Range(0, 50)]
[SerializeField]
private byte _fadeRange = 10;
private TMP_Text _textComponent;
private Coroutine _outputCoroutine;
private Action<TypewriterState> _outputEndCallback;
public void OutputText(string text, Action<TypewriterState> onOutputEnd = null)
{
if (State == TypewriterState.Outputting)
{
StopCoroutine(_outputCoroutine);
State = TypewriterState.Interrupted;
OnOutputEnd(false);
}
_textComponent.text = text;
_outputEndCallback = onOutputEnd;
if (!isActiveAndEnabled)
{
State = TypewriterState.Completed;
OnOutputEnd(true);
return;
}
if (FadeRange > 0)
{
_outputCoroutine = StartCoroutine(OutputCharactersFading());
}
else
{
_outputCoroutine = StartCoroutine(OutputCharactersNoFading());
}
}
public void CompleteOutput()
{
if (State == TypewriterState.Outputting)
{
State = TypewriterState.Completed;
StopCoroutine(_outputCoroutine);
OnOutputEnd(true);
}
}
private void OnValidate()
{
if (State == TypewriterState.Outputting)
{
OutputText(_textComponent.text);
}
}
private void Awake()
{
_textComponent = GetComponent<TMP_Text>();
}
private void OnDisable()
{
if (State == TypewriterState.Outputting)
{
State = TypewriterState.Interrupted;
StopCoroutine(_outputCoroutine);
OnOutputEnd(true);
}
}
private IEnumerator OutputCharactersNoFading(bool skipFirstCharacter = true)
{
State = TypewriterState.Outputting;
_textComponent.maxVisibleCharacters = skipFirstCharacter ? 1 : 0;
_textComponent.ForceMeshUpdate();
var timer = 0f;
var interval = 1.0f / OutputSpeed;
var textInfo = _textComponent.textInfo;
while (_textComponent.maxVisibleCharacters < textInfo.characterCount)
{
timer += Time.deltaTime;
if (timer >= interval)
{
timer = 0;
_textComponent.maxVisibleCharacters++;
}
yield return null;
}
State = TypewriterState.Completed;
OnOutputEnd(false);
}
private IEnumerator OutputCharactersFading()
{
State = TypewriterState.Outputting;
var textInfo = _textComponent.textInfo;
_textComponent.maxVisibleCharacters = textInfo.characterCount;
_textComponent.ForceMeshUpdate();
if (textInfo.characterCount == 0)
{
State = TypewriterState.Completed;
OnOutputEnd(false);
yield break;
}
for (int i = 0; i < textInfo.characterCount; i++)
{
SetCharacterAlpha(i, 0);
}
var timer = 0f;
var interval = 1.0f / OutputSpeed;
var headCharacterIndex = 0;
while (State == TypewriterState.Outputting)
{
timer += Time.deltaTime;
var isFadeCompleted = true;
var tailIndex = headCharacterIndex - FadeRange + 1;
for (int i = headCharacterIndex; i > -1 && i >= tailIndex; i--)
{
if (!textInfo.characterInfo[i].isVisible)
{
continue;
}
var step = headCharacterIndex - i;
var alpha = (byte)Mathf.Clamp((timer / interval + step) / FadeRange * 255, 0, 255);
isFadeCompleted &= alpha == 255;
SetCharacterAlpha(i, alpha);
}
_textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);
if (timer >= interval)
{
if (headCharacterIndex < textInfo.characterCount - 1)
{
timer = 0;
headCharacterIndex++;
}
else if (isFadeCompleted)
{
State = TypewriterState.Completed;
OnOutputEnd(false);
yield break;
}
}
yield return null;
}
}
private void SetCharacterAlpha(int index, byte alpha)
{
var materialIndex = _textComponent.textInfo.characterInfo[index].materialReferenceIndex;
var vertexColors = _textComponent.textInfo.meshInfo[materialIndex].colors32;
var vertexIndex = _textComponent.textInfo.characterInfo[index].vertexIndex;
vertexColors[vertexIndex + 0].a = alpha;
vertexColors[vertexIndex + 1].a = alpha;
vertexColors[vertexIndex + 2].a = alpha;
vertexColors[vertexIndex + 3].a = alpha;
}
private void OnOutputEnd(bool isShowAllCharacters)
{
_outputCoroutine = null;
if (isShowAllCharacters)
{
var textInfo = _textComponent.textInfo;
for (int i = 0; i < textInfo.characterCount; i++)
{
SetCharacterAlpha(i, 255);
}
_textComponent.maxVisibleCharacters = textInfo.characterCount;
_textComponent.ForceMeshUpdate();
}
if (_outputEndCallback != null)
{
var temp = _outputEndCallback;
_outputEndCallback = null;
temp.Invoke(State);
}
}
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(Typewriter))]
class TypewriterEditor : UnityEditor.Editor
{
private Typewriter Target => (Typewriter)target;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
UnityEditor.EditorGUILayout.Space();
UnityEditor.EditorGUI.BeginDisabledGroup(!Application.isPlaying || !Target.isActiveAndEnabled);
GUILayout.BeginHorizontal();
if (GUILayout.Button("Restart"))
{
Target.OutputText(Target.GetComponent<TMP_Text>().text);
}
if (GUILayout.Button("Complete"))
{
Target.CompleteOutput();
}
GUILayout.EndHorizontal();
UnityEditor.EditorGUI.EndDisabledGroup();
}
}
#endif
|