俗话说的好,一流程序写架构,三流程序写UI。可是在游戏开发过程中,特别是引擎和工具链开发的时候,UI是绕不过去的坑,UE4现在是各大厂越来越流行了,各种工具层出不穷,可是和unity相比,Slate UI做编辑器扩展和插件的时候,难度不是大了一个level,最为关键的是,UE4的编辑器埋藏了无数的暗坑,只有写的时候自己体会,所以在这记录下遇到的坑爹问题。
先说Slate框架,知乎上已经有大神做过分析,基本上Slate就是一套自创的从DX或者OpenGL写起的UI框架,和在UE4里用UMG做游戏UI一样,Slate除了底层的渲染功能实现之外,定义了一套自己的语法-目的是定义UI中的层级结构和布局-也就是Slot。理论上我们的任何一个编辑器扩展功能都可以纯用Slate写完。但是稍微看过一点UE4代码的肯定都知道这是一个巨大且繁琐的工程,特别是VS还不支持Slate的诡异语法。所以UE4自己也造了很多的轮子去封装很多的UI工作,比如加个按钮,加编辑器属性等。
那么问题就来了,这些UE4自己造的轮子,我们怎么能快速学习上手,并且为我所用呢,其实就是一个字:“抄”,在开发过程中,各种官方的插件 和UnrealEd这个模块本身,是我们最好的参考。配合UE4自带的WidgetReflector工具,我们能很快定位各个UI组件的入口,从而方便的“抄”代码,为我所用。
当然,以上这些方法论不是本文的重点,接下来还是具体的讲一讲编辑器扩展这里面的实际内容。我会假设你对UE4基本的插件制作和编译已经驾轻就熟。
1.FExtender
编辑器扩展最常见的功能就是加个按钮啦,在UE4的编辑器布局里,我们在下拉菜单和工具条加按钮和条目是很方便的,直接调用Extender即可
UE4编辑器里的菜单栏,工具条,还有编辑器里的菜单,都有相应的Extender类,例如FMenuExtender
,添加按钮或者菜单条目,我们需要指定下面四个东西:ExtensionPoint
一般来说这个是UE4编辑器规定好的,例如Settings
就是加在设置那一栏菜单,比较常见的还有WindowLayOut
,EditMain
HookPosition
其实就是EExtensionHook
这个enum
UICommandList
Commandlist就是你的UI要执行的函数,下面的代码:
FXXCommands::Register();
PluginCommands = MakeShareable(new FUICommandList);
PluginCommands->MapAction(FXXCommands::Get().PluginAction2,
FExecuteAction::CreateRaw(this, &UIDelegateFunctionName),
FCanExecuteAction());
就是一段最简单的创建Commandlist的代码,其中Delegate是UE4自己定义的委托,根据函数指针的类型有CreateRaw
CreateSP
等方法可以去调用。
我们指定了这几个元素就可以调用extender直接修改UE4 Editor了,比如下面这段代码:
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender());
MenuExtender->AddMenuExtension("EditMain", EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw(this, &AddMenuCommands));
就是给Edit菜单添加一个可点击条目。
2.DetailCustomization
只要读过UE4 C++文档的就会知道C++里的UPROPERTY宏,可以随时方便的显示自定义的类的属性,修改等。实际上每种自定义属性的UI,在UE4里都有相对应的实现,下面这张图可以明确看出对于每种UPROPERTY类型UE4都实现了一个UI:
UE4所有的PROPERYTY宏能够发挥作用,其实都来自于一个叫IDetailView的class,具体原理来说也很简单,也就是parse这个UObject中的所有UPROPERTY的类型,依次生成相应的slate对象。IDetailView可以用来做很多事情,特别是对于数值展示修改等等,我们在任一个slate节点中插入IDetailView的对象,UEEditor就会自动生成相应的数值面板界面:
TSharedPtr<IDetailsView> myDetailView;
myDetailView = EditModule.CreateDetailView(DetailsViewArgs);
myDetailView->SetObject(myUObject);
然后在slate中:
+ SVerticalBox::Slot()
.AutoHeight()
[
DetailView->AsShared()
]
可以说是很有用的功能,在实际的扩展中,除了应用DetailView,有时候还需要对做DetailCustomization,一种是对detailview的customization,比如修改某个actor的界面,添加按钮等等, 另一种是PropertyTypeCustomization
这两种Customization的方式都是通过类继承来实现,分别是IDetailCustomization
和 IPropertyTypeCustomization
例如我们需要定义某个actor的信息显示面板,我们需要一个:
class FXActorDetail :public IDetailCustomization
然后在CustomDetail函数里写上我们自己的slate代码,比如给actor添加一个按钮
void FXActorDetail::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
DetailLayout.EditCategory((CategoryName))
.AddCustomRow((NewRowFilterString))
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text((TextLeftToButton))
]
.ValueContent()
.MaxDesiredWidth(125.f)
.MinDesiredWidth(125.f)
[
SNew(SButton)
.ContentPadding(2)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
.OnClicked((ObjectPtr), (FunctionPtr))
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text((ButtonText))
]
];
}
对PropertyType的customization稍微有些不一样的地方, PropertyType依赖于IDetailPropertyRow
和FDetailWidgetRow
这两个类,我们要做的是新建出自己的widgetrow类来表示自己的属性,同时用slate代码自定义他们的样式,参考UE4表示component 移动属性的代码:
IDetailPropertyRow& MobilityRow = Category.AddProperty(MobilityHandle);
MobilityRow.CustomWidget()
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("Mobility", "Mobility"))
.ToolTipText(this, &FMobilityCustomization::GetMobilityToolTip)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
.MaxDesiredWidth(0)
[
SAssignNew(ButtonOptionsPanel, SUniformGridPanel)
];
3.EditMode扩展
除了简单的按钮,属性显示,UE4编辑器还有一个很强大的功能就是EdMode扩展,这个功能允许自定义编辑器的模式,从而实现除了标准的游戏编辑器之外的各种功能,比如地形编辑,笔刷等等,Edmode允许你自定义物体的渲染隐藏 显示 笔刷等等。
添加一个EdMode到UnrealEditor,一般这段代码会写在你的插件的StartUpModule
函数里:
FMyEdMode:Public FEdMode
FEditorModeRegistry::Get().RegisterMode<FMyEdMode:Public>(FMyEdMode:Public::EM_MyEdModeId, LOCTEXT("EdModeName", ""), FSlateIcon(FMyEdModeStyle::Get()->GetStyleSetName(), "Plugins.Tab"), true);
每个FEdMode有一个EdModeToolkit,一般定义自己的EdMode的时候,我们也会自定义customtoolkit,toolkit hold了 所有你的EdModeTool,比如你的EdModeTool是一个slate类,你可以在这里实现你的特殊的操作模式的UI等等,然后在EdMode中gettookkit去使用。
FEdMode类中可以implement各种各样的和编辑有关的功能。如图
Enter/Exit
退出和进入你的编辑模式的行为,一般是初始化Toolkit和隐藏 显示物体等代码。
例如:
FEdMode::Enter();
ToggleVisibility(true);
if (!Toolkit.IsValid())
{
Toolkit = MakeShareable(new FMyEdModeToolkit);
Toolkit->Init(Owner->GetToolkitHost());
}
Selection
在EdMode中很常见的一点就是重载selection功能,UE4允许你自定义可以选中的物体,只要重载EdMode的IsSelectionAllowed
就可以,例如只允许选中StaticMesh:
bool FMyEdMode::IsSelectionAllowed(AActor* InActor, bool bInSelection) const
{
if (InActor->IsA(AStaticMeshActor::StaticClass()))
{
return true;
}
else
{
return false;
}
}
但是实际上这里存在的坑就是UE4的selection和deselection函数都会根据这个函数的返回值判断,也就是说如果你的actor在编辑过程中在某个EdMode下被选中而同时你切换到另一个不允许选中的EdMode,你就再也没法取消选中这个物体了。
这里的解决方法是你可以自己写个DeSelect的方法(抄一遍UnrealEd)在enter的时候调用一下就好了
void FLAEEdMode::DeselectAll()
{
// Make a list of selected actors . . .
TArray<AActor*> ActorsToDeselect;
for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
{
AActor* Actor = static_cast<AActor*>(*It);
checkSlow(Actor->IsA(AActor::StaticClass()));
ActorsToDeselect.Add(Actor);
}
for (int32 ActorIndex = 0; ActorIndex < ActorsToDeselect.Num(); ++ActorIndex)
{
AActor* Actor = ActorsToDeselect[ActorIndex];
if (UActorGroupingUtils::IsGroupingActive())
{
// if this actor is a group, do a group select/deselect
AGroupActor* SelectedGroupActor = Cast<AGroupActor>(Actor);
if (SelectedGroupActor)
{
GEditor->SelectGroup(SelectedGroupActor, true, false, false);
}
else
{
// Select/Deselect this actor's entire group, starting from the top locked group.
// If none is found, just use the actor.
AGroupActor* ActorLockedRootGroup = AGroupActor::GetRootForActor(Actor, true);
if (ActorLockedRootGroup)
{
GEditor->SelectGroup(ActorLockedRootGroup, false, false, false);
}
}
}
// Don't do any work if the actor's selection state is already the selected state.
const bool bActorSelected = Actor->IsSelected();
if (bActorSelected)
{
GEditor->GetSelectedActors()->Select(Actor, false);
{
if (GEditor->GetSelectedComponentCount() > 0)
{
GEditor->GetSelectedComponents()->Modify();
}
GEditor->GetSelectedComponents()->BeginBatchSelectOperation();
for (UActorComponent* Component : Actor->GetComponents())
{
if (Component)
{
GEditor->GetSelectedComponents()->Deselect(Component);
// Remove the selection override delegates from the deselected components
if (USceneComponent* SceneComponent = Cast<USceneComponent>(Component))
{
FComponentEditorUtils::BindComponentSelectionOverride(SceneComponent, false);
}
}
}
GEditor->GetSelectedComponents()->EndBatchSelectOperation(false);
}
}
SetActorSelectionFlags(Actor);
}
}
自定义EdMode面板
EdMode面板的制定没有Toolbar和DetailView那么方便,一般是需要用slate代码去写。首先是定义EdMode的图标:
建一个FMyEdModeStyle
的类,这个类的主要目的是定义路标,字体等样式数据,在Slate中叫SlateImageBrush:
#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( FMyEdModeStyle::InContent( RelativePath, ".png" ), __VA_ARGS__ )
我们需要一个StyleSet在这个类里:
TSharedPtr< FSlateStyleSet > FLAEEdModeStyle::StyleSet = NULL;
void FLAEEdModeStyle::Initialize()
{
// Const icon sizes
const FVector2D Icon8x8(8.0f, 8.0f);
const FVector2D Icon267x140(170.0f, 50.0f);
// Only register once
if (StyleSet.IsValid())
{
return;
}
StyleSet = MakeShareable(new FSlateStyleSet("FMyEdMode"));
StyleSet->SetCoreContentRoot(FPaths::EngineContentDir() / TEXT("Slate"));
const FTextBlockStyle NormalText = FEditorStyle::GetWidgetStyle<FTextBlockStyle>("NormalText");
StyleSet->Set("Plugins.Tab", new IMAGE_BRUSH("icon_40x", Icon40x40));
StyleSet->Set("Plugins.Mode.Edit", new IMAGE_BRUSH("mode_edit", Icon40x40));
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
}
然后初始化这个StyleSet,注意对于EdMode的图标,把Icon注册在Plugins.Mode.Edit即可。最后在插件的StartUpModule
里调用Initialize。
接下来是具体面板上的按钮,图标等,一般的做法是在Tookkit成员里新建一个Slate的类:也就是一个SCompoundWidget的子类:
class SLAEEdModeTools :public SCompoundWidget
我们可以把所有的ui代码写在这个类的Construct
函数里,在EdMode中,我们可以这样得到我们的UI Slate类:
auto tools = Toolkit->GetInlineContent().Get();
在构建UI时,如果我们需要得到当前的EdMode数据:
auto MyMode = GLevelEditorModeTools().GetActiveMode(FMyEdMode::EM_MyEdModeId);
具体的UI构建就可以根据需求来实现,比如UE4默认的摆放模式的代码:
for (const FPlacementCategoryInfo& Category : Categories)
{
Tabs->AddSlot()
.AutoHeight()
[
CreatePlacementGroupTab(Category)
];
}
就是根据当前所有能摆放的actor种类构建一个tab.
未完待续:自定义asset,自动LD
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。