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.h
和YaksueComponent.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.cs
的PrivateDependencyModuleNames
中添加 "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());
如果没问题的话,编译后引擎应该可以正常启动。
不过我这里出了一个问题:
经调试,发现是这里的GUnrealEd
为NULL
导致的。这个问题在这里也有讨论,其解决方法在我这里也奏效,即将插件的加载阶段修改成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。方式是:
- 先调用
PDI->SetHitProxy
,参数是新创建一个proxy,在构造函数中可以传入想记录的信息。 - 使用
PDI
绘制想接收点击事件的图形。 - 最后调用
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;
}