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

UE4 Level Editor

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/

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();

https://answers.unrealengine.com/questions/518445/actors-spawned-in-editor-do-not-run-their-construc.html

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
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) ---拉丁谚语