前言
使用UE4/5时,开发者可以利用众多命令和编辑器工具来快速完成任务和调试。在日常游戏开发中,经常会遇到一些常见问题。一旦某个问题解决的次数增多,自然而然地会形成各种工具来进行封装。
那这些命令和编辑的工具是怎么自定义的呢?
这里根据平时涉及到使用的工具方式归纳为了几种方式。
- UE4/5工具蓝图(EditorUtilityWidget)
最方便快捷的开发工具方式,直接使用蓝图连连看。 - UE4/5编辑器菜单栏扩展(常用)
直接在常用的几个窗口菜单栏中进行追加和扩展,是最常用的一种开发方式。- 关卡编辑窗口菜单扩展
- 蓝图编辑器菜单扩展
- 右键菜单扩展
- 编辑器控制台命令(IConsoleCommand)
在编辑器中按下"~"键,然后输入自定义的命令字即可执行。这种方式可以在运行时进行快速的调试,也可以在非运行时使用。UE4/5本身也有很多检测命令都是通过这种方式实现的,例如stat unit等。 - 系统命令行(xxxCommandlet)
用于通过系统命令行进行一些批量的操作,可以不用打开Editor。 - 其他(暂不说明)
项目和插件的创建
- 新建一个CommandProj的UE C++项目,然后为其新建一个EditorToolSet的插件
- 创建插件完成后,将插件的设置中将CanContainContent设置为true
- 在EditorToolSet插件目录创建Content目录
Note: 后续所有和工具和命令相关的代码和资源都放在此插件中
UE工具蓝图(EditorUtilityWidget)
如何创建
- 直接在插件EditorToolSet的Content目录下右键进行创建
逻辑写在蓝图的Graph中,和普通的蓝图的使用方法一模一样
扩展1:可以使用一个C++类继承于EditorUtilityWidget,再创建蓝图的时候选择父类为自己继承后的C++,这样就可以把部分逻辑写在C++中。
扩展2:可以专门封装一个蓝图静态库以供调用
如何使用
- 直接右键创建好的蓝图,Run Editor Utility Widget
使用一次后会出现在菜单栏
思考:是否有办法,引擎刚进入就可以直接在菜单栏使用,而不是一定要使用一次。
参考解决方案:监听引擎刚初始化的事件,然后扫描插件Content目录下相关的资源,如果是EditorUtility类型就添加到菜单栏。
UE编辑器菜单栏扩展
如何创建
这里其实一开始创建项目的时候就已经说到过了
如何使用
创建后,重新编译后打开菜单栏上的Window选项,一般会追加在最下方
点击使用后就是一个Message弹窗
追加与新增
- 追加
先说追加,追加就是在已有的菜单栏中进行追加选项
在创建的插件目录中模块cpp中有这样一个方法,也就是追加在当前关卡编辑中Window栏的Layout下
- 新增
但想要新增一栏怎么做呢?
- 关卡编辑的菜单栏的新增
新增一个FEditorMenuBar.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ToolBars/FEditorMenuBar.h"
#include "EditorToolSetCommands.h"
#include "LevelEditor.h"
FEditorMenuBar::FEditorMenuBar()
{
}
void FEditorMenuBar::Initialize()
{
BindCommands();
//获得LevelEditor模块
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>(FName("LevelEditor"));
TSharedPtr<FExtender> MenuExtender = GetExtender();
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
void FEditorMenuBar::BuildMenubar(FMenuBarBuilder& MenuBarBuilder)
{
//新增一个MyTools项,是一个下拉菜单
MenuBarBuilder.AddPullDownMenu(
FText::FromString(TEXT("MyTools")),
FText::GetEmpty(),
FNewMenuDelegate::CreateRaw(this, &FEditorMenuBar::FillPullDownMenu));
}
void FEditorMenuBar::FillPullDownMenu(FMenuBuilder& MenuBuilder)
{
/*MenuBuilder.AddSubMenu(
FText::FromString(TEXT("FirstTypeTool")),
FText(),
FNewMenuDelegate::CreateLambda([=](FMenuBuilder& MenuBuilder)
{
MenuBuilder.AddMenuEntry(FEditorToolSetCommands::Get().DoSomething01);
}));*/
MenuBuilder.AddSubMenu(
FText::FromString(TEXT("FirstTypeTool")),
FText(),
FNewMenuDelegate::CreateRaw(this, &FEditorMenuBar::FillFirstTypeToolMenu));
MenuBuilder.BeginSection(FName("S"), FText::FromString("Others"));
{
MenuBuilder.AddWidget(
SNew(SImage),
FText::FromString("IamgeWidget")
);
MenuBuilder.AddEditableText(
FText::FromString("EditableText"),
FText::FromString("EditableText_Tips"),
FSlateIcon(),
FText::FromString("Hello World!!!")
);
}
/*MenuBuilder.BeginSection(FName("S"), FText::FromString("Others"));
{
MenuBuilder.AddWidget(
SNew(SImage),
LOCTEXT("IamgeWidget", "Iamge widget")
);
MenuBuilder.AddEditableText(
LOCTEXT("EditableText", "EditableText"),
LOCTEXT("EditableText_Tips", "EditableText_Tips"),
FSlateIcon(),
LOCTEXT("Text", "Hello World!!!")
);
}*/
MenuBuilder.EndSection();
MenuBuilder.AddSeparator();
MenuBuilder.AddMenuEntry(FEditorToolSetCommands::Get().DoSomething01);
}
void FEditorMenuBar::FillFirstTypeToolMenu(FMenuBuilder& MenuBuilder)
{
MenuBuilder.AddMenuEntry(FEditorToolSetCommands::Get().DoSomething02);
}
TSharedRef<FExtender> FEditorMenuBar::GetExtender()
{
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender());
//在关卡编辑器Help选项后进行新增
MenuExtender->AddMenuBarExtension("Help", EExtensionHook::After, CommandList, FMenuBarExtensionDelegate::CreateRaw(this, &FEditorMenuBar::BuildMenubar));
return MenuExtender.ToSharedRef();
}
上述核心代码在Initialize方法中,获得了LevelEditorModule,然后给他添加Extender,在创建的Extender中注册了一个委托BuildMenubar,可以在这个方法中看到其为这个新增的项定义为了一个下拉菜单项,也就是图示中的MyTools。
- 蓝图编辑器的扩展
添加一个FEditorToolBar.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ToolBars/FEditorToolBar.h"
#include "EditorToolSetCommands.h"
#include "BlueprintEditorModule.h"
FEditorToolBar::FEditorToolBar()
{
}
void FEditorToolBar::Initialize()
{
BindCommands();
//获得BlueprintEditor模块
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]);
return GetExtender();
}));
TSharedRef<FExtender> ToolbarExtender(new FExtender());
const auto ExtensionDelegate = FMenuBarExtensionDelegate::CreateRaw(this, &FEditorToolBar::BuildMenubar);
ToolbarExtender->AddMenuBarExtension("Debug", EExtensionHook::After, CommandList, ExtensionDelegate);
BlueprintEditorModule.GetMenuExtensibilityManager()->AddExtender(ToolbarExtender);
}
//在蓝图编辑里面添加到顶部菜单
void FEditorToolBar::BuildMenubar(FMenuBarBuilder& MenuBarBuilder)
{
MenuBarBuilder.AddPullDownMenu(
FText::FromString(TEXT("MyTools")),
FText::GetEmpty(),
FNewMenuDelegate::CreateLambda([&](FMenuBuilder& MenuBuilder) {
MenuBuilder.AddMenuEntry(FEditorToolSetCommands::Get().DoSomething01);
})
);
}
//在蓝图编辑里面添加到次级菜单
void FEditorToolBar::BuildToolbar(FToolBarBuilder& ToolbarBuilder)
{
ToolbarBuilder.BeginSection(NAME_None);
const auto Blueprint = Cast<UBlueprint>(ContextObject);
FString InStyleName = "Hello";
UE_LOG(LogTemp, Log, TEXT("InStyleName=%s"), *InStyleName);
ToolbarBuilder.AddComboButton(
FUIAction(),
FOnGetContent::CreateLambda([&]()
{
const FEditorToolSetCommands& Commands = FEditorToolSetCommands::Get();
FMenuBuilder MenuBuilder(true, CommandList);
if (!1)
{
MenuBuilder.AddMenuEntry(Commands.DoSomething01);
}
else
{
MenuBuilder.AddMenuEntry(Commands.DoSomething01);
MenuBuilder.AddMenuEntry(Commands.DoSomething02);
}
return MenuBuilder.MakeWidget();
}),
FText::FromString("ToolBar"),
FText::FromString("ToolBarToolTip"),
FSlateIcon("UnLuaEditorStyle", *InStyleName)
);
ToolbarBuilder.EndSection();
}
TSharedRef<FExtender> FEditorToolBar::GetExtender()
{
TSharedRef<FExtender> ToolbarExtender(new FExtender());
const auto ExtensionDelegate = FToolBarExtensionDelegate::CreateRaw(this, &FEditorToolBar::BuildToolbar);
ToolbarExtender->AddToolBarExtension("Debugging", EExtensionHook::After, CommandList, ExtensionDelegate);
return ToolbarExtender;
}
对比于在关卡编辑器的菜单中新增一个项,其实蓝图编辑器也是大同小异,这里额外书写了一下在蓝图编辑器的ToolBar上新增的方法。
这部分代码是在给蓝图编辑顶部的菜单栏添加一项MyTools的下拉菜单栏。
而这一部分代码是在给ToolBar中添加一项ToolBar的按钮组合。
这里类似也能用到关卡编辑器中
编辑器控制台命令(IConsoleCommand)
如何创建
新增一个类FOpenConsoleCommand.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
/**
*
*/
class EDITORTOOLSET_API FOpenConsoleCommand
{
public:
FOpenConsoleCommand();
~FOpenConsoleCommand();
void RegisterAllConsoleCommands();
void UnRegisterAllConsoleCommands();
void DisplayTheWindow(FString WindowTitle);
private:
IConsoleCommand* DisplayTestCommand;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "EditorConsoleCommands/FOpenConsoleCommand.h"
#include "MainFrame.h"
FOpenConsoleCommand::FOpenConsoleCommand()
{
}
FOpenConsoleCommand::~FOpenConsoleCommand()
{
}
void FOpenConsoleCommand::RegisterAllConsoleCommands()
{
//DisplayTestCommand = IConsoleManager::Get().RegisterConsoleCommand(TEXT("DisplayTest"), TEXT("DisplayTest"), FConsoleCommandDelegate::CreateRaw(this, &FMyToolBarFuncModule::PluginButtonClicked), ECVF_Default);
//注册ShowMyWin Editor控制台命令
DisplayTestCommand = IConsoleManager::Get().RegisterConsoleCommand(TEXT("ShowMyWin"), TEXT("ShowMyWin"), FConsoleCommandWithArgsDelegate::CreateLambda([this](const TArray<FString>& Args) {
FString Title;
for (int i = 0; i < Args.Num(); i++)
{
Title.Append(Args[i]);
}
if (Title.IsEmpty())
{
Title = TEXT("Empty");
}
this->DisplayTheWindow(Title);
}), ECVF_Default);
}
void FOpenConsoleCommand::UnRegisterAllConsoleCommands()
{
if (DisplayTestCommand)
{
IConsoleManager::Get().UnregisterConsoleObject(DisplayTestCommand);
DisplayTestCommand = nullptr;
}
}
void FOpenConsoleCommand::DisplayTheWindow(FString WindowTitle)
{
TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
.Title(FText::FromString(WindowTitle))
.ClientSize(FVector2D(800, 400))
.SupportsMaximize(false)
.SupportsMinimize(false);
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
if (MainFrameModule.GetParentWindow().IsValid())
{
FSlateApplication::Get().AddWindowAsNativeChild
(CookbookWindow, MainFrameModule.GetParentWindow()
.ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(CookbookWindow);
}
}
通过上述代码,定义了一个DisplayTestCommand,然后使用IConsoleManager注册命令的委托,上述命令定义为:ShowMyWin,其委托是一个lambda表达式
如何使用
根据上述定义的命令,在编辑器的命令行,输入ShowMyWin命令,就可以很方便的执行了。
可以很方便的进行代码的调试
系统命令行(Commandlet)
如何创建
先在插件目录新增一个Commandlets目录
然后创建一个Commandlet结尾的.h和.cpp。并继承于UCommandlet类
例如上图的ExportFileCommandlet
// Fill out your copyright notice in the Description page of Project Settings.
#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 SaveFileTest(FString str);
};
// Fill out your copyright notice in the Description page of Project Settings.
#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);
FString TempDir = ParamsMap[TEXT("tempdir")];
SaveFileTest(TempDir);
return 0;
}
void UExportFileCommandlet::SaveFileTest(FString str)
{
FString Directory = IPluginManager::Get().FindPlugin("EditorToolSet")->GetBaseDir() / TEXT("Saved");
IFileManager& FileManager = IFileManager::Get();
if (!FileManager.DirectoryExists(*Directory))
{
FileManager.MakeDirectory(*Directory);
}
FString FileName("Test");
const FString FilePath = FString::Printf(TEXT("%s/%s.lua"), *Directory, *FileName);
//FString FileContent;
// FFileHelper::LoadFileToString(FileContent, *FilePath);
bool bResult = FFileHelper::SaveStringToFile(str, *FilePath);
if (!bResult)
{
UE_LOG(LogTemp, Log, TEXT("SaveFailed"));
}
}
这样就定义了一个系统命令行,这里也就是ExportFile。
如何使用
直接打开window的cmd,然后找到UE的编辑器和我们所创建的项目CommandProj的uproject文件,添加上相关的参数就可以执行所定义commandlet,这里还是以ExportFile为例.
./UnrealEditor.exe D:\Unreal4Projects\CommandProj\CommandProj.uproject --skipcompile -run=ExportFile -tempdir=HelloDir
上述命令执行完毕后会在相应的Plugins目录下,创建一个Saved目录,然后创建Test.lua,并写入HelloDir
可以在非运行游戏执行游戏某些操作,用于封装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);
}
}
常用的操作方法函数
直接打开EditorUtility
void FEditorBar::OpenUtilityEditor()
{
FString FromRoot = IPluginManager::Get().FindPlugin("EditorToolSet")->GetBaseDir() / TEXT("Resources");
UE_LOG(LogTemp, Log, TEXT("[OpenUtilityEditor] Open the LuaModuleEditor...%s"), *FromRoot);
UEditorUtilityWidgetBlueprint* EditorWidget = LoadObject<UEditorUtilityWidgetBlueprint>(nullptr,
TEXT("EditorUtilityWidgetBlueprint'/EditorToolSet/EditorUtilityTestWidgetBlueprint.EditorUtilityTestWidgetBlueprint'"));
if (EditorWidget)
{
UEditorUtilitySubsystem* EditorUtilitySubsystem = GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>();
EditorUtilitySubsystem->SpawnAndRegisterTab(EditorWidget);
}
else
{
UE_LOG(LogTemp, Error, TEXT("[OpenUtilityEditor] Cannot load the EditorUtilityWidgetBlueprint!"));
}
}
思考:既然有方法可以直接打开EditorUtility蓝图,是否可以上述4种方式中,使用方式2来调用方式1中创建的蓝图?
打开目录选择器
FString FEditorBar::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;
}
提示提示框
FText DialogText = FText::Format(
LOCTEXT("PluginButtonDialogText", "Add code to {0} in {1} to override this button's actions"),
FText::FromString(TEXT("FEditorToolSetModule::PluginButtonClicked()")),
FText::FromString(TEXT("EditorToolSet.cpp"))
);
FMessageDialog::Open(EAppMsgType::Ok, DialogText);
总结
使用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
https://www.zhihu.com/people/SQTaoger - 编辑器控制台命令(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