Unreal PyToolkit 插件
本文章转载自 智伤帝的个人博客 - 原文链接前言
仓库已经提前搭建好了。github地址
最近一直在忙,没有把文章总结整理好。
这个是基于 C++ 蓝图开发的 Python 插件
有一部分功能通过 C++ API 开发蓝图从而暴露到蓝图里面。
进而可以实现 Python 调用 C++ API 操作 Unreal 底层。
使用官方 Python 插件扩展蓝图比起第三方 UnrealEnginePython 要更加方便。
虽然现在官方的插件还不是很成熟,但是通过蓝图直接暴露 C++ API 的思路的确更加简单。
UnrealEnginePython 里面写扩展还需要处理 Python 的 C++ 部分,对于纯 Unreal C++ 开发来说的确不太灵活。
也难怪官方居然抛弃了相当成熟的第三方 Python 插件。
C++ 蓝图编写
其实蓝图编写我也是参照youtube 上的 unreal Python 教程学习的 youtube地址 B站地址 github地址
我之前编写 FBX 动画导入对比面板的时候也有所提到 链接
还有最近写的一篇文章也有比较详细的描述 Unreal Python 结合 C++ 开发蓝图库插件
看完教程,特别的需求就需要查论坛、查文档和查 Unreal 的 C++ 源码。
在文档方面, Unreal 这的特别不行
不说 C++ 文档各种案例都没有,描述都是参数类型和极其简单的描述。
文档的搜索还经常搜不到 API 信息, Python 的文档都比 C++ 强多了
一般首选查官方的 问答区 和 论坛
关键字用 C++ 或者 blueprint 开头,然后加上具体需求的英文关键字。
但是不要抱太好的期望,虚幻的社区也是不太活跃,有些问题有人问了,也没人回♂
如果上述的方法没有收获,就只能采用比较麻烦的方案了。
用 VScode 查引擎的 C++ 源码 。
VScode 可以直接搜索到文件内容,但是如果匹配很多文件的话就只能一个一个找有用的信息。
通常这种操作都比较麻烦,因为并不是所有的 API 函数都可以暴露到蓝图里面。
有些没有 Unreal 宏设置的内部函数蓝图调用会无法编译通过,后来找来程序帮忙才知道这个原因,我还是太菜了
并没有系统的学习 C++ ,Unreal 的 C++ 编程也还没深入看教程学习,最近一直围绕着 Python 开发解决问题
倒是有查过 大象无形 工具书,但是很多要实现的需求里头并没有,以后找时间要系统地学习一下 大象无形 工具书。
Python Startup 脚本调用
虽然我 C++ 开发很一般,但是抄代码的能力总归还是有的。
我想要实现 C++ 插件自动执行 Content 目录下的 initialize.py 脚本的效果。
官方的 Python 插件提供了 Startup 脚本的方案。
但是配置到官方的插件里并不好,配置拆散不是个事情。
于是我就参照 官方的 Python 插件实现自动加载 initialize.py 的效果。
其实参照了源码真的不复杂。
就是插件启动的时候调用 Unreal 的 Ticker ,触发一次 Tick 之后就停止函数的执行而已。
这样确保界面启动了之后再去执行相关的 Python 脚本,不容易导致错误。
// Copyright Epic Games, Inc. All Rights Reserved.
#include "PyToolkit.h"
#define LOCTEXT_NAMESPACE "FPyToolkitModule"
void FPyToolkitModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
// Initialize the tick handler
TickHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda((float DeltaTime)
{
Tick(DeltaTime);
return true;
}));
}
void FPyToolkitModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module.For modules that support dynamic reloading,
// we call this function before unloading the module.
}
void FPyToolkitModule::Tick(const float InDeltaTime) {
// 参考 Python 官方插件 | 引擎初始化完成之后 通过 tick 来初始化 initialize.py 脚本
if (!bHasTicked) {
bHasTicked = true;
FString InitScript = TEXT("py \"") + FPaths::ProjectPluginsDir() / TEXT("PyToolkit/Content/initialize.py") + TEXT("\"");
GEngine->Exec(NULL, InitScript.GetCharArray().GetData());
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FPyToolkitModule, PyToolkit)
如果不用 Tick 来触发而是直接用 GEngine->Exec 在插件的构造函数执行 Python 代码会直接报错的。
Python 环境配置和初始化
Content 目录下的 Python 目录默认会添加到 Python sys.path 里面
Python 目录里面添加的依赖库如下:
PySideFBX Python SDKdayu_widgetsdayu_pathsingledispatchQt.py
上面是我在 Python 目录下添加的脚本库
initialize.py 脚本初始化
这个部分的代码也在 FBX 动画导入对比面板的文章里面有所提及。
不过需要注意的是当时提到的 __QtAppTick__ 回调函数里面,其实不需要不断执行 QtWidgets.QApplication.processEvent
加上表面上也不会对虚幻有什么实质的影响。
不过这边的程序用了第三方的 imgui 来写虚幻的 UI
结果发现如果加上这行 Qt 代码会导致 imgui 的 gui 组件无法聚焦♂
输入组件的焦点会被抢走。
后面我注释掉那一句也没有对 Qt 的界面产生影响~
json 配置 Unreal 菜单
Unreal 官方论坛的解决方案
在官方论坛可以查到关于 Python 生成菜单的方案。
大佬贴心的贴出了可运行的代码,我的代码也是在这个基础上优化成 json 配置菜单的效果。
section - 配置分组
menu
section - 设置分组
label - 配置显示名称
type - 配置 command 执行的类型 可以填写 python 和 command
command - 根据 type 配置执行相应的命令
{
"section":{
"Model":"建模",
"Anim":"动画",
"FX":"特效",
"Render":"渲染",
"Help":"帮助"
},
"menu":{
"Model_Tool" : {
"section": "Model",
"label": "演示:模型处理工具(打印到屏幕)",
"type": "PYTHON",
"command": "unreal.SystemLibrary.print_string(None,'模型处理工具',text_color=)"
},
"Anim_Tool" : {
"section": "Anim",
"label": "动画导入比较面板",
"type": "COMMAND",
"command": "py \"{Content}/Anim/FBXImporter/main.py\""
},
"FX_Tool" : {
"section": "FX",
"label": "Sequencer 导出选择元素动画为骨骼蒙皮",
"type": "COMMAND",
"command": "py \"{Content}/FX/sequencer_export_fbx.py\""
},
"SequencerFBX" : {
"section": "Render",
"label": "批量渲染 Sequencer 工具",
"type": "COMMAND",
"command": "py \"{Content}/Anim/sequencer_batch_render/render_tool.py\""
},
"Document" : {
"section": "Help",
"label": "帮助文档",
"type": "PYTHON",
"command": "import webbrowser;webbrowser.open_new_tab('https://github.com/FXTD-ODYSSEY/Unreal-PyToolkit')"
}
}
}
这个是 json 配置部分,自动生成对应分组脚本的脚本。
type_map = {
"command": unreal.ToolMenuStringCommandType.COMMAND,
"python": unreal.ToolMenuStringCommandType.PYTHON
}
def read_menu_json(path):
with open(path, 'r') as f:
data = json.load(f, object_pairs_hook=OrderedDict, encoding='utf-8')
menu_section_dict = data['section']
menu_entry_dict = data['menu']
for menu, data in menu_entry_dict.items():
data['type'] = type_map.lower()]
data['command'] = data['command'].format(Content=DIR)
return menu_section_dict, menu_entry_dict
def create_menu():
# NOTE 读取 menu json 配置
menu_section_dict, menu_entry_dict = read_menu_json("%s/menu.json" % DIR)
# NOTE https://forums.unrealengine.com/development-discussion/python-scripting/1767113-making-menus-in-py
menus = unreal.ToolMenus.get()
# NOTE 获取主界面的主菜单位置
main_menu = menus.find_menu("LevelEditor.MainMenu")
if not main_menu:
raise RuntimeError(
"Failed to find the 'Main' menu. Something is wrong in the force!")
# NOTE 添加一个下拉菜单
script_menu = main_menu.add_sub_menu(
main_menu.get_name(), "PythonTools", "Tools", "PyToolkit")
# NOTE 初始化下拉菜单的 Section 分组
for section, label in menu_section_dict.items():
script_menu.add_section(section, label)
# NOTE 根据 json 来配置菜单显示的 Entry
for menu, data in menu_entry_dict.items():
entry = unreal.ToolMenuEntry(
name=menu,
type=unreal.MultiBlockType.MENU_ENTRY,
insert_position=unreal.ToolMenuInsert(
"", unreal.ToolMenuInsertType.FIRST)
)
entry.set_label(data.get('label', "untitle"))
command = data.get('command', '')
entry.set_string_command(data.get("type", 0), "", string=command)
script_menu.add_menu_entry(data.get('section', ''), entry)
# NOTE 刷新组件
menus.refresh_all_widgets()
c++ 蓝图实现 RenderTargetCube 渲染出 TextureCube
C++ 蓝图实现的功能在上次的 C++ 开发蓝图插件的总结里面有所提及 链接
这里刚好遇到了一个 Python API 没有的需求,于是就再补充讲一讲。
官方 API 提供了 RenderTarget 输出 Texture2D 的方法 链接
但是并没有提供 RenderTargetCube 输出 TextureCube 的方法
但是既然后有 RenderTarget 的操作方法,其实就是输出 RenderTargetCube 基本没有太大区别。
我这里的 C++ 代码就是抄引擎的源码然后稍微修改一下出来的。
UTextureCube* UPyToolkitBPLibrary::RenderTargetCubeCreateStaticTextureCube(UTextureRenderTargetCube* RenderTarget, FString InName)
{
FString Name;
FString PackageName;
IAssetTools& AssetTools = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>(&#34;AssetTools&#34;).Get();
//Use asset name only if directories are specified, otherwise full path
if (!InName.Contains(TEXT(&#34;/&#34;)))
{
FString AssetName = RenderTarget->GetOutermost()->GetName();
const FString SanitizedBasePackageName = UPackageTools::SanitizePackageName(AssetName);
const FString PackagePath = FPackageName::GetLongPackagePath(SanitizedBasePackageName) + TEXT(&#34;/&#34;);
AssetTools.CreateUniqueAssetName(PackagePath, InName, PackageName, Name);
}
else
{
InName.RemoveFromStart(TEXT(&#34;/&#34;));
InName.RemoveFromStart(TEXT(&#34;Content/&#34;));
InName.StartsWith(TEXT(&#34;Game/&#34;)) == true ? InName.InsertAt(0, TEXT(&#34;/&#34;)) : InName.InsertAt(0, TEXT(&#34;/Game/&#34;));
AssetTools.CreateUniqueAssetName(InName, TEXT(&#34;&#34;), PackageName, Name);
}
UTextureCube* NewTex = nullptr;
// create a static 2d texture
NewTex = RenderTarget->ConstructTextureCube(CreatePackage(NULL, *PackageName), Name, RenderTarget->GetMaskedFlags() | RF_Public | RF_Standalone);
if (NewTex != nullptr)
{
// package needs saving
NewTex->MarkPackageDirty();
// Notify the asset registry
FAssetRegistryModule::AssetCreated(NewTex);
}
return NewTex;
}
基本上方法调用为 ConstructTextureCube 即可
总结
作为 TA ,开发 C++ 要牢记,我们只是大自然的搬运工。
不要接引擎压根都没有的功能需求,这种功能开发通常需要交给程序去搞。
我们的工作是将引擎的功能整合自动化。
查源码虽然很麻烦,很绕,但是很多类用法可以参考源代码的
页:
[1]