« 浅谈《守望先锋》中的 ECS 构架 | 返回首页 | skynet 1.1 »

Paradox 的数据文件格式

Paradox 是我很喜欢的一个游戏公司,在所谓 P 社 5 萌中,十字军之王和钢铁雄心都只有浅尝,但在维多利亚和群星上均投入了大量时间和精力。

这些游戏基于同一套引擎,所以数据文件格式也是共通的。P 社开放了 Mod ,允许玩家来修改游戏,所以数据文件都是明文文本存放在文件系统中,这给了我们一个极好的学习机会:对于游戏从业者,我很有兴趣看看成熟引擎是如何管理游戏数据和游戏逻辑的。

据我所接触到的国内游戏公司,包括我们自己公司在内,游戏数据大都是基于 excel 这种二维表来表达的。我把它称为 csv 模式。这种模式的特点是,基础数据结构基于若干张二维表,每张表有不确定的行数,但每行有固定了列数。用它做基础数据结构的缺陷是很明显的,比如它很难表达树状层级结构。这往往就依赖做一个中间层,规范一些使用格式,在其上模拟出复杂数据结构。

另一种在软件行业广泛使用的基础数据结构是 json/xml 模式。json 比 xml 要简单。它的特点就是定义了两种基础的复合结构,字典和数组,允许结构嵌套。基于这种模式管理游戏数据的我也见过一些。不过对于策划来说,编辑树结构的数据终究不如 excel 拉表方便。查看起来也没有特别好的可视化工具,所以感觉用的人要少一些。

最开始,我以为 P 社的数据文件是偏向于后一种 json 模式。但实际研究下来又觉得有很大的不同。今天我尝试用 lpeg 写了一个简单的 parser 试图把它读进 lua vm ,写完 parser 后突然醒悟过来,其实它就是基于的嵌套 list ,不正是 lisp 吗?想明白这点后,有种醍醐灌顶的感觉,的确 lisp 模式要比 json 模式简洁的多,并不比 csv 模式复杂。但表达能力却强于它们两者,的确是一个更好的数据组织方案。

我们来看一个从群星中随便摘录的例子(有点长,但挺有代表性):

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 }
        }
    }
}

起初,我很疑惑在这个格式中,为啥赋值和相等都用的 = ,这不是容易引起歧义么?但是你从 lisp 的角度来看就简单了。等于号只是为了便于策划书写和阅读的一个变形。所谓 id = primitive.16 你可以理解为 ( id, primitive.16 ) 而 iscountrytype = default 一样可以理解为 ( iscountrytype , default ) 。 而

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") ) )

基础数据结构只要能表达出来,怎么理解这些 list 是更上层的工作,这就和我们在 csv 中去模拟树结构是一样的道理。只不过 years_passed > 25 这样的东西,被翻译成 ( years_passed, > , 25 ) 有三个元素。上层解析的时候,如果确定它是一个逻辑表达式,就很容易在 2 个元素的 list 中间插入一个 = 补全。

这种结构很容易描述一些控制结构,比如上面例子中的 if 。我还在其它数据中发现了 repeat while 等控制结构,这些都是上层的工作,和底层数据模型无关。但不得不说,lisp 模式比 csv 模式更容易做此类控制结构。

把这种数据结构翻译成 lua 也很容易:只需要用 lua table 的 array 来保存即可。但为了使用方便,可以加一个代理结构。如果上层业务想把一个 list 解析成字典,就在 cache 中临时生成一个 hash 表加快查询即可。我们甚至可以把它直接存在 C 内存中,只在 lua 中暴露出遍历以及高层的访问方法。所谓高层的访问方法指,可以直接读取 if repeat 等控制结构,或是把带 AND OR 这样的复合 list 直接翻译成一个条件表达式。


8 月 8 日补充:

我实现了一个简单的 lua parser : https://github.com/cloudwu/pdxparser

Comments

果然还是P社游戏在逻辑上能达到如此的复杂程度,以至于需要独树一帜的语法。当然我觉得两者也是互相促进的,有了这种语法也能鼓励策划和程序员将整个系统开发的更加复杂,有更多的条件判断。 我觉得这就是p社游戏相比其他游戏极为重要的特殊之处,有着特别丰富且很多藏得很深的变化和逻辑,尤其像eu4这种老怪物项目。很多其他游戏都难以在成体系的开发和持续更新中保持这种风格。
果然还是P社游戏在逻辑上能达到如此的复杂程度,以至于需要独树一帜的语法。当然我觉得两者也是互相促进的,有了这种语法也能鼓励策划和程序员将整个系统开发的更加复杂,有更多的条件判断。 我觉得这就是p社游戏相比其他游戏极为重要的特殊之处,有着特别丰富且很多藏得很深的变化和逻辑,尤其像eu4这种老怪物项目。很多其他游戏都难以在成体系的开发和持续更新中保持这种风格。
支持,这里面大部分逻辑就应该让策划去亲手掌控,可惜这种程序化的结构描述即使是大厂的策划,能驾驭的也不多吧。
哈哈居然看到了p社相关的内容 年初自己想做一个可视化的事件链浏览工具,尝试写了一个解析器,在处理等号和列表控制逻辑的时候确实遇到了困难,没学过lisp所以一直比较困惑这种模式 虽然最终还是写出来了,但现在看起来还有很多优化的地方
仔细一看,这就是一颗语法树啊。哈哈。
lisp 应该是 (= name random ),象这种需求这么大,应该开发一种,象网页处理的语言,可以 excel 处理数据,可以生成和处理逻辑,最后生成统一格式。我也在处理 word 文件,也遇到同样的问题,现在只能word + csv = 批量文件,遇到if else 一点都不好处理。
策划恨不得躺着把表填完,这么复杂的东西还是留给程序员吧
这个有点像linux kernel的devices tree的结构。
神海里也是用scheme作为DSL配置数据和生成代码
HOCON 还是基于 dict 的,不要被表象欺骗。 用 = 还是 : 都是很次要的。
这个有点像 HOCON 格式
这并不是 dict 而是 list ,这里是有很大区别的。
@dwing 同意,感觉不如lua更清晰。而且同样无法拉表啊。
@dwing 同意,感觉不如lua更清晰。而且同样无法拉表啊。
这就是把逻辑写成类似json的结构,结构倒是简单了,但不如直接写lua看起来直观清晰,而且描述纯数据跟json没什么区别,还是没法"拉表". 与其单独设计这么一套"语言",不如就直接用lua语法好了,也支持树形数据结构,需要"拉表"就用转换工具把excel转换成lua table.

Post a comment

非这个主题相关的留言请到:留言本