1. Preface

Have you ever thought about how does Unreal Engine implements the editor tool of USplineComponent? In this post we introduce an editor tool for creating custom gizmo in preview scene for our actor components.

2. Component Visualizer

We begin with a custom component given below, it’s a quiet simple component:

C++
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class YourModule_API UVisualComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	// Sets default values for this component's properties
	UVisualComponent();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FVector Point1{100, 0, 0};

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FVector Point2{0, 100, 0};

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FVector Point3{0, 0, 100};

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
	                           FActorComponentTickFunction* ThisTickFunction) override;
};
C++

There are three points here, our mission is draw gizmos for these points and connect them with lines. If simply want to create transform widgets for them, you can use the meta property MakeEditWidget:

C++
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(MakeEditWidget))
FVector Point1{100, 0, 0};
C++

Then we need to create a editor module. Note that it must be loaded in phase PostEngineInit instead of Default.

JSON
{
			"Name": "YourEditorModule",
			"Type": "Editor",
			"LoadingPhase": "PostEngineInit" // Not "Default"
		}
JSON

Then, inherits from the FComponentVisualizer and implements the interface of it, don’t forget to add the Component Visualizer module reference:

C++
class YourEditorModule_API FMyComponentVisualizer : public FComponentVisualizer
{
protected:

	bool bEditingOriginalPoint{false};

	int32 EditIndex{0};
	
public:

	virtual void OnRegister() override;

	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& DeltaRotate, FVector& DeltaScale) override;

	virtual UActorComponent* GetEditedComponent() const override {return VisualEditingComponent;}

	TObjectPtr<UActorComponent> VisualEditingComponent;
};
C++
C#
PrivateDependencyModuleNames.AddRange(
            new string[]
            {
                "CoreUObject",
                "Engine",
                "Slate",
                "SlateCore",
                "UnrealEd",
                "ComponentVisualizers"
            }
        );
C#

We start with the interface DrawVisualization, it will be called after you clicked the actor which holds your component. Which means it represents what will happed after you clicking the actor in preview scene:

C++
void FMyComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View,
	FPrimitiveDrawInterface* PDI)
{
	FComponentVisualizer::DrawVisualization(Component, View, PDI);
	VisualEditingComponent = const_cast<UActorComponent*>(Component);

	if (const UVisualComponent* VisualComponent = Cast<UVisualComponent>(Component))
	{
		const AActor* OwnerActor = VisualComponent->GetOwner();

		static float PointThickness{15.0f};

		PDI->DrawPoint(VisualComponent->Point1 /** World Location */,	FLinearColor::Red,	 PointThickness, SDPG_Foreground /** Depth Priority Group */);
		PDI->DrawPoint(VisualComponent->Point2,							FLinearColor::Blue,	 PointThickness, SDPG_Foreground);
		PDI->DrawPoint(VisualComponent->Point3,							FLinearColor::Green, PointThickness, SDPG_Foreground);
	}
}
C++

Then register it when the module is loaded:

C++
class FYourEditorModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};

void FYourEditorModule::StartupModule()
{
    const TSharedPtr<FMyComponentVisualizer> Visualizer = MakeShareable(new FMyComponentVisualizer);
    if (Visualizer.IsValid())
    {
        GUnrealEd->RegisterComponentVisualizer(UVisualComponent::StaticClass()->GetFName(), Visualizer);
        Visualizer->OnRegister();
    }
}

void FYourEditorModule::ShutdownModule()
{
    GUnrealEd->UnregisterComponentVisualizer(UVisualComponent::StaticClass()->GetFName());
}
C++

Build your project and open the editor, you can gain the following result, I used world location here instead of relative location:

2. Interactive with the points

To implement the click event we implement this method of Component Visualizer:

C++
virtual bool VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click) override;
C++

This method uses a pointer of HComponentVisProxy as the parameter, as a result, we can implement our own Component visualizer component. It is only used in editor, so implement this in the editor module:

C++
struct HVisualHitProxy final : public HComponentVisProxy
{
	DECLARE_HIT_PROXY()

public:
	explicit HVisualHitProxy(const UActorComponent* InComponent, const int InIndex): HComponentVisProxy(InComponent)
	{
		VisualComponent = Cast<UVisualComponent>(InComponent);
		Index = InIndex;
		check(VisualComponent != nullptr)
	}

	const UVisualComponent* VisualComponent;

	int32 Index{};
};
C++

After implement the declation in .h file, use the macro in your .cpp file:

C++
IMPLEMENT_HIT_PROXY(HVisualHitProxy, HComponentVisProxy)
C++

Modify our method of DrawVisualiztion methods as:

C++
void FMyComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View,
	FPrimitiveDrawInterface* PDI)
{
	FComponentVisualizer::DrawVisualization(Component, View, PDI);
	VisualEditingComponent = const_cast<UActorComponent*>(Component);

	if (const UVisualComponent* VisualComponent = Cast<UVisualComponent>(Component))
	{
		const AActor* OwnerActor = VisualComponent->GetOwner();

		static float PointThickness{15.0f};

		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 0));
		PDI->DrawPoint(VisualComponent->Point1 /** World Location */,	FLinearColor::Red,	 PointThickness, SDPG_Foreground /** Depth Priority Group */);
		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 1));
		PDI->DrawPoint(VisualComponent->Point2,							FLinearColor::Blue,	 PointThickness, SDPG_Foreground);
		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 2));
		PDI->DrawPoint(VisualComponent->Point3,							FLinearColor::Green, PointThickness, SDPG_Foreground);
		PDI->SetHitProxy(nullptr);
	}
}
C++

Here also reveals why we use PDI->DrawPoint instead of DrawDebugLine in DrawDebugHelpers.h as usual. the former method consumes the hit proxy to generate the click-raycast:

C++
inline void FViewElementPDI::DrawPoint(
	const FVector& Position,
	const FLinearColor& Color,
	float PointSize,
	uint8 DepthPriorityGroup
	)
{
	float ScaledPointSize = PointSize;

	bool bIsPerspective = (ViewInfo->ViewMatrices.GetProjectionMatrix().M[3][3] < 1.0f) ? true : false;
	if( !bIsPerspective )
	{
		const float ZoomFactor = FMath::Min<float>(View->ViewMatrices.GetProjectionMatrix().M[0][0], View->ViewMatrices.GetProjectionMatrix().M[1][1]);
		ScaledPointSize = ScaledPointSize / ZoomFactor;
	}

	FBatchedElements &Elements = GetElements(DepthPriorityGroup);

	Elements.AddPoint(
		Position,
		ScaledPointSize,
		Color,
		CurrentHitProxy ? CurrentHitProxy->Id : FHitProxyId()
	);
}
C++

By this way, we create the click hit proxy with data that indicates which point we have clicked, in editor, if we clicked the proxy, the method FMyComponentVisualizer::VisProxyHandleClick will be called, and the parameter of the method is the hit proxy we created before:

C++
bool FMyComponentVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy,
	const FViewportClick& Click)
{
	const HVisualHitProxy* Proxy = static_cast<HVisualHitProxy*>(VisProxy);
	if (Proxy == nullptr)
	{
		return false;
	}

	EditIndex = Proxy->Index;
	switch (Proxy->Index)
	{
	case 0:
		UE_LOG(LogTemp, Log, TEXT("Index0"));
		break;
	case 1:
		UE_LOG(LogTemp, Log, TEXT("Index1"));
		break;
	case 2:
		UE_LOG(LogTemp, Log, TEXT("Index3"));
		break;
	default:
		UE_LOG(LogTemp, Log, TEXT("Impossible"));
		break;
	}

	return true;
}
C++

Click the point and now you can get the correct log info, but wait, where is our gizmo to edit there position? This requires the method as followed:

C++
bool FComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const 
C++
C++
bool FMyComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const
{
	if (const UVisualComponent* VisualComponent = Cast<UVisualComponent>(GetEditedComponent()))
	{
		switch (EditIndex)
		{
		case 0:
			OutLocation = VisualComponent->Point1;
			break;
		case 1:
			OutLocation = VisualComponent->Point2;
			break;
		case 2:
			OutLocation = VisualComponent->Point3;
			break;
		default:
			UE_LOG(LogTemp, Log, TEXT("Impossible"));
			return false;
		}
		return true;
	}
	return false;
}
C++

Note that GetWidgetLocation should not change the actual location of the point, otherwise it will be hard to use when dealing with situation that the points indicated for relative location. Actually, we modify the location of the points in methods HandleInputDelta. The methods use the input delta of the widget to modift the location of the point, which finally moves the points.

As you can seen now we draw the widget but the gizmo actually cannot control the location of the point, we use the following method to handle this:

C++
bool FMyComponentVisualizer::HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport,
	FVector& DeltaTranslate, FRotator& DeltaRotate, FVector& DeltaScale)
{
	if (!DeltaTranslate.IsNearlyZero())
	{
		if (UVisualComponent* VisualComponent = Cast<UVisualComponent>(GetEditedComponent()))
		{
			switch (EditIndex)
			{
			case 0:
				VisualComponent->Point1 += DeltaTranslate;
				break;
			case 1:
				VisualComponent->Point2 += DeltaTranslate;
				break;
			case 2:
				VisualComponent->Point3 += DeltaTranslate;
				break;
			default:
				UE_LOG(LogTemp, Log, TEXT("Impossible"));
				return false;
			}
			return true;
		}
	}

	return false;
}
C++

Now it is perfect:

As we said before, we add lines between the points:

C++
void FMyComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View,
	FPrimitiveDrawInterface* PDI)
{
	FComponentVisualizer::DrawVisualization(Component, View, PDI);
	VisualEditingComponent = const_cast<UActorComponent*>(Component);

	if (const UVisualComponent* VisualComponent = Cast<UVisualComponent>(Component))
	{
		const AActor* OwnerActor = VisualComponent->GetOwner();

		static float PointThickness{15.0f};
		static float LineThickness{5.0f};

		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 0));
		PDI->DrawPoint(VisualComponent->Point1 /** World Location */,	FLinearColor::Red,	 PointThickness, SDPG_Foreground /** Depth Priority Group */);
		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 1));
		PDI->DrawPoint(VisualComponent->Point1,							FLinearColor::Blue,	 PointThickness, SDPG_Foreground);
		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 2));
		PDI->DrawPoint(VisualComponent->Point3,							FLinearColor::Green, PointThickness, SDPG_Foreground);
		PDI->SetHitProxy(nullptr);

		PDI->DrawLine(VisualComponent->Point1, VisualComponent->Point2, FLinearColor::White, SDPG_Foreground);
		PDI->DrawLine(VisualComponent->Point2, VisualComponent->Point3, FLinearColor::White, SDPG_Foreground);
		PDI->DrawLine(VisualComponent->Point3, VisualComponent->Point1, FLinearColor::White, SDPG_Foreground);
	}
}
C++

There are also many other images:

What to draw some complex geometry? Use the following(Requires the module RenderCore):

C++
...
		PDI->SetHitProxy(new HVisualHitProxy(VisualComponent, 2));
		PDI->DrawPoint(VisualComponent->Point3,							FLinearColor::Green, PointThickness, SDPG_Foreground);
		PDI->SetHitProxy(nullptr);

		
		PDI->DrawLine(VisualComponent->Point1, VisualComponent->Point2, FLinearColor::White, SDPG_Foreground);
		PDI->DrawLine(VisualComponent->Point2, VisualComponent->Point3, FLinearColor::White, SDPG_Foreground);
		PDI->DrawLine(VisualComponent->Point3, VisualComponent->Point1, FLinearColor::White, SDPG_Foreground);

		auto* Proxy = new FDynamicColoredMaterialRenderProxy(GEngine->GeomMaterial->GetRenderProxy(), FLinearColor::Yellow);
		PDI->RegisterDynamicResource(Proxy);
		DrawSphere(PDI, VisualComponent->Point3, FRotator::ZeroRotator, {10, 10, 10}, 16, 16, Proxy, SDPG_Foreground);
	}
}
C++

It supports:


Reference

Component Visualizers | Unreal Engine Community Wiki
Component visualizers allow you to visualize and edit otherwise non-rendering component data in the editor viewport.
unrealcommunity.wiki
Visualize Actor Components in the Editor with Component Visualizers – Matt’s Game Dev Notebook
unrealist.org
实践UE的ComponentVisualizer(组件可视化)的基础功能_ue class可视-CSDN博客
文章浏览阅读2.1k次。ComponentVisualizer作用通过ComponentVisualizer,你可以将一些非渲染的数据可视化出来,在编辑器视窗中画一些图形,并通过鼠标点击和按键等与之交互进而操纵它们。引擎中一个好的例子就是SplineComponent。你看到的白线,点击的顶点都是由ComponentVisualizer提供的:本篇内容FComponentVisualizer定义如下:/** Base class for a component visualizer, that draw editor _ue class可视
blog.csdn.net

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

One thought on “Visual Component : Add custom gizmo to your components”
  1. Wow, marvelous weblog layout! How lengthy have you ever been running a blog for? you make running a blog glance easy. The full look of your website is excellent, as well as the content!

Leave a Reply

Your email address will not be published. Required fields are marked *