星际争霸2编辑器的初接触
最近在接手改进我们的怪物 AI 的底层模块。这部分策划是希望可以由他们来直接配置而不是提交需求文档让程序实现。
我们的前一个版本有较大的性能问题,光是空跑一些场景,没有玩家的情况下 CPU 符合都相当之高了。我觉得是允许策划的配置项过于细节,以及底层模块实现的方式不对,导致了大量无用的 lua 脚本空转导致的。
目前的 AI 脚本是每个挂在独立的 NPC 上,利用心跳(大约是 0.5s 一次),定期让 NPC 去思考现在应该去干些什么。这些干些什么的具体逻辑在很细节的层面都是要去运行一个策划配置的脚本在一个沙盒中运行的。在实际监测中,一个心跳的一段 AI 脚本居然会跑上万行 lua 代码,想不慢都难啊。
游戏开发和很多其他软件开发的一个巨大区别就是,你无法把程序得到正确结果当成任务的完成。运行时间往往成为重要的约束条件。如果一件事情在规定的时间片执行不完,代码实现的再正确都没有意义了。而局部的优化热点往往也意义不大。因为如果只是需要小幅度的提高性能,那么采购好一些的硬件就够了。一个模块的性能,要从数量级上的提高,必须重新思考需求,改变需求,重定义我们要做什么。
我决定看看星际争霸2 的地图编辑器是如何工作的。
我没有玩过魔兽争霸3 的编辑器,也没有玩过星际 2 的。但似乎,它们可以让用户自定义出各种形式的游戏来,并不局限在 RTS 一种类型中。我相信这个发展了超过十年的自定义地图的编辑模式,一定有很多成熟的业务抽象。
有限的时间内,我没有从网上找到太多的相关资料。在暴雪的官方网站也没能看到完整的文档。星际2 的编辑器内建了一个叫做银河(Galaxy)的脚本语言,似乎所有 GUI 界面上的编辑器操作,都可以完整的对应成一段 galaxy 脚本。很可惜的是,暴雪似乎鼓励玩家用 GUI 编辑器创造地图,而脚本只是背后之物,galaxy 的手册我并没有找到。
我只好自己把弄编辑器,在自己的使用中,推想暴雪解决问题的思路。短短两天的研究肯定会有许多错误和遗漏,也希望借公开的 blog ,有行家看到能够赐教。
简单而言,自定义星际2 的地图,编写控制脚本的人是以上帝视角来看待世界的。脚本并不单独挂在单个 NPC 单位上。这和我们之前的设计很不一样。
自定义游戏,是由数据加代码构成的。数据包括了地形数据,放置在地图上的单位,还有路径、点、区域等等类型的对象构成。这些可以很方便的用 GUI 编辑器生成。后面在代码中引用。
而代码,是由一个个 Trigger 构成的。官方翻译为触发器。每张地图有若干 Trigger ,每个都有全局的名字,平坦的存在于地图的数据结构中。从编辑器角度看,这些 Trigger 必须一开始全部实现好,在地图加载时加载到内存中。后续代码能做的事情只是开启或关闭一些 Trigger。
从脚本角度看,Trigger 似乎是一个动态对象,可以动态生成,而不是一段静态的代码。但是编辑器里似乎做不到动态创建 Trigger ,看起来暴雪也不鼓励这种动态创建过程。
Trigger 由事件(Event) 条件(Condition) 动作(Action) 局部变量四部分构成。
Event 全部是可以枚举出来的东西,不存在自定义事件这种东西。所以在编辑器里可以通过菜单选取。暴雪在编辑器设计上下了一番功夫。所有的事件都有一个英文短词用于脚本方法的定义,同时有一个长句子用来显示在编辑器选单中。根据使用者的语言,可以配置为比如中文的。另还有一段长说明,方便使用者了解用途。
Event 可以说全部是全局事件,比如单位死亡,单位受到攻击,单位进入区域等等,可以指定一个对象,也可以监控一类或全部对象。UI 事件也被纳入同一体系,比如有 UI 按钮被按下这种事件。
Event 可以说是 Trigger 的过滤器,系统知道什么时候该考虑哪些 Trigger 可能需要执行。
Trigger 是否被触发还需要看条件是否被满足,这就要看 Condition 的配置了。如果你需要刺蛇被攻击时做一些什么,也可以定义 Event 为任意单位被攻击,再在条件里写上单位类型为刺蛇。
Action 就是需要完成的一系列事情了。这些事情都是瞬发的,比如把单位移到你定义好的点,删除单位等等。
不同的 Trigger 可以拥有相同的 Event 以及相同的条件,那么事件触发,且条件满足时,多个 Trigger 会同时运行。当然这个同时是逻辑上的,内部还是有一个先后,我推测是一个类似 coroutine 的机制来驱动的。因为 Action 里的事情都可以立刻完成(这里有个例外,下面会提到),所以不会有冲突。
那些,如果要执行的事情是一个需要很长时间才能做完的动作怎么办?比如你需要把一个单位按定义好的移动速度,沿正常的寻路得到的路径移动到一个目的地。这就得引入指令序列这个概念。
每个单位身上都有一个指令队列,保存的是一系列的指令 (order) 。指令和 Action 是两类完全不同的东西。比如让一个单位从当前位置移动到一个点就是一个 order ,而把单位(不通过游戏内在机制)瞬移到那个点就是一个 action 。order 是在队列中循序执行的。在 Trigger 里能做的是添加一个 action,这个 action 是把 order 加入队列中。当然加的方法有三类:清空现有队列,加入一条指令;把指令加到队列最后;把指令插入到当前位置(立刻执行,但不清掉原有指令)。
Trigger 里可以有多个 Action ,它们只有执行次序的。但 Trigger 之间可以并发。大部分 Action 都会立刻执行结束,只有一类特殊的 Action 会把当前的 Trigger 挂起,只到条件满足才会执行后续的 action 。
这类特殊的 action 叫做等待。目前我发现有三种等待指令。
最常用的是等待一段(游戏)时间,比如我们可以利用它在游戏开始后 10 秒显示一行字。只需要把游戏开始事件加到一个 Trigger 中,在 action 里依次加入等待 10 秒,显示文字。
其它两个是等待一个 Trigger 被触发,以及等待一个条件表达式成立。我怀疑它们可以用于 Trigger 间的协作,但没有深入研究到底怎么做。不过可以确定的是,Trigger 是图灵完备的,它可以有分支和循环(它们是特殊的 Action)。Trigger 也可以看成是某种意义上的传统函数,可以不给 Trigger 设置事件和条件,而用一个 Action 直接运行一个 Trigger 。
指令和动作的概念分离对我的启发很大。
也就是说,基于 Trigger 的流程控制是面对 action 的,而 Trigger 属于整个世界而不是单个对象。order 则是附属在对象上,对象的流程控制用 order 组成。order 只可以循序的排列在队列中,Action 则可以有复杂的控制结构。
假设我们需要让一个单位移动到 A 点,然后驻留 5 秒,再移动到 B 点。目前在星际的编辑器中很难简单做到。关键在于让单位停留一段时间,我没有在 order 列表中找到。如果要实现,大约是这样的。添加一个 order 让单位移动到 A 点。另外写一个 trigger 在单位到达 A 点时触发,在这个 trigger 内等待 10 秒,再发送下一个指令让其移动到 B 点。
这里,假设支持让一个让单位等待一段时间的指令,它和现有的等待一段时间的 Action 一定是两样东西。而我们之前的设计没有区分这两者,所以实现的很不干净。
建议继续学习:
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:云风的 BLOG 来源: 云风的 BLOG
- 标签: 编辑器
- 发布时间:2012-10-22 13:22:35
- [2528] 代理的加密部分
- [1329] 创业笔记 | 从0到1开公司是什么体验
- [649] vimgtd-在vim(gvim)中实现GT
- [574] 查找第K小的元素
- [72] Oracle MTS模式下 进程地址与会话信
- [64] Go Reflect 性能
- [63] 【社会化设计】自我(self)部分――欢迎区
- [63] 如何拿下简短的域名
- [62] 图书馆的世界纪录
- [61] android 开发入门