俗話說的好,一流程序寫架構,三流程序寫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基本的插件製做和編譯已經得心應手。ide
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了,好比下面這段代碼:ui
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