Comprehensive GameplayAbilities Analysis Series

You can write an initter any way you want, including reading an analogue value rather than a set of discreet values. Mind you if you want to use something other than curve data tables you will have to override UAbilitySystemGlobals and change what it loads up into the initter. However depending on how often you actually need to run your growth function it will probably cause performance issues. You could set up a timer where if your character has grown you run the initter every couple of seconds or whatever value works for your growth speed.

Alternatively, the initter is only as complex as its InitAttributeSetDefaults function. If you make that optimized it should work just fine for you. Of course you don’t even need to use the pre-existing InitAttributeSetDefaults override and could create your own UpdateAttributeValuesBasedOnGrowth and then do the bare-minimum operations there to update your values (rather than the full group lookup etc.).

Whatever you do you don’t want to do it on tick, you need to have a timer for it even if that timer is 0.2 seconds.

This thread is indeed the most helpful one, providing details and thoughts behind the code.
Really hope it grows into a real “series”, including chained basic attacks, spells, gameplay tags, cues, etc…

And also, I think people using GAS are using online subsystems for the most cases. The experiences about creating online battle games would be appreciated too.

Thanks for the compliment. I can definitely do a writeup on our process of how we actually translate input to attacks and how we chain attacks, but there definitely won’t be anything about online stuff because we strictly do single-player games.

I’ve just started looking at this thread and implemented my own FAttributeSetInitter, but I am also struggling to understand the issue with regard to the POD check.

My AttributeSet is using FGameplayAttributeData rather than a float. I essentially copied the FAttributeSetInitter code wihtout that check and it appears (on face value) to work OK?

I’m very much a novice so I’m not entirely sure what the issue is here… sorry if this should be obvious, but I’m just not getting it.

Can anyone elaborate as to why that check exists and what the ramifications are with removing it if your Attributes are FGameplayAttributeData rather than a float?

As I said in the original post, this is actually required to have it work with FGameplayAttributeData. They originally used floats, hence the check, but that’s deprecated at this point.

It does work, that’s the point, whether it’s float or FGameplayAttributeData seems to make no difference.

How can i make a Random Item Property Generator like diablo using GAS? as far as i know you cannot add tags and effects on the fly.
It could generate a ranged projectile of ice, a aura of healing or a chain ray of acid.
So all of those descriptors are tags, and cost and damage effects values can change. but how can i implement that without creation a GameAbility for each possible combination?

You’d want a whole bunch of gameplay effects each covering an individual modificatoin, then just randomize which effects your item grants.

Thanks for all the information.

Here’s my FAttributeSetInitterCurveEval struct i came up with, if anyone is still interested.
Works with 4.25.



#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemGlobals.h"
#include <AttributeSet.h>
#include <AbilitySystemComponent.h>

// thanks to : https://forums.unrealengine.com/community/community-content-tools-and-tutorials/116578-comprehensive-gameplayabilities-analysis-series
struct RPG_API FAttributeSetInitterCurveEval : public FAttributeSetInitter
{
struct FPropertyCurvePair
{
FPropertyCurvePair(FProperty* InProperty, FRealCurve* InCurve)
: Property(InProperty), Curve(InCurve)
{
}

FProperty* Property;
FRealCurve* Curve;
};

struct FAttributeDefaultCurveList
{

struct FPropertyCurvePair
{
FPropertyCurvePair(FProperty* InProperty, FRealCurve* InCurve)
: Property(InProperty), Curve(InCurve)
{
}

FProperty* Property;
FRealCurve* Curve;
};

void AddPair(FProperty* InProperty, FRealCurve* InValue)
{
List.Add(FPropertyCurvePair(InProperty, InValue));
}

TArray<FPropertyCurvePair> List;
};

struct FAttributeSetDefaultsCurveCollection
{
TMap<TSubclassOf<UAttributeSet>, FAttributeDefaultCurveList> DataMap;
};

TMap<FName, FAttributeSetDefaultsCurveCollection> Defaults;

bool IsSupportedProperty(FProperty* Property) const
{
return (Property && (CastField<FNumericProperty>(Property) || FGameplayAttribute::IsGameplayAttributeDataProperty(Property)));
}

TSubclassOf<UAttributeSet> RPGFindBestAttributeClass(TArray<TSubclassOf<UAttributeSet> >& ClassList, FString PartialName)
{
for (auto Class : ClassList)
{
if (Class->GetName().Contains(PartialName))
{
return Class;
}
}

return nullptr;
}


virtual void PreloadAttributeSetData(const TArray<UCurveTable*>& CurveData) override
{
if (!ensure(CurveData.Num() > 0))
{
return;
}

/**
* Get list of AttributeSet classes loaded
*/

TArray<TSubclassOf<UAttributeSet> > ClassList;
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (TestClass->IsChildOf(UAttributeSet::StaticClass()))
{
ClassList.Add(TestClass);
/*#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// This can only work right now on POD attribute sets. If we ever support FStrings or TArrays in AttributeSets
// we will need to update this code to not use memcpy etc.
for (TFieldIterator<UProperty> PropIt(TestClass, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt)
{
if (!PropIt->HasAllPropertyFlags(CPF_IsPlainOldData))
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to Handle AttributeClass %s because it has a non POD property: %s"),
*TestClass->GetName(), *PropIt->GetName());
return;
}
}
#endif*/
}
}


//Iterate over the table array...
for (const UCurveTable* CurTable : CurveData)
{
for (const TPair<FName, FRealCurve*>& CurveRow : CurTable->GetRowMap())
{
//The entire row name, i.e. Class.Player.MaxHealth
FString RowName = CurveRow.Key.ToString();
FString ClassName;
FString SetName;
FString AttributeName;
FString Temp;

//Split the RowName into ClassName (Class) and the put the rest in Temp (Player.MaxHealth)
RowName.Split(TEXT("."), &ClassName, &Temp);
//Split the remainder into the SetName (Player) and the AttributeName (MaxHealth)
Temp.Split(TEXT("."), &SetName, &AttributeName);

//If some of these ended up unpopulated just disregard this row...
if (!ensure(!ClassName.IsEmpty() && !SetName.IsEmpty() && !AttributeName.IsEmpty()))
{
ABILITY_LOG(Warning, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to parse row %s in %s"), *RowName, *CurTable->GetName());
continue;
}

// Find the AttributeSet
TSubclassOf<UAttributeSet> Set = RPGFindBestAttributeClass(ClassList, SetName);
if (!Set)
{
// This is ok, we may have rows in here that don't correspond directly to attributes
ABILITY_LOG(Verbose, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to match AttributeSet from %s (row: %s)"), *SetName, *RowName);
continue;
}

// Find the UProperty
FProperty* Property = FindFProperty<FProperty>(*Set, *AttributeName);
//The IsSupportedProperty() just does: return (Property && (Cast<UNumericProperty>(Property) || FGameplayAttribute::IsGameplayAttributeDataProperty(Property)));
//meaning "is this a number of a FGameplayAttribute?"
if (!IsSupportedProperty(Property))
{
ABILITY_LOG(Verbose, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to match Attribute from %s (row: %s)"), *AttributeName, *RowName);
continue;
}

FRealCurve* Curve = CurveRow.Value;
FName ClassFName = FName(*ClassName);
//Get the rich curve collection corresponding to our ClassName (or create it)
FAttributeSetDefaultsCurveCollection& DefaultCollection = Defaults.FindOrAdd(ClassFName);


int32 LastLevel = Curve->GetKeyTime(Curve->GetLastKeyHandle());
//DefaultCollection.DataMap.SetNum(FMath::Max(LastLevel, DefaultCollection.DataMap.Num()));

//At this point we know the Name of this "class"/"group", the AttributeSet, and the Property Name. Now loop through the values on the curve to get the attribute default value at each level.
for (auto KeyIter = Curve->GetKeyHandleIterator(); KeyIter; ++KeyIter)
{
const FKeyHandle& KeyHandle = *KeyIter;

TPair<float, float> LevelValuePair = Curve->GetKeyTimeValuePair(KeyHandle);
int32 Level = LevelValuePair.Key;
float Value = LevelValuePair.Value;

//Find the attribute list matching the current UAttributeSet
FAttributeDefaultCurveList* DefaultDataList = DefaultCollection.DataMap.Find(Set);
if (DefaultDataList == nullptr)
{
//If there is no list matching this attribute set... create it.
ABILITY_LOG(Verbose, TEXT("Initializing new default set for %s. PropertySize: %d.. DefaultSize: %d"), *Set->GetName(), Set->GetPropertiesSize(), UAttributeSet::StaticClass()->GetPropertiesSize());

DefaultDataList = &DefaultCollection.DataMap.Add(Set);
}

// Import curve value into default data
//Just add the current property together with its matching curve to the attribute list.
check(DefaultDataList);

DefaultDataList->AddPair(Property, Curve);
}


}
}

}


virtual void InitAttributeSetDefaults(UAbilitySystemComponent* AbilitySystemComponent, FName GroupName, int32 Level, bool bInitialInit) const override
{
//The profiler counter is commented out here, even though it exists in the Discrete Levels version. It seems that, despite these stats existing in AbilitySystemStats.h,
//they're not exported out of the module so they can't be used in your game module. You'll have to make your own stats if you want to profile this. I haven't gotten around to doing that yet.
//SCOPE_CYCLE_COUNTER(STAT_InitAttributeSetDefaults);
check(AbilitySystemComponent != nullptr);

//This whole block will look if the provided group exists in the preloaded data. If it doesn't it checks for the Default group. If that isn't there either, the whole operation is stopped.
const FAttributeSetDefaultsCurveCollection* Collection = Defaults.Find(GroupName);
if (!Collection)
{
ABILITY_LOG(Warning, TEXT("Unable to find DefaultAttributeSet Group %s. Failing back to Defaults"), *GroupName.ToString());
Collection = Defaults.Find(FName(TEXT("Default")));
if (!Collection)
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::InitAttributeSetDefaults Default DefaultAttributeSet not found! Skipping Initialization"));
return;
}
}

//Iterate over all the spawned attribute sets of the provided ASC
for (const UAttributeSet* Set : AbilitySystemComponent->SpawnedAttributes)
{
//Check our preloaded data to see if we have any curves for the givenn attribute set...
const FAttributeDefaultCurveList* DefaultDataList = Collection->DataMap.Find(Set->GetClass());
if (DefaultDataList)
{
ABILITY_LOG(Log, TEXT("Initializing Set %s"), *Set->GetName());
//We found data for the given attribute set. Iterate over it and populate the data
for (auto& DataPair : DefaultDataList->List)
{
check(DataPair.Property);

if (Set->ShouldInitProperty(bInitialInit, DataPair.Property))
{
FGameplayAttribute AttributeToModify(DataPair.Property);
AbilitySystemComponent->SetNumericAttributeBase(AttributeToModify, DataPair.Curve->Eval(Level));
}
}
}
}

AbilitySystemComponent->ForceReplication();
}


virtual void ApplyAttributeDefault(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute& InAttribute, FName GroupName, int32 Level) const override
{
//The profiler counter is commented out here, even though it exists in the Discrete Levels version. It seems that, despite these stats existing in AbilitySystemStats.h,
//they're not exported out of the module so they can't be used in your game module. You'll have to make your own stats if you want to profile this. I haven't gotten around to doing that yet.
//SCOPE_CYCLE_COUNTER(STAT_InitAttributeSetDefaults);
check(AbilitySystemComponent != nullptr);

//This whole block will look if the provided group exists in the preloaded data. If it doesn't it checks for the Default group. If that isn't there either, the whole operation is stopped.
const FAttributeSetDefaultsCurveCollection* Collection = Defaults.Find(GroupName);
if (!Collection)
{
ABILITY_LOG(Warning, TEXT("Unable to find DefaultAttributeSet Group %s. Failing back to Defaults"), *GroupName.ToString());
Collection = Defaults.Find(FName(TEXT("Default")));
if (!Collection)
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::InitAttributeSetDefaults Default DefaultAttributeSet not found! Skipping Initialization"));
return;
}
}

//Iterate over all the spawned attribute sets of the provided ASC
for (const UAttributeSet* Set : AbilitySystemComponent->SpawnedAttributes)
{
//Check our preloaded data to see if we have any curves for the givenn attribute set...
const FAttributeDefaultCurveList* DefaultDataList = Collection->DataMap.Find(Set->GetClass());
if (DefaultDataList)
{
ABILITY_LOG(Log, TEXT("Initializing Set %s"), *Set->GetName());
//We found data for the given attribute set. Iterate over it and populate the data
for (auto& DataPair : DefaultDataList->List)
{
check(DataPair.Property);

if (DataPair.Property == InAttribute.GetUProperty())
{
FGameplayAttribute AttributeToModify(DataPair.Property);
AbilitySystemComponent->SetNumericAttributeBase(AttributeToModify, DataPair.Curve->Eval(Level));
}
}
}
}

AbilitySystemComponent->ForceReplication();
}
};


I’m using version 4.25 and when trying to load the editor, I seem to be hitting an error on this line:
GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetReimport.AddUObject(this, &UAbilitySystemGlobals::OnTableReimported);

Anyone know what’s wrong?



// Handle array of global curve tables for attribute defaults
for (const FSoftObjectPath& AttribDefaultTableName : GlobalAttributeSetDefaultsTableNames)
{
if (AttribDefaultTableName.IsValid())
{
UCurveTable* AttribTable = Cast<UCurveTable>(AttribDefaultTableName.TryLoad());
if (AttribTable)
{
GlobalAttributeDefaultsTables.Add(AttribTable);
bLoadedAnyDefaults = true;
}
}
}

if (bLoadedAnyDefaults)
{
// Subscribe for reimports if in the editor
#if WITH_EDITOR
if (GIsEditor && !RegisteredReimportCallback)
{
GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetReimport.AddUObject(this, &UAbilitySystemGlobals::OnTableReimported);
RegisteredReimportCallback = true;
}
#endif


ReloadAttributeDefaults();
}
}