目前GAS系列文章是在作者边学边实践的过程中的产出,因此未来100%会继续改善。

GAS-1

在这篇文章,我们来引进GAS系统中的两个部件。

Ability System Component

Ability System Component(ASC)是整个GAS系统中最基础的一个组件,它负责主要的对象与GAS的交互。在后续提及Gameplay Effect(GE)的时候,可以发现GE本身就是通过ASC进行施加的,同时这个过程中需要的 FGameplayEffectContextHandleFGameplayEffectSpecHandle 都是通过ASC进行构建。不仅如此,ASC还和其所在类对应的AttributeSet,以及处理各种Gameplay Effect与使用技能Gameplay Ability都密切相关。因此对于一个角色,如果要将其加入GAS系统,你第一时间就是给予其一个Ability System Component。

ASC有两个代表Actor,分别是OwnerActor和AvatarActor,前者是实际构建(拥有)该ASC的Actor,而后者是该ASC所作用的Actor。显然二者并不一样,对于玩家所操控的角色,一般会将OwnerActor设置为PlayerState,而将AvatarActor设置为Character。这两个Actor都需要一个指向同一个ASC的指针,依照Epic的惯例,需要实现接口 IAbilitySystemInterface 中的 GetAbilitySystemComponent() 函数。

在PlayerState的构造函数中,我们来实际构造ASC:

1
2
3
4
5
6
7
// 构造ASC
AbilitySystemComponent = CreateDefaultSubobject<UAuraAbilitySystemComponent>("Ability System Component");
AbilitySystemComponent->SetIsReplicated(true);
// Rule of thumb
// Tip: 若设置成Mixed,那么ASC的Owner Actor的Owner必须是Controller, 这或许和Mixed方式在Replicate需要判断Client的身份有关
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);

对于多人游戏,一定要将ASC设置为Replicated。在GAS中还可以指定Replicate的模式,有三种模式:

1
2
3
4
5
6
7
8
9
10
11
/** How gameplay effects will be replicated to clients */
UENUM()
enum class EGameplayEffectReplicationMode : uint8
{
/** Only replicate minimal gameplay effect info. Note: this does not work for Owned AbilitySystemComponents (Use Mixed instead). */
Minimal,
/** Only replicate minimal gameplay effect info to simulated proxies but full info to owners and autonomous proxies */
Mixed,
/** Replicate full gameplay info to all */
Full,
};

在上面的代码中我们设置为Mixed,这是多人游戏中对于玩家角色中最经常采用的折中方案,本地玩家能得到GE的完整信息,而本地客户端中其余玩家操控的角色对应的代理客户端则同步最小的Info。

如果我们要创建一个敌人的类,它也要接入GAS系统,则可以直接在其Pawn类中构建ASC,同时将ReplicationMode设置为Minimal。

注意:Mixed模式需要保证它的Owner Actor的Owner必须是Controller, 这或许和Mixed方式在Replicate中需要判断Client的身份有关。

InitAbilityActorInfo

InitAbilityActorInfo 函数设置一个ASC的OwnerActor和AvatarActor,该函数需要在服务器和客户端分别独自调用。需要注意的是,对于多人游戏,需要保证每个客户端本地的PlayerController被Replicate结束后才可调用InitAbilityActorInfo。因此有个Rule of Thumb,它需要保证调用该函数之前已经完成了PlayerController控制了Character的工作,以不扰乱对于Gameplay架构中基类的初始化工作。

1
2
3
原观点:InitAbilityActorInfo calls FGameplayAbilityActorInfo::InitFromActor() which for players caches their PlayerController. For multiplayer games this step must succeed in order to activate local predicted abilities. Keep in mind that the PlayerController may not be replicated over yet when an ASC begins play client-side, because there is no guarantee for in which order actors are spawned client-side.

出处:[link](https://dev.epicgames.com/community/learning/tutorials/DPpd/unreal-engine-gameplay-ability-system-best-practices-for-setup#whenshouldicallinitabilityactorinfo/refreshabilityactorinfoforaplayer?)

因此在客户端我们可以重写Character或者Controller中的 OnRep_PlayerState 方法,在里面执行InitAbilityActorInfo。当然对于客户端,直接在Character的 PossessedBy或者Controller的 OnPossess 中调用。

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
// server-called only
void AAuraCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);

// 在服务器需要在角色被Controller控制后方可设置Ability Actor Info
InitAbilityActorInfo();
}

// client-called only
void AAuraCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();

// 在客户端需要同时保证Controller和PlayerState都是有效的才能设置Ability Actor Info
InitAbilityActorInfo();
}


void AAuraCharacter::InitAbilityActorInfo()
{
AAuraPlayerState* AuraPlayerState = CastChecked<AAuraPlayerState>(GetPlayerState());
AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
AttributeSet = AuraPlayerState->GetAttributeSet();
}

Attribute Set

我们实际的属性在类 UAttributeSet 中。每一个属性都是浮点值,由结构体 FGameplayAttributeData 定义。如果开发游戏的过程中存在与Gameplay相关的数值属性,可以考虑将其添加到你的AttributeSet中。Attribute由AttributeSet负责复制。

每个属性都由 BaseValueCurrentValue 组成。前者是属性永远保存的数值,而后者则是在前者的基础上添加一些短暂的修改(例如 GameplayEffect )。

应当只使用 GameplayEffect 来修改属性,以让 AbilitySystemComponent 可以 predict 属性变化(挖坑

如何在 AttributeSet 中定义我们想要的属性?首先定义 AttributeSet.h 中提供的推荐的宏:

1
2
3
4
5
6
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

然后可以在你Subclass的 AttributeSet 中这样定义属性:

1
2
3
UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health)

别忘了对应的 OnRep 函数:

1
2
3
4
5
6
7
8
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const;

void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
// boiler-plate
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}

GAMEPLAYATTRIBUTE_REPNOTIFY 是与Predict相关的宏,挖坑(

最后记得在 GetLifetimeReplicatedProps 注册该属性,就完成了属性的定义。

*Clamp 属性

这个是我后面添加的部分,主要涉及到了几个重要的钩子函数。

首先是 PreAttributeChange,它在任何Attribute要发生修改前进行调用(侦测 CurrentValue 的修改)。我们可以改变它传进的浮点值引用来Clamp:

1
2
3
4
5
6
7
8
9
10
11
12
13
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);

if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}

但是这样就真的够了吗?假设我们拾取了超过 MaxHealth 上限的血药,的确可以通过 showdebug abilitysystem 看到我们 Health 属性卡在了上界。但是如果这时候受伤了,我们依然是满血!(这里所有的 GameplayEffect 应该都影响到了 BaseValue

我们看看这个函数,我们修改的是 NewValue,而这个 NewValue 仅仅是最终通过某个叫 modifier 的东西得到的输出。我们更改这个数值则不会影响 modifier 内部的工作,即便内部产生了超出 Clamp 的数值。因此在后面受伤的时候,modifier 会通过内部超出上限的值(实际上应该是 BaseValue ?后面有测试,应该就是)

BaseValue 被修改前,另一个钩子函数 PreAttributeBaseChange 被调用,而这里的 NewValue 就是 BaseValue 的引用 。因此在这里实现Clamp逻辑即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
void UAuraAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
{
Super::PreAttributeBaseChange(Attribute, NewValue);

if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
}
}

有种做法是在钩子函数 PostGameplayEffectExecute 中直接使用 SetAttribute 来Clamp。PostGameplayEffectExecute 在进行了有 BaseValue 修改后的 GameplayEffect 后调用,而 SetAttribute 内部就是直接修改 BaseValue。因此这种做法等同于上面在 PreAttributeBaseChange Clamp的做法。