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:
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
:
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.
{
"Name": "YourEditorModule",
"Type": "Editor",
"LoadingPhase": "PostEngineInit" // Not "Default"
}
JSONThen, inherits from the FComponentVisualizer
and implements the interface of it, don’t forget to add the Component Visualizer module reference:
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++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:
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:
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
:
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:
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:
IMPLEMENT_HIT_PROXY(HVisualHitProxy, HComponentVisProxy)
C++Modify our method of DrawVisualiztion methods as:
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:
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:
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:
bool FComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const
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:
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:
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
):
...
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:
DrawPlane10x10
DrawTriangle
DrawBox
DrawSphere
DrawCone
DrawCylinder
DrawTorus
DrawDisc
DrawRectangleMesh
DrawFlatArrow
DrawWireBox
DrawCircle
DrawArc
DrawRectangle
DrawWireSphere
DrawWireSphereAutoSides
DrawWireCylinder
DrawWireCapsule
DrawWireChoppedCone
DrawWireCone
DrawWireSphereCappedCone
DrawOrientedWireBox
DrawDirectionalArrow
DrawConnectedArrow
DrawWireStar
DrawDashedLine
DrawWireDiamond
DrawCoordinateSystem
DrawFrustumWireframe
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!