« 带娃玩桌游的一些记录 | 返回首页 | 球面地图网格的设计 »

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 筛选出当前国家内所有大陆性气候的星球,做一系列 ... 操作 。

Comments

我最后说一句scala的问题,在很早以前,上世纪80年代到90年代的美国,黑客使用晦涩难懂的perl脚本几乎颠覆了整个欧美和世界,那是一个提现个人编程能力的世界,perl脚本语言就是在当年那个操作系统环境,把个人的编程能力的表现力发挥到了极限。
计算机和互联网发展到现在,很多现成的解决方案和源代码垂手可得,那么编程语言的表现力这是高级程序员首选的因素,现在很多实验性的算法和源码都是先用python来写,然后再用C++或者C去重写,现在能够有可能替代Python的这种表现力的除了C++以外,我看就是julia和scala了,当然了还有perl6和Ruby也是很强的。

我在网上看到过一篇关于Aauto语言Lua的本质的文章,国内能达到这个水平的不多,能有这种对自己水平自知之名的态度的人更是很少,我个人猜测是你,个人观点。这帮人可是脸皮真够厚的,为了一点点虚荣和脸面,在2009年以前的E语言版本,竟然是故意伪造的,这个事就类似唐俊伪造斯坦福大学的学历一样,不但伪造了大学的,还伪造了小学到初中的简历,真是让人睁目结舌,然后哈哈大笑。

这件事肯恩对很多国内的程序员打击很大,因为闹的沸沸扬扬的易语言和国产Aauto语言竟然是骗子,很多人盯着骂我呢。这个我理解你们扎心的伤,但是这个事其实很简单就能依靠你的知识分析出来,如果aauto是Lua改写的,那么Lua语言是必须使用C语言编译器的,C语言编译后的程序无论是bin还是dll还是exe,是都具备标准的操作系统的信息的,这可以使用Dll分析工具检查出来,我检查的结果却是包含Delphi编译器的信息,如果是使用lua源码改写的,那么用该跟lua的语法是类似的,我从aauto的源码中看不到任何跟Lua语言有类似的东西,却看到了很多delphi的痕迹,一个人能写出delphi这种水平的东西吗?所以,结论就是,骗子,技术败类,他还在知乎大肆辱骂我,引以为戒。

另外,我强调一遍,Aauto和易语言,这是两个骗子,这两个语言的内核都是pascal和Delphi2009年的编译器,然后在外面加了一个Lua的外壳而已,这不是什么国产的语言,是骗子,跟汉芯和龙芯一样的货色,这些人是技术败类。

我总结了我近些年关于计算机的研究,
读万卷书不如行万里路(多实践),
行万里路不如高人指路,
高人指路不如身有天赋,
纵有天赋还要努力进步。

你要是有时间,关注一下scala这门语言,在很早以前我就听说过这个语言,一直没上心,后来我听说脸书和推特打算把后台的代码整体从Ruby迁移到scala,我这才买了本书看,这一看不得了,这门语言可不简单。
传统的结构化设计的观点是,代码的结构越简单越好,一段代码只解决一个问题,这样降低别人读取你源码的时间开销,这个在大规模项目中多人协作的模式下是对的。
scala的理念截然相反,尽量把多行代码表达的问题,几行代码处理掉,这样做的好处就是大幅压缩源码的编码开销,并且测试和维护也比较方便,标准的java程序表达一个问题需要1000行代码,那么C++只需要一半,scala最多使用300行就够了,这是两种不同的编程理念的竞争。
我挺喜欢这门语言的,从语言的角度比Ruby更强大,就是JVM限制了他的发展。

这三个分类也是当年java设计者的初衷,J2ee,J2se(现在的安卓的JDK),j2me(仅仅处理applet,其实就是个浏览器)

一个标准的计算机语言,能够完成的最基本的功能就是格式化显示,在早期的操作系统就是格式化显示需要的结果。早期图形是作为IO出现的,后来3D图形卡的崛起,芯片在内存中划出一块保护区域,作为显卡和CPU专用的通道,这部分是可以CPU和显卡和显存直接交换数据,而不需要操作系统的许可和访问的,
进阶的功能,那就是这门语言能够描述内存的数据并控制和存储数据,这个数据可以是类库或者类库生成的对象。
最后,就是这门语言可以描述自己本身,描述自己本身就附加描述操作系统内核了。目前能做到这个的只有C和C++还有ada

本地OS系统之上的文档格式化工具,你怎么设计都是没问题的,因为本地计算能力太强大,但是从目前主流的文档格式化工具,都有编程浏览器模型(CSS+HTML)的趋势。包括Atom和微软的Visual Studio Code都是使用CSS+html格式化内容。

是这样,我在很早以前,在你的博客上说要开发一门自己的新计算机语言,对这个事我深有研究。我们看到的所有的代码,都是通过CPU的L3送到L2再送到L1然后交给你寄存器去运算执行,然后由寄存器给出结果,以此交给L3,L3交给内存了就。那么堆栈机(比如JVM),堆栈机的瓶颈在于文件的IO速度,那么JS(其实JS就是C--,对比C++,JS是一个简化版本的C(这个事网景早在1993年就尝试过了,我在2006年自己写过一个标准JJC(精简C)文件),当然这不重要),JS的瓶颈在于网络(2005年之前),dom是一个浏览器格式化窗口的对象,一般来讲优秀的web端程序,比如PHP,JSP等等,把CSS放在文件开头,然后把资源,比如图片,内置对象,视频对象的链接等等放在中间,把JS放在文件最后,这样做的原因就是JS的执行的速度其实是远远高于中间的资源加载的。不知道网速变快以后会如何设计,反正目前是这样子。

偶然从muduo的书中了解这个博客,看的博文是http://blog.codingnow.com/2007/06/kiss.html,从2007年时的博客下面的留言中可以一窥那时互联网的风气,人们那时的表达语言很强烈,有很好组织语言的能力与讨论问题的态度,和今天很多帖子下面的留言形成鲜明对比。这个博客类似近代互联网的一个记录,我想如果能深入分析各个时期的评论会有很有意思的发现。支持博主继续将这个blog以这个风格做下去,会是一笔很宝贵的财富吧。

风格挺灰的

大佬 这个网站都有20年了吧我草

厉害

这个和Google的配置文件GCL有点像。词法上看起来像是JSON这样的纯数据格式,但是实际上是图灵完备的语言。

不过也不完全一样,GCL虽然在解释器的支持下可以被“运行”,但是GCL最终的目的还是作为一个数据格式,更具体地说,是作为配置文件。接收这个配置文件的程序,也许内嵌了GCL解释器,但还是把GCL当成一个非常普通的配置文件来看。而群星的配置文件,不只是一堆变量,完全就是游戏内逻辑的描述,只是写成了配置文件的形式。

好老的网站啊哈哈

Post a comment

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