« August 2021 | Main | October 2021 »

September 18, 2021

Paradox 脚本语言的一点研究

因为一直给群星维护汉化 Mod 的缘故,我花了不少时间去理解 Paradox 的配置脚本语言。

我认为它用了类 lisp 的语法来描述数据。用数据去描述游戏逻辑。P 社的游戏都有玩家共建的 Wiki 提供了丰富的资源来帮助玩家创作 Mod 。学习它的脚本语言是非常容易的。

最近一段时间,我仔细阅读了 Wiki 中所有关于 Modding 的文章,相比之前零星的了解,算是做了一次系统的学习。

这套脚本语言还是以配置数据为主,但也提供了很多逻辑控制手段,很值得学习。

我以一个游戏程序员的角度去看,在此记录一些有启发的东西。当然,我自己并未用它开发完整的 Mod ,可能理解会有所偏差。

从游戏逻辑控制(官方称为 Dynamic modding )的角度,它定义了两个重要的概念:Effect 和 Condition 。它们均以数据列表的形式出现在脚本中,从词法上看,和其它静态数据并无不同。但结合上下文,它们是一段段类似通用语言中的语句段。

Effect 是一段顺序执行的语言段,它的运行伴随着游戏状态的变化;我们可以把一组 Effect 的语句段单独列出来,并起个名字,这被称为 scripted effect 。但我看来,它更像是通用语言中的(有副作用的)函数 。scripted effect 甚至还可以有参数,但没有返回值。

而 Condition 则可以看成是一组类型为布尔量表达式,它的值只能是 true 或 false ,且不会修改游戏的状态。组成 condition 的更小单元叫 trigger ,trigger 也可以被单独定义,被成为 scripted trigger 。它就像一个无副作用,布尔类型的函数。

把有副作用的命令式代码段 effect 和无副作用的条件代码段 condition 分离,可以极大的减少面条班的 if else 逻辑。

我猜想,引擎在实现的时候,还可以对 Condition 的结果做一些 cache ,避免在同一个 game tick 内重复运算。这对群星这种对象繁多、逻辑复杂的策略游戏是有很大的性能收益的。

例如,游戏中 event 的数据结构中,就组合了 effect 和 condition 。

country_event = {
    id = action.8
    hide_window = yes
    is_triggered_only = yes
    trigger = {
        is_country_type = default
        from = { is_country_type = default }
        NOT = { has_communications = from }
        is_hostile = from
    }
    immediate = {
        establish_communications = from
        fromfrom = {
            conquer = root
            set_controller = root
        }
    }
}

上面这个实例中,就演示了 action.8 这个事件会在 trigger 中定义的 condition 满足的时候,执行 immediate 中定义的 effect 。


第一次看到这段代码,可能很难理解。这里就需要了解另一个重要概念 scope 了。

群星里的对象是被组织在一个个 scope 中的,有 country sector system planet pop fleet ship leader 等等这样的 scope 。每层 scope 都会包含特定的对象以及若干 scope 。

比如星区 sector 这个 scope 中,包含了星区这个对象的各种属性外,还包含了星区内的所有星系 system scope 引用;同样,星系内也有所在星区的 scope 的引用。

这些 scope 如树状层次组织起来。同一个 scope 不只一条脉络。比如,你可以从 国家-星区-星系-星球-人口 最终这个人口的 scope ;同时也可以从 国家-派系-人口 找到它。同一个 scope 可以以不同的分类方式同时存在于多个树中。

每个定义好的 effect 或 trigger 都需要在特定类型的 scope 中运行。比如 habitability 宜居度就是一个只能在星球 planet 这个 scope 中运行的 trigger 。它可以探知某个 pop 在那个 planet 上的宜居度如何。

habitability = { who =  value = 0.6 }

这条就是说,在当前星球的 scope 下,针对 who 这个东西的宜居度是否为 0.6 。这里的 value = 0.6 非常有迷惑性,这里的等于号其实是一个比较操作符。上面这一条大致相当于这样一段 lua 代码:

_ENV:habitability(who, function(value) return value == 0.6 end)

我们从当前的环境中取到 habitability 这个函数,把当前环境和 who 这个目标传给它;计算出相对宜居度的 value ,交给 value == 0.6 去判断,得到一个布尔量的结果。

在很多语言中,我们需要写 a:foo(args) 这样去调用一个函数;但在这里不同,需要先进入 a 这个 scope ,再调用 foo ;也就是写成 a = { foo = args } 。

这样做的好处是,在一个 scope 中可以做很多和当前 scope 对象有关的事情,而不必反复的写 scope 本身。这有点像 C++ 中在成员函数中省略 this 的语法。

不过,这里还可以做的更多。比如:

pop = { planet = { habitability = { who = prev value > 0.6 } } }

这读作:判断人口所在星球相对该人口的宜居度是否大于 0.6 。

它可以看成是调用了 pop.planet.habitability 这个函数,只不过按该脚本的语法,我们需要用 pop = {} 和 planet = {} 进入两层 scope 。(再群星 3.0 的更新中,提供了 dot 语法做语法糖,简化过多 scope 层次切换的花括号)。

但和很多别的语言不同。如果是在 Lua 中,你写 pop.planet:habitability() 时,只有 pop.planet 这个对象传递给了 habitability 方法。这还是通过 : 这个语法糖实现的。如果你写 . 的话,这个信息也被扔掉了。

但是,此处却可以用 prev 引用到 pop ,也就是 planet 的上个 scope 层次。(同时也可以用 this 引用当前的层次。)

scope 的设计给了我不少的启发。除了 prev 可以引用前一个层次的 scope 外,还可以用 root 引用最外的层次,以及用 from 引用调用栈的上一个层次。有了这一系列的语法糖,很容易描述对象和对象之间的关系。相比而言,在传统的面向对象语言中,通常只提供一个 this/self 指代自己。而游戏中的大量逻辑是面向多个对象的:有战斗中的对手,有管理层次中的上下级,等等。


trigger 和 effect 段中都可以借助 scope 语法简化很多复杂业务的写法。

比如,可以在 country 的 scope 中写:

any_planet_within_border = { is_planet_class = pc_gaia }

就可以判断当前国家境内是否有至少一个盖娅星球。这个 any_ 的 trigger 会迭代该 scope 下所有的星球,对每个星球 scope 都执行 is_planet_class = pc_gaia 这个 trigger 条件。

还可以在写:

every_owned_planet = { limit = { is_planet_type = pc_continental } … }

用 limit 指定的 condition 筛选出当前国家内所有大陆性气候的星球,做一系列 ... 操作 。

September 13, 2021

带娃玩桌游的一些记录

云豆目前 7 岁,可可 4 岁半。我从三年前就不断尝试带娃玩桌游,实际能玩的下去的并不多。最近一段时间是云豆接受桌游的爆发期,很多游戏都玩得津津有味,非常值得记录一下。

首先是我认为 4~6 岁可以玩进去的游戏。这类游戏不多,具体和娃的天性相关很大。我在网上参考别家的娃爱玩的游戏尝试了一堆,大多是玩不下去的。核心问题是:孩子太小的话,很难理解 “规则” 这件事。许多游戏即使能玩,也是破坏了规则,玩个热闹而已。

在我家能重复玩的这类游戏有这么几个。

企鹅敲冰块:一些蓝色和白色的六边形塑料块,靠摩擦力相互卡住拜满棋盘,轮流敲掉一些,防止棋盘中心的企鹅掉下去。本来游戏还配有轮盘转的规则,对每次的敲法做出一些限制的,但娃太小的话很难遵循这些细则玩下去。

北极熊钓鱼 kayanak :用两张带洞洞的纸板夹住 A4 打印纸,架在纸盒上。纸盒里有很多钢珠,用一个木制钓鱼杆带上磁铁的鱼饵,捅破打印纸去钓下面的鱼。原本游戏配有骰子和北极熊的指示物在地图上移动,选择钓点。但实际操作时,娃太小很难理解这些规则。不过纯粹的捅破纸洞找鱼的乐趣就能满足他们了。

角斗士 Blokus :一些像俄罗斯方块一些的塑料片,交替放在棋盘格上,看谁放的更多。有正方形版的和三角形版的,玩起来都差不多。4 岁的娃不太能理解角对角的概念,但是类似拼图的玩法一个人玩也很嗨。

三只小猪 :飞行棋的升级版。基本还是投骰子走棋盘,不过多了个转盘环节可以抢走对手的道具。策略上也多了一点可以自由选择顺时针行动或逆时针行动。4 岁多的孩子能理解规则,但做正确的决策还有点困难。玩的过程还要反复提醒规则。大一点点后,这类游戏又显得毫无趣味,比较鸡肋。感觉只适合教育孩子:桌游都有内在的“规则”,需要按规则游戏。

Uno 优诺牌 :这个成人也可以玩。小孩多花点时间可以理解到规则,不过玩起来基本没什么章法。似乎也没觉得有太多乐趣。

拯救小羊 :就是记忆游戏。 6 个不同的塑料碗扣住一些小羊道具。依次投骰子来说出指定碗下面的小羊数量。对了就拿走一只(数量减少)。可可三岁的时候还玩不太懂,最近(四岁半)突然有一天就很喜欢玩这个,记忆力看起来还不错。

还试过一些叠叠乐之内的游戏好像都没什么兴趣再开,就不记录了。


云豆现在 7 岁,二年级,似乎突然间很多为成人设计的桌游都能玩进去了。会主动思考内在的策略。不过他的阅读文字和逻辑思维能力还不太行,所以还玩不了带文字阅读的游戏。我主要选的都是不带文字或仅有数字的。

有些传统适合低龄的桌游,如卡卡颂、卡坦岛、失落之城、火车票、我还没有在家里开过。这主要是因为以前开桌游店都买过,因为换城市都送人了,不太想又重新买一次,所以近年来尝试开的都是最近几年新出的游戏。

云豆曾经的最爱是 Azul ,中文译名叫做花砖物语。前段时间几乎每晚都要开两到三盘,即使没人配他玩,他也想自己一个人刷分,研究怎么拿高分(虽然游戏并没有单人模式)。Azul 在 bgg 上家庭游戏中排名挺高的,后来连着出了两款同系列的,一共三款。我开了其中两款:原版和最新的菱形块的(第三作)。感觉各有千秋。

原版的感觉更好一些,多人玩的对抗性很强。云豆已经几次认真思考过怎么击败对手这个问题。我也顺便教育了他,在游戏中重要的是在遵守规则的前提下,利用规则干扰对手的行动。而不要太考虑场外的因素:例如,“照顾对手的情绪,不去拿对手想要的牌”,这是对游戏的不尊重;“竞争的游戏需要多考虑相对得分,比对手少扣分就是胜利,而不是追求自己拿高分(而对手可能拿更高的分)”。

第三作玩起来更轻松一些,玩起来更和谐,竞争成分少一点。这些可可都能勉强理解规则,都是和哥哥玩都是被绝对压制,完全没有胜算,所以她也就不太愿意参加。

另外一款兄妹俩都能玩的游戏是 Chromino (骨米洛)。初玩的时候,似乎没有什么策略,但玩多了还是能感受到一些选择策略的。不过运气成分还是重了点。想明白其中一些策略后,哥哥对妹妹也是碾压的。这个云豆也连玩了一周,开了几十局。

Patchwork 拼布艺术,可可很喜欢。不过她偶尔不遵守规则,比如“不准移动之前摆好的牌” 这条她就不愿意遵守,总想把棋盘摆满。所以云豆不太愿意和她玩。我个人觉得游戏还是挺有趣的,只是不小心会得负分。把负数这个概念教给孩子还是很不容易的。

Kingdomino 多米诺王国。可可也比较喜欢,不过玩不过哥哥。算分对她来说非常困难,因为需要算一些基本乘法,她十以内的加减法还不行。云豆很快就学会了规则,但比起另外一些游戏,似乎不太喜欢。可能是因为一开始就需要舍弃一些看似很好的牌,否则太容易卡死了,最终得不了几分。

UBONGO 3d 乌邦果。这个云豆非常擅长。但限时和拿分的玩法感觉画蛇添足,单纯解题就够了。可可玩起来比较吃力,所以也就不爱玩了。这个云豆只在实在没人一起玩的时候才尝试一下。

Arboretum 树木园 。这个我的评价是高级版的失落之城,但策略点有点隐晦。云豆还比较喜欢,开了大约 4 局,他还不太能掌握其中的策略点。等他玩懂了后,再玩失落之城或是 Battle Line 这样的数字卡片游戏应该都没问题了。

五子棋。两个小孩我都尝试教了。可可还太小,除了基础规则外,完全不理解怎么才能赢得游戏、云豆玩了两天,大概下了 7,8 盘后开窍了。虽然还远比不上成人,但下起来也有模有样了。不过我旁观他和同学下五子棋,双方都还是在期盼对手犯错,自己捡漏赢得游戏。我和他讲,不能指望对方犯错自己就可以胜利,这个道理他现在不能理解。

教完五子棋后,我尝试对云豆启蒙了一下围棋。出乎我的意料,他很快就理解了围棋的基本规则。在 11 路棋盘上下的有点模样。很快就自己发现了不能贴着对手行棋,甚至还击败了他妈妈(她和娃一起了解的基础规则)。不过他还没能领略围棋的魅力,很快就被那些道具华丽的其他桌游把注意力吸引过去了。

云豆当下的最爱是 The Mind ,上个周末从周五晚上都周日全部在开这个游戏。我没有给他讲解过这个游戏该怎么玩,全靠他自己在不断游戏中慢慢理解。我和他两人局最多到 Level 11 ;带上奶奶的三人局最多到 Level 7 。

The Mind 这个游戏非常值得一提 ,中文名为心灵感应,是一个协作游戏。100 张卡片,分别写着 1 到 100 的数字。2 到 4 人游戏。第一轮(Level 1 )随机(洗乱)发给每个玩家一张卡,各自看过后准备开始游戏。

游戏过程中禁止任何语言交流、也不准有肢体暗示。大家只需要把手放在桌面上,由其中一个做主持宣布开始。如果谁感觉自己握有场上最小的数字卡就举手,然后翻出自己最小的那张。如果错误,集体扣一点血,同时把更小的卡弃掉;正确就继续,直到全部卡都正确翻开。

例如, A 玩家觉得自己手上的 20 是当下最小的卡,他抢先举手;如果 B 玩家和 C 玩家都没有比 20 小的卡,就顺利过关进入下一环节;如果 B 玩家手上有 18 ,比 20 小,此时就报告错误,把 18 正面朝上扔掉,并集体扣掉一张 life 卡。

如果过关(全部人打出全部卡,且 life 卡没有耗尽),就重新洗掉所有的牌,进入下一 level 。到 Level 2 的时候,每人随机抽 2 张,Level 3 时抽 3 张,以此类推。

游戏最关键的策略是 timing ,从开始令下后大家的沉默时长是最主要的信息,每个人都需要从中判断出场面的局势。另外,还有一张手里剑的卡可以提供额外的信息。任何人都可以在沉默时间内提出使用手里剑卡,如果全员同意,每个玩家都可以牌面朝上扔掉手中最小的数字卡给大家看,然后游戏继续;如果有一人不同意,游戏就重新准备、开始。

游戏被设计成完成某些特定 Level ,有额外奖励。或奖励 life 卡,或奖励手里剑卡。云豆在游戏时特别在意这些奖励卡,每次拿到都很开心。擅于用这些奖励也是完成游戏的必须。


另外,还有一些单人解题类桌游也挺有意思。不过这些只够玩一次,题目做出来就没什么意思了。

例如:

小鳄鱼洗澡的桌游版。可可解一题大约需要 10 分钟,超出这个时间她会失去耐心;云豆很喜欢,但是难度对他太低,一题只要 2 分钟左右,一个晚上就解决了全部题目。

多米诺迷宫。可可还玩不了,云豆则完全没问题。难度也适中。