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

View File

@@ -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<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)
{
Layout = NewObject<URadialMenuLayout>(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<SWidget> URadialMenuWidget::RebuildWidget()
Layout = NewObject<URadialMenuLayout>(this);
}
MySlateWidget = SNew(SRadialMenu, Layout);
SynchronizeProperties();
return MySlateWidget.ToSharedRef();
}

View File

@@ -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<SWidget> 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<UTexture2D> DotTexture;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
float DotRadius = 0.0f;
UPROPERTY()
TObjectPtr<URadialMenuLayout> Layout;
TSharedPtr<SRadialMenu> MySlateWidget;
};