脚本对象的故事

ScriptableObjects是Unity开发最伟大的礼物之一。 它们在脚本和游戏中资产的视觉处理之间占据了一个独特的有用交集。 在一个系统中,您需要能够定义事物类型 ,然后创建这些事物的全局实例,这些实例的唯一字段值不同,而ScriptableObject是天赐之物。 它允许您直接在Unity编辑器中执行此操作,而无需通过代码。

但是游戏很复杂,有时仅更改实例之间的字段是不够的。 也许您希望一个人的行为与另一个人不同。 那么正确的方法是什么? 您是否应该像状态机一样控制字段背后的行为? 您应该使用继承吗? 还是这个问题天生就有缺陷? 同一事物的实例之间不同行为的概念是否背离了ScriptableObjects的全部目的?

几年来,我一直在探索独立游戏的开发,同时每天都在一些高科技公司工作。 我仍处于发布第一个游戏的旅程的中间,但是一路走来,我陷入了一些光荣的混乱,试图在我的代码库中实现可重用性和简单性之间的正确平衡。

这就是其中的一团糟:我如何使用ScriptableObject的故事,ScriptableObject是一种便利工具,应该可以节省时间,并且将其扭曲成一个可怕的,难以理解的时间。

我的动机很纯粹。 我坚持不懈地遵循一项核心设计原则,在此过程中,我沉迷于它的重量。 也许您已经了解得更多,在这种情况下,您仍然可以通过纯粹的schadenfreude享受它。 但是也许您也刚刚开始。 而且,如果这种回顾可以防止你们中的一个人在类似的兔子洞中损失数周的时间,则只需要像我一样,设法使自己退出困境,好吧,好吗,好吗?


我开始制作一款游戏,成为我历史上最喜欢的游戏之一:最终幻想:战术。 FF:T是一种战术RPG,从您开始时是角色,卑鄙的乡绅和化学家组成的团队,只剩下匕首和背上的衬衫。 但是,在冒险之旅结束时,它们已经成为忍者,骑士,召唤者,神秘主义者等等,所有这些都取决于您在玩游戏时如何选择对他们进行专门化处理。

在我的游戏中(有时被称为“激光战术”,“流氓战术”和“杀戮命令”),我选择科幻而不是幻想来将自己与主要灵感区分开来,因为我还没有找到科幻TRPG具有与FF:T相同的基本原理。 我编写了所有设计文档和电子表格,其中列举了25个乔布斯的层次树,角色可以学习数百种能力。 只需将这些想法纳入代码即可。 但是,嘿,我是一个经验丰富的程序员。 那应该是容易的部分,对吗?

老实说,我可以就自己在游戏中所犯的错误做一个完整的系列文章,主题包括范围的不断缩小,以及我第一次涉足程序生成领域。 但是目前,我们正在谈论ScriptableObjects,以及游戏中这些实体的设计。 在此示例中,这意味着角色(玩家控制和敌人),乔布斯和能力。


基本架构如下所示:

每个角色都有一个Species ,一组共享的特征。 他们还具有当前的Job ,以及他们已解锁的Job的记录。

每个工作定义了角色可以学习的一组能力

最后,为了区分刚刚解锁士兵任务的角色和掌握所有士兵能力的10级士兵,我们还需要一个数据类: JobProgress 。 它跟踪任务中角色的等级,以及已解锁的任务能力。

这里需要注意的主要设计选择是为每个工作和能力编写特定的类的决定。 我正在寻找〜25个职位,每个职位具有4–8个能力,总计〜100–200个能力。

当时,编写125–225个唯一的类听起来很耗时,容易出错并且设计不佳。 但是等等,这些能力都将共享共同的组成部分,对吗? 它们都将具有类似效果的组合,例如,伤害敌人,恢复健康,移动角色,给予恩赐/疾病等等。 还可以通过多种方法来瞄准能力(自我,近战,远程,射程,锥形,爆炸等)。

解锁每个工作或能力的限制也可以分解为一组构建块:角色是否是特定物种? 在工作Y中达到X级了吗? 它学会了Z能力吗?

我要描述的工程原理是可重用性 ; 我决心将Jobs and Abilities广义地定义为ScriptableObjects,而不是单独实现每一个。 相反,我将自己编写微小的限制效果的定义,然后根据需要组合和重用它们。 目的是减少代码重复,并且我创建的Jobs and Abilities越多,获得的收益就越多。

让我们看看对我有什么帮助…

在工作和能力的背景下,“限制”定义了角色必须通过才能通过解锁的检查。 让我们看一下“招聘工作”及其后代“ 士兵”的一些假设要求。

招聘 :仅限生物(人类或半机械人,无机器人)
士兵 :新兵等级3以上

但是,如果没有用于Human,Cyborg,Robot,Recruit,Soldier等的类,则需要从中构建这些检查的模块。

因此,我对Restriction代码定义是:一个对Character执行检查的类,称为IsMet(Character) ,如果Character满足其要求,则返回true

我定义了两个Restriction子类: SpeciesRestrictionJobRestriction ,如果Character是有效的Species或已达到指定Job的所需级别,则分别从IsMet(Character)返回true。

当游戏需要知道角色是否已解锁任务时,它将对角色运行所有任务限制的IsMet函数。 如果它们全部通过,则作业将被解锁。

能力也有限制,但我会为您省去更多的图表(不过,我有它们,请让我看看它们,它们很棒)。 这里的要点是,所有工作/能力限制都具有相同的输入(角色)和概念性输出(无论事物是否已解锁)。 他们不需要任何其他上下文。

这个系统并不可怕,尽管根据游戏的规模可能会比必要的系统更大。 当我进入“效果”以及与“限制”相交的时候,我的麻烦才真正开始。

不执行任何操作的异能几乎是无用的,因此每个异能都具有1个或多个效应,例如伤害,治疗或移动角色。

但是,如果一项能力的个别效果必须限制为不同的角色怎么办? 举一个在很多游戏中都看到的例子, 《偷生活》 。 此能力有两种效果: 伤害目标自我治疗 。 每个效果都需要选择应用的角色,具体取决于谁是源,谁是目标。

“等一下,”我想。 “ 选择只是表达限制的一种不同方式,不是吗?”因此,我的代码库中的限制概念也逐渐涵盖了这一点。

看起来很棒……等等……Restriction bool Met(Character)方法仅将Character作为单个参数,并且不提供(或直到这一点为止)任何其他信息。 效果如何单独说明所讨论的角色是能力的目标还是来源 ? 显然,这种限制需要一些情境。

因此,我停下来,以这作为我以前对“限制”的定义不适合该用例的一个标志,并以其他方式实现了这一部分。

开个玩笑,那太容易了。 “不!”我诅咒着脑袋里的声音,喃喃地说着罐头和蠕虫,“我比这个问题还聪明! 我将使用模板!”

因此,简单的Restriction类变为Restriction ,而bool IsMet(Character)变为bool IsMet(T) 。 到现在为止,所有已经成为Restriction的东西都变成了Restriction ,现在与能力相关的Restrictions变成了Restriction

“还有其他什么呢?”我反对更好的判断,“我注意到我也可以在其他地方使用限制-以能力选择目标的方式! 一些能力只能影响生物学特性,其他能力只能影响机械特性,而其他能力则不能选择单个目标,而只能在成年或震荡中……”

通过闸门,基于限制子类的多层继承,基于它们所需的信息量和类型,转换函数,重载的IsMet(...)方法,仅仅是其他属性类型转换的属性,以及最糟糕的是: 自定义Unity编辑器 。 每当我想对游戏中的任何内容应用模块化过滤时,“限制”的抽象概念(如果考虑的话,它就与if语句的概念一样广泛)开始使用。

注意:使用自定义Unity Editor时,我一无所获。 用它们来进行不良设计是不好的。

这是我最终仅实现两项工作和一项能力的结果:

如果您要保持评分,那就是七个类,三个子类和十二个编辑器项。 对于三个游戏概念。

是的,其中许多类将被重用,其中许多类将被重用数十次,这很棒(尽管当大多数能力具有多个效果,而大多数效果都有其自己的限制时,编辑器项目列表将继续迅速膨胀)。 但是,实际上,大多数这些可重用的片段只有几行长……难道它们只是它们的基类的功能吗? 这仍然符合可重用性原则,只是没有必要将代码拆分为其他实体。

现在,这种类型的设计本身并没有本质上的坏处(尽管在我看来,它肯定是不好的 )。 的确,对于一些以这种方式工作,经过适当计划的开发人员,理论上可以节省时间或代码行。

但是对我而言,这意味着实现仅使用一次的整个Restriction子类。 最重要的是,事实证明,当您将实体拆分成具有数十个连接和难以理解的结构的多层网络时,它否定了ScriptableObjects在Unity Editor中提供的好处,因为它需要花费额外的时间才能将其解开每当尝试查看各个部分如何组合在一起时。

最棒的是,当我开始开发当前的游戏SCUM(#SCUMgame)时,我还是没上过课,因为我仍然认为将事情分解成很小的模块化块总是更可靠。 这是我的Unity Editor的屏幕截图,仅实现了8个乘员命令(SCUM等效于Abilities),并且您猜到了它的效果限制


在宏伟的计划中,进行此练习不是一次,而是两次,这使我对ScriptableObjects的正确用法和限制感到惊讶。 现在,我对如何最好地利用它们有了更好的了解,至少对我而言:

  • 要定义游戏中的实体类型,是有形的,例如“枪”或“房间”,还是概念性的,例如“工作”或“谜语”。
  • 作为一组相关字段的容器,例如全球敌人的统计数据或预制件,例如同一视觉效果的不同颜色版本,或可能循环播放的音频效果的列表。

那我应该在这里做什么? 好吧,这是我最终在SCUM中所做的事情,也是我对仍在我身边的你们两个中的任何一个提供的主要建议:

默认情况下,在事物的定义中保留事物应如何表现的详细信息。

为您当前和已知的未来需求编写代码。 不要使您的设计复杂化,以解决未知或将来可能发生的情况。 您将不需要它。

还记得最后一张图中的蓝线吗? 按照此指导,该行下方的所有内容都会消失。

这是否意味着子类化SO不好? 当然不是。 如果我回到Rogue Tactics,Job仍然是ScriptableObject。 我只是直接将Job子类化,而这些子类将负责它们的行为差异。 我最终会这样:

Job和Ability成为抽象的ScriptableObjects,并获得所需的方法IsUnlocked(Character) ,该方法IsUnlocked(Character)每个作业/每个功能执行一次曾经在模块化限制中进行的所有检查。 Ability还获得了Execute(ActionContext) ,它通过组合效果来执行一旦处理的所有事情。

但是,这并不意味着每个Job都需要自己的类。 一种经常发生的情况是第二级作业,一旦您在起始作业中达到第三级,这些作业就会被解锁。 新兵入伍。 熟练者会变成Psions。 技术人员成为机械师。 唯一的区别是所需的开始工作及其能力。 我是否需要针对士兵,Psion和机械师的单独班级? 当然不是。

您可以通过Ability来想象类似的情况。 游戏中可能有十二种能力会伤害一个敌人角色并造成某种形式的属性惩罚或疾病; 它们都可以是Ability的一个DamageAndAfflict子类的实例。 等等。

上面的所有工作真的值得不必几十次做吗? 地狱无。 这就是应该使用ScriptableObjects的方式。 他们不需要从数十个微小的部分中被弗兰肯斯坦联合在一起。 每个人只需要知道应该做什么。

关于我的第一个问题-“如果要让ScriptableObject的实例表现出不同的行为呢?”,这就是我遵循上述原则的答案:将ScriptableObject子类化为具有不同行为的方法很好。 只是在进行拆分时不要太细粒度,以确保您以后可以重用某些部件,否则会使您一团糟而不是节省了自己。

真的就是这么简单,坦白地说,我花了很长时间才把它弄清楚,即使我从大学开始就已经在理论上了解它们。

如果在阅读本文的任何时候,您已经在我的设计和您的设计之间画出了一条平行线,并且遇到了使零件适合的麻烦,那么……可重用性很好,但这只是一个原则。 一些已发布的游戏很好地利用了它,而其他一些则没有。 但是您知道所有这些已发布的游戏有什么共同点吗? 他们的开发人员(希望)知道他们如何工作。

直到下一次,
DZ

Dane Zeke Liergaard于2011年8月从威斯康星大学麦迪逊分校获得了计算机科学学士学位。同月,他将他拥有的一切东西(包括他的猫)搬到了西雅图,并装进了2004年的Chevy Aveo中。 Dane从2011年到2016年在Amazon,Inc.工作,于2014年在客户服务/电话基础架构与Amazon Smile之间过渡。

2016年5月,他在位于加利福尼亚州威尼斯海滩的Google办公室工作,负责YouTube的订阅体系结构。 2017年9月,在洛杉矶工作约1.25年(绰绰有余)后,他转到了Google西雅图。 在此过渡期间,他还将头衔从SWE(软件工程师)更改为DPE(开发人员程序工程师)。 有关DPE是什么和做的很好的解释,请查看Dane同事@fhinkel的这篇文章:

开发人员计划工程师-说什么!?

我们关心开发人员

medium.com

dzlier@google.com
5ive项目符号Unity发布者页面
@DZLiergaard
@ 5iveBullets
工作github个人资料
个人github个人资料