一、前言
点关注不迷路,持续输出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();
}
}
}