NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

在虚幻引擎中,资产是具有持久属性的对象,可以在编辑器中进行操作。 Unreal 附带多种资源类型,从 UStaticMesh 到 UMetasoundSources 等等。 自定义资源类型是实现专门对象的好方法,这些对象需要专门构建的编辑器来进行高效操作。 通过在插件中实现这些类型,它们可以在项目和开发人员之间轻松共享。

在本教程中,我们将编写一个插件,将自定义资源类型添加到引擎中。 我们的资产类型将代表我们可以从中抽取样本的正态分布。 我们将设置一个编辑器来显示分布的概率密度函数 (PFD),并让我们同时编辑其平均值和标准差。

2、创建插件

要继续操作,请打开一个空白的 C++ Unreal 游戏项目。 首先导航到顶部菜单栏中的“编辑”>“插件”,然后单击对话框窗口左上角的“添加”。 选择“Blank”插件模板,输入名称“AssetTutorialPlugin”,然后单击“创建插件”。

插件创建完成后,切换到 Visual Studio。 应出现一个对话框,要求你重新加载修改后的解决方案。 单击“重新加载全部”并在出现提示时停止调试。 如果创建插件时 Visual Studio 未打开,请打开项目文件夹,右键单击 .uproject 文件并单击“生成 Visual Studio 项目文件”,然后打开生成的 .sln 文件。

在 Visual Studio 中,在解决方案资源管理器中找到项目的 Plugins 文件夹。 它应该具有如上所示的结构。 .uplugin 文件包含有关您的插件的信息以及启用插件时要加载的模块列表。 模块包含代码和编译设置(在模块的 .Build.cs 文件中设置)。

3、添加编辑器模块

Unreal 为我们创建了一个与我们的插件同名的模块。 它在我们的 .uplugin 文件中作为运行时模块列出。 为了实现我们的自定义资产编辑器,我们需要一个未加载到打包游戏中的附加编辑器模块。

在文件资源管理器中打开项目文件夹,导航到 “Plugins\AssetTutorialPlugin\Source” 并创建“AssetTutorialPlugin”模块文件夹的副本。 将副本重命名为“AssetTutorialPluginEditor”,并将所有文件名和文件内容中出现的所有“AssetTutorialPlugin”替换为“AssetTutorialPluginEditor”。 然后导航回项目的根文件夹,右键单击 .uproject 文件并重新生成 Visual Studio 项目文件。 打开 .uplugin 文件并编辑“模块”列表以包含新的编辑器模块,如下所示。

	"Modules": [
		{
			"Name": "AssetTutorialPlugin",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		},
		{
			"Name": "AssetTutorialPluginEditor",
			"Type": "Editor",
			"LoadingPhase": "Default"
		}
	]

4、创建自定义资产类型

通过在 Visual Studio 中构建和调试项目并将构建配置设置为“开发编辑器”来重新启动虚幻编辑器,然后导航到顶部菜单栏中的“工具>新建 C++ 类...”。 切换到对话框顶部的“All Classes”,然后选择“Object”作为父类。 单击“下一步”,将“Class Type”设置为“Public”,输入“NormalDistribution”作为名称,然后从名称输入字段旁边的下拉菜单中选择“AssetTutorialPlugin (Runtime)”作为目标模块。 然后点击“创建班级”。 创建完成后,切换回 Visual Studio 并重新加载解决方案(出现提示时停止调试)。

我们现在将声明并定义我们的自定义资产类型。 在此步骤中,你决定资产类型应具有哪些属性以及它支持哪些操作。 出于本教程的目的,我们将创建一个简单的资产类型,允许使用 std::normal_distribution 从具有给定均值和标准差的正态分布中抽取样本。

5、声明自定义资产类型

打开新创建的“NormalDistribution.h”以声明自定义资源类型,如下所示。

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include <random>
#include "NormalDistribution.generated.h"

UCLASS(BlueprintType)
class ASSETTUTORIALPLUGIN_API UNormalDistribution : public UObject
{
	GENERATED_BODY()
public:
	UNormalDistribution();

	UPROPERTY(EditAnywhere)
	float Mean;

	UPROPERTY(EditAnywhere)
	float StandardDeviation;

	UFUNCTION(BlueprintCallable)
	float DrawSample();

	UFUNCTION(CallInEditor)
	void LogSample();
private:
	std::mt19937 RandomNumberGenerator;
};

我们的自定义资产类型的声明方式与任何其他 UObject 派生类类似,因此我们包含 . generated.h 文件并确保调用 UCLASS() 和 GENERATED_BODY() 宏。

6、定义自定义资产类型

现在打开“NormalDistribution.cpp”来定义自定义资产类型的功能,如下所示。

#include "NormalDistribution.h"

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

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

void UNormalDistribution::LogSample()
{
    UE_LOG(LogTemp, Log, TEXT("%f"), DrawSample())
}

Unreal 现在可以识别我们的 NormalDistribution 类型,正如你在构建项目并重新启动 Unreal 编辑器时看到的那样,然后打开“Tools>Class Viewer”,确保取消选中“Actors Only”过滤器并搜索“NormalDistribution” 。 但是,我们还无法通过在内容浏览器中右键单击来创建 NormalDistribution 资源。 为了实现这一点,我们需要将 UNormalDistribution 注册为资产类型并提供一个工厂来创建新实例。

7、注册自定义资产类型

再次打开“工具>新建 C++ 类...”对话框。 这次,选择“None”作为父类,将“Class Type”设置为“Public”,将类命名为“NormalDistributionActions”,并选择“AssetTutorialPluginEditor(编辑器)”作为目标模块。 然后单击“创建类”并像以前一样返回到 Visual Studio。

我们需要实现一个继承自 IAssetTypeActions 的类来向引擎注册我们的资产类型。 通过重写界面的方法,我们可以设置资产在编辑器的内容浏览器中的外观和行为。 我们可以选择名称、类别、颜色、右键单击资产时上下文菜单的操作等。

打开“NormalDistributionActions.h”为我们的资产类型声明资产类型操作,如下所示。 请注意,类名称为“FNormalDistributionAssetTypeActions”,以符合 Unreal 命名约定。

#pragma once

#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"

class FNormalDistributionAssetTypeActions : public FAssetTypeActions_Base
{
public:
	UClass* GetSupportedClass() const override;
	FText GetName() const override;
	FColor GetTypeColor() const override;
	uint32 GetCategories() override;
};

使用“NormalDistributionActions.cpp”定义资产类型操作的函数,如下所示。

#include "NormalDistributionActions.h"
#include "NormalDistribution.h"

UClass* FNormalDistributionAssetTypeActions::GetSupportedClass() const
{
    return UNormalDistribution::StaticClass();
}

FText FNormalDistributionAssetTypeActions::GetName() const
{
    return INVTEXT("Normal Distribution");
}

FColor FNormalDistributionAssetTypeActions::GetTypeColor() const
{
    return FColor::Cyan;
}

uint32 FNormalDistributionAssetTypeActions::GetCategories()
{
    return EAssetTypeCategories::Misc;
}

8、注册资产类型操作

FNormalDistributionAssetTypeActions 不是从 UObject 派生的,因此引擎和编辑器不知道它的存在。 我们需要手动将其注册到引擎的AssetToolsModule中。 由于我们希望只要插件处于活动状态,我们的自定义资源类型就可以在编辑器中使用,因此手动注册的好地方是 FAssetTutorialPluginEditorModule 类的 StartupModule() 函数。 当首次加载模块并调用 StartupModule() 函数时,将创建此类型的唯一对象。 因此我们可以使用它来执行模块范围的设置和注册。 当模块关闭时,我们还将取消注册资产类型操作。

打开“AssetTutorialPluginEditor.h”来声明我们的编辑器模块类,如下所示。

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "NormalDistributionActions.h"

class FAssetTutorialPluginEditorModule : public IModuleInterface
{
public:
	void StartupModule() override;
	void ShutdownModule() override;
private:
	TSharedPtr<FNormalDistributionAssetTypeActions> NormalDistributionAssetTypeActions;
};

打开“AssetTutorialPluginEditor.cpp”来定义我们的编辑器模块类的函数,如下所示。

#include "AssetTutorialPluginEditor.h"

void FAssetTutorialPluginEditorModule::StartupModule()
{
	NormalDistributionAssetTypeActions = MakeShared<FNormalDistributionAssetTypeActions>();
	FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(NormalDistributionAssetTypeActions.ToSharedRef());
}

void FAssetTutorialPluginEditorModule::ShutdownModule()
{
	if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
	FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(NormalDistributionAssetTypeActions.ToSharedRef());
}

IMPLEMENT_MODULE(FAssetTutorialPluginEditorModule, AssetTutorialPluginEditor)

9、添加模块依赖项

现在编译将会失败,因为我们的资产类型操作类位于 AssetTutorialPluginEditor 模块中,而我们的 UNormalDistribution 则始终位于 AssetTutorialPlugin 模块中。 我们需要添加运行时模块作为编辑器模块的依赖项。 此外,我们需要添加对 UnrealEd 模块的依赖项,这是注册资产类型操作所需的。

打开“AssetTutorialPluginEditor.Build.cs”并编辑以“PrivateDependencyModuleNames.AddRange(...)”开头的语句,如下所示。

		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				"AssetTutorialPlugin",
				"UnrealEd"
				// ... add private dependencies that you statically link with here ...	
			}
			);

10、创建工厂

在内容浏览器中右键单击以创建新资产时,我们的正态分布资产类型仍然没有显示。 但我们已经快到了! 再次在虚幻编辑器中使用“Tools>New C++ Class...”,切换到“All Classes”,搜索“factory”并选择“Factory”作为父类。 您可能需要折叠一些层次结构,例如“ActorFactory”,直到仅出现“Factory”。 选择“Public”,将其命名为“NormalDistributionFactory”,然后选择“AssetTutorialPluginEditor(编辑器)”作为目标模块。 创建类并切换回 Visual Studio。

从 UFactory 派生的类用于指定所选资产类型的创建或导入逻辑。 有些工厂在创建资产时会打开一个对话框来收集用户的设置。 我们将创建一个最小工厂,当请求资产类型的新实例时,它基本上只是包装对 NewObject() 的调用。

打开“NormalDistributionFactory.h”并声明我们的工厂类型,如下所示。

#pragma once

#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "NormalDistributionFactory.generated.h"

UCLASS()
class UNormalDistributionFactory : public UFactory
{
    GENERATED_BODY()
public:
    UNormalDistributionFactory();
    UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn);
};

打开“NormalDistributionFactory.cpp”并定义构造函数和 FactoryCreateNew(),如下所示。 在构造函数中将 bCreateNew 设置为 true 将允许我们在内容浏览器中创建我们类型的资源!

#include "NormalDistributionFactory.h"
#include "NormalDistribution.h"

UNormalDistributionFactory::UNormalDistributionFactory()
{
    SupportedClass = UNormalDistribution::StaticClass();
    bCreateNew = true;
}

UObject* UNormalDistributionFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
    return NewObject<UNormalDistribution>(InParent, Class, Name, Flags, Context);
}

UNormalDistributionFactory 确实派生自 UObject,因此当加载其模块时,其 UCLASS 会自动注册到引擎。 编译并运行虚幻编辑器,在内容浏览器中右键单击,导航至“Miscellaneous>Normal Distribution”,看看我们已经取得了什么成果。

创建一个新的正态分布资产并双击它以打开其编辑器。 你将看到默认的资产编辑器,它仅显示可用于编辑资产属性的详细信息视图。 请注意,我们有一个“Log Sample”按钮,因为我们将“CallInEditor”说明符添加到 UNormalDistribution 中 LogSample 函数的 UFUNCTION() 声明中。 你可以使用平均值和标准差,按“记录样本”并检查输出日志以验证我们的资产是否按预期工作。

11、创建资产编辑器

我想使用概率分布函数的交互式图来编辑我的正态分布。 为此,我们需要创建一个 Slate 小部件来绘制一些线条,然后我们需要让引擎知道我们要在资源编辑器中使用它。

12、创建交互式 PDF 绘图板小部件

“Tools>New C++ Class...”,“None”作为父类,设置为“Public”,命名为“SNormalDistributionWidget”,选择“AssetTutorialPluginEditor(编辑器)”作为目标模块。 创建类并切换回 Visual Studio。

Slate 是 Unreal 的 UI 框架,可用于在应用程序的窗口中定位和绘制交互式文本、线条、纹理、材质等。 Unreal 附带了大量的小部件,从处理布局的面板小部件(如 SHorizontalBox)到显示(可能是动态)内容的 STextBlock 之类的叶小部件。

现在,我们将创建一个叶子小部件,它将显示 PDF 绘图,并让我们通过在其上拖动鼠标来编辑分布。 打开“SNormalDistributionWidget.h”并声明我们的小部件,如下所示。

#pragma once

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

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

class SNormalDistributionWidget : public SLeafWidget
{
public:
	SLATE_BEGIN_ARGS(SNormalDistributionWidget)
		: _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()

	void Construct(const FArguments& InArgs);

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

	FReply OnMouseButtonDown(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override;
	FReply OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override;
	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;
};

让我们对上面的声明进行一些详细说明。 我们首先声明一些委托类型:FOnMeanChanged 和 FOnStandardDeviationChanged。 这些类型的对象可以绑定到其他对象的成员函数,当我们的小部件触发某些事件时,这些对象会做出反应。 通过使用委托,我们的小部件与 UNormalDistribution 实现保持分离。

我们继续声明我们的 SLeafWidget 派生类型,利用一些 Slate 宏来使用 Slate 的声明语法来实例化我们的小部件。 像 Mean 和 StandardDeviation 这样的 Slate 属性也可以使用委托对象进行初始化,这样我们就可以在需要时轮询其他对象来获取这些值。

当我们的小部件在声明性 Synatx 中实例化时,Slate 会调用 Construct() 成员函数。 其余的公共函数重写虚拟 SWidget 成员函数来定义我们的小部件的行为。 Slate 会给我们分配一个特定的 FGeometry,它代表屏幕上我们可以绘制的一个矩形。 它会考虑我们想要的尺寸,但我们不能依赖分配的几何形状为该尺寸。 我们希望绘图有一个动态边距,能够响应分配的几何图形,这就是 GetPointsTransform() 的目的。

接下来,打开“SNormalDistributionWidget.cpp”来定义我们的小部件的功能,如下所示。

#include "SNormalDistributionWidget.h"
#include "Editor.h"

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

int32 SNormalDistributionWidget::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 SNormalDistributionWidget::ComputeDesiredSize(float) const
{
    return FVector2D(200.0, 200.0);
}

FReply SNormalDistributionWidget::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 SNormalDistributionWidget::OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent)
{
    if (GEditor) GEditor->EndTransaction();
    return FReply::Handled().ReleaseMouseCapture();
}

FReply SNormalDistributionWidget::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 SNormalDistributionWidget::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);
}

上面显示的实现的一些细节:在 OnPaint() 中,我们传递了一个“OutDrawElements”列表,我们可以向其中添加文本、线条等来构建视觉表示。 我们的 PDF 在多个点进行评估,x 值范围从 0 到 1。计算出的“PointTransform”负责将点从其原始空间放置到“AllottedGeometry”指定的空间。 使用 FSlateDrawElement::MakeLines() 添加将变换后的点连接到绘制元素的线后,我们只需返回“LayerId”,因为我们只绘制 1 层元素。

为了在通过将鼠标拖动到小部件上来设置平均值和标准差时启用撤消/重做,我们分别在 OnMouseButtonDown() 和 OnMouseButtonUp() 中的 GEditor 上使用 BeginTransaction() 和 EndTransaction()。 我们还捕获任何单击的鼠标,直到释放鼠标按钮,这样即使在拖出小部件时我们也可以编辑我们的分布。

在 OnMouseMove() 中,我们仅在当前捕获鼠标时更新分布。 平均值和标准偏差是根据鼠标的位置计算的,考虑到分配的几何形状和我们当前的点变换,然后在它们被绑定时调用事件处理程序。

最后,GetPointsTransform() 会考虑动态边距并翻转 y 轴,因为 Slate 小部件的原点位于左上角。

13、创建资产编辑器工具包

现在我们有了一个小部件,我们仍然需要在打开正态分布资产编辑器时显示它。 有两种方法可以解决此问题:要么保留显示正在编辑的资产的详细信息视图的默认资产编辑器,要么创建并注册从 IDetailCustomization 派生的类。 这样的类可以将交互式 PDF 图添加到正态分布的所有详细视图中。 另一种方法是创建一个从 FAssetEditorToolkit 派生的类,并使用我们的自定义资产编辑器覆盖默认资产编辑器。 我们将采用后一种方式,因为它使我们能够在默认资产编辑器布局中包含输出日志。

最后一次,运行虚幻编辑器,转到“Tools>New C++ Class...”,选择“None”作为父级,将其设置为“Public”,将其命名为“NormalDistributionEditorToolkit”并选择“AssetTutorialPluginEditor (Editor)” 作为目标模块。 创建类并切换回 Visual Studio。

使用我们自己的资产编辑器工具包,我们可以定义资产编辑器的布局并注册选项卡生成器,这些选项卡生成器用于使用包含我们选择的小部件的选项卡填充我们的布局。

打开“NormalDistributionEditorToolkit.h”来声明我们的资产编辑器工具包类,如下所示。

#pragma once

#include "CoreMinimal.h"
#include "NormalDistribution.h"
#include "Toolkits/AssetEditorToolkit.h"

class FNormalDistributionEditorToolkit : public FAssetEditorToolkit
{
public:
	void InitEditor(const TArray<UObject*>& InObjects);

	void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
	void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;

	FName GetToolkitFName() const override { return "NormalDistributionEditor"; }
	FText GetBaseToolkitName() const override { return INVTEXT("Normal Distribution Editor"); }
	FString GetWorldCentricTabPrefix() const override { return "Normal Distribution "; }
	FLinearColor GetWorldCentricTabColorScale() const override { return {}; }

	float GetMean() const;
	float GetStandardDeviation() const;
	void SetMean(float Mean);
	void SetStandardDeviation(float StandardDeviation);
private:
	UNormalDistribution* NormalDistribution;
};

我们将在 InitEditor() 中创建布局,并在相应的函数中(取消)注册选项卡生成器。 我们还为 FAssetEditorToolkit 的纯虚拟成员函数提供重写。 请注意,我们不会在以世界为中心的模式下使用此编辑器。 此外,我们保留一个指向我们正在编辑的 UNormalDistribution 的简单指针。 请注意,一般来说,这样的指针是危险的,因为垃圾收集器可能会在我们不注意的情况下销毁 UObject 派生类的该对象。 但在这种情况下,可以安全地假设只要编辑器打开,资产就会保留在内存中,并且如果在编辑时删除资产,编辑器会自动关闭。 我们的工具包中还有一些资产属性的 getter 和 setter,我们将使用它们绑定到小部件的委托参数。

现在打开“NormalDistributionEditorToolkit.cpp”来定义我们的工具包的函数,如下所示。

#include "NormalDistributionEditorToolkit.h"
#include "Widgets/Docking/SDockTab.h"
#include "SNormalDistributionWidget.h"
#include "Modules/ModuleManager.h"

void FNormalDistributionEditorToolkit::InitEditor(const TArray<UObject*>& InObjects)
{
	NormalDistribution = Cast<UNormalDistribution>(InObjects[0]);

	const TSharedRef<FTabManager::FLayout> Layout = FTabManager::NewLayout("NormalDistributionEditorLayout")
	->AddArea
	(
		FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
		->Split
		(
			FTabManager::NewSplitter()
			->SetSizeCoefficient(0.6f)
			->SetOrientation(Orient_Horizontal)
			->Split
			(
				FTabManager::NewStack()
				->SetSizeCoefficient(0.8f)
				->AddTab("NormalDistributionPDFTab", ETabState::OpenedTab)
			)
			->Split
			(
				FTabManager::NewStack()
				->SetSizeCoefficient(0.2f)
				->AddTab("NormalDistributionDetailsTab", ETabState::OpenedTab)
			)
		)
		->Split
		(
			FTabManager::NewStack()
			->SetSizeCoefficient(0.4f)
			->AddTab("OutputLog", ETabState::OpenedTab)
		)
	);
	FAssetEditorToolkit::InitAssetEditor(EToolkitMode::Standalone, {}, "NormalDistributionEditor", Layout, true, true, InObjects);
}

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

	WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(INVTEXT("Normal Distribution Editor"));

	InTabManager->RegisterTabSpawner("NormalDistributionPDFTab", FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&)
	{
		return SNew(SDockTab)
		[
			SNew(SNormalDistributionWidget)
			.Mean(this, &FNormalDistributionEditorToolkit::GetMean)
			.StandardDeviation(this, &FNormalDistributionEditorToolkit::GetStandardDeviation)
			.OnMeanChanged(this, &FNormalDistributionEditorToolkit::SetMean)
			.OnStandardDeviationChanged(this, &FNormalDistributionEditorToolkit::SetStandardDeviation)
		];
	}))
	.SetDisplayName(INVTEXT("PDF"))
	.SetGroup(WorkspaceMenuCategory.ToSharedRef());

	FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
	FDetailsViewArgs DetailsViewArgs;
	DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
	TSharedRef<IDetailsView> DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
	DetailsView->SetObjects(TArray<UObject*>{ NormalDistribution });
	InTabManager->RegisterTabSpawner("NormalDistributionDetailsTab", FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&)
	{
		return SNew(SDockTab)
		[
			DetailsView
		];
	}))
	.SetDisplayName(INVTEXT("Details"))
	.SetGroup(WorkspaceMenuCategory.ToSharedRef());
}

void FNormalDistributionEditorToolkit::UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
	FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
	InTabManager->UnregisterTabSpawner("NormalDistributionPDFTab");
	InTabManager->UnregisterTabSpawner("NormalDistributionDetailsTab");
}

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

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

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

void FNormalDistributionEditorToolkit::SetStandardDeviation(float StandardDeviation)
{
	NormalDistribution->Modify();
	NormalDistribution->StandardDeviation = StandardDeviation;
}

上面 InitEditor() 的实现首先从 InObjects 参数获取正在编辑的资源对象。 请注意,编辑器可以支持同时编辑多个资源。 我们的示例编辑器没有,只是抓取数组中的第一个对象。 然后我们使用 FTabManager::NewLayout() 定义布局。 使用 AddTab() 添加选项卡时,我们使用自己的选项卡类型:“NormalDistributionPDFTab”和“NormalDistributionDetailsTab”。 我们的工具包还提供了这些选项卡类型名称的选项卡生成器。 引擎已提供“OutputLog”类型的选项卡生成器。 InitEditor() 最后调用父类的 InitAssetEditor() 函数,该函数将处理繁重的工作。

在定义 RegisterTabSpawners() 和 UnregisterTabSpawners() 时,我们也要注意调用父类的实现,因为它们包含一些逻辑。 然后我们传递一些 lambda 委托,这些委托在调用时使用 Slate 的声明性语法简单地创建 SDockTab 及其内容。 创建 SNormalDistributionWidget 时,我们将资产属性的 getter 和 setter 传递给“NormalDistributionWidget.h”中声明的相应属性和事件。 我们还通过对 RegisterTabSpawner() 返回值进行链接函数调用来设置选项卡的名称和组。 我们不仅为 PDF 小部件注册一个选项卡生成器,还为基本详细信息视图注册一个选项卡生成器,我们可以使用 FPropertyEditorModule 创建该视图。

设置器实现中的一个重要细节是对资产对象上的Modify() 的调用。 这会弄脏资产,以便我们可以保存更改,并确保事务缓冲区在编辑之前填充对象的状态,从而启用撤消/重做。

14、使用我们的资产编辑器工具包

差不多了。 当请求资产编辑器时,我们仍然需要创建工具包类的实例。 打开“NormalDistributionActions.h”并将以下成员函数声明添加到 FNormalDistributionAssetTypeActions。

	void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor) override;

接下来,打开“NormalDistributionActions.cpp”并添加以下行来定义 OpenAssetEditor()。

#include "NormalDistributionEditorToolkit.h"

void FNormalDistributionAssetTypeActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor)
{
    MakeShared<FNormalDistributionEditorToolkit>()->InitEditor(InObjects);
}

你可能想知道 MakeShared() 返回的临时 TSharedRef。 创建的 FNormalDistributionEditorToolkit 不会在 TSharedRef 超出范围时被销毁,因为它通过 FAssetEditorToolkit 从 TSharedFromThis 派生,并且当它完全初始化时,它会在其他地方引用。

现在我们的资产类型操作已经设置完毕,以便在打开资产编辑器时使用我们的资产编辑器工具包,我们可以构建并返回虚幻编辑器,创建正态分布资产并通过单击并拖动其图来编辑其属性 概率分布函数。 如果我们想检查它的行为方式,可以单击详细信息选项卡中的“Log Sample”按钮,将采样值写入输出日志。

请注意,如果你在 InitEditor() 中更改资源工具包的默认布局,则需要在虚幻编辑器中重新加载默认布局以检查您的更改,因为布局会被缓存。 要重置虚幻编辑器布局,请单击自定义资源编辑器顶部菜单栏中的“窗口>加载布局>默认编辑器布局”。

本教程的完整代码可从这里获取。


原文链接:Creating a Custom Asset Type with its own Editor in C++

BimAnt翻译整理,转载请标明出处