战斗框架迭代二三事

前言

在战斗框架迭代中一些个人的经验记录。

质量保障

做迭代的第一目标,不要因为迭代搞出一堆的BUG!

比较不幸的是,由于项目QA人手不够和缺乏相关工具的原因,对于战斗框架的迭代并没有整理出一个很安全的验证手段。后续如果有机会的话还是应该做一些自动化验证的方式保证质量的。

目前主要的测试手段仅有:

  • 提前储存对应版本的线上战斗录像,在一段功能开发完成后批量跑录像。以作简单的报错验证功能。

    最通用的功能,但是由于帧同步游戏,在逐步开发后出现不同步现象,基本失去了战斗的有效性

  • 对于比较容易复现的情况,开发时写出对应的发现方式和DEBUG指令。

    这种方式比较吃人力,但是目前也没有好的方案。

  • 自己开发的测试录像系统,能够在本地报错时存储这段战斗的单机录像,也能够报错后直接回放出现对应战局。

    结合上一条,算是对QA人力的优化

  • 开发了一个战斗信息记录工具,记录BUFF的添加/免疫/移除、造成伤害、技能使用、道具刷新等战斗常用信息,能让让QA发现错误后给程序高效率的还原出战斗现场。

    也是一点QA优化

玩家类的抽象

原本游戏中并没有做玩家这个身份的抽象,所有玩家相关信息均是构建战局时创建初始角色时直接添加到Character身上的。仅在ActorManager中做了一些特殊索引和记号,于是会有很多抽象的事,比如

  • 玩家创建的召唤物Actor召唤的道具的特效需要传递一个归属Actor信息,否则没有办法根据玩家选择的皮肤做变化。

在本次迭代中,在战斗框架中额外提取出了Player玩家这个概念,以PlayerManager进行管理。主要做了一些

  • 战斗外的一些技能选择信息迁移
  • 玩家的皮肤和相关的特效变化信息、一些如喊招的局外信息记录
  • 玩家的指令流不直接转到Actor身上通过递归方式传递,能够比较清晰的处理到玩家的变身、操控等机制上
  • 更好的划分一些伤害来源、任务计数等效果。能够准确地反应一些战斗数据归属于玩家。
  • 可以比较合适的创建玩家的机制,比如某些积分赛玩法中攻击玩家获得积分,当玩家控制的角色转换后在不同角色身上使用同一个组件维护信息。
  • 统一了一些归属于玩家的机制,例如扔出道具、被操控的角色都可以用统一接口尝试判断是否和玩家产生关联。

在这里主要是做了很多结构上的分离后对框架优化了,属于为老框架填坑。本身并没有很特殊的区分。

这里主要用到LUA的一个性质,函数也是一个一等公民可以作为变量,同时通过闭包可以获取一些upValue来储存数据,这意味着我们可以做出共享组件在不同Character间共享接口和调用函数,减少一些二次获取的代码复杂度。

移动机制的组件化

整理目前游戏中存在的位移系统,主要归纳如下:

  1. Dash:冲锋类效果,特点为固定朝前方向持续移动

    关键词:持续、连续

  2. Carried:携带类效果,特点为持续将一个角色的位置和另一个角色/道具始终同步到另一个角色身上。

    关键词:持续、连续

  3. Followed:固定朝着某个位置持续移动,持续通过速度/施力/设位置的方式移动角色朝目标位置

    关键词:持续、连续

  4. Transport:直接改变指定角色位置功能

    关键词:瞬时、大距离

  5. Throw / Jump:提供一个瞬间的速度/力改动

    关键词:瞬时、连续

  6. Catch:禁用一段时间Body后,设置到某些位置

    关键词:延时、大距离

目前,由于缺乏统一规范,大部分位移技能效果都是通过定时器进行持续的位置设置,缺乏明确的时序、优先级和状态区分。这导致了以下几个问题:

  1. 实现方式不一致,如冲刺效果有速度实现/施力实现/连续设置位置实现
  2. 当多个技能效果同时作用于一个角色时,最终结果会根据添加顺序覆盖。尤其是涉及到碰撞顺序、角色技能触发等不同时序的情况
  3. 部分老旧角色代码未能全面兼容,使用缓存的角色位置数据可能导致角色位置设置异常。
  4. 目前并没有明确的状态管理,现在能够兼容主要是依靠各类Prepare的使用限制/Buff添加的效果来侧面做的管理
  5. 每个技能都自己实现了一套事件监控和异常处理,但是不能确定所有技能都妥善处理了和其他效果的兼容

为了优化这一情况,将常用的移动效果进行封装,并提供统一的接口。对老代码均做统一迭代。

这里有一定困难点是如何知道哪些老代码实现了移动相关功能。由于脚本语言是LUA写的,同时接口命名没有特异性(谁知道SetPosition是节点还是道具还是角色啊)导致难以查出相应功能实现。

最后通过Hook角色对应接口和人力排查技能逻辑完成这部分。在多次尝试后确认基本不存在老代码直接调用移动接口了。

通用能力系统

原本战斗方面主要分为技能SKILL负责逻辑,以及BUFF部分负责数值。

但是目前有一些需求,本身存在逻辑,但是和原本的技能体系不太一致。如:

  • 不具备升级需求
  • 会有频繁的动态启用和关闭的需求
  • 有可能存在多个来源的情况,需要避免来源间互相干扰
  • 不需要展示,不必兼容技能表中的大量无用字段
  • 一般不和玩家操作相关

完全使用技能来制作的话,会存在多个技能配置成本高的情况,策划对于技能配表的复杂性深有体会、

如果使用BUFF来做的话,由于其包含参数,会多次拓展BUFF类型,也会增大BUFF配置成本。

使用技能组件或BUFF组件的话,由于组件配表容易被忽略,后续迭代时难以发现可能存在的逻辑。

因此在战斗中拓展新的能力机制Ability以处理这种情况。

本身和技能逻辑基本一致,主要特点如下:

  • 不存在升级,一个能力只导入一次配表。
  • 使用层数机制计算来源、多个来源添加同一个能力会导致计数增加,移除时也只移除对应的层数
  • 使用激活与否来管理是否起效,一次引入后,直到游戏结束前都不会移除。
  • 配置方面省略无效字段,比较简洁
  • 不在游戏中途触发销毁,减少中途销毁的风险
  • 可以被技能和BUFF通过配表引入,也可以技能代码中直接添加

比较常见的能力系统主要引用如临时免死功能、多个来源的特殊数值叠加等方面。

伤害系统迭代

伤害计算在最初设计是一个非常面向过程的流程。

一般来说,伤害都是在BUFF系统中,在函数中获取到BUFF配表的基础数值、或获取来源和目标的影响参数,例如攻击防御等属性值、然后根据基本规则如保护性衰减、再触发许多技能的回调机制,结合成一个公式计算出伤害具体数值,应用到生命值变化上。

这是一个最基础的伤害系统框架,一个函数中的公式。

但是在各种各样的技能机制拓展、例如分摊伤害、伤害额外增加等机制增加后。这个公式已经有超过三十多种影响因素了。尤其是很多以注册回调方式存在的伤害影响因素根本无法事先事后判断出来。

因此这对游戏的开发迭代和测试都造成了极大的麻烦,尤其是我们项目前期没有做好文档维护的情况下更是愈发严峻,例如:

  1. 策划无法预测自己改动伤害计算流程的成本,只能不断新加配置项,最后导致了防御、防御2、百分比防御、百分比防御3、提前百分比防御、固定伤害值等各种机制卡在公式中。
  2. 难以测试,QA在测试中也无法判断某些伤害是否符合预期,只能发给程序策划联合排查。整体耗时太长成本太高。

为此,需要对伤害系统进行迭代。

用聚合器的方式重构,将伤害计算本身作为一个对象DamageCompute

所以的伤害数值改动,我们称之为伤害影响DamageAdjust。包括增减、乘除都作为一个伤害影响保存在伤害类中,在出具体数值时,就像树遍历一样按照优先级和先后顺序启用,计算其导致的数值变化。

为了增强策划的可控性,在BUFF添加计算中,我们允许额外指定一个伤害修改 DamgeModify,可以在运行时,对某个具体的BUFF添加过程中应用。调整本次的伤害类。

伤害修饰通过两个方面调整伤害的计算过程:

  • 它可以禁用某个来源因素的影响,比如不考虑防御力,不考虑攻击力。通过这种方式来制作某些不使用伤害的过程。
  • 它可以修改伤害影响的优先级,比如可以临时将百分比防御的优先级调整到固定攻击之前,通过这种微调公式的方式也可以做一些临时的测试。

在伤害本身作为对象后,我们就可以事后拿出储存的信息。也同步更改了战斗相关的信息界面,可以对于某次单独伤害展开所有的影响因素和中间数值,方便QA和策划快速判断伤害计算是否异常,异常出现在何处,能够快速排查到相关问题。

在实测中,伤害计算的耗时还是增加了一些,但是由于伤害迭代本身是作为BUFF和数值迭代的一部分,这部分性能增加被其他部分的优化抵掉了。因此从整体迭代上来来说还是OK的。