战斗(攻防)系统

概述

本文简对GCS战斗系统中的攻防流程进行介绍,用于帮助你更好地使用GCS。

讨厌写这一部分的内容,因为这更像一个设计文档,而聪明的程序员完全可以反推并制作自己的版本。

发起攻击流程

任意对象,包括环境/陷阱,都能发起攻击,而游戏中的人形角色其攻击流程一般如下:

战斗(攻防)系统.001

流程步骤

  1. 激活技能:Attacker(玩家/AI)激活不同的技能,如近战Ability,或者法术Ability。
  2. 发起攻击请求:技能会播放不同的战斗动作(动画),你在动画中放置AttackTrace/BulletTrace动画通知状态,并配置相关的攻击请求参数。
  3. Ability处理攻击请求:AttackTrace通知状态的开始和结束,会通过Gameplay事件(攻击信息)将攻击请求交由Ability处理。而Ability则会构建需要应用的GameplayEffects实例。
  4. Melee:如果是Melee请求,则会激活角色或者武器的碰撞检测实例(HitBox),并将相关信息传递给碰撞检测实例。碰撞检测实例获取到有效目标后,会告知Ability进行游戏效果应用。
  5. Bullet:如果是Bullet请求,则会通过子弹系统生成子弹实例,并将相关信息(游戏效果实例)传递给子弹实例。每一个子弹实例拥有自己的碰撞检测实例,当获取到有效目标后,由子弹将攻击信息应用到目标上。

游戏中更多环境,陷阱,也能发起攻击,只需要所需的攻击信息而已。

在攻击信息应用到目标之前,你都可以对过程中产生的游戏效果实例(GameplayEffectSpec/GameplayEffectContainerSpec)进行修改,比如:

  1. 将攻击定义中的数值参数传递到GE。
  2. 根据情况为GE添加不同的Tag并在之后的流程中使用(比如用Tag表示是否为火焰伤害或者魔法伤害)
  3. 子弹随着飞行距离衰减伤害。
  4. 子弹飞行过程中为GE添加新的目标。

整个攻击流程的最终目的是:生成并修改GameplayEffect实例,然后应用到目标。

伤害计算

发起攻击的最终的目的是为了给目标施加GameplayEffects,而在GameplayEffect执行过程中,会修改目标的特定属性。

GameplayEffects有很多种方式可以修改目标属性,GCS的参考内容提供了大量不同的Effects供你参考,其中:

GCS_Execution_Damage更是提供了一个魂类游戏中的伤害计算逻辑的默认的参考实现

关于默认实现

该实现非常通用,且通过非常少量的GameplayAttributes实现了多类型的伤害和伤害减免(抵消)逻辑,且并非硬编码,你可以添加更多的伤害类型(如神圣伤害,神圣伤害减免等)。

你可以直接使用,也可以作为参考来创建你自己的版本。

战斗(攻防)系统.002

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

处理攻击的流程

战斗(攻防)系统.003

战斗流程(CombatFlow)

CombatFlow是一个UObject,每一个战斗系统组件可以指定一个CombatFlow。

你应该继承内置的GCS_BaseCombatFlow,并重写相关函数以实现你的自定义逻辑,或者参考现有的CombatFlow并创建一个全新的。

处理属性变更

在通过技能系统中的使用指南中提到,属性系统组件会将所有属性变更的回调转发到CombatFlow进行处理。因此你只需要重写HandleGameplayEffectExecute即可。

战斗(攻防)系统.004

如何处理属性,并产生什么样的攻击结果,完全由你自己的游戏机制决定,而参考实现中提供了魂类游戏常见的流程。

产生攻击结果

当你在HandleGameplayEffectExecute中针对属性变化进行处理后,你应该根据你自己的游戏逻辑,将该过程中的信息转换成攻击结果,并注册到战斗系统组件。

战斗(攻防)系统.005

例如:在处理完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(动画通知状态)来配置一个攻击请求。

战斗(攻防)系统.006
  • TraceToControls:定义了在动画通知状态激活过程中,需要激活的碰撞检测实例
  • AttackDefinitionHandle:指向攻击定义表中的某一个攻击定义,它包含“这一击”相关的所有静态数据(如攻击力,Cost等信息)。

上图动作是用右手武器攻击,所以应该开启名为"RightHandWeapon"的检测实例(它绑定了武器的Mesh)
如果动作是用拳头攻击或者用脚踩踏,你也应该开启对应的检测实例(如RightHand,LeftFoot。它们绑定了角色Mesh)

远程攻击

在需要发射子弹的Montage中(比如射箭),你通过添加ANS_GCS_BulleTrace在攻击帧定义攻击请求。

战斗(攻防)系统.007
  • BulletDefinitionHandle:指向子弹定义表中的某一个子弹定义,它包含“该子弹”相关的所有静态数据。(比如实际生成多少个弹丸,它的攻击力等等。)
  • TargetingSourceType:决定子弹的生成位置是如何计算的。
战斗(攻防)系统.008
  • SourceSocketName:如果TargetingSourceType是Pawn开头的,那么SourceSocketName用于决定在角色Mesh的哪个Socket位置上生成。
  • SourceWeaponSocketName:如果TargetingSource是Weapon开头的,那么SourceWeaponSocketName用于决定在武器的哪个位置上生成的。

自定义攻击请求类型

你可以通过蓝图/C++创建一个GCS_AttackRequest的子类,以添加更多的信息,并在后续流程中使用。

GCS_AttackRequest_Bullet举例,当你将TargetingSourceType选择为Custom时,你应该继承GCS_AttackRequest_Bullet来创建一个新的版本,并实现GetTargetingTransform

战斗(攻防)系统.009

在战斗流程中使用攻击请求

攻击请求本身随着GameplayEffectContext在不同的流程中进行传递,意味着你可以随时随时地访问攻击请求中中配置的各种信息。

比如你可以通过Context拿到攻击请求对象,并获取其关联的攻击定义。

战斗(攻防)系统.010

GoodToKnow

攻击请求是一个实例化的UObject,意味着它不是运行时创建的,而是在编辑器中创建并存储到硬盘上,这是它能高效地通过网络传输的秘诀。

攻击请求是Const类型,意味着它只能存储静态数据供游戏逻辑使用,你不应该在游戏运行时修改它。

如果你在蓝图定义一个AttackRequest类型的变量,你可以看到它允许你内联创建一个实例并随着Outer资产的保存而保存。

战斗(攻防)系统.011
战斗(攻防)系统.012

攻击定义

概念

攻击定义,是一个结构体,游戏中有多少种攻击,就应该有多少个攻击定义,它包含战斗游戏中"每一击"的静态信息。一个近战技能会播放一个攻击动画,但可以产生多个不同种类的攻击。

每种攻击/武器挥舞/Projectile都有与之相关的非常具体的属性,诸如伤害类型,数值加成,挥舞方向等内容。

像艾尔登法环这样的游戏,它有成千上万个不同的攻击定义,因为在GCS中使用DataTable管理攻击定义,并全部使用SoftClass/Object引用,意味着它不会拖慢你的项目性能。

创建攻击定义表

前面提到发起攻击请求时,需要指定攻击定义,而你可以通过新建类型为FGCS_AttackDefinition的数据表,然后在表里定义你会用到的所有攻击定义。

通常攻击定义的数量,取决于你的游戏机制的复杂度,以及有多少攻击变体。

像EldenRing中的技能“水鸟乱舞Waterfowl Dance”,该技能的攻击定义高达20个!!!每一击都有着不同的伤害类型,韧性伤害,以及附带的属性。

战斗(攻防)系统.013

这里有大量字段可以配置,其中,最重要的属性如下:

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(攻击结果处理器)来根据不同的攻击结果进行不同的处理。

战斗(攻防)系统.014
战斗(攻防)系统.015

你可以查看GCS_BaseCombatFlow蓝图了解具体的用法。

内置处理器

Send Gampelay Event(发送游戏事件)

它允许您根据攻击结果向攻击者/受害者发送不同的游戏事件。

战斗(攻防)系统.016

您还可以使用 TagQuery 来产生不同的逻辑分支。

比如: 当攻击结果的SourceTags中包含Block标签时,才会触发防御时的受击反应. 当攻击结果的SourceTags中包含Hit标签时,才会触发被击中时的受击反应。

自定义“攻击结果处理器”

你通过创建GCS_AttackResultProcessor的子类来构建新的处理器。

比如:你可以创建一个“伤害统计处理器”,将每一次受到的攻击的结果记录到别的其他的系统,(比如一个Log系统,或者战报系统,可以将消息发送到UI。)