[UE4]Editor Extention and Programming
Keywords: UE4, Editor Extention and Programming
Documents
Documents - Fragments
UE4 - Easy example of How to create menus in the editor
http://www.danielmayor.com/ue4-simple-menus
Creating an Editor Module
https://wiki.unrealengine.com/Creating_an_Editor_Module
Customizing the editor’s toolbar buttons menu via custom plugin
https://answers.unrealengine.com/questions/25609/customizing-the-editors-toolbar-buttons-menu-via-c.html
UE4 Editor Toolbar Extention
https://blog.csdn.net/hui211314ddhui/article/details/79375548
batch operations with editor utility blueprints in UE4
https://www.youtube.com/watch?v=5Ty_rQp34PQ
Unreal’s Property Specifiers
https://benui.ca/unreal/uproperty/
Editor Programming Related
How to set display index / order of property in Editor
e.g.
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayPriority = "1"))
int32 Value1 = -1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayPriority = "2"))
int32 Value1 = -1;
Reference:
Metadata Specifiers
https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Reference/Metadata/index.html
Editor Event
Properties changed event
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
Actor moved event
#if WITH_EDITOR
/** Called after an actor has been moved in the editor */
virtual void PostEditMove(bool bFinished);
#endif
How to customize Transform of Actor in Editor
Header: Add VisibleAnywhere
on properties.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = CameraEvent)
UBoxComponent* BoxComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = CameraEvent)
UArrowComponent* DirectionComp;
Constructor:
BoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComp"));
BoxComp->SetupAttachment(GetDefaultAttachComponent());
DirectionComp = CreateDefaultSubobject<UArrowComponent>(TEXT("TriggerDirectionComp"));
DirectionComp->SetupAttachment(BoxComp);
In Bluerpint Editor:
In Level Editor:
In Bluerpint Editor, you can’t modify Location and Rotation of RootComponent, but you can modify them in Level Editor.
How to add editor button event in Actor Blueprint
How to spawn actor in Editor (Level Editor)
example:
AActor* newActor = GEditor->AddActor(
GEditor->GetLevelViewportClients()[0]->GetWorld()->GetCurrentLevel(), AMyActorClass, MyTransform);
// modify properties on newActor
newActor->RerunConstructionScripts();
How to limit the lenght of array in Actor Blueprint editor.
Add EditFixedSize
in the property:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Config", EditFixedSize)
TArray<int32> IdList;
Then initilize array in constructor.
AMyActor::AMyActor()
{
IdList.Init(0, 5);
}
How to creat editor widget using C++
UE4 – C++ Editor Utility Widgets (4.22)
https://isaratech.com/ue4-c-editor-utility-widgets-4-22/
Issue:
Blueprint EditorUtilityWidget
Can’t inherit custom cpp class.
Solution:
Modify MyPlugin.uplugin
.
Origin:
"Modules": [
{
"Name": "MyExtender",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
New:
"Modules": [
{
"Name": "MyExtender",
"Type": "Editor",
"LoadingPhase": "PostEngineInit"
}
]
Then delete all intermediate files and re-generate project files.
How to open the Windows file explorer (open directory dialog) to select the file
Example:
void UMyUserWidget::OnTestBtnClicked()
{
TSharedPtr<SWindow> ParentWindow = FSlateApplication::Get().FindWidgetWindow(TakeWidget());
void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr;
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
if (DesktopPlatform)
{
const FString ContentDireAbsolute = FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir());
FString OutDire;
DesktopPlatform->OpenDirectoryDialog(ParentWindowHandle, TEXT("Choose Folder"), ContentDireAbsolute, OutDire);
GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Cyan, OutDire);
}
}
Select files dialog:
/*
* Opens a file dialog for the specified data. Leave FileTypes empty to be able to select any files.
* Filetypes must be in the format of: <File type Description>|*.<actual extension>
* You can combine multiple extensions by placing ";" between them
* For example: Text Files|*.txt|Excel files|*.csv|Image Files|*.png;*.jpg;*.bmp will display 3 lines for 3 different type of files.
*/
TArray<FString> OutFileNames;
//Opening the file picker!
uint32 SelectionFlag = 0; //A value of 0 represents single file selection while a value of 1 represents multiple file selection
DesktopPlatform->OpenFileDialog(ParentWindowHandle, DialogTitle, DefaultPath, FString(""), FileTypes, SelectionFlag, OutFileNames);
Reference
https://www.orfeasel.com/creating-a-file-picker/
UProperty min & max value
Limit the range of numberic:
UPROPERTY(EditAnywhere, Category = "Camera", meta = (ClampMin = "-89.0", ClampMax = "0.0", UIMin = "-89.0", UIMax = "0.0"))
float CameraMinPitch;
How to add customized tips bar in editor
Quoted from Engine\Source\Runtime\Engine\Private\ShaderCompiler\ShaderCompiler.cpp
FText StatusUpdate;
if ( MaterialName != NULL )
{
FFormatNamedArguments Args;
Args.Add( TEXT("MaterialName"), FText::FromString( MaterialName ) );
StatusUpdate = FText::Format( NSLOCTEXT("ShaderCompilingManager", "CompilingShadersForMaterialStatus", "Compiling shaders: {MaterialName}..."), Args );
}
else
{
StatusUpdate = NSLOCTEXT("ShaderCompilingManager", "CompilingShadersStatus", "Compiling shaders...");
}
FScopedSlowTask SlowTask(1, StatusUpdate, GIsEditor && !IsRunningCommandlet());
SlowTask.EnterProgressFrame(1);
How to add customized progress bar in editor
Begin:
FText Description = NSLOCTEXT("UnrealEd", "PerformingCSGOperation", "Performing CSG operation");
GWarn->BeginSlowTask( Description, true );
End:
GWarn->EndSlowTask();
References: UHCsgUtils::ComposeBrushCSG in HoudiniEngine
How to load asset data in a batch.
1st way: AssetRegistryModule
static const FName AssetRegistryName(TEXT("AssetRegistry"));
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryName);
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
AssetRegistry.GetAssetsByClass(UMaterial::StaticClass()->GetFName(), AssetDataArray, true);
AssetRegistry.GetAssetsByClass(UMaterialInstance::StaticClass()->GetFName(), AssetDataArray, true);
Example: SMaterialAnalyzer::BuildBasicMaterialTree()
2nd way: ObjectLibrary
const auto ObjectLibrary = UObjectLibrary::CreateLibrary(UMaterialInterface::StaticClass(), false, true);
ObjectLibrary->LoadAssetDataFromPath(TEXT("/Game/TestAssets"));
TArray<FAssetData> AssetDatas;
ObjectLibrary->GetAssetDataList(AssetDatas);
for(auto& Data : AssetDatas)
{
UObject* Obj = Data.GetAsset();
UClass* AssetClass = Data.GetClass();
if(const UMaterialInterface* Material = Cast<UMaterialInterface>(Obj))
{
}
}
UObjectLibrary::GetAssetDataList()
is the wrapper of AssetRegistry::GetAssetDataList()
.
You can’t get UserWidget blueprints if using AssetRegistry::GetAssetsByClass()
,
because the function UUserWidget::IsAsset()
which overrided from UObject return false
. Instead of calling AssetRegistry::GetAssetsByPath()
.
Example: get UserWidget blueprints
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
TArray<FAssetData> AssetData;
AssetRegistry.GetAssetsByPath(TEXT("/Game/TestContents"), AssetData, true, true);
for (FAssetData& Data : AssetData)
{
if (UWidgetBlueprint* Widget = Cast<UWidgetBlueprint>(Data.GetAsset()))
{
}
}
How to draw debug line in Animation Editor’s viewport.
Example from FKawaiiPhysicsEditMode::Render():
void FKawaiiPhysicsEditMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
const USkeletalMeshComponent* SkelMeshComp = GetAnimPreviewScene().GetPreviewMeshComponent();
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
if (SkelMeshComp && SkelMeshComp->GetSkeletalMeshAsset() && SkelMeshComp->GetSkeletalMeshAsset()->GetSkeleton())
#else
if (SkelMeshComp && SkelMeshComp->SkeletalMesh && SkelMeshComp->SkeletalMesh->GetSkeleton())
#endif
{
RenderSphericalLimits(PDI);
RenderCapsuleLimit(PDI);
RenderPlanerLimit(PDI);
PDI->SetHitProxy(nullptr);
if (IsValidSelectCollision())
{
FCollisionLimitBase* Collision = GetSelectCollisionLimitRuntime();
if (Collision)
{
FTransform BoneTransform = FTransform::Identity;
if (Collision->DrivingBone.BoneIndex >= 0)
{
BoneTransform = RuntimeNode->ForwardedPose.GetComponentSpaceTransform(
Collision->DrivingBone.GetCompactPoseIndex(RuntimeNode->ForwardedPose.GetPose().GetBoneContainer()));
}
PDI->DrawPoint(BoneTransform.GetLocation(), FLinearColor::White, 10.0f, SDPG_Foreground);
DrawDashedLine(PDI, Collision->Location, BoneTransform.GetLocation(),
FLinearColor::White, 1, SDPG_Foreground);
DrawCoordinateSystem(PDI, BoneTransform.GetLocation(), Collision->Rotation.Rotator(), 20, SDPG_World + 1);
}
}
}
#if ENGINE_MAJOR_VERSION == 5
FAnimNodeEditMode::Render(View, Viewport, PDI);
#else
FKawaiiPhysicsEditModeBase::Render(View, Viewport, PDI);
#endif
}
How to write a C++ blueprint function with pass-by-reference paramters
Example:
// Use existing data, add some more
UFUNCTION(BlueprintCallable)
void UseAndFillDogInfo(
UPARAM(ref) FDogInfo& SearchParams);
Unreal’s UPARAM parameter
https://benui.ca/unreal/uparam/
How to get a UClass pointer by string (class path name)
const UClass* Class_Button = FEditorClassUtils::GetClassFromString(TEXT("/Script/UMG.Button"));
if(ChildWidget->IsA(Class_Button))
{
}
Blueprint Maintaining
Origin: How to create Blueprint assets using C++?
#include "MyBlueprintLibrary.h"
#include "AssetRegistry/AssetRegistryModule.h"
#if WITH_EDITOR
#include "KismetCompilerModule.h"
#include "Kismet2/KismetEditorUtilities.h"
#endif
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Kismet2/BlueprintEditorUtils.h"
DEFINE_LOG_CATEGORY_STATIC(LogMyBlueprintLibrary, Log, All);
/**
* Create Blueprint asset file in editor.
* arguments example:
* @BlueprintPath: Game/TestBP/
* @ParentClass: AActor::StaticClass()
*/
UBlueprint* UMyBlueprintLibrary::CreateBlueprint(const FString& BlueprintPath, TSubclassOf<UObject> ParentClass)
{
// make sure the asset is a new file, doesn't exists with that path.
if (StaticLoadObject(UObject::StaticClass(), nullptr, *BlueprintPath))
{
UE_LOG(LogMyBlueprintLibrary, Error, TEXT("Create Blueprint Failed - [%s] alreay exists!"), *BlueprintPath);
return nullptr;
}
// make sure the parent class is valid
if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(ParentClass))
{
UE_LOG(LogMyBlueprintLibrary, Error, TEXT("Create Blueprint Failed - parent class is invalid! %s"), *BlueprintPath);
return nullptr;
}
// create asset package
UPackage* Package = CreatePackage(*BlueprintPath);
if (!Package)
{
UE_LOG(LogMyBlueprintLibrary, Error, TEXT("Create Blueprint Failed - create package failed! %s"), *BlueprintPath);
return nullptr;
}
// find the blueprint classes to create
UClass* BpClass = nullptr;
UClass* BpGenClass = nullptr;
FModuleManager::LoadModuleChecked<IKismetCompilerInterface>("KismetCompiler").GetBlueprintTypesForClass(ParentClass, BpClass, BpGenClass);
// create asset
UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(ParentClass, Package, *FPaths::GetBaseFilename(BlueprintPath), BPTYPE_Normal, BpClass, BpGenClass);
// register asset
FAssetRegistryModule::AssetCreated(Blueprint);
Blueprint->MarkPackageDirty();
//Blueprint->PostLoad();
return Blueprint;
}
bool UMyBlueprintLibrary::CompileBlueprint(const FString& BlueprintPath, FString& OutMessage)
{
// load blueprint asset
UBlueprint* Blueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), nullptr, *BlueprintPath));
if (!Blueprint)
{
UE_LOG(LogMyBlueprintLibrary, Error, TEXT("Compile Blueprint Failed - Path doesn't lead to a valid Blueprint. %s"), *BlueprintPath);
return false;
}
// compile blueprint
FCompilerResultsLog Result;
FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipSave, &Result);
// format the result
FString Logs = Result.Messages.Num() > Result.NumWarnings + Result.NumErrors ? "\n--- Logs ---" : "";
FString Warnings = Result.NumWarnings > 0 ? "\n--- Warnings ---" : "";
FString Errors = Result.NumErrors > 0 ? "\n--- Errors ---" : "";
for (TSharedRef<FTokenizedMessage> Message : Result.Messages)
{
switch (Message.Get().GetSeverity())
{
case EMessageSeverity::Type::Info:
Logs += "\n" + Message.Get().ToText().ToString();
break;
case EMessageSeverity::Type::Warning:
case EMessageSeverity::Type::PerformanceWarning:
Warnings += "\n" + Message.Get().ToText().ToString();
break;
case EMessageSeverity::Type::Error:
Errors += "\n" + Message.Get().ToText().ToString();
break;
}
}
OutMessage = Logs + Warnings + Errors;
return Result.NumErrors == 0;
}
USCS_Node* UMyBlueprintLibrary::AddComponentToBlueprint(const FString& BlueprintPath, USceneComponent* ComponentTemplate, const FString& ParentNodeName)
{
// load blueprint asset
UBlueprint* Blueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), nullptr, *BlueprintPath));
if (!Blueprint)
{
UE_LOG(LogMyBlueprintLibrary, Error, TEXT("Add Node to Blueprint Failed - Path doesn't lead to a valid Blueprint. %s"), *BlueprintPath);
return nullptr;
}
// create new node in blueprint asset
USCS_Node* NewNode = Blueprint->SimpleConstructionScript->CreateNode(ComponentTemplate->GetClass(), *ComponentTemplate->GetName());
//Parent node
if (USCS_Node* ParentNode = Blueprint->SimpleConstructionScript->FindSCSNode(*ParentNodeName))
{
ParentNode->AddChildNode(NewNode);
}
else
{
TArray<USCS_Node*> AllNodes = Blueprint->SimpleConstructionScript->GetAllNodes();
// if there are no nodes in the blueprint or if the root node is still the default node
if (AllNodes.Num() == 0 || AllNodes[0] == Blueprint->SimpleConstructionScript->GetDefaultSceneRootNode())
{
// use this component as root node
Blueprint->SimpleConstructionScript->AddNode(NewNode);
}
else
{
// use the first node as parent (root node)
AllNodes[0]->AddChildNode(NewNode);
}
}
// copy component's settings onto node
UEditorEngine::CopyPropertiesForUnrelatedObjects(ComponentTemplate, NewNode->ComponentTemplate);
// notify the engine we modified the blueprint components
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
return NewNode;
}
USCS_Node* UMyBlueprintLibrary::AddStaticMeshComponentToBlueprint(const FString& BlueprintPath)
{
UStaticMeshComponent* Component = NewObject<UStaticMeshComponent>(GetTransientPackage(), "My_Static_Mesh_Component_Name", RF_Transient);
Component->SetRelativeLocation(FVector(0.f, 0.f, 0.f));
Component->SetRelativeRotation(FRotator(0.f, 0.f, 0.f));
Component->SetRelativeScale3D(FVector(1.f, 1.f, 1.f));
Component->SetMobility(EComponentMobility::Type::Static);
USCS_Node* NewNode = AddComponentToBlueprint(BlueprintPath, Component, "None");
return NewNode;
}
Export Assets
How to export mesh asset to FBX file
UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, TEXT("StaticMesh'/Game/Meshes/Cube.Cube'"));
UFbxExportOption* Option = NewObject<UFbxExportOption>();
Option->Collision = false;
Option->LevelOfDetail = false;
Option->bASCII = false;
UAssetExportTask* Task = NewObject<UAssetExportTask>();
Task->bAutomated = true;
Task->Filename = TEXT("E:/cube.fbx");
Task->Object = Mesh;
Task->Options = Cast<UObject>(Option);
bool Res = UExporterFBX::RunAssetExportTask(Task);
How to import mesh asset from FBX file
Origin:
https://zhuanlan.zhihu.com/p/495322681
FString FilePath = TEXT("E:/cube.fbx");
UFbxFactory* FbxFactory = NewObject<UFbxFactory>();
if(!FbxFactory->FactoryCanImport(FilePath))
{
UE_LOG(LogTemp, Warning, TEXT("%s CAN NOT BE IMPORTED!"), *FilePath);
return;
}
UAssetImportTask* ImportTask = NewObject<UAssetImportTask>();
ImportTask->bAutomated = true;
FbxFactory->AssetImportTask = ImportTask;
// do not detect import type on import:
// detection will change ImportUI->MeshTypeToImport settings
// incorrect detection(always occur) will result in import error
FbxFactory->SetDetectImportTypeOnImport(false);
FbxFactory->ImportUI->MeshTypeToImport = FBXIT_SkeletalMesh;
FbxFactory->ImportUI->bImportMaterials = false;
FbxFactory->ImportUI->SkeletalMeshImportData->bImportMorphTargets = true;
// get BaseFileName without extension
const FString BaseFileName = FPaths::GetBaseFilename(FilePath);
const FString PackageName = "/Game/TestAssets/" + BaseFileName;
UPackage* Package = FindPackage(nullptr, *PackageName);
if(!Package && FEditorFileUtils::IsMapPackageAsset(PackageName))
{
ensure(false);
}
if(!FPackageName::DoesPackageExist(PackageName))
{
Package = CreatePackage(*PackageName);
if(Package)
{
Package->FullyLoad();
}
else
{
UE_LOG(LogTemp,Warning,TEXT("Create Package: %s Failed!"),*PackageName);
return ;
}
}
bool OutCanceled = false;
FbxFactory->ImportObject(FbxFactory->ResolveSupportedClass(),Package, FName(*BaseFileName),
EObjectFlags::RF_Transactional | EObjectFlags::RF_Public | EObjectFlags::RF_Standalone,
FilePath, nullptr, OutCanceled);
Testing & Optimization
Testing Automation
Automation System Overview, Overview of the automation system used for unit testing, feature testing, and content stress testing.
https://docs.unrealengine.com/4.27/en-US/TestingAndOptimization/Automation/
静水流深(Still Water Runs Deep) ---拉丁谚语