GASDocumentation 案例项目分析(三)
前言
上回说到,FireGun 这个类有一个名为 DamageGameplayEffect
的 GE 用于伤害计算以及击中特效播放,并且最终通过 Projectile->DamageEffectSpecHandle
的形式将该 GE 传递给了 Projectile。不难想象最终一定会由子弹本身的碰撞等事件来执行该 GE 以计算伤害(DamageExecCalc)及播放 GC。
1 | UPROPERTY(BlueprintReadOnly, EditAnywhere) |
上一节主要分析了前者,即 GDDamageExecCalculation
的实现,本节就从 GameplayCue(GC)开始分析。
GameplayCue
首先来看一下文档中关于 GC 的解释:
GASDocumentation_Chinese:GameplayCue
GameplayCue(GC)
执行非游戏逻辑相关的功能, 像音效, 粒子效果, 镜头抖动等等。 GameplayCue
一般是可同步(除非在客户端明确执行(Executed)
, 添加(Added)
和移除(Removed)
)和可预测的。
我们可以在 ASC 中通过发送一个强制带有 “GameplayCue” 父名的相应 GameplayTag
和 GameplayCueManager
的事件类型(Execute, Add或Remove)来触发GameplayCue
。GameplayCueNotify
对象和其他实现 IGameplayCueInterface
的 Actor 可以基于 GameplayCue
的 GameplayTag(GameplayCueTag)
来订阅(Subscribe)这些事件。
Note: 再次强调,
GameplayCue
的GameplayTag
需要以GameplayCue
为开头, 举例来说, 一个有效的GameplayCue
的GameplayTag
可能是GameplayCue.A.B.C
。
有两个GameplayCueNotify
类, Static
和 Actor
。它们各自响应不同的事件, 并且不同的 GameplayEffect
类型可以触发它们,根据你的逻辑重写相关的事件。
GameplayCue类 | 事件 | GameplayEffect类型 | 描述 |
---|---|---|---|
GameplayCueNotify_Static | Execute | Instant或Periodic | Static GameplayCueNotify直接操作ClassDefaultObject(意味着没有实例)并且对于一次性效果(像击打伤害)是极好的. |
GameplayCueNotify_Actor | Add或Remove | Duration或Infinite | Actor GameplayCueNotify会在添加(Added)时生成一个新的实例, 因为其是实例化的, 所以可以随时间推移执行操作直到被移除(Removed). 这对循环的声音和粒子效果是很好的, 其会在持续(Duration)或无限(Infinite)GameplayEffect被移除或手动调用移除时移除. 其也自带选项来管理允许同时添加(Added)多少个, 因此多个相同效果的应用只启用一次声音或粒子效果. |
默认情况下, 游戏开始时 GameplayCueManager
会扫描游戏的全部目录以寻找 GameplayCueNotify
并将其加载进内存. 我们可以设置 DefaultGame.ini
来修改 GameplayCueManager
的扫描路径。
查看 GE_GunDamage
这个 GE 蓝图,会发现在 Display 中指定了一个 Tag 为 GameplayCue.Hero.FireGun.Impact
的 GC。配置在这里的 GC 会在 GE 生效时触发。
上文中也说过一个合法的 GC 的 Tag 必须以 GameplayCue.A.B.C
的形式来组织,游戏开始时 GameplayCueManager
会扫描全部 GC 并加载,GC 在创建时就需要绑定一个 Tag,因此这样就完成了 Tag -> GC 的对应,在 GE 的 Display 中配置相应的 Tag 就可以播放对应的特效或声效了。
在 FireGun 文件夹中,可以找到一个名为 GC_FireGunImpact
的 GC,打开该蓝图就会发现相应的 Tag 配置的刚好是 GameplayCue.Hero.FireGun.Impact
,也就是说在 GE_GunDamage
生效时就会播放这个 GC 了。
该 GC 继承自 GameplayCueNotify_Static
并且重写了 OnExcute()
函数,查看蓝图中的逻辑就会发现该 GC 会提取 EffectContext
信息并在 HitResult.Location
的位置播放一个特效,该效果就是打击特效了。
博主在之后加了一个音效播放(
GE_GunDamage 的生效方式
上文中我们说到,FireGun 这个 GA(GameplayAbility)最终会生成一颗子弹,并把 GE_GunDamage
包装为 DamageEffectSpecHandle
传给 Projectile,而这个 GE 要负责伤害的计算,以及播放一个 GC;那么,这个 GE 是什么时候生效的呢?这就需要回到 BP_GunProjectile
中来了。
查看该蓝图,貌似逻辑较为复杂,我们一点一点分析。
首先,在 BeginPlay()
时通过 Range
及 Speed
设置了子弹的 LifeSpan
。
剩下的都是发生碰撞时的执行逻辑,这里可以看出子弹不会攻击释放者。
这里获取了被击中目标的 ASC,可以看出,只有在 Server 端才会触发之后的逻辑,Client 会直接销毁子弹。即,GE 只会发生在 Server 上,Client 通过 Attribute 同步的方式接收信息。
这部分的目的就是为了获取到被击中的方向并赋给 FinalHit
,以播放不同方向的受击动画。
最后这部分就是 GE 真正生效的地方了,我们终于用到了 DamageEffectSpecHandle
并且将 FinalHit
设置给了它的 EffectContext
并且最终将该 GE 作用于被击中的 Target,最后再销毁该子弹。
GE 生效的过程我们已经分析过,分为计算伤害与播放 GC 两个部分。
属性修改、受击动画
现在,我们的 Projectile 中携带的 GE 终于对 Target 生效了,播放了击中的烟雾特效并且修改了 Target 的 Damage 属性,接下来的问题是,如何将 Damage 属性的改变反映到 Health 中呢? 这部分的逻辑在上一节提到过的 UGDAttributeSetBase::PostGameplayEffectExecute
中完成。
Meta Attribute
在分析这部分内容之前,需要先了解以下 Damage
这个特殊的属性,GDAttriibuteSetBase.h
中对于该属性是这样注释的:
1 | // Damage is a meta attribute used by the DamageExecution to calculate final damage, |
这是一个“临时变量”并且只存在于 Server 端,更重要的是,这是一个 meta Attribute
。
查看 GASDocumentation_Chinese:metaAttribute 的解释:
一些 Attribute
被视为占位符, 其是用于预计和 Attribute
交互的临时值, 这些 Attribute
被叫做 Meta Attribute
。例如, 我们通常定义伤害值为 Meta Attribute
, 使用伤害值 Meta Attribute
作为占位符, 而不是使用 GameplayEffect
直接修改生命值 Attribute
, 使用这种方法, 伤害值就可以在 GameplayEffectExecutionCalculation
中由 buff 和 debuff 修改, 并且可以在 AttributeSet
中进一步操作, 例如, 在最终将生命值减去伤害值之前, 要将伤害值减去当前的护盾值. 伤害值 Meta Attribute
在 GameplayEffect
之间不是持久化的, 并且可以被任何一方重写. Meta Attribute
一般是不可同步的。
Meta Attribute
对于在"我们应该造成多少伤害?"和"我们该如何处理伤害值?"这种问题之中的伤害值和治疗值做了很好的解构, 这种解构意味着 GameplayEffect
和 ExecutionCalculation
无需了解目标是如何处理伤害值的。继续看伤害值的例子, GameplayEffect
确定造成多少伤害, 之后 AttributeSet
决定如何使用该伤害值, 不是所有的 Character
都有相同的Attribute
, 特别是使用了 AttributeSet
子类的话, AttributeSet
基类可能只有一个生命值 Attribute
, 但是它的子类可能增加了一个护盾值 Attribute
, 拥有护盾值 Attribute
的子类 AttributeSet
可能会以不同于 AttributeSet
基类的方式分配收到的伤害。
尽管 Meta Attribute
是一个很好的设计模式, 但其并不是强制使用的. 如果你只有一个用于所有伤害实例的 Execution Calculation
和一个所有 Character
共用的 AttributeSet
类, 那么你就可以在 Exeuction Calculation
中分配伤害到生命, 护盾等等, 并直接修改那些 Attribute
, 这种方式你只会丢失灵活性, 但总体上并无大碍。
--------------- 分界线 ------------------
可以看出,Damage
属性是对于伤害与生命值等属性之间的又一次解耦,将伤害值本身抽取出来成为一个属性,这样可以处理更复杂的情况。虽然 Damage
不会同步到客户端,但 Damage
对于 Health
的修改会由 Health
本身同步到客户端。
一般而言,在使用 GAS 的项目中,AttributeSet
都会有这样一个属性。
PostGameplayEffectExecute
回到这个函数中,在 案例分析一 中分析过,该函数在 GE 执行后触发,并且在调用时属性还没有被同步到客户端,可以在这里再对属性进行一些操作,不会造成二次同步的问题。
1 | void UGDAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data) |
观察签名就会发现,该函数接受一个 FGameplayEffectModCallbackData&
的参数,想必是包含了 GE 的所有上下文信息。
1 | FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext(); |
可以看出,Data
中不仅包含了 FGameplayEffectContextHandle
,还包含了关于 Target 与 Source 的信息,可以处理许多逻辑。
这里先就 Data.EvaluatedData.Attribute == GetDamageAttribute()
的情况进行分析。
1 | // Store a local copy of the amount of damage done and clear the damage attribute |
可以看出这里储存了一份 Damage
的备份,并把属性中的 Damage
清零。
1 | // Apply the health change and then clamp it |
之后通过 LocalDamageDone
设置了 Health
的值,完成了扣血的效果。
1 | // Play HitReact animation and sound with a multicast RPC. |
此外,这里还获取了 context 中储存的 HitResult
以得到受击方向。GetHitReactDirection
定义在 GDCharacterBase.h
中,输出一个代表方向的枚举值。PlayHitReact
也定义在该类中,最终会通知 ABP_Hero
(Animation BluePrint)播放对应的受击动画。
1 | // Show damage number for the Source player unless it was self damage |
除此之外,还函数还处理了伤害的 PopupText,会调用 AGDPlayerController::ShowDamageNumber
以显示伤害数字。最后还处理了增加攻击者的经验及金币等的情况,这些功能都不是很重要,这里就不详细分析了。
总的来看,在 PostGameplayEffectExecute
完成了 Damage -> -Health
的计算,并且处理了许多显示上的逻辑,如播放受击动画、伤害漂字等。
还有高手?
没有高手了,FireGun 的整个 GA 流程已经被我们分析完毕,现在就来梳理一下整个 GA 的作用流程。
首先,GDGA_FireGun
直接继承自 UGDGameplayAbility
,在 UGDGA_FireGun::ActivateAbility
中,通过UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent
的方式播放射击动画并等待动画事件。
在 Montage 中,共触发了两个事件,分别为生成一个 Projectile(BP_GunProjectile
) 以及结束 Ability。GDGA_FireGun
中有一个 Damage
属性代表子弹的伤害,同时存放了一个实际赋值为 GE_GunDamage
的 GE,该 GE 最终会实例化赋给生成的子弹,负责计算伤害以及播放击中效果的 GC。
在 BP_GunProjectile
中设置了碰撞事件,会在被击中的角色的 ASC 上执行该 GE 以造成伤害以及播放特效。此外,还额外传递了 HitResult
给 Projectile->DamageEffectSpecHandle
以保存击中方向的信息。
在被击中角色的 UGDAttributeSetBase::PostGameplayEffectExecute
中,会处理掉 Projectile 对 Damage
的改动,并进行实际的扣血操作,此外还会根据 HitResult
的方向来播放不同的受击动画以及处理伤害漂字等效果。
至此,FireGun 的整个流程也就分析完毕。