【游戏开发实战】Unity UGUI实现文字打字效果(文字剧情),支持富文本,附Demo工程

一、前言

点关注不迷路,持续输出Unity干货文章。

嗨,大家好,我是新发。如下,使用Unity UGUI实现文字打字效果。
在这里插入图片描述
本文Demo工程已上传到CodeChina,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityUGUITextTypeEffectDemo
注:我使用的Unity版本:2020.2.7f1c1 (64-bit)
在这里插入图片描述

二、原理

原理就是通过协程,再利用Substring截取文本,这样就可以一个字一个字显示了,为了支持富文本,所以我们还需要检测关键字,为了方便大家理解,我画成图:
在这里插入图片描述

富文本关键字:

关键字含义示例
<b></b>粗体<b>我是粗体</b>
<i></i>斜体<b>我是斜体</b>
<size=40></size>字号<size=40>40号字</size>
<color=#EA5B44FF></color>颜色<color=#EA5B44FF>带颜色的字</color>
[speed=0.3]速度,一个字0.3秒[speed=0.3]0.3秒显示一个字

上面的文字文本如下:

"[speed=0.3]大家好,[speed=0.1]我是<color=#EA5B44FF>新发</color>,是一名<color=#EA5B44FF>Unity3D游戏开发工程师</color>。"
"我从2015年开始在CSDN写博客,到现在有近[speed=0.2]<size=40>两万粉丝</size>啦。"
"感谢大家的[speed=0.3]<size=40>关注</size>与<b>支持</b>。"
"我会持续输出Unity[speed=0.3]干货文章[speed=0.1],希望可以帮助到想学Unity的同学,共勉!"
"好了,那么我们下一篇文章再见,[speed=0.4]<i>拜拜</i>。"

三、脚本使用方法

一个脚本:TypeTextComponent,如下:(完整代码见文章末尾)
在这里插入图片描述
拓展了Text组件,提供一个TypeText的接口,内部会使用协程进行挨个字挨个字显示。

// 一个字一个字显示,支持富文本
// speed:每个字显示的时间
// onComplete:显示完整文本回调函数
public static void TypeText(this Text label, string text, float speed = 0.05f, 
								TypeTextComponent.OnComplete onComplete = null)

我们只需要调用这个TypeText接口,例:

// public Text text;

text.TypeText("一个字一个字显示,支持富文本");

另外,有时候需要跳过剧情,一步到位显示完整的文本,所以再拓展两个接口:

// 检测文本剧情是否可跳过
public static bool IsSkippable(this Text label)
// 跳过文本剧情
public static void SkipTypeText(this Text label)

四、测试

我们先做个简单的界面,如下:
在这里插入图片描述
写个Main脚本(完整代码见文章末尾),挂到Canvas节点上,赋值Text对象(ContentText)。
在这里插入图片描述
为了实现点击对白立刻跳过剧情,一步到位显示完整的对话。
在这里插入图片描述
我们给对白的白色框框加个事件监听,当鼠标点击白色框框时,会调用Main.OnClickWindow方法:
在这里插入图片描述
对应的OnClickWindow方法如下:

// Main.cs

/// <summary>
/// 对白被点击
/// </summary>
public void OnClickWindow()
{
    if (text.IsSkippable())
    {
        // 跳过,直接显示全部文本
        text.SkipTypeText();
    }
    else
    {
        // ...
    }
}

运行测试效果如下:
在这里插入图片描述
完毕。
喜欢Unity的同学,不要忘记点击关注,如果有什么Unity相关的技术难题,也欢迎留言或私信~

五、完整代码

1、TypeTextComponent.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public class TypeTextComponent : MonoBehaviour
{
    public delegate void OnComplete();

    [SerializeField]
    private float _defaultSpeed = 0.05f;

    private Text label;
    private string _currentText;
    private string _finalText;
    private Coroutine _typeTextCoroutine;

    private static readonly string[] _uguiSymbols = { "b", "i" };
    private static readonly string[] _uguiCloseSymbols = { "b", "i", "size", "color" };
    private OnComplete _onCompleteCallback;

    private void Init()
    {
        if (label == null)
            label = GetComponent<Text>();
    }

    public void Awake()
    {
        Init();
    }

    public void SetText(string text, float speed = -1)
    {
        Init();

        _defaultSpeed = speed > 0 ? speed : _defaultSpeed;
        _finalText = ReplaceSpeed(text);
        label.text = "";

        if (_typeTextCoroutine != null)
        {
            StopCoroutine(_typeTextCoroutine);
        }

        _typeTextCoroutine = StartCoroutine(TypeText(text));
    }

    public void SkipTypeText()
    {
        if (_typeTextCoroutine != null)
            StopCoroutine(_typeTextCoroutine);
        _typeTextCoroutine = null;

        label.text = _finalText;

        if (_onCompleteCallback != null)
            _onCompleteCallback();
    }

    public IEnumerator TypeText(string text)
    {
        _currentText = "";

        var len = text.Length;
        var speed = _defaultSpeed;
        var tagOpened = false;
        var tagType = "";
        for (var i = 0; i < len; i++)
        {
            if (text[i] == '[' && i + 6 < len && text.Substring(i, 7).Equals("[speed="))
            {
                var parseSpeed = "";
                for (var j = i + 7; j < len; j++)
                {
                    if (text[j] == ']')
                        break;
                    parseSpeed += text[j];
                }

                if (!float.TryParse(parseSpeed, out speed))
                    speed = 0.05f;

                i += 8 + parseSpeed.Length - 1;
                continue;
            }

            // ngui color tag
            if (text[i] == '[' && i + 7 < len && text[i + 7] == ']')
            {
                _currentText += text.Substring(i, 8);
                i += 8 - 1;
                continue;
            }

            var symbolDetected = false;
            for (var j = 0; j < _uguiSymbols.Length; j++)
            {
                var symbol = string.Format("<{0}>", _uguiSymbols[j]);
                if (text[i] == '<' && i + (1 + _uguiSymbols[j].Length) < len && text.Substring(i, 2 + _uguiSymbols[j].Length).Equals(symbol))
                {
                    _currentText += symbol;
                    i += (2 + _uguiSymbols[j].Length) - 1;
                    symbolDetected = true;
                    tagOpened = true;
                    tagType = _uguiSymbols[j];
                    break;
                }
            }

            if (text[i] == '<' && i + (1 + 15) < len && text.Substring(i, 2 + 6).Equals("<color=#") && text[i + 16] == '>')
            {
                _currentText += text.Substring(i, 2 + 6 + 8);
                i += (2 + 14) - 1;
                symbolDetected = true;
                tagOpened = true;
                tagType = "color";
            }

            if (text[i] == '<' && i + 5 < len && text.Substring(i, 6).Equals("<size="))
            {
                var parseSize = "";
                var size = (float)label.fontSize;
                for (var j = i + 6; j < len; j++)
                {
                    if (text[j] == '>') break;
                    parseSize += text[j];
                }

                if (float.TryParse(parseSize, out size))
                {
                    _currentText += text.Substring(i, 7 + parseSize.Length);
                    i += (7 + parseSize.Length) - 1;
                    symbolDetected = true;
                    tagOpened = true;
                    tagType = "size";
                }
            }

            // exit symbol
            for (var j = 0; j < _uguiCloseSymbols.Length; j++)
            {
                var symbol = string.Format("</{0}>", _uguiCloseSymbols[j]);
                if (text[i] == '<' && i + (2 + _uguiCloseSymbols[j].Length) < len && text.Substring(i, 3 + _uguiCloseSymbols[j].Length).Equals(symbol))
                {
                    _currentText += symbol;
                    i += (3 + _uguiCloseSymbols[j].Length) - 1;
                    symbolDetected = true;
                    tagOpened = false;
                    break;
                }
            }

            if (symbolDetected) continue;

            _currentText += text[i];
            label.text = _currentText + (tagOpened ? string.Format("</{0}>", tagType) : "");
            yield return new WaitForSeconds(speed);
        }

        _typeTextCoroutine = null;

        if (_onCompleteCallback != null)
            _onCompleteCallback();
    }

    private string ReplaceSpeed(string text)
    {
        var result = "";
        var len = text.Length;
        for (var i = 0; i < len; i++)
        {
            if (text[i] == '[' && i + 6 < len && text.Substring(i, 7).Equals("[speed="))
            {
                var speedLength = 0;
                for (var j = i + 7; j < len; j++)
                {
                    if (text[j] == ']')
                        break;
                    speedLength++;
                }

                i += 8 + speedLength - 1;
                continue;
            }

            result += text[i];
        }

        return result;
    }

    public bool IsSkippable()
    {
        return _typeTextCoroutine != null;
    }

    public void SetOnComplete(OnComplete onComplete)
    {
        _onCompleteCallback = onComplete;
    }

}

public static class TypeTextComponentUtility
{

    public static void TypeText(this Text label, string text, float speed = 0.05f, TypeTextComponent.OnComplete onComplete = null)
    {
        var typeText = label.GetComponent<TypeTextComponent>();
        if (typeText == null)
        {
            typeText = label.gameObject.AddComponent<TypeTextComponent>();
        }

        typeText.SetText(text, speed);
        typeText.SetOnComplete(onComplete);
    }

    public static bool IsSkippable(this Text label)
    {
        var typeText = label.GetComponent<TypeTextComponent>();
        if (typeText == null)
        {
            typeText = label.gameObject.AddComponent<TypeTextComponent>();
        }

        return typeText.IsSkippable();
    }

    public static void SkipTypeText(this Text label)
    {
        var typeText = label.GetComponent<TypeTextComponent>();
        if (typeText == null)
        {
            typeText = label.gameObject.AddComponent<TypeTextComponent>();
        }

        typeText.SkipTypeText();
    }

}
2、Main.cs
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public Text text;
    private Queue<string> scripts = new Queue<string>();

    public void Start()
    {
        scripts.Enqueue("[speed=0.3]大家好,[speed=0.1]我是<color=#EA5B44FF>新发</color>,是一名<color=#EA5B44FF>Unity3D游戏开发工程师</color>。");
        scripts.Enqueue("我从2015年开始在CSDN写博客,到现在有近[speed=0.2]<size=40>两万粉丝</size>啦。");
        scripts.Enqueue("感谢大家的[speed=0.3]<size=40>关注</size>与<b>支持</b>。");
        scripts.Enqueue("我会持续输出Unity[speed=0.3]干货文章[speed=0.1],希望可以帮助到想学Unity的同学,共勉!");
        scripts.Enqueue("好了,那么我们下一篇文章再见,[speed=0.4]<i>拜拜</i>。");

        ShowScript();
    }

    private void ShowScript()
    {
        if (scripts.Count <= 0)
        {
            return;
        }

        text.TypeText(scripts.Dequeue(), onComplete: () => Debug.Log("TypeText Complete"));
    }

    /// <summary>
    /// 对白被点击
    /// </summary>
    public void OnClickWindow()
    {
        if (text.IsSkippable())
        {
            // 跳过,直接显示全部文本
            text.SkipTypeText();
        }
        else
        {
            ShowScript();
        }
    }
}
实现打字效果需要使用TextMeshPro的Typewriter Effect功能,同时需要在脚本中读取文本框内的文字。具体实现过程如下: 1. 在Unity中创建一个UI文本框,使用TextMeshPro组件。在TextMeshPro组件的Inspector界面中找到Typewriter Effect选项,勾选开启。 2. 在脚本中获取文本框中的文字,可以通过如下代码实现: ```csharp using TMPro; public class TypeWriter : MonoBehaviour { public TextMeshProUGUI textMeshPro; private string text; void Start() { text = textMeshPro.text; textMeshPro.text = ""; } } ``` 在脚本中声明一个TextMeshProUGUI类型的公共变量textMeshPro,并在Start里将文本框中的文字赋值给text,并将文本框清空。 3. 实现Typewriter Effect功能。使用TextMeshPro组件的Animate方法,在每一帧中逐渐显示文字。同时需要保证每一帧显示的文字都是当前text的子字符串。代码实现如下: ```csharp void Update() { if (textMeshPro.text != text) { int length = textMeshPro.text.Length + 1; textMeshPro.text = text.Substring(0, length); textMeshPro.ForceMeshUpdate(); } } ``` 在Update方法中判断当前文本框中的文字是否与text相同,如果不同则将text中的子字符串逐渐显示,并调用ForceMeshUpdate方法立即更新UI。需要注意的是,字符串的长度需要每帧加一,实现逐字显示的效果。 4. 将脚本挂在UI文本框所在的GameObject上。在Inspector界面中将textMeshPro参数绑定到UI文本框的TextMeshPro组件上,即可实现打字效果并使用文本框中的文字。 以上是Unity TextMeshPro实现打字效果并引用文本框内的文字的步骤。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林新发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值