Calmer的文章

  • 首页
  • 文章归档
  • 关于页面

  • 搜索
体验游戏 笔记 推荐 工具链 工具使用 小游戏 插件 UI 软件 教程

UE4/5中编辑器和命令的扩展

发表于 2022-01-16 | 分类于 游戏开发 | 0 | 阅读次数 1712

前言

使用UE4/5时,开发者可以利用众多命令和编辑器工具来快速完成任务和调试。在日常游戏开发中,经常会遇到一些常见问题。一旦某个问题解决的次数增多,自然而然地会形成各种工具来进行封装。

那这些命令和编辑的工具是怎么自定义的呢?
这里根据平时涉及到使用的工具方式归纳为了几种方式。

  • UE4/5工具蓝图(EditorUtilityWidget)
    最方便快捷的开发工具方式,直接使用蓝图连连看。
  • UE4/5编辑器菜单栏扩展(常用)
    直接在常用的几个窗口菜单栏中进行追加和扩展,是最常用的一种开发方式。
    • 关卡编辑窗口菜单扩展
    • 蓝图编辑器菜单扩展
    • 右键菜单扩展
  • 编辑器控制台命令(IConsoleCommand)
    在编辑器中按下"~"键,然后输入自定义的命令字即可执行。这种方式可以在运行时进行快速的调试,也可以在非运行时使用。UE4/5本身也有很多检测命令都是通过这种方式实现的,例如stat unit等。
  • 系统命令行(xxxCommandlet)
    用于通过系统命令行进行一些批量的操作,可以不用打开Editor。
  • 其他(暂不说明)

项目和插件的创建

  1. 新建一个CommandProj的UE C++项目,然后为其新建一个EditorToolSet的插件
    20220220112200.png
  2. 创建插件完成后,将插件的设置中将CanContainContent设置为true
    image.png
  3. 在EditorToolSet插件目录创建Content目录
    image.png

Note: 后续所有和工具和命令相关的代码和资源都放在此插件中


UE工具蓝图(EditorUtilityWidget)

如何创建

  1. 直接在插件EditorToolSet的Content目录下右键进行创建
    20220220103321.png
    逻辑写在蓝图的Graph中,和普通的蓝图的使用方法一模一样

扩展1:可以使用一个C++类继承于EditorUtilityWidget,再创建蓝图的时候选择父类为自己继承后的C++,这样就可以把部分逻辑写在C++中。
扩展2:可以专门封装一个蓝图静态库以供调用

如何使用

  1. 直接右键创建好的蓝图,Run Editor Utility Widget
    20220220103457.png
    使用一次后会出现在菜单栏
    20220220103844.png

思考:是否有办法,引擎刚进入就可以直接在菜单栏使用,而不是一定要使用一次。
参考解决方案:监听引擎刚初始化的事件,然后扫描插件Content目录下相关的资源,如果是EditorUtility类型就添加到菜单栏。

UE编辑器菜单栏扩展

如何创建

这里其实一开始创建项目的时候就已经说到过了
20220220112200.png

如何使用

创建后,重新编译后打开菜单栏上的Window选项,一般会追加在最下方
20220220112237.png
点击使用后就是一个Message弹窗
image.png

追加与新增

  1. 追加
    先说追加,追加就是在已有的菜单栏中进行追加选项
    在创建的插件目录中模块cpp中有这样一个方法,也就是追加在当前关卡编辑中Window栏的Layout下
    image.png
  2. 新增
    但想要新增一栏怎么做呢?
  • 关卡编辑的菜单栏的新增
    image.png
    新增一个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。

  • 蓝图编辑器的扩展

image.png

image.png
添加一个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上新增的方法。
image.png这部分代码是在给蓝图编辑顶部的菜单栏添加一项MyTools的下拉菜单栏。
image.png而这一部分代码是在给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表达式

如何使用

20220224000000.png
根据上述定义的命令,在编辑器的命令行,输入ShowMyWin命令,就可以很方便的执行了。

可以很方便的进行代码的调试

系统命令行(Commandlet)

如何创建

先在插件目录新增一个Commandlets目录
20220224000231.png
然后创建一个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为例.
20220224000704.png

./UnrealEditor.exe D:\Unreal4Projects\CommandProj\CommandProj.uproject --skipcompile -run=ExportFile -tempdir=HelloDir

上述命令执行完毕后会在相应的Plugins目录下,创建一个Saved目录,然后创建Test.lua,并写入HelloDir
20220224000859.png

可以在非运行游戏执行游戏某些操作,用于封装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


参考链接

  1. UE工具蓝图(EditorUtilityWidget)
    https://docs.unrealengine.com/4.27/en-US/InteractiveExperiences/UMG/UserGuide/EditorUtilityWidgets/
  2. 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
  3. 编辑器控制台命令(IConsoleCommand)
    stat unlua
    stat unit
    https://www.bilibili.com/read/cv5545647
  4. 系统命令行(Commandlet)
    https://zhuanlan.zhihu.com/p/377903983 https://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/CommandLineArguments/#概述
  5. 其他
    https://segmentfault.com/a/1190000018367388
  • 本文作者: Calmer
  • 本文链接: https://mytechplayer.com/archives/ue5中编辑器和命令的扩展
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
# 工具链
ImageToolLibrary插件
UE4的一些疑问
  • 文章目录
  • 站点概览
Calmer

Calmer

88 日志
7 分类
10 标签
RSS
Creative Commons
0%
© 2020 — 2025 Calmer
由 Halo 强力驱动
蜀ICP备20010026号-1川公网安备51019002006543
Copyright © 2020-2025 Calmer的文章