【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
 
                
                
                
        
    
 
 |