February 6, 2025

实现技能系统的动态伤害计算

前言

最近在开发一个自走棋+塔防的游戏,游戏玩法的最底层逻辑来自塔对敌人造成伤害,击杀敌人防止敌人移动至终点,游戏中的塔和敌人有各种各样的Buff和技能,在自走棋的玩法下,还存在一个羁绊系统,不同羁绊的数值也会影响技能和Buff的数值,虽然是使用的一个开源的基于UE的GAS思想开发的技能系统,但要在原有的框架下,满足复杂的动态伤害计算还要易于拓展也是极其难的一件事,在看了100万遍GAS相关资料,还跟完了一个Unity的技能系统开发教程,终于是从中有些许启发(当然直接学习UE的GAS是最好的路子,但是对于我UE零基础来说没有更多的时间去折腾了QWQ)

本文只进行了普通的代码和基础的思路展示,目前的动态伤害计算还需要在更多的技能需求中不断迭代

失败的做法

GAS的框架下,【塔】攻击【敌人】 最简单最直观也最符合直觉的做法就是,【塔】对敌人施加一个【伤害GE】,这个GE的Modifier使用一个AttrbuteBasedMMC获取【塔】的攻击力,并作用于【敌人】的生命值,完成了基于塔攻击力,对敌人造成伤害的效果

但当需要实现一个暴击的效果怎么做?一个最直接的想法就是在每次Apply【伤害GE】时,根据暴击率进行判定,判定成功则将塔的攻击力乘以暴击倍率,在对敌人施加GE后再恢复原攻击力

这样做的弊端在于,单一的伤害效果与单位的Attribute相互耦合,当【其他的伤害GE】需要同样的Attribute做计算时,就会被影响,当然不同的伤害GE用不同的攻击力Attribute也是一种办法,但是显然这种方式极其不优雅

我称这种伤害为特殊伤害效果,暴击是一个简单的例子,简单来讲,暴击是基于概率的,如果有一个技能,会根据目标的某种Buff层数,造成额外伤害,则这是基于Buff的,如果塔存在一个防御值,则对伤害进行减伤,当然还有各种千奇百怪的情况

并且这种方案在每个需要有特殊伤害效果的地方Apply都要在其前后改变Attribute的数值,这种方式显然不妥,在游戏的各种技能越来越多后,多种特殊伤害效果叠加,伤害效果还要进行动态计算时,其维护难度是指数级上升的

需要解决的问题

前面一种拍脑袋方案在我开发了七八个Ability和十几个GE后慢慢感受到了不对劲,照这样下去,我将在开发第20个技能的时候,花费一整年的时间开发这个技能,并花大量的时间适配先前开发的一些技能,并修复新出现的几十个Bug(bushi

动态伤害计算的复杂性主要体现在以下几个方面:

重新审视这个问题,一个GE对目标造成的伤害应该是

最终伤害=基础伤害值 + 特殊伤害效果1+ 特殊伤害效果2+ 特殊伤害效果3+ 特殊伤害效果……

举个例子,当特殊伤害效果是暴击时,那么就需要对暴击进行判定,判定成功则计算暴击值作为一个加权值

当特殊伤害效果为,当每第五次攻击时,本次攻击造成120%的伤害,那么就计算攻击力Attribute * 0.2作为该效果的加权值

总结特殊伤害效果就是一个加权值,而一个动态的伤害数值的GE就需要知道特殊伤害效果,能不能加(条件),加多少(逻辑

对于GE来说,条件可以使用Tag来简单配置,但是显然难以满足像“每第五次攻击”,“当判定暴击”这种需求,而GE作为一个单纯的配置文件,是无法实现很多复杂伤害效果的逻辑的,但我们又不能在应用GE时去做条件判定和逻辑运算(前面说到这种方式的不妥)

那么我们的需求就在于:使伤害计算有较高的颗粒度,低耦合,伤害计算模块化,可配置

解决方案

我从MMC中得到启发,结合SetByCaller得出目前的解决方案

Adjustment系统

这里引入Adjustment的概念,我们称其为调整器系统,作用于GE(就像MMC作用于GE一样)

核心设计思想

他是一个简单的抽象类,包含两个方法,这两个方法也就是我们前面说到的条件逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class GameplayEffectAdjustment : ScriptableObject
{
public virtual bool CanApplyAdjustment(
GameplayEffectSpec effectSpec,
AbilitySystemComponent source,
AbilitySystemComponent target)
{
return true;
}

public abstract void ApplyAdjustment(
GameplayEffectSpec effectSpec,
AbilitySystemComponent source,
AbilitySystemComponent target);
}

然后在GE的配置处,除了常规的Modifier,Tag,叠层等配置外,新增目标调整器源调整器,让设计者可以自由配置

在AbilitySystemCompoent中声明两个列表用于存储来源方目标方的调整器,并在需要应用调整器时,调用一下以下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void TryApplyGameplayEffectAdjustments(
GameplayEffectSpec spec,
AbilitySystemComponent source,
AbilitySystemComponent target)
{
// 处理来源方的调整器
foreach (var adjPair in source.ActiveSourceAdjustments)
{
var adjustments = adjPair.Value;
foreach (var adjustment in adjustments)
{
if (adjustment.CanApplyAdjustment(spec, source, target))
{
adjustment.ApplyAdjustment(spec, source, target);
}
}
}
// 处理目标方的调整器
foreach (var adjPair in target.ActiveTargetAdjustments)
{
var adjustments = adjPair.Value;
foreach (var adjustment in adjustments)
{
if (adjustment.CanApplyAdjustment(spec, source, target))
{
adjustment.ApplyAdjustment(spec, source, target);
}
}
}
}

使用者只需要继承GameplayEffectAdjustment并实现条件和逻辑的代码,且以一种低耦合的状态存在,并自由配置到任意GE上

在为任意目标添加该GE时,就会在ACS的对应列表中存储调整器

下面以一个简单的示意图展示整体的流程

致谢

一个UEGAS系统的Unity实现的开源框架,虽然功能还有待完善,但目前的版本给我的开发带来了极大的便利,感谢作者EX-Hard,赞美开源!
https://github.com/No78Vino/gameplay-ability-system-for-unity

About this Post

This post is written by Yuan, licensed under CC BY-NC 4.0.

#Unity