UE simple plug-in production

This article mainly provides several examples of writing UE plug-ins, so as to understand how to use C++ to create custom plug-ins in UE:

  • Write a simple plugin that darkens the scene
  • Write a custom window to display all animation assets in the project

Write a simple plugin that darkens the scene

参考:Unreal Engine 5 - Writing Plugins in C++

Write a simple plug-in to understand how to use C++ to create a custom plug-in in UE. Here, a Button will be added to the Viewport window. After clicking the button, an object of type PoseProcessVolume will be added to the scene, and it will be used to darken the scene. Realize the Night effect, as shown in the figure below:
insert image description here

There are also some parameter operations here:

  • Check Infinete Extent in the PoseProcessVolume Settings of the created PoseProcessVolumn, so that the post-processing range is infinite
  • Change the intensity of DirectionalLight to 1.0
  • PostProcessVolumn, adjust its Expoture->ExposureCompensation value to -3

Create a Plugin of the Editor Toolbar Button type

In my UE project of C++ project type, select "Edit"->"Plugins" from the menu, and click "New Plugin". Since the EditorToolbar is to be modified, select "Editor Toolbar Button" in the pop-up window , create a plugin named "MakeLevelDark" (PS: UE project of blueprint type does not seem to have so many options).

Here I failed to create, probably because my engine version has been upgraded, the original project has not been upgraded, resulting in compilation errors, so Unable to clean target while not hot-reloading.close the editor and try againI deleted the Binaries, Intermediate, Saved and *.sln files of the original project, and re-imported once. Created successfully:
insert image description here

It will automatically open VS2022, and you can see the newly created related files, MakeLevelDark, MakeLevelDarkCommands and MakeLevelDarkStyle three classes, and a Module Build.cs file:
insert image description here

And at this time, the Toolbar of the Viewport will automatically appear the corresponding Button:
insert image description here
after clicking, it will prompt:
insert image description here

So here we need Override to correspond to the functions in the Module PluginButtonClickedto execute the custom content, and the corresponding Module class where the corresponding function is located:

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FToolBarBuilder;
class FMenuBuilder;

class FMakeLevelDarkModule : public IModuleInterface
{
    
    
public:

	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
	
	// 需要自定义的函数
	void PluginButtonClicked();
	
private:

	void RegisterMenus();


private:
	TSharedPtr<class FUICommandList> PluginCommands;
};

write C++ code

Based on specific functions, it is divided into the following sections:

  • When clicking the button, find the Directional Light in the Level and change its Intensity
  • Determine whether PostProcessVolume exists in the scene, if not, give a Message prompt and create a new one
  • Modify the Extent and Exposure Compensation parameters of PoseProcessVolume

Directional Light part
In order to find the Directional Light in the Level, here is a general function to find Actors in the scene:

#include <Kismet/GameplayStatics.h>

// 返回场景里找到的特定类型的第一个对象
AActor* FMakeLevelDarkModule::FindActor(TSubclassOf<AActor> ActorClass)
{
    
    
    // 获取World
	const UWorld* World = GEditor->GetEditorWorldContext().World();
	if (World)
	{
    
    
		TArray<AActor*>  FoundActors;
		UGameplayStatics::GetAllActorsOfClass(World, ActorClass, FoundActors);
		if (FoundActors.Num() > 0)
			return FoundActors[0];
	}

	return nullptr;
}

Then use this function to find the Directional Light in Level and change its Intensity:

AActor* FoundActor = FindActor(ADirectionalLight::StaticClass());

if (FoundActor)
{
    
    
	ADirectionalLight* Sun = Cast<ADirectionalLight>(FoundActor);

	if (Sun)
		Sun->GetLightComponent()->SetIntensity(1.f);
}

PS: The Include path prompt in the Components folder of VS 2022 is gone, and it always gives me an error, and then the compilation is actually good, which cheated me for a long time. . .


PostProcessVolume part
Here you may need to add the Actor corresponding to PostProcessVolume in the scene, so also design a function:

AActor* FMakeLevelDarkModule::AddActor(TSubclassOf<AActor> ActorClass)
{
    
    
	ULevel* Level = GEditor->GetEditorWorldContext().World()->GetCurrentLevel();
	return GEditor->AddActor(Level, ActorClass, FTransform());
}

Then continue the following logic:

FoundActor = FindActor(APostProcessVolume::StaticClass());

// 没找到则创建
if (!FoundActor)
{
    
    
	DialogText = FText::FromString("PostProcessVolume Not Found, Creating One");
    // 在创建PoseProcessVolume的时候加个Message提示
	FMessageDialog::Open(EAppMsgType::Ok, DialogText);

	FoundActor = AddActor(APostProcessVolume::StaticClass());
}

// 直接修改
if (FoundActor)
{
    
    
	APostProcessVolume* PPVol;
	PPVol = Cast<APostProcessVolume>(FoundActor);
	if (PPVol)
	{
    
    
		PPVol->Settings.AutoExposureBias = -3.f;
		PPVol->Settings.bOverride_AutoExposureBias = true;
		PPVol->bUnbound = true;
	}
}


Customize the window to display all animation assets in the project

There seem to be two approaches:

  • Through blueprint implementation, create Editor Widget blueprint (note that it is Editor Widget asset, not Widget Blueprint)
  • Create Plugins implementation through C++

Reference: Slate Editor Window Quickstart Guide

The first method is not mentioned, it is not a plug-in, here is the use of C++ to create a Slate Editor Window to create a plug-in

I haven’t written anything related to Slate before, so let’s learn about it first, which is divided into the following steps:

  • create plugin
  • Write a simple Widget class, add it to the Editor Window corresponding to the plugin, and learn how to write Slate C++
  • Make this Widget class complex so that it can draw all animation assets

Create a plugin of type Editor Standalone Window

Not much to say about this, the plugin I created is called AnimationLibraryPreviewer, but this time I reported an error again:

Failed to compile plugin source code. See output log for more information.

It doesn't seem to have anything to do with Live Coding. It's very annoying. I can only delete the Cache file and import it again. . . . .

At this point, the following files will be created, which can be found to be the same as those created by the previous Editor Toolbar Button template plugin:
insert image description here

Based on the UE template, you can already find the Menu to open the Editor Window in the drop-down page of the Window. The initial window is as shown in the figure below:
insert image description here

Let me change FAnimationPreviewLibraryModule::OnSpawnPluginTabthe function on the window, you can take a look at the corresponding code, and learn the syntax for describing Slate in UE:

TSharedRef<SDockTab> FAnimationPreviewLibraryModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
    
    
	// 创建一个长Text
    FText WidgetText = FText::Format(
        LOCTEXT("WindowWidgetText", "Add code to {0} in {1} to override this window's contents"),
        FText::FromString(TEXT("FAnimationPreviewLibraryModule::OnSpawnPluginTab")),
        FText::FromString(TEXT("FAnimationPreviewLibraryModule.cpp"))
        );

    return SNew(SDockTab)					// 1. 创建了DockableTab
    .TabRole(ETabRole::NomadTab)			// 2. 指定Tab类型
    [
        // Put your tab content here!
        SNew(SBox)							// 3. 创建Box
        .HAlign(HAlign_Center)				// 4. 水平和竖直方向的align方式都为居中
        .VAlign(VAlign_Center)
        [
            SNew(STextBlock)				// 5. 里面再创建一个TextBlock, 指定Text为WidgetText
            .Text(WidgetText)
        ]
    ];

}

The key points are as follows:

  • SNew(SDockTab) creates a SDockTab, which represents the Dockable EditorWindow
  • There are five types of ETabRole: MajorTab, PanelTab, NomadTab, DocumentTab, NumRoles, where Nomad means nomads, here can be translated as a free Tab, quite image
  • The parent-child relationship of UI Element is []realized by

In fact, using UMG to create a Widget blueprint is almost such an operation. UMG is a visual editor based on Slate encapsulation (a layer of encapsulation of Slate's UObject nature). It also adds many events and methods. For more Slate syntax, refer to Slate Overview

Create the Menu class

You can add Slate widgets directly to the FAnimationPreviewLibraryModule class generated by the Plugin wizard. However, there are some limitations to how you can handle your widgets’ callbacks if you do this. Therefore, you should create a dedicated Slate widget to hold the menu’s contents, then add that widget to the FAnimationPreviewLibraryModule class.

Slate WidgetHere, I did not choose to add Slate widgets directly in the FAnimationPreviewLibraryModule class, but chose to create a new class in the corresponding plug-in project AnimPreviewLibraryWindowMenu. In fact, I created a custom Slate Widget (similar to the custom Visual Element in Unity) .

As shown in the figure below, pay attention to select the corresponding Project in the drop-down arrow, otherwise it will be added to the default Game Code
insert image description here

The created class will come with a Construct function:

// SAnimPreviewLibraryWindowMenu.cpp
#include "SAnimPreviewLibraryWindowMenu.h"
#include "SlateOptMacros.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SAnimPreviewLibraryWindowMenu::Construct(const FArguments& InArgs)
{
    
    
	// 注意这里的[]是必须要有内容的, 否则会编译报错
	/*
	ChildSlot
	[
		// Populate the widget
	];
	*/
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

The filling code here is:

void SAnimPreviewLibraryWindowMenu::Construct(const FArguments& InArgs)
{
    
    
    ChildSlot
    [
        SNew(SVerticalBox)                                      // 1. 先创建SVerticalBox
        + SVerticalBox::Slot()                                  // 2. 添加Slot, 里面存SHorizontalBox, 代表第一行
        .AutoHeight()
        [
            SNew(SHorizontalBox)
            + SHorizontalBox::Slot()
            .VAlign(VAlign_Top)
            [
                SNew(STextBlock)
                .Text(FText::FromString("Test Button"))
            ]
            + SHorizontalBox::Slot()
            .VAlign(VAlign_Top)
            [
                SNew(SButton)
                .Text(FText::FromString("Press Me"))
            ]
        ]
        + SVerticalBox::Slot()                                  // 3. 添加Slot, 里面存SHorizontalBox, 代表第二行
        .AutoHeight()
        [
            SNew(SHorizontalBox)
            + SHorizontalBox::Slot()
            .VAlign(VAlign_Top)
            [
                SNew(STextBlock)
                .Text(FText::FromString("Test Checkbox"))
            ]
            + SHorizontalBox::Slot()
            .VAlign(VAlign_Top)
            [
                SNew(SCheckBox)
            ]
        ]
     ];
}

Next, you need to let the original Editor window create the newly created UI Element, and you need to modify the original one OnSpawnPluginTab. The code is as follows:

TSharedRef<SDockTab> FAnimationPreviewLibraryModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
    
    
	return SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		[
			// Put your tab content here!
			SNew(SAnimPreviewLibraryWindowMenu)
		];
}

You can’t compile Slate windows while Live Coding is active and your project is open in Unreal Editor. Make sure to close Unreal Editor before trying to compile your code.

Close Editor, press Ctrl + F5 in VS, restart, and then open the editor window, as shown in the following figure:
insert image description here

Briefly mention the processing method of the relevant Event here. The idea is to specify a callback function for it when creating a Widget similar to Button

For example, the writing method of Button:

+SHorizontalBox::Slot()
.VAlign(VAlign_Top)
[
    SNew(SButton)
    .Text(FText::FromString("Press Me"))
    // SP代表Shared pointer delegates
    // FOnClicked是UE里给Slate提供回调的类, OnTestButtonClicked是此类自定义的函数
    .OnClicked(FOnClicked::CreateSP(this, &SAnimPreviewLibraryWindowMenu::OnTestButtonClicked))
]

// 回调函数签名为:
FReply SAnimPreviewLibraryWindowMenu::OnTestButtonClicked()
{
    
    
    return FReply::Handled();
}

The callback of Checkbox looks like this:

+SHorizontalBox::Slot()
.VAlign(VAlign_Top)
[
	// 添加对CheckBox的回调
    SNew(SCheckBox)
    // 当CheckState改变时的回调
    .OnCheckStateChanged(FOnCheckStateChanged::CreateSP(this, &SAnimPreviewLibraryWindowMenu::OnTestCheckboxStateChanged))
    // IsChecked应该是Check的时候的回调吧
    .IsChecked(FIsChecked::CreateSP(this, &SAnimPreviewLibraryWindowMenu::IsTestBoxChecked))
]

load assets

The idea here is:

  • Find the paths of all uassets under the Content folder in the project
  • Load uassets one by one to determine which types are AnimAsset
  • Draw these animation assets to the Editor Window, each asset corresponds to an Object Field
  • When clicking on an animation asset, the Project Content window is automatically locked to the corresponding file

Load and find all AnimAsset
codes as follows:

TArray<UObject*> MeshAssets;
EngineUtils::FindOrLoadAssetsByPath(TEXT("/Game/"), MeshAssets, EngineUtils::ATL_Regular);

for (UObject* f : MeshAssets)
{
    
    
    UAnimationAsset* animx = Cast<UAnimationAsset>(f);
    if (animx)
    {
    
    
        UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(animx ->GetName()));
    }
}

Animation assets are drawn on the Editor Window
. I originally wanted to draw them, but I found it very troublesome. I used Widget Reflector to check the class that draws similar things in UE. It is called, SDetailSingleItemRowthis is with a thumbnail, and it is also drawn in the details of the asset. Object Field, but it is not exposed. But I see that it can be used SObjectPropertyEntryBox, although it does not have thumbnails, but its Constructfunctions and GetDesiredWidthinterfaces are exposed.

So modify the original SAnimPreviewLibraryWindowMenu::Constructfunction directly here:

void SAnimPreviewLibraryWindowMenu::Construct(const FArguments& InArgs)
{
    
    
	// 1. 读资源
    FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
    TArray<FAssetData> AssetData;

    TArray<UObject*> MeshAssets;
    EngineUtils::FindOrLoadAssetsByPath(TEXT("/Game/"), MeshAssets, EngineUtils::ATL_Regular);


    TArray<UAnimationAsset*> AnimAsset;
    for (UObject* f : MeshAssets)
    {
    
    
        UAnimationAsset* animx = Cast<UAnimationAsset>(f);
        if (animx)
        {
    
    
            UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(animx->GetName()));
            AnimAsset.Add(animx);
        }
    }


	// 2. 绘制一个大的Container
    TSharedPtr<SVerticalBox> Container = SNew(SVerticalBox);
    for (int32 idx = 0; idx < AnimAsset.Num(); idx++)
    {
    
    
        Container->AddSlot()
        [
            SNew(SObjectPropertyEntryBox)
            .EnableContentPicker(false)
            .AllowClear(false)
            .AllowedClass(UAnimationAsset::StaticClass())
            .DisplayBrowse(true)
            .DisplayThumbnail(true)
            .ObjectPath(AnimAsset[idx]->GetPathName())
        ];
    }

	// 3. Container加到ChildSlot里
    ChildSlot
    [
        SNew(SVerticalBox)
        + SVerticalBox::Slot()
        .AutoHeight()
        [
            Container.ToSharedRef()
        ]
    ];
}

Disable some options that allow users to select assets in the Object Field, and the final effect is as follows:

insert image description here

It's kind of ugly and you can't use the scroll wheel, but it's a small problem, so be it

By the way, a custom asset may be created in the game, and then you need to open it and edit the custom asset window. At this time, you need to use the FAssetEditorToolkitclass to complete



appendix

About Slots

Reference: https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/UMG/UserGuide/Slots/
Reference: https://forums.unrealengine.com/t/how-to-define-a-slate- via-c/385892

Slots are the invisible glue that bind Widgets together. In Slate they are a lot more explicit in that you must first create a Slot then choose what control to place inside of it. In UMG, however, you have Panel Widgets that automatically use the right kind of Slot when Child Widgets are added to them.

Slots are used to connect Widgets (similar to umxl files in Unity used to connect Visual Elements). In UMG, when adding sub-Widgets to Widgets, the system will automatically create Slots, and then create corresponding sub-Widgets in it. In Slate, Then you need to manually create the Slot yourself.

You can declere only one widget in single slot, in order to have more then single widget you need a panel widget. If you ever used UMG you should already understand this concept because UMG inherent this limitation. So if you want to widget one after nother verticuly you can use SVerticalBox, you a…

Only one widget is allowed in the slot. If you want to have multiple widgets, you need to create a panel widget. Like in UE, when creating UI Element vertically, you need to use a SVerticalBox, as shown in the following figure:

SNew(SVerticalBox)
	// 在SVerticalBox的SNew后面连续调用+SVerticalBox::Slot(), 每个Slot里再创建一个子Widget
	+ SVerticalBox::Slot()
	[
        SNew(SButton) 
        Content() 
    	[ 
           SNew(STextBlock) 
        ]
    ]
	+ SVerticalBox::Slot()
	[
        SNew(SButton) 
        Content() 
    	[ 
           SNew(STextBlock) 
        ]
    ]

widget reflector

Reference: https://forums.unrealengine.com/t/how-to-define-a-slate-via-c/385892

Now best way to find examples for slate is editor source code, There tool called widget reflector, in Window->Devlopment Tools which let you explore widget tree of editor, so if you wonder how specific widget was done, you scan that widget and you should get name and then you can search it in github to see how this widget got coded.

You can look at the Widget sample provided by UE. In UE4, it is in Window->Devlopment Tools->widget reflector, and in UE5 it is moved to Tools -> Debug->widget reflector.


UE reads the file

Reference: https://forums.unrealengine.com/t/using-ffilemanagergeneric-for-listing-files-in-directory/286866/4
Reference: https://forums.unrealengine.com/t/find-files-by -extension-ifilemanager-findfilesrecursive/37463/6

The main reason is that the path to read assets in UE is not a global path, but a path mechanism created by itself, which was actually written before:

TArray<UObject*> MeshAssets;
EngineUtils::FindOrLoadAssetsByPath(TEXT("/Game/"), MeshAssets, EngineUtils::ATL_Regular);

for (UObject* f : MeshAssets)
{
    
    
    UAnimationAsset* animx = Cast<UAnimationAsset>(f);
    if (animx)
    {
    
    
        UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(animx ->GetName()));
    }
}

UE obtains the global path of files with a specific suffix under the folder

A few important points:

  • The path in UE is generally FPathsobtained through static members in the class
  • The path corresponding to the file in a specific folder can be found through FFileManagerGeneric

for example:

// 找到特定文件夹下的所有xml文件(不包含子文件夹里的文件)
TArray<FString> FileNames;
FFileManagerGeneric FileMgr;
FileMgr.SetSandboxEnabled(true);// don't ask why, I don't know :P
FString wildcard("*.xml"); // May be "" (empty string) to search all files
FString search_path(FPaths::Combine(*FPaths::GameDir(), TEXT("Data"), *wildcard));

FileMgr.FindFiles(FileNames, *search_path, 
                                 true,  // to list files
                                 false); // to skip directories

// 打印出来
for (auto f : FileNames)
{
    
    
    FString filename(f);
    UE_LOG(LogTemp, Warning, TEXT("%s"), *f);
}

FileNames.Empty();// Clear array

As another example, this way of writing is recursive:

TArray<FString> FileNames;
FFileManagerGeneric FileMgr;
FileMgr.SetSandboxEnabled(true);
FString searchFolder(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Content")));

// 其实就是const TCHAR* extension = L"*.uasset";
const TCHAR* extension = _T("*.uasset");
FileMgr.FindFilesRecursive(FileNames, *searchFolder, extension,
    true,  // to list files
    false); // to skip directories

Guess you like

Origin blog.csdn.net/alexhu2010q/article/details/130011140