說起游戲配置表,不論是游戲程序還是游戲策劃,都是每天都在打交道的、最普通不過的東西。
相信不少游戲程序員,擼過大量這樣的代碼:
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é)果:
- 第一行是列名
- 第二行是程序用途的類型聲明,如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表格。
同時(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)記某一列是可以需要進(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ī)制來處理:
拆表
在很多時(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í)慣的角度來說,這種想法無可厚非,畢竟多年的開發(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í)間,去陪戀人、家人和朋友 :) ”