UE4编辑器扩展踩坑血泪史

栏目: 后端 · 发布时间: 5年前

内容简介:俗话说的好,一流程序写架构,三流程序写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编辑器扩展踩坑血泪史

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编辑器扩展踩坑血泪史

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的方式都是通过类继承来实现,分别是 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去使用。

UE4编辑器扩展踩坑血泪史

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

一目了然

一目了然

Robert Hoekman, Jr. / 段江玲 / 机械工业出版社华章公司 / 2012-3-19 / 59.00元

可用性或易用性是软件或Web设计师的重要设计目标之一。本书深入阐述了如何设计出简单易用的基于Web的软件,以帮助读者理解、掌握显性设计的精髓。作者从软件开发初期谈起,一直到软件设计后期,分析诸多案例并论证了自己的见解或设计原则。本书在第1版的基础上进行了重大改进,尤其是在设计思想上,作者在本书中谈到“以用户为中心的设计”、“以任务为中心的设计”以及“以情景为中心的设计”的理念。这种设计理念也将更直......一起来看看 《一目了然》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具