UPBGE Script Lifecycle (源代碼分析)

UPBGE 游戲開發(fā)系列教程:
#????UPBGE?- Blender 游戲引擎繼承者
## ???UPBGE?Python?Scripting?
## ?? Logic Nodes (源代碼分析)
## ?? Script Lifecycle (源代碼分析)
## ?? UPBGE Python API (源代碼分析)
源文檔:https://github.com/Jeangowhy/opendocs/blob/main/upbge.md
很有必要對(duì) `KX_GameObject(SCA_IObject)` 的生命周期深入研究,必需要有一個(gè)明確的?Script lifecycle 概念。但是這么重要的基礎(chǔ)概念內(nèi)容,官方文檔卻不重視,即使是 `update()` 方法的說明也少得可憐,What Is A Python Component? 有提及。這些方法都涉及了 C++ 源代碼,大概是開發(fā)團(tuán)隊(duì)真的是沒太多人力可用。
游戲運(yùn)行環(huán)境由 `LA_Launcher` 類型配置,包括 Python 環(huán)境的初始化和游戲循環(huán)結(jié)構(gòu)。這個(gè)入口類型定義的 `InitEngine()` 方法執(zhí)行以初始 UPBGE 游戲引擎,初始化引擎環(huán)境,包括場景實(shí)例 `KX_Scene` 的設(shè)置,然后進(jìn)入 `EngineMainLoop()`,直到程序運(yùn)行結(jié)束。
Python-3.10.2\Python\pythonrun.c
upbge-0.30\source\blender\editors\space_view3d\view3d_view.c
upbge-0.30\source\gameengine\GamePlayer\GPG_ghost.cpp
upbge-0.30\source\gameengine\BlenderRoutines\BL_KetsjiEmbedStart.cpp
upbge-0.30\source\gameengine\Launcher\LA_Launcher.cpp
upbge-0.30\source\gameengine\Ketsji\KX_KetsjiEngine.h
`KX_Scene` 是游戲場景對(duì)象,是游戲?qū)ο笊森h(huán)境,邏輯上也是游戲?qū)ο蟮娜萜鳌?/p>
`KX_PythonProxyManager` 是游戲?qū)ο蟮淖灾行?,?fù)責(zé)調(diào)用所有游戲?qū)ο蟮?`Update()` 方法。
`SCA_LogicManager` 是邏輯塊注冊中心,負(fù)責(zé)調(diào)用所有 Logic Bricks 對(duì)象的管理,每個(gè)邏輯處理周期對(duì)應(yīng)一次 `UpdateFrame()` 調(diào)用。
以下是 UPBGE 引擎工作流程概要:
確實(shí),KX_PythonProxy 的 `Update()` 方法優(yōu)先于 `Start()` 執(zhí)行,后者只在初始化執(zhí)行一次,后續(xù)就不再執(zhí)行。
頂級(jí)父類型 `EXP_PyObjectPlus` 提供的 `GetProxy()` 是 `Py_Header` 宏函數(shù)生成的方法,
返回一個(gè) `PyObject` 對(duì)象,也就是腳本中的對(duì)象調(diào)用接口。
upbge-0.30\source\gameengine\Ketsji\KX_KetsjiEngine.cpp
upbge-0.30\source\gameengine\Ketsji\KX_PythonProxyManager.cpp:
upbge-0.30\source\gameengine\Expressions\EXP_PyObjectPlus.h
upbge-0.30\source\gameengine\Expressions\intern\PyObjectPlus.cpp
upbge-0.30\source\gameengine\Ketsji\KX_GameObject.cpp
腳本涉及的生命周期事件可以表示如下流程,Python 對(duì)象的初始化魔術(shù)方法會(huì)先于引擎運(yùn)行:
綜合以上,一個(gè) `Get Property` 節(jié)點(diǎn)可以讀取 Game Properties 數(shù)據(jù),也可以讀取 Python 初始化方法設(shè)置的屬性數(shù)據(jù),而且需要使用 `self['prop'] = value` 這樣的方式設(shè)置的數(shù)據(jù),才能被邏輯節(jié)點(diǎn)的 `evaluate(self)` 函數(shù)檢測到。因?yàn)?,邏輯?jié)點(diǎn)的檢測代碼先于 GameObject 對(duì)象的 `start()` 方法執(zhí)行,所以在 Game Object 面板中配置的屬性數(shù)據(jù)不能在邏輯節(jié)點(diǎn)讀取。
為了調(diào)試邏輯節(jié)點(diǎn),可以生成腳本組件代碼,然后再手動(dòng)將組件掛載到游戲?qū)ο蟮?Game Components。在邏輯節(jié)點(diǎn)編輯器的側(cè)欄面板,選擇 `Apply as Component`,再點(diǎn)擊 `Apply To Selected` 將邏輯統(tǒng)戰(zhàn)掛載到選中的對(duì)象上,并在 Game Components 列表中生成相應(yīng)的組件。
腳本組件方式掛載的邏輯樹,腳本組件面板提供 `Only Run At Startup` 選項(xiàng),勾選它才表示在游戲開始時(shí)執(zhí)行邏輯樹?;蛘呤褂?`Execution Condition`,指定一個(gè)邏輯條件,它就是一個(gè)字符串,相當(dāng)于是邏輯樹的 condition 條件輸入端口。但是它需要經(jīng)過一次映射轉(zhuǎn)換,即讀取 self.objcet 對(duì)應(yīng)字段的值使用執(zhí)行條件。
參考 bgelogic 目錄下的生成代碼,以下代碼對(duì)應(yīng)一個(gè)名稱為 **ArchitectureBasic** 的邏輯樹,`On Update` 節(jié)點(diǎn)驅(qū)動(dòng) `Print` 節(jié)點(diǎn)打印 `Get Property` 獲取到的 GameObject 屬性數(shù)據(jù):
假設(shè)邏輯樹生成的腳本組件,掛載到游戲?qū)ο笊?,并且沒有勾選腳本組件 `Only Run At Startup` 選項(xiàng),表示在游戲開始時(shí)不執(zhí)行邏輯樹。那么,使用 `Execution Condition` 指定一個(gè)邏輯條件,它是字符串值。設(shè)置了個(gè)執(zhí)行條件后,代碼邏輯就會(huì)對(duì) `owner[self.condition]` 進(jìn)行檢測,如果游戲?qū)ο笊舷鄳?yīng)的屬性數(shù)據(jù)邏輯值為 `True` 才繼承執(zhí)行。
注意,默認(rèn)的 network.stopped 配置值為 False,因?yàn)?`NL__ArchitectureBasic` 這個(gè)屬性是沒有默認(rèn)定義的。另外,默認(rèn)的 consumed 狀態(tài)值 False 表示此邏輯樹還沒有被執(zhí)行過(消費(fèi)掉)。**Stopped** 是邏輯樹的一種運(yùn)行狀態(tài),但是對(duì)于一個(gè)未曾運(yùn)行過的邏輯樹,如果不勾選起始運(yùn)行選項(xiàng),到這一步就無法再繼續(xù)運(yùn)行求值流程,即使指定的**執(zhí)行條件**已經(jīng)滿足,也不會(huì)執(zhí)行求值,這多少有點(diǎn)邏輯設(shè)計(jì)上的缺陷。解決方法如下:
1. 直接在手動(dòng)掛載的邏輯節(jié)點(diǎn)樹生成的組件代碼上修改 `network.stopped = False`
2. 游戲?qū)ο蟮某跏蓟椒ㄖ性鲈O(shè)置屬性值 `self['NL__ArchitectureBasic'] = True`
但是,這樣做的結(jié)果就是:不激活 `Only Run At Startup` 或者指定 `Execution Condition`也會(huì)執(zhí)行邏輯樹。
第一種方式涉及腳本文件的處理,在邏輯樹生成組件代碼時(shí),默認(rèn)是內(nèi)嵌在 Blender 文件內(nèi)部的,可以使用文件菜單將腳本保存為外部腳本文件,下次再生成代碼時(shí),還是先相覆蓋掉內(nèi)嵌的代碼,但不會(huì)自動(dòng)覆蓋外部文件,除非手動(dòng)保存,或者在關(guān)閉 Blender 時(shí)確認(rèn)保存腳本文件。
邏輯樹的求值方法返回布爾值,指示節(jié)點(diǎn)樹的消費(fèi)狀態(tài),消費(fèi)過就不過再被執(zhí)行,避免“雙花”問題[Doge]。