天天看點

SRPG遊戲開發(四十五)第十章 遊戲劇情 - 三 文本劇本 (Text Script)第十章 遊戲劇情(Game Plot)

傳回總目錄

第十章 遊戲劇情(Game Plot)

在大部分的RPG中,故事劇情是非常重要的。例如某些播放某些過場動畫,人物台詞等文字叙述的顯示。這些可以推動整個遊戲流程。

在Unity商店中,有一些劇情類的插件。我們編寫的這個可以配合那些插件使用。

文章目錄

  • 第十章 遊戲劇情(Game Plot)
    • 三 文本劇本 (Text Script)
      • 1 常量字元串(Constant String)
      • 2 字段與屬性(Fields and Properties)
      • 3 構造器(Constructor)
      • 4 格式化文本(Format Text)
        • 4.1 格式化指令(Format Command)
        • 4.2 格式化所有指令(Format Commands of Text)
      • 5 重建文本(Recreate Text)

三 文本劇本 (Text Script)

之前,我們已經介紹了關于劇本的内容。這一節我們來建立劇本類。

在這之前,我們規定将格式化方法(相當于非常縮水的詞法分析器(Lexical Analyzer),沒有按字元讀取分析,甚至都不完整)也放入之中。當然,你也可以隻儲存每條指令的字元串,在運作時再格式化。

關于詞法分析器(Lexical Analyzer)
  • 百度百科
  • Wikipedia

關于更複雜的文法分析器(Syntactic Analyzer)與詞法分析器(Lexical Analyzer)

如果你對它們感興趣,可以看一看“yacc”,“lex”,“bison”等分析器。

我們在上一節已經建立好了劇本類:

namespace DR.Book.SRPG_Dev.ScriptManagement
{
    /// <summary>
    /// 劇本(腳本)
    /// </summary>
    public class TxtScript : IScenario
    {
        /// <summary>
        /// 劇本的一條指令
        /// </summary>
        public class Command : IScenarioContent
        {
            // 省略
        }

        // TODO 劇本
    }
}
           

在這一節中,我們來添加内容進去。

大緻需要的程式集為:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; // 可能需要正規表達式,如果你熟悉它的話
           

1 常量字元串(Constant String)

我們的劇本規定了:

  • 每一條語句以

    ";"

    分割;
  • 語句參數之間使用

    [" ", "\t", "\n"]

    來分割;
  • 注釋使用

    "//"

  • 辨別符字首為

    "#"

則在

TxtScript

中建立:

#region Const/Static
        /// <summary>
        /// 用于指令的分割符
        /// </summary>
        public const string k_CommandSeparator = ";";

        /// <summary>
        /// 空格
        /// </summary>
        public const string k_Space = " ";

        /// <summary>
        /// 分隔符
        /// </summary>
        public const string k_Separator = "\t";

        /// <summary>
        /// 換行符
        /// </summary>
        public const string k_NewLine = "\n";

        /// <summary>
        /// 注釋字首
        /// </summary>
        public const string k_CommentingPrefix = "//";

        /// <summary>
        /// 預設劇本辨別字首
        /// </summary>
        public const string k_DefaultFlagMark = "#";
        #endregion
           

注意:我們在之前的Config中,換行符使用的是

Enviroment.Newline

,它與

"\n"

是有差別的。它的取值取決于你的平台,在Windows下,它表示

"\r\n"

,而在Unix Like系統下,它表示

"\n"

。在程式設計時選取

"\n"

是更好的選擇。Config中使用的并不多,你也可以進行更改。

2 字段與屬性(Fields and Properties)

我們的字段與屬性幾乎完全是按照

IScenario

來寫的,隻是我在這裡加了額外變量:

  • m_Buffer

    ,它用于存儲在格式化之前的文本,預留它可能會有用;
  • m_FlagMark

    ,它用于存儲目前的劇情辨別符,你可能已經将

    #

    作為了注釋,那麼它也要修改。
  • m_CommentingPrefix

    ,它用于存儲目前注釋字首,有可能你更喜歡用

    #

    表示注釋。

字段(Fields):

#region Fields
        private string m_Name;
        private string m_Buffer;
        private string m_FlagMark = k_DefaultFlagMark;
        private string m_CommentingPrefix = k_CommentingPrefix;
        private string m_Error = string.Empty;
        private readonly List<Command> m_Commands = new List<Command>();
        #endregion
           

屬性(Properties):

#region Properties
        /// <summary>
        /// 劇本名(可能為null)
        /// </summary>
        public string name
        {
            get { return m_Name; }
            private set { m_Name = value; }
        }

        /// <summary>
        /// 劇本的原始副本
        /// </summary>
        public string buffer
        {
            get { return m_Buffer; }
            private set { m_Buffer = value; }
        }

        /// <summary>
        /// 用作劇本辨別的符号
        /// </summary>
        public string flagMark
        {
            get { return m_FlagMark; }
            set { m_FlagMark = value; }
        }

        /// <summary>
        /// 注釋
        /// </summary>
        public string commentingPrefix
        {
            get { return m_CommentingPrefix; }
            set { m_CommentingPrefix = value; }
        }

        /// <summary>
        /// 錯誤
        /// </summary>
        public string formatError
        {
            get { return m_Error; }
            protected set { m_Error = value; }
        }

        /// <summary>
        /// 是否讀取過劇本文本
        /// </summary>
        public bool isLoaded
        {
            get { return !string.IsNullOrEmpty(m_Buffer); }
        }

        /// <summary>
        /// 内容(動作)
        /// </summary>
        protected List<Command> commands
        {
            get { return m_Commands; }
        }

        /// <summary>
        /// 指令數量
        /// </summary>
        public int contentCount
        {
            get { return m_Commands.Count; }
        }

        public IScenarioContent GetContent(int index)
        {
            return m_Commands[index];
        }
        #endregion
           

其中,

isLoaded

我使用了

!string.IsNullOrEmpty(m_Buffer)

,這是因為我規定了如果讀取成功,将會儲存進

m_Buffer

,否則

m_Buffer

不會有值。

由于我們儲存了原始副本(

m_Buffer

),是以我們順便更改一下

ToString()

方法:

public override string ToString()
        {
            if (!isLoaded)
            {
                return base.ToString();
            }
            return buffer;
        }
           

3 構造器(Constructor)

在構造器上,除了預設構造器,我們還應該可以更改“劇情辨別符”與“注釋字首”,因為每個人的習慣是不同的。

這些符号的使用習慣因人而異,我在這裡隻是可以更改兩個,你也可以添加你需要修改的參數。

比如,習慣Python的程式員可能更喜歡用

"#"

表示注釋,而指令分割使用

"\n"

建立構造器:

#region Constructor
        public TxtScript()
        {

        }

        public TxtScript(string flagMark, string commentingPrefix)
        {
            /// 防止 flagMark 有空格,
            /// 它可以有特殊字元,但不推薦含有特殊符号,
            /// 你也可以使用 Trim() 去除兩邊的特殊字元,
            /// 或使用 Regex.Replace(flagMark, @"\s", "") 去除所有特殊字元。
            /// \s 是正規表達式的比對符,包含任何空白的字元([" ", "\f", "\n", "\t", "\v"...])
            /// 這些限定不是必須的,也許你就喜歡有空格也說不定,
            /// 你在建立語言規則之前,應該考慮這些。
            if (flagMark != null)
            {
                flagMark = flagMark.Replace(" ", "");
            }

            // 防止 commentingPrefix 有空格。
            if (commentingPrefix != null)
            {
                commentingPrefix = commentingPrefix.Replace(" ", "");
            }

            if (!string.IsNullOrEmpty(flagMark))
            {
                m_FlagMark = flagMark;
            }

            if (!string.IsNullOrEmpty(commentingPrefix))
            {
                m_CommentingPrefix = commentingPrefix;
            }
        }
        #endregion
           

4 格式化文本(Format Text)

在我們的接口中,有格式化文本的方法

bool Load(string fileName, string scriptText);

這要求我們将文本字元串(String)格式化成指令(Command),而傳回是否成功。

  • 在格式化之前,我們先重置參數;
  • 格式化成功之後,再指派參數;
  • 格式化方法單獨建立一個函數。

通過接口建立方法:

public bool Load(string fileName, string scriptText)
        {
            string script = Regex.Unescape(scriptText).Trim();

            if (string.IsNullOrEmpty(script))
            {
                formatError = "TxtScript Load -> `scriptText` is null or empty";
                return false;
            }

            name = string.Empty;
            buffer = string.Empty;
            formatError = null;
            commands.Clear();

            bool loaded = FormatScriptCommands(script);
            if (loaded)
            {
                name = fileName;
                buffer = script;
            }

            return loaded;
        }
           

好了,這樣這個方法就确定了。

protected virtual bool FormatScriptCommands(string script)

是具體的格式化方法:

/// <summary>
        /// 按内容格式化劇本,不含行号與注釋
        /// </summary>
        /// <param name="script"></param>
        /// <returns></returns>
        protected virtual bool FormatScriptCommands(string script)
        {
            // TODO 格式化
            return true;
        }
           

按照以下步驟進行格式化:

  • 其一,按照

    [";"]

    分割劇本字元串;
  • 其二,按照

    [" ","\t","\n"]

    分割指令字元串,生成指令,并将指令添加到劇本中;

在進行代碼的編寫之前,我們先要注意一些問題:指令存在注釋,而且如果指令有多行,注釋可能穿插其中。

例如:

keycode arg0
    arg1    // 注釋1 注釋 注釋
    arg2;
           

注釋在指令中間,這就導緻我們不能直接分割,而需要分步。

我們先來準備子方法,第二步的格式化指令

FormatCommand

4.1 格式化指令(Format Command)

經過分析,我們要解決的主要問題是注釋。而解決它是要:

  • 先按

    ["\n"]

    分割指令字元串;
  • 去除每行注釋;
  • 再按

    [" ", "\t"]

    分割每行字元串;
  • 生成指令,并添加到劇本。

這樣,我們方法的參數就可以确定了。

還有一種情況需要注意,一條指令隻有

;

或隻有注釋,而沒有其它内容。是以我們的傳回值有三種情況:

  • 格式化成功,有内容;
  • 格式化失敗;
  • 格式化成功,但沒有内容。

為了這個結果,我們可以建立一個枚舉:

namespace DR.Book.SRPG_Dev.ScriptManagement
{
    /// <summary>
    /// 格式化結果
    /// </summary>
    public enum FormatContentResult
    {
        /// <summary>
        /// 成功
        /// </summary>
        Succeed,

        /// <summary>
        /// 失敗
        /// </summary>
        Failure,

        /// <summary>
        /// 隻有注釋
        /// </summary>
        Commenting
    }
}
           

了解它們後,我們進行編寫。

我們先來建立方法:

/// <summary>
        /// 格式化每一條内容
        /// </summary>
        /// <param name="index">下标,不是行号</param>
        /// <param name="commandText"></param>
        /// <param name="separators"></param>
        /// <param name="newLineSeparator"></param>
        /// <param name="command"></param>
        /// <returns></returns>
        protected virtual FormatContentResult FormatCommand(
            int index,
            string commandText,
            string[] separators,
            string[] newLineSeparator,
            out Command command)
        {
            ScenarioContentType type = ScenarioContentType.Action;
            List<string> arguments = new List<string>();
            
            // TODO 具體實作
            
            command = new Command(index, type, arguments.ToArray());
            return FormatContentResult.Succeed;
        }
           

然後按照以下步驟填充:

  • 其一,以

    ["\n"]

    分割指令字元串:
    // 按 ["\n"] 分割每一條内容
                string[] lines = commandText.Split(newLineSeparator, StringSplitOptions.RemoveEmptyEntries);
                for (int li = 0; li < lines.Length; li++)
                {
                    // TODO 對每一行格式化
                }
               
  • 其二,格式化每一行:
    • 首先,去除每一行的注釋:
      string line = lines[li].Trim();
      
                      // 删除每行注釋
                      int commentingIndex = line.IndexOf(commentingPrefix);
                      if (commentingIndex != -1)
                      {
                          line = line.Substring(0, commentingIndex).TrimEnd();
                      }
      
                      // 如果每行為空,則下一行
                      if (string.IsNullOrEmpty(line))
                      {
                          continue;
                      }
                 
    • 然後,以

      [" ","\t"]

      分割每行,并判斷是否是“劇情辨別符”:
      // 按 [" ","\t"] 分割每行
                      string[] lineValues = line.Split(separators, StringSplitOptions.RemoveEmptyEntries);
      
                      // 如果是辨別,如果arguments.Count不是0,則不是第一個參數
                      if (lineValues[0].StartsWith(flagMark) && arguments.Count == 0)
                      {
                          type = ScenarioContentType.Flag;
                      }
                 
    • 最後,添加參數:
      // 添加内容
                      for (int vi = 0; vi < lineValues.Length; vi++)
                      {
                          string value = lineValues[vi].Trim();
                          if (!string.IsNullOrEmpty(value))
                          {
                              arguments.Add(value);
                          }
                      }
                 
  • 其三,判斷傳回值
    // 隻有注釋
                if (arguments.Count == 0)
                {
                    command = null;
                    return FormatContentResult.Commenting;
                }
    
                 如果辨別參數大于1,則文法錯誤。
                 也可以不在這裡判斷,而在具體運作代碼時判斷。
                //if (type == ScenarioContentType.Flag && arguments.Count > 1)
                //{
                //    command = null;
                //    formatError = string.Format("TxtScript FormatError -> syntactic error: {0}", commandText);
                //    return FormatContentResult.Failure;
                //}
    
                command = new Command(index, type, arguments.ToArray());
                return FormatContentResult.Succeed;
               

4.2 格式化所有指令(Format Commands of Text)

我們之前,已經建立了格式化文本的方法:

/// <summary>
        /// 按内容格式化劇本,不含行号與注釋
        /// </summary>
        /// <param name="script"></param>
        /// <returns></returns>
        protected virtual bool FormatScriptCommands(string script)
        {
            // TODO 格式化
            return true;
        }
           

而現在,我們要進行填充:

  • 其一,按照

    [";"]

    分割字元串;
    // 以 [";"] 分割文本,并删除空白。
                string[] commandTexts = script.Split(
                    new string[] { k_CommandSeparator },
                    StringSplitOptions.RemoveEmptyEntries);
               
  • 其二,在生成指令之前,我們先來準備指令分隔符;
    // 分割劇本每個動作的分隔符: [" ","\t","\n"] 
                string[] separators = new string[] { k_Space, k_Separator };
                string[] newLineSeparator = new string[] { k_NewLine };
               
  • 其三,循環生成指令:
    for (int i = 0; i < commandTexts.Length; i++)
                {
                    // 删除左右空格和左右各種特殊轉義符
                    string commandText = commandTexts[i].Trim();
    
                    // 如果為空,下一個動作
                    if (string.IsNullOrEmpty(commandText))
                    {
                        continue;
                    }
    
                    // 格式化每一次動作,生成指令
                    Command command;
                    FormatContentResult formatResult = FormatCommand(
                        i, // 不是行号,是下标
                        commandText,
                        separators,
                        newLineSeparator,
                        out command);
    
                    // 成功添加
                    if (formatResult == FormatContentResult.Succeed)
                    {
                        commands.Add(command);
                    }
                    // 失敗傳回
                    else if (formatResult == FormatContentResult.Failure)
                    {
                        return false;
                    }
                    // 隻有注釋,下一動作
                    else
                    {
                        continue;
                    }
                }
               

5 重建文本(Recreate Text)

這是一個幫助方法,可以重新生成一個文本。

/// <summary>
        /// 重建立立文本,
        /// if `commandSeparator` == null: Enviroment.NewLine.
        /// 如果你需要每條指令貼在一起,傳入 `string.Empty`.
        /// </summary>
        /// <param name="commandSeparator"></param>
        /// <param name="withCommenting"></param>
        /// <returns></returns>
        public string RecreateText(string commandSeparator)
        {
            if (commandSeparator == null)
            {
                commandSeparator = Environment.NewLine;
            }
            string[] texts = commands.Select(cmd => cmd.ToString()).ToArray();
            return string.Join(commandSeparator, texts);
        }
           

繼續閱讀