Paradox 的數(shù)據(jù)文件格式
Paradox 是我很喜歡的一個(gè)游戲公司,在所謂 P 社 5 萌中,十字軍之王和鋼鐵雄心都只有淺嘗,但在維多利亞和群星上均投入了大量時(shí)間和精力。 這些游戲基于同一套引擎,所以數(shù)據(jù)文件格式也是共通的。P 社開(kāi)放了 Mod ,允許玩家來(lái)修改游戲,所以數(shù)據(jù)文件都是明文文本存放在文件系統(tǒng)中,這給了我們一個(gè)極好的學(xué)習(xí)機(jī)會(huì):對(duì)于游戲從業(yè)者,我很有興趣看看成熟引擎是如何管理游戲數(shù)據(jù)和游戲邏輯的。
據(jù)我所接觸到的國(guó)內(nèi)游戲公司,包括我們自己公司在內(nèi),游戲數(shù)據(jù)大都是基于 excel 這種二維表來(lái)表達(dá)的。我把它稱(chēng)為 csv 模式。這種模式的特點(diǎn)是,基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)基于若干張二維表,每張表有不確定的行數(shù),但每行有固定了列數(shù)。用它做基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的缺陷是很明顯的,比如它很難表達(dá)樹(shù)狀層級(jí)結(jié)構(gòu)。這往往就依賴(lài)做一個(gè)中間層,規(guī)范一些使用格式,在其上模擬出復(fù)雜數(shù)據(jù)結(jié)構(gòu)。
另一種在軟件行業(yè)廣泛使用的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)是 json/xml 模式。json 比 xml 要簡(jiǎn)單。它的特點(diǎn)就是定義了兩種基礎(chǔ)的復(fù)合結(jié)構(gòu),字典和數(shù)組,允許結(jié)構(gòu)嵌套。基于這種模式管理游戲數(shù)據(jù)的我也見(jiàn)過(guò)一些。不過(guò)對(duì)于策劃來(lái)說(shuō),編輯樹(shù)結(jié)構(gòu)的數(shù)據(jù)終究不如 excel 拉表方便。查看起來(lái)也沒(méi)有特別好的可視化工具,所以感覺(jué)用的人要少一些。
最開(kāi)始,我以為 P 社的數(shù)據(jù)文件是偏向于后一種 json 模式。但實(shí)際研究下來(lái)又覺(jué)得有很大的不同。今天我嘗試用 lpeg 寫(xiě)了一個(gè)簡(jiǎn)單的 parser 試圖把它讀進(jìn) lua vm ,寫(xiě)完 parser 后突然醒悟過(guò)來(lái),其實(shí)它就是基于的嵌套 list ,不正是 lisp 嗎?想明白這點(diǎn)后,有種醍醐灌頂?shù)母杏X(jué),的確 lisp 模式要比 json 模式簡(jiǎn)潔的多,并不比 csv 模式復(fù)雜。但表達(dá)能力卻強(qiáng)于它們兩者,的確是一個(gè)更好的數(shù)據(jù)組織方案。
我們來(lái)看一個(gè)從群星中隨便摘錄的例子(有點(diǎn)長(zhǎng),但挺有代表性):
- country_event = {
- id = primitive.16
- hide_window = yes
- trigger = {
- is_country_type = primitive
- has_country_flag = early_space_age
- #NOT = { has_country_flag = recently_advanced }
- OR = {
- AND = {
- exists = from
- from = {
- OR = {
- is_country_type = default
- is_country_type = awakened_fallen_empire
- }
- }
- }
- years_passed > 25
- }
- }
- mean_time_to_happen = {
- years = 100
- modifier = {
- factor = 0.6
- has_country_flag = acquired_tech
- }
- }
- immediate = {
- remove_country_flag = early_space_age
- set_country_flag = primitives_can_into_space
- set_country_type = default
- change_country_flag = random
- if = {
- limit = { is_species_class = MAM }
- set_graphical_culture = mammalian_01
- }
- if = {
- limit = { is_species_class = REP }
- set_graphical_culture = reptilian_01
- }
- if = {
- limit = { is_species_class = AVI }
- set_graphical_culture = avian_01
- }
- if = {
- limit = { is_species_class = ART }
- set_graphical_culture = arthropoid_01
- }
- if = {
- limit = { is_species_class = MOL }
- set_graphical_culture = molluscoid_01
- }
- if = {
- limit = { is_species_class = FUN }
- set_graphical_culture = fungoid_01
- }
- change_government = {
- authority = random
- civics = random
- }
- set_name = random
- if = {
- limit = {
- home_planet = {
- has_observation_outpost = yes
- }
- }
- home_planet = {
- observation_outpost_owner = {
- country_event = { id = primitive.17 }
- }
- }
- }
- add_minerals = 1000 # enough for a spaceport and then some
- add_energy = 500
- add_influence = 300
- capital_scope = {
- every_tile = {
- limit = {
- has_blocker = yes
- NOR = {
- has_blocker = tb_decrepit_dwellings
- has_blocker = tb_failing_infrastructure
- }
- }
- remove_blocker = yes
- }
- while = {
- limit = {
- num_pops < 8
- any_tile = {
- has_grown_pop = no
- has_growing_pop = no
- has_blocker = no
- }
- }
- random_tile = {
- limit = {
- has_grown_pop = no
- has_growing_pop = no
- has_blocker = no
- }
- create_pop = {
- species = owner
- }
- }
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_mineral_food_deposit
- set_building = "building_capital_2"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_mineral_deposit
- set_building = "building_mining_network_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_mineral_deposit
- set_building = "building_mining_network_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_farmland_deposit
- set_building = "building_hydroponics_farm_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_farmland_deposit
- set_building = "building_hydroponics_farm_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_energy_deposit
- set_building = "building_power_plant_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_energy_deposit
- set_building = "building_power_plant_1"
- }
- random_tile = {
- limit = {
- has_grown_pop = yes
- OR = {
- has_building = "building_primitive_farm"
- has_building = "building_primitive_factory"
- has_building = no
- }
- }
- clear_deposits = yes
- add_deposit = d_energy_deposit
- set_building = "building_power_plant_1"
- }
- remove_all_armies = yes
- create_army = {
- name = random
- owner = PREV
- species = owner_main_species
- type = "defense_army"
- }
- create_army = {
- name = random
- owner = PREV
- species = owner_main_species
- type = "defense_army"
- }
- create_army = {
- name = random
- owner = PREV
- species = owner_main_species
- type = "defense_army"
- }
- create_army = {
- name = random
- owner = PREV
- species = owner_main_species
- type = "defense_army"
- }
- }
- random_owned_ship = {
- limit = { is_ship_size = primitive_space_station }
- fleet = { destroy_fleet = THIS }
- }
- }
- }
起初,我很疑惑在這個(gè)格式中,為啥賦值和相等都用的 = ,這不是容易引起歧義么?但是你從 lisp 的角度來(lái)看就簡(jiǎn)單了。等于號(hào)只是為了便于策劃書(shū)寫(xiě)和閱讀的一個(gè)變形。所謂 id = primitive.16 你可以理解為 ( id, primitive.16 ) 而 iscountrytype = default 一樣可以理解為 ( iscountrytype , default ) 。 而
- create_army = {
- name = random
- owner = PREV
- species = owner_main_species
- type = "defense_army"
- }
本質(zhì)上是 ( create_army , ( ( name, random ) , (owner, PREV), (species, owner_main_species), (type, "defense_army") ) )。
基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)只要能表達(dá)出來(lái),怎么理解這些 list 是更上層的工作,這就和我們?cè)?csv 中去模擬樹(shù)結(jié)構(gòu)是一樣的道理。只不過(guò) years_passed > 25 這樣的東西,被翻譯成 ( years_passed, > , 25 ) 有三個(gè)元素。上層解析的時(shí)候,如果確定它是一個(gè)邏輯表達(dá)式,就很容易在 2 個(gè)元素的 list 中間插入一個(gè) = 補(bǔ)全。
這種結(jié)構(gòu)很容易描述一些控制結(jié)構(gòu),比如上面例子中的 if 。我還在其它數(shù)據(jù)中發(fā)現(xiàn)了 repeat while 等控制結(jié)構(gòu),這些都是上層的工作,和底層數(shù)據(jù)模型無(wú)關(guān)。但不得不說(shuō),lisp 模式比 csv 模式更容易做此類(lèi)控制結(jié)構(gòu)。
把這種數(shù)據(jù)結(jié)構(gòu)翻譯成 lua 也很容易:只需要用 lua table 的 array 來(lái)保存即可。但為了使用方便,可以加一個(gè)代理結(jié)構(gòu)。如果上層業(yè)務(wù)想把一個(gè) list 解析成字典,就在 cache 中臨時(shí)生成一個(gè) hash 表加快查詢(xún)即可。我們甚至可以把它直接存在 C 內(nèi)存中,只在 lua 中暴露出遍歷以及高層的訪(fǎng)問(wèn)方法。所謂高層的訪(fǎng)問(wèn)方法指,可以直接讀取 if repeat 等控制結(jié)構(gòu),或是把帶 AND OR 這樣的復(fù)合 list 直接翻譯成一個(gè)條件表達(dá)式。
原文鏈接:https://blog.codingnow.com/2017/07/paradox_data_format.html#more
【本文為51CTO專(zhuān)欄作者“云風(fēng)”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)51CTO聯(lián)系原作者獲取授權(quán)】






















