UE4自定义资产类型编辑器实现

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

推荐:用 NSDT设计器 快速搭建可编程3D场景。

在本教程中,我们将编写一个插件,将自定义资源类型添加到引擎中。 我们的资产类型将代表我们可以从中抽取样本的正态分布。 我们将设置一个编辑器来显示分布的概率密度函数 (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() 中更改资源工具包的默认布局,则需要在虚幻编辑器中重新加载默认布局以检查您的更改,因为布局会被缓存。 要重置虚幻编辑器布局,请单击自定义资源编辑器顶部菜单栏中的“窗口>加载布局>默认编辑器布局”。
在这里插入图片描述

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


原文链接:虚幻引擎自定义资产类型 — BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/32342.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL语言的四大组成部分——DCL(数据控制语言)

1️⃣前言 SQL语言中的DCL&#xff08;Data Control Language&#xff09;是一组用于控制数据库用户访问权限的语言&#xff0c;主要包括GRANT、REVOKE、DENY等关键字。 文章目录 1️⃣前言2️⃣DCL语言3️⃣GRANT关键字4️⃣REVOKE关键字5️⃣DENY关键字6️⃣总结附&#xff1…

【kubernetes】部署网络组件Calico与CoreDNS

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机配置开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

基于YOLO V8的车牌识别

赵春江 2023年6月 1、前言 十年前就想实现车牌识别这项任务&#xff0c;虽然当时这项技术就已较成熟&#xff08;与现在的实现方法不同&#xff09;&#xff0c;但那时的我还具备这个能力。弹指一瞬间&#xff0c;没想到十年间人工智能技术已经发展到一个新的高度&#xff0c…

Nacos架构与原理 - 健康检查机制

文章目录 注册中心的健康检查机制Nacos 健康检查机制临时实例健康检查机制永久实例健康检查机制集群模式下的健康检查机制 注册中心的健康检查机制 想象发生地质灾害,被掩埋在废墟下,搜救队需定位才能施救。两种方法: 大喊求救,告知位置与健康状况,让搜救队知晓搜救队使用专业…

社区活动 | OpenVINO™ DevCon 中国系列工作坊第二期 | 使用 OpenVINO™ 加速生成式 AI...

生成式 AI 领域一直在快速发展&#xff0c;许多潜在应用随之而来&#xff0c;这些应用可以从根本上改变人机交互与协作的未来。这一最新进展的一个例子是 GPT 模型的发布&#xff0c;它具有解决复杂问题的能力&#xff0c;比如通过医学和法律考试这种类似于人类的能力。然而&am…

CnOpenData数字经济专利申请与授权数据

一、数据简介 自人类社会进入信息时代以来&#xff0c;数字技术的快速发展和广泛应用衍生出数字经济。与农耕时代的农业经济、工业时代的工业经济大有不同&#xff0c;数字经济是一种新的经济、新的动能、新的业态&#xff0c;并引发了社会和经济的整体性深刻变革。现阶段&…

openEuler操作系统禁用 Nouveau

目录 一、什么是openEuler 二、什么是Nouveau 三、禁用Nouveau Liunx系统安装NVIDIA显卡驱动时需要禁用Nouveau&#xff0c;openEuler操作系统也不例外&#xff0c;但是网上openEuler操作系统如何禁用Nouveau的资料比较少&#xff0c;而且基本都不靠谱&#xff0c;我找到一个…

Keras-深度学习-神经网络-人脸识别模型

目录 模型搭建 模型训练 模型搭建 ①导入所需的库&#xff0c;导入了 Keras 和其他必要的库&#xff0c;用于构建和处理图像数据。 from keras.models import Sequential from keras.layers import Dense, Flatten, Conv2D, MaxPooling2D import os from PIL import Image …

streamlit——搭建学生评分网站(告别问卷星)

streamlit搭建多人评分网站 文章目录 streamlit搭建多人评分网站一、引言二、数据准备三、streamlit代码四、数据合并代码 一、引言 当需要对班级内多人进行打分时&#xff0c;为了不使用问卷星等平台进行评分&#xff0c;使用pandas进行操作数据&#xff0c;使用streamlit进行…

chatgpt赋能python:Python要点:从入门到精通

Python要点&#xff1a;从入门到精通 Python是一门高级编程语言&#xff0c;是一种解释型、面向对象、动态数据类型的语言。它的设计思想是“代码易读易写”&#xff0c;在数据科学、人工智能、自动化测试、Web开发等领域广泛应用。本文将从入门到精通的角度来介绍Python的要点…

内网穿透技术

文章目录 前言1. 安装JAVA2. MCSManager安装3.局域网访问MCSM4.创建我的世界服务器5.局域网联机测试6.安装cpolar内网穿透7. 配置公网访问地址8.远程联机测试9. 配置固定远程联机端口地址9.1 保留一个固定tcp地址9.2 配置固定公网TCP地址9.3 使用固定公网地址远程联机 转载自内…

自学黑客(网络安全),一般人我劝你还是算了吧(自学网络安全学习路线--第九章 Internet安全协议)【建议收藏】

文章目录 一、自学网络安全学习的误区和陷阱二、学习网络安全的一些前期准备三、自学网络安全学习路线一、安全协议概述二、IPSec协议1、概述2、IP封装过程3、IPSec不安全性4、IPSec的功能5、IPSec体系结构6、IPSec的AH7、IPSec的AH8、IPSec的ESP9、IPSec的ESP10、ISAKMP11、IK…

中职网络搭建(服务器)—Linux LVM(标准答案)

题目要求如下 使用fdisk-l查看磁盘信息 我们添加的磁盘分别是sdb和sdc Fdisk /dev/sdb 依次输入n,p,1&#xff0c;回车,2G &#xff08;新建2G的主分区&#xff09; 依次输入n,e,2&#xff0c;回车&#xff0c;回车&#xff08;使用剩余的全部空间建立扩展分区&#xff09; 依…

高德地图的使用

JS API 结合 Vue 使用 高德地图 jsapi 下载、引入 npm add amap/amap-jsapi-loaderimport AMapLoader from amap/amap-jsapi-loader 使用2.0版本的loader需要在window对象下先配置 securityJsCode JS API 安全密钥使用 JS API 使用 script 标签同步加载增加代理服务器设置…

数据挖掘(6.1)--神经网络

目录 神经网络简介 BP算法 Delta学习规则的基本原理 BP神经网络的结构 BP神经网络的算法描述 神经网络训练一般步骤 后向传播算法的主要步骤 优缺点 BP算法简单举例 神经网络简介 神经网络是一种计算模型&#xff0c;它受到人脑处理信息的生物神经网络过程的启发。人…

第44步 深度学习图像识别:ResNet50建模(Tensorflow)

基于WIN10的64位系统演示 一、写在前面 &#xff08;1&#xff09;ResNet50 ResNet50是一种深度学习模型&#xff0c;由微软研究院的研究人员在2015年提出。"ResNet"的全称是"Residual Network"&#xff0c;意为"残差网络"&#xff0c;"…

React 基本介绍

目录 1、React是什么 2、React 三大颠覆性的特点 2.1 组件 2.2 JSX 2.3 Virtual DOM 3、Flux 架构&#xff08;redux&#xff09; 3.1 Flux 3.2 redux 4、打包工具&#xff08;webpack&#xff09; 4.1 webpack与RequireJS、browserify 4.2 模块规范 4.3 非 JavaSc…

实验篇(7.2) 17. 站对站安全隧道 - FortiGate作为SSL客户端(SSL) ❀ 远程访问

【简介】虽然常用的站到站的连接用的是IPsec VPN&#xff0c;但是在某些特殊情况下&#xff0c;UDP500或4500端口被阻断&#xff0c;IPsec VPN无法连接&#xff0c;那么还有其它办法实现站到站的连接吗&#xff1f;SSL VPN也可以的。 实验要求与环境 OldMei集团深圳总部部署了域…

【云原生】二进制k8s集群(下)部署高可用master节点

本次部署说明 在上一篇文章中&#xff0c;就已经完成了二进制k8s集群部署的搭建&#xff0c;但是单机master并不适用于企业的实际运用&#xff08;因为单机master中&#xff0c;仅仅只有一台master作为节点服务器的调度指挥&#xff0c;一旦宕机。就意味着整个集群的瘫痪&#…

【软考网络管理员】2023年软考网管初级常见知识考点(23)- 路由器的配置

涉及知识点 华为路由器的配置&#xff0c;华为路由器命令大全&#xff0c;软考大纲路由命令&#xff0c;静态路由和动态路由的配置命令&#xff0c;软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;CSDN博主-…