Startup data: granting abilities from a data asset
Each character gets a data asset listing the abilities it starts with. When the character is possessed, it
loads that asset and the asset grants each ability to the character's ability system component. That's how
GA_Hero_SpawnAxe finally runs: it's in the "activate on given" list, so granting it triggers it. Which
abilities a character starts with is now data (DA_Hero), not code.
The data asset
UCLASS()
class UDataAsset_StartupDataBase : public UDataAsset
{
public:
virtual void GiveToAbilitySystemComponent(UWarriorAbilitySystemComponent* InASCToGive, int32 ApplyLevel = 1);
protected:
UPROPERTY(EditDefaultsOnly, Category = "Statup Data")
TArray<TSubclassOf<UWarriorGameplayAbility>> ActivatedOnGivenAbilities;
UPROPERTY(EditDefaultsOnly, Category = "Statup Data")
TArray<TSubclassOf<UWarriorGameplayAbility>> ReactiveAbilities;
void GrantAbilities(const TArray<TSubclassOf<UWarriorGameplayAbility>>& InAbilitiesToGive,
UWarriorAbilitySystemComponent* InASCToGive, int32 ApplyLevel = 1);
};
TSubclassOf<UWarriorGameplayAbility>: a class, not an object. You're listing which kinds of ability to grant; the ability system creates the instances. The template also filters the editor dropdown to only showUWarriorGameplayAbilitysubclasses. It's the same<T>idea as the templates page.- Two lists, two jobs:
- ActivatedOnGiven: abilities with the
OnGivenpolicy that run once, straight away. The axe spawn goes here. - Reactive: granted now, but they wait for something to trigger them. Think hit reactions and death, which react to events.
- ActivatedOnGiven: abilities with the
virtualGive…: the hero version (UDataAsset_HeroStartupData, empty for now) can override it to grant hero-only extras later, callingSuper::for the shared part.- Base + hero subclass again: the same pattern as characters, anim instances and weapons. Enemies will get their own subclass.
Granting: the loop
for (const TSubclassOf<UWarriorGameplayAbility>& Ability : InAbilitiesToGive)
{
if (!Ability) continue; // skip an empty slot in the editor list
FGameplayAbilitySpec AbilitySpec(Ability);
AbilitySpec.SourceObject = InASCToGive->GetAvatarActor();
AbilitySpec.Level = ApplyLevel;
InASCToGive->GiveAbility(AbilitySpec);
}
- The spec, from the last lesson: "this ability, at this level, on this ASC". You build it, then
GiveAbilityit. SourceObject: records what granted this ability. Inside the ability you can read it back withGetCurrentSourceObject(). Using the avatar (the character) here means "the character itself granted it". It becomes more useful when a weapon grants abilities: the ability can then find out which weapon it belongs to.Level: GAS abilities have levels, so damage and costs can scale by level. The default of 1 is fine for now.if (!Ability) continue;: someone added a slot to the array and left it empty. Skip it, don't crash.- The loop is by const reference (why).
The character side (work in progress)
// WarriorBaseCharacter.h
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "CharacterData")
TSoftObjectPtr<UDataAsset_StartupDataBase> CharacterStartupData;
// AWarriorBaseCharacter::PossessedBy
WarriorAbilitySystemComponent->InitAbilityActorInfo(this, this);
ensureMsgf(!CharacterStartupData.IsNull(), TEXT("Forgot to assign startup data to %s"), *GetName());
// AWarriorHeroCharacter::PossessedBy
Super::PossessedBy(NewController);
if (!CharacterStartupData.IsNull())
if (UDataAsset_StartupDataBase* LoadedData = CharacterStartupData.LoadSynchronous())
LoadedData->GiveToAbilitySystemComponent(WarriorAbilitySystemComponent);
Why a soft pointer?
This is your mock Q9 in real code. The startup data lists abilities; the
abilities reference montages, weapon Blueprints, meshes and sounds. A hard pointer would load all of that whenever the
character class loads, even in a menu. A TSoftObjectPtr stores only the asset's path, so nothing
loads until you ask.
IsNull(): "was a path assigned at all?" It's different from "is it loaded yet?", which isIsValid()/Get()returning non-null.LoadSynchronous(): "load it now, and wait". Simple, but the game freezes until it's loaded. For the one player at spawn that's acceptable. For enemies spawning mid-fight it would hitch, so the usual answer there is an async load (UAssetManager/FStreamableManager) with a callback when it's ready.
Your ensure, in real code
The base character checks with ensureMsgf: if nobody assigned startup data, you get a message with a
callstack in the log, once, and the game keeps running. The character just starts without abilities. That's the
right choice here: a missing asset is a mistake to report, not a reason to crash the editor. Compare
checkf on the input config, where the game can't run at all without it.
%s + *GetName() puts the character's name in the message, so you know which
Blueprint is missing its data. (The same *-on-FString trick as the Debug helper.)
Order matters: why grant after Super::PossessedBy
The base PossessedBy calls InitAbilityActorInfo. Granting needs that done first: an
OnGiven ability activates during GiveAbility, and activation needs to know the owner and
avatar. Calling Super first guarantees the ASC is set up before anything is granted. Swap the order and the
axe-spawn ability would try to run without an avatar.
- Include order in
WarriorHeroCharacter.cpp:AbilitySystem/WarriorAbilitySystemComponent.his now aboveCharacters/WarriorHeroCharacter.h. Unreal's convention is a .cpp includes its own header first. That proves the header compiles on its own and doesn't secretly rely on something included before it (mock Q1). Move it back below. class UDataAsset_StartupDataBase;is forward-declared twice inWarriorBaseCharacter.h. That's legal and harmless; delete one.- Category
"Statup Data"is missing an r, and it shows in the Details panel. - The parameter is named
InAbiliitiesToGivein the .h andInAbiliitiesTGivein the .cpp. It compiles, because parameter names in a declaration and a definition don't have to match; the compiler only checks types. Still worth making them match. if (InAbilitiesToGive.IsEmpty()) return;is redundant: a loop over an empty array runs zero times. Harmless, and some people like it as a signal of intent.
Quiz yourself
Why is the list TSubclassOf<UWarriorGameplayAbility> rather than ability objects?
You're choosing which classes of ability to grant; GAS creates the instances. TSubclassOf also limits the editor picker to valid subclasses.
ActivatedOnGiven vs Reactive abilities: what goes in each?
ActivatedOnGiven: one-shot setup abilities that run as soon as they're granted (spawn the axe). Reactive: granted now, triggered later by events (hit react, death).
What is AbilitySpec.SourceObject for?
It records what granted the ability. The ability can read it back (GetCurrentSourceObject()), which is useful when e.g. a weapon grants abilities and they need their weapon.
Why is CharacterStartupData a soft pointer?
A hard pointer would load the data asset and everything it references (abilities → montages, weapons, sounds) whenever the character class loads. Soft stores only a path until you load it.
IsNull() vs IsValid() on a TSoftObjectPtr?
IsNull(): no path assigned at all. IsValid(): the asset is currently loaded. A soft pointer can be assigned but not loaded.
What's the cost of LoadSynchronous(), and when would you avoid it?
The game thread waits until the asset loads, which means a hitch. Fine for the player at spawn; for enemies spawning during play, use an async load with a callback.
Why ensureMsgf for missing startup data, but checkf for the missing input config?
Without startup data the character still works, just without abilities: report it and carry on. Without the input config nothing can be bound, so crash loudly with a clear message.
Why must abilities be granted after Super::PossessedBy?
The base version calls InitAbilityActorInfo. OnGiven abilities activate during GiveAbility and need the owner/avatar info already set.
Why should a .cpp include its own header first?
It proves the header compiles on its own. If it secretly needed something, that shows up as an error now, instead of later when include order or unity grouping changes.
The parameter is named differently in the .h and .cpp. Does it compile?
Yes. Only the types have to match; names in the declaration are ignored by the compiler.