Skip to content

Instantly share code, notes, and snippets.

@timoxley
Last active November 7, 2024 05:03
Show Gist options
  • Save timoxley/6e48c36c329c07edd6a8a611e54a6f78 to your computer and use it in GitHub Desktop.
Save timoxley/6e48c36c329c07edd6a8a611e54a6f78 to your computer and use it in GitHub Desktop.
Create Widget Task - Unreal Engine State Tree Task with static SendStateTreeEvent function for sending events from widgets
// inspired by https://unrealist.org/dev-log-03-statetree-isnt-just-for-ai/
#include "CreateWidgetTask.h"
#include "StateTreeExecutionContext.h"
#include "Blueprint/UserWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(CreateWidgetTask)
TMap<UUserWidget*, FOnStateTreeEvent> UStateTreeWidgetTask::WidgetEventDelegateMap;
bool UStateTreeWidgetTask::SendStateTreeEvent(UUserWidget* Widget, const FStateTreeEvent& StateTreeEvent)
{
if (!Widget)
{
UE_LOG(LogTemp, Error, TEXT("Widget is nullptr %s"), *GetNameSafe(Widget));
return false;
}
if (auto const Found = WidgetEventDelegateMap.Find(Widget))
{
auto Delegate = *Found;
if (!IsValid(Widget))
{
// shouldn't happen, but let's just clean up the delegate if the widget is invalid for good measure
Delegate.Unbind();
WidgetEventDelegateMap.Remove(Widget);
UE_LOG(LogTemp, Error, TEXT("Widget is invalid %s, cleaning up delegate."), *GetNameSafe(Widget));
return false;
}
return Delegate.ExecuteIfBound(StateTreeEvent);
}
UE_LOG(LogTemp, Error, TEXT("Failed to find Delegate for Widget %s"), *GetNameSafe(Widget));
return false;
}
void UStateTreeWidgetTask::ListenForStateTreeEvent(UStateTreeNodeBlueprintBase* Node, UUserWidget* Widget)
{
if (!IsValid(Widget)) { return; }
FOnStateTreeEvent Delegate;
Delegate.BindWeakLambda(
Node,
[Node](FStateTreeEvent const& StateTreeEvent)
{
Node->SendEvent(StateTreeEvent);
}
);
WidgetEventDelegateMap.Add(Widget, Delegate);
WidgetEventDelegateMap = WidgetEventDelegateMap.FilterByPredicate(
[](auto KV)
{
const auto Valid = IsValid(KV.Key);
if (!Valid)
{
UE_LOG(LogTemp, Warning, TEXT("WidgetEventDelegateMap contains invalid Widget %s"), *GetNameSafe(KV.Key));
}
return Valid;
}
);
}
EStateTreeRunStatus UStateTreeWidgetTask::EnterState(FStateTreeExecutionContext& Context, FStateTreeTransitionResult const& Transition)
{
// Reset status to running since the same task may be restarted.
RunStatus = EStateTreeRunStatus::Running;
ON_SCOPE_EXIT
{
if (bHasLatentEnterState)
{
// Note: the name contains latent just to differentiate it from the deprecated version (the old version did not allow latent actions to be started).
ReceiveLatentEnterState(Transition);
}
};
if (!bEnabled)
{
RunStatus = EStateTreeRunStatus::Succeeded;
return RunStatus;
}
if (!IsValid(TargetPlayerPawn))
{
UE_LOG(LogTemp, Error, TEXT("TargetPlayerPawn is invalid %s"), *GetNameSafe(TargetPlayerPawn));
RunStatus = EStateTreeRunStatus::Failed;
return RunStatus;
}
if (!IsValid(WidgetTemplate))
{
UE_LOG(LogTemp, Error, TEXT("WidgetTemplate is invalid %s"), *GetNameSafe(WidgetTemplate));
RunStatus = EStateTreeRunStatus::Failed;
return RunStatus;
}
if (Widget)
{
// this shouldn't happen, but just in case
UE_LOG(LogTemp, Warning, TEXT("Widget is already created %s"), *GetNameSafe(Widget));
return RunStatus;
}
auto const Controller = Cast<APlayerController>(TargetPlayerPawn->GetController());
if (!Controller)
{
UE_LOG(LogTemp, Error, TEXT("Failed to get PlayerController from Pawn %s. Controller: %s"), *GetNameSafe(TargetPlayerPawn), *GetNameSafe(TargetPlayerPawn->GetController()));
RunStatus = EStateTreeRunStatus::Failed;
return RunStatus;
}
// Create the widget.
Widget = DuplicateObject<UUserWidget>(WidgetTemplate, Controller);
if (!Widget)
{
UE_LOG(LogTemp, Error, TEXT("Failed to duplicate WidgetTemplate %s"), *GetNameSafe(WidgetTemplate));
RunStatus = EStateTreeRunStatus::Failed;
return RunStatus;
}
Widget->SetFlags(RF_Transactional);
Widget->SetOwningPlayer(Controller);
// Listen for state tree events.
ListenForStateTreeEvent(this, Widget);
Widget->Initialize();
if (bAddToViewport)
{
Widget->AddToViewport(ZIndex);
}
return Super::EnterState(Context, Transition);
}
void UStateTreeWidgetTask::ExitState(FStateTreeExecutionContext& Context, FStateTreeTransitionResult const& Transition)
{
if (Widget)
{
if (WidgetEventDelegateMap.Contains(Widget))
{
auto Delegate = WidgetEventDelegateMap.FindRef(Widget);
Delegate.Unbind();
}
}
if (IsValid(Widget))
{
Widget->RemoveFromParent();
}
Widget = nullptr;
Super::ExitState(Context, Transition);
}
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/StateTreeTaskBlueprintBase.h"
#include "CreateWidgetTask.generated.h"
DECLARE_DELEGATE_OneParam(FOnStateTreeEvent, const FStateTreeEvent&)
UCLASS(BlueprintType, Blueprintable, DisplayName = "Create Instanced Widget Task")
class UStateTreeWidgetTask : public UStateTreeTaskBlueprintBase
{
GENERATED_BODY()
public:
// mapping between widget and sendevent delegates
// to support static function SendStateTreeEvent
static TMap<UUserWidget*, FOnStateTreeEvent> WidgetEventDelegateMap;
UFUNCTION(BlueprintCallable, Category = "StateTree", meta=(DefaultToSelf="Widget"))
static bool SendStateTreeEvent(UUserWidget* Widget, const FStateTreeEvent& StateTreeEvent);
static void ListenForStateTreeEvent(UStateTreeNodeBlueprintBase* Node, UUserWidget* Widget);
/** Actor where to draw the widget. */
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<APawn> TargetPlayerPawn;
UPROPERTY(EditAnywhere, Instanced)
TObjectPtr<UUserWidget> WidgetTemplate;
UPROPERTY(Transient)
TObjectPtr<UUserWidget> Widget;
UPROPERTY(EditAnywhere, Category = "Parameter")
bool bEnabled = true;
UPROPERTY(EditAnywhere, Category = "Parameter")
bool bAddToViewport = true;
UPROPERTY(EditAnywhere, Category = "Parameter", meta=(EditCondition="bAddToViewport", EditConditionHides))
int32 ZIndex = 0;
protected:
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, FStateTreeTransitionResult const& Transition) override;
virtual void ExitState(FStateTreeExecutionContext& Context, FStateTreeTransitionResult const& Transition) override;
};
@timoxley
Copy link
Author

timoxley commented Nov 7, 2024

before you shout about having the instanced widget rather than a subclass:

This allows really convenient injection of values directly into widget MVVM model instances from inside the state tree, both inline and as state tree bindings e.g.:

image

This is also why it's using UStateTreeTaskBlueprintBase rather than FStateTreeTaskBase, unreal doesn't like Instanced UObjects in a USTRUCT, they want to be on a UObject and UStateTreeTaskBlueprintBase fits the bill.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment