Unity 游戲配置表代碼自動(dòng)生成術(shù)

說起游戲配置表,不論是游戲程序還是游戲策劃,都是每天都在打交道的、最普通不過的東西。

相信不少游戲程序員,擼過大量這樣的代碼:

class SkillSetting
{
    public int Id;
    public string Name;
    public string Desc;
    public int Arg1;
    public int Arg2;
    public int Arg3;
    // .....
    // .....
}

class SkillSettingManager
{
    // .....
    public void Init ()
    {
        var table = TableReader.Read("jineng.txt")
        for (var line = 0; line <= table.RowsCount; line++)
        {
            Id = table.GetRow(line, "id");
            Name = table.GetRow(line, "name");
            Desc = table.GetRow(line, "desc");
            Arg1 = table.GetRow(line, "arg1");
            Arg2 = table.GetRow(line, "arg2");
            Arg3 = table.GetRow(line, "arg3");
            // ......
        }
    }
}
// ....
// .....
// ......
class BuffSettingManager { .... }
class TaskSettingManager { .... }
class MissionSettingManager { .... }

// .... 接近上百個(gè)XXX SettingManager

相信不少游戲策劃,都遇到過這樣的配置表:

id name desc arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8
1 降龍十八掌 哼哼哈嘿 0 0 1 1 2 4 5 6
......

好吧。大家都或多或少遇過這些問題:

  • 大量的配置表代碼需要手工擼,枯燥繁雜
  • 大量的手工配置表代碼跟隨著大量的BUG
  • 策劃配置表跟程序代碼經(jīng)常命名不一
  • 策劃新人看不懂配置表的這一列究竟是干嘛的
  • 策劃同學(xué)想要更方便的工具提升工作體驗(yàn)
  • 配置表加載嚴(yán)重影響游戲啟動(dòng)速度
  • 運(yùn)營(yíng)需求對(duì)游戲配置表修改熱重載,手工代碼沒有支持
  • 配置表相關(guān)聯(lián)的功能和BUG消磨大量的研發(fā)、運(yùn)營(yíng)時(shí)間

嗯,多么痛的領(lǐng)悟。

接下來拋磚引玉,讓我們一起探討一種游戲配置表的優(yōu)化方案。

需求

編輯游戲配置表的用戶首要就是我們的策劃同學(xué)了,而策劃同學(xué)最喜歡最順手的工具非Excel(或WPS表格)莫屬了。 當(dāng)然了,也見過自己開發(fā)編輯器工具的,但我們畢竟不是全職做工具開發(fā),沒必要額外的增大工作量。
因此我們可以在Excel表格設(shè)計(jì)上,動(dòng)動(dòng)手腳。策劃同學(xué)有這樣的需求:

  • 配置表的列頭帶上注釋
  • 策劃同學(xué)可以在任意他們喜歡的地方做注釋
  • 策劃同學(xué)可以在他們的配置表中的添加文檔性東西如圖表、文字框
  • 有時(shí)候同樣的表,可以拆分成多張,方便編輯

拿到這樣的一份配置表后,程序同學(xué)有這樣的需求:

  • 希望配置表的代碼是可以自動(dòng)生成
  • 部分復(fù)雜邏輯的可以自定義擴(kuò)展
  • 為配置表的列定義類型

方案

編輯

針對(duì)這些需求,我們就有了這樣的這個(gè)結(jié)果:

Excel源表
  • 第一行是列名
  • 第二行是程序用途的類型聲明,如int, Dictionary<int, string>
  • 第三行是該列的注釋
  • 列名以#開頭,則這一列為注釋列(忽略該列單元格內(nèi)容忽略)
  • 行的第一個(gè)單元格內(nèi)容以#開頭,則這一行為注釋行(所有行單元格內(nèi)容忽略)
  • 可以加入圖表、第二張工作表作為輔助內(nèi)容

將這樣的一張表,保存為SettingSrc/Test.xls文件。

下面我們使用一個(gè)名為TableML的執(zhí)行程序,可以從https://github.com/mr-kelly/TableML/releases下載測(cè)試,并包含源碼和單元測(cè)試。

TableML.exe --Src SettingSrc --To Setting --CodeFile Code.cs

在SettingSrc目錄下執(zhí)行這個(gè)配置表編譯命令,會(huì)把所有的Excel文件,編譯成Tab分隔的表格純文本(TSV),并生成一個(gè)代碼文件Code.cs。
編譯后的TSV文件Test.tml純文本內(nèi)容,實(shí)質(zhì)就是剝離了注釋的Excel表格。

Excel表編譯結(jié)果

同時(shí)命令生成代碼:

/// <summary>
/// Auto Generate for Tab File: "Test.bytes"
/// Singleton class for less memory use
/// </summary>
public partial class TestSetting : TableRowFieldParser
{
        /// <summary>
        /// ID Column/編號(hào)/主鍵
        /// </summary>
        public string Id { get; private set;}
        
        /// <summary>
        /// Name/名字
        /// </summary>
        public I18N Value { get; private set;}

        // .............
}
public class TestSettings 
{
     IEnumerator GetAll()
     {
          // ...
     }
}

至此,程序同學(xué),把策劃同學(xué)的游戲配置表編譯優(yōu)化成純文本格式,生成的Code.cs文件也導(dǎo)入U(xiǎn)nity工程,使用TestSettings.GetAll來獲取所有的配置表內(nèi)容了。

多語(yǔ)言

標(biāo)注列的類型為I18N

既然表的第二行支持類型說明,那自然而然,我們可以標(biāo)記某一列是可以需要進(jìn)行翻譯的。比如,把這一列標(biāo)記成I18N,我們通過腳本,把所有帶有I18N列中的字符串進(jìn)行收集,自動(dòng)生成一個(gè)翻譯表;而生成的代碼中對(duì)應(yīng)I18N這個(gè)類,則對(duì)翻譯表進(jìn)行處理,來實(shí)現(xiàn)字符串的多語(yǔ)言翻譯。 細(xì)節(jié)不多敘述。

而在游戲做多語(yǔ)言版本的過程中,光字符串翻譯是不夠的,我們有些時(shí)候,不同的版本還有不同的游戲數(shù)值。鑒于我們的表編譯機(jī)制,我們可以加入一些類似預(yù)編譯指令的機(jī)制來處理:

預(yù)編譯指令

拆表

在很多時(shí)候,策劃同學(xué)喜歡將一個(gè)表的東西,分成多個(gè)文件來寫。比如有游戲的道具比較多,一般會(huì)分成SettingSrc/Item/Weapon.xls,SettingSrc/Item/Equipd.xls, SettingSrc/Item/Common.xls等多張表格,盡管他們是不同的文件,但是它們的列頭都是一樣的,這樣讓編輯起來更加的容易,而且多人編輯時(shí),不容易做成沖突。

在程序代碼中,它們也會(huì)被一個(gè)統(tǒng)一的ItemSettingsManager類讀取成統(tǒng)一的配置類型。

我們可以應(yīng)用之前的“#”符號(hào),來對(duì)他們的文件名修改一下:SettingSrc/Item/#Weapon.xls,SettingSrc/Item/#Equipd.xls, SettingSrc/Item/#Common.xls,這樣,來騙過編譯程序,再執(zhí)行剛才的編譯命令:

TableML --Src SettingSrc --To Setting --CodeFile Code.cs

這樣就會(huì)讓代碼生成器,忽略#號(hào)后面的字符串,把它們統(tǒng)一合并成ItemSettings類。

public class ItemSettings
{
    // 把三個(gè)表一起進(jìn)行加載
    public static readonly string[] TabFilePaths = {
       "Item/#Weapon.xls", "Item/#Equipd.xls", "Item/#Common.xls"
   };
   // ...
}

至于還有一些細(xì)節(jié)功能的,如自定義類型、自定義解析、自定義代碼模板(讓C++、Lua都支持)等擴(kuò)展代碼能力的功能,這里不多作解釋。具體的細(xì)節(jié)可以參考命令的源碼中的單元測(cè)試: GitHub: TableML,而一些邏輯細(xì)節(jié)也可以參考這里的文章。

常見問題

在TableML嘗試應(yīng)用的過程中,曾經(jīng)遇到過不少疑問:

考慮其它格式讓策劃同學(xué)編輯?如Lua、JSON?

考慮到策劃同學(xué)和其他同學(xué)的使用體驗(yàn)和習(xí)慣,Excel表格是他們最順手、功能強(qiáng)大的工具。

既然編譯,為什么不直接編譯成序列化的字節(jié)?

選擇編譯成純文本格式,更多出于工程考慮的,一考慮文本格式能對(duì)版本管理(如SVN)更友好,二考慮開發(fā)調(diào)試也更容易。性能方面,在我經(jīng)歷的項(xiàng)目上,對(duì)這種Tab分隔的表格文本解析,比序列化還要快。

我更喜歡自己擼,沒必要代碼自動(dòng)生成?

從程序習(xí)慣的角度來說,這種想法無(wú)可厚非,畢竟多年的開發(fā)習(xí)慣如此,而且自由度更高,寫起這些代碼來自然是舒暢的 。 但是從工業(yè)角度來想,人工寫出的代碼維護(hù)Bug成本,比自動(dòng)生成代碼維護(hù)成本要高。并且,為自動(dòng)生成的代碼添加功能(比如,運(yùn)行時(shí)動(dòng)態(tài)重載),只需要在生成代碼的代碼中稍微修改,就全局生效。

Excel文件怎么進(jìn)行版本比較?

在我看來這是一個(gè)非常關(guān)鍵的問題。這也是為什么Excel表被編譯成TSV純文本的一個(gè)重要原因。另外除了Excel表,TableML命令中還支持TSV翻譯到TSV,就是直接把Excel文件另存為TSV格式的配置表文件,再進(jìn)行編譯。
另外,Beyond Compare 4支持Excel文件的直接比較;TortoiseSVN中雙擊Excel文件,也會(huì)打開Excel文件顯示差異的地方(但較蹩腳)。

我項(xiàng)目的配置表都是自動(dòng)生成的,程序策劃沒意見、也挺智能的?

恭喜你們項(xiàng)目的質(zhì)量棒棒的,也希望一同分享您的方案!獨(dú)樂樂不如眾樂樂!

所以呢

說這么多,技術(shù)角度來說,自動(dòng)化的配置表編輯和加載,沒有什么技術(shù)含量,更多的是一種思想,但是我相信這對(duì)項(xiàng)目管理、規(guī)范化是起到積極的作用的,減少重復(fù)勞動(dòng)的時(shí)間,增加生產(chǎn)力。做游戲項(xiàng)目,消耗時(shí)間的,往往不是高深難解的問題,而是一些簡(jiǎn)單問題的重復(fù)輪回。

藉著本文拋磚引玉,也希望大家各抒己見,提出一些讓策劃、程序皆大歡喜的方法和技巧,讓更多更好的方案通過思想的交流碰撞出來,“節(jié)約更多時(shí)間,去陪戀人、家人和朋友 :) ”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容