我们将在UE5编辑器插件中实现以下功能:
1. 在关卡工具栏添加按钮
2. 点击按钮弹出Slate窗口
3. 窗口内嵌小地图,支持放大/缩小/移动
4. 小地图用圆点标注指定物品位置
5. 支持鼠标框选圆点并统计数量
6. 点击圆点可查看物品信息
实现方案如下:
mermaid
graph TDA[插件模块] --> B[工具栏按钮]A --> C[Slate窗口]
C --> D[小地图控件]
D --> E[地图导航 缩放/平移]
D --> F[物品位置映射]
D --> G[圆点绘制]
D --> H[框选交互]
D --> I[点击交互]
C --> J[信息面板]
步骤1:创建插件基础
1. 创建"EditorMapViewer"插件(选择编辑器工具栏按钮模板)
2. 目录结构:
3. EditorMapViewer/
├── Resources/
├── Source/
│ ├── EditorMapViewer/
│ │ ├── Private/
│ │ │ ├── EditorMapViewerCommands.cpp
│ │ │ ├── EditorMapViewerModule.cpp
│ │ │ ├── SMapViewerWindow.cpp
│ │ │ ├── SMapViewport.cpp
│ │ ├── Public/
│ │ │ ├── EditorMapViewerCommands.h
│ │ │ ├── EditorMapViewerModule.h
│ │ │ ├── SMapViewerWindow.h
│ │ │ ├── SMapViewport.h
│ ├── EditorMapViewer.Build.cs
步骤2:实现小地图视口控件(核心)
SMapViewport.h:
C++
#pragma once#include "CoreMinimal.h"#include "Widgets/SCompoundWidget.h"#include "Engine/World.h"class SMapViewport : public SCompoundWidget
{
public:SLATE_BEGIN_ARGS(SMapViewport) {}SLATE_ARGUMENT(TWeakObjectPtr<UWorld>, World)SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
// 鼠标交互处理virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
private:// 绘制函数virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
// 坐标转换:世界坐标 -> 小地图坐标FVector2D WorldToMap(const FVector& WorldPos) const;
// 获取当前关卡中所有指定物品void RefreshItemData();
// 地图导航void PanMap(const FVector2D& PanAmount);void ZoomMap(float ZoomDelta, const FVector2D& MousePosition);
TWeakObjectPtr<UWorld> World;
TArray<TPair<FVector, FString>> Items; // 物品位置和信息
FVector2D MapPanOffset = FVector2D::ZeroVector;float MapScale = 1.0f;
FVector2D SelectionStart;
FVector2D SelectionEnd;bool bIsPanning = false;bool bIsSelecting = false;
};
SMapViewport.cpp:
C++
#include "SMapViewport.h"#include "Editor.h"#include "EngineUtils.h"#include "SlateOptMacros.h"#include "Widgets/Layout/SScrollBox.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SMapViewport::Construct(const FArguments& InArgs)
{
World = InArgs._World;RefreshItemData();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
FVector2D SMapViewport::WorldToMap(const FVector& WorldPos) const
{// 简单投影:忽略Y轴(根据需求调整)return FVector2D(WorldPos.X, WorldPos.Z) * MapScale + MapPanOffset;
}
void SMapViewport::RefreshItemData()
{
Items.Empty();if (!World.IsValid()) return;
// 示例:获取所有PointLight(替换为你的物品类)for (TActorIterator<APointLight> It(World.Get()); It; ++It)
{if (*It)
{
Items.Add(TPair<FVector, FString>(It->GetActorLocation(), It->GetActorLabel()));
}
}
}
FReply SMapViewport::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
{// 检查是否点击到物品const FVector2D LocalPos = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());for (const auto& Item : Items)
{const FVector2D ItemPos = WorldToMap(Item.Key);if (FVector2D::Distance(ItemPos, LocalPos) < 10.0f)
{// 点击物品事件(可扩展为显示信息)FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Item.Value));return FReply::Handled();
}
}
// 开始框选
SelectionStart = LocalPos;
SelectionEnd = LocalPos;
bIsSelecting = true;return FReply::Handled().CaptureMouse(AsShared());
}else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
{// 开始平移
bIsPanning = true;return FReply::Handled().CaptureMouse(AsShared());
}return FReply::Unhandled();
}
FReply SMapViewport::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{if (bIsPanning)
{PanMap(MouseEvent.GetCursorDelta());return FReply::Handled();
}else if (bIsSelecting)
{
SelectionEnd = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());return FReply::Handled();
}return FReply::Unhandled();
}
FReply SMapViewport::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{if ((MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && bIsSelecting) ||
(MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && bIsPanning))
{
bIsPanning = false;
bIsSelecting = false;
// 框选统计if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
{
FBox2D SelectionBox(SelectionStart, SelectionEnd);
int32 SelectedCount = 0;
for (const auto& Item : Items)
{if (SelectionBox.IsInside(WorldToMap(Item.Key)))
{
SelectedCount++;
}
}
// 显示统计结果
FNotificationInfo Info(FText::FromString(FString::Printf(TEXT("选中物品数量: %d"), SelectedCount)));FSlateNotificationManager::Get().AddNotification(Info);
}
return FReply::Handled().ReleaseMouseCapture();
}return FReply::Unhandled();
}
FReply SMapViewport::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{ZoomMap(MouseEvent.GetWheelDelta(), MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()));return FReply::Handled();
}
void SMapViewport::PanMap(const FVector2D& PanAmount)
{
MapPanOffset += PanAmount;
}
void SMapViewport::ZoomMap(float ZoomDelta, const FVector2D& MousePosition)
{const float OldScale = MapScale;
MapScale = FMath::Clamp(MapScale + ZoomDelta * 0.1f, 0.1f, 5.0f);
// 以鼠标为中心缩放
MapPanOffset = MousePosition - (MousePosition - MapPanOffset) * (MapScale / OldScale);
}
int32 SMapViewport::OnPaint(...) const
{// 1. 绘制背景FSlateDrawElement::MakeBox(...);
// 2. 绘制物品圆点for (const auto& Item : Items)
{const FVector2D Pos = WorldToMap(Item.Key);FSlateDrawElement::MakeCircle(
OutDrawElements,
LayerId++,
AllottedGeometry.ToPaintGeometry(Pos - FVector2D(5,5), FVector2D(10,10)),FLinearColor::Red
);
}
// 3. 绘制框选矩形if (bIsSelecting)
{FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId++,
AllottedGeometry.ToPaintGeometry(
SelectionStart,
SelectionEnd - SelectionStart
),FLinearColor::Green
);
}
return LayerId;
}
步骤3:实现主窗口
SMapViewerWindow.h:
C++
#pragma once#include "CoreMinimal.h"#include "Widgets/SCompoundWidget.h"class SMapViewport;
class SMapViewerWindow : public SCompoundWidget
{
public:SLATE_BEGIN_ARGS(SMapViewerWindow) {}SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
TSharedPtr<SMapViewport> MapViewport;
TSharedPtr<STextBlock> InfoText;
};
SMapViewerWindow.cpp:
C++
#include "SMapViewerWindow.h"#include "SMapViewport.h"#include "Editor.h"BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SMapViewerWindow::Construct(const FArguments& InArgs){
ChildSlot
[SNew(SVerticalBox)
+SVerticalBox::Slot().FillHeight(1.0f)
[SAssignNew(MapViewport, SMapViewport)
.World(GEditor->GetEditorWorldContext().World())
]
+SVerticalBox::Slot().AutoHeight().Padding(5)
[SAssignNew(InfoText, STextBlock)
.Text(LOCTEXT("DefaultHint", "左键框选物品 | 右键拖动 | 滚轮缩放"))
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
步骤4:集成到插件模块
EditorMapViewerModule.cpp:
C++
void FEditorMapViewerModule::OnOpenMapViewer(){
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(LOCTEXT("WindowTitle", "关卡物品地图查看器"))
.ClientSize(FVector2D(800, 600));
Window->SetContent(SNew(SMapViewerWindow));
// 添加到编辑器窗口
FSlateApplication::Get().AddWindow(Window);
}
步骤5:配置构建文件
EditorMapViewer.Build.cs:
C#
PrivateDependencyModuleNames.AddRange(new string[] {"CoreUObject","Engine","Slate","SlateCore","EditorStyle","UnrealEd","LevelEditor","InputCore","RenderCore", // 新增"RHI", // 新增"ApplicationCore" // 新增
});
要求完善OnPaint函数,不能简写,所有函数参数都要写出来,小圆点的功能也要写完整,点击更行按钮后要更行绘制小圆点