UPBGE Script Lifecycle (源代码分析)

UPBGE 游戏开发系列教程:
# 🥚 UPBGE - Blender 游戏引擎继承者
## 🎨 UPBGE Python Scripting
## 🎨 Logic Nodes (源代码分析)
## 🎨 Script Lifecycle (源代码分析)
## 🎨 UPBGE Python API (源代码分析)
源文档:https://github.com/Jeangowhy/opendocs/blob/main/upbge.md
很有必要对 `KX_GameObject(SCA_IObject)` 的生命周期深入研究,必需要有一个明确的 Script lifecycle 概念。但是这么重要的基础概念内容,官方文档却不重视,即使是 `update()` 方法的说明也少得可怜,What Is A Python Component? 有提及。这些方法都涉及了 C++ 源代码,大概是开发团队真的是没太多人力可用。
游戏运行环境由 `LA_Launcher` 类型配置,包括 Python 环境的初始化和游戏循环结构。这个入口类型定义的 `InitEngine()` 方法执行以初始 UPBGE 游戏引擎,初始化引擎环境,包括场景实例 `KX_Scene` 的设置,然后进入 `EngineMainLoop()`,直到程序运行结束。
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` 是游戏场景对象,是游戏对象生成环境,逻辑上也是游戏对象的容器。
`KX_PythonProxyManager` 是游戏对象的注册中心,负责调用所有游戏对象的 `Update()` 方法。
`SCA_LogicManager` 是逻辑块注册中心,负责调用所有 Logic Bricks 对象的管理,每个逻辑处理周期对应一次 `UpdateFrame()` 调用。
以下是 UPBGE 引擎工作流程概要:
确实,KX_PythonProxy 的 `Update()` 方法优先于 `Start()` 执行,后者只在初始化执行一次,后续就不再执行。
顶级父类型 `EXP_PyObjectPlus` 提供的 `GetProxy()` 是 `Py_Header` 宏函数生成的方法,
返回一个 `PyObject` 对象,也就是脚本中的对象调用接口。
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 对象的初始化魔术方法会先于引擎运行:
综合以上,一个 `Get Property` 节点可以读取 Game Properties 数据,也可以读取 Python 初始化方法设置的属性数据,而且需要使用 `self['prop'] = value` 这样的方式设置的数据,才能被逻辑节点的 `evaluate(self)` 函数检测到。因为,逻辑节点的检测代码先于 GameObject 对象的 `start()` 方法执行,所以在 Game Object 面板中配置的属性数据不能在逻辑节点读取。
为了调试逻辑节点,可以生成脚本组件代码,然后再手动将组件挂载到游戏对象的 Game Components。在逻辑节点编辑器的侧栏面板,选择 `Apply as Component`,再点击 `Apply To Selected` 将逻辑统战挂载到选中的对象上,并在 Game Components 列表中生成相应的组件。
脚本组件方式挂载的逻辑树,脚本组件面板提供 `Only Run At Startup` 选项,勾选它才表示在游戏开始时执行逻辑树。或者使用 `Execution Condition`,指定一个逻辑条件,它就是一个字符串,相当于是逻辑树的 condition 条件输入端口。但是它需要经过一次映射转换,即读取 self.objcet 对应字段的值使用执行条件。
参考 bgelogic 目录下的生成代码,以下代码对应一个名称为 **ArchitectureBasic** 的逻辑树,`On Update` 节点驱动 `Print` 节点打印 `Get Property` 获取到的 GameObject 属性数据:
假设逻辑树生成的脚本组件,挂载到游戏对象上,并且没有勾选脚本组件 `Only Run At Startup` 选项,表示在游戏开始时不执行逻辑树。那么,使用 `Execution Condition` 指定一个逻辑条件,它是字符串值。设置了个执行条件后,代码逻辑就会对 `owner[self.condition]` 进行检测,如果游戏对象上相应的属性数据逻辑值为 `True` 才继承执行。
注意,默认的 network.stopped 配置值为 False,因为 `NL__ArchitectureBasic` 这个属性是没有默认定义的。另外,默认的 consumed 状态值 False 表示此逻辑树还没有被执行过(消费掉)。**Stopped** 是逻辑树的一种运行状态,但是对于一个未曾运行过的逻辑树,如果不勾选起始运行选项,到这一步就无法再继续运行求值流程,即使指定的**执行条件**已经满足,也不会执行求值,这多少有点逻辑设计上的缺陷。解决方法如下:
1. 直接在手动挂载的逻辑节点树生成的组件代码上修改 `network.stopped = False`
2. 游戏对象的初始化方法中增设置属性值 `self['NL__ArchitectureBasic'] = True`
但是,这样做的结果就是:不激活 `Only Run At Startup` 或者指定 `Execution Condition`也会执行逻辑树。
第一种方式涉及脚本文件的处理,在逻辑树生成组件代码时,默认是内嵌在 Blender 文件内部的,可以使用文件菜单将脚本保存为外部脚本文件,下次再生成代码时,还是先相覆盖掉内嵌的代码,但不会自动覆盖外部文件,除非手动保存,或者在关闭 Blender 时确认保存脚本文件。
逻辑树的求值方法返回布尔值,指示节点树的消费状态,消费过就不过再被执行,避免“双花”问题[Doge]。