如何设计回合制游戏的AI?Shadowrun实例(上)

来自 灯塔实验室 2014-11-29
深度

[ 转载自 灯塔实验室 ]

如何设计回合制游戏的AI?Shadowrun实例(上)

本文转自灯塔实验室,原文来自Gamasutra,译者J_Lu。

我们最近发售的作品暗影狂奔:龙陨——导演剪辑版里(Shadowrun: Dragonfall – Director’s Cut),拥有一个我们自己研发的AI系统——“秋葵汤饭”(Gumbo)。在这篇文章里,我们会详细介绍Gumbo是怎么被开发出来的。我们最初定下的目标是什么,以及最后我们是如何达成这个目标的。我们希望这篇文章能对你自己的回合制(或者即时战略)游戏里的AI起到抛砖引玉的作用。

首先是一点背景知识介绍。2012年4月,暗影狂奔:归来(Shadowrun: Returns)在Kickstarter上成功筹集到了180万美金,之后在13年的7月25日发售。之后的几个月,我们放出了一个大型的DLC下载包——龙陨。最后在2014年9月18日,龙陨的导演剪辑版正式发售。

龙陨这部DLC一开始只是作为我们在Kickstarter上设立的一个达成金额的目标。但当它最后被制作完时,却已经变成了一个很庞大的补充包,并且在游戏的核心部分改进了很多。

我们制作组内部和许多玩家对于新加的部分都表示很满意。不过我们内部却觉得仍有改进的余地。因此我们决定制作导演剪辑版。所有买过之前DLC的玩家都会免费得到一份,因为导演剪辑版已经不是单纯意义上对原有游戏的补充和修改,它自己已经是一部完整的游戏,所以系列的新玩家也能够直接享受最新最好的版本。

开发之初我们在清单上写下我们想着重加强的游戏部分时,更加聪明的AI被放在了最首要的位置。(特别是在游戏的战斗系统也有了很大的进化的情况下)。回归和龙陨的AI都是在Unity中由XML树型分支(branching tree of xml)推导出来的。它们中也用了很多自定义的代码来实现游戏过程中的读取数据和执行动作。尽管这个系统在敌人基本行为的多样性上是比较成功的,但是我们一直觉得,在有些方面我们可以做得更好。在现有的系统里,任何设计人员所希望的调整和特殊行为都要通过工程方面的支持来实现。这非常耗时,因为资源库和代码的重建是免不了的。这让迭代的速度变得非常慢,而且对于设计者来说,观察AI的决策(decision making)也变得比较困难。

目的

在游戏理论和经济学里,有一个术语叫实用度(Utility)。这个术语描述了一种能力,它能够满足AI的需求。在我们的游戏里,有三种方法能够让实用度最大化:减少所受的伤害,支援友军,最大化对敌的伤害。这几个目的往往要求了一系列的行为,而每一种都有各自的潜在随机因素和失败的机率。在一个动态的有智慧的世界里,每一个AI都应该以最大化各自的实用度作为行动准则。

以下是我们主要的开发目的。

简单

设计者在设计角色,道具以及地图的时候就应该能非常直观地看到战斗具体是什么样子的。AI应该充当设计环节中的最后一部分。设计者应该能很容易地监测AI的一举一动,并且如果他想的话也很容易在AI上做出修改。

安全

AI递归或失败(Recursion or failure)在回合制游戏里会产生比较负面的影响。在我们的第一个游戏里,我们一旦检测到AI卡住了或者处于无限循环中,就会引入了一个叫“卡死超时”(brain freeze timeouts)的机制。那只是一个临时解决问题的办法,而且绝对谈不上理想。我们心目中新的AI解决办法应该会让有出问题的代码优雅地报告失败,而不是像之前那样直接全部杀死、结束。

可扩展性

AI有了一套基本的行为和算法,这只是整个开发流程的第一步。因此,在任何可能领域附加的特性都应该能用尽可能简单的方法整合到现有的AI中去。一些常用的套路有:加强某一种特性来达到一种前后一致的设计,并同时降低问题发生的可能性。

快速

虽然回合制游戏看起来节奏很慢,但是AI做决定的速度越快越好。某一个由AI控制的单位移动到地图上的某一位置,并且决定采取某种攻击,在这两者之间的间隔是百害而无一利的。这原因就在于,这种停顿和间隔会在游戏里重复无数次。同理,地图上所有的NPC也不可能有很多时间来思考下一步该做什么,即使还没有轮到他们战斗,但是在游戏中这种状态会随时发生变化。我们的新AI会尽可能快地处理上述所有问题。

我们的解决方法

我们决定开发一种叫做GumboScript的轻量化新语言。

有一位Kickstarter的网友问我们为什么不用Lua或者其他现有编程的语言。虽然那些很成熟的语言有着很多很完整的特性,但是很多时候和我们的目标相背,而且会将有些问题复杂化。所以我们觉得自己写一个语言是更好的选择。

为了减少字符串解析(String Parsing),我们的GumboScript只会被读一次,然后被储存在C#类里,就像我们其余所有的代码。我们在内存里也设计了一小块固定区域,一次性地注册了一些数据类型。这些数据类型会被限制在一套基本的集合里,这种方法有助于避免不一致的情况(Discrepancy)。我也希望在尽可能大的程度上避免脚本和原生代码之间的绑定、暴露。最后由于我们的游戏是多平台的,不使用这些有内存访问限制和文件访问限制的第三方库或者runtime,是更为安全稳妥的做法。

文章评论
游戏葡萄订阅号