C++ Pickup Notification similar to Warframe - 4.26

In Warframe, pickup notifications can either appear or increase an existing value.

Frequently interacting with dummy resources. In this clip, the new value of gained resources appends to an existing notification.

Contrary to a Cold War zombies style, where new notifications stack on top of each other.

Still frequently interacting with dummy resources. In this clip, the new value of gained resources always get their own notification.

Both variants can be great for different types of games. That's also why the decision to opt-in/out for one variant will not be abandoned.

Note: The white squares are icon/image placeholders. It's the typical quick visual clue about what you gained.

What (native) classes will be created?

  1. UPickupNotification (Abstract, Blueprintable, Transient)

    This UUserWidget-derived class represent an individual, non-serializable pickup notification. These notification have no awareness about where they are positioned relative to other pickup notification.

  2. UPickupNotificationContainer (Blueprintable)

    This intangible UUserWidget-derived (or directly from UWidget-derived) class manages each UPickupNotification instance. This class is responsible for instantiating each pickup notification with Update() and stores them sorted in the way you'd see them layered-up visually.

    An instance of this class will be added to the content hierarchy of your primary in-game user-interface widget.

Key points before we progress

This guide assumes you have:

  1. Your own way of referring to gameplay things.
    This guide uses Gameplay Tags.

  2. A User Widget serving as your main in-game user interface.
    This guide uses its own, which is typical for every game.

  3. Any event leading up to the notification being displayed.
    This guide randomly interacts with actors.

    It's up to you how you call UPickupNotificationContainer::Update. The way I do it is through the Gameplay Ability System, which was originally included in this guide, but eventually left out for better topic separation.

By design, UPickupNotification relies on its blueprint subclass to choose when the fade-in/fade-out animations play, how the notification moves up, and more.

Contrary to UPickupNotificationContainer, which works on its own, but could be customized trough a blueprint subclass.

This guide uses UUserWidget in anticipation for a desire to implement a wider range of customizations through blueprint subclasses. Using UWidget instead of UUserWidget will work effortlessly on UPickupNotificationContainer, but requires additional effort for UPickupNotification if you follow this guide.

Implementing it - The code

Modules: "Core", "CoreUObject", "Engine", "GameplayTags"

UPickupNotification

Public/PickupNotification.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameplayTags/Classes/GameplayTagContainer.h" #include "PickupNotification.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnNotificationFadingOut, UPickupNotification*, PickupNotification); /** * */ UCLASS(Abstract, Blueprintable, Transient) class YOURPROJECT_CPP_API UPickupNotification : public UUserWidget { GENERATED_BODY() public: UPickupNotification(const FObjectInitializer& ObjectInitializer); /** Every initialization that should happen before we ever get added to viewport. (This doesn't add to viewport.) * Native subclasses can overload this with more variables if necessary */ virtual void PreViewportInit(const FGameplayTag& InPickupType, const float& InMagnitude); /** Add Magnitude */ virtual void AddMagnitude(const float DeltaMagnitude); /** Cosmetically move the notification up */ virtual void MoveUpwards(); protected: virtual void NativeOnInitialized() override; /** * It's up to the blueprint subclass to call this. * The Fade-in animation params can be tuned by implementing this, and playing the animation yourself. */ UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintCosmetic) void PlayFadeInAnimation(); virtual void PlayFadeInAnimation_Implementation(); /** * It's up to the blueprint subclass to call this. * The Fade-out animation params can be tuned by implementing this, and playing the animation yourself. */ UFUNCTION(BlueprintCallable, BlueprintNativeEvent, BlueprintCosmetic) void PlayFadeOutAnimation(); virtual void PlayFadeOutAnimation_Implementation(); UFUNCTION() virtual void FadeInStarted(); UFUNCTION() virtual void FadeInFinished(); UFUNCTION() virtual void FadeOutStarted(); UFUNCTION() virtual void FadeOutFinished(); /** Called after magnitude was added */ UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic) void K2_MagnitudeAdded(const float DeltaMagnitude); /** Cosmetically move the notification up */ UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic) void K2_MoveUpwards(); public: /** Lets anyone bound know we're fading out. */ UPROPERTY(BlueprintAssignable) FOnNotificationFadingOut OnNotificationFadingOut; FORCEINLINE const FGameplayTag& GetPickupType() const { return PickupType; } FORCEINLINE const float& GetMagnitude() const { return Magnitude; } protected: UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Transient) FGameplayTag PickupType; UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Transient) float Magnitude; /** True if FadeInAnim ever started playing */ UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Transient) bool bFadeInHappened; UPROPERTY(Transient, meta = (BindWidgetAnim)) UWidgetAnimation* FadeInAnim; UPROPERTY(Transient, meta = (BindWidgetAnim)) UWidgetAnimation* FadeOutAnim; };

Private/PickupNotification.cpp

#include "PickupNotification.h"
#include "PickupNotificationContainer.h"

#define LOCTEXT_NAMESPACE "PickupNotification"

UPickupNotification::UPickupNotification(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
    , PickupType(FGameplayTag::EmptyTag)
    , Magnitude(0.f)
    , bFadeInHappened(false)
{
    RenderOpacity = 0.f;
}

void UPickupNotification::PreViewportInit(const FGameplayTag& InPickupType, const float& InMagnitude)
{
    PickupType = InPickupType;
    Magnitude = InMagnitude;
}

void UPickupNotification::AddMagnitude(const float DeltaMagnitude)
{
    Magnitude += DeltaMagnitude;
K2_MagnitudeAdded(DeltaMagnitude); } void UPickupNotification::MoveUpwards() { K2_MoveUpwards(); } void UPickupNotification::NativeOnInitialized() { FWidgetAnimationDynamicEvent FadeInStartedEvent; FWidgetAnimationDynamicEvent FadeInFinishedEvent; FWidgetAnimationDynamicEvent FadeOutStartedEvent; FWidgetAnimationDynamicEvent FadeOutFinishedEvent; FadeInStartedEvent.BindDynamic(this, &UPickupNotification::FadeInStarted); FadeInFinishedEvent.BindDynamic(this, &UPickupNotification::FadeInFinished); FadeOutStartedEvent.BindDynamic(this, &UPickupNotification::FadeOutStarted); FadeOutFinishedEvent.BindDynamic(this, &UPickupNotification::FadeOutFinished); BindToAnimationStarted(FadeInAnim, FadeInStartedEvent); BindToAnimationFinished(FadeInAnim, FadeInFinishedEvent); BindToAnimationStarted(FadeOutAnim, FadeOutStartedEvent); BindToAnimationFinished(FadeOutAnim, FadeOutFinishedEvent); Super::NativeOnInitialized(); } void UPickupNotification::PlayFadeInAnimation_Implementation() { PlayAnimation(FadeInAnim); } void UPickupNotification::PlayFadeOutAnimation_Implementation() { PlayAnimation(FadeOutAnim); } void UPickupNotification::FadeInStarted() { bFadeInHappened = true; } void UPickupNotification::FadeInFinished() { } void UPickupNotification::FadeOutStarted() { OnNotificationFadingOut.Broadcast(this); } void UPickupNotification::FadeOutFinished() { RemoveFromParent(); } #undef LOCTEXT_NAMESPACE

UPickupNotificationContainer

Public/PickupNotificationContainer.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameplayTags/Classes/GameplayTagContainer.h"
#include "PickupNotificationContainer.generated.h"

class UPickupNotification;

/**
 * This class manages each UPickupNotification instance.
 */
UCLASS(Blueprintable)
class YOURPROJECT_CPP_API UPickupNotificationContainer : public UUserWidget
{
    GENERATED_BODY()

public:
    UPickupNotificationContainer(const FObjectInitializer& ObjectInitializer);

// Add BlueprintCallable if native isn't (the only one) calling Update. /** Please add a call to parents function at some stage if you're implementing this. */ UFUNCTION(BlueprintNativeEvent, BlueprintCosmetic) bool Update(FGameplayTag PickupType, float DeltaMagnitude); virtual bool Update_Implementation(FGameplayTag PickupType, float DeltaMagnitude); protected: /** Removes the notification */ UFUNCTION() void OnPickupNotificationFadingOut(UPickupNotification* PickupNotification); protected: /** PickupNotification class to spawn */ UPROPERTY(EditAnywhere, BlueprintReadWrite, NoClear, meta = (ExposedOnSpawn = "True")) TSubclassOf<UPickupNotification> PickupNotificationClass; /** Any new pickup notification that gets added to viewport will have this ZOrder. Default is 0 */ UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposedOnSpawn = "True")) int32 PickupNotificationZOrder; /** * False: Always create a new notification. * True: First see if we can append the value to an existing notification. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ExposedOnSpawn = "True")) bool bAllowAppendingToExistingNotification; /** * Map for finding the notification from any way you identify gameplay elements. */ TMap<FGameplayTag, UPickupNotification*> NotificationsMap; /** * Map tracking the index of each notification inside Notifications. * This just ensures O(1) remove speed. It could be ditched if you find it excessive. */ TMap<UPickupNotification*, int32> NotificationsIndex; /** * Hot array, consciously kept sorted in a way that matches the cosmetic result. * Note: Update() relies on at least one entry to exist. */ TArray<UPickupNotification*> Notifications{ nullptr }; };

Private/PickupNotificationContainer.cpp

#include "PickupNotificationContainer.h"
#include "PickupNotification.h" #include "Editor/WidgetCompilerLog.h" #define LOCTEXT_NAMESPACE "UPickupNotificationContainer" UPickupNotificationContainer::UPickupNotificationContainer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , PickupNotificationClass(nullptr) , PickupNotificationZOrder(0) , bAllowAppendingToExistingNotification(true) { } bool UPickupNotificationContainer::Update_Implementation(FGameplayTag PickupType, float DeltaMagnitude) { if (bAllowAppendingToExistingNotification) { if (UPickupNotification** Notification = NotificationsMap.Find(PickupType)) { (*Notification)->AddMagnitude(DeltaMagnitude); return true; } } UPickupNotification*& Notification = Notifications.GetData()[0]; if (Notification) { const int32 Num = Notifications.Num(); for (int32 Index = 1; Index < Num; ++Index) { Notification->MoveUpwards(); NotificationsIndex[Notification] = Index; Notifications.SwapMemory(0, Index); if (!Notification) { goto create; } } Notification->MoveUpwards(); NotificationsIndex[Notification] = Num; Notifications.Emplace(CopyTemp(Notification)); } create: UPickupNotification*& NewNotification = Notifications.GetData()[0]; NewNotification = CreateWidget<UPickupNotification>(this, PickupNotificationClass); NewNotification->OnNotificationFadingOut.AddDynamic(this, &UPickupNotificationContainer::OnPickupNotificationFadingOut); NotificationsIndex.Add(NewNotification, 0); NotificationsMap.Add(PickupType, NewNotification); NewNotification->PreViewportInit(PickupType, DeltaMagnitude); NewNotification->AddToViewport(PickupNotificationZOrder); return true; } void UPickupNotificationContainer::OnPickupNotificationFadingOut(UPickupNotification* PickupNotification) { NotificationsMap.Remove(PickupNotification->GetPickupType()); PickupNotification->OnNotificationFadingOut.RemoveAll(this); UPickupNotification** Data = Notifications.GetData(); const int32 RemovedIndex = NotificationsIndex.FindAndRemoveChecked(PickupNotification); const int32 TopIndex = Notifications.Num() - 1; Data[RemovedIndex] = nullptr; if (TopIndex && !Data[TopIndex]) { Notifications.RemoveAt(TopIndex, 1, true); } } #undef LOCTEXT_NAMESPACE

Implementing it - Blueprints

Pickup Notification

Create a new blueprint class derived from PickupNotification.

Add two animations named FadeInAnim and FadeOutAnim.

Let's decorate the content hierarchy and keyframe the animations before we dive into the nodes:

Pickup Notification BP - Designer

Now, let's dive into the Graph section of our Pickup Notification blueprint subclass:

Pickup Notification BP - Graph

Pickup Notification Container

Create a new blueprint class derived from PickupNotificationContainer.

You can now decorate any User Widget content hierarchy with your PickupNotificationContainer:

Using Pickup Notification Container BP

Restart the editor if you can't see your pickup notification blueprint class in the drop down menu for PickupNotificationClass