Unreal Python 结合 C++ 开发蓝图库插件

本文章转载自 智伤帝的个人博客 - 原文链接

前言

  上个月的这个时候我写了一篇文章关于如何嵌入 PySide 调用 Qt 的 GUI 开发。 链接
  Python 虽然很好,但是有些功能,并没有从 C++ 里面暴露出来。
  这种情况就需要通过 C++ 的蓝图开发来将这部分的功能进行暴露。
  这样 Python 基本上可以做任何 Unreal 的事情。

  如何开发蓝图库也基本可以参照上篇文章提到的 Unreal Python 教程。 链接

为什么需要开发 C++ 蓝图

  上面的视频链接有很详细如何通过 Unreal C++ API 开发一个 Unreal 的蓝图,暴露给 Python 调用。
  Unreal 的 Python 插件其实已经将 Unreal 内置的所有蓝图暴露给了 Python。
  因此 蓝图 能够做到的事情, Python 是绝对可以做到的。
  而且经过一个多月的使用来看, Python 的 API 文档做得比 蓝图 的 文档要好很多。
  有时候直接查 Python 的 API 反而更有效率,甚至会发现一些其他插件所引入的蓝图。

  那么 Python 相较于 蓝图 的有什么异同?
  我目前的使用感受来看,除了失去图形节点编程的可视化之外,基本上碾压蓝图,当然运行效率上没有测试过。
  蓝图 和 Python 的定位有很大的不一样。
  蓝图可以作为游戏运行逻辑的一部分, Python 只能当做编辑器的自动化工具。(Python 效率太低了,运行脚本宁愿用 lua 调 C++)

  蓝图自身有它的优缺点,效率比不过 C++ 链接
  但是图形化编程,对于非 coding 人员很友好,而且一些简单的逻辑也比较直观。
  但是复杂蓝图的连线还是太让人劝退了。

  Python 对于像我这种工具向开发的 TA 来说太友好了,毕竟很多 DCC 都使用 Python 。
  Unreal 的 Python 插件大部分是对 蓝图 的分装,基本上蓝图有的功能都可以通过 Python 来调。
  同时 Python 还可以实现一些神奇的功能,比如说通过 Python 开发一个蓝图节点 ,调用 Python 的第三方库诸如 PySide 包,或者调用系统的 cmd 或者 shell 命令。
  因此从引擎的提升来说, Python 的确在这方面更胜一筹,复杂逻辑通过代码看也比较直观。

  当然很明显, 蓝图不能实现的引擎操作,基本上也不用指望 Python 能够调用什么 API 来实现了。
  这种情况下就需要 C++ 来扩展蓝图,实现 Python 调用。

C++ 开发蓝图库插件

  我们目前的需求并不是开发游戏调用的蓝图,因此我们可以开发一个蓝图库插件。
  这样可以轻易将这些蓝图迁移到不同的项目里面去。

  Unreal 搭建蓝图库开发其实并不难,按照官方的指引去做即可。
  首先需要创建一个 C++ 工程,如果是蓝图工程是无法写 C++ 代码的。
  然后打开插件面板,选择 New Plugin

  然后引擎就会自动创建一个基础插件的模板出来。
  后续就是在这个基础模板上调用 C++ 的 API 实现一些特殊的功能。

unreal C++ 插件注意事项

  插件的默认结构是 .uplugin 文件加 Resource 和 Source 文件夹。
  uplugin 就是一个 Json 配置,配置了插件在引擎的插件列表的显示,以及加载方式。
  Resource 存放插件显示的图标。
  Source 存放的是 C++ 源码了。


  前面两个不需要太过关注,重点的 Source 文件夹的东西。
  里面会有 *.Build.cs 文件以及 Public 和 Private 文件夹。
  *.Build.cs是 C# 代码,通过虚幻的 Reflect 机制生成 Intermediate 的中间代码用来编译生成 dll。
  Public 默认存放头文件
  Private 默认存放cpp源码


  引用了引擎内部的一些库,需要在 build.cs 文件里面添加上。
  否则编译的时候回报某些类型无法识别的错误。
  试过排查这种小错误花了我大半天。

  前面两个部分是添加路径的,用来缩短头文件索引的路径长度。
  后面的 Private 和 Public Module 则是最重要的索引头文件的,必须要在这里配置才能在 c++ 里面调用。
  这里怎么填写可以参考引擎 Source 源码下的文件夹名称。

  cs 里面配置就可以找 Source 源码的一些头文件进行引用了。
  因为虚幻开源了,所以内部 Private 和 Public 没有什么区别,也可能是我的 C++ 造诣还不够。

  配置头文件就可以愉快地使用官方提供的一些 C++ 了。

C++ 实现 Add Component 蓝图功能

  这个功能看似非常简单,奈何 Python 就是实现不了。

  就是给现有 Actor 添加新的 Component 组件而已。
  但是查了 API 文档,即便使用 Attach 相关的方法,也无法新的 Component 添加到 Actor 上。
  应该说 Python 的操作没有问题, Component 也加上了,可以通过 Python 获取到,但是 Component 没有注册,无法在 UI 上显示出来。

  经过我查阅大量网上的资料之后,只在论坛上找到了一个通过 C++ 实现的方案。 链接
  这段代码里面有很关键的 RegisterComponent 的操作。
  而这些操作并没有暴露到 Python 或者说 蓝图 里面。
  当然这个添加 Component 的方法估计也和 Unreal 的机制有关,我对 Unreal 引擎还不是很熟,就不做无关的揣测了。

  Python 的文档在 Actor 的部分有所涉及。
  不过这个问题就非常蛋疼,
  unreal.EditorLevelLibrary.get_all_level_actors_components 可以获取所有注册的 Component
  Actor 也可以删除现有的 Component ,偏偏无法添加新的 Component


  C++ 的部分我简化了上面回答的代码。
  如果没有传入具体的 Component 类型就返回 None 给 Python 就好了。

UActorComponent* URedArtToolkitBPLibrary::AddComponent(AActor* a, USceneComponent *future_parent, FName name, UClass* NewComponentClass)
{

    UActorComponent* retComponent = nullptr;
    if (NewComponentClass)
    {
        UActorComponent* NewComponent = NewObject<UActorComponent>(a, NewComponentClass, name);
    
        FTransform CmpTransform;// = dup_source->GetComponentToWorld();
    
        //NewComponent->AttachToComponent(sc, FAttachmentTransformRules::KeepWorldTransform);
    
        // Do Scene Attachment if this new Comnponent is a USceneComponent
        if (USceneComponent* NewSceneComponent = Cast<USceneComponent>(NewComponent))
    
        {
            if (future_parent != 0)
                NewSceneComponent->AttachToComponent(future_parent, FAttachmentTransformRules::KeepWorldTransform);
    
            else
                NewSceneComponent->AttachToComponent(a->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
    

            NewSceneComponent->SetComponentToWorld(CmpTransform);
    
        }
        a->AddInstanceComponent(NewComponent);
    
        NewComponent->OnComponentCreated();
    
        NewComponent->RegisterComponent();
    

        a->RerunConstructionScripts();
    
        retComponent = NewComponent;
    }

    return retComponent;
}

  头文件怎么去 #include ,我基本就是用 VScode 搜索引擎源码,查找头文件的位置,然后逐个添加。

  C++有点麻烦的地方就是 cpp 代码写完之后还要将函数注册到 头文件 里面
  不过基本上复制 cpp 的函数第一行就可以了,只需要把 :: 前面的类名删除掉而已。

  下面就是点击 VS 上面的 本地 windows 调试,编译插件并启动项目。

  我用 VS2017 编译经常遇到 clxx.dll 命令行过长 的错误。
  网上了查了要将项目的编译改为 Release 版本,或者升级到 VS2019 才可以解决。(网上查到这个是 VS 的 BUG)

  后来我是随便将一些 Intermediate 文件夹删除,然后重新调用 UnrealHeaderTool 生成反射代码就不会有这个编译报错了。
  完成到这里基本可以参照老外的教程,使用 Python 可以在 unreal 库下找到刚才蓝图扩展的类的,类下面就由刚才扩展的 函数 了。
  行数名称自动将 C++ 的驼峰转为 Python pep8 规范的 sneak 写法。

C++ 蓝图获取当前 Sequencer 选择的元素

  上面介绍了 C++ 的编写的流程,就不再追溯,这里着重看蓝图的实现。
  我最近有一个需求是要获取当前打开的 Sequencer 里面的元素,然后进行 FBX 导出。
  但是查遍了 Unreal 的 Python 文档也没有找到这个方法。
  对了这里记录一个天坑,之前被坑惨了的。
  Unreal Python 的老外教程里面也记录一些使用 Sequencer 处理的 Python 方案。
  但是我发现到我调用这些 API 的时候, Unreal 居然报错找不到这些 API。
  然后我就以为是我当前 Unreal 版本出 BUG 了,或者是官方删除了这个功能。
  后来折腾了好久之后才发现,我没有开启 Sequencer Scripting 插件,所以那些调用蓝图没有加载(:з」∠)
  我当时还不知道 Python 调用的就是蓝图, 踩了这个坑才有了深刻的认识。


  回到这里要实现的功能,我查了 C++ 相关的问题,总算是找到了一个比较可靠的回复。 链接
  于是我就抄了这里的代码。

  不过上面的代码有点旧,其中 IAssetEditorInstance* AssetEditor = UAssetEditorSubsystem().FindEditorForAsset(LevelSeq, true); 编译会报错
  修改为 IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(LevelSeq, true); 解决问题。

  经过修改之后上面的代码可以获取到当前 Sequencer 打开的 LevelSequence
  原理也不复杂,就是遍历项目所有的 LevelSequence 然后找到那个开启了 Editor 的 LevelSequence
  然后在从这个 LevelSequence 里获取 Editor 再从 Editor 获取 Sequencer。

  虽然这个遍历有点不太合理,但是我在测试的项目上还是很奏效的。
  但是当我将代码编译放到我们正在开发的项目上之后,出现了大问题。
  项目有大量的 LevelSequence ,遍历需要很长的时间,并且遍历之后大量的材质启动了编译,导致电脑很卡。


  于是我又查了一下 C++ API 文档,发现有个很有用的函数 GetAllEditedAssets。
  这个函数可以获取当前打开在编辑器里面的 Assets ,能打开的 Asset 肯定就那么几个。
  这样找 Editor 的速度可就快多了。

ULevelSequence* URedArtToolkitBPLibrary::GetFocusSequence()
{
    UAssetEditorSubsystem* sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
    
    TArray <UObject*> assets = sub->GetAllEditedAssets();
    

    for (UObject* asset : assets)
    {
        IAssetEditorInstance* AssetEditor = sub->FindEditorForAsset(asset, false);
    
        FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;

        if (LevelSequenceEditor != nullptr)
        {
            ULevelSequence* LevelSeq = Cast<ULevelSequence>(asset);
    
            return LevelSeq;
        }
    }
    return nullptr;
}

  上面只是找 LevelSequence ,还需要找当前 LevelSequence 里面选择的元素。
  好在 Sequencer 提供了 GetSelectedObjects 的方法
  通过 LevelSequence 可以获取到 Sequencer

TArray<FGuid> URedArtToolkitBPLibrary::GetFocusBindings(ULevelSequence* LevelSeq)
    
{
    IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(LevelSeq, false);
    

    FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;
    TArray<FGuid> SelectedGuid;
    
    if (LevelSequenceEditor != nullptr)
    {
        ISequencer* Sequencer = LevelSequenceEditor->GetSequencer().Get();
    
        Sequencer->GetSelectedObjects(SelectedGuid);
    
        return SelectedGuid;
    }
    return SelectedGuid;
}

  这样获取返回的是 Guid , Python 有 Guid 类。
  可以通过 LevelSequence 的 get_bindings 方法获取 sequence 相关的 binding
  再调用 get_id 方法获取 guid ,然后通过 C++ 的蓝图将获取到的 id 筛选一遍。

# NOTE 获取当前 Sequencer 中的 LevelSequence
sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
# NOTE 获取当前 Sequencer 中选中的 Bindings
id_list = unreal.RedArtToolkitBPLibrary.get_focus_bindings(sequence)
bindings_list = [binding for binding in sequence.get_bindings() if binding.get_id() in id_list]

  这样就获取到了当前选择的 SequencerBindingProxy 类。
  通过 unreal.SequencerTools.export_fbx 就可以将选择的元素导出 FBX 了。

import unreal
from Qt import QtCore, QtWidgets, QtGui

def alert(msg=u"msg", title=u"警告", button_text=u"确定"):
    # NOTE 生成 Qt 警告窗口
    msg_box = QtWidgets.QMessageBox()
    msg_box.setIcon(QtWidgets.QMessageBox.Warning)
    msg_box.setWindowTitle(title)
    msg_box.setText(msg)
    msg_box.addButton(button_text, QtWidgets.QMessageBox.AcceptRole)
    unreal.parent_external_window_to_slate(msg_box.winId())
    msg_box.exec_()

def unreal_export_fbx(fbx_file):
    # NOTE 获取当前 Sequencer 中的 LevelSequence
    sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
    if not sequence:
        alert(u"请打开定序器")
        return

    # NOTE 获取当前 Sequencer 中选中的 Bindings
    id_list = unreal.RedArtToolkitBPLibrary.get_focus_bindings(sequence)
    bindings_list = [binding for binding in sequence.get_bindings() if binding.get_id() in id_list]

    if bindings_list:
        # NOTE 导出 FBX 文件
        option = unreal.FbxExportOption()
        option.set_editor_property("collision",False)
        world = unreal.EditorLevelLibrary.get_editor_world()
        unreal.SequencerTools.export_fbx(world,sequence,bindings_list,option,fbx_file)
    else:
        alert(u"请选择定序器的元素进行 FBX 导出")
        return

  上面就是完整的示例代码。
  当然导出的 FBX 是带动画的,还需要将动画处理成带 蒙皮骨骼 的 FBX 。
  这个操作我是通过 FBX Python SDK 实现的。
  官方的 ExportScene01 包含了蒙皮创建,关键帧处理等等的操作,绝大部分的代码可以照抄。
  这里蒙皮转换的需求很简单,因此稍微修改一下就可以用了。
  处理完成之后将 FBX 输出到临时目录,然后用 Python 调 windows 命令打开路径。

总结

  其实调用 C++ API 并不难,这种程度的操作还没有修改到 Unreal 的底层,很多机制也没有用到,我作为个外行还是可以应付的。
  而且 Unreal C++ 本身做了很多工作,比如实现了 垃圾回收,含有智能指针,都降低了开发难度(同时增加了学习的难度)

  Unreal 开发比较难受的地方时教程文档各方面都不全, Unity 文档还有代码示例,Unreal 因为开源,基本上就是让你直接看源码(:з」∠)
  有时候遇到的一些奇奇怪怪的问题还找不到任何网上的提问,就很难受了。

  最后引擎编译非常耗时,如果要搞这一块的研究,一定一定要配台好电脑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343