From 1328f6e5f7a1470e1e3beeb0568f5bd9b39b9f46 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 15 May 2026 18:14:38 -0400 Subject: [PATCH] Working on radial menus --- Content/Testing/BP_Test.uasset | 3 - Content/Widgets/WB_Menu.uasset | 4 +- Content/Widgets/white-dot.uasset | 3 + Content/testing/WB_radtest.uasset | 4 +- Content/testing/bp_test.uasset | 3 + Plugins/UEWingman/README.md | 80 ++++++----------- Source/Integration/RadialMenu.cpp | 137 +++++++++++++++++++++++++++++- Source/Integration/RadialMenu.h | 56 +++++++++++- 8 files changed, 225 insertions(+), 65 deletions(-) delete mode 100644 Content/Testing/BP_Test.uasset create mode 100644 Content/Widgets/white-dot.uasset create mode 100644 Content/testing/bp_test.uasset diff --git a/Content/Testing/BP_Test.uasset b/Content/Testing/BP_Test.uasset deleted file mode 100644 index 3a9e6533..00000000 --- a/Content/Testing/BP_Test.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0c3960b7b58e58a3d68a1c0d15bf8503e4ea54e7206c4934ef6375938e6062f8 -size 55626 diff --git a/Content/Widgets/WB_Menu.uasset b/Content/Widgets/WB_Menu.uasset index 155d4898..fca9f410 100644 --- a/Content/Widgets/WB_Menu.uasset +++ b/Content/Widgets/WB_Menu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d75814fe450eaefcc0b3a9f1873dbd2a549420e7e4e63105ea764af8276d217e -size 286810 +oid sha256:445dceda8e05bfdcf89d0f6f9fdf4f3b69c784a1d3172cd33aed96b78bce9299 +size 284391 diff --git a/Content/Widgets/white-dot.uasset b/Content/Widgets/white-dot.uasset new file mode 100644 index 00000000..f7e279a0 --- /dev/null +++ b/Content/Widgets/white-dot.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9fc629beac31fddd37c486e5be147246d890135c8c8de6b8d623f6cd41b29a3 +size 10300 diff --git a/Content/testing/WB_radtest.uasset b/Content/testing/WB_radtest.uasset index 8d413db9..cf2e486a 100644 --- a/Content/testing/WB_radtest.uasset +++ b/Content/testing/WB_radtest.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:028d277767553c79980e7f2d1841133f9c930641a7bdcaa3fbab9e8062977aa0 -size 37399 +oid sha256:537faa7cfd82106dbcc8cfb73cf61cd24cb8a3b007bd7863b9dfa46db77f273e +size 38926 diff --git a/Content/testing/bp_test.uasset b/Content/testing/bp_test.uasset new file mode 100644 index 00000000..0d632d65 --- /dev/null +++ b/Content/testing/bp_test.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:241f4a4bcb109f3d0e3f87ca058a5171e1dc767dbbca4e6e8273b2fa29ecd4d5 +size 52727 diff --git a/Plugins/UEWingman/README.md b/Plugins/UEWingman/README.md index 1a844c34..a5cd740e 100644 --- a/Plugins/UEWingman/README.md +++ b/Plugins/UEWingman/README.md @@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials. ## How Does it Work? -This tool adds a command interpreter plugin to the Unreal +This tool adds a command line interpreter plugin to the Unreal Editor. You can type commands, and the plugin in the editor -will execute them. You can actually type commands directly -from the command-line. Here's an example of what you might -see: +will execute them. ``` - $ ue-wingman.py Graph_Dump Graph=/Game/Testing/BP_Test,graph:EventGraph + $ ue-wingman Graph_Dump /Game/Testing/BP_Test,graph:EventGraph node K2Node_Event_0: Event BeginPlay output-pins OutputDelegate @@ -32,33 +30,30 @@ see: output-pins OutputDelegate, DeltaSeconds ``` -There are tons of commands built in: Graph_Dump, -GraphNode_Add, GraphPin_Connect, -BlueprintComponent_Add, Widget_Add, and so forth. -Using these commands, it's possible to examine and modify -blueprints, widgets, and materials. +The ue-wingman command has tons of subcommands: Graph_Dump, +GraphNode_Add, GraphPin_Connect, BlueprintComponent_Add, +Widget_Add, and so forth. Using these commands, it's +possible to examine and modify blueprints, widgets, and +materials. But, of course, these commands aren't really intended for humans. -They're intended for an AI agent. The AI is given access to these -commands using what's called "Model Context Protocol," which is -a goofy name for "a mechanism that an AI can use to send -commands to other software." +They're intended for an AI agent. -## Why Choose this Particular Unreal Engine MCP? +## Why Choose this Particular Unreal AI Plugin? -There are a *lot* of Unreal Engine MCPs out there. Some of +There are a *lot* of Unreal Engine AI plugins out there. Some of them are, shall we say, not carefully engineered. I'm a reasonably skilled software engineer and I've designed this plugin to be robust and capable of sustained development. -This MCP is also designed to be as broadly general as -possible. I've seen MCPs that claim "can create 22 different +This plugin is also designed to be as broadly general as +possible. I've seen plugins that claim "can create 22 different kinds of graph nodes!" This makes me ask: why not just provide the *entire catalog* of all possible graph nodes? -I've seen MCPs claim "you can edit 15 different material +I've seen plugins claim "you can edit 15 different material expression properties!" Why not provide access to *all* editable material expression properties? I've tried to make -every tool in this MCP as capable as possible, with as few +every tool in this plugin as capable as possible, with as few limits as possible. Some of the MCPs out there expose the entire Unreal API to @@ -76,15 +71,16 @@ commands. ## Installation -There are three parts to UE Wingman: +There are two parts to UE Wingman: * The Unreal Plugin, which does 99% of the work. -* The python program "ue-wingman.py" which a human can - use to send commands to the plugin. - -* The python program "ue-wingman-mcp.py", which an AI - can use to send commands to the plugin. +* The python program "ue-wingman.py" + +The python program is actually less than 100 lines of code: +all it does is package up its command line arguments, send +them to the plugin, and let the plugin do the work. Then it +prints the output. If you build Unreal from source, the best way to install the plugin is to drop the entire UEWingman source folder into @@ -106,25 +102,6 @@ and no other dependencies. To install the human version, ue-wingman.py, just drop it into a folder on your PATH. -To install the AI version, ue-wingman-mcp.py, you have to -usually set up some config file for your AI agent. I use -Claude Code, for that, you have to create a file ".mcp.json" -in your project folder, and it needs to have this inside it: - -``` -{ - "mcpServers": { - "ue-wingman": { - "command": "python3", - "args": ["Plugins/UEWingman/ue-wingman-mcp.py"] - } - } -} -``` - -You can usually ask your AI agent for help creating this -config file. - ## The "User Manual" You might be interested in seeing the "user manual" for the @@ -135,18 +112,17 @@ $ ue-wingman.py Documentation_Manual ``` Of course, you're not the intended user: your AI agent is. -When the AI agent starts up ue-wingman-mcp, it is -automatically told to read the user manual. From there, the -User Manual says, among other things, that the AI agent can -get a listing of built-in commands. You can see that too: +You should put a note into your agent's system prompt to +let it know about the ue-wingman.py command, and to let +it know that it can type ue-wingman.py Documentation_Manual. +This in turn will tell your agent about this command: ``` $ ue-wingman.py Documentation_Commands ``` -With these two commands at your disposal, you'll have a better -understanding of what exactly your AI agent is doing with this -plugin, and how it all works. +Using these commands, you can learn more about what this +plugin can do. ## Fun things to Try diff --git a/Source/Integration/RadialMenu.cpp b/Source/Integration/RadialMenu.cpp index ab7a2193..ed98f4af 100644 --- a/Source/Integration/RadialMenu.cpp +++ b/Source/Integration/RadialMenu.cpp @@ -1,5 +1,7 @@ #include "RadialMenu.h" #include "Rendering/DrawElements.h" +#include "Engine/Texture2D.h" +#include "Styling/SlateBrush.h" #include "Widgets/SLeafWidget.h" @@ -122,6 +124,31 @@ public: Invalidate(EInvalidateWidgetReason::Layout); } + void SetLineThickness(float NewLineThickness) + { + LineThickness = NewLineThickness; + Invalidate(EInvalidateWidgetReason::Paint); + } + + void SetLineColor(FLinearColor NewLineColor) + { + LineColor = NewLineColor; + Invalidate(EInvalidateWidgetReason::Paint); + } + + void SetDotTexture(UTexture2D* NewDotTexture) + { + DotTexture = NewDotTexture; + DotBrush.SetResourceObject(NewDotTexture); + Invalidate(EInvalidateWidgetReason::Paint); + } + + void SetDotRadius(float NewDotRadius) + { + DotRadius = NewDotRadius; + Invalidate(EInvalidateWidgetReason::Paint); + } + protected: virtual FVector2D ComputeDesiredSize(float) const override { @@ -151,7 +178,6 @@ protected: if (!Layout.IsValid()) return LayerId; const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5; - const FLinearColor Color = InWidgetStyle.GetColorAndOpacityTint(); const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry(); for (const FRadialMenuItem &Item : Layout->GetItems()) @@ -160,25 +186,127 @@ protected: Points.Add(Center + Item.Point1); Points.Add(Center + Item.Point2); Points.Add(Center + Item.Point3); - FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, 1.0f); + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, LineColor, true, LineThickness); + } + + if (DotRadius <= 0.0f) return LayerId; + if (!DotTexture.IsValid()) return LayerId; + + const FVector2D DotSize(DotRadius * 2.0f); + for (const FRadialMenuItem &Item : Layout->GetItems()) + { + for (const FVector2D& Point : { Item.Point1, Item.Point3 }) + { + const FVector2D LocalPoint = Center + Point; + FSlateDrawElement::MakeBox( + OutDrawElements, + LayerId, + AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(DotRadius))), + &DotBrush, + ESlateDrawEffect::None, + LineColor); + } } return LayerId; } private: TWeakObjectPtr Layout; + float LineThickness = 1.0f; + FLinearColor LineColor = FLinearColor::White; + TWeakObjectPtr DotTexture; + FSlateBrush DotBrush; + float DotRadius = 0.0f; }; -void URadialMenuWidget::Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread) +void URadialMenuWidget::SetNumItems(int32 NewNumItems) { + if (NumItems == NewNumItems) return; + + NumItems = NewNumItems; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetItemHeight(float NewItemHeight) +{ + if (ItemHeight == NewItemHeight) return; + + ItemHeight = NewItemHeight; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetInnerRadius(float NewInnerRadius) +{ + if (InnerRadius == NewInnerRadius) return; + + InnerRadius = NewInnerRadius; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetMinSpoke(float NewMinSpoke) +{ + if (MinSpoke == NewMinSpoke) return; + + MinSpoke = NewMinSpoke; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetSpread(float NewSpread) +{ + if (Spread == NewSpread) return; + + Spread = NewSpread; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetLineThickness(float NewLineThickness) +{ + if (LineThickness == NewLineThickness) return; + + LineThickness = NewLineThickness; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor) +{ + if (LineColor == NewLineColor) return; + + LineColor = NewLineColor; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture) +{ + if (DotTexture == NewDotTexture) return; + + DotTexture = NewDotTexture; + SynchronizeProperties(); +} + +void URadialMenuWidget::SetDotRadius(float NewDotRadius) +{ + if (DotRadius == NewDotRadius) return; + + DotRadius = NewDotRadius; + SynchronizeProperties(); +} + +void URadialMenuWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + if (!Layout) { Layout = NewObject(this); } - Layout->Configure(NItems, ItemHeight, InnerRadius, MinSpoke, Spread); + Layout->Configure(NumItems, ItemHeight, InnerRadius, MinSpoke, Spread); if (MySlateWidget.IsValid()) { + MySlateWidget->SetLineThickness(LineThickness); + MySlateWidget->SetLineColor(LineColor); + MySlateWidget->SetDotTexture(DotTexture); + MySlateWidget->SetDotRadius(DotRadius); MySlateWidget->Refresh(); } } @@ -190,6 +318,7 @@ TSharedRef URadialMenuWidget::RebuildWidget() Layout = NewObject(this); } MySlateWidget = SNew(SRadialMenu, Layout); + SynchronizeProperties(); return MySlateWidget.ToSharedRef(); } diff --git a/Source/Integration/RadialMenu.h b/Source/Integration/RadialMenu.h index 98deb6c7..0969d244 100644 --- a/Source/Integration/RadialMenu.h +++ b/Source/Integration/RadialMenu.h @@ -10,6 +10,7 @@ #include "RadialMenu.generated.h" class SRadialMenu; +class UTexture2D; USTRUCT(BlueprintType) @@ -86,7 +87,31 @@ class URadialMenuWidget : public UWidget public: UFUNCTION(BlueprintCallable) - void Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread); + void SetNumItems(int32 NewNumItems); + + UFUNCTION(BlueprintCallable) + void SetItemHeight(float NewItemHeight); + + UFUNCTION(BlueprintCallable) + void SetInnerRadius(float NewInnerRadius); + + UFUNCTION(BlueprintCallable) + void SetMinSpoke(float NewMinSpoke); + + UFUNCTION(BlueprintCallable) + void SetSpread(float NewSpread); + + UFUNCTION(BlueprintCallable) + void SetLineThickness(float NewLineThickness); + + UFUNCTION(BlueprintCallable) + void SetLineColor(FLinearColor NewLineColor); + + UFUNCTION(BlueprintCallable) + void SetDotTexture(UTexture2D* NewDotTexture); + + UFUNCTION(BlueprintCallable) + void SetDotRadius(float NewDotRadius); UFUNCTION(BlueprintCallable) URadialMenuLayout* GetLayout() const { return Layout; } @@ -94,11 +119,38 @@ public: protected: virtual TSharedRef RebuildWidget() override; virtual void ReleaseSlateResources(bool bReleaseChildren) override; + virtual void SynchronizeProperties() override; private: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(ClampMin="0", AllowPrivateAccess="true")) + int32 NumItems = 8; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float ItemHeight = 20.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float InnerRadius = 20.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float MinSpoke = 20.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float Spread = 20.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float LineThickness = 2.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + FLinearColor LineColor = FLinearColor::White; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + TObjectPtr DotTexture; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true")) + float DotRadius = 0.0f; + UPROPERTY() TObjectPtr Layout; TSharedPtr MySlateWidget; }; -