概述
GGA同时还有另外一个代码模块,叫GenericGameplayAttribute。它主要负责对GAS系统中GameplayAttribute这块进行一定拓展,并包含了一些内置的GameplayAttributeSet 以及 AttributeSet代码生成器。这个模块非常轻量。
属性与Tag映射
GAS非常拥抱GameplayTags,然而GameplayAttribute并不是由GameplayTag驱动的,为此,GGA提供了可选的GameplayAttribute到GameplayTag的映射功能。
你可以通过Tag拿到Attribute,你也可以通过Attribute拿到Tag。
你也可以在运行时注册Attribute到某个Tag(这个操作是全局的)。
内置属性
GenericGameplayAttributes模块自带了三个比较常用的属性集,分别是AS_Health,AS_Mana,以及AS_Stamina。
你可以使用它,也可以不使用。它足够抽象,且是自动生成的(见下文)。
如何设计AttributeSet
如果你参考网络上的各种案例,教程,你会发现,大家习惯于使用一个AttributeSet,把游戏中所有用到的属性都定义在其中,而且会在AttributeSet中写很多游戏性逻辑,我认为这是不可取的。
相反,我认为AttributeSet应该根据游戏中的功能进行划分,且仅仅是起数据定义作用,不需要包含游戏逻辑,只需要在外部根据属性变化进行逻辑处理。
假定你的游戏中,需要Health,和Stamina两种属性,你应该定义两个AttributeSet。
AS_Health(生命属性集)
Health | 当前血量 |
MaxHealth | 最大血量 |
Healing | 要恢复的血量 |
Damage | 要扣除的血量 |
AS_Stamina(体力属性集)
Stamina | 当前体力 |
MaxStamina | 最大体力 |
Healing | 要恢复的体力 |
Damage | 要扣除的体力 |
这样做的好处是:
- 在多人游戏中,不同的角色可能需要的属性是不一样的,为了避免大量的网络同步消耗,你应该只给需要的角色所需的属性集。
- 同时这种模块化的设计,能够在未来走得更远。(比如你使用GameFeature,你的某一个子Feature定义了它专用的属性集)
不要滥用GameplayAttributes,这里有一个很棒的文章教你如何用少量的属性,完成复杂的任务。
关于代码生成
GGA提供了代码生成功能帮助你生成C++的AttributeSet代码。
即便是对于C++用户来说,添加新属性也是一个非常繁琐的过程,需要手写很多样板代码。
要使用代码生成功能,你需要安装Windows平台上的VisualStudio或者Mac平台上的XCode(请参考官方文档),并将你的项目转换成代码项目。
代码生成指南
新增属性集的步骤:
- 创建新的属性集定义资产:在项目浏览器的空白处右键->GenericGameplayAbilities->CoreConfig->AttributeSetDefinition
- 命名新的属性集资产:建议以ASDefinition_为前缀命名新建的属性集定义,比如ASDefinition_Health|Combat|Stamina
- 进行代码生成配置:指定代码在哪个项目模块及其路径中生成,代码文件名,具备的属性等,也可以从模板下拉列表中选择阈值的模板帮你快速填充配置。
- 点击生成/重新生成按钮,代码生成完毕后,需要重新编译项目。
Note
你可以在项目设置->GGA Attribute Set Generator设置中,添加其它的可允许的代码子路径列表,比如“MyFolder/Attributes”,默认情况下,只允许模块名/Attributes|AttributeSets两个文件夹。
生成的代码
这是上面的配置会生成的代码:
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "NativeGameplayTags.h"
#include "AS_Combat.generated.h"
namespace AS_Combat
{
GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Damage)
GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DamageNegation)
GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(GuardDamageNegation)
GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamage)
GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamageNegation)
}
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class GENERICGAMEPLAYATTRIBUTES_API UAS_Combat : public UAttributeSet
{
GENERATED_BODY()
public:
UAS_Combat();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
// The damage that will apply to target
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Damage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData Damage{ 0.0 };
ATTRIBUTE_ACCESSORS(ThisClass, Damage)
// The damage reduction(percentage) for incoming health damage
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_DamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData DamageNegation{ 0.0 };
ATTRIBUTE_ACCESSORS(ThisClass, DamageNegation)
// The damage reduction(percentage) for incoming health damage while guarding
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GuardDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData GuardDamageNegation{ 0.0 };
ATTRIBUTE_ACCESSORS(ThisClass, GuardDamageNegation)
// The stamina damage that will apply to target
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData StaminaDamage{ 0.0 };
ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamage)
// The damage reduction(percentage) for incoming stamina damage
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData StaminaDamageNegation{ 0.0 };
ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamageNegation)
UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageAttribute"), Category = "Attribute|CombatSet")
static FGameplayAttribute Bp_GetDamageAttribute();
UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamage"), Category = "Attribute|CombatSet")
float Bp_GetDamage() const;
UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamage"), Category = "Attribute|CombatSet")
void Bp_SetDamage(float NewValue);
UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamage"), Category = "Attribute|CombatSet")
void Bp_InitDamage(float NewValue);
UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageNegationAttribute"), Category = "Attribute|CombatSet")
static FGameplayAttribute Bp_GetDamageNegationAttribute();
UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamageNegation"), Category = "Attribute|CombatSet")
float Bp_GetDamageNegation() const;
UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamageNegation"), Category = "Attribute|CombatSet")
void Bp_SetDamageNegation(float NewValue);
UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamageNegation"), Category = "Attribute|CombatSet")
void Bp_InitDamageNegation(float NewValue);
UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetGuardDamageNegationAttribute"), Category = "Attribute|CombatSet")
static FGameplayAttribute Bp_GetGuardDamageNegationAttribute();
UFUNCTION(BlueprintPure,meta=(DisplayName="GetGuardDamageNegation"), Category = "Attribute|CombatSet")
float Bp_GetGuardDamageNegation() const;
UFUNCTION(BlueprintCallable,meta=(DisplayName="SetGuardDamageNegation"), Category = "Attribute|CombatSet")
void Bp_SetGuardDamageNegation(float NewValue);
UFUNCTION(BlueprintCallable,meta=(DisplayName="InitGuardDamageNegation"), Category = "Attribute|CombatSet")
void Bp_InitGuardDamageNegation(float NewValue);
UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageAttribute"), Category = "Attribute|CombatSet")
static FGameplayAttribute Bp_GetStaminaDamageAttribute();
UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamage"), Category = "Attribute|CombatSet")
float Bp_GetStaminaDamage() const;
UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamage"), Category = "Attribute|CombatSet")
void Bp_SetStaminaDamage(float NewValue);
UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamage"), Category = "Attribute|CombatSet")
void Bp_InitStaminaDamage(float NewValue);
UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageNegationAttribute"), Category = "Attribute|CombatSet")
static FGameplayAttribute Bp_GetStaminaDamageNegationAttribute();
UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamageNegation"), Category = "Attribute|CombatSet")
float Bp_GetStaminaDamageNegation() const;
UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamageNegation"), Category = "Attribute|CombatSet")
void Bp_SetStaminaDamageNegation(float NewValue);
UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamageNegation"), Category = "Attribute|CombatSet")
void Bp_InitStaminaDamageNegation(float NewValue);
protected:
/** Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes. (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before) */
virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty);
UFUNCTION()
virtual void OnRep_Damage(const FGameplayAttributeData& OldValue);
UFUNCTION()
virtual void OnRep_DamageNegation(const FGameplayAttributeData& OldValue);
UFUNCTION()
virtual void OnRep_GuardDamageNegation(const FGameplayAttributeData& OldValue);
UFUNCTION()
virtual void OnRep_StaminaDamage(const FGameplayAttributeData& OldValue);
UFUNCTION()
virtual void OnRep_StaminaDamageNegation(const FGameplayAttributeData& OldValue);
};如你所见,只是一个简单的属性集,就需要这么多代码。所以代码生成是必然的选择!
生成的蓝图
一些思考
由于GAS的属性系统必须在C++完成,所以这阻碍了很大一部分的蓝图用户,同时虚幻商城中也出现了各种针对蓝图用户的解决方案。甚至出现了纯蓝图的GameplayAttribute解决方案。
但我认为,如果只是因为对代码项目产生恐惧,而绕了一个大弯去用各种黑科技来使用GameplayAttribute,我认为是本末倒置了。万一未来虚幻自带了GameplayAttribute的蓝图支持?万一未来虚幻团队让GAS不再必须使用C++?