前言
仓库已经提前搭建好了。 😁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 脚本,不容易导致错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include "PyToolkit.h" #define LOCTEXT_NAMESPACE "FPyToolkitModule" void FPyToolkitModule::StartupModule () { TickHandle = FTicker::GetCoreTicker ().AddTicker (FTickerDelegate::CreateLambda ([this ](float DeltaTime) { Tick (DeltaTime); return true ; })); } void FPyToolkitModule::ShutdownModule () { } void FPyToolkitModule::Tick (const float InDeltaTime) { 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 目录里面添加的依赖库如下:
上面是我在 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 配置执行相应的命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 { "section" : { "Model" : "建模" , "Anim" : "动画" , "FX" : "特效" , "Render" : "渲染" , "Help" : "帮助" } , "menu" : { "Model_Tool" : { "section" : "Model" , "label" : "演示:模型处理工具(打印到屏幕)" , "type" : "PYTHON" , "command" : "unreal.SystemLibrary.print_string(None,'模型处理工具',text_color=[255,255,255,255])" } , "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 配置部分,自动生成对应分组脚本的脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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[data['type' ].lower()] data['command' ] = data['command' ].format (Content=DIR) return menu_section_dict, menu_entry_dict def create_menu (): menu_section_dict, menu_entry_dict = read_menu_json("%s/menu.json" % DIR) menus = unreal.ToolMenus.get() 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!" ) script_menu = main_menu.add_sub_menu( main_menu.get_name(), "PythonTools" , "Tools" , "PyToolkit" ) for section, label in menu_section_dict.items(): script_menu.add_section(section, label) 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) menus.refresh_all_widgets()
c++ 蓝图实现 RenderTargetCube 渲染出 TextureCube
C++ 蓝图实现的功能在上次的 C++ 开发蓝图插件的总结里面有所提及 链接 这里刚好遇到了一个 Python API 没有的需求,于是就再补充讲一讲。 官方 API 提供了 RenderTarget 输出 Texture2D 的方法 链接
但是并没有提供 RenderTargetCube 输出 TextureCube 的方法 但是既然后有 RenderTarget 的操作方法,其实就是输出 RenderTargetCube 基本没有太大区别。 我这里的 C++ 代码就是抄引擎的源码然后稍微修改一下出来的。😜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 UTextureCube* UPyToolkitBPLibrary::RenderTargetCubeCreateStaticTextureCube (UTextureRenderTargetCube* RenderTarget, FString InName) { FString Name; FString PackageName; IAssetTools& AssetTools = FModuleManager::Get ().LoadModuleChecked <FAssetToolsModule>("AssetTools" ).Get (); if (!InName.Contains (TEXT ("/" ))) { FString AssetName = RenderTarget->GetOutermost ()->GetName (); const FString SanitizedBasePackageName = UPackageTools::SanitizePackageName (AssetName); const FString PackagePath = FPackageName::GetLongPackagePath (SanitizedBasePackageName) + TEXT ("/" ); AssetTools.CreateUniqueAssetName (PackagePath, InName, PackageName, Name); } else { InName.RemoveFromStart (TEXT ("/" )); InName.RemoveFromStart (TEXT ("Content/" )); InName.StartsWith (TEXT ("Game/" )) == true ? InName.InsertAt (0 , TEXT ("/" )) : InName.InsertAt (0 , TEXT ("/Game/" )); AssetTools.CreateUniqueAssetName (InName, TEXT ("" ), PackageName, Name); } UTextureCube* NewTex = nullptr ; NewTex = RenderTarget->ConstructTextureCube (CreatePackage (NULL , *PackageName), Name, RenderTarget->GetMaskedFlags () | RF_Public | RF_Standalone); if (NewTex != nullptr ) { NewTex->MarkPackageDirty (); FAssetRegistryModule::AssetCreated (NewTex); } return NewTex; }
基本上方法调用为 ConstructTextureCube
即可
总结
作为 TA ,开发 C++ 要牢记,我们只是大自然的搬运工。 不要接引擎压根都没有的功能需求,这种功能开发通常需要交给程序去搞。 我们的工作是将引擎的功能整合自动化。 查源码虽然很麻烦,很绕,但是很多类用法可以参考源代码的 😎