内容简介:俗话说的好,一流程序写架构,三流程序写UI。可是在游戏开发过程中,特别是引擎和工具链开发的时候,UI是绕不过去的坑,UE4现在是各大厂越来越流行了,各种工具层出不穷,可是和unity相比,Slate UI做编辑器扩展和插件的时候,难度不是大了一个level,最为关键的是,UE4的编辑器埋藏了无数的暗坑,只有写的时候自己体会,所以在这记录下遇到的坑爹问题。先说Slate框架,知乎上已经有大神做过分析,基本上Slate就是一套自创的从DX或者OpenGL写起的UI框架,和在UE4里用UMG做游戏UI一样,Sla
俗话说的好,一流程序写架构,三流程序写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() [ WallDetailView->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 FLAEEdModeToolkit); 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 看一个大型云商转型的血泪史
- 记一次阿里云服务器安装Python的血泪史
- 记一次阿里云服务器安装Python的血泪史
- React 出海应用 首屏加载时间从20S降到10S以下 血泪史
- Mac OS 上使用 ffmpeg 的 “血泪” 总结
- 【来自一线的血泪总结】你的系统上线时是否踩过这些坑?【石杉的架构笔记】
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。