From 8a3d2002473f542ac957d44e9455ad706dd80504 Mon Sep 17 00:00:00 2001 From: jyelon Date: Wed, 15 Apr 2026 22:55:02 -0400 Subject: [PATCH] First step of new focus management system --- Content/Luprex/lxPlayerController.uasset | 4 +- Content/Widgets/WB_Hotkeys.uasset | 4 +- Source/Integration/InputEvents.cpp | 112 ++++++++++++++++++++ Source/Integration/InputEvents.h | 106 ++++++++++++++++++ Source/Integration/LuprexGameModeBase.cpp | 6 +- Source/Integration/PlayerControllerBase.cpp | 108 +++++++++++++++++++ Source/Integration/PlayerControllerBase.h | 30 ++++++ 7 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 Source/Integration/InputEvents.cpp create mode 100644 Source/Integration/InputEvents.h diff --git a/Content/Luprex/lxPlayerController.uasset b/Content/Luprex/lxPlayerController.uasset index 71454a5e..abc06ca4 100644 --- a/Content/Luprex/lxPlayerController.uasset +++ b/Content/Luprex/lxPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d83ee5c9814613ba4dfa2ecb67825597b0c511ec875e240f2b6fe1ccd255b704 -size 149833 +oid sha256:3d61a99885bd6a4572505819651c69da3f51cbe2f3c92bc679f37de7d81df8c5 +size 144474 diff --git a/Content/Widgets/WB_Hotkeys.uasset b/Content/Widgets/WB_Hotkeys.uasset index 5cfed7a1..0b0eefad 100644 --- a/Content/Widgets/WB_Hotkeys.uasset +++ b/Content/Widgets/WB_Hotkeys.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f71256d141fd446321d1a5d492b9e36f919faa803d7fed6b56900f907374c80 -size 318118 +oid sha256:afa2f3a5d654905c17c717355e398385856360c5bf75a7904ecd7c54a49db18d +size 311655 diff --git a/Source/Integration/InputEvents.cpp b/Source/Integration/InputEvents.cpp new file mode 100644 index 00000000..b7fb9085 --- /dev/null +++ b/Source/Integration/InputEvents.cpp @@ -0,0 +1,112 @@ +//////////////////////////////////////////////////////////// +// +// InputEvents.cpp +// +//////////////////////////////////////////////////////////// + +#include "InputEvents.h" +#include "Common.h" + +bool FlxEventRequest::operator==(const FlxEventRequest &Other) const +{ + return (Widget == Other.Widget) && + (TakeControl == Other.TakeControl) && + (ShowPointer == Other.ShowPointer) && + (Hotkeys == Other.Hotkeys); +} + +bool FlxEventRequests::SanityCheck(const FlxEventRequest &Request) +{ + if (Request.Widget == nullptr) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget.")); + return false; + } + if (Request.ShowPointer && !Request.TakeControl) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: ShowPointer requires TakeControl.")); + return false; + } + if (Request.TakeControl && !Request.Hotkeys.IsEmpty()) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: Widget asked for all events, and also, specific events")); + return false; + } + return true; +} + +void FlxEventRequests::SplitHighLow(View &High, View &Low) +{ + int32 NumHigh = 0; + while ((NumHigh < Requests.Num()) && (Requests[NumHigh].TakeControl)) NumHigh++; + int32 NumLow = Requests.Num() - NumHigh; + High = View(Requests.GetData(), NumHigh); + Low = View(Requests.GetData() + NumHigh, NumLow); +} + +void FlxEventRequests::Request(const FlxEventRequest &NewRequest) +{ + // Divide the array into a high-priority slice and a low-priority slice. + View High, Low; + SplitHighLow(High, Low); + + // This is a simple test to see if anything is going to change. + // If not, we return early and avoid setting the dirty bit. + if (NewRequest.TakeControl) + { + if ((High.Num() > 0) && (High[0] == NewRequest)) return; + } + else + { + if ((Low.Num() > 0) && (Low[0] == NewRequest)) return; + } + + // We're going to build a new version of the requests array. + TArray Updated; + + // Add all high priority requests to the updated array, new request first. + if (NewRequest.TakeControl) Updated.Add(NewRequest); + for (const FlxEventRequest &Req : High) + if (Req.Widget != NewRequest.Widget) Updated.Add(Req); + + // Add all low priority requests to the updated array, new request first. + if (!NewRequest.TakeControl) Updated.Add(NewRequest); + for (const FlxEventRequest &Req : Low) + if (Req.Widget != NewRequest.Widget) Updated.Add(Req); + + Swap(Requests, Updated); + Dirty = true; +} + +void FlxEventRequests::Remove(UUserWidget *Widget) +{ + int32 N = Requests.Num(); + Requests.RemoveAll([Widget](const FlxEventRequest &Entry) + { + return Entry.Widget == Widget; + }); + if (Requests.Num() < N) Dirty = true; +} + +void FlxEventRequests::GarbageCollect() +{ + int32 N = Requests.Num(); + Requests.RemoveAll([](const FlxEventRequest &Entry) + { + UUserWidget *W = Entry.Widget; + return W == nullptr || !IsValid(W) || W->GetParent() == nullptr; + }); + if (Requests.Num() < N) Dirty = true; +} + +FlxEventRequests::InputMode FlxEventRequests::GetRequestedMode() const +{ + if ((Requests.Num() > 0) && (Requests[0].TakeControl)) + { + return InputMode::UIOnly; + } + else + { + return InputMode::GameOnly; + } +} diff --git a/Source/Integration/InputEvents.h b/Source/Integration/InputEvents.h new file mode 100644 index 00000000..f5d1c2d0 --- /dev/null +++ b/Source/Integration/InputEvents.h @@ -0,0 +1,106 @@ +//////////////////////////////////////////////////////////// +// +// InputEvents.h +// +// Custom input event dispatching system. Uses Unreal's +// built-in input modes (GameOnly / UIOnly) with an +// enhanced input component for character mode hotkeys. +// +//////////////////////////////////////////////////////////// + +#pragma once + +#include "CoreMinimal.h" +#include "InputCoreTypes.h" +#include "Blueprint/UserWidget.h" +#include "InputEvents.generated.h" + +//////////////////////////////////////////////////////////// +// +// FlxEventRequest +// +// A widget's declaration of interest in input events. +// +// Widget: The widget that wants to receive events. +// TakeControl: If true, activating this request puts +// the system into Widget Mode. +// ShowPointer: If true, the mouse pointer should be +// visible when this widget has control. +// Hotkeys: Keys that go to this widget when the +// player is in Character mode. +// +//////////////////////////////////////////////////////////// + +USTRUCT(BlueprintType) +struct FlxEventRequest +{ + GENERATED_BODY() + + FlxEventRequest() = default; + FlxEventRequest(UUserWidget *InWidget, bool InTakeControl, bool InShowPointer, const TArray &InHotkeys) + : Widget(InWidget), TakeControl(InTakeControl), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {} + + bool operator == (const FlxEventRequest &Other) const; + + UPROPERTY(BlueprintReadWrite) + UUserWidget* Widget = nullptr; + + UPROPERTY(BlueprintReadWrite) + bool TakeControl = false; + + UPROPERTY(BlueprintReadWrite) + bool ShowPointer = false; + + UPROPERTY(BlueprintReadWrite) + TArray Hotkeys; +}; + +USTRUCT() +struct FlxEventRequests +{ + GENERATED_BODY() + +private: + UPROPERTY() + // High priority requests are always before low-priority. + // Otherwise, these are in order of most recent first. + TArray Requests; + + UPROPERTY() + bool Dirty = true; + +public: + enum class InputMode { UIOnly, GameOnly }; + + using View = TArrayView; + + // Get the requests array. + const TArray &GetRequests() const { return Requests; } + + // Sanity check a request to see if it is reasonable. + static bool SanityCheck(const FlxEventRequest &Request); + + // Divide Requests into a high-priority slice and a low-priority slice. + void SplitHighLow(View &High, View &Low); + + // Apply a request. Replaces any previous request by the same widget. + void Request(const FlxEventRequest &NewRequest); + + // Remove all requests by the specified widget. + void Remove(UUserWidget *Widget); + + // Remove any requests by dead widgets or widgets with no parents. + void GarbageCollect(); + + // Return true if the requests have changed since the last ClearDirty. + bool IsDirty() { return Dirty; } + + // Clear the dirty flag. + void ClearDirty() { Dirty = false; } + + // Set the dirty flag. + void SetDirty() { Dirty = true; } + + // Get the currently-requested mode. + InputMode GetRequestedMode() const; +}; diff --git a/Source/Integration/LuprexGameModeBase.cpp b/Source/Integration/LuprexGameModeBase.cpp index d1d70e17..f35bebfd 100644 --- a/Source/Integration/LuprexGameModeBase.cpp +++ b/Source/Integration/LuprexGameModeBase.cpp @@ -169,7 +169,11 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev UpdateTangibles(); UpdatePossessedTangible(); AlxPlayerControllerBase *PC = Cast(GetWorld()->GetFirstPlayerController()); - if (PC != nullptr) PC->UpdateLookAt(); + if (PC != nullptr) + { + PC->UpdateLookAt(); + PC->UpdateEventDispatch(); + } } } diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index 8ba0771a..210c4631 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -4,6 +4,24 @@ #include "TangibleManager.h" #include "Kismet/GameplayStatics.h" #include "Engine/GameInstance.h" +#include "Engine/GameViewportClient.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/SViewport.h" +#include "Slate/SObjectWidget.h" + +FString AlxPlayerControllerBase::GetUserWidgetName(SWidget *W) +{ + while (W) + { + if (W->GetType() == FName("SObjectWidget")) + { + UUserWidget *UW = static_cast(W)->GetWidgetObject(); + if (UW) return UW->GetClass()->GetName(); + } + W = W->GetParentWidget().Get(); + } + return TEXT("Unknown Widget"); +} AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context) { @@ -53,6 +71,96 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context) return ScreenPosition; } +void AlxPlayerControllerBase::BeginPlay() +{ + Super::BeginPlay(); + CharacterModeInput = NewObject(this); + CharacterModeInput->bBlockInput = false; + PushInputComponent(CharacterModeInput); +} + +void AlxPlayerControllerBase::UpdateEventDispatch() +{ + EventRequests.GarbageCollect(); + + // If we're in GameOnly mode, check that focus is still on the viewport. + if (CurrentInputMode == InputMode::GameOnly) + { + UGameViewportClient *GVC = GetWorld() ? GetWorld()->GetGameViewport() : nullptr; + if (GVC) + { + TSharedPtr ViewportWidget = GVC->GetGameViewportWidget(); + if (ViewportWidget.IsValid()) + { + TSharedPtr Focused = FSlateApplication::Get().GetKeyboardFocusedWidget(); + if (Focused.Get() != ViewportWidget.Get()) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, keyboard focus must stay on viewport, but was stolen by: %s. Restoring."), *GetUserWidgetName(Focused.Get())); + EventRequests.SetDirty(); + } + if (!ViewportWidget->HasMouseCapture()) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, viewport must have mouse capture, but lost it. Restoring.")); + EventRequests.SetDirty(); + } + } + } + } + + if (!EventRequests.IsDirty()) return; + EventRequests.ClearDirty(); + + CurrentInputMode = EventRequests.GetRequestedMode(); + const TArray &Requests = EventRequests.GetRequests(); + + if (CurrentInputMode == InputMode::UIOnly) + { + SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget())); + } + else + { + SetInputMode(FInputModeGameOnly()); + + CharacterModeInput->KeyBindings.Empty(); + TSet BoundKeys; + for (const FlxEventRequest &Req : Requests) + { + for (const FKey &Key : Req.Hotkeys) + { + if (!BoundKeys.Contains(Key)) + { + BoundKeys.Add(Key); + CharacterModeInput->BindKey(Key, IE_Pressed, this, &AlxPlayerControllerBase::ForwardKeyEvent); + } + } + } + } +} + +void AlxPlayerControllerBase::ForwardKeyEvent(FKey Key) +{ + // TODO: implement +} + + +void AlxPlayerControllerBase::RequestEvents(const FlxEventRequest &Request) +{ + if (!FlxEventRequests::SanityCheck(Request)) return; + AlxPlayerControllerBase *PC = FromContext(Request.Widget); + PC->EventRequests.Request(Request); +} + +void AlxPlayerControllerBase::UnRequestEvents(UUserWidget *Widget) +{ + if (Widget == nullptr) + { + UE_LOG(LogLuprexIntegration, Error, TEXT("UnRequestEvents called with null widget.")); + return; + } + AlxPlayerControllerBase *PC = FromContext(Widget); + PC->EventRequests.Remove(Widget); +} + void AlxPlayerControllerBase::UpdateLookAt() { UlxTangibleManager *TM = GetGameInstance()->GetSubsystem(); diff --git a/Source/Integration/PlayerControllerBase.h b/Source/Integration/PlayerControllerBase.h index 8e70f54f..afacfa18 100644 --- a/Source/Integration/PlayerControllerBase.h +++ b/Source/Integration/PlayerControllerBase.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "Engine/HitResult.h" #include "GameFramework/PlayerController.h" +#include "InputEvents.h" #include "PlayerControllerBase.generated.h" UCLASS(BlueprintType, Blueprintable) @@ -11,6 +12,8 @@ class INTEGRATION_API AlxPlayerControllerBase : public APlayerController GENERATED_BODY() public: + using InputMode = FlxEventRequests::InputMode; + UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection") static void SetLookAt(const UObject *Context, const FHitResult &HitResult); @@ -26,6 +29,12 @@ public: UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection") static void SetLookAtChanged(const UObject *Context); + UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events") + static void RequestEvents(const FlxEventRequest &Request); + + UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events") + static void UnRequestEvents(UUserWidget *Widget); + // Blueprint events UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection") void CalculateLookAt(); @@ -33,14 +42,35 @@ public: UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection") void LookAtChanged(); + virtual void BeginPlay() override; + // Called by GameMode each tick. void UpdateLookAt(); + // Rebuild input component and switch input mode. + void UpdateEventDispatch(); + + // Handler for character mode hotkey presses. + void ForwardKeyEvent(FKey Key); + + // Walk up from a Slate widget to find the nearest UMG widget class name. + static FString GetUserWidgetName(SWidget *W); + // Get the player controller, cast to AlxPlayerControllerBase. static AlxPlayerControllerBase *FromContext(const UObject *Context); UPROPERTY() FHitResult CurrentLookAt; + UPROPERTY() + FlxEventRequests EventRequests; + + // Input component for Character Mode: catches hotkeys only. + UPROPERTY() + UInputComponent *CharacterModeInput = nullptr; + + // Current input mode. + InputMode CurrentInputMode = InputMode::GameOnly; + bool MustCallLookAtChanged = false; };