Unity2D对话系统

概述

当玩家角色移动到NPC身边时,NPC头上会显示“R图标。

此时按下R键会弹出对话框,显示角色台词,并在对话框左侧显示当前说话角色的头像,再次按下R键显示下一句台词;当对话结束,按下R键则关闭对话框。

项目细节包括:

  • TextAsset文件中的文本显示在对话框中。

  • 使用协程让台词按顺序逐个字符显示。

  • 当台词还在展开过程中,提前按下R键,直接显示完整台词。

感谢麦扣老师:https://m-studio-m.github.io/


UI制作

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

特别的,项目的CanvasRenderMode设置为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++;
    }
}

上一篇
下一篇