实践UE的ComponentVisualizer(组件可视化)的基础功能

ComponentVisualizer作用

通过ComponentVisualizer,你可以将一些非渲染的数据可视化出来,在编辑器视窗中画一些图形,并通过鼠标点击和按键等与之交互进而操纵它们。

引擎中一个好的例子就是SplineComponent。你看到的白线,点击的顶点都是由ComponentVisualizer提供的:
在这里插入图片描述

本篇内容

FComponentVisualizer定义如下:

/** Base class for a component visualizer, that draw editor information for a particular component class */
class UNREALED_API FComponentVisualizer : public TSharedFromThis<FComponentVisualizer>
{
public:
	FComponentVisualizer() {}
	virtual ~FComponentVisualizer() {}

	/** */
	virtual void OnRegister() {}
	/** Only show this visualizer if the actor is selected */
	virtual bool ShowWhenSelected() { return true; }
	/** Draw visualization for the supplied component */
	virtual void DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI) {}
	/** Draw HUD on viewport for the supplied component */
	virtual void DrawVisualizationHUD(const UActorComponent* Component, const FViewport* Viewport, const FSceneView* View, FCanvas* Canvas) {}
	/** */
	virtual bool VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click) { return false; }
	/** */
	virtual void EndEditing() {}
	/** */
	virtual bool GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const { return false; }
	/** */
	virtual bool GetCustomInputCoordinateSystem(const FEditorViewportClient* ViewportClient, FMatrix& OutMatrix) const { return false; }
	/** */
	virtual bool HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltalRotate, FVector& DeltaScale) { return false; }
	/** */
	virtual bool HandleInputKey(FEditorViewportClient* ViewportClient,FViewport* Viewport,FKey Key,EInputEvent Event) { return false; }
	/** Handle click modified by Alt, Ctrl and/or Shift. The input HitProxy may not be on this component. */
	virtual bool HandleModifiedClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) { return false; }
	/** Handle box select input */
	virtual bool HandleBoxSelect(const FBox& InBox, FEditorViewportClient* InViewportClient,FViewport* InViewport) { return false; }
	/** Handle frustum select input */
	virtual bool HandleFrustumSelect(const FConvexVolume& InFrustum, FEditorViewportClient* InViewportClient, FViewport* InViewport) { return false; }
	/** Return whether focus on selection should focus on bounding box defined by active visualizer */
	virtual bool HasFocusOnSelectionBoundingBox(FBox& OutBoundingBox) { return false; }
	/** Pass snap input to active visualizer */
	virtual bool HandleSnapTo(const bool bInAlign, const bool bInUseLineTrace, const bool bInUseBounds, const bool bInUsePivot, AActor* InDestination) { return false;  }
	/** Get currently edited component, this is needed to reset the active visualizer after undo/redo */
	virtual UActorComponent* GetEditedComponent() const { return nullptr;  }

	/** */
	virtual TSharedPtr<SWidget> GenerateContextMenu() const { return TSharedPtr<SWidget>(); }
	/** */
	virtual bool IsVisualizingArchetype() const { return false; }

	// So deprecated code expecting this as an inner class still works
	using FPropertyNameAndIndex = ::FPropertyNameAndIndex;

	/** Find the name of the property that points to this component */
	UE_DEPRECATED(4.24, "Please use the FComponentPropertyPath class to build property name paths for components.")
	static FPropertyNameAndIndex GetComponentPropertyName(const UActorComponent* Component);

	/** Get a component pointer from the property name */
	UE_DEPRECATED(4.24, "Please use the FComponentPropertyPath::GetComponent() to retrieve a component pointer from a property name path.")
	static UActorComponent* GetComponentFromPropertyName(const AActor* CompOwner, const FPropertyNameAndIndex& Property);

	/** Notify that a component property has been modified */
	static void NotifyPropertyModified(UActorComponent* Component, FProperty* Property, EPropertyChangeType::Type PropertyChangeType = EPropertyChangeType::Unspecified);

	/** Notify that many component properties have been modified */
	static void NotifyPropertiesModified(UActorComponent* Component, const TArray<FProperty*>& Properties, EPropertyChangeType::Type PropertyChangeType = EPropertyChangeType::Unspecified);
};

而需要做的,就是创建它一个子类,并override所需的虚函数。
本篇的内容基本上就是从零开始创建一个ComponentVisualizer,并实践其基础功能。(引擎版本UE5.0EA)

0. 准备环境(新建插件和创建Component)

Blank为模板创建一个插件。这里我将插件命名为TestCompVis
在这里插入图片描述


“组件可视化”是针对某一类组件的可视化,因此这里我新创建了一个名为UYaksueComponent的组件作为测试。其代码YaksueComponent.hYaksueComponent.cpp放在了TestCompVis\Private目录下。

YaksueComponent.h内容如下:

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Components/ActorComponent.h"

#include "YaksueComponent.generated.h"


UCLASS(meta = (BlueprintSpawnableComponent))
class UYaksueComponent : public UActorComponent
{
	GENERATED_BODY()
};

而初始的YaksueComponent.cpp文件还不需要任何内容,所以保持为空白。

编译代码后。现在在为Actor添加组件的时候,可以看到YaksueComponent了:
在这里插入图片描述
(注意:如果在添加组件时找不到新创建的组件,可以检查代码在定义组件时所写的宏有没有BlueprintSpawnableComponent

1. 创建ComponentVisualizer并注册

先创建一个空白的ComponentVisualizer,YaksueComponentVisualizer.h内容如下:

#pragma once

#include "ComponentVisualizer.h"

class FYaksueComponentVisualizer : public FComponentVisualizer
{
};

由于继承了FComponentVisualizer了,而它是UnrealEd模块中的类,所以需要在插件模块中指明依赖这个模块。即在TestCompVis.Build.csPrivateDependencyModuleNames中添加 "UnrealEd"
在这里插入图片描述否则,编译时就会出现报错“无法解析的外部符号”。


接下来需要在模块的启动函数中注册,将组件可视化与组件对应起来。

由于用到了GUnrealEd,所以需要包含文件:

#include "UnrealEdGlobals.h"
#include "Editor/UnrealEdEngine.h"

然后,在FTestCompVisModule::StartupModule()中添加代码来注册:

TSharedPtr<FYaksueComponentVisualizer> Visualizer = MakeShareable(new FYaksueComponentVisualizer);
GUnrealEd->RegisterComponentVisualizer(UYaksueComponent::StaticClass()->GetFName(), Visualizer);
Visualizer->OnRegister();

对应的,在FTestCompVisModule::ShutdownModule()中添加代码来注销:

GUnrealEd->UnregisterComponentVisualizer(UYaksueComponent::StaticClass()->GetFName());

如果没问题的话,编译后引擎应该可以正常启动。


不过我这里出了一个问题:
在这里插入图片描述
经调试,发现是这里的GUnrealEdNULL导致的。这个问题在这里也有讨论,其解决方法在我这里也奏效,即将插件的加载阶段修改成PostEngineInit
在这里插入图片描述
我有些奇怪地是。之前我也创建过几次ComponentVisualizer,但是并没有遇到这个问题。所以现在还不清楚这一步是否是必做的。

2. 绘制编辑时图形(DrawVisualization)

组件可视化所显示的图形是在DrawVisualization函数中完成的。
因此需要override这个虚函数,并调用PDI(FPrimitiveDrawInterface)的接口。比如画线就是DrawLine

下面作为测试,我要画个四面体:
在这里插入图片描述
代码上,就是定义四个点的位置,并在它们之间画线:

void FYaksueComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
	//四面体的四个顶点:
	FVector Pos[4] = { FVector(0,0,100),FVector(100,0,0), FVector(-50,-86,0),FVector(-50,86,0) };
	//在所有顶点之间画线:
	for (int i = 0; i < 4; i++)
		for (int j = i + 1; j < 4; j++)
			PDI->DrawLine(
				Pos[i],						//起点
				Pos[j],						//终点
				FLinearColor(0, 1, 1, 1),	//颜色
				SDPG_Foreground);			//前景
}

运行编辑器后,创建YaksueComponent组件,应该会看到场景中心的青色四面体:
在这里插入图片描述
(注意:由于当前代码的逻辑中并没有考虑Actor的位置,所以四面体的位置应该会永远在场景中心)

3. 通过ComponentVisProxy接收点击

我想要接收四面体的四个角被点击的事件,而要达成这个目标大致需要三步:

3.1 定义HitProxy

首先,我需要针对顶点被点击的行为,定义一个HComponentVisProxy类型(在YaksueComponentVisualizer.h中):

struct HYaksuePointVisProxy : public HComponentVisProxy
{
	DECLARE_HIT_PROXY();
	HYaksuePointVisProxy(const UActorComponent* InComponent, int32 InPointIndex);
	//记录哪一个点:
	int32 PointIndex;
};

这个proxy可以在你点击的时候收集一些信息。在这里,我记录了被点击的顶点的序号。
然后在YaksueComponentVisualizer.cpp中使用宏(第一个参数是新定义的类名,第二个参数是基类名):

IMPLEMENT_HIT_PROXY(HYaksuePointVisProxy, HComponentVisProxy);

这个类的构造函数的内容如下:

HYaksuePointVisProxy::HYaksuePointVisProxy(const UActorComponent * InComponent, int32 InPointIndex)
	: HComponentVisProxy(
		InComponent,	//组件
		HPP_Wireframe),	//优先级
	PointIndex(InPointIndex)
{
}

3.2 放置HitProxy

接下来,要在DrawVisualization中为各个想点击的内容来设置proxy。方式是:

  1. 先调用PDI->SetHitProxy,参数是新创建一个proxy,在构造函数中可以传入想记录的信息。
  2. 使用PDI绘制想接收点击事件的图形。
  3. 最后调用PDI->SetHitProxy,参数是NULL,表明设置结束。

我这里,就是画四个点并并设置Proxy:

for (int i = 0; i < 4; i++)
{
	//开始设置Proxy
	PDI->SetHitProxy(new HYaksuePointVisProxy(Component, i));
	
	//画点:
	PDI->DrawPoint(
		Pos[i],						//位置
		FLinearColor(1, 0, 0, 1),	//颜色
		20.f,						//大小
		SDPG_Foreground);			//前景
	
	//结束设置Proxy
	PDI->SetHitProxy(NULL);
}

3.3 接收HitProxy

当proxy被点击时,事件会传入VisProxyHandleClick函数中,可以在此对其做后续处理。

当前,我只是log出了被点击的点的序号

bool FYaksueComponentVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click)
{
	const HYaksuePointVisProxy* Proxy = (HYaksuePointVisProxy*)VisProxy;

	//测试输出所点击的点的序号
	UE_LOG(LogYaksueComponentVisualizer, Warning, TEXT("click point : %d"), Proxy->PointIndex);

	return true;
}

(注意,我这里的log需要先声明宏DEFINE_LOG_CATEGORY_STATIC(LogYaksueComponentVisualizer, Log, All);

接下来,就可以在编辑器中看到效果了:
在这里插入图片描述

4. 指定操纵器控件的位置(GetWidgetLocation)

接下来我想可以操纵四面体的顶点比如平移。但是在此之前,我需要指定操纵器控件的位置——他应该在所选择的顶点上。

为此,我需要存储一个“当前所选顶点序号”到组件上。另外,我还要将之前的四个点位置变成组件的成员,而非DrawVisualization中的局部变量:

class UYaksueComponent : public UActorComponent
{
	GENERATED_UCLASS_BODY()

public:
	TArray<FVector> Points;		//点的位置
	int32 SelectingPointIndex;	//正在选择的点序号
};

然后在其构造中指定四个点的位置(代码暂时省略)。
而DrawVisualization中也需要调整(代码暂时省略)。


然后,我需要知道当前组件可视化的目标是哪个组件。我这里的逻辑是:通过ComponentVisProxy中点击的proxy上的组件来判断。

于是,我的ComponentVisProxy代码改动如下:

bool FYaksueComponentVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click)
{
	const HYaksuePointVisProxy* Proxy = (HYaksuePointVisProxy*)VisProxy;

	//设置当前正在编辑的组件:
	CurrentEditingComponent = (UYaksueComponent*)Proxy->Component.Get();

	//设置当前所选的点的序号
	CurrentEditingComponent->SelectingPointIndex = Proxy->PointIndex;
	
	//测试输出所点击的点的序号
	//UE_LOG(LogYaksueComponentVisualizer, Warning, TEXT("click point : %d"), Proxy->PointIndex);

	return true;
}

以上准备就绪后,就可以对GetWidgetLocation这个虚函数进行override了。当前的逻辑就是让所选点的位置作为结果:

bool FYaksueComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const
{
	//位置就是当前所选点的位置:
	OutLocation = CurrentEditingComponent->Points[CurrentEditingComponent->SelectingPointIndex];

	return true;
}

效果:
在这里插入图片描述

5. 处理操纵器控件的输入(HandleInputDelta)

处理操纵器控件的输入只要对HandleInputDelta进行override就可以了。

在这里,我处理了操纵器控件在位移上的变化,并将位移变化加到原始的顶点位置上。

bool FYaksueComponentVisualizer::HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltalRotate, FVector& DeltaScale)
{
	//处理位移:
	if (!DeltaTranslate.IsZero())
	{
		//当前所选的顶点加上位移:
		CurrentEditingComponent->Points[CurrentEditingComponent->SelectingPointIndex] += DeltaTranslate;
	}

	return true;
}

效果如下:
在这里插入图片描述

最终代码

TestCompVis.h

#pragma once

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

class FTestCompVisModule : public IModuleInterface
{
public:

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

TestCompVis.cpp

#include "TestCompVis.h"
#include "YaksueComponentVisualizer.h"
#include "YaksueComponent.h"

#include "UnrealEdGlobals.h"
#include "Editor/UnrealEdEngine.h"

#define LOCTEXT_NAMESPACE "FTestCompVisModule"

void FTestCompVisModule::StartupModule()
{
	// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module

	//注册组件可视化:
	TSharedPtr<FYaksueComponentVisualizer> Visualizer = MakeShareable(new FYaksueComponentVisualizer);
	GUnrealEd->RegisterComponentVisualizer(UYaksueComponent::StaticClass()->GetFName(), Visualizer);
	Visualizer->OnRegister();
}

void FTestCompVisModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.

	//注销组件可视化:
	GUnrealEd->UnregisterComponentVisualizer(UYaksueComponent::StaticClass()->GetFName());
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FTestCompVisModule, TestCompVis)

YaksueComponent.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Components/ActorComponent.h"

#include "YaksueComponent.generated.h"


UCLASS(meta = (BlueprintSpawnableComponent))
class UYaksueComponent : public UActorComponent
{
	GENERATED_UCLASS_BODY()

public:
	TArray<FVector> Points;		//点的位置
	int32 SelectingPointIndex;	//正在选择的点序号
};

YaksueComponent.cpp

#include"YaksueComponent.h"

UYaksueComponent::UYaksueComponent(const FObjectInitializer & ObjectInitializer)
	: Super(ObjectInitializer)
{
	//四个顶点位置:
	Points.Add(FVector(0, 0, 100));
	Points.Add(FVector(100, 0, 0));
	Points.Add(FVector(-50, -86, 0));
	Points.Add(FVector(-50, 86, 0));
}

YaksueComponentVisualizer.h

#pragma once

#include "ComponentVisualizer.h"

class FYaksueComponentVisualizer : public FComponentVisualizer
{
public:

	class UYaksueComponent* CurrentEditingComponent;//当前正在编辑的组件,应在VisProxyHandleClick中设置。

	//绘制组件可视化的图形:
	virtual void DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI) override;
	
	//接收点击事件
	virtual bool VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click) override;

	//指定操纵器控件的位置
	virtual bool GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const override;

	//处理操纵器控件的输入
	virtual bool HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltalRotate, FVector& DeltaScale) override;
};

//针对点的VisProxy
struct HYaksuePointVisProxy : public HComponentVisProxy
{
	DECLARE_HIT_PROXY();
	HYaksuePointVisProxy(const UActorComponent* InComponent, int32 InPointIndex);
	//记录哪一个点:
	int32 PointIndex;
};

YaksueComponentVisualizer.cpp

#include"YaksueComponentVisualizer.h"
#include"YaksueComponent.h"

DEFINE_LOG_CATEGORY_STATIC(LogYaksueComponentVisualizer, Log, All);
//UE_LOG(LogYaksueComponentVisualizer, Warning, TEXT("log %s"),);

IMPLEMENT_HIT_PROXY(HYaksuePointVisProxy, HComponentVisProxy);

HYaksuePointVisProxy::HYaksuePointVisProxy(const UActorComponent * InComponent, int32 InPointIndex)
	: HComponentVisProxy(
		InComponent,	//组件
		HPP_Wireframe),	//优先级
	PointIndex(InPointIndex)
{
}

void FYaksueComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
	//将组件类型转换为UYaksueComponent
	const UYaksueComponent* comp = (UYaksueComponent*)Component;

	//在所有顶点之间画线:
	for (int i = 0; i < 4; i++)
		for (int j = i + 1; j < 4; j++)
			PDI->DrawLine(
				comp->Points[i],			//起点
				comp->Points[j],			//终点
				FLinearColor(0, 1, 1, 1),	//颜色
				SDPG_Foreground);			//前景

	//绘制四个顶点并放置Proxy
	for (int i = 0; i < 4; i++)
	{
		//开始设置Proxy
		PDI->SetHitProxy(new HYaksuePointVisProxy(Component, i));
		
		//画点:
		PDI->DrawPoint(
			comp->Points[i],						//位置
			FLinearColor(1, 0, 0, 1),	//颜色
			20.f,						//大小
			SDPG_Foreground);			//前景
		
		//结束设置Proxy
		PDI->SetHitProxy(NULL);
	}
}

bool FYaksueComponentVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click)
{
	const HYaksuePointVisProxy* Proxy = (HYaksuePointVisProxy*)VisProxy;

	//设置当前正在编辑的组件:
	CurrentEditingComponent = (UYaksueComponent*)Proxy->Component.Get();

	//设置当前所选的点的序号
	CurrentEditingComponent->SelectingPointIndex = Proxy->PointIndex;
	
	//测试输出所点击的点的序号
	//UE_LOG(LogYaksueComponentVisualizer, Warning, TEXT("click point : %d"), Proxy->PointIndex);

	return true;
}

bool FYaksueComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const
{
	//位置就是当前所选点的位置:
	OutLocation = CurrentEditingComponent->Points[CurrentEditingComponent->SelectingPointIndex];

	return true;
}

bool FYaksueComponentVisualizer::HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltalRotate, FVector& DeltaScale)
{
	//处理位移:
	if (!DeltaTranslate.IsZero())
	{
		//当前所选的顶点加上位移:
		CurrentEditingComponent->Points[CurrentEditingComponent->SelectingPointIndex] += DeltaTranslate;
	}

	return true;
}

参考资料

Component Visualizers | Unreal Engine Community Wiki

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值