内容简介:俗话说的好,一流程序写架构,三流程序写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 的 “血泪” 总结
- 【来自一线的血泪总结】你的系统上线时是否踩过这些坑?【石杉的架构笔记】
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning ASP.NET 4 in C# and Vb
Imar Spaanjaars / Wrox / 2010-3-19 / GBP 29.99
This book is for anyone who wants to learn how to build rich and interactive web sites that run on the Microsoft platform. With the knowledge you gain from this book, you create a great foundation to ......一起来看看 《Beginning ASP.NET 4 in C# and Vb》 这本书的介绍吧!