Working on radial menus

This commit is contained in:
2026-05-15 18:14:38 -04:00
parent 5d2377df1d
commit 1328f6e5f7
8 changed files with 225 additions and 65 deletions

Binary file not shown.

Binary file not shown.

BIN
Content/Widgets/white-dot.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
Content/testing/bp_test.uasset LFS Normal file

Binary file not shown.

View File

@@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials.
## How Does it Work? ## 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 Editor. You can type commands, and the plugin in the editor
will execute them. You can actually type commands directly will execute them.
from the command-line. Here's an example of what you might
see:
``` ```
$ 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 node K2Node_Event_0: Event BeginPlay
output-pins OutputDelegate output-pins OutputDelegate
@@ -32,33 +30,30 @@ see:
output-pins OutputDelegate, DeltaSeconds output-pins OutputDelegate, DeltaSeconds
``` ```
There are tons of commands built in: Graph_Dump, The ue-wingman command has tons of subcommands: Graph_Dump,
GraphNode_Add, GraphPin_Connect, GraphNode_Add, GraphPin_Connect, BlueprintComponent_Add,
BlueprintComponent_Add, Widget_Add, and so forth. Widget_Add, and so forth. Using these commands, it's
Using these commands, it's possible to examine and modify possible to examine and modify blueprints, widgets, and
blueprints, widgets, and materials. materials.
But, of course, these commands aren't really intended for humans. 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 They're intended for an AI agent.
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."
## 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 them are, shall we say, not carefully engineered. I'm a
reasonably skilled software engineer and I've designed reasonably skilled software engineer and I've designed
this plugin to be robust and capable of sustained development. this plugin to be robust and capable of sustained development.
This MCP is also designed to be as broadly general as This plugin is also designed to be as broadly general as
possible. I've seen MCPs that claim "can create 22 different possible. I've seen plugins that claim "can create 22 different
kinds of graph nodes!" This makes me ask: why not just kinds of graph nodes!" This makes me ask: why not just
provide the *entire catalog* of all possible graph nodes? 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* expression properties!" Why not provide access to *all*
editable material expression properties? I've tried to make 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. limits as possible.
Some of the MCPs out there expose the entire Unreal API to Some of the MCPs out there expose the entire Unreal API to
@@ -76,15 +71,16 @@ commands.
## Installation ## 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 Unreal Plugin, which does 99% of the work.
* The python program "ue-wingman.py" which a human can * The python program "ue-wingman.py"
use to send commands to the plugin.
The python program is actually less than 100 lines of code:
* The python program "ue-wingman-mcp.py", which an AI all it does is package up its command line arguments, send
can use to send commands to the plugin. 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 If you build Unreal from source, the best way to install the
plugin is to drop the entire UEWingman source folder into 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 To install the human version, ue-wingman.py, just drop it into
a folder on your PATH. 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" ## The "User Manual"
You might be interested in seeing the "user manual" for the 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. Of course, you're not the intended user: your AI agent is.
When the AI agent starts up ue-wingman-mcp, it is You should put a note into your agent's system prompt to
automatically told to read the user manual. From there, the let it know about the ue-wingman.py command, and to let
User Manual says, among other things, that the AI agent can it know that it can type ue-wingman.py Documentation_Manual.
get a listing of built-in commands. You can see that too: This in turn will tell your agent about this command:
``` ```
$ ue-wingman.py Documentation_Commands $ ue-wingman.py Documentation_Commands
``` ```
With these two commands at your disposal, you'll have a better Using these commands, you can learn more about what this
understanding of what exactly your AI agent is doing with this plugin can do.
plugin, and how it all works.
## Fun things to Try ## Fun things to Try

View File

@@ -1,5 +1,7 @@
#include "RadialMenu.h" #include "RadialMenu.h"
#include "Rendering/DrawElements.h" #include "Rendering/DrawElements.h"
#include "Engine/Texture2D.h"
#include "Styling/SlateBrush.h"
#include "Widgets/SLeafWidget.h" #include "Widgets/SLeafWidget.h"
@@ -122,6 +124,31 @@ public:
Invalidate(EInvalidateWidgetReason::Layout); 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: protected:
virtual FVector2D ComputeDesiredSize(float) const override virtual FVector2D ComputeDesiredSize(float) const override
{ {
@@ -151,7 +178,6 @@ protected:
if (!Layout.IsValid()) return LayerId; if (!Layout.IsValid()) return LayerId;
const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5; const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5;
const FLinearColor Color = InWidgetStyle.GetColorAndOpacityTint();
const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry(); const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry();
for (const FRadialMenuItem &Item : Layout->GetItems()) for (const FRadialMenuItem &Item : Layout->GetItems())
@@ -160,25 +186,127 @@ protected:
Points.Add(Center + Item.Point1); Points.Add(Center + Item.Point1);
Points.Add(Center + Item.Point2); Points.Add(Center + Item.Point2);
Points.Add(Center + Item.Point3); 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; return LayerId;
} }
private: private:
TWeakObjectPtr<URadialMenuLayout> Layout; TWeakObjectPtr<URadialMenuLayout> Layout;
float LineThickness = 1.0f;
FLinearColor LineColor = FLinearColor::White;
TWeakObjectPtr<UTexture2D> 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) if (!Layout)
{ {
Layout = NewObject<URadialMenuLayout>(this); Layout = NewObject<URadialMenuLayout>(this);
} }
Layout->Configure(NItems, ItemHeight, InnerRadius, MinSpoke, Spread); Layout->Configure(NumItems, ItemHeight, InnerRadius, MinSpoke, Spread);
if (MySlateWidget.IsValid()) if (MySlateWidget.IsValid())
{ {
MySlateWidget->SetLineThickness(LineThickness);
MySlateWidget->SetLineColor(LineColor);
MySlateWidget->SetDotTexture(DotTexture);
MySlateWidget->SetDotRadius(DotRadius);
MySlateWidget->Refresh(); MySlateWidget->Refresh();
} }
} }
@@ -190,6 +318,7 @@ TSharedRef<SWidget> URadialMenuWidget::RebuildWidget()
Layout = NewObject<URadialMenuLayout>(this); Layout = NewObject<URadialMenuLayout>(this);
} }
MySlateWidget = SNew(SRadialMenu, Layout); MySlateWidget = SNew(SRadialMenu, Layout);
SynchronizeProperties();
return MySlateWidget.ToSharedRef(); return MySlateWidget.ToSharedRef();
} }

View File

@@ -10,6 +10,7 @@
#include "RadialMenu.generated.h" #include "RadialMenu.generated.h"
class SRadialMenu; class SRadialMenu;
class UTexture2D;
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
@@ -86,7 +87,31 @@ class URadialMenuWidget : public UWidget
public: public:
UFUNCTION(BlueprintCallable) 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) UFUNCTION(BlueprintCallable)
URadialMenuLayout* GetLayout() const { return Layout; } URadialMenuLayout* GetLayout() const { return Layout; }
@@ -94,11 +119,38 @@ public:
protected: protected:
virtual TSharedRef<SWidget> RebuildWidget() override; virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void ReleaseSlateResources(bool bReleaseChildren) override; virtual void ReleaseSlateResources(bool bReleaseChildren) override;
virtual void SynchronizeProperties() override;
private: 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<UTexture2D> DotTexture;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
float DotRadius = 0.0f;
UPROPERTY() UPROPERTY()
TObjectPtr<URadialMenuLayout> Layout; TObjectPtr<URadialMenuLayout> Layout;
TSharedPtr<SRadialMenu> MySlateWidget; TSharedPtr<SRadialMenu> MySlateWidget;
}; };