Last active
November 7, 2024 05:03
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.:
This is also why it's using
UStateTreeTaskBlueprintBase
rather thanFStateTreeTaskBase
, unreal doesn't likeInstanced
UObject
s in aUSTRUCT
, they want to be on aUObject
andUStateTreeTaskBlueprintBase
fits the bill.