Startup data: granting abilities from a data asset

Commit 5960ce1 + work in progress · DataAsset_StartupDataBase, DataAsset_HeroStartupData, AWarriorBaseCharacter::CharacterStartupData · 9 min

In one breath

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);
};

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 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.

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.

Tidy-ups worth doing before you commit

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.