UE4編輯器擴展踩坑血淚史

俗話說的好,一流程序寫架構,三流程序寫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便可
clipboard.png
UE4編輯器裏的菜單欄,工具條,還有編輯器裏的菜單,都有相應的Extender類,例如FMenuExtender,添加按鈕或者菜單條目,咱們須要指定下面四個東西:
ExtensionPoint 通常來講這個是UE4編輯器規定好的,例如Settings就是加在設置那一欄菜單,比較常見的還有WindowLayOutEditMain工具

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的方式都是經過類繼承來實現,分別是IDetailCustomizationIPropertyTypeCustomization
例如咱們須要定義某個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依賴於IDetailPropertyRowFDetailWidgetRow這兩個類,咱們要作的是新建出本身的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

相關文章
相關標籤/搜索