《调教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]