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?
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user