├── .p4ignore.txt ├── Resources └── Icon128.png ├── Source └── WibblyWires │ ├── Public │ └── WibblyWires.h │ ├── WibblyWires.Build.cs │ └── Private │ ├── WibblyWires.cpp │ ├── WibblyConnectionDrawingPolicy.h │ ├── Verlet.h │ └── WibblyConnectionDrawingPolicy.cpp ├── WibblyWires.uplugin ├── .gitignore ├── README.md └── LICENSE.md /.p4ignore.txt: -------------------------------------------------------------------------------- 1 | .git/ -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geordiemhall/WibblyWires/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/WibblyWires/Public/WibblyWires.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Geordie Hall. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | class FWibblyWiresModule : public IModuleInterface 8 | { 9 | public: 10 | 11 | /** IModuleInterface implementation */ 12 | virtual void StartupModule() override; 13 | virtual void ShutdownModule() override; 14 | }; 15 | -------------------------------------------------------------------------------- /WibblyWires.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "WibblyWires", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "Geordie Hall", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "EnabledByDefault": true, 18 | "Modules": [ 19 | { 20 | "Name": "WibblyWires", 21 | "Type": "EditorNoCommandlet", 22 | "LoadingPhase": "PostEngineInit" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | /Binaries 3 | *.pdb 4 | *.dll 5 | 6 | # Plugin Intermediate and Binaries 7 | /Plugins/*/Intermediate 8 | /Plugins/*/Binaries 9 | 10 | # Builds 11 | Build/* 12 | 13 | # Whitelist PakBlacklist-.txt files 14 | !Build/*/ 15 | Build/*/** 16 | !Build/*/PakBlacklist*.txt 17 | 18 | # Don't ignore icon files in Build 19 | !Build/**/*.ico 20 | 21 | # Generated project files 22 | /DerivedDataCache 23 | /Intermediate 24 | /Saved 25 | *.sdf 26 | *.sln 27 | *.v12.suo 28 | *.opensdf 29 | *.xcodeproj 30 | *.xcworkspace/ 31 | *.VC.opendb 32 | .vscode/ 33 | .vs/ 34 | 35 | # Temporary VS files 36 | *.TMP 37 | 38 | # OS generated files 39 | Thumbs.db 40 | *.db 41 | .DS_Store 42 | 43 | # Merge files 44 | *.orig -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WibblyWires 2 | 3 | An Unreal plugin that makes your Blueprint wires nice and wibbly. 4 | 5 | Very serious plugin that should only be used for serious purposes. 6 | 7 | https://twitter.com/geordiemhall/status/1551105117247913985 8 | 9 | (Note that this whole thing is very hacked together, and doesn't include the 'cutting' feature in the GIF since that needed engine changes) 10 | 11 | Update: I finally made a PR for the cut/slice feature for those wanting to try it out (on non-wibbly wires no less!) 12 | https://github.com/EpicGames/UnrealEngine/pull/11872 13 | Though I do vaguely remember the spline bounds needing to be more conservative for the wibbly version 14 | 15 | Released as MIT, though would appreciate it not being put on the marketplace, since I've thought about doing it myself eventually. 16 | -------------------------------------------------------------------------------- /Source/WibblyWires/WibblyWires.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Geordie Hall. All rights reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class WibblyWires : ModuleRules 6 | { 7 | public WibblyWires(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange(new string[] { }); 12 | 13 | PrivateIncludePaths.AddRange(new string[] { }); 14 | 15 | PublicDependencyModuleNames.AddRange(new string[] 16 | { 17 | "Core", 18 | }); 19 | 20 | PrivateDependencyModuleNames.AddRange(new string[] 21 | { 22 | "CoreUObject", 23 | "Engine", 24 | "Slate", 25 | "SlateCore", 26 | "GraphEditor", 27 | "UnrealEd", 28 | "BlueprintGraph", 29 | "DeveloperSettings", 30 | }); 31 | 32 | DynamicallyLoadedModuleNames.AddRange(new string[] { }); 33 | } 34 | } -------------------------------------------------------------------------------- /Source/WibblyWires/Private/WibblyWires.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Geordie Hall. All rights reserved. 2 | 3 | #include "WibblyWires.h" 4 | 5 | #include "BlueprintEditorModule.h" 6 | #include "EdGraphUtilities.h" 7 | #include "WibblyConnectionDrawingPolicy.h" 8 | #include "Framework/Notifications/NotificationManager.h" 9 | #include "Widgets/Notifications/SNotificationList.h" 10 | #include "BlueprintEditorModule.h" 11 | 12 | #define LOCTEXT_NAMESPACE "FWibblyWiresModule" 13 | 14 | static TSharedPtr GraphConnectionFactory; 15 | 16 | void FWibblyWiresModule::StartupModule() 17 | { 18 | GraphConnectionFactory = MakeShared(); 19 | FEdGraphUtilities::RegisterVisualPinConnectionFactory(GraphConnectionFactory); 20 | } 21 | 22 | void FWibblyWiresModule::ShutdownModule() 23 | { 24 | if (GraphConnectionFactory.IsValid()) 25 | { 26 | FEdGraphUtilities::UnregisterVisualPinConnectionFactory(GraphConnectionFactory); 27 | GraphConnectionFactory = nullptr; 28 | } 29 | } 30 | 31 | #undef LOCTEXT_NAMESPACE 32 | 33 | IMPLEMENT_MODULE(FWibblyWiresModule, WibblyWires) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Geordie Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Source/WibblyWires/Private/WibblyConnectionDrawingPolicy.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Geordie Hall. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "BlueprintConnectionDrawingPolicy.h" 7 | #include "ConnectionDrawingPolicy.h" 8 | #include "Verlet.h" 9 | #include "EdGraphUtilities.h" 10 | #include "Engine/SpringInterpolator.h" 11 | 12 | struct FWireId 13 | { 14 | FWireId(UEdGraphPin* InStartPin, UEdGraphPin* InEndPin) 15 | : StartPin(InStartPin) 16 | , EndPin(InEndPin) 17 | , StartPinHandle(FGraphPinHandle(InStartPin)) 18 | , EndPinHandle(FGraphPinHandle(InEndPin)) 19 | { 20 | uint32 StartHash = StartPin ? GetTypeHash(StartPin->PinId) : 0; 21 | uint32 EndHash = EndPin ? GetTypeHash(EndPin->PinId) : 0; 22 | Hash = HashCombine(StartHash, EndHash); 23 | } 24 | 25 | FORCEINLINE bool operator ==(const FWireId& Other) const 26 | { 27 | return StartPin == Other.StartPin && EndPin == Other.EndPin; 28 | } 29 | 30 | FORCEINLINE bool operator !=(const FWireId& Other) const 31 | { 32 | return StartPin != Other.StartPin && EndPin != Other.EndPin; 33 | } 34 | 35 | friend uint32 GetTypeHash(const FWireId& WireId) 36 | { 37 | return WireId.Hash; 38 | } 39 | 40 | bool IsPreviewConnector() const 41 | { 42 | return StartPin == nullptr || EndPin == nullptr; 43 | } 44 | 45 | const UEdGraphPin* GetConnectedPin() const 46 | { 47 | return StartPin == nullptr ? EndPin : StartPin; 48 | } 49 | 50 | public: 51 | const UEdGraphPin* StartPin; 52 | const UEdGraphPin* EndPin; 53 | FGraphPinHandle StartPinHandle; 54 | FGraphPinHandle EndPinHandle; 55 | 56 | private: 57 | uint32 Hash; 58 | }; 59 | 60 | struct FWireState 61 | { 62 | float DesiredRopeLength; 63 | float LerpedRopeLength; 64 | FVector2f DesiredRopeCenterPoint; 65 | FRK4SpringInterpolator SpringCenterPoint; 66 | float DesiredSlackMultiplier; 67 | FVector2f LastStartPoint; 68 | FVector2f LastEndPoint; 69 | FLinearColor Color; 70 | 71 | FWireState() = default; 72 | FWireState(FVector2f StartPoint, FVector2f EndPoint, float SpringStiffness, float SpringDampeningRatio, float InDesiredSlackMultiplier); 73 | 74 | FVector2f CalculateDesiredCenterPointWithRopeLengthDelta(FVector2f StartPoint, FVector2f EndPoint, float RopeLengthDelta); 75 | FVector2f CalculateDesiredCenterPoint(FVector2f StartPoint, FVector2f EndPoint); 76 | float CalculateDesiredRopeLength(FVector2f StartPoint, FVector2f EndPoint); 77 | FVector2f Update(FVector2f StartPoint, FVector2f EndPoint, float DeltaTime); 78 | }; 79 | 80 | struct FGraphState 81 | { 82 | TMap Wires; 83 | FVerletState VerletWires; 84 | }; 85 | 86 | /** 87 | * A drawing policy that wibbles 88 | */ 89 | class FWibblyConnectionDrawingPolicy : public FKismetConnectionDrawingPolicy 90 | { 91 | public: 92 | 93 | struct Factory : public FGraphPanelPinConnectionFactory 94 | { 95 | public: 96 | virtual ~Factory() = default; 97 | 98 | // FGraphPanelPinConnectionFactory 99 | virtual class FConnectionDrawingPolicy* CreateConnectionPolicy( 100 | const UEdGraphSchema* Schema, 101 | int32 InBackLayerID, 102 | int32 InFrontLayerID, 103 | float InZoomFactor, 104 | const FSlateRect& InClippingRect, 105 | FSlateWindowElementList& InDrawElements, 106 | UEdGraph* InGraphObj) const override; 107 | // ~FGraphPanelPinConnectionFactory 108 | }; 109 | 110 | FWibblyConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj); 111 | 112 | virtual void DrawConnection(int32 LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params) override; 113 | 114 | private: 115 | 116 | UEdGraph* GraphObj; 117 | FGraphState& GraphState; 118 | }; 119 | -------------------------------------------------------------------------------- /Source/WibblyWires/Private/Verlet.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "WibblyWires.h" 4 | #include "Framework/Application/SlateApplication.h" 5 | 6 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 7 | typedef FVector2f FVectorType; 8 | typedef FBox2f FBoxType; 9 | #else 10 | typedef FVector2D FVectorType; 11 | typedef FBox2D FBoxType; 12 | #endif 13 | 14 | extern float WireFriction; 15 | extern float SecondsBeforeBreaking; 16 | extern float WireShrinkRate; 17 | 18 | struct FVerletPoint 19 | { 20 | FVectorType Position; 21 | FVectorType LastPosition; 22 | FVectorType Acceleration; 23 | bool bIsPinned = false; 24 | 25 | FVerletPoint(FVectorType InitialPosition, bool IsPinned = false, FVectorType InitialVelocity = FVectorType::ZeroVector) 26 | { 27 | Position = InitialPosition; 28 | LastPosition = Position - InitialVelocity; 29 | bIsPinned = IsPinned; 30 | Acceleration = FVectorType::ZeroVector; 31 | } 32 | 33 | FVectorType CalculateVelocity() const 34 | { 35 | return Position - LastPosition; 36 | } 37 | 38 | void UpdatePosition(float deltaTime) 39 | { 40 | if (!bIsPinned) 41 | { 42 | const FVectorType Velocity = CalculateVelocity() * WireFriction; 43 | LastPosition = Position; 44 | Position = Position + Velocity + Acceleration * deltaTime * deltaTime; 45 | } 46 | 47 | Acceleration = FVectorType::ZeroVector; 48 | } 49 | 50 | void Accelerate(FVectorType Impulse) 51 | { 52 | Acceleration += Impulse; 53 | } 54 | 55 | void AddVelocity(FVectorType Velocity) 56 | { 57 | LastPosition -= Velocity; 58 | } 59 | }; 60 | 61 | struct FVerletStick 62 | { 63 | int32 Point0Index; 64 | int32 Point1Index; 65 | float DesiredLength; 66 | 67 | FVerletStick() = delete; 68 | 69 | FVerletStick(int32 InPoint0, int32 InPoint1, float InDesiredLength) 70 | : Point0Index(InPoint0) 71 | , Point1Index(InPoint1) 72 | , DesiredLength(InDesiredLength) 73 | { 74 | } 75 | 76 | void ConstrainLength(FVerletPoint& Point0, FVerletPoint& Point1) 77 | { 78 | FVectorType Delta = Point1.Position - Point0.Position; 79 | float CurrentLength = Delta.Size(); 80 | float Difference = DesiredLength - CurrentLength; 81 | float HalfPercent = (Difference / CurrentLength) * 0.5f; 82 | FVectorType HalfOffset = Delta * HalfPercent; 83 | 84 | // If either is pinned, the the other will need to cover the full adjustment 85 | // If both are pinned then this will be wasted, but not micro-optimizing yet 86 | if (Point0.bIsPinned || Point1.bIsPinned) 87 | { 88 | HalfOffset *= 2.f; 89 | } 90 | 91 | if (!Point0.bIsPinned) 92 | { 93 | Point0.Position -= HalfOffset; 94 | } 95 | 96 | if (!Point1.bIsPinned) 97 | { 98 | Point1.Position += HalfOffset; 99 | } 100 | } 101 | }; 102 | 103 | struct FVerletChain 104 | { 105 | FVectorType Gravity = FVectorType(0.f, 1500.f); 106 | TArray Points; 107 | TArray Sticks; 108 | FLinearColor LineColor; 109 | float LineThickness; 110 | bool bHasFullyShrunk = false; 111 | float CreationTime; 112 | bool bHasBroken = false; 113 | 114 | FVerletChain(FLinearColor InLineColor, float InLineThickness) 115 | { 116 | LineColor = InLineColor; 117 | LineThickness = InLineThickness; 118 | CreationTime = FSlateApplication::Get().GetCurrentTime(); 119 | } 120 | 121 | // Adds a new point and automatically connects it to the previous point with a stick 122 | void AddToChain(FVectorType NewPoint, bool bIsPinned = false) 123 | { 124 | Points.Add(FVerletPoint(NewPoint, bIsPinned)); 125 | 126 | int32 PointCount = Points.Num(); 127 | if (PointCount >= 2) 128 | { 129 | int32 P0Index = PointCount - 2; 130 | int32 P1Index = PointCount - 1; 131 | FVerletPoint P0 = Points[P0Index]; 132 | FVerletPoint P1 = Points[P1Index]; 133 | float DesiredLength = FVectorType::Distance(P0.Position, P1.Position); 134 | Sticks.Add(FVerletStick(P0Index, P1Index, DesiredLength)); 135 | } 136 | } 137 | 138 | // Offsets the whole simulation by some translation 139 | void Translate(FVectorType Translation) 140 | { 141 | for (FVerletPoint& Point : Points) 142 | { 143 | Point.Position += Translation; 144 | Point.LastPosition += Translation; 145 | } 146 | } 147 | 148 | void ShrinkSticksBy(float Multiplier) 149 | { 150 | for (FVerletStick& Stick : Sticks) 151 | { 152 | Stick.DesiredLength *= Multiplier; 153 | } 154 | } 155 | 156 | void UnpinAll() 157 | { 158 | SetAllPinned(false); 159 | } 160 | 161 | void SetAllPinned(bool bIsPinned) 162 | { 163 | for (FVerletPoint& Point : Points) 164 | { 165 | Point.bIsPinned = bIsPinned; 166 | } 167 | } 168 | 169 | float GetSecondsSinceCreated() const 170 | { 171 | return FSlateApplication::Get().GetCurrentTime() - CreationTime; 172 | } 173 | 174 | void Update(float DeltaTime) 175 | { 176 | static const float MaxDeltaTime = 1.0f / 30.f; 177 | DeltaTime = FMath::Min(MaxDeltaTime, DeltaTime); 178 | 179 | float TimeSinceCreated = GetSecondsSinceCreated(); 180 | if (TimeSinceCreated > SecondsBeforeBreaking && !bHasBroken) 181 | { 182 | SetAllPinned(false); 183 | bHasBroken = true; 184 | } 185 | 186 | const int32 Substeps = 10; 187 | float SubDeltaTime = DeltaTime / Substeps; 188 | for (int32 i = 0; i < Substeps; i++) 189 | { 190 | ApplyGravity(); 191 | 192 | UpdatePositions(SubDeltaTime); 193 | 194 | for (int32 j = 0; j < 5; j++) 195 | { 196 | ApplyConstraints(); 197 | ApplyCollisions(); 198 | } 199 | } 200 | } 201 | 202 | FBoxType CalcBounds() const 203 | { 204 | FBoxType Bounds(EForceInit::ForceInitToZero); 205 | 206 | for (const FVerletPoint& Point : Points) 207 | { 208 | Bounds += Point.Position; 209 | } 210 | 211 | return Bounds; 212 | } 213 | 214 | private: 215 | 216 | void ShrinkSticks(float DeltaTime) 217 | { 218 | bHasFullyShrunk = true; 219 | 220 | for (int32 i = 0; i < Sticks.Num(); i++) 221 | { 222 | FVerletStick& Stick = Sticks[i]; 223 | 224 | if (Stick.DesiredLength < 1.f) 225 | { 226 | Stick.DesiredLength = 0.1f; 227 | Points[Stick.Point0Index].bIsPinned = true; 228 | Points[Stick.Point1Index].bIsPinned = true; 229 | Points[Stick.Point1Index].Position = Points[Stick.Point0Index].Position; 230 | } 231 | else 232 | { 233 | Stick.DesiredLength = FMath::Max(Stick.DesiredLength - (WireShrinkRate * DeltaTime), 0.1f); 234 | bHasFullyShrunk = false; 235 | break; 236 | } 237 | } 238 | } 239 | 240 | void UpdatePositions(float DeltaTime) 241 | { 242 | for (FVerletPoint& Point : Points) 243 | { 244 | Point.UpdatePosition(DeltaTime); 245 | } 246 | } 247 | 248 | void ApplyConstraints() 249 | { 250 | for (FVerletStick& Stick : Sticks) 251 | { 252 | Stick.ConstrainLength(Points[Stick.Point0Index], Points[Stick.Point1Index]); 253 | } 254 | } 255 | 256 | void ApplyCollisions() 257 | { 258 | // Nothing for now 259 | } 260 | 261 | void ApplyGravity() 262 | { 263 | for (FVerletPoint& Point : Points) 264 | { 265 | Point.Accelerate(Gravity); 266 | } 267 | } 268 | }; 269 | 270 | class FVerletState 271 | { 272 | public: 273 | void TranslateVerletChains(FVectorType Translation) 274 | { 275 | for (FVerletChain& Chain : VerletChains) 276 | { 277 | Chain.Translate(Translation); 278 | } 279 | } 280 | 281 | void UpdateVerletChains(float DeltaTime) 282 | { 283 | for (FVerletChain& Chain : VerletChains) 284 | { 285 | Chain.Update(DeltaTime); 286 | } 287 | 288 | // Delete any chains that are entirely below the bottom of the screen 289 | VerletChains.RemoveAllSwap([](const FVerletChain& Chain) 290 | { 291 | if (Chain.GetSecondsSinceCreated() > 30.f) 292 | { 293 | return true; 294 | } 295 | 296 | FBoxType Bounds = Chain.CalcBounds(); 297 | if (Bounds.Min.Y > 2000.f || Bounds.Max.Y < -1000.f || Bounds.Min.X > 3000.f || Bounds.Max.X < -1000.f) 298 | { 299 | return true; 300 | } 301 | 302 | return false; 303 | }); 304 | } 305 | 306 | void RenderVerletChains(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, float ThicknessScale) 307 | { 308 | int32 MaxPointCount = 0; 309 | 310 | for (const FVerletChain& Chain : VerletChains) 311 | { 312 | MaxPointCount = FMath::Max(MaxPointCount, Chain.Points.Num()); 313 | } 314 | 315 | // Re-use this array between graphs and frames to save on allocations 316 | static TArray Points; 317 | Points.Reserve(MaxPointCount); 318 | 319 | for (const FVerletChain& Chain : VerletChains) 320 | { 321 | Points.Reset(); 322 | 323 | for (const FVerletPoint& Point : Chain.Points) 324 | { 325 | Points.Add(Point.Position); 326 | } 327 | 328 | float Opacity = FMath::Clamp(1.f - ((Chain.GetSecondsSinceCreated() - 1.f) / 1.f), 0.f, 1.f); 329 | 330 | // TODO: Catmull-Rom spline through these points so can get away with fewer segments 331 | FSlateDrawElement::MakeLines( 332 | OutDrawElements, 333 | LayerId, 334 | FPaintGeometry(), 335 | Points, 336 | ESlateDrawEffect::NoPixelSnapping, 337 | Chain.LineColor.CopyWithNewOpacity(Opacity), 338 | true, // bAntiAlias 339 | Chain.LineThickness * ThicknessScale); 340 | } 341 | } 342 | 343 | private: 344 | TArray VerletChains; 345 | }; -------------------------------------------------------------------------------- /Source/WibblyWires/Private/WibblyConnectionDrawingPolicy.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Geordie Hall. All rights reserved. 2 | 3 | 4 | #include "WibblyConnectionDrawingPolicy.h" 5 | 6 | #include "EdGraphSchema_K2.h" 7 | #include "Verlet.h" 8 | #include "WibblySettings.h" 9 | #include "Engine/SpringInterpolator.h" 10 | 11 | // For each graph guid, store a map from wire id to wire state 12 | static TMap GraphStates; 13 | 14 | static int32 EnableWibblyWires = 1; 15 | FAutoConsoleVariableRef CVarEnableWibblyWires( 16 | TEXT("WibblyWires.Enabled"), 17 | EnableWibblyWires, 18 | TEXT("Whether BP wires should be Wibbly.")); 19 | 20 | static float ThicknessMultiplier = 1.5f; 21 | FAutoConsoleVariableRef CVarThicknessMultiplier( 22 | TEXT("WibblyWires.ThicknessMultiplier"), 23 | ThicknessMultiplier, 24 | TEXT("How much thicker to draw the wire lines.")); 25 | 26 | static int32 BounceWires = 0; 27 | FAutoConsoleVariableRef CVarBounceWires( 28 | TEXT("WibblyWires.BounceWires"), 29 | BounceWires, 30 | TEXT("Whether wires have some bounce when they extend too far") 31 | ); 32 | 33 | static float RopeLengthHangMultiplier = 1.f; 34 | FAutoConsoleVariableRef CVarWireLength( 35 | TEXT("WibblyWires.WireLength"), 36 | RopeLengthHangMultiplier, 37 | TEXT("How much extra length should wires have") 38 | ); 39 | 40 | float WireShrinkRate = 150.f; 41 | FAutoConsoleVariableRef CVarWireShrinkRate( 42 | TEXT("WibblyWires.WireShrinkRate"), 43 | WireShrinkRate, 44 | TEXT("How quickly should wires get sucked back into their nodes after having been cut") 45 | ); 46 | 47 | float SecondsBeforeBreaking = 1.f; 48 | FAutoConsoleVariableRef CVarSecondsBeforeBreaking( 49 | TEXT("WibblyWires.SecondsBeforeBreaking"), 50 | SecondsBeforeBreaking, 51 | TEXT("How many seconds should cut wires dangle before detaching from their nodes and falling") 52 | ); 53 | 54 | float WireFriction = 0.9996f; 55 | FAutoConsoleVariableRef CVarWireFriction( 56 | TEXT("WibblyWires.WireFriction"), 57 | WireFriction, 58 | TEXT("Friction multiplier for velocities, should be very close to 1.") 59 | ); 60 | 61 | FAutoConsoleCommand CVarResetWireStates( 62 | TEXT("WibblyWires.ResetWireStates"), 63 | TEXT("Resets wire states so that they're reinitialized with latest defaults etc."), 64 | FConsoleCommandDelegate::CreateLambda([]() 65 | { 66 | GraphStates.Reset(); 67 | }) 68 | ); 69 | 70 | FWireState::FWireState(FVector2f StartPoint, FVector2f EndPoint, float SpringStiffness, float SpringDampeningRatio, float InDesiredSlackMultiplier) 71 | { 72 | LastStartPoint = StartPoint; 73 | LastEndPoint = EndPoint; 74 | DesiredSlackMultiplier = InDesiredSlackMultiplier; 75 | 76 | // Snap to the desired rope length 77 | DesiredRopeLength = CalculateDesiredRopeLength(StartPoint, EndPoint); 78 | LerpedRopeLength = DesiredRopeLength * 1.1f; // Start off a little off from desired so there's an initial bounce 79 | 80 | // Snap to the desired center point 81 | float LengthDelta = LerpedRopeLength - (EndPoint - StartPoint).Size(); 82 | DesiredRopeCenterPoint = CalculateDesiredCenterPointWithRopeLengthDelta(StartPoint, EndPoint, LengthDelta); 83 | SpringCenterPoint.SetSpringConstants(SpringStiffness, SpringDampeningRatio); 84 | SpringCenterPoint.Reset(FVector(DesiredRopeCenterPoint.X, DesiredRopeCenterPoint.Y, 0.f)); 85 | } 86 | 87 | FVector2f FWireState::CalculateDesiredCenterPointWithRopeLengthDelta(FVector2f StartPoint, FVector2f EndPoint, float RopeLengthDelta) 88 | { 89 | FVector2f Center = CalculateDesiredCenterPoint(StartPoint, EndPoint); 90 | Center.Y += RopeLengthDelta * RopeLengthHangMultiplier; 91 | return Center; 92 | } 93 | 94 | FVector2f FWireState::CalculateDesiredCenterPoint(FVector2f StartPoint, FVector2f EndPoint) 95 | { 96 | if (StartPoint.X > EndPoint.X) 97 | { 98 | Swap(StartPoint, EndPoint); 99 | } 100 | 101 | FVector2f Delta = EndPoint - StartPoint; 102 | FVector2f Direction = Delta.GetSafeNormal(); 103 | FVector2f UpDirection(0.f, 1.f); 104 | float DotWithUp = Direction | UpDirection; 105 | DotWithUp = FMath::Pow(FMath::Abs(DotWithUp), 2.f) * FMath::Sign(DotWithUp); 106 | float NormalizedDotWithUp = DotWithUp * 0.5f + 0.5f; 107 | float CenterX = FMath::Lerp(StartPoint.X, EndPoint.X, NormalizedDotWithUp); 108 | // This won't be quite the same as deriving a CenterY from the real CenterX, but we'll see how it looks cause avoids some trig 109 | FVector2f Center = FMath::Lerp(StartPoint, EndPoint, NormalizedDotWithUp); 110 | return Center; 111 | } 112 | 113 | float FWireState::CalculateDesiredRopeLength(FVector2f StartPoint, FVector2f EndPoint) 114 | { 115 | FVector2f Delta = EndPoint - StartPoint; 116 | float TightRopeLength = Delta.Size(); 117 | return TightRopeLength * DesiredSlackMultiplier; 118 | } 119 | 120 | FVector2f FWireState::Update(FVector2f StartPoint, FVector2f EndPoint, float DeltaTime) 121 | { 122 | LastStartPoint = StartPoint; 123 | LastEndPoint = EndPoint; 124 | 125 | // Ensure start point is always the left-most point so we can make some assumptions with our math 126 | if (StartPoint.X > EndPoint.X) 127 | { 128 | Swap(StartPoint, EndPoint); 129 | } 130 | 131 | FVector2f Delta = EndPoint - StartPoint; 132 | 133 | float TightRopeLength = Delta.Size(); 134 | 135 | DesiredRopeLength = TightRopeLength * DesiredSlackMultiplier; 136 | LerpedRopeLength = FMath::Max(TightRopeLength, FMath::Lerp(LerpedRopeLength, DesiredRopeLength, DeltaTime * 20.f)); 137 | float LengthDelta = LerpedRopeLength - TightRopeLength; 138 | 139 | DesiredRopeCenterPoint = CalculateDesiredCenterPointWithRopeLengthDelta(StartPoint, EndPoint, LengthDelta); 140 | 141 | // Calculate desired center point 142 | FVector2f LerpedCenterPoint = FVector2f(FVector2D(SpringCenterPoint.Update(FVector(DesiredRopeCenterPoint.X, DesiredRopeCenterPoint.Y , 0.f), DeltaTime))); 143 | 144 | FVector2f Velocity = FVector2f(FVector2D(SpringCenterPoint.GetVelocity())); 145 | if (BounceWires && LerpedCenterPoint.Y > DesiredRopeCenterPoint.Y && Velocity.Y > 0.1f) 146 | { 147 | Velocity.Y = FMath::Abs(Velocity.Y) * -0.9f; 148 | SpringCenterPoint.SetVelocity(FVector(Velocity.X, Velocity.Y, 0.f)); 149 | } 150 | 151 | return LerpedCenterPoint; 152 | } 153 | 154 | FConnectionDrawingPolicy* FWibblyConnectionDrawingPolicy::Factory::CreateConnectionPolicy(const UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) const 155 | { 156 | if (!GetDefault()->bEnableWibblyWires) 157 | { 158 | return nullptr; 159 | } 160 | 161 | if (EnableWibblyWires) 162 | { 163 | if (Schema->IsA(UEdGraphSchema_K2::StaticClass())) 164 | { 165 | return new FWibblyConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj); 166 | } 167 | } 168 | else 169 | { 170 | // Release our memory if not even enabled 171 | GraphStates.Empty(); 172 | } 173 | 174 | return nullptr; 175 | } 176 | 177 | namespace FRK4SpringInterpolatorUtils 178 | { 179 | static FORCEINLINE bool IsValidValue(FVector2f Value, float MaxAbsoluteValue = RK4_SPRING_INTERPOLATOR_MAX_VALUE) 180 | { 181 | return Value.GetAbsMax() < MaxAbsoluteValue; 182 | } 183 | 184 | static FORCEINLINE bool IsValidValue(FVector3f Value, float MaxAbsoluteValue = RK4_SPRING_INTERPOLATOR_MAX_VALUE) 185 | { 186 | return Value.GetAbsMax() < MaxAbsoluteValue; 187 | } 188 | 189 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 190 | static FORCEINLINE bool AreEqual(FVector2f A, FVector2f B, float ErrorTolerance = UE_KINDA_SMALL_NUMBER) 191 | { 192 | return A.Equals(B, ErrorTolerance); 193 | } 194 | 195 | static FORCEINLINE bool AreEqual(FVector3f A, FVector3f B, float ErrorTolerance = UE_KINDA_SMALL_NUMBER) 196 | { 197 | return A.Equals(B, ErrorTolerance); 198 | } 199 | #else 200 | static FORCEINLINE bool AreEqual(FVector2f A, FVector2f B, float ErrorTolerance = KINDA_SMALL_NUMBER) 201 | { 202 | return A.Equals(B, ErrorTolerance); 203 | } 204 | #endif 205 | 206 | } 207 | 208 | FWibblyConnectionDrawingPolicy::FWibblyConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) 209 | : FKismetConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj) 210 | , GraphObj(InGraphObj) 211 | , GraphState(GraphStates.FindOrAdd(InGraphObj->GraphGuid)) 212 | { 213 | } 214 | 215 | void FWibblyConnectionDrawingPolicy::DrawConnection(int32 LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params) 216 | { 217 | const FVector2f& P0 = Start; 218 | const FVector2f& P1 = End; 219 | 220 | const float DefaultStiffness = 100.f; 221 | const float DefaultDampeningRatio = 0.4f; 222 | 223 | float WireThickness = Params.WireThickness * ThicknessMultiplier * FSlateApplication::Get().GetApplicationScale() * ZoomFactor; 224 | 225 | const FWireId WireId(Params.AssociatedPin1, Params.AssociatedPin2); 226 | FWireState* WireState = GraphState.Wires.Find(WireId); 227 | 228 | // Create a new wire if needed 229 | if (!WireState) 230 | { 231 | bool bIsPreviewConnector = Params.AssociatedPin1 == nullptr || Params.AssociatedPin2 == nullptr; 232 | float StiffnessVariance = FMath::FRandRange(0.3f, 1.5f); 233 | float DampeningVariance = FMath::FRandRange(0.7f, 1.2f); 234 | float SlackMultiplier = 1.3f + FMath::FRandRange(0.f, 0.3f); 235 | float Stiffness = DefaultStiffness * StiffnessVariance + (bIsPreviewConnector ? 0.3f : 0.f); 236 | float DampeningRatio = FMath::Clamp(DefaultDampeningRatio * DampeningVariance, 0.3f, 0.9f); 237 | FWireState NewWireState(Start, End, Stiffness, DampeningRatio, SlackMultiplier); 238 | NewWireState.Color = Params.WireColor; 239 | 240 | for (const auto& ExistingState : GraphState.Wires) 241 | { 242 | if (!ExistingState.Key.IsPreviewConnector()) 243 | { 244 | continue; 245 | } 246 | 247 | const UEdGraphPin* ConnectedPin = ExistingState.Key.GetConnectedPin(); 248 | if (ConnectedPin != Params.AssociatedPin1 && ConnectedPin != Params.AssociatedPin2) 249 | { 250 | continue; 251 | } 252 | 253 | const float DistThresholdSqr = 30.f * 30.f; 254 | if (FVector2f::DistSquared(ExistingState.Value.LastStartPoint, Start) < DistThresholdSqr && FVector2f::DistSquared(ExistingState.Value.LastEndPoint, End) < DistThresholdSqr) 255 | { 256 | // Inherit our initial state from this existing thing, since it was probably a preview connector that got connected 257 | NewWireState = ExistingState.Value; 258 | } 259 | } 260 | 261 | WireState = &GraphState.Wires.Add(WireId, MoveTemp(NewWireState)); 262 | } 263 | 264 | // Clamp our tick rate to 30fps to avoid editor hitches hiding our animations, we'd rather they just pause 265 | static const float MaxDeltaTime = 1.f / 30.f; 266 | const float DeltaTime = FMath::Min(FSlateApplication::Get().GetDeltaTime(), MaxDeltaTime); 267 | FVector2f CenterPoint = WireState->Update(Start, End, DeltaTime); 268 | 269 | // Don't need these anymore! 270 | // const FVector2f SplineTangent = ComputeSplineTangent(P0, P1); 271 | // const FVector2f P0Tangent = (Params.StartDirection == EGPD_Output) ? SplineTangent : -SplineTangent; 272 | // const FVector2f P1Tangent = (Params.EndDirection == EGPD_Input) ? SplineTangent : -SplineTangent; 273 | 274 | // Magic number to get more of a bend 275 | const FVector2f P0Tangent = (CenterPoint - P0) * 1.3f; 276 | const FVector2f P1Tangent = (P1 - CenterPoint) * 1.3f; 277 | 278 | if (Settings->bTreatSplinesLikePins) 279 | { 280 | // Distance to consider as an overlap 281 | const float QueryDistanceTriggerThresholdSquared = FMath::Square(Settings->SplineHoverTolerance + WireThickness * 0.5f); 282 | 283 | // Distance to pass the bounding box cull test. This is used for the bCloseToSpline output that can be used as a 284 | // dead zone to avoid mistakes caused by missing a double-click on a connection. 285 | const float QueryDistanceForCloseSquared = FMath::Square(FMath::Sqrt(QueryDistanceTriggerThresholdSquared) + Settings->SplineCloseTolerance); 286 | 287 | bool bCloseToSpline = false; 288 | { 289 | // The curve will include the endpoints but can extend out of a tight bounds because of the tangents 290 | // P0Tangent coefficient maximizes to 4/27 at a=1/3, and P1Tangent minimizes to -4/27 at a=2/3. 291 | // const float MaximumTangentContribution = 4.0f / 27.0f; 292 | 293 | // Note (Geordie): If we don't use the engine's tangent limits then need to use full control-point bounds 294 | const float MaximumTangentContribution = 1.f / 3.f; 295 | FBox2f Bounds(ForceInit); 296 | 297 | Bounds += FVector2f(P0); 298 | Bounds += FVector2f(P0 + MaximumTangentContribution * P0Tangent); 299 | Bounds += FVector2f(P1); 300 | Bounds += FVector2f(P1 - MaximumTangentContribution * P1Tangent); 301 | 302 | bCloseToSpline = Bounds.ComputeSquaredDistanceToPoint(LocalMousePosition) < QueryDistanceForCloseSquared; 303 | 304 | // Draw the bounding box for debugging 305 | #if 0 306 | #define DrawSpaceLine(Point1, Point2, DebugWireColor) {const FVector2f FakeTangent = (Point2 - Point1).GetSafeNormal(); FSlateDrawElement::MakeDrawSpaceSpline(DrawElementsList, LayerId, Point1, FakeTangent, Point2, FakeTangent, ClippingRect, 1.0f, ESlateDrawEffect::None, DebugWireColor); } 307 | 308 | if (bCloseToSpline) 309 | { 310 | const FLinearColor BoundsWireColor = bCloseToSpline ? FLinearColor::Green : FLinearColor::White; 311 | 312 | FVector2f TL = Bounds.Min; 313 | FVector2f BR = Bounds.Max; 314 | FVector2f TR = FVector2f(Bounds.Max.X, Bounds.Min.Y); 315 | FVector2f BL = FVector2f(Bounds.Min.X, Bounds.Max.Y); 316 | 317 | DrawSpaceLine(TL, TR, BoundsWireColor); 318 | DrawSpaceLine(TR, BR, BoundsWireColor); 319 | DrawSpaceLine(BR, BL, BoundsWireColor); 320 | DrawSpaceLine(BL, TL, BoundsWireColor); 321 | } 322 | #endif 323 | } 324 | 325 | if (bCloseToSpline) 326 | { 327 | // Find the closest approach to the spline 328 | FVector2f ClosestPoint(ForceInit); 329 | float ClosestDistanceSquared = FLT_MAX; 330 | 331 | const int32 NumStepsToTest = 16; 332 | const float StepInterval = 1.0f / (float)NumStepsToTest; 333 | FVector2f Point1 = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, 0.0f); 334 | for (float TestAlpha = 0.0f; TestAlpha < 1.0f; TestAlpha += StepInterval) 335 | { 336 | const FVector2f Point2 = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, TestAlpha + StepInterval); 337 | 338 | const FVector2f ClosestPointToSegment = FMath::ClosestPointOnSegment2D(LocalMousePosition, Point1, Point2); 339 | const float DistanceSquared = (LocalMousePosition - ClosestPointToSegment).SizeSquared(); 340 | 341 | if (DistanceSquared < ClosestDistanceSquared) 342 | { 343 | ClosestDistanceSquared = DistanceSquared; 344 | ClosestPoint = ClosestPointToSegment; 345 | } 346 | 347 | Point1 = Point2; 348 | } 349 | 350 | // Record the overlap 351 | if (ClosestDistanceSquared < QueryDistanceTriggerThresholdSquared) 352 | { 353 | if (ClosestDistanceSquared < SplineOverlapResult.GetDistanceSquared()) 354 | { 355 | const float SquaredDistToPin1 = (Params.AssociatedPin1 != nullptr) ? (P0 - ClosestPoint).SizeSquared() : FLT_MAX; 356 | const float SquaredDistToPin2 = (Params.AssociatedPin2 != nullptr) ? (P1 - ClosestPoint).SizeSquared() : FLT_MAX; 357 | 358 | SplineOverlapResult = FGraphSplineOverlapResult(Params.AssociatedPin1, Params.AssociatedPin2, ClosestDistanceSquared, SquaredDistToPin1, SquaredDistToPin2, true); 359 | } 360 | } 361 | else if (ClosestDistanceSquared < QueryDistanceForCloseSquared) 362 | { 363 | SplineOverlapResult.SetCloseToSpline(true); 364 | } 365 | } 366 | } 367 | 368 | // Draw the spline itself 369 | FSlateDrawElement::MakeDrawSpaceSpline( 370 | DrawElementsList, 371 | LayerId, 372 | P0, P0Tangent, 373 | P1, P1Tangent, 374 | WireThickness, 375 | ESlateDrawEffect::None, 376 | Params.WireColor 377 | ); 378 | 379 | if (Params.bDrawBubbles || (MidpointImage != nullptr)) 380 | { 381 | // This table maps distance along curve to alpha 382 | FInterpCurve SplineReparamTable; 383 | const float SplineLength = MakeSplineReparamTable(P0, P0Tangent, P1, P1Tangent, SplineReparamTable); 384 | 385 | // Draw bubbles on the spline 386 | if (Params.bDrawBubbles) 387 | { 388 | const float BubbleSpacing = 64.f * ZoomFactor; 389 | const float BubbleSpeed = 192.f * ZoomFactor; 390 | const FVector2f BubbleSize = BubbleImage->ImageSize * ZoomFactor * 0.2f * Params.WireThickness; 391 | 392 | float Time = (FPlatformTime::Seconds() - GStartTime); 393 | const float BubbleOffset = FMath::Fmod(Time * BubbleSpeed, BubbleSpacing); 394 | const int32 NumBubbles = FMath::CeilToInt(SplineLength/BubbleSpacing); 395 | for (int32 i = 0; i < NumBubbles; ++i) 396 | { 397 | const float Distance = ((float)i * BubbleSpacing) + BubbleOffset; 398 | if (Distance < SplineLength) 399 | { 400 | const float Alpha = SplineReparamTable.Eval(Distance, 0.f); 401 | FVector2f BubblePos = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, Alpha); 402 | BubblePos -= (BubbleSize * 0.5f); 403 | 404 | FSlateDrawElement::MakeBox( 405 | DrawElementsList, 406 | LayerId, 407 | FPaintGeometry( BubblePos, BubbleSize, ZoomFactor ), 408 | BubbleImage, 409 | ESlateDrawEffect::None, 410 | Params.WireColor 411 | ); 412 | } 413 | } 414 | } 415 | 416 | // Draw the midpoint image 417 | if (MidpointImage != nullptr) 418 | { 419 | // Determine the spline position for the midpoint 420 | const float MidpointAlpha = SplineReparamTable.Eval(SplineLength * 0.5f, 0.f); 421 | const FVector2f Midpoint = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, MidpointAlpha); 422 | 423 | // Approximate the slope at the midpoint (to orient the midpoint image to the spline) 424 | const FVector2f MidpointPlusE = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, MidpointAlpha + KINDA_SMALL_NUMBER); 425 | const FVector2f MidpointMinusE = FMath::CubicInterp(P0, P0Tangent, P1, P1Tangent, MidpointAlpha - KINDA_SMALL_NUMBER); 426 | const FVector2f SlopeUnnormalized = MidpointPlusE - MidpointMinusE; 427 | 428 | // Draw the arrow 429 | const FVector2f MidpointDrawPos = Midpoint - MidpointRadius; 430 | const float AngleInRadians = SlopeUnnormalized.IsNearlyZero() ? 0.0f : FMath::Atan2(SlopeUnnormalized.Y, SlopeUnnormalized.X); 431 | 432 | FSlateDrawElement::MakeRotatedBox( 433 | DrawElementsList, 434 | LayerId, 435 | FPaintGeometry(MidpointDrawPos, MidpointImage->ImageSize * ZoomFactor, ZoomFactor), 436 | MidpointImage, 437 | ESlateDrawEffect::None, 438 | AngleInRadians, 439 | TOptional(), 440 | FSlateDrawElement::RelativeToElement, 441 | Params.WireColor 442 | ); 443 | } 444 | } 445 | 446 | } --------------------------------------------------------------------------------