前言
使用UE4/5时,开发者可以利用众多命令和编辑器工具来快速完成任务和调试。在日常游戏开发中,经常会遇到一些常见问题。一旦某个问题解决的次数增多,自然而然地会形成各种工具来进行封装。
那这些命令和编辑的工具是怎么自定义的呢?
这里根据平时涉及到使用的工具方式归纳为了几种方式。
- UE4/5工具蓝图(EditorUtilityWidget)
最方便快捷的开发工具方式,直接使用蓝图连连看。 - UE4/5编辑器菜单栏扩展(常用)
直接在常用的几个窗口菜单栏中进行追加和扩展,是最常用的一种开发方式。- 主(关卡)窗口菜单扩展
- 蓝图窗口菜单扩展
- 右键菜单扩展
- 编辑器控制台命令(IConsoleCommand)
在编辑器中按下"~"键,然后输入自定义的命令字即可执行。这种方式可以在运行时进行快速的调试,也可以在非运行时使用。UE4/5本身也有很多检测命令都是通过这种方式实现的,例如stat unit等。 - 系统命令行(xxxCommandlet)
用于通过系统命令行进行一些批量的操作,可以不用打开Editor。 - 其他(暂不说明)
项目和插件的创建
- 新建一个EditorToolFramework的UE45 C++项目,为其新建一个EditorToolSet的插件
- 插件设置包含Content目录
Note: 后续所有和工具和命令相关的代码和资源都放在此插件中
UE4/5工具蓝图(EditorUtilityWidget)
创建
- 在插件EditorToolSet的Content目录下右键进行创建
逻辑写在蓝图的Graph中,和普通的蓝图的使用方法一模一样
扩展1:可以使用一个C++类继承于EditorUtilityWidget,再创建蓝图的时候选择父类为自己继承后的C++,这样就可以把部分逻辑写在C++中。
扩展2:可以专门封装一个蓝图静态库以供调用
使用
- 右键创建好的蓝图,Run Editor Utility Widget
使用一次后会出现在菜单栏
思考:是否有办法,引擎刚进入就可以直接在菜单栏使用,而不用预先使用一次。
参考解决方案:监听引擎刚初始化的事件,然后扫描插件Content目录下相关的资源,如果是EditorUtility类型就添加到菜单栏。
UE4/5编辑器菜单栏扩展
创建
上述创建插件的时候已经根据模板默认创建过了
使用
创建后,重新编译后打开菜单栏上的Window选项,一般会追加在最下方
点击使用后就是一个Message弹窗
新增
根据模板示例代码,大致分为3个步骤
- 定义和注册命令
- 实现和映射命令
- 添加菜单按钮响应,按钮归类
在插件源码目录新增ToolBars目录
- 两个子目录
- MainMenu目录:用于主(关卡)窗口菜单栏扩展
- BPMenu目录:用于蓝图菜单栏扩展
- 三个类
- 以Commands结尾的类:用于命令定义和注册
- 以Controller结尾的类:用于实现命令的具体逻辑
- 以MenuBar结尾的类:用于界面表现逻辑的实现和归类
定义和注册命令
以FEditorMainMenuCommands为例子,在Commands类中进行定义和注册。
#pragma once
#include "CoreMinimal.h"
#include "EditorToolSetStyle.h"
class EDITORTOOLSET_API FEditorMainMenuCommands : public TCommands<FEditorMainMenuCommands>
{
public:
FEditorMainMenuCommands()
: TCommands<FEditorMainMenuCommands>(
TEXT("EditorMainMenu"), NSLOCTEXT("Contexts", "EditorToolSet", "EditorToolSet Plugin"), NAME_None,
FEditorToolSetStyle::GetStyleSetName())
{
}
virtual ~FEditorMainMenuCommands() override;
//2.注册命令
virtual void RegisterCommands() override;
public:
//1.定义命令
TSharedPtr<FUICommandInfo> MainMenuShowMessage;
TSharedPtr<FUICommandInfo> MainMenuOpenWindow;
TSharedPtr<FUICommandInfo> MainMenuChooseFolder;
};
#include "ToolBars/MainMenu/FEditorMainMenuCommands.h"
#define LOCTEXT_NAMESPACE "FEditorMainMenuCommands"
FEditorMainMenuCommands::~FEditorMainMenuCommands(){}
void FEditorMainMenuCommands::RegisterCommands()
{
UI_COMMAND(MainMenuShowMessage, "MainMenuShowMessage", "MainMenuShowMessage Help", EUserInterfaceActionType::Button, FInputGesture());
UI_COMMAND(MainMenuOpenWindow, "MainMenuOpenWindow", "MainMenuOpenWindow Help", EUserInterfaceActionType::Button, FInputGesture());
UI_COMMAND(MainMenuChooseFolder, "MainMenuChooseFolder", "MainMenuChooseFolder Help", EUserInterfaceActionType::Button, FInputGesture());
}
#undef LOCTEXT_NAMESPACE
实现和映射命令
以FEditorMainMenuController为例,在Controller里实现和映射命令
#pragma once
#include "CoreMinimal.h"
class EDITORTOOLSET_API FEditorMainMenuController
{
public:
FEditorMainMenuController();
~FEditorMainMenuController();
TSharedRef<FUICommandList> GetCommandList();
//映射命令
void BindCommands();
//命令实现具体逻辑
void MainMenuShowMessage();
void MainMenuOpenWindow();
void MainMenuChooseFolder();
protected:
//命令列表
TSharedRef<FUICommandList> CommandList;
};
#include "ToolBars/MainMenu/FEditorMainMenuController.h"
#include "EditorToolSetBPLibrary.h"
#include "ToolBars/MainMenu/FEditorMainMenuCommands.h"
FEditorMainMenuController::FEditorMainMenuController():CommandList(new FUICommandList)
{
//在构造函数中进行命令上下文注册
FEditorMainMenuCommands::Register();
}
FEditorMainMenuController::~FEditorMainMenuController()
{
FEditorMainMenuCommands::Unregister();
}
//打开消息弹窗命令
void FEditorMainMenuController::MainMenuShowMessage()
{
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("MainMenuShowMessage")));
}
//打开BPUtility命令
void FEditorMainMenuController::MainMenuOpenWindow()
{
UEditorToolSetBPLibrary::OpenBPUtilityByPath(TEXT("EditorUtilityWidgetBlueprint'/EditorToolSet/BP_RootEditorUtility.BP_RootEditorUtility'"));
}
//打开文件夹选择弹窗命令
void FEditorMainMenuController::MainMenuChooseFolder()
{
const FString Path = UEditorToolSetBPLibrary::ChooseFolderByExplorer();
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Path));
}
TSharedRef<FUICommandList> FEditorMainMenuController::GetCommandList()
{
return CommandList;
}
//映射
void FEditorMainMenuController::BindCommands()
{
const auto& Commands = FEditorMainMenuCommands::Get();
CommandList->MapAction(Commands.MainMenuOpenWindow,
FExecuteAction::CreateRaw(this, &FEditorMainMenuController::MainMenuOpenWindow));
CommandList->MapAction(Commands.MainMenuShowMessage,
FExecuteAction::CreateRaw(this, &FEditorMainMenuController::MainMenuShowMessage));
CommandList->MapAction(Commands.MainMenuChooseFolder,
FExecuteAction::CreateRaw(this, &FEditorMainMenuController::MainMenuChooseFolder));
}
添加菜单按钮响应命令,按钮归类
- 主(关卡)窗口菜单扩展
以FEditorMainMenuBar为例,在其中添加一个下拉菜单和二级菜单选项
#pragma once
#include "CoreMinimal.h"
#include "FEditorMainMenuController.h"
class EDITORTOOLSET_API FEditorMainMenuBar
{
public:
FEditorMainMenuBar();
virtual ~FEditorMainMenuBar() = default;
virtual void Initialize();
void BindController(FEditorMainMenuController* Controller);
public:
FText ToolName;
protected:
void BuildMenubar(FMenuBarBuilder& MenuBarBuilder);
void FillPullDownMenu(FMenuBuilder& MenuBuilder);
private:
FEditorMainMenuController* EditorMainMenuController;
};
#include "ToolBars/MainMenu/FEditorMainMenuBar.h"
#include "LevelEditor.h"
#include "ToolBars/MainMenu/FEditorMainMenuCommands.h"
FEditorMainMenuBar::FEditorMainMenuBar()
{
ToolName = FText::FromString(TEXT("MainTools"));
}
void FEditorMainMenuBar::BindController(FEditorMainMenuController* Controller)
{
EditorMainMenuController = Controller;
}
void FEditorMainMenuBar::Initialize()
{
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>(FName("LevelEditor"));
TSharedRef<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddMenuBarExtension("Help", EExtensionHook::After, EditorMainMenuController->GetCommandList(), FMenuBarExtensionDelegate::CreateRaw(this, &FEditorMainMenuBar::BuildMenubar));
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
void FEditorMainMenuBar::BuildMenubar(FMenuBarBuilder& MenuBarBuilder)
{
//添加下拉菜单
MenuBarBuilder.AddPullDownMenu(
ToolName,
FText::GetEmpty(),
FNewMenuDelegate::CreateRaw(this, &FEditorMainMenuBar::FillPullDownMenu));
}
void FEditorMainMenuBar::FillPullDownMenu(FMenuBuilder& MenuBuilder)
{
//二级菜单
MenuBuilder.AddSubMenu(
FText::FromString(TEXT("Common")),
FText::GetEmpty(),
FNewMenuDelegate::CreateLambda([](FMenuBuilder& MenuBuilder)
{
MenuBuilder.AddMenuEntry(FEditorMainMenuCommands::Get().MainMenuShowMessage);
MenuBuilder.AddMenuEntry(FEditorMainMenuCommands::Get().MainMenuChooseFolder);
}));
//响应打开窗口命令
MenuBuilder.AddMenuEntry(FEditorMainMenuCommands::Get().MainMenuOpenWindow);
MenuBuilder.AddSeparator();
MenuBuilder.BeginSection(FName("S"), FText::FromString("Others"));
{
MenuBuilder.AddEditableText(
FText::FromString("EditableText"),
FText::FromString("EditableText_Tips"),
FSlateIcon(),
FText::FromString("Hello Editor!!!")
);
MenuBuilder.AddWidget(SNew(SImage),FText::FromString("ImageWidget"));
}
MenuBuilder.EndSection();
}
上述核心代码在Initialize方法中获得LevelEditorModule后,添加Extender,并在创建的Extender中注册了一个委托BuildMenubar,这个方法中可以看到这个新增项为一个下拉菜单项。
- 蓝图窗口的扩展(具体代码见插件源码)
对比于在关卡编辑器的菜单中新增一个项,其实蓝图编辑器也是大同小异,这里额外书写了在蓝图编辑器的ToolBar上新增的方法。如下Initialize方法:
void FEditorBPMenuBar::Initialize()
{
//蓝图编辑器菜单栏,BPMenuTools
TSharedRef<FExtender> ToolbarExtender(new FExtender());
const auto ExtensionDelegate = FMenuBarExtensionDelegate::CreateRaw(this, &FEditorBPMenuBar::BuildMenubar);
ToolbarExtender->AddMenuBarExtension("Debug", EExtensionHook::After, EditorBPMenuController->GetCommandList(),
ExtensionDelegate);
BlueprintEditorModule.GetMenuExtensibilityManager()->AddExtender(ToolbarExtender);
//蓝图编辑器ToolBar
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>(FName("Kismet"));
auto& ExtenderDelegates = BlueprintEditorModule.GetMenuExtensibilityManager()->GetExtenderDelegates();
ExtenderDelegates.Add(FAssetEditorExtender::CreateLambda(
[&](const TSharedRef<FUICommandList>, const TArray<UObject*> ContextSensitiveObjects)
{
ContextObject = ContextSensitiveObjects.Num() < 1 ? nullptr : Cast<UBlueprint>(ContextSensitiveObjects[0]);
TSharedRef<FExtender> ToolbarExtender(new FExtender());
const auto ExtensionDelegate = FToolBarExtensionDelegate::CreateRaw(this, &FEditorBPMenuBar::BuildToolbar);
ToolbarExtender->AddToolBarExtension("Debugging", EExtensionHook::After,
EditorBPMenuController->GetCommandList(), ExtensionDelegate);
return ToolbarExtender;
}));
}
这里类似也能用到关卡编辑器中
初始化
- 在插件StartupModule里构造
- 在引擎初始化回调里进行初始化
编辑器控制台命令(IConsoleCommand)
创建
- FOpenConsoleCommand:用于模块注册和管理
- FEditorCommandBase: 命令基类
- Commands目录下,每一个类都是一条Console命令
示例
以FShowWindowCommand为例
#pragma once
#include "CoreMinimal.h"
#include "EditorConsoleCommands/FEditorCommandBase.h"
class EDITORTOOLSET_API FShowWindowCommand :public FEditorCommandBase
{
public:
FShowWindowCommand();
virtual ~FShowWindowCommand() override;
virtual void Execute(const TArray<FString>& Args) override;
};
#include "EditorConsoleCommands/Commands/FShowWindowCommand.h"
#include "Interfaces/IMainFrameModule.h"
FShowWindowCommand::FShowWindowCommand()
{
CommandName = TEXT("ShowWindow");
HelpContent = TEXT("ShowWindowHelp");
}
FShowWindowCommand::~FShowWindowCommand()
{}
void FShowWindowCommand::Execute(const TArray<FString>& Args)
{
FEditorCommandBase::Execute(Args);
FString WindowTitle;
for (int i = 0; i < Args.Num(); i++)
{
WindowTitle.Append(Args[i]);
}
const TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
.Title(FText::FromString(WindowTitle))
.ClientSize(FVector2D(800, 400))
.SupportsMaximize(false)
.SupportsMinimize(false);
const IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
if (MainFrameModule.GetParentWindow().IsValid())
{
FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow, MainFrameModule.GetParentWindow().ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(CookbookWindow);
}
}
其继承与命令基类FEditorCommandBase,实现虚函数Execute即可。运行命令ShowWindow:打开一个以参数拼接成名字的窗口
初始化
- OpenConsoleCommand在模块Startup时初始化
- 单独命令初始,以FShowWindowCommand为例
使用
根据上述定义的命令,在编辑器的命令行,输入ShowWindow命令,就可以很方便的执行了。
可以很方便的进行代码的调试
系统命令行(Commandlet)
如何创建
先在插件目录新增一个Commandlets目录
然后创建一个Commandlet结尾的.h和.cpp。并继承于UCommandlet类
例如上图的ExportFileCommandlet
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "ExportFileCommandlet.generated.h"
UCLASS()
class EDITORTOOLSET_API UExportFileCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
virtual int32 Main(const FString& Params) override;
private:
void SaveFile(const FString& ContentStr) const;
};
#include "Commandlets/ExportFileCommandlet.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/FileHelper.h"
int32 UExportFileCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString Content = ParamsMap[TEXT("content")];
SaveFile(Content);
return 0;
}
void UExportFileCommandlet::SaveFile(const FString& ContentStr) const
{
const FString Directory = IPluginManager::Get().FindPlugin(TEXT("EditorToolSet"))->GetBaseDir() / TEXT("Saved");
IFileManager& FileManager = IFileManager::Get();
if (!FileManager.DirectoryExists(*Directory))
{
FileManager.MakeDirectory(*Directory);
}
const FString FileName(TEXT("Test"));
const FString FilePath = FString::Printf(TEXT("%s/%s.txt"), *Directory, *FileName);
if (!FFileHelper::SaveStringToFile(ContentStr, *FilePath))
{
UE_LOG(LogTemp, Log, TEXT("SaveFailed"));
}
}
这样就定义了一个系统命令行,也就是ExportFile
使用
直接打开window的cmd,然后找到UE的编辑器和我们所创建的项目CommandProj的uproject文件,添加上相关的参数就可以执行所定义commandlet,这里还是以ExportFile为例.
./UnrealEditor.exe E:\UEProjects\EditorToolFramework\EditorToolFramework.uproject --skipcompile -run=ExportFile -content=HelloEditor
上述命令执行完毕后会在相应的Plugins目录下,创建一个Saved目录,然后创建Test.txt,并写入HelloEditor
可以在非运行游戏执行游戏某些操作,用于封装bat进行批量操作
其他
- 常用c++库方法导出为dll给其他语言使用,如:python来实现
- 其他软件
扩展
控制台命令,直接调用接口去执行(运行时)
void UKismetSystemLibrary::ExecuteConsoleCommand(const UObject* WorldContextObject, const FString& Command, APlayerController* Player)
{
// First, try routing through the primary player
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull);
APlayerController* TargetPC = Player || !World ? Player : World->GetFirstPlayerController();
if (TargetPC)
{
TargetPC->ConsoleCommand(Command, true);
}
else
{
GEngine->Exec(World, *Command);
}
}
常用EditorToolSetBPLibaray
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "EditorToolSetBPLibrary.generated.h"
UCLASS()
class EDITORTOOLSET_API UEditorToolSetBPLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category="EditorToolSet")
static void OpenBPUtilityByPath(const FString& Path);
UFUNCTION(BlueprintCallable, Category="EditorToolSet")
static FString ChooseFolderByExplorer();
UFUNCTION(BlueprintCallable, Category="EditorToolSet")
static FString GetToolSetPluginBasePath();
};
#include "EditorToolSetBPLibrary.h"
#include "DesktopPlatformModule.h"
#include "EditorUtilitySubsystem.h"
#include "EditorUtilityWidgetBlueprint.h"
#include "Interfaces/IPluginManager.h"
void UEditorToolSetBPLibrary::OpenBPUtilityByPath(const FString& Path)
{
if (UEditorUtilityWidgetBlueprint* EditorWidget = LoadObject<UEditorUtilityWidgetBlueprint>(nullptr, *Path))
{
UEditorUtilitySubsystem* EditorUtilitySubsystem = GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>();
EditorUtilitySubsystem->SpawnAndRegisterTab(EditorWidget);
}
else
{
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString("Cannot load the EditorUtilityWidgetBlueprint!"));
}
}
FString UEditorToolSetBPLibrary::ChooseFolderByExplorer()
{
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
FString OutputDirectory;
const FString DefaultPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
UE_LOG(LogTemp, Log, TEXT("DefaultPath: %s"), *DefaultPath);
bool successfullySelected = false;
if (DesktopPlatform)
{
const void* ParentWindowWindowHandle = FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr);
successfullySelected = DesktopPlatform->OpenDirectoryDialog(
ParentWindowWindowHandle,
FString(TEXT("选择目录")),
DefaultPath,
OutputDirectory
);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Cannot find the DesktopPlatform!"));
}
if (!successfullySelected)
{
return FString(TEXT(""));
}
FPaths::MakePathRelativeTo(OutputDirectory, *DefaultPath);
OutputDirectory = TEXT("/Game/") + OutputDirectory;
return OutputDirectory;
}
FString UEditorToolSetBPLibrary::GetToolSetPluginBasePath()
{
return IPluginManager::Get().FindPlugin(TEXT("EditorToolSet"))->GetBaseDir();
}
总结
使用UE4/5开发必然涉及到各种编辑器的开发,并且UE4/5引擎源码是开放的,因此几乎可以实现你能想到的所有功能。在此对工具命令的开发方式和框架进行了总结,后续可以在此基础上进行快速开发。
框架源码
Github: https://github.com/CalmLoader/UE4-5-EditorToolFramework
参考链接
- UE工具蓝图(EditorUtilityWidget)
https://docs.unrealengine.com/4.27/en-US/InteractiveExperiences/UMG/UserGuide/EditorUtilityWidgets/ - UE编辑器菜单栏扩展(MenuToolBar)
参考unlua蓝图的按钮功能源码
https://zhuanlan.zhihu.com/p/331677329
https://zhuanlan.zhihu.com/p/432072854
https://zhuanlan.zhihu.com/p/302480201 - 编辑器控制台命令(IConsoleCommand)
stat unlua
stat unit
https://www.bilibili.com/read/cv5545647 - 系统命令行(Commandlet)
https://zhuanlan.zhihu.com/p/377903983
https://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/CommandLineArguments/#概述 - 其他
https://segmentfault.com/a/1190000018367388