你说得对,但是EmiteInnaACTSystemFramework是一款由EmiteInna自主研发,适用于PC平台的Unity第三人称上帝视角3D动作游戏代码框架。您将在游戏里扮演一个名叫游戏程序的角色,通过跳跃,冲刺和各种各样的丝滑小连招击败你的对手,修复程序中的bug,找回失散多年的代码能力,发掘未知的设计模式和模拟算法,揭开游戏行业无法入行、毕业即失业的真相。

本篇是ACT系列之一,github仓库位于:https://github.com/EmiteInna/EmiteInnaActSystem

简述

技能系统的重构是一件非常有艺术性的行为,这也是为什么明明是这么麻烦的事情我还要坚持将其完成的原因。本篇日志比起实现和建模,更多的是对于技能这一必须存在但又复杂的机制做一个艺术和哲学上的思考。

在第五篇日志的时候我对技能进行了一系列的建模

这个也就是我一直在做的事情,为什么要这么做呢,是因为归根结底,在ACT中,技能这个东西,它可以总结为:“我释放某个技能→进入某个动画→在这个动画的某个时间我触发了某个效果”,而这句话中效果这个东西它当然是包含了音效和特效的,那么为什么我要把它和动作分开呢?因为动画是一个持续的过程,而效果是一个瞬间的过程(哪怕是协程类的持续性效果,它也是瞬间发生的),那么仔细想想瞬间的过程和持续的过程它的主要区别在哪呢?主要区别在于是否需要判断打断的逻辑,现在我们已经通过分层状态机把一些琐碎的判断去优化掉了,那么到底怎么样才能算是一个完整的技能,答案就只是加入了几句,变为了在如此的状态下-我释放某个技能-在技能的时长的某个时间中我播放某个动画-在技能时长的某个时间中我产生了什么效果-在技能时长的某个时间中我被打断了,这个打断使我进入了一个怎样的后续-在技能时长结束之后我进入下一个状态-或是释放下一个技能。

实际上,如果更加抽象地来看,技能更类似于角色进行了某一段程序:

将我们的角色视为os上的一个进程,在平时,它受到我们的控制并进行操作,我们把这个模式视作是一个一直在运行的技能,它会无限循环地进行下去。而在释放技能时,角色实际上进入了另外一种模式,玩家依然可以通过操作来影响角色的行为,但方式和之前的时候大相径庭,除此之外,这种类型的技能并非无限持续下去,而是有时候具有一定的时长。

通过这种思考方式,我们可以明显地看出——技能实际上是角色这个进程所在运行的一段程序。

什么是“时长”?时长并不是关键,实际上有些技能在放出来之后也可以无限地持续下去,关键是前一句——并非无限持续下去,这意味着技能和常规状态的本质区别:需要一个退出的模式,退出到哪呢?常规状态。

从这里我们可以看出来,其实技能无非是一个加强版的状态机,而加强的具体内容则是一个维度——时间,在时间维度的存在下,状态机的状态变化不再完全依赖外界的交互(在该语境下我们把玩家的操作也看做和外界的交互之一),而是自己达到某种时间时进行一系列的行为。

如此一来,我们就可以很轻松地建模技能系统,而且有一个很显然的可用参考:动画序列。

这也是为什么一开始我们在定制技能编辑器的时候,它呈现出的是一个带有时间轴的就像动画编辑器一样的轨道,并且我们可以在里面放置节点。

和原来模式的区别

仔细观察上面的解释和原本的建模,就会发现一些区别,其中最大的区别正是之前我强调过的那条——“退出”。

技能的退出并不是因为技能时长的到达,而是时长的到达会提醒角色“这个技能需要退出了!”。这两个过程完全不同,而它们最大的区别就体现在实际上技能的退出并不一定是因为时长的到达,而在之前的设计中我们却正是这样设计的。

最简单也是常见的例子就是动作游戏中的“取消后摇”操作,它在整个攻击动作做完之前就提前进入了移动状态来打断后摇,而这种东西的实现方法非常简单。

在之前我们将技能比作程序,而技能中的动画节点、音效节点、伤害节点显然都是这个程序中的一条条语句,那么打断显然也是。因此对于取消后摇的操作,我们只需要在允许打断的位置插入一个判断节点来判断是否打断技能就行了。比原来还要优雅和易于维护。

连招实现的头脑风暴

上面的讨论其实已经足够建模出一个技能的设计模式了,对于大部分的游戏来说它显然已经合格了,但是对于包含且强调连招的动作游戏,它还缺少一个非常关键的部分——多个技能之间的联动。

如果我们用之前单个技能的建模方式去实现连招,这个逻辑就会产生错误——因为在连招的每一段中玩家都进行了取消后摇的操作,而这个操作的结果是进入下一段连招而不是结束当前的技能,这意味着假如原本的单技能系统是一个函数,而取消后摇依靠的是直接return这个函数,而其它的语句都是一条条顺序执行的,那么现在的连招依然是一个函数,只不过中间加入了continue这个操作——在一定情况下它跳过了一些时间。

当然,我们可以就用这个建模方式,将技能节点作为一个一个语句加入到技能轨道这个函数之中,中间包含了一些if、continue、return,甚至更复杂的while、else等语义,这样毫无疑问也可以实现连招的机制。但我认为这里存在一个更优雅的方法,并且它能解决另外一个问题——单个技能的复用。

我们把技能系统和角色解耦单独拿出来,目的正是实现对技能的抽象,在这里不妨将这个抽象继续下去。我们规定单个技能就是一个只包含if、return和普通语句的函数,这个函数它肯定会有一个返回值,这个返回值能返回什么呢?当然是这个函数本身的运行状态,在脑暴出各种复杂的技能反馈之前,我们简单认为这个返回值返回的就是这个技能是否成功释放,而其它的返回值我们可以通过别的东西来返回。

为什么这样做呢?或者说为什么突然开始讨论返回值了呢?第一个问题是因为这个返回值是技能这个“函数”的返回值,而它的作用是为整个技能的逻辑实现服务,而第二个问题正是因为在上一节的讨论中我们认为“技能”就是技能系统里直接和角色联动的最上层了,但实际上并非如此,对于连招来说,一整套连招才是和角色联动的最上层。

换句话说,我们在技能之上还拥有一个对象,技能通过组合模式隶属于这个东西,而这个东西负责调节技能和技能之间的联动,而技能通过返回值的方式来将执行的信息传回这个东西,通过这种方式,我们实现了自己想要的东西。

树形结构思想——优秀的前辈行为树

上面提到的那个所谓的“对象”,以及技能在其中的存在方式、返回值的信息传递方式,熟悉游戏设计模式的同学应该很容易发现我正在构建的这个东西和什么是大同小异的——行为树。

于是到了最后我的整个技能系统的设计是这样的。

上述提到的“技能”,我称之为“轨道”(Track),里面包含一个时间轴,以及动画、音效、位移、伤害等一系列需要执行的逻辑(以节点形式存在),以及一个用于打断的节点,每个节点会拥有它的起始时间和持续时间以及触发间隔,在时间内每个间隔都会触发一次效果(伤害、打断判断常用),也可以设置为只在进入时执行一次(动画、音效、位移常用)

而那个“对象”,我称之为“行动”(Action),里面包含若干个节点,包括行为树的顺序节点、并行节点、选择节点等逻辑节点,而行为节点当然就是轨道本身了,以及一些预设的节点(比如用于连招的判断节点,包含一个判断输入的节点,如果在规定时间有输入则返回成功,否则返回失败),以及特殊的节点用于退出整个Action并让角色回到Idle状态。

这样一来连招的实现就很简单了——在一个顺序节点下将所有连招一字排开,然后在每个之间加入判断输入的节点,当然,通过一些条件节点也能够实现分支连招的逻辑。

后续

创立整个技能系统的目的除了将角色和技能解耦,当然还有一个重要的目的也就是实现可视化编辑了,实际上行为树这个设计模式就是为了实现优秀的可视化编辑才诞生的,那我这个技能系统当然最后也要实现可视化才行咯。

不过现在还没做好整个可视化内容,再摸会鱼吧。