Add new plugins, refactor exp, introduce stat forge to replace GAS
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesAssetHelpers.h"
|
||||
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Engine/ObjectLibrary.h"
|
||||
#include "Blueprint/BlueprintSupport.h"
|
||||
|
||||
int UStevesAssetHelpers::FindBlueprintSoftPaths(const TArray<FDirectoryPath>& InPaths,
|
||||
UObjectLibrary* ObjectLibrary,
|
||||
TArray<FSoftObjectPath>& OutSoftPaths)
|
||||
{
|
||||
// LoadBlueprintAssetDataFromPaths requires FString array, LoadBlueprintAssetDataFromPath just makes one array per call so no better
|
||||
// I like using FDirectoryPath for settings though since it enables browsing
|
||||
TArray<FString> StrPaths;
|
||||
for (auto& Dir : InPaths)
|
||||
{
|
||||
StrPaths.Add(Dir.Path);
|
||||
}
|
||||
return FindBlueprintSoftPaths(StrPaths, ObjectLibrary, OutSoftPaths);
|
||||
}
|
||||
|
||||
int UStevesAssetHelpers::FindBlueprintSoftPaths(const TArray<FString>& InPaths,
|
||||
UObjectLibrary* ObjectLibrary,
|
||||
TArray<FSoftObjectPath>& OutSoftPaths)
|
||||
{
|
||||
ObjectLibrary->LoadBlueprintAssetDataFromPaths(InPaths);
|
||||
ObjectLibrary->LoadAssetsFromAssetData();
|
||||
// Now they're all loaded, add them
|
||||
TArray<FAssetData> FoundAssets;
|
||||
ObjectLibrary->GetAssetDataList(FoundAssets);
|
||||
int Count = 0;
|
||||
for (auto& Asset : FoundAssets)
|
||||
{
|
||||
// Need to resolve BP generated class
|
||||
const FString GeneratedClassTag = Asset.GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
||||
if (GeneratedClassTag.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogStevesUEHelpers, Warning, TEXT("Unable to find GeneratedClass value for asset %s"), *Asset.GetObjectPathString());
|
||||
continue;
|
||||
}
|
||||
FSoftObjectPath StringRef;
|
||||
StringRef.SetPath(FPackageName::ExportTextPathToObjectPath(GeneratedClassTag));
|
||||
OutSoftPaths.Add(StringRef);
|
||||
++Count;
|
||||
}
|
||||
|
||||
// Don't use OutSoftPaths.Num() in case it wasn't empty to start with
|
||||
return Count;
|
||||
}
|
||||
|
||||
int UStevesAssetHelpers::FindBlueprintClasses(const TArray<FDirectoryPath>& InPaths,
|
||||
UObjectLibrary* ObjectLibrary,
|
||||
TArray<UClass*>& OutClasses)
|
||||
{
|
||||
// LoadBlueprintAssetDataFromPaths requires FString array, LoadBlueprintAssetDataFromPath just makes one array per call so no better
|
||||
// I like using FDirectoryPath for settings though since it enables browsing
|
||||
TArray<FString> StrPaths;
|
||||
for (auto& Dir : InPaths)
|
||||
{
|
||||
StrPaths.Add(Dir.Path);
|
||||
}
|
||||
return FindBlueprintClasses(StrPaths, ObjectLibrary, OutClasses);
|
||||
}
|
||||
|
||||
int UStevesAssetHelpers::FindBlueprintClasses(const TArray<FString>& InPaths,
|
||||
UObjectLibrary* ObjectLibrary,
|
||||
TArray<UClass*>& OutClasses)
|
||||
{
|
||||
TArray<FSoftObjectPath> SoftPaths;
|
||||
FindBlueprintSoftPaths(InPaths, ObjectLibrary, SoftPaths);
|
||||
int Count = 0;
|
||||
for (auto& SoftRef : SoftPaths)
|
||||
{
|
||||
if (UClass* TheClass = Cast<UClass>(SoftRef.ResolveObject()))
|
||||
{
|
||||
OutClasses.Add(TheClass);
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
|
||||
#include "StevesBPL.h"
|
||||
#include "Misc/ConfigCacheIni.h"
|
||||
#include "StevesUiHelpers.h"
|
||||
#include "StevesUI/StevesUI.h"
|
||||
#include "ContentStreaming.h"
|
||||
|
||||
void UStevesBPL::SetWidgetFocus(UWidget* Widget)
|
||||
{
|
||||
SetWidgetFocusProperly(Widget);
|
||||
}
|
||||
|
||||
UPanelSlot* UStevesBPL::InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex)
|
||||
{
|
||||
return StevesUiHelpers::InsertChildWidgetAt(Parent, Child, AtIndex);
|
||||
}
|
||||
|
||||
FStevesBalancedRandomStream UStevesBPL::MakeBalancedRandomStream(int64 Seed)
|
||||
{
|
||||
return FStevesBalancedRandomStream(Seed);
|
||||
}
|
||||
|
||||
void UStevesBPL::AddViewOriginToStreaming(const FVector& ViewOrigin,
|
||||
float ScreenWidth,
|
||||
float FOV,
|
||||
float BoostFactor,
|
||||
bool bOverrideLocation,
|
||||
float Duration,
|
||||
AActor* ActorToBoost)
|
||||
{
|
||||
IStreamingManager::Get().AddViewInformation(ViewOrigin,
|
||||
ScreenWidth,
|
||||
ScreenWidth / FMath::Tan(FMath::DegreesToRadians(FOV * 0.5f)),
|
||||
BoostFactor,
|
||||
bOverrideLocation,
|
||||
Duration,
|
||||
ActorToBoost);
|
||||
}
|
||||
|
||||
void UStevesBPL::UpdateStreaming(float DeltaTime, bool bBlockUntilDone)
|
||||
{
|
||||
FStreamingManagerCollection& SM = IStreamingManager::Get();
|
||||
SM.UpdateResourceStreaming(DeltaTime, true);
|
||||
if (bBlockUntilDone)
|
||||
{
|
||||
SM.BlockTillAllRequestsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
float UStevesBPL::GetPerceivedLuminance(const FLinearColor& InColour)
|
||||
{
|
||||
// ITU BT.709
|
||||
return
|
||||
InColour.R * 0.2126f +
|
||||
InColour.G * 0.7152f +
|
||||
InColour.B * 0.0722f;
|
||||
}
|
||||
|
||||
float UStevesBPL::GetPerceivedLuminance2(const FLinearColor& InColour)
|
||||
{
|
||||
// ITU BT.601
|
||||
return
|
||||
InColour.R * 0.299f +
|
||||
InColour.G * 0.587f +
|
||||
InColour.B * 0.114f;
|
||||
}
|
||||
|
||||
float UStevesBPL::HeadingAngle2D(const FVector2D& Dir)
|
||||
{
|
||||
const FVector2D NormDir = Dir.GetSafeNormal();
|
||||
float Angle = FMath::Acos(NormDir.X);
|
||||
|
||||
if(NormDir.Y < 0.0f)
|
||||
{
|
||||
Angle *= -1.0f;
|
||||
}
|
||||
|
||||
return Angle;
|
||||
}
|
||||
|
||||
float UStevesBPL::AngleBetween2D(const FVector2D& DirA, const FVector2D& DirB)
|
||||
{
|
||||
const float HeadingA = HeadingAngle2D(DirA);
|
||||
const float HeadingB = HeadingAngle2D(DirB);
|
||||
|
||||
// Get the shortest route
|
||||
return FMath::FindDeltaAngleRadians(HeadingA, HeadingB);
|
||||
}
|
||||
|
||||
FString UStevesBPL::GetProjectVersion()
|
||||
{
|
||||
FString AppVersion;
|
||||
GConfig->GetString(
|
||||
TEXT("/Script/EngineSettings.GeneralProjectSettings"),
|
||||
TEXT("ProjectVersion"),
|
||||
AppVersion,
|
||||
GGameIni
|
||||
);
|
||||
|
||||
return AppVersion;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesDebugRenderSceneProxy.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "SceneManagement.h"
|
||||
#include "DynamicMeshBuilder.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialRenderProxy.h"
|
||||
|
||||
// Added in UE 5.2
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
|
||||
#include "Materials/MaterialRenderProxy.h"
|
||||
#endif
|
||||
|
||||
|
||||
void FStevesDebugRenderSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views,
|
||||
const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
|
||||
{
|
||||
FDebugRenderSceneProxy::GetDynamicMeshElements(Views, ViewFamily, VisibilityMap, Collector);
|
||||
|
||||
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
|
||||
{
|
||||
if (VisibilityMap & (1 << ViewIndex))
|
||||
{
|
||||
const FSceneView* View = Views[ViewIndex];
|
||||
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
|
||||
|
||||
// Draw Circles
|
||||
for (const auto& C : Circles)
|
||||
{
|
||||
DrawCircle(PDI, C.Centre, C.X, C.Y, C.Color, C.Radius, C.NumSegments, SDPG_World, C.Thickness, 0, C.Thickness > 0);
|
||||
}
|
||||
|
||||
// Draw Arcs
|
||||
for (const auto& C : Arcs)
|
||||
{
|
||||
::DrawArc(PDI,
|
||||
C.Centre,
|
||||
C.X, C.Y,
|
||||
C.MinAngle, C.MaxAngle,
|
||||
C.Radius, C.NumSegments,
|
||||
C.Color, SDPG_Foreground);
|
||||
}
|
||||
// Draw Cylinders (properly! superclass ignores transforms)
|
||||
for (const auto& C : CylindersImproved)
|
||||
{
|
||||
::DrawWireCylinder(PDI,
|
||||
C.Centre,
|
||||
C.X,
|
||||
C.Y,
|
||||
C.Z,
|
||||
C.Color,
|
||||
C.Radius,
|
||||
C.HalfHeight,
|
||||
C.NumSegments,
|
||||
SDPG_Foreground);
|
||||
}
|
||||
for (const auto& C : CapsulesImproved)
|
||||
{
|
||||
::DrawWireCapsule(PDI,
|
||||
#if ENGINE_MAJOR_VERSION >= 5
|
||||
C.Base,
|
||||
#else
|
||||
C.Location,
|
||||
#endif
|
||||
C.X,
|
||||
C.Y,
|
||||
C.Z,
|
||||
C.Color,
|
||||
C.Radius,
|
||||
C.HalfHeight,
|
||||
16,
|
||||
SDPG_Foreground);
|
||||
}
|
||||
|
||||
|
||||
for (const auto& Mesh : MeshesImproved)
|
||||
{
|
||||
FDynamicMeshBuilder MeshBuilder(View->GetFeatureLevel());
|
||||
MeshBuilder.AddVertices(Mesh.Vertices);
|
||||
MeshBuilder.AddTriangles(Mesh.Indices);
|
||||
|
||||
// Parent caches these (only within this function) but let's assume that's not worth it. Will people really
|
||||
// have lots of meshes with a shared colour in this single context to make it worth it?
|
||||
const auto MatRenderProxy = new FColoredMaterialRenderProxy(GEngine->WireframeMaterial->GetRenderProxy(), Mesh.Color);
|
||||
FDynamicMeshBuilderSettings Settings;
|
||||
Settings.bWireframe = true;
|
||||
Settings.bUseSelectionOutline = false;
|
||||
Settings.bUseWireframeSelectionColoring = true;
|
||||
MeshBuilder.GetMesh(Mesh.LocalToWorld, MatRenderProxy, SDPG_World, Settings, nullptr, ViewIndex, Collector);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FPrimitiveViewRelevance FStevesDebugRenderSceneProxy::GetViewRelevance(const FSceneView* View) const
|
||||
{
|
||||
// More useful defaults than FDebugRenderSceneProxy
|
||||
FPrimitiveViewRelevance Result;
|
||||
Result.bDrawRelevance = IsShown(View);
|
||||
Result.bDynamicRelevance = true;
|
||||
Result.bShadowRelevance = false;
|
||||
Result.bEditorPrimitiveRelevance = UseEditorCompositing(View);
|
||||
return Result;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesDynamicNavMeshVolume.h"
|
||||
|
||||
#include "NavigationSystem.h"
|
||||
#include "Components/BrushComponent.h"
|
||||
#include "PhysicsEngine/BodySetup.h"
|
||||
|
||||
|
||||
AStevesDynamicNavMeshVolume::AStevesDynamicNavMeshVolume()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void AStevesDynamicNavMeshVolume::SetLocationAndDimensions(const FVector& Location, const FVector& NewDimensions)
|
||||
{
|
||||
SetActorLocation(Location);
|
||||
UpdateDimensions(NewDimensions);
|
||||
NotifyNavSystem();
|
||||
}
|
||||
|
||||
void AStevesDynamicNavMeshVolume::SetDimensions(const FVector& NewDimensions)
|
||||
{
|
||||
UpdateDimensions(NewDimensions);
|
||||
NotifyNavSystem();
|
||||
}
|
||||
|
||||
void AStevesDynamicNavMeshVolume::UpdateDimensions(const FVector& NewDimensions)
|
||||
{
|
||||
// Volumes are built using UCubeBuilder, but we can't use that class at runtime (Editor only)
|
||||
// It generates the 6 faces as polys, like old BSP stuff. No idea why for a cube, we don't need that here
|
||||
// Just box it baby
|
||||
|
||||
if (auto Body = GetBrushComponent()->GetBodySetup())
|
||||
{
|
||||
Body->AggGeom.ConvexElems.Empty();
|
||||
if (Body->AggGeom.BoxElems.Num() == 0)
|
||||
{
|
||||
Body->AggGeom.BoxElems.Emplace();
|
||||
}
|
||||
auto& Box = Body->AggGeom.BoxElems[0];
|
||||
Box.X = NewDimensions.X;
|
||||
Box.Y = NewDimensions.Y;
|
||||
Box.Z = NewDimensions.Z;
|
||||
// Bounds are in World Space, hence use actor location as origin
|
||||
GetBrushComponent()->Bounds = FBoxSphereBounds(GetActorLocation(), NewDimensions*0.5f, NewDimensions.GetMax()*0.5f);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AStevesDynamicNavMeshVolume::NotifyNavSystem()
|
||||
{
|
||||
if (UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld()))
|
||||
{
|
||||
NavSys->OnNavigationBoundsUpdated(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesEasings.h"
|
||||
|
||||
float UStevesEasings::EaseAlpha(float InAlpha, EStevesEaseFunction Func)
|
||||
{
|
||||
constexpr float BackC1 = 1.70158f;
|
||||
constexpr float BackC2 = BackC1 * 1.525f;
|
||||
constexpr float BackC3 = BackC1 + 1.f;
|
||||
constexpr float ElasticC4 = UE_TWO_PI / 3.f;
|
||||
constexpr float ElasticC5 = UE_TWO_PI / 4.5f;
|
||||
|
||||
switch(Func)
|
||||
{
|
||||
default:
|
||||
case EStevesEaseFunction::Linear:
|
||||
return InAlpha;
|
||||
case EStevesEaseFunction::EaseIn_Sine:
|
||||
return 1.f - FMath::Cos(InAlpha * UE_HALF_PI);
|
||||
case EStevesEaseFunction::EaseOut_Sine:
|
||||
return FMath::Sin(InAlpha * UE_HALF_PI);
|
||||
case EStevesEaseFunction::EaseInOut_Sine:
|
||||
return -(FMath::Cos(UE_PI * InAlpha) - 1.f) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Quad:
|
||||
return InAlpha*InAlpha;
|
||||
case EStevesEaseFunction::EaseOut_Quad:
|
||||
return 1.f - (1.f - InAlpha) * (1.f - InAlpha);
|
||||
case EStevesEaseFunction::EaseInOut_Quad:
|
||||
return InAlpha < 0.5f ? 2.f * InAlpha * InAlpha : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 2.f) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Cubic:
|
||||
return FMath::Pow(InAlpha, 3);
|
||||
case EStevesEaseFunction::EaseOut_Cubic:
|
||||
return 1.f - FMath::Pow(1.f - InAlpha, 3);
|
||||
case EStevesEaseFunction::EaseInOut_Cubic:
|
||||
return InAlpha < 0.5f ? 4.f * FMath::Pow(InAlpha, 3) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 3) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Quart:
|
||||
return FMath::Pow(InAlpha, 4);
|
||||
case EStevesEaseFunction::EaseOut_Quart:
|
||||
return 1 - FMath::Pow(1.f - InAlpha, 4.f);
|
||||
case EStevesEaseFunction::EaseInOut_Quart:
|
||||
return InAlpha < 0.5f ? 8.f * FMath::Pow(InAlpha, 4) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 4) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Quint:
|
||||
return FMath::Pow(InAlpha, 5);
|
||||
case EStevesEaseFunction::EaseOut_Quint:
|
||||
return 1 - FMath::Pow(1.f - InAlpha, 5);
|
||||
case EStevesEaseFunction::EaseInOut_Quint:
|
||||
return InAlpha < 0.5f ? 16.f * FMath::Pow(InAlpha, 5) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 5) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Expo:
|
||||
return InAlpha <= 0 ? 0 : FMath::Pow(2.f, 10.f * InAlpha - 10.f);
|
||||
case EStevesEaseFunction::EaseOut_Expo:
|
||||
return InAlpha >= 1.f ? 1.f : 1.f - FMath::Pow(2.f, -10.f * InAlpha);
|
||||
case EStevesEaseFunction::EaseInOut_Expo:
|
||||
if (InAlpha <= 0.f)
|
||||
return 0;
|
||||
if (InAlpha >= 1.f)
|
||||
return 1;
|
||||
return InAlpha < 0.5f
|
||||
? FMath::Pow(2.f, 20.f * InAlpha - 10.f) / 2.f
|
||||
: (2.f - FMath::Pow(2.f, -20.f * InAlpha + 10.f)) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Circ:
|
||||
return 1.f - FMath::Sqrt(1.f - FMath::Pow(InAlpha, 2));
|
||||
case EStevesEaseFunction::EaseOut_Circ:
|
||||
return FMath::Sqrt(1.f - FMath::Pow(InAlpha - 1.f, 2));
|
||||
case EStevesEaseFunction::EaseInOut_Circ:
|
||||
return InAlpha < 0.5f
|
||||
? (1.f - FMath::Sqrt(1.f - FMath::Pow(2.f * InAlpha, 2))) / 2.f
|
||||
: (FMath::Sqrt(1.f - FMath::Pow(-2.f * InAlpha + 2.f, 2)) + 1.f) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Back:
|
||||
return BackC3 * FMath::Pow(InAlpha, 3) - BackC1 * InAlpha * InAlpha;
|
||||
case EStevesEaseFunction::EaseOut_Back:
|
||||
return 1.f + BackC3 * FMath::Pow(InAlpha - 1.f, 3) + BackC1 * FMath::Pow(InAlpha - 1.f, 2.f);
|
||||
case EStevesEaseFunction::EaseInOut_Back:
|
||||
return InAlpha < 0.5f
|
||||
? (FMath::Pow(2.f * InAlpha, 2) * ((BackC2 + 1.f) * 2.f * InAlpha - BackC2)) / 2.f
|
||||
: (FMath::Pow(2.f * InAlpha - 2.f, 2) * ((BackC2 + 1) * (InAlpha * 2.f - 2.f) + BackC2) + 2.f) / 2.f;
|
||||
case EStevesEaseFunction::EaseIn_Elastic:
|
||||
if (InAlpha <= 0.f)
|
||||
return 0;
|
||||
|
||||
if (InAlpha >= 1.f)
|
||||
return 1;
|
||||
return -FMath::Pow(2.f, 10.f * InAlpha - 10.f) * FMath::Sin((InAlpha * 10.f - 10.75f) * ElasticC4);
|
||||
case EStevesEaseFunction::EaseOut_Elastic:
|
||||
if (InAlpha <= 0.f)
|
||||
return 0;
|
||||
if (InAlpha >= 1.f)
|
||||
return 1;
|
||||
return FMath::Pow(2.f, -10.f * InAlpha) * FMath::Sin((InAlpha * 10.f - 0.75f) * ElasticC4) + 1.f;
|
||||
case EStevesEaseFunction::EaseInOut_Elastic:
|
||||
if (InAlpha <= 0.f)
|
||||
return 0;
|
||||
if (InAlpha >= 1.f)
|
||||
return 1;
|
||||
return InAlpha < 0.5f
|
||||
? -(FMath::Pow(2.f, 20.f * InAlpha - 10.f) * FMath::Sin((20.f * InAlpha - 11.125f) * ElasticC5)) /
|
||||
2.f
|
||||
: (FMath::Pow(2.f, -20.f * InAlpha + 10.f) * FMath::Sin((20.f * InAlpha - 11.125f) * ElasticC5)) /
|
||||
2.f + 1.f;
|
||||
case EStevesEaseFunction::EaseIn_Bounce:
|
||||
return 1 - EaseAlpha(1 - InAlpha, EStevesEaseFunction::EaseOut_Bounce);
|
||||
case EStevesEaseFunction::EaseOut_Bounce:
|
||||
{
|
||||
constexpr float n1 = 7.5625f;
|
||||
constexpr float d1 = 2.75f;
|
||||
|
||||
if (InAlpha < 1.f / d1) { return n1 * InAlpha * InAlpha; }
|
||||
else if (InAlpha < 2.f / d1)
|
||||
{
|
||||
const float newAlpha = InAlpha - 1.5f/d1;
|
||||
return n1 * newAlpha * newAlpha + 0.75f;
|
||||
}
|
||||
else if (InAlpha < 2.5f / d1)
|
||||
{
|
||||
const float newAlpha = InAlpha - 2.25f/d1;
|
||||
return n1 * newAlpha * newAlpha + 0.9375f;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float newAlpha = InAlpha - 2.625f/d1;
|
||||
return n1 * newAlpha * newAlpha + 0.984375f;
|
||||
}
|
||||
}
|
||||
case EStevesEaseFunction::EaseInOut_Bounce:
|
||||
return InAlpha < 0.5f
|
||||
? (1.f - EaseAlpha(1.f - 2.f * InAlpha, EStevesEaseFunction::EaseOut_Bounce)) / 2.f
|
||||
: (1.f + EaseAlpha(2.f * InAlpha - 1.f, EStevesEaseFunction::EaseOut_Bounce)) / 2.f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
|
||||
#include "StevesEditorVisComponent.h"
|
||||
#include "StaticMeshResources.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "StevesDebugRenderSceneProxy.h"
|
||||
#include "DynamicMeshBuilder.h"
|
||||
|
||||
UStevesEditorVisComponent::UStevesEditorVisComponent(const FObjectInitializer& ObjectInitializer)
|
||||
: UPrimitiveComponent(ObjectInitializer)
|
||||
{
|
||||
// set up some constants
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetCastShadow(false);
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// Note: this makes this component invisible on level instances, not sure why
|
||||
SetIsVisualizationComponent(true);
|
||||
#endif
|
||||
SetHiddenInGame(true);
|
||||
bVisibleInReflectionCaptures = false;
|
||||
bVisibleInRayTracing = false;
|
||||
bVisibleInRealTimeSkyCaptures = false;
|
||||
AlwaysLoadOnClient = false;
|
||||
bIsEditorOnly = true;
|
||||
|
||||
}
|
||||
|
||||
FPrimitiveSceneProxy* UStevesEditorVisComponent::CreateSceneProxy()
|
||||
{
|
||||
auto Ret = new FStevesDebugRenderSceneProxy(this);
|
||||
|
||||
const FTransform& XForm = GetComponentTransform();
|
||||
for (auto& L : Lines)
|
||||
{
|
||||
Ret->Lines.Add(FDebugRenderSceneProxy::FDebugLine(XForm.TransformPosition(L.Start),
|
||||
XForm.TransformPosition(L.End), L.Colour));
|
||||
}
|
||||
for (auto& A : Arrows)
|
||||
{
|
||||
Ret->ArrowLines.Add(FDebugRenderSceneProxy::FArrowLine(XForm.TransformPosition(A.Start),
|
||||
XForm.TransformPosition(A.End), A.Colour));
|
||||
}
|
||||
for (auto& C : Circles)
|
||||
{
|
||||
FQuat WorldRot = XForm.TransformRotation(C.Rotation.Quaternion());
|
||||
Ret->Circles.Add(FStevesDebugRenderSceneProxy::FDebugCircle(
|
||||
XForm.TransformPosition(C.Location),
|
||||
WorldRot.GetForwardVector(), WorldRot.GetRightVector(),
|
||||
XForm.GetMaximumAxisScale() * C.Radius,
|
||||
C.NumSegments, C.Colour
|
||||
));
|
||||
}
|
||||
for (auto& Arc : Arcs)
|
||||
{
|
||||
FQuat WorldRot = XForm.TransformRotation(Arc.Rotation.Quaternion());
|
||||
Ret->Arcs.Add(FStevesDebugRenderSceneProxy::FDebugArc(
|
||||
XForm.TransformPosition(Arc.Location),
|
||||
WorldRot.GetForwardVector(), WorldRot.GetRightVector(),
|
||||
Arc.MinAngle, Arc.MaxAngle,
|
||||
XForm.GetMaximumAxisScale() * Arc.Radius,
|
||||
Arc.NumSegments, Arc.Colour
|
||||
));
|
||||
}
|
||||
for (auto& S : Spheres)
|
||||
{
|
||||
Ret->Spheres.Add(FStevesDebugRenderSceneProxy::FSphere(
|
||||
XForm.GetMaximumAxisScale() * S.Radius,
|
||||
XForm.TransformPosition(S.Location),
|
||||
S.Colour
|
||||
));
|
||||
}
|
||||
for (auto& Box : Boxes)
|
||||
{
|
||||
FVector HalfSize = Box.Size * 0.5f;
|
||||
FBox DBox(-HalfSize, HalfSize);
|
||||
// Apply local rotation first then parent transform
|
||||
FTransform CombinedXForm = FTransform(Box.Rotation, Box.Location) * XForm;
|
||||
Ret->Boxes.Add(FStevesDebugRenderSceneProxy::FDebugBox(
|
||||
DBox, Box.Colour, CombinedXForm));
|
||||
}
|
||||
for (auto& Cylinder : Cylinders)
|
||||
{
|
||||
// Apply local rotation first then parent transform
|
||||
const FTransform CombinedXForm = FTransform(Cylinder.Rotation, Cylinder.Location) * XForm;
|
||||
const float HalfH = Cylinder.Height * 0.5f * CombinedXForm.GetScale3D().Z;
|
||||
const float R = Cylinder.Radius * CombinedXForm.GetScale3D().Z;
|
||||
const FVector Centre = CombinedXForm.TransformPosition(FVector::ZeroVector);
|
||||
const FVector LocalX = CombinedXForm.TransformVectorNoScale(FVector::ForwardVector);
|
||||
const FVector LocalY = CombinedXForm.TransformVectorNoScale(FVector::RightVector);
|
||||
const FVector LocalZ = CombinedXForm.TransformVectorNoScale(FVector::UpVector);
|
||||
Ret->CylindersImproved.Add(FStevesDebugRenderSceneProxy::FDebugCylinder(
|
||||
Centre, LocalX, LocalY, LocalZ, R, HalfH, 16, Cylinder.Colour));
|
||||
}
|
||||
for (auto& Capsule : Capsules)
|
||||
{
|
||||
// Apply local rotation first then parent transform
|
||||
const FTransform CombinedXForm = FTransform(Capsule.Rotation, Capsule.Location) * XForm;
|
||||
const float HalfH = Capsule.Height * 0.5f * CombinedXForm.GetScale3D().Z;
|
||||
const float R = Capsule.Radius * CombinedXForm.GetScale3D().Z;
|
||||
const FVector Position = CombinedXForm.TransformPosition(FVector::ZeroVector);
|
||||
const FVector LocalX = CombinedXForm.TransformVectorNoScale(FVector::ForwardVector);
|
||||
const FVector LocalY = CombinedXForm.TransformVectorNoScale(FVector::RightVector);
|
||||
const FVector LocalZ = CombinedXForm.TransformVectorNoScale(FVector::UpVector);
|
||||
Ret->CapsulesImproved.Add(FStevesDebugRenderSceneProxy::FCapsule(
|
||||
Position, R,
|
||||
LocalX, LocalY, LocalZ,
|
||||
HalfH, Capsule.Colour));
|
||||
}
|
||||
for (auto& Mesh : Meshes)
|
||||
{
|
||||
// Apply local rotation first then parent transform
|
||||
if (IsValid(Mesh.Mesh))
|
||||
{
|
||||
const FTransform CombinedXForm = FTransform(Mesh.Rotation, Mesh.Location, Mesh.Scale) * XForm;
|
||||
const FStaticMeshLODResources& Lod = Mesh.Mesh->GetLODForExport(Mesh.bUseLowestLOD ? Mesh.Mesh->GetNumLODs() - 1 : 0);
|
||||
TArray<FDynamicMeshVertex> Vertices;
|
||||
TArray<uint32> Indices;
|
||||
|
||||
Lod.IndexBuffer.GetCopy(Indices);
|
||||
auto& PosBuffer = Lod.VertexBuffers.PositionVertexBuffer;
|
||||
uint32 NumVerts = PosBuffer.GetNumVertices();
|
||||
Vertices.Reserve(NumVerts);
|
||||
for (uint32 i = 0; i < NumVerts; ++i)
|
||||
{
|
||||
Vertices.Add(FDynamicMeshVertex(PosBuffer.VertexPosition(i)));
|
||||
}
|
||||
|
||||
Ret->MeshesImproved.Add(FStevesDebugRenderSceneProxy::FDebugMesh(CombinedXForm.ToMatrixWithScale(), Vertices, Indices, Mesh.Colour));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return Ret;
|
||||
|
||||
}
|
||||
|
||||
FBoxSphereBounds UStevesEditorVisComponent::CalcBounds(const FTransform& LocalToWorld) const
|
||||
{
|
||||
// Get superclass bounds in LOCAL space (don't pass LocalToWorld)
|
||||
FBoxSphereBounds B = Super::CalcBounds(FTransform::Identity);
|
||||
|
||||
// Now we need to merge in all components
|
||||
for (auto& L : Lines)
|
||||
{
|
||||
// Re-centre the origin of the line to make box extents
|
||||
FVector Extents = L.Start.GetAbs().ComponentMax(L.End.GetAbs());
|
||||
B = B + FBoxSphereBounds(FVector::ZeroVector, Extents, Extents.GetMax());
|
||||
}
|
||||
for (auto& A : Arrows)
|
||||
{
|
||||
// Re-centre the origin of the line to make box extents
|
||||
FVector Extents = A.Start.GetAbs().ComponentMax(A.End.GetAbs());
|
||||
B = B + FBoxSphereBounds(FVector::ZeroVector, Extents, Extents.GetMax());
|
||||
}
|
||||
for (auto& C : Circles)
|
||||
{
|
||||
B = B + FBoxSphereBounds(C.Location, FVector(C.Radius), C.Radius);
|
||||
}
|
||||
for (auto& Arc : Arcs)
|
||||
{
|
||||
// Just use the entire circle for simplicity
|
||||
B = B + FBoxSphereBounds(Arc.Location, FVector(Arc.Radius), Arc.Radius);
|
||||
}
|
||||
for (auto& S : Spheres)
|
||||
{
|
||||
B = B + FBoxSphereBounds(S.Location, FVector(S.Radius), S.Radius);
|
||||
}
|
||||
for (auto& Box : Boxes)
|
||||
{
|
||||
FVector HalfSize = Box.Size * 0.5f;
|
||||
FBox DBox(-HalfSize, HalfSize);
|
||||
// Apply local rotation only, world is done later
|
||||
FTransform BoxXForm = FTransform(Box.Rotation, Box.Location);
|
||||
DBox = DBox.TransformBy(BoxXForm);
|
||||
B = B + FBoxSphereBounds(DBox);
|
||||
}
|
||||
for (auto& Cylinder : Cylinders)
|
||||
{
|
||||
FVector HalfSize = FVector(Cylinder.Radius, Cylinder.Radius, Cylinder.Height * 0.5f);
|
||||
FBox DBox(-HalfSize, HalfSize);
|
||||
// Apply local rotation only, world is done later
|
||||
FTransform XForm = FTransform(Cylinder.Rotation, Cylinder.Location);
|
||||
DBox = DBox.TransformBy(XForm);
|
||||
B = B + FBoxSphereBounds(DBox);
|
||||
}
|
||||
for (auto& Capsule : Capsules)
|
||||
{
|
||||
FVector HalfSize = FVector(Capsule.Radius, Capsule.Radius, Capsule.Height * 0.5f + Capsule.Radius * 2.f);
|
||||
FBox DBox(-HalfSize, HalfSize);
|
||||
// Apply local rotation only, world is done later
|
||||
FTransform XForm = FTransform(Capsule.Rotation, Capsule.Location);
|
||||
DBox = DBox.TransformBy(XForm);
|
||||
B = B + FBoxSphereBounds(DBox);
|
||||
}
|
||||
for (auto& Mesh : Meshes)
|
||||
{
|
||||
if (IsValid(Mesh.Mesh))
|
||||
{
|
||||
const FTransform XForm = FTransform(Mesh.Rotation, Mesh.Location, Mesh.Scale);
|
||||
B = B + Mesh.Mesh->GetBounds().TransformBy(XForm);
|
||||
}
|
||||
|
||||
}
|
||||
return B.TransformBy(LocalToWorld);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,716 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesGameSubsystem.h"
|
||||
|
||||
#include "EngineUtils.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "StevesGameViewportClientBase.h"
|
||||
#include "StevesPluginSettings.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Engine/AssetManager.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "GameFramework/InputSettings.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/PlayerInput.h"
|
||||
#include "StevesUI/KeySprite.h"
|
||||
#include "StevesUI/StevesUI.h"
|
||||
#include "TimerManager.h"
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "UnrealClient.h"
|
||||
#include "Slate/SceneViewport.h"
|
||||
|
||||
//PRAGMA_DISABLE_OPTIMIZATION
|
||||
|
||||
void UStevesGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
#if !UE_SERVER
|
||||
CreateInputDetector();
|
||||
InitTheme();
|
||||
InitForegroundCheck();
|
||||
NotifyEnhancedInputMappingsChanged();
|
||||
InitViewport();
|
||||
#endif
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::Deinitialize()
|
||||
{
|
||||
Super::Deinitialize();
|
||||
#if !UE_SERVER
|
||||
DestroyInputDetector();
|
||||
#endif
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::InitViewport()
|
||||
{
|
||||
FViewport::ViewportResizedEvent.AddUObject(this, &UStevesGameSubsystem::ViewportResized);
|
||||
if (auto GI = GetGameInstance())
|
||||
{
|
||||
if (auto VC = GI->GetGameViewportClient())
|
||||
{
|
||||
VC->OnToggleFullscreen().AddUObject(this, &UStevesGameSubsystem::FullscreenToggled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::ViewportResized(FViewport* Viewport, unsigned Unused)
|
||||
{
|
||||
FIntPoint Sz = Viewport->GetSizeXY();
|
||||
OnViewportResized.Broadcast(Sz.X, Sz.Y);
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::FullscreenToggled(bool bFullscreen)
|
||||
{
|
||||
if (auto GI = GetGameInstance())
|
||||
{
|
||||
if (auto VC = GI->GetGameViewportClient())
|
||||
{
|
||||
FIntPoint Sz = VC->GetGameViewport()->GetSizeXY();
|
||||
OnViewportResized.Broadcast(Sz.X, Sz.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::CreateInputDetector()
|
||||
{
|
||||
#if !UE_SERVER
|
||||
if (!InputDetector.IsValid())
|
||||
{
|
||||
InputDetector = MakeShareable(new FInputModeDetector());
|
||||
FSlateApplication::Get().RegisterInputPreProcessor(InputDetector);
|
||||
|
||||
InputDetector->OnInputModeChanged.BindUObject(this, &UStevesGameSubsystem::OnInputDetectorModeChanged);
|
||||
InputDetector->OnButtonInputModeChanged.BindUObject(this, &UStevesGameSubsystem::OnButtonInputDetectorModeChanged);
|
||||
InputDetector->OnAxisInputModeChanged.BindUObject(this, &UStevesGameSubsystem::OnAxisInputDetectorModeChanged);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::DestroyInputDetector()
|
||||
{
|
||||
#if !UE_SERVER
|
||||
if (InputDetector.IsValid())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(InputDetector);
|
||||
InputDetector.Reset();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::NotifyEnhancedInputMappingsChanged()
|
||||
{
|
||||
// delay to ensure there's a tick in between which updates the mappings, it's not synchronous
|
||||
auto DelayedFunc = [this]()
|
||||
{
|
||||
OnEnhancedInputMappingsChanged.Broadcast();
|
||||
};
|
||||
FTimerHandle TempHandle;
|
||||
GetWorld()->GetTimerManager().SetTimer(TempHandle, FTimerDelegate::CreateLambda(DelayedFunc), 0.05f, false);
|
||||
}
|
||||
|
||||
TSoftObjectPtr<UInputAction> UStevesGameSubsystem::FindEnhancedInputAction(const FString& Name)
|
||||
{
|
||||
if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::LoadModulePtr<FAssetRegistryModule>(TEXT("AssetRegistry")))
|
||||
{
|
||||
IAssetRegistry& AssetRegistry = AssetRegistryModule->Get();
|
||||
if (auto Settings = GetDefault<UStevesPluginSettings>())
|
||||
{
|
||||
for (const auto& Dir : Settings->EnhancedInputActionSearchDirectories)
|
||||
{
|
||||
if (!FPackageName::IsValidPath(Dir.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<FAssetData> Assets;
|
||||
FString Package = FPaths::Combine(Dir.Path, Name);
|
||||
if (AssetRegistry.GetAssetsByPackageName(FName(*Package), Assets, true))
|
||||
{
|
||||
for (const FAssetData& Asset : Assets)
|
||||
{
|
||||
if (Asset.GetClass() == UInputAction::StaticClass())
|
||||
{
|
||||
return TSoftObjectPtr<UInputAction>(Asset.GetSoftObjectPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::RegisterInterestInEnhancedInputAction(const UInputAction* Action, ETriggerEvent TriggerEvent)
|
||||
{
|
||||
// Avoid registering duplicate interest
|
||||
FEnhancedInputInterest Interest(Action, TriggerEvent);
|
||||
if (!RegisteredEnhancedInputActionInterests.Contains(Interest))
|
||||
{
|
||||
if (auto GI = GetGameInstance())
|
||||
{
|
||||
if (auto PC = GI->GetFirstLocalPlayerController())
|
||||
{
|
||||
if (auto EIC = Cast<UEnhancedInputComponent>(PC->InputComponent))
|
||||
{
|
||||
EIC->BindAction(Action, TriggerEvent, this, &ThisClass::EnhancedInputActionTriggered);
|
||||
RegisteredEnhancedInputActionInterests.Add(Interest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::UnregisterAllInterestInEnhancedInputActions()
|
||||
{
|
||||
if (auto GI = GetGameInstance())
|
||||
{
|
||||
if (auto PC = GI->GetFirstLocalPlayerController())
|
||||
{
|
||||
if (auto EIC = Cast<UEnhancedInputComponent>(PC->InputComponent))
|
||||
{
|
||||
TArray<uint32> HandlesToRemove;
|
||||
for (auto& Binding : EIC->GetActionEventBindings())
|
||||
{
|
||||
if (Binding->IsBoundToObject(this))
|
||||
{
|
||||
HandlesToRemove.Add(Binding->GetHandle());
|
||||
}
|
||||
}
|
||||
|
||||
for (const uint32 Handle : HandlesToRemove)
|
||||
{
|
||||
EIC->RemoveBindingByHandle(Handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RegisteredEnhancedInputActionInterests.Empty();
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::EnhancedInputActionTriggered(const FInputActionInstance& InputActionInstance)
|
||||
{
|
||||
OnEnhancedInputActionTriggered.Broadcast(InputActionInstance.GetSourceAction(), InputActionInstance.GetTriggerEvent());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void UStevesGameSubsystem::InitTheme()
|
||||
{
|
||||
DefaultUiTheme = LoadObject<UUiTheme>(nullptr, *DefaultUiThemePath, nullptr);
|
||||
}
|
||||
|
||||
|
||||
void UStevesGameSubsystem::InitForegroundCheck()
|
||||
{
|
||||
// Check foreground status every 0.5 seconds
|
||||
GetWorld()->GetTimerManager().SetTimer(ForegroundCheckHandle, this, &UStevesGameSubsystem::CheckForeground, 0.5, true);
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::CheckForeground()
|
||||
{
|
||||
bool bNewForeground = bIsForeground;
|
||||
|
||||
if (IsValid(GEngine) && IsValid(GEngine->GameViewport) && GEngine->GameViewport->Viewport)
|
||||
bNewForeground = GEngine->GameViewport->Viewport->IsForegroundWindow();
|
||||
|
||||
if (bNewForeground != bIsForeground)
|
||||
{
|
||||
bIsForeground = bNewForeground;
|
||||
InputDetector->bIgnoreEvents = !bIsForeground;
|
||||
|
||||
OnWindowForegroundChanged.Broadcast(bIsForeground);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
void UStevesGameSubsystem::OnInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
|
||||
{
|
||||
// We can't check this during Initialize because it's too early
|
||||
if (!bCheckedViewportClient)
|
||||
{
|
||||
auto GI = GetGameInstance();
|
||||
auto VC = Cast<UStevesGameViewportClientBase>(GI->GetGameViewportClient());
|
||||
if (!VC)
|
||||
UE_LOG(LogStevesUEHelpers, Log, TEXT("Consider using UStevesGameViewportClientBase for your GameViewportClient"))
|
||||
|
||||
bCheckedViewportClient = true;
|
||||
|
||||
}
|
||||
|
||||
auto Settings = GetDefault<UStevesPluginSettings>();
|
||||
if (Settings->bHideMouseWhenGamepadUsed)
|
||||
{
|
||||
auto GI = GetGameInstance();
|
||||
auto VC = GI->GetGameViewportClient();
|
||||
auto SVC = Cast<UStevesGameViewportClientBase>(VC);
|
||||
if (VC)
|
||||
{
|
||||
if (NewMode == EInputMode::Gamepad)
|
||||
{
|
||||
// First move mouse pointer out of the way because it still generates mouse hits (unless we make source changes to Slate, ugh)
|
||||
MoveMouseOffScreen(true);
|
||||
}
|
||||
else if (NewMode == EInputMode::Mouse)
|
||||
{
|
||||
if (SVC)
|
||||
SVC->SetSuppressMouseCursor(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
OnInputModeChanged.Broadcast(PlayerIndex, NewMode);
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::MoveMouseOffScreen(bool bAlsoHide) const
|
||||
{
|
||||
if (auto GI = GetGameInstance())
|
||||
{
|
||||
auto VC = GI->GetGameViewportClient();
|
||||
auto SVC = Cast<UStevesGameViewportClientBase>(VC);
|
||||
auto PC = GI->GetFirstLocalPlayerController();
|
||||
|
||||
FVector2D Sz;
|
||||
VC->GetViewportSize(Sz);
|
||||
// -1 because if you move cursor outside window when captured, Slate blows up when you press Return, ughghh
|
||||
// To make sure we don't generate a mouse move by doing this
|
||||
// Bizarrely the ViewportSize is correct for moving the mouse pointer but the mouse events have different geometry
|
||||
InputDetector->IgnoreNextMouseMove();
|
||||
PC->SetMouseLocation(Sz.X-1,Sz.Y-1);
|
||||
|
||||
// Now hide it
|
||||
// I've seen people use PC->bShowMouseCursor but this messes with capturing when you switch back & forth
|
||||
// especially when pausing in the editor
|
||||
// instead, I'm using a separate flag to suppress it, see UiFixGameViewportClient for usage
|
||||
if (SVC && bAlsoHide)
|
||||
SVC->SetSuppressMouseCursor(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::OnButtonInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
|
||||
{
|
||||
// This is specifically for button changes; if this is a different main input mode it will also be registered in OnInputDetectorModeChanged
|
||||
// Just relay this one
|
||||
OnButtonInputModeChanged.Broadcast(PlayerIndex, NewMode);
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::OnAxisInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
|
||||
{
|
||||
// This is specifically for button changes; if this is a different main input mode it will also be registered in OnInputDetectorModeChanged
|
||||
// Just relay this one
|
||||
OnAxisInputModeChanged.Broadcast(PlayerIndex, NewMode);
|
||||
}
|
||||
|
||||
FFocusSystem* UStevesGameSubsystem::GetFocusSystem()
|
||||
{
|
||||
return &FocusSystem;
|
||||
}
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetInputImageSprite(EInputBindingType BindingType,
|
||||
FName ActionOrAxis,
|
||||
FKey Key,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIdx,
|
||||
const UUiTheme* Theme)
|
||||
{
|
||||
switch(BindingType)
|
||||
{
|
||||
case EInputBindingType::Action:
|
||||
return GetInputImageSpriteFromAction(ActionOrAxis, DevicePreference, PlayerIdx, Theme);
|
||||
case EInputBindingType::Axis:
|
||||
return GetInputImageSpriteFromAxis(ActionOrAxis, DevicePreference, PlayerIdx, Theme);
|
||||
case EInputBindingType::Key:
|
||||
return GetInputImageSpriteFromKey(Key, PlayerIdx, Theme);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// This is not threadsafe! But only used in UI thread in practice
|
||||
TArray<FInputActionKeyMapping> GS_TempActionMap;
|
||||
TArray<FInputAxisKeyMapping> GS_TempAxisMap;
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromAction(const FName& Name,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIdx,
|
||||
const UUiTheme* Theme)
|
||||
{
|
||||
UInputSettings* Settings = UInputSettings::GetInputSettings();
|
||||
GS_TempActionMap.Empty();
|
||||
Settings->GetActionMappingByName(Name, GS_TempActionMap);
|
||||
|
||||
// For default, prefer latest press keyboard/mouse for buttons
|
||||
if (DevicePreference == EInputImageDevicePreference::Auto)
|
||||
DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
|
||||
|
||||
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
|
||||
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
|
||||
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
|
||||
const auto Preferred = GetPreferedActionOrAxisMapping<FInputActionKeyMapping>(GS_TempActionMap, Name, DevicePreference, LastInput, LastButtonInput, LastAxisInput);
|
||||
if (Preferred)
|
||||
{
|
||||
return GetInputImageSpriteFromKey(Preferred->Key, PlayerIdx, Theme);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromAxis(const FName& Name,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIdx,
|
||||
const UUiTheme* Theme)
|
||||
{
|
||||
// Look up the key for this axis
|
||||
UInputSettings* Settings = UInputSettings::GetInputSettings();
|
||||
GS_TempAxisMap.Empty();
|
||||
Settings->GetAxisMappingByName(Name, GS_TempAxisMap);
|
||||
|
||||
// For default, prefer mouse for axes
|
||||
if (DevicePreference == EInputImageDevicePreference::Auto)
|
||||
DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
|
||||
|
||||
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
|
||||
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
|
||||
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
|
||||
const auto Preferred = GetPreferedActionOrAxisMapping<FInputAxisKeyMapping>(GS_TempAxisMap, Name, DevicePreference, LastInput, LastButtonInput, LastAxisInput);
|
||||
if (Preferred)
|
||||
{
|
||||
return GetInputImageSpriteFromKey(Preferred->Key, PlayerIdx, Theme);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIdx,
|
||||
APlayerController* PC,
|
||||
UUiTheme* Theme)
|
||||
{
|
||||
|
||||
if (const UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
||||
{
|
||||
const TArray<FKey> Keys = Subsystem->QueryKeysMappedToAction(Action);
|
||||
|
||||
// For default, prefer mouse for axes
|
||||
if (DevicePreference == EInputImageDevicePreference::Auto)
|
||||
{
|
||||
if (Action->ValueType == EInputActionValueType::Boolean)
|
||||
{
|
||||
DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
|
||||
}
|
||||
else
|
||||
{
|
||||
DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
|
||||
}
|
||||
}
|
||||
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
|
||||
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
|
||||
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
|
||||
if (const FKey* PreferredKey = GetPreferedKeyMapping(Keys, DevicePreference, LastInput, LastButtonInput, LastAxisInput))
|
||||
{
|
||||
return GetInputImageSpriteFromKey(*PreferredKey, PlayerIdx, Theme);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
TSoftObjectPtr<UDataTable> UStevesGameSubsystem::GetGamepadImages(int PlayerIndex, const UUiTheme* Theme)
|
||||
{
|
||||
// TODO: determine type of controller
|
||||
return Theme->XboxControllerImages;
|
||||
}
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromKey(const FKey& InKey, int PlayerIndex, const UUiTheme* Theme)
|
||||
{
|
||||
if (!IsValid(Theme))
|
||||
Theme = GetDefaultUiTheme();
|
||||
|
||||
if (Theme)
|
||||
{
|
||||
if (InKey.IsGamepadKey())
|
||||
return GetImageSpriteFromTable(InKey, GetGamepadImages(PlayerIndex, Theme));
|
||||
else
|
||||
return GetImageSpriteFromTable(InKey, Theme->KeyboardMouseImages);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
UPaperSprite* UStevesGameSubsystem::GetImageSpriteFromTable(const FKey& InKey,
|
||||
const TSoftObjectPtr<UDataTable>& Asset)
|
||||
{
|
||||
// Sync load for simplicity for now
|
||||
const auto Table = Asset.LoadSynchronous();
|
||||
// Rows are named the same as the key name
|
||||
const auto SpriteRow = Table->FindRow<FKeySprite>(InKey.GetFName(), "Find Key Image");
|
||||
if (SpriteRow)
|
||||
{
|
||||
return SpriteRow->Sprite;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterface<ISlateTextureAtlasInterface> AtlasRegion, bool bMatchSize)
|
||||
{
|
||||
if(Brush->GetResourceObject() != AtlasRegion.GetObject())
|
||||
{
|
||||
Brush->SetResourceObject(AtlasRegion.GetObject());
|
||||
|
||||
if (bMatchSize)
|
||||
{
|
||||
if (AtlasRegion)
|
||||
{
|
||||
const FSlateAtlasData AtlasData = AtlasRegion->GetSlateAtlasData();
|
||||
Brush->ImageSize = AtlasData.GetSourceDimensions();
|
||||
}
|
||||
else
|
||||
{
|
||||
Brush->ImageSize = FVector2D(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FStevesTextureRenderTargetPoolPtr UStevesGameSubsystem::GetTextureRenderTargetPool(FName Name, bool bAutoCreate)
|
||||
{
|
||||
// On the assumption there won't be *loads* of pools, not worth a map, just iterate
|
||||
for (auto Tex : TextureRenderTargetPools)
|
||||
{
|
||||
if (Tex->GetName() == Name)
|
||||
return Tex;
|
||||
}
|
||||
|
||||
if (bAutoCreate)
|
||||
{
|
||||
FStevesTextureRenderTargetPoolPtr Pool = MakeShared<FStevesTextureRenderTargetPool>(Name, this);
|
||||
TextureRenderTargetPools.Add(Pool);
|
||||
return Pool;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::ShouldProcessInputEvents() const
|
||||
{
|
||||
return !bIgnoreEvents;
|
||||
}
|
||||
|
||||
UStevesGameSubsystem::FInputModeDetector::FInputModeDetector()
|
||||
{
|
||||
// 4 local players should be plenty usually (will expand if necessary)
|
||||
LastInputModeByPlayer.Init(DefaultInputMode, 4);
|
||||
LastButtonPressByPlayer.Init(DefaultButtonInputMode, 4);
|
||||
LastAxisMoveByPlayer.Init(DefaultAxisInputMode, 4);
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
if (ShouldProcessInputEvents())
|
||||
{
|
||||
// Key down also registers for gamepad buttons
|
||||
ProcessKeyOrButton(InKeyEvent.GetUserIndex(), InKeyEvent.GetKey());
|
||||
}
|
||||
|
||||
// Don't consume
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::HandleAnalogInputEvent(FSlateApplication& SlateApp,
|
||||
const FAnalogInputEvent& InAnalogInputEvent)
|
||||
{
|
||||
if (ShouldProcessInputEvents())
|
||||
{
|
||||
if (InAnalogInputEvent.GetAnalogValue() > GamepadAxisThreshold)
|
||||
SetMode(InAnalogInputEvent.GetUserIndex(), EInputMode::Gamepad, false);
|
||||
}
|
||||
|
||||
// Don't consume
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
|
||||
{
|
||||
if (ShouldProcessInputEvents())
|
||||
{
|
||||
if (bIgnoreNextMouseMove)
|
||||
{
|
||||
bIgnoreNextMouseMove = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
FVector2D Dist = MouseEvent.GetScreenSpacePosition() - MouseEvent.GetLastScreenSpacePosition();
|
||||
if (FMath::Abs(Dist.X) > MouseMoveThreshold || FMath::Abs(Dist.Y) > MouseMoveThreshold)
|
||||
{
|
||||
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't consume
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
|
||||
{
|
||||
if (ShouldProcessInputEvents())
|
||||
{
|
||||
// We don't care which button
|
||||
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse, true);
|
||||
}
|
||||
|
||||
// Don't consume
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
|
||||
const FPointerEvent* InGestureEvent)
|
||||
{
|
||||
if (ShouldProcessInputEvents())
|
||||
{
|
||||
SetMode(InWheelEvent.GetUserIndex(), EInputMode::Mouse, false);
|
||||
}
|
||||
|
||||
// Don't consume
|
||||
return false;
|
||||
}
|
||||
|
||||
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastInputMode(int PlayerIndex)
|
||||
{
|
||||
if (PlayerIndex >= 0 && PlayerIndex < LastInputModeByPlayer.Num())
|
||||
return LastInputModeByPlayer[PlayerIndex];
|
||||
|
||||
// Assume default if never told
|
||||
return DefaultInputMode;
|
||||
}
|
||||
|
||||
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastButtonInputMode(int PlayerIndex)
|
||||
{
|
||||
if (PlayerIndex >= 0 && PlayerIndex < LastButtonPressByPlayer.Num())
|
||||
return LastButtonPressByPlayer[PlayerIndex];
|
||||
|
||||
// Assume default if never told
|
||||
return DefaultButtonInputMode;
|
||||
}
|
||||
|
||||
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastAxisInputMode(int PlayerIndex)
|
||||
{
|
||||
if (PlayerIndex >= 0 && PlayerIndex < LastAxisMoveByPlayer.Num())
|
||||
return LastAxisMoveByPlayer[PlayerIndex];
|
||||
|
||||
// Assume default if never told
|
||||
return DefaultAxisInputMode;
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::FInputModeDetector::ProcessKeyOrButton(int PlayerIndex, FKey Key)
|
||||
{
|
||||
if (Key.IsGamepadKey())
|
||||
{
|
||||
SetMode(PlayerIndex, EInputMode::Gamepad, IsAGamepadButton(Key));
|
||||
}
|
||||
else if (Key.IsMouseButton())
|
||||
{
|
||||
// Assuming mice don't have analog buttons!
|
||||
SetMode(PlayerIndex, EInputMode::Mouse, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We assume anything that's not mouse and not gamepad is a keyboard
|
||||
// Assuming keyboards don't have analog buttons!
|
||||
SetMode(PlayerIndex, EInputMode::Keyboard, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool UStevesGameSubsystem::FInputModeDetector::IsAGamepadButton(const FKey& Key)
|
||||
{
|
||||
|
||||
// Key.IsButtonAxis() returns true for some thumbstick movement events, because the axis type is EInputAxisType::Button for
|
||||
// some reason. That means you get button events for thumbstick movements, which is super dumb.
|
||||
// See core engine InputCoreTypes.cpp for the stick axes which are defined FKeyDetails::GamepadKey | FKeyDetails::ButtonAxis
|
||||
// This is for some kind of virtual input but it's a nasty hack, omit them
|
||||
return Key.IsGamepadKey() &&
|
||||
Key != EKeys::Gamepad_LeftStick_Up &&
|
||||
Key != EKeys::Gamepad_LeftStick_Down &&
|
||||
Key != EKeys::Gamepad_LeftStick_Left &&
|
||||
Key != EKeys::Gamepad_LeftStick_Right &&
|
||||
Key != EKeys::Gamepad_RightStick_Up &&
|
||||
Key != EKeys::Gamepad_RightStick_Down &&
|
||||
Key != EKeys::Gamepad_RightStick_Left &&
|
||||
Key != EKeys::Gamepad_RightStick_Right;
|
||||
|
||||
}
|
||||
|
||||
void UStevesGameSubsystem::FInputModeDetector::SetMode(int PlayerIndex, EInputMode NewMode, bool bIsButton)
|
||||
{
|
||||
bool bButtonChanged = false;
|
||||
bool bAxisChanged = false;
|
||||
bool bMainChanged = false;
|
||||
|
||||
if (bIsButton)
|
||||
{
|
||||
if (NewMode != EInputMode::Unknown && NewMode != GetLastButtonInputMode(PlayerIndex))
|
||||
{
|
||||
if (PlayerIndex >= LastButtonPressByPlayer.Num())
|
||||
LastButtonPressByPlayer.SetNum(PlayerIndex + 1);
|
||||
|
||||
LastButtonPressByPlayer[PlayerIndex] = NewMode;
|
||||
|
||||
bButtonChanged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (NewMode != EInputMode::Unknown && NewMode != GetLastAxisInputMode(PlayerIndex))
|
||||
{
|
||||
if (PlayerIndex >= LastAxisMoveByPlayer.Num())
|
||||
LastAxisMoveByPlayer.SetNum(PlayerIndex + 1);
|
||||
|
||||
LastAxisMoveByPlayer[PlayerIndex] = NewMode;
|
||||
|
||||
bAxisChanged = true;
|
||||
}
|
||||
|
||||
}
|
||||
// Whether it's a button or not it can affect the main input mode
|
||||
if (NewMode != EInputMode::Unknown && NewMode != GetLastInputMode(PlayerIndex))
|
||||
{
|
||||
if (PlayerIndex >= LastInputModeByPlayer.Num())
|
||||
LastInputModeByPlayer.SetNum(PlayerIndex + 1);
|
||||
|
||||
LastInputModeByPlayer[PlayerIndex] = NewMode;
|
||||
|
||||
bMainChanged = true;
|
||||
}
|
||||
|
||||
// Raise events at the end once all state has changed
|
||||
if (bButtonChanged)
|
||||
{
|
||||
// ReSharper disable once CppExpressionWithoutSideEffects
|
||||
OnButtonInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
|
||||
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Button mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
|
||||
}
|
||||
if (bAxisChanged)
|
||||
{
|
||||
// ReSharper disable once CppExpressionWithoutSideEffects
|
||||
OnAxisInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
|
||||
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Axis mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
|
||||
}
|
||||
if (bMainChanged)
|
||||
{
|
||||
// ReSharper disable once CppExpressionWithoutSideEffects
|
||||
OnInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
|
||||
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Input mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//PRAGMA_ENABLE_OPTIMIZATION
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesGameViewportClientBase.h"
|
||||
|
||||
|
||||
#include "Engine/Console.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
void UStevesGameViewportClientBase::Init(FWorldContext& WorldContext, UGameInstance* OwningGameInstance,
|
||||
bool bCreateNewAudioDevice)
|
||||
{
|
||||
Super::Init(WorldContext, OwningGameInstance, bCreateNewAudioDevice);
|
||||
|
||||
bSuppressMouseCursor = false;
|
||||
}
|
||||
|
||||
EMouseCursor::Type UStevesGameViewportClientBase::GetCursor(FViewport* InViewport, int32 X, int32 Y)
|
||||
{
|
||||
if (FSlateApplication::Get().IsActive() && bSuppressMouseCursor)
|
||||
return EMouseCursor::None;
|
||||
|
||||
return Super::GetCursor(InViewport, X, Y);
|
||||
}
|
||||
|
||||
void UStevesGameViewportClientBase::SetSuppressMouseCursor(bool bSuppress)
|
||||
{
|
||||
bSuppressMouseCursor = bSuppress;
|
||||
FSlateApplication::Get().OnCursorSet(); // necessary to make slate wake up
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright Steve Streeting
|
||||
// Licensed under the MIT License (see License.txt)
|
||||
#include "StevesLightFlicker.h"
|
||||
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
TMap<EStevesLightFlickerPattern, FRichCurve> UStevesLightFlickerHelper::Curves;
|
||||
TMap<FString, FRichCurve> UStevesLightFlickerHelper::CustomCurves;
|
||||
FCriticalSection UStevesLightFlickerHelper::CriticalSection;
|
||||
|
||||
const TMap<EStevesLightFlickerPattern, FString> UStevesLightFlickerHelper::StandardPatterns {
|
||||
// Quake lighting flicker functions
|
||||
// https://github.com/id-Software/Quake/blob/bf4ac424ce754894ac8f1dae6a3981954bc9852d/qw-qc/world.qc#L328-L372
|
||||
{ EStevesLightFlickerPattern::Flicker1, TEXT("mmnmmommommnonmmonqnmmo") },
|
||||
{ EStevesLightFlickerPattern::SlowStrongPulse, TEXT("abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba") },
|
||||
{ EStevesLightFlickerPattern::Candle1, TEXT("mmmmmaaaaammmmmaaaaaabcdefgabcdefg") },
|
||||
{ EStevesLightFlickerPattern::FastStrobe, TEXT("mamamamamama") },
|
||||
{ EStevesLightFlickerPattern::GentlePulse1, TEXT("jklmnopqrstuvwxyzyxwvutsrqponmlkj") },
|
||||
{ EStevesLightFlickerPattern::Flicker2, TEXT("nmonqnmomnmomomno") },
|
||||
{ EStevesLightFlickerPattern::Candle2, TEXT("mmmaaaabcdefgmmmmaaaammmaamm") },
|
||||
{ EStevesLightFlickerPattern::Candle3, TEXT("mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa") },
|
||||
{ EStevesLightFlickerPattern::SlowStrobe, TEXT("aaaaaaaazzzzzzzz") },
|
||||
{ EStevesLightFlickerPattern::FlourescentFlicker, TEXT("mmamammmmammamamaaamammma") },
|
||||
{ EStevesLightFlickerPattern::SlowPulseNoBlack, TEXT("abcdefghijklmnopqrrqponmlkjihgfedcba") },
|
||||
|
||||
/// Eniko's torch pattern from Kitsune Tails
|
||||
{ EStevesLightFlickerPattern::Torch1, TEXT("mnkjcfcdafdehifkjlm") },
|
||||
/// No black version of Torch1
|
||||
{ EStevesLightFlickerPattern::Torch2, TEXT("mnkjcfcecfdehifkjlm") }
|
||||
};
|
||||
|
||||
float UStevesLightFlickerHelper::EvaluateLightCurve(EStevesLightFlickerPattern CurveType, float Time)
|
||||
{
|
||||
return GetLightCurve(CurveType).Eval(Time);
|
||||
}
|
||||
|
||||
const FRichCurve& UStevesLightFlickerHelper::GetLightCurve(EStevesLightFlickerPattern CurveType)
|
||||
{
|
||||
FScopeLock ScopeLock(&CriticalSection);
|
||||
|
||||
if (auto pCurve = Curves.Find(CurveType))
|
||||
{
|
||||
return *pCurve;
|
||||
}
|
||||
|
||||
auto& Curve = Curves.Emplace(CurveType);
|
||||
BuildCurve(CurveType, Curve);
|
||||
return Curve;
|
||||
}
|
||||
|
||||
const FRichCurve& UStevesLightFlickerHelper::GetLightCurve(const FString& CurveStr)
|
||||
{
|
||||
FScopeLock ScopeLock(&CriticalSection);
|
||||
|
||||
if (auto pCurve = CustomCurves.Find(CurveStr))
|
||||
{
|
||||
return *pCurve;
|
||||
}
|
||||
|
||||
auto& Curve = CustomCurves.Emplace(CurveStr);
|
||||
BuildCurve(CurveStr, Curve);
|
||||
return Curve;
|
||||
}
|
||||
|
||||
void UStevesLightFlickerHelper::BuildCurve(EStevesLightFlickerPattern CurveType, FRichCurve& OutCurve)
|
||||
{
|
||||
if (auto pTxt = StandardPatterns.Find(CurveType))
|
||||
{
|
||||
BuildCurve(*pTxt, OutCurve);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UStevesLightFlickerHelper::BuildCurve(const FString& QuakeCurveChars, FRichCurve& OutCurve)
|
||||
{
|
||||
OutCurve.Reset();
|
||||
|
||||
for (int i = 0; i < QuakeCurveChars.Len(); ++i)
|
||||
{
|
||||
// We actually build the curve a..z = 0..1, and then use a default max value of 2 to restore the original behaviour.
|
||||
// Actually the curve is 0..1.04 due to original behaviour that z is 2.08 not 2
|
||||
const int CharIndex = QuakeCurveChars[i] - 'a';
|
||||
const float Val = (float)CharIndex / 24.f; // to ensure m==1, z==2.08 (rescaled to half that so 0..1.04)
|
||||
// Quake default was each character was 0.1s
|
||||
OutCurve.AddKey(i * 0.1f, Val);
|
||||
}
|
||||
|
||||
// To catch empty
|
||||
if (QuakeCurveChars.IsEmpty())
|
||||
{
|
||||
OutCurve.AddKey(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
UStevesLightFlickerComponent::UStevesLightFlickerComponent(const FObjectInitializer& Initializer):
|
||||
Super(Initializer),
|
||||
TimePos(0),
|
||||
CurrentValue(0),
|
||||
Curve(nullptr)
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bTickEvenWhenPaused = false;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = false;
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
GenerateCurveAndPlay();
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::GenerateCurveAndPlay()
|
||||
{
|
||||
if (FlickerPattern == EStevesLightFlickerPattern::Custom)
|
||||
{
|
||||
Curve = &UStevesLightFlickerHelper::GetLightCurve(CustomFlickerPattern);
|
||||
}
|
||||
else
|
||||
{
|
||||
Curve = &UStevesLightFlickerHelper::GetLightCurve(FlickerPattern);
|
||||
}
|
||||
TimePos = 0;
|
||||
if (bAutoPlay)
|
||||
{
|
||||
Play();
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::ValueUpdate()
|
||||
{
|
||||
CurrentValue = FMath::Lerp(MinValue, MaxValue, Curve->Eval(TimePos));
|
||||
OnLightFlickerUpdate.Broadcast(CurrentValue);
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::Play(bool bResetTime)
|
||||
{
|
||||
if (GetOwnerRole() == ROLE_Authority || !GetIsReplicated())
|
||||
{
|
||||
if (bResetTime)
|
||||
{
|
||||
TimePos = 0;
|
||||
}
|
||||
ValueUpdate();
|
||||
|
||||
PrimaryComponentTick.SetTickFunctionEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::Pause()
|
||||
{
|
||||
if (GetOwnerRole() == ROLE_Authority || !GetIsReplicated())
|
||||
{
|
||||
PrimaryComponentTick.SetTickFunctionEnable(false);
|
||||
}
|
||||
}
|
||||
|
||||
float UStevesLightFlickerComponent::GetCurrentValue() const
|
||||
{
|
||||
return CurrentValue;
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::OnRep_TimePos()
|
||||
{
|
||||
ValueUpdate();
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::TickComponent(float DeltaTime,
|
||||
ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
TimePos += DeltaTime * Speed;
|
||||
const float MaxTime = Curve->GetLastKey().Time;
|
||||
while (TimePos > MaxTime)
|
||||
{
|
||||
TimePos -= MaxTime;
|
||||
}
|
||||
ValueUpdate();
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::SetFlickerPattern(EStevesLightFlickerPattern Pattern,
|
||||
const FString& CustomPatternString)
|
||||
{
|
||||
FlickerPattern = Pattern;
|
||||
CustomFlickerPattern = CustomPatternString;
|
||||
GenerateCurveAndPlay();
|
||||
}
|
||||
|
||||
EStevesLightFlickerPattern UStevesLightFlickerComponent::GetFlickerPattern(FString& CustomString)
|
||||
{
|
||||
CustomString = CustomFlickerPattern;
|
||||
return FlickerPattern;
|
||||
}
|
||||
|
||||
void UStevesLightFlickerComponent::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(UStevesLightFlickerComponent, TimePos);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesMathHelpers.h"
|
||||
|
||||
#include "Chaos/CastingUtilities.h"
|
||||
#include "Chaos/GeometryQueries.h"
|
||||
#include "PhysicsEngine/ConvexElem.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Chaos/ChaosEngineInterface.h"
|
||||
|
||||
|
||||
bool StevesMathHelpers::OverlapConvex(const FKConvexElem& Convex,
|
||||
const FTransform& ConvexTransform,
|
||||
const FCollisionShape& Shape,
|
||||
const FVector& ShapePos,
|
||||
const FQuat& ShapeRot,
|
||||
FMTDResult& OutResult)
|
||||
{
|
||||
const FPhysicsShapeAdapter ShapeAdapter(ShapeRot, Shape);
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
const Chaos::FConvexPtr& ChaosConvex = Convex.GetChaosConvexMesh();
|
||||
#else
|
||||
const TSharedPtr<Chaos::FConvex, ESPMode::ThreadSafe>& ChaosConvex = Convex.GetChaosConvexMesh();
|
||||
#endif
|
||||
if (!ChaosConvex.IsValid())
|
||||
return false;
|
||||
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
const auto& ChaosConvexObj = *ChaosConvex.GetReference();
|
||||
#else
|
||||
const auto& ChaosConvexObj = *ChaosConvex.Get();
|
||||
#endif
|
||||
|
||||
const Chaos::FImplicitObject& ShapeGeom = ShapeAdapter.GetGeometry();
|
||||
const FTransform& ShapeGeomTransform = ShapeAdapter.GetGeomPose(ShapePos);
|
||||
|
||||
OutResult.Distance = 0;
|
||||
|
||||
bool bHasOverlap = false;
|
||||
|
||||
Chaos::FMTDInfo MTDInfo;
|
||||
if (Chaos::Utilities::CastHelper(ShapeGeom,
|
||||
ShapeGeomTransform,
|
||||
[&](const auto& Downcast, const auto& FullGeomTransform)
|
||||
{
|
||||
return Chaos::OverlapQuery(ChaosConvexObj,
|
||||
ConvexTransform,
|
||||
Downcast,
|
||||
FullGeomTransform,
|
||||
/*Thickness=*/
|
||||
0,
|
||||
&MTDInfo);
|
||||
}))
|
||||
{
|
||||
bHasOverlap = true;
|
||||
OutResult.Distance = MTDInfo.Penetration;
|
||||
OutResult.Direction = MTDInfo.Normal;
|
||||
}
|
||||
|
||||
return bHasOverlap;
|
||||
}
|
||||
|
||||
float StevesMathHelpers::GetDistanceToConvex2D(const TArray<FVector2f>& ConvexPoints, const FVector& LocalPoint)
|
||||
{
|
||||
return GetDistanceToConvex2D(ConvexPoints, FVector2f(LocalPoint.X, LocalPoint.Y));
|
||||
}
|
||||
|
||||
float StevesMathHelpers::GetDistanceToConvex2D(const TArray<FVector2f>& ConvexPoints, const FVector2f& LocalPoint)
|
||||
{
|
||||
// Assume inside until 1 or more tests show it's outside
|
||||
bool bInside = true;
|
||||
float ClosestOutside = 1e30f;
|
||||
float ClosestInside = 1e30f;
|
||||
const int N = ConvexPoints.Num();
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
const int OtherIdx = (i + 1) % N;
|
||||
const FVector2f& Start = ConvexPoints[i];
|
||||
const FVector2f& End = ConvexPoints[OtherIdx];
|
||||
const FVector2f Line = End - Start;
|
||||
// Determine inside / outside first
|
||||
// This may be the distance, but might not be if perpendicular projection is outside line segment
|
||||
// So save the normalise for later
|
||||
// Simple cross product to get the (non unit length) normal
|
||||
const FVector2f Normal = FVector2f(-Line.Y, Line.X);
|
||||
const FVector2f ToPoint = LocalPoint - ConvexPoints[i];
|
||||
const float DotNormal = Normal.Dot(ToPoint);
|
||||
|
||||
if (DotNormal > 0)
|
||||
{
|
||||
// If >0 result is outside, point must be outside
|
||||
bInside = false;
|
||||
}
|
||||
|
||||
// Do a perpendicular projection onto the line segment to see if we're within the limits of it
|
||||
const float DotLine = Line.Dot(ToPoint);
|
||||
const float T = DotLine / Line.SquaredLength();
|
||||
|
||||
float Dist;
|
||||
if (T < 0)
|
||||
{
|
||||
// Outside line segment, closest point is start
|
||||
Dist = (LocalPoint - Start).Length();
|
||||
}
|
||||
else if (T > 1)
|
||||
{
|
||||
// Outside line segment, closest point is end
|
||||
Dist = (LocalPoint - End).Length();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Within line segment
|
||||
Dist = FMath::Sqrt(ToPoint.SquaredLength() - FMath::Square(T * Line.Length()));
|
||||
}
|
||||
|
||||
if (DotNormal > 0)
|
||||
{
|
||||
ClosestOutside = FMath::Min(ClosestOutside, Dist);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClosestInside = FMath::Min(ClosestInside, Dist);
|
||||
}
|
||||
}
|
||||
|
||||
return bInside ? -ClosestInside : ClosestOutside;
|
||||
}
|
||||
|
||||
int StevesMathHelpers::Fill2DRegionWithRectangles(int StartX,
|
||||
int StartY,
|
||||
int Width,
|
||||
int Height,
|
||||
std::function<bool(int, int)> CellIncludeFunc,
|
||||
TArray<FIntRect>& OutRects)
|
||||
{
|
||||
int RectCount = 0;
|
||||
|
||||
const int Len = Width*Height;
|
||||
TArray<bool> bDoneMarkers;
|
||||
bDoneMarkers.SetNumUninitialized(Len);
|
||||
int CellsTodo = 0;
|
||||
|
||||
int StartN = 0;
|
||||
const int EndX = StartX + Width - 1;
|
||||
const int EndY = StartY + Height - 1;
|
||||
// Initialise done markers based on func
|
||||
for (int y = 0; y < Height; ++y)
|
||||
{
|
||||
for (int x = 0; x < Width; ++x)
|
||||
{
|
||||
const bool Include = CellIncludeFunc(x+StartX, y+StartY);
|
||||
bDoneMarkers[y*Width + x] = !Include;
|
||||
if (Include)
|
||||
{
|
||||
if (++CellsTodo == 1)
|
||||
{
|
||||
// This is the one we'll start with, might as well calculate it while we're here
|
||||
StartN = y*Width + x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (CellsTodo > 0)
|
||||
{
|
||||
// Find next starting point, from last one, until not done
|
||||
for (; StartN < Len && bDoneMarkers[StartN]; ++StartN) {}
|
||||
|
||||
// Shouldn't happen, but just in case
|
||||
if (StartN >= Len)
|
||||
break;
|
||||
|
||||
// NOTE: this X/Y is local (based at 0,0 not StartX/StartY, for use with DoneMarkers)
|
||||
const int LocalStartX = StartN % Width;
|
||||
const int LocalStartY = StartN / Width;
|
||||
|
||||
// We try not to create long & thin rects if we can help it, unlike greedy meshing
|
||||
// We're greedy in alternate dims to try to make fatter quads
|
||||
bool bCanExtendX = true, bCanExtendY = true;
|
||||
bool bExtendingX = true;
|
||||
int W = 1;
|
||||
int H = 1;
|
||||
|
||||
while (bCanExtendX || bCanExtendY)
|
||||
{
|
||||
// Try extending right
|
||||
if (bExtendingX)
|
||||
{
|
||||
bool ExtendOK = LocalStartX+W < Width;
|
||||
for (int TestY = LocalStartY; ExtendOK && TestY < LocalStartY+H; ++TestY)
|
||||
{
|
||||
if (bDoneMarkers[TestY*Width + LocalStartX+W])
|
||||
{
|
||||
// No good
|
||||
ExtendOK = false;
|
||||
}
|
||||
}
|
||||
if (ExtendOK)
|
||||
{
|
||||
++W;
|
||||
}
|
||||
else
|
||||
{
|
||||
bCanExtendX = false;
|
||||
}
|
||||
// Flip extending axis if possible
|
||||
if (bCanExtendY)
|
||||
bExtendingX = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try extending down
|
||||
bool ExtendOK = LocalStartY+H < Height;
|
||||
for (int TestX = LocalStartX; ExtendOK && TestX < LocalStartX+W; ++TestX)
|
||||
{
|
||||
if (bDoneMarkers[(LocalStartY+H)*Width + TestX])
|
||||
{
|
||||
// No good
|
||||
ExtendOK = false;
|
||||
}
|
||||
}
|
||||
if (ExtendOK)
|
||||
{
|
||||
++H;
|
||||
}
|
||||
else
|
||||
{
|
||||
bCanExtendY = false;
|
||||
}
|
||||
// Flip extending axis if possible
|
||||
if (bCanExtendX)
|
||||
bExtendingX = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We've calculated the max extension
|
||||
for (int y = LocalStartY; y < LocalStartY+H; ++y)
|
||||
{
|
||||
for (int x = LocalStartX; x < LocalStartX+W; ++x)
|
||||
{
|
||||
bDoneMarkers[y*Width+x] = true;
|
||||
--CellsTodo;
|
||||
}
|
||||
}
|
||||
OutRects.Add(FIntRect(StartX+LocalStartX, StartY+LocalStartY, StartX+LocalStartX+W-1, StartY+LocalStartY+H-1));
|
||||
++RectCount;
|
||||
|
||||
}
|
||||
|
||||
return RectCount;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesPluginSettings.h"
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesPooledActorSystem.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "StevesPooledActor.h"
|
||||
|
||||
UStevesPooledActorSystem* UStevesPooledActorSystem::Get(const UObject* WorldContext)
|
||||
{
|
||||
if (IsValid(WorldContext))
|
||||
{
|
||||
if (auto World = WorldContext->GetWorld())
|
||||
{
|
||||
return World->GetSubsystem<UStevesPooledActorSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TDeque<TObjectPtr<AActor>>* UStevesPooledActorSystem::GetPool(UClass* Class, bool bCreate)
|
||||
{
|
||||
auto pPool = Pools.Find(Class);
|
||||
if (!pPool && bCreate)
|
||||
{
|
||||
// Create new pool
|
||||
pPool = &Pools.Emplace(Class);
|
||||
}
|
||||
|
||||
return pPool;
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::AddActorToPool(AActor* Actor)
|
||||
{
|
||||
if (!IsValid(Actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UClass* Class = Actor->GetClass()->GetAuthoritativeClass();
|
||||
|
||||
if (auto pPool = GetPool(Class, true))
|
||||
{
|
||||
pPool->PushLast(Actor);
|
||||
DisableActor(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::ReleasePooledActor(AActor* Actor)
|
||||
{
|
||||
// This is really just a synonym so that Get/Release pattern is more pleasingly symmetrical
|
||||
// We have AddActorToPool for externally created actors
|
||||
AddActorToPool(Actor);
|
||||
}
|
||||
|
||||
AActor* UStevesPooledActorSystem::GetPooledActor(TSubclassOf<AActor> ActorClass,
|
||||
FVector const& Location,
|
||||
FRotator const& Rotation,
|
||||
bool& bWasReused)
|
||||
{
|
||||
UClass* Class = ActorClass->GetAuthoritativeClass();
|
||||
|
||||
if (auto pPool = GetPool(Class, false))
|
||||
{
|
||||
// We use the end of the deque meaning we get most recently used actor for better potential cache use
|
||||
TObjectPtr<AActor> Ret = nullptr;
|
||||
// In case someone has destroyed the actors we've got in the pool, keep trying if not valid until empty
|
||||
while (pPool->TryPopLast(Ret))
|
||||
{
|
||||
if (IsValid(Ret.Get()))
|
||||
{
|
||||
ReviveActor(Ret.Get(), Location, Rotation);
|
||||
bWasReused = true;
|
||||
return Ret.Get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to spawn a new one
|
||||
// We don't add this to the pool of course, because the caller is getting it
|
||||
bWasReused = false;
|
||||
return SpawnNewActor(Class, Location, Rotation);
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::PreWarmActorPool(TSubclassOf<AActor> ActorClass,
|
||||
int Count)
|
||||
{
|
||||
UClass* Class = ActorClass->GetAuthoritativeClass();
|
||||
if (auto pPool = GetPool(Class, true))
|
||||
{
|
||||
for (int i = pPool->Num(); i < Count; ++i)
|
||||
{
|
||||
auto Actor = SpawnNewActor(Class, StorageLocation, FRotator::ZeroRotator);
|
||||
DisableActor(Actor);
|
||||
pPool->PushLast(TObjectPtr<AActor>(Actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AActor* UStevesPooledActorSystem::SpawnNewActor(UClass* Class, const FVector& Location, const FRotator& Rotation)
|
||||
{
|
||||
FActorSpawnParameters Params;
|
||||
Params.Name = FName(FString::Printf(TEXT("Pooled_%s"), *Class->GetName()));
|
||||
Params.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
AActor* Ret = GetWorld()->SpawnActor(Class, &Location, &Rotation, Params);
|
||||
#if WITH_EDITOR
|
||||
Ret->SetActorLabel(Ret->GetName());
|
||||
#endif
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::DisableActor(AActor* Actor)
|
||||
{
|
||||
if (Actor)
|
||||
{
|
||||
Actor->SetActorHiddenInGame(true);
|
||||
if (auto Prim = Cast<UPrimitiveComponent>(Actor->GetRootComponent()))
|
||||
{
|
||||
Prim->SetSimulatePhysics(false);
|
||||
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3
|
||||
Prim->ResetSceneVelocity();
|
||||
#endif
|
||||
}
|
||||
Actor->SetActorLocation(StorageLocation, false, nullptr, ETeleportType::ResetPhysics);
|
||||
|
||||
if (Actor->Implements<UStevesPooledActor>())
|
||||
{
|
||||
IStevesPooledActor::Execute_DeactivateOnAddedToPool(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::ReviveActor(AActor* Actor, const FVector& Location, const FRotator& Rotator)
|
||||
{
|
||||
if (Actor)
|
||||
{
|
||||
Actor->SetActorHiddenInGame(false);
|
||||
// We don't enable physics, because caller may not want that.
|
||||
Actor->SetActorLocation(Location, false, nullptr, ETeleportType::ResetPhysics);
|
||||
Actor->SetActorRotation(Rotator, ETeleportType::ResetPhysics);
|
||||
|
||||
if (Actor->Implements<UStevesPooledActor>())
|
||||
{
|
||||
IStevesPooledActor::Execute_ReactivateOnRemovedFromPool(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::DrainActorPool(TSubclassOf<AActor> ActorClass, int NumberToKeep)
|
||||
{
|
||||
UClass* Class = ActorClass->GetAuthoritativeClass();
|
||||
if (auto pPool = GetPool(Class, false))
|
||||
{
|
||||
DrainPool(pPool, NumberToKeep);
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::DrainPool(TDeque<TObjectPtr<AActor>>* pPool, int NumberToKeep)
|
||||
{
|
||||
while (pPool->Num() > NumberToKeep)
|
||||
{
|
||||
TObjectPtr<AActor> Actor = nullptr;
|
||||
if(pPool->TryPopLast(Actor))
|
||||
{
|
||||
Actor->Destroy();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Should never happen but just in case, prevent infinite loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::DrainAllActorPools()
|
||||
{
|
||||
for (auto& Pair : Pools)
|
||||
{
|
||||
DrainPool(&Pair.Value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesPooledActorSystem::Deinitialize()
|
||||
{
|
||||
Super::Deinitialize();
|
||||
|
||||
DrainAllActorPools();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright Steve Streeting
|
||||
// Licensed under the MIT License (see License.txt)
|
||||
#include "StevesReplicatedPhysicsActor.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
|
||||
AStevesReplicatedPhysicsActor::AStevesReplicatedPhysicsActor(const FObjectInitializer& ObjInit)
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
bReplicates = true;
|
||||
bStaticMeshReplicateMovement = true;
|
||||
|
||||
const auto MeshComp = GetStaticMeshComponent();
|
||||
MeshComp->SetMobility(EComponentMobility::Movable);
|
||||
MeshComp->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
// I think this is actually not needed, since on clients the role is ROLE_SimulatedProxy not ROLE_AutonomousProxy
|
||||
// When using ROLE_SimulatedProxy physics is replicated all the time
|
||||
MeshComp->bReplicatePhysicsToAutonomousProxy = true;
|
||||
|
||||
// The default MinNetUpdateFrequency of 2 is too slow at responding to woken physics objects
|
||||
#if ENGINE_MINOR_VERSION >= 5
|
||||
SetMinNetUpdateFrequency(10);
|
||||
#else
|
||||
MinNetUpdateFrequency = 10;
|
||||
#endif
|
||||
|
||||
// We do NOT replicate MeshComp itself! That's expensive and unnecessary
|
||||
|
||||
}
|
||||
|
||||
void AStevesReplicatedPhysicsActor::BeginPlay()
|
||||
{
|
||||
SetReplicateMovement(true);
|
||||
|
||||
const auto MeshComp = GetStaticMeshComponent();
|
||||
|
||||
// Server test, includes dedicated and listen servers
|
||||
if (IsServer())
|
||||
{
|
||||
// Server collision. Block all but allow pawns through
|
||||
// Subclasses can change this if they like
|
||||
MeshComp->SetCollisionResponseToAllChannels(ECR_Block);
|
||||
MeshComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
MeshComp->SetSimulatePhysics(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're on a client so will be receiving replicated physics
|
||||
// Ignore all collisions; if we don't do this, things jank because of conflicting collisions
|
||||
MeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
|
||||
// We DON'T disable physics. Physics still needs to be enabled for replicated physics to be respected.
|
||||
// Otherwise objects will not move on the client.
|
||||
|
||||
// There's one case where this is problematic: replicated physics actors that are already asleep
|
||||
// once a new client joins a game in-progress will locally simulate themselves through the floor, because no further
|
||||
// updates will be received about their physics from the server.
|
||||
// My solution is to start all objects asleep on the client; if they're not, the server will send updates and
|
||||
// the objects will wake up. If they're asleep on the server, we're good.
|
||||
MeshComp->PutAllRigidBodiesToSleep();
|
||||
}
|
||||
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
bool AStevesReplicatedPhysicsActor::IsServer() const
|
||||
{
|
||||
// Server test, includes dedicated and listen servers
|
||||
// I prefer this to Authority test because you can't replicate objects client->server or client->client anyway
|
||||
return GetWorld()->GetNetMode() < NM_Client;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesSpringArmComponent.h"
|
||||
#include "VisualLogger/VisualLogger.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
|
||||
|
||||
|
||||
UStevesSpringArmComponent::UStevesSpringArmComponent(): bEnableSmoothCollisionAvoidance(1)
|
||||
{
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::UpdateDesiredArmLocation(bool bDoTrace,
|
||||
bool bDoLocationLag,
|
||||
bool bDoRotationLag,
|
||||
float DeltaTime)
|
||||
{
|
||||
// Smooth target & socket offsets
|
||||
if (SmoothTargetOffsetTarget.IsSet())
|
||||
{
|
||||
TargetOffset = FMath::VInterpTo(TargetOffset, SmoothTargetOffsetTarget.GetValue(), DeltaTime, SmoothTargetOffsetSpeed);
|
||||
if (TargetOffset.Equals(SmoothTargetOffsetTarget.GetValue()))
|
||||
{
|
||||
CancelTargetOffsetSmooth();
|
||||
}
|
||||
}
|
||||
if (SmoothSocketOffsetTarget.IsSet())
|
||||
{
|
||||
SocketOffset = FMath::VInterpTo(SocketOffset, SmoothSocketOffsetTarget.GetValue(), DeltaTime, SmoothSocketOffsetSpeed);
|
||||
if (SocketOffset.Equals(SmoothSocketOffsetTarget.GetValue()))
|
||||
{
|
||||
CancelSocketOffsetSmooth();
|
||||
}
|
||||
}
|
||||
Super::UpdateDesiredArmLocation(bDoTrace, bDoLocationLag, bDoRotationLag, DeltaTime);
|
||||
}
|
||||
|
||||
FVector UStevesSpringArmComponent::BlendLocations(const FVector& DesiredArmLocation,
|
||||
const FVector& TraceHitLocation,
|
||||
bool bHitSomething,
|
||||
float DeltaTime)
|
||||
{
|
||||
|
||||
#if WITH_EDITORONLY_DATA && ENABLE_VISUAL_LOG
|
||||
if (bHitSomething && bVisualLogCameraCollision)
|
||||
{
|
||||
FVector NewDesiredLoc = PreviousDesiredLoc - PreviousDesiredRot.Vector() * TargetArmLength;
|
||||
// Add socket offset in local space
|
||||
NewDesiredLoc += FRotationMatrix(PreviousDesiredRot).TransformVector(SocketOffset);
|
||||
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(SpringArm), false, GetOwner());
|
||||
FHitResult Result;
|
||||
if (GetWorld()->SweepSingleByChannel(Result, PreviousArmOrigin, NewDesiredLoc, FQuat::Identity, ProbeChannel, FCollisionShape::MakeSphere(ProbeSize), QueryParams))
|
||||
{
|
||||
UE_VLOG_ARROW(this, LogStevesUEHelpers, Log, PreviousArmOrigin, Result.Location, FColor::Cyan, TEXT(""));
|
||||
UE_VLOG_LOCATION(this, LogStevesUEHelpers, Log, Result.Location, ProbeSize, FColor::Cyan, TEXT("%s"), *Result.GetActor()->GetActorNameOrLabel());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// These locations are in world space, we only want to blend the arm length, not the rest
|
||||
const FVector Base = Super::BlendLocations(DesiredArmLocation, TraceHitLocation, bHitSomething, DeltaTime);
|
||||
|
||||
if (bEnableSmoothCollisionAvoidance)
|
||||
{
|
||||
// Convert these back to arm lengths
|
||||
// PreviousArmOrigin has been already set and is the world space origin
|
||||
const float TargetArmLen = (Base - PreviousArmOrigin).Length();
|
||||
|
||||
const float NewArmLen = FMath::FInterpTo(PrevArmLength.Get(TargetArmLen), TargetArmLen, DeltaTime, SmoothCollisionAvoidanceSpeed);
|
||||
|
||||
// Perform the same transformation again using the new arm length
|
||||
// Now offset camera position back along our rotation
|
||||
FVector Ret = PreviousDesiredLoc - PreviousDesiredRot.Vector() * NewArmLen;
|
||||
// Add socket offset in local space
|
||||
Ret += FRotationMatrix(PreviousDesiredRot).TransformVector(SocketOffset);
|
||||
|
||||
PrevArmLength = NewArmLen;
|
||||
|
||||
return Ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Base;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::SetTargetOffsetSmooth(const FVector& NewTargetOffset, float Speed)
|
||||
{
|
||||
SmoothTargetOffsetTarget = NewTargetOffset;
|
||||
SmoothTargetOffsetSpeed = Speed;
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::CancelTargetOffsetSmooth()
|
||||
{
|
||||
SmoothTargetOffsetTarget.Reset();
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::SetSocketOffsetSmooth(const FVector& NewSocketOffset, float Speed)
|
||||
{
|
||||
SmoothSocketOffsetTarget = NewSocketOffset;
|
||||
SmoothSocketOffsetSpeed = Speed;
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::CancelSocketOffsetSmooth()
|
||||
{
|
||||
SmoothSocketOffsetTarget.Reset();
|
||||
}
|
||||
|
||||
void UStevesSpringArmComponent::JumpToDesiredLocation()
|
||||
{
|
||||
CancelTargetOffsetSmooth();
|
||||
CancelSocketOffsetSmooth();
|
||||
UpdateDesiredArmLocation(false, false, false, 0);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesTextureRenderTargetPool.h"
|
||||
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Kismet/KismetRenderingLibrary.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
FStevesTextureRenderTargetReservation::~FStevesTextureRenderTargetReservation()
|
||||
{
|
||||
//UE_LOG(LogStevesUEHelpers, Log, TEXT("FStevesTextureRenderTargetReservation: destruction"));
|
||||
if (ParentPool.IsValid() && Texture.IsValid())
|
||||
{
|
||||
ParentPool.Pin()->ReleaseReservation(Texture.Get());
|
||||
Texture = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void FStevesTextureRenderTargetPool::ReleaseReservation(UTextureRenderTarget2D* Tex)
|
||||
{
|
||||
if (!Tex)
|
||||
{
|
||||
UE_LOG(LogStevesUEHelpers, Warning, TEXT("FStevesTextureRenderTargetPool: Attempted to release a null texture"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < Reservations.Num(); ++i)
|
||||
{
|
||||
const FReservationInfo& R = Reservations[i];
|
||||
if (R.Texture.IsValid() && R.Texture.Get() == Tex)
|
||||
{
|
||||
UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Released texture reservation on %s"), *Tex->GetName());
|
||||
UnreservedTextures.Add(R.Key, Tex);
|
||||
Reservations.RemoveAtSwap(i);
|
||||
ReservedTextures.Remove(Tex);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogStevesUEHelpers, Warning, TEXT("FStevesTextureRenderTargetPool: Attempted to release a reservation on %s that was not found"), *Tex->GetName());
|
||||
|
||||
}
|
||||
|
||||
FStevesTextureRenderTargetPool::~FStevesTextureRenderTargetPool()
|
||||
{
|
||||
DrainPool(true);
|
||||
}
|
||||
|
||||
void FStevesTextureRenderTargetPool::AddReferencedObjects(FReferenceCollector& Collector)
|
||||
{
|
||||
// We need to hold on to the texture references
|
||||
Collector.AddReferencedObjects(ReservedTextures);
|
||||
Collector.AddReferencedObjects(UnreservedTextures);
|
||||
}
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5
|
||||
FString FStevesTextureRenderTargetPool::GetReferencerName() const
|
||||
{
|
||||
return "FStevesTextureRenderTargetPool";
|
||||
}
|
||||
#endif
|
||||
|
||||
FStevesTextureRenderTargetReservationPtr FStevesTextureRenderTargetPool::ReserveTexture(FIntPoint Size,
|
||||
ETextureRenderTargetFormat Format, const UObject* Owner)
|
||||
{
|
||||
const FTextureKey Key {Size, Format};
|
||||
UTextureRenderTarget2D* Tex = nullptr;
|
||||
if (auto Pooled = UnreservedTextures.Find(Key))
|
||||
{
|
||||
Tex = *Pooled;
|
||||
UnreservedTextures.RemoveSingle(Key, Tex);
|
||||
UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Re-used pooled texture %s"), *Tex->GetName());
|
||||
}
|
||||
else if (Size.X > 0 && Size.Y > 0)
|
||||
{
|
||||
// No existing texture, so create
|
||||
// Texture owner should be a valid UObject that will determine lifespan
|
||||
UObject* TextureOwner = PoolOwner.IsValid() ? PoolOwner.Get() : GetTransientPackage();
|
||||
Tex = NewObject<UTextureRenderTarget2D>(TextureOwner);
|
||||
Tex->RenderTargetFormat = Format;
|
||||
Tex->InitAutoFormat(Size.X, Size.Y);
|
||||
Tex->UpdateResourceImmediate(true);
|
||||
|
||||
UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Created new texture %s"), *Tex->GetName());
|
||||
}
|
||||
|
||||
// Record reservation
|
||||
Reservations.Add(FReservationInfo(Key, Owner, Tex));
|
||||
|
||||
// Reservation doesn't keep the texture alive; if caller doesn't hold a strong pointer to it, it'll be destroyed
|
||||
// So we need to hold it ourselves
|
||||
ReservedTextures.Add(Tex);
|
||||
|
||||
return MakeShared<FStevesTextureRenderTargetReservation>(Tex, this->AsShared(), Owner);
|
||||
}
|
||||
|
||||
void FStevesTextureRenderTargetPool::RevokeReservations(const UObject* ForOwner)
|
||||
{
|
||||
for (int i = 0; i < Reservations.Num(); ++i)
|
||||
{
|
||||
const FReservationInfo& R = Reservations[i];
|
||||
if (!ForOwner || R.Owner == ForOwner)
|
||||
{
|
||||
if (R.Texture.IsValid())
|
||||
{
|
||||
UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Revoked texture reservation on %s"), *R.Texture->GetName());
|
||||
UnreservedTextures.Add(R.Key, R.Texture.Get());
|
||||
ReservedTextures.Remove(R.Texture.Get());
|
||||
}
|
||||
// Can't use RemoveAtSwap because it'll change order
|
||||
Reservations.RemoveAt(i);
|
||||
// Adjust index backwards to compensate
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FStevesTextureRenderTargetPool::DrainPool(bool bForceAndRevokeReservations)
|
||||
{
|
||||
if (bForceAndRevokeReservations)
|
||||
RevokeReservations();
|
||||
|
||||
for (auto& TexPair : UnreservedTextures)
|
||||
{
|
||||
UKismetRenderingLibrary::ReleaseRenderTarget2D(TexPair.Value);
|
||||
}
|
||||
UnreservedTextures.Empty();
|
||||
ReservedTextures.Empty();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUEHelpers.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FStevesUEHelpers"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogStevesUEHelpers)
|
||||
|
||||
void FStevesUEHelpers::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
UE_LOG(LogStevesUEHelpers, Log, TEXT("Steve's UE Helpers Module Started"))
|
||||
}
|
||||
|
||||
void FStevesUEHelpers::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
UE_LOG(LogStevesUEHelpers, Log, TEXT("Steve's UE Helpers Module Stopped"))
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FStevesUEHelpers, StevesUEHelpers)
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/FocusSystem.h"
|
||||
#include "StevesUI/FocusableUserWidget.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogFocusSystem)
|
||||
|
||||
TWeakObjectPtr<UFocusableUserWidget> FFocusSystem::GetHighestFocusPriority()
|
||||
{
|
||||
int Highest = -999;
|
||||
TWeakObjectPtr<UFocusableUserWidget> Ret;
|
||||
|
||||
for (auto && S : ActiveAutoFocusWidgets)
|
||||
{
|
||||
if (S.IsValid() && S->IsRequestingFocus() && S->GetAutomaticFocusPriority() > Highest)
|
||||
{
|
||||
Highest = S->GetAutomaticFocusPriority();
|
||||
Ret = S;
|
||||
}
|
||||
}
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void FFocusSystem::FocusableWidgetConstructed(UFocusableUserWidget* Widget)
|
||||
{
|
||||
UE_LOG(LogFocusSystem, Display, TEXT("FocusableUserWidget %s opened"), *Widget->GetName());
|
||||
// check to make sure we never dupe, shouldn't normally be a problem
|
||||
// but let's just be safe, there will never be that many
|
||||
bool bPresent = false;
|
||||
for (auto && S : ActiveAutoFocusWidgets)
|
||||
{
|
||||
if (S.Get() == Widget)
|
||||
{
|
||||
bPresent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bPresent)
|
||||
ActiveAutoFocusWidgets.Add(Widget);
|
||||
|
||||
if (Widget->IsRequestingFocus())
|
||||
{
|
||||
auto Highest = GetHighestFocusPriority();
|
||||
if (!Highest.IsValid() || Highest->GetAutomaticFocusPriority() <= Widget->GetAutomaticFocusPriority())
|
||||
{
|
||||
// give new stack the focus if it's equal or higher priority than anything else
|
||||
UE_LOG(LogFocusSystem, Display, TEXT("Giving focus to %s"), *Widget->GetName());
|
||||
Widget->TakeFocusIfDesired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FFocusSystem::FocusableWidgetDestructed(UFocusableUserWidget* Widget)
|
||||
{
|
||||
UE_LOG(LogFocusSystem, Display, TEXT("FocusableUserWidget %s closed"), *Widget->GetName());
|
||||
|
||||
for (int i = 0; i < ActiveAutoFocusWidgets.Num(); ++i)
|
||||
{
|
||||
if (ActiveAutoFocusWidgets[i].Get() == Widget)
|
||||
{
|
||||
ActiveAutoFocusWidgets.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if the menu closing had focus, give it to the highest remaining stack
|
||||
if (Widget->HasFocusedDescendants())
|
||||
{
|
||||
auto Highest = GetHighestFocusPriority();
|
||||
// Make sure player controller is valid too, this could be on shutdown
|
||||
if (Highest.IsValid() && Highest->GetOwningPlayer())
|
||||
{
|
||||
UE_LOG(LogFocusSystem, Display, TEXT("Giving focus to %s"), *Highest->GetName());
|
||||
Highest->TakeFocusIfDesired();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/FocusableButton.h"
|
||||
#include "StevesUI/SFocusableButton.h"
|
||||
#include "Components/ButtonSlot.h"
|
||||
#include "Framework/Application/NavigationConfig.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
|
||||
|
||||
UFocusableButton::UFocusableButton(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UFocusableButton::RebuildWidget()
|
||||
{
|
||||
MyButton = SNew(SFocusableButton)
|
||||
// This part is standard button behaviour
|
||||
.OnClicked(BIND_UOBJECT_DELEGATE(FOnClicked, SlateHandleClicked))
|
||||
.OnPressed(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandlePressed))
|
||||
.OnReleased(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleased))
|
||||
.OnHovered_UObject( this, &ThisClass::SlateHandleHovered )
|
||||
.OnUnhovered_UObject( this, &ThisClass::SlateHandleUnhovered )
|
||||
.ButtonStyle(&GetStyle())
|
||||
.ClickMethod(GetClickMethod())
|
||||
.TouchMethod(GetTouchMethod())
|
||||
.PressMethod(GetPressMethod())
|
||||
.IsFocusable(GetIsFocusable())
|
||||
// Our new events
|
||||
.OnFocusReceived(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusReceived))
|
||||
.OnFocusLost(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusLost))
|
||||
;
|
||||
|
||||
if ( GetChildrenCount() > 0 )
|
||||
{
|
||||
Cast<UButtonSlot>(GetContentSlot())->BuildSlot(MyButton.ToSharedRef());
|
||||
}
|
||||
|
||||
RefreshFocussedStyle();
|
||||
|
||||
return MyButton.ToSharedRef();
|
||||
}
|
||||
|
||||
void UFocusableButton::RefreshFocussedStyle_Implementation()
|
||||
{
|
||||
// Copy Widget style but make normal same as hovered
|
||||
FocussedStyle = GetStyle();
|
||||
FocussedStyle.Normal = FocussedStyle.Hovered;
|
||||
}
|
||||
|
||||
void UFocusableButton::SimulatePress()
|
||||
{
|
||||
if (MyButton)
|
||||
{
|
||||
// In order to get the visual change we need to call SButton::Press() and SButton::Release(), but they're both private
|
||||
// The only public method we can use to simulate the click properly is OnKeyDown/Up or OnMouseButtonDown/Up
|
||||
// Use Virtual_Accept since that is general
|
||||
FKeyEvent KeyEvent(
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
EKeys::Virtual_Gamepad_Accept.GetVirtualKey(),
|
||||
#else
|
||||
EKeys::Virtual_Accept,
|
||||
#endif
|
||||
FModifierKeysState(), 0, false, 0, 0);
|
||||
MyButton->OnKeyDown(MyButton->GetCachedGeometry(), KeyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableButton::SimulateRelease()
|
||||
{
|
||||
if (MyButton)
|
||||
{
|
||||
// In order to get the visual change we need to call SButton::Press() and SButton::Release(), but they're both private
|
||||
// The only public method we can use to simulate the click properly is OnKeyDown/Up or OnMouseButtonDown/Up
|
||||
// Use Virtual_Accept since that is general
|
||||
FKeyEvent KeyEvent(
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
EKeys::Virtual_Gamepad_Accept.GetVirtualKey(),
|
||||
#else
|
||||
EKeys::Virtual_Accept,
|
||||
#endif
|
||||
FModifierKeysState(), 0, false, 0, 0);
|
||||
MyButton->OnKeyUp(MyButton->GetCachedGeometry(), KeyEvent);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableButton::SlateHandleFocusReceived()
|
||||
{
|
||||
ApplyFocusStyle();
|
||||
OnFocusReceived.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableButton::SlateHandleFocusLost()
|
||||
{
|
||||
UndoFocusStyle();
|
||||
OnFocusLost.Broadcast();
|
||||
}
|
||||
|
||||
|
||||
void UFocusableButton::ApplyFocusStyle()
|
||||
{
|
||||
if (MyButton.IsValid() && bUseHoverStyleWhenFocussed)
|
||||
{
|
||||
MyButton->SetButtonStyle(&FocussedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableButton::UndoFocusStyle()
|
||||
{
|
||||
if (MyButton.IsValid())
|
||||
{
|
||||
MyButton->SetButtonStyle(&GetStyle());
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableButton::SlateHandleHovered()
|
||||
{
|
||||
if (bTakeFocusOnHover)
|
||||
{
|
||||
SetFocus();
|
||||
}
|
||||
OnHovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableButton::SlateHandleUnhovered()
|
||||
{
|
||||
if (bLoseFocusOnUnhover)
|
||||
{
|
||||
Unfocus();
|
||||
}
|
||||
OnUnhovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableButton::Unfocus() const
|
||||
{
|
||||
APlayerController* OwningPlayer = GetOwningPlayer();
|
||||
if (OwningPlayer == nullptr || !OwningPlayer->IsLocalController() || OwningPlayer->Player ==
|
||||
nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ULocalPlayer* LocalPlayer = OwningPlayer->GetLocalPlayer())
|
||||
{
|
||||
TOptional<int32> UserIndex = FSlateApplication::Get().GetUserIndexForController(
|
||||
LocalPlayer->GetControllerId());
|
||||
if (UserIndex.IsSet())
|
||||
{
|
||||
TSharedPtr<SWidget> SafeWidget = GetCachedWidget();
|
||||
if (SafeWidget.IsValid())
|
||||
{
|
||||
if (SafeWidget->HasUserFocus(UserIndex.GetValue()))
|
||||
{
|
||||
FSlateApplication::Get().ClearUserFocus(UserIndex.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/FocusableCheckBox.h"
|
||||
#include "StevesUI/SFocusableCheckBox.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
|
||||
TSharedRef<SWidget> UFocusableCheckBox::RebuildWidget()
|
||||
{
|
||||
MyCheckbox = SNew(SFocusableCheckBox)
|
||||
.OnCheckStateChanged( BIND_UOBJECT_DELEGATE(FOnCheckStateChanged, SlateOnCheckStateChangedCallback) )
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
.Style(&GetWidgetStyle())
|
||||
.ClickMethod(GetClickMethod())
|
||||
.TouchMethod(GetTouchMethod())
|
||||
.PressMethod(GetPressMethod())
|
||||
#else
|
||||
.Style(&WidgetStyle)
|
||||
.ClickMethod(ClickMethod)
|
||||
.TouchMethod(TouchMethod)
|
||||
.PressMethod(PressMethod)
|
||||
#endif
|
||||
.HAlign( HorizontalAlignment )
|
||||
.IsFocusable(GetIsFocusable())
|
||||
// Our new events
|
||||
.OnHovered(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleHovered))
|
||||
.OnUnhovered(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleUnhovered))
|
||||
.OnFocusReceived(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusReceived))
|
||||
.OnFocusLost(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusLost))
|
||||
;
|
||||
|
||||
if ( GetChildrenCount() > 0 )
|
||||
{
|
||||
MyCheckbox->SetContent(GetContentSlot()->Content ? GetContentSlot()->Content->TakeWidget() : SNullWidget::NullWidget);
|
||||
}
|
||||
|
||||
RefreshFocussedStyle();
|
||||
|
||||
|
||||
return MyCheckbox.ToSharedRef();
|
||||
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::RefreshFocussedStyle_Implementation()
|
||||
{
|
||||
// Copy Widget style but make normal same as hovered
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
FocussedStyle = GetWidgetStyle();
|
||||
#else
|
||||
FocussedStyle = WidgetStyle;
|
||||
#endif
|
||||
FocussedStyle.UncheckedImage = FocussedStyle.UncheckedHoveredImage;
|
||||
FocussedStyle.CheckedImage = FocussedStyle.CheckedHoveredImage;
|
||||
FocussedStyle.UndeterminedImage = FocussedStyle.UndeterminedHoveredImage;
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::SlateHandleFocusReceived()
|
||||
{
|
||||
ApplyFocusStyle();
|
||||
OnFocusReceived.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::SlateHandleFocusLost()
|
||||
{
|
||||
UndoFocusStyle();
|
||||
OnFocusLost.Broadcast();
|
||||
}
|
||||
|
||||
|
||||
void UFocusableCheckBox::ApplyFocusStyle()
|
||||
{
|
||||
if (MyCheckbox.IsValid() && bUseHoverStyleWhenFocussed)
|
||||
{
|
||||
MyCheckbox->SetStyle(&FocussedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::UndoFocusStyle()
|
||||
{
|
||||
if (MyCheckbox.IsValid())
|
||||
{
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
MyCheckbox->SetStyle(&GetWidgetStyle());
|
||||
#else
|
||||
MyCheckbox->SetStyle(&WidgetStyle);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::SlateHandleHovered()
|
||||
{
|
||||
if (bTakeFocusOnHover)
|
||||
{
|
||||
SetFocus();
|
||||
}
|
||||
OnHovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::SlateHandleUnhovered()
|
||||
{
|
||||
if (bLoseFocusOnUnhover)
|
||||
{
|
||||
Unfocus();
|
||||
}
|
||||
OnUnhovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableCheckBox::Unfocus() const
|
||||
{
|
||||
APlayerController* OwningPlayer = GetOwningPlayer();
|
||||
if (OwningPlayer == nullptr || !OwningPlayer->IsLocalController() || OwningPlayer->Player ==
|
||||
nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ULocalPlayer* LocalPlayer = OwningPlayer->GetLocalPlayer())
|
||||
{
|
||||
TOptional<int32> UserIndex = FSlateApplication::Get().GetUserIndexForController(
|
||||
LocalPlayer->GetControllerId());
|
||||
if (UserIndex.IsSet())
|
||||
{
|
||||
TSharedPtr<SWidget> SafeWidget = GetCachedWidget();
|
||||
if (SafeWidget.IsValid())
|
||||
{
|
||||
if (SafeWidget->HasUserFocus(UserIndex.GetValue()))
|
||||
{
|
||||
FSlateApplication::Get().ClearUserFocus(UserIndex.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/FocusablePanel.h"
|
||||
|
||||
|
||||
#include "StevesUI.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
void UFocusablePanel::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
// Find focus widget
|
||||
|
||||
InitialFocusWidget = GetInitialFocusWidget();
|
||||
#if WITH_EDITOR
|
||||
if (!InitialFocusWidget.IsValid())
|
||||
{
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Initial focus widget `%s` not found on %s, focus will be lost"), *InitialFocusWidgetName.ToString(), *GetName())
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void UFocusablePanel::NativeDestruct()
|
||||
{
|
||||
Super::NativeDestruct();
|
||||
|
||||
InitialFocusWidget.Reset();
|
||||
}
|
||||
|
||||
bool UFocusablePanel::SetFocusToInitialWidget()
|
||||
{
|
||||
if (!InitialFocusWidget.IsValid())
|
||||
{
|
||||
// if not set, attempt to get
|
||||
InitialFocusWidget = GetInitialFocusWidget();
|
||||
}
|
||||
if (InitialFocusWidget.IsValid())
|
||||
{
|
||||
SetWidgetFocusProperly(InitialFocusWidget.Get());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UWidget* UFocusablePanel::GetInitialFocusWidget_Implementation()
|
||||
{
|
||||
if (!InitialFocusWidgetName.IsNone())
|
||||
{
|
||||
return WidgetTree->FindWidget(InitialFocusWidgetName);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
void UFocusablePanel::SetInitialFocusWidget(UWidget* NewInitialFocus)
|
||||
{
|
||||
if (NewInitialFocus)
|
||||
{
|
||||
InitialFocusWidgetName = NewInitialFocus->GetFName();
|
||||
}
|
||||
}
|
||||
|
||||
bool UFocusablePanel::RestorePreviousFocus() const
|
||||
{
|
||||
if (PreviousFocusWidget.IsValid())
|
||||
{
|
||||
SetWidgetFocusProperly(PreviousFocusWidget.Get());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UFocusablePanel::SavePreviousFocus()
|
||||
{
|
||||
const auto SW = FSlateApplication::Get().GetUserFocusedWidget(0);
|
||||
if (SW)
|
||||
{
|
||||
PreviousFocusWidget = FindWidgetFromSlate(SW.Get(), this);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviousFocusWidget.Reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusablePanel::SetFocusProperly_Implementation()
|
||||
{
|
||||
if (!RestorePreviousFocus())
|
||||
SetFocusToInitialWidget();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#include "StevesUI/FocusableSlider.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
|
||||
#include "SFocusableSlider.h"
|
||||
|
||||
TSharedRef<SWidget> UFocusableSlider::RebuildWidget()
|
||||
{
|
||||
MySlider = SNew(SFocusableSlider)
|
||||
.Style(&GetWidgetStyle())
|
||||
.OnMouseCaptureBegin(BIND_UOBJECT_DELEGATE(FSimpleDelegate, HandleOnMouseCaptureBegin))
|
||||
.OnMouseCaptureEnd(BIND_UOBJECT_DELEGATE(FSimpleDelegate, HandleOnMouseCaptureEnd))
|
||||
.OnControllerCaptureBegin(BIND_UOBJECT_DELEGATE(FSimpleDelegate, HandleOnControllerCaptureBegin))
|
||||
.OnControllerCaptureEnd(BIND_UOBJECT_DELEGATE(FSimpleDelegate, HandleOnControllerCaptureEnd))
|
||||
.OnValueChanged(BIND_UOBJECT_DELEGATE(FOnFloatValueChanged, HandleOnValueChanged))
|
||||
// Our new events
|
||||
.OnHovered(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleHovered))
|
||||
.OnUnhovered(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleUnhovered))
|
||||
.OnFocusReceived(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusReceived))
|
||||
.OnFocusLost(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleFocusLost))
|
||||
;
|
||||
|
||||
RefreshFocussedStyle();
|
||||
|
||||
|
||||
return MySlider.ToSharedRef();
|
||||
|
||||
}
|
||||
|
||||
void UFocusableSlider::RefreshFocussedStyle_Implementation()
|
||||
{
|
||||
// Copy Widget style but make normal same as hovered
|
||||
FocussedStyle = GetWidgetStyle();
|
||||
FocussedStyle.NormalBarImage = FocussedStyle.HoveredBarImage;
|
||||
FocussedStyle.NormalThumbImage = FocussedStyle.HoveredThumbImage;
|
||||
}
|
||||
|
||||
void UFocusableSlider::SlateHandleFocusReceived()
|
||||
{
|
||||
ApplyFocusStyle();
|
||||
OnFocusReceived.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableSlider::SlateHandleFocusLost()
|
||||
{
|
||||
UndoFocusStyle();
|
||||
OnFocusLost.Broadcast();
|
||||
}
|
||||
|
||||
|
||||
void UFocusableSlider::ApplyFocusStyle()
|
||||
{
|
||||
if (MySlider.IsValid() && bUseHoverStyleWhenFocussed)
|
||||
{
|
||||
MySlider->SetStyle(&FocussedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableSlider::UndoFocusStyle()
|
||||
{
|
||||
if (MySlider.IsValid())
|
||||
{
|
||||
MySlider->SetStyle(&GetWidgetStyle());
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableSlider::SlateHandleHovered()
|
||||
{
|
||||
if (bTakeFocusOnHover)
|
||||
{
|
||||
SetFocus();
|
||||
}
|
||||
OnHovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableSlider::SlateHandleUnhovered()
|
||||
{
|
||||
if (bLoseFocusOnUnhover)
|
||||
{
|
||||
Unfocus();
|
||||
}
|
||||
OnUnhovered.Broadcast();
|
||||
}
|
||||
|
||||
void UFocusableSlider::Unfocus() const
|
||||
{
|
||||
APlayerController* OwningPlayer = GetOwningPlayer();
|
||||
if (OwningPlayer == nullptr || !OwningPlayer->IsLocalController() || OwningPlayer->Player ==
|
||||
nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ULocalPlayer* LocalPlayer = OwningPlayer->GetLocalPlayer())
|
||||
{
|
||||
TOptional<int32> UserIndex = FSlateApplication::Get().GetUserIndexForController(
|
||||
LocalPlayer->GetControllerId());
|
||||
if (UserIndex.IsSet())
|
||||
{
|
||||
TSharedPtr<SWidget> SafeWidget = GetCachedWidget();
|
||||
if (SafeWidget.IsValid())
|
||||
{
|
||||
if (SafeWidget->HasUserFocus(UserIndex.GetValue()))
|
||||
{
|
||||
FSlateApplication::Get().ClearUserFocus(UserIndex.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/FocusableUserWidget.h"
|
||||
|
||||
#include "StevesUEHelpers.h"
|
||||
|
||||
void UFocusableUserWidget::SetFocusProperly_Implementation()
|
||||
{
|
||||
// Default is to call normal
|
||||
SetFocus();
|
||||
}
|
||||
|
||||
|
||||
bool UFocusableUserWidget::IsRequestingFocus_Implementation() const
|
||||
{
|
||||
// Subclasses can override this
|
||||
return bEnableAutomaticFocus;
|
||||
}
|
||||
|
||||
bool UFocusableUserWidget::TakeFocusIfDesired_Implementation()
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (IsRequestingFocus() &&
|
||||
GS && (GS->GetLastInputModeUsed() != EInputMode::Gamepad || GS->GetLastInputModeUsed() != EInputMode::Keyboard))
|
||||
{
|
||||
SetFocusProperly();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FReply UFocusableUserWidget::NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
FReply Reply = Super::NativeOnFocusReceived(InGeometry, InFocusEvent);
|
||||
|
||||
if (!Reply.IsEventHandled())
|
||||
{
|
||||
SetFocusProperly();
|
||||
return Reply.Handled();
|
||||
}
|
||||
|
||||
return Reply;
|
||||
}
|
||||
|
||||
void UFocusableUserWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (bEnableAutomaticFocus)
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
GS->GetFocusSystem()->FocusableWidgetConstructed(this);
|
||||
}
|
||||
}
|
||||
|
||||
void UFocusableUserWidget::NativeDestruct()
|
||||
{
|
||||
Super::NativeDestruct();
|
||||
|
||||
if (bEnableAutomaticFocus)
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
GS->GetFocusSystem()->FocusableWidgetDestructed(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/InputImage.h"
|
||||
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "StevesGameSubsystem.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "InputAction.h"
|
||||
|
||||
TSharedRef<SWidget> UInputImage::RebuildWidget()
|
||||
{
|
||||
auto Ret = Super::RebuildWidget();
|
||||
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS && !bSubbedToInputEvents)
|
||||
{
|
||||
bSubbedToInputEvents = true;
|
||||
GS->OnInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
|
||||
GS->OnButtonInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
|
||||
GS->OnAxisInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
|
||||
if (InputAction)
|
||||
{
|
||||
// Enhanced input now has a mappings rebuilt hook which we can use (Since July 2022)
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetOwningLocalPlayer()))
|
||||
{
|
||||
Subsystem->ControlMappingsRebuiltDelegate.AddUniqueDynamic(this, &UInputImage::OnEnhancedInputMappingsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateImage();
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void UInputImage::OnInputModeChanged(int ChangedPlayerIdx, EInputMode InputMode)
|
||||
{
|
||||
if (ChangedPlayerIdx == PlayerIndex)
|
||||
{
|
||||
// Delay update, in case multiple received in short succession
|
||||
MarkImageDirty();
|
||||
// auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
// UE_LOG(LogTemp, Warning, TEXT("Updating image for input mode change: %s Button device: %s"),
|
||||
// *UEnum::GetValueAsString(InputMode),
|
||||
// *UEnum::GetValueAsString(GS->GetLastInputButtonPressed(ChangedPlayerIdx)));
|
||||
}
|
||||
}
|
||||
|
||||
void UInputImage::OnEnhancedInputMappingsChanged()
|
||||
{
|
||||
MarkImageDirty();
|
||||
}
|
||||
|
||||
void UInputImage::SetCustomTheme(UUiTheme* Theme)
|
||||
{
|
||||
CustomTheme = Theme;
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
void UInputImage::BeginDestroy()
|
||||
{
|
||||
Super::BeginDestroy();
|
||||
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
GS->OnInputModeChanged.RemoveAll(this);
|
||||
GS->OnButtonInputModeChanged.RemoveAll(this);
|
||||
GS->OnAxisInputModeChanged.RemoveAll(this);
|
||||
}
|
||||
}
|
||||
|
||||
void UInputImage::SetVisibility(ESlateVisibility InVisibility)
|
||||
{
|
||||
Super::SetVisibility(InVisibility);
|
||||
|
||||
switch(InVisibility)
|
||||
{
|
||||
case ESlateVisibility::Collapsed:
|
||||
case ESlateVisibility::Hidden:
|
||||
break;
|
||||
default:
|
||||
case ESlateVisibility::Visible:
|
||||
case ESlateVisibility::HitTestInvisible:
|
||||
case ESlateVisibility::SelfHitTestInvisible:
|
||||
// Make sure we update when our visibility is changed
|
||||
UpdateImage();
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void UInputImage::SetFromAction(FName Name)
|
||||
{
|
||||
BindingType = EInputBindingType::Action;
|
||||
ActionOrAxisName = Name;
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
void UInputImage::SetFromAxis(FName Name)
|
||||
{
|
||||
BindingType = EInputBindingType::Axis;
|
||||
ActionOrAxisName = Name;
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
void UInputImage::SetFromKey(FKey K)
|
||||
{
|
||||
BindingType = EInputBindingType::Key;
|
||||
Key = K;
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
void UInputImage::SetFromInputAction(UInputAction* Action)
|
||||
{
|
||||
BindingType = EInputBindingType::EnhancedInputAction;
|
||||
InputAction = Action;
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
void UInputImage::UpdateImage()
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
UPaperSprite* Sprite = nullptr;
|
||||
if (BindingType == EInputBindingType::EnhancedInputAction && !InputAction.IsNull())
|
||||
{
|
||||
if (auto IA = InputAction.LoadSynchronous())
|
||||
{
|
||||
Sprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, DevicePreference, PlayerIndex, GetOwningPlayer(), CustomTheme);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex, CustomTheme);
|
||||
}
|
||||
|
||||
if (Sprite)
|
||||
{
|
||||
if (bHiddenBecauseBlank || bOverrideHiddenState)
|
||||
{
|
||||
// Use Internal so as not to recurse back here
|
||||
SetVisibilityInternal(OldVisibility);
|
||||
bHiddenBecauseBlank = false;
|
||||
}
|
||||
// Match size is needed incase size has changed
|
||||
// Need to make it update region in case inside a scale box or something else that needs to adjust
|
||||
SetBrushFromAtlasInterface(Sprite, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsVisible())
|
||||
{
|
||||
bHiddenBecauseBlank = true;
|
||||
OldVisibility = GetVisibility();
|
||||
// Use Internal so as not to recurse back here
|
||||
SetVisibilityInternal(ESlateVisibility::Hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
bIsDirty = false;
|
||||
DelayUpdate = 0;
|
||||
}
|
||||
|
||||
void UInputImage::MarkImageDirty()
|
||||
{
|
||||
if (UpdateDelay > 0)
|
||||
{
|
||||
bIsDirty = true;
|
||||
DelayUpdate = UpdateDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateImage();
|
||||
}
|
||||
}
|
||||
|
||||
// Tickables
|
||||
// We need a tick rather than FTimer because timers won't run when game is paused, and UIs are useful while paused!
|
||||
ETickableTickType UInputImage::GetTickableTickType() const
|
||||
{
|
||||
return ETickableTickType::Conditional;
|
||||
}
|
||||
|
||||
void UInputImage::FinishDestroy()
|
||||
{
|
||||
|
||||
// Enhanced input now has a mappings rebuilt hook which we can use (Since July 2022)
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetOwningLocalPlayer()))
|
||||
{
|
||||
Subsystem->ControlMappingsRebuiltDelegate.RemoveAll(this);
|
||||
}
|
||||
|
||||
Super::FinishDestroy();
|
||||
}
|
||||
|
||||
bool UInputImage::IsTickableWhenPaused() const
|
||||
{
|
||||
// UIs need to update while game is paused
|
||||
return true;
|
||||
}
|
||||
bool UInputImage::IsTickableInEditor() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UInputImage::IsTickable() const
|
||||
{
|
||||
// Only need to tick while dirty
|
||||
return bIsDirty;
|
||||
}
|
||||
|
||||
void UInputImage::Tick(float DeltaTime)
|
||||
{
|
||||
DelayUpdate -= DeltaTime;
|
||||
if (DelayUpdate <= 0)
|
||||
{
|
||||
UpdateImage();
|
||||
}
|
||||
}
|
||||
|
||||
TStatId UInputImage::GetStatId() const
|
||||
{
|
||||
RETURN_QUICK_DECLARE_CYCLE_STAT( UInputImage, STATGROUP_Tickables );
|
||||
}
|
||||
|
||||
// Tickables
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/MenuBase.h"
|
||||
|
||||
#include "StevesUI.h"
|
||||
#include "StevesGameSubsystem.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "StevesUI/MenuStack.h"
|
||||
#include "Components/ContentWidget.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
void UMenuBase::Close(bool bWasCancel)
|
||||
{
|
||||
// Deliberately raise before parent so stack is always last in event sequence
|
||||
OnClosed.Broadcast(this, bWasCancel);
|
||||
if (ParentStack.IsValid())
|
||||
{
|
||||
ParentStack->PopMenuIfTop(this, bWasCancel);
|
||||
} else
|
||||
{
|
||||
// standalone mode
|
||||
RemoveFromParent();
|
||||
PreviousFocusWidget.Reset();
|
||||
}
|
||||
AfterClosed.Broadcast(this, bWasCancel);
|
||||
}
|
||||
|
||||
void UMenuBase::AddedToStack(UMenuStack* Parent)
|
||||
{
|
||||
ParentStack = MakeWeakObjectPtr(Parent);
|
||||
|
||||
Open(false);
|
||||
OnAddedToStack(Parent);
|
||||
}
|
||||
|
||||
|
||||
void UMenuBase::InputModeChanged(EInputMode OldMode, EInputMode NewMode)
|
||||
{
|
||||
if (OldMode == EInputMode::Mouse &&
|
||||
(NewMode == EInputMode::Gamepad || NewMode == EInputMode::Keyboard))
|
||||
{
|
||||
if (bRequestFocus)
|
||||
SetFocusProperly();
|
||||
}
|
||||
else if ((OldMode == EInputMode::Gamepad || OldMode == EInputMode::Keyboard) &&
|
||||
NewMode == EInputMode::Mouse)
|
||||
{
|
||||
SavePreviousFocus();
|
||||
}
|
||||
}
|
||||
|
||||
bool UMenuBase::IsTopOfStack() const
|
||||
{
|
||||
if (ParentStack.IsValid())
|
||||
{
|
||||
return ParentStack->GetTopMenu() == this;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UMenuBase::RemovedFromStack(UMenuStack* Parent)
|
||||
{
|
||||
// This works whether embedded or not
|
||||
RemoveFromParent();
|
||||
PreviousFocusWidget.Reset();
|
||||
OnRemovedFromStack(Parent);
|
||||
}
|
||||
|
||||
void UMenuBase::SupercededInStack(UMenuBase* ByMenu)
|
||||
{
|
||||
SavePreviousFocus();
|
||||
|
||||
if (bEmbedInParentContainer)
|
||||
{
|
||||
if (bHideWhenSuperceded)
|
||||
RemoveFromParent();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bHideWhenSuperceded)
|
||||
SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
OnSupercededInStack(ByMenu);
|
||||
}
|
||||
|
||||
void UMenuBase::RegainedFocusInStack()
|
||||
{
|
||||
Open(true);
|
||||
OnRegainedFocusInStack();
|
||||
}
|
||||
|
||||
void UMenuBase::EmbedInParent()
|
||||
{
|
||||
if (ParentStack.IsValid() &&
|
||||
ParentStack->MenuContainer != nullptr)
|
||||
{
|
||||
ParentStack->MenuContainer->SetContent(this);
|
||||
}
|
||||
else
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Cannot embed %s in parent, missing container"), *this->GetName())
|
||||
|
||||
}
|
||||
|
||||
void UMenuBase::Open(bool bIsRegain)
|
||||
{
|
||||
if (ParentStack.IsValid() && bEmbedInParentContainer && bHideWhenSuperceded)
|
||||
EmbedInParent();
|
||||
else
|
||||
AddToViewport();
|
||||
SetVisibility(bBlockClicks ? ESlateVisibility::Visible : ESlateVisibility::SelfHitTestInvisible);
|
||||
|
||||
auto PC = GetOwningPlayer();
|
||||
switch (InputModeSetting)
|
||||
{
|
||||
default:
|
||||
case EInputModeChange::DoNotChange:
|
||||
break;
|
||||
case EInputModeChange::UIOnly:
|
||||
PC->SetInputMode(FInputModeUIOnly());
|
||||
break;
|
||||
case EInputModeChange::GameAndUI:
|
||||
PC->SetInputMode(FInputModeGameAndUI());
|
||||
break;
|
||||
case EInputModeChange::GameOnly:
|
||||
PC->SetInputMode(FInputModeGameOnly());
|
||||
break;
|
||||
}
|
||||
|
||||
switch (MousePointerVisibility)
|
||||
{
|
||||
default:
|
||||
case EMousePointerVisibilityChange::DoNotChange:
|
||||
break;
|
||||
case EMousePointerVisibilityChange::Visible:
|
||||
PC->bShowMouseCursor = true;
|
||||
break;
|
||||
case EMousePointerVisibilityChange::Hidden:
|
||||
PC->bShowMouseCursor = false;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (GamePauseSetting)
|
||||
{
|
||||
default:
|
||||
case EGamePauseChange::DoNotChange:
|
||||
break;
|
||||
case EGamePauseChange::Paused:
|
||||
UGameplayStatics::SetGamePaused(GetWorld(), true);
|
||||
break;
|
||||
case EGamePauseChange::Unpaused:
|
||||
UGameplayStatics::SetGamePaused(GetWorld(), false);
|
||||
break;
|
||||
}
|
||||
|
||||
TakeFocusIfDesired();
|
||||
|
||||
}
|
||||
|
||||
bool UMenuBase::RequestClose(bool bWasCancel)
|
||||
{
|
||||
if (ValidateClose(bWasCancel))
|
||||
{
|
||||
Close(bWasCancel);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UMenuBase::ValidateClose_Implementation(bool bWasCancel)
|
||||
{
|
||||
// Default always pass
|
||||
return true;
|
||||
}
|
||||
|
||||
void UMenuBase::OnSupercededInStack_Implementation(UMenuBase* ByMenu)
|
||||
{
|
||||
}
|
||||
|
||||
void UMenuBase::OnRegainedFocusInStack_Implementation()
|
||||
{
|
||||
}
|
||||
|
||||
void UMenuBase::OnAddedToStack_Implementation(UMenuStack* Parent)
|
||||
{
|
||||
}
|
||||
|
||||
void UMenuBase::OnRemovedFromStack_Implementation(UMenuStack* Parent)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/MenuStack.h"
|
||||
|
||||
#include "StevesUI.h"
|
||||
#include "StevesGameSubsystem.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "StevesUI/MenuBase.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
|
||||
void UMenuStack::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
// We could technically do input change detection in our own input processor, but since this already does it nicely...
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
GS->OnInputModeChanged.AddDynamic(this, &UMenuStack::InputModeChanged);
|
||||
LastInputMode = GS->GetLastInputModeUsed();
|
||||
|
||||
// Ensure that mouse is offscreen & hidden in gamepad mode
|
||||
// I've seen mouse *occasionally* end up still in the middle of the screen in gamepad mode,
|
||||
// causing confusing highlighting effects. I'm not sure if it's because of a resolution change or
|
||||
// something else, but it needs to never be there. The stack is a decent place to do this
|
||||
if (LastInputMode == EInputMode::Gamepad)
|
||||
{
|
||||
GS->MoveMouseOffScreen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMenuStack::NativeDestruct()
|
||||
{
|
||||
Super::NativeDestruct();
|
||||
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
GS->OnInputModeChanged.RemoveDynamic(this, &UMenuStack::InputModeChanged);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void UMenuStack::SavePreviousInputMousePauseState()
|
||||
{
|
||||
auto PC = GetOwningPlayer();
|
||||
UGameViewportClient* GVC = GetWorld()->GetGameViewport();
|
||||
|
||||
if (GVC->IgnoreInput())
|
||||
{
|
||||
PreviousInputMode = EInputModeChange::UIOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
#if (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION >= 26) || ENGINE_MAJOR_VERSION >= 5
|
||||
const auto CaptureMode = GVC->GetMouseCaptureMode();
|
||||
#else
|
||||
const auto CaptureMode = GVC->CaptureMouseOnClick();
|
||||
#endif
|
||||
|
||||
// Game-only mode captures permanently, that seems to be the best way to detect
|
||||
if (CaptureMode == EMouseCaptureMode::CapturePermanently ||
|
||||
CaptureMode == EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown)
|
||||
{
|
||||
PreviousInputMode = EInputModeChange::GameOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviousInputMode = EInputModeChange::GameAndUI;
|
||||
}
|
||||
}
|
||||
PreviousMouseVisibility = PC->bShowMouseCursor ? EMousePointerVisibilityChange::Visible : EMousePointerVisibilityChange::Hidden;
|
||||
PreviousPauseState = UGameplayStatics::IsGamePaused(GetWorld()) ? EGamePauseChange::Paused : EGamePauseChange::Unpaused;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void UMenuStack::ApplyInputModeChange(EInputModeChange Change) const
|
||||
{
|
||||
auto PC = GetOwningPlayer();
|
||||
|
||||
if (!PC) // possible during shutdown
|
||||
return;
|
||||
|
||||
switch (Change)
|
||||
{
|
||||
case EInputModeChange::DoNotChange:
|
||||
break;
|
||||
case EInputModeChange::UIOnly:
|
||||
PC->SetInputMode(FInputModeUIOnly());
|
||||
break;
|
||||
case EInputModeChange::GameAndUI:
|
||||
PC->SetInputMode(FInputModeGameAndUI());
|
||||
break;
|
||||
case EInputModeChange::GameOnly:
|
||||
PC->SetInputMode(FInputModeGameOnly());
|
||||
break;
|
||||
case EInputModeChange::Restore:
|
||||
ApplyInputModeChange(PreviousInputMode);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Change != EInputModeChange::DoNotChange && bFlushOnInputModeChange)
|
||||
{
|
||||
PC->FlushPressedKeys();
|
||||
}
|
||||
}
|
||||
|
||||
void UMenuStack::ApplyMousePointerVisibility(EMousePointerVisibilityChange Change) const
|
||||
{
|
||||
auto PC = GetOwningPlayer();
|
||||
|
||||
if (!PC) // possible during shutdown
|
||||
return;
|
||||
|
||||
switch (Change)
|
||||
{
|
||||
case EMousePointerVisibilityChange::DoNotChange:
|
||||
break;
|
||||
case EMousePointerVisibilityChange::Visible:
|
||||
PC->bShowMouseCursor = true;
|
||||
break;
|
||||
case EMousePointerVisibilityChange::Hidden:
|
||||
PC->bShowMouseCursor = false;
|
||||
break;
|
||||
case EMousePointerVisibilityChange::Restore:
|
||||
ApplyMousePointerVisibility(PreviousMouseVisibility);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UMenuStack::ApplyGamePauseChange(EGamePauseChange Change) const
|
||||
{
|
||||
switch (Change)
|
||||
{
|
||||
case EGamePauseChange::DoNotChange:
|
||||
break;
|
||||
case EGamePauseChange::Paused:
|
||||
UGameplayStatics::SetGamePaused(GetWorld(), true);
|
||||
break;
|
||||
case EGamePauseChange::Unpaused:
|
||||
UGameplayStatics::SetGamePaused(GetWorld(), false);
|
||||
break;
|
||||
case EGamePauseChange::Restore:
|
||||
ApplyGamePauseChange(PreviousPauseState);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool UMenuStack::HandleKeyDownEvent(const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
Super::HandleKeyDownEvent(InKeyEvent);
|
||||
|
||||
// Hardcoding the Back / Exit menu navigation inputs because input mappings can't be trusted in UMG
|
||||
// This is probably OK though, no-one redefines menu controls, right?
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
if (BackKeys.Contains(Key))
|
||||
{
|
||||
// This is "Back"
|
||||
// Request close but allow veto
|
||||
if (Menus.Num() > 0)
|
||||
{
|
||||
auto Top = Menus.Last();
|
||||
Top->RequestClose(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (InstantCloseKeys.Contains(Key))
|
||||
{
|
||||
// Shortcut to exit all menus
|
||||
// Make sure we're open long enough
|
||||
FDateTime CurrTime = FDateTime::Now();
|
||||
FTimespan Diff = CurrTime - TimeFirstOpen;
|
||||
if (Diff.GetTicks() > (int64)(MinTimeOpen * ETimespan::TicksPerSecond))
|
||||
{
|
||||
CloseAll(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void UMenuStack::InputModeChanged(int PlayerIndex, EInputMode NewMode)
|
||||
{
|
||||
if (Menus.Num())
|
||||
{
|
||||
Menus.Last()->InputModeChanged(LastInputMode, NewMode);
|
||||
}
|
||||
LastInputMode = NewMode;
|
||||
}
|
||||
|
||||
UMenuBase* UMenuStack::PushMenuByClass(TSubclassOf<UMenuBase> MenuClass)
|
||||
{
|
||||
const FName Name = MakeUniqueObjectName(this->GetOuter(), MenuClass);
|
||||
TSubclassOf<UUserWidget> BaseClass = MenuClass;
|
||||
const auto NewMenu = Cast<UMenuBase>(CreateWidgetInstance(*this, BaseClass, Name));
|
||||
PushMenuByObject(NewMenu);
|
||||
|
||||
return NewMenu;
|
||||
}
|
||||
|
||||
void UMenuStack::PushMenuByObject(UMenuBase* NewMenu)
|
||||
{
|
||||
if (NewMenu)
|
||||
{
|
||||
if (Menus.Num() > 0)
|
||||
{
|
||||
auto Top = Menus.Last();
|
||||
Top->SupercededInStack(NewMenu);
|
||||
// We keep this allocated, to restore later on back
|
||||
}
|
||||
Menus.Add(NewMenu);
|
||||
bool IsFirstMenu = Menus.Num() == 1;
|
||||
|
||||
if (IsFirstMenu)
|
||||
BeforeFirstMenuOpened();
|
||||
|
||||
NewMenu->AddedToStack(this);
|
||||
|
||||
if (IsFirstMenu)
|
||||
FirstMenuOpened();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Tried to push a null menu onto the stack"));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void UMenuStack::PopMenu(bool bWasCancel)
|
||||
{
|
||||
if (Menus.Num() > 0)
|
||||
{
|
||||
auto Top = Menus.Last();
|
||||
Top->RemovedFromStack(this);
|
||||
Menus.Pop();
|
||||
// No explicit destroy in UMG, let GC do it
|
||||
// we could try to re-use but probably not worth it vs complexity of reset for now
|
||||
|
||||
if (Menus.Num() == 0)
|
||||
{
|
||||
LastMenuClosed(bWasCancel);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto NewTop = Menus.Last();
|
||||
NewTop->RegainedFocusInStack();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UMenuStack::PopMenuIfTop(UMenuBase* UiMenuBase, bool bWasCancel)
|
||||
{
|
||||
if (Menus.Num() > 0 && Menus.Last() == UiMenuBase)
|
||||
{
|
||||
PopMenu(bWasCancel);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Tried to pop menu %s but it wasn't the top level"), *UiMenuBase->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UMenuStack::BeforeFirstMenuOpened()
|
||||
{
|
||||
SavePreviousInputMousePauseState();
|
||||
|
||||
ApplyInputModeChange(InputModeSettingOnOpen);
|
||||
ApplyMousePointerVisibility(MousePointerVisibilityOnOpen);
|
||||
ApplyGamePauseChange(GamePauseSettingOnOpen);
|
||||
}
|
||||
|
||||
void UMenuStack::FirstMenuOpened()
|
||||
{
|
||||
// Don't use world time (even real time) since map can change while open
|
||||
TimeFirstOpen = FDateTime::Now();
|
||||
|
||||
if (!IsInViewport() && bAutoAddToViewport)
|
||||
{
|
||||
AddToViewport();
|
||||
}
|
||||
}
|
||||
|
||||
void UMenuStack::RemoveFromParent()
|
||||
{
|
||||
if (Menus.Num() > 0)
|
||||
{
|
||||
CloseAll(false);
|
||||
// LastMenuClosed will re-call this so don't duplicate
|
||||
return;
|
||||
}
|
||||
|
||||
Super::RemoveFromParent();
|
||||
|
||||
}
|
||||
|
||||
UMenuBase* UMenuStack::GetTopMenu() const
|
||||
{
|
||||
if (Menus.Num() > 0)
|
||||
{
|
||||
return Menus.Top();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UMenuBase* UMenuStack::GetPreviousMenu() const
|
||||
{
|
||||
if (Menus.Num() > 1)
|
||||
{
|
||||
return Menus.Last(1);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UMenuStack::UMenuStack():
|
||||
LastInputMode(EInputMode::Unknown),
|
||||
PreviousInputMode(EInputModeChange::GameOnly),
|
||||
PreviousMouseVisibility(EMousePointerVisibilityChange::Visible),
|
||||
PreviousPauseState(EGamePauseChange::Unpaused),
|
||||
MenuContainer(nullptr)
|
||||
{
|
||||
// Default to enabling automatic focus for menus (can still be overridden in serialized properties)
|
||||
bEnableAutomaticFocus = true;
|
||||
}
|
||||
|
||||
void UMenuStack::LastMenuClosed(bool bWasCancel)
|
||||
{
|
||||
if (bAutoRemoveFromViewport)
|
||||
{
|
||||
RemoveFromParent(); // this will do MenuSystem interaction
|
||||
}
|
||||
OnClosed.Broadcast(this, bWasCancel);
|
||||
|
||||
ApplyInputModeChange(InputModeSettingOnClose);
|
||||
ApplyMousePointerVisibility(MousePointerVisibilityOnClose);
|
||||
ApplyGamePauseChange(GamePauseSettingOnClose);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void UMenuStack::CloseAll(bool bWasCancel)
|
||||
{
|
||||
// We don't go through normal pop sequence, this is a shot circuit
|
||||
for (int i = Menus.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
UMenuBase* Menu = Menus[i];
|
||||
if (IsValid(Menu))
|
||||
{
|
||||
Menus[i]->RemovedFromStack(this);
|
||||
}
|
||||
}
|
||||
Menus.Empty();
|
||||
LastMenuClosed(bWasCancel);
|
||||
}
|
||||
|
||||
bool UMenuStack::IsRequestingFocus_Implementation() const
|
||||
{
|
||||
// Delegate to top menu
|
||||
return Menus.Num() > 0 && Menus.Last()->IsRequestingFocus();
|
||||
}
|
||||
|
||||
void UMenuStack::SetFocusProperly_Implementation()
|
||||
{
|
||||
// Delegate to top menu
|
||||
if (Menus.Num() > 0)
|
||||
Menus.Last()->SetFocusProperly();
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
|
||||
#include "StevesUI/MultiSubtitleVerticalbox.h"
|
||||
|
||||
#include "SubtitleManager.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Engine/Font.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
|
||||
TSharedRef<SWidget> UMultiSubtitleVerticalbox::RebuildWidget()
|
||||
{
|
||||
auto Ret = Super::RebuildWidget();
|
||||
|
||||
if (!bSubscribed)
|
||||
{
|
||||
if (auto SM = FSubtitleManager::GetSubtitleManager())
|
||||
{
|
||||
SM->OnSetSubtitleText().AddUObject(this, &UMultiSubtitleVerticalbox::SetSubtitleText);
|
||||
bSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SubtitleUpdateTimer.IsValid() && GetWorld())
|
||||
{
|
||||
GetWorld()->GetTimerManager().SetTimer(
|
||||
SubtitleUpdateTimer, this, &UMultiSubtitleVerticalbox::UpdateSubtitles, 1, true);
|
||||
}
|
||||
|
||||
if (ConcurrentLines != TextBlocks.Num())
|
||||
{
|
||||
// If there aren't enough textblocks in the vertical box, add some
|
||||
while (TextBlocks.Num() < ConcurrentLines)
|
||||
{
|
||||
TextBlocks.Add(CreateTextBlock());
|
||||
}
|
||||
|
||||
// If there are too many, remove them and mark them for cleanup
|
||||
while (TextBlocks.Num() > ConcurrentLines)
|
||||
{
|
||||
TObjectPtr<UTextBlock> Last = TextBlocks.Pop();
|
||||
|
||||
if (Last != nullptr)
|
||||
{
|
||||
RemoveChild(Last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the subtitles list if needed
|
||||
if (ConcurrentLines != Subtitles.Num())
|
||||
{
|
||||
Subtitles.Empty();
|
||||
Subtitles.Init({}, ConcurrentLines);
|
||||
}
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
|
||||
void UMultiSubtitleVerticalbox::BeginDestroy()
|
||||
{
|
||||
Super::BeginDestroy();
|
||||
|
||||
if (bSubscribed)
|
||||
{
|
||||
if (auto SM = FSubtitleManager::GetSubtitleManager())
|
||||
{
|
||||
SM->OnSetSubtitleText().RemoveAll(this);
|
||||
bSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timer if it has been set up
|
||||
if (SubtitleUpdateTimer.IsValid() && GetWorld())
|
||||
{
|
||||
GetWorld()->GetTimerManager().ClearTimer(SubtitleUpdateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TObjectPtr<UTextBlock> UMultiSubtitleVerticalbox::CreateTextBlock()
|
||||
{
|
||||
TObjectPtr<UTextBlock> NewBlock = NewObject<UTextBlock>(this);
|
||||
|
||||
if (NewBlock != nullptr)
|
||||
{
|
||||
// Copy the style settings over
|
||||
NewBlock->SetColorAndOpacity(ColorAndOpacity);
|
||||
NewBlock->SetMinDesiredWidth(MinDesiredWidth);
|
||||
NewBlock->SetFont(Font);
|
||||
NewBlock->SetStrikeBrush(StrikeBrush);
|
||||
NewBlock->SetShadowOffset(ShadowOffset);
|
||||
NewBlock->SetShadowColorAndOpacity(ShadowColorAndOpacity);
|
||||
|
||||
AddChildToVerticalBox(NewBlock);
|
||||
|
||||
return NewBlock;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
void UMultiSubtitleVerticalbox::SetSubtitleText(const FText& InText)
|
||||
{
|
||||
// When there is no sound playing that generates a subtitle, InText
|
||||
// will be empty - let UpdateSubtitles() handle timing out
|
||||
if (InText.IsEmptyOrWhitespace())
|
||||
return;
|
||||
|
||||
bool bFoundMatch = false;
|
||||
uint64 Now = FDateTime::UtcNow().GetTicks();
|
||||
|
||||
// Go through the displayed subtitle list looking for the current subtitle
|
||||
// so its update time can be bumped if it exists in the list.
|
||||
for (FSubtitleHistory& Entry : Subtitles)
|
||||
{
|
||||
if (Entry.Subtitle.EqualTo(InText))
|
||||
{
|
||||
bFoundMatch = true;
|
||||
Entry.LastUpdate = Now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bFoundMatch)
|
||||
{
|
||||
// Try to locate an entry in the list that isn't in use already, and
|
||||
// update it to contain the current subtitle.
|
||||
for (FSubtitleHistory& Entry : Subtitles)
|
||||
{
|
||||
if (Entry.Subtitle.IsEmpty())
|
||||
{
|
||||
bFoundMatch = true;
|
||||
Entry.Subtitle = InText;
|
||||
Entry.LastUpdate = Now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Still not found a place to put the new subtitle? Replace the oldest
|
||||
// subtitle in the list with the new one.
|
||||
if (!bFoundMatch)
|
||||
{
|
||||
Subtitles.Sort();
|
||||
FSubtitleHistory& Entry = Subtitles.Last();
|
||||
|
||||
Entry.Subtitle = InText;
|
||||
Entry.LastUpdate = Now;
|
||||
}
|
||||
|
||||
UpdateSubtitles();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UMultiSubtitleVerticalbox::UpdateSubtitles()
|
||||
{
|
||||
uint64 Now = FDateTime::UtcNow().GetTicks();
|
||||
|
||||
// Handle timeout of subtitles that haven't been updated recently
|
||||
for (FSubtitleHistory& Entry : Subtitles)
|
||||
{
|
||||
if (!Entry.Subtitle.IsEmpty() &&
|
||||
(Now - Entry.LastUpdate) > (MaximumAge * ETimespan::TicksPerMillisecond * 1000))
|
||||
{
|
||||
Entry.Subtitle = FText::GetEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
Subtitles.Sort();
|
||||
|
||||
// Update the list of textblocks with the subtitle texts
|
||||
for (int i = 0; i < TextBlocks.Num(); ++i)
|
||||
{
|
||||
if (TextBlocks[i] != nullptr && i < Subtitles.Num())
|
||||
{
|
||||
TextBlocks[i]->SetText(Subtitles[i].Subtitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
|
||||
#include "StevesUI/NamedOptionWidgetBase.h"
|
||||
|
||||
#include "StevesUI.h"
|
||||
|
||||
void UNamedOptionWidgetBase::ClearOptions()
|
||||
{
|
||||
Super::ClearOptions();
|
||||
|
||||
NameIndex.Reset();
|
||||
}
|
||||
|
||||
int UNamedOptionWidgetBase::AddNamedOption(FName Name, FText Option)
|
||||
{
|
||||
int Idx = Super::AddOption(Option);
|
||||
NameIndex.Add(Name, Idx);
|
||||
return Idx;
|
||||
}
|
||||
|
||||
void UNamedOptionWidgetBase::SetOptions(const TArray<FText>& NewOptions, int NewSelectedIndex)
|
||||
{
|
||||
Super::SetOptions(NewOptions, NewSelectedIndex);
|
||||
|
||||
|
||||
UE_LOG(LogStevesUI, Warning, TEXT("Using SetOptions on a Named Option Widget loses the name lookup index, consider using SetNamedOptions instead"))
|
||||
NameIndex.Reset();
|
||||
}
|
||||
|
||||
void UNamedOptionWidgetBase::SetNamedOptions(const TMap<FName, FText>& InOptions,
|
||||
FName NewSelectedName)
|
||||
{
|
||||
Options.Reset();
|
||||
NameIndex.Reset();
|
||||
for (const auto& Pair : InOptions)
|
||||
{
|
||||
const int Idx = Options.Add(Pair.Value);
|
||||
NameIndex.Add(Pair.Key, Idx);
|
||||
}
|
||||
SetSelectedName(NewSelectedName);
|
||||
}
|
||||
|
||||
FName UNamedOptionWidgetBase::GetSelectedName() const
|
||||
{
|
||||
return SelectedName;
|
||||
}
|
||||
|
||||
int UNamedOptionWidgetBase::GetIndexForName(FName Name)
|
||||
{
|
||||
if (auto pIdx = NameIndex.Find(Name))
|
||||
{
|
||||
return *pIdx;
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FName UNamedOptionWidgetBase::GetNameForIndex(int Index)
|
||||
{
|
||||
if (auto pName = NameIndex.FindKey(Index))
|
||||
{
|
||||
return *pName;
|
||||
}
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
void UNamedOptionWidgetBase::SetSelectedIndex(int NewIndex)
|
||||
{
|
||||
// Update which name this is
|
||||
// Do this first so that the event called in the superclass has this data as well
|
||||
SelectedName = GetNameForIndex(NewIndex);
|
||||
|
||||
Super::SetSelectedIndex(NewIndex);
|
||||
}
|
||||
|
||||
void UNamedOptionWidgetBase::SetSelectedName(FName Name)
|
||||
{
|
||||
SetSelectedIndex(GetIndexForName(Name));
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/OptionWidgetBase.h"
|
||||
|
||||
#include "StevesUI.h"
|
||||
#include "StevesGameSubsystem.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Components/Button.h"
|
||||
#include "Components/Image.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
void UOptionWidgetBase::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
GS->OnInputModeChanged.AddDynamic(this, &UOptionWidgetBase::InputModeChanged);
|
||||
UpdateFromInputMode(GS->GetLastInputModeUsed());
|
||||
}
|
||||
else
|
||||
UE_LOG(LogStevesUI, Error, TEXT("StevesGameSubsystem is missing!"));
|
||||
|
||||
// Check we can bind everything we need, bind click
|
||||
if (!MouseVersion)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a MouseVersion instance."), *this->GetClass()->GetName());
|
||||
if (!GamepadVersion)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a GamepadVersion instance."), *this->GetClass()->GetName());
|
||||
if (MouseUpButton)
|
||||
MouseUpButton->OnClicked.AddDynamic(this, &UOptionWidgetBase::MouseUpClicked);
|
||||
else
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a MouseUpButton instance."), *this->GetClass()->GetName());
|
||||
if (MouseDownButton)
|
||||
MouseDownButton->OnClicked.AddDynamic(this, &UOptionWidgetBase::MouseDownClicked);
|
||||
else
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a MouseDownButton instance."), *this->GetClass()->GetName());
|
||||
if (!MouseUpImage)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a MouseUpImage instance."), *this->GetClass()->GetName());
|
||||
if (!MouseDownImage)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a MouseDownImage instance."), *this->GetClass()->GetName());
|
||||
if (!GamepadUpImage)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a GamepadUpImage instance."), *this->GetClass()->GetName());
|
||||
if (!GamepadDownImage)
|
||||
UE_LOG(LogStevesUI, Error, TEXT("%s should have a GamepadDownImage instance."), *this->GetClass()->GetName());
|
||||
|
||||
|
||||
SynchronizeProperties();
|
||||
|
||||
// To support option set up in designer
|
||||
SetSelectedIndex(SelectedIndex);
|
||||
}
|
||||
|
||||
|
||||
void UOptionWidgetBase::NativeDestruct()
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
GS->OnInputModeChanged.RemoveAll(this);
|
||||
}
|
||||
|
||||
if (MouseUpButton)
|
||||
MouseUpButton->OnClicked.RemoveAll(this);
|
||||
if (MouseDownButton)
|
||||
MouseDownButton->OnClicked.RemoveAll(this);
|
||||
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::ChangeOption(int Delta)
|
||||
{
|
||||
SetSelectedIndex(FMath::Clamp(SelectedIndex + Delta, 0, Options.Num() - 1));
|
||||
}
|
||||
|
||||
|
||||
void UOptionWidgetBase::MouseUpClicked()
|
||||
{
|
||||
ChangeOption(1);
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::MouseDownClicked()
|
||||
{
|
||||
ChangeOption(-1);
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::ClearOptions()
|
||||
{
|
||||
Options.Empty();
|
||||
SetSelectedIndex(-1);
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::UpdateFromInputMode(EInputMode Mode)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case EInputMode::Mouse:
|
||||
SetMouseMode();
|
||||
break;
|
||||
case EInputMode::Keyboard:
|
||||
case EInputMode::Gamepad:
|
||||
SetButtonMode();
|
||||
break;
|
||||
case EInputMode::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::InputModeChanged(int PlayerIndex, EInputMode NewMode)
|
||||
{
|
||||
UpdateFromInputMode(NewMode);
|
||||
}
|
||||
|
||||
|
||||
void UOptionWidgetBase::SetMouseMode()
|
||||
{
|
||||
if (!MouseVersion)
|
||||
return;
|
||||
|
||||
bool bHadFocus = false;
|
||||
if (GamepadVersion)
|
||||
{
|
||||
bHadFocus = GamepadVersion->HasKeyboardFocus();
|
||||
GamepadVersion->SetVisibility(ESlateVisibility::Hidden);
|
||||
}
|
||||
|
||||
MouseVersion->SetVisibility(ESlateVisibility::Visible);
|
||||
|
||||
if (bHadFocus)
|
||||
SetFocusProperly();
|
||||
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::SetButtonMode()
|
||||
{
|
||||
if (!GamepadVersion)
|
||||
return;
|
||||
const bool bHadFocus = (MouseUpButton && MouseUpButton->HasKeyboardFocus()) || (MouseDownButton && MouseDownButton->HasKeyboardFocus());
|
||||
|
||||
if (MouseVersion)
|
||||
MouseVersion->SetVisibility(ESlateVisibility::Hidden);
|
||||
|
||||
GamepadVersion->SetVisibility(ESlateVisibility::Visible);
|
||||
|
||||
if (bHadFocus)
|
||||
SetFocusProperly();
|
||||
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::SetFocusProperly_Implementation()
|
||||
{
|
||||
if (GamepadVersion && GamepadVersion->IsVisible())
|
||||
GamepadVersion->SetFocus();
|
||||
else if (MouseUpButton && MouseDownButton)
|
||||
MouseUpButton->GetVisibility() == ESlateVisibility::Visible ? MouseUpButton->SetFocus() : MouseDownButton->SetFocus();
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::SetSelectedIndex(int NewIndex)
|
||||
{
|
||||
const bool bRaiseEvent = SelectedIndex != NewIndex;
|
||||
|
||||
SelectedIndex = NewIndex;
|
||||
|
||||
const FText NewText = GetSelectedOption();
|
||||
if (MouseText)
|
||||
MouseText->SetText(NewText);
|
||||
if (GamepadText)
|
||||
GamepadText->SetText(NewText);
|
||||
|
||||
UpdateUpDownButtons();
|
||||
|
||||
if (bRaiseEvent)
|
||||
{
|
||||
// BP opportunity
|
||||
PostSelectedOptionChanged();
|
||||
OnSelectedOptionChanged.Broadcast(this, SelectedIndex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::UpdateUpDownButtons()
|
||||
{
|
||||
const bool CanDecrease = SelectedIndex > 0;
|
||||
const bool CanIncrease = SelectedIndex < Options.Num() - 1;
|
||||
if (MouseDownButton)
|
||||
MouseDownButton->SetVisibility(CanDecrease ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
|
||||
if (MouseUpButton)
|
||||
MouseUpButton->SetVisibility(CanIncrease ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
|
||||
if (GamepadDownImage)
|
||||
GamepadDownImage->SetVisibility(CanDecrease ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
|
||||
if (GamepadUpImage)
|
||||
GamepadUpImage->SetVisibility(CanIncrease ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
|
||||
|
||||
}
|
||||
|
||||
|
||||
int UOptionWidgetBase::AddOption(FText Option)
|
||||
{
|
||||
const int Ret = Options.Add(Option);
|
||||
if (GetSelectedIndex() == -1)
|
||||
{
|
||||
SetSelectedIndex(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUpDownButtons();
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void UOptionWidgetBase::SetOptions(const TArray<FText>& InOptions, int NewSelectedIndex)
|
||||
{
|
||||
Options = InOptions;
|
||||
SetSelectedIndex(NewSelectedIndex);
|
||||
}
|
||||
|
||||
FText UOptionWidgetBase::GetSelectedOption() const
|
||||
{
|
||||
if (Options.IsValidIndex(SelectedIndex))
|
||||
return Options[SelectedIndex];
|
||||
|
||||
return FText();
|
||||
}
|
||||
|
||||
EInputMode UOptionWidgetBase::GetCurrentInputMode() const
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(GetWorld());
|
||||
if (GS)
|
||||
return GS->GetLastInputModeUsed();
|
||||
|
||||
return EInputMode::Unknown;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/RichTextBlockInputImageDecorator.h"
|
||||
|
||||
|
||||
#include "StevesHelperCommon.h"
|
||||
#include "StevesUEHelpers.h"
|
||||
#include "Fonts/FontMeasure.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Misc/DefaultValueHelper.h"
|
||||
#include "Widgets/Layout/SScaleBox.h"
|
||||
#include "Widgets/Layout/SBox.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
|
||||
// Slate SNew only supports 5 custom arguments so we need to batch things up
|
||||
struct FRichTextInputImageParams
|
||||
{
|
||||
/// What type of an input binding this image should look up
|
||||
EInputBindingType BindingType;
|
||||
/// If BindingType is Action/Axis, the name of it
|
||||
FName ActionOrAxisName;
|
||||
/// If BindingType is Key, the key
|
||||
FKey Key;
|
||||
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
|
||||
TSoftObjectPtr<UInputAction> InputAction;
|
||||
/// Player index, if binding type is action or axis
|
||||
int PlayerIndex;
|
||||
/// Where there are multiple mappings, which to prefer
|
||||
EInputImageDevicePreference DevicePreference = EInputImageDevicePreference::Auto;
|
||||
|
||||
/// Initial Sprite to use
|
||||
UPaperSprite* InitialSprite;
|
||||
/// Parent decorator, for looking up things later
|
||||
URichTextBlockInputImageDecorator* Decorator;
|
||||
};
|
||||
// Basically the same as SRichInlineImage but I can't re-use that since private
|
||||
class SRichInlineInputImage : public SCompoundWidget
|
||||
{
|
||||
protected:
|
||||
/// What type of an input binding this image should look up
|
||||
EInputBindingType BindingType = EInputBindingType::Key;
|
||||
/// If BindingType is Action/Axis, the name of it
|
||||
FName ActionOrAxisName;
|
||||
/// If BindingType is Key, the key
|
||||
FKey Key;
|
||||
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
|
||||
TSoftObjectPtr<UInputAction> InputAction;
|
||||
/// Player index, if binding type is action or axis
|
||||
int PlayerIndex = 0;
|
||||
/// Where there are multiple mappings, which to prefer
|
||||
EInputImageDevicePreference DevicePreference = EInputImageDevicePreference::Auto;
|
||||
/// Parent decorator, for looking up things later
|
||||
URichTextBlockInputImageDecorator* Decorator = nullptr;
|
||||
|
||||
FSlateBrush Brush;
|
||||
float TimeUntilNextSpriteCheck = 0;
|
||||
uint16 MaxCharHeight = 0;
|
||||
TOptional<int32> RequestedWidth;
|
||||
TOptional<int32> RequestedHeight;
|
||||
SBox* ContainerBox = nullptr;
|
||||
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SRichInlineInputImage)
|
||||
{}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
void Construct(const FArguments& InArgs, FRichTextInputImageParams InParams,
|
||||
const FTextBlockStyle& TextStyle, TOptional<int32> Width, TOptional<int32> Height, EStretch::Type Stretch)
|
||||
{
|
||||
BindingType = InParams.BindingType;
|
||||
ActionOrAxisName = InParams.ActionOrAxisName;
|
||||
DevicePreference = InParams.DevicePreference;
|
||||
Key = InParams.Key;
|
||||
InputAction = InParams.InputAction;
|
||||
PlayerIndex = InParams.PlayerIndex;
|
||||
Decorator = InParams.Decorator;
|
||||
RequestedWidth = Width;
|
||||
RequestedHeight = Height;
|
||||
|
||||
// Sadly, we cannot hook into the events needed to update based on input changes here
|
||||
// All attempts to use GetStevesGameSubsystem() fail because the world pointer
|
||||
// doesn't work, I think perhaps because this Slate Construct call is in another thread which pre-dates it.
|
||||
// We will need to do the work to update the brush from the main thread later
|
||||
|
||||
// We can use static methods though
|
||||
if (IsValid(InParams.InitialSprite))
|
||||
UStevesGameSubsystem::SetBrushFromAtlas(&Brush, InParams.InitialSprite, true);
|
||||
TimeUntilNextSpriteCheck = 0.25f;
|
||||
|
||||
const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
||||
MaxCharHeight = FontMeasure->GetMaxCharacterHeight(TextStyle.Font, 1.0f);
|
||||
float IconHeight = FMath::Min(static_cast<float>(MaxCharHeight), Brush.ImageSize.Y);
|
||||
if (Height.IsSet())
|
||||
{
|
||||
IconHeight = Height.GetValue();
|
||||
}
|
||||
|
||||
float IconWidth = Brush.ImageSize.X * (IconHeight / Brush.ImageSize.Y) ;
|
||||
if (Width.IsSet())
|
||||
{
|
||||
IconWidth = Width.GetValue();
|
||||
}
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SBox)
|
||||
.HeightOverride(IconHeight)
|
||||
.WidthOverride(IconWidth)
|
||||
[
|
||||
SNew(SScaleBox)
|
||||
.Stretch(Stretch)
|
||||
.StretchDirection(EStretchDirection::DownOnly)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SImage)
|
||||
.Image(&Brush)
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override
|
||||
{
|
||||
SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
|
||||
|
||||
// I would prefer to hook into the events here but there is NO safe teardown in these RichText decorators
|
||||
// with which to unsub from events, so we're down to brute forcing this in Tick() le sigh
|
||||
// At least limit the frequency and only change as needed
|
||||
TimeUntilNextSpriteCheck -= InDeltaTime;
|
||||
if (TimeUntilNextSpriteCheck <= 0)
|
||||
{
|
||||
auto GS = GetStevesGameSubsystem(Decorator->GetWorld());
|
||||
if (GS)
|
||||
{
|
||||
// Can only support default theme, no way to edit theme in decorator config
|
||||
UPaperSprite* Sprite = nullptr;
|
||||
if (BindingType == EInputBindingType::EnhancedInputAction && !InputAction.IsNull())
|
||||
{
|
||||
if (auto IA = InputAction.LoadSynchronous())
|
||||
{
|
||||
auto PC = UGameplayStatics::GetPlayerController(Decorator->GetWorld(), PlayerIndex);
|
||||
Sprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, DevicePreference, PlayerIndex, PC);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex);
|
||||
}
|
||||
if (Sprite && Brush.GetResourceObject() != Sprite)
|
||||
{
|
||||
UStevesGameSubsystem::SetBrushFromAtlas(&Brush, Sprite, true);
|
||||
|
||||
// Deal with aspect ratio changes
|
||||
TSharedPtr<SWidget> Widget = ChildSlot.GetWidget();
|
||||
SBox* Box = static_cast<SBox*>(Widget.Get());
|
||||
float IconHeight = FMath::Min(static_cast<float>(MaxCharHeight), Brush.ImageSize.Y);
|
||||
if (RequestedHeight.IsSet())
|
||||
{
|
||||
IconHeight = RequestedHeight.GetValue();
|
||||
}
|
||||
|
||||
float IconWidth = Brush.ImageSize.X * (IconHeight / Brush.ImageSize.Y) ;
|
||||
if (RequestedWidth.IsSet())
|
||||
{
|
||||
IconWidth = RequestedWidth.GetValue();
|
||||
}
|
||||
|
||||
Box->SetWidthOverride(IconWidth);
|
||||
Box->SetHeightOverride(IconHeight);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
TimeUntilNextSpriteCheck = 0.5f;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Again, wish I could just subclass FRichInlineImage here, le sigh
|
||||
class FRichInlineInputImage : public FRichTextDecorator
|
||||
{
|
||||
public:
|
||||
FRichInlineInputImage(URichTextBlock* InOwner, URichTextBlockInputImageDecorator* InDecorator)
|
||||
: FRichTextDecorator(InOwner)
|
||||
, Decorator(InDecorator)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override
|
||||
{
|
||||
if (RunParseResult.Name == TEXT("input"))
|
||||
{
|
||||
return RunParseResult.MetaData.Contains(TEXT("key")) ||
|
||||
RunParseResult.MetaData.Contains(TEXT("action")) ||
|
||||
RunParseResult.MetaData.Contains(TEXT("axis")) ||
|
||||
RunParseResult.MetaData.Contains(TEXT("eaction"));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
virtual TSharedPtr<SWidget> CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const override
|
||||
{
|
||||
FRichTextInputImageParams Params;
|
||||
Params.PlayerIndex = 0;
|
||||
Params.BindingType = EInputBindingType::Key;
|
||||
Params.Key = EKeys::AnyKey;
|
||||
Params.Decorator = Decorator;
|
||||
|
||||
auto GS = GetStevesGameSubsystem(Decorator->GetWorld());
|
||||
|
||||
if (const FString* PlayerStr = RunInfo.MetaData.Find(TEXT("player")))
|
||||
{
|
||||
int PTemp;
|
||||
Params.PlayerIndex = FDefaultValueHelper::ParseInt(*PlayerStr, PTemp) ? PTemp : 0;
|
||||
}
|
||||
|
||||
if (const FString* KeyStr = RunInfo.MetaData.Find(TEXT("key")))
|
||||
{
|
||||
Params.BindingType = EInputBindingType::Key;
|
||||
Params.Key = FKey(**KeyStr);
|
||||
}
|
||||
else if (const FString* ActionStr = RunInfo.MetaData.Find(TEXT("action")))
|
||||
{
|
||||
Params.BindingType = EInputBindingType::Action;
|
||||
Params.ActionOrAxisName = **ActionStr;
|
||||
}
|
||||
else if (const FString* AxisStr = RunInfo.MetaData.Find(TEXT("axis")))
|
||||
{
|
||||
Params.BindingType = EInputBindingType::Axis;
|
||||
Params.ActionOrAxisName = **AxisStr;
|
||||
}
|
||||
else if (const FString* EInputStr = RunInfo.MetaData.Find(TEXT("eaction")))
|
||||
{
|
||||
Params.BindingType = EInputBindingType::EnhancedInputAction;
|
||||
// Try to find the input action
|
||||
if (GS)
|
||||
{
|
||||
Params.InputAction = GS->FindEnhancedInputAction(*EInputStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (const FString* PreferStr = RunInfo.MetaData.Find(TEXT("prefer")))
|
||||
{
|
||||
if (PreferStr->Equals("auto", ESearchCase::IgnoreCase))
|
||||
{
|
||||
Params.DevicePreference = EInputImageDevicePreference::Auto;
|
||||
}
|
||||
else if (PreferStr->Equals("gkm", ESearchCase::IgnoreCase))
|
||||
{
|
||||
Params.DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse;
|
||||
}
|
||||
else if (PreferStr->Equals("gmk", ESearchCase::IgnoreCase))
|
||||
{
|
||||
Params.DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
|
||||
}
|
||||
else if (PreferStr->Equals("gmkbutton", ESearchCase::IgnoreCase))
|
||||
{
|
||||
Params.DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
|
||||
}
|
||||
else if (PreferStr->Equals("gmkaxis", ESearchCase::IgnoreCase))
|
||||
{
|
||||
Params.DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Axis;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the initial sprite here
|
||||
// The Slate widget can't do it in Construct because World pointer doesn't work (thread issues?)
|
||||
// Also annoying: can't keep Brush on this class because this method is const. UGH
|
||||
if (GS)
|
||||
{
|
||||
if (Params.BindingType == EInputBindingType::EnhancedInputAction && !Params.InputAction.IsNull())
|
||||
{
|
||||
if (auto IA = Params.InputAction.LoadSynchronous())
|
||||
{
|
||||
auto PC = UGameplayStatics::GetPlayerController(Decorator->GetWorld(), Params.PlayerIndex);
|
||||
Params.InitialSprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, Params.DevicePreference, Params.PlayerIndex, PC);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can only support default theme, no way to edit theme in decorator config
|
||||
Params.InitialSprite = GS->GetInputImageSprite(Params.BindingType, Params.ActionOrAxisName, Params.Key, Params.DevicePreference, Params.PlayerIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Might be false because this gets executed in the editor too
|
||||
// TODO use a placeholder?
|
||||
Params.InitialSprite = nullptr;
|
||||
}
|
||||
|
||||
|
||||
// Support the same width/height/stretch overrides as standard rich text images
|
||||
TOptional<int32> Width;
|
||||
if (const FString* WidthString = RunInfo.MetaData.Find(TEXT("width")))
|
||||
{
|
||||
int32 WidthTemp;
|
||||
Width = FDefaultValueHelper::ParseInt(*WidthString, WidthTemp) ? WidthTemp : TOptional<int32>();
|
||||
}
|
||||
|
||||
TOptional<int32> Height;
|
||||
if (const FString* HeightString = RunInfo.MetaData.Find(TEXT("height")))
|
||||
{
|
||||
int32 HeightTemp;
|
||||
Height = FDefaultValueHelper::ParseInt(*HeightString, HeightTemp) ? HeightTemp : TOptional<int32>();
|
||||
}
|
||||
|
||||
EStretch::Type Stretch = EStretch::ScaleToFit;
|
||||
if (const FString* SstretchString = RunInfo.MetaData.Find(TEXT("stretch")))
|
||||
{
|
||||
static const UEnum* StretchEnum = StaticEnum<EStretch::Type>();
|
||||
int64 StretchValue = StretchEnum->GetValueByNameString(*SstretchString);
|
||||
if (StretchValue != INDEX_NONE)
|
||||
{
|
||||
Stretch = static_cast<EStretch::Type>(StretchValue);
|
||||
}
|
||||
}
|
||||
|
||||
// SNew only supports 5 custom arguments! Thats why we batch up in struct
|
||||
return SNew(SRichInlineInputImage, Params, TextStyle, Width, Height, Stretch);
|
||||
}
|
||||
|
||||
private:
|
||||
URichTextBlockInputImageDecorator* Decorator;
|
||||
};
|
||||
|
||||
TSharedPtr<ITextDecorator> URichTextBlockInputImageDecorator::CreateDecorator(URichTextBlock* InOwner)
|
||||
{
|
||||
return MakeShareable(new FRichInlineInputImage(InOwner, this));
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/SFocusableButton.h"
|
||||
|
||||
void SFocusableButton::Construct(const FArguments& InArgs)
|
||||
{
|
||||
// Call SButton construct and pass through
|
||||
SButton::Construct(SButton::FArguments()
|
||||
.ButtonStyle(InArgs._ButtonStyle)
|
||||
.ClickMethod(InArgs._ClickMethod)
|
||||
.ContentPadding(InArgs._ContentPadding)
|
||||
.ContentScale(InArgs._ContentScale)
|
||||
.FlowDirectionPreference(InArgs._FlowDirectionPreference)
|
||||
.ForegroundColor(InArgs._ForegroundColor)
|
||||
.HAlign(InArgs._HAlign)
|
||||
.VAlign(InArgs._VAlign)
|
||||
.IsFocusable(InArgs._IsFocusable)
|
||||
.OnClicked(InArgs._OnClicked)
|
||||
.OnHovered(InArgs._OnHovered)
|
||||
.OnPressed(InArgs._OnPressed)
|
||||
.OnReleased(InArgs._OnReleased)
|
||||
.OnUnhovered(InArgs._OnUnhovered)
|
||||
.PressMethod(InArgs._PressMethod)
|
||||
.TouchMethod(InArgs._TouchMethod)
|
||||
.DesiredSizeScale(InArgs._DesiredSizeScale)
|
||||
.HoveredSoundOverride(InArgs._HoveredSoundOverride)
|
||||
.PressedSoundOverride(InArgs._PressedSoundOverride)
|
||||
.ButtonColorAndOpacity(InArgs._ButtonColorAndOpacity)
|
||||
.TextFlowDirection(InArgs._TextFlowDirection)
|
||||
.TextShapingMethod(InArgs._TextShapingMethod)
|
||||
.Clipping(InArgs._Clipping)
|
||||
.Cursor(InArgs._Cursor)
|
||||
.Tag(InArgs._Tag)
|
||||
.Visibility(InArgs._Visibility)
|
||||
.AccessibleParams(InArgs._AccessibleParams)
|
||||
.AccessibleText(InArgs._AccessibleText)
|
||||
.ForceVolatile(InArgs._ForceVolatile)
|
||||
.IsEnabled(InArgs._IsEnabled)
|
||||
.RenderOpacity(InArgs._RenderOpacity)
|
||||
.RenderTransform(InArgs._RenderTransform)
|
||||
.RenderTransformPivot(InArgs._RenderTransformPivot)
|
||||
.Text(InArgs._Text)
|
||||
.TextStyle(InArgs._TextStyle)
|
||||
.ToolTip(InArgs._ToolTip)
|
||||
.ToolTipText(InArgs._ToolTipText)
|
||||
);
|
||||
|
||||
OnFocusReceivedDelegate = InArgs._OnFocusReceived;
|
||||
OnFocusLostDelegate = InArgs._OnFocusLost;
|
||||
}
|
||||
|
||||
FReply SFocusableButton::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
auto Ret = SButton::OnFocusReceived(MyGeometry, InFocusEvent);
|
||||
|
||||
OnFocusReceivedDelegate.ExecuteIfBound();
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void SFocusableButton::OnFocusLost(const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
SButton::OnFocusLost(InFocusEvent);
|
||||
|
||||
OnFocusLostDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void SFocusableButton::SetOnFocusReceived(FSimpleDelegate InOnFocusReceived)
|
||||
{
|
||||
OnFocusReceivedDelegate = InOnFocusReceived;
|
||||
}
|
||||
|
||||
void SFocusableButton::SetOnFocusLost(FSimpleDelegate InOnFocusLost)
|
||||
{
|
||||
OnFocusLostDelegate = InOnFocusLost;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
|
||||
class SFocusableButton : public SButton
|
||||
{
|
||||
// Seems in slate we have to duplicate all the args from superclass
|
||||
SLATE_BEGIN_ARGS(SFocusableButton)
|
||||
: _Content()
|
||||
, _ButtonStyle( &FCoreStyle::Get().GetWidgetStyle< FButtonStyle >( "Button" ) )
|
||||
, _TextStyle( &FCoreStyle::Get().GetWidgetStyle< FTextBlockStyle >("NormalText") )
|
||||
, _HAlign( HAlign_Fill )
|
||||
, _VAlign( VAlign_Fill )
|
||||
, _ContentPadding(FMargin(4.0, 2.0))
|
||||
, _Text()
|
||||
, _ClickMethod( EButtonClickMethod::DownAndUp )
|
||||
, _TouchMethod( EButtonTouchMethod::DownAndUp )
|
||||
, _PressMethod( EButtonPressMethod::DownAndUp )
|
||||
, _DesiredSizeScale( FVector2D(1,1) )
|
||||
, _ContentScale( FVector2D(1,1) )
|
||||
, _ButtonColorAndOpacity(FLinearColor::White)
|
||||
, _ForegroundColor( FCoreStyle::Get().GetSlateColor( "InvertedForeground" ) )
|
||||
, _IsFocusable( true )
|
||||
{
|
||||
}
|
||||
|
||||
/** Slot for this button's content (optional) */
|
||||
SLATE_DEFAULT_SLOT( FArguments, Content )
|
||||
|
||||
/** The visual style of the button */
|
||||
SLATE_STYLE_ARGUMENT( FButtonStyle, ButtonStyle )
|
||||
|
||||
/** The text style of the button */
|
||||
SLATE_STYLE_ARGUMENT( FTextBlockStyle, TextStyle )
|
||||
|
||||
/** Horizontal alignment */
|
||||
SLATE_ARGUMENT( EHorizontalAlignment, HAlign )
|
||||
|
||||
/** Vertical alignment */
|
||||
SLATE_ARGUMENT( EVerticalAlignment, VAlign )
|
||||
|
||||
/** Spacing between button's border and the content. */
|
||||
SLATE_ATTRIBUTE( FMargin, ContentPadding )
|
||||
|
||||
/** The text to display in this button, if no custom content is specified */
|
||||
SLATE_ATTRIBUTE( FText, Text )
|
||||
|
||||
/** Called when the button is clicked */
|
||||
SLATE_EVENT( FOnClicked, OnClicked )
|
||||
|
||||
/** Called when the button is pressed */
|
||||
SLATE_EVENT( FSimpleDelegate, OnPressed )
|
||||
|
||||
/** Called when the button is released */
|
||||
SLATE_EVENT( FSimpleDelegate, OnReleased )
|
||||
|
||||
SLATE_EVENT( FSimpleDelegate, OnHovered )
|
||||
|
||||
SLATE_EVENT( FSimpleDelegate, OnUnhovered )
|
||||
|
||||
/** Sets the rules to use for determining whether the button was clicked. This is an advanced setting and generally should be left as the default. */
|
||||
SLATE_ARGUMENT( EButtonClickMethod::Type, ClickMethod )
|
||||
|
||||
/** How should the button be clicked with touch events? */
|
||||
SLATE_ARGUMENT( EButtonTouchMethod::Type, TouchMethod )
|
||||
|
||||
/** How should the button be clicked with keyboard/controller button events? */
|
||||
SLATE_ARGUMENT( EButtonPressMethod::Type, PressMethod )
|
||||
|
||||
SLATE_ATTRIBUTE( FVector2D, DesiredSizeScale )
|
||||
|
||||
SLATE_ATTRIBUTE( FVector2D, ContentScale )
|
||||
|
||||
SLATE_ATTRIBUTE( FSlateColor, ButtonColorAndOpacity )
|
||||
|
||||
SLATE_ATTRIBUTE( FSlateColor, ForegroundColor )
|
||||
|
||||
/** Sometimes a button should only be mouse-clickable and never keyboard focusable. */
|
||||
SLATE_ARGUMENT( bool, IsFocusable )
|
||||
|
||||
/** The sound to play when the button is pressed */
|
||||
SLATE_ARGUMENT( TOptional<FSlateSound>, PressedSoundOverride )
|
||||
|
||||
/** The sound to play when the button is hovered */
|
||||
SLATE_ARGUMENT( TOptional<FSlateSound>, HoveredSoundOverride )
|
||||
|
||||
/** Which text shaping method should we use? (unset to use the default returned by GetDefaultTextShapingMethod) */
|
||||
SLATE_ARGUMENT( TOptional<ETextShapingMethod>, TextShapingMethod )
|
||||
|
||||
/** Which text flow direction should we use? (unset to use the default returned by GetDefaultTextFlowDirection) */
|
||||
SLATE_ARGUMENT( TOptional<ETextFlowDirection>, TextFlowDirection )
|
||||
|
||||
// This is the bit we're adding
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusReceived )
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusLost )
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
void Construct( const FArguments& InArgs );
|
||||
|
||||
virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override;
|
||||
virtual void OnFocusLost(const FFocusEvent& InFocusEvent) override;
|
||||
|
||||
void SetOnFocusReceived(FSimpleDelegate InOnFocusReceived);
|
||||
void SetOnFocusLost(FSimpleDelegate InOnFocusLost);
|
||||
|
||||
protected:
|
||||
FSimpleDelegate OnFocusReceivedDelegate;
|
||||
FSimpleDelegate OnFocusLostDelegate;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "SFocusableCheckBox.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
void SFocusableCheckBox::Construct(const FArguments& InArgs)
|
||||
{
|
||||
// Call superclass, have to re-construct the args because they're not compatible
|
||||
SCheckBox::Construct(SCheckBox::FArguments()
|
||||
.Padding(InArgs._Padding)
|
||||
.Style(InArgs._Style)
|
||||
.Type(InArgs._Type)
|
||||
.CheckedImage(InArgs._CheckedImage)
|
||||
.ClickMethod(InArgs._ClickMethod)
|
||||
.ForegroundColor(InArgs._ForegroundColor)
|
||||
.HAlign(InArgs._HAlign)
|
||||
.IsChecked(InArgs._IsChecked)
|
||||
.IsFocusable(InArgs._IsFocusable)
|
||||
.PressMethod(InArgs._PressMethod)
|
||||
.TouchMethod(InArgs._TouchMethod)
|
||||
.UncheckedImage(InArgs._UncheckedImage)
|
||||
.UndeterminedImage(InArgs._UndeterminedImage)
|
||||
.CheckedImage(InArgs._CheckedImage)
|
||||
.UncheckedHoveredImage(InArgs._UncheckedHoveredImage)
|
||||
.UndeterminedHoveredImage(InArgs._UndeterminedHoveredImage)
|
||||
.CheckedHoveredImage(InArgs._CheckedHoveredImage)
|
||||
.UncheckedPressedImage(InArgs._UncheckedPressedImage)
|
||||
.UndeterminedPressedImage(InArgs._UndeterminedPressedImage)
|
||||
.CheckedPressedImage(InArgs._CheckedPressedImage)
|
||||
.CheckedSoundOverride(InArgs._CheckedSoundOverride)
|
||||
.UncheckedSoundOverride(InArgs._UncheckedSoundOverride)
|
||||
.HoveredSoundOverride(InArgs._HoveredSoundOverride)
|
||||
.BorderBackgroundColor(InArgs._BorderBackgroundColor)
|
||||
.OnCheckStateChanged(InArgs._OnCheckStateChanged)
|
||||
.IsEnabled(InArgs._IsEnabled)
|
||||
.Clipping(InArgs._Clipping)
|
||||
.Cursor(InArgs._Cursor)
|
||||
.Tag(InArgs._Tag)
|
||||
.Visibility(InArgs._Visibility)
|
||||
.AccessibleParams(InArgs._AccessibleParams)
|
||||
.AccessibleText(InArgs._AccessibleText)
|
||||
.ForceVolatile(InArgs._ForceVolatile)
|
||||
.IsEnabled(InArgs._IsEnabled)
|
||||
.RenderOpacity(InArgs._RenderOpacity)
|
||||
.RenderTransform(InArgs._RenderTransform)
|
||||
.RenderTransformPivot(InArgs._RenderTransformPivot)
|
||||
.ToolTip(InArgs._ToolTip)
|
||||
.ToolTipText(InArgs._ToolTipText)
|
||||
);
|
||||
|
||||
OnHoveredDelegate = InArgs._OnHovered;
|
||||
OnUnhoveredDelegate = InArgs._OnUnhovered;
|
||||
OnFocusReceivedDelegate = InArgs._OnFocusReceived;
|
||||
OnFocusLostDelegate = InArgs._OnFocusLost;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
FReply SFocusableCheckBox::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
// Default 4.26.1 behaviour is to return Unhandled for ANY keys apart from Accept, which breaks keyboard nav
|
||||
// This is a bug and it's been fixed in the source version but not released yet, so work around it
|
||||
if (FSlateApplication::Get().GetNavigationActionFromKey(InKeyEvent) == EUINavigationAction::Accept)
|
||||
{
|
||||
// for accept, let the immediate superclass do it
|
||||
return SCheckBox::OnKeyDown(MyGeometry, InKeyEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
// for any other key, the super-super needs to handle it to support navigation
|
||||
return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SFocusableCheckBox::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
||||
{
|
||||
SCheckBox::OnMouseEnter(MyGeometry, MouseEvent);
|
||||
|
||||
// SCheckbox doesn't have hovered / unhovered events so we need to add them
|
||||
OnHoveredDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void SFocusableCheckBox::OnMouseLeave(const FPointerEvent& MouseEvent)
|
||||
{
|
||||
SCheckBox::OnMouseLeave(MouseEvent);
|
||||
|
||||
OnUnhoveredDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
FReply SFocusableCheckBox::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
auto Ret = SCheckBox::OnFocusReceived(MyGeometry, InFocusEvent);
|
||||
|
||||
OnFocusReceivedDelegate.ExecuteIfBound();
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void SFocusableCheckBox::OnFocusLost(const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
SCheckBox::OnFocusLost(InFocusEvent);
|
||||
|
||||
OnFocusLostDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void SFocusableCheckBox::SetOnFocusReceived(FSimpleDelegate InOnFocusReceived)
|
||||
{
|
||||
OnFocusReceivedDelegate = InOnFocusReceived;
|
||||
}
|
||||
|
||||
void SFocusableCheckBox::SetOnFocusLost(FSimpleDelegate InOnFocusLost)
|
||||
{
|
||||
OnFocusLostDelegate = InOnFocusLost;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
|
||||
|
||||
class SFocusableCheckBox : public SCheckBox
|
||||
{
|
||||
public:
|
||||
|
||||
// Seems in slate we have to duplicate all the args from superclass
|
||||
SLATE_BEGIN_ARGS( SFocusableCheckBox )
|
||||
: _Content()
|
||||
, _Style( &FCoreStyle::Get().GetWidgetStyle< FCheckBoxStyle >("Checkbox") )
|
||||
, _Type()
|
||||
, _OnCheckStateChanged()
|
||||
, _IsChecked( ECheckBoxState::Unchecked )
|
||||
, _HAlign( HAlign_Fill )
|
||||
, _CheckBoxContentUsesAutoWidth(true)
|
||||
, _Padding()
|
||||
, _ClickMethod( EButtonClickMethod::DownAndUp )
|
||||
, _TouchMethod(EButtonTouchMethod::DownAndUp)
|
||||
, _PressMethod(EButtonPressMethod::DownAndUp)
|
||||
, _ForegroundColor(FSlateColor(FColor::White))
|
||||
, _BorderBackgroundColor (FSlateColor(FColor::White))
|
||||
, _IsFocusable( true )
|
||||
, _UncheckedImage( nullptr )
|
||||
, _UncheckedHoveredImage( nullptr )
|
||||
, _UncheckedPressedImage( nullptr )
|
||||
, _CheckedImage( nullptr )
|
||||
, _CheckedHoveredImage( nullptr )
|
||||
, _CheckedPressedImage( nullptr )
|
||||
, _UndeterminedImage( nullptr )
|
||||
, _UndeterminedHoveredImage( nullptr )
|
||||
, _UndeterminedPressedImage( nullptr )
|
||||
{
|
||||
}
|
||||
/** Content to be placed next to the check box, or for a toggle button, the content to be placed inside the button */
|
||||
SLATE_DEFAULT_SLOT( FArguments, Content )
|
||||
|
||||
/** The style structure for this checkbox' visual style */
|
||||
SLATE_STYLE_ARGUMENT( FCheckBoxStyle, Style )
|
||||
|
||||
/** Type of check box (set by the Style arg but the Style can be overridden with this) */
|
||||
SLATE_ARGUMENT( TOptional<ESlateCheckBoxType::Type>, Type )
|
||||
|
||||
/** Called when the checked state has changed */
|
||||
SLATE_EVENT( FOnCheckStateChanged, OnCheckStateChanged )
|
||||
|
||||
/** Whether the check box is currently in a checked state */
|
||||
SLATE_ATTRIBUTE( ECheckBoxState, IsChecked )
|
||||
|
||||
/** How the content of the toggle button should align within the given space*/
|
||||
SLATE_ARGUMENT( EHorizontalAlignment, HAlign )
|
||||
|
||||
/** Whether or not the content portion of the checkbox should layout using auto-width. When true the content will always be arranged at its desired size as opposed to resizing to the available space. */
|
||||
SLATE_ARGUMENT(bool, CheckBoxContentUsesAutoWidth)
|
||||
|
||||
/** Spacing between the check box image and its content (set by the Style arg but the Style can be overridden with this) */
|
||||
SLATE_ATTRIBUTE( FMargin, Padding )
|
||||
|
||||
/** Sets the rules to use for determining whether the button was clicked. This is an advanced setting and generally should be left as the default. */
|
||||
SLATE_ARGUMENT( EButtonClickMethod::Type, ClickMethod )
|
||||
|
||||
/** How should the button be clicked with touch events? */
|
||||
SLATE_ARGUMENT(EButtonTouchMethod::Type, TouchMethod)
|
||||
|
||||
/** How should the button be clicked with keyboard/controller button events? */
|
||||
SLATE_ARGUMENT(EButtonPressMethod::Type, PressMethod)
|
||||
|
||||
/** Foreground color for the checkbox's content and parts (set by the Style arg but the Style can be overridden with this) */
|
||||
SLATE_ATTRIBUTE( FSlateColor, ForegroundColor )
|
||||
|
||||
/** The color of the background border (set by the Style arg but the Style can be overridden with this) */
|
||||
SLATE_ATTRIBUTE( FSlateColor, BorderBackgroundColor )
|
||||
|
||||
SLATE_ARGUMENT( bool, IsFocusable )
|
||||
|
||||
SLATE_EVENT( FOnGetContent, OnGetMenuContent )
|
||||
|
||||
/** The sound to play when the check box is checked */
|
||||
SLATE_ARGUMENT( TOptional<FSlateSound>, CheckedSoundOverride )
|
||||
|
||||
/** The sound to play when the check box is unchecked */
|
||||
SLATE_ARGUMENT( TOptional<FSlateSound>, UncheckedSoundOverride )
|
||||
|
||||
/** The sound to play when the check box is hovered */
|
||||
SLATE_ARGUMENT( TOptional<FSlateSound>, HoveredSoundOverride )
|
||||
|
||||
/** The unchecked image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UncheckedImage)
|
||||
|
||||
/** The unchecked hovered image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UncheckedHoveredImage)
|
||||
|
||||
/** The unchecked pressed image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UncheckedPressedImage)
|
||||
|
||||
/** The checked image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, CheckedImage)
|
||||
|
||||
/** The checked hovered image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, CheckedHoveredImage)
|
||||
|
||||
/** The checked pressed image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, CheckedPressedImage)
|
||||
|
||||
/** The undetermined image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UndeterminedImage)
|
||||
|
||||
/** The undetermined hovered image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UndeterminedHoveredImage)
|
||||
|
||||
/** The undetermined pressed image for the checkbox - overrides the style's */
|
||||
SLATE_ARGUMENT(const FSlateBrush*, UndeterminedPressedImage)
|
||||
|
||||
// This is the bit we're adding
|
||||
// SCheckBox is missing hovered/unhovered too
|
||||
SLATE_EVENT( FSimpleDelegate, OnHovered )
|
||||
SLATE_EVENT( FSimpleDelegate, OnUnhovered )
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusReceived )
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusLost )
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
void Construct( const FArguments& InArgs );
|
||||
|
||||
virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override;
|
||||
virtual void OnFocusLost(const FFocusEvent& InFocusEvent) override;
|
||||
|
||||
void SetOnFocusReceived(FSimpleDelegate InOnFocusReceived);
|
||||
void SetOnFocusLost(FSimpleDelegate InOnFocusLost);
|
||||
|
||||
protected:
|
||||
FSimpleDelegate OnHoveredDelegate;
|
||||
FSimpleDelegate OnUnhoveredDelegate;
|
||||
FSimpleDelegate OnFocusReceivedDelegate;
|
||||
FSimpleDelegate OnFocusLostDelegate;
|
||||
|
||||
|
||||
public:
|
||||
virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override;
|
||||
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
|
||||
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "SFocusableSlider.h"
|
||||
|
||||
void SFocusableSlider::Construct(const FArguments& InArgs)
|
||||
{
|
||||
// Call superclass, have to re-construct the args because they're not compatible
|
||||
SSlider::Construct(SSlider::FArguments()
|
||||
.Locked(InArgs._Locked)
|
||||
.Orientation(InArgs._Orientation)
|
||||
.Style(InArgs._Style)
|
||||
.Value(InArgs._Value)
|
||||
.MaxValue(InArgs._MaxValue)
|
||||
.MinValue(InArgs._MinValue)
|
||||
.IsFocusable(InArgs._IsFocusable)
|
||||
.IsEnabled(InArgs._IsEnabled)
|
||||
.IndentHandle(InArgs._IndentHandle)
|
||||
.MouseUsesStep(InArgs._MouseUsesStep)
|
||||
.RequiresControllerLock(InArgs._RequiresControllerLock)
|
||||
.Clipping(InArgs._Clipping)
|
||||
.Cursor(InArgs._Cursor)
|
||||
.Tag(InArgs._Tag)
|
||||
.Visibility(InArgs._Visibility)
|
||||
.AccessibleParams(InArgs._AccessibleParams)
|
||||
.AccessibleText(InArgs._AccessibleText)
|
||||
.ForceVolatile(InArgs._ForceVolatile)
|
||||
.RenderOpacity(InArgs._RenderOpacity)
|
||||
.RenderTransform(InArgs._RenderTransform)
|
||||
.RenderTransformPivot(InArgs._RenderTransformPivot)
|
||||
.ToolTip(InArgs._ToolTip)
|
||||
.ToolTipText(InArgs._ToolTipText)
|
||||
.OnValueChanged(InArgs._OnValueChanged)
|
||||
);
|
||||
|
||||
OnHoveredDelegate = InArgs._OnHovered;
|
||||
OnUnhoveredDelegate = InArgs._OnUnhovered;
|
||||
OnFocusReceivedDelegate = InArgs._OnFocusReceived;
|
||||
OnFocusLostDelegate = InArgs._OnFocusLost;
|
||||
}
|
||||
|
||||
|
||||
void SFocusableSlider::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
||||
{
|
||||
SSlider::OnMouseEnter(MyGeometry, MouseEvent);
|
||||
|
||||
// SSlider doesn't have hovered / unhovered events so we need to add them
|
||||
OnHoveredDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void SFocusableSlider::OnMouseLeave(const FPointerEvent& MouseEvent)
|
||||
{
|
||||
SSlider::OnMouseLeave(MouseEvent);
|
||||
|
||||
OnUnhoveredDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
FReply SFocusableSlider::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
auto Ret = SSlider::OnFocusReceived(MyGeometry, InFocusEvent);
|
||||
|
||||
OnFocusReceivedDelegate.ExecuteIfBound();
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
void SFocusableSlider::OnFocusLost(const FFocusEvent& InFocusEvent)
|
||||
{
|
||||
SSlider::OnFocusLost(InFocusEvent);
|
||||
|
||||
OnFocusLostDelegate.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void SFocusableSlider::SetOnFocusReceived(FSimpleDelegate InOnFocusReceived)
|
||||
{
|
||||
OnFocusReceivedDelegate = InOnFocusReceived;
|
||||
}
|
||||
|
||||
void SFocusableSlider::SetOnFocusLost(FSimpleDelegate InOnFocusLost)
|
||||
{
|
||||
OnFocusLostDelegate = InOnFocusLost;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Widgets/Input/SSlider.h"
|
||||
|
||||
|
||||
class SFocusableSlider : public SSlider
|
||||
{
|
||||
public:
|
||||
|
||||
// Seems in slate we have to duplicate all the args from superclass
|
||||
SLATE_BEGIN_ARGS(SFocusableSlider)
|
||||
: _IndentHandle(true)
|
||||
, _MouseUsesStep(false)
|
||||
, _RequiresControllerLock(true)
|
||||
, _Locked(false)
|
||||
, _Orientation(EOrientation::Orient_Horizontal)
|
||||
, _SliderBarColor(FLinearColor::White)
|
||||
, _SliderHandleColor(FLinearColor::White)
|
||||
, _Style(&FCoreStyle::Get().GetWidgetStyle<FSliderStyle>("Slider"))
|
||||
, _StepSize(0.01f)
|
||||
, _Value(1.f)
|
||||
, _MinValue(0.0f)
|
||||
, _MaxValue(1.0f)
|
||||
, _IsFocusable(true)
|
||||
, _OnMouseCaptureBegin()
|
||||
, _OnMouseCaptureEnd()
|
||||
, _OnValueChanged()
|
||||
{
|
||||
}
|
||||
|
||||
/** Whether the slidable area should be indented to fit the handle. */
|
||||
SLATE_ATTRIBUTE( bool, IndentHandle )
|
||||
|
||||
/** Sets new value if mouse position is greater/less than half the step size. */
|
||||
SLATE_ARGUMENT( bool, MouseUsesStep )
|
||||
|
||||
/** Sets whether we have to lock input to change the slider value. */
|
||||
SLATE_ARGUMENT( bool, RequiresControllerLock )
|
||||
|
||||
/** Whether the handle is interactive or fixed. */
|
||||
SLATE_ATTRIBUTE( bool, Locked )
|
||||
|
||||
/** The slider's orientation. */
|
||||
SLATE_ARGUMENT( EOrientation, Orientation)
|
||||
|
||||
/** The color to draw the slider bar in. */
|
||||
SLATE_ATTRIBUTE( FSlateColor, SliderBarColor )
|
||||
|
||||
/** The color to draw the slider handle in. */
|
||||
SLATE_ATTRIBUTE( FSlateColor, SliderHandleColor )
|
||||
|
||||
/** The style used to draw the slider. */
|
||||
SLATE_STYLE_ARGUMENT( FSliderStyle, Style )
|
||||
|
||||
/** The input mode while using the controller. */
|
||||
SLATE_ATTRIBUTE(float, StepSize)
|
||||
|
||||
/** A value that drives where the slider handle appears. Value is normalized between 0 and 1. */
|
||||
SLATE_ATTRIBUTE( float, Value )
|
||||
|
||||
/** The minimum value that can be specified by using the slider. */
|
||||
SLATE_ARGUMENT(float, MinValue)
|
||||
/** The maximum value that can be specified by using the slider. */
|
||||
SLATE_ARGUMENT(float, MaxValue)
|
||||
|
||||
/** Sometimes a slider should only be mouse-clickable and never keyboard focusable. */
|
||||
SLATE_ARGUMENT(bool, IsFocusable)
|
||||
|
||||
/** Invoked when the mouse is pressed and a capture begins. */
|
||||
SLATE_EVENT(FSimpleDelegate, OnMouseCaptureBegin)
|
||||
|
||||
/** Invoked when the mouse is released and a capture ends. */
|
||||
SLATE_EVENT(FSimpleDelegate, OnMouseCaptureEnd)
|
||||
|
||||
/** Invoked when the Controller is pressed and capture begins. */
|
||||
SLATE_EVENT(FSimpleDelegate, OnControllerCaptureBegin)
|
||||
|
||||
/** Invoked when the controller capture is released. */
|
||||
SLATE_EVENT(FSimpleDelegate, OnControllerCaptureEnd)
|
||||
|
||||
/** Called when the value is changed by the slider. */
|
||||
SLATE_EVENT( FOnFloatValueChanged, OnValueChanged )
|
||||
|
||||
// This is the bit we're adding
|
||||
// SCheckBox is missing hovered/unhovered too
|
||||
SLATE_EVENT( FSimpleDelegate, OnHovered )
|
||||
SLATE_EVENT( FSimpleDelegate, OnUnhovered )
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusReceived )
|
||||
SLATE_EVENT( FSimpleDelegate, OnFocusLost )
|
||||
|
||||
SLATE_END_ARGS()
|
||||
public:
|
||||
|
||||
void Construct( const FArguments& InArgs );
|
||||
|
||||
virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) override;
|
||||
virtual void OnFocusLost(const FFocusEvent& InFocusEvent) override;
|
||||
|
||||
void SetOnFocusReceived(FSimpleDelegate InOnFocusReceived);
|
||||
void SetOnFocusLost(FSimpleDelegate InOnFocusLost);
|
||||
|
||||
protected:
|
||||
FSimpleDelegate OnHoveredDelegate;
|
||||
FSimpleDelegate OnUnhoveredDelegate;
|
||||
FSimpleDelegate OnFocusReceivedDelegate;
|
||||
FSimpleDelegate OnFocusLostDelegate;
|
||||
|
||||
|
||||
public:
|
||||
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
|
||||
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/SmoothChangingProgressBar.h"
|
||||
|
||||
void USmoothChangingProgressBar::SetPercentSmoothly(float InPercent)
|
||||
{
|
||||
UnregisterTimer();
|
||||
|
||||
TargetPercent = InPercent;
|
||||
const float Curr = GetPercent();
|
||||
if (!FMath::IsNearlyEqual(Curr, TargetPercent))
|
||||
{
|
||||
if (FMath::IsNearlyZero(PercentChangeSpeed) || !MyProgressBar.IsValid())
|
||||
{
|
||||
SetPercent(InPercent);
|
||||
}
|
||||
else
|
||||
{
|
||||
SmoothChangeHandle = MyProgressBar->RegisterActiveTimer(
|
||||
PercentChangeFrequency,
|
||||
FWidgetActiveTimerDelegate::CreateUObject(this, &USmoothChangingProgressBar::TickPercent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void USmoothChangingProgressBar::StopSmoothPercentChange()
|
||||
{
|
||||
UnregisterTimer();
|
||||
}
|
||||
|
||||
void USmoothChangingProgressBar::BeginDestroy()
|
||||
{
|
||||
Super::BeginDestroy();
|
||||
|
||||
UnregisterTimer();
|
||||
}
|
||||
|
||||
void USmoothChangingProgressBar::UnregisterTimer()
|
||||
{
|
||||
if (SmoothChangeHandle.IsValid() && MyProgressBar.IsValid())
|
||||
{
|
||||
MyProgressBar->UnRegisterActiveTimer(SmoothChangeHandle.Pin().ToSharedRef());
|
||||
SmoothChangeHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
EActiveTimerReturnType USmoothChangingProgressBar::TickPercent(double CurrTime, float DeltaTime)
|
||||
{
|
||||
const float CurrPercent = GetPercent();
|
||||
const float Direction = FMath::Sign(TargetPercent - CurrPercent);
|
||||
const float Change = DeltaTime * Direction * PercentChangeSpeed;
|
||||
|
||||
const float NewPercent = Direction > 0
|
||||
? FMath::Min(CurrPercent + Change, TargetPercent)
|
||||
: FMath::Max(CurrPercent + Change, TargetPercent);
|
||||
SetPercent(NewPercent);
|
||||
|
||||
// Stop this if reached target (will unregister itself)
|
||||
if (FMath::IsNearlyEqual(TargetPercent, GetPercent()))
|
||||
{
|
||||
return EActiveTimerReturnType::Stop;
|
||||
}
|
||||
return EActiveTimerReturnType::Continue;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
|
||||
#include "StevesUI/StevesSubtitleTextblock.h"
|
||||
|
||||
#include "SubtitleManager.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
TSharedRef<SWidget> UStevesSubtitleTextblock::RebuildWidget()
|
||||
{
|
||||
auto Ret = Super::RebuildWidget();
|
||||
|
||||
if (!bSubscribed)
|
||||
{
|
||||
if (auto SM = FSubtitleManager::GetSubtitleManager())
|
||||
{
|
||||
SM->OnSetSubtitleText().AddUObject(this, &UStevesSubtitleTextblock::SetSubtitleText);
|
||||
bSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Ret;
|
||||
|
||||
}
|
||||
|
||||
void UStevesSubtitleTextblock::BeginDestroy()
|
||||
{
|
||||
Super::BeginDestroy();
|
||||
|
||||
if (bSubscribed)
|
||||
{
|
||||
if (auto SM = FSubtitleManager::GetSubtitleManager())
|
||||
{
|
||||
SM->OnSetSubtitleText().RemoveAll(this);
|
||||
bSubscribed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UStevesSubtitleTextblock::SetSubtitleText(const FText& InText)
|
||||
{
|
||||
// We get called even when subtitles are disabled
|
||||
if (UGameplayStatics::AreSubtitlesEnabled() || InText.IsEmptyOrWhitespace())
|
||||
{
|
||||
SetText(InText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI.h"
|
||||
|
||||
|
||||
|
||||
#include "StevesUI/FocusableUserWidget.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "Components/PanelWidget.h"
|
||||
#include "Components/Widget.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogStevesUI);
|
||||
|
||||
UWidget* FindWidgetFromSlate(SWidget* SW, UWidget* Parent)
|
||||
{
|
||||
if (!Parent)
|
||||
return nullptr;
|
||||
|
||||
if (Parent->GetCachedWidget().Get() == SW)
|
||||
return Parent;
|
||||
|
||||
auto PW = Cast<UPanelWidget>(Parent);
|
||||
if (PW)
|
||||
{
|
||||
for (int i = 0; i < PW->GetChildrenCount(); ++i)
|
||||
{
|
||||
const auto Found = FindWidgetFromSlate(SW, PW->GetChildAt(i));
|
||||
if (Found)
|
||||
return Found;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// User widgets aren't panels but can have their own tree
|
||||
auto UW = Cast<UUserWidget>(Parent);
|
||||
if (UW)
|
||||
{
|
||||
return FindWidgetFromSlate(SW, UW->WidgetTree->RootWidget);
|
||||
}
|
||||
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SetWidgetFocusProperly(UWidget* Widget)
|
||||
{
|
||||
auto FW = Cast<UFocusableUserWidget>(Widget);
|
||||
if (FW)
|
||||
FW->SetFocusProperly();
|
||||
else
|
||||
Widget->SetFocus();
|
||||
|
||||
}
|
||||
|
||||
const FKey* GetPreferedKeyMapping(const TArray<FKey>& AllKeys,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
EInputMode LastInputDevice,
|
||||
EInputMode LastButtonInputDevice,
|
||||
EInputMode LastAxisInputDevice)
|
||||
{
|
||||
// Same as GetPreferedActionOrAxisMapping, just with key directly
|
||||
const FKey* MouseMapping = nullptr;
|
||||
const FKey* KeyboardMapping = nullptr;
|
||||
const FKey* GamepadMapping = nullptr;
|
||||
for (const FKey& Key : AllKeys)
|
||||
{
|
||||
// notice how we take the LAST one in the list as the final version
|
||||
// this is because UInputSettings::GetActionMappingByName *reverses* the mapping list from Project Settings
|
||||
if (Key.IsGamepadKey())
|
||||
{
|
||||
GamepadMapping = &Key;
|
||||
}
|
||||
else if (Key.IsMouseButton()) // registers true for mouse axes too
|
||||
{
|
||||
MouseMapping = &Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyboardMapping = &Key;
|
||||
}
|
||||
}
|
||||
|
||||
const FKey* Preferred = nullptr;
|
||||
if (GamepadMapping && LastInputDevice == EInputMode::Gamepad)
|
||||
{
|
||||
// Always prefer gamepad if used last
|
||||
Preferred = GamepadMapping;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (DevicePreference)
|
||||
{
|
||||
// Auto should be pre-converted to another
|
||||
case EInputImageDevicePreference::Auto:
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Device Preference should have been converted before this call"))
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse:
|
||||
Preferred = KeyboardMapping ? KeyboardMapping : MouseMapping;
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Mouse_Keyboard:
|
||||
Preferred = MouseMapping ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button:
|
||||
// Use the latest button press
|
||||
Preferred = (MouseMapping && (LastButtonInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Axis:
|
||||
// Use the latest button press
|
||||
Preferred = (MouseMapping && (LastAxisInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Preferred;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "StevesHelperCommon.h"
|
||||
|
||||
class UWidget;
|
||||
class SWidget;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogStevesUI, Warning, Warning)
|
||||
|
||||
|
||||
/**
|
||||
* @brief Tries to locate a UMG widget which is using the speficied Slate widget as its native implementation
|
||||
* @param SW Slate widget
|
||||
* @param Parent Parent widget under which the UMG widget should be found
|
||||
* @return The UMG widget if found, otherwise nullptr
|
||||
*/
|
||||
UWidget* FindWidgetFromSlate(SWidget* SW, UWidget* Parent);
|
||||
|
||||
/**
|
||||
* @brief Set the focus to a given widget "properly", which means that if this is a widget derived
|
||||
* from UFocusableWidget, it calls SetFocusProperly on it which allows a customised implementation.
|
||||
* This is done because SetFocus() is not virtual and cannot be changed, and bIsFocusable seems to get
|
||||
* reset by Slate code even if I try to set it and then override Native methods.
|
||||
* @param Widget A UWidget
|
||||
*/
|
||||
void SetWidgetFocusProperly(UWidget* Widget);
|
||||
|
||||
template <typename T>
|
||||
const T* GetPreferedActionOrAxisMapping(const TArray<T>& AllMappings, const FName& Name,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
EInputMode LastInputDevice,
|
||||
EInputMode LastButtonInputDevice,
|
||||
EInputMode LastAxisInputDevice)
|
||||
{
|
||||
const T* MouseMapping = nullptr;
|
||||
const T* KeyboardMapping = nullptr;
|
||||
const T* GamepadMapping = nullptr;
|
||||
for (const T& ActionMap : AllMappings)
|
||||
{
|
||||
// notice how we take the LAST one in the list as the final version
|
||||
// this is because UInputSettings::GetActionMappingByName *reverses* the mapping list from Project Settings
|
||||
if (ActionMap.Key.IsGamepadKey())
|
||||
{
|
||||
GamepadMapping = &ActionMap;
|
||||
}
|
||||
else if (ActionMap.Key.IsMouseButton()) // registers true for mouse axes too
|
||||
{
|
||||
MouseMapping = &ActionMap;
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyboardMapping = &ActionMap;
|
||||
}
|
||||
}
|
||||
|
||||
const T* Preferred = nullptr;
|
||||
if (GamepadMapping && LastInputDevice == EInputMode::Gamepad)
|
||||
{
|
||||
// Always prefer gamepad if used last
|
||||
Preferred = GamepadMapping;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (DevicePreference)
|
||||
{
|
||||
// Auto should be pre-converted to another
|
||||
case EInputImageDevicePreference::Auto:
|
||||
UE_LOG(LogStevesUI, Error, TEXT("Device Preference should have been converted before this call"))
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse:
|
||||
Preferred = KeyboardMapping ? KeyboardMapping : MouseMapping;
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Mouse_Keyboard:
|
||||
Preferred = MouseMapping ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button:
|
||||
// Use the latest button press
|
||||
Preferred = (MouseMapping && (LastButtonInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Axis:
|
||||
// Use the latest button press
|
||||
Preferred = (MouseMapping && (LastAxisInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Preferred;
|
||||
}
|
||||
|
||||
|
||||
const FKey* GetPreferedKeyMapping(const TArray<FKey>& AllKeys,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
EInputMode LastInputDevice,
|
||||
EInputMode LastButtonInputDevice,
|
||||
EInputMode LastAxisInputDevice);
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/TabButton.h"
|
||||
|
||||
void UTabButton::Initialise(int InTabIndex,const FText& InLabel)
|
||||
{
|
||||
TabIndex=InTabIndex;
|
||||
SetTabLabel(InLabel);
|
||||
}
|
||||
|
||||
void UTabButton::SetTabLabel_Implementation(const FText& InLabel)
|
||||
{
|
||||
}
|
||||
|
||||
void UTabButton::SetTabSelected(bool bSelected)
|
||||
{
|
||||
bTabSelected=bSelected;
|
||||
RefreshForSelectionChanged();
|
||||
}
|
||||
|
||||
void UTabButton::RefreshForSelectionChanged_Implementation()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void UTabButton::NotifyTabSelected()
|
||||
{
|
||||
OnTabSelected.Broadcast(TabIndex);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUI/TabListWidget.h"
|
||||
#include "Components/HorizontalBoxSlot.h"
|
||||
#include "Components/WidgetSwitcher.h"
|
||||
#include "StevesUI/TabButton.h"
|
||||
#include "StevesUI/FocusableButton.h"
|
||||
#include "StevesUI/MenuBase.h"
|
||||
#include "Widgets/SPanel.h"
|
||||
|
||||
|
||||
void UTabListWidget::SetTargetWidgetSwitcher(UWidgetSwitcher* WidgetSwitcher)
|
||||
{
|
||||
TargetWidgetSwitcher=WidgetSwitcher;
|
||||
}
|
||||
|
||||
bool UTabListWidget::RegisterTab(const FName TabID, const TSubclassOf<UTabButton> ButtonWidgetType,FText Label,
|
||||
UWidget* ContentWidget)
|
||||
{
|
||||
|
||||
if (RegisteredTabs.Contains(TabID))
|
||||
{
|
||||
UE_LOG(LogTemp,Error,TEXT("UTabListWidget::RegisterTab Already Registered ID %s"),*TabID.ToString());
|
||||
return false;
|
||||
}
|
||||
if (TabButtonContainer==nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp,Error,TEXT("UTabListWidget::RegisterTab TabButtonContainer is not set"));
|
||||
return false;
|
||||
}
|
||||
FName const ButtonName( *FString::Printf(TEXT("TabButton_%s"),*TabID.ToString()));
|
||||
UTabButton* const NewTabButton = Cast<UTabButton>(CreateWidget(this,ButtonWidgetType,ButtonName));
|
||||
const int CurrentTabCount = RegisteredTabs.Num();
|
||||
const int NewTabIndex = CurrentTabCount;
|
||||
|
||||
FTabListRegisteredTabInfo TabInfo;
|
||||
TabInfo.TabIndex = NewTabIndex;
|
||||
TabInfo.TabButton = NewTabButton;
|
||||
TabInfo.ContentInstance = ContentWidget;
|
||||
RegisteredTabs.Add(TabID, TabInfo);
|
||||
UPanelSlot* ButtonPanelSlot=TabButtonContainer->AddChild(NewTabButton);
|
||||
UHorizontalBoxSlot* HorizontalBoxSlot=Cast<UHorizontalBoxSlot>(ButtonPanelSlot);
|
||||
if (HorizontalBoxSlot)
|
||||
{
|
||||
HorizontalBoxSlot->SetPadding(TabButtonPadding);
|
||||
}
|
||||
|
||||
NewTabButton->OnTabSelected.AddUniqueDynamic(this,&UTabListWidget::TabPressed);
|
||||
NewTabButton->TabIndex=NewTabIndex;
|
||||
NewTabButton->Initialise(NewTabIndex,Label);
|
||||
|
||||
// If first tab added, select this
|
||||
if (CurrentTabCount==0)
|
||||
{
|
||||
SelectTab(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UTabListWidget::TabPressed(int TabIndex)
|
||||
{
|
||||
SelectTab(TabIndex);
|
||||
}
|
||||
|
||||
|
||||
bool UTabListWidget::GetTabDataForIndex(const int Index,FTabListRegisteredTabInfo& TabInfo)
|
||||
{
|
||||
for (const auto& InfoPair : RegisteredTabs)
|
||||
{
|
||||
if (InfoPair.Value.TabIndex==Index)
|
||||
{
|
||||
TabInfo=InfoPair.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UTabListWidget::IncrementTabIndex(int Amount)
|
||||
{
|
||||
int NewTabIndex=SelectedTabIndex+Amount;
|
||||
while (NewTabIndex<0) NewTabIndex+=RegisteredTabs.Num();
|
||||
NewTabIndex%=RegisteredTabs.Num();
|
||||
SelectTab(NewTabIndex);
|
||||
|
||||
}
|
||||
void UTabListWidget::SelectTab(int TabIndex)
|
||||
{
|
||||
if (TabIndex==SelectedTabIndex) return; // No Change
|
||||
|
||||
// If a tab button has previously been selected, deselect
|
||||
if (SelectedTabIndex!=-1)
|
||||
{
|
||||
FTabListRegisteredTabInfo PreviousTabInfo;
|
||||
if (GetTabDataForIndex(SelectedTabIndex,PreviousTabInfo))
|
||||
{
|
||||
PreviousTabInfo.TabButton->SetTabSelected(false);
|
||||
} else
|
||||
{
|
||||
UE_LOG(LogTemp,Warning,TEXT("Couldn't find button for previously index %i"),SelectedTabIndex);
|
||||
}
|
||||
}
|
||||
SelectedTabIndex=TabIndex;
|
||||
FTabListRegisteredTabInfo CurrentTabInfo;
|
||||
if (GetTabDataForIndex(SelectedTabIndex,CurrentTabInfo))
|
||||
{
|
||||
CurrentTabInfo.TabButton->SetTabSelected(true);
|
||||
}
|
||||
|
||||
if (!TargetWidgetSwitcher)
|
||||
{
|
||||
UE_LOG(LogTemp,Warning,TEXT("TargetWidgetSwitcher not set"));
|
||||
return;
|
||||
}
|
||||
if (!CurrentTabInfo.ContentInstance)
|
||||
{
|
||||
UE_LOG(LogTemp,Warning,TEXT("Content not set for tab index %i"),SelectedTabIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a current content widget, cache the current focus so that it can be restored when moving back to this tab
|
||||
UWidget* PreviousContentWidget=TargetWidgetSwitcher->GetActiveWidget();
|
||||
if (PreviousContentWidget)
|
||||
{
|
||||
if (UFocusablePanel* TargetFocusableWidget=Cast<UFocusablePanel>(PreviousContentWidget))
|
||||
{
|
||||
TargetFocusableWidget->SavePreviousFocus();
|
||||
}
|
||||
}
|
||||
|
||||
TargetWidgetSwitcher->SetActiveWidget(CurrentTabInfo.ContentInstance);
|
||||
|
||||
// If this is an instance of UFocusableUserWidget, notify to activate default focus
|
||||
if (UFocusableUserWidget* TargetFocusableWidget=Cast<UFocusableUserWidget>(CurrentTabInfo.ContentInstance))
|
||||
{
|
||||
UE_LOG(LogTemp,Warning,TEXT("Focusable widget found for widget %s"),*CurrentTabInfo.ContentInstance->GetName());
|
||||
TargetFocusableWidget->SetFocusProperly();
|
||||
}else
|
||||
{
|
||||
UE_LOG(LogTemp,Warning,TEXT("Widget not focusable widget %s"),*CurrentTabInfo.ContentInstance->GetName());
|
||||
}
|
||||
// Notify
|
||||
OnContentWidgetChanged.Broadcast(PreviousContentWidget,CurrentTabInfo.ContentInstance);
|
||||
}
|
||||
|
||||
bool UTabListWidget::HandleKeyDownEvent(const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
Super::HandleKeyDownEvent(InKeyEvent);
|
||||
if (!this->GetIsEnabled()) return false; // Ignore if not enabled
|
||||
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
if (PreviousTabKeys.Contains(Key))
|
||||
{
|
||||
IncrementTabIndex(-1);
|
||||
return true;
|
||||
}
|
||||
if (NextTabKeys.Contains(Key))
|
||||
{
|
||||
IncrementTabIndex(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,660 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
// Original Copyright (c) Sam Bloomberg https://github.com/redxdev/UnrealRichTextDialogueBox (MIT License)
|
||||
|
||||
#include "StevesUI/TypewriterTextWidget.h"
|
||||
#include "Engine/Font.h"
|
||||
#include "Styling/SlateStyle.h"
|
||||
#include "Widgets/Text/SRichTextBlock.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
#include "Internationalization/TextChar.h"
|
||||
#endif
|
||||
|
||||
//PRAGMA_DISABLE_OPTIMIZATION
|
||||
|
||||
void URichTextBlockForTypewriter::ReleaseSlateResources(bool bReleaseChildren)
|
||||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
|
||||
TextLayout.Reset();
|
||||
TextMarshaller.Reset();
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> URichTextBlockForTypewriter::RebuildWidget()
|
||||
{
|
||||
// Copied from URichTextBlock::RebuildWidget
|
||||
UpdateStyleData();
|
||||
|
||||
TArray< TSharedRef< class ITextDecorator > > CreatedDecorators;
|
||||
CreateDecorators(CreatedDecorators);
|
||||
|
||||
TextMarshaller = FRichTextLayoutMarshaller::Create(CreateMarkupParser(), CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get());
|
||||
|
||||
MyRichTextBlock =
|
||||
SNew(SRichTextBlock)
|
||||
.TextStyle(bOverrideDefaultStyle ? &GetDefaultTextStyleOverride() : &GetDefaultTextStyle())
|
||||
.Marshaller(TextMarshaller)
|
||||
.CreateSlateTextLayout(
|
||||
FCreateSlateTextLayout::CreateWeakLambda(this, [this] (SWidget* InOwner, const FTextBlockStyle& InDefaultTextStyle) mutable
|
||||
{
|
||||
TextLayout = FSlateTextLayout::Create(InOwner, InDefaultTextStyle);
|
||||
return StaticCastSharedPtr<FSlateTextLayout>(TextLayout).ToSharedRef();
|
||||
}));
|
||||
|
||||
return MyRichTextBlock.ToSharedRef();
|
||||
}
|
||||
|
||||
UTypewriterTextWidget::UTypewriterTextWidget(const FObjectInitializer& ObjectInitializer) :
|
||||
Super(ObjectInitializer), LineText(nullptr), bHasMoreLineParts(0), SkipToLineEndCountdown(0)
|
||||
{
|
||||
bHasFinishedPlaying = true;
|
||||
}
|
||||
|
||||
|
||||
void UTypewriterTextWidget::ClearLetterCountdownTimer()
|
||||
{
|
||||
NextLetterCountdown = 0;
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::SetText(const FText& InText)
|
||||
{
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
ClearLetterCountdownTimer();
|
||||
|
||||
LineText->SetText(InText);
|
||||
|
||||
bHasFinishedPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
FText UTypewriterTextWidget::GetText() const
|
||||
{
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
return LineText->GetText();
|
||||
}
|
||||
|
||||
return FText();
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::PlayLine(const FText& InLine, float Speed)
|
||||
{
|
||||
CurrentLine = InLine;
|
||||
RemainingLinePart = CurrentLine.ToString();
|
||||
PlayNextLinePart(Speed);
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::PlayNextLinePart(float Speed)
|
||||
{
|
||||
check(GetWorld());
|
||||
|
||||
ClearLetterCountdownTimer();
|
||||
|
||||
CurrentRunName = "";
|
||||
CurrentLetterIndex = 0;
|
||||
CachedLetterIndex = 0;
|
||||
CurrentSegmentIndex = 0;
|
||||
MaxLetterIndex = 0;
|
||||
NumberOfLines = 0;
|
||||
CombinedTextHeight = 0;
|
||||
PauseTime = 0;
|
||||
CurrentPlaySpeed = Speed;
|
||||
Segments.Empty();
|
||||
CachedSegmentText.Empty();
|
||||
|
||||
if (RemainingLinePart.IsEmpty())
|
||||
{
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
LineText->SetText(FText::GetEmpty());
|
||||
}
|
||||
|
||||
bHasFinishedPlaying = true;
|
||||
OnTypewriterLineFinished.Broadcast(this);
|
||||
OnLineFinishedPlaying();
|
||||
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
LineText->SetVisibility(ESlateVisibility::Hidden);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
LineText->SetText(FText::GetEmpty());
|
||||
LineText->SetVisibility(ESlateVisibility::Hidden);
|
||||
}
|
||||
|
||||
|
||||
bHasMoreLineParts = false;
|
||||
bHasFinishedPlaying = false;
|
||||
|
||||
if (bFirstPlayLine)
|
||||
{
|
||||
// Delay the very first PlayLine after construction, CalculateWrappedString is not reliable until a couple
|
||||
// of UI geometry updates. At first the geometry is 0, then it's just wrong, and then finally it settles.
|
||||
|
||||
StartPlayLineCountdown = 0.2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
StartPlayLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::StartPlayLine()
|
||||
{
|
||||
CalculateWrappedString(RemainingLinePart);
|
||||
|
||||
if (MaxNumberOfLines > 0 && NumberOfLines > MaxNumberOfLines)
|
||||
{
|
||||
int MaxLength = CalculateMaxLength();
|
||||
int TerminatorIndex = FindLastTerminator(RemainingLinePart, MaxLength);
|
||||
int Length = TerminatorIndex + 1;
|
||||
const FString& FirstLinePart = RemainingLinePart.Left(Length);
|
||||
|
||||
CalculateWrappedString(FirstLinePart);
|
||||
|
||||
RemainingLinePart.RightChopInline(Length);
|
||||
bHasMoreLineParts = true;
|
||||
}
|
||||
|
||||
// Clear the lines - this is needed to prevent an occasional visible version of all lines for a single frame
|
||||
TSharedPtr<FSlateTextLayout> Layout = LineText->GetTextLayout();
|
||||
Layout->ClearLines();
|
||||
|
||||
NextLetterCountdown = NextLetterCountdownInterval = LetterPlayTime / CurrentPlaySpeed;
|
||||
|
||||
bFirstPlayLine = false;
|
||||
|
||||
PlayNextLetter();
|
||||
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::SkipToLineEnd()
|
||||
{
|
||||
// Clear all timers
|
||||
StartPlayLineCountdown = 0;
|
||||
ClearLetterCountdownTimer();
|
||||
|
||||
CurrentLetterIndex = MaxLetterIndex - 1;
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
LineText->SetText(FText::FromString(CalculateSegments(nullptr)));
|
||||
}
|
||||
|
||||
bHasFinishedPlaying = true;
|
||||
OnTypewriterLineFinished.Broadcast(this);
|
||||
OnLineFinishedPlaying();
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::FindWordVowels(const FString& Word, TArray<int>& VowelsPos)
|
||||
{
|
||||
static const FString Vowels = FString(TEXT("iaeouyIAEOUIY"));
|
||||
static const FString LetterY = FString(TEXT("yY"));
|
||||
static const FString LetterE = FString(TEXT("eE"));
|
||||
static const FString LetterR = FString(TEXT("rR"));
|
||||
|
||||
int32 Unused;
|
||||
int i = 0;
|
||||
int32 WordLen = Word.Len();
|
||||
|
||||
if (LetterY.FindChar(Word[0], Unused)) // Trim at start if "y" is at the beginning of the word
|
||||
{
|
||||
i = 1;
|
||||
}
|
||||
if (WordLen > 3 && LetterE.FindChar(Word[WordLen - 1], Unused) && (Vowels.
|
||||
FindChar(Word[WordLen - 3], Unused) || LetterR.FindChar(Word[WordLen - 3], Unused)))
|
||||
// Trim at end if word ending with "vowel-*-e" or "r-*-e"
|
||||
{
|
||||
WordLen -= 1;
|
||||
}
|
||||
while (i < WordLen)
|
||||
{
|
||||
if (Vowels.FindChar(Word[i], Unused))
|
||||
{
|
||||
VowelsPos.Add(i);
|
||||
if (i+1 < WordLen && Vowels.FindChar(Word[i+1], Unused)) // 2 vowel letters in consecutive
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (VowelsPos.IsEmpty())
|
||||
{
|
||||
VowelsPos.Add(Word.Len()/2);
|
||||
}
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||
{
|
||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
if (bPlayWhenPaused || !World->IsPaused())
|
||||
{
|
||||
// For replacement of timers to allow to run when paused
|
||||
if (NextLetterCountdown > 0)
|
||||
{
|
||||
NextLetterCountdown -= InDeltaTime;
|
||||
if (NextLetterCountdown <= 0)
|
||||
{
|
||||
NextLetterCountdown = NextLetterCountdownInterval; // Reset countdown
|
||||
PlayNextLetter();
|
||||
}
|
||||
}
|
||||
if (StartPlayLineCountdown > 0)
|
||||
{
|
||||
StartPlayLineCountdown -= InDeltaTime;
|
||||
if (StartPlayLineCountdown <= 0)
|
||||
{
|
||||
StartPlayLineCountdown = 0;
|
||||
StartPlayLine();
|
||||
}
|
||||
}
|
||||
if (SkipToLineEndCountdown > 0)
|
||||
{
|
||||
SkipToLineEndCountdown -= InDeltaTime;
|
||||
if (SkipToLineEndCountdown <= 0)
|
||||
{
|
||||
SkipToLineEndCountdown = 0;
|
||||
SkipToLineEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
bFirstPlayLine = true;
|
||||
}
|
||||
|
||||
int UTypewriterTextWidget::OnStartNewWord(const FString SegmentRemain)
|
||||
{
|
||||
FString NewWord;
|
||||
int i = 0;
|
||||
while (i < SegmentRemain.Len())
|
||||
{
|
||||
if (i == SegmentRemain.Len()-1)
|
||||
{
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
if (FTextChar::IsWhitespace(SegmentRemain[i]))
|
||||
#else
|
||||
if (FText::IsWhitespace(SegmentRemain[i]))
|
||||
#endif
|
||||
{
|
||||
bLastSegmentEndsWithBlank = true;
|
||||
NewWord = SegmentRemain.Mid(0, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
NewWord = SegmentRemain.Mid(0, i+1);
|
||||
}
|
||||
if (!NewWord.IsEmpty() && !(NewWord.Len() == 1 && IsPunctuation(NewWord[0])))
|
||||
{
|
||||
OnTypewriterStartWord.Broadcast(this, NewWord);
|
||||
}
|
||||
break;
|
||||
}
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
if (FTextChar::IsWhitespace(SegmentRemain[i]))
|
||||
#else
|
||||
if (FText::IsWhitespace(SegmentRemain[i]))
|
||||
#endif
|
||||
{
|
||||
NewWord = SegmentRemain.Mid(0, i);
|
||||
if (!NewWord.IsEmpty() && !(NewWord.Len() == 1 && IsPunctuation(NewWord[0])))
|
||||
{
|
||||
OnTypewriterStartWord.Broadcast(this, NewWord);
|
||||
}
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::PlayNextLetter()
|
||||
{
|
||||
// Incorporate pauses as a multiple of play timer (may not be exact but close enough)
|
||||
if (PauseTime > 0)
|
||||
{
|
||||
PauseTime -= LetterPlayTime/CurrentPlaySpeed;
|
||||
if (PauseTime > 0)
|
||||
return;
|
||||
}
|
||||
|
||||
FString NewRunName;
|
||||
const FString WrappedString = CalculateSegments(&NewRunName);
|
||||
if (IsValid(LineText))
|
||||
{
|
||||
LineText->SetText(FText::FromString(WrappedString));
|
||||
if (WrappedString.Len()>1) // For some reason this causes issues when just one char
|
||||
{
|
||||
LineText->SetVisibility(ESlateVisibility::Visible);
|
||||
}
|
||||
}
|
||||
|
||||
if (NewRunName != CurrentRunName)
|
||||
{
|
||||
CurrentRunName = NewRunName;
|
||||
OnTypewriterRunNameChanged.Broadcast(this, NewRunName);
|
||||
}
|
||||
|
||||
OnPlayLetter();
|
||||
OnTypewriterLetterAdded.Broadcast(this, WrappedString.Right(1));
|
||||
|
||||
// TODO: How do we keep indexing of text i18n-friendly?
|
||||
if (CurrentLetterIndex < MaxLetterIndex)
|
||||
{
|
||||
++CurrentLetterIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearLetterCountdownTimer();
|
||||
SkipToLineEndCountdown = EndHoldTime;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool UTypewriterTextWidget::IsSentenceTerminator(TCHAR Letter) const
|
||||
{
|
||||
int32 Unused;
|
||||
return SentenceTerminators.FindChar(Letter, Unused);
|
||||
}
|
||||
|
||||
bool UTypewriterTextWidget::IsClauseTerminator(TCHAR Letter) const
|
||||
{
|
||||
int32 Unused;
|
||||
return ClauseTerminators.FindChar(Letter, Unused);
|
||||
}
|
||||
|
||||
bool UTypewriterTextWidget::IsPunctuation(TCHAR Letter) const
|
||||
{
|
||||
static const FString Punctuations = FString(TEXT("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"));
|
||||
int32 Unused;
|
||||
return Punctuations.FindChar(Letter, Unused);
|
||||
}
|
||||
|
||||
int UTypewriterTextWidget::FindLastTerminator(const FString& CurrentLineString, int Count) const
|
||||
{
|
||||
int TerminatorIndex = CurrentLineString.FindLastCharByPredicate([this](TCHAR Char) { return IsSentenceTerminator(Char); }, Count);
|
||||
if (TerminatorIndex != INDEX_NONE)
|
||||
{
|
||||
return TerminatorIndex;
|
||||
}
|
||||
|
||||
TerminatorIndex = CurrentLineString.FindLastCharByPredicate([this](TCHAR Char) { return IsClauseTerminator(Char); }, Count);
|
||||
if (TerminatorIndex != INDEX_NONE)
|
||||
{
|
||||
return TerminatorIndex;
|
||||
}
|
||||
|
||||
TerminatorIndex = CurrentLineString.FindLastCharByPredicate(
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
FTextChar::IsWhitespace,
|
||||
#else
|
||||
FText::IsWhitespace,
|
||||
#endif
|
||||
Count);
|
||||
if (TerminatorIndex != INDEX_NONE)
|
||||
{
|
||||
return TerminatorIndex;
|
||||
}
|
||||
|
||||
return (Count - 1);
|
||||
}
|
||||
|
||||
int UTypewriterTextWidget::CalculateMaxLength()
|
||||
{
|
||||
int MaxLength = 0;
|
||||
int CurrentNumberOfLines = 1;
|
||||
for (int i = 0; i < Segments.Num(); i++)
|
||||
{
|
||||
const FTypewriterTextSegment& Segment = Segments[i];
|
||||
MaxLength += Segment.Text.Len();
|
||||
if (Segment.Text.Equals(FString(TEXT("\n"))))
|
||||
{
|
||||
CurrentNumberOfLines++;
|
||||
if (MaxNumberOfLines > 0 && CurrentNumberOfLines > MaxNumberOfLines)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MaxLength;
|
||||
}
|
||||
|
||||
void UTypewriterTextWidget::CalculateWrappedString(const FString& CurrentLineString)
|
||||
{
|
||||
// Rich Text views give you:
|
||||
// - A blank block at the start for some reason
|
||||
// - One block per line (newline characters stripped)
|
||||
// - Split for different runs (decorators)
|
||||
// - The newlines we add are the only newlines in the output so that's the number of lines
|
||||
// If we've got here, that means the text isn't empty so 1 line at least
|
||||
NumberOfLines = 1;
|
||||
MaxLetterIndex = 0;
|
||||
CombinedTextHeight = 0;
|
||||
Segments.Empty();
|
||||
if (IsValid(LineText) && LineText->GetTextLayout().IsValid())
|
||||
{
|
||||
TSharedPtr<FSlateTextLayout> Layout = LineText->GetTextLayout();
|
||||
TSharedPtr<FRichTextLayoutMarshaller> Marshaller = LineText->GetTextMarshaller();
|
||||
|
||||
const FGeometry& TextBoxGeometry = LineText->GetCachedGeometry();
|
||||
const FVector2D TextBoxSize = TextBoxGeometry.GetLocalSize();
|
||||
|
||||
Layout->ClearLines();
|
||||
Layout->SetWrappingWidth(TextBoxSize.X);
|
||||
Marshaller->SetText(CurrentLineString, *Layout.Get());
|
||||
Layout->UpdateLayout();
|
||||
|
||||
bool bHasWrittenText = false;
|
||||
auto Views = Layout->GetLineViews();
|
||||
for (int v = 0; v < Views.Num(); ++v)
|
||||
{
|
||||
const FTextLayout::FLineView& View = Views[v];
|
||||
|
||||
for (int b = 0; b < View.Blocks.Num(); ++b)
|
||||
{
|
||||
TSharedRef<ILayoutBlock> Block = View.Blocks[b];
|
||||
TSharedRef<IRun> Run = Block->GetRun();
|
||||
|
||||
FTypewriterTextSegment Segment;
|
||||
Run->AppendTextTo(Segment.Text, Block->GetTextRange());
|
||||
|
||||
// HACK: For some reason image decorators (and possibly other decorators that don't
|
||||
// have actual text inside them) result in the run containing a zero width space instead of
|
||||
// nothing. This messes up our checks for whether the text is empty or not, which doesn't
|
||||
// have an effect on image decorators but might cause issues for other custom ones.
|
||||
// Instead of emptying text, which might have some unknown effect, just mark it as empty
|
||||
const bool bTextIsEmpty = Segment.Text.IsEmpty() ||
|
||||
(Segment.Text.Len() == 1 && Segment.Text[0] == 0x200B);
|
||||
const int TextLen = bTextIsEmpty ? 0 : Segment.Text.Len();
|
||||
const bool bRunIsEmpty = Segment.RunInfo.Name.IsEmpty();
|
||||
|
||||
Segment.RunInfo = Run->GetRunInfo();
|
||||
Segments.Add(Segment);
|
||||
|
||||
// A segment with a named run should still take up time for the typewriter effect.
|
||||
MaxLetterIndex += FMath::Max(TextLen, Segment.RunInfo.Name.IsEmpty() ? 0 : 1);
|
||||
|
||||
if (!bTextIsEmpty || !bRunIsEmpty)
|
||||
{
|
||||
bHasWrittenText = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bHasWrittenText)
|
||||
{
|
||||
CombinedTextHeight += View.TextHeight;
|
||||
}
|
||||
// Add check for an unnecessary newline after ever line even if there's nothing else to do, otherwise
|
||||
// we end up inserting a newline after a simple single line of text
|
||||
const bool bHasMoreText = v < (Views.Num() - 1);
|
||||
if (bHasWrittenText && bHasMoreText)
|
||||
{
|
||||
Segments.Add(FTypewriterTextSegment{TEXT("\n")});
|
||||
++NumberOfLines;
|
||||
++MaxLetterIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Layout->SetWrappingWidth(0);
|
||||
|
||||
// Set the desired vertical size so that we're already the size we need to accommodate all lines
|
||||
// Without this, a flexible box size will grow as lines are added
|
||||
FVector2D Sz = GetMinimumDesiredSize();
|
||||
Sz.Y = CombinedTextHeight;
|
||||
SetMinimumDesiredSize(Sz);
|
||||
|
||||
LineText->SetText(LineText->GetText());
|
||||
}
|
||||
else
|
||||
{
|
||||
Segments.Add(FTypewriterTextSegment{CurrentLineString});
|
||||
MaxLetterIndex = Segments[0].Text.Len();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FString UTypewriterTextWidget::CalculateSegments(FString* OutCurrentRunName)
|
||||
{
|
||||
FString Result = CachedSegmentText;
|
||||
|
||||
int32 Idx = CachedLetterIndex;
|
||||
while (Idx <= CurrentLetterIndex && CurrentSegmentIndex < Segments.Num())
|
||||
{
|
||||
const FTypewriterTextSegment& Segment = Segments[CurrentSegmentIndex];
|
||||
if (!Segment.RunInfo.Name.IsEmpty())
|
||||
{
|
||||
Result += FString::Printf(TEXT("<%s"), *Segment.RunInfo.Name);
|
||||
|
||||
if (Segment.RunInfo.MetaData.Num() > 0)
|
||||
{
|
||||
for (const TTuple<FString, FString>& MetaData : Segment.RunInfo.MetaData)
|
||||
{
|
||||
Result += FString::Printf(TEXT(" %s=\"%s\""), *MetaData.Key, *MetaData.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (Segment.Text.IsEmpty())
|
||||
{
|
||||
Result += TEXT("/>");
|
||||
++Idx; // This still takes up an index for the typewriter effect.
|
||||
}
|
||||
else
|
||||
{
|
||||
Result += TEXT(">");
|
||||
}
|
||||
}
|
||||
|
||||
bool bIsSegmentComplete = true;
|
||||
if (!Segment.Text.IsEmpty())
|
||||
{
|
||||
int32 LettersLeft = CurrentLetterIndex - Idx + 1;
|
||||
bIsSegmentComplete = LettersLeft >= Segment.Text.Len();
|
||||
LettersLeft = FMath::Min(LettersLeft, Segment.Text.Len());
|
||||
Idx += LettersLeft;
|
||||
|
||||
Result += Segment.Text.Mid(0, LettersLeft);
|
||||
|
||||
if (bNewWordEvent)
|
||||
{
|
||||
if (LettersLeft == 1)
|
||||
{
|
||||
if (CurrentSegmentIndex == 0) // First letter in a line
|
||||
{
|
||||
NextBlankLetterLeft = LettersLeft + OnStartNewWord(Segment.Text.Mid(LettersLeft-1, Segment.Text.Len()-LettersLeft+1));
|
||||
}
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
if (bLastSegmentEndsWithBlank && !FTextChar::IsWhitespace(Segment.Text[LettersLeft-1]))
|
||||
#else
|
||||
if (bLastSegmentEndsWithBlank && !FText::IsWhitespace(Segment.Text[LettersLeft-1]))
|
||||
#endif
|
||||
{
|
||||
bLastSegmentEndsWithBlank = false;
|
||||
NextBlankLetterLeft = LettersLeft + OnStartNewWord(Segment.Text.Mid(LettersLeft-1, Segment.Text.Len()-LettersLeft+1));
|
||||
}
|
||||
}
|
||||
if (LettersLeft-1 == NextBlankLetterLeft)
|
||||
{
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
if (!FTextChar::IsWhitespace(Segment.Text[LettersLeft-1])) // Current letter is not a blank
|
||||
#else
|
||||
if (!FText::IsWhitespace(Segment.Text[LettersLeft-1])) // Current letter is not a blank
|
||||
#endif
|
||||
{
|
||||
NextBlankLetterLeft = LettersLeft + OnStartNewWord(Segment.Text.Mid(LettersLeft-1, Segment.Text.Len()-LettersLeft+1));
|
||||
}
|
||||
else
|
||||
{
|
||||
NextBlankLetterLeft = LettersLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add pause for sentence ends
|
||||
if (Result.Len() > 0 &&
|
||||
IsSentenceTerminator(Result[Result.Len() - 1]) &&
|
||||
CurrentLetterIndex < MaxLetterIndex - 1) // Don't pause on the last letter, that's the end pause's job
|
||||
{
|
||||
// Look ahead to make sure we only pause on LAST sentence terminator in a chain of them,
|
||||
// and also optionally not if there isn't whitespace after (e.g. to not pause on ".txt")
|
||||
if (LettersLeft < Segment.Text.Len())
|
||||
{
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7
|
||||
const bool bIsWhitespace = FTextChar::IsWhitespace(Segment.Text[LettersLeft]);
|
||||
#else
|
||||
const bool bIsWhitespace = FText::IsWhitespace(Segment.Text[LettersLeft]);
|
||||
#endif
|
||||
|
||||
if (!IsSentenceTerminator(Segment.Text[LettersLeft]) &&
|
||||
(!bPauseOnlyIfWhitespaceFollowsSentenceTerminator || bIsWhitespace))
|
||||
{
|
||||
PauseTime = PauseTimeAtSentenceTerminators;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Segment.RunInfo.Name.IsEmpty())
|
||||
{
|
||||
Result += TEXT("</>");
|
||||
}
|
||||
if (OutCurrentRunName)
|
||||
{
|
||||
*OutCurrentRunName = Segment.RunInfo.Name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (bIsSegmentComplete)
|
||||
{
|
||||
CachedLetterIndex = Idx;
|
||||
CachedSegmentText = Result;
|
||||
++CurrentSegmentIndex;
|
||||
NextBlankLetterLeft = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
//PRAGMA_ENABLE_OPTIMIZATION
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesVisualLogger.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "EngineStats.h"
|
||||
#include "Stats/Stats.h"
|
||||
|
||||
|
||||
void FStevesVisualLogger::InternalPolyLogfImpl(const UObject* Object,
|
||||
const FLogCategoryBase& Category,
|
||||
ELogVerbosity::Type Verbosity,
|
||||
const TArray<FVector>& Points,
|
||||
const FColor& Color,
|
||||
const uint16 Thickness)
|
||||
{
|
||||
#if ENABLE_VISUAL_LOG
|
||||
const FName CategoryName = Category.GetCategoryName();
|
||||
|
||||
SCOPE_CYCLE_COUNTER(STAT_VisualLog); \
|
||||
UWorld *World = nullptr; \
|
||||
FVisualLogEntry *CurrentEntry = nullptr; \
|
||||
if (FVisualLogger::CheckVisualLogInputInternal(Object, CategoryName, Verbosity, &World, &CurrentEntry) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// EVisualLoggerShapeElement::Path doesn't support a label, so description is always blank
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
CurrentEntry->AddPath(Points, CategoryName, Verbosity, Color, "", Thickness);
|
||||
#else
|
||||
CurrentEntry->AddElement(Points, CategoryName, Verbosity, Color, "", Thickness);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "StevesAssetHelpers.generated.h"
|
||||
|
||||
class UObjectLibrary;
|
||||
|
||||
/// Class to help out with asset related tasks, mostly C++ only but defined as a BPL in case that's useful later
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesAssetHelpers : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Find the soft object paths of blueprints matching an object library definition, in a given set of paths.
|
||||
* @param InPaths The asset paths to search. Must be game-directory form e.g. /Game/Data
|
||||
* @param ObjectLibrary The object library which defines the (super)class which you're looking for. You should
|
||||
* have created this similar to: `UObjectLibrary::CreateLibrary(UYourAssetClass::StaticClass(), true, GIsEditor && !IsRunningCommandlet());`
|
||||
* @param OutSoftPaths Output array of soft object paths to the blueprints found. You can resolve these using their ResolveObject() function.
|
||||
* @returns The number of blueprint assets found
|
||||
*/
|
||||
static int FindBlueprintSoftPaths(const TArray<FDirectoryPath>& InPaths, UObjectLibrary* ObjectLibrary, TArray<FSoftObjectPath>& OutSoftPaths);
|
||||
/**
|
||||
* Find the soft object paths of blueprints matching an object library definition, in a given set of paths.
|
||||
* @param InPaths The asset paths to search. Must be game-directory form e.g. /Game/Data
|
||||
* @param ObjectLibrary The object library which defines the (super)class which you're looking for. You should
|
||||
* have created this similar to: `UObjectLibrary::CreateLibrary(UYourAssetClass::StaticClass(), true, GIsEditor && !IsRunningCommandlet());`
|
||||
* @param OutSoftPaths Output array of soft object paths to the blueprints found. You can resolve these using their ResolveObject() function.
|
||||
* @returns The number of blueprint assets found
|
||||
*/
|
||||
static int FindBlueprintSoftPaths(const TArray<FString>& InPaths, UObjectLibrary* ObjectLibrary, TArray<FSoftObjectPath>& OutSoftPaths);
|
||||
|
||||
/**
|
||||
* Find a list of loaded classes for blueprints matching an object library definition, in a given set of paths.
|
||||
* @param InPaths The asset paths to search. Must be game-directory form e.g. /Game/Data
|
||||
* @param ObjectLibrary The object library which defines the (super)class which you're looking for. You should
|
||||
* have created this similar to: `UObjectLibrary::CreateLibrary(UYourAssetClass::StaticClass(), true, GIsEditor && !IsRunningCommandlet());`
|
||||
* @param OutClasses Output array of loaded UClass objects representing the blueprints found.
|
||||
* @returns The number of blueprint assets found
|
||||
*/
|
||||
static int FindBlueprintClasses(const TArray<FDirectoryPath>& InPaths, UObjectLibrary* ObjectLibrary, TArray<UClass*>& OutClasses);
|
||||
/**
|
||||
* Find the soft object paths of blueprints matching an object library definition, in a given set of paths.
|
||||
* @param InPaths The asset paths to search. Must be game-directory form e.g. /Game/Data
|
||||
* @param ObjectLibrary The object library which defines the (super)class which you're looking for. You should
|
||||
* have created this similar to: `UObjectLibrary::CreateLibrary(UYourAssetClass::StaticClass(), true, GIsEditor && !IsRunningCommandlet());`
|
||||
* @param OutClasses Output array of loaded UClass objects representing the blueprints found.
|
||||
* @returns The number of blueprint assets found
|
||||
*/
|
||||
static int FindBlueprintClasses(const TArray<FString>& InPaths, UObjectLibrary* ObjectLibrary, TArray<UClass*>& OutClasses);
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "StevesBalancedRandomStream.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
|
||||
#include "StevesMathHelpers.h"
|
||||
#include "Components/PanelSlot.h"
|
||||
#include "StevesBPL.generated.h"
|
||||
|
||||
class UPanelWidget;
|
||||
class UWidget;
|
||||
/**
|
||||
* Blueprint library exposing various things in a Blueprint-friendly way e.g. using by-value FVectors so they can
|
||||
* be entered directly if required, rather than const& as in C++. Also use degrees not radians.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesBPL : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Return whether a sphere overlaps a cone
|
||||
* @param ConeOrigin Origin of the cone
|
||||
* @param ConeDir Direction of the cone, must be normalised
|
||||
* @param ConeAngle Angle of the cone, in degrees
|
||||
* @param Distance Length of the cone
|
||||
* @param SphereCentre Centre of the sphere
|
||||
* @param SphereRadius Radius of the sphere
|
||||
* @return True if the sphere overlaps the cone
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Math")
|
||||
static bool SphereOverlapCone(FVector ConeOrigin, FVector ConeDir, float ConeAngle, float Distance, FVector SphereCentre, float SphereRadius)
|
||||
{
|
||||
return StevesMathHelpers::SphereOverlapCone(ConeOrigin, ConeDir, FMath::DegreesToRadians(ConeAngle*0.5f), Distance, SphereCentre, SphereRadius);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set the focus to a given widget "properly", which means that if this is a widget derived
|
||||
* from UFocusableWidget, it calls SetFocusProperly on it which allows a customised implementation.
|
||||
* @param Widget The widget to give focus to
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|UI")
|
||||
static void SetWidgetFocus(UWidget* Widget);
|
||||
|
||||
/**
|
||||
* Insert a child widget at a specific index
|
||||
* @param Parent The container widget
|
||||
* @param Child The child widget to add
|
||||
* @param AtIndex The index at which the new child should exist
|
||||
* @returns The slot the child was inserted at
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|UI")
|
||||
static UPanelSlot* InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="StevesUEHelpers|Random", meta=(NativeMakeFunc))
|
||||
static FStevesBalancedRandomStream MakeBalancedRandomStream(int64 Seed);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Random")
|
||||
static float BalancedRandom(FStevesBalancedRandomStream& Stream) { return Stream.Rand(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Random")
|
||||
static FVector2D BalancedRandom2D(FStevesBalancedRandomStream& Stream) { return Stream.Rand2D(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Random")
|
||||
static FVector BalancedRandom3D(FStevesBalancedRandomStream& Stream) { return Stream.Rand3D(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Random")
|
||||
static FVector BalancedRandomVector(FStevesBalancedRandomStream& Stream) { return Stream.RandUnitVector(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Random")
|
||||
static FVector BalancedRandomPointInBox(FStevesBalancedRandomStream& Stream, const FVector& Min, const FVector& Max) { return Stream.RandPointInBox(FBox(Min, Max)); }
|
||||
|
||||
|
||||
/**
|
||||
* Let the content streaming system know that there is a viewpoint other than a possessed camera that should be taken
|
||||
* into account when deciding what to stream in. This can be useful when you're using a scene capture component,
|
||||
* which if it's capturing a scene that isn't close to a player, can result in blurry textures.
|
||||
* @param ViewOrigin The world space view point
|
||||
* @param ScreenWidth The width in pixels of the screen being rendered
|
||||
* @param FOV Horizontal field of view, in degrees
|
||||
* @param BoostFactor How much to boost the LOD by (1 being normal, higher being higher detail)
|
||||
* @param bOverrideLocation Whether this is an override location, which forces the streaming system to ignore all other regular locations
|
||||
* @param Duration How long the streaming system should keep checking this location (in seconds). 0 means just for the next Tick.
|
||||
* @param ActorToBoost Optional pointer to an actor who's textures should have their streaming priority boosted
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Streaming")
|
||||
static void AddViewOriginToStreaming(const FVector& ViewOrigin,
|
||||
float ScreenWidth,
|
||||
float FOV,
|
||||
float BoostFactor = 1.0f,
|
||||
bool bOverrideLocation = false,
|
||||
float Duration = 0.0f,
|
||||
AActor* ActorToBoost = nullptr);
|
||||
|
||||
|
||||
/**
|
||||
* Force content streaming to update. Useful if you need things to stream in sooner than usual.
|
||||
* @param DeltaTime The amount of time to tell the streaming system that has passed.
|
||||
* @param bBlockUntilDone If true, this call will not return until the streaming system has updated
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Streaming")
|
||||
static void UpdateStreaming(float DeltaTime, bool bBlockUntilDone = false);
|
||||
|
||||
/// Calculate the perceived luminance of a colour using ITU BT.709 standard
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Colour")
|
||||
static float GetPerceivedLuminance(const FLinearColor& InColour);
|
||||
|
||||
/// Calculate the perceived luminance of a colour using ITU BT.601 standard (more R & B)
|
||||
UFUNCTION(BlueprintCallable, Category="StevesUEHelpers|Colour")
|
||||
static float GetPerceivedLuminance2(const FLinearColor& InColour);
|
||||
|
||||
|
||||
/**
|
||||
* Equivalent to FVector::HeadingAngle but for FVector2D
|
||||
* @param Dir Input direction in 2D, does not need to be normalised
|
||||
* @return 'Heading' angle between +/-PI. 0 is pointing down +X.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category="StevesUEHelpers|Math")
|
||||
static float HeadingAngle2D(const FVector2D& Dir);
|
||||
|
||||
/**
|
||||
* Find the angle between two 2D vectors
|
||||
* @param DirA Input direction in 2D, does not need to be normalised
|
||||
* @param DirB Input direction in 2D, does not need to be normalised
|
||||
* @return Difference in heading angle, between +/-PI, positive is anti-clockwise. 0 means directions match.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category="StevesUEHelpers|Math")
|
||||
static float AngleBetween2D(const FVector2D& DirA, const FVector2D& DirB);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="StevesUEHelpers|Project")
|
||||
static FString GetProjectVersion();
|
||||
};
|
||||
@@ -0,0 +1,929 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Math/Halton.h"
|
||||
#include "StevesBalancedRandomStream.generated.h"
|
||||
|
||||
// Credit to Andrew Wilmott for lots of the algorithms here
|
||||
// See https://github.com/andrewwillmott/distribute-lib
|
||||
// Used under Unlicense
|
||||
|
||||
namespace StevesRandConstants
|
||||
{
|
||||
constexpr uint32 kSafeMaxSeed2D = 43046721 - 1;
|
||||
constexpr uint32 kSafeMaxSeed3D = 9765625 - 1;
|
||||
constexpr float kOneOverThree = 1.0f / 3.0f;
|
||||
constexpr float kOneOverFive = 1.0f / 5.0f;
|
||||
|
||||
}
|
||||
|
||||
/// "Balanced" random stream, using the Halton Sequence
|
||||
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
|
||||
/// This is a generic stream which can do 1D, 2D and 3D sequences. If you only need a single type, it's
|
||||
/// more efficient to use FStevesBalancedRandomStream1D/2D/3D
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesBalancedRandomStream
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
uint32 InitialSeed = 0;
|
||||
uint32 Seed = 0;
|
||||
uint32 Base3Seed = 0;
|
||||
uint32 Base5Seed = 0;
|
||||
|
||||
static int32 SafeSeed(uint32 InSeed)
|
||||
{
|
||||
// Halton sequence gets unstable when seed gets too high, especially with higher bases
|
||||
while (InSeed > StevesRandConstants::kSafeMaxSeed3D)
|
||||
{
|
||||
InSeed -= StevesRandConstants::kSafeMaxSeed3D;
|
||||
}
|
||||
return InSeed;
|
||||
}
|
||||
|
||||
void UpdateSeeds()
|
||||
{
|
||||
// For simplicity this version just derives the other seeds from the main one rather than
|
||||
// calculating the sequence value at the same time like Andrew's does
|
||||
Base3Seed = 0;
|
||||
|
||||
// Average iterations: 1.5
|
||||
for (int i = 0, k = Seed; k; i += 2, k /= 3)
|
||||
{
|
||||
const int d = (k % 3);
|
||||
Base3Seed |= d << i;
|
||||
}
|
||||
|
||||
Base5Seed = 0;
|
||||
|
||||
// Average iterations: 2.5
|
||||
for (int i = 0, k = Seed; k; i += 3, k /= 5)
|
||||
{
|
||||
const int d = (k % 5);
|
||||
Base5Seed |= d << i;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 SafeSeedInc()
|
||||
{
|
||||
Seed = SafeSeed(Seed + 1);
|
||||
|
||||
UpdateSeeds();
|
||||
return Seed;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
FStevesBalancedRandomStream()
|
||||
: InitialSeed(0)
|
||||
, Seed(0)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
FStevesBalancedRandomStream( uint32 InSeed )
|
||||
{
|
||||
Initialize(InSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
FStevesBalancedRandomStream( FName InName )
|
||||
{
|
||||
Initialize(InName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream with the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
void Initialize( uint32 InSeed )
|
||||
{
|
||||
InitialSeed = SafeSeed(InSeed);
|
||||
Seed = InitialSeed;
|
||||
|
||||
UpdateSeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream using the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
void Initialize( FName InName )
|
||||
{
|
||||
uint32 StartSeed;
|
||||
if (InName != NAME_None)
|
||||
{
|
||||
StartSeed = GetTypeHash(InName.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
StartSeed = FPlatformTime::Cycles();
|
||||
}
|
||||
Initialize(StartSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this random stream to the initial seed value.
|
||||
*/
|
||||
void Reset()
|
||||
{
|
||||
Initialize(InitialSeed);
|
||||
}
|
||||
|
||||
uint32 GetInitialSeed() const
|
||||
{
|
||||
return InitialSeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new random seed.
|
||||
*/
|
||||
void GenerateNewSeed()
|
||||
{
|
||||
Initialize(SafeSeed(FMath::Rand()));
|
||||
}
|
||||
|
||||
|
||||
/// Return a value between 0..1, inclusive
|
||||
float Rand()
|
||||
{
|
||||
return Halton(SafeSeedInc(), 2);
|
||||
}
|
||||
|
||||
/// Return a 2D value with each element between 0..1, inclusive
|
||||
/// Use this rather than calling Rand() twice to ensure balanced distribution
|
||||
FVector2D Rand2D()
|
||||
{
|
||||
const float X = Halton(Seed, 2);
|
||||
const float Y = Halton(Base3Seed, 3);
|
||||
|
||||
SafeSeedInc();
|
||||
|
||||
return FVector2D(X, Y);
|
||||
}
|
||||
|
||||
/// Return a 3D value with each element between 0..1, inclusive
|
||||
/// Use this rather than calling Rand() twice to ensure balanced distribution
|
||||
FVector Rand3D()
|
||||
{
|
||||
const float X = Halton(Seed, 2);
|
||||
const float Y = Halton(Base3Seed, 3);
|
||||
const float Z = Halton(Base5Seed, 5);
|
||||
SafeSeedInc();
|
||||
return FVector(X, Y, Z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random vector of unit size.
|
||||
*
|
||||
* @return Random unit vector.
|
||||
*/
|
||||
FVector RandUnitVector()
|
||||
{
|
||||
const FVector2D PitchYaw = Rand2D();
|
||||
return FRotator(PitchYaw.X, PitchYaw.Y, 0).RotateVector(FVector::UpVector);
|
||||
}
|
||||
|
||||
/// Random point in a 3D box
|
||||
FORCEINLINE FVector RandPointInBox(const FBox& Box)
|
||||
{
|
||||
const FVector R3 = Rand3D();
|
||||
return FVector(FMath::Lerp(Box.Min.X, Box.Max.X, R3.X),
|
||||
FMath::Lerp(Box.Min.Y, Box.Max.Y, R3.Y),
|
||||
FMath::Lerp(Box.Min.Z, Box.Max.Z, R3.Z));
|
||||
}
|
||||
|
||||
/// Random point in a 2D rectangle
|
||||
FORCEINLINE FVector2D RandPointInBox2D(const FBox2D& Rect)
|
||||
{
|
||||
const FVector2D R2 = Rand2D();
|
||||
return FVector2D(FMath::Lerp(Rect.Min.X, Rect.Max.X, R2.X),
|
||||
FMath::Lerp(Rect.Min.Y, Rect.Max.Y, R2.Y));
|
||||
}
|
||||
|
||||
/// Random point in a circle
|
||||
FORCEINLINE FVector2D RandPointInCircle(float Radius = 1.0)
|
||||
{
|
||||
// Just use rejection sampling for simplicity / speed
|
||||
while (true)
|
||||
{
|
||||
const FVector2D Candidate = Rand2D();
|
||||
if (Candidate.SquaredLength() <= 1.0)
|
||||
{
|
||||
return Candidate * Radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Random point in a sphere
|
||||
FORCEINLINE FVector RandPointInSphere(float Radius = 1.0)
|
||||
{
|
||||
// Just use rejection sampling for simplicity / speed
|
||||
while (true)
|
||||
{
|
||||
const FVector Candidate = Rand3D();
|
||||
if (Candidate.SquaredLength() <= 1.0)
|
||||
{
|
||||
return Candidate * Radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Random value in a range (inclusive)
|
||||
float RandRange(float Min, float Max)
|
||||
{
|
||||
return FMath::Lerp(Min, Max, Rand());
|
||||
}
|
||||
|
||||
/// Random colour value
|
||||
FLinearColor RandColour(const FLinearColor& From, const FLinearColor& To)
|
||||
{
|
||||
return FLinearColor::LerpUsingHSV(From, To, Rand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current seed.
|
||||
*
|
||||
* @return Current seed.
|
||||
*/
|
||||
uint32 GetCurrentSeed() const
|
||||
{
|
||||
return Seed;
|
||||
}
|
||||
|
||||
|
||||
FString ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("FStevesBalancedRandomStream(InitialSeed=%u, Seed=%u)"), InitialSeed, Seed);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
/// "Balanced" random stream, using the Halton Sequence, one dimension only (more efficient for this than FStevesBalancedRandomStream)
|
||||
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesBalancedRandomStream1D
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
uint32 InitialSeed = 0;
|
||||
uint32 Seed = 0;
|
||||
|
||||
public:
|
||||
|
||||
FStevesBalancedRandomStream1D()
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
FStevesBalancedRandomStream1D( uint32 InSeed )
|
||||
{
|
||||
Initialize(InSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
FStevesBalancedRandomStream1D( FName InName )
|
||||
{
|
||||
Initialize(InName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream with the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
void Initialize( uint32 InSeed )
|
||||
{
|
||||
InitialSeed = Seed = InSeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream using the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
void Initialize( FName InName )
|
||||
{
|
||||
uint32 StartSeed;
|
||||
if (InName != NAME_None)
|
||||
{
|
||||
StartSeed = GetTypeHash(InName.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
StartSeed = FPlatformTime::Cycles();
|
||||
}
|
||||
Initialize(StartSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this random stream to the initial seed value.
|
||||
*/
|
||||
void Reset()
|
||||
{
|
||||
Initialize(InitialSeed);
|
||||
}
|
||||
|
||||
uint32 GetInitialSeed() const
|
||||
{
|
||||
return InitialSeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new random seed.
|
||||
*/
|
||||
void GenerateNewSeed()
|
||||
{
|
||||
Initialize(FMath::Rand());
|
||||
}
|
||||
|
||||
|
||||
/// Return a value between 0..1, inclusive
|
||||
FORCEINLINE float Rand()
|
||||
{
|
||||
return Halton(Seed++, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for rand implementations.
|
||||
*
|
||||
* @return A random number in [0..A)
|
||||
*/
|
||||
FORCEINLINE int32 RandHelper( int32 A )
|
||||
{
|
||||
// GetFraction guarantees a result in the [0,1) range.
|
||||
return ((A > 0) ? FMath::TruncToInt(Rand() * float(A)) : 0);
|
||||
}
|
||||
|
||||
/// Random float value in a range (inclusive)
|
||||
FORCEINLINE float RandRange(float Min, float Max)
|
||||
{
|
||||
return FMath::Lerp(Min, Max, Rand());
|
||||
}
|
||||
|
||||
/// Random int value in a range (inclusive)
|
||||
FORCEINLINE int32 RandRange( int32 Min, int32 Max )
|
||||
{
|
||||
const int32 Range = (Max - Min) + 1;
|
||||
|
||||
return Min + RandHelper(Range);
|
||||
}
|
||||
|
||||
/// Random colour value
|
||||
FORCEINLINE FLinearColor RandColour(const FLinearColor& From, const FLinearColor& To)
|
||||
{
|
||||
return FLinearColor::LerpUsingHSV(From, To, Rand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current seed.
|
||||
*
|
||||
* @return Current seed.
|
||||
*/
|
||||
FORCEINLINE uint32 GetCurrentSeed() const
|
||||
{
|
||||
return Seed;
|
||||
}
|
||||
|
||||
|
||||
FString ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("FStevesBalancedRandomStream1D(InitialSeed=%u, Seed=%u)"), InitialSeed, Seed);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/// "Balanced" 2D random stream, using the Halton Sequence. More efficient than the general FStevesBalancedRandomStream for 2D work
|
||||
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesBalancedRandomStream2D
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
uint32 InitialSeed = 0;
|
||||
uint32 Base2Seed = 0;
|
||||
uint32 Base3Seed = 0;
|
||||
|
||||
|
||||
FVector2f CurrentValue = FVector2f::ZeroVector;
|
||||
|
||||
|
||||
public:
|
||||
|
||||
FStevesBalancedRandomStream2D()
|
||||
{
|
||||
Initialize(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
FStevesBalancedRandomStream2D( uint32 InSeed )
|
||||
{
|
||||
Initialize(InSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
FStevesBalancedRandomStream2D( FName InName )
|
||||
{
|
||||
Initialize(InName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream with the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
void Initialize( uint32 InSeed )
|
||||
{
|
||||
// Halton sequence gets unstable when seed gets too high, especially with higher bases
|
||||
while (InSeed >= StevesRandConstants::kSafeMaxSeed2D)
|
||||
{
|
||||
InSeed -= StevesRandConstants::kSafeMaxSeed2D;
|
||||
}
|
||||
|
||||
InitialSeed = Base2Seed = InSeed;
|
||||
|
||||
CurrentValue.X = Halton(Base2Seed, 2);
|
||||
|
||||
CurrentValue.Y = 0;
|
||||
Base3Seed = 0;
|
||||
|
||||
float ip = StevesRandConstants::kOneOverThree;
|
||||
float p = ip;
|
||||
|
||||
for (int i = 0, k = Base2Seed; k; i += 2, k /= 3)
|
||||
{
|
||||
int d = (k % 3);
|
||||
Base3Seed |= d << i;
|
||||
CurrentValue.Y += d * p;
|
||||
p *= ip;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream using the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
void Initialize( FName InName )
|
||||
{
|
||||
uint32 StartSeed;
|
||||
if (InName != NAME_None)
|
||||
{
|
||||
StartSeed = GetTypeHash(InName.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
StartSeed = FPlatformTime::Cycles();
|
||||
}
|
||||
Initialize(StartSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this random stream to the initial seed value.
|
||||
*/
|
||||
void Reset()
|
||||
{
|
||||
Initialize(InitialSeed);
|
||||
}
|
||||
|
||||
uint32 GetInitialSeed() const
|
||||
{
|
||||
return InitialSeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new random seed.
|
||||
*/
|
||||
void GenerateNewSeed()
|
||||
{
|
||||
Initialize(FMath::Rand());
|
||||
}
|
||||
|
||||
/// Return a 2D value with each element between 0..1, inclusive
|
||||
FVector2f Rand2D()
|
||||
{
|
||||
// Wrap back to 0 at end of safe range
|
||||
if (Base2Seed >= StevesRandConstants::kSafeMaxSeed2D)
|
||||
{
|
||||
Initialize(0);
|
||||
}
|
||||
|
||||
// This uses Andrew Wilmott's approach of calculating the next value at the same time as incrementing
|
||||
// We calculate the new value while initialising / incrementing, so it's currently correct
|
||||
const FVector2f Ret = CurrentValue;
|
||||
|
||||
/////////////////////////////////////
|
||||
// base 2
|
||||
|
||||
uint32_t OldBase2Seed = Base2Seed;
|
||||
Base2Seed++;
|
||||
uint32_t Diff = Base2Seed ^ OldBase2Seed;
|
||||
|
||||
// bottom bit always changes, higher bits
|
||||
// change less frequently.
|
||||
float s = 0.5f;
|
||||
|
||||
// Diff will be of the form 0 * 1 +, i.e. one bits up until the last carry.
|
||||
// expected iterations = 1 + 0.5 + 0.25 + ... = 2
|
||||
do
|
||||
{
|
||||
if (OldBase2Seed & 1)
|
||||
CurrentValue.X -= s;
|
||||
else
|
||||
CurrentValue.X += s;
|
||||
|
||||
s *= 0.5f;
|
||||
|
||||
Diff = Diff >> 1;
|
||||
OldBase2Seed = OldBase2Seed >> 1;
|
||||
}
|
||||
while (Diff);
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
// base 3: use 2 bits for each base 3 digit.
|
||||
|
||||
uint32_t Mask = 0x3; // also the max base 3 digit
|
||||
uint32_t Add = 0x1; // amount to Add to force carry once digit==3
|
||||
s = StevesRandConstants::kOneOverThree;
|
||||
|
||||
Base3Seed++;
|
||||
|
||||
// expected iterations: 1.5
|
||||
while (true)
|
||||
{
|
||||
if ((Base3Seed & Mask) == Mask)
|
||||
{
|
||||
Base3Seed += Add; // force carry into next 2-bit digit
|
||||
CurrentValue.Y -= 2 * s;
|
||||
|
||||
Mask = Mask << 2;
|
||||
Add = Add << 2;
|
||||
|
||||
s *= StevesRandConstants::kOneOverThree;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentValue.Y += s; // we know digit n has gone from a to a + 1
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random vector of unit size.
|
||||
*
|
||||
* @return Random unit vector.
|
||||
*/
|
||||
FVector RandUnitVector()
|
||||
{
|
||||
const FVector2f PitchYaw = Rand2D();
|
||||
return FRotator(PitchYaw.X, PitchYaw.Y, 0).RotateVector(FVector::UpVector);
|
||||
}
|
||||
|
||||
|
||||
/// Random point in a 2D rectangle
|
||||
FORCEINLINE FVector2f RandPointInBox2D(const FBox2D& Rect)
|
||||
{
|
||||
const FVector2f R2 = Rand2D();
|
||||
return FVector2f(FMath::Lerp(Rect.Min.X, Rect.Max.X, R2.X),
|
||||
FMath::Lerp(Rect.Min.Y, Rect.Max.Y, R2.Y));
|
||||
}
|
||||
|
||||
/// Random point in a circle
|
||||
FORCEINLINE FVector2f RandPointInCircle(float Radius = 1.0)
|
||||
{
|
||||
// Just use rejection sampling for simplicity / speed
|
||||
while (true)
|
||||
{
|
||||
const FVector2f Candidate = Rand2D();
|
||||
if (Candidate.SquaredLength() <= 1.0)
|
||||
{
|
||||
return Candidate * Radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current seed.
|
||||
*
|
||||
* @return Current seed.
|
||||
*/
|
||||
uint32 GetCurrentSeed() const
|
||||
{
|
||||
return Base2Seed;
|
||||
}
|
||||
|
||||
|
||||
FString ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("FStevesBalancedRandomStream2D(InitialSeed=%u, Seed=%u)"), InitialSeed, Base2Seed);
|
||||
}
|
||||
};
|
||||
|
||||
/// "Balanced" random 3D stream, using the Halton Sequence. Optimised for 3D only, more efficient than FStevesBalancedRandomStream
|
||||
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesBalancedRandomStream3D
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
uint32 InitialSeed = 0;
|
||||
uint32 Base2Seed = 0;
|
||||
uint32 Base3Seed = 0;
|
||||
uint32 Base5Seed = 0;
|
||||
|
||||
FVector3f CurrentValue = FVector3f::ZeroVector;
|
||||
public:
|
||||
|
||||
FStevesBalancedRandomStream3D()
|
||||
{
|
||||
Initialize(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
FStevesBalancedRandomStream3D( uint32 InSeed )
|
||||
{
|
||||
Initialize(InSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a new random stream from the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
FStevesBalancedRandomStream3D( FName InName )
|
||||
{
|
||||
Initialize(InName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream with the specified seed value.
|
||||
*
|
||||
* @param InSeed The seed value.
|
||||
*/
|
||||
void Initialize( uint32 InSeed )
|
||||
{
|
||||
while (InSeed > StevesRandConstants::kSafeMaxSeed3D)
|
||||
{
|
||||
InSeed -= StevesRandConstants::kSafeMaxSeed3D;
|
||||
}
|
||||
|
||||
InitialSeed = Base2Seed = InSeed;
|
||||
|
||||
CurrentValue.X = Halton(Base2Seed, 2);
|
||||
|
||||
CurrentValue.Y = 0.0f;
|
||||
Base3Seed = 0;
|
||||
|
||||
float p = StevesRandConstants::kOneOverThree;
|
||||
|
||||
for (int i = 0, k = Base2Seed; k; i += 2, k /= 3)
|
||||
{
|
||||
int d = (k % 3);
|
||||
Base3Seed |= d << i;
|
||||
CurrentValue.Y += d * p;
|
||||
p *= StevesRandConstants::kOneOverThree;
|
||||
}
|
||||
|
||||
CurrentValue.Z = 0.0f;
|
||||
Base5Seed = 0;
|
||||
|
||||
p = StevesRandConstants::kOneOverFive;
|
||||
|
||||
for (int i = 0, k = Base2Seed; k; i += 3, k /= 5)
|
||||
{
|
||||
int d = (k % 5);
|
||||
Base5Seed |= d << i;
|
||||
CurrentValue.Z += d * p;
|
||||
p *= StevesRandConstants::kOneOverFive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this random stream using the specified name.
|
||||
*
|
||||
* @note If NAME_None is provided, the stream will be seeded using the current time.
|
||||
* @param InName The name value from which the stream will be initialized.
|
||||
*/
|
||||
void Initialize( FName InName )
|
||||
{
|
||||
uint32 StartSeed;
|
||||
if (InName != NAME_None)
|
||||
{
|
||||
StartSeed = GetTypeHash(InName.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
StartSeed = FPlatformTime::Cycles();
|
||||
}
|
||||
Initialize(StartSeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this random stream to the initial seed value.
|
||||
*/
|
||||
void Reset()
|
||||
{
|
||||
Initialize(InitialSeed);
|
||||
}
|
||||
|
||||
uint32 GetInitialSeed() const
|
||||
{
|
||||
return Base2Seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new random seed.
|
||||
*/
|
||||
void GenerateNewSeed()
|
||||
{
|
||||
Initialize(FMath::Rand());
|
||||
}
|
||||
|
||||
/// Return a 3D value with each element between 0..1, inclusive
|
||||
FVector Rand3D()
|
||||
{
|
||||
if (Base2Seed >= StevesRandConstants::kSafeMaxSeed3D)
|
||||
{
|
||||
Initialize(0);
|
||||
}
|
||||
|
||||
// This uses Andrew Wilmott's approach of calculating the next value at the same time as incrementing
|
||||
// We calculate the new value while initialising / incrementing, so it's currently correct
|
||||
const FVector3f Ret = CurrentValue;
|
||||
|
||||
// base 2: 1 bit per digit
|
||||
uint32_t OldBase2 = Base2Seed;
|
||||
Base2Seed++;
|
||||
uint32_t Diff = Base2Seed ^ OldBase2;
|
||||
|
||||
// bottom bit always changes, higher bits
|
||||
// change less frequently.
|
||||
float s = 0.5f;
|
||||
|
||||
// diff will be of the form 0 * 1 + , i.e. one bits up until the last carry.
|
||||
// expected iterations = 1 + 0.5 + 0.25 + ... = 2
|
||||
do
|
||||
{
|
||||
if (OldBase2 & 1)
|
||||
CurrentValue.X -= s;
|
||||
else
|
||||
CurrentValue.X += s;
|
||||
|
||||
s *= 0.5f;
|
||||
|
||||
Diff = Diff >> 1;
|
||||
OldBase2 = OldBase2 >> 1;
|
||||
}
|
||||
while (Diff);
|
||||
|
||||
|
||||
// base 3: use 2 bits for each base 3 digit.
|
||||
uint32_t Mask = 0x3; // also the max base 3 digit
|
||||
uint32_t Add = 0x1; // amount to add to force carry once digit==3
|
||||
s = StevesRandConstants::kOneOverThree;
|
||||
|
||||
Base3Seed++;
|
||||
|
||||
// expected iterations: 1.5
|
||||
while (true)
|
||||
{
|
||||
if ((Base3Seed & Mask) == Mask)
|
||||
{
|
||||
Base3Seed += Add; // force carry into next 2-bit digit
|
||||
CurrentValue.Y -= 2 * s;
|
||||
|
||||
Mask = Mask << 2;
|
||||
Add = Add << 2;
|
||||
|
||||
s *= StevesRandConstants::kOneOverThree;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentValue.Y += s; // we know digit n has gone from a to a + 1
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// base 5: use 3 bits for each base 5 digit.
|
||||
Mask = 0x7;
|
||||
Add = 0x3; // amount to add to force carry once digit==dmax
|
||||
uint32_t Dmax = 0x5; // max digit
|
||||
|
||||
s = StevesRandConstants::kOneOverFive;
|
||||
|
||||
Base5Seed++;
|
||||
|
||||
// expected iterations: 1.25
|
||||
while (true)
|
||||
{
|
||||
if ((Base5Seed & Mask) == Dmax)
|
||||
{
|
||||
Base5Seed += Add; // force carry into next 3-bit digit
|
||||
CurrentValue.Z -= 4 * s;
|
||||
|
||||
Mask = Mask << 3;
|
||||
Dmax = Dmax << 3;
|
||||
Add = Add << 3;
|
||||
|
||||
s *= StevesRandConstants::kOneOverFive;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentValue.Z += s; // we know digit n has gone from a to a + 1
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return FVector(Ret.X, Ret.Y, Ret.Z);
|
||||
}
|
||||
|
||||
|
||||
/// Random point in a 3D box
|
||||
FORCEINLINE FVector RandPointInBox(const FBox& Box)
|
||||
{
|
||||
const FVector R3 = Rand3D();
|
||||
return FVector(FMath::Lerp(Box.Min.X, Box.Max.X, R3.X),
|
||||
FMath::Lerp(Box.Min.Y, Box.Max.Y, R3.Y),
|
||||
FMath::Lerp(Box.Min.Z, Box.Max.Z, R3.Z));
|
||||
}
|
||||
|
||||
|
||||
/// Random point in a sphere
|
||||
FORCEINLINE FVector RandPointInSphere(float Radius = 1.0)
|
||||
{
|
||||
// Just use rejection sampling for simplicity / speed
|
||||
while (true)
|
||||
{
|
||||
const FVector Candidate = Rand3D();
|
||||
if (Candidate.SquaredLength() <= 1.0)
|
||||
{
|
||||
return Candidate * Radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current seed.
|
||||
*
|
||||
* @return Current seed.
|
||||
*/
|
||||
uint32 GetCurrentSeed() const
|
||||
{
|
||||
return InitialSeed;
|
||||
}
|
||||
|
||||
|
||||
FString ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("FStevesBalancedRandomStream(InitialSeed=%u, Seed=%u)"), InitialSeed, Base2Seed);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "DebugRenderSceneProxy.h"
|
||||
|
||||
/**
|
||||
* An extension to FDebugRenderSceneProxy to support other shapes, e.g. circles and arcs
|
||||
*/
|
||||
class FStevesDebugRenderSceneProxy : public FDebugRenderSceneProxy
|
||||
{
|
||||
public:
|
||||
FStevesDebugRenderSceneProxy(const UPrimitiveComponent* InComponent)
|
||||
: FDebugRenderSceneProxy(InComponent)
|
||||
{
|
||||
}
|
||||
|
||||
STEVESUEHELPERS_API virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily,
|
||||
uint32 VisibilityMap, FMeshElementCollector& Collector) const override;
|
||||
|
||||
STEVESUEHELPERS_API virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override;
|
||||
|
||||
struct FDebugCircle
|
||||
{
|
||||
FDebugCircle(const FVector& InCentre, const FVector& InX, const FVector& InY, float InRadius, int InNumSegments,
|
||||
const FColor& InColor, float InThickness = 0) :
|
||||
Centre(InCentre),
|
||||
X(InX),
|
||||
Y(InY),
|
||||
Radius(InRadius),
|
||||
NumSegments(InNumSegments),
|
||||
Color(InColor),
|
||||
Thickness(InThickness)
|
||||
{
|
||||
}
|
||||
|
||||
FVector Centre;
|
||||
FVector X;
|
||||
FVector Y;
|
||||
float Radius;
|
||||
int NumSegments;
|
||||
FColor Color;
|
||||
float Thickness;
|
||||
};
|
||||
|
||||
/// An arc which is a section of a circle
|
||||
struct FDebugArc
|
||||
{
|
||||
FDebugArc(const FVector& InCentre, const FVector& InX, const FVector& InY, float InMinAngle, float InMaxAngle,
|
||||
float InRadius, int InNumSegments, const FColor& InColor) :
|
||||
Centre(InCentre),
|
||||
X(InX),
|
||||
Y(InY),
|
||||
MinAngle(InMinAngle),
|
||||
MaxAngle(InMaxAngle),
|
||||
Radius(InRadius),
|
||||
NumSegments(InNumSegments),
|
||||
Color(InColor)
|
||||
{
|
||||
}
|
||||
|
||||
FVector Centre;
|
||||
FVector X;
|
||||
FVector Y;
|
||||
float MinAngle;
|
||||
float MaxAngle;
|
||||
float Radius;
|
||||
int NumSegments;
|
||||
FColor Color;
|
||||
};
|
||||
|
||||
/// Replacement for FWireCylinder because that's garbage and doesn't reflect the component transform
|
||||
struct FDebugCylinder
|
||||
{
|
||||
FDebugCylinder(const FVector &InCentre, const FVector& InX, const FVector& InY, const FVector& InZ, const float InRadius, const float InHalfHeight, int InNumSegments, const FColor &InColor) :
|
||||
Centre(InCentre),
|
||||
X(InX), Y(InY), Z(InZ),
|
||||
Radius(InRadius),
|
||||
HalfHeight(InHalfHeight),
|
||||
NumSegments(InNumSegments),
|
||||
Color(InColor)
|
||||
{
|
||||
}
|
||||
|
||||
FVector Centre;
|
||||
FVector X;
|
||||
FVector Y;
|
||||
FVector Z;
|
||||
float Radius;
|
||||
float HalfHeight;
|
||||
int NumSegments;
|
||||
FColor Color;
|
||||
};
|
||||
|
||||
struct FDebugMesh
|
||||
{
|
||||
FDebugMesh(const FMatrix& InLocalToWorld,
|
||||
const TArray<FDynamicMeshVertex>& InVertices,
|
||||
const TArray<uint32>& InIndices,
|
||||
const FColor& InColor)
|
||||
: LocalToWorld(InLocalToWorld),
|
||||
Vertices(InVertices),
|
||||
Indices(InIndices),
|
||||
Color(InColor)
|
||||
{
|
||||
}
|
||||
|
||||
FMatrix LocalToWorld;
|
||||
TArray<FDynamicMeshVertex> Vertices;
|
||||
TArray <uint32> Indices;
|
||||
FColor Color;
|
||||
};
|
||||
|
||||
TArray<FDebugCircle> Circles;
|
||||
TArray<FDebugArc> Arcs;
|
||||
TArray<FDebugCylinder> CylindersImproved; // Because we need our own
|
||||
TArray<FCapsule> CapsulesImproved; // Because we need our own
|
||||
TArray<FDebugMesh> MeshesImproved; // Because we need our own
|
||||
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "NavMesh/NavMeshBoundsVolume.h"
|
||||
#include "StevesDynamicNavMeshVolume.generated.h"
|
||||
|
||||
/// NavMesh bounds that can be changed at runtime (requires that your NavMesh data Runtime Generation is set to Dynamic)
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API AStevesDynamicNavMeshVolume : public ANavMeshBoundsVolume
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AStevesDynamicNavMeshVolume();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="DynamicNavMesh")
|
||||
void SetLocationAndDimensions(const FVector& Location, const FVector& NewDimensions);
|
||||
UFUNCTION(BlueprintCallable, Category="DynamicNavMesh")
|
||||
void SetDimensions(const FVector& NewDimensions);
|
||||
|
||||
protected:
|
||||
void UpdateDimensions(const FVector& NewDimensions);
|
||||
void NotifyNavSystem();
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
|
||||
#include "StevesEasings.generated.h"
|
||||
|
||||
/// Easing functions
|
||||
/// See https://easings.net/
|
||||
/// Could have used UE EEasingFunc but it's missing some nice options like back/elastic
|
||||
UENUM(BlueprintType)
|
||||
enum class EStevesEaseFunction : uint8
|
||||
{
|
||||
Linear,
|
||||
EaseIn_Sine,
|
||||
EaseOut_Sine,
|
||||
EaseInOut_Sine,
|
||||
EaseIn_Quad,
|
||||
EaseOut_Quad,
|
||||
EaseInOut_Quad,
|
||||
EaseIn_Cubic,
|
||||
EaseOut_Cubic,
|
||||
EaseInOut_Cubic,
|
||||
EaseIn_Quart,
|
||||
EaseOut_Quart,
|
||||
EaseInOut_Quart,
|
||||
EaseIn_Quint,
|
||||
EaseOut_Quint,
|
||||
EaseInOut_Quint,
|
||||
EaseIn_Expo,
|
||||
EaseOut_Expo,
|
||||
EaseInOut_Expo,
|
||||
EaseIn_Circ,
|
||||
EaseOut_Circ,
|
||||
EaseInOut_Circ,
|
||||
EaseIn_Back,
|
||||
EaseOut_Back,
|
||||
EaseInOut_Back,
|
||||
EaseIn_Elastic,
|
||||
EaseOut_Elastic,
|
||||
EaseInOut_Elastic,
|
||||
EaseIn_Bounce,
|
||||
EaseOut_Bounce,
|
||||
EaseInOut_Bounce
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesEasings : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Convert a linear alpha value into an eased alpha value using an easing function
|
||||
* @param InAlpha The input linear alpha
|
||||
* @param Func The easing function
|
||||
* @return The eased version of the alpha
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static float EaseAlpha(float InAlpha, EStevesEaseFunction Func);
|
||||
|
||||
/**
|
||||
* Interpolate with easing function support
|
||||
* @param A Input Value from
|
||||
* @param B Input Value to
|
||||
* @param Alpha Value between 0 and 1
|
||||
* @param Func Easing function
|
||||
* @return Interpolated value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static float EaseFloat(float A, float B, float Alpha, EStevesEaseFunction Func)
|
||||
{
|
||||
return A + EaseAlpha(Alpha, Func) * (B - A);
|
||||
}
|
||||
/**
|
||||
* Interpolate with easing function support
|
||||
* @param A Input Value from
|
||||
* @param B Input Value to
|
||||
* @param Alpha Value between 0 and 1
|
||||
* @param Func Easing function
|
||||
* @return Interpolated value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static FVector EaseVector(const FVector& A, const FVector& B, float Alpha, EStevesEaseFunction Func)
|
||||
{
|
||||
return A + EaseAlpha(Alpha, Func) * (B - A);
|
||||
}
|
||||
/**
|
||||
* Interpolate with easing function support
|
||||
* @param A Input Value from
|
||||
* @param B Input Value to
|
||||
* @param Alpha Value between 0 and 1
|
||||
* @param Func Easing function
|
||||
* @return Interpolated value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static FRotator EaseRotator(const FRotator& A, const FRotator& B, float Alpha, EStevesEaseFunction Func, bool bShortest)
|
||||
{
|
||||
if (bShortest)
|
||||
return EaseQuat(FQuat(A), FQuat(B), Alpha, Func).Rotator();
|
||||
|
||||
return A + EaseAlpha(Alpha, Func) * (B - A);
|
||||
}
|
||||
/**
|
||||
* Interpolate with easing function support
|
||||
* @param A Input Value from
|
||||
* @param B Input Value to
|
||||
* @param Alpha Value between 0 and 1
|
||||
* @param Func Easing function
|
||||
* @return Interpolated value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static FQuat EaseQuat(const FQuat& A, const FQuat& B, float Alpha, EStevesEaseFunction Func)
|
||||
{
|
||||
return FQuat::Slerp(A, B, EaseAlpha(Alpha, Func));
|
||||
}
|
||||
/**
|
||||
* Interpolate with easing function support
|
||||
* @param A Input Value from
|
||||
* @param B Input Value to
|
||||
* @param Alpha Value between 0 and 1
|
||||
* @param Func Easing function
|
||||
* @return Interpolated value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesEaseMath")
|
||||
static FTransform EaseTransform(const FTransform& A, const FTransform& B, float Alpha, EStevesEaseFunction Func)
|
||||
{
|
||||
return FTransform(
|
||||
EaseQuat(A.GetRotation(), B.GetRotation(), Alpha, Func),
|
||||
EaseVector(A.GetLocation(), B.GetLocation(), Alpha, Func),
|
||||
EaseVector(A.GetScale3D(), B.GetScale3D(), Alpha, Func));
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,348 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
#include "StevesEditorVisComponent.generated.h"
|
||||
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisLine
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Start location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisLine")
|
||||
FVector Start;
|
||||
/// End location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisLine")
|
||||
FVector End;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisLine")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisLine(const FVector& InStart, const FVector& InEnd,
|
||||
const FColor& InColour)
|
||||
: Start(InStart),
|
||||
End(InEnd),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisLine():
|
||||
Start(FVector::ZeroVector),
|
||||
End(FVector(100, 0, 0)),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisCircle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCircle")
|
||||
FVector Location;
|
||||
/// Rotation relative to component; circles will be rendered in the X/Y plane
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCircle")
|
||||
FRotator Rotation;
|
||||
/// Circle radius
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCircle")
|
||||
float Radius;
|
||||
/// The number of line segments to render the circle with
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCircle")
|
||||
int NumSegments;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCircle")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisCircle(const FVector& InLocation, const FRotator& InRotation, float InRadius, int InNumSegments,
|
||||
const FColor& InColour)
|
||||
: Location(InLocation),
|
||||
Rotation(InRotation),
|
||||
Radius(InRadius),
|
||||
NumSegments(InNumSegments),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisCircle():
|
||||
Location(FVector::ZeroVector),
|
||||
Rotation(FRotator::ZeroRotator),
|
||||
Radius(50), NumSegments(12),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisArc
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
FVector Location;
|
||||
/// Rotation relative to component; arcs will be rendered in the X/Y plane
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
FRotator Rotation;
|
||||
/// Minimum angle to render arc from
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
float MinAngle;
|
||||
/// Maximum angle to render arc to
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
float MaxAngle;
|
||||
/// Circle radius
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
float Radius;
|
||||
/// The number of line segments to render the circle with
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
int NumSegments;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisArc")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisArc(const FVector& InLocation, const FRotator& InRotation, float InMinAngle, float InMaxAngle,
|
||||
float InRadius, int InNumSegments,
|
||||
const FColor& InColour)
|
||||
: Location(InLocation),
|
||||
Rotation(InRotation),
|
||||
MinAngle(InMinAngle),
|
||||
MaxAngle(InMaxAngle),
|
||||
Radius(InRadius),
|
||||
NumSegments(InNumSegments),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisArc():
|
||||
Location(FVector::ZeroVector),
|
||||
Rotation(FRotator::ZeroRotator),
|
||||
MinAngle(0),
|
||||
MaxAngle(180),
|
||||
Radius(50), NumSegments(12),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisSphere
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisSphere")
|
||||
FVector Location;
|
||||
/// Sphere radius
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisSphere")
|
||||
float Radius;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisSphere")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisSphere(const FVector& InLocation, float InRadius, const FColor& InColour) :
|
||||
Location(InLocation),
|
||||
Radius(InRadius),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisSphere():
|
||||
Location(FVector::ZeroVector),
|
||||
Radius(50),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisBox
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisBox")
|
||||
FVector Location;
|
||||
/// Size of box in each axis
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisBox")
|
||||
FVector Size;
|
||||
/// Rotation relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisBox")
|
||||
FRotator Rotation;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisBox")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisBox(const FVector& InLocation, const FVector& InSize, const FRotator& InRot,
|
||||
const FColor& InColour) :
|
||||
Location(InLocation),
|
||||
Size(InSize),
|
||||
Rotation(InRot),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisBox():
|
||||
Location(FVector::ZeroVector),
|
||||
Size(FVector(50, 50, 50)),
|
||||
Rotation(FRotator::ZeroRotator),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisCylinder
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCylinder")
|
||||
FVector Location;
|
||||
/// Height of cylinder
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCylinder")
|
||||
float Height;
|
||||
/// Radius of cylinder
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCylinder")
|
||||
float Radius;
|
||||
/// Rotation relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCylinder")
|
||||
FRotator Rotation;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisCylinder")
|
||||
FColor Colour;
|
||||
|
||||
FStevesEditorVisCylinder(const FVector& InLocation, float InHeight, float InRadius, const FRotator& InRot,
|
||||
const FColor& InColour) :
|
||||
Location(InLocation),
|
||||
Height(InHeight),
|
||||
Radius(InRadius),
|
||||
Rotation(InRot),
|
||||
Colour(InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisCylinder():
|
||||
Location(FVector::ZeroVector),
|
||||
Height(50),
|
||||
Radius(10),
|
||||
Rotation(FRotator::ZeroRotator),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisCapsule : public FStevesEditorVisCylinder
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FStevesEditorVisCapsule(const FVector& InLocation, float InHeight, float InRadius, const FRotator& InRot,
|
||||
const FColor& InColour) : FStevesEditorVisCylinder(InLocation, InHeight, InRadius, InRot, InColour)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisCapsule() : FStevesEditorVisCylinder()
|
||||
{
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesEditorVisMesh
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// The mesh do display
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
TObjectPtr<UStaticMesh> Mesh;
|
||||
/// Location relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
FVector Location;
|
||||
/// Scale of the mesh compared to component scale
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
FVector Scale;
|
||||
/// Rotation relative to component
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
FRotator Rotation;
|
||||
/// The colour of the line render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
FColor Colour;
|
||||
/// Whether to use the lowest detail LOD for vis
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisMesh")
|
||||
bool bUseLowestLOD = true;
|
||||
|
||||
FStevesEditorVisMesh(UStaticMesh* InMesh,
|
||||
const FVector& InLocation,
|
||||
const FVector& InScale,
|
||||
const FRotator& InRot,
|
||||
const FColor& InColour,
|
||||
bool InUseLowestLOD = true) :
|
||||
Mesh(InMesh),
|
||||
Location(InLocation),
|
||||
Scale(InScale),
|
||||
Rotation(InRot),
|
||||
Colour(InColour),
|
||||
bUseLowestLOD(InUseLowestLOD)
|
||||
{
|
||||
}
|
||||
|
||||
FStevesEditorVisMesh() : Mesh(nullptr),
|
||||
Location(FVector::ZeroVector),
|
||||
Scale(FVector::OneVector),
|
||||
Rotation(FRotator::ZeroRotator),
|
||||
Colour(FColor::White)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* A component that solely exists to provide arbitrary editor visualisation when not selected.
|
||||
* FComponentVisualizer can only display visualisation when selected.
|
||||
* To display vis on an unselected object, you need a UPrimitiveComponent, and sometimes you don't want/need one of those
|
||||
* in your actor. Instead, add UStevesEditorVisComponent at construction of your actor, or registration of another component,
|
||||
* but only in a WITH_EDITOR block. Then, get nice visualisation of your actor/component without making more invasive changes
|
||||
* to your code.
|
||||
*
|
||||
* If you want to, you can add this to your Blueprints too. This component automatically marks itself as "visualisation
|
||||
* only" so shouldn't have a runtime impact.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup="Utility", hidecategories=(Collision,Physics,Object,LOD,Lighting,TextureStreaming),
|
||||
meta=(DisplayName="Steves Editor Visualisation", BlueprintSpawnableComponent))
|
||||
class STEVESUEHELPERS_API UStevesEditorVisComponent : public UPrimitiveComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Circles to render
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisLine> Lines;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisLine> Arrows;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisCircle> Circles;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisArc> Arcs;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisSphere> Spheres;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisBox> Boxes;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisCylinder> Cylinders;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisCapsule> Capsules;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StevesVisComponent")
|
||||
TArray<FStevesEditorVisMesh> Meshes;
|
||||
|
||||
UStevesEditorVisComponent(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
|
||||
virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
|
||||
/// Needs to update on transform since proxy is detached
|
||||
virtual bool ShouldRecreateProxyOnUpdateTransform() const override { return true; }
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include "Engine/DataTable.h"
|
||||
|
||||
#include "StevesFixedDataTableRowHandle.generated.h"
|
||||
|
||||
/// Just a type to denote that this table row handle should be edited differently
|
||||
///
|
||||
/// Usage:
|
||||
/// UPROPERTY(EditAnywhere, meta=(DataTable="/Game/Data/DT_YourData"))
|
||||
/// FStevesFixedDataTableRowHandle Row;
|
||||
///
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FStevesFixedDataTableRowHandle : public FDataTableRowHandle
|
||||
{
|
||||
GENERATED_USTRUCT_BODY()
|
||||
};
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "PaperSprite.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
#include "StevesHelperCommon.h"
|
||||
#include "StevesTextureRenderTargetPool.h"
|
||||
#include "StevesUI/FocusSystem.h"
|
||||
#include "StevesUI/InputImage.h"
|
||||
#include "StevesUI/UiTheme.h"
|
||||
#include "Templates/TypeHash.h"
|
||||
|
||||
#include "StevesGameSubsystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, int, PlayerIndex, EInputMode, InputMode);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEnhancedInputMappingsChanged);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWindowForegroundChanged, bool, bFocussed);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnEnhancedInputActionTriggered, const UInputAction*, Action, ETriggerEvent, TriggeredEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnViewportResized, int, XSize, int, YSize);
|
||||
|
||||
/// Entry point for all the top-level features of the helper system
|
||||
UCLASS(Config=Game)
|
||||
class STEVESUEHELPERS_API UStevesGameSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/// The default UiTheme path, the theme to use if controls don't specifically link to one.
|
||||
/// Customise this in DefaultGame.ini
|
||||
/// [/Script/StevesUEHelpers.StevesGameSubsystem]
|
||||
/// DefaultUiThemePath="/Game/Some/Other/UiTheme.UiTheme"
|
||||
/// Regardless, remember to register this file as a Primary Asset in Project Settings,
|
||||
/// as described in https://docs.unrealengine.com/en-US/Engine/Basics/AssetsAndPackages/AssetManagement/index.html
|
||||
/// so that it's included when packaging
|
||||
UPROPERTY(Config)
|
||||
FString DefaultUiThemePath;
|
||||
|
||||
struct FEnhancedInputInterest
|
||||
{
|
||||
|
||||
TWeakObjectPtr<const UInputAction> Action;
|
||||
ETriggerEvent Trigger;
|
||||
FEnhancedInputInterest(const UInputAction* TheAction, ETriggerEvent TheTriggerEvent) : Action(TheAction), Trigger(TheTriggerEvent) {}
|
||||
|
||||
|
||||
friend uint32 GetTypeHash(const FEnhancedInputInterest& Arg)
|
||||
{
|
||||
return HashCombine(GetTypeHash(Arg.Action), GetTypeHash(Arg.Trigger));
|
||||
}
|
||||
|
||||
friend bool operator==(const FEnhancedInputInterest& Lhs, const FEnhancedInputInterest& RHS)
|
||||
{
|
||||
return Lhs.Action == RHS.Action
|
||||
&& Lhs.Trigger == RHS.Trigger;
|
||||
}
|
||||
|
||||
friend bool operator!=(const FEnhancedInputInterest& Lhs, const FEnhancedInputInterest& RHS)
|
||||
{
|
||||
return !(Lhs == RHS);
|
||||
}
|
||||
};
|
||||
|
||||
TSet<FEnhancedInputInterest> RegisteredEnhancedInputActionInterests;
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
protected:
|
||||
DECLARE_DELEGATE_TwoParams(FInternalInputModeChanged, int /* PlayerIndex */, EInputMode)
|
||||
/**
|
||||
* We seem to need a separate non-UObject for an IInputProcessor, my attempt to combine the 2 fails.
|
||||
* So this class just acts as a safe relay between Slate and UInputHelper
|
||||
*
|
||||
* This class should be registered as an input processor in order to capture all input events & detect
|
||||
* what kind of devices are being used. We can't use PlayerController to do this reliably because in UMG
|
||||
* mode, all the mouse move events are consumed by Slate and you never see them, so it's not possible to
|
||||
* detect when the user moved a mouse.
|
||||
*
|
||||
* This class should be instantiated and used from some UObject of your choice, e.g. your GameInstance class,
|
||||
* something like this:
|
||||
*
|
||||
* InputDetector = MakeShareable(new FInputModeDetector());
|
||||
* FSlateApplication::Get().RegisterInputPreProcessor(InputDetector);
|
||||
* InputDetector->OnInputModeChanged.BindUObject(this, &UMyGameInstance::OnInputDetectorModeChanged);
|
||||
*
|
||||
* Note how the OnInputModeChanged on this object is a simple delegate, not a dynamic multicast etc, because
|
||||
* this is not a UObject. You should relay the input mode event changed through the owner if you want to distribute
|
||||
* the information further.
|
||||
*/
|
||||
class FInputModeDetector : public IInputProcessor, public TSharedFromThis<FInputModeDetector>
|
||||
{
|
||||
protected:
|
||||
TArray<EInputMode> LastInputModeByPlayer;
|
||||
TArray<EInputMode> LastButtonPressByPlayer;
|
||||
TArray<EInputMode> LastAxisMoveByPlayer;
|
||||
|
||||
const EInputMode DefaultInputMode = EInputMode::Mouse;
|
||||
const EInputMode DefaultButtonInputMode = EInputMode::Keyboard;
|
||||
const EInputMode DefaultAxisInputMode = EInputMode::Mouse;
|
||||
const float MouseMoveThreshold = 1;
|
||||
const float GamepadAxisThreshold = 0.2f;
|
||||
|
||||
bool ShouldProcessInputEvents() const;
|
||||
public:
|
||||
/// Whether this detector should ignore events (e.g. because the application is in the background)
|
||||
bool bIgnoreEvents = false;
|
||||
|
||||
bool bIgnoreNextMouseMove = false;
|
||||
|
||||
/// Event raised when main input mode changes for any reason
|
||||
FInternalInputModeChanged OnInputModeChanged;
|
||||
/// Event raised when button input mode changes only
|
||||
FInternalInputModeChanged OnButtonInputModeChanged;
|
||||
/// Event raised when axis input mode changes only
|
||||
FInternalInputModeChanged OnAxisInputModeChanged;
|
||||
|
||||
|
||||
FInputModeDetector();
|
||||
|
||||
void IgnoreNextMouseMove() { bIgnoreNextMouseMove = true;}
|
||||
|
||||
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override;
|
||||
virtual bool
|
||||
HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override;
|
||||
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
|
||||
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
|
||||
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
|
||||
const FPointerEvent* InGestureEvent) override;
|
||||
|
||||
/// Get the last input mode from any kind of input
|
||||
EInputMode GetLastInputMode(int PlayerIndex = 0);
|
||||
/// Get the last input mode from button inputs (ignores axis changes, good for detecting if keyboard or mouse buttons are being used)
|
||||
EInputMode GetLastButtonInputMode(int PlayerIndex = 0);
|
||||
/// Get the last input mode from axis movement (ignores button presses, good for detecting if keyboard or mouse axes are being used for motion)
|
||||
EInputMode GetLastAxisInputMode(int PlayerIndex = 0);
|
||||
|
||||
// Needed but unused
|
||||
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
|
||||
|
||||
protected:
|
||||
static bool IsAGamepadButton(const FKey& Key);
|
||||
void ProcessKeyOrButton(int PlayerIndex, FKey Key);
|
||||
void SetMode(int PlayerIndex, EInputMode NewMode, bool bIsButton);
|
||||
};
|
||||
|
||||
protected:
|
||||
TSharedPtr<FInputModeDetector> InputDetector;
|
||||
FFocusSystem FocusSystem;
|
||||
bool bCheckedViewportClient = false;
|
||||
|
||||
FTimerHandle ForegroundCheckHandle;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="StevesGameSubsystem")
|
||||
bool bIsForeground = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category="StevesGameSubsystem")
|
||||
TObjectPtr<UUiTheme> DefaultUiTheme;
|
||||
|
||||
TArray<FStevesTextureRenderTargetPoolPtr> TextureRenderTargetPools;
|
||||
|
||||
void CreateInputDetector();
|
||||
void DestroyInputDetector();
|
||||
void InitTheme();
|
||||
void InitForegroundCheck();
|
||||
void CheckForeground();
|
||||
void InitViewport();
|
||||
void ViewportResized(FViewport* Viewport, unsigned Unused);
|
||||
void FullscreenToggled(bool bFullscreen);
|
||||
|
||||
|
||||
// Called by detector
|
||||
void OnInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
|
||||
void OnButtonInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
|
||||
void OnAxisInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
|
||||
|
||||
|
||||
TSoftObjectPtr<UDataTable> GetGamepadImages(int PlayerIndex, const UUiTheme* Theme);
|
||||
UPaperSprite* GetImageSpriteFromTable(const FKey& Key, const TSoftObjectPtr<UDataTable>& Asset);
|
||||
UFUNCTION()
|
||||
void EnhancedInputActionTriggered(const FInputActionInstance& InputActionInstance);
|
||||
|
||||
public:
|
||||
|
||||
/// Event raised when main input mode changed between gamepad and keyboard / mouse (for any of axis / button events)
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnInputModeChanged OnInputModeChanged;
|
||||
|
||||
/// Event raised when the last button input changed between gamepad / keyboard / mouse
|
||||
/// This can happen at a different time to OnInputModeChanged, e.g. if that was triggered by a mouse move, but the
|
||||
/// last button pressed was still keyboard, you'd get this event later
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnInputModeChanged OnButtonInputModeChanged;
|
||||
|
||||
/// Event raised when the last axis input changed between gamepad / keyboard / mouse
|
||||
/// This can happen at a different time to OnInputModeChanged, e.g. if that was triggered by a button press, but the
|
||||
/// last axis moved was still mouse, you'd get this event later
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnInputModeChanged OnAxisInputModeChanged;
|
||||
|
||||
/// DEPRECATED: Event raised just after the Enhanced Input mappings have changed
|
||||
/// User-triggered via NotifyEnhancedInputMappingsChanged
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnEnhancedInputMappingsChanged OnEnhancedInputMappingsChanged;
|
||||
|
||||
/// Event fired when an enhanced input event that an interest has previously been registered in triggers.
|
||||
/// Nothing will fire on this event unless you call RegisterInterestInEnhancedInputAction to listen for it.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnEnhancedInputActionTriggered OnEnhancedInputActionTriggered;
|
||||
|
||||
/// Event raised when the game window's foreground status changes
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnWindowForegroundChanged OnWindowForegroundChanged;
|
||||
|
||||
/// Event raised when the viewport changes size
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnViewportResized OnViewportResized;
|
||||
|
||||
/// Gets the device where the most recent input event of any kind happened
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
EInputMode GetLastInputModeUsed(int PlayerIndex = 0) const { return InputDetector->GetLastInputMode(PlayerIndex); }
|
||||
/// Gets the device where the most recent button press happened
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
EInputMode GetLastInputButtonPressed(int PlayerIndex = 0) const { return InputDetector->GetLastButtonInputMode(PlayerIndex); }
|
||||
/// Gets the device where the most recent axis move happened
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
EInputMode GetLastInputAxisMoved(int PlayerIndex = 0) const { return InputDetector->GetLastAxisInputMode(PlayerIndex); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
bool LastInputWasGamePad(int PlayerIndex = 0) const { return GetLastInputModeUsed(PlayerIndex) == EInputMode::Gamepad; }
|
||||
|
||||
/// Gets the default UI theme object (defaults to our own)
|
||||
/// You can override this if you want
|
||||
UUiTheme* GetDefaultUiTheme() { return DefaultUiTheme; };
|
||||
|
||||
/// Changes the default theme to a different one
|
||||
void SetDefaultUiTheme(UUiTheme* NewTheme) { DefaultUiTheme = NewTheme; }
|
||||
|
||||
/// Get the global focus system
|
||||
FFocusSystem* GetFocusSystem();
|
||||
|
||||
/// Return whether the game is currently in the foreground
|
||||
bool IsForeground() const { return bIsForeground; }
|
||||
|
||||
/**
|
||||
* @brief Get an input button / key / axis image as a sprite based on any combination of action / axis binding or manual key
|
||||
* @param BindingType The type of input binding to look up
|
||||
* @param ActionOrAxis The name of the action or axis, if BindingType is looking for that
|
||||
* @param Key The explicit key you want to display, if the BindingType is set to custom key
|
||||
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
|
||||
* @param PlayerIndex The player index to look up the binding for
|
||||
* @param Theme Optional explicit theme, if blank use the default theme
|
||||
* @return
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
UPaperSprite* GetInputImageSprite(EInputBindingType BindingType,
|
||||
FName ActionOrAxis,
|
||||
FKey Key,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIndex = 0,
|
||||
const UUiTheme* Theme = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get an input button / key / axis image as a sprite based on an enhanced input action
|
||||
* @param Action The input action
|
||||
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
|
||||
* @param PlayerIdx The player index to look up the binding for
|
||||
* @param PC The player controller to look up the binding for
|
||||
* @param Theme Optional explicit theme, if blank use the default theme
|
||||
* @return
|
||||
*/
|
||||
UPaperSprite* GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
|
||||
EInputImageDevicePreference DevicePreference,
|
||||
int PlayerIdx,
|
||||
APlayerController* PC,
|
||||
UUiTheme* Theme = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get an input button / key image from an action
|
||||
* @param Name The name of the action
|
||||
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
|
||||
* @param PlayerIndex The player index to look up the binding for
|
||||
* @param Theme Optional explicit theme, if blank use the default theme
|
||||
* @return
|
||||
*/
|
||||
UPaperSprite* GetInputImageSpriteFromAction(const FName& Name,
|
||||
EInputImageDevicePreference DevicePreference =
|
||||
EInputImageDevicePreference::Gamepad_Keyboard_Mouse,
|
||||
int PlayerIndex = 0,
|
||||
const UUiTheme* Theme = nullptr);
|
||||
/**
|
||||
* @brief Get an input image from an axis
|
||||
* @param Name The name of the axis
|
||||
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
|
||||
* @param PlayerIndex The player index to look up the binding for
|
||||
* @param Theme Optional explicit theme, if blank use the default theme
|
||||
* @return
|
||||
*/
|
||||
UPaperSprite* GetInputImageSpriteFromAxis(const FName& Name,
|
||||
EInputImageDevicePreference DevicePreference =
|
||||
EInputImageDevicePreference::Gamepad_Mouse_Keyboard,
|
||||
int PlayerIndex = 0,
|
||||
const UUiTheme* Theme = nullptr);
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Get an input image for a specific key
|
||||
* @param Key The key to look up
|
||||
* @param Theme Optional explicit theme, if blank use the default theme
|
||||
* @return
|
||||
*/
|
||||
UPaperSprite* GetInputImageSpriteFromKey(const FKey& Key, int PlayerIndex = 0, const UUiTheme* Theme = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Set the content of a slate brush from an atlas (e.g. sprite)
|
||||
* @param Brush The brush to update
|
||||
* @param AtlasRegion Atlas to use as source e.g. a Sprite
|
||||
* @param bMatchSize Whether to resize the brush to match the atlas entry
|
||||
*/
|
||||
static void SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterface<ISlateTextureAtlasInterface> AtlasRegion,
|
||||
bool bMatchSize);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a pool of texture render targets. If a pool doesn't exist with the given name, it can be created.
|
||||
* @param Name Identifier for the pool.
|
||||
* @param bAutoCreate
|
||||
* @return The pool, or null if it doesn't exist and bAutoCreate is false
|
||||
*/
|
||||
FStevesTextureRenderTargetPoolPtr GetTextureRenderTargetPool(FName Name, bool bAutoCreate = true);
|
||||
|
||||
/**
|
||||
* DEPRECATED - no longer required
|
||||
* Notify this subsystem that changes have been made to the Enhanced Input mappings, e.g. adding or removing a context.
|
||||
* Unfortunately, the Enhanced Input plugin currently provides NO WAY for us to monitor context changes automatically,
|
||||
* so we need the user to tell us when they make a change.
|
||||
* This call is however slightly delayed before being acted upon, because EI defers the rebuild of mappings until the next tick.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
void NotifyEnhancedInputMappingsChanged();
|
||||
|
||||
/** Attempt to find an enhanced input action by name in the configured folders.
|
||||
*/
|
||||
TSoftObjectPtr<UInputAction> FindEnhancedInputAction(const FString& Name);
|
||||
|
||||
|
||||
/// Register an interest in an enhanced input action. Calling this will result in OnEnhancedInputActionTriggered being called
|
||||
/// when this action is triggered.
|
||||
/// This is mainly for use in UI bindings. You only need to call it once for each UI-specific action.
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
void RegisterInterestInEnhancedInputAction(const UInputAction* Action, ETriggerEvent TriggerEvent);
|
||||
|
||||
// Unregister all previously registered interests in input actions. This can be needed if a new scene is loaded, as previously
|
||||
// regsitered InputActions will no longer fire
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
void UnregisterAllInterestInEnhancedInputActions();
|
||||
|
||||
/// Moves the mouse pointer offscreen so that it can't trigger hovers on anything. This happens by
|
||||
/// default when switching to gamepad mode, but in edge cases I've found it useful to ensure that
|
||||
/// some other effect doesn't interfere with gamepad navigation by relocating the mouse pointer to
|
||||
/// the centre (I've seen this rarely but can't fully explain it). By default all MenuStacks call this
|
||||
/// on open if the current input is gamepad, to ensure the mouse is out of the way.
|
||||
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
|
||||
void MoveMouseOffScreen(bool bAlsoHide = true) const;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
|
||||
#include "StevesGameViewportClientBase.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesGameViewportClientBase : public UGameViewportClient
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
bool bSuppressMouseCursor;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
virtual void Init(FWorldContext& WorldContext, UGameInstance* OwningGameInstance,
|
||||
bool bCreateNewAudioDevice) override;
|
||||
virtual EMouseCursor::Type GetCursor(FViewport* Viewport, int32 X, int32 Y) override;
|
||||
|
||||
virtual void SetSuppressMouseCursor(bool bSuppress);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include "UObject/ObjectMacros.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EInputMode : uint8
|
||||
{
|
||||
Mouse,
|
||||
Keyboard,
|
||||
Gamepad,
|
||||
Unknown
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EInputModeChange : uint8
|
||||
{
|
||||
DoNotChange UMETA(DisplayName="No Change"),
|
||||
Restore UMETA(DisplayName="Restore To Previous"),
|
||||
UIOnly UMETA(DisplayName="UI Only"),
|
||||
GameAndUI UMETA(DisplayName="Game And UI"),
|
||||
GameOnly UMETA(DisplayName="Game Only")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EMousePointerVisibilityChange : uint8
|
||||
{
|
||||
DoNotChange UMETA(DisplayName="No Change"),
|
||||
Restore UMETA(DisplayName="Restore To Previous"),
|
||||
Visible UMETA(DisplayName="Pointer Visible"),
|
||||
Hidden UMETA(DisplayName="Pointer Hidden")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EGamePauseChange : uint8
|
||||
{
|
||||
DoNotChange UMETA(DisplayName="No Change"),
|
||||
Restore UMETA(DisplayName="Restore To Previous"),
|
||||
Paused UMETA(DisplayName="Pause Game"),
|
||||
Unpaused UMETA(DisplayName="Unpause Game")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EInputBindingType : uint8
|
||||
{
|
||||
/// A legacy button action, will be looked up based on input mappings
|
||||
Action = 0,
|
||||
/// An legacy axis action, will be looked up based on input mappings
|
||||
Axis = 1,
|
||||
/// A manually specified FKey (which can be key, button, axis)
|
||||
Key = 2,
|
||||
/// An EnhancedInput action
|
||||
EnhancedInputAction = 3
|
||||
};
|
||||
|
||||
/// What order of preference should we return input images where an action/axis has multiple mappings
|
||||
UENUM(BlueprintType)
|
||||
enum class EInputImageDevicePreference : uint8
|
||||
{
|
||||
/// For actions, use Gamepad_Keyboard_Mouse_Button, for axes, use Gamepad_Mouse_Keyboard
|
||||
Auto,
|
||||
/// Gamepad first, then keyboard, then mouse
|
||||
Gamepad_Keyboard_Mouse UMETA(DisplayName="Gamepad, Keyboard, Mouse"),
|
||||
/// Gamepad first, then mouse, then keyboard - this is usually best for axes
|
||||
Gamepad_Mouse_Keyboard UMETA(DisplayName="Gamepad, Mouse, Keyboard"),
|
||||
/// Gamepad first, then whichever of mouse or keyboard last had a BUTTON pressed (ignore axes) - this is usually best for actions (buttons)
|
||||
Gamepad_Keyboard_Mouse_Button UMETA(DisplayName="Gamepad, Most Recent Button Keyboard/Mouse"),
|
||||
/// Gamepad first, then whichever of mouse or keyboard last had an AXIS moved (ignore buttons) - this is usually best for directionals
|
||||
Gamepad_Keyboard_Mouse_Axis UMETA(DisplayName="Gamepad, Most Recent Axis Keyboard/Mouse")
|
||||
};
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright Steve Streeting
|
||||
// Licensed under the MIT License (see License.txt)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "Curves/RichCurve.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "StevesLightFlicker.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EStevesLightFlickerPattern : uint8
|
||||
{
|
||||
Flicker1,
|
||||
Flicker2,
|
||||
SlowStrongPulse,
|
||||
Candle1,
|
||||
Candle2,
|
||||
Candle3,
|
||||
FastStrobe,
|
||||
SlowStrobe,
|
||||
GentlePulse1,
|
||||
FlourescentFlicker,
|
||||
SlowPulseNoBlack,
|
||||
Torch1,
|
||||
Torch2,
|
||||
|
||||
Custom
|
||||
|
||||
};
|
||||
/**
|
||||
* Helper class to get lighting flicker curves
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesLightFlickerHelper : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
static TMap<EStevesLightFlickerPattern, FRichCurve> Curves;
|
||||
static TMap<FString, FRichCurve> CustomCurves;
|
||||
static FCriticalSection CriticalSection;
|
||||
static const TMap<EStevesLightFlickerPattern, FString> StandardPatterns;
|
||||
|
||||
|
||||
static void BuildCurve(EStevesLightFlickerPattern CurveType, FRichCurve& OutCurve);
|
||||
static void BuildCurve(const FString& QuakeCurveChars, FRichCurve& OutCurve);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Directly evaluate a lighting curve. Alternatively, see ULightingCurveComponent.
|
||||
* @param CurveType The type of curve
|
||||
* @param Time The time index (0..1 period)
|
||||
* @return Normalised value of the curve at this time
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category="Lighting Curves")
|
||||
static float EvaluateLightCurve(EStevesLightFlickerPattern CurveType, float Time);
|
||||
|
||||
static const FRichCurve& GetLightCurve(EStevesLightFlickerPattern CurveType);
|
||||
static const FRichCurve& GetLightCurve(const FString& CurveStr);
|
||||
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLightFlickerUpdate, float, LightValue);
|
||||
/**
|
||||
* This is like a generated version of TimelineComponent, providing a generated lighting curve.
|
||||
*/
|
||||
UCLASS(Blueprintable, ClassGroup=(Lights), meta=(BlueprintSpawnableComponent))
|
||||
class UStevesLightFlickerComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_UCLASS_BODY()
|
||||
|
||||
protected:
|
||||
UPROPERTY(EditAnywhere, Category="Light Flicker")
|
||||
EStevesLightFlickerPattern FlickerPattern = EStevesLightFlickerPattern::Candle1;
|
||||
|
||||
/// If using a custom pattern, provide your own Quake-style string of letters, a-z (a = 0, m = 1, z = 2)
|
||||
UPROPERTY(EditAnywhere, Category="Light Flicker")
|
||||
FString CustomFlickerPattern;
|
||||
|
||||
/// Max output intensity multiplier value. Defaults to 2 since that's what Quake used!
|
||||
/// We can *very slightly* exceed this max with 'z' as per standard Quake where z was 2.08
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
|
||||
float MaxValue = 2;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
|
||||
float MinValue = 0;
|
||||
|
||||
/// Playback speed
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
|
||||
float Speed = 1;
|
||||
|
||||
/// Whether to auto-start
|
||||
UPROPERTY(EditAnywhere, Category="Light Flicker")
|
||||
bool bAutoPlay = true;
|
||||
|
||||
UPROPERTY(ReplicatedUsing=OnRep_TimePos)
|
||||
float TimePos;
|
||||
float CurrentValue;
|
||||
|
||||
const FRichCurve* Curve;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_TimePos();
|
||||
void ValueUpdate();
|
||||
void GenerateCurveAndPlay();
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnLightFlickerUpdate OnLightFlickerUpdate;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Light Flicker")
|
||||
void Play(bool bResetTime = false);
|
||||
UFUNCTION(BlueprintCallable, Category="Light Flicker")
|
||||
void Pause();
|
||||
UFUNCTION(BlueprintPure, Category="Light Flicker")
|
||||
float GetCurrentValue() const;
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime,
|
||||
ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
/// Change the flicker pattern dynamically
|
||||
UFUNCTION(BlueprintCallable, Category="Light Flicker")
|
||||
void SetFlickerPattern(EStevesLightFlickerPattern Pattern, const FString& CustomPatternString = FString(""));
|
||||
|
||||
/// Get the flicker pattern
|
||||
UFUNCTION(BlueprintPure, Category="Light Flicker")
|
||||
EStevesLightFlickerPattern GetFlickerPattern(FString& CustomString);
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
/// Log once per session only
|
||||
#define STEVES_LOG_ONCE(CategoryName, Verbosity, Format, ...) \
|
||||
{ \
|
||||
static bool bLogged = false; \
|
||||
UE_CLOG(!bLogged, CategoryName, Verbosity, Format, ##__VA_ARGS__); \
|
||||
bLogged = true; \
|
||||
}
|
||||
|
||||
/// Suppress repeat logs if the value is the same as last time at the call site
|
||||
#define STEVES_LOG_NOREPEAT(TypeName, Value, CategoryName, Verbosity, Format, ...) \
|
||||
{ \
|
||||
static TOptional<TypeName> LastValue; \
|
||||
UE_CLOG(!LastValue.IsSet() || LastValue != Value, CategoryName, Verbosity, Format, ##__VA_ARGS__); \
|
||||
LastValue = Value; \
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include "Math/UnrealMathUtility.h"
|
||||
#include "Math/MathFwd.h"
|
||||
#include "CollisionShape.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
|
||||
struct FKConvexElem;
|
||||
|
||||
/// Helper maths routines that UE4 is missing, all static
|
||||
class STEVESUEHELPERS_API StevesMathHelpers
|
||||
{
|
||||
public:
|
||||
|
||||
/**
|
||||
* @brief Return whether a sphere overlaps a cone
|
||||
* @param ConeOrigin Origin of the cone
|
||||
* @param ConeDir Direction of the cone, must be normalised
|
||||
* @param ConeHalfAngle Half-angle of the cone, in radians
|
||||
* @param Distance Length of the cone
|
||||
* @param SphereCentre Centre of the sphere
|
||||
* @param SphereRadius Radius of the sphere
|
||||
* @return True if the sphere overlaps the cone
|
||||
*/
|
||||
static bool SphereOverlapCone(const FVector& ConeOrigin, const FVector& ConeDir, float ConeHalfAngle, float Distance, const FVector& SphereCentre, float SphereRadius)
|
||||
{
|
||||
// Algorithm from https://www.geometrictools.com/GTE/Mathematics/IntrSphere3Cone3.h
|
||||
|
||||
const float SinHalfAngle = FMath::Sin(ConeHalfAngle);
|
||||
const float InvSinHalfAngle = 1.f/SinHalfAngle;
|
||||
|
||||
const FVector U = ConeOrigin - (SphereRadius * InvSinHalfAngle) * ConeDir;
|
||||
const FVector CmU = SphereCentre - U;
|
||||
const float AdCmU = FVector::DotProduct(ConeDir, CmU);
|
||||
if (AdCmU > 0)
|
||||
{
|
||||
const float CosHalfAngle = FMath::Cos(ConeHalfAngle);
|
||||
const float CosHalfAngleSq = CosHalfAngle * CosHalfAngle;
|
||||
|
||||
const float sqrLengthCmU = FVector::DotProduct(CmU, CmU);
|
||||
if (AdCmU * AdCmU >= sqrLengthCmU * CosHalfAngleSq)
|
||||
{
|
||||
const FVector CmV = SphereCentre - ConeOrigin;
|
||||
const float AdCmV = FVector::DotProduct(ConeDir, CmV);
|
||||
if (AdCmV < -SphereRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AdCmV > Distance + SphereRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float rSinAngle = SphereRadius * SinHalfAngle;
|
||||
if (AdCmV >= -rSinAngle)
|
||||
{
|
||||
if (AdCmV <= Distance - rSinAngle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float TanHalfAngle = FMath::Tan(ConeHalfAngle);
|
||||
|
||||
const FVector barD = CmV - Distance * ConeDir;
|
||||
const float lengthAxBarD = FVector::CrossProduct(ConeDir, barD).Size();
|
||||
const float hmaxTanAngle = Distance * TanHalfAngle;
|
||||
if (lengthAxBarD <= hmaxTanAngle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const float AdBarD = AdCmV - Distance;
|
||||
const float diff = lengthAxBarD - hmaxTanAngle;
|
||||
const float sqrLengthCmBarK = AdBarD * AdBarD + diff * diff;
|
||||
return sqrLengthCmBarK <= SphereRadius * SphereRadius;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const float sqrLengthCmV = FVector::DotProduct(CmV, CmV);
|
||||
return sqrLengthCmV <= SphereRadius * SphereRadius;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly test the overlap of any collision shape with a convex element.
|
||||
* @param Convex The convex element
|
||||
* @param ConvexTransform The world transform of the convex element
|
||||
* @param Shape The test shape
|
||||
* @param ShapePos The test shape world position
|
||||
* @param ShapeRot The test shape world rotation
|
||||
* @param OutResult Details of the result if returning true
|
||||
* @return Whether this shape overlaps the convex element
|
||||
*/
|
||||
static bool OverlapConvex(const FKConvexElem& Convex,
|
||||
const FTransform& ConvexTransform,
|
||||
const FCollisionShape& Shape,
|
||||
const FVector& ShapePos,
|
||||
const FQuat& ShapeRot,
|
||||
FMTDResult& OutResult);
|
||||
|
||||
/**
|
||||
* Return the distance to a convex polygon in 2D where points are in the same space
|
||||
* @param ConvexPoints Points on the convex polygon, anti-clockwise order, in a chosen space
|
||||
* @param LocalPoint Point to test, in same space as convex points
|
||||
* @return The distance to this convex polygon in 2D space. <= 0 if inside
|
||||
*/
|
||||
static float GetDistanceToConvex2D(const TArray<FVector2f>& ConvexPoints,
|
||||
const FVector& LocalPoint);
|
||||
/**
|
||||
* Return the distance to a convex polygon in 2D where points are in the same space
|
||||
* @param ConvexPoints Points on the convex polygon, anti-clockwise order, in a chosen space
|
||||
* @param LocalPoint Point to test, in same space as convex points
|
||||
* @return The distance to this convex polygon in 2D space. <= 0 if inside
|
||||
*/
|
||||
static float GetDistanceToConvex2D(const TArray<FVector2f>& ConvexPoints,
|
||||
const FVector2f& LocalPoint);
|
||||
/**
|
||||
* Return the distance to a convex polygon in 2D world space, converting between spaces
|
||||
* @param ConvexPoints Points on the convex polygon, anti-clockwise order, in local space
|
||||
* @param ConvexTransform World transform for convex polygon
|
||||
* @param WorldPoint Point in world space
|
||||
* @return The distance to this convex polygon in 2D space. <= 0 if inside
|
||||
*/
|
||||
static float GetDistanceToConvex2D(const TArray<FVector2f>& ConvexPoints,
|
||||
const FTransform& ConvexTransform,
|
||||
const FVector& WorldPoint)
|
||||
{
|
||||
checkf(ConvexTransform.GetMaximumAxisScale() == ConvexTransform.GetMinimumAxisScale(), TEXT("Non-uniform scale not supported in GetDistanceToConvex2D"));
|
||||
const FVector LocalPoint = ConvexTransform.InverseTransformPosition(WorldPoint);
|
||||
|
||||
// Need to rescale distance back up to world scale, only uniform scale supported for simplicity
|
||||
return GetDistanceToConvex2D(ConvexPoints, LocalPoint) * ConvexTransform.GetScale3D().X;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a 2D point is inside a triangle
|
||||
* @param p Point to test
|
||||
* @param v0 First triangle point
|
||||
* @param v1 Second triangle point
|
||||
* @param v2 Third triangle point
|
||||
* @return Whether point p is inside the triangle.
|
||||
*/
|
||||
static bool IsPointInTriangle2D(const FVector& p,
|
||||
const FVector2f& v0,
|
||||
const FVector2f& v1,
|
||||
const FVector2f& v2)
|
||||
{
|
||||
const float s = (v0.X - v2.X) * (p.Y - v2.Y) - (v0.Y - v2.Y) * (p.X - v2.X);
|
||||
const float t = (v1.X - v0.X) * (p.Y - v0.Y) - (v1.Y - v0.Y) * (p.X - v0.X);
|
||||
|
||||
if ((s < 0) != (t < 0) && s != 0 && t != 0)
|
||||
return false;
|
||||
|
||||
const float d = (v2.X - v1.X) * (p.Y - v1.Y) - (v2.Y - v1.Y) * (p.X - v1.X);
|
||||
return d == 0 || (d < 0) == (s + t <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that tries to fill a 2D area with the largest rectangles it can. The area is abstractly defined as a boundary
|
||||
* index area with start X/Y and width/height, and will call back the CellIncludeFunc to determine whether a given
|
||||
* cell index X/Y should be considered valid to include in a rectangle. This means you can define irregular grids of
|
||||
* "valid" cells, and this function will fill the area with the largest rectangles it can while staying out of "invalid"
|
||||
* cells.
|
||||
* If you return "true" from every call to your CellIncludeFunc then the result will be a single rectangle covering
|
||||
* the entire area. It's expected that you will return "false" for some X/Y combinations and that will cause the area
|
||||
* to be split into multiple rectangles.
|
||||
* The returned rectangles will not overlap, and the entire valid area will be filled.
|
||||
* @param StartX The start X index. This is defined by your own data, so you can address a subset if you want.
|
||||
* @param StartY The start Y index.This is defined by your own data, so you can address a subset if you want.
|
||||
* @param Width The width of the area to fill. This is defined by your own data, so you can address a subset if you want.
|
||||
* @param Height The height of the area to fill. This is defined by your own data, so you can address a subset if you want.
|
||||
* @param CellIncludeFunc Your function which given an X/Y cell index, must return true if that cell is valid to be
|
||||
* included in a rectangle.
|
||||
* @param OutRects Array of rectangles which this function should append results to. Will not be cleared before adding.
|
||||
* @return The number of rectangles added by this call. Each rectangle is a min/max inclusive X/Y value.
|
||||
*/
|
||||
static int Fill2DRegionWithRectangles(int StartX,
|
||||
int StartY,
|
||||
int Width,
|
||||
int Height,
|
||||
std::function<bool(int, int)> CellIncludeFunc,
|
||||
TArray<FIntRect>& OutRects);
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
|
||||
#include "StevesPluginSettings.generated.h"
|
||||
|
||||
/**
|
||||
* Settings for the plug-in.
|
||||
*/
|
||||
UCLASS(config=Engine, DefaultConfig)
|
||||
class STEVESUEHELPERS_API UStevesPluginSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(config, EditAnywhere, Category = StevesUEHelpers, meta = (ToolTip = "Whether to automatically hide the mouse cursor and move it offscreen when using a gamepad"))
|
||||
bool bHideMouseWhenGamepadUsed = true;
|
||||
|
||||
/// Which directories to search for Enhanced Input Actions when referenced just by name in e.g. Rich Text Decorator
|
||||
UPROPERTY(config, EditAnywhere, Category = StevesUEHelpers, meta = (DisplayName = "Directories to search for Enhanced Input Actions", RelativeToGameContentDir, LongPackageName))
|
||||
TArray<FDirectoryPath> EnhancedInputActionSearchDirectories;
|
||||
|
||||
UStevesPluginSettings() {}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "StevesPooledActor.generated.h"
|
||||
|
||||
UINTERFACE(MinimalAPI)
|
||||
class UStevesPooledActor : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Optional interface for pooled actors to implement. You don't have to do this, but if you implement this interface
|
||||
* for actors you add/remove from UStevesPooledActorSystem, you get additional calls to let you know when that actor is
|
||||
* being added or removed from the pool.
|
||||
*/
|
||||
class STEVESUEHELPERS_API IStevesPooledActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
/**
|
||||
* This function is called when this actor is added to the pool. Implement this if you want to perform actions to disable the
|
||||
* actor other than the default ones (hiding the actor, disabling collision / physics on the root component, moving to a storage location).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Pooling")
|
||||
void DeactivateOnAddedToPool();
|
||||
|
||||
/**
|
||||
* This function is called when the actor is removed from the pool, to be re-used again. Implement this to re-activate
|
||||
* or reset the actor; the default behaviour is just to unhide the actor (collision / physics are not re-enabled automatically).
|
||||
* You could also do this in the caller to UStevesPooledActorSystem::GetPooledActor, but this interface is here if
|
||||
* you want to do it on the object itself.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Pooling")
|
||||
void ReactivateOnRemovedFromPool();
|
||||
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Containers/Deque.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "StevesPooledActorSystem.generated.h"
|
||||
|
||||
/**
|
||||
* General actor pooling system
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesPooledActorSystem : public UWorldSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
TMap<UClass*, TDeque<TObjectPtr<AActor>>> Pools;
|
||||
|
||||
/// The location that actors in the pool are stored at
|
||||
UPROPERTY(BlueprintReadWrite, Category="Pooling")
|
||||
FVector StorageLocation = FVector(0,0,-100000);
|
||||
|
||||
TDeque<TObjectPtr<AActor>>* GetPool(UClass* Class, bool bCreate);
|
||||
void DisableActor(AActor* Actor);
|
||||
void ReviveActor(AActor* Actor, const FVector& Location, const FRotator& Rotator);
|
||||
|
||||
AActor* SpawnNewActor(UClass* ActorClass, const FVector& Location, const FRotator& Rotation);
|
||||
void DrainPool(TDeque<TObjectPtr<AActor>>* pPool, int NumberToKeep);
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
static UStevesPooledActorSystem* Get(const UObject* WorldContext);
|
||||
|
||||
/// Get the location that actors in the pool are stored at
|
||||
const FVector& GetStorageLocation() const
|
||||
{
|
||||
return StorageLocation;
|
||||
}
|
||||
|
||||
/// Set the location that actors are stored at when in the pool
|
||||
void SetStorageLocation(const FVector& Loc)
|
||||
{
|
||||
this->StorageLocation = Loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an actor to the pool. Will make the actor invisible, teleport it to the storage location, and disable all
|
||||
* physics. You must NOT destroy this actor!
|
||||
* @param Actor
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
void AddActorToPool(AActor* Actor);
|
||||
/**
|
||||
* Release an actor back to the pool, instead of destroying it. Will make the actor invisible, teleport it to a
|
||||
* far away location, and disable physics. You must NOT destroy this actor!
|
||||
* @param Actor
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
void ReleasePooledActor(AActor* Actor);
|
||||
|
||||
/**
|
||||
* Re-use or spawn an actor of a given class. Note that if the actor is re-used, you will need to re-enable physics
|
||||
* yourself since we do not store that state or assume it will be the same. It will be teleported to the correct location and set visible.
|
||||
* @param ActorClass The class of the actor
|
||||
* @param Location Location in the world
|
||||
* @param Rotation Rotation in the world
|
||||
* @param bWasReUsed This is set to true if the actor was re-used from the pool, and therefore will need to have its
|
||||
* physics reset by the caller (if not done by an IStevesPooledActor implementation)
|
||||
* @return Spawned or re-used actor. Remember to check physics settings, both will be disabled if re-used.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
AActor* GetPooledActor(TSubclassOf<AActor> ActorClass,
|
||||
const FVector& Location,
|
||||
const FRotator& Rotation,
|
||||
bool& bWasReUsed);
|
||||
|
||||
|
||||
template< class T >
|
||||
T* GetPooledActor(UClass* Class, const FVector& Location, const FRotator& Rotation, bool& bOutWasReused)
|
||||
{
|
||||
return Cast<T>(GetPooledActor(Class, Location, Rotation, bOutWasReused));
|
||||
}
|
||||
|
||||
template< class T >
|
||||
T* GetPooledActor(UClass* Class, const FVector& Location, const FRotator& Rotation)
|
||||
{
|
||||
bool Dummy;
|
||||
return Cast<T>(GetPooledActor(Class, Location, Rotation, Dummy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-warm the actor pool by creating a number of instances and making them invisible
|
||||
* @param ActorClass The class of actors to pre-warm the pool with
|
||||
* @param Count The number of actors to pre-create
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
void PreWarmActorPool(TSubclassOf<AActor> ActorClass, int Count = 20);
|
||||
|
||||
/**
|
||||
* Drain the actor pool for a single class of actor, destroying any surplus actors.
|
||||
* @param ActorClass The actor class to drain the pool for
|
||||
* @param NumberToKeep The number of actors to keep, if any
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
void DrainActorPool(TSubclassOf<AActor> ActorClass, int NumberToKeep = 0);
|
||||
|
||||
/**
|
||||
* Drain all actor pools, destroying all pooled actors.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Pooling")
|
||||
void DrainAllActorPools();
|
||||
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright Steve Streeting
|
||||
// Licensed under the MIT License (see License.txt)
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "StevesReplicatedPhysicsActor.generated.h"
|
||||
|
||||
/// A static mesh actor which is simulated on the server and replicated to clients.
|
||||
/// The results will be pretty smooth on the clients due to a very specific set of settings.
|
||||
/// You MUST spawn this actor ONLY on the server!
|
||||
UCLASS(Blueprintable)
|
||||
class STEVESUEHELPERS_API AStevesReplicatedPhysicsActor : public AStaticMeshActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AStevesReplicatedPhysicsActor(const FObjectInitializer& ObjInit);
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
bool IsServer() const;
|
||||
};
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Math/RandomStream.h"
|
||||
#include "StevesShuffleBag.generated.h"
|
||||
|
||||
/// A shufflebag is an array that contains a certain number of items, and can randomly withdraw one
|
||||
/// at a time until there are none left. No repeat items will be retrieved until the bag is empty.
|
||||
/// Requires a type T which is the contained item (keep it small, will be copied around), and optionally
|
||||
/// TRandStream which must be a type which implements the same interface as FRandomStream but may generate
|
||||
/// numbers differently.
|
||||
template<typename T, typename TRandStream = FRandomStream>
|
||||
struct FStevesShuffleBag
|
||||
{
|
||||
protected:
|
||||
TArray<T> Bag;
|
||||
TRandStream RandomStream;
|
||||
// This is the dividing line between items still in the bag and those which have already been pulled.
|
||||
// Any index < SentinelIndex is an item in the bag, any >= SentinelIndex have already been pulled.
|
||||
int SentinelIndex;
|
||||
|
||||
|
||||
public:
|
||||
/// Construct using a zero seed & standard capacity
|
||||
FStevesShuffleBag()
|
||||
{
|
||||
Init(0);
|
||||
}
|
||||
|
||||
/// Construct using a known seed & capacity
|
||||
FStevesShuffleBag(int32 Seed, int Capacity = 32)
|
||||
{
|
||||
Init(Seed);
|
||||
}
|
||||
|
||||
|
||||
/// (Re-)initialise the shuffle bag, emptying its contents and resetting the seed.
|
||||
void Init(int32 Seed, int Capacity = 32)
|
||||
{
|
||||
Bag.Reset(Capacity);
|
||||
RandomStream.Initialize(Seed);
|
||||
SentinelIndex = -1;
|
||||
}
|
||||
|
||||
/// Reserve space for a given number of entries
|
||||
void Reserve(int Capacity)
|
||||
{
|
||||
Bag.Reserve(Capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more copies of an item to the bag.
|
||||
* @param NewItem The new item to add
|
||||
* @param Count The number of copies of this item to add. You can control the probability of items
|
||||
* by adding multiple of the same thing.
|
||||
* @note If you have already started pulling items from the bag, these items will NOT be available
|
||||
* to be pulled until the bag is next re-filled. They will count as having been pulled already
|
||||
* (so preferably add all your items before you start pulling items from the bag)
|
||||
*/
|
||||
void Add(const T& NewItem, int Count = 1)
|
||||
{
|
||||
for (int i = 0; i < Count; ++i)
|
||||
{
|
||||
Bag.Add(NewItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove one instance of an item from the bag and discard it. This is different to pulling the
|
||||
/// item from the bag since it will not re-enter the bag on Reset. The item may have been pulled
|
||||
/// already.
|
||||
/// Returns whether any item was removed
|
||||
bool Remove(const T& Item)
|
||||
{
|
||||
int Index = Bag.IndexOfByKey(Item);
|
||||
if (Index != INDEX_NONE)
|
||||
{
|
||||
Bag.RemoveAt(Index);
|
||||
if (Index < SentinelIndex)
|
||||
{
|
||||
// This item hadn't been pulled yet
|
||||
--SentinelIndex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Remove all instance of an item from the bag and discard them. This is different to pulling the
|
||||
/// items from the bag since they will not re-enter the bag on Reset.
|
||||
/// Returns the number of items removed.
|
||||
int RemoveAll(const T& Item)
|
||||
{
|
||||
int RemovedCount = 0;
|
||||
while (Remove(Item))
|
||||
{
|
||||
++RemovedCount;
|
||||
}
|
||||
return RemovedCount;
|
||||
}
|
||||
|
||||
/// Empty the bag, discarding the contents
|
||||
void Empty()
|
||||
{
|
||||
// Keep allocations
|
||||
Bag.Reset();
|
||||
SentinelIndex = -1;
|
||||
}
|
||||
|
||||
/// Reset the bag, returning all previously retrieved contents to the bag
|
||||
void Reset()
|
||||
{
|
||||
// Any items < SentinelIndex are in the bag
|
||||
SentinelIndex = Bag.Num();
|
||||
}
|
||||
|
||||
/// Pull a random item from those remaining in the bag. If the bag is empty, it will be
|
||||
/// refilled using the tokens that were previously pulled (via Reset).
|
||||
T Next()
|
||||
{
|
||||
if (SentinelIndex <= 0)
|
||||
{
|
||||
// Empty / new bag
|
||||
Reset();
|
||||
|
||||
}
|
||||
|
||||
// Empty bag
|
||||
if (SentinelIndex == 0)
|
||||
{
|
||||
return T();
|
||||
}
|
||||
|
||||
// rand max is inclusive, and we need to exclude >= SentinelIndex
|
||||
int NextIdx = RandomStream.RandRange(0, SentinelIndex - 1);
|
||||
// Copy out
|
||||
T Ret = Bag[NextIdx];
|
||||
|
||||
--SentinelIndex;
|
||||
if (NextIdx != SentinelIndex) {
|
||||
// swap the item that's now beyond the sentinel into our slot
|
||||
Bag.Swap(NextIdx, SentinelIndex);
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
/// Get the number of items left in the bag
|
||||
int GetNumRemaining() const
|
||||
{
|
||||
return SentinelIndex > 0 ? SentinelIndex : 0;
|
||||
}
|
||||
|
||||
/// Get the number of items which have already been pulled from the bag
|
||||
int GetNumPulled() const
|
||||
{
|
||||
return GetNumTotal() - GetNumRemaining();
|
||||
}
|
||||
|
||||
/// Get the number of items both remaining in the bag and which have already been pulled from it
|
||||
int GetNumTotal() const
|
||||
{
|
||||
return Bag.Num();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// A shufflebag is an array that contains a certain number of items, and can randomly withdraw one
|
||||
/// at a time until there are none left. No repeat items will be retrieved until the bag is empty.
|
||||
/// Because Blueprints can't support templated types, FStevesIndexShuffleBag provides a concretely
|
||||
/// typed shuffle bag which simply provides array indices rather than values. When you pull out an
|
||||
/// item, instead of pulling out the *actual* item, you pull out an index into another array.
|
||||
/// You can therefore use this with any other array so long as you keep them synchronised.
|
||||
/// If you want to customise the probabilities, add multiple entries of the same item to *your* array
|
||||
/// before constructing the index shuffle bag.
|
||||
/// Use MakeIndexShuffleBag to create.
|
||||
UCLASS(BlueprintType)
|
||||
class UStevesIndexShuffleBag : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
FStevesShuffleBag<int> IndexBag;
|
||||
public:
|
||||
|
||||
UStevesIndexShuffleBag()
|
||||
{
|
||||
}
|
||||
|
||||
/// Reinitialise the shuffle bag with a new size / seed.
|
||||
UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
|
||||
void Reinitialise(int Count, int32 Seed)
|
||||
{
|
||||
IndexBag.Init(Seed, Count);
|
||||
|
||||
for (int i = 0; i < Count; ++i)
|
||||
{
|
||||
IndexBag.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new shuffle bag of indexes for accessing items in an array
|
||||
* @param Outer The
|
||||
* @param Count The number of items in the array you'll be accessing with these indexes
|
||||
* @param Seed The seed for the random number generator
|
||||
* @return The new index shuffle bag
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
|
||||
static UStevesIndexShuffleBag* MakeIndexShuffleBag(UObject* Outer, int Count, int32 Seed = 0)
|
||||
{
|
||||
auto Ret = NewObject<UStevesIndexShuffleBag>(Outer);
|
||||
Ret->Reinitialise(Count, Seed);
|
||||
return Ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Get the number of items left in the bag
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
|
||||
int GetNumRemaining() const
|
||||
{
|
||||
return IndexBag.GetNumRemaining();
|
||||
}
|
||||
|
||||
/// Get the number of items which have already been pulled from the bag
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
|
||||
int GetNumPulled() const
|
||||
{
|
||||
return IndexBag.GetNumPulled();
|
||||
}
|
||||
|
||||
/// Get the number of items both remaining in the bag and which have already been pulled from it
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
|
||||
int GetNumTotal() const
|
||||
{
|
||||
return IndexBag.GetNumTotal();
|
||||
}
|
||||
|
||||
/// Reset the bag, returning all previously retrieved contents to the bag
|
||||
UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
|
||||
void Reset()
|
||||
{
|
||||
IndexBag.Reset();
|
||||
}
|
||||
|
||||
/// Pull a random index from those remaining in the bag. If the bag is empty, it will be
|
||||
/// refilled using the indexes that were previously pulled (via Reset).
|
||||
UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
|
||||
int Next()
|
||||
{
|
||||
return IndexBag.Next();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "StevesSpringArmComponent.generated.h"
|
||||
|
||||
|
||||
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
||||
class STEVESUEHELPERS_API UStevesSpringArmComponent : public USpringArmComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Whether to move the camera smoothly to avoid collisions rather than jumping instantly
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=CameraCollision)
|
||||
uint32 bEnableSmoothCollisionAvoidance : 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=CameraCollision)
|
||||
float SmoothCollisionAvoidanceSpeed = 5;
|
||||
|
||||
protected:
|
||||
TOptional<float> PrevArmLength;
|
||||
|
||||
TOptional<FVector> SmoothTargetOffsetTarget;
|
||||
float SmoothTargetOffsetSpeed;
|
||||
TOptional<FVector> SmoothSocketOffsetTarget;
|
||||
float SmoothSocketOffsetSpeed;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
UPROPERTY(Transient, EditAnywhere, Category=CameraCollision)
|
||||
bool bVisualLogCameraCollision = false;
|
||||
#endif
|
||||
|
||||
public:
|
||||
UStevesSpringArmComponent();
|
||||
|
||||
/// Smoothly change the target offset, instead of jumping
|
||||
UFUNCTION(BlueprintCallable, Category="SpringArm")
|
||||
void SetTargetOffsetSmooth(const FVector& NewTargetOffset, float Speed = 5);
|
||||
/// Interrupt a smooth target offset change, freeze where we are
|
||||
UFUNCTION(BlueprintCallable, Category="SpringArm")
|
||||
void CancelTargetOffsetSmooth();
|
||||
|
||||
/// Smoothly change the socket offset, instead of jumping
|
||||
UFUNCTION(BlueprintCallable, Category="SpringArm")
|
||||
void SetSocketOffsetSmooth(const FVector& NewSocketOffset, float Speed = 5);
|
||||
/// Interrupt a smooth socket offset change, freeze where we are
|
||||
UFUNCTION(BlueprintCallable, Category="SpringArm")
|
||||
void CancelSocketOffsetSmooth();
|
||||
|
||||
/// Immediately jump to where the arm wants to be, ignoring interpolation
|
||||
/// Useful if you need the camera to be where it should be right now
|
||||
UFUNCTION(BlueprintCallable, Category="SpringArm")
|
||||
void JumpToDesiredLocation();
|
||||
protected:
|
||||
virtual void UpdateDesiredArmLocation(bool bDoTrace,
|
||||
bool bDoLocationLag,
|
||||
bool bDoRotationLag,
|
||||
float DeltaTime) override;
|
||||
virtual FVector BlendLocations(const FVector& DesiredArmLocation,
|
||||
const FVector& TraceHitLocation,
|
||||
bool bHitSomething,
|
||||
float DeltaTime) override;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "UObject/GCObject.h"
|
||||
|
||||
typedef TSharedPtr<struct FStevesTextureRenderTargetReservation> FStevesTextureRenderTargetReservationPtr;
|
||||
typedef TSharedPtr<struct FStevesTextureRenderTargetPool> FStevesTextureRenderTargetPoolPtr;
|
||||
|
||||
/// Holder for an assigned texture. While this structure exists, the texture will be considered assigned
|
||||
/// and will not be returned from any other request. Once this structure is destroyed the texture will
|
||||
/// be free for re-use. For that reason, only pass this structure around by SharedRef/SharedPtr.
|
||||
/// The texture is held by a weak pointer however, the strong pointer is held by the pool. The texture will continue
|
||||
/// to be available to this reservation except if the pool is told to forcibly release textures.
|
||||
struct STEVESUEHELPERS_API FStevesTextureRenderTargetReservation
|
||||
{
|
||||
public:
|
||||
/// The texture. May be null if the pool has forcibly reclaimed the texture prematurely
|
||||
TWeakObjectPtr<UTextureRenderTarget2D> Texture;
|
||||
TWeakPtr<struct FStevesTextureRenderTargetPool> ParentPool;
|
||||
TWeakObjectPtr<const UObject> CurrentOwner;
|
||||
|
||||
FStevesTextureRenderTargetReservation() = default;
|
||||
|
||||
FStevesTextureRenderTargetReservation(UTextureRenderTarget2D* InTexture,
|
||||
FStevesTextureRenderTargetPoolPtr InParent,
|
||||
const UObject* InOwner)
|
||||
: Texture(InTexture),
|
||||
ParentPool(InParent),
|
||||
CurrentOwner(InOwner)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
~FStevesTextureRenderTargetReservation();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A pool of render target textures. To save pre-creating render textures as assets, and to control the re-use of
|
||||
* these textures at runtime.
|
||||
* A pool needs to be owned by a UObject, which will in turn own the textures and so will ultimately control the
|
||||
* ultimate lifecycle of textures if not released specifically.
|
||||
* See FCompElementRenderTargetPool for inspiration
|
||||
*/
|
||||
struct STEVESUEHELPERS_API FStevesTextureRenderTargetPool : public FGCObject, public TSharedFromThis<FStevesTextureRenderTargetPool>
|
||||
{
|
||||
|
||||
protected:
|
||||
/// The name of the pool. It's possible to have more than one texture pool.
|
||||
FName Name;
|
||||
|
||||
struct FTextureKey
|
||||
{
|
||||
FIntPoint Size;
|
||||
ETextureRenderTargetFormat Format;
|
||||
|
||||
friend bool operator==(const FTextureKey& Lhs, const FTextureKey& RHS)
|
||||
{
|
||||
return Lhs.Size == RHS.Size
|
||||
&& Lhs.Format == RHS.Format;
|
||||
}
|
||||
|
||||
friend bool operator!=(const FTextureKey& Lhs, const FTextureKey& RHS)
|
||||
{
|
||||
return !(Lhs == RHS);
|
||||
}
|
||||
|
||||
friend uint32 GetTypeHash(const FTextureKey& Key)
|
||||
{
|
||||
return HashCombine(GetTypeHash(Key.Size), static_cast<uint32>(Key.Format));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
TWeakObjectPtr<UObject> PoolOwner;
|
||||
TMultiMap<FTextureKey, TObjectPtr<UTextureRenderTarget2D>> UnreservedTextures;
|
||||
TSet<TObjectPtr<UTextureRenderTarget2D>> ReservedTextures;
|
||||
|
||||
/// Weak reverse tracking of reservations, mostly for debugging
|
||||
struct FReservationInfo
|
||||
{
|
||||
FTextureKey Key;
|
||||
TWeakObjectPtr<const UObject> Owner;
|
||||
TWeakObjectPtr<UTextureRenderTarget2D> Texture;
|
||||
|
||||
FReservationInfo(const FTextureKey& InKey, const UObject* InOwner, UTextureRenderTarget2D* InTexture)
|
||||
: Key(InKey),
|
||||
Owner(InOwner),
|
||||
Texture(InTexture)
|
||||
{
|
||||
}
|
||||
};
|
||||
TArray<FReservationInfo> Reservations;
|
||||
|
||||
|
||||
friend struct FStevesTextureRenderTargetReservation;
|
||||
/// Release a reservation on a texture, allowing it back into the pool
|
||||
/// Protected because only FStevesTextureRenderTargetReservation will need to do this.
|
||||
void ReleaseReservation(UTextureRenderTarget2D* Tex);
|
||||
public:
|
||||
|
||||
explicit FStevesTextureRenderTargetPool(const FName& InName, UObject* InOwner)
|
||||
: Name(InName), PoolOwner(InOwner)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~FStevesTextureRenderTargetPool() override;
|
||||
|
||||
const FName& GetName() const { return Name; }
|
||||
|
||||
// FGCObject
|
||||
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5
|
||||
// FGCObject
|
||||
virtual FString GetReferencerName() const override;
|
||||
#endif
|
||||
/**
|
||||
* Reserve a texture for use as a render target. This will create a new texture target if needed.
|
||||
* @param Size The dimensions of the texture
|
||||
* @param Format Format of the texture
|
||||
* @param Owner The UObject which will temporarily own this texture (mostly for debugging, this object won't in fact "own" it
|
||||
* as per garbage collection rules, the reference is weak
|
||||
* @return A shared pointer to a structure which holds the reservation for this texture. When that structure is
|
||||
* destroyed, it will release the texture back to the pool.
|
||||
*/
|
||||
FStevesTextureRenderTargetReservationPtr ReserveTexture(FIntPoint Size, ETextureRenderTargetFormat Format, const UObject* Owner);
|
||||
|
||||
/**
|
||||
* Forcibly revoke reservations in this pool, either for all owners or for a specific owner.
|
||||
* Reservations which are revoked will have their weak texture pointers invalidated.
|
||||
* @param ForOwner If null, revoke all reservations for any owner, or if provided, just for a specific owner.
|
||||
*/
|
||||
void RevokeReservations(const UObject* ForOwner = nullptr);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Destroy previously created textures and free the memory.
|
||||
* @param bForceAndRevokeReservations If false, only destroys unreserved textures. If true, destroys reserved textures
|
||||
* as well (the weak pointer on their reservations will cease to be valid)
|
||||
*/
|
||||
void DrainPool(bool bForceAndRevokeReservations = false);
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
|
||||
#include "StevesGameSubsystem.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogStevesUEHelpers, Verbose, Verbose);
|
||||
|
||||
class FStevesUEHelpers : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
|
||||
inline UStevesGameSubsystem* GetStevesGameSubsystem(UWorld* WorldContext)
|
||||
{
|
||||
if (IsValid(WorldContext) && WorldContext->IsGameWorld())
|
||||
{
|
||||
auto GI = WorldContext->GetGameInstance();
|
||||
if (IsValid(GI))
|
||||
return GI->GetSubsystem<UStevesGameSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogFocusSystem, Log, All)
|
||||
|
||||
class FFocusSystem
|
||||
{
|
||||
protected:
|
||||
TArray<TWeakObjectPtr<class UFocusableUserWidget>> ActiveAutoFocusWidgets;
|
||||
|
||||
TWeakObjectPtr<UFocusableUserWidget> GetHighestFocusPriority();
|
||||
public:
|
||||
void FocusableWidgetConstructed(UFocusableUserWidget* Widget);
|
||||
void FocusableWidgetDestructed(UFocusableUserWidget* Widget);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Button.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "Styling/SlateTypes.h"
|
||||
#include "Widgets/SWidget.h"
|
||||
|
||||
#include "FocusableButton.generated.h"
|
||||
|
||||
class SFocusableButton;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonFocusReceivedEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonFocusLostEvent);
|
||||
|
||||
/**
|
||||
* This is a simple subclass of UButton to provide some missing features
|
||||
*
|
||||
* * Focus events
|
||||
* * Focus style based on hover style
|
||||
* * Assign focus to self on hover to prevent double-highlighting
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UFocusableButton : public UButton
|
||||
{
|
||||
GENERATED_UCLASS_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bUseHoverStyleWhenFocussed = true;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Button|Event")
|
||||
FOnButtonFocusReceivedEvent OnFocusReceived;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Button|Event")
|
||||
FOnButtonFocusLostEvent OnFocusLost;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bTakeFocusOnHover = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bLoseFocusOnUnhover = true;
|
||||
|
||||
// Simulate a button press
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
void SimulatePress();
|
||||
// Simulate a button release
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
void SimulateRelease();
|
||||
|
||||
protected:
|
||||
FButtonStyle FocussedStyle;
|
||||
|
||||
void SlateHandleFocusReceived();
|
||||
void SlateHandleFocusLost();
|
||||
|
||||
void ApplyFocusStyle();
|
||||
void UndoFocusStyle();
|
||||
void SlateHandleHovered();
|
||||
void SlateHandleUnhovered();
|
||||
|
||||
void Unfocus() const;
|
||||
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
|
||||
/// Update the focussed style based on changes made to the default widget style.
|
||||
/// Call this if you make runtime changes to the base style of this button.
|
||||
/// Needed because we can't override SetStyle
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Focus")
|
||||
void RefreshFocussedStyle();
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/CheckBox.h"
|
||||
#include "FocusableCheckBox.generated.h"
|
||||
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCheckBoxHoveredEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCheckBoxUnhoveredEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCheckBoxFocusReceivedEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCheckBoxFocusLostEvent);
|
||||
/**
|
||||
* This is a simple subclass of UCheckBox to provide some missing features
|
||||
*
|
||||
* * Focus events
|
||||
* * Hover events
|
||||
* * Focus style based on hover style
|
||||
* * Assign focus to self on hover to prevent double-highlighting
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UFocusableCheckBox : public UCheckBox
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bUseHoverStyleWhenFocussed = true;
|
||||
|
||||
UPROPERTY( BlueprintAssignable, Category = "CheckBox|Event" )
|
||||
FOnCheckBoxHoveredEvent OnHovered;
|
||||
|
||||
UPROPERTY( BlueprintAssignable, Category = "CheckBox|Event" )
|
||||
FOnCheckBoxUnhoveredEvent OnUnhovered;
|
||||
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="CheckBox|Event")
|
||||
FOnCheckBoxFocusReceivedEvent OnFocusReceived;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="CheckBox|Event")
|
||||
FOnCheckBoxFocusLostEvent OnFocusLost;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bTakeFocusOnHover = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bLoseFocusOnUnhover = true;
|
||||
|
||||
protected:
|
||||
FCheckBoxStyle FocussedStyle;
|
||||
|
||||
void SlateHandleFocusReceived();
|
||||
void SlateHandleFocusLost();
|
||||
|
||||
void ApplyFocusStyle();
|
||||
void UndoFocusStyle();
|
||||
void SlateHandleHovered();
|
||||
void SlateHandleUnhovered();
|
||||
|
||||
void Unfocus() const;
|
||||
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
public:
|
||||
|
||||
/// Update the focussed style based on changes made to the default widget style.
|
||||
/// Call this if you make runtime changes to the base style of this checkbox.
|
||||
/// Needed because we can't override SetStyle
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Focus")
|
||||
void RefreshFocussedStyle();
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "FocusableInputInterceptorUserWidget.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
|
||||
void UFocusableInputInterceptorUserWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (!InputPreprocessor.IsValid())
|
||||
{
|
||||
InputPreprocessor = MakeShareable(new FUiInputPreprocessor());
|
||||
InputPreprocessor->OnUiKeyDown.BindUObject(this, &UFocusableInputInterceptorUserWidget::HandleKeyDownEvent);
|
||||
}
|
||||
FSlateApplication::Get().RegisterInputPreProcessor(InputPreprocessor);
|
||||
|
||||
}
|
||||
|
||||
void UFocusableInputInterceptorUserWidget::NativeDestruct()
|
||||
{
|
||||
Super::NativeDestruct();
|
||||
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(InputPreprocessor);
|
||||
}
|
||||
|
||||
|
||||
bool UFocusableInputInterceptorUserWidget::HandleKeyDownEvent(const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
// Do nothing by default
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "FocusableUserWidget.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
|
||||
#include "FocusableInputInterceptorUserWidget.generated.h"
|
||||
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class STEVESUEHELPERS_API UFocusableInputInterceptorUserWidget : public UFocusableUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// Nested class which we'll use to poke into input events before anything else eats them
|
||||
// Without this it seems impossible to pick up e.g. Gamepad "B" button with UMG up
|
||||
// It means we hardcode the controls for menus but that's OK in practice
|
||||
class FUiInputPreprocessor : public IInputProcessor, public TSharedFromThis<FUiInputPreprocessor>
|
||||
{
|
||||
public:
|
||||
DECLARE_DELEGATE_RetVal_OneParam(bool, FOnUiKeyDown, const FKeyEvent&);
|
||||
FOnUiKeyDown OnUiKeyDown;
|
||||
|
||||
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override
|
||||
{
|
||||
return OnUiKeyDown.Execute(InKeyEvent);
|
||||
}
|
||||
// Required by IInputProcessor but we don't need
|
||||
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Crs) override{}
|
||||
};
|
||||
|
||||
protected:
|
||||
TSharedPtr<FUiInputPreprocessor> InputPreprocessor;
|
||||
|
||||
public:
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct();
|
||||
|
||||
UFUNCTION()
|
||||
virtual bool HandleKeyDownEvent(const FKeyEvent& InKeyEvent);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "FocusableUserWidget.h"
|
||||
|
||||
#include "FocusablePanel.generated.h"
|
||||
|
||||
/// Base class for a UI Panel which has the concept of having focus, delegated to one of its children.
|
||||
/// When told, it can initialise focus to a default widget. It can also remember which of its children
|
||||
/// are currently focussed and restore that later.
|
||||
/// Calling SetFocusProperly does the default behaviour of preferring previous but falling back on the default.
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UFocusablePanel : public UFocusableUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
|
||||
public:
|
||||
/// The name of the widget which will be initially focussed by default
|
||||
/// This is a name because we can't link directly at edit time
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Focus")
|
||||
FName InitialFocusWidgetName;
|
||||
|
||||
// I'd love to make the above a drop-down but it's a lot of faff
|
||||
// See Engine\Source\Editor\UMGEditor\Private\Customizations\WidgetNavigationCustomization.cpp
|
||||
// Specifically OnGenerateWidgetList
|
||||
|
||||
/**
|
||||
* @brief Set the current focus to the initial focus widget
|
||||
* @return Whether the focus was successfully set
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
bool SetFocusToInitialWidget();
|
||||
|
||||
/**
|
||||
* @brief Get's the desired focus widget. Allows implementation in BP for dynamically generated menus
|
||||
* @return The initial focus widget
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="Focus")
|
||||
UWidget* GetInitialFocusWidget();
|
||||
|
||||
/**
|
||||
* @brief Set the desired initial focus widget. Call to set the initial widget dynamically without overriding GetInitialFocusWidget
|
||||
* @param NewInitialFocus The initial focus widget
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
void SetInitialFocusWidget(UWidget* NewInitialFocus);
|
||||
|
||||
/**
|
||||
* @brief Try to restore focus to the previously focussed child
|
||||
* @return Whether the focus was successfully set
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
bool RestorePreviousFocus() const;
|
||||
|
||||
/**
|
||||
* @brief Try to save the currently focussed child as something that can be restored later.
|
||||
* @return Whether focus was saved
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Focus")
|
||||
bool SavePreviousFocus();
|
||||
|
||||
|
||||
/// When SetFocusProperly is called, either restores previous selection or gives it to the initial selection
|
||||
virtual void SetFocusProperly_Implementation() override;
|
||||
protected:
|
||||
|
||||
/// The widget that should get the focus on init if in keyboard / gamepad mode
|
||||
/// Looked up at runtime from the FName
|
||||
TWeakObjectPtr<UWidget> InitialFocusWidget;
|
||||
|
||||
/// Previously focussed child which can be restored
|
||||
TWeakObjectPtr<UWidget> PreviousFocusWidget;
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Slider.h"
|
||||
#include "FocusableSlider.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSliderHoveredEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSliderUnhoveredEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSliderFocusReceivedEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSliderFocusLostEvent);
|
||||
|
||||
/**
|
||||
* A focusable version of the slider control, which highlights things like hover when the controller
|
||||
* has the focus on it.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UFocusableSlider : public USlider
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bUseHoverStyleWhenFocussed = true;
|
||||
|
||||
UPROPERTY( BlueprintAssignable, Category = "Slider|Event" )
|
||||
FOnSliderHoveredEvent OnHovered;
|
||||
|
||||
UPROPERTY( BlueprintAssignable, Category = "Slider|Event" )
|
||||
FOnSliderUnhoveredEvent OnUnhovered;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Slider|Event")
|
||||
FOnSliderFocusReceivedEvent OnFocusReceived;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Slider|Event")
|
||||
FOnSliderFocusLostEvent OnFocusLost;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bTakeFocusOnHover = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Focus")
|
||||
bool bLoseFocusOnUnhover = true;
|
||||
|
||||
protected:
|
||||
FSliderStyle FocussedStyle;
|
||||
|
||||
void SlateHandleFocusReceived();
|
||||
void SlateHandleFocusLost();
|
||||
|
||||
void ApplyFocusStyle();
|
||||
void UndoFocusStyle();
|
||||
void SlateHandleHovered();
|
||||
void SlateHandleUnhovered();
|
||||
|
||||
void Unfocus() const;
|
||||
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
public:
|
||||
|
||||
/// Update the focussed style based on changes made to the default widget style.
|
||||
/// Call this if you make runtime changes to the base style of this checkbox.
|
||||
/// Needed because we can't override SetStyle
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Focus")
|
||||
void RefreshFocussedStyle();
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
|
||||
#include "FocusableUserWidget.generated.h"
|
||||
|
||||
// Hacky intermediate type for UUserWidget so that we can have focusable child widgets via SetFocusProperly
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UFocusableUserWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// If enabled, this widget will opt-in to the list of widgets which can be given focus
|
||||
/// automatically when another UFocusableUserWidget with focus is removed from the viewport.
|
||||
/// Useful for making sure something has the focus at all times without having to have cross-dependencies
|
||||
/// between UI parts, or events everywhere
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Focus")
|
||||
bool bEnableAutomaticFocus = false;
|
||||
|
||||
/// If bEnableAutomaticFocus is enabled, then this is the focus priority associated with this widget.
|
||||
/// In the event that there is more than one auto focus widget available, the highest priority widget will win.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Focus")
|
||||
int AutomaticFocusPriority = 0;
|
||||
|
||||
virtual FReply NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent) override;
|
||||
|
||||
public:
|
||||
/// UWidget::SetFocus is not virtual FFS. This does the same as SetFocus by default but can be overridden,
|
||||
/// e.g. to delegate focus to specific children
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Focus")
|
||||
void SetFocusProperly();
|
||||
|
||||
/// Whether this widget is *currently* requesting focus. Default is to use IsAutomaticFocusEnabled but subclasses
|
||||
/// may override this to be volatile
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Focus")
|
||||
bool IsRequestingFocus() const;
|
||||
|
||||
/// Tell this widget to take the focus if it desires to
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Focus")
|
||||
bool TakeFocusIfDesired();
|
||||
|
||||
virtual bool IsAutomaticFocusEnabled() const { return bEnableAutomaticFocus; }
|
||||
virtual int GetAutomaticFocusPriority() const { return AutomaticFocusPriority; }
|
||||
|
||||
protected:
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "UiTheme.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "InputAction.h"
|
||||
#include "Components/Image.h"
|
||||
#include "StevesHelperCommon.h"
|
||||
#include "Tickable.h"
|
||||
#include "InputImage.generated.h"
|
||||
|
||||
/// A special widget containing an image which populates itself based on an input action / axis and can dynamically
|
||||
/// change based on the active input method.
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UInputImage : public UImage, public FTickableGameObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/// What type of an input binding this image should look up
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
EInputBindingType BindingType;
|
||||
|
||||
/// If BindingType is Action/Axis, the name of it
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
FName ActionOrAxisName;
|
||||
|
||||
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
|
||||
UPROPERTY(EditAnywhere, Category="InputImage") // can't be inside #if
|
||||
TSoftObjectPtr<UInputAction> InputAction;
|
||||
|
||||
/// Where there are multiple mappings, which to prefer
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
EInputImageDevicePreference DevicePreference = EInputImageDevicePreference::Auto;
|
||||
|
||||
/// If BindingType is Key, the key
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
FKey Key;
|
||||
|
||||
/// When input mode changes, how quickly to update
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
float UpdateDelay = 0.1f;
|
||||
|
||||
/// Option to set this image as visible when bound, even if its state is hidden from outside
|
||||
/// The default behaviour is that if this image is hidden by other means, visibility is not overridden.
|
||||
/// This is safer but can result in a 1-frame blank image sometimes. Instead if you set this to true you can
|
||||
/// set your images to hidden in the designer and have them display themselves when there's something to show.
|
||||
/// You can then control invisiblity for other reasons from a parent widget.
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
bool bOverrideHiddenState = false;
|
||||
|
||||
/// The player index for which the input should be looked up
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
int PlayerIndex = 0;
|
||||
|
||||
/// Custom theme to use for this input image set; if not supplied will use UStevesGameSubsystem::DefaultUiTheme
|
||||
UPROPERTY(EditAnywhere, Category="InputImage")
|
||||
TObjectPtr<UUiTheme> CustomTheme;
|
||||
|
||||
bool bSubbedToInputEvents = false;
|
||||
bool bIsDirty = true;
|
||||
float DelayUpdate = 0;
|
||||
bool bHiddenBecauseBlank;
|
||||
ESlateVisibility OldVisibility;
|
||||
|
||||
public:
|
||||
|
||||
/// Tell this image to display the bound action for the current input method
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual void SetFromAction(FName Name);
|
||||
|
||||
/// Tell this image to display the bound axis for the current input method
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual void SetFromAxis(FName Name);
|
||||
|
||||
/// Tell this image to display a specific key image
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual void SetFromKey(FKey K);
|
||||
|
||||
/// Tell this image to display Enhanced InputAction
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual void SetFromInputAction(UInputAction* Action);
|
||||
|
||||
/// Get the binding type that we'll use to populate the image
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual EInputBindingType GetBindingType() const { return BindingType; }
|
||||
|
||||
/// If BindingType is Action/Axis, get the name of the action or axis to look up the image for
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual FName GetActionOrAxisName() const { return ActionOrAxisName; };
|
||||
|
||||
/// If BindingType is Key, get the key
|
||||
UFUNCTION(BlueprintCallable, Category="InputImage")
|
||||
virtual FKey GetKey() const { return Key; }
|
||||
|
||||
/// Get the custom theme, if any
|
||||
virtual UUiTheme* GetCustomTheme() const { return CustomTheme; }
|
||||
/// Change the custom theme for this image
|
||||
virtual void SetCustomTheme(UUiTheme* Theme);
|
||||
|
||||
virtual void BeginDestroy() override;
|
||||
|
||||
virtual void SetVisibility(ESlateVisibility InVisibility) override;
|
||||
|
||||
// FTickableGameObject begin
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual TStatId GetStatId() const override;
|
||||
virtual bool IsTickableWhenPaused() const override;
|
||||
virtual bool IsTickableInEditor() const override;
|
||||
virtual bool IsTickable() const override;
|
||||
virtual ETickableTickType GetTickableTickType() const override;
|
||||
// FTickableGameObject end
|
||||
|
||||
virtual void FinishDestroy() override;
|
||||
|
||||
protected:
|
||||
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void MarkImageDirty();
|
||||
virtual void UpdateImage();
|
||||
|
||||
UFUNCTION()
|
||||
void OnInputModeChanged(int ChangedPlayerIdx, EInputMode InputMode);
|
||||
UFUNCTION()
|
||||
void OnEnhancedInputMappingsChanged();
|
||||
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "InputCoreTypes.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "PaperSprite.h"
|
||||
#include "KeySprite.generated.h"
|
||||
|
||||
/// Struct for the rows of a DataTable which will hold the mapping from an FKey to a Paper Sprite which represents it
|
||||
/// Used for on-screen prompts
|
||||
USTRUCT(BlueprintType)
|
||||
struct STEVESUEHELPERS_API FKeySprite : public FTableRowBase
|
||||
{
|
||||
GENERATED_USTRUCT_BODY()
|
||||
|
||||
// Import a DataTable using this struct by creating a CSV file like this:
|
||||
//
|
||||
// Name,Key,Sprite
|
||||
// 1,Enter,"PaperSprite'/Game/Textures/UI/Frames/Keyboard_Black_Enter'"
|
||||
// 2,SpaceBar,"PaperSprite'/Game/Textures/UI/Frames/Keyboard_Black_Space'"
|
||||
//
|
||||
// Key is just the latter part of EKeys::Name
|
||||
// Sprite is the path to the Paper2D sprite (most likely from a shared sprite sheet)
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="KeySprite")
|
||||
FKey Key;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="KeySprite")
|
||||
TObjectPtr<UPaperSprite> Sprite = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "MenuStack.h"
|
||||
#include "FocusablePanel.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
|
||||
#include "MenuBase.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMenuClosed, UMenuBase*, Menu, bool, bWasCancelled);
|
||||
|
||||
/// This class is a type of focusable panel designed for menus or other dialogs.
|
||||
/// It can be added to a UMenuStack to put it in context of a larger navigable group,
|
||||
/// and if so represents one level in the chain of an assumed modal stack. Use UMenuStack::PushMenuByClass/Object
|
||||
/// to add an entry of this type to the stack
|
||||
/// If you use this class standalone instead without a stack, then you must call Open() on this instance to
|
||||
/// make it add itself to the viewport.
|
||||
UCLASS(Abstract, BlueprintType)
|
||||
class STEVESUEHELPERS_API UMenuBase : public UFocusablePanel
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Raised just as the menu is closing
|
||||
UPROPERTY(BlueprintAssignable, Category="Menu")
|
||||
FOnMenuClosed OnClosed;
|
||||
|
||||
/// Raised just after the menu has closed
|
||||
UPROPERTY(BlueprintAssignable, Category="Menu")
|
||||
FOnMenuClosed AfterClosed;
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, Category="Menu")
|
||||
TWeakObjectPtr<UMenuStack> ParentStack;
|
||||
|
||||
/// Whether this menu should request focus when it is displayed
|
||||
/// The widget which is focussed will either be the InitialFocusWidget on newly displayed, or
|
||||
/// the previously selected widget if regaining focus
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Focus")
|
||||
bool bRequestFocus = true;
|
||||
|
||||
/// Set this property to true if you want this menu to embed itself in the parent UMenuStack's MenuContainer
|
||||
/// If false, this Menu will be added to the viewport independently and can float over other menus in the stack
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Layout")
|
||||
bool bEmbedInParentContainer = true;
|
||||
|
||||
/// Whether to hide this menu when it's superceded by another in the stack.
|
||||
/// If you set this to "false" when bEmbedInParentContainer=true, then the superceding menu should have its
|
||||
/// own bEmbedInParentContainer set to false in order to overlay on top of this one.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
bool bHideWhenSuperceded = true;
|
||||
|
||||
/// Whether this panel should block clicks itself (useful for preventing click-through).
|
||||
/// Set to false if you want to be able to click through the panel to other elements.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
bool bBlockClicks = true;
|
||||
|
||||
/// How this menu should set the input mode when it becomes the top of the stack
|
||||
/// This can be useful if your menus have variable input settings between levels in the stack
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EInputModeChange InputModeSetting = EInputModeChange::DoNotChange;
|
||||
|
||||
/// How this menu should set the mouse pointer visibility when it becomes the top of the stack
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EMousePointerVisibilityChange MousePointerVisibility = EMousePointerVisibilityChange::DoNotChange;
|
||||
|
||||
/// How this menu should set the game pause state when it becomes top of the stack
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EGamePauseChange GamePauseSetting = EGamePauseChange::DoNotChange;
|
||||
|
||||
virtual void EmbedInParent();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
bool ValidateClose(bool bWasCancel);
|
||||
|
||||
/// Called when this menu is superceded by another menu being pushed on to this stack
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void OnSupercededInStack(UMenuBase* ByMenu);
|
||||
/// Called when this menu is superceded by another menu being pushed on to this stack
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void OnRegainedFocusInStack();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void OnAddedToStack(UMenuStack* Parent);
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void OnRemovedFromStack(UMenuStack* Parent);
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* @brief Open this menu. You should only call this if you're NOT using this in a UMenuStack, because the stack will
|
||||
* call it for you when you add this menu to it
|
||||
|
||||
* @param bIsRegainedFocus Set this to true if the reason this menu is opening is that it regained focus in a stack
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
void Open(bool bIsRegainedFocus = false);
|
||||
/**
|
||||
* @brief Request this menu to close. The menu can veto this request.
|
||||
* @param bWasCancel Set this to true if the reason for closure was a cancellation action
|
||||
* @return True if the request was approved
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
bool RequestClose(bool bWasCancel);
|
||||
/**
|
||||
* @brief Close this menu. This ALWAYS closes the menu, if you want it to be able to veto it, call RequestClose
|
||||
* @param bWasCancel Set this to true if the reason for closure was a cancellation action
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
void Close(bool bWasCancel);
|
||||
|
||||
|
||||
|
||||
TWeakObjectPtr<UMenuStack> GetParentStack() const { return ParentStack; }
|
||||
virtual bool IsRequestingFocus_Implementation() const override { return bRequestFocus; }
|
||||
|
||||
void AddedToStack(UMenuStack* Parent);
|
||||
void RemovedFromStack(UMenuStack* Parent);
|
||||
void SupercededInStack(UMenuBase* ByMenu);
|
||||
void RegainedFocusInStack();
|
||||
void InputModeChanged(EInputMode OldMode, EInputMode NewMode);
|
||||
|
||||
/// Return whether this menu is currently at the top of the menu stack
|
||||
/// Note: if this menu is not owned by a stack, will always return true
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
bool IsTopOfStack() const;
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
|
||||
#include "FocusableInputInterceptorUserWidget.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
#include "StevesHelperCommon.h"
|
||||
|
||||
|
||||
#include "MenuStack.generated.h"
|
||||
|
||||
class UContentWidget;
|
||||
class UMenuBase;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMenuStackClosed, class UMenuStack*, Stack, bool, bWasCancel);
|
||||
|
||||
/// Represents a modal stack of menus which take focus and have a concept of "Back"
|
||||
/// Each level within is a MenuBase, which must be "pushed" on to the stack.
|
||||
/// Contained within MenuSystem (multiple menu stacks supported)
|
||||
/// Has a Priority so that when multiple menu stacks are open, higher priority gets focus,
|
||||
/// and when closed, next highest priority gets focus back. Focus is given when the first MenuBase is pushed onto the stack,
|
||||
/// and given up when the last one is popped.
|
||||
/// You can style this widget to be the general surrounds in which all MenuBase levels live inside
|
||||
/// Create a Blueprint subclass of this and make sure you include a UContentWidget with the name
|
||||
/// "MenuContainer" somewhere in the tree, which is where the menu contents will be placed.
|
||||
UCLASS(Abstract, BlueprintType)
|
||||
class STEVESUEHELPERS_API UMenuStack : public UFocusableInputInterceptorUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
EInputMode LastInputMode;
|
||||
EInputModeChange PreviousInputMode;
|
||||
EMousePointerVisibilityChange PreviousMouseVisibility;
|
||||
EGamePauseChange PreviousPauseState;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UMenuBase>> Menus;
|
||||
|
||||
virtual void BeforeFirstMenuOpened();
|
||||
virtual void FirstMenuOpened();
|
||||
virtual void LastMenuClosed(bool bWasCancel);
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
void SavePreviousInputMousePauseState();
|
||||
virtual void ApplyInputModeChange(EInputModeChange Change) const;
|
||||
virtual void ApplyMousePointerVisibility(EMousePointerVisibilityChange Change) const;
|
||||
virtual void ApplyGamePauseChange(EGamePauseChange Change) const;
|
||||
|
||||
virtual bool HandleKeyDownEvent(const FKeyEvent& InKeyEvent) override;
|
||||
UFUNCTION()
|
||||
void InputModeChanged(int PlayerIndex, EInputMode NewMode);
|
||||
|
||||
FDateTime TimeFirstOpen;
|
||||
|
||||
public:
|
||||
/// Input keys which go back a level in the menu stack (default Esc and B gamepad button)
|
||||
/// Clear this list if you don't want this behaviour
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
|
||||
TArray<FKey> BackKeys = TArray<FKey> { EKeys::Escape, EKeys::Gamepad_FaceButton_Right };
|
||||
|
||||
/// Input keys which instantly close the menu stack (default gamepad start)
|
||||
/// Clear this list if you don't want this behaviour
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
|
||||
TArray<FKey> InstantCloseKeys = TArray<FKey> { EKeys::Gamepad_Special_Right };
|
||||
|
||||
/// Whether to automatically add this stack to the viewport when a menu is pushed onto it, if not already added
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Viewport")
|
||||
bool bAutoAddToViewport = true;
|
||||
/// Whether to automatically remove this stack from the viewport when the last menu is removed
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Viewport")
|
||||
bool bAutoRemoveFromViewport = true;
|
||||
|
||||
/// This property will bind to a blueprint variable of the same name to contain the actual menu content
|
||||
/// If not set, or the UiMenuBase is set to not use this container, levels are added independently to viewport
|
||||
/// Use a NamedSlot for this most of the time, it gives you the most layout flexibility.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="Menu")
|
||||
TObjectPtr<UContentWidget> MenuContainer;
|
||||
|
||||
/// Event raised when the stack is closed for any reason. If bWasCancel, the menu stack was closed because the
|
||||
/// last item was cancelled.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnMenuStackClosed OnClosed;
|
||||
|
||||
/// How this stack should change the input mode when it opens (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EInputModeChange InputModeSettingOnOpen = EInputModeChange::DoNotChange;
|
||||
|
||||
/// When changing input mode, whether to flush pressed keys so we start fresh
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
bool bFlushOnInputModeChange = false;
|
||||
|
||||
/// How this stack should set the mouse pointer visibility when it opens (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EMousePointerVisibilityChange MousePointerVisibilityOnOpen = EMousePointerVisibilityChange::DoNotChange;
|
||||
|
||||
/// How this stack should set the game pause state when it opens (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EGamePauseChange GamePauseSettingOnOpen = EGamePauseChange::DoNotChange;
|
||||
|
||||
/// How this stack should change the input mode when it closes (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EInputModeChange InputModeSettingOnClose = EInputModeChange::DoNotChange;
|
||||
|
||||
/// How this stack should set the mouse pointer visibility when it closes (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EMousePointerVisibilityChange MousePointerVisibilityOnClose = EMousePointerVisibilityChange::DoNotChange;
|
||||
|
||||
/// How this stack should set the game pause state when it closes (default no change)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behavior")
|
||||
EGamePauseChange GamePauseSettingOnClose = EGamePauseChange::DoNotChange;
|
||||
|
||||
/// Minimum amount of time a menu should be open before responding to instant close key (prevent fast close because of leaked input)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Behaviour")
|
||||
float MinTimeOpen = 0.5f;
|
||||
|
||||
/// Push a new menu level by class. This will instantiate the new menu, display it, and inform the previous menu that it's
|
||||
/// been superceded. Use the returned instance if you want to cache it
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
UMenuBase* PushMenuByClass(TSubclassOf<UMenuBase> MenuClass);
|
||||
|
||||
/// Push a new menu level by instance on to the stack. This will display the new menu and inform the previous menu that it's
|
||||
/// been superceded, which will most likely mean it will be hidden (but will retain its state)
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
void PushMenuByObject(UMenuBase* NewMenu);
|
||||
|
||||
/// Pop the top level of the menu stack. This *destroys* the top level menu, meaning it will lose all of its state.
|
||||
/// You won't need to call this manually most of the time, because calling Close() on the MenuBase will do it.
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
void PopMenu(bool bWasCancel);
|
||||
|
||||
/// Get the number of active levels in the menu
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
int Count() const { return Menus.Num(); }
|
||||
|
||||
/// Close the entire stack at once. This does not give any of the menus chance to do anything before close, so if you
|
||||
/// want them to do that, use PopMenu() until Count() == 0 instead
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
void CloseAll(bool bWasCancel);
|
||||
|
||||
/// Whether the top MenuBase on this stack is requesting focus
|
||||
virtual bool IsRequestingFocus_Implementation() const override;
|
||||
|
||||
virtual void SetFocusProperly_Implementation() override;
|
||||
virtual void PopMenuIfTop(UMenuBase* UiMenuBase, bool bWasCancel);
|
||||
virtual void RemoveFromParent() override;
|
||||
|
||||
/// Return the menu which is currently top of the stack
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
UMenuBase* GetTopMenu() const;
|
||||
/// Return the menu which is currently right below the top of the stack
|
||||
UFUNCTION(BlueprintCallable, Category="Menu")
|
||||
UMenuBase* GetPreviousMenu() const;
|
||||
|
||||
UMenuStack();
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "MultiSubtitleVerticalbox.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
* A text block that can show multiple lines of subtitles. The subtitles are
|
||||
* displayed in the text box with the most recent line first, and subtitles
|
||||
* time out after a configurable time once they stop being emitted by the
|
||||
* engine.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UMultiSubtitleVerticalbox : public UVerticalBox
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/// The number of subtitles that may be shown at any given time.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1.0"), Category = "Subtitles")
|
||||
uint8 ConcurrentLines = 1;
|
||||
|
||||
/// The length of time in seconds subtitles should remain in the block for.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = ( ClampMin = "1.0" ), Category = "Subtitles")
|
||||
float MaximumAge = 4;
|
||||
|
||||
/** The color of the text shown in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance")
|
||||
FSlateColor ColorAndOpacity = FLinearColor::White;
|
||||
;
|
||||
|
||||
/** The minimum desired size for the text in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance")
|
||||
float MinDesiredWidth;
|
||||
|
||||
/** The font to render the text with in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance")
|
||||
FSlateFontInfo Font;
|
||||
|
||||
/** The brush to strike through text with in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance")
|
||||
FSlateBrush StrikeBrush;
|
||||
|
||||
/** The direction the shadow is cast in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance")
|
||||
FVector2D ShadowOffset = FVector2D(1.0f, 1.0f);
|
||||
|
||||
/** The color of the shadow in child text boxes */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Textbox Appearance", meta = (DisplayName = "Shadow Color"))
|
||||
FLinearColor ShadowColorAndOpacity = FLinearColor::Transparent;
|
||||
|
||||
virtual void BeginDestroy() override;
|
||||
|
||||
protected:
|
||||
bool bSubscribed;
|
||||
|
||||
void SetSubtitleText(const FText& Text);
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
struct FSubtitleHistory
|
||||
{
|
||||
FText Subtitle;
|
||||
uint64 LastUpdate;
|
||||
|
||||
// Less than operator needed by TArray<> to sort the subtitles in
|
||||
// order of update.
|
||||
const bool operator<(const FSubtitleHistory& rhs) const
|
||||
{
|
||||
// Use > here to sort in reverse. The or ensures stability if two
|
||||
// subtitles are playing simultaneously
|
||||
return !this -> Subtitle.IsEmpty() && (this->LastUpdate > rhs.LastUpdate || this->Subtitle.CompareTo(rhs.Subtitle) < 0);
|
||||
}
|
||||
};
|
||||
|
||||
/** Create a new TextBlock widget and add it to the vertical box. This will
|
||||
* copy the 'Textbox Appearance' settings across into the new text block
|
||||
* before adding it to the vertical box.
|
||||
*
|
||||
* @return A pointer to the newly created TextBlock, or nullptr on error
|
||||
*/
|
||||
TObjectPtr<UTextBlock> CreateTextBlock();
|
||||
|
||||
/** Update the subtitle history list to remove timed-out lines, and
|
||||
* then set the contents of the text box to the new list of subtitles.
|
||||
*/
|
||||
void UpdateSubtitles();
|
||||
|
||||
// Handle for a timer that calls UpdateSubtitles() once per second
|
||||
FTimerHandle SubtitleUpdateTimer;
|
||||
|
||||
// The list of text blocks in the widget
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UTextBlock>> TextBlocks;
|
||||
|
||||
// The list of subtitles to show
|
||||
TArray<FSubtitleHistory> Subtitles;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "OptionWidgetBase.h"
|
||||
#include "NamedOptionWidgetBase.generated.h"
|
||||
|
||||
/**
|
||||
* Subclass of UOptionWidget base to provide convenient access via FNames, on top of the regular integers.
|
||||
* Unlike UOptionWidgetBase, this widget can only be populated in code, you can't set up the options
|
||||
* and the default selection in the designer.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UNamedOptionWidgetBase : public UOptionWidgetBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
TMap<FName, int> NameIndex;
|
||||
FName SelectedName = NAME_None;
|
||||
|
||||
public:
|
||||
virtual void ClearOptions() override;
|
||||
/**
|
||||
* @brief Adds a new named option
|
||||
* @param Name The name to identify the option by
|
||||
* @param Option The text for the new option
|
||||
* @return The index for the new option
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="NamedOptionWidget")
|
||||
virtual int AddNamedOption(FName Name, FText Option);
|
||||
|
||||
|
||||
virtual void SetOptions(const TArray<FText>& Options, int NewSelectedIndex = 0) override;
|
||||
/**
|
||||
* @brief Sets all of the named options available for this control
|
||||
* @param NewOptions All options to be available, indexed by name
|
||||
* @param NewSelectedName Which of the options to select by default
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="NamedOptionWidget")
|
||||
virtual void SetNamedOptions(const TMap<FName, FText>& NewOptions, FName NewSelectedName = NAME_None);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="NamedOptionWidget")
|
||||
virtual FName GetSelectedName() const;
|
||||
|
||||
/// Get the index in this option list corresponding to a name
|
||||
UFUNCTION(BlueprintCallable, Category="NamedOptionWidget")
|
||||
virtual int GetIndexForName(FName Name);
|
||||
|
||||
/// Get the name in this option list corresponding to an index
|
||||
UFUNCTION(BlueprintCallable, Category="NamedOptionWidget")
|
||||
virtual FName GetNameForIndex(int Index);
|
||||
|
||||
virtual void SetSelectedIndex(int NewIndex) override;
|
||||
/**
|
||||
* @brief Change the selected option by name
|
||||
* @param Name The name of the new option to set, can be None for no selection
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="NamedOptionWidget")
|
||||
virtual void SetSelectedName(FName Name);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "FocusableUserWidget.h"
|
||||
#include "Styling/SlateTypes.h"
|
||||
#include "StevesHelperCommon.h"
|
||||
#include "OptionWidgetBase.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
class UImage;
|
||||
class UButton;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSelectedOptionChanged, class UOptionWidgetBase*, Widget, int, NewIndex);
|
||||
|
||||
UCLASS(Abstract, BlueprintType)
|
||||
class STEVESUEHELPERS_API UOptionWidgetBase : public UFocusableUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
|
||||
public:
|
||||
|
||||
// -- Properties automatically bound to Blueprint widget
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UWidget> MouseVersion;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UButton> MouseUpButton;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UButton> MouseDownButton;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UImage> MouseUpImage;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UImage> MouseDownImage;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UTextBlock> MouseText;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UButton> GamepadVersion;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UImage> GamepadUpImage;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UImage> GamepadDownImage;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (BindWidget), Category="OptionWidget")
|
||||
TObjectPtr<UTextBlock> GamepadText;
|
||||
|
||||
/// Event raised when the selected option changes
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnSelectedOptionChanged OnSelectedOptionChanged;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
/// Remove all options
|
||||
virtual void ClearOptions();
|
||||
|
||||
/**
|
||||
* @brief Adds a new option
|
||||
* @param Option The text for the new option
|
||||
* @return The index for the new option
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual int AddOption(FText Option);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Sets all of the options available for this control
|
||||
* @param Options All options to be available
|
||||
* @param NewSelectedIndex Which of the options to select by default
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void SetOptions(const TArray<FText>& Options, int NewSelectedIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="OptionWidget")
|
||||
virtual int GetSelectedIndex() const { return SelectedIndex; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="OptionWidget")
|
||||
virtual FText GetSelectedOption() const;
|
||||
/**
|
||||
* @brief Change the selected index option
|
||||
* @param NewIndex The new index to set, can be -1 for no selection
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void SetSelectedIndex(int NewIndex);
|
||||
|
||||
virtual void SetFocusProperly_Implementation() override;
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Content)
|
||||
TArray<FText> Options;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Content)
|
||||
int SelectedIndex;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void SetMouseMode();
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void SetButtonMode();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void UpdateFromInputMode(EInputMode Mode);
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual void ChangeOption(int Delta);
|
||||
UFUNCTION(BlueprintCallable, Category="OptionWidget")
|
||||
virtual EInputMode GetCurrentInputMode() const;
|
||||
virtual void UpdateUpDownButtons();
|
||||
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="OptionWidget")
|
||||
void PostSelectedOptionChanged();
|
||||
|
||||
protected:
|
||||
UFUNCTION()
|
||||
void InputModeChanged(int PlayerIndex, EInputMode NewMode);
|
||||
UFUNCTION()
|
||||
void MouseUpClicked();
|
||||
UFUNCTION()
|
||||
void MouseDownClicked();
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/RichTextBlockDecorator.h"
|
||||
|
||||
#include "RichTextBlockInputImageDecorator.generated.h"
|
||||
|
||||
class UUiTheme;
|
||||
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API URichTextBlockInputImageDecorator : public URichTextBlockDecorator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
virtual TSharedPtr<ITextDecorator> CreateDecorator(URichTextBlock* InOwner) override;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ProgressBar.h"
|
||||
#include "SmoothChangingProgressBar.generated.h"
|
||||
|
||||
/**
|
||||
* A specialised progress bar that can be told to change its percent smoothly instead of all at once.
|
||||
* Note: Because SetPercent isn't virtual on UProgressBar, you need to use the alternate SetPercentSmoothly
|
||||
* function instead, and call StopSmoothPercentChange to interrupt it if you need to manually set it using
|
||||
* SetPercent.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API USmoothChangingProgressBar : public UProgressBar
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
TWeakPtr<FActiveTimerHandle> SmoothChangeHandle;
|
||||
|
||||
float TargetPercent;
|
||||
|
||||
void UnregisterTimer();
|
||||
EActiveTimerReturnType TickPercent(double CurrTime, float DeltaTime);
|
||||
public:
|
||||
/// The speed at which the progress bar changes. This value means the max percentage changes
|
||||
/// in one second. Set this to 0 to make changes instant. Changes to this value only affect
|
||||
/// the next call to SetPercentSmoothly.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Progress")
|
||||
float PercentChangeSpeed = 1.0f;
|
||||
|
||||
/// The frequency at which we should update the bar. Set this to 0 to update every frame,
|
||||
/// or > 0 to update every X seconds (useful to save tick time for slow updates).
|
||||
/// Changes to this value only affect the next call to SetPercentSmoothly.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Progress")
|
||||
float PercentChangeFrequency = 0.0f;
|
||||
|
||||
/// Changes the bar percentage smoothly from its current value.
|
||||
/// Automatically interrupts any existing smooth change.
|
||||
UFUNCTION(BlueprintCallable, Category="Progress")
|
||||
void SetPercentSmoothly(float InPercent);
|
||||
|
||||
/// Stop any pending smooth changes to percent
|
||||
/// Call this if you need to interrupt any current smooth change and
|
||||
UFUNCTION(BlueprintCallable, Category="Progress")
|
||||
void StopSmoothPercentChange();
|
||||
|
||||
virtual void BeginDestroy() override;
|
||||
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "StevesSubtitleTextblock.generated.h"
|
||||
|
||||
/**
|
||||
* A simple Text Block that automatically displays subtitle text. Just drop it in and style it
|
||||
* how you want, subtitles will appear here when played, and the default canvas-rendered crappy
|
||||
* versions will be disabled.
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UStevesSubtitleTextblock : public UTextBlock
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void BeginDestroy() override;
|
||||
|
||||
protected:
|
||||
bool bSubscribed;
|
||||
|
||||
void SetSubtitleText(const FText& Text);
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "TabButton.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTabSelected,int,TabIndex);
|
||||
|
||||
/**
|
||||
* Tab button - widget that is instantiated on a tab list
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UTabButton : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
|
||||
public:
|
||||
|
||||
///
|
||||
|
||||
UPROPERTY(BlueprintAssignable,BlueprintCallable, Category="TabButton")
|
||||
FOnTabSelected OnTabSelected;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="TabButton")
|
||||
int TabIndex;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="TabButton")
|
||||
bool bTabSelected=false;
|
||||
|
||||
void Initialise(int InTabIndex,const FText& InLabel);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category="TabButton")
|
||||
void SetTabLabel(const FText& InLabel);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
void SetTabSelected(bool bSelected);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
void NotifyTabSelected();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category="TabButton")
|
||||
void RefreshForSelectionChanged();
|
||||
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "StevesUI/FocusableInputInterceptorUserWidget.h"
|
||||
#include "TabListWidget.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContentWidgetChanged,UWidget*,PreviousContentWidget,UWidget*,NewContentWidget);
|
||||
|
||||
class UWidgetSwitcher;
|
||||
class UTabButton;
|
||||
/** Information about a registered tab in the tab list */
|
||||
USTRUCT()
|
||||
struct FTabListRegisteredTabInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** The index of this tab */
|
||||
UPROPERTY()
|
||||
int32 TabIndex;
|
||||
|
||||
/** The button for this tab */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UTabButton> TabButton;
|
||||
|
||||
/** The content to be activated. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UWidget> ContentInstance;
|
||||
|
||||
FTabListRegisteredTabInfo()
|
||||
: TabIndex(INDEX_NONE)
|
||||
, TabButton(nullptr)
|
||||
, ContentInstance(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tab list widget - provides a tab selection bar that can control a widget switcher
|
||||
*/
|
||||
UCLASS()
|
||||
class STEVESUEHELPERS_API UTabListWidget : public UFocusableInputInterceptorUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
|
||||
/// Event called when tab content has changed
|
||||
UPROPERTY(BlueprintCallable,BlueprintAssignable)
|
||||
FOnContentWidgetChanged OnContentWidgetChanged;
|
||||
|
||||
/// Input keys which navigate to the previous tab (left)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
|
||||
TArray<FKey> PreviousTabKeys = TArray<FKey> { EKeys::Gamepad_LeftShoulder, EKeys::Q };
|
||||
|
||||
/// Input keys which navigate to the next tab (right)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
|
||||
TArray<FKey> NextTabKeys = TArray<FKey> { EKeys::Gamepad_RightShoulder, EKeys::E };
|
||||
|
||||
/// Map of tabs, buttons, content
|
||||
TMap<FName,FTabListRegisteredTabInfo> RegisteredTabs;
|
||||
|
||||
// Where the tab buttons will be added
|
||||
UPROPERTY(BlueprintReadWrite, meta = (BindWidget), Category="TabButton")
|
||||
TObjectPtr<UPanelWidget> TabButtonContainer;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="TabButton")
|
||||
int SelectedTabIndex=-1;
|
||||
|
||||
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category="TabButton")
|
||||
FMargin TabButtonPadding=FMargin(10,0);
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="TabButton")
|
||||
TObjectPtr<UWidgetSwitcher> TargetWidgetSwitcher;
|
||||
|
||||
/**
|
||||
* Registers tab content with this tab widget
|
||||
* @param TabID The ID to use for this widget
|
||||
* @param ButtonWidgetType The class of button to use
|
||||
* @param ContentWidget The widget to activate when pressed
|
||||
* @return True If successful
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
bool RegisterTab(FName TabID, TSubclassOf<UTabButton> ButtonWidgetType,FText Label, UWidget* ContentWidget);
|
||||
|
||||
/**
|
||||
* Called when a given tab button has been pressed
|
||||
* @param TabIndex
|
||||
*/
|
||||
UFUNCTION()
|
||||
void TabPressed(int TabIndex);
|
||||
|
||||
/**
|
||||
* Assign a widget switcher to be driven by this tab selection.
|
||||
* @param WidgetSwitcher
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
void SetTargetWidgetSwitcher(UWidgetSwitcher* WidgetSwitcher);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
void IncrementTabIndex(int Amount);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="TabButton")
|
||||
void SelectTab(int TabIndex);
|
||||
|
||||
virtual bool HandleKeyDownEvent(const FKeyEvent& InKeyEvent) override;
|
||||
|
||||
private:
|
||||
bool GetTabDataForIndex(int Index,FTabListRegisteredTabInfo& TabInfo);
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
// Original Copyright (c) Sam Bloomberg https://github.com/redxdev/UnrealRichTextDialogueBox (MIT License)
|
||||
// Updates:
|
||||
// 1. Fixed adding a spurious newline to single-line text
|
||||
// 2. Expose line finished as event
|
||||
// 3. Changed names of classes to indicate functionality better (typewriter not dialogue)
|
||||
// 4. Update minimum desired size on calculate so that flexi boxes can start at the correct size
|
||||
// instead of growing when the newline is added
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Components/RichTextBlock.h"
|
||||
#include "Framework/Text/RichTextLayoutMarshaller.h"
|
||||
#include "Framework/Text/SlateTextLayout.h"
|
||||
#include "TypewriterTextWidget.generated.h"
|
||||
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTypewriterLineFinished, class UTypewriterTextWidget*, Widget);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterLetterAdded, class UTypewriterTextWidget*, Widget, const FString&, Char);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterStartWord, class UTypewriterTextWidget*, Widget, const FString&, Word);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterRunNameChanged, class UTypewriterTextWidget*, Widget, const FString&, NewRunName);
|
||||
/**
|
||||
* A text block that exposes more information about text layout for typewriter widget.
|
||||
*/
|
||||
UCLASS()
|
||||
class URichTextBlockForTypewriter : public URichTextBlock
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
FORCEINLINE TSharedPtr<FSlateTextLayout> GetTextLayout() const
|
||||
{
|
||||
return TextLayout;
|
||||
}
|
||||
|
||||
FORCEINLINE TSharedPtr<FRichTextLayoutMarshaller> GetTextMarshaller() const
|
||||
{
|
||||
return TextMarshaller;
|
||||
}
|
||||
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
private:
|
||||
TSharedPtr<FSlateTextLayout> TextLayout;
|
||||
TSharedPtr<FRichTextLayoutMarshaller> TextMarshaller;
|
||||
};
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class STEVESUEHELPERS_API UTypewriterTextWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UTypewriterTextWidget(const FObjectInitializer& ObjectInitializer);
|
||||
void ClearLetterCountdownTimer();
|
||||
|
||||
/// Event called when a line has finished playing, whether on its own or when skipped to end
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnTypewriterLineFinished OnTypewriterLineFinished;
|
||||
|
||||
/// Event called when one or more new letters have been displayed
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnTypewriterLetterAdded OnTypewriterLetterAdded;
|
||||
|
||||
/// Event called when a new word has started
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnTypewriterStartWord OnTypewriterStartWord;
|
||||
|
||||
/// Event called when the "run name" of the text changes aka the rich text style markup. Also called when reverts to default.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnTypewriterRunNameChanged OnTypewriterRunNameChanged;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Typewriter")
|
||||
TObjectPtr<URichTextBlockForTypewriter> LineText;
|
||||
|
||||
/// The amount of time between printing individual letters (for the "typewriter" effect).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
float LetterPlayTime = 0.025f;
|
||||
|
||||
/// The amount of time to wait after finishing the line before actually marking it completed.
|
||||
/// This helps prevent accidentally progressing dialogue on short lines.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
float EndHoldTime = 0.15f;
|
||||
|
||||
/// How long to pause at sentence terminators ('.', '!', '?') before proceeding (ignored if at end of text)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
float PauseTimeAtSentenceTerminators = 0.5f;
|
||||
|
||||
/// Characters which will terminate a sentence, which will potentially pause
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
FString SentenceTerminators = ".!?";
|
||||
|
||||
/// Characters which terminate a clause, which is a preferred place to split an overly long line
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
FString ClauseTerminators = ",;:";
|
||||
|
||||
/// If true (default), only pauses at sentence terminators if there's whitespace following them.
|
||||
/// This prevents pausing on strings like ".txt"
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
bool bPauseOnlyIfWhitespaceFollowsSentenceTerminator = true;
|
||||
|
||||
/// If set > 0, splits a single PlayLine into multiple segments of this number of lines maximum
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
int MaxNumberOfLines = 0;
|
||||
|
||||
/// If set to true, the text will continue to be played when paused
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
bool bPlayWhenPaused = false;
|
||||
|
||||
/// If set to true, OnTypewriterStartWord will be called when a new word starts
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||
bool bNewWordEvent = false;
|
||||
|
||||
/// Set Text immediately
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void SetText(const FText& InText);
|
||||
|
||||
/// Set Text immediately
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
FText GetText() const;
|
||||
|
||||
|
||||
/**
|
||||
* Play a line of text.
|
||||
* Note: if, when line splits are calculated, this line exceeds MaxNumberOfLines, then only this number of lines
|
||||
* will be played by this call. In that case, HasMoreLineParts() will return true, and you will need to call
|
||||
* PlayNextLinePart() to play the remainder of the line.
|
||||
* @param InLine The input line
|
||||
* @param Speed
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void PlayLine(const FText& InLine, float Speed = 1.0f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void GetCurrentLine(FText& OutLine) const { OutLine = CurrentLine; }
|
||||
|
||||
/// Return whether the entire line has finished playing
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
bool HasFinishedPlayingLine() const { return bHasFinishedPlaying; }
|
||||
|
||||
/// Returns whether the number of lines exceeded MaxNumberOfLines and there are still parts to play.
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
bool HasMoreLineParts() const { return bHasMoreLineParts; }
|
||||
|
||||
/// If HasMoreLineParts() is true, play the next part of the line originally requested by PlayLine
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void PlayNextLinePart(float Speed = 1.0f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void SkipToLineEnd();
|
||||
|
||||
/// Get the name of the current rich text run, if any
|
||||
const FString& GetCurrentRunName() const { return CurrentRunName; }
|
||||
|
||||
/// Get typewriter play speed
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
float GetPlaySpeed() const {return CurrentPlaySpeed;}
|
||||
|
||||
/// Set typewriter play speed
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void SetPlaySpeed(float Speed) {CurrentPlaySpeed = Speed;}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||
void FindWordVowels(const FString& Word, TArray<int>& VowelsPos);
|
||||
|
||||
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
|
||||
|
||||
protected:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
/// Called when on or more letters are added, if subclasses want to override
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
|
||||
void OnPlayLetter();
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
|
||||
void OnLineFinishedPlaying();
|
||||
|
||||
int OnStartNewWord(const FString SegmentRemain);
|
||||
|
||||
/// Name of the current rich text run, if any
|
||||
/// Can be used to identify the style of the most recently played letter
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Typewriter")
|
||||
FString CurrentRunName;
|
||||
|
||||
|
||||
private:
|
||||
void PlayNextLetter();
|
||||
bool IsSentenceTerminator(TCHAR Letter) const;
|
||||
bool IsClauseTerminator(TCHAR Letter) const;
|
||||
bool IsPunctuation(TCHAR Letter) const;
|
||||
int FindLastTerminator(const FString& CurrentLineString, int Count) const;
|
||||
|
||||
int CalculateMaxLength();
|
||||
void CalculateWrappedString(const FString& CurrentLineString);
|
||||
FString CalculateSegments(FString* OutCurrentRunName);
|
||||
void StartPlayLine();
|
||||
|
||||
UPROPERTY()
|
||||
FText CurrentLine;
|
||||
|
||||
FString RemainingLinePart;
|
||||
|
||||
struct FTypewriterTextSegment
|
||||
{
|
||||
FString Text;
|
||||
FRunInfo RunInfo;
|
||||
};
|
||||
TArray<FTypewriterTextSegment> Segments;
|
||||
|
||||
// The section of the text that's already been printed out and won't ever change.
|
||||
// This lets us cache some of the work we've already done. We can't cache absolutely
|
||||
// everything as the last few characters of a string may change if they're related to
|
||||
// a named run that hasn't been completed yet.
|
||||
FString CachedSegmentText;
|
||||
int32 CachedLetterIndex = 0;
|
||||
|
||||
int32 CurrentSegmentIndex = 0;
|
||||
int32 CurrentLetterIndex = 0;
|
||||
int32 MaxLetterIndex = 0;
|
||||
int32 NumberOfLines = 0;
|
||||
int32 NextBlankLetterLeft = -1;
|
||||
float CombinedTextHeight = 0;
|
||||
|
||||
uint32 bHasFinishedPlaying : 1;
|
||||
uint32 bHasMoreLineParts : 1;
|
||||
|
||||
// Properties related to animation
|
||||
float NextLetterCountdown = 0;
|
||||
float NextLetterCountdownInterval = 0;
|
||||
float StartPlayLineCountdown = 0;
|
||||
float SkipToLineEndCountdown = 0;
|
||||
|
||||
float CurrentPlaySpeed = 1;
|
||||
float PauseTime = 0;
|
||||
bool bFirstPlayLine = true;
|
||||
bool bLastSegmentEndsWithBlank = false;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "Engine/DataTable.h"
|
||||
|
||||
|
||||
#include "UiTheme.generated.h"
|
||||
|
||||
/// Custom asset to conveniently hold theme information for the UI
|
||||
/// Currently only lightly used to provide simple access to button images, but I intend to use
|
||||
/// this more extensively later
|
||||
UCLASS(Blueprintable)
|
||||
class STEVESUEHELPERS_API UUiTheme : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category="StevesUI")
|
||||
TSoftObjectPtr<UDataTable> KeyboardMouseImages;
|
||||
UPROPERTY(EditDefaultsOnly, Category="StevesUI")
|
||||
TSoftObjectPtr<UDataTable> XboxControllerImages;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUiHelpers.h"
|
||||
|
||||
#include "Components/HorizontalBoxSlot.h"
|
||||
#include "Components/PanelWidget.h"
|
||||
#include "Components/VerticalBoxSlot.h"
|
||||
|
||||
UPanelSlot* StevesUiHelpers::InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex)
|
||||
{
|
||||
checkf(AtIndex <= Parent->GetChildrenCount(), TEXT("Insertion index %d is greater than child count"), AtIndex);
|
||||
|
||||
// Short-circuit for simple case of insert at end
|
||||
const int OrigCount = Parent->GetChildrenCount();
|
||||
if (OrigCount == AtIndex)
|
||||
{
|
||||
return Parent->AddChild(Child);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: cannot use InsertChildAt, that's editor-only
|
||||
// There are a few options with the runtime API
|
||||
// 1. Remove all children and re-add them in the right order
|
||||
// 2. Remove all children on or after the insert point and re-add those after adding the new item
|
||||
// 3. Use ReplaceChildAt to insert a new item and move everything down, with AddChild at the end
|
||||
//
|
||||
// I'm going with 2. because that seems to blend best performance with the most well defined behaviour
|
||||
// I'm not sure ReplaceChildAt is designed to move children around (note: ShiftChild exists but again, editor-only)
|
||||
|
||||
TArray<UWidget*> WidgetsToReplaceReversed;
|
||||
TArray<FMargin> HVBoxPadding;
|
||||
|
||||
// Keep slot padding info
|
||||
HVBoxPadding.Reserve(OrigCount - AtIndex);
|
||||
// Go backwards for consistency with below
|
||||
const auto Slots = Parent->GetSlots();
|
||||
for (int i = OrigCount - 1; i >= AtIndex; --i)
|
||||
{
|
||||
if (Slots.IsValidIndex(i))
|
||||
{
|
||||
const auto Slot = Slots[i];
|
||||
if (const auto HSlot = Cast<UHorizontalBoxSlot>(Slot))
|
||||
{
|
||||
HVBoxPadding.Add(HSlot->GetPadding());
|
||||
}
|
||||
else if (const auto VSlot = Cast<UVerticalBoxSlot>(Slot))
|
||||
{
|
||||
HVBoxPadding.Add(VSlot->GetPadding());
|
||||
}
|
||||
else
|
||||
{
|
||||
HVBoxPadding.Add(FMargin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WidgetsToReplaceReversed.Reserve(OrigCount - AtIndex);
|
||||
// Go backwards so we can remove as we go and not remove from the middle
|
||||
for (int i = OrigCount - 1; i >= AtIndex; --i)
|
||||
{
|
||||
WidgetsToReplaceReversed.Add(Parent->GetChildAt(i));
|
||||
Parent->RemoveChildAt(i);
|
||||
}
|
||||
// insert item
|
||||
auto Slot = Parent->AddChild(Child);
|
||||
// add back previous, reverse order
|
||||
for (int i = WidgetsToReplaceReversed.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
auto PrevSlot = Parent->AddChild(WidgetsToReplaceReversed[i]);
|
||||
|
||||
// Restore the padding on H/VBox slots
|
||||
if (HVBoxPadding.IsValidIndex(i))
|
||||
{
|
||||
if (auto HSlot = Cast<UHorizontalBoxSlot>(PrevSlot))
|
||||
{
|
||||
HSlot->SetPadding(HVBoxPadding[i]);
|
||||
}
|
||||
else if (auto VSlot = Cast<UVerticalBoxSlot>(PrevSlot))
|
||||
{
|
||||
VSlot->SetPadding(HVBoxPadding[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Slot;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Widget.h"
|
||||
|
||||
/// Helper Ui routines that UE4 is missing, all static
|
||||
/// Exposed to BP in in StevesBPL
|
||||
class STEVESUEHELPERS_API StevesUiHelpers
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Insert a child widget at a specific index
|
||||
* @param Parent The container widget
|
||||
* @param Child The child widget to add
|
||||
* @param AtIndex The index at which the new child should exist
|
||||
* @returns The slot the child was inserted at
|
||||
*/
|
||||
static UPanelSlot* InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex = 0);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "VisualLogger/VisualLogger.h"
|
||||
|
||||
class STEVESUEHELPERS_API FStevesVisualLogger
|
||||
{
|
||||
protected:
|
||||
static void InternalPolyLogfImpl(const UObject* LogOwner,
|
||||
const FLogCategoryBase& Category,
|
||||
ELogVerbosity::Type Verbosity,
|
||||
const TArray<FVector>& Points,
|
||||
const FColor& Color,
|
||||
const uint16 Thickness);
|
||||
|
||||
|
||||
static void InternalPolyLogf(const UObject* LogOwner,
|
||||
const FLogCategoryBase& Category,
|
||||
ELogVerbosity::Type Verbosity,
|
||||
const FVector& Center,
|
||||
const FVector& UpAxis,
|
||||
const float OuterRadius,
|
||||
const float InnerRadius,
|
||||
const int NumPoints,
|
||||
const FColor& Color,
|
||||
const uint16 Thickness)
|
||||
{
|
||||
|
||||
const bool bIsStar = !FMath::IsNearlyEqual(OuterRadius, InnerRadius);
|
||||
|
||||
const FQuat Rotation = FQuat::FindBetweenNormals(FVector::UpVector, UpAxis);
|
||||
const FVector YAxis = Rotation.RotateVector(FVector::YAxisVector);
|
||||
|
||||
const int Count = bIsStar ? NumPoints * 2 : NumPoints;
|
||||
const float RotInc = UE_TWO_PI / Count;
|
||||
TArray<FVector> Vertices;
|
||||
// Start
|
||||
Vertices.Add(Center + YAxis * OuterRadius);
|
||||
for (int i = 1; i < Count; ++i)
|
||||
{
|
||||
const float Scale = bIsStar && (i % 2) != 0 ? InnerRadius : OuterRadius;
|
||||
const FQuat Rot = FQuat(UpAxis, RotInc * i);
|
||||
const FVector V = Center + Rot.RotateVector(YAxis * Scale);
|
||||
Vertices.Add(V);
|
||||
}
|
||||
// End
|
||||
Vertices.Add(Center + YAxis * OuterRadius);
|
||||
|
||||
InternalPolyLogfImpl(LogOwner, Category, Verbosity, Vertices, Color, Thickness);
|
||||
}
|
||||
|
||||
public:
|
||||
static void TriangleLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const FColor& Color, const uint16 Thickness)
|
||||
{
|
||||
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, 3, Color, Thickness);
|
||||
}
|
||||
|
||||
static void SquareLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const FColor& Color, const uint16 Thickness)
|
||||
{
|
||||
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, 4, Color, Thickness);
|
||||
}
|
||||
|
||||
static void PolyLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const int NumPoints, const FColor& Color, const uint16 Thickness)
|
||||
{
|
||||
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, NumPoints, Color, Thickness);
|
||||
}
|
||||
|
||||
static void StarLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float OuterRadius, const float InnerRadius, const int NumPoints, const FColor& Color, const uint16 Thickness)
|
||||
{
|
||||
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness);
|
||||
}
|
||||
};
|
||||
|
||||
#if ENABLE_VISUAL_LOG
|
||||
|
||||
// 2D Triangle shape
|
||||
#define UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::TriangleLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, 0)
|
||||
#define UE_CVLOG_TRIANGLE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, 0);}
|
||||
#define UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::TriangleLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, Thickness)
|
||||
#define UE_CVLOG_TRIANGLE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness);}
|
||||
|
||||
// 2D Square shape
|
||||
#define UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::SquareLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, 0)
|
||||
#define UE_CVLOG_SQUARE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, 0);}
|
||||
#define UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::SquareLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, Thickness)
|
||||
#define UE_CVLOG_SQUARE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness);}
|
||||
|
||||
// 2D poly shape
|
||||
#define UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::PolyLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, NumPoints, Color, 0)
|
||||
#define UE_CVLOG_POLY(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, 0);}
|
||||
#define UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::PolyLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness)
|
||||
#define UE_CVLOG_POLY_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness);}
|
||||
|
||||
// 2D star shape
|
||||
#define UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::StarLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, 0)
|
||||
#define UE_CVLOG_STAR(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, 0);}
|
||||
#define UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::StarLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness)
|
||||
#define UE_CVLOG_STAR_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness);}
|
||||
|
||||
#else
|
||||
#define UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_CVLOG_TRIANGLE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_CVLOG_TRIANGLE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
|
||||
// Square shape
|
||||
#define UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_CVLOG_SQUARE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
#define UE_CVLOG_SQUARE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
|
||||
|
||||
// 2D poly shape
|
||||
#define UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
|
||||
#define UE_CVLOG_POLY(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
|
||||
#define UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
|
||||
#define UE_CVLOG_POLY_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
|
||||
|
||||
// Any 2D star shape
|
||||
#define UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
|
||||
#define UE_CVLOG_STAR(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
|
||||
#define UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
|
||||
#define UE_CVLOG_STAR_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
using UnrealBuildTool;
|
||||
using System.IO;
|
||||
|
||||
public class StevesUEHelpers : ModuleRules
|
||||
{
|
||||
public StevesUEHelpers(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
}
|
||||
);
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"EnhancedInput",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"UMG",
|
||||
"Paper2D",
|
||||
"DeveloperSettings"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"RenderCore",
|
||||
"PhysicsCore",
|
||||
"Chaos",
|
||||
"ChaosCore",
|
||||
"NavigationSystem"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesFixedDataTableCustomisationLayout.h"
|
||||
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "Containers/Map.h"
|
||||
#include "DataTableEditorUtils.h"
|
||||
#include "Delegates/Delegate.h"
|
||||
#include "DetailWidgetRow.h"
|
||||
#include "Editor.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Fonts/SlateFontInfo.h"
|
||||
#include "Framework/Commands/UIAction.h"
|
||||
#include "HAL/Platform.h"
|
||||
#include "HAL/PlatformCrt.h"
|
||||
#include "Internationalization/Internationalization.h"
|
||||
#include "Internationalization/Text.h"
|
||||
#include "Misc/Attribute.h"
|
||||
#include "PropertyCustomizationHelpers.h"
|
||||
#include "PropertyEditorModule.h"
|
||||
#include "PropertyHandle.h"
|
||||
#include "Templates/Casts.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Widgets/DeclarativeSyntaxSupport.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
|
||||
class SToolTip;
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSsFixedDataTableCustomisationLayout"
|
||||
|
||||
void FStevesFixedDataTableCustomisationLayout::CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
|
||||
{
|
||||
DataTablePropertyHandle = InStructPropertyHandle->GetChildHandle("DataTable");
|
||||
RowNamePropertyHandle = InStructPropertyHandle->GetChildHandle("RowName");
|
||||
|
||||
if (InStructPropertyHandle->HasMetaData(TEXT("DataTable")))
|
||||
{
|
||||
// Find data table from asset ref
|
||||
const FString& DataTablePath = InStructPropertyHandle->GetMetaData(TEXT("DataTable"));
|
||||
if (UDataTable* DataTable = LoadObject<UDataTable>(nullptr, *DataTablePath, nullptr))
|
||||
{
|
||||
UObject* Existing = nullptr;
|
||||
const bool TablePicked = DataTablePropertyHandle->GetValue(Existing) == FPropertyAccess::Success;
|
||||
if (!TablePicked || Existing != DataTable)
|
||||
{
|
||||
DataTablePropertyHandle->SetValue(DataTable);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogDataTable, Warning, TEXT("No Datatable found at %s"), *DataTablePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogDataTable, Warning, TEXT("No Datatable meta tag present on property %s"), *InStructPropertyHandle->GetPropertyDisplayName().ToString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
FPropertyComboBoxArgs ComboArgs(RowNamePropertyHandle,
|
||||
FOnGetPropertyComboBoxStrings::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowStrings),
|
||||
FOnGetPropertyComboBoxValue::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowValueString));
|
||||
ComboArgs.ShowSearchForItemCount = 1;
|
||||
|
||||
|
||||
TSharedRef<SWidget> BrowseTableButton = PropertyCustomizationHelpers::MakeBrowseButton(
|
||||
FSimpleDelegate::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked),
|
||||
LOCTEXT("SsBrowseToDatatable", "Browse to DataTable in Content Browser"));
|
||||
HeaderRow
|
||||
.NameContent()
|
||||
[
|
||||
InStructPropertyHandle->CreatePropertyNameWidget()
|
||||
]
|
||||
.ValueContent()
|
||||
.MaxDesiredWidth(0.0f) // don't constrain the combo button width
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
[
|
||||
PropertyCustomizationHelpers::MakePropertyComboBox(ComboArgs)
|
||||
]
|
||||
+SHorizontalBox::Slot()
|
||||
.Padding(2.0f)
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
.AutoWidth()
|
||||
[
|
||||
BrowseTableButton
|
||||
]
|
||||
]; ;
|
||||
|
||||
FDataTableEditorUtils::AddSearchForReferencesContextMenu(HeaderRow, FExecuteAction::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnSearchForReferences));
|
||||
}
|
||||
|
||||
void FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked()
|
||||
{
|
||||
if (DataTablePropertyHandle.IsValid())
|
||||
{
|
||||
UObject* SourceDataTable = nullptr;
|
||||
if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
|
||||
{
|
||||
TArray<FAssetData> Assets;
|
||||
Assets.Add(SourceDataTable);
|
||||
GEditor->SyncBrowserToObjects(Assets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FStevesFixedDataTableCustomisationLayout::GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const
|
||||
{
|
||||
if (RowNamePropertyHandle.IsValid() && RowNamePropertyHandle->IsValidHandle() && DataTablePropertyHandle.IsValid() && DataTablePropertyHandle->IsValidHandle())
|
||||
{
|
||||
// If either handle is multiple value or failure, fail
|
||||
UObject* SourceDataTable = nullptr;
|
||||
if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
|
||||
{
|
||||
OutDataTable = Cast<UDataTable>(SourceDataTable);
|
||||
|
||||
if (RowNamePropertyHandle->GetValue(OutName) == FPropertyAccess::Success)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FStevesFixedDataTableCustomisationLayout::OnSearchForReferences()
|
||||
{
|
||||
UDataTable* DataTable;
|
||||
FName RowName;
|
||||
|
||||
if (GetCurrentValue(DataTable, RowName) && DataTable)
|
||||
{
|
||||
TArray<FAssetIdentifier> AssetIdentifiers;
|
||||
AssetIdentifiers.Add(FAssetIdentifier(DataTable, RowName));
|
||||
|
||||
FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers, FReferenceViewerParams());
|
||||
}
|
||||
}
|
||||
|
||||
FString FStevesFixedDataTableCustomisationLayout::OnGetRowValueString() const
|
||||
{
|
||||
if (!RowNamePropertyHandle.IsValid() || !RowNamePropertyHandle->IsValidHandle())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
FName RowNameValue;
|
||||
const FPropertyAccess::Result RowResult = RowNamePropertyHandle->GetValue(RowNameValue);
|
||||
if (RowResult == FPropertyAccess::Success)
|
||||
{
|
||||
if (RowNameValue.IsNone())
|
||||
{
|
||||
return LOCTEXT("DataTable_None", "None").ToString();
|
||||
}
|
||||
return RowNameValue.ToString();
|
||||
}
|
||||
else if (RowResult == FPropertyAccess::Fail)
|
||||
{
|
||||
return LOCTEXT("DataTable_None", "None").ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return LOCTEXT("MultipleValues", "Multiple Values").ToString();
|
||||
}
|
||||
}
|
||||
|
||||
void FStevesFixedDataTableCustomisationLayout::OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const
|
||||
{
|
||||
UDataTable* DataTable = nullptr;
|
||||
FName IgnoredRowName;
|
||||
|
||||
// Ignore return value as we will show rows if table is the same but row names are multiple values
|
||||
GetCurrentValue(DataTable, IgnoredRowName);
|
||||
|
||||
TArray<FName> AllRowNames;
|
||||
if (DataTable != nullptr)
|
||||
{
|
||||
for (TMap<FName, uint8*>::TConstIterator Iterator(DataTable->GetRowMap()); Iterator; ++Iterator)
|
||||
{
|
||||
AllRowNames.Add(Iterator.Key());
|
||||
}
|
||||
|
||||
// Sort the names alphabetically.
|
||||
AllRowNames.Sort(FNameLexicalLess());
|
||||
}
|
||||
|
||||
for (const FName& RowName : AllRowNames)
|
||||
{
|
||||
OutStrings.Add(MakeShared<FString>(RowName.ToString()));
|
||||
OutRestrictedItems.Add(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "Containers/Array.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "IPropertyTypeCustomization.h"
|
||||
#include "Templates/SharedPointer.h"
|
||||
#include "UObject/NameTypes.h"
|
||||
|
||||
class IPropertyHandle;
|
||||
class SToolTip;
|
||||
class UDataTable;
|
||||
class UScriptStruct;
|
||||
struct FAssetData;
|
||||
|
||||
/// Drop-down for data table row name when the table itself is fixed in meta tags
|
||||
class FStevesFixedDataTableCustomisationLayout : public IPropertyTypeCustomization
|
||||
{
|
||||
public:
|
||||
static TSharedRef<IPropertyTypeCustomization> MakeInstance()
|
||||
{
|
||||
return MakeShareable( new FStevesFixedDataTableCustomisationLayout );
|
||||
}
|
||||
|
||||
virtual void CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
|
||||
// Not needed, but must be implemented
|
||||
virtual void CustomizeChildren(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override {}
|
||||
|
||||
protected:
|
||||
bool GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const;
|
||||
void OnSearchForReferences();
|
||||
void OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const;
|
||||
FString OnGetRowValueString() const;
|
||||
void BrowseTableButtonClicked();
|
||||
|
||||
TSharedPtr<IPropertyHandle> DataTablePropertyHandle;
|
||||
TSharedPtr<IPropertyHandle> RowNamePropertyHandle;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#include "StevesUEHelpersEd.h"
|
||||
#include "StevesFixedDataTableCustomisationLayout.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FStevesUEHelpersEdModule"
|
||||
|
||||
void FStevesUEHelpersEdModule::StartupModule()
|
||||
{
|
||||
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
PropertyModule.RegisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FStevesFixedDataTableCustomisationLayout::MakeInstance));
|
||||
}
|
||||
|
||||
void FStevesUEHelpersEdModule::ShutdownModule()
|
||||
{
|
||||
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
PropertyModule.UnregisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle");
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FStevesUEHelpersEdModule, StevesUEHelpersEd)
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright Steve Streeting 2020 onwards
|
||||
// Released under the MIT license
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FStevesUEHelpersEdModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class StevesUEHelpersEd : ModuleRules
|
||||
{
|
||||
public StevesUEHelpersEd(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
}
|
||||
);
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
|
||||
"UnrealEd",
|
||||
"PropertyEditor",
|
||||
"DataTableEditor"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user