Add new plugins, refactor exp, introduce stat forge to replace GAS

This commit is contained in:
2026-07-15 22:48:04 +02:00
parent 3c084d9669
commit da0a0b643f
267 changed files with 33330 additions and 48 deletions

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 "Misc/ConfigCacheIni.h"
#include "StevesUiHelpers.h"
#include "StevesUI/StevesUI.h"
#include "ContentStreaming.h"
void UStevesBPL::SetWidgetFocus(UWidget* Widget)
{
SetWidgetFocusProperly(Widget);
}
UPanelSlot* UStevesBPL::InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex)
{
return StevesUiHelpers::InsertChildWidgetAt(Parent, Child, AtIndex);
}
FStevesBalancedRandomStream UStevesBPL::MakeBalancedRandomStream(int64 Seed)
{
return FStevesBalancedRandomStream(Seed);
}
void UStevesBPL::AddViewOriginToStreaming(const FVector& ViewOrigin,
float ScreenWidth,
float FOV,
float BoostFactor,
bool bOverrideLocation,
float Duration,
AActor* ActorToBoost)
{
IStreamingManager::Get().AddViewInformation(ViewOrigin,
ScreenWidth,
ScreenWidth / FMath::Tan(FMath::DegreesToRadians(FOV * 0.5f)),
BoostFactor,
bOverrideLocation,
Duration,
ActorToBoost);
}
void UStevesBPL::UpdateStreaming(float DeltaTime, bool bBlockUntilDone)
{
FStreamingManagerCollection& SM = IStreamingManager::Get();
SM.UpdateResourceStreaming(DeltaTime, true);
if (bBlockUntilDone)
{
SM.BlockTillAllRequestsFinished();
}
}
float UStevesBPL::GetPerceivedLuminance(const FLinearColor& InColour)
{
// ITU BT.709
return
InColour.R * 0.2126f +
InColour.G * 0.7152f +
InColour.B * 0.0722f;
}
float UStevesBPL::GetPerceivedLuminance2(const FLinearColor& InColour)
{
// ITU BT.601
return
InColour.R * 0.299f +
InColour.G * 0.587f +
InColour.B * 0.114f;
}
float UStevesBPL::HeadingAngle2D(const FVector2D& Dir)
{
const FVector2D NormDir = Dir.GetSafeNormal();
float Angle = FMath::Acos(NormDir.X);
if(NormDir.Y < 0.0f)
{
Angle *= -1.0f;
}
return Angle;
}
float UStevesBPL::AngleBetween2D(const FVector2D& DirA, const FVector2D& DirB)
{
const float HeadingA = HeadingAngle2D(DirA);
const float HeadingB = HeadingAngle2D(DirB);
// Get the shortest route
return FMath::FindDeltaAngleRadians(HeadingA, HeadingB);
}
FString UStevesBPL::GetProjectVersion()
{
FString AppVersion;
GConfig->GetString(
TEXT("/Script/EngineSettings.GeneralProjectSettings"),
TEXT("ProjectVersion"),
AppVersion,
GGameIni
);
return AppVersion;
}

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.5f;
switch(Func)
{
default:
case EStevesEaseFunction::Linear:
return InAlpha;
case EStevesEaseFunction::EaseIn_Sine:
return 1.f - FMath::Cos(InAlpha * UE_HALF_PI);
case EStevesEaseFunction::EaseOut_Sine:
return FMath::Sin(InAlpha * UE_HALF_PI);
case EStevesEaseFunction::EaseInOut_Sine:
return -(FMath::Cos(UE_PI * InAlpha) - 1.f) / 2.f;
case EStevesEaseFunction::EaseIn_Quad:
return InAlpha*InAlpha;
case EStevesEaseFunction::EaseOut_Quad:
return 1.f - (1.f - InAlpha) * (1.f - InAlpha);
case EStevesEaseFunction::EaseInOut_Quad:
return InAlpha < 0.5f ? 2.f * InAlpha * InAlpha : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 2.f) / 2.f;
case EStevesEaseFunction::EaseIn_Cubic:
return FMath::Pow(InAlpha, 3);
case EStevesEaseFunction::EaseOut_Cubic:
return 1.f - FMath::Pow(1.f - InAlpha, 3);
case EStevesEaseFunction::EaseInOut_Cubic:
return InAlpha < 0.5f ? 4.f * FMath::Pow(InAlpha, 3) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 3) / 2.f;
case EStevesEaseFunction::EaseIn_Quart:
return FMath::Pow(InAlpha, 4);
case EStevesEaseFunction::EaseOut_Quart:
return 1 - FMath::Pow(1.f - InAlpha, 4.f);
case EStevesEaseFunction::EaseInOut_Quart:
return InAlpha < 0.5f ? 8.f * FMath::Pow(InAlpha, 4) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 4) / 2.f;
case EStevesEaseFunction::EaseIn_Quint:
return FMath::Pow(InAlpha, 5);
case EStevesEaseFunction::EaseOut_Quint:
return 1 - FMath::Pow(1.f - InAlpha, 5);
case EStevesEaseFunction::EaseInOut_Quint:
return InAlpha < 0.5f ? 16.f * FMath::Pow(InAlpha, 5) : 1.f - FMath::Pow(-2.f * InAlpha + 2.f, 5) / 2.f;
case EStevesEaseFunction::EaseIn_Expo:
return InAlpha <= 0 ? 0 : FMath::Pow(2.f, 10.f * InAlpha - 10.f);
case EStevesEaseFunction::EaseOut_Expo:
return InAlpha >= 1.f ? 1.f : 1.f - FMath::Pow(2.f, -10.f * InAlpha);
case EStevesEaseFunction::EaseInOut_Expo:
if (InAlpha <= 0.f)
return 0;
if (InAlpha >= 1.f)
return 1;
return InAlpha < 0.5f
? FMath::Pow(2.f, 20.f * InAlpha - 10.f) / 2.f
: (2.f - FMath::Pow(2.f, -20.f * InAlpha + 10.f)) / 2.f;
case EStevesEaseFunction::EaseIn_Circ:
return 1.f - FMath::Sqrt(1.f - FMath::Pow(InAlpha, 2));
case EStevesEaseFunction::EaseOut_Circ:
return FMath::Sqrt(1.f - FMath::Pow(InAlpha - 1.f, 2));
case EStevesEaseFunction::EaseInOut_Circ:
return InAlpha < 0.5f
? (1.f - FMath::Sqrt(1.f - FMath::Pow(2.f * InAlpha, 2))) / 2.f
: (FMath::Sqrt(1.f - FMath::Pow(-2.f * InAlpha + 2.f, 2)) + 1.f) / 2.f;
case EStevesEaseFunction::EaseIn_Back:
return BackC3 * FMath::Pow(InAlpha, 3) - BackC1 * InAlpha * InAlpha;
case EStevesEaseFunction::EaseOut_Back:
return 1.f + BackC3 * FMath::Pow(InAlpha - 1.f, 3) + BackC1 * FMath::Pow(InAlpha - 1.f, 2.f);
case EStevesEaseFunction::EaseInOut_Back:
return InAlpha < 0.5f
? (FMath::Pow(2.f * InAlpha, 2) * ((BackC2 + 1.f) * 2.f * InAlpha - BackC2)) / 2.f
: (FMath::Pow(2.f * InAlpha - 2.f, 2) * ((BackC2 + 1) * (InAlpha * 2.f - 2.f) + BackC2) + 2.f) / 2.f;
case EStevesEaseFunction::EaseIn_Elastic:
if (InAlpha <= 0.f)
return 0;
if (InAlpha >= 1.f)
return 1;
return -FMath::Pow(2.f, 10.f * InAlpha - 10.f) * FMath::Sin((InAlpha * 10.f - 10.75f) * ElasticC4);
case EStevesEaseFunction::EaseOut_Elastic:
if (InAlpha <= 0.f)
return 0;
if (InAlpha >= 1.f)
return 1;
return FMath::Pow(2.f, -10.f * InAlpha) * FMath::Sin((InAlpha * 10.f - 0.75f) * ElasticC4) + 1.f;
case EStevesEaseFunction::EaseInOut_Elastic:
if (InAlpha <= 0.f)
return 0;
if (InAlpha >= 1.f)
return 1;
return InAlpha < 0.5f
? -(FMath::Pow(2.f, 20.f * InAlpha - 10.f) * FMath::Sin((20.f * InAlpha - 11.125f) * ElasticC5)) /
2.f
: (FMath::Pow(2.f, -20.f * InAlpha + 10.f) * FMath::Sin((20.f * InAlpha - 11.125f) * ElasticC5)) /
2.f + 1.f;
case EStevesEaseFunction::EaseIn_Bounce:
return 1 - EaseAlpha(1 - InAlpha, EStevesEaseFunction::EaseOut_Bounce);
case EStevesEaseFunction::EaseOut_Bounce:
{
constexpr float n1 = 7.5625f;
constexpr float d1 = 2.75f;
if (InAlpha < 1.f / d1) { return n1 * InAlpha * InAlpha; }
else if (InAlpha < 2.f / d1)
{
const float newAlpha = InAlpha - 1.5f/d1;
return n1 * newAlpha * newAlpha + 0.75f;
}
else if (InAlpha < 2.5f / d1)
{
const float newAlpha = InAlpha - 2.25f/d1;
return n1 * newAlpha * newAlpha + 0.9375f;
}
else
{
const float newAlpha = InAlpha - 2.625f/d1;
return n1 * newAlpha * newAlpha + 0.984375f;
}
}
case EStevesEaseFunction::EaseInOut_Bounce:
return InAlpha < 0.5f
? (1.f - EaseAlpha(1.f - 2.f * InAlpha, EStevesEaseFunction::EaseOut_Bounce)) / 2.f
: (1.f + EaseAlpha(2.f * InAlpha - 1.f, EStevesEaseFunction::EaseOut_Bounce)) / 2.f;
}
}

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.05f, false);
}
TSoftObjectPtr<UInputAction> UStevesGameSubsystem::FindEnhancedInputAction(const FString& Name)
{
if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::LoadModulePtr<FAssetRegistryModule>(TEXT("AssetRegistry")))
{
IAssetRegistry& AssetRegistry = AssetRegistryModule->Get();
if (auto Settings = GetDefault<UStevesPluginSettings>())
{
for (const auto& Dir : Settings->EnhancedInputActionSearchDirectories)
{
if (!FPackageName::IsValidPath(Dir.Path))
{
continue;
}
TArray<FAssetData> Assets;
FString Package = FPaths::Combine(Dir.Path, Name);
if (AssetRegistry.GetAssetsByPackageName(FName(*Package), Assets, true))
{
for (const FAssetData& Asset : Assets)
{
if (Asset.GetClass() == UInputAction::StaticClass())
{
return TSoftObjectPtr<UInputAction>(Asset.GetSoftObjectPath());
}
}
}
}
}
}
return nullptr;
}
void UStevesGameSubsystem::RegisterInterestInEnhancedInputAction(const UInputAction* Action, ETriggerEvent TriggerEvent)
{
// Avoid registering duplicate interest
FEnhancedInputInterest Interest(Action, TriggerEvent);
if (!RegisteredEnhancedInputActionInterests.Contains(Interest))
{
if (auto GI = GetGameInstance())
{
if (auto PC = GI->GetFirstLocalPlayerController())
{
if (auto EIC = Cast<UEnhancedInputComponent>(PC->InputComponent))
{
EIC->BindAction(Action, TriggerEvent, this, &ThisClass::EnhancedInputActionTriggered);
RegisteredEnhancedInputActionInterests.Add(Interest);
}
}
}
}
}
void UStevesGameSubsystem::UnregisterAllInterestInEnhancedInputActions()
{
if (auto GI = GetGameInstance())
{
if (auto PC = GI->GetFirstLocalPlayerController())
{
if (auto EIC = Cast<UEnhancedInputComponent>(PC->InputComponent))
{
TArray<uint32> HandlesToRemove;
for (auto& Binding : EIC->GetActionEventBindings())
{
if (Binding->IsBoundToObject(this))
{
HandlesToRemove.Add(Binding->GetHandle());
}
}
for (const uint32 Handle : HandlesToRemove)
{
EIC->RemoveBindingByHandle(Handle);
}
}
}
}
RegisteredEnhancedInputActionInterests.Empty();
}
void UStevesGameSubsystem::EnhancedInputActionTriggered(const FInputActionInstance& InputActionInstance)
{
OnEnhancedInputActionTriggered.Broadcast(InputActionInstance.GetSourceAction(), InputActionInstance.GetTriggerEvent());
}
void UStevesGameSubsystem::InitTheme()
{
DefaultUiTheme = LoadObject<UUiTheme>(nullptr, *DefaultUiThemePath, nullptr);
}
void UStevesGameSubsystem::InitForegroundCheck()
{
// Check foreground status every 0.5 seconds
GetWorld()->GetTimerManager().SetTimer(ForegroundCheckHandle, this, &UStevesGameSubsystem::CheckForeground, 0.5, true);
}
void UStevesGameSubsystem::CheckForeground()
{
bool bNewForeground = bIsForeground;
if (IsValid(GEngine) && IsValid(GEngine->GameViewport) && GEngine->GameViewport->Viewport)
bNewForeground = GEngine->GameViewport->Viewport->IsForegroundWindow();
if (bNewForeground != bIsForeground)
{
bIsForeground = bNewForeground;
InputDetector->bIgnoreEvents = !bIsForeground;
OnWindowForegroundChanged.Broadcast(bIsForeground);
}
}
void UStevesGameSubsystem::OnInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
{
// We can't check this during Initialize because it's too early
if (!bCheckedViewportClient)
{
auto GI = GetGameInstance();
auto VC = Cast<UStevesGameViewportClientBase>(GI->GetGameViewportClient());
if (!VC)
UE_LOG(LogStevesUEHelpers, Log, TEXT("Consider using UStevesGameViewportClientBase for your GameViewportClient"))
bCheckedViewportClient = true;
}
auto Settings = GetDefault<UStevesPluginSettings>();
if (Settings->bHideMouseWhenGamepadUsed)
{
auto GI = GetGameInstance();
auto VC = GI->GetGameViewportClient();
auto SVC = Cast<UStevesGameViewportClientBase>(VC);
if (VC)
{
if (NewMode == EInputMode::Gamepad)
{
// First move mouse pointer out of the way because it still generates mouse hits (unless we make source changes to Slate, ugh)
MoveMouseOffScreen(true);
}
else if (NewMode == EInputMode::Mouse)
{
if (SVC)
SVC->SetSuppressMouseCursor(false);
}
}
}
OnInputModeChanged.Broadcast(PlayerIndex, NewMode);
}
void UStevesGameSubsystem::MoveMouseOffScreen(bool bAlsoHide) const
{
if (auto GI = GetGameInstance())
{
auto VC = GI->GetGameViewportClient();
auto SVC = Cast<UStevesGameViewportClientBase>(VC);
auto PC = GI->GetFirstLocalPlayerController();
FVector2D Sz;
VC->GetViewportSize(Sz);
// -1 because if you move cursor outside window when captured, Slate blows up when you press Return, ughghh
// To make sure we don't generate a mouse move by doing this
// Bizarrely the ViewportSize is correct for moving the mouse pointer but the mouse events have different geometry
InputDetector->IgnoreNextMouseMove();
PC->SetMouseLocation(Sz.X-1,Sz.Y-1);
// Now hide it
// I've seen people use PC->bShowMouseCursor but this messes with capturing when you switch back & forth
// especially when pausing in the editor
// instead, I'm using a separate flag to suppress it, see UiFixGameViewportClient for usage
if (SVC && bAlsoHide)
SVC->SetSuppressMouseCursor(true);
}
}
void UStevesGameSubsystem::OnButtonInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
{
// This is specifically for button changes; if this is a different main input mode it will also be registered in OnInputDetectorModeChanged
// Just relay this one
OnButtonInputModeChanged.Broadcast(PlayerIndex, NewMode);
}
void UStevesGameSubsystem::OnAxisInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
{
// This is specifically for button changes; if this is a different main input mode it will also be registered in OnInputDetectorModeChanged
// Just relay this one
OnAxisInputModeChanged.Broadcast(PlayerIndex, NewMode);
}
FFocusSystem* UStevesGameSubsystem::GetFocusSystem()
{
return &FocusSystem;
}
UPaperSprite* UStevesGameSubsystem::GetInputImageSprite(EInputBindingType BindingType,
FName ActionOrAxis,
FKey Key,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
const UUiTheme* Theme)
{
switch(BindingType)
{
case EInputBindingType::Action:
return GetInputImageSpriteFromAction(ActionOrAxis, DevicePreference, PlayerIdx, Theme);
case EInputBindingType::Axis:
return GetInputImageSpriteFromAxis(ActionOrAxis, DevicePreference, PlayerIdx, Theme);
case EInputBindingType::Key:
return GetInputImageSpriteFromKey(Key, PlayerIdx, Theme);
default:
return nullptr;
}
}
// This is not threadsafe! But only used in UI thread in practice
TArray<FInputActionKeyMapping> GS_TempActionMap;
TArray<FInputAxisKeyMapping> GS_TempAxisMap;
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromAction(const FName& Name,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
const UUiTheme* Theme)
{
UInputSettings* Settings = UInputSettings::GetInputSettings();
GS_TempActionMap.Empty();
Settings->GetActionMappingByName(Name, GS_TempActionMap);
// For default, prefer latest press keyboard/mouse for buttons
if (DevicePreference == EInputImageDevicePreference::Auto)
DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
const auto Preferred = GetPreferedActionOrAxisMapping<FInputActionKeyMapping>(GS_TempActionMap, Name, DevicePreference, LastInput, LastButtonInput, LastAxisInput);
if (Preferred)
{
return GetInputImageSpriteFromKey(Preferred->Key, PlayerIdx, Theme);
}
return nullptr;
}
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromAxis(const FName& Name,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
const UUiTheme* Theme)
{
// Look up the key for this axis
UInputSettings* Settings = UInputSettings::GetInputSettings();
GS_TempAxisMap.Empty();
Settings->GetAxisMappingByName(Name, GS_TempAxisMap);
// For default, prefer mouse for axes
if (DevicePreference == EInputImageDevicePreference::Auto)
DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
const auto Preferred = GetPreferedActionOrAxisMapping<FInputAxisKeyMapping>(GS_TempAxisMap, Name, DevicePreference, LastInput, LastButtonInput, LastAxisInput);
if (Preferred)
{
return GetInputImageSpriteFromKey(Preferred->Key, PlayerIdx, Theme);
}
return nullptr;
}
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
APlayerController* PC,
UUiTheme* Theme)
{
if (const UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
const TArray<FKey> Keys = Subsystem->QueryKeysMappedToAction(Action);
// For default, prefer mouse for axes
if (DevicePreference == EInputImageDevicePreference::Auto)
{
if (Action->ValueType == EInputActionValueType::Boolean)
{
DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
}
else
{
DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
}
}
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
if (const FKey* PreferredKey = GetPreferedKeyMapping(Keys, DevicePreference, LastInput, LastButtonInput, LastAxisInput))
{
return GetInputImageSpriteFromKey(*PreferredKey, PlayerIdx, Theme);
}
}
return nullptr;
}
TSoftObjectPtr<UDataTable> UStevesGameSubsystem::GetGamepadImages(int PlayerIndex, const UUiTheme* Theme)
{
// TODO: determine type of controller
return Theme->XboxControllerImages;
}
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromKey(const FKey& InKey, int PlayerIndex, const UUiTheme* Theme)
{
if (!IsValid(Theme))
Theme = GetDefaultUiTheme();
if (Theme)
{
if (InKey.IsGamepadKey())
return GetImageSpriteFromTable(InKey, GetGamepadImages(PlayerIndex, Theme));
else
return GetImageSpriteFromTable(InKey, Theme->KeyboardMouseImages);
}
return nullptr;
}
UPaperSprite* UStevesGameSubsystem::GetImageSpriteFromTable(const FKey& InKey,
const TSoftObjectPtr<UDataTable>& Asset)
{
// Sync load for simplicity for now
const auto Table = Asset.LoadSynchronous();
// Rows are named the same as the key name
const auto SpriteRow = Table->FindRow<FKeySprite>(InKey.GetFName(), "Find Key Image");
if (SpriteRow)
{
return SpriteRow->Sprite;
}
return nullptr;
}
void UStevesGameSubsystem::SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterface<ISlateTextureAtlasInterface> AtlasRegion, bool bMatchSize)
{
if(Brush->GetResourceObject() != AtlasRegion.GetObject())
{
Brush->SetResourceObject(AtlasRegion.GetObject());
if (bMatchSize)
{
if (AtlasRegion)
{
const FSlateAtlasData AtlasData = AtlasRegion->GetSlateAtlasData();
Brush->ImageSize = AtlasData.GetSourceDimensions();
}
else
{
Brush->ImageSize = FVector2D(0, 0);
}
}
}
}
FStevesTextureRenderTargetPoolPtr UStevesGameSubsystem::GetTextureRenderTargetPool(FName Name, bool bAutoCreate)
{
// On the assumption there won't be *loads* of pools, not worth a map, just iterate
for (auto Tex : TextureRenderTargetPools)
{
if (Tex->GetName() == Name)
return Tex;
}
if (bAutoCreate)
{
FStevesTextureRenderTargetPoolPtr Pool = MakeShared<FStevesTextureRenderTargetPool>(Name, this);
TextureRenderTargetPools.Add(Pool);
return Pool;
}
return nullptr;
}
bool UStevesGameSubsystem::FInputModeDetector::ShouldProcessInputEvents() const
{
return !bIgnoreEvents;
}
UStevesGameSubsystem::FInputModeDetector::FInputModeDetector()
{
// 4 local players should be plenty usually (will expand if necessary)
LastInputModeByPlayer.Init(DefaultInputMode, 4);
LastButtonPressByPlayer.Init(DefaultButtonInputMode, 4);
LastAxisMoveByPlayer.Init(DefaultAxisInputMode, 4);
}
bool UStevesGameSubsystem::FInputModeDetector::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
if (ShouldProcessInputEvents())
{
// Key down also registers for gamepad buttons
ProcessKeyOrButton(InKeyEvent.GetUserIndex(), InKeyEvent.GetKey());
}
// Don't consume
return false;
}
bool UStevesGameSubsystem::FInputModeDetector::HandleAnalogInputEvent(FSlateApplication& SlateApp,
const FAnalogInputEvent& InAnalogInputEvent)
{
if (ShouldProcessInputEvents())
{
if (InAnalogInputEvent.GetAnalogValue() > GamepadAxisThreshold)
SetMode(InAnalogInputEvent.GetUserIndex(), EInputMode::Gamepad, false);
}
// Don't consume
return false;
}
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
if (ShouldProcessInputEvents())
{
if (bIgnoreNextMouseMove)
{
bIgnoreNextMouseMove = false;
}
else
{
FVector2D Dist = MouseEvent.GetScreenSpacePosition() - MouseEvent.GetLastScreenSpacePosition();
if (FMath::Abs(Dist.X) > MouseMoveThreshold || FMath::Abs(Dist.Y) > MouseMoveThreshold)
{
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse, false);
}
}
}
// Don't consume
return false;
}
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
if (ShouldProcessInputEvents())
{
// We don't care which button
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse, true);
}
// Don't consume
return false;
}
bool UStevesGameSubsystem::FInputModeDetector::HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
const FPointerEvent* InGestureEvent)
{
if (ShouldProcessInputEvents())
{
SetMode(InWheelEvent.GetUserIndex(), EInputMode::Mouse, false);
}
// Don't consume
return false;
}
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastInputMode(int PlayerIndex)
{
if (PlayerIndex >= 0 && PlayerIndex < LastInputModeByPlayer.Num())
return LastInputModeByPlayer[PlayerIndex];
// Assume default if never told
return DefaultInputMode;
}
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastButtonInputMode(int PlayerIndex)
{
if (PlayerIndex >= 0 && PlayerIndex < LastButtonPressByPlayer.Num())
return LastButtonPressByPlayer[PlayerIndex];
// Assume default if never told
return DefaultButtonInputMode;
}
EInputMode UStevesGameSubsystem::FInputModeDetector::GetLastAxisInputMode(int PlayerIndex)
{
if (PlayerIndex >= 0 && PlayerIndex < LastAxisMoveByPlayer.Num())
return LastAxisMoveByPlayer[PlayerIndex];
// Assume default if never told
return DefaultAxisInputMode;
}
void UStevesGameSubsystem::FInputModeDetector::ProcessKeyOrButton(int PlayerIndex, FKey Key)
{
if (Key.IsGamepadKey())
{
SetMode(PlayerIndex, EInputMode::Gamepad, IsAGamepadButton(Key));
}
else if (Key.IsMouseButton())
{
// Assuming mice don't have analog buttons!
SetMode(PlayerIndex, EInputMode::Mouse, true);
}
else
{
// We assume anything that's not mouse and not gamepad is a keyboard
// Assuming keyboards don't have analog buttons!
SetMode(PlayerIndex, EInputMode::Keyboard, true);
}
}
bool UStevesGameSubsystem::FInputModeDetector::IsAGamepadButton(const FKey& Key)
{
// Key.IsButtonAxis() returns true for some thumbstick movement events, because the axis type is EInputAxisType::Button for
// some reason. That means you get button events for thumbstick movements, which is super dumb.
// See core engine InputCoreTypes.cpp for the stick axes which are defined FKeyDetails::GamepadKey | FKeyDetails::ButtonAxis
// This is for some kind of virtual input but it's a nasty hack, omit them
return Key.IsGamepadKey() &&
Key != EKeys::Gamepad_LeftStick_Up &&
Key != EKeys::Gamepad_LeftStick_Down &&
Key != EKeys::Gamepad_LeftStick_Left &&
Key != EKeys::Gamepad_LeftStick_Right &&
Key != EKeys::Gamepad_RightStick_Up &&
Key != EKeys::Gamepad_RightStick_Down &&
Key != EKeys::Gamepad_RightStick_Left &&
Key != EKeys::Gamepad_RightStick_Right;
}
void UStevesGameSubsystem::FInputModeDetector::SetMode(int PlayerIndex, EInputMode NewMode, bool bIsButton)
{
bool bButtonChanged = false;
bool bAxisChanged = false;
bool bMainChanged = false;
if (bIsButton)
{
if (NewMode != EInputMode::Unknown && NewMode != GetLastButtonInputMode(PlayerIndex))
{
if (PlayerIndex >= LastButtonPressByPlayer.Num())
LastButtonPressByPlayer.SetNum(PlayerIndex + 1);
LastButtonPressByPlayer[PlayerIndex] = NewMode;
bButtonChanged = true;
}
}
else
{
if (NewMode != EInputMode::Unknown && NewMode != GetLastAxisInputMode(PlayerIndex))
{
if (PlayerIndex >= LastAxisMoveByPlayer.Num())
LastAxisMoveByPlayer.SetNum(PlayerIndex + 1);
LastAxisMoveByPlayer[PlayerIndex] = NewMode;
bAxisChanged = true;
}
}
// Whether it's a button or not it can affect the main input mode
if (NewMode != EInputMode::Unknown && NewMode != GetLastInputMode(PlayerIndex))
{
if (PlayerIndex >= LastInputModeByPlayer.Num())
LastInputModeByPlayer.SetNum(PlayerIndex + 1);
LastInputModeByPlayer[PlayerIndex] = NewMode;
bMainChanged = true;
}
// Raise events at the end once all state has changed
if (bButtonChanged)
{
// ReSharper disable once CppExpressionWithoutSideEffects
OnButtonInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Button mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
}
if (bAxisChanged)
{
// ReSharper disable once CppExpressionWithoutSideEffects
OnAxisInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Axis mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
}
if (bMainChanged)
{
// ReSharper disable once CppExpressionWithoutSideEffects
OnInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
//UE_LOG(LogStevesUEHelpers, Display, TEXT("Input mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
}
}
//PRAGMA_ENABLE_OPTIMIZATION

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 Seed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream(InitialSeed=%u, Seed=%u)"), InitialSeed, Seed);
}
};
/// "Balanced" random stream, using the Halton Sequence, one dimension only (more efficient for this than FStevesBalancedRandomStream)
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
USTRUCT(BlueprintType)
struct STEVESUEHELPERS_API FStevesBalancedRandomStream1D
{
GENERATED_BODY()
protected:
uint32 InitialSeed = 0;
uint32 Seed = 0;
public:
FStevesBalancedRandomStream1D()
{ }
/**
* Creates and initializes a new random stream from the specified seed value.
*
* @param InSeed The seed value.
*/
FStevesBalancedRandomStream1D( uint32 InSeed )
{
Initialize(InSeed);
}
/**
* Creates and initializes a new random stream from the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
FStevesBalancedRandomStream1D( FName InName )
{
Initialize(InName);
}
/**
* Initializes this random stream with the specified seed value.
*
* @param InSeed The seed value.
*/
void Initialize( uint32 InSeed )
{
InitialSeed = Seed = InSeed;
}
/**
* Initializes this random stream using the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
void Initialize( FName InName )
{
uint32 StartSeed;
if (InName != NAME_None)
{
StartSeed = GetTypeHash(InName.ToString());
}
else
{
StartSeed = FPlatformTime::Cycles();
}
Initialize(StartSeed);
}
/**
* Resets this random stream to the initial seed value.
*/
void Reset()
{
Initialize(InitialSeed);
}
uint32 GetInitialSeed() const
{
return InitialSeed;
}
/**
* Generates a new random seed.
*/
void GenerateNewSeed()
{
Initialize(FMath::Rand());
}
/// Return a value between 0..1, inclusive
FORCEINLINE float Rand()
{
return Halton(Seed++, 2);
}
/**
* Helper function for rand implementations.
*
* @return A random number in [0..A)
*/
FORCEINLINE int32 RandHelper( int32 A )
{
// GetFraction guarantees a result in the [0,1) range.
return ((A > 0) ? FMath::TruncToInt(Rand() * float(A)) : 0);
}
/// Random float value in a range (inclusive)
FORCEINLINE float RandRange(float Min, float Max)
{
return FMath::Lerp(Min, Max, Rand());
}
/// Random int value in a range (inclusive)
FORCEINLINE int32 RandRange( int32 Min, int32 Max )
{
const int32 Range = (Max - Min) + 1;
return Min + RandHelper(Range);
}
/// Random colour value
FORCEINLINE FLinearColor RandColour(const FLinearColor& From, const FLinearColor& To)
{
return FLinearColor::LerpUsingHSV(From, To, Rand());
}
/**
* Gets the current seed.
*
* @return Current seed.
*/
FORCEINLINE uint32 GetCurrentSeed() const
{
return Seed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream1D(InitialSeed=%u, Seed=%u)"), InitialSeed, Seed);
}
};
/// "Balanced" 2D random stream, using the Halton Sequence. More efficient than the general FStevesBalancedRandomStream for 2D work
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
USTRUCT(BlueprintType)
struct STEVESUEHELPERS_API FStevesBalancedRandomStream2D
{
GENERATED_BODY()
protected:
uint32 InitialSeed = 0;
uint32 Base2Seed = 0;
uint32 Base3Seed = 0;
FVector2f CurrentValue = FVector2f::ZeroVector;
public:
FStevesBalancedRandomStream2D()
{
Initialize(0);
}
/**
* Creates and initializes a new random stream from the specified seed value.
*
* @param InSeed The seed value.
*/
FStevesBalancedRandomStream2D( uint32 InSeed )
{
Initialize(InSeed);
}
/**
* Creates and initializes a new random stream from the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
FStevesBalancedRandomStream2D( FName InName )
{
Initialize(InName);
}
/**
* Initializes this random stream with the specified seed value.
*
* @param InSeed The seed value.
*/
void Initialize( uint32 InSeed )
{
// Halton sequence gets unstable when seed gets too high, especially with higher bases
while (InSeed >= StevesRandConstants::kSafeMaxSeed2D)
{
InSeed -= StevesRandConstants::kSafeMaxSeed2D;
}
InitialSeed = Base2Seed = InSeed;
CurrentValue.X = Halton(Base2Seed, 2);
CurrentValue.Y = 0;
Base3Seed = 0;
float ip = StevesRandConstants::kOneOverThree;
float p = ip;
for (int i = 0, k = Base2Seed; k; i += 2, k /= 3)
{
int d = (k % 3);
Base3Seed |= d << i;
CurrentValue.Y += d * p;
p *= ip;
}
}
/**
* Initializes this random stream using the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
void Initialize( FName InName )
{
uint32 StartSeed;
if (InName != NAME_None)
{
StartSeed = GetTypeHash(InName.ToString());
}
else
{
StartSeed = FPlatformTime::Cycles();
}
Initialize(StartSeed);
}
/**
* Resets this random stream to the initial seed value.
*/
void Reset()
{
Initialize(InitialSeed);
}
uint32 GetInitialSeed() const
{
return InitialSeed;
}
/**
* Generates a new random seed.
*/
void GenerateNewSeed()
{
Initialize(FMath::Rand());
}
/// Return a 2D value with each element between 0..1, inclusive
FVector2f Rand2D()
{
// Wrap back to 0 at end of safe range
if (Base2Seed >= StevesRandConstants::kSafeMaxSeed2D)
{
Initialize(0);
}
// This uses Andrew Wilmott's approach of calculating the next value at the same time as incrementing
// We calculate the new value while initialising / incrementing, so it's currently correct
const FVector2f Ret = CurrentValue;
/////////////////////////////////////
// base 2
uint32_t OldBase2Seed = Base2Seed;
Base2Seed++;
uint32_t Diff = Base2Seed ^ OldBase2Seed;
// bottom bit always changes, higher bits
// change less frequently.
float s = 0.5f;
// Diff will be of the form 0 * 1 +, i.e. one bits up until the last carry.
// expected iterations = 1 + 0.5 + 0.25 + ... = 2
do
{
if (OldBase2Seed & 1)
CurrentValue.X -= s;
else
CurrentValue.X += s;
s *= 0.5f;
Diff = Diff >> 1;
OldBase2Seed = OldBase2Seed >> 1;
}
while (Diff);
/////////////////////////////////////
// base 3: use 2 bits for each base 3 digit.
uint32_t Mask = 0x3; // also the max base 3 digit
uint32_t Add = 0x1; // amount to Add to force carry once digit==3
s = StevesRandConstants::kOneOverThree;
Base3Seed++;
// expected iterations: 1.5
while (true)
{
if ((Base3Seed & Mask) == Mask)
{
Base3Seed += Add; // force carry into next 2-bit digit
CurrentValue.Y -= 2 * s;
Mask = Mask << 2;
Add = Add << 2;
s *= StevesRandConstants::kOneOverThree;
}
else
{
CurrentValue.Y += s; // we know digit n has gone from a to a + 1
break;
}
}
return Ret;
}
/**
* Returns a random vector of unit size.
*
* @return Random unit vector.
*/
FVector RandUnitVector()
{
const FVector2f PitchYaw = Rand2D();
return FRotator(PitchYaw.X, PitchYaw.Y, 0).RotateVector(FVector::UpVector);
}
/// Random point in a 2D rectangle
FORCEINLINE FVector2f RandPointInBox2D(const FBox2D& Rect)
{
const FVector2f R2 = Rand2D();
return FVector2f(FMath::Lerp(Rect.Min.X, Rect.Max.X, R2.X),
FMath::Lerp(Rect.Min.Y, Rect.Max.Y, R2.Y));
}
/// Random point in a circle
FORCEINLINE FVector2f RandPointInCircle(float Radius = 1.0)
{
// Just use rejection sampling for simplicity / speed
while (true)
{
const FVector2f Candidate = Rand2D();
if (Candidate.SquaredLength() <= 1.0)
{
return Candidate * Radius;
}
}
}
/**
* Gets the current seed.
*
* @return Current seed.
*/
uint32 GetCurrentSeed() const
{
return Base2Seed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream2D(InitialSeed=%u, Seed=%u)"), InitialSeed, Base2Seed);
}
};
/// "Balanced" random 3D stream, using the Halton Sequence. Optimised for 3D only, more efficient than FStevesBalancedRandomStream
/// This is deterministic and more uniform in appearance than a general random stream (although not perfectly uniform)
USTRUCT(BlueprintType)
struct STEVESUEHELPERS_API FStevesBalancedRandomStream3D
{
GENERATED_BODY()
protected:
uint32 InitialSeed = 0;
uint32 Base2Seed = 0;
uint32 Base3Seed = 0;
uint32 Base5Seed = 0;
FVector3f CurrentValue = FVector3f::ZeroVector;
public:
FStevesBalancedRandomStream3D()
{
Initialize(0);
}
/**
* Creates and initializes a new random stream from the specified seed value.
*
* @param InSeed The seed value.
*/
FStevesBalancedRandomStream3D( uint32 InSeed )
{
Initialize(InSeed);
}
/**
* Creates and initializes a new random stream from the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
FStevesBalancedRandomStream3D( FName InName )
{
Initialize(InName);
}
/**
* Initializes this random stream with the specified seed value.
*
* @param InSeed The seed value.
*/
void Initialize( uint32 InSeed )
{
while (InSeed > StevesRandConstants::kSafeMaxSeed3D)
{
InSeed -= StevesRandConstants::kSafeMaxSeed3D;
}
InitialSeed = Base2Seed = InSeed;
CurrentValue.X = Halton(Base2Seed, 2);
CurrentValue.Y = 0.0f;
Base3Seed = 0;
float p = StevesRandConstants::kOneOverThree;
for (int i = 0, k = Base2Seed; k; i += 2, k /= 3)
{
int d = (k % 3);
Base3Seed |= d << i;
CurrentValue.Y += d * p;
p *= StevesRandConstants::kOneOverThree;
}
CurrentValue.Z = 0.0f;
Base5Seed = 0;
p = StevesRandConstants::kOneOverFive;
for (int i = 0, k = Base2Seed; k; i += 3, k /= 5)
{
int d = (k % 5);
Base5Seed |= d << i;
CurrentValue.Z += d * p;
p *= StevesRandConstants::kOneOverFive;
}
}
/**
* Initializes this random stream using the specified name.
*
* @note If NAME_None is provided, the stream will be seeded using the current time.
* @param InName The name value from which the stream will be initialized.
*/
void Initialize( FName InName )
{
uint32 StartSeed;
if (InName != NAME_None)
{
StartSeed = GetTypeHash(InName.ToString());
}
else
{
StartSeed = FPlatformTime::Cycles();
}
Initialize(StartSeed);
}
/**
* Resets this random stream to the initial seed value.
*/
void Reset()
{
Initialize(InitialSeed);
}
uint32 GetInitialSeed() const
{
return Base2Seed;
}
/**
* Generates a new random seed.
*/
void GenerateNewSeed()
{
Initialize(FMath::Rand());
}
/// Return a 3D value with each element between 0..1, inclusive
FVector Rand3D()
{
if (Base2Seed >= StevesRandConstants::kSafeMaxSeed3D)
{
Initialize(0);
}
// This uses Andrew Wilmott's approach of calculating the next value at the same time as incrementing
// We calculate the new value while initialising / incrementing, so it's currently correct
const FVector3f Ret = CurrentValue;
// base 2: 1 bit per digit
uint32_t OldBase2 = Base2Seed;
Base2Seed++;
uint32_t Diff = Base2Seed ^ OldBase2;
// bottom bit always changes, higher bits
// change less frequently.
float s = 0.5f;
// diff will be of the form 0 * 1 + , i.e. one bits up until the last carry.
// expected iterations = 1 + 0.5 + 0.25 + ... = 2
do
{
if (OldBase2 & 1)
CurrentValue.X -= s;
else
CurrentValue.X += s;
s *= 0.5f;
Diff = Diff >> 1;
OldBase2 = OldBase2 >> 1;
}
while (Diff);
// base 3: use 2 bits for each base 3 digit.
uint32_t Mask = 0x3; // also the max base 3 digit
uint32_t Add = 0x1; // amount to add to force carry once digit==3
s = StevesRandConstants::kOneOverThree;
Base3Seed++;
// expected iterations: 1.5
while (true)
{
if ((Base3Seed & Mask) == Mask)
{
Base3Seed += Add; // force carry into next 2-bit digit
CurrentValue.Y -= 2 * s;
Mask = Mask << 2;
Add = Add << 2;
s *= StevesRandConstants::kOneOverThree;
}
else
{
CurrentValue.Y += s; // we know digit n has gone from a to a + 1
break;
}
};
// base 5: use 3 bits for each base 5 digit.
Mask = 0x7;
Add = 0x3; // amount to add to force carry once digit==dmax
uint32_t Dmax = 0x5; // max digit
s = StevesRandConstants::kOneOverFive;
Base5Seed++;
// expected iterations: 1.25
while (true)
{
if ((Base5Seed & Mask) == Dmax)
{
Base5Seed += Add; // force carry into next 3-bit digit
CurrentValue.Z -= 4 * s;
Mask = Mask << 3;
Dmax = Dmax << 3;
Add = Add << 3;
s *= StevesRandConstants::kOneOverFive;
}
else
{
CurrentValue.Z += s; // we know digit n has gone from a to a + 1
break;
}
};
return FVector(Ret.X, Ret.Y, Ret.Z);
}
/// Random point in a 3D box
FORCEINLINE FVector RandPointInBox(const FBox& Box)
{
const FVector R3 = Rand3D();
return FVector(FMath::Lerp(Box.Min.X, Box.Max.X, R3.X),
FMath::Lerp(Box.Min.Y, Box.Max.Y, R3.Y),
FMath::Lerp(Box.Min.Z, Box.Max.Z, R3.Z));
}
/// Random point in a sphere
FORCEINLINE FVector RandPointInSphere(float Radius = 1.0)
{
// Just use rejection sampling for simplicity / speed
while (true)
{
const FVector Candidate = Rand3D();
if (Candidate.SquaredLength() <= 1.0)
{
return Candidate * Radius;
}
}
}
/**
* Gets the current seed.
*
* @return Current seed.
*/
uint32 GetCurrentSeed() const
{
return InitialSeed;
}
FString ToString() const
{
return FString::Printf(TEXT("FStevesBalancedRandomStream(InitialSeed=%u, Seed=%u)"), InitialSeed, Base2Seed);
}
};

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.2f;
bool ShouldProcessInputEvents() const;
public:
/// Whether this detector should ignore events (e.g. because the application is in the background)
bool bIgnoreEvents = false;
bool bIgnoreNextMouseMove = false;
/// Event raised when main input mode changes for any reason
FInternalInputModeChanged OnInputModeChanged;
/// Event raised when button input mode changes only
FInternalInputModeChanged OnButtonInputModeChanged;
/// Event raised when axis input mode changes only
FInternalInputModeChanged OnAxisInputModeChanged;
FInputModeDetector();
void IgnoreNextMouseMove() { bIgnoreNextMouseMove = true;}
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override;
virtual bool
HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override;
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
const FPointerEvent* InGestureEvent) override;
/// Get the last input mode from any kind of input
EInputMode GetLastInputMode(int PlayerIndex = 0);
/// Get the last input mode from button inputs (ignores axis changes, good for detecting if keyboard or mouse buttons are being used)
EInputMode GetLastButtonInputMode(int PlayerIndex = 0);
/// Get the last input mode from axis movement (ignores button presses, good for detecting if keyboard or mouse axes are being used for motion)
EInputMode GetLastAxisInputMode(int PlayerIndex = 0);
// Needed but unused
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
protected:
static bool IsAGamepadButton(const FKey& Key);
void ProcessKeyOrButton(int PlayerIndex, FKey Key);
void SetMode(int PlayerIndex, EInputMode NewMode, bool bIsButton);
};
protected:
TSharedPtr<FInputModeDetector> InputDetector;
FFocusSystem FocusSystem;
bool bCheckedViewportClient = false;
FTimerHandle ForegroundCheckHandle;
UPROPERTY(BlueprintReadOnly, Category="StevesGameSubsystem")
bool bIsForeground = true;
UPROPERTY(BlueprintReadWrite, Category="StevesGameSubsystem")
TObjectPtr<UUiTheme> DefaultUiTheme;
TArray<FStevesTextureRenderTargetPoolPtr> TextureRenderTargetPools;
void CreateInputDetector();
void DestroyInputDetector();
void InitTheme();
void InitForegroundCheck();
void CheckForeground();
void InitViewport();
void ViewportResized(FViewport* Viewport, unsigned Unused);
void FullscreenToggled(bool bFullscreen);
// Called by detector
void OnInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
void OnButtonInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
void OnAxisInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode);
TSoftObjectPtr<UDataTable> GetGamepadImages(int PlayerIndex, const UUiTheme* Theme);
UPaperSprite* GetImageSpriteFromTable(const FKey& Key, const TSoftObjectPtr<UDataTable>& Asset);
UFUNCTION()
void EnhancedInputActionTriggered(const FInputActionInstance& InputActionInstance);
public:
/// Event raised when main input mode changed between gamepad and keyboard / mouse (for any of axis / button events)
UPROPERTY(BlueprintAssignable)
FOnInputModeChanged OnInputModeChanged;
/// Event raised when the last button input changed between gamepad / keyboard / mouse
/// This can happen at a different time to OnInputModeChanged, e.g. if that was triggered by a mouse move, but the
/// last button pressed was still keyboard, you'd get this event later
UPROPERTY(BlueprintAssignable)
FOnInputModeChanged OnButtonInputModeChanged;
/// Event raised when the last axis input changed between gamepad / keyboard / mouse
/// This can happen at a different time to OnInputModeChanged, e.g. if that was triggered by a button press, but the
/// last axis moved was still mouse, you'd get this event later
UPROPERTY(BlueprintAssignable)
FOnInputModeChanged OnAxisInputModeChanged;
/// DEPRECATED: Event raised just after the Enhanced Input mappings have changed
/// User-triggered via NotifyEnhancedInputMappingsChanged
UPROPERTY(BlueprintAssignable)
FOnEnhancedInputMappingsChanged OnEnhancedInputMappingsChanged;
/// Event fired when an enhanced input event that an interest has previously been registered in triggers.
/// Nothing will fire on this event unless you call RegisterInterestInEnhancedInputAction to listen for it.
UPROPERTY(BlueprintAssignable)
FOnEnhancedInputActionTriggered OnEnhancedInputActionTriggered;
/// Event raised when the game window's foreground status changes
UPROPERTY(BlueprintAssignable)
FOnWindowForegroundChanged OnWindowForegroundChanged;
/// Event raised when the viewport changes size
UPROPERTY(BlueprintAssignable)
FOnViewportResized OnViewportResized;
/// Gets the device where the most recent input event of any kind happened
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
EInputMode GetLastInputModeUsed(int PlayerIndex = 0) const { return InputDetector->GetLastInputMode(PlayerIndex); }
/// Gets the device where the most recent button press happened
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
EInputMode GetLastInputButtonPressed(int PlayerIndex = 0) const { return InputDetector->GetLastButtonInputMode(PlayerIndex); }
/// Gets the device where the most recent axis move happened
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
EInputMode GetLastInputAxisMoved(int PlayerIndex = 0) const { return InputDetector->GetLastAxisInputMode(PlayerIndex); }
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
bool LastInputWasGamePad(int PlayerIndex = 0) const { return GetLastInputModeUsed(PlayerIndex) == EInputMode::Gamepad; }
/// Gets the default UI theme object (defaults to our own)
/// You can override this if you want
UUiTheme* GetDefaultUiTheme() { return DefaultUiTheme; };
/// Changes the default theme to a different one
void SetDefaultUiTheme(UUiTheme* NewTheme) { DefaultUiTheme = NewTheme; }
/// Get the global focus system
FFocusSystem* GetFocusSystem();
/// Return whether the game is currently in the foreground
bool IsForeground() const { return bIsForeground; }
/**
* @brief Get an input button / key / axis image as a sprite based on any combination of action / axis binding or manual key
* @param BindingType The type of input binding to look up
* @param ActionOrAxis The name of the action or axis, if BindingType is looking for that
* @param Key The explicit key you want to display, if the BindingType is set to custom key
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
* @param PlayerIndex The player index to look up the binding for
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
UPaperSprite* GetInputImageSprite(EInputBindingType BindingType,
FName ActionOrAxis,
FKey Key,
EInputImageDevicePreference DevicePreference,
int PlayerIndex = 0,
const UUiTheme* Theme = nullptr);
/**
* @brief Get an input button / key / axis image as a sprite based on an enhanced input action
* @param Action The input action
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
* @param PlayerIdx The player index to look up the binding for
* @param PC The player controller to look up the binding for
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UPaperSprite* GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
APlayerController* PC,
UUiTheme* Theme = nullptr);
/**
* @brief Get an input button / key image from an action
* @param Name The name of the action
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
* @param PlayerIndex The player index to look up the binding for
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UPaperSprite* GetInputImageSpriteFromAction(const FName& Name,
EInputImageDevicePreference DevicePreference =
EInputImageDevicePreference::Gamepad_Keyboard_Mouse,
int PlayerIndex = 0,
const UUiTheme* Theme = nullptr);
/**
* @brief Get an input image from an axis
* @param Name The name of the axis
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
* @param PlayerIndex The player index to look up the binding for
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UPaperSprite* GetInputImageSpriteFromAxis(const FName& Name,
EInputImageDevicePreference DevicePreference =
EInputImageDevicePreference::Gamepad_Mouse_Keyboard,
int PlayerIndex = 0,
const UUiTheme* Theme = nullptr);
/**
* @brief Get an input image for a specific key
* @param Key The key to look up
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UPaperSprite* GetInputImageSpriteFromKey(const FKey& Key, int PlayerIndex = 0, const UUiTheme* Theme = nullptr);
/**
* @brief Set the content of a slate brush from an atlas (e.g. sprite)
* @param Brush The brush to update
* @param AtlasRegion Atlas to use as source e.g. a Sprite
* @param bMatchSize Whether to resize the brush to match the atlas entry
*/
static void SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterface<ISlateTextureAtlasInterface> AtlasRegion,
bool bMatchSize);
/**
* Retrieve a pool of texture render targets. If a pool doesn't exist with the given name, it can be created.
* @param Name Identifier for the pool.
* @param bAutoCreate
* @return The pool, or null if it doesn't exist and bAutoCreate is false
*/
FStevesTextureRenderTargetPoolPtr GetTextureRenderTargetPool(FName Name, bool bAutoCreate = true);
/**
* DEPRECATED - no longer required
* Notify this subsystem that changes have been made to the Enhanced Input mappings, e.g. adding or removing a context.
* Unfortunately, the Enhanced Input plugin currently provides NO WAY for us to monitor context changes automatically,
* so we need the user to tell us when they make a change.
* This call is however slightly delayed before being acted upon, because EI defers the rebuild of mappings until the next tick.
*/
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
void NotifyEnhancedInputMappingsChanged();
/** Attempt to find an enhanced input action by name in the configured folders.
*/
TSoftObjectPtr<UInputAction> FindEnhancedInputAction(const FString& Name);
/// Register an interest in an enhanced input action. Calling this will result in OnEnhancedInputActionTriggered being called
/// when this action is triggered.
/// This is mainly for use in UI bindings. You only need to call it once for each UI-specific action.
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
void RegisterInterestInEnhancedInputAction(const UInputAction* Action, ETriggerEvent TriggerEvent);
// Unregister all previously registered interests in input actions. This can be needed if a new scene is loaded, as previously
// regsitered InputActions will no longer fire
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
void UnregisterAllInterestInEnhancedInputActions();
/// Moves the mouse pointer offscreen so that it can't trigger hovers on anything. This happens by
/// default when switching to gamepad mode, but in edge cases I've found it useful to ensure that
/// some other effect doesn't interfere with gamepad navigation by relocating the mouse pointer to
/// the centre (I've seen this rarely but can't fully explain it). By default all MenuStacks call this
/// on open if the current input is gamepad, to ensure the mouse is out of the way.
UFUNCTION(BlueprintCallable, Category="StevesGameSubsystem")
void MoveMouseOffScreen(bool bAlsoHide = true) const;
};

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

View File

@@ -0,0 +1,112 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "CoreMinimal.h"
#include "StevesUI/FocusableInputInterceptorUserWidget.h"
#include "TabListWidget.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContentWidgetChanged,UWidget*,PreviousContentWidget,UWidget*,NewContentWidget);
class UWidgetSwitcher;
class UTabButton;
/** Information about a registered tab in the tab list */
USTRUCT()
struct FTabListRegisteredTabInfo
{
GENERATED_BODY()
public:
/** The index of this tab */
UPROPERTY()
int32 TabIndex;
/** The button for this tab */
UPROPERTY()
TObjectPtr<UTabButton> TabButton;
/** The content to be activated. */
UPROPERTY()
TObjectPtr<UWidget> ContentInstance;
FTabListRegisteredTabInfo()
: TabIndex(INDEX_NONE)
, TabButton(nullptr)
, ContentInstance(nullptr)
{}
};
/**
* Tab list widget - provides a tab selection bar that can control a widget switcher
*/
UCLASS()
class STEVESUEHELPERS_API UTabListWidget : public UFocusableInputInterceptorUserWidget
{
GENERATED_BODY()
public:
/// Event called when tab content has changed
UPROPERTY(BlueprintCallable,BlueprintAssignable)
FOnContentWidgetChanged OnContentWidgetChanged;
/// Input keys which navigate to the previous tab (left)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
TArray<FKey> PreviousTabKeys = TArray<FKey> { EKeys::Gamepad_LeftShoulder, EKeys::Q };
/// Input keys which navigate to the next tab (right)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
TArray<FKey> NextTabKeys = TArray<FKey> { EKeys::Gamepad_RightShoulder, EKeys::E };
/// Map of tabs, buttons, content
TMap<FName,FTabListRegisteredTabInfo> RegisteredTabs;
// Where the tab buttons will be added
UPROPERTY(BlueprintReadWrite, meta = (BindWidget), Category="TabButton")
TObjectPtr<UPanelWidget> TabButtonContainer;
UPROPERTY(BlueprintReadOnly, Category="TabButton")
int SelectedTabIndex=-1;
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category="TabButton")
FMargin TabButtonPadding=FMargin(10,0);
UPROPERTY(BlueprintReadOnly, Category="TabButton")
TObjectPtr<UWidgetSwitcher> TargetWidgetSwitcher;
/**
* Registers tab content with this tab widget
* @param TabID The ID to use for this widget
* @param ButtonWidgetType The class of button to use
* @param ContentWidget The widget to activate when pressed
* @return True If successful
*/
UFUNCTION(BlueprintCallable, Category="TabButton")
bool RegisterTab(FName TabID, TSubclassOf<UTabButton> ButtonWidgetType,FText Label, UWidget* ContentWidget);
/**
* Called when a given tab button has been pressed
* @param TabIndex
*/
UFUNCTION()
void TabPressed(int TabIndex);
/**
* Assign a widget switcher to be driven by this tab selection.
* @param WidgetSwitcher
*/
UFUNCTION(BlueprintCallable, Category="TabButton")
void SetTargetWidgetSwitcher(UWidgetSwitcher* WidgetSwitcher);
UFUNCTION(BlueprintCallable, Category="TabButton")
void IncrementTabIndex(int Amount);
UFUNCTION(BlueprintCallable, Category="TabButton")
void SelectTab(int TabIndex);
virtual bool HandleKeyDownEvent(const FKeyEvent& InKeyEvent) override;
private:
bool GetTabDataForIndex(int Index,FTabListRegisteredTabInfo& TabInfo);
};

View File

@@ -0,0 +1,243 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
// Original Copyright (c) Sam Bloomberg https://github.com/redxdev/UnrealRichTextDialogueBox (MIT License)
// Updates:
// 1. Fixed adding a spurious newline to single-line text
// 2. Expose line finished as event
// 3. Changed names of classes to indicate functionality better (typewriter not dialogue)
// 4. Update minimum desired size on calculate so that flexi boxes can start at the correct size
// instead of growing when the newline is added
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/RichTextBlock.h"
#include "Framework/Text/RichTextLayoutMarshaller.h"
#include "Framework/Text/SlateTextLayout.h"
#include "TypewriterTextWidget.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTypewriterLineFinished, class UTypewriterTextWidget*, Widget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterLetterAdded, class UTypewriterTextWidget*, Widget, const FString&, Char);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterStartWord, class UTypewriterTextWidget*, Widget, const FString&, Word);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTypewriterRunNameChanged, class UTypewriterTextWidget*, Widget, const FString&, NewRunName);
/**
* A text block that exposes more information about text layout for typewriter widget.
*/
UCLASS()
class URichTextBlockForTypewriter : public URichTextBlock
{
GENERATED_BODY()
public:
FORCEINLINE TSharedPtr<FSlateTextLayout> GetTextLayout() const
{
return TextLayout;
}
FORCEINLINE TSharedPtr<FRichTextLayoutMarshaller> GetTextMarshaller() const
{
return TextMarshaller;
}
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
TSharedPtr<FSlateTextLayout> TextLayout;
TSharedPtr<FRichTextLayoutMarshaller> TextMarshaller;
};
UCLASS(Blueprintable)
class STEVESUEHELPERS_API UTypewriterTextWidget : public UUserWidget
{
GENERATED_BODY()
public:
UTypewriterTextWidget(const FObjectInitializer& ObjectInitializer);
void ClearLetterCountdownTimer();
/// Event called when a line has finished playing, whether on its own or when skipped to end
UPROPERTY(BlueprintAssignable)
FOnTypewriterLineFinished OnTypewriterLineFinished;
/// Event called when one or more new letters have been displayed
UPROPERTY(BlueprintAssignable)
FOnTypewriterLetterAdded OnTypewriterLetterAdded;
/// Event called when a new word has started
UPROPERTY(BlueprintAssignable)
FOnTypewriterStartWord OnTypewriterStartWord;
/// Event called when the "run name" of the text changes aka the rich text style markup. Also called when reverts to default.
UPROPERTY(BlueprintAssignable)
FOnTypewriterRunNameChanged OnTypewriterRunNameChanged;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Typewriter")
TObjectPtr<URichTextBlockForTypewriter> LineText;
/// The amount of time between printing individual letters (for the "typewriter" effect).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
float LetterPlayTime = 0.025f;
/// The amount of time to wait after finishing the line before actually marking it completed.
/// This helps prevent accidentally progressing dialogue on short lines.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
float EndHoldTime = 0.15f;
/// How long to pause at sentence terminators ('.', '!', '?') before proceeding (ignored if at end of text)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
float PauseTimeAtSentenceTerminators = 0.5f;
/// Characters which will terminate a sentence, which will potentially pause
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
FString SentenceTerminators = ".!?";
/// Characters which terminate a clause, which is a preferred place to split an overly long line
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
FString ClauseTerminators = ",;:";
/// If true (default), only pauses at sentence terminators if there's whitespace following them.
/// This prevents pausing on strings like ".txt"
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
bool bPauseOnlyIfWhitespaceFollowsSentenceTerminator = true;
/// If set > 0, splits a single PlayLine into multiple segments of this number of lines maximum
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
int MaxNumberOfLines = 0;
/// If set to true, the text will continue to be played when paused
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
bool bPlayWhenPaused = false;
/// If set to true, OnTypewriterStartWord will be called when a new word starts
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
bool bNewWordEvent = false;
/// Set Text immediately
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void SetText(const FText& InText);
/// Set Text immediately
UFUNCTION(BlueprintCallable, Category = "Typewriter")
FText GetText() const;
/**
* Play a line of text.
* Note: if, when line splits are calculated, this line exceeds MaxNumberOfLines, then only this number of lines
* will be played by this call. In that case, HasMoreLineParts() will return true, and you will need to call
* PlayNextLinePart() to play the remainder of the line.
* @param InLine The input line
* @param Speed
*/
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void PlayLine(const FText& InLine, float Speed = 1.0f);
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void GetCurrentLine(FText& OutLine) const { OutLine = CurrentLine; }
/// Return whether the entire line has finished playing
UFUNCTION(BlueprintCallable, Category = "Typewriter")
bool HasFinishedPlayingLine() const { return bHasFinishedPlaying; }
/// Returns whether the number of lines exceeded MaxNumberOfLines and there are still parts to play.
UFUNCTION(BlueprintCallable, Category = "Typewriter")
bool HasMoreLineParts() const { return bHasMoreLineParts; }
/// If HasMoreLineParts() is true, play the next part of the line originally requested by PlayLine
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void PlayNextLinePart(float Speed = 1.0f);
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void SkipToLineEnd();
/// Get the name of the current rich text run, if any
const FString& GetCurrentRunName() const { return CurrentRunName; }
/// Get typewriter play speed
UFUNCTION(BlueprintCallable, Category = "Typewriter")
float GetPlaySpeed() const {return CurrentPlaySpeed;}
/// Set typewriter play speed
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void SetPlaySpeed(float Speed) {CurrentPlaySpeed = Speed;}
UFUNCTION(BlueprintCallable, Category = "Typewriter")
void FindWordVowels(const FString& Word, TArray<int>& VowelsPos);
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
protected:
virtual void NativeConstruct() override;
/// Called when on or more letters are added, if subclasses want to override
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
void OnPlayLetter();
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
void OnLineFinishedPlaying();
int OnStartNewWord(const FString SegmentRemain);
/// Name of the current rich text run, if any
/// Can be used to identify the style of the most recently played letter
UPROPERTY(BlueprintReadOnly, Category = "Typewriter")
FString CurrentRunName;
private:
void PlayNextLetter();
bool IsSentenceTerminator(TCHAR Letter) const;
bool IsClauseTerminator(TCHAR Letter) const;
bool IsPunctuation(TCHAR Letter) const;
int FindLastTerminator(const FString& CurrentLineString, int Count) const;
int CalculateMaxLength();
void CalculateWrappedString(const FString& CurrentLineString);
FString CalculateSegments(FString* OutCurrentRunName);
void StartPlayLine();
UPROPERTY()
FText CurrentLine;
FString RemainingLinePart;
struct FTypewriterTextSegment
{
FString Text;
FRunInfo RunInfo;
};
TArray<FTypewriterTextSegment> Segments;
// The section of the text that's already been printed out and won't ever change.
// This lets us cache some of the work we've already done. We can't cache absolutely
// everything as the last few characters of a string may change if they're related to
// a named run that hasn't been completed yet.
FString CachedSegmentText;
int32 CachedLetterIndex = 0;
int32 CurrentSegmentIndex = 0;
int32 CurrentLetterIndex = 0;
int32 MaxLetterIndex = 0;
int32 NumberOfLines = 0;
int32 NextBlankLetterLeft = -1;
float CombinedTextHeight = 0;
uint32 bHasFinishedPlaying : 1;
uint32 bHasMoreLineParts : 1;
// Properties related to animation
float NextLetterCountdown = 0;
float NextLetterCountdownInterval = 0;
float StartPlayLineCountdown = 0;
float SkipToLineEndCountdown = 0;
float CurrentPlaySpeed = 1;
float PauseTime = 0;
bool bFirstPlayLine = true;
bool bLastSegmentEndsWithBlank = false;
};

View File

@@ -0,0 +1,27 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Engine/DataTable.h"
#include "UiTheme.generated.h"
/// Custom asset to conveniently hold theme information for the UI
/// Currently only lightly used to provide simple access to button images, but I intend to use
/// this more extensively later
UCLASS(Blueprintable)
class STEVESUEHELPERS_API UUiTheme : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category="StevesUI")
TSoftObjectPtr<UDataTable> KeyboardMouseImages;
UPROPERTY(EditDefaultsOnly, Category="StevesUI")
TSoftObjectPtr<UDataTable> XboxControllerImages;
};

View File

@@ -0,0 +1,89 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#include "StevesUiHelpers.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/PanelWidget.h"
#include "Components/VerticalBoxSlot.h"
UPanelSlot* StevesUiHelpers::InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex)
{
checkf(AtIndex <= Parent->GetChildrenCount(), TEXT("Insertion index %d is greater than child count"), AtIndex);
// Short-circuit for simple case of insert at end
const int OrigCount = Parent->GetChildrenCount();
if (OrigCount == AtIndex)
{
return Parent->AddChild(Child);
}
else
{
// Note: cannot use InsertChildAt, that's editor-only
// There are a few options with the runtime API
// 1. Remove all children and re-add them in the right order
// 2. Remove all children on or after the insert point and re-add those after adding the new item
// 3. Use ReplaceChildAt to insert a new item and move everything down, with AddChild at the end
//
// I'm going with 2. because that seems to blend best performance with the most well defined behaviour
// I'm not sure ReplaceChildAt is designed to move children around (note: ShiftChild exists but again, editor-only)
TArray<UWidget*> WidgetsToReplaceReversed;
TArray<FMargin> HVBoxPadding;
// Keep slot padding info
HVBoxPadding.Reserve(OrigCount - AtIndex);
// Go backwards for consistency with below
const auto Slots = Parent->GetSlots();
for (int i = OrigCount - 1; i >= AtIndex; --i)
{
if (Slots.IsValidIndex(i))
{
const auto Slot = Slots[i];
if (const auto HSlot = Cast<UHorizontalBoxSlot>(Slot))
{
HVBoxPadding.Add(HSlot->GetPadding());
}
else if (const auto VSlot = Cast<UVerticalBoxSlot>(Slot))
{
HVBoxPadding.Add(VSlot->GetPadding());
}
else
{
HVBoxPadding.Add(FMargin());
}
}
}
WidgetsToReplaceReversed.Reserve(OrigCount - AtIndex);
// Go backwards so we can remove as we go and not remove from the middle
for (int i = OrigCount - 1; i >= AtIndex; --i)
{
WidgetsToReplaceReversed.Add(Parent->GetChildAt(i));
Parent->RemoveChildAt(i);
}
// insert item
auto Slot = Parent->AddChild(Child);
// add back previous, reverse order
for (int i = WidgetsToReplaceReversed.Num() - 1; i >= 0; --i)
{
auto PrevSlot = Parent->AddChild(WidgetsToReplaceReversed[i]);
// Restore the padding on H/VBox slots
if (HVBoxPadding.IsValidIndex(i))
{
if (auto HSlot = Cast<UHorizontalBoxSlot>(PrevSlot))
{
HSlot->SetPadding(HVBoxPadding[i]);
}
else if (auto VSlot = Cast<UVerticalBoxSlot>(PrevSlot))
{
VSlot->SetPadding(HVBoxPadding[i]);
}
}
}
return Slot;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "CoreMinimal.h"
#include "Components/Widget.h"
/// Helper Ui routines that UE4 is missing, all static
/// Exposed to BP in in StevesBPL
class STEVESUEHELPERS_API StevesUiHelpers
{
public:
/**
* Insert a child widget at a specific index
* @param Parent The container widget
* @param Child The child widget to add
* @param AtIndex The index at which the new child should exist
* @returns The slot the child was inserted at
*/
static UPanelSlot* InsertChildWidgetAt(UPanelWidget* Parent, UWidget* Child, int AtIndex = 0);
};

View File

@@ -0,0 +1,125 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "CoreMinimal.h"
#include "VisualLogger/VisualLogger.h"
class STEVESUEHELPERS_API FStevesVisualLogger
{
protected:
static void InternalPolyLogfImpl(const UObject* LogOwner,
const FLogCategoryBase& Category,
ELogVerbosity::Type Verbosity,
const TArray<FVector>& Points,
const FColor& Color,
const uint16 Thickness);
static void InternalPolyLogf(const UObject* LogOwner,
const FLogCategoryBase& Category,
ELogVerbosity::Type Verbosity,
const FVector& Center,
const FVector& UpAxis,
const float OuterRadius,
const float InnerRadius,
const int NumPoints,
const FColor& Color,
const uint16 Thickness)
{
const bool bIsStar = !FMath::IsNearlyEqual(OuterRadius, InnerRadius);
const FQuat Rotation = FQuat::FindBetweenNormals(FVector::UpVector, UpAxis);
const FVector YAxis = Rotation.RotateVector(FVector::YAxisVector);
const int Count = bIsStar ? NumPoints * 2 : NumPoints;
const float RotInc = UE_TWO_PI / Count;
TArray<FVector> Vertices;
// Start
Vertices.Add(Center + YAxis * OuterRadius);
for (int i = 1; i < Count; ++i)
{
const float Scale = bIsStar && (i % 2) != 0 ? InnerRadius : OuterRadius;
const FQuat Rot = FQuat(UpAxis, RotInc * i);
const FVector V = Center + Rot.RotateVector(YAxis * Scale);
Vertices.Add(V);
}
// End
Vertices.Add(Center + YAxis * OuterRadius);
InternalPolyLogfImpl(LogOwner, Category, Verbosity, Vertices, Color, Thickness);
}
public:
static void TriangleLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const FColor& Color, const uint16 Thickness)
{
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, 3, Color, Thickness);
}
static void SquareLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const FColor& Color, const uint16 Thickness)
{
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, 4, Color, Thickness);
}
static void PolyLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float Radius, const int NumPoints, const FColor& Color, const uint16 Thickness)
{
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, Radius, Radius, NumPoints, Color, Thickness);
}
static void StarLogf(const UObject* LogOwner, const FLogCategoryBase& Category, ELogVerbosity::Type Verbosity, const FVector& Center, const FVector& UpAxis, const float OuterRadius, const float InnerRadius, const int NumPoints, const FColor& Color, const uint16 Thickness)
{
InternalPolyLogf(LogOwner, Category, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness);
}
};
#if ENABLE_VISUAL_LOG
// 2D Triangle shape
#define UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::TriangleLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, 0)
#define UE_CVLOG_TRIANGLE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, 0);}
#define UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::TriangleLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, Thickness)
#define UE_CVLOG_TRIANGLE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness);}
// 2D Square shape
#define UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::SquareLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, 0)
#define UE_CVLOG_SQUARE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, 0);}
#define UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::SquareLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, Color, Thickness)
#define UE_CVLOG_SQUARE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color, Thickness);}
// 2D poly shape
#define UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::PolyLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, NumPoints, Color, 0)
#define UE_CVLOG_POLY(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, 0);}
#define UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::PolyLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness)
#define UE_CVLOG_POLY_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color, Thickness);}
// 2D star shape
#define UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color) if(FVisualLogger::IsRecording()) FStevesVisualLogger::StarLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, 0)
#define UE_CVLOG_STAR(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, 0);}
#define UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording()) FStevesVisualLogger::StarLogf(LogOwner, CategoryName, ELogVerbosity::Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness)
#define UE_CVLOG_STAR_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness) if(FVisualLogger::IsRecording() && Condition) {UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color, Thickness);}
#else
#define UE_VLOG_TRIANGLE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_CVLOG_TRIANGLE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_VLOG_TRIANGLE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_CVLOG_TRIANGLE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
// Square shape
#define UE_VLOG_SQUARE(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_CVLOG_SQUARE(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_VLOG_SQUARE_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
#define UE_CVLOG_SQUARE_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, Color)
// 2D poly shape
#define UE_VLOG_POLY(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
#define UE_CVLOG_POLY(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
#define UE_VLOG_POLY_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
#define UE_CVLOG_POLY_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, Radius, NumPoints, Color)
// Any 2D star shape
#define UE_VLOG_STAR(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
#define UE_CVLOG_STAR(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
#define UE_VLOG_STAR_THICK(LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
#define UE_CVLOG_STAR_THICK(Condition, LogOwner, CategoryName, Verbosity, Center, UpAxis, OuterRadius, InnerRadius, NumPoints, Color)
#endif

View File

@@ -0,0 +1,56 @@
using UnrealBuildTool;
using System.IO;
public class StevesUEHelpers : ModuleRules
{
public StevesUEHelpers(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
}
);
PrivateIncludePaths.AddRange(
new string[] {
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"Slate",
"SlateCore",
"UMG",
"Paper2D",
"DeveloperSettings"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"RenderCore",
"PhysicsCore",
"Chaos",
"ChaosCore",
"NavigationSystem"
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
}
);
}
}

View File

@@ -0,0 +1,201 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#include "StevesFixedDataTableCustomisationLayout.h"
#include "AssetRegistry/AssetData.h"
#include "Containers/Map.h"
#include "DataTableEditorUtils.h"
#include "Delegates/Delegate.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "Engine/DataTable.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Commands/UIAction.h"
#include "HAL/Platform.h"
#include "HAL/PlatformCrt.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Misc/Attribute.h"
#include "PropertyCustomizationHelpers.h"
#include "PropertyEditorModule.h"
#include "PropertyHandle.h"
#include "Templates/Casts.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Text/STextBlock.h"
class SToolTip;
#define LOCTEXT_NAMESPACE "FSsFixedDataTableCustomisationLayout"
void FStevesFixedDataTableCustomisationLayout::CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
DataTablePropertyHandle = InStructPropertyHandle->GetChildHandle("DataTable");
RowNamePropertyHandle = InStructPropertyHandle->GetChildHandle("RowName");
if (InStructPropertyHandle->HasMetaData(TEXT("DataTable")))
{
// Find data table from asset ref
const FString& DataTablePath = InStructPropertyHandle->GetMetaData(TEXT("DataTable"));
if (UDataTable* DataTable = LoadObject<UDataTable>(nullptr, *DataTablePath, nullptr))
{
UObject* Existing = nullptr;
const bool TablePicked = DataTablePropertyHandle->GetValue(Existing) == FPropertyAccess::Success;
if (!TablePicked || Existing != DataTable)
{
DataTablePropertyHandle->SetValue(DataTable);
}
}
else
{
UE_LOG(LogDataTable, Warning, TEXT("No Datatable found at %s"), *DataTablePath);
}
}
else
{
UE_LOG(LogDataTable, Warning, TEXT("No Datatable meta tag present on property %s"), *InStructPropertyHandle->GetPropertyDisplayName().ToString());
}
FPropertyComboBoxArgs ComboArgs(RowNamePropertyHandle,
FOnGetPropertyComboBoxStrings::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowStrings),
FOnGetPropertyComboBoxValue::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowValueString));
ComboArgs.ShowSearchForItemCount = 1;
TSharedRef<SWidget> BrowseTableButton = PropertyCustomizationHelpers::MakeBrowseButton(
FSimpleDelegate::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked),
LOCTEXT("SsBrowseToDatatable", "Browse to DataTable in Content Browser"));
HeaderRow
.NameContent()
[
InStructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MaxDesiredWidth(0.0f) // don't constrain the combo button width
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
PropertyCustomizationHelpers::MakePropertyComboBox(ComboArgs)
]
+SHorizontalBox::Slot()
.Padding(2.0f)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.AutoWidth()
[
BrowseTableButton
]
]; ;
FDataTableEditorUtils::AddSearchForReferencesContextMenu(HeaderRow, FExecuteAction::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnSearchForReferences));
}
void FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked()
{
if (DataTablePropertyHandle.IsValid())
{
UObject* SourceDataTable = nullptr;
if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
{
TArray<FAssetData> Assets;
Assets.Add(SourceDataTable);
GEditor->SyncBrowserToObjects(Assets);
}
}
}
bool FStevesFixedDataTableCustomisationLayout::GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const
{
if (RowNamePropertyHandle.IsValid() && RowNamePropertyHandle->IsValidHandle() && DataTablePropertyHandle.IsValid() && DataTablePropertyHandle->IsValidHandle())
{
// If either handle is multiple value or failure, fail
UObject* SourceDataTable = nullptr;
if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
{
OutDataTable = Cast<UDataTable>(SourceDataTable);
if (RowNamePropertyHandle->GetValue(OutName) == FPropertyAccess::Success)
{
return true;
}
}
}
return false;
}
void FStevesFixedDataTableCustomisationLayout::OnSearchForReferences()
{
UDataTable* DataTable;
FName RowName;
if (GetCurrentValue(DataTable, RowName) && DataTable)
{
TArray<FAssetIdentifier> AssetIdentifiers;
AssetIdentifiers.Add(FAssetIdentifier(DataTable, RowName));
FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers, FReferenceViewerParams());
}
}
FString FStevesFixedDataTableCustomisationLayout::OnGetRowValueString() const
{
if (!RowNamePropertyHandle.IsValid() || !RowNamePropertyHandle->IsValidHandle())
{
return FString();
}
FName RowNameValue;
const FPropertyAccess::Result RowResult = RowNamePropertyHandle->GetValue(RowNameValue);
if (RowResult == FPropertyAccess::Success)
{
if (RowNameValue.IsNone())
{
return LOCTEXT("DataTable_None", "None").ToString();
}
return RowNameValue.ToString();
}
else if (RowResult == FPropertyAccess::Fail)
{
return LOCTEXT("DataTable_None", "None").ToString();
}
else
{
return LOCTEXT("MultipleValues", "Multiple Values").ToString();
}
}
void FStevesFixedDataTableCustomisationLayout::OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const
{
UDataTable* DataTable = nullptr;
FName IgnoredRowName;
// Ignore return value as we will show rows if table is the same but row names are multiple values
GetCurrentValue(DataTable, IgnoredRowName);
TArray<FName> AllRowNames;
if (DataTable != nullptr)
{
for (TMap<FName, uint8*>::TConstIterator Iterator(DataTable->GetRowMap()); Iterator; ++Iterator)
{
AllRowNames.Add(Iterator.Key());
}
// Sort the names alphabetically.
AllRowNames.Sort(FNameLexicalLess());
}
for (const FName& RowName : AllRowNames)
{
OutStrings.Add(MakeShared<FString>(RowName.ToString()));
OutRestrictedItems.Add(false);
}
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,40 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "Containers/Array.h"
#include "Containers/UnrealString.h"
#include "IPropertyTypeCustomization.h"
#include "Templates/SharedPointer.h"
#include "UObject/NameTypes.h"
class IPropertyHandle;
class SToolTip;
class UDataTable;
class UScriptStruct;
struct FAssetData;
/// Drop-down for data table row name when the table itself is fixed in meta tags
class FStevesFixedDataTableCustomisationLayout : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance()
{
return MakeShareable( new FStevesFixedDataTableCustomisationLayout );
}
virtual void CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
// Not needed, but must be implemented
virtual void CustomizeChildren(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override {}
protected:
bool GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const;
void OnSearchForReferences();
void OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const;
FString OnGetRowValueString() const;
void BrowseTableButtonClicked();
TSharedPtr<IPropertyHandle> DataTablePropertyHandle;
TSharedPtr<IPropertyHandle> RowNamePropertyHandle;
};

View File

@@ -0,0 +1,22 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#include "StevesUEHelpersEd.h"
#include "StevesFixedDataTableCustomisationLayout.h"
#define LOCTEXT_NAMESPACE "FStevesUEHelpersEdModule"
void FStevesUEHelpersEdModule::StartupModule()
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FStevesFixedDataTableCustomisationLayout::MakeInstance));
}
void FStevesUEHelpersEdModule::ShutdownModule()
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle");
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FStevesUEHelpersEdModule, StevesUEHelpersEd)

View File

@@ -0,0 +1,13 @@
// Copyright Steve Streeting 2020 onwards
// Released under the MIT license
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FStevesUEHelpersEdModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,30 @@
using UnrealBuildTool;
public class StevesUEHelpersEd : ModuleRules
{
public StevesUEHelpersEd(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UnrealEd",
"PropertyEditor",
"DataTableEditor"
}
);
}
}