当前位置: 首页 > news >正文

UE4编辑器扩展踩坑血泪史

UE4编辑器扩展踩坑血泪史

https://segmentfault.com/a/1190000018367388

 

 

 

 

 

 

 

 

 

SegmentFault

  • 首页
  • 问答
  • 专栏
  • 资讯
  • 课程
  • 活动
  • 发现
  •  

Wentao_Wang

  •  
  • 136
  •  
  •  

UE4编辑器扩展踩坑血泪史

c++

发布于 2019-03-02

俗话说的好,一流程序写架构,三流程序写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

阅读 5.4k更新于 2019-03-03

本作品系原创, 采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


Wentao_Wang

  •  
  • 136
  •  
  •  

0 条评论

得票时间

 

推荐阅读

VS Code、ATOM这些开源文本编辑器的代码实现中有哪些奇技淫巧?

研究 V8 比较多,也关注了一下 vscode 和 atom 的性能,每次 vscode、atom 的 change log 我都会看一遍。印象最深的是 vscode 1.14 的一次更新日志,doApplyEdits Lines inserted using splice · Issue #351 · Mi...

justjavac 阅读 9.5k 60 赞 5 评论

C++学习 | 面试官:我们只想要这样的C++工程师

这个很正常哈,主要是咱们平时很少去关注企业的招聘要求,所以不知道怎么学,更不知道要学哪些内容。所以就就分析了大量公司的校园招聘,根据招聘要求给大家总结出了每个方向的学习路径。

就就校招 阅读 1.6k 13 赞

程序猿专用十大在线编译器(IDE)整理

① 主流的脚手架都支持,比如在线create-react-app,vue-cli等(在线 fork 修改),支持 github 登录(项目导入),也支持 cli 上传例子,例子可以在线访问和下载,当然也支持内嵌到其他博客等网页中。② 地址:[链接]③ 图示

代码牛 阅读 6.5k 10 赞

面试官:我们只想要这样的C++工程师

这个很正常哈,主要是咱们平时很少去关注企业的招聘要求,所以不知道怎么学,更不知道要学哪些内容。所以就就分析了大量公司的校园招聘,根据招聘要求给大家总结出了每个方向的学习路径。

神奇的Aym 阅读 924 3 赞

Android Studio 2.2新特性

Android studio 2.2的提升包含三大要素:更快,更加智能,Android平台支持。开发更加迅速,例如新的布局编辑器,其让开发者能更快的更简单的编写应用界面。开发更加智能,例如新的APK分析工具,增强了的布局代码...

neu 阅读 5.7k 2 赞

使用Docker搭建Linuc C++编译环境

我现在做的是 Linux C++ 开发,但手头上没有 Linux 设备。目前的常用的方法有两种: 云主机; 虚拟机。 但是使用云主机不方便,使用虚拟机对电脑有一定要求。还有第三种方法:Docker。 本文介绍我使用 Docker 搭...

chenBright 阅读 1.1k 2 赞

QtQuick系列教程之开发环境的搭建

Qt是一个跨平台应用程序和 UI 开发框架。使用 Qt 您只需一次性开发应用程序,无须重新编写源代码,便可跨不同桌面和嵌入式操作系统部署这些应用程序。

xiangzhihong 阅读 536 2 赞

初学C++,选择哪个编译器比较合适?为什么?

C/C++开发环境,下面说说自己的一些看法,将日常开发中身边人经常使用的环境罗列出来,如果你有不同意见,欢迎留言讨论。最后,如果大家如果在自学遇到困难,想找一个C++的学习环境,可以加入我们的C++学习圈,点...

科技图文 阅读 221 1 赞

Wentao.Wang

用户专栏

Physically based rendering

10 人关注

16 篇文章

Planets

广告位促销,月曝光三千万,10 元/天

产品

热门问答

热门专栏

热门课程

最新活动

技术圈

酷工作

移动客户端

课程

Java 开发课程

PHP 开发课程

Python 开发课程

前端开发课程

移动开发课程

资源

每周精选

用户排行榜

徽章

帮助中心

声望与权限

社区服务中心

合作

关于我们

广告投放

职位发布

讲师招募

联系我们

合作伙伴

关注

产品技术日志

社区运营日志

市场运营日志

团队日志

社区访谈

条款

服务条款

隐私政策

下载 App


Copyright © 2011-2020 SegmentFault.

浙ICP备 15005796号-2 浙公网安备 33010602002000号 杭州堆栈科技有限公司版权所有

 

相关文章:

  • require函数
  • UE4 给static mesh 动态添加Socket
  • UE4 角色用Child Actor组建添加装备 这样方便随时添加,更换套装等行为
  • Unity Assets目录下的特殊文件夹名称
  • Unity-ShaderVariantCollection
  • Unity渲染教程(九):复杂材质 https://www.jianshu.com/p/5e3af869870f
  • HttpWebRequest(System.Net)模拟HTTP发送POST
  • C#中用HttpWebRequest中发送GET/HTTP/HTTPS请求 (转载)
  • System.Net.HttpWebRequest.GetRequestStream超时问题
  • System.Net.HttpWebRequest.GetResponse() 远程服务器
  • 【转载】HttpWebRequest的GetResponse或GetRequestStream偶尔超时 + 总结各种超时死掉的可能和相应的解决办法
  • UE4 AIController
  • [UE4]创建自定义AIController的方法(C++)
  • eclipse + pydev远程调试OpenStack
  • 调用shell jenkins不能自动结束
  • [iOS]Core Data浅析一 -- 启用Core Data
  • [译]如何构建服务器端web组件,为何要构建?
  • angular2 简述
  • create-react-app项目添加less配置
  • Django 博客开发教程 8 - 博客文章详情页
  • es的写入过程
  • Java 23种设计模式 之单例模式 7种实现方式
  • JAVA_NIO系列——Channel和Buffer详解
  • JAVA并发编程--1.基础概念
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • Python 反序列化安全问题(二)
  • Python十分钟制作属于你自己的个性logo
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • SSH 免密登录
  • webpack+react项目初体验——记录我的webpack环境配置
  • 代理模式
  • 分布式熔断降级平台aegis
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 力扣(LeetCode)22
  • 十年未变!安全,谁之责?(下)
  • 事件委托的小应用
  • 通过几道题目学习二叉搜索树
  • 微信开源mars源码分析1—上层samples分析
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • 阿里云ACE认证学习知识点梳理
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • #{}和${}的区别是什么 -- java面试
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (分类)KNN算法- 参数调优
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (附源码)ssm码农论坛 毕业设计 231126
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (四)Linux Shell编程——输入输出重定向
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • .Net IE10 _doPostBack 未定义
  • .NET Micro Framework 4.2 beta 源码探析
  • .Net Web项目创建比较不错的参考文章
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?