uasset python_Unreal Python 结合 C++ 开发蓝图库插件
本文章轉(zhuǎn)載自 智傷帝的個人博客 - 原文鏈接
前言
上個月的這個時候我寫了一篇文章關(guān)于如何嵌入 PySide 調(diào)用 Qt 的 GUI 開發(fā)。 鏈接
Python 雖然很好,但是有些功能,并沒有從 C++ 里面暴露出來。
這種情況就需要通過 C++ 的藍圖開發(fā)來將這部分的功能進行暴露。
這樣 Python 基本上可以做任何 Unreal 的事情。
如何開發(fā)藍圖庫也基本可以參照上篇文章提到的 Unreal Python 教程。 鏈接
為什么需要開發(fā) C++ 藍圖
上面的視頻鏈接有很詳細如何通過 Unreal C++ API 開發(fā)一個 Unreal 的藍圖,暴露給 Python 調(diào)用。
Unreal 的 Python 插件其實已經(jīng)將 Unreal 內(nèi)置的所有藍圖暴露給了 Python。
因此 藍圖 能夠做到的事情, Python 是絕對可以做到的。
而且經(jīng)過一個多月的使用來看, Python 的 API 文檔做得比 藍圖 的 文檔要好很多。
有時候直接查 Python 的 API 反而更有效率,甚至?xí)l(fā)現(xiàn)一些其他插件所引入的藍圖。
那么 Python 相較于 藍圖 的有什么異同?
我目前的使用感受來看,除了失去圖形節(jié)點編程的可視化之外,基本上碾壓藍圖,當(dāng)然運行效率上沒有測試過。
藍圖 和 Python 的定位有很大的不一樣。
藍圖可以作為游戲運行邏輯的一部分, Python 只能當(dāng)做編輯器的自動化工具。(Python 效率太低了,運行腳本寧愿用 lua 調(diào) C++)
藍圖自身有它的優(yōu)缺點,效率比不過 C++ 鏈接
但是圖形化編程,對于非 coding 人員很友好,而且一些簡單的邏輯也比較直觀。
但是復(fù)雜藍圖的連線還是太讓人勸退了。
Python 對于像我這種工具向開發(fā)的 TA 來說太友好了,畢竟很多 DCC 都使用 Python 。
Unreal 的 Python 插件大部分是對 藍圖 的分裝,基本上藍圖有的功能都可以通過 Python 來調(diào)。
同時 Python 還可以實現(xiàn)一些神奇的功能,比如說通過 Python 開發(fā)一個藍圖節(jié)點 ,調(diào)用 Python 的第三方庫諸如 PySide 包,或者調(diào)用系統(tǒng)的 cmd 或者 shell 命令。
因此從引擎的提升來說, Python 的確在這方面更勝一籌,復(fù)雜邏輯通過代碼看也比較直觀。
當(dāng)然很明顯, 藍圖不能實現(xiàn)的引擎操作,基本上也不用指望 Python 能夠調(diào)用什么 API 來實現(xiàn)了。
這種情況下就需要 C++ 來擴展藍圖,實現(xiàn) Python 調(diào)用。
C++ 開發(fā)藍圖庫插件
我們目前的需求并不是開發(fā)游戲調(diào)用的藍圖,因此我們可以開發(fā)一個藍圖庫插件。
這樣可以輕易將這些藍圖遷移到不同的項目里面去。
Unreal 搭建藍圖庫開發(fā)其實并不難,按照官方的指引去做即可。
首先需要創(chuàng)建一個 C++ 工程,如果是藍圖工程是無法寫 C++ 代碼的。
然后打開插件面板,選擇 New Plugin
然后引擎就會自動創(chuàng)建一個基礎(chǔ)插件的模板出來。
后續(xù)就是在這個基礎(chǔ)模板上調(diào)用 C++ 的 API 實現(xiàn)一些特殊的功能。
unreal C++ 插件注意事項
插件的默認結(jié)構(gòu)是 .uplugin 文件加 Resource 和 Source 文件夾。
uplugin 就是一個 Json 配置,配置了插件在引擎的插件列表的顯示,以及加載方式。
Resource 存放插件顯示的圖標(biāo)。
Source 存放的是 C++ 源碼了。
前面兩個不需要太過關(guān)注,重點的 Source 文件夾的東西。
里面會有 *.Build.cs 文件以及 Public 和 Private 文件夾。
*.Build.cs是 C# 代碼,通過虛幻的 Reflect 機制生成 Intermediate 的中間代碼用來編譯生成 dll。
Public 默認存放頭文件
Private 默認存放cpp源碼
引用了引擎內(nèi)部的一些庫,需要在 build.cs 文件里面添加上。
否則編譯的時候回報某些類型無法識別的錯誤。
試過排查這種小錯誤花了我大半天。
前面兩個部分是添加路徑的,用來縮短頭文件索引的路徑長度。
后面的 Private 和 Public Module 則是最重要的索引頭文件的,必須要在這里配置才能在 c++ 里面調(diào)用。
這里怎么填寫可以參考引擎 Source 源碼下的文件夾名稱。
cs 里面配置就可以找 Source 源碼的一些頭文件進行引用了。
因為虛幻開源了,所以內(nèi)部 Private 和 Public 沒有什么區(qū)別,也可能是我的 C++ 造詣還不夠。
配置頭文件就可以愉快地使用官方提供的一些 C++ 了。
C++ 實現(xiàn) Add Component 藍圖功能
這個功能看似非常簡單,奈何 Python 就是實現(xiàn)不了。
就是給現(xiàn)有 Actor 添加新的 Component 組件而已。
但是查了 API 文檔,即便使用 Attach 相關(guān)的方法,也無法新的 Component 添加到 Actor 上。
應(yīng)該說 Python 的操作沒有問題, Component 也加上了,可以通過 Python 獲取到,但是 Component 沒有注冊,無法在 UI 上顯示出來。
經(jīng)過我查閱大量網(wǎng)上的資料之后,只在論壇上找到了一個通過 C++ 實現(xiàn)的方案。 鏈接
這段代碼里面有很關(guān)鍵的 RegisterComponent 的操作。
而這些操作并沒有暴露到 Python 或者說 藍圖 里面。
當(dāng)然這個添加 Component 的方法估計也和 Unreal 的機制有關(guān),我對 Unreal 引擎還不是很熟,就不做無關(guān)的揣測了。
Python 的文檔在 Actor 的部分有所涉及。
不過這個問題就非常蛋疼,
unreal.EditorLevelLibrary.get_all_level_actors_components 可以獲取所有注冊的 Component
Actor 也可以刪除現(xiàn)有的 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(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(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 代碼寫完之后還要將函數(shù)注冊到 頭文件 里面
不過基本上復(fù)制 cpp 的函數(shù)第一行就可以了,只需要把 :: 前面的類名刪除掉而已。
下面就是點擊 VS 上面的 本地 windows 調(diào)試,編譯插件并啟動項目。
我用 VS2017 編譯經(jīng)常遇到 clxx.dll 命令行過長 的錯誤。
網(wǎng)上了查了要將項目的編譯改為 Release 版本,或者升級到 VS2019 才可以解決。(網(wǎng)上查到這個是 VS 的 BUG)
后來我是隨便將一些 Intermediate 文件夾刪除,然后重新調(diào)用 UnrealHeaderTool 生成反射代碼就不會有這個編譯報錯了。
完成到這里基本可以參照老外的教程,使用 Python 可以在 unreal 庫下找到剛才藍圖擴展的類的,類下面就由剛才擴展的 函數(shù) 了。
行數(shù)名稱自動將 C++ 的駝峰轉(zhuǎn)為 Python pep8 規(guī)范的 sneak 寫法。
C++ 藍圖獲取當(dāng)前 Sequencer 選擇的元素
上面介紹了 C++ 的編寫的流程,就不再追溯,這里著重看藍圖的實現(xiàn)。
我最近有一個需求是要獲取當(dāng)前打開的 Sequencer 里面的元素,然后進行 FBX 導(dǎo)出。
但是查遍了 Unreal 的 Python 文檔也沒有找到這個方法。
對了這里記錄一個天坑,之前被坑慘了的。
Unreal Python 的老外教程里面也記錄一些使用 Sequencer 處理的 Python 方案。
但是我發(fā)現(xiàn)到我調(diào)用這些 API 的時候, Unreal 居然報錯找不到這些 API。
然后我就以為是我當(dāng)前 Unreal 版本出 BUG 了,或者是官方刪除了這個功能。
后來折騰了好久之后才發(fā)現(xiàn),我沒有開啟 Sequencer Scripting 插件,所以那些調(diào)用藍圖沒有加載(:з」∠)
我當(dāng)時還不知道 Python 調(diào)用的就是藍圖, 踩了這個坑才有了深刻的認識。
回到這里要實現(xiàn)的功能,我查了 C++ 相關(guān)的問題,總算是找到了一個比較可靠的回復(fù)。 鏈接
于是我就抄了這里的代碼。
不過上面的代碼有點舊,其中 IAssetEditorInstance* AssetEditor = UAssetEditorSubsystem().FindEditorForAsset(LevelSeq, true); 編譯會報錯
修改為 IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem()->FindEditorForAsset(LevelSeq, true); 解決問題。
經(jīng)過修改之后上面的代碼可以獲取到當(dāng)前 Sequencer 打開的 LevelSequence
原理也不復(fù)雜,就是遍歷項目所有的 LevelSequence 然后找到那個開啟了 Editor 的 LevelSequence
然后在從這個 LevelSequence 里獲取 Editor 再從 Editor 獲取 Sequencer。
雖然這個遍歷有點不太合理,但是我在測試的項目上還是很奏效的。
但是當(dāng)我將代碼編譯放到我們正在開發(fā)的項目上之后,出現(xiàn)了大問題。
項目有大量的 LevelSequence ,遍歷需要很長的時間,并且遍歷之后大量的材質(zhì)啟動了編譯,導(dǎo)致電腦很卡。
于是我又查了一下 C++ API 文檔,發(fā)現(xiàn)有個很有用的函數(shù) GetAllEditedAssets。
這個函數(shù)可以獲取當(dāng)前打開在編輯器里面的 Assets ,能打開的 Asset 肯定就那么幾個。
這樣找 Editor 的速度可就快多了。
ULevelSequence* URedArtToolkitBPLibrary::GetFocusSequence()
{
UAssetEditorSubsystem* sub = GEditor->GetEditorSubsystem();
TArray assets = sub->GetAllEditedAssets();
for (UObject* asset : assets)
{
IAssetEditorInstance* AssetEditor = sub->FindEditorForAsset(asset, false);
FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;
if (LevelSequenceEditor != nullptr)
{
ULevelSequence* LevelSeq = Cast(asset);
return LevelSeq;
}
}
return nullptr;
}
上面只是找 LevelSequence ,還需要找當(dāng)前 LevelSequence 里面選擇的元素。
好在 Sequencer 提供了 GetSelectedObjects 的方法
通過 LevelSequence 可以獲取到 Sequencer
TArray URedArtToolkitBPLibrary::GetFocusBindings(ULevelSequence* LevelSeq)
{
IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem()->FindEditorForAsset(LevelSeq, false);
FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;
TArray SelectedGuid;
if (LevelSequenceEditor != nullptr)
{
ISequencer* Sequencer = LevelSequenceEditor->GetSequencer().Get();
Sequencer->GetSelectedObjects(SelectedGuid);
return SelectedGuid;
}
return SelectedGuid;
}
這樣獲取返回的是 Guid , Python 有 Guid 類。
可以通過 LevelSequence 的 get_bindings 方法獲取 sequence 相關(guān)的 binding
再調(diào)用 get_id 方法獲取 guid ,然后通過 C++ 的藍圖將獲取到的 id 篩選一遍。
# NOTE 獲取當(dāng)前 Sequencer 中的 LevelSequence
sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
# NOTE 獲取當(dāng)前 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]
這樣就獲取到了當(dāng)前選擇的 SequencerBindingProxy 類。
通過 unreal.SequencerTools.export_fbx 就可以將選擇的元素導(dǎo)出 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 獲取當(dāng)前 Sequencer 中的 LevelSequence
sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
if not sequence:
alert(u"請打開定序器")
return
# NOTE 獲取當(dāng)前 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 導(dǎo)出 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 導(dǎo)出")
return
上面就是完整的示例代碼。
當(dāng)然導(dǎo)出的 FBX 是帶動畫的,還需要將動畫處理成帶 蒙皮骨骼 的 FBX 。
這個操作我是通過 FBX Python SDK 實現(xiàn)的。
官方的 ExportScene01 包含了蒙皮創(chuàng)建,關(guān)鍵幀處理等等的操作,絕大部分的代碼可以照抄。
這里蒙皮轉(zhuǎn)換的需求很簡單,因此稍微修改一下就可以用了。
處理完成之后將 FBX 輸出到臨時目錄,然后用 Python 調(diào) windows 命令打開路徑。
總結(jié)
其實調(diào)用 C++ API 并不難,這種程度的操作還沒有修改到 Unreal 的底層,很多機制也沒有用到,我作為個外行還是可以應(yīng)付的。
而且 Unreal C++ 本身做了很多工作,比如實現(xiàn)了 垃圾回收,含有智能指針,都降低了開發(fā)難度(同時增加了學(xué)習(xí)的難度)
Unreal 開發(fā)比較難受的地方時教程文檔各方面都不全, Unity 文檔還有代碼示例,Unreal 因為開源,基本上就是讓你直接看源碼(:з」∠)
有時候遇到的一些奇奇怪怪的問題還找不到任何網(wǎng)上的提問,就很難受了。
最后引擎編譯非常耗時,如果要搞這一塊的研究,一定一定要配臺好電腦。
總結(jié)
以上是生活随笔為你收集整理的uasset python_Unreal Python 结合 C++ 开发蓝图库插件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么做批注_BIM平台是什么?有何用?怎
- 下一篇: python高并发架构_python高并