|
《调教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[0]);
- 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, ”[MySection]”, ”Mean”));
- Data->StandardDeviation = FCString::Atof(*GetValueFromFile(Buffer, ”[MySection]”, ”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[0], 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, ”[MySection]”, ”Mean”));
- Data->StandardDeviation =
- FCString::Atof(*GetValueFromFile(LoadedDataChar, ”[MySection]”, ”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(”[MySection]\r\n”));
- Ar.Logf(TEXT(”Mean=%f\r\n”), Data->Mean);
- Ar.Logf(TEXT(”StandardDeviation=%f”), Data->StandardDeviation);
-
- return true;
- }
复制代码 此刻编译并重启编纂器,任意改削一个 UCustomNormalDistribution 资产,并导出,可以看到它被正确导出到磁盘了。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|