浅谈技能系统
前言
在比较复杂的游戏中,最为关键的,也就是技能系统了。
技能系统,很容易遭到程序员、策划、测试,甚至于玩家的挑战。一个技能框架的可读性、扩展性、安全性以及健壮性都是极其重要的,我们作为一个设计者,必须要把这些问题都考虑在内。作为一个综合性系统,程序、美术、音效,甚至于策划精妙绝伦或着是与无理取闹的需求,我们都必须要考虑进去。
在技能系统这一块,也许算是我做的比较多的,也是思考的最为深入的一个Gameplay的系统。
所以忍不住想写那么一些东西,去分享一下我的想法、以及做法。
抛开需求
我们先把策划人员的需求放下,我们当作没有这一类人。
我们仔细想想我们曾经玩过的游戏,当然,或多或少你是玩过游戏的,不然我很怀疑你是怎么做到游戏程序员的。
技能究竟是什么?
如下图,在一个典型的技能里,它通常都包含有动作、特效、声效,以及本次影响某些作用,以及持续影响某些作用或表现。至于持续影响的部分,则是可能有其他系统接管的,例如:BUFF系统(见 链接)。
为什么我们将一个技能分成两种部分,一是表现上的,一个是数值上的。从一个设计者的角度而言,如果这次的技能发生了,并且确确实实的命中的目标,我们就应该进行这次的结算,不管表现是否由于什么原因进行了中断,或者跳过。并且,如果有伺服器进行参与的时候,我们总是应该以服务器为准。
所以,为了更好的复用我们的代码(客户端以及伺服器同一套代码时,或者是跨项目间的重用),我们在设计技能系统的时候,一般都会注意,将表现的控制,以及数值处理上的控制分开(也就是所谓的逻辑与表现分离)。对于一些非常成熟的项目框架,甚至于我们可能还会在我们的技能系统上继承于某些Interface,例如,ISubSystem(ISubSystem可能是我们的框架提供出来,用于系统级扩展的一些接口),这种设计的框架,可能会给人一种,稍微优雅的直观感受。这时,我们可能会忍不住的Show一下自己的设计,于是搞了一个对于我们系统稍作抽象的Interface,叫IAbilitySystem(派生于ISubSystem)。然后再写一个叫做AbilitySystem去实现我们的技能系统,然后美曰其名的笑谈:以后别人看不顺眼我们的AbilitySystem,就可以写一个叫做SkillSystem的玩意去替代,反正我们外部持有的对象是ISubSystem/IAbilitySystem。
这种情况下,你的代码固然丑(很多程序员都认为别人的代码很丑,没有问题吧),但也会因为设计良好而被人点赞。
你付出的,仅仅是对你愿意暴露出去的API,再在interface声明一次。麻烦?也许吧。所以这就是设计者该去权衡的地方了,一旦不当,就成了所谓的设计过度。
在现在这种情况下,我们可以简单的分出几个类型了,这里不探讨设计过度与否。
把这个ISubSystem扩展开,其实就很接近我们整个Gameplay框架的一个原型了。当然这已经不是我们聊的事情了。
技能框架的思考
如果我们的野心足够大,我们可以想要的会是一个让我们一劳永逸的技能框架,这个想法有些膨胀了,但其实我是觉得可以做到的。尽管不是一个可以应用于所有类型的游戏的技能体系,但框架本身是可以被复用的,代码不行,思路也一样。对于不同的技能体系而言,这应该有些困难,因为我们思考的方向、要点、难点,其实都不一样。
比如一个回合制的战斗系统(梦幻模拟战),跟一个即时战斗的ACT战斗系统(战神),不管是设计难度还是实现难度,都相差十分巨大,巨大到跟你知道的你跟马云的差别一样。
所以,不管是为了效率、实现难度,我们可能会针对不同的技能应用场合,设计不同的技能系统,并选择一个最优解。尽管他们的大体框架很接近,但是细节上的实现却是完全颠覆的。
我们以 回合制(Turn-based)的游戏来开始我们的话题。
思考下面的技能。
火球:角色朝前方发射一个火球,火球命中目标(角色)的时候,对目标造成伤害,火球消失。
大爆炸火球:角色朝前方发射一个火球,火球命中目标(障碍、角色)的时候,造成爆炸,对周围的目标造成伤害,并且对爆炸波及的目标,造成持续性灼烧伤害。
如果是有实现过技能系统的同学,自然很容易就知道,技能 火球的实现。
我们先来看看一个“十分切题”的代码, 这里的十分切题并不意味着是最优解。
public class Fireball : IAbility
{
/// <summary>
/// Config {
/// float velocity; // 速度
/// float radius; // 火球半径
/// string fxName; // 特效名称
/// //其他
/// }
/// </summary>
public FireBallConfig Config { get; private set; }
/// <summary>
/// 技能持有者
/// </summary>
public IAbilityOwner Owner { get; set; }
public void Launch(Action<FireBall, List<IAbilityTarget>> onHit)
{
// 执行动作表现
// 延迟 0-n秒 执行特效表现
// 延迟 0-n秒 播放飞行声效
// 命中后回调
}
}
乍看之下,这种代码好像没有什么问题。但是一旦选择这种代码,我们再去思考一下,需求人员如何跟我们进行数据以及功能的交换,美术人员如何参与技能表现的调优?
可能造成如下结果:
一:
策划人员维护一张超级庞大的表,然后所有的技能配置项都填在里面,每一行就是一个技能,但是由于字段含义,势必会造成一行中很多空缺字段,维护难度,增删难度,剧增。
二:
策划人员维护一大堆技能表,不同的技能表分别配置不同类型的技能。很大情况下,策划人员可能后期都在找这个技能配置在哪个表中。
三:
一旦美术同学发现哪里需要调优,我们可能要先把美术同学的资源放进来,然后经过一大堆的游戏逻辑,终于到了可以展示我们技能的场景,好不容易等到MP满了,我们可以释放我们的技能看看了( 别怀疑,这种做法在以前很盛行,其实更多的主要是项目一直处于所谓的紧张期,当然还有很重要的一个原因,所使用的引擎太土鳖)。
四:
它无法量产。
五:
它的命名太过“策划化”了,如果我们要实现一模一样的功能,但是名字叫做冰刺(Icicle)的技能,然后用了火球(Fireball)这个名字的技能,总觉得有一种莫名的喜感,就是不知道策划同学会不会无所谓就是了。
换句话说, 如果这是一个大型项目,我觉得上面的代码完全是技能系统的外行,很有可能一开始设计这种技能的人到了后期每天会被鞭一百次。当然, 对于不注重技能扩展性,或者是技能所在的比重并不大的项目而言,这完全是没有问题的,因为这都不是重点,也不是需要量产的功能,自然也就不是问题了。将就一下,随便就过去了。
我觉得,作为一个合格的技能体系,它应该至少符合下面的几点要求。
1. 代码尽可能优雅的。
2. 框架是可以轻易扩展的。
3. 可以量产的。
4. 可以游离于游戏之外,单独进行重放的。
那么,有没有那么一个共同的抽象,可以让我们做到这种技能体系呢?
我们尝试把数值逻辑与表现逻辑分开。
在表现层上,我们需要一个工具/插件,它至少符合以下几点:
1. 支持保序的表现
2. 可以扩展的
3. 可以同时编辑我们不同类型的Track的
4. 可以离线验证我们技能
5. 可以与数值逻辑部分交换上下文的事件系统(EventSystem、EventManager)
如果你使用Unity,可以考察一下uSequencer/Timeline系统。
如果你使用UE4,毫无疑问自然要考虑的就是Blueprint了。
技能框架的设计
我们这是一个框架,应该从全面的角度去思考问题,如果单纯的从某个技能出发,是很难设计出一个可以扩展的框架的。
假设我们在与策划人员在参与讨论的时候,已经对即将要去实现的技能体系有了大概的认知。
我们可以尝试从以下方面去考虑一下,我们的技能能不能这样。
1. 对技能的特点进行抽象,要知道,策划人员往往并不具备技能特征抽象的能力,或者说习惯。
2. 对技能需要配置的数据项进行归纳,往往这些数据项都是需要进行随时配置的,我们要对策划描述中出现的数值/词语变得敏感。比如,速度、方向、重力、加速度、大小等物理特征,都可能需要程序人员转化成数值配置项。
3. 尝试着将技能套入我们的技能框架。
4. 与策划人员进行反复沟通,看看能不能尽可能的切入我们已有的技能体系,而不要做一只特立独行的鸡。功能需求,是需要沟通的,不是需求方让你做什么,你就做什么,做出来了,就觉得很厉害,做不出来就不行。
5. 给你编辑好的技能,一个合适的命名。合适的技能命名会具有比较高的辨识度,可以让你看见它的时候,一下子反应出这是什么,而不是一个黑人问号脸。
6. 你的技能应该如何设计成可以独立运行。有时候,我们虽然知道数值逻辑与表现逻辑需要分离,但是实现起来可能并不是那么轻松的,你需要绞尽脑汁的将数据项从表现系统抽离。一个最漂亮的做法无异于:我们可以在一个空场景中,加载1到N个角色,这些角色,至少包含一个是技能发起方,其他的自然是可被搜寻的对象。这个技能施展的时候,这个技能发起方可以有一些动作,动作伴随特效,然后他往前一砍,一个特效往前飞行,碰到某个可以被搜寻的对象的时候,这个对象会被捕获到你的技能流程里面,交由技能流程做处理,这个技能流程可能需要进行例如这个对象是否HP为0等等一系列是否符合捕获条件的判断。然后,这个对象头上可能会冒出一些伤害数值,以及受击特效,受击动作,如果技能可能把一个角色打飞,可能这时候还会对应不同的后续处理。
7. 往往一个技能表现会有两部分。一部分是发起者(Sponsor)的,一部分则是回应者(Responders)的。对于类似于三国无双这种游戏而言,可能一刀下去就是一大片的。这种情况自然也要纳入考虑。
8. 更复杂的一些技能系统,可能会存在着连携技能。就是你跟另一个合作伙伴同时在使用可以共鸣的一些技能的时候,会产生出双人组合技,这种系统就要看具体的需求了。
我们看看能不能把技能Sponsor部分按照下面的方式划分,还有声效部分就不写了。
对于Responders方,我们看看能不能分成这样一种方式。
接下来,就是对我们抽象出来的节点做一个代码实现的事情了。
这两部分串行起来的方式,其实就是OnHit,每次OnHit就会触发一类受击表现。
然而,总会出现有非常规的问题,比如,霸体的受击表现。
这里可能就会出现一种情况,一种Attack表现,对应N中OnHit表现了。至于飘血、数值,走的又是另一套方式,一般可能会走EventMessage事件分发机制。这样既可以解耦,也可以在技能编辑器下免去没有数值处理对象的尴尬。
此外,我们还应该对技能描述尝试抽象出一个个独立的函数(或方法),这些方法我们可以重用在其他技能的Timeline(或BP)之中,例如,发射火球,这种功能,我们可以视为“Load and fire a VFX”;受击动作、攻击动作,可以视为“Play animation clip”等等。而且,这个才是所有技能设计之中,最应该优先分割、考虑的。也是我们未来可以真正复用到其他项目之中的技能功能子集。
来源:知乎 www.zhihu.com
作者: 青狐秀水
【知乎日报】千万用户的选择,做朋友圈里的新鲜事分享大牛。 点击下载