If you are familiar with the GAS, you know that GAS uses UGameplayAttributeSet for gameplay attribute. A gameplay attribute is used to decribes the data of a character, like max HP, max MP or many other data.
A gameplay attribute is required for not only need to describe basic data of the character, but also the ability to work with a gameplay attribute modifier system to accomplish the gameplay buff system.
1. Gameplay Attributes
A FPS or ARPG may not contain a lot of gameplay attributes, which means that you can use GAS easily, but if you are working with a SLG or JRPG, in which a character may have a lot of complex buff. The buff system of GAS is too simple to accomplish those complex demand, we need to implement the buff system by ourselves. For that, we need to implement a gameplay attribute system first.
By my method, first of all, you need to enumerate all the attributes you need first, it doesn’t need to be an enumeration, it can also be strings or gameplay tags.
The following are the attributes I used, you can replace them with your attribute set for your gameplay.
/** * @enum EGameplayAttributeType : EGameplayAttributeType defines the gameplay attribute */ UENUM(BlueprintType) enum class EGameplayAttributeType : uint8 { MaxHealth, MaxMana, HealthRecover, ManaRecover, StoppingAtk, PenetrateAtk, PenetrateValue, PenetrateRate, Def, PhysicsDefenseRate, CriticalRate, CriticalScaleRate, ManaAtk, ManaPenetrateValue, ManaPenetrateRate, ManaDef, ManaDefenseRate, EnumCount };
Current health point and current mana point do not belongs to gameplay atttribute set, but record as a member of UBattleComponent or sth else.
2. Gameplay Attribute Meta
A gameplay attribute meta is used for a property to appoint that the property is used for decribes an attribute enumerated before, for example:
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "MaxHealth", MinValue = 0, ClampMin = 0)) float MaxHealth;
The meta property of MaxHealth appoints that MaxHealth is used for “MaxHealth” and clamp to 0.
We create a new class for encapsulation of the meta data, which allows us to access the data easily:
/** * @class FGameplayAttributeMeta : FGameplayAttributeMeta is meta data of UGameplayAttributeData, which contains a EGameplayAttributeType * enumeration and a clamp min, clamp max value. */ class CODEERO_API FGameplayAttributeMeta { private: inline static UEnum* GameplayAttributeEnumInfo = nullptr; public: EGameplayAttributeType GameplayAttributeType; float MinValue; float MaxValue; public: explicit FGameplayAttributeMeta(const FProperty* Property) { if (Property->HasMetaData("AttributeType")) { const FString TypeStr = Property->GetMetaData("AttributeType"); const int32 EnumIndex = GetGameplayAttributeEnumInfo()->GetIndexByName(FName(TypeStr)); check(EnumIndex != INDEX_NONE) GameplayAttributeType = static_cast<EGameplayAttributeType>(EnumIndex); const FString MinValueStr = Property->GetMetaData("MinValue"); const FString MaxValueStr = Property->GetMetaData("MaxValue"); MinValueStr.IsEmpty() ? MinValue = -1e10 : MinValue = FCString::Atof(*MinValueStr); MaxValueStr.IsEmpty() ? MaxValue = 1e10 : MaxValue = FCString::Atof(*MaxValueStr); } } FVector2f GetValueRange() const { return FVector2f(MinValue, MaxValue); } public: static UEnum* GetGameplayAttributeEnumInfo(); };
This field contains a gameplay attribute enumeration, a min value and a max value, sometimes we need to clamp the value of the property, for example, the physics penetrate rate should be from 0 to 1.
The implementation of GetGameplayAttributeEnumInfo is:
UEnum* FGameplayAttributeMeta::GetGameplayAttributeEnumInfo() { { if (GameplayAttributeEnumInfo == nullptr) { GameplayAttributeEnumInfo = FindObject<UEnum>(ANY_PACKAGE, TEXT("EGameplayAttributeType")); check(GameplayAttributeEnumInfo != nullptr) } return GameplayAttributeEnumInfo; } }
Note that the GameplayAttributeEnumInfo is static. For this method should no be run in after game started, it just runs for once when initialize the game.
3. Gameplay Attribute Field Info
Each FGameplayAttributeFieldInfo correspounds for a member in UGameplayAttributeData, which also correspounds a gameplay attribute. The instances of FGameplayAttributeField should only be constructed when the game start for performance.
class CODEERO_API FGameplayAttributeFieldInfo { public: const FProperty* PropertyField; TSharedPtr<FGameplayAttributeMeta> GameplayAttributeMeta; public: FGameplayAttributeFieldInfo(const FProperty* Property, FGameplayAttributeMeta* GameplayAttributeMeta) { this->PropertyField = Property; this->GameplayAttributeMeta = MakeShareable(GameplayAttributeMeta); } };
4. Gameplay Attribute Data
UGameplayAttributeData is used for init gameplay attribute, which describes the real time attributes of a character. UGameplayAttributeData is a UDataAsset so that it is data-driven ready.
UGameplayAttributeData are not required for using every attributes we mentioned before, you can have multiple UGameplayAttributeData, each one is a subset of the gameplay attribute enumeration, however, this will also makes the gameplay logic be complicated.
UGameplayAttribtueData can not only contains gameplay attribute data, if contains other data, check if there is a AttributeType in meta data to find out that whether the attribute you are now dealing with is a gameplay attribute.
/** * @class UGameplayAttributeData : UGameplayAttributeData is used to init UGameplayAttribute, which describes the attributes of character in real time */ UCLASS(BlueprintType, NotBlueprintable, EditInlineNew) class CODEERO_API UGameplayAttributeData final : public UDataAsset { GENERATED_BODY() public: UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "MaxHealth", MinValue = 0, ClampMin = 0)) float MaxHealth; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "MaxMana", MinValue = 0, ClampMin = 0)) float MaxMana; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "HealthRecover", MinValue = 0, ClampMin = 0)) float HealthRecover; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaRecover", MinValue = 0, ClampMin = 0)) float ManaRecover; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "StoppingAtk", MinValue = 0, ClampMin = 0)) float StoppingAtk; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "PenetrateAtk", MinValue = 0, ClampMin = 0)) float PenetrateAtk; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "PenetrateValue", MinValue = 0, ClampMin = 0)) float PenetrateValue; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "PenetrateRate", MinValue = 0, MaxValue = 1)) float PenetrateRate; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "Def", MinValue = 0)) float Def; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "PhysicsDefenseRate", MinValue = 0, MaxValue = 1)) float PhysicsDefenseRate; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "CriticalRate", MinValue = 0, MaxValue = 1)) float CriticalRate; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "CriticalScaleRate")) float CriticalScaleRate; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaAtk", MinValue = 0)) float ManaAtk; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaPenetrateValue", MinValue = 0, MaxValue = 1)) float ManaPenetrateValue; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaPenetrateRate", MinValue = 0, MaxValue = 1)) float ManaPenetrateRate; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaDef", MinValue = 0)) float ManaDef; UPROPERTY(EditAnywhere, BlueprintReadOnly, meta=(AttributeType = "ManaDefenseRate", MinValue = 0, MaxValue = 1)) float ManaDefenseRate; private: inline static TSharedPtr<TArray<FGameplayAttributeFieldInfo>> GameplayAttributeFieldInfoList = nullptr; public: static TArray<FGameplayAttributeFieldInfo>* GetGameplayAttributeFieldInfoList(); };
Note that he GameplayAttributeFieldInfoList is a static member, which should only be init when the game starts for performance.
The method GetGameplayAttributeFieldInfoList uses reflection system, which means is has a bad performance, however, this method is only called the the game starts for once, so it actually has no cost.
The method is implemented as:
TArray<FGameplayAttributeFieldInfo>* UGameplayAttributeData::GetGameplayAttributeFieldInfoList() { if (GameplayAttributeFieldInfoList == nullptr) { GameplayAttributeFieldInfoList = MakeShareable(new TArray<FGameplayAttributeFieldInfo>); for (TFieldIterator<FProperty> PropertyIterator(StaticClass()); PropertyIterator; ++PropertyIterator) { const FProperty* CurrentProperty = *PropertyIterator; FGameplayAttributeMeta* GameplayAttributeMeta = new FGameplayAttributeMeta(CurrentProperty); FGameplayAttributeFieldInfo GameplayAttributeFieldInfo(CurrentProperty, GameplayAttributeMeta); GameplayAttributeFieldInfoList->Add(GameplayAttributeFieldInfo); } } return GameplayAttributeFieldInfoList.Get(); }
5. Gameplay Attribute Modifer
A UGameplayAttributeModifer is used for buff system or sth to modify the gameplay attribute of a character, for example, the effect of a weapon is that +15% atk, which need to create a gameplay attribute modifer, which GameplayAttributeType is atk, and TargetGameplayAttributeModiferOperator is EGameplayAttributeModiferOperator::Multi, and set the ModiferValue to 0.15.
UENUM(BlueprintType) enum class EGameplayAttributeModifierOperator : uint8 { Add, Multi, FinalAdd, FinalMulti }; UCLASS(Blueprintable, BlueprintType, EditInlineNew) class CODEERO_API UGameplayAttributeModifier : public UObject { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly, EditAnywhere, DisplayName = "Target Attribute Field") EGameplayAttributeType TargetGameplayAttribute; UPROPERTY(BlueprintReadOnly, EditAnywhere, DisplayName = "Target Operator") EGameplayAttributeModifierOperator TargetGameplayAttributeModifierOperator; UPROPERTY(BlueprintReadOnly, EditAnywhere, DisplayName = "Value") float ModifierValue; };
What makes it special is that it is specified by EditInlineNew, so that it can be created in a buff, game designers can appoint the GameplayAttributeType easily without using index of the array or sth else, makes it easy to use:
6. Gameplay Attribute
A UGameplayAttribute is used to describe the realtime gameplay attribute of a character, the data it contains can be modifed by gameplay modifer:
In my gameplay framework, the final value of the gameplay attribute is : FinalValue = (OriginalValue * MultiModifiedValue + AddModifiedValue) * FinalMultiModifiedValue + FinalAddModifedValue
Mark the property with UPROPERTY() if you need to save game, I don’t need to do that for it is only a example project so I didn’t do that.
As a result, the implementation is:
UCLASS(Blueprintable) class CODEERO_API UGameplayAttribute : public UObject { GENERATED_BODY() public: TArray<float> BaseValue; TArray<float> CurrentValue; inline static TArray<FVector2f> Range = TArray<FVector2f>(); public: static TArray<FVector2f> GetAttributeRange(); TArray<float> AddModifierValues; TArray<float> MultiModifierValues; TArray<float> FinalAddModifierValues; TArray<float> FinalMultiModifierValues; TArray<TSharedPtr<TSet<UGameplayAttributeModifier*>>> GameplayAttributeModifierSets; public: UGameplayAttribute(); private: void CalculateCurrentValue(); void CalculateSingleCurrentValueByAttributeType(EGameplayAttributeType GameplayAttribute); void ClampCurrentValue(); void ClampSingleCurrentValueByAttributeType(EGameplayAttributeType GameplayAttribute); public: UFUNCTION(BlueprintCallable) void InitWithGameplayAttributeData(const UGameplayAttributeData* GameplayAttributeData); void RegisterGameplayAttributeModifer(UGameplayAttributeModifier* GameplayAttributeModifier); void UnRegisterGameplayAttributeModifer(const UGameplayAttributeModifier* GameplayAttributeModifier); UFUNCTION(BlueprintCallable) float GetAttributeValue(EGameplayAttributeType GameplayAttribute); UFUNCTION(BlueprintCallable) void SetBaseValue(EGameplayAttributeType GameplayAttribute, float Value); };
The .cpp file is:
TArray<FVector2f> UGameplayAttribute::GetAttributeRange() { if (Range.Num() == 0) { for (int i = 0; i < static_cast<int>(EGameplayAttributeType::EnumCount); ++i) { FVector2f CurrentRange = (*UGameplayAttributeData::GetGameplayAttributeFieldInfoList())[i].GameplayAttributeMeta->GetValueRange(); Range.Add(CurrentRange); } } check (Range.Num() != 0) return Range; } UGameplayAttribute::UGameplayAttribute() { constexpr int GameplayAttributeEnumCount = static_cast<int>(EGameplayAttributeType::EnumCount); BaseValue.Init(0, GameplayAttributeEnumCount); CurrentValue.Init(0, GameplayAttributeEnumCount); AddModifierValues.Init(0, GameplayAttributeEnumCount); MultiModifierValues.Init(1, GameplayAttributeEnumCount); FinalAddModifierValues.Init(0, GameplayAttributeEnumCount); FinalMultiModifierValues.Init(1, GameplayAttributeEnumCount); GameplayAttributeModifierSets.Init(MakeShareable(new TSet<UGameplayAttributeModifier*>()), GameplayAttributeEnumCount); } void UGameplayAttribute::CalculateCurrentValue() { for (int i = 0; i < static_cast<int>(EGameplayAttributeType::EnumCount); ++i) { CurrentValue[i] = (BaseValue[i] * MultiModifierValues[i] + AddModifierValues[i]) * FinalMultiModifierValues[i] + FinalAddModifierValues[i]; } ClampCurrentValue(); } void UGameplayAttribute::CalculateSingleCurrentValueByAttributeType(EGameplayAttributeType GameplayAttribute) { const int GameplayAttributeIndex = static_cast<int>(GameplayAttribute); check(static_cast<int>(GameplayAttributeIndex) < static_cast<int>(EGameplayAttributeType::EnumCount)) CurrentValue[GameplayAttributeIndex] = (BaseValue[GameplayAttributeIndex] * MultiModifierValues[GameplayAttributeIndex] + AddModifierValues[GameplayAttributeIndex]) * FinalMultiModifierValues[GameplayAttributeIndex] + FinalAddModifierValues[GameplayAttributeIndex]; ClampSingleCurrentValueByAttributeType(GameplayAttribute); } void UGameplayAttribute::ClampCurrentValue() { for (int i = 0; i < static_cast<int>(EGameplayAttributeType::EnumCount); ++i) { CurrentValue[i] = FMath::Clamp(CurrentValue[i], GetAttributeRange()[i].X, GetAttributeRange()[i].Y); } } void UGameplayAttribute::ClampSingleCurrentValueByAttributeType(EGameplayAttributeType GameplayAttribute) { const int GameplayAttributeIndex = static_cast<int>(GameplayAttribute); CurrentValue[GameplayAttributeIndex] = FMath::Clamp(CurrentValue[GameplayAttributeIndex], GetAttributeRange()[GameplayAttributeIndex].X, GetAttributeRange()[GameplayAttributeIndex].Y); } void UGameplayAttribute::InitWithGameplayAttributeData(const UGameplayAttributeData* GameplayAttributeData) { for (const auto& FieldInfo : *UGameplayAttributeData::GetGameplayAttributeFieldInfoList()) { if (const FFloatProperty* FloatProperty = CastField<FFloatProperty>(FieldInfo.PropertyField)) { BaseValue.Add(FloatProperty->GetPropertyValue_InContainer(GameplayAttributeData)); CurrentValue.Add(FloatProperty->GetPropertyValue_InContainer(GameplayAttributeData)); } } ClampCurrentValue(); } void UGameplayAttribute::RegisterGameplayAttributeModifer(UGameplayAttributeModifier* GameplayAttributeModifier) { const EGameplayAttributeType GameplayAttributeType = GameplayAttributeModifier->TargetGameplayAttribute; const EGameplayAttributeModifierOperator GameplayAttributeModifierOperator = GameplayAttributeModifier->TargetGameplayAttributeModifierOperator; const float Value = GameplayAttributeModifier->ModifierValue; switch (GameplayAttributeModifierOperator) { case EGameplayAttributeModifierOperator::Add : AddModifierValues[static_cast<int>(GameplayAttributeType)] += Value; break; case EGameplayAttributeModifierOperator::Multi : MultiModifierValues[static_cast<int>(GameplayAttributeType)] += Value; break; case EGameplayAttributeModifierOperator::FinalAdd : FinalAddModifierValues[static_cast<int>(GameplayAttributeType)] += Value; break; case EGameplayAttributeModifierOperator::FinalMulti : FinalMultiModifierValues[static_cast<int>(GameplayAttributeType)] += Value; break; default: ; } GameplayAttributeModifierSets[static_cast<int>(GameplayAttributeType)]->Add(GameplayAttributeModifier); CalculateSingleCurrentValueByAttributeType(GameplayAttributeType); ClampSingleCurrentValueByAttributeType(GameplayAttributeType); } void UGameplayAttribute::UnRegisterGameplayAttributeModifer(const UGameplayAttributeModifier* GameplayAttributeModifier) { const EGameplayAttributeType GameplayAttributeType = GameplayAttributeModifier->TargetGameplayAttribute; const EGameplayAttributeModifierOperator GameplayAttributeModifierOperator = GameplayAttributeModifier->TargetGameplayAttributeModifierOperator; const float Value = GameplayAttributeModifier->ModifierValue; switch (GameplayAttributeModifierOperator) { case EGameplayAttributeModifierOperator::Add : AddModifierValues[static_cast<int>(GameplayAttributeType)] -= Value; break; case EGameplayAttributeModifierOperator::Multi : MultiModifierValues[static_cast<int>(GameplayAttributeType)] -= Value; break; case EGameplayAttributeModifierOperator::FinalAdd : FinalAddModifierValues[static_cast<int>(GameplayAttributeType)] -= Value; break; case EGameplayAttributeModifierOperator::FinalMulti : FinalMultiModifierValues[static_cast<int>(GameplayAttributeType)] -= Value; break; default: ; } GameplayAttributeModifierSets[static_cast<int>(GameplayAttributeType)]->Remove(GameplayAttributeModifier); CalculateSingleCurrentValueByAttributeType(GameplayAttributeType); ClampSingleCurrentValueByAttributeType(GameplayAttributeType); } float UGameplayAttribute::GetAttributeValue(EGameplayAttributeType GameplayAttribute) { return CurrentValue[static_cast<int>(GameplayAttribute)]; } void UGameplayAttribute::SetBaseValue(EGameplayAttributeType GameplayAttribute, const float Value) { BaseValue[static_cast<int>(GameplayAttribute)] = Value; CalculateSingleCurrentValueByAttributeType(GameplayAttribute); }
Now it’s done, you can write a simple test for it:
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGameplayAttributeModiferTest, "GameplayTest.GameplayAttribute.GameplayAttributeModifer", EAutomationTestFlags::EngineFilter | EAutomationTestFlags::EditorContext) bool FGameplayAttributeModiferTest::RunTest(const FString& Parameters) { UGameplayAttribute* GameplayAttribute = NewObject<UGameplayAttribute>(); UE_LOG(LogAutomaticTest, Log, TEXT("Set MaxHealth(Base) To 100")) GameplayAttribute->SetBaseValue(EGameplayAttributeType::MaxHealth, 100); UE_LOG(LogAutomaticTest, Log, TEXT("Current MaxHealth : %f"), GameplayAttribute->GetAttributeValue(EGameplayAttributeType::MaxHealth)); UE_LOG(LogAutomaticTest, Log, TEXT("Register a +25 Modifer to MaxHealth")) UGameplayAttributeModifier* HealthGameplayAttributeModifer = NewObject<UGameplayAttributeModifier>(); HealthGameplayAttributeModifer->TargetGameplayAttribute = EGameplayAttributeType::MaxHealth; HealthGameplayAttributeModifer->TargetGameplayAttributeModifierOperator = EGameplayAttributeModifierOperator::Add; HealthGameplayAttributeModifer->ModifierValue = 25; GameplayAttribute->RegisterGameplayAttributeModifer(HealthGameplayAttributeModifer); UE_LOG(LogAutomaticTest, Log, TEXT("Current MaxHealth : %f"), GameplayAttribute->GetAttributeValue(EGameplayAttributeType::MaxHealth)); UE_LOG(LogAutomaticTest, Log, TEXT("MaxHealth + 20%")) UGameplayAttributeModifier* HealthMultiGameplayAttributeModifer = NewObject<UGameplayAttributeModifier>(); HealthMultiGameplayAttributeModifer->TargetGameplayAttribute = EGameplayAttributeType::MaxHealth; HealthMultiGameplayAttributeModifer->TargetGameplayAttributeModifierOperator = EGameplayAttributeModifierOperator::Multi; HealthMultiGameplayAttributeModifer->ModifierValue = 0.2f; GameplayAttribute->RegisterGameplayAttributeModifer(HealthMultiGameplayAttributeModifer); UE_LOG(LogAutomaticTest, Log, TEXT("Current MaxHealth : %f"), GameplayAttribute->GetAttributeValue(EGameplayAttributeType::MaxHealth)); UE_LOG(LogAutomaticTest, Log, TEXT("Unregister MaxHealth + 25 Modifier")) GameplayAttribute->UnRegisterGameplayAttributeModifer(HealthGameplayAttributeModifer); UE_LOG(LogAutomaticTest, Log, TEXT("Current MaxHealth : %f"), GameplayAttribute->GetAttributeValue(EGameplayAttributeType::MaxHealth)); return true; }
The result is:
LogAutomaticTest: Set MaxHealth(Base) To 100
LogAutomaticTest: Current MaxHealth : 100.000000
LogAutomaticTest: Register a +25 Modifer to MaxHealth
LogAutomaticTest: Current MaxHealth : 125.000000
LogAutomaticTest: MaxHealth + 20
LogAutomaticTest: Current MaxHealth : 145.000000
LogAutomaticTest: Unregister MaxHealth + 25 Modifier
LogAutomaticTest: Current MaxHealth : 120.000008
It works well!