Working on radial menus
This commit is contained in:
BIN
Content/Testing/BP_Test.uasset
LFS
BIN
Content/Testing/BP_Test.uasset
LFS
Binary file not shown.
BIN
Content/Widgets/WB_Menu.uasset
LFS
BIN
Content/Widgets/WB_Menu.uasset
LFS
Binary file not shown.
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
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
BIN
Content/testing/bp_test.uasset
LFS
Normal file
Binary file not shown.
@@ -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 "ue-wingman-mcp.py", which an AI
|
The python program is actually less than 100 lines of code:
|
||||||
can use to send commands to the plugin.
|
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
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user