IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 【Unity】带有字符淡入效果的TextMeshPro打字机效果组件 -> 正文阅读

[游戏开发]【Unity】带有字符淡入效果的TextMeshPro打字机效果组件


【Unity】带有字符淡入效果的TextMeshPro打字机效果组件

在TextMeshPro中,可以通过 TMP_Text.maxVisibleCharacters 属性控制可见字符的个数,实现简单的打字机效果。如果要为打字机效果增加字符淡入效果,可以通过调整字符Mesh的顶点颜色来实现。下面的代码实现了一个基础的带有字符淡入效果的TextMeshPro打字机效果组件,主要实现步骤已在代码注释中进行了说明。

已知问题:

  1. FadeRange大于0时,会强制将可见字符的透明度设为完全不透明。
    要修复此问题,需要在开始输出字符前记录所有字符的原始透明度,并在执行字符淡化时代入记录的原始透明度进行计算。
  2. 带有删除线、下划线、背景色等效果的文本不能正常显示。
  3. 输出字符的过程中改变TextMeshPro组件的RectTransform参数,会导致文本显示异常。

示例

源代码

using System;
using System.Collections;

using TMPro;

using UnityEngine;

/// <summary>
/// 打字机效果状态。
/// </summary>
public enum TypewriterState
{
    /// <summary>
    /// 已完成输出。
    /// </summary>
    Completed,

    /// <summary>
    /// 正在输出。
    /// </summary>
    Outputting,

    /// <summary>
    /// 输出被中断。
    /// </summary>
    Interrupted
}

/// <summary>
/// 用于TextMeshPro的打字机效果组件。
/// 已知问题:
///   1. FadeRange大于0时,会强制将可见字符的透明度设为完全不透明。
///      要修复此问题,需要在开始输出字符前记录所有字符的原始透明度,并在执行字符淡化时代入记录的原始透明度进行计算。
///   2. 带有删除线、下划线、背景色等效果的文本不能正常显示。
///   3. 输出字符的过程中改变TextMeshPro组件的RectTransform参数,会导致文本显示异常。
/// </summary>
[RequireComponent(typeof(TMP_Text))]
public class Typewriter : MonoBehaviour
{
    /// <summary>
    /// 字符输出速度(字数/秒)。
    /// </summary>
    public byte OutputSpeed
    {
        get { return _outputSpeed; }
        set
        {
            _outputSpeed = value;
            CompleteOutput();
        }
    }

    /// <summary>
    /// 字符淡化范围(字数)。
    /// </summary>
    public byte FadeRange
    {
        get { return _fadeRange; }
        set
        {
            _fadeRange = value;
            CompleteOutput();
        }
    }

    /// <summary>
    /// 打字机效果状态。
    /// </summary>
    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;

    /// <summary>
    /// TextMeshPro组件。
    /// </summary>
    private TMP_Text _textComponent;

    /// <summary>
    /// 用于输出字符的协程。
    /// </summary>
    private Coroutine _outputCoroutine;

    /// <summary>
    /// 字符输出结束时的回调。
    /// </summary>
    private Action<TypewriterState> _outputEndCallback;


    /// <summary>
    /// 输出文字。
    /// </summary>
    /// <param name="text"></param>
    /// <param name="onOutputEnd"></param>
    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());
        }
    }

    /// <summary>
    /// 完成正在进行的打字机效果,将所有文字显示出来。
    /// </summary>
    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);
        }
    }

    /// <summary>
    /// 以不带淡入效果输出字符的协程。
    /// </summary>
    /// <param name="skipFirstCharacter"></param>
    /// <returns></returns>
    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);
    }

    /// <summary>
    /// 以带有淡入效果输出字符的协程。
    /// </summary>
    /// <returns></returns>
    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;
        }
    }

    /// <summary>
    /// 设置字符的顶点颜色Alpha值。
    /// </summary>
    /// <param name="index"></param>
    /// <param name="alpha"></param>
    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;

        //newVertexColors[vertexIndex + 0] = (Color)newVertexColors[vertexIndex + 0] * ColorTint;
        //newVertexColors[vertexIndex + 1] = (Color)newVertexColors[vertexIndex + 1] * ColorTint;
        //newVertexColors[vertexIndex + 2] = (Color)newVertexColors[vertexIndex + 2] * ColorTint;
        //newVertexColors[vertexIndex + 3] = (Color)newVertexColors[vertexIndex + 3] * ColorTint;
    }

    /// <summary>
    /// 处理输出结束逻辑。
    /// </summary>
    /// <param name="isShowAllCharacters"></param>
    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
  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-07-29 11:59:42  更:2021-07-29 12:00:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 18:31:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码