(机翻微调)
你好!我们是Patrick Conaboy和Jeff Brock,我们是LoR设计团队的高级软件工程师。我们团队中的工程师负责构建工具,编写游戏逻辑并为卡牌设计人员提供支持。在本文中,我们将介绍LoR的工程师是如何构建新的脚本系统,使设计人员可以轻松地迭代和试验新的卡牌创意。
正如我们过去提到的,作为LoR工程师,我们的主要目标是为设计师和艺术家提供支持。我们旨在构建工具和系统,使设计人员无需依赖工程师就可以直接运用代码库。牢记这些优先事项,我们将概述新的卡牌脚本系统的工作原理,并通过查看某个蘑菇爱好者,深入探讨这种灵活性至关重要的例子。
建立我们的新脚本系统
早在开发初期,LoR就利用了《英雄联盟》中的许多技术来使实验变得简单快捷。在原型制作过程中,我们在涉足卡牌游戏领域之前评估了几种不同的策略类型游戏。在我们确定游戏开发方向之前,不值得先构建自定义系统,因此我们依靠Riot设计师从LoL获得熟悉的现有技术,例如我们用于设计能力的BlockBuilder工具。
一旦LoR成形并开始进行预生产,我们知道必须推出一种用于设计卡牌的新系统。使用LoL技术非常适合早期开发,但是我们很快遇到了一些限制。
主要问题与英雄能力有关。LoL为自己的需求定义了非常明确的模块:例如,可以将技能放进一个模块的子集中,造成状态影响,造成爆破或造成伤害。但是对于卡牌游戏,我们发现自己需要一套较为宽松的操作规则,因为卡牌经常通过洗乱牌组或在以后的回合中完成动作来故意破坏规则。
基本上,设计师会不断提出一些很酷的想法,然后遇到瓶颈,因为他们需要工程师用我们的视觉脚本语言创建特定的自定义模块。随着设计团队的扩大以及越来越多的设计师希望尝试新的游戏风格,这种转变变得越来越困难。
利用IRONPYTHON的灵活性
我们的工程团队反复研究了一些想法,最终找到了与IronPython集成的针对设计人员的基于脚本的解决方案。
IronPython的一个巨大好处是我们可以直接从脚本访问C#对象并调用C#方法。这是将我们的C#游戏引擎连接到我们的Python卡牌脚本的胶水。如果设计师想尝试一种独特的新游戏玩法,那么他们不需要工程师先构建东西。
示例:访问逻辑
当对卡牌施加增益效果时,设计者可以访问:
- 哪张牌获得增益
- 哪张牌施加增益
- 施加的是什么增益
- 施加增益时游戏整体处于什么状态
让我们用一张KDA新牌「别挡我道」来举例。
此处,设计人员想要创造一张使「己方增益改为持续生效」的牌。通过我们的Python实现,他们可以直接看到所有必需的对象。
由于他们拥有使用脚本语言编写的所有数据,因此他们可以侦听添加的buff,并仅需两行代码即可直接更改持续时间:
## EventMutateEffectBeforeAdd
buffEffect.Duration = duration.Indefinite
向不熟悉的Python程序员解释下,「##」是我们的引擎理解卡牌正在监听事件的方式:此处为EventMutateEffectBeforeAdd。
通过将其存储在设计人员可以进行调整和平衡的地方,这可以使「己方增益改为持续生效」的游戏效果逻辑出现在C#游戏引擎之外。
示例:构建库
过去,如果设计人员想要库中的某些功能,则需要等待工程师有时间在游戏引擎中进行构建。使用我们的Python解决方案,设计人员可以构建完全驻留在脚本中的代码库,因此他们可以创建整个游戏系统,而无需让其他开发人员参与。
例如,设计师开设了历史记录系统,该系统可以跟踪游戏过程中发生的所有事情。此历史记录系统是一个Python脚本,用于跟踪事件并存储以后可以引用的数据。它还公开了一个API,供设计人员直接检索该数据并将其用于卡牌脚本中。这对于像英雄这样的复杂牌很重要,后者通常需要保持「打出过几个法术」之类数值的记录。
一张依赖历史数值的卡牌。
这里的权衡是设计师需要提高技术水平:在许多工作室中,设计师仅使用可视脚本系统,很少需要深入研究代码。我们发现,与是技术设计师的合作非常有用,他们最终培训了其他设计师,并且很快教会新入职的员工。
通用架构
每张卡牌都有自己的小Python脚本与之相关联。我们能够从C#中调用Python并传递C#对象引用。
我们在C#代码中也有标记,还有一个单独的脚本可以生成假的Python,当Python脚本编写者处理这些外来的C#对象时,我们用它来自动完成。如果没有这一点,他们在不直接阅读代码的情况下,就不知道允许对C#对象调用什么方法。
最后,我们为Visual Studio Code写了一个插件,让设计师可以直接访问可用的方法。这为他们提供了方便的参考,比如他们可能想要参考的游戏事件,以及快速访问所有其他脚本文件。
在每个卡牌脚本中,设计者都会调出他们感兴趣的事件。例如,如果一张卡牌使让你在任意玩家受到伤害时都抽一张牌,那么你可以让脚本监听伤害发生的时间,并添加修改器。
这是一个非常简单的两行脚本文件:
## EventDoDamage
game.Draw()
请注意,此处的「game」是对传递给Python的C#游戏状态的引用。
卡牌里的一切都是小兵
我们脚本系统的一个基础方面是如何处理卡牌存储。设计师们把所有的东西都存储在卡牌里:有点像「LOL里的一切都是小兵」的笑话,LoR中所有的东西都存储在卡牌里。例如,「水晶」其实就是一张卡牌,它存储了我们前面提到的历史追踪。当一个单位被打出时,Nexus有一个脚本,它可以监听并递增自己存储的计数,这些数值可以被其他卡牌引用。
你是说那些小东西能储存东西?
这是LoR存储数据方式中非常关键的一环。设计师可以合成全新的游戏机制,而不需要工程师构建任何新的东西,因为他们可以触发游戏状态的任何变化,在卡牌存储中存储任何值,并且可以访问工程师所做的所有游戏状态操作的基本模块(moveCard、doDamage、attachScript等)。
故事时间:过渡
切换一个底层游戏系统是一项巨大的工程。我们的新脚本系统绝对达到了我们的预期,但过渡过程中也遇到了一些坎坷。
有一个故事真正证明了转换底层系统的复杂性,它发生在三年前的一次大型内部游戏测试中。我们刚刚完成了用Python构建游戏的工作,但这些代码路径只有在有Python脚本要加载时才会被采用。这样做是为了不影响游戏测试,因为游戏测试仍在原始的LoL模块系统上运行。我们启动了模块脚本构建的部署,并亲自跑过玩法测试的演示,确信构建是稳定的,而且基于Python的系统在我们的测试环境上是独立的。
想象一下:演示结束了,对了,当一群人说:「嘿,嗯……一切都坏了」,噩梦到了极致。这没道理啊,因为没有人真正进行过任何更改:我们刚刚对已经测试过的现有内容进行了重建和重新部署。
所以发生了什么呢?
我们有一个固定数量的共享构建节点,我们重用这些节点,并尽量只清理严格必要的内容,以避免每次都复制重复的资产。由于我们之前在迭代,Python构建一直在其中几个节点上运行,但一旦Python构建在节点上运行,就有效地感染了它,留下了Python脚本。我们为测试所做的基于块脚本的重建/重新部署恰好运行在被Python构建污染的节点上。所以有些卡牌在加载Python脚本,有些卡牌在加载模块脚本,有些卡牌两者都在加载,所以什么都不会发生。
当我们远程进来后,发现同时存在Python和块数据,只是花了几个小时就补好了,游戏测试就继续了。但这绝对是一个诡异的事件,它展示了在现场游戏中过渡到一个底层系统时的复杂性。
我们最大的收获是什么?虽然利用新数据的存在来控制哪个系统被激活可能是一个优雅的解决方案,但有在替换遗留系统时,哪怕只要有一个好的过渡就是一项重要的保障。
看看提莫
提莫及其相关的毒泡芙菇带来了复杂的技术挑战,真正突出了我们新脚本系统的影响。
这些卡牌基于一个游戏系统,该系统几乎完全存在于我们的Python脚本环境中。设计人员能够快速迭代这些想法:例如,将毒泡芙菇设定为现有卡牌上的对象,而非放在牌组中的单独卡牌。我们建立了一个毒泡芙菇函数的Python库,该库定义了一个非常清晰的API,用于说明卡牌如何将蘑菇植入对手的牌组中。毒泡芙菇在游戏中的表示形式,添加方式以及它们的作用都在Python脚本系统中定义。
迭代蘑菇
随着时间推移,我们进行了几次毒泡芙菇迭代,提莫的蘑菇越来越酷。随着我们从模块系统转移到Python系统,设计人员越来越能够自行解决错误,切无需花费工程时间。
毒泡芙菇卡牌最初是由提莫和他的随从们种在牌组中的,当你抽到蘑菇卡牌时,它们会施放咒语来造成伤害。升级的提莫会使卡组中现有的毒泡芙菇数量翻倍:这既非常令人兴奋也非常危险。如果放到卡组中的蘑菇足够多,可以使整个游戏崩溃。使用新的Python脚本语言,我们可以顺利地在毒泡芙菇系统上移植并同时进行一些代码清理,从而可以将毒泡芙菇从单个卡牌切换到现有卡牌上的陷阱。
不那么有趣
原始卡牌迭代问题的一个例子与伤害增幅的其他牌有关。基础系列中的「欢乐匠」牌会增加法术和技能造成的任何伤害。但是,如果你有一个欢乐匠并且敌方在你的牌组中放置了一个毒泡芙菇,而你将抽到带蘑菇的卡牌:换句话说,从技术上讲,伤害来源是你自己,因此伤害应该被增幅。将毒泡芙菇变成卡牌上的陷阱后,使我们有机会进行一些伤害来源重定向,我们用它来澄清这种伤害来自敌人而不是自身造成的。
表现
一旦将毒泡芙菇转换为卡牌上的陷阱,就会遇到另一个问题:表现。原始的毒泡芙菇系统会将陷阱植入敌方的牌组中,游戏将逐个遍历每个毒泡芙菇,随机生成一个介于0和牌组大小之间的数字,并将陷阱专门放置在该牌上。这导致了完全随机的分布,这非常好。
但是,当我们为「好友挑战」功能做准备时,我们意识到,如果两个朋友可以互相对抗,那么可能会有一些玩家想要合力测试看看牌组中最多可以种多少个毒泡芙菇。这将导致服务器因需要分配数量庞大的蘑菇而倒下,所以一场对局可能会让该服务器上运行的所有其他对局崩溃。我们对毒泡芙菇插入算法进行了调整,改成在卡组中一张张地去估计每张卡上应该种多少蘑菇。
一种简单的计算方法是,将蘑菇的数量除以牌组中卡牌的数量,然后对数字稍加调整,即使分散均匀,也感觉是随机的。我们知道这个想法很糟糕,并且与毒泡芙菇卡牌所承诺的随机性背道而驰,因此我们查看了统计模型以弄清楚什么样的系统才更现实。
为此,我们生成了一个标准正态分布,用于确定每张卡牌上应植入多少个毒泡芙菇,然后我们为每张卡牌随机选择曲线上的一个点。之后,我们进行一些微调,确保总数对得上。
展望未来
在过渡到新的Python系统的过程中,我们能够进行很多代码清理。例如,陷阱本身是C#游戏引擎中的一等公民,因为它们是他们自己的卡牌类型。这是提莫和他的随从们来说独有的机制,这太过分了。
在移除「毒泡芙菇」牌并将其变成陷阱时,我们考虑了很多面向未来的问题。如果设计人员后来想要多个陷阱类型的卡牌来模仿蘑菇机制中的其他卡牌上的动作,那么他们可以通过扩展我们构建的「陷阱」Python库轻松地迭代其想法。
结语
集成工程和设计的方式是在LoR项目工作的绝佳体验之一。开发环境是高度协作的,这使工程师可以更好地了解设计师将来的需求。我们可以通过我们的解决方案更具创造力;我们不仅获得了技术规范,还参与了有关需要解决的问题的讨论。
卡牌团队中工程师与设计师的比例确实反映了这一点。早期,我们有两名设计师和四名工程师。随着时间的推移,我们开发出了可以更好地支持设计师的工具:我们现在大约有15位设计师,只有3位工程师。
LoR的工程师一直在针对这一个目标衡量我们的进度:使设计师的生活更轻松。通过早期的原型制作,很明显,艺术家和设计师依靠工具技术完成工作的方式有多少……以及这如何减慢他们的工作速度。我们需要构建工具和流程来使事情顺利进行,减少工程瓶颈并鼓励创造力和实验。寻找乐趣的工作是他们的工作,构建足够高可用性的技术是我们的工作,设计师只需要在需要时与我们交谈即可。
谢谢阅读!如果你有任何疑问,或者只是想告诉我们你最喜欢的卡牌,请在下面发表评论。
作者:Jeff Brock和Patrick Conaboy