Paradox是我非常喜欢的一家游戏公司。所谓的悖论5萌,十字军之王和钢铁雄心都只是浅尝辄止,但是维多利亚和星辰却投入了大量的时间和精力。这些游戏基于相同的引擎,因此数据文件格式也很常见。Paradox开放了Mod,允许玩家修改游戏,所以数据文件以纯文本形式存储在文件系统中,这给了我们一个极好的学习机会:对于游戏从业者来说,我很想看看成熟的引擎是什么如何管理游戏数据和游戏逻辑。据我接触过的国内游戏公司,包括我们自己公司,大部分游戏数据都是基于excel这样的二维表来表示的。我称之为csv模式。这种模式的特点是基本数据结构是基于若干个二维表,每个表的行数不确定,但每一行的列数固定。将其用作基本数据结构的缺点是显而易见的,例如难以表达树状的层次结构。这往往依赖于作为一个中间层,标准化一些使用格式,并在其上模拟复杂的数据结构。软件行业广泛使用的另一种底层数据结构是json/xml模式。json比xml简单。它的特点是定义了字典和数组两种基本的复合结构,并允许结构嵌套。我也见过一些基于这种模型管理游戏数据的。但是对于规划来说,编辑树状结构的数据终究不如在excel中拉表方便。没有特别好的查看可视化工具,所以感觉用的人比较少。一开始以为是P公司的数据文件偏向于后一种json方式。但是,实际研究发现,还是有很大区别的。今天尝试用lpeg写了一个简单的解析器,尝试读入luavm。写完解析器,突然发现其实是基于嵌套列表的,不就是lisp吗?赋能的感觉是lisp模式比json模式简洁很多,并不比csv模式复杂。但是表达能力比他们两个都强,确实是比较好的数据组织方案。我们来看一个从群星中随便摘录的例子(有点长,但坚持有代表性):country_event={id=primitive.16hide_window=yestrigger={is_country_type=primitivehas_country_flag=early_space_age=#recentry}NOT={AND={exists=fromfrom={OR={is_country_type=defaultis_country_type=awakened_fallen_empire}}}years_passed>25}}mean_time_to_happen={years=100modifier={factor=0.6has_country_flag=acquired_tech}}immediate={remove_country_flag=early_space_ageset_country_flag_country_change_set=intocounttypes_can=randomif={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=randomcivics=random}set_name=randomif={limit={home_planet={has_observation_outpost=yes}}home_planet={observation_outpost_owner={country_event={id=primitive.17}}}}add_minerals=1000#enoughforaspaceportandthensomeadd_energy=500add_influence=300capital_scope={every_tile={限制={has_blocker=yesNOR={has_blocker=tb_decrepit_dwellingshas_blocker=tb_failing_infrastructure}}remove_blocker=yes}while={limit={num_pops<8any_tile={has_grown_pop=nohas_growing_pop=nohas_blocker=no}}random_tile={limit={has_grown_pop=nohas_growing_blocker==no}create_pop={species=owner}}}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_mineral_food_depositset_building="building_capital_2"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_mineral_depositset_building="building_mining_network_1"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_mineral_depositset_building="building_tile"mining={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_farmland_depositset_building="building_hydroponics_farm_1"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_far""has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_farmland_depositset_building="building_hydroponics_farm_1"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_energy_depositset_building="building_power_plant_1"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd__=d_energy_depositset_building="building_power_plant_1"}random_tile={limit={has_grown_pop=yesOR={has_building="building_primitive_farm"has_building="building_primitive_factory"has_building=no}}clear_deposits=yesadd_deposit=d_energy_depositset_building="building_power_plant_1"}remove_all_armies={escreate_armyrandomowner=PREVspecies=owner_main_speciestype="defense_army"}create_army={name=randomowner=PREVspecies=owner_main_speciestype="defense_army"}create_army={name=randomowner=PREVspecies=owner_main_speciestype="defense_army"}创建te_army={name=randomowner=PREVspecies=owner_main_speciestype="defense_army"}}random_owned_ship={limit={is_ship_size=primitive_space_station}fleet={destroy_fleet=THIS}}}}一开始我很纳闷为什么要赋值和=来表示相等,不是很容易引起歧义吗?但是从lisp的角度来看,等号只是规划写作和阅读的一种变形。所谓id=primitive.16可以理解为(id,primitive.16),iscountrytype=default也可以理解为(iscountrytype,default)。而create_army={name=randomowner=PREVspecies=owner_main_speciestype="defense_army"}本质上是(create_army,((name,random),(owner,PREV),(species,owner_main_species),(type,"defense_army")))。只要能表达出基本的数据结构,如何理解这些列表是更高层次的工作,这和我们在csv中模拟树结构是一个道理。只是像years_passed>25这样的东西转换为(years_passed,>,25)三个元素。上层解析时,如果判断为逻辑表达式,很容易在2个元素的列表中间插入一个=补全。这种结构使得描述一些控制结构变得容易,比如上面例子中的if。我在其他数据中也发现了诸如repeatwhile之类的控制结构,这些都是上层的工作,与底层数据模型无关。但是不得不说lisp模式比csv模式更容易做这样的控制结构。把这个数据结构翻译成lua也很容易:用lua表的数组保存就行了。但是为了方便使用,可以添加一个代理结构。如果上层业务要将列表解析成字典,只需要在缓存中临时生成一个哈希表来加快查询速度即可。我们甚至可以直接存放在C内存中,只暴露lua中的遍历和高层访问方法。所谓高级访问方式,就是可以直接读取ifrepeat等控制结构,或者直接将一个ANDOR的复合列表翻译成条件表达式。原文链接:https://blog.codingnow.com/2017/07/paradox_data_format.html#more【本文为专栏作家“云峰”原创稿件,转载请联系原作者获得授权】点此查看看作者更多好文章
