概述
本文简对GCS战斗系统中的攻防流程进行介绍,用于帮助你更好地使用GCS。
我讨厌写这一部分的内容,因为这更像一个设计文档,而聪明的程序员完全可以反推并制作自己的版本。
发起攻击流程
任意对象,包括环境/陷阱,都能发起攻击,而游戏中的人形角色其攻击流程一般如下:

流程步骤
- 激活技能:Attacker(玩家/AI)激活不同的技能,如近战Ability,或者法术Ability。
- 发起攻击请求:技能会播放不同的战斗动作(动画),你在动画中放置AttackTrace/BulletTrace动画通知状态,并配置相关的攻击请求参数。
- Ability处理攻击请求:AttackTrace通知状态的开始和结束,会通过Gameplay事件(攻击信息)将攻击请求交由Ability处理。而Ability则会构建需要应用的GameplayEffects实例。
- Melee:如果是Melee请求,则会激活角色或者武器的碰撞检测实例(HitBox),并将相关信息传递给碰撞检测实例。碰撞检测实例获取到有效目标后,会告知Ability进行游戏效果应用。
- Bullet:如果是Bullet请求,则会通过子弹系统生成子弹实例,并将相关信息(游戏效果实例)传递给子弹实例。每一个子弹实例拥有自己的碰撞检测实例,当获取到有效目标后,由子弹将攻击信息应用到目标上。
游戏中更多环境,陷阱,也能发起攻击,只需要所需的攻击信息而已。
在攻击信息应用到目标之前,你都可以对过程中产生的游戏效果实例(GameplayEffectSpec/GameplayEffectContainerSpec)进行修改,比如:
- 将攻击定义中的数值参数传递到GE。
- 根据情况为GE添加不同的Tag并在之后的流程中使用(比如用Tag表示是否为火焰伤害或者魔法伤害)
- 子弹随着飞行距离衰减伤害。
- 子弹飞行过程中为GE添加新的目标。
整个攻击流程的最终目的是:生成并修改GameplayEffect实例,然后应用到目标。
伤害计算
发起攻击的最终的目的是为了给目标施加GameplayEffects,而在GameplayEffect执行过程中,会修改目标的特定属性。
GameplayEffects有很多种方式可以修改目标属性,GCS的参考内容提供了大量不同的Effects供你参考,其中:
GCS_Execution_Damage更是提供了一个魂类游戏中的伤害计算逻辑的默认的参考实现。
关于默认实现
该实现非常通用,且通过非常少量的GameplayAttributes实现了多类型的伤害和伤害减免(抵消)逻辑,且并非硬编码,你可以添加更多的伤害类型(如神圣伤害,神圣伤害减免等)。
你可以直接使用,也可以作为参考来创建你自己的版本。

得益于通用游戏技能系统,你可以完全通过蓝图来利用GAS的大量特性,而无需编写C++。
处理攻击的流程

战斗流程(CombatFlow)
CombatFlow是一个UObject,每一个战斗系统组件可以指定一个CombatFlow。
你应该继承内置的GCS_BaseCombatFlow
,并重写相关函数以实现你的自定义逻辑,或者参考现有的CombatFlow并创建一个全新的。
处理属性变更
在通过技能系统中的使用指南中提到,属性系统组件会将所有属性变更的回调转发到CombatFlow进行处理。因此你只需要重写HandleGameplayEffectExecute即可。

如何处理属性,并产生什么样的攻击结果,完全由你自己的游戏机制决定,而参考实现中提供了魂类游戏常见的流程。
产生攻击结果
当你在HandleGameplayEffectExecute中针对属性变化进行处理后,你应该根据你自己的游戏逻辑,将该过程中的信息转换成攻击结果,并注册到战斗系统组件。

例如:在处理完IncomingDamage后,你应该把相关信息构造为AttackResult结构体,并注册到战斗系统上。TaggedValues即包含了此过程中产生的一些信息,如“实际变化的血量”等。你也可以在这里
自定义CombatFlow
攻击者可以发起不同的攻击请求,但每一个目标对每一个攻击请求可能有不同的 “处理”。你可以通过自定义GCS_CombatFlow实现不同的攻击处理流程,并在CombatSystemComponent上配置不同的CombatFlow Class。
假设一种箭射向敌人,通常,它应该会触发命中反应。但是假设你的敌人体型很大,以至于你不希望他做出反应。这时你就可以指定不同的CombatFlow,而无需在攻击者层面进行大量的If/Else判断。
何时采用完全不同的CombatFlow?
1.你不希望一条狗可以弹反你的进攻。
2.你不希望超巨大的Boss会因为你帮他的脚挠痒而摔倒。
3.你不希望一个建筑物能够闪避你的进攻。
攻击请求(AttackRequest)
如果你想发起任何形式的攻击,你需要通过攻击请求。GCS有不同类型的攻击请求,如Melee或者Bullet。且攻击请求一般通过动画通知状态触发。
这一部分解释你如何发起不同的攻击请求。同时所有继承自GA_GCS_Attack
的GameplayAbility会自动处理攻击请求。
近战攻击
当一个近战攻击技能播放一个Montage时,你可以在合适的攻击帧范围内为其添加ANS_GCS_AttackTrace
(动画通知状态)来配置一个攻击请求。

- TraceToControls:定义了在动画通知状态激活过程中,需要激活的
碰撞检测实例
。 - AttackDefinitionHandle:指向攻击定义表中的某一个攻击定义,它包含“这一击”相关的所有静态数据(如攻击力,Cost等信息)。
上图动作是用右手武器攻击,所以应该开启名为"RightHandWeapon"的检测实例(它绑定了武器的Mesh)
如果动作是用拳头攻击或者用脚踩踏,你也应该开启对应的检测实例(如RightHand,LeftFoot。它们绑定了角色Mesh)
远程攻击
在需要发射子弹的Montage中(比如射箭),你通过添加ANS_GCS_BulleTrace
在攻击帧定义攻击请求。

- BulletDefinitionHandle:指向子弹定义表中的某一个子弹定义,它包含“该子弹”相关的所有静态数据。(比如实际生成多少个弹丸,它的攻击力等等。)
- TargetingSourceType:决定子弹的生成位置是如何计算的。

- SourceSocketName:如果TargetingSourceType是Pawn开头的,那么SourceSocketName用于决定在角色Mesh的哪个Socket位置上生成。
- SourceWeaponSocketName:如果TargetingSource是Weapon开头的,那么SourceWeaponSocketName用于决定在武器的哪个位置上生成的。
自定义攻击请求类型
你可以通过蓝图/C++创建一个GCS_AttackRequest
的子类,以添加更多的信息,并在后续流程中使用。
拿GCS_AttackRequest_Bullet
举例,当你将TargetingSourceType选择为Custom时,你应该继承GCS_AttackRequest_Bullet
来创建一个新的版本,并实现GetTargetingTransform
。

在战斗流程中使用攻击请求
攻击请求本身随着GameplayEffectContext在不同的流程中进行传递,意味着你可以随时随时地访问攻击请求中中配置的各种信息。
比如你可以通过Context拿到攻击请求对象,并获取其关联的攻击定义。

GoodToKnow
攻击请求是一个实例化的UObject,意味着它不是运行时创建的,而是在编辑器中创建并存储到硬盘上,这是它能高效地通过网络传输的秘诀。
攻击请求是Const类型,意味着它只能存储静态数据供游戏逻辑使用,你不应该在游戏运行时修改它。
如果你在蓝图定义一个AttackRequest类型的变量,你可以看到它允许你内联创建一个实例并随着Outer资产的保存而保存。


攻击定义
概念
攻击定义,是一个结构体,游戏中有多少种攻击,就应该有多少个攻击定义,它包含战斗游戏中"每一击"的静态信息。一个近战技能会播放一个攻击动画,但可以产生多个不同种类的攻击。
每种攻击/武器挥舞/Projectile都有与之相关的非常具体的属性,诸如伤害类型,数值加成,挥舞方向等内容。
像艾尔登法环这样的游戏,它有成千上万个不同的攻击定义,因为在GCS中使用DataTable管理攻击定义,并全部使用SoftClass/Object引用,意味着它不会拖慢你的项目性能。
创建攻击定义表
前面提到发起攻击请求时,需要指定攻击定义,而你可以通过新建类型为FGCS_AttackDefinition
的数据表,然后在表里定义你会用到的所有攻击定义。
通常攻击定义的数量,取决于你的游戏机制的复杂度,以及有多少攻击变体。
像EldenRing中的技能“水鸟乱舞Waterfowl Dance”,该技能的攻击定义高达20个!!!每一击都有着不同的伤害类型,韧性伤害,以及附带的属性。

这里有大量字段可以配置,其中,最重要的属性如下:
AttackTags:你通过为Attack添加游戏标签来描述它的特性,这些Tags会添加到攻击信息中,所以在处理攻击信息时,你可以利用此信息来做更多的逻辑判断。
案例:在处理攻击结果时,你可以检查Attacker的攻击是否带火属性,同时再检测自身是否弱火,若满足条件,你可以再给自身添加额外的表现。
SetByCallerMagnitudes:本质上是一个GameplayTag->float的键值对,你在这里可以填写任意信息,它会被传入GameplayEffectSpec,所以你可以在Calculation中做更多的操作。
案例:如果你有一系列连招,那么可能最后一段是有极大的伤害加成,而具体加成多少,你可以在这里填写,只要你的Calculation中考虑到了这一点。
管理攻击定义表
经过我的了解,艾尔登法环根据武器的类型来划分不同的攻击定义表。
所以你可以按照:DT_Atk_Sword_A,DT_Atk_Sword_B, DT_Atk_GreatSword_A, DT_Atk_GreatWord_B这样的方式来规划和组织你的攻击定义表。
攻击结果
每当CombatFlow
处理完一次受到的攻击后,会产生攻击结果,并记录到战斗系统组件上,且通过网络同步到客户端。
每当一个战斗结果生成后,GCS都会根据其内容触发不同的战斗反馈,比如播放HitReaction,播放GameplayCue产生视觉效果等等。
处理攻击结果
在CombatFlow中,你可以以数据驱动的方式 配置不同的AttackResultProcessors(攻击结果处理器)来根据不同的攻击结果进行不同的处理。


你可以查看GCS_BaseCombatFlow蓝图了解具体的用法。
内置处理器
Send Gampelay Event(发送游戏事件)
它允许您根据攻击结果向攻击者/受害者发送不同的游戏事件。

您还可以使用 TagQuery 来产生不同的逻辑分支。
比如: 当攻击结果的SourceTags中包含Block
标签时,才会触发防御时的受击反应. 当攻击结果的SourceTags中包含Hit
标签时,才会触发被击中时的受击反应。
自定义“攻击结果处理器”
你通过创建GCS_AttackResultProcessor的子类来构建新的处理器。
比如:你可以创建一个“伤害统计处理器”,将每一次受到的攻击的结果记录到别的其他的系统,(比如一个Log系统,或者战报系统,可以将消息发送到UI。)