前言

  仓库已经提前搭建好了。 😁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
// 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([this](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 目录里面添加的依赖库如下:

  上面是我在 Python 目录下添加的脚本库

alt

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')"
}
}
}

alt

  这个是 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():
# 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++ 代码就是抄引擎的源码然后稍微修改一下出来的。😜

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();

//Use asset name only if directories are specified, otherwise full path
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;

// 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++ 要牢记,我们只是大自然的搬运工。
  不要接引擎压根都没有的功能需求,这种功能开发通常需要交给程序去搞。
  我们的工作是将引擎的功能整合自动化。
  查源码虽然很麻烦,很绕,但是很多类用法可以参考源代码的 😎