#UI for Buffs and Debuffs

1 messages · Page 1 of 1 (latest)

candid pelican
#

Here is my implementation to showcase "buffs" and "debuffs".

Basically, every time that I use the teleport ability, I gain a temporary gameplay effect that increase an attribute (movement speed in this case).
The shoe with wings icon represent this "buff".
The others reds icons are just placeholder widgets to have an idea how on the UI will be with a lot of differents buffs and debuffs at the same time.

For those interested, but wanting a little challenge, you can try it yourself without reading the rest, it is not that complicated because we kind of already did it.
check back what we did with the message when we pickup a potion or crystal. This is really close to that.

#

Lets check back on how we get to know when a gameplay effect is applied to then make a message appear in the overlay :

#

This is great. We can reuse this, and have some data passed down when a GE with a tag "Buff" or "Debuff" is applied.
Lets make something to contains the data.

#pragma once

#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Engine/DataAsset.h"
#include "StatusEffectInfo.generated.h"

USTRUCT(BlueprintType)
struct FEffectInfo
{
    GENERATED_BODY()

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    FGameplayTag EffectTag = FGameplayTag();

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    FText EffectName = FText();

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    TObjectPtr<const UTexture2D> EffectIcon = nullptr;

    UPROPERTY(BlueprintReadOnly)
    bool bIsDebuff = false;
    
    UPROPERTY(BlueprintReadOnly)
    bool bHasDuration = true;
    
    UPROPERTY(BlueprintReadOnly)
    float Duration = 0.f;
    
    UPROPERTY(BlueprintReadOnly)
    bool bDisplayStack = false;

    UPROPERTY(BlueprintReadOnly)
    int32 StackCount = 0;
};

/**
 * 
 */
UCLASS()
class AURA_API UStatusEffectInfo : public UDataAsset
{
    GENERATED_BODY()

public:
    FEffectInfo FindEffectInfoForTag(const FGameplayTag& EffectTag, bool bLogNotFound = false) const;
    
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    TArray<FEffectInfo> EffectsInformation;
};

#include "AbilitySystem/Data/StatusEffectInfo.h"

#include "Aura/AuraLogChannels.h"

FEffectInfo UStatusEffectInfo::FindEffectInfoForTag(const FGameplayTag& EffectTag, bool bLogNotFound) const
{
    for (FEffectInfo Info : EffectsInformation)
    {
        if (Info.EffectTag.MatchesTagExact(EffectTag))
        {
            return Info;
        }
    }

    if (bLogNotFound)
    {
        UE_LOG(LogAura, Error, TEXT("Can't find Info for AttributeTag [%s] on AttributeInfo [%s]."), *EffectTag.ToString(), *GetNameSafe(this));
    }

    return FEffectInfo();
}
#

Lets now modify a bit EffectAssetTags to pass down more params, and also make a delegate for when a GE is removed :

DECLARE_MULTICAST_DELEGATE_FiveParams(FEffectAssetTags, const FGameplayTagContainer& /*AssetTags*/, bool /*bHasDuration*/, const float /*Duration*/, bool /*DisplayStackCount*/, const int32 /*StackCount*/);
DECLARE_MULTICAST_DELEGATE_OneParam(FEffectRemovedSignature, const FGameplayTagContainer& /*AssetTags*/);

    FEffectAssetTags EffectAssetTags;
    FEffectRemovedSignature EffectRemovedDelegate;
void UAuraAbilitySystemComponent::AbilityActorInfoSet()
{
    OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::ClientEffectApplied);
    OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &UAuraAbilitySystemComponent::OnRemoveGameplayEffectCallback);
}
void UAuraAbilitySystemComponent::ClientEffectApplied_Implementation(UAbilitySystemComponent* AbilitySystemComponent,
                                                                     const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveEffectHandle)
{
    FGameplayTagContainer TagContainer;
    EffectSpec.GetAllAssetTags(TagContainer);
    const bool HasDuration = EffectSpec.Def->DurationPolicy == EGameplayEffectDurationType::HasDuration;
    const bool DisplayStackCount = EffectSpec.Def->StackingType != EGameplayEffectStackingType::None && EffectSpec.Def->StackLimitCount > 1;
    EffectAssetTags.Broadcast(TagContainer, HasDuration, EffectSpec.Duration, DisplayStackCount, EffectSpec.GetStackCount());
}

void UAuraAbilitySystemComponent::OnRemoveGameplayEffectCallback_Implementation(const FActiveGameplayEffect& EffectRemoved)
{
    FGameplayTagContainer TagContainer;
    EffectRemoved.Spec.GetAllAssetTags(TagContainer);
    EffectRemovedDelegate.Broadcast(TagContainer);
}
#

And inside** UOverlayWidgetController::BindCallbacksToDependencies()** (cant make in one post unfortunaly).

    GetAuraASC()->EffectAssetTags.AddLambda(
            [this](const FGameplayTagContainer& AssetTags, bool HasDuration, const float Duration, bool DisplayStackCount, const int32 StackCount)
            {
                FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
                FGameplayTag BuffTag = FGameplayTag::RequestGameplayTag(FName("Buff"));
                FGameplayTag DebuffTag = FGameplayTag::RequestGameplayTag(FName("Debuff"));
                for (const FGameplayTag& Tag : AssetTags)
                {
                    if (Tag.MatchesTag(MessageTag))
                    {
                        FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
                        MessageWidgetRowDelegate.Broadcast(*Row);
                    }
                    if (Tag.MatchesTag(BuffTag))
                    {
                        FEffectInfo EffectInfo = StatusEffectData->FindEffectInfoForTag(Tag);
                        if (EffectInfo.EffectTag.IsValid())
                        {
                            EffectInfo.bHasDuration = HasDuration;
                            EffectInfo.Duration = Duration;
                            EffectInfo.bDisplayStack = DisplayStackCount;
                            EffectInfo.StackCount = StackCount;
                            StatusEffectWidgetDelegate.Broadcast(EffectInfo);
                        }
                    }
#
                    if (Tag.MatchesTag(DebuffTag))
                    {
                        FEffectInfo EffectInfo = StatusEffectData->FindEffectInfoForTag(Tag);
                        if (EffectInfo.EffectTag.IsValid())
                        {
                            EffectInfo.bIsDebuff = true;
                            EffectInfo.bHasDuration = HasDuration;
                            EffectInfo.Duration = Duration;
                            EffectInfo.bDisplayStack = DisplayStackCount;
                            EffectInfo.StackCount = StackCount;
                            StatusEffectWidgetDelegate.Broadcast(EffectInfo);
                        }
                    }
                }
            }
        );

#

This is it for the code. Now it is time to show a ton of screenshots for the widgets.
First, for the circular progress bar, I just created a material following this tutorial : https://www.youtube.com/watch?v=BgOAbAdi8f0

#

Discord did not put the files in the right order, but if you download them, the files names will help to know where to look first

#

Then I created a widget "container" or wrapper for the buffs/debuffs widget. I added some widgets to have a taste of what it look like and make adjustement for width, height , etc.
The Buffs Box and Debuffs Box are simply Wrap Box and the BaseBox is a horizontal box.

#

And I add the Buffs_Debuffs_Container inside WBP_Overlay, and I set the widget controller for it. And thats it.

lost turtle
# candid pelican Lets now modify a bit **EffectAssetTags** to pass down more params, and also mak...

Hi! I'm getting a crash in ClientEffectApplied_Implementation when accessing EffectSpec.Def in multiplayer.

The code you provided works fine on the server, but crashes on clients with:
EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000038

The crash happens at line:
const bool HasDuration = EffectSpec.Def->DurationPolicy == ...

Client log shows:

  • EffectSpec.Def is NULL
  • But EffectSpec.Duration, StackCount, and Level are valid

I'm creating GameplayEffects dynamically using:
UGameplayEffect* Effect = NewObject<UGameplayEffect>(GetTransientPackage(), ...);

Is this the cause? How should I fix it for multiplayer

unkempt peak
lost turtle
unkempt peak
lost turtle