Initial push to repo

This commit is contained in:
2026-07-03 19:56:31 +02:00
commit 4cf4176d57
1305 changed files with 43455 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright © 2020 Steve Streeting
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@@ -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;
}

View File

@@ -0,0 +1,105 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#include "StevesBPL.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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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.5;
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;
}
}

View File

@@ -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);
}

View File

@@ -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.05, 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

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#include "StevesPluginSettings.h"

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}
}
}
}

View File

@@ -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());
}
}
}
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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)
{
}

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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 InitialSeed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream(InitialSeed=%i, Seed=%u)"), InitialSeed, InitialSeed);
}
};
/// "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 InitialSeed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream1D(InitialSeed=%i, Seed=%u)"), InitialSeed, InitialSeed);
}
};
/// "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 InitialSeed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream2D(InitialSeed=%i, Seed=%u)"), InitialSeed, InitialSeed);
}
};
/// "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 InitialSeed;
}
/**
* 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=%i, Seed=%u)"), InitialSeed, InitialSeed);
}
};

View File

@@ -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
};

View File

@@ -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();
};

View File

@@ -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));
}
};

View File

@@ -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; }
};

View File

@@ -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()
};

View File

@@ -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.2;
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;
};

View File

@@ -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);
};

View File

@@ -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")
};

View File

@@ -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);
};

View File

@@ -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; \
}

View File

@@ -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);
};

View File

@@ -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() {}
};

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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();
}
};

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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;
};

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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();
};

Some files were not shown because too many files have changed in this diff Show More