SRPG游戏开发(四十九)第十章 游戏剧情 - 七 text命令执行器与文本界面 (text Command Exeuctor and UI Text Panel)

返回总目录

第十章 游戏剧情(Game Plot)

在大部分的RPG中,故事剧情是非常重要的。例如某些播放某些过场动画,人物台词等文字叙述的显示。这些可以推动整个游戏流程。

在Unity商店中,有一些剧情类的插件。我们编写的这个可以配合那些插件使用。



七 text命令执行器与文本界面 (text Command Exeuctor and UI Text Panel)

我们接下来进行的是text命令,但写命令之前,我们要先有UI能够显示我们的文本。

关于UIManager等问题,第六章的示例已经很好了,你也可以查看本章的源码,或者使用自己的UIManager。两者大同小异,不再阐述。


1 界面结构 (Panel Structure)

我们在这里规定,文本界面有三个显示固定文本窗口,用它们来显示文本:

  • 对话用屏幕上方;

  • 对话用屏幕下方;

  • 过场用屏幕下方(比对话用稍大)。

具体位置如 图 10.1 显示:

UI Text Panel

  • 图 10.1:UI Text Panel

我们的对话文本窗口包含:

  • 背景(Background);

  • 头像(Profile);

  • 文本背景(BackgroundInset);

  • 文本(Text);

  • 文本图标(Icon)(按键提示符);

而我们的过场用文本窗口包含:

  • 文本背景(BackgroundInset);

  • 文本(Text);

  • 文本图标(Icon)(按键提示符);

它们是雷同的,所以我们可以建立一个子UI,能省掉我们很多事。


2 子UI文本窗口(Sub UI Text Window)

首先要明确的是,子窗口的作用:用来显示文本。

而显示方式有多种,一般情况下:

  • 逐字显示;

  • 逐行显示;

  • 直接全部显示。

它们各有各的用处,主要是看游戏的需求。

根据结构,我们先创建一个UIBehaviour(它在UnityEngine.EventSystems命名空间下):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace DR.Book.SRPG_Dev.UI
{
    [AddComponentMenu("SRPG/UI/Text Window")]
    public class SubUITextWindow : UIBehaviour
    {
        #region UI Fields
        [SerializeField]
        private Image m_ImgBackground;
        [SerializeField]
        private Image m_ImgProfile;
        [SerializeField]
        private Image m_ImgBackgroundInset;
        [SerializeField]
        private Text m_TxtText;
        [SerializeField]
        private Image m_ImgIcon;
        #endregion

        #region UI Properties
        // 省略UI属性
        #endregion

        /// TODO 其它内容
    }
}
2.1 字段与属性(Fields and Properties)

我们这里选择 逐字写入 ,我们就需要一个写入速度,我们称它为写入间隔。而写入的过程,我们使用Coroutine(当然,你也可以使用Update())。

所以我们需要的字段:

  • 写入间隔;

  • 需要写入的文本;

  • 是否正在写入文本。

创建字段与属性:

        #region Fields
        [SerializeField]
        private float m_WordInterval = 0.05f;

        private string m_Text = string.Empty;
        private Coroutine m_WritingCoroutine = null;
        #endregion

        #region Properties
        /// <summary>
        /// 写入时每个字符的时间间隔
        /// </summary>
        public float wordInterval
        {
            get { return m_WordInterval; }
            set { m_WordInterval = Mathf.Max(0f, value); }
        }

        /// <summary>
        /// 是否在写入状态
        /// </summary>
        public bool isWriting
        {
            get { return m_WritingCoroutine != null; }
        }

        /// <summary>
        /// 获取应写入的完整文本
        /// </summary>
        public string text
        {
            get { return m_Text; }
        }
        #endregion
2.2 写入完成事件(Write Text Done Event)

我们在完成写入文本时,最好可以进行通知(例如通知我们的UITextPanel),这使用事件是一个好方法。

创建写入完成事件:

        #region Unity Event
        /// <summary>
        /// 当文本写入完成时:
        /// Args: 
        ///     SubUITextWindow textWindow; // 写入的窗口
        /// </summary>
        [Serializable]
        public class TextWriteDoneEvent : UnityEvent<SubUITextWindow> { }

        [Space, SerializeField]
        private TextWriteDoneEvent m_TextWriteDoneEvent = new TextWriteDoneEvent();

        /// <summary>
        /// 当文本写入完成时:
        /// Args: 
        ///     SubUITextWindow textWindow; // 写入的窗口
        /// </summary>
        public TextWriteDoneEvent textWriteDone
        {
            get
            {
                if (m_TextWriteDoneEvent == null)
                {
                    m_TextWriteDoneEvent = new TextWriteDoneEvent();
                }
                return m_TextWriteDoneEvent;
            }
            set { m_TextWriteDoneEvent = value; }
        }

        protected void OnTextWriteDone()
        {
            textWriteDone.Invoke(this);
            DisplayIcon(true);
        }
        #endregion

其中DisplayIcon(true);是显示文本图标,我们稍后补充。

2.3 异步写入文本(Write Text Async)

我们在写入开始之前:

  • 如果正在写入,需要停止正在写入的内容;(这应该不会发生,如果发生也许是一个错误)

  • 保存完整的文本;

  • 隐藏文本图标;

  • 清空m_TxtText组件的文本;

做完这些,我们就可以开始写入文本了,写入方法为IEnumerator WritingText()

创建方法:

        /// <summary>
        /// 逐字写入文本
        /// </summary>
        /// <param name="text"></param>
        public void WriteTextAsync(string text)
        {
            if (isWriting)
            {
                StopCoroutine(m_WritingCoroutine);
                m_WritingCoroutine = null;
            }

            m_Text = text;
            DisplayIcon(false);

            // Text组件不存在
            if (txtText == null)
            {
                OnTextWriteDone();
                return;
            }

            // 没有要写入的内容
            txtText.text = string.Empty;
            if (string.IsNullOrEmpty(text))
            {
                OnTextWriteDone();
                return;
            }

            m_WritingCoroutine = StartCoroutine(WritingText());
        }

WritingText是具体的写入过程,我们直接进行循环写入就可以了:

        private IEnumerator WritingText()
        {
            int index = 0; // 正在写入的下标
            string curText = string.Empty; // 当前Text组件文本
            while (txtText.text != m_Text)
            {
                yield return new WaitForSeconds(wordInterval);
                curText += m_Text[index++];
                txtText.text = curText;
            }

            OnTextWriteDone();
            m_WritingCoroutine = null;
        }

Tips:关于富文本(About Rich Text)

上面的方法没有对富文本处理,如果使用富文本,在逐字加载时会出现问题。
比如你想替换某些单词的颜色,使用<color=red>Text</color>,在逐字加载时:

  • <
  • <c
  • <co

这很不友好。

如果你对富文本的处理感兴趣,查看源码。源码包含了富文本的处理,使用的是正则表达式。

Unity支持的富文本格式:

  • <color=red>字体红色</color>:改变字体颜色;
  • <b>加粗</b>:加粗;
  • <i>斜体</i>:斜体;
  • <size=40>字体大小</size>:改变字体大小。

这样,我们就可以写入文本了。不过你还要知道,在一般游戏中,对话时按下按键后,可以不用等待,然后完全显示文本。

这还需要我们有一个方法:

        /// <summary>
        /// 如果正在写入,立即写入文本
        /// </summary>
        public void WriteText()
        {
            if (isWriting)
            {
                StopCoroutine(m_WritingCoroutine);
                m_WritingCoroutine = null;

                txtText.text = m_Text;
                OnTextWriteDone();
            }
        }
2.4 直接写入文本(Write Text Direct)

这个方法在SRPG或RPG中比较少见,而在J-AVG中比较常见。

  • 在多数RPG与SRPG中,一般都是某些操作直接跳过剧情(比如手柄select按键,键盘esc等),并不会直接显示文本;

  • 在J-AVG中,存在快进模式,对话显示一帧后直接跳过。

创建方法:

        /// <summary>
        /// 立即写入文本
        /// </summary>
        /// <param name="text"></param>
        public void WriteText(string text)
        {
            if (isWriting)
            {
                StopCoroutine(m_WritingCoroutine);
                m_WritingCoroutine = null;
            }

            SetText(text);
            OnTextWriteDone();
        }
2.5 显示与隐藏(Show and Hide)

这个不用多说,我们需要显示与隐藏的有:

  • 窗口

  • 按键提示符

创建方法:

        public void Display(bool open)
        {
            if (gameObject.activeSelf != open)
            {
                gameObject.SetActive(open);
            }
        }

        public void DisplayIcon(bool show)
        {
            if (imgIcon != null && imgIcon.enabled != show)
            {
                imgIcon.enabled = show;
            }
        }
2.6 Unity回调(Unity Callback)

正常境况下,打开窗口就应该开始写入文本,并且隐藏了光标。

但你在调试时有可能希望先打开UI,而且可能你在Inspector面板设置了Text的文本。

所以在打开窗口时,不要忘记初始化m_Text和隐藏我们的文本图标。

        #region Unity Callback
        protected override void Awake()
        {
            if (txtText != null)
            {
                m_Text = txtText.text;
            }
        }

        protected override void OnEnable()
        {
            DisplayIcon(false);
        }
        #endregion
2.7 按键提示符(Key Press Prompt)

按键提示符可以有多种方式:

  • 比如文本最后的三个.的数量不断变化;

  • 比如某些图标动画。

我这里只是让图标可以闪烁。而闪烁的方式也有许多:

  • GameObjectactiveSelf,根据计时器(或动画)切换truefalse

  • Imageenable,根据计时器(或动画)切换truefalse

  • Imagecolor,根据计时器(或动画)切换源颜色和透明色;

  • 还可以利用shader。

由于之前我在显示和隐藏时使用了enable,所以这里我使用color

using UnityEngine;
using UnityEngine.UI;

namespace DR.Book.SRPG_Dev
{
    public class Twinkle2D : MonoBehaviour
    {
        public Image m_Image;
        public float m_Interval = 0.5f;

        private Color m_OldColor;
        private bool m_Hide = false;
        private float m_Timer = 0f;

        private void OnEnable()
        {
            if (m_Image != null)
            {
                m_OldColor = m_Image.color;
            }
        }

        private void OnDisable()
        {
            m_Hide = false;
            m_Timer = 0f;

            if (m_Image != null)
            {
                m_Image.color = m_OldColor;
            }
        }

        private void Update()
        {
            if (m_Image == null || !m_Image.enabled)
            {
                return;
            }

            m_Timer += Time.deltaTime;
            if (m_Timer >= m_Interval)
            {
                m_Timer -= m_Interval;

                m_Image.color = m_Hide ? Color.clear : m_OldColor;
                m_Hide = !m_Hide;
            }
        }
    }
}

在按键提示符上挂载此组件。


3 文本界面(UI Text Panel)

关于文本界面,我们需要这几个窗口。

        private SubUITextWindow m_TopTextWindow;
        private SubUITextWindow m_BottomTextWindow;
        private SubUITextWindow m_GlobalTextWindow;

        public bool isWriting
        {
            get { return m_TopTextWindow.isWriting 
                    || m_BottomTextWindow.isWriting
                    || m_GlobalTextWindow.isWriting; }
        }

        #region Get Writing Window
        public SubUITextWindow GetWritingWindow()
        {
            if (m_TopTextWindow.isWriting)
            {
                return m_TopTextWindow;
            }

            if (m_BottomTextWindow.isWriting)
            {
                return m_BottomTextWindow;
            }

            if (m_GlobalTextWindow.isWriting)
            {
                return m_GlobalTextWindow;
            }

            return null;
        }
        #endregion

写入文本的工作是子窗口完成的,我们在主面板中,只需要调用它们。

不过你要注意,只有两个对话窗口可以同时显示。

  • 显示上窗口或下窗口时,全局窗口隐藏;

  • 显示全局窗口时,上窗口和下窗口隐藏。

        /// <summary>
        /// 立即写入文本
        /// </summary>
        public void WriteTextImmediately()
        {
            SubUITextWindow window = GetWritingWindow();
            if (window == null)
            {
                return;
            }

            window.WriteText();
        }

        /// <summary>
        /// 写入文本
        /// </summary>
        /// <param name="position"></param>
        /// <param name="text"></param>
        /// <param name="async"></param>
        public void WriteText(string position, string text, bool async)
        {
            SubUITextWindow textWindow;
            // 我这里用的字符串,你可以用Enum,这取决于你的文本执行器
            switch (position)
            {
                case "top":
                    m_GlobalTextWindow.Display(false);
                    textWindow = m_TopTextWindow;
                    break;
                case "bottom":
                    m_GlobalTextWindow.Display(false);
                    textWindow = m_BottomTextWindow;
                    break;
                default:
                    m_TopTextWindow.Display(false);
                    m_BottomTextWindow.Display(false);
                    textWindow = m_GlobalTextWindow;
                    break;
            }

            textWindow.Display(true);
            if (async)
            {
                textWindow.WriteTextAsync(text);
            }
            else
            {
                textWindow.WriteText(text);
            }
        }

最后,写一个隐藏按键提示符的方法:

        /// <summary>
        /// 隐藏按键提示符
        /// </summary>
        public void HideIcon()
        {
            m_TopTextWindow.DisplayIcon(false);
            m_BottomTextWindow.DisplayIcon(false);
            m_GlobalTextWindow.DisplayIcon(false);
        }

到现在为止,我们已经完成了文本界面的编写,我们之后来编写如何使用text命令与其UI配合。


4 text命令(text Command)

我们需要解析的命令如下:

text top            // 上对话文本窗口
    1               // 第一行 TextInfoConfig id == 1
    2;              // 第二行 TextInfoConfig id == 2

var customId = 2;
text bottom         // 下对话文本窗口
    customId;       // 第一行 TextInfoConfig id == customId

calc customId += 1;
text global         // 全局文本窗口
    1               // 第一行 TextInfoConfig id == 1
    customId;       // 第二行 TextInfoConfig id == customId

我们需要的参数为:

  • topbottomglobal:使用哪个窗口;

  • 12customId:文本在 TextInfoConfig 中的Id,如果是变量就先取值;

  • 是否异步:这个参数在脚本中没有显示,它是从游戏设置中读取。

创建执行器类与参数类:

using System.Text;

namespace DR.Book.SRPG_Dev.ScriptManagement
{
    using DR.Book.SRPG_Dev.Models;
    using DR.Book.SRPG_Dev.UI;

    public class TextExecutor : ScenarioContentExecutor<TextExecutor.TextArgs>
    {
        public struct TextArgs
        {
            public string position;
            public string text;
            public bool async;
        }

        public override string code
        {
            get { return "text"; }
        }

        // TODO ParseArgs and Run

    }
}
4.1 ParseArgs

我们转换参数已经做过很多,不再过多的说明。

创建方法:

        public override bool ParseArgs(IScenarioContent content, ref TextArgs args, out string error)
        {
            // text position text0 text1 ...;
            if (content.length < 3)
            {
                error = GetLengthErrorString();
                return false;
            }

            string position = content[1].ToLower();
            if (position != "top" && position != "bottom" && position != "global")
            {
                error = string.Format(
                    "{0} ParseArgs error: position must be one of [top, bottom, global].",
                    typeName,
                    content[1]);
                return false;
            }
            args.position = position;

            TextInfoConfig config = TextInfoConfig.Get<TextInfoConfig>();
            StringBuilder builder = new StringBuilder();

            int index = 2;
            while (index < content.length)
            {
                // 可能是个变量
                int id = -1;
                if (!ParseOrGetVarValue(content[index], ref id, out error))
                {
                    return false;
                }

                TextInfo info = config[id];
                if (info == null)
                {
                    error = string.Format(
                        "{0} ParseArgs error: text id `{1}` was not found.",
                        typeName,
                        content[index]);
                    return false;
                }

                builder.AppendLine(info.text);
                index++;
            }

            args.text = builder.ToString();

            /// 从游戏设置中读取
            /// 最常见的形式是类似J-AVG快进的形式
            args.async = true;

            error = null;
            return true;
        }
4.2 Run

在运行时,我们需要获取UI,使用UI来完成写入文本。

        protected override ScenarioActionStatus Run(IGameAction gameAction, IScenarioContent content, TextArgs args, out string error)
        {
            UITextPanel panel = UIManager.views.OpenView<UITextPanel>(UINames.k_UITextPanel, false);
            panel.WriteText(args.position, args.text, args.async);

            error = null;

            // 如果是快进模式,要等待一帧,否则有可能看不到界面,连闪屏都没有。
            return args.async ? ScenarioActionStatus.WaitWriteTextDone : ScenarioActionStatus.NextFrame;
        }

5 输入处理(Input)

我们还没有做输入处理,当我们按键时:

  • 正在写入:立即显示文本;

  • 写入完成:继续下一条命令。

这在ScenarioAction中完成:

        public override void OnMouseLButtonDown(Vector3 mousePosition)
        {
            if (status == ScenarioActionStatus.WaitWriteTextDone)
            {
                WriteTextDone();
                return;
            }
        }

        public void WriteTextDone()
        {
            if (status == ScenarioActionStatus.WaitWriteTextDone)
            {
                UITextPanel panel = UIManager.views.GetView<UITextPanel>(UINames.k_UITextPanel);
                if (panel.isWriting)
                {
                    panel.WriteTextImmediately();
                }
                else
                {
                    panel.HideIcon();
                    status = ScenarioActionStatus.Continue;
                }
            }
        }

至此,我们的text命令就全部完成了。

不过我们没有使用到SubUITextWindow中的写入完成事件textWriteDone,保留它是因为,将来你可能需要别的操作。例如源码中,在UITextPanel中使用MessageCenter发送一个写入完成消息,这个消息在源码中没有用到,只是展示了可以这么做;你也可以在每次写入完成时播放一个声音,提示玩家。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值