概述

当玩家角色移动到NPC身边时,NPC头上会显示“R键”图标。
此时按下R键会弹出对话框,显示角色台词,并在对话框左侧显示当前说话角色的头像,再次按下R键显示下一句台词;当对话结束,按下R键则关闭对话框。
项目细节包括:
-
将TextAsset文件中的文本显示在对话框中。
-
使用协程让台词按顺序逐个字符显示。
-
当台词还在展开过程中,提前按下R键,直接显示完整台词。
感谢麦扣老师:https://m-studio-m.github.io/
UI制作

项目中使用Panel来充当文本框,在其下方再创建Text和Image游戏物体来承担文本和图像显示工作。

特别的,项目的Canvas的RenderMode设置为World Space,这样Panel就不会遮盖住整个屏幕。
因此,Event Camera设置为主摄像机。
为了让对话框显示在最上层,Order in Layer的值为10。
之后调整好UI位置即可。
将TextAsset中的文本显示在对话框中
这一节将用到TextAsset类型变量。

当我们把一个文本文件放到项目文件夹中后,它将被转换为TextAsset。通过读取TextAsset文件,就能访问到我们在文本文件中保存的信息。
这里的“文本文件”支持上面列出的格式,这里为了方便我们就用txt格式。

我们创建脚本DialogSystem,并将它挂载在Panel上。(可以根据需要,将它挂载其他物体上)

成员变量:
-
textLabel:文本框Panel下的Text物体。
- textFile:我们准备的txt文本文件。
-
index:当前显示的文本行数索引值。
-
textList:存放字符串的列表。
成员方法:
-
GetTextFromFile(TextAsset file):在该方法中,我们要将textFile文件中的文本按照换行符分割成句子,并将句子按每一句存放进textList列表。
-
Awake():在该方法中调用GetTextFromFile(TextAsset file)方法进行句子分割。
-
Update():判断玩家是否按下R键,若按下R键,则将textList中存放的元素赋值给textLabel,实现显示台词的功能。特别的,当index指向textLabel最后一项元素时,按下R键则关闭对话框。
-
OnEnable():当对话框被调出后,自动显示第一句话。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogSystem : MonoBehaviour
{
[Header("UI组件")]
public Text textLabel;
[Header("文本文件")]
public TextAsset textFile;
public int index;
private List<string> textList = new List<string>();
private void Awake()
{
//文本框就绪前将文本文件中的内容放进textList中
GetTextFromFile(textFile);
}
//当文本框就绪后,就显示第一条文本
private void OnEnable()//会比Start先调用
{
textLabel.text = textList[index];
index++;
}
private void Update()
{
//当按下R键,且最后一条文本已经展示完,则关闭对话框,重置index
if (Input.GetKeyDown(KeyCode.R) && index == textList.Count)
{
gameObject.SetActive(false);
index = 0;
return;
}
//当按下R键,显示下一条文本
if (Input.GetKeyDown(KeyCode.R) && textFinished)
{
textLabel.text = textList[index];
index++;
}
}
//将TextAsset文件按照每一行切割成字符串,添加进textList里
private void GetTextFromFile(TextAsset file)
{
textList.Clear();
index = 0;
//将TextAsset文件按照换行切割成字符串
//windows换行符是\r\n,再去掉空元素
string[] lineDate = file.text.Split(new char[] { '\r', '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lineDate)
{
textList.Add(line);
}
}
实现让台词按顺序逐个字符显示
这一节将介绍如何用协程来实现按顺序逐个字符显示台词。
我们修改脚本,并将显示说话人物的头像功能也顺便做了。

成员变量:
-
faceImage:文本框Panel下的Image物体。
-
textSpeed:文本框显示速度,每个字符显示中间的间隔时间。
-
textFinished:判断当前这一句台词是否显示完毕,若没有,则玩家按下R键不受理,不然会出现乱码。
成员方法:
-
IEnumerator SetTextUI():Update()方法中,会使用StartCoroutine(SetTextUI())开启协程,进入到SetTextUI()方法里;在SetTextUI()里,遇到yield return new WaitForSeconds(textSpeed);语句后等待textSpeed秒,在继续执行代码;同时切换说话人物头像的功能也在该方法中实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogSystem : MonoBehaviour
{
[Header("UI组件")]
public Text textLabel;
public Image faceImage;
[Header("文本文件")]
public TextAsset textFile;
public int index;
public float textSpeed;
public Sprite faceA, faceB;
private List<string> textList = new List<string>();
private bool textFinished;//当前这句话输出完了没
private bool cancelTyping;//是否结束逐个字符显示
private void Awake()
{
//文本框就绪前将文本文件中的内容放进textList中
GetTextFromFile(textFile);
}
//当文本框就绪后,就显示第一条文本
private void OnEnable()//会比Start先调用
{
StartCoroutine(SetTextUI());
}
private void Update()
{
//当按下R键,且最后一条文本已经展示完,则关闭对话框,重置index
if (Input.GetKeyDown(KeyCode.R) && index == textList.Count)
{
gameObject.SetActive(false);
index = 0;
return;
}
//当按下R键,显示下一条文本
//只有该条文本显示完毕后才能显示下一条,不然会乱码(用了协程)
if (Input.GetKeyDown(KeyCode.R) && textFinished)
{
StartCoroutine(SetTextUI());//协程
}
}
//将TextAsset文件按照每一行切割成字符串,添加进textList里
private void GetTextFromFile(TextAsset file)
{
textList.Clear();
index = 0;
//将TextAsset文件按照换行切割成字符串
//windows换行符是\r\n,再去掉空元素
string[] lineDate = file.text.Split(new char[] { '\r', '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lineDate)
{
textList.Add(line);
}
}
//让文本框显示让下一条文本的每个字符按先后顺序显示
//并显示说话角色的头像图片
IEnumerator SetTextUI()
{
textFinished = false;
//因为下面textLabel的文本是累加每个字符,因此每句话之前要先清空
textLabel.text = "";
//切换文本框说话人物的图片
switch (textList[index])
{
case "A":
faceImage.sprite = faceA;
index++; //字符A、B只用来判断该显示什么图片,不显示在文本中
break;
case "B":
faceImage.sprite = faceB;
index++;
break;
}
//让一句话中的每个字符按顺序先后显示
for (int i = 0; i < textList[index].Length; ++i)
{
textLabel.text += textList[index][i];
yield return new WaitForSeconds(textSpeed);//等待textSpeed秒后继续
}
textFinished = true;
index++;
}
}
直接显示完整台词
这一节将介绍当台词还在展开过程中,提前按下R键,直接显示完整台词的方法。
我们直接修改之前的代码,添加一个bool类型变量cancelTyping。
并在Update()方法中添加判断
-
当玩家按下R键,如果textFinished为true,说明当前台词没有在打印中,直接启用协程打印下一句台词。
-
如果textFinished为false,说明当前台词正在打印,则将cancelTyping置为true。
在协程函数SetTextUI()方法里,若检测到cancelTyping为true,则跳出打印循环,将目前textList元素(字符串)直接赋值给textLabel就实现了该功能。
为了方便“跳出打印循环”操作,我们将之前的for循环修改为while循环。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogSystem : MonoBehaviour
{
[Header("UI组件")]
public Text textLabel;
public Image faceImage;
[Header("文本文件")]
public TextAsset textFile;
public int index;
public float textSpeed;
public Sprite faceA, faceB;
private List<string> textList = new List<string>();
private bool textFinished;//当前这句话输出完了没
private bool cancelTyping;//是否结束逐个字符显示
private void Awake()
{
//文本框就绪前将文本文件中的内容放进textList中
GetTextFromFile(textFile);
}
//当文本框就绪后,就显示第一条文本
private void OnEnable()//会比Start先调用
{
//textLabel.text = textList[index];
//index++;
StartCoroutine(SetTextUI());
}
private void Update()
{
//当按下R键,且最后一条文本已经展示完,则关闭对话框,重置index
if (Input.GetKeyDown(KeyCode.R) && index == textList.Count)
{
gameObject.SetActive(false);
index = 0;
return;
}
////当按下R键,显示下一条文本
////只有该条文本显示完毕后才能显示下一条,不然会乱码(用了协程)
//if (Input.GetKeyDown(KeyCode.R) && textFinished)
//{
// //textLabel.text = textList[index];
// //index++;
// StartCoroutine(SetTextUI());//协程
//}
//按下R键后进行判断
//如果显示下一条文本的动作已就绪,则开始逐个字符显示句子
//如果正在逐个字符显示句子,则将cancelTyping设置为true——
//表示取消逐个字符显示,直接显示该条所有字符
if(Input.GetKeyDown(KeyCode.R))
{
if(textFinished && !cancelTyping)
{
StartCoroutine(SetTextUI());
}
else if(!textFinished && !cancelTyping)
{
cancelTyping = true;
}
}
}
//将TextAsset文件按照每一行切割成字符串,添加进textList里
private void GetTextFromFile(TextAsset file)
{
textList.Clear();
index = 0;
//将TextAsset文件按照换行切割成字符串
//windows换行符是\r\n,再去掉空元素
string[] lineDate = file.text.Split(new char[] { '\r', '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
foreach(string line in lineDate)
{
textList.Add(line);
}
}
//让文本框显示让下一条文本的每个字符按先后顺序显示
//并显示说话角色的头像图片
IEnumerator SetTextUI()
{
textFinished = false;
//因为下面textLabel的文本是累加每个字符,因此每句话之前要先清空
textLabel.text = "";
//切换文本框说话人物的图片
switch(textList[index])
{
case "A":
faceImage.sprite = faceA;
index++; //字符A、B只用来判断该显示什么图片,不显示在文本中
break;
case "B":
faceImage.sprite = faceB;
index++;
break;
}
//让一句话中的每个字符按顺序先后显示
//for(int i=0;i<textList[index].Length;++i)
//{
// textLabel.text += textList[index][i];
// yield return new WaitForSeconds(textSpeed);//等待textSpeed秒后继续
//}
//让一句话中的每个字符按顺序先后显示
//若cancelTyping为true,则跳出,显示该条文本所有字符
int letter = 0;
while (!cancelTyping && letter < textList[index].Length)
{
textLabel.text += textList[index][letter];
letter++;
yield return new WaitForSeconds(textSpeed);//等待textSpeed秒后继续
}
if (cancelTyping)
{
textLabel.text = textList[index];
cancelTyping = false;
}
textFinished = true;
index++;
}
}