摘要:Aroma开发日志#1-利用Unity开发类旷野之息的“化学反应”系统,创造塞尔达式的化学引擎 塞尔达传说 怎么建造

⚠️ 未经作者授权 禁止转载
概述
此文章为我们的游戏,Aro 的开发日志。塞尔达旷野之息中,最让我印象深刻的一个 体系,就是它的化学反应 体系(至少官方是这么称呼的),也就是其游戏当中的,元素与万物交互反应的 体系。像是火可以点燃木箭,木箭又可以点燃地上的木头,木头可以将草点燃,被点燃的草又会烧到角色和敌人。 这样一套 体系不仅仅是为玩家带来了更 诚恳的体验,它实际上是旷野之息这款游戏的根基,他为游戏的谜题设计,关卡设计带来了极大的设计空间和玩法深度。因此我当时就非常想尝试复刻这样的一套 体系。最近我终于有机会如愿以偿,进行了这套 体系的复刻。站内游戏链接: 寻味奇旅 | 机核 GCORES (Demo即将上线,尽情期待)Aro 游戏定位
这是一款面向中轻度玩家的,以解谜为主 战斗为辅,重点考验玩家的 思索& 领会,而尽量不考验玩家的战斗操作和反应能力的游戏。游戏选用3D轴侧视角,一方面是致敬老塞尔达和Tunic,另一方面也是基于我们的休闲解密定位,减轻玩家的操作负担(不用操作视角), 并且希望在玩家遇到谜题时,可以有更加可控的画面信息呈现。同时也照顾游戏的美学表现和方便关卡引导,让中轻度玩家可以畅快游玩。设计思路 - 元素的传导
其实要实现塞尔达式的化学引擎,要做到的最关键的一步就是将各个游戏中的物品之间交互的“中间件”抽象出来。我们可以称呼这种中间件为元素。 接着我们在针对每一种元素,定义具有统一接口的规范化的生产者和接收者。比如我们拿火焰来举例:
在Unity中,生产者(Property)和接收者(Sensor)分别对应着逻辑统一固定的两个脚本。 并且由接收者脚本直接管理 情形变更,其他脚本通过监听接收者的 情形变更(委托调用)的形式,来对每个物体的具体逻辑进行实现。假设我们主角的主角有一个喷火技能,还有一只羊和一个篝火:
主角的喷火技能 由于是攻击判定,我们可以为攻击判定携带上温度信息,比如击中敌人就给其加100度, 接着羊达到100度就进入燃烧 情形,燃烧 情形的羊激活自身上的温度生产者,寻找其他的温度接收者引燃对方:
接着,羊和篝火各自分别实现对于燃烧这一 情形的表现就可以了:
因此其实就是三步走:将一个机制抽象为一个元素围绕这个元素开发对应的生产者和接收者利用代码/蓝图,串联组件,监听 情形,实现各自的效果如此一来,当年的游戏当中具有十几二十个可以着火的 物品时,你突发奇想说,我想让路边的小草也可以被火点燃,你只需要为这个小草添加上与温度相关的两个组件,实现一下燃烧和被烧光的效果,你就会发现它已经自动兼容了游戏里之前所有能着火的 物品。他们无一例外都可以对小草产生效果。你就会 觉悟到,这就是化学 体系的真正威力了。组件化的 思索
其实我上面所说的这样的概念应该叫做“组件化”,将每一个功能拆分为一个单独的组件。每个组件的逻辑实现非常的简单,代码非常的健壮。而配置一个敌人或NPC,只需要配置上对应的组件, 接着补齐组件之间连携的代码或者蓝图,就可以实现一个敌人的逻辑了。
性能优化Tips一、分帧更新当同一帧内存在大量需要更新的Property或者Sensor时肯定会卡。可以不要让Property和Sensor本身调用Update,而是在其启用时将自己注册到一个统一的Manager。Manager中设置每帧更新上限。
二、Lazy更新比方说针对前面讲到的温度传导 体系,TempProperty可以只在其温度不为常温的时候才对附近Sensor展开检查。TempSensor也可以只在自己的累积温度不为0(常温)的时候才要求TempMgr更新自己。这样场景中没有发生任何温度变化的时候,即便有几万个Sensor或Property在场景中也不会触发任何的更新需求。“元素”的设计 制度
可感知性 + 可 领会 + 可被利用 由于前面举例也说到了温度,这里我们就用温度举例来讨论。在塞尔达中,温度被分为了五个档位, 并且为了提升沉浸感和动态 全球交互感,它的温度 一个明显的连续变化数值。但塞尔达这样设计是 由于他地图上有两个利用温度进行“软锁”的区域,希望玩家拥有对应的对抗温度的“钥匙”才能解锁, 并且塞尔达利用UI显示的表现了“一般热”和“超级热”的区别,否则很难向玩家传递这种温度的变化。 体系的设计需要考虑其的设计目的,尽可能的做减法,同时要考虑复杂 体系对于玩家的可感知度。特别是这样的一套元素 体系,它能给玩家带来乐趣的核心体验是来源于让玩家有一个琢磨这套 制度的 进修 经过, 并且在彻底 领会 制度后利用 制度打破游戏,达成解密或意料之外的行为时的“我好 智慧”的体验。因此“元素” 体系更加需要充分表达各个物体所处的 情形, 并且确保这种 情形是容易被 领会的。经过了简化的当前温度只被我划分了简单的:冷/常温/热,三个档位。看似简单,但其实也是从复杂的档位做减法而得来的。具体 怎样表现可感知型等,可以看上面羊被点燃的动图,它利用自身的Fresnel颜色变化来表现当前的温度状况。这也是我们游戏提升沉浸感的一系列设计中的一环,也就是尽量的避免UI,用直观化的3D场景展现 情形和信息。这是我们这个慢节奏的轴侧视角游戏的优势。而对于可被利用性,其实就是 创新涌现性,其根本是在于单一元素与其他元素之间的“化学反应”。设计思路 - 元素间的反应
仔细想来,塞尔达并没有真正意义上的元素反应。它只有很基础的元素间的物理影响,比如冰冻的敌人摩擦力低,可以被风吹走。以及火焰可以沿着草地蔓延等。我们期望设计元素间的反应,是希望利用这个反应达成 下面内容目的:- 核心目的:增加游戏元素 体系的深度,为解密服务为主,为战斗服务次之。
- 它具有一定的平衡性,单一元素反应的 结局尽量即可以成为解密的软锁/硬锁,也可以被玩家所用获得优势。
- 它足够的直观清晰。 由于我们游戏的一大体验乐趣是玩家通过自己的研究和琢磨发现了一些道具的 独特用法。因此我们游戏中不能弹出详细的文字教学告知玩家一些道具的用法,这样剥夺了玩家通过研究掌握乐趣的方式。但同时我们游戏也依然需要有及其充分的关卡引导教学,通过关卡设计的方式来让玩家 领会机制的运作。而足够直观清晰是这一切成立的前提(不然关卡真的不知道 如何做教学了)
现在我们来引入一个新的元素,一个塞尔达中没有的元素,比如游戏中常见的毒元素。我们来 思索毒与火一起反应会发生 何呢? 由于我是神界玩家,我最直觉的认知是毒+火会爆炸。OK,那这个就设计完毕了... 是不是有点过于简单了。但这里其实可以与其他 几许我们组内想到的方案进行对比, 接着我们可以在这个 经过中 拓展资料出设计 制度。我们的脑暴如下:毒+火=毒液被蒸发升华首先,元素反应作为这个 体系的涌现式的根基,我们希望的 一个元素反应可以最大化的影响玩家的决策。同时这个元素反应需要兼容到我们游戏的角色3C和关卡构成上。 接着,我们来看这个设计,这个方案限制了毒的初始表现形式必须是毒液。一种 领会方式是:通过火来蒸发毒液, 创新出毒雾。毒液与毒雾的最大区别可能是Y轴上的影响范围的区别,但角色本身是站在地上的,毒液与毒雾对于玩家自身3C而言没有区别, 由于我们是一款轴侧视角的游戏,可以预见的大部分敌人也都是地上的,毒液与毒雾的表现形式并没有带来本质的变化。也许可能的 影响是部分怪被设置在高处需要用毒雾去击杀,或者是有些在高处的机关需要用毒雾激活。但实际上这些应用方式基本都还轮不到火来出场,如果把毒的表现形式一开始就设计成毒雾,利用雾气向下走的一些物理规律,或者单独可以喷出毒雾的机关就足以实现了。没有必要浪费一个化学反应位置。另一种 领会方式是清除毒雾/毒液,这个说实话比毒液蒸发升华要好一点。 由于它对于玩家和敌人具有决策层面上的影响,也可以设计需要清除毒通过的区域等。不过这种影响比较单一,容易变成火是解毒的Key这样一种单一 思索,无论是对于玩家还是谜题设计者而言。毒+火= 独特火焰(附毒的火,地狱之火)这个设计是极其的容易踩坑,对于CRPG、动作RPG或者战棋类游戏而言,这个设计算得上 一个不错的设计:- 数值提升: 由于前面说的这些类游戏本身就比较重数值,游戏本身就会建立玩家对于数值的敏感度,因此这个 独特火焰可以表现为一种数值提升,或者针对某一类怪物的弱点攻击。
- 一种新元素(复合元素): 独特火焰它可以变为第三种元素,也就是这个 独特火焰还可以与更多的元素继续反应,这在于回合制等超慢节奏或者超长TTK的游戏里而言 一个非常好的设计,它增大了玩家与元素 体系之间通过技能操作带来的交互感。
但对于我们游戏而言,其劣势还是挺大的:- 单纯的复合元素:首先,对于我们游戏而言,复合元素如果没有新机制,它就不能叫元素反应。一个同时具有毒的特性+火的特性的元素能干 何?能造成更大的伤害吗?当然我们可以设计这种 物品,比如我们游戏中现在实装有一个技能,它是 创新一个龙卷风,其本身具有风的元素效果,同时可以被火点燃,变为火龙卷。我们单独的将这一类机制归纳为元素染色机制,他是某些个技能所独有的效果。 然而他不是元素反应, 由于我们需要利用其 创新解密, 创新限制条件和优势条件。
- 新机制的复合元素:我们可以脑暴一下这个 独特火焰可以带来的新机制,解密角度而言,比如它能烧开 独特的障碍物,点燃 独特的机关,比火有更高的温度,或者是冷火。似乎很难摆脱专门为他设计的 独特机关,更高的温度也难以直观表达,冷火不如我直接设计冰元素出来。再加上我们游戏并非回合制,TTK也不长,这个元素在战斗中除非数值强的爆炸,或者针对怪物做机制表现,不然玩家很难感知其威力的差异。 并且其需要两种元素复合才能诞生出来效果,还需要考虑玩家的投入产出比,如果你想让这个机制在游戏中发挥 影响,就需要让他强到玩家愿意攒两个甚至多个技能才 创新出两种元素并让他们复合。如果其中一种元素,比如说毒,脱离开玩家技能变为场地效果的话。那 创新 独特火焰就变成了一种 独特机关用 独特解法的,钥匙对应门的关系,不符合我们元素反应 体系的设计初衷(拓展深度)。
毒+火=爆炸现在回看爆炸,就可以发现它可以有 下面内容设计优点了:- 表现直观:它的表现是巨大,清晰,单次的,易于展现
- 平衡设计:爆炸本身带火属性,可以点燃附近的其他毒,可以快速的清除毒雾,同时针对附近的怪也有爆炸伤害,这是优势。但同时爆炸也会炸到离得近的玩家,因此攻击距离近的火焰技能虽然可以快速清毒,但却会伤及玩家自身,玩家需要寻找远程火技能或者其他方式来产生火,这是限制。又同时 由于爆炸会引爆其他毒形成了传导功能,可以用于设计谜题,延申玩家的火技能的触达范围,这是优势。同时 由于连环爆炸,可能有一些玩家需要想办法获得 物品在毒当中,玩家得用别的方式清毒或者是阻断毒雾的连接,否则 物品就会被毒的连环爆炸炸碎,这是限制。
- 元素兼容:假设我们抽象一个叫做冲击力的概念出来的话,爆炸无疑是冲击力很强的行为。因此我们可以在游戏底层设计一套冲击力机制,所有的攻击判定都有对应的冲击力等级,这样就等于 创新了一项耳机的元素。这样我们可以设计需要爆炸才能炸开的石头墙,但它不会爆炸成为让简单的钥匙对应门的关系,而依然符合元素 体系的设计初衷, 由于只要是达到了类似爆炸等级的冲击力攻击,都可以炸碎这一堵墙,比如Boss的攻击,比如后期才会提供给玩家的更强的技能,比如场地机关或道具,比如将来的其他元素反应 结局。这样我们就用一套新的元素 体系避免了爆炸的单一性,实现了涌现性。
自此我们可以 拓展资料涌现性的设计 技巧了:- 如果遇到了钥匙对应门的简单对应关系,尝试设计更底层的 体系兜住所有的机制,打通它们。让玩家可以 领会其中的 制度,并利用 体系。
- 最好找到 杰出的平衡点。像是上面的毒火爆炸其实并不是一开始我们就 觉悟到 怎样去设计它的平衡性的,都是在考虑其各种表现的 结局后统一实现的。比如我还设计了一种专门的敌人,它身上会带有毒气,玩家难以近战应对它。前期玩家需要利用风元素短暂吹散它身上的毒气, 接着利用其他手段战斗,中期获取近战火技能后虽然可以一放技能就炸对方一大罐血,但同时也会炸到自身。此时 领会机制的玩家,可以驱散敌人毒雾后点燃敌人, 接着远离,敌人重新覆盖毒雾后会直接触发火焰爆炸把自己炸死。这就是中期对于乐于 思索的玩家的奖励。而后期拥有了远程火技能,这个小怪用毒 创新的限制就完全变为了玩家的优势,直接无脑秒杀,利用机制来让玩家感受到自身成长的强大。
元素反应代码构建
元素反应的前提是每个物体需要清晰的记录自己处于 何“ 情形”,我们可以用Buff的方式去 领会这些 情形。 接着我们可以建立其一个元素反应关系表,表格的基本逻辑是,当有一个新的元素要被加入的时候,它有几种元素反应公式(配方)?每一种配方按照优先级顺序排序,配方包含产出 何Buff,需要角色同时具有哪些其他Buff才能产出?
比如上图,燃烧 情形首先检查角色自身是否潮湿,湿了就进入蒸汽 情形;否则检查角色是否被冰冻,是就进入潮湿 情形;否则检查角色是中毒,是就进入爆炸 情形。这样玩家也就会发现,角色潮湿或者冻结的时候中毒,被点火不会爆炸欸。这算是比较符合直觉的。而对于Buff管理, 由于我们游戏中不仅仅是玩家或敌人会有Buff,所有能参与到元素反应 体系的物体都会有Buff。因此我们可以建立统一逻辑的BuffRuntimeEntity,挂载到所有需要参与元素反应的物体上,同时我们也可以建立通用的组件化代码,将温度传感器 毒雾传感器 受击Box等这种检测器直接与BuffRuntimeEntity建立连接关系,避免重复编写代码。
像这样,编写一个专门联通BuffRuntimeEntity(Buff Runner)与毒雾传感器的代码模块。这样如果我们需要让毒雾传感器直接向Buff Runner报告 情形,就只需要加上它就可以了。而对于部分需要对中毒逻辑单独处理的物体,就可以不添加这个连接模块,编写新的代码即可。这样传感器与Buff管理器之间也解耦了。在Buff Runner中我们编写各个Buff添加入角色/物品的Buff池的添加逻辑,处理好元素反应即可。 接着再用委托的方式,解耦的调用各个Buff被添加到不同物体上的效果。
同时,我们利用建立一个Buff Manager来统一管理所有的Buff Runner,不然每个Buff都要分别在每一帧检测自己有没有需要更新的Buff。可以当一个Buff Runner身上有Buff时才向Buff Manager进行注册,由Buff Manager进行统一调度管理。
另外 由于Buff具有大量的计时需求,最好的 行为应当是每一个Buff记录自己的过期 时刻,每帧更新时只需要比对游戏 时刻与过期 时刻就好了,不需要每帧单独的更新一个Buff的Timer。