Attribute操作指北

定义属性

AttributeSet继承自UAttributeSet

定义辅助宏ATTRIBUTE_ACCESSORS(ClassName, PropertyName)

这个类有一个宏可以快速定义一个Attribute的Get()、Set()、Init(),在AttributeSet.h我们可以看见这个宏的定义与注释

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* This defines a set of helper functions for accessing and initializing attributes, to avoid having to manually write these functions.
* It would creates the following functions, for attribute Health
*
* static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
* FORCEINLINE float UMyHealthSet::GetHealth() const;
* FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
* FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
*
* To use this in your game you can define something like this, and then add game-specific functions as necessary:
*
* #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
*
* ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
*/

//-----------------------翻译---------------------------------------

/**
* 这定义了一组用于访问和初始化属性的辅助函数,从而避免了你手动编写这些函数的繁琐工作。
* 以属性 Health 为例,它会自动生成以下四个函数:
*
* static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
* FORCEINLINE float UMyHealthSet::GetHealth() const;
* FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
* FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
*
* 要在你的游戏中使用它,你可以像这样定义一个宏,然后根据需要在其中添加特定于游戏的函数:
*
* #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
*
* ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
*/

#define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
static FGameplayAttribute Get##PropertyName##Attribute() \
{ \
static FProperty* Prop = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
return Prop; \
}

#define GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
FORCEINLINE float Get##PropertyName() const \
{ \
return PropertyName.GetCurrentValue(); \
}

#define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
FORCEINLINE void Set##PropertyName(float NewVal) \
{ \
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \
if (ensure(AbilityComp)) \
{ \
AbilityComp->SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \
}; \
}

#define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
FORCEINLINE void Init##PropertyName(float NewVal) \
{ \
PropertyName.SetBaseValue(NewVal); \
PropertyName.SetCurrentValue(NewVal); \
}

那么,我们在我们自己的AttributeSet类里边就可以调用这个宏去一键搞定很多事
拿我一直在跟进的Warrior项目举例

1
2
3
4
5
6
7
8
// WarriorAttributeSet.h

// 在include后定义好宏
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

在我们定义好这个宏后我们只需要输入

1
2
3
4
// example
UPROPERTY(...)
FGameplayAttributeData 属性名;
ATTRIBUTE_ACCESSORS(AttributeSet类名, 属性名)

这样,我们就可以获得

1
2
3
Get属性名()
Set属性名()
Init属性名()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// WarriorAttributeSet.h

UCLASS()
class WARRIOR_API UWarriorAttributeSet : public UAttributeSet
{
GENERATED_BODY()

public:
UWarriorAttributeSet();

// 当前血量属性
UPROPERTY(BlueprintReadOnly, Category = "Health")
FGameplayAttributeData CurrentHealth;
ATTRIBUTE_ACCESSORS(UWarriorAttributeSet, CurrentHealth)

// 最大血量属性
UPROPERTY(BlueprintReadOnly, Category = "Health")
...
};

我们可以直接在.cpp文件中去调用宏为我们定义好的Init函数去初始化属性

1
Init属性名();
1
2
3
4
5
6
7
8
// WarriorAttributeSet.cpp
UWarriorAttributeSet::UWarriorAttributeSet()
{
// 暂时初始化为1
InitCurrentHealth(1.f);
InitMaxHealth(1.f);
...
}

当然,所谓任何对属性的更改都应该在GE中进行,这也包括初始化

使用GameplayEffect修改AttributeSet

GE有两种方式修改AttributeSet

方案1(快、常用):GE蓝图+曲线表格(Curve Table)

先直接定义一个GE,连自定义的父类都用不着,就是一个改Attribute值的工具罢了
这个方案主要是在修饰符(Modifiers)里边利用曲线表格的数据改数值
当AttibuteSet定义好后所有的GE都可以直接捕捉,这也导致当一个项目有很多个AttributeSet时会很难找到想改的属性

定义曲线表格(Curve Table)


曲线表格可以根据等级来给不同的属性不同的值,其中行号为等级列标为属性

定义GE

这个方案里边GE就是用来改值的工具,蓝图都不用连,只需要添加GameplayEffect下的修饰符(Modifiers)即可
GE的持续时间策略:

GE有3种持续时间策略:实时、无限、拥有持续时间

1
2
3
4
5
6
1. Instant(即时)
一旦应用,立即生效并立刻销毁。
2. Duration(有限持续)
效果会持续一段特定的时间,时间到期后自动移除
3. Infinite(无限持续)
除非被手动移除(通过代码、标签清除或其他 GE 抵消),否则永远生效。

修饰符:

  • 修饰符可以选取已定义的AttributeSet的所有Attribute
  • 可对一个Attribute进行加、乘、除、覆盖、无效

    通常初始化属性选择覆盖
    要使用曲线表格,我们需要将 幅度计算类型 改为 可扩展浮点


方案二(细、大量计算):GE蓝图+计算类(GameplayEffectExecutionCalculation)

这个方案在创建完GE后不需要用到修饰符了,要用执行(Excutions),然后选择一个计算类GameplayEffectExecutionCalculation

这个计算类太难了到时候新发一个文档介绍

制作GESpec、ContextHandle

要想应用GE(待会的GE就标识GameplayEffect)就需要使用GESpec,这可以大概理解为GE的实例化,但在GE的传输中大多用GESpecHandle,相当于GESpec的标签

一个GESpec由如下部分组成

  • GE本体指针(在制作Spec的时候用的是类指针)
  • Level:当前实例等级
  • Duration:持续时间(策略不同该值不同)
  • Context:上下文
  • SetByCallerMagnitudes(Map<Tag, float>):动态修改器
  • Handle:Spec的标签

这个Context作为元数据又有很多东西

  • Instigator:肇事者(逻辑意义上的)
  • EffectCauser:来源(物理意义上的)
  • TargetData:目标数据
  • Tag Snapshots:
    • SourceTags:来源标签组
    • TargetTags:目标标签组

制作Context

直接用ASC提供的MakeEffectContext()函数即可,不要用new!

1
FGameplayEffectContextHandle ContextHandle = ASC->MakeEffectContext();

MakeEffectContext()函数会进行如下操作

  1. 调用工厂模式创建实例(?)
  2. 绑定拥有者(Owner)与同步对象(Sync Object)
  3. 自动填充默认发起者(Instigator) 和 来源对象(SourceObject)
  4. 初始化内部容器:没值的全赋空值

但在必要时还需要去手动该值

比如Warrior项目里的

1
2
3
4
5
6
// 添加技能来源
ContextHandle.SetAbility(this);
// 接收GE的出处
ContextHandle.AddSourceObject(GetAvatarActorFromActorInfo());
// 接收肇事者与武器(可以是自己)
ContextHandle.AddInstigator(GetAvatarActorFromActorInfo(), GetAvatarActorFromActorInfo());

制作Spec

有了Context后就要开始制作Spec了、
用ASC提供的MakeOutgoingSpec()

1
FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(GEClass, Level, ContextHandle)
  • GEClass就是GE的类引用
  • Level可以根据该值选定数值
  • ContextHandle即刚刚所创建的ContextHandle

这样,一个GESpec就知道了GE所做的事情,level对应的数值,Context所包含的GE外的附加信息

应用GE

在cpp内提供了两个函数用于应用GE到自身/目标

ApplyGamepalyEffectToSelf/Target()

声明:

1
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectToSelf(const UGameplayEffect *GameplayEffect, [UAbilitySystemComponent *Target], float Level, const FGameplayEffectContextHandle& EffectContext, FPredictionKey PredictionKey)

形参作用:

  • GameplayEffect,注意这里是个GE的对象指针,通常而言我们手上应该只有类引用
    1
    TSubclassOf<UGameplayEffect> GameplayEffectClass;
    若我们需要直接得到GE的对象引用,我们应该使用GE的CDO (Class Default Object)
    CDO可理解为类引用的一个默认对象,每个类引用TSubclassOf<>都仅有一个
    要想拿到CDO,我们需要使用GetDefaultObject<>()
    1
    UGameplayEffect* GECDO = GEClass->GetDefaultObject<UGameplayEffect>();
  • Target当应用对象为Target时用到,传入Target的ASC
  • Level提供等级,可根据等级调整数值
  • EffectContext即上下文,提供GE作用域等各种信息
  • PredictionKey管理网络同步,调用函数时可以不传该值

调用方法: 通常要传入GECDO, Level, 再使用ASC提供的MakeEffectContext直接创建Context

1
InASCToGive->ApplyGameplayEffectToSelf(EffectCDO, Level, ASC->MakeEffectContext());

返回类型: FActiveGameplayEffectHandle可以追踪已存在的GE,它是用来手动移除 (Remove) 或 查询剩余时间 的唯一凭据
特点: 在这个函数中无需自己创建GESpec,拿到GECDO即可,在函数体里会自动拿传入的GE对象引用、Level、Context制作Spec,最终调用ApplyGamepalyEffectSpecToSelf/Targe()
在函数内,Spec创建、调用写法如下,当然,手写还是用ASC提供的MakeOutgoingSpec即可

1
2
FGameplayEffectSpec Spec(GameplayEffect, EffectContext, Level);
return ApplyGameplayEffectSpecToSelf(Spec, PredictionKey);

ApplyGamepalyEffectSpecToSelf/Target

特点: 可以高度自定义GESpec,说到底ApplyGamepalyEffectToSelf/Target()就是调用这个函数
声明:

1
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToTarget(const FGameplayEffectSpec &Spec, [UAbilitySystemComponent *Target], FPredictionKey PredictionKey)

形参作用:

  • Spec
  • Target当应用对象为Target时用到,传入Target的ASC
  • PredictionKey管理网络同步,调用函数时可以不传该值
    至于怎么创建Spec刚刚说过了

返回类型: FActiveGameplayEffectHandle可以追踪已存在的GE,它是用来手动移除 (Remove) 或 查询剩余时间 的唯一凭据