GASDocumentation 案例项目分析(一)
前言
终于准备开新坑啦!终于使用了一次正常封面 在 STL 系列基本写完之后就摸了挺久的,其实这段时间也没有闲着,把 MP-TPS 的那个教程基本看完了,现在只剩第一部分的联机插件没有实现,准备最近开始继续更新 MP-TPS 系列,把这个项目的内容好好总结一下。
说回正题,目前的准备是将 GAS 结合到这个项目中,因此有必要好好学习一下 GAS 系统。但是总所周知,GAS 系统虽功能强大,学习曲线却异常陡峭,本来打算通过看 ActionRPG 项目的方式来学习 GAS,奈何这个项目本身也十分复杂。最后,在机缘巧合之下终于找到了这个项目———— GASDocumentation 以及某位好心人翻译的中文版 GASdocumentation_Chinese,感谢这些开源贡献者。
GASDocumentation 这个项目提供了十分完善的 GAS 文档,包含了 GAS 系统的各个模块的详细介绍,虽说没有细到分析源码的地步,但作为入门阶段的百科全书已经十分优秀了。除此之外,该项目还自带了一个同名的 UE4 工程,简单展示了 GAS 的使用方法,可以说是无比良心了。
相比于 ActionRPG,GASDocumentation 更加地简单,且较为完整地展示了 GAS 的基本用法,很适合用于熟悉 GAS 框架。话不多说,就让我们一同揭开 GAS 的秘密吧。
几个重要的类
在分析具体技能的实现之前,需要先明白与 GAS 相关的几个类在项目中是如何组织的,下面开始一一分析。
GDAbilitySystemComponent
项目中使用的 ASC(AbilitySystemComponent) 组件为一个自定义的从 AbilitySystemComponent
中派生的子类,查看该类的定义:
1 | UCLASS() |
该类有两个 bool
类型的成员变量与一个 ReceiveDamage
的虚函数。
1 | void UGDAbilitySystemComponent::ReceiveDamage( |
该函数中广播了一个 FReceivedDamageDelegate
类型的动态委托,目前还看不出广播的作用。
1 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FReceivedDamageDelegate, |
GDAttributeSetBase
在 GDAttributeSetBase
类中,定义了一些项目中使用到的属性,如 Health、Mana 等,但有很多属性实际上是没有用到的。同样地,与 GAS 关系较为密切的有两个函数:
1 | // AttributeSet Overrides |
查看这两个函数的原始定义:
1 | /** |
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
是AttributeSet
中的主要函数之一, 其在修改发生前响应Attribute
的CurrentValue
变化, 其是通过引用参数NewValue
限制(Clamp)CurrentValue
即将进行的修改的理想位置。Epic对于PreAttributeChange()
的注释说明不要将该函数用于游戏逻辑事件, 而主要在其中做限制操作。
1 | /** |
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
仅在即刻(Instant)GameplayEffect
对Attribute
的BaseValue
修改之后触发, 当GameplayEffect
对其修改时, 这就是一个处理更多Attribute
操作的有效位置。当PostGameplayEffectExecute()
被调用时, 对Attribute
的修改已经发生, 但是还没有被同步回客户端, 因此在这里限制值不会造成对客户端的二次同步, 客户端只会接收到限制后的值。
总之 PreAttributeChange
发生在属性修改前,PostGameplayEffectExecute
发生在 Effect 执行(Execute)之后,具体这两个函数做了什么,等分析到的时候再具体说明。
GDGameplayAbility
项目中使用的所有 Ability 也全都派生自 GDGameplayAbility
而不是 GameplayAbility
本身,来看看 GDGameplayAbility
实现了哪些新的功能。
1 | UCLASS() |
这里定义了三个新的成员变量与一个 override 的函数。其中,比较重要的是 AbilityInputID
,这一变量将技能与 EGDAbilityInputID
的枚举类型对应了起来。查看项目的输入设置就会发现,输入的名称与 EGDAbilityInputID
中的枚举类型是一一对应的,这就意味着输入与 EGDAbilityInputID
这个枚举类也存在着一一对应的关系,这里先记住这件事情,在下文中会说明这样做的目的。
1 | UENUM(BlueprintType) |
GDPlayerState
项目中 ASC 位于 PlayerState 上,而 Character 本身只实现 AbilitySystemInterface
的接口,即提供一个函数访问 ASC 组件。即 PlayerState 为 ASC 的 OwnActor 而 Character 为 AvatarActor。
首先,在构造函数中构造了 ASC 组件与 AttributeSet,并设置了网络同步的模式。
1 | AGDPlayerState::AGDPlayerState() |
其次,在 BeginPlay()
中绑定了许多的回调函数,用于处理 UI 的显示,这里就不贴上来了。
由于项目中的 OwnActor 与 AvatarActor 是不同的 Actor,因此项目中选择了在 Character 中初始化 ASC 组件,即指定相应的 OwnActor 与 AvatarActor 以及绑定输入。
GDCharacterBase
该类直接派生自 Character
并且实现了 AbilitySystemInterface
的接口。
1 | class GASDOCUMENTATION_API AGDCharacterBase : public ACharacter, public IAbilitySystemInterface |
在该类中,AbilitySystemComponent
与相应的属性集合 AttributeSetBase
的指针均以 TWeakObjectPtr
的类型保存,这种弱引用的方式更加轻量级,但有可能会被 GC。
1 | // Instead of TWeakObjectPtrs, you could just have UPROPERTY() hard references or no references at all |
与 GAS 强相关的有以下的一些变量与函数:
1 | // Default abilities for this Character. These will be removed on Character death and |
其中,CharacterAbilities
为开局就赋予玩家的默认能力,DefaultAttributes
用于初始化玩家的各项属性,StartupEffects
是开局就起作用的其他 GE。由于该项目也考虑到了联机的场景,因此 AddCharacterAbilities()
只在 Server 进行,而客户端则需要等待 Ability Specs 同步;同样的,RemoveCharacterAbilities()
也只在 Server 进行;InitializeAttributes()
在 Server 与 Client 都会进行而不是等待同步。
AddCharacterAbilities()
负责赋予玩家默认能力,查看该函数的实现:
1 | /// @brief 赋予角色默认 Abilities,并完成按键的绑定 |
在 AGDCharacterBase::AddCharacterAbilities()
中,会遍历 CharacterAbilities
即默认能力,并将 FGameplayAbilitySpec
(可以理解为能力的实例)赋予玩家(GiveAbility
)。
1 | FGameplayAbilitySpec::FGameplayAbilitySpec( |
查看 FGameplayAbilitySpec
的函数签名,会发现与 int32 InInputID
这个参数对应的是 static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID)
。在 GDGameplayAbility
的分析中说过,这一参数将技能与对应的 EGDAbilityInputID
枚举类联系了起来。
但是想要建立输入到技能的联系,还缺少了 输入 -> EGDAbilityInputID
之间的联系,这又是在哪里完成的呢?答案就在 GDHeroCharacter
中。
GDHeroCharacter
这个类派生自 GDCharacterBase
,为游戏中玩家与 AI 直接控制的角色,相比于基类多了很多成员,这里只分析与 GAS 相关的一些函数:
输入绑定
1 | void AGDHeroCharacter::BindASCInput() |
前面说到,FGameplayAbilitySpec::FGameplayAbilitySpec
将技能与 AbilityInputID
联系了起来,而 BindASCInput()
则实现了输入与 AbilityInputID
之间的绑定,如此一来,输入与技能之间就通过 EGDAbilityInputID
这个桥梁,间接联系了起来。觉得不明白的读者可以参考 绑定输入到 ASC。
ASC 的初始化
由于该项目具有联机功能,且 ASC 同时存在于 Character 与 PlayerState 上,因此,ASC 的初始化与输入绑定的时机就很重要了。
1 | // Server only |
在 ASC 的初始化方面,作者选择了在 PossessedBy()
中初始化 Server 端的 ASC,在 OnRep_PlayerState()
中初始化 Client 端的 ASC,作者的解释是:在客户端,BeginPlay 要早于 Possession,因此在 BeginPlay 中不能执行任何 ASC 相关的逻辑,因为此时 PlayerState 还没有被同步到客户端。
1 | // Called to bind functionality to input |
此外,在 SetupPlayerInputComponent()
中也执行了 BindASCInput()
用于绑定输入到 ASC。
总结
本节主要分析了 GASDocumentation
中与 GAS 相关的几个重要的类,包括 GDAbilitySystemComponent
、GDAttributeSetBase
、GDGameplayAbility
以及 GDCharacterBase
。本来想继续写的,但是发现篇幅似乎有点长了,具体技能的分析就放到下一节吧!绝对不是要水文章数