hyc1200 发表于 2024-7-15 18:09

《调教UE5:编纂器拓展指南》自定义数据类型

《调教UE5》系列记录的是笔者在开发UE引擎中总结的些许经验。文中所有不雅概念和结论仅代表个人见解,作为自学笔记和日后反思之参考,亦可能存在谬误或过时之处。如有讹夺和不妥,恳请多加斧正。<hr/><hr/>目录

不成忽略的事前筹备 ~♡
ㅤ创建 CustomDataType 模块
ㅤ创建 CustomDataTypeEditor 模块
自定义数据类型 ~♡
ㅤ新建数据类型
ㅤFactory
ㅤAssetTypeActions
为自定义数据类型构建编纂器 ~♡
导入和导出 ~♡
ㅤ导入为自定义资产类型
ㅤ重导入自定义资产类型
ㅤ导出自定义资产类型<hr/>\large\textbf{珍贵的不是倡议誓言的决心,} \\ \large\textbf{而是恪守誓言的行为。} \\
<hr/>本章将探索自定义数据类型的方式。
本章中使用的案例原型来自 Creating a Custom Asset Type with its own Editor in C++ ,笔者仅在此基础长进行了些许学习探讨,有兴趣的读者也可前往原文阅读。
不成忽略的事前筹备 ~♡

在正式开始之前,我们需要筹备两个新模块:一个“CustomDataType”运行时模块(Runtime)和一个“CustomDataTypeEditor”编纂器模块(Editor)。运行时模块存放自定义数据类型,编纂器模块则负责存放所有与操作自定义数据类型相关的部门,诸如创建、改削和导入导出等行为。



在名为“ExtendEditor”的项目中组织如下的文件布局。



创建 CustomDataType 模块

新建“CustomDataType”模块中的文件必要文件。
// Source/CustomDataType/CustomDataType.Build.cs

using UnrealBuildTool;

public class CustomDataType : ModuleRules
{
        public CustomDataType(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        ”Core”,
                        ”CoreUObject”,
                        ”Engine”,
                        ”InputCore”,
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}
// Source/CustomDataType/Public/CustomDataType.h

#pragma once

#include ”CoreMinimal.h”
#include ”Modules/ModuleManager.h”

class FCustomDataTypeModule : public IModuleInterface
{
public:

        virtual void StartupModule() override;
        virtual void ShutdownModule() override;
};
// Source/CustomDataType/Private/CustomDataType.cpp

#pragma once

#include ”CustomDataType.h”

IMPLEMENT_MODULE(FCustomDataTypeModule, CustomDataType)


void FCustomDataTypeModule::StartupModule()
{
        IModuleInterface::StartupModule();
}

void FCustomDataTypeModule::ShutdownModule()
{
        IModuleInterface::ShutdownModule();
}
将“CustomDataType”模块添加到项目依赖中。
// Source/ExtendEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class ExtendEditorTarget : TargetRules
{
        public ExtendEditorTarget( TargetInfo Target) : base(Target)
        {
                Type = TargetType.Game;
                DefaultBuildSettings = BuildSettingsVersion.V2;
                ExtraModuleNames.AddRange( new string[] { ”ExtendEditor”, ”CustomDataType” } );
        }
}
// Source/ExtendEditor.uproject

{
        ”FileVersion”: 3,
        ”EngineAssociation”: ”5.0”,
        ”Category”: ””,
        ”Description”: ””,
        ”Modules”: [
                {
                        ”Name”: ”ExtendEditor”,
                        ”Type”: ”Runtime”,
                        ”LoadingPhase”: ”Default”
                        ]
                },
                {
                        ”Name”: ”CustomDataType”,
                        ”Type”: ”Runtime”,
                        ”LoadingPhase”: ”Default”
                }
        ]
}

创建 CustomDataTypeEditor 模块

新建“CustomDataTypeEditor”模块中的文件必要文件。
// Source/CustomDataTypeEditor/CustomDataTypeEditor.Build.cs

using UnrealBuildTool;

public class CustomDataTypeEditor : ModuleRules
{
        public CustomDataTypeEditor(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        ”Core”,
                        ”CoreUObject”,
                        ”Engine”,
                        ”InputCore”,
                        ”CustomDataType”,
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}
// Source/CustomDataTypeEditor/Public/CustomDataTypeEditor.h

#pragma once

#include ”CoreMinimal.h”
#include ”Modules/ModuleManager.h”

class FCustomDataTypeEditorModule : public IModuleInterface
{
public:

        virtual void StartupModule() override;
        virtual void ShutdownModule() override;
};
// Source/CustomDataTypeEditor/Private/CustomDataTypeEditor.cpp

#pragma once

#include ”CustomDataTypeEditor.h”

IMPLEMENT_MODULE(FCustomDataTypeEditorModule, CustomDataTypeEditor)

void FCustomDataTypeEditorModule::StartupModule()
{
        IModuleInterface::StartupModule();
}

void FCustomDataTypeEditorModule::ShutdownModule()
{
        IModuleInterface::ShutdownModule();
}
将“CustomDataTypeEditor”模块添加到项目依赖中。
// Source/ExtendEditorEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class ExtendEditorEditorTarget : TargetRules
{
        public ExtendEditorEditorTarget( TargetInfo Target) : base(Target)
        {
                Type = TargetType.Editor;
                DefaultBuildSettings = BuildSettingsVersion.V2;
                ExtraModuleNames.AddRange( new string[]
                {
                        ”ExtendEditor”,
                        ”CustomDataType”,
                        ”CustomDataTypeEditor”
                } );
        }
}
// Source/ExtendEditor.uproject

{
        ”FileVersion”: 3,
        ”EngineAssociation”: ”5.0”,
        ”Category”: ””,
        ”Description”: ””,
        ”Modules”: [
                {
                        ”Name”: ”ExtendEditor”,
                        ”Type”: ”Runtime”,
                        ”LoadingPhase”: ”Default”
                },
                {
                        ”Name”: ”CustomDataType”,
                        ”Type”: ”Runtime”,
                        ”LoadingPhase”: ”Default”
                },
                {
                        ”Name”: ”CustomDataTypeEditor”,
                        ”Type”: ”Editor”,
                        ”LoadingPhase”: ”Default”
                }
        ]
}

{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

自定义数据类型 ~♡

要在 UE5 中添加一个可在 ContentBrowser 中进行基本操作的自定义数据类型,一共需要三样内容:

[*]自定义的数据类型
[*]与该数据类型关联的 Factory
[*]与该数据类型关联的 AssetTypeActions



新建数据类型

首先来新建自定义数据类型,我们将新的数据类型定名为“UCustomNormalDistribution”,担任自 UObject。这个数据类型用于描述一个正态分布,并有一个函数用于给出符合该正态分布的随机值。



在 CustomDataType 文件夹下新建文件“CustomNormalDistribution.h”和“CustomNormalDistribution.cpp”。


// CustomNormalDistribution.h

#pragma once

#include ”CoreMinimal.h”
#include <random>
#include ”CustomNormalDistribution.generated.h”

UCLASS(BlueprintType)
class CUSTOMDATATYPE_API UCustomNormalDistribution : public UObject
{
        GENERATED_BODY()
       
public:
        UCustomNormalDistribution();

        UFUNCTION(BlueprintCallable)
        float DrawSample();

        UFUNCTION(CallInEditor)
        void LogSample();

public:
        UPROPERTY(EditAnywhere)
        float Mean;

        UPROPERTY(EditAnywhere)
        float StandardDeviation;

private:
        std::mt19937 RandomNumberGenerator;
};
// CustomNormalDistribution.cpp

#pragma once

#include ”CustomNormalDistribution.h”

UCustomNormalDistribution::UCustomNormalDistribution()
        : Mean(0.5f)
        , StandardDeviation(0.2f)
{}

float UCustomNormalDistribution::DrawSample()
{
        return std::normal_distribution<>(Mean, StandardDeviation)(RandomNumberGenerator);
}

void UCustomNormalDistribution::LogSample()
{
        UE_LOG(LogTemp, Log, TEXT(”%f”), DrawSample())
}

{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

Factory

UFactory 是一个工厂类,用于创建和导入新对象。它是一个抽象类,不能直接实例化,它的子类必需实现 FactoryCreateNew 函数,以便在 ContentBrowser 中创建资产。
可以理解为 UFactory 定义了资源创建的方式,除了必需实现的 FactoryCreateNew 函数,还可以通过定义内部变量或覆写虚函数的方式来自定义相关的法则。



UFactory 内部变量



UFactory 部门函数

为 UCustomNormalDistribution 创建相关的 Factory,我们将它定名为“UCustomNormalDistributionFactory”,担任自 UFactory。



在 CustomDataTypeEditor 文件夹下新建文件“CustomNormalDistributionFactory.h”和“CustomNormalDistributionFactory.cpp”。



在 UCustomNormalDistributionFactory 的构造函数中将工厂与 UCustomNormalDistribution 绑定,并允许其创建新资产的行为。
// CustomNormalDistributionFactory.h

#pragma once

#include ”CoreMinimal.h”
#include ”Factories/Factory.h”
#include ”CustomNormalDistributionFactory.generated.h”

UCLASS()
class UCustomNormalDistributionFactory : public UFactory
{
        GENERATED_BODY()
public:
        UCustomNormalDistributionFactory();
        virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
// CustomNormalDistributionFactory.cpp

#include ”CustomNormalDistributionFactory.h”
#include ”CustomNormalDistribution.h”

UCustomNormalDistributionFactory::UCustomNormalDistributionFactory()
{
        SupportedClass = UCustomNormalDistribution::StaticClass();
        bCreateNew = true;
}

UObject* UCustomNormalDistributionFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
        return NewObject<UCustomNormalDistribution>(InParent, Class, Name, Flags, Context);
}
// CustomDataTypeEditor.Build.cs

using UnrealBuildTool;

public class CustomDataTypeEditor : ModuleRules
{
        public CustomDataTypeEditor(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        ”Core”,
                        ”CoreUObject”,
                        ”Engine”,
                        ”InputCore”,
                        ”CustomDataType”,
                        ”UnrealEd”,
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}
{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

AssetTypeActions

FAssetTypeActions_Base 是所有 AssetTypeActions 的基类,提供有关特定资产类型的操作和其他信息。它是可选的,但一般默认需要提供,否则无法完成诸如双击打开资产编纂器等操作。
当编纂器在 ContentBrowser 中措置资产操作(创建资产、双击打开编纂资源等)时,会搜寻是否有对应的 AssetTypeActionsClass 被注册。
AssetTypeActions 中有各种与资产操作以及资产信息显示有关的虚函数,我们可以通过覆写它们来自定义资产操作的行为和资产显示方式。



FAssetTypeActions_Base 部门函数

为 UCustomNormalDistribution 创建相关的 AssetTypeActions,我们将它定名为“FCustomNormalDistributionActions”,担任自 FAssetTypeActions_Base。



在 CustomDataTypeEditor 文件夹下新建文件“CustomNormalDistributionActions.h”和“CustomNormalDistributionActions.cpp”。



我们通过 FCustomNormalDistributionActions 自定义在 ContentBrowser 中使用右键菜单创建 UCustomNormalDistribution 时的颜色和友好显示名称。而且我们为 UCustomNormalDistribution这个特定资产类型的右键菜单里添加一个“Custom Action”按钮。稍后,我们将注册一个单独的 Category 供从右键菜单创建 UCustomNormalDistribution 时使用。
// CustomNormalDistributionActions.h

#pragma once

#include ”CoreMinimal.h”
#include ”AssetTypeActions_Base.h”

class FCustomNormalDistributionActions : public FAssetTypeActions_Base
{
public:
        FCustomNormalDistributionActions(EAssetTypeCategories::Type InAssetCategory);
       
        virtual UClass* GetSupportedClass() const override;
        virtual FText GetName() const override;
        virtual FColor GetTypeColor() const override;
        virtual uint32 GetCategories() override;
       
        virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
        virtual bool HasActions(const TArray<UObject*>& InObjects) const override;

private:

        EAssetTypeCategories::Type AssetCategory;
};
// CustomNormalDistributionActions.cpp

#include ”CustomNormalDistributionActions.h”
#include ”CustomNormalDistribution.h”


FCustomNormalDistributionActions::FCustomNormalDistributionActions(EAssetTypeCategories::Type InAssetCategory)
        : AssetCategory(InAssetCategory)
{
}

UClass* FCustomNormalDistributionActions::GetSupportedClass() const
{
        return UCustomNormalDistribution::StaticClass();
}

FText FCustomNormalDistributionActions::GetName() const
{
        return INVTEXT(”Custom Normal Distribution”);
}

FColor FCustomNormalDistributionActions::GetTypeColor() const
{
        return FColor::Orange;
}

uint32 FCustomNormalDistributionActions::GetCategories()
{
        return AssetCategory;
}

void FCustomNormalDistributionActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
        FAssetTypeActions_Base::GetActions(InObjects, MenuBuilder);
       
        MenuBuilder.AddMenuEntry(
                FText::FromString(”Custom Action”),
                FText::FromString(”This is a custom action”),
                FSlateIcon(),
                FUIAction()
                );

}

// HasActions() 必需返回 true 以使 GetActions() 有效
bool FCustomNormalDistributionActions::HasActions(const TArray<UObject*>& InObjects) const
{
        return true;
}
在 CustomDataTypeEditor 模块启动时注册 FCustomNormalDistributionActions。
// CustomDataTypeEditor.h

#pragma once

#include ”CoreMinimal.h”
#include ”Modules/ModuleManager.h”
#include ”CustomNormalDistributionActions.h”

class FCustomDataTypeEditorModule : public IModuleInterface
{
public:

        /** IModuleInterface implementation */
        virtual void StartupModule() override;
        virtual void ShutdownModule() override;

private:
        // 记录注册的 AssetTypeActions 以供模块停用时卸载
        TSharedPtr<FCustomNormalDistributionActions> CustomNormalDistributionActions;
};
// CustomDataTypeEditor.cpp

#pragma once

#include ”CustomDataTypeEditor.h”

#include ”AssetToolsModule.h”
#include ”AssetTypeCategories.h”

IMPLEMENT_MODULE(FCustomDataTypeEditorModule, CustomDataTypeEditor)

void FCustomDataTypeEditorModule::StartupModule()
{
        // 注册新的 Category
        EAssetTypeCategories::Type Category =
                FAssetToolsModule::GetModule().Get().RegisterAdvancedAssetCategory(
                        FName(TEXT(”Example”)), FText::FromString(”Example”));
        // 注册 AssetTypeActions
        CustomNormalDistributionActions = MakeShared<FCustomNormalDistributionActions>(Category);
        FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(
                CustomNormalDistributionActions.ToSharedRef());
}

void FCustomDataTypeEditorModule::ShutdownModule()
{
        if (FModuleManager::Get().IsModuleLoaded(”AssetTools”))
        {
                FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(
                        CustomNormalDistributionActions.ToSharedRef());
        }
}
// CustomDataTypeEditor.Build.cs

using UnrealBuildTool;

public class CustomDataTypeEditor : ModuleRules
{
        public CustomDataTypeEditor(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        ”Core”,
                        ”CoreUObject”,
                        ”Engine”,
                        ”InputCore”,
                        ”CustomDataType”,
                        ”UnrealEd”,
                        ”AssetTools”,
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}
编译并重启编纂器,我们可以通过右键菜单创建新的 UCustomNormalDistribution 资产,在该资产上单击右键,会看到右键菜单里添加了一个 Custom Action 按钮。





{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

为自定义数据类型构建编纂器 ~♡



双击资产打开编纂器这个操作的入口在 UAssetEditorSubsystem::OpenEditorForAsset(),我们可以顺藤摸瓜来寻找合适的介入时机。



当 UAssetEditorSubsystem::OpenEditorForAsset() 开始执行时,会调用 IAssetTypeActions 中的 OpenAssetEditor() 虚函数,这时如果没有提供任何子类的重写,则会默认打开一个 SimpleAssetEditor,查看得知,FSimpleAssetEditor 是一个担任自 FAssetEditorToolkit 的类。
因此我们要做的就是在本身的 AssetTypeActions 中重写 OpenAssetEditor(),然后用它调用自定义的 FAssetEditorToolkit 类,来打开本身的编纂器。我们假设将这个 FAssetEditorToolkit 类定名为“FCustomNormalDistributionEditorToolkit”。于是这个流程变成了如下这样。



当我们双击打开一个 UObject 时,这个 UObject 将以输入参数的形式逐级传递,并最终抵达 InitEditor()。
InitEditor() 负责规划窗口布局并调用 FAssetEditorToolkit::InitAssetEditor()。RegisterTabSpawners() 向规划好的布局中填充内容。这是我们需要存眷的两个重点函数。
每个 AssetEditor 都有本身的 TabSpawners,而每个 AssetEditor 都是在运行时动态创建的。因此每次打开一个新的 AssetEditor 时,都需要从头注册 TabSpawners。

弄清楚道理后就好操作了。首先在 CustomNormalDistributionActions 中重写 OpenAssetEditor()。
这里暂时忽略 OpenAssetEditor() 的 EditWithinLevelEditor 输入,只传递 InObjects 输入。而且假设接下来的 FCustomNormalDistributionEditorToolkit 里的入口函数为 InitEditor()。
// CustomNormalDistributionActions.h

#pragma once

#include ”CoreMinimal.h”
#include ”AssetTypeActions_Base.h”

class FCustomNormalDistributionActions : public FAssetTypeActions_Base
{
public:
        ...

        virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor) override;
};
// CustomNormalDistributionActions.cpp

#include ”CustomNormalDistributionEditorToolkit.h”

void FCustomNormalDistributionActions::OpenAssetEditor(const TArray<UObject*>& InObjects,
        TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
        MakeShared<FCustomNormalDistributionEditorToolkit>()->InitEditor(InObjects);
}
然后在 CustomDayaTypeEditor 模块下新建 FCustomNormalDistributionEditorToolkit 类。
RegisterTabSpawners() 和 UnregisterTabSpawners() 是关键函数。如前所述,我们在 InitEditor() 创建布局,并在 RegisterTabSpawners() 中注册 TabSpawners。
我们提前假设了已经存在一个 SCustomNormalDistributionWidget 小部件类,我们在稍后很快会来创建它。



// CustomNormalDistributionEditorToolkit.h

#pragma once

#include ”CoreMinimal.h”
#include ”CustomNormalDistribution.h”
#include ”Toolkits/AssetEditorToolkit.h”

class FCustomNormalDistributionEditorToolkit : public FAssetEditorToolkit
{
public:
        // 外部调用的入口,它可以是任意名字,可以具有任意参数。
        void InitEditor(const TArray<UObject*>& InObjects);

        // 必需实现的虚函数
        virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
        virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
        virtual FName GetToolkitFName() const override { return ”CustomNormalDistributionEditor”; }
        virtual FText GetBaseToolkitName() const override { return INVTEXT(”Custom Normal Distribution Editor”); }
        virtual FString GetWorldCentricTabPrefix() const override { return ”Custom Normal Distribution”; }
        virtual FLinearColor GetWorldCentricTabColorScale() const override { return {}; }

        float GetMean() const;
        float GetStandardDeviation() const;
        void SetMean(float Mean);
        void SetStandardDeviation(float StandardDeviation);
       
private:
       
        UCustomNormalDistribution* NormalDistribution = nullptr;
};
// CustomNormalDistributionEditorToolkit.cpp

#pragma once

#include ”CustomNormalDistributionEditorToolkit.h”
#include ”Widgets/Docking/SDockTab.h”
#include ”SCustomNormalDistributionWidget.h”
#include ”Modules/ModuleManager.h”

void FCustomNormalDistributionEditorToolkit::InitEditor(const TArray<UObject*>& InObjects)
{
        NormalDistribution = Cast<UCustomNormalDistribution>(InObjects);

        const TSharedRef<FTabManager::FLayout> Layout =
                FTabManager::NewLayout(”CustomNormalDistributionEditorLayout”)
                ->AddArea
                (
                        FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
                        ->Split
                        (
                                FTabManager::NewSplitter()
                                ->SetSizeCoefficient(0.6f)
                                ->SetOrientation(Orient_Horizontal)
                                ->Split
                                (
                                        FTabManager::NewStack()
                                        ->SetSizeCoefficient(0.8f)
                                        ->AddTab(”CustomNormalDistributionPDFTab”, ETabState::OpenedTab)
                                )
                                ->Split
                                (
                                        FTabManager::NewStack()
                                        ->SetSizeCoefficient(0.2f)
                                        ->AddTab(”CustomNormalDistributionDetailsTab”, ETabState::OpenedTab)
                                )
                        )
                        ->Split
                        (
                                FTabManager::NewStack()
                                ->SetSizeCoefficient(0.4f)
                                ->AddTab(”OutputLog”, ETabState::OpenedTab)
                        )
                );

        FAssetEditorToolkit::InitAssetEditor(EToolkitMode::Standalone, {}, ”CustomNormalDistributionEditor”, Layout, true, true, InObjects);
}

void FCustomNormalDistributionEditorToolkit::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
        FAssetEditorToolkit::RegisterTabSpawners(InTabManager);

        WorkspaceMenuCategory =
                InTabManager->AddLocalWorkspaceMenuCategory(INVTEXT(”CustomNormalDistributionTabs”));

        // 注册 SCustomNormalDistributionWidget TabSpawner
        InTabManager->RegisterTabSpawner(”CustomNormalDistributionPDFTab”,
                FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&)
                {
                        return SNew(SDockTab)
                        [
                                SNew(SCustomNormalDistributionWidget)
                                .Mean(this, &FCustomNormalDistributionEditorToolkit::GetMean)
                                .StandardDeviation(this, &FCustomNormalDistributionEditorToolkit::GetStandardDeviation)
                                .OnMeanChanged(this, &FCustomNormalDistributionEditorToolkit::SetMean)
                                .OnStandardDeviationChanged(this, &FCustomNormalDistributionEditorToolkit::SetStandardDeviation)
                        ];
                }))
        .SetDisplayName(INVTEXT(”PDF”))
        .SetGroup(WorkspaceMenuCategory.ToSharedRef());

        // 创建 CustomNormalDistribution DetailsView
        FPropertyEditorModule& PropertyEditorModule =
                FModuleManager::GetModuleChecked<FPropertyEditorModule>(”PropertyEditor”);
        FDetailsViewArgs DetailsViewArgs;
        DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
        TSharedRef<IDetailsView> DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
        DetailsView->SetObjects(TArray<UObject*>{ NormalDistribution });

        // 注册 CustomNormalDistribution DetailsView TabSpawner
        InTabManager->RegisterTabSpawner(”CustomNormalDistributionDetailsTab”,
                FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&)
                {
                        return SNew(SDockTab)
                        [
                                DetailsView
                        ];
                }))
        .SetDisplayName(INVTEXT(”Details”))
        .SetGroup(WorkspaceMenuCategory.ToSharedRef());
}

void FCustomNormalDistributionEditorToolkit::UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
        FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
        InTabManager->UnregisterTabSpawner(”CustomNormalDistributionPDFTab”);
        InTabManager->UnregisterTabSpawner(”CustomNormalDistributionDetailsTab”);
}

float FCustomNormalDistributionEditorToolkit::GetMean() const
{
        return NormalDistribution->Mean;
}

float FCustomNormalDistributionEditorToolkit::GetStandardDeviation() const
{
        return NormalDistribution->StandardDeviation;
}

void FCustomNormalDistributionEditorToolkit::SetMean(float Mean)
{
        NormalDistribution->Modify();
        NormalDistribution->Mean = Mean;
}

void FCustomNormalDistributionEditorToolkit::SetStandardDeviation(float StandardDeviation)
{
        NormalDistribution->Modify();
        NormalDistribution->StandardDeviation = StandardDeviation;
}
继续在 CustomDayaTypeEditor 模块下新建 SCustomNormalDistributionWidget 小部件类。
向 UCustomNormalDistribution 传递数据的逻辑位于 OnMouseMove() 中。OnPaint() 读取 UCustomNormalDistribution 中的数据并按照读取到的数据绘制由512个点组成的线段图形。



// SCustomNormalDistributionWidget.h

#pragma once

#include ”CoreMinimal.h”
#include ”Widgets/SLeafWidget.h”

DECLARE_DELEGATE_OneParam(FOnMeanChanged, float /*NewMean*/)
DECLARE_DELEGATE_OneParam(FOnStandardDeviationChanged, float /*NewStandardDeviation*/)

class SCustomNormalDistributionWidget : public SLeafWidget
{
public:
       
        SLATE_BEGIN_ARGS(SCustomNormalDistributionWidget)
                : _Mean(0.5f)
                , _StandardDeviation(0.2f)
        {}
       
        SLATE_ATTRIBUTE(float, Mean)
        SLATE_ATTRIBUTE(float, StandardDeviation)
        SLATE_EVENT(FOnMeanChanged, OnMeanChanged)
        SLATE_EVENT(FOnStandardDeviationChanged, OnStandardDeviationChanged)
       
        SLATE_END_ARGS()

public:
       
        void Construct(const FArguments& InArgs);

        virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
        virtual FVector2D ComputeDesiredSize(float) const override;

        virtual FReply OnMouseButtonDown(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override;
        virtual FReply OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override;
        virtual FReply OnMouseMove(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override;

private:
       
        TAttribute<float> Mean;
        TAttribute<float> StandardDeviation;

        FOnMeanChanged OnMeanChanged;
        FOnStandardDeviationChanged OnStandardDeviationChanged;

        FTransform2D GetPointsTransform(const FGeometry& AllottedGeometry) const;
};
// SCustomNormalDistributionWidget.cpp

#pragma once

#include ”SCustomNormalDistributionWidget.h”

void SCustomNormalDistributionWidget::Construct(const FArguments& InArgs)
{
    Mean = InArgs._Mean;
    StandardDeviation = InArgs._StandardDeviation;
    OnMeanChanged = InArgs._OnMeanChanged;
    OnStandardDeviationChanged = InArgs._OnStandardDeviationChanged;
}

int32 SCustomNormalDistributionWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    const int32 NumPoints = 512;
    TArray<FVector2D> Points;
    Points.Reserve(NumPoints);
    const FTransform2D PointsTransform = GetPointsTransform(AllottedGeometry);
    for (int32 PointIndex = 0; PointIndex < NumPoints; ++PointIndex)
    {
      const float X = PointIndex / (NumPoints - 1.0);
      const float D = (X - Mean.Get()) / StandardDeviation.Get();
      const float Y = FMath::Exp(-0.5f * D * D);
      Points.Add(PointsTransform.TransformPoint(FVector2D(X, Y)));
    }
    FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Points);
    return LayerId;
}

FVector2D SCustomNormalDistributionWidget::ComputeDesiredSize(float) const
{
    return FVector2D(200.0, 200.0);
}

FReply SCustomNormalDistributionWidget::OnMouseButtonDown(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent)
{
    if (GEditor && GEditor->CanTransact() && ensure(!GIsTransacting))
      GEditor->BeginTransaction(TEXT(””), INVTEXT(”Edit Normal Distribution”), nullptr);
    return FReply::Handled().CaptureMouse(SharedThis(this));
}

FReply SCustomNormalDistributionWidget::OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent)
{
    if (GEditor) GEditor->EndTransaction();
    return FReply::Handled().ReleaseMouseCapture();
}

FReply SCustomNormalDistributionWidget::OnMouseMove(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent)
{
    if (!HasMouseCapture()) return FReply::Unhandled();
    const FTransform2D PointsTransform = GetPointsTransform(AllottedGeometry);
    const FVector2D LocalPosition = AllottedGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
    const FVector2D NormalizedPosition = PointsTransform.Inverse().TransformPoint(LocalPosition);
    if (OnMeanChanged.IsBound())
      OnMeanChanged.Execute(NormalizedPosition.X);
    if (OnStandardDeviationChanged.IsBound())
      OnStandardDeviationChanged.Execute(FMath::Max(0.025f, FMath::Lerp(0.025f, 0.25f, NormalizedPosition.Y)));
    return FReply::Handled();
}

FTransform2D SCustomNormalDistributionWidget::GetPointsTransform(const FGeometry& AllottedGeometry) const
{
    const double Margin = 0.05 * AllottedGeometry.GetLocalSize().GetMin();
    const FScale2D Scale((AllottedGeometry.GetLocalSize() - 2.0 * Margin) * FVector2D(1.0, -1.0));
    const FVector2D Translation(Margin, AllottedGeometry.GetLocalSize().Y - Margin);
    return FTransform2D(Scale, Translation);
}
// CustomDataTpeEditor.Build.cs

using UnrealBuildTool;

public class CustomDataTypeEditor : ModuleRules
{
        public CustomDataTypeEditor(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        ”Core”,
                        ”CoreUObject”,
                        ”Engine”,
                        ”InputCore”,
                        ”CustomDataType”,
                        ”UnrealEd”,
                        ”AssetTools”,
                        ”Slate”,
                        ”SlateCore”,
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}
编译并重启编纂器,创建一个 UCustomNormalDistribution 资产,双击打开编纂器。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

导入和导出 ~♡

导入为自定义资产类型

探索发现,新建和导入的功能似乎无法同时存在于一个工厂里,概念上确实讲得通。如果有读者知道如何让他们共存的方式,欢迎奉告我。
为了能够实现导入的功能,我们需要增加一个新的工厂。


从文件导入

先来到 CustomNormalDistribution.h 里,添加一个路径变量,这个变量用于记录数据的导入来源。
// CustomNormalDistribution.h

UCLASS(BlueprintType)
class CUSTOMDATATYPE_API UCustomNormalDistribution : public UObject
{
        GENERATED_BODY()
       
public:
        UCustomNormalDistribution();

        UFUNCTION(BlueprintCallable)
        float DrawSample();

        UFUNCTION(CallInEditor)
        void LogSample();

public:
        UPROPERTY(EditAnywhere)
        float Mean;

        UPROPERTY(EditAnywhere)
        float StandardDeviation;

// 新增部门
#if WITH_EDITORONLY_DATA
        UPROPERTY(VisibleAnywhere)
        FString SourceFilePath;
#endif

private:
        std::mt19937 RandomNumberGenerator;
};
然后创建新工厂。在 CustomDataTypeEditor 文件夹下创建“CustomNormalDistributionImportFactory.h”和“CustomNormalDistributionImportFactory.cpp”。



我们需要重写虚函数 FactoryCreateText() 和 FactoryCanImport(),GetValueFromFile() 是一个辅助函数,辅佐我们从文件读取参数。
// CustomNormalDistributionImportFactory.h

#pragma once

#include ”CoreMinimal.h”
#include ”Factories/Factory.h”
#include ”CustomNormalDistributionImportFactory.generated.h”

UCLASS()
class UCustomNormalDistributionImportFactory : public UFactory
{
        GENERATED_BODY()
public:
        UCustomNormalDistributionImportFactory();

        virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override;
        virtual bool FactoryCanImport(const FString& Filename) override;

        FString GetValueFromFile(const TCHAR*& Buffer, FString SectionName, FString VarName);
};
首先,我们需要在构造函数里设定 UCustomNormalDistributionImportFactory 的行为。我们添加一个可导入的后缀名 .cnd,并在 FactoryCanImport() 中进行判断。我们假设数据的文件格式与 .ini 文件类似,它具有一些 Section,每个 Section 下有名称分歧的参数,且 Section 之间可能具有同名参数,就像这样:



这样的好处是,由于我们使用了与 .ini 文件同样的格式,我们可以使用 ConfigCacheIni.h 中的现有的功能来读取文件。当然也可以替换为任意的自定义法则。
// CustomNormalDistributionImportFactory.cpp

#include ”CustomNormalDistributionImportFactory.h”
#include ”CustomNormalDistribution.h”
#include ”EditorFramework/AssetImportData.h”
// #include ”Misc/ConfigCacheIni.h”

UCustomNormalDistributionImportFactory::UCustomNormalDistributionImportFactory()
{
        SupportedClass = UCustomNormalDistribution::StaticClass();

        // 必需封锁可新建
        // 添加可导入的文件名后缀
        // 开启可导入
        // 导入的文件格式为 Text(另一种格式为二进制)
        bCreateNew = false;
        Formats.Add(TEXT(”cnd;Custom Normal Distribution”));
        bEditorImport = true;
        bText = true;
}

UObject* UCustomNormalDistributionImportFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName,
        EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd,
        FFeedbackContext* Warn)
{

        GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetPreImport.Broadcast(this, InClass, InParent, InName, Type);

        // 查抄传入的类型和后缀名
        if (InClass != UCustomNormalDistribution::StaticClass()
                || FCString::Stricmp(Type, TEXT(”cnd”)) != 0) return nullptr;

        UCustomNormalDistribution* Data = CastChecked<UCustomNormalDistribution>(
                NewObject<UCustomNormalDistribution>(InParent, InName, Flags));

        // 从文件获取值
        Data->Mean = FCString::Atof(*GetValueFromFile(Buffer, ””, ”Mean”));
        Data->StandardDeviation = FCString::Atof(*GetValueFromFile(Buffer, ””, ”StandardDeviation”));

        // 从文件获取值的另一种方式。我们特意将文件内容的书写格式与.ini相似,所以也可以借用.ini方式措置。
        // FConfigCacheIni Config(EConfigCacheType::Temporary);
        // Config.LoadFile(CurrentFilename);
        // Config.GetFloat(TEXT(”MySection”), TEXT(”Mean”), Data->Mean, CurrentFilename);
        // Config.GetFloat(TEXT(”MySection”), TEXT(”StandardDeviation”), Data->StandardDeviation, CurrentFilename);

        // 储存导入的路径
        Data->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, Data->GetPackage());

        GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetPostImport.Broadcast(this, Data);
       
        return Data;
}

bool UCustomNormalDistributionImportFactory::FactoryCanImport(const FString& Filename)
{
        return FPaths::GetExtension(Filename).Equals(TEXT(”cnd”));
}

FString UCustomNormalDistributionImportFactory::GetValueFromFile(const TCHAR*& Buffer, FString SectionName, FString VarName)
{
        FString Str(Buffer);

        Str = Str.Replace(TEXT(”\r”), TEXT(””));
       
        TArray<FString> Lines;
        Str.ParseIntoArray(Lines, TEXT(”\n”), true);
       
        bool bInSection = false;
       
        for (FString Line : Lines)
        {
                if (Line == SectionName)
                {
                        bInSection = true;
                }
                else if (Line.StartsWith(”[”) && Line.EndsWith(”]”))
                {
                        bInSection = false;
                }

                if (bInSection)
                {
                        int32 Pos = Line.Find(”=”);
                        if (Pos != INDEX_NONE)
                        {
                                FString Name = Line.Left(Pos);
                                FString Value = Line.Mid(Pos + 1);

                                if (Name == VarName)
                                {
                                        return Value;
                                }
                        }
                }
        }
       
        return ””;
}
编译并重启编纂器,拖拽我们的文件到 ContentBrowser 或使用面板上的导入按钮,此刻就可以导入本身的 .cnd 文件了。



TODO:导入时对话框



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

重导入自定义资产类型

重导入需要实现三个虚函数:CanReimport(),SetReimportPaths() 和 Reimport()。我们可以将重导入的逻辑写在之前的 UCustomNormalDistributionImportFactory 工厂里。



来到 CustomNormalDistributionImportFactory.h 拓展 UCustomNormalDistributionImportFactory 工厂类。
// CustomNormalDistributionImportFactory.h

#pragma once

#include ”CoreMinimal.h”
#include ”Factories/Factory.h”
#include ”EditorReimportHandler.h” // 新增头文件
#include ”CustomNormalDistributionImportFactory.generated.h”

UCLASS()
class UCustomNormalDistributionImportFactory : public UFactory, public FReimportHandler
{
        GENERATED_BODY()
public:
        UCustomNormalDistributionImportFactory();

        virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override;
        virtual bool FactoryCanImport(const FString& Filename) override;

        FString GetValueFromFile(const TCHAR*& Buffer, FString SectionName, FString VarName);

        // 新增 Reimporter 虚函数实现
        virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
        virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
        virtual EReimportResult::Type Reimport(UObject* Obj) override;
};
与导入文件分歧,重导入时我们要使用辅助类 FileHelper 来本身读取文件内容。
// CustomNormalDistributionImportFactory.cpp

#include ”Misc/FileHelper.h”

bool UCustomNormalDistributionImportFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
{
        UCustomNormalDistribution* Data = Cast<UCustomNormalDistribution>(Obj);
        if (Data)
        {
                OutFilenames.Add(UAssetImportData::ResolveImportFilename(
                        Data->SourceFilePath, Data->GetPackage()));
                return true;
        }
        return false;
}

void UCustomNormalDistributionImportFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
{
        UCustomNormalDistribution* Data = Cast<UCustomNormalDistribution>(Obj);
        if (Data && ensure(NewReimportPaths.Num() == 1))
        {
                Data->SourceFilePath =
                        UAssetImportData::SanitizeImportFilename(NewReimportPaths, Data->GetPackage());
        }
}

EReimportResult::Type UCustomNormalDistributionImportFactory::Reimport(UObject* Obj)
{
        UCustomNormalDistribution* Data = Cast<UCustomNormalDistribution>(Obj);
        if (!Data)
        {
                return EReimportResult::Failed;
        }

        const FString Filename =
                UAssetImportData::ResolveImportFilename(Data->SourceFilePath, Data->GetPackage());
        if (!FPaths::GetExtension(Filename).Equals(TEXT(”cnd”)))
        {
                return EReimportResult::Failed;
        }

        CurrentFilename = Filename;
        FString LoadedData;
        if (FFileHelper::LoadFileToString(LoadedData, *CurrentFilename))
        {
                const TCHAR* LoadedDataChar = *LoadedData;
                Data->Modify();
                Data->MarkPackageDirty();

                Data->Mean = FCString::Atof(*GetValueFromFile(LoadedDataChar, ””, ”Mean”));
                Data->StandardDeviation =
                        FCString::Atof(*GetValueFromFile(LoadedDataChar, ””, ”StandardDeviation”));

                Data->SourceFilePath =
                        UAssetImportData::SanitizeImportFilename(CurrentFilename, Data->GetPackage());
        }

        return EReimportResult::Succeeded;
}
然后我们给 UCustomNormalDistribution 资产的右键菜单添加一个“Reimport”按钮,调用 UCustomNormalDistributionImportFactory::Reimport。来到 UCustomNormalDistributionActions.h 中添加如下函数。
// UCustomNormalDistributionActions.h

class FCustomNormalDistributionActions : public FAssetTypeActions_Base
{
public:
        ...
        void ExecuteReimport(TArray<TWeakObjectPtr<class UCustomNormalDistribution>> Objects);

...
};
实现 ExecuteReimport()。
// UCustomNormalDistributionActions.cpp

#include ”EditorReimportHandler.h”

void FCustomNormalDistributionActions::ExecuteReimport(TArray<TWeakObjectPtr<UCustomNormalDistribution>> Objects)
{
        for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt)
        {
                auto Object = (*ObjIt).Get();
                if (Object)
                {
                        FReimportManager::Instance()->Reimport(Object, /*bAskForNewFileIfMissing=*/true);
                }
        }
}
编译并重启编纂器,导入一个 .cnd 文件资产,就可以通过右键菜单中的 Reimport 进行重导入了。

TODO:从新文件重导入资产



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

导出自定义资产类型

我们可以担任一个 UExporter 类,来定义自定义数据类型的导出操作。



在 CustomDataTypeEditor 文件夹下新建“CustomNormalDistributionExporter.h”和“CustomNormalDistributionExporter.cpp”文件。



与 UFactory 类似,我们需要在构造函数中设定 UCustomNormalDistributionExporter 的行为,并重写 SupportsObject() 和 ExportText() 两个虚函数。
此处使用 ExportText(),因为我们但愿直接将数据保留为 .cnd 文件,如果要保留为二进制,则可以使用 ExportBinary()。
// CustomNormalDistributionExporter.h

#pragma once

#include ”CoreMinimal.h”
#include ”Exporters/Exporter.h”
#include ”CustomNormalDistributionExporter.generated.h”

UCLASS()
class UCustomNormalDistributionExporter : public UExporter
{
        GENERATED_BODY()
       
public:

        UCustomNormalDistributionExporter();
       
        virtual bool SupportsObject(UObject* Object) const override;
        virtual bool ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags) override;
       
};
要使用 ExportText(),则需要将 bText 设置为 true,否则默认不会调用 ExportText() 而是 ExportBinary()。
// CustomNormalDistributionExporter.cpp

#pragma once

#include ”CustomNormalDistributionExporter.h”
#include ”CustomNormalDistribution.h”

UCustomNormalDistributionExporter::UCustomNormalDistributionExporter()
{
        SupportedClass = UCustomNormalDistribution::StaticClass();
        PreferredFormatIndex = 0;
        FormatExtension.Add(TEXT(”cnd”));
        FormatDescription.Add(TEXT(”Custom Normal Distribution”));
        bText = true;
}

bool UCustomNormalDistributionExporter::SupportsObject(UObject* Object) const
{
        return (SupportedClass && Object->IsA(SupportedClass));
}

bool UCustomNormalDistributionExporter::ExportText(const FExportObjectInnerContext* Context, UObject* Object,
        const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags)
{
        UCustomNormalDistribution* Data = Cast<UCustomNormalDistribution>(Object);

        if (!Data)
        {
                return false;
        }
       
        // 输出内容
        Ar.Log(TEXT(”\r\n”));
        Ar.Logf(TEXT(”Mean=%f\r\n”), Data->Mean);
        Ar.Logf(TEXT(”StandardDeviation=%f”), Data->StandardDeviation);
       
        return true;
}
此刻编译并重启编纂器,任意改削一个 UCustomNormalDistribution 资产,并导出,可以看到它被正确导出到磁盘了。



页: [1]
查看完整版本: 《调教UE5:编纂器拓展指南》自定义数据类型