0%

UE4编辑器扩展与slate基础

UE4编辑器扩展

模块认识

虚幻文档模块 模块是是虚幻引擎软件层面上的基本架构块,对引擎功能进行了独立封装。只有经过更改的模块会重新编译一次。可以运行时控制何时加载卸载哪些模块,也可以通过条件编译来卸载加载模块(如不同平台)。

模块构成

  • UE包含Engine类文件和项目类文件,Engine类文件包含引擎相关模块,不和实际项目存放在一起,但会被项目引用,在打包时链接上。实际项目中的模块放在Source文件夹下,引擎模块大致包括如下:

    • Core: 提供基本的运行时功能。
    • CoreUObject: 包含UObject系统。
    • Engine: 提供游戏引擎的核心功能。
    • Renderer: 负责图形渲染。
    • Physics: 处理物理模拟。
    • Animation: 管理动画系统。
    • AI: 提供人工智能功能。
    • Networking: 处理网络通信。
    • UI: 管理用户界面。
  • Source文件夹子文件目录,这些名字就代表一个个模块名字,当然,也可以将模块放在子文件夹中,如果要自定义模块,那么就要按着这个结构来: [pAJCpnS.png](https://imgse.com/i/pAJCpnS build文件定义模块的构建配置,包括依赖的其他模块和库(例如引用了引擎库中哪些模块)。 Target文件定义了模块的构建目标,比如构建平台、构建类型、依赖关系等。

  • UBT负责虚幻编译,也就负责操控模块的依赖关系,他在查找模块的依赖关系时会直接查看Build文件,也就是说模块必须拥有Build文件才能被UBT发现。如此以来,该自定义模块中的代码就可以使用被依赖模块中的代码了,生成IDE文件时,也会包含这些代码。

using UnrealBuildTool;

public class ModuleTest: ModuleRules
{
public ModuleTest(ReadOnlyTargetRules Target) : base(Target)
{
PrivateDependencyModuleNames.AddRange(new string[] {"Core", "CoreUObject", "Engine"});
}
}

  • 此外,还需要在Private文件夹中建立一个叫[ModuleName]Module的cpp文件,这个文件不需要创建.h文件,在这个类中写入IMPLEMENT_MODULE(FDefaultModuleImpl,ModuleTest)宏,并在这个文件中新建扩展IModuleInterface的类,为模块提供默认实现,不过也可以手动编写模块类、构造函数等。

IModuleInterface重要函数:StartupModule、ShutdownModule函数。

  • 在项目的.uproject文件中进行模块注册,可以控制模块在哪个加载阶段中加载:
"Modules": [
         {
             "Name": "MyProject",

         "Type": "Runtime",

         "LoadingPhase": "Default"
     },

     {
         "Name": "ModuleTest",

         "Type": "Runtime"
     }
 ]

.uproject文件中的注册与build文件中模块引用: 前者影响整个项目构建运行环境,侧重于控制模块的编译顺序,后者影响当前模块构建的过程(确保编译当前模块时所依赖的模块已经构建完毕) 也可以理解为,前者是将自定义模块加入到“主模块”的引用当中,使之能在项目启动时被识别。 此外,主模块的Build文件夹也需要注册新加入的自定义的模块。

  • 如果按照这样的方式新建了模块,那么就可以在虚幻的“新建C++类”中找到这个模块了: pAJPdMV.md.png 创建类后,.h文件自动添加到public文件夹中,.cpp文件自动添加到private文件夹中,当然也可以手动新建类,手动添加publicprivate。

  • 封装规则:

    • 如果头文件放在private中,那么内容会在该模块内公开,但不可用在其他模块中
    • public内属于公开文件夹,所有(引用的)模块均可使用
    • 如果既不放在private,也不放在public,则默认private,这种一般属于“游戏主模块”会干的事
    • private和public中的文件需要同名对应
    • 在虚幻引擎中新建类,这些工作都会自动完成
  • 虚幻文档中的推荐时将模块区分为“运行时”与“编辑器”之分,例如: pAJiELT.png 编辑器版本支持运行时与编辑器版本编译,运行时版本支持运行时版本编译,因为运行时不需要编辑器版本的代码。 具体步奏可以参考

细节面板扩展

可以直接通过虚幻反射系统的UProperty宏信息来直接设置如何显示属性,也可以通过自定义细节面板来实现更详细的方式。这里分为两个大类。

宏信息 先参考这个

自定义细节面板 IPropertyTypeCustomization支持Struct IDetailCustomization支持UClass 引用 如果要自定义一个UClass或者Struct在细节面板中的展示,那么需要做到以下几点:

  • 重新定义一个类,在这个类中写表现逻辑

    • 类继承接口IPropertyTypeCustomization或者IDetailCustomization
    • 类中定义静态方法static TSharedRef<IDetailCustomization> MakeInstance()
    • 类中重新实现虚方法virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;,如果是UStruct则是
      virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle,
        FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
        override;
    virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle,
        IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils&
        CustomizationUtils) override;
    
  • 在自定义结构所在的模块向PropertyEditor进行注册

    void StartupModule(){
        FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
        PropertyModule.RegisterCustomPropertyTypeLayout("%targerStruct%"),FOnGetPropertyTypeCustomizationInstance::CreateStatic(&TargetCustomization::MakeInstance);
        //MakeInstance的目的就是让PropertyEditor可以在这个地方构造自定义对象单例
    }
    
    

    void ShutupModule(){
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
    if(PropertyModule){
    PropertyModule->UnregisterCustomPropertyTypeLayout("%targerStruct%");
    }
    }
    //这里的%targerStruct%必须是去掉A或者F的字符串(%号无意义去除),UE是通过反射获取对应的Class的

  • 在这个模块的Build.cs中添加依赖

PrivateDependencyModuleNames.AddRange(new string[]{
    ...
    "PropertyEditor",
    ...
})

自定义的表现逻辑类和目标结构在模块初进行注册绑定后,Editor就不会为这个目标结构生成默认的细节面板了,此时直接在UE中打开他的细节面板会直接显示空。 转而调用对应的CustomizeDetails(或者CustomizeHeader和CustomizeChildren),此时需要在这些重新实现的方法中编写属性的表现逻辑。

细节面板重新自定义代表这个细节面板的所有相关逻辑都会消失,除了最基础的输入框和名称展示,右键的复制粘贴操作也会消失,甚至“确认输入后刷新面板”也需要重新编写事件

以IPropertyTypeCustomization举例:

TSharedRef<IPropertyHandle> redirectHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(F%targerStruct%, targetProperty)).ToSharedRef();//获取对应的属性句柄
ChildBuilder.AddProperty(redirectHandle);

这样子编写,对应属性就会原封不动按照默认的表现展示出来,对于一些其实不需要自定义修改但是又不想重新编写逻辑的属性很适用。 如果要自定义编写,则最基础的方法是

ChildBuilder.AddCustomRow(NSLOCTEXT(FText::FromString(TEXT("ChildRow")))
    .NameContent()
    [
        ...
    ]
    .ValueContent()
    [
        ...
    ]
);

用来向细节面板中添加一行,Name就是左边的内容,Value就是右边的内容,而方框内就是Slate的写法了。

Slate最佳实践

在知乎上找到一张图醍醐灌顶 也就是说这种链式编程的写法是和UMG表现上是一致的。 这里也不做过多展开了,直接归纳一些常见常用的基础写法。 可以使用SNew或者SAssignNew构建Slate实例(实质是SWidget实例),然后将这个实例可以直接放在中括号中,如上图所示,这样就可以直接展示SWidget中的对应控件逻辑了。

除了使用AddProperty,如果想在属性内进行特殊个性化也是能实现的。

handleTemp->CreatePropertyValueWidget(false)
handleTemp->CreatePropertyNameWidget()
handleTemp->CreateDefaultPropertyButtonWidgets()

IPropertyHandle的方法CreatePropertyValueWidget,直接返回默认的属性SWidget,将他放置在中括号中给予插槽,就能以默认的形式展现属性的值或者名字,可以用这种方法直接还原属性值的表现或者名字的表现。Value后的bool参数表示是否添加默认的属性按钮,和第三个方法一样。 默认的属性按钮: pAUDsht.png

布局控件:

  • SOverlay——叠加布局,不是很常用
  • SHorizontaiBox、SVerticalBox——添加子控件布局,挺常用的
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
    SNew(STextBlock)
    .Text(FText::FromString("Left"))
]
+ SHorizontalBox::Slot()
[
    SNew(STextBlock)
    .Text(FText::FromString("Center"))
]
+ SHorizontalBox::Slot()
[
    SNew(STextBlock)
    .Text(FText::FromString("Right"))
]

Slot就是插槽,将目标属性“插入”槽中的意思 可以直接在Slot()方法后接.方法控制布局

SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()//如果是SVerticalBox,则为AutoHeight
[
    SNew(STextBlock)
    .Text(FText::FromString("Auto Width"))
]
  • SScrollBox——滚动区域

属性控件:

  • SButton——按钮
  • STextBlock——文本块
  • SSearchBox——搜索框
  • ComboBox——下拉菜单

更多内容看这里 细节讲一下ComboBox

SNew(SComboBox<TSharedPtr<FItemData>>)
.OptionsSource(&ItemDataArray) //这个集合必须是全局变量,不知道为什么
.OnSelectionChanged_Lambda([handleTemp](TSharedPtr<FItemData> NewData,ESelectInfo::Type SelectInfo)
{
    handleTemp->SetValue(NewData->Value);
})
.OnGenerateWidget_Lambda([](TSharedPtr<FItemData> Item)->TSharedRef<SWidget>
{
    return SNew(STextBlock).Text(FText::FromString(FString::FromInt(Item->Value).Append(" | ").Append(Item->ItemID)));
})
.Content()
[
    handleTemp->CreatePropertyValueWidget(false)
]

pAUDuXF.png OptionsSourced的值必须是TSharedPtr的集合地址,也就是说ItemDataArray必须是TArray< TSharedPtr< Type>>类。

很意外的发现这个TArray还必须是全局变量,不然细节面板不会获取到具体值,可能和细节面板生命周期有关系。

OnSelectionChanged_Lambda表示选定值变化之后的操作,也有一个OnSelectionChanged的非Lambda表达式的差分版本,注意“将选中值设定要目标值”的逻辑需要在这里重新编写。 OnGenerateWidget_Lambda规定了每一列的具体展示内容,也有一个OnGenerateWidget的非Lambda表达式的差分版本,返回SWidget,所以理论上也可以在这里自定义其他内容。 Content中填写默认展示框的SWidget。