where this is a requested feature multiple times on the Forum with no real answer I will include the steps and my implementation
for the purpose of this instruction the base project name is MyGame, so any time you see MyGame
or MYGAME
you can replace it with your [ProjectName]
first we need the struct we are going to be dealing with and a UActorComponent (this also has no reason not to works with any of the children) that will be holding it for me these both reside in MyGameEntitySpawner.h
USTRUCT(BlueprintType)
struct FMyGameTransformStruct
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FTransform transform;
// if these will always be offsets
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bIsOffset = true;
};
USTRUCT(BlueprintType)
struct FMyGameSpawnPoint : public FMyGameTransformStruct
{
GENERATED_BODY()
public:
// other stuff for a spawnPoint
// ...
};
//...
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MYGAME_API UMyGameEntitySpawner : public UActorComponent
{
GENERATED_BODY()
public:
// should be in #ifdef WITH_EDITORONLY_DATA but even when I put that it complains about needing what is already there...
UPROPERTY(VisibleAnywhere)
int32 VisualizerSelectedIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FMyGameSpawnPoint> SpawnPoints;
// ...
};
These next two steps can be done in either order, but the name must be exact, and a json database is easier to edit then deleting, and adding another module
(Visual Studio as of 17.11.0 update has added the ability to create a module and other âcommonâ unreal Engine items inside visual studio for modules it means you donât have to blindly copy/make files and hope you named things consistently)
we need to create a Module.
Module Name = [ProjectName]GameEditor;
Module Type = Editor
Module Loading Phase = PostEngineInit
IDE will probably still ask you to Reload (at least we didnât need to open the editor âYAYâ)
need to Edit the âMyGame.uprojectâ (this file is effectively a json database so names, orders and spelling are specific) This might be generated by Visual Studio, but I am a mad person and did this in reverse.
find
"Modules": [
{
// this is your gamestuff
},
{
"Name": "MyGameEditor",
"Type": "Editor",
"LoadingPhase": "PostEngineInit"
}
]
if you are having trouble with your module and Unreal says you canât launch your project this is the part you would Remove (JSon does not support comments, and there is validation done, so you need to remove it)
your project is currently not in a run-able state before we take some extra steps.
go to âMyGameEditor.Target.csâ ([ProjectName]Editor.Targets.cs)
find the line
// before
ExtraModuleNames.AddRange( new string[] { "MyGame" } );
// after
ExtraModuleNames.AddRange( new string[] { "MyGame", "MyGameEditor" } );
âMyGameEditor.Build.csâ and change it to the following
PublicDependencyModuleNames.AddRange(new string[] {"Core", "CoreUObject", "Engine", "MyGame" });
PrivateDependencyModuleNames.AddRange(new string[] { "UnrealEd", "ComponentVisualizers" });
now we deal with the actual implementation that as of writing Visual studio only got half way there, and maybe named things differently then guides would have (Visual studio is hard enforcing âpredominant class name should match file name excluding the prefixâ so it has the word âModuleâ at the end of the cpp and h) I will be proceeding with the names âMyGameEditorModuleâ for the file names of the cpp and h of the module executable
the âMyGameEditorModule.hâ should look like this
#pragma once
#include "CoreMinimal.h"
#include "Modules//ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "Modules/ModuleInterface.h"
#include "UnrealEd.h"
DECLARE_LOG_CATEGORY_EXTERN(MyGameEditor, All, All)
class FMyGameEditorModule : public IModuleInterface
{
public:
// don't believe the guides saying we wont need these
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
then âMyGameEditorModule.cppâ should look like this
#include "MyGameEditorModule.h"
IMPLEMENT_MODULE(FMyGameEditorModule, MyGameEditor);
void FMyGameEditorModule::StartupModule() { }
void FMyGameEditorModule::ShutdownModule() { }
at this point you want to hit build (Ctrl+B) with the Module in focus, and if it doesnât have errors it should be safe to launch the editor (if you are concerned about crashes then attach the debugger)
if the Engine launches normally, then one the top bar âToolsâ->âDebugâ->âModulesâ, and in the search box type your [ProjectName] (âMyGameâ)in my case) and you should see the game itself, and then [ProjectName]Editor.
we now have an editor module, you can do the extra step of hitting play to see if anything breaks, (if you are doing all of this by hand without the template at this time go ahead âToolsâ->âRefresh Visual Studio filesâ) , but we are off the implement our visualizer.
we now want to add the Visualizer to the module. you can either do this through the Add C++ because we are in the editor (be sure to put it in the EditorModule we are making), or we can use the Visual Studio template add (right click on the module in the solution browser âAddâ->âUnreal itemâ)
with the editor closed
explicit implementation directly follows, Interface implementation will be after (MyGameEditorModule.h will be the same as above, and MyGameEditorModule.cpp will need specific class FNames so it will not be different for the interface)
MyGameEditorModule.cpp
#include "MyGameEditorModule.h"
#include "MyGameTransformVisualizer.h"
// the headers of the class(es) that we are actually going to be working with
#include "MyGameEntitySpawner.h"
IMPLEMENT_MODULE(FMyGameEditorModule, MyGameGameEditor);
void FMyGameEditorModule::StartupModule()
{
if ( GUnrealEd )
{
TSharedPtr<FMyGameTransformVisualizer> MyGameTransformVisualizer = MakeShareable(new FMyGameTransformVisualizer());
if ( MyGameTransformVisualizer.IsValid() )
{
// each class type the Visualizer class will be used with because these are FName of the class we cannot use an interface here (I wish we could)
GUnrealEd->RegisterComponentVisualizer(UMyGameEntitySpawner::StaticClass()->GetFName(), MyGameTransformVisualizer);
// add other classes that the visualizer will be working with
MyGameTransformVisualizer->OnRegister();
}
}
}
void FMyGameEditorModule::ShutdownModule()
{
if ( GUnrealEd )
{
// what we register we need to unregister
GUnrealEd->UnregisterComponentVisualizer(UMyGameEntitySpawner::StaticClass()->GetFName());
}
}
for this example âMyGameTransformVisualizerâ
MyGameTransformVisualizer.h
#pragma once
#include "CoreMinimal.h"
// this is needed to draw the visualization in the first place
#include "ComponentVisualizer.h"
// we could put the headers of the class(es) we intend to use the visualizer for, but we only need a pointer to them,
// so we will forward declare the pointer
/**Base class for clickable targeting editing proxies*/
struct HMyGameTransformVisProxy : public HComponentVisProxy
{
DECLARE_HIT_PROXY();
HMyGameTransformVisProxy (const UActorComponent* InComponent, int32 inElementIndex)
: HComponentVisProxy(InComponent, HPP_Wireframe), ElementIndex(inElementIndex)
{}
int32 ElementIndex;
};
/**
*
*/
class MYGAMEEDITOR_API FMyGameTransformVisualizer : public FComponentVisualizer
{
public:
FMyGameTransformVisualizer();
~FMyGameTransformVisualizer();
// Begin FComponentVisualizer interface
// the things being drawn to the screen
virtual void DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI) override;
// the click events on the object
virtual bool VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click) override;
// cleanup when we are no longer focused on the actor
virtual void EndEditing() override;
// allows us to place the widget where we would like;
// widget strangely returns to Actors <0,0,0> after edit but "livable" I guess
virtual bool GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const override;
// applying modifications onto the transform
virtual bool HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltaRotate, FVector& DeltaScale) override;
// End FComponentVisualizer interface
/** the target component we are currently editing an unimplemented Getter is even less useful */
class UMyGameEntitySpawner* EditedComponent = nullptr;
// for an interface implementation I am leaving this here, because this and the line above will be the only difference
// technically the U variant would make sense for safety checks but we need the interface functions, and we are told
// not to put anything in the U
// class IMyGameTransformStructInterface* HeldComponent = nullptr;
private:
/**Index of target in selected component*/
int32 CurrentlySelectedIndex;
};
MyGameTransformVisualizer.cpp
#include "MyGameTransformVisualizer.h"
// the class(es) we are implementing into the visualizer
// note that without interfaces you will either need a separate visualizer class for each Object type with duplicated code
// or have the drawVisualizer have a lot of attempts to cast to the different object types which will add more overhead to
// the editor and still a lot of duplicated code
#include "MyGameEntitySpawner.h"
IMPLEMENT_HIT_PROXY(HMyGameTransformVisProxy, HComponentVisProxy)
FMyGameTransformVisualizer::FMyGameTransformVisualizer() { }
FMyGameTransformVisualizer::~FMyGameTransformVisualizer() { }
void FMyGameTransformVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
if( const UMyGameEntitySpawner* spawner = Cast<const UMyGameEntitySpawner>(Component) )
{
// only need the index twice, and range based for loops are more "sane"
int32 ii = 0;
for ( FMyGameSpawnPoint point : (spawner->SpawnPoints) )
{
// sorry colorblind people
FLinearColor Color = (ii == CurrentlySelectedIndex) ? FLinearColor::Green : FLinearColor::Red;
FVector Start = point.transform.GetLocation();
if ( point.bIsOffset )
{
Start += spawner->GetOwner()->GetActorLocation();
}
// writing text to the display outside of World-Context is hard, at least they don't crash
// if ( ii == CurrentlySelectedIndex )
// {
// DrawDebugString(EditedComponent->GetWorld(), Start, FString::FromInt(ii),
// nullptr, FColor::Magenta, 0.0f, true, 20.0f);
// GEngine->AddOnScreenDebugMessage(INDEX_NONE, 1.0f, FColor::Magenta, FString::FromInt(ii));
// }
// only 1 of these endpoints per iteration, and won't change
const FVector End = Start + point.transform.GetRotation().Vector() * 100.0f;
// the callback for the index
PDI->SetHitProxy(new HMyGameTransformVisProxy(Component, ii));
// drawing the line
PDI->DrawLine(Start, End, Color, SDPG_World);
// putting a dot on the End of the line to simulate an arrow (so I don't have to do arrow barbs)
PDI->DrawPoint(End, FLinearColor::Blue, 5.0f, SDPG_World);
// we need accurate ii (about the same performance as a full for-loop)
ii++;
}
}
}
bool FMyGameTransformVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click)
{
bool bEditing = false;
if ( VisProxy && VisProxy->Component.IsValid() )
{
bEditing = true;
if ( VisProxy->IsA(HMyGameTransformVisProxy::StaticGetType()) )
{
HMyGameTransformVisProxy* Proxy = (HMyGameTransformVisProxy*)VisProxy;
// don't think to hard about this it is fine. I hate having to right this more then you hate me for writing it.
EditedComponent = const_cast<UMyGameEntitySpawner*>(Cast<const UMyGameEntitySpawner>(Proxy->Component));
UE_LOG(LogTemp, Error, TEXT("editedComponent Transform %i belonging to %s")
,Proxy->ElementIndex, *EditedComponent->GetOwner()->GetName());
CurrentlySelectedIndex = Proxy->ElementIndex;
EditedComponent->VisualizerSelectedIndex = Proxy->ElementIndex;
}
}
else
{
if ( EditedComponent != nullptr && IsValid(EditedComponent) ) { EditedComponent->VisualizerSelectedIndex = INDEX_NONE; }
CurrentlySelectedIndex = INDEX_NONE;
}
return bEditing;
}
void FMyGameTransformVisualizer::EndEditing()
{
CurrentlySelectedIndex = INDEX_NONE;
EditedComponent = nullptr;
}
bool FMyGameTransformVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const
{
// should we even be working on this (null check because IsValid does not check for null on raw pointers and can crash)
if ( EditedComponent != nullptr && IsValid(EditedComponent) && CurrentlySelectedIndex != INDEX_NONE )
{
// could local var the struct, but it would be for 3 accesses
// we need this part regardless and works out the "same" barring FloatingPointApproximation
OutLocation = EditedComponent->SpawnPoints[CurrentlySelectedIndex].transform.GetLocation();
if ( EditedComponent->SpawnPoints[CurrentlySelectedIndex].bIsOffset )
{
// the other part of the offset.
OutLocation += EditedComponent->GetOwner()->GetActorLocation();
}
return true;
}
return false;
}
bool FMyGameTransformVisualizer::HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltaRotate, FVector& DeltaScale)
{
bool bHandled = false;
if ( EditedComponent != nullptr && IsValid(EditedComponent) && CurrentlySelectedIndex != INDEX_NONE )
{
// by getting and working on the pointer to the FTransform we save a reassignment through either
// the copy constructor, or a 3 arg call to FTransform::constructor.
FTransform* trans = &(EditedComponent->SpawnPoints[CurrentlySelectedIndex].transform);
FVector loc = trans->GetLocation();
loc += DeltaTranslate;
trans->SetLocation(loc);
// FQuat will always be a magic thing.
FRotator rot = trans->GetRotation().Rotator();
rot += DeltaRotate;
trans->SetRotation(FQuat(rot));
loc = trans->GetScale3D();
loc += DeltaScale;
trans->SetScale3D(loc);
bHandled = true;
}
return bHandled;
}
at this point you should be able to build the module (hopefully I have documented all the steps). do a double check by building your game, and as long as you donât have any errors you should be albe to launch the editor, select an actor with the designated component, and for each Transform in the array get there will be a red-line with a blue-dot that when clicked will turn green. you can move them around with either the widget that appears when they are selected or you modify the Transform in the details window
it will also behave with the editors rotation mode in the viewport. Scale is currently not modifying anything but the Widget mode does work.
these âfeaturesâ with the explicit might be because I am working with an UActorComponent not a USceneComponent
- after finishing any Widget interaction the widget will return to the Actors <0,0,0>, and modify the actor. but clicking the green line again will put the widget back
- the visualizations only show up when the Actor is selected in the Actorâs Outliner, selecting the Componenent these Visualizations are visualizing, or any other Component of the Actor will make the Visualizations disappear.
- I would like to have the CurrentlySelectedIndex show when one is selected, but
writing text to the screen outside of World-Context or SLATE
is apparently non trivial if someone can figure this out, or provide the needed âSLATEâ operations I would really appreciate it.
- because of the previous the is an Error message in the Log when an index is selected (this is not an actual error, but will stand out against Display messages)