3 Min. read

How to Deal Damage to World Actors With GAS

While working on The Bug Squad I run into a situation where I needed to apply damage to world actors — in my case explosive barrels — using the Gameplay Ability System.

The standard way to do this in UE is to set bCanBeDamaged to true on the actor and override the TakeDamage function and/or related events. This, however, doesn’t work with GAS where damage is applied via gameplay effects.

A quick search in the Unreal Slackers Discord showed the common practice is to give an AbilitySystemComponent to these actors. Apparently, this is also the approach Epic uses in Fortnite for world objects that can be destroyed such as rocks, trees, walls, etc.

Implementation

Since there can easily be hundreds of these actors in the world, it’s advisable to lazily initialize their AbilitySystemComponent when needed (i.e. when the actor takes damage). Here’s my current implementation:

GameplayActor.h
UCLASS(Abstract)
class AGameplayActor : public AActor
{
    GENERATED_BODY()
 
public:
 
    AGameplayActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
 
    UPROPERTY(Transient, Replicated)
    TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
 
    /** Initializes the ability system component. */
    void InitializeAbilitySystem();
 
protected:
 
    /** Called on authority and clients when the ability system component is initialized. */
    virtual void AbilitySystemInitialized();
 
    // AActor interface
public:
    void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    void OnSubobjectCreatedFromReplication(UObject* NewSubObject) override;
};
GameplayActor.cpp
AGameplayActor::AGameplayActor(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    bReplicates = true;
}
 
void AGameplayActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
    FDoRepLifetimeParams SharedParams;
    SharedParams.bIsPushBased = true;
 
    DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, AbilitySystemComponent, SharedParams);
}
 
void AGameplayActor::OnSubobjectCreatedFromReplication(UObject* NewSubObject)
{
    Super::OnSubobjectCreatedFromReplication(NewSubObject);
 
    // Initialize ASC immediately as it is created (OnRep would be called too late)
    if (AbilitySystemComponent == nullptr)
    {
        AbilitySystemComponent = Cast<UAbilitySystemComponent>(NewSubObject);
        if (AbilitySystemComponent != nullptr)
        {
            AbilitySystemInitialized();
        }
    }
}
 
void AGameplayActor::InitializeAbilitySystem()
{
    if (!ensure(HasAuthority()) || AbilitySystemComponent != nullptr)
    {
        // Don't initialize twice or on clients
        return;
    }
 
    AbilitySystemComponent = NewObject<UAbilitySystemComponent>(this);
    AbilitySystemComponent->SetIsReplicated(true);
    AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
    AbilitySystemComponent->RegisterComponent();
    MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, AbilitySystemComponent, this);
 
    // Attribute set (i.e. containing the Health attribute) can be initialized here
    // Startup gameplay effects can be applied here too
 
    AbilitySystemInitialized();
}
 
void AGameplayActor::AbilitySystemInitialized()
{
    check(AbilitySystemComponent != nullptr);
    if (!HasAuthority())
    {
        // This is done automatically on the server when the component is registered
        AbilitySystemComponent->InitAbilityActorInfo(this, this);
    }
}

InitializeAbilitySystem can be called on authority whenever the AbilitySystemComponent needs to be initialized. The same function can be used to setup attribute sets or apply startup gameplay effects.

Gameplay actors and characters can share the same base attribute set which includes health and damage attributes, which allows damage to be applied via the same code path to both.

On clients, the AbilitySystemComponent will be created automatically via replication. The AbilitySystemInitialized callback can be used for initialization that’s shared between server and clients.

Initializing the ability system

In my project I’m calling InitializeAbilitySystem when applying area effects via my custom FGameplayAbilityTargetData subclasses:

TArray<FActiveGameplayEffectHandle> FMyCustomGameplayAbilityTargetData::ApplyGameplayEffectSpec(
    FGameplayEffectSpec& Spec, const FPredictionKey PredictionKey)
{
    // Activate gameplay actors
    for (const TWeakObjectPtr<AActor>& TargetActor : GetActors())
    {
        AGameplayActor* GameplayActor = Cast<AGameplayActor>(TargetActor);
        if (GameplayActor != nullptr && GameplayActor->GetAbilitySystemComponent() == nullptr)
        {
            GameplayActor->InitializeAbilitySystem();
        }
    }
 
    return FGameplayAbilityTargetData::ApplyGameplayEffectSpec(Spec, PredictionKey);
}

And here’s the explosive barrels in The Bug Squad implemented using this method:

Early explosive barrel implementation using GAS.