First step of new focus management system
This commit is contained in:
Binary file not shown.
Binary file not shown.
112
Source/Integration/InputEvents.cpp
Normal file
112
Source/Integration/InputEvents.cpp
Normal file
@@ -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<FlxEventRequest> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
Source/Integration/InputEvents.h
Normal file
106
Source/Integration/InputEvents.h
Normal file
@@ -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<FKey> &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<FKey> 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<FlxEventRequest> Requests;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
bool Dirty = true;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class InputMode { UIOnly, GameOnly };
|
||||||
|
|
||||||
|
using View = TArrayView<FlxEventRequest>;
|
||||||
|
|
||||||
|
// Get the requests array.
|
||||||
|
const TArray<FlxEventRequest> &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;
|
||||||
|
};
|
||||||
@@ -169,7 +169,11 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
|
|||||||
UpdateTangibles();
|
UpdateTangibles();
|
||||||
UpdatePossessedTangible();
|
UpdatePossessedTangible();
|
||||||
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(GetWorld()->GetFirstPlayerController());
|
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(GetWorld()->GetFirstPlayerController());
|
||||||
if (PC != nullptr) PC->UpdateLookAt();
|
if (PC != nullptr)
|
||||||
|
{
|
||||||
|
PC->UpdateLookAt();
|
||||||
|
PC->UpdateEventDispatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
#include "TangibleManager.h"
|
#include "TangibleManager.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "Engine/GameInstance.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<SObjectWidget*>(W)->GetWidgetObject();
|
||||||
|
if (UW) return UW->GetClass()->GetName();
|
||||||
|
}
|
||||||
|
W = W->GetParentWidget().Get();
|
||||||
|
}
|
||||||
|
return TEXT("Unknown Widget");
|
||||||
|
}
|
||||||
|
|
||||||
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
|
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
|
||||||
{
|
{
|
||||||
@@ -53,6 +71,96 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
|
|||||||
return ScreenPosition;
|
return ScreenPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AlxPlayerControllerBase::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
CharacterModeInput = NewObject<UInputComponent>(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<SViewport> ViewportWidget = GVC->GetGameViewportWidget();
|
||||||
|
if (ViewportWidget.IsValid())
|
||||||
|
{
|
||||||
|
TSharedPtr<SWidget> 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<FlxEventRequest> &Requests = EventRequests.GetRequests();
|
||||||
|
|
||||||
|
if (CurrentInputMode == InputMode::UIOnly)
|
||||||
|
{
|
||||||
|
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetInputMode(FInputModeGameOnly());
|
||||||
|
|
||||||
|
CharacterModeInput->KeyBindings.Empty();
|
||||||
|
TSet<FKey> 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()
|
void AlxPlayerControllerBase::UpdateLookAt()
|
||||||
{
|
{
|
||||||
UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>();
|
UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Engine/HitResult.h"
|
#include "Engine/HitResult.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "InputEvents.h"
|
||||||
#include "PlayerControllerBase.generated.h"
|
#include "PlayerControllerBase.generated.h"
|
||||||
|
|
||||||
UCLASS(BlueprintType, Blueprintable)
|
UCLASS(BlueprintType, Blueprintable)
|
||||||
@@ -11,6 +12,8 @@ class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
using InputMode = FlxEventRequests::InputMode;
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
||||||
static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
|
static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
|
||||||
|
|
||||||
@@ -26,6 +29,12 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
||||||
static void SetLookAtChanged(const UObject *Context);
|
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
|
// Blueprint events
|
||||||
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
||||||
void CalculateLookAt();
|
void CalculateLookAt();
|
||||||
@@ -33,14 +42,35 @@ public:
|
|||||||
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
||||||
void LookAtChanged();
|
void LookAtChanged();
|
||||||
|
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
// Called by GameMode each tick.
|
// Called by GameMode each tick.
|
||||||
void UpdateLookAt();
|
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.
|
// Get the player controller, cast to AlxPlayerControllerBase.
|
||||||
static AlxPlayerControllerBase *FromContext(const UObject *Context);
|
static AlxPlayerControllerBase *FromContext(const UObject *Context);
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FHitResult CurrentLookAt;
|
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;
|
bool MustCallLookAtChanged = false;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user