概述
GGA同时还有另外一个代码模块,叫GenericGameplayAttribute。它主要负责对GAS系统中GameplayAttribute这块进行一定拓展,并包含了一些内置的GameplayAttribute。这个模块非常轻量。
属性与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,这里有一个很棒的文章教你如何用少量的属性,完成复杂的任务。
添加新属性(代码生成器)
即便是对于C++用户来说,添加新属性也是一个非常繁琐的过程,需要手写很多样板代码。
GGA提供了一个额外的命令行工具,允许你通过编写简单的Json,来为你生成C++的AttributeSet代码。
你不需要手写代码,你只需要将你的项目转换成代码项目。
请加入我的Discord,然后进入#GGA-Resources频道下载代码生成器。
视频演示
生成案例
下面是一个Json生成C++代码的案例。
1{2 "ModuleName": "GENERICGAMEPLAYATTRIBUTES",3 "ClassName": "AS_Combat",4 "EnableGGA": true,5 "TagPrefix": "GGF.Attribute.CombatSet.",6 "Category": "Attribute|CombatSet",7 "FileName": "AS_Combat",8 "CopyRight": "// Copyright 2024 https://yuewu.dev/en All Rights Reserved.",9 "HeaderOutputDirectory": "E:/GenericGame5.4/Plugins/GenericGameplayAbilities/Source/GenericGameplayAttributes/Public/Attributes",10 "CppOutputDirectory": "E:/GenericGame5.4/Plugins/GenericGameplayAbilities/Source/GenericGameplayAttributes/Private/Attributes",11 "HeaderIncludes": [],12 "CppIncludes": [13 "Attributes/AS_Combat.h"14 ],15 "Attributes": [16 {17 "Name": "Damage",18 "Default": 0,19 "Comment": "The damage that will apply to target"20 },21 {22 "Name": "DamageNegation",23 "Default": 0,24 "Comment": "The damage reduction(percentage) for incoming health damage"25 },26 {27 "Name": "StaminaDamage",28 "Default": 0,29 "Comment": "The stamina damage that will apply to target"30 },31 {32 "Name": "StaminaDamageNegation",33 "Default": 0,34 "Comment": "The damage reduction(percentage) for incoming stamina damage"35 },36 {37 "Name": "PostureDamage",38 "Default": 0,39 "Comment": "The posture(stance) damage that will apply to target"40 },41 {42 "Name": "PostureDamageNegation",43 "Default": 0,44 "Comment": "The damage reduction(percentage) for incoming posture damage"45 }46 ]47}
上面的Json会生成下面的代码:
1// Copyright 2024 https://yuewu.dev/en All Rights Reserved.23#pragma once45#include "CoreMinimal.h"6#include "AttributeSet.h"7#include "AbilitySystemComponent.h"8#include "NativeGameplayTags.h"910#include "AS_Combat.generated.h"1112namespace AS_Combat13{1415 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Damage)1617 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(DamageNegation)1819 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamage)2021 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(StaminaDamageNegation)2223 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(PostureDamage)2425 GENERICGAMEPLAYATTRIBUTES_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(PostureDamageNegation)262728}2930#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \31GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \32GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \33GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \34GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)3536UCLASS()37class GENERICGAMEPLAYATTRIBUTES_API UAS_Combat : public UAttributeSet38{39 GENERATED_BODY()404142public:4344 UAS_Combat();4546 virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;4748 virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;4950 virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;5152 // The damage that will apply to target53 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Damage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))54 FGameplayAttributeData Damage{ 0.0 };55 ATTRIBUTE_ACCESSORS(ThisClass, Damage)5657 // The damage reduction(percentage) for incoming health damage58 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_DamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))59 FGameplayAttributeData DamageNegation{ 0.0 };60 ATTRIBUTE_ACCESSORS(ThisClass, DamageNegation)6162 // The stamina damage that will apply to target63 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))64 FGameplayAttributeData StaminaDamage{ 0.0 };65 ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamage)6667 // The damage reduction(percentage) for incoming stamina damage68 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_StaminaDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))69 FGameplayAttributeData StaminaDamageNegation{ 0.0 };70 ATTRIBUTE_ACCESSORS(ThisClass, StaminaDamageNegation)7172 // The posture(stance) damage that will apply to target73 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_PostureDamage, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))74 FGameplayAttributeData PostureDamage{ 0.0 };75 ATTRIBUTE_ACCESSORS(ThisClass, PostureDamage)7677 // The damage reduction(percentage) for incoming posture damage78 UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_PostureDamageNegation, Category = "Attribute|CombatSet", Meta = (AllowPrivateAccess = true))79 FGameplayAttributeData PostureDamageNegation{ 0.0 };80 ATTRIBUTE_ACCESSORS(ThisClass, PostureDamageNegation)8182 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageAttribute"), Category = "Attribute|CombatSet")83 static FGameplayAttribute Bp_GetDamageAttribute();8485 UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamage"), Category = "Attribute|CombatSet")86 float Bp_GetDamage() const;8788 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamage"), Category = "Attribute|CombatSet")89 void Bp_SetDamage(float NewValue);9091 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamage"), Category = "Attribute|CombatSet")92 void Bp_InitDamage(float NewValue);9394 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetDamageNegationAttribute"), Category = "Attribute|CombatSet")95 static FGameplayAttribute Bp_GetDamageNegationAttribute();9697 UFUNCTION(BlueprintPure,meta=(DisplayName="GetDamageNegation"), Category = "Attribute|CombatSet")98 float Bp_GetDamageNegation() const;99100 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetDamageNegation"), Category = "Attribute|CombatSet")101 void Bp_SetDamageNegation(float NewValue);102103 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitDamageNegation"), Category = "Attribute|CombatSet")104 void Bp_InitDamageNegation(float NewValue);105106 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageAttribute"), Category = "Attribute|CombatSet")107 static FGameplayAttribute Bp_GetStaminaDamageAttribute();108109 UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamage"), Category = "Attribute|CombatSet")110 float Bp_GetStaminaDamage() const;111112 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamage"), Category = "Attribute|CombatSet")113 void Bp_SetStaminaDamage(float NewValue);114115 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamage"), Category = "Attribute|CombatSet")116 void Bp_InitStaminaDamage(float NewValue);117118 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetStaminaDamageNegationAttribute"), Category = "Attribute|CombatSet")119 static FGameplayAttribute Bp_GetStaminaDamageNegationAttribute();120121 UFUNCTION(BlueprintPure,meta=(DisplayName="GetStaminaDamageNegation"), Category = "Attribute|CombatSet")122 float Bp_GetStaminaDamageNegation() const;123124 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetStaminaDamageNegation"), Category = "Attribute|CombatSet")125 void Bp_SetStaminaDamageNegation(float NewValue);126127 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitStaminaDamageNegation"), Category = "Attribute|CombatSet")128 void Bp_InitStaminaDamageNegation(float NewValue);129130131 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetPostureDamageAttribute"), Category = "Attribute|CombatSet")132 static FGameplayAttribute Bp_GetPostureDamageAttribute();133134 UFUNCTION(BlueprintPure,meta=(DisplayName="GetPostureDamage"), Category = "Attribute|CombatSet")135 float Bp_GetPostureDamage() const;136137 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetPostureDamage"), Category = "Attribute|CombatSet")138 void Bp_SetPostureDamage(float NewValue);139140 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitPostureDamage"), Category = "Attribute|CombatSet")141 void Bp_InitPostureDamage(float NewValue);142143 UFUNCTION(BlueprintCallable,BlueprintPure,meta=(DisplayName="GetPostureDamageNegationAttribute"), Category = "Attribute|CombatSet")144 static FGameplayAttribute Bp_GetPostureDamageNegationAttribute();145146 UFUNCTION(BlueprintPure,meta=(DisplayName="GetPostureDamageNegation"), Category = "Attribute|CombatSet")147 float Bp_GetPostureDamageNegation() const;148149 UFUNCTION(BlueprintCallable,meta=(DisplayName="SetPostureDamageNegation"), Category = "Attribute|CombatSet")150 void Bp_SetPostureDamageNegation(float NewValue);151152 UFUNCTION(BlueprintCallable,meta=(DisplayName="InitPostureDamageNegation"), Category = "Attribute|CombatSet")153 void Bp_InitPostureDamageNegation(float NewValue);154155protected:156157 /** 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) */158 virtual void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty);159160 UFUNCTION()161 virtual void OnRep_Damage(const FGameplayAttributeData& OldValue);162163 UFUNCTION()164 virtual void OnRep_DamageNegation(const FGameplayAttributeData& OldValue);165166 UFUNCTION()167 virtual void OnRep_StaminaDamage(const FGameplayAttributeData& OldValue);168169 UFUNCTION()170 virtual void OnRep_StaminaDamageNegation(const FGameplayAttributeData& OldValue);171172 UFUNCTION()173 virtual void OnRep_PostureDamage(const FGameplayAttributeData& OldValue);174175 UFUNCTION()176 virtual void OnRep_PostureDamageNegation(const FGameplayAttributeData& OldValue);177};
如你所见,只是一个简单的属性集,就需要这么多代码。所以代码生成是必然的选择!
生成的蓝图



代码生成器指南
它只是一个简单的用Golang编写的可执行文件(gga.exe),用户只需要如何使用即可(也能得到源码),它生成的代码毕竟是代码,不满意你仍然可以修改。
将项目转换成C++项目
确保将项目转换成C++项目,并添加插件相关的依赖,我会在之后添加有关这部分的更详细的教程。
定义属性集并生成代码
让我们假设你有一个目录包含gga.exe,以及1个配置好的Json。

"ModuleName"必须全大写哟,也别忘记添加"EnableGGA": true,
只需要在gga.exe所在目录打开CMD(win10) 或者Terminal(Win11),然后依次敲入如下命令即可。
./gga.exe gen -f ./Sample.json
然后你应该重新编译你的代码模块。
说明: ./表示当前路径, gga.exe则是生成器,gen表示执行的操作是生成操作, -f 选项用于指定要生成的文件 ./xxx.json 则是实际的属性定义。
修改代码生成模板

你可以看到这两个文件就是用来生产代码的模板.你可以对其进行微调 (但不要破坏其中的 if/else/for语句)
代码生成器的未来计划
我计划将代码生成器整合入虚幻编辑器,这样你可以在引擎中通过数据表配置属性,然后通过资产Action调用来生成属性。


一些思考
由于GAS的属性系统必须在C++完成,所以这阻碍了很大一部分的蓝图用户,同时虚幻商城中也出现了各种针对蓝图用户的解决方案。甚至出现了纯蓝图的GameplayAttribute解决方案。
但我认为,如果只是因为对代码项目产生恐惧,而绕了一个大弯去用各种黑科技来使用GameplayAttribute,我认为是本末倒置了。万一未来虚幻自带了GameplayAttribute的蓝图支持?万一未来虚幻团队让GAS不再必须使用C++?