编辑器扩展:插槽编辑器

前言

image-20210819103621100

一个简易的插槽编辑器插件

准备工作

先看编辑器扩展:自定义预览试图, 我们已经完成了插件和预览试图的创建, 本文将进行插槽编辑相关功能的添加和扩展

插槽编辑类

参考UnrealEd模块的SSocketManager类, 我们自己模仿着写一个类出来作为编辑插槽的Slate

为什么不直接用? 首先方便我们自己修改, 另外, 该对象是private. private.. private…, 不过我们还是可以继承ISocketManager接口来省掉一部分工作(不继承也没关系, 自己声明类似的函数即可)

代码较多, 多数是参考SSocketManager类的实现, 不过由于我们没有用到StaticMeshEditor类, 所以于此相关的代码都需要替换成我们自己的代码, 我这里把类似的代码都放到了我们的顶层UI对象里, 即我们的SSHud_SimpleSocket

1
class SSocketEdit : public ISocketManager, public FNotifyHook

同样的, 有两个数据结构也是模仿着抄写一遍, 名字改一下而已, 比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SocketListItem_SimpleSocketEdit
{
public:
SocketListItem_SimpleSocketEdit(UStaticMeshSocket* InSocket)
: Socket(InSocket)
{
}

/** The static mesh socket this represents */
UStaticMeshSocket* Socket;

/** Delegate for when the context menu requests a rename */
DECLARE_DELEGATE(FOnRenameRequested);
FOnRenameRequested OnRenameRequested;
};

最后在顶层UI类里面添加进去这个slate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ SSplitter::Slot()
.Value(0.9f)
[
SNew(SSplitter)
.Orientation(EOrientation::Orient_Horizontal)
+ SSplitter::Slot()
.Value(0.7f)
[
SAssignNew(ViewPort, SEditorViewport_SimpleSocket)
.HUD(SharedThis(this))
.PreviewMesh(EditMesh)
]
+ SSplitter::Slot()
.Value(0.3f)
[
SAssignNew(SocketEditor, SSocketEdit)
.StaticMesh(EditMesh)
.HUD(SharedThis(this))
]
]

用的是SSplitter 而非SHorizontalBox之类的, 毕竟SSplitter可以自由缩放窗口

FEditorViewportClient_SimpleSocket

视图Slate类负责渲染, 那么还需要一个与此对应的类来负责交互和数据管理, 那么就是这个类

1
class FEditorViewportClient_SimpleSocket : public FEditorViewportClient, public TSharedFromThis<FEditorViewportClient_SimpleSocket>

另外有了插槽编辑类, 那么FEditorViewportClient需要与其进行数据交互, 我们通过hud类来作为上层管理来对这两个对象进行数据交互, 比如在FEditorViewportClient中选中了某个插槽, 那么需要同步更新至插槽编辑类中, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void FEditorViewportClient_SimpleSocket::ProcessClick(class FSceneView& View, class HHitProxy* HitProxy, FKey Key, EInputEvent Event, uint32 HitX, uint32 HitY)
{
const bool bCtrlDown = Viewport->KeyState(EKeys::LeftControl) || Viewport->KeyState(EKeys::RightControl);

bool ClearSelectedSockets = true;

if (HitProxy)
{
if (HitProxy->IsA(HSMESocketProxy_SimpleSocket::StaticGetType()))
{
HSMESocketProxy_SimpleSocket* SocketProxy = (HSMESocketProxy_SimpleSocket*)HitProxy;

UStaticMeshSocket* Socket = NULL;

if (SocketProxy->SocketIndex < StaticMesh->Sockets.Num())
{
Socket = StaticMesh->Sockets[SocketProxy->SocketIndex];
}

if (Socket)
{
HudPtr.Pin()->SetSelectedSocket(Socket);
}

ClearSelectedSockets = false;
}

}

if (ClearSelectedSockets && HudPtr.Pin()->GetSelectedSocket())
{
HudPtr.Pin()->SetSelectedSocket(NULL);
}


Invalidate();
}

该函数实现了点击的逻辑, 我们视图中出现的像钻石形状的插槽物体其实是该类绘制出来的一个代理, 可以点击交互, 选中以后我们通知hud我们选中了插槽


另外, 该类有很多功能函数, 比如

1
2
3
4
5
6
7
8
9
//监听按键输入, 可以监听Delete来实现删除插槽等等
virtual bool InputKey(FViewport* Viewport, int32 ControllerId, FKey Key, EInputEvent Event, float AmountDepressed = 1.f, bool bGamepad = false) override;
//可以绘制插槽的名称和移动数据等
virtual void DrawCanvas(FViewport& InViewport, FSceneView& View, FCanvas& Canvas) override;
//绘制移动旋转的UI
virtual bool InputWidgetDelta(FViewport* Viewport, EAxisList::Type CurrentAxis, FVector& Drag, FRotator& Rot, FVector& Scale) override;
//下面两个函数用于检测对插槽代理的控制,如位移旋转等
virtual void TrackingStarted(const struct FInputEventState& InInputState, bool bIsDragging, bool bNudge) override;
virtual void TrackingStopped() override;

还有一堆名字上带WidgetMode的函数, 用于控制切换移动/旋转/缩放的不同操控UI

最后通过void FEditorViewportClient_SimpleSocket::Draw(const FSceneView* View, FPrimitiveDrawInterface* PDI)函数来完成实时绘制插槽代理的任务, 我这里修改了一下代码, 让选中的插槽显示不同的颜色

1
2
3
4
5
6
7
//........
FColor SocketColor = (Socket == HudPtr.Pin()->GetSelectedSocket()? SocketColor_Selected : SocketColor_Default);
FMatrix SocketTM;
Socket->GetSocketMatrix(SocketTM, StaticMeshComponent);
PDI->SetHitProxy(new HSMESocketProxy_SimpleSocket(i));
DrawWireDiamond(PDI, SocketTM, 5.f, SocketColor, SDPG_Foreground);
PDI->SetHitProxy(NULL);

最终效果

录制_2021_08_19_11_20_31_986

后记

  1. 场景内的模型, 包括插槽预览模型都是作为Component添加到场景对象PreviewScene内的
  2. 换模型从hud对象中发起, 分别要通知到插槽编辑类SSocketEdit和视图对象SEditorViewport_SimpleSocket, 然后视图类再刷新场景内的模型组件,再通知到视图管理类FEditorViewportClient_SimpleSocket
  3. StaticMeshEditor的所有命令都是从Editor对象发起, 我这里直接各自去绑定命令和输入了, 比如插槽编辑类右键插槽名字弹出的菜单是自己绑定的命令
  4. 模型的选择可以参考文章编辑器扩展:自定义属性界面, 用的是SObjectPropertyEntryBox
  5. 最近一次编辑过的模型对象保存在一个编辑器subsystem中, 其实也可以用其他方式保存