4

俗话说的好,一流程序写架构,三流程序写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即可
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了,比如下面这段代码:

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


Wentao_Wang
136 声望54 粉丝