UPBGE Python Scripting

UPBGE 游戏开发系列教程:
# 🥚 UPBGE - Blender 游戏引擎继承者
## 🎨 UPBGE Python Scripting
## 🎨 Logic Nodes (源代码分析)
## 🎨 Script Lifecycle (源代码分析)
## 🎨 UPBGE Python API (源代码分析)
首先,UPBGE 编程环境就是一个 Blender Python API 环境,通过嵌入 Python 脚本引擎以及导出内部类型接口到脚本环境,就可以通过 .py 脚本来调用 UPBGE 游戏引擎中的各种模块、类型对象。同时这又是一个完整的 Python 解释器提供的运行环境,可以像在 Blender Python Console 那样执行脚本代码:
Python 3.5 引入了新的 @ 运算符,它与 numpy.dot() 等价,是矩阵乘法,向量点积。
源文档:https://github.com/Jeangowhy/opendocs/blob/main/upbge.md
了解 UPBGE 脚本编程环境,最基本的要求是熟练使用 Python 脚本编程,最好可以使用 C/C++ 开发扩展模块。如果要开发联网对战游戏,则还需要熟悉 TCP/IP/UPD 等协议栈,或者是基于现有的 RPC 框架开发游戏客户端之间的远程调用功能。
其次,从制作的角度来看,如果不需要处理上游的工序,包括游戏概念设计、艺术设计、故事剧本、地形、场景搭建、角色动画等等,单从功能出发,可能涉及到游戏的经济系统、数值系统、玩法系统的开发,这些都需要对 UPBGE Python API 导出的模块功能非常熟悉,才可能根据需要开发出相应功能的最佳实现。
在编写脚本代码过程中,会经常用到一个混入编程模式,Mixins And Traits,所谓混入即:不通过类继承这种约束关系实现调用其它类型功能代码,是一种代码复用编程方法,Mixins 或者 Traits 就是这样的一种类型实现。UPBGE 官方文档 Python Scripting 部分提供了一系列基础教程。
https://upbge.org/#/documentation/docs/latest/manual/manual/python/index.html
According to a relevant Wikipedia article, a mixin is “a class that
contains methods for use by other classes without having to be the
parent class of those other classes”.
Such a class is sometimes called a trait and often named as an
adjective like Damageable or Openable to describe a specific aspect
or characteristic of the target object.
Application Modules
- *bpy.context* [Context Access]
- *bpy.data* [Data Access]
- *bpy.msgbus* [Message Bus]
- *bpy.ops* [Operators]
- *bpy.types* [Types]
- *bpy.utils* [Utilities]
- *bpy.path* [Path Utilities]
- *bpy.app* [Application Data]
- *bpy.props* [Property Definitions]
Game Engine Modules
- *bge.types* [Game Types]
- *bge.logic* [Game Logic]
- *bge.render* [Rasterizer]
- *bge.texture* [Video Texture]
- [FFmpeg Video and Image Status]
- [Image Blending Modes]
- *bge.events* [Game Keys]
- *bge.constraints* [Physics Constraints]
- *bge.app* [Application Data]
- *bgui* [Game GUI]
Standalone Modules
- *bgl* [OpenGL Wrapper]
- *bl_math* [Additional Math Functions]
- *blf* [Font Drawing]
- *bmesh* [BMesh Module]
- *bpy_extras* [Extra Utilities]
- *freestyle* [Freestyle Module]
- *gpu* [GPU Module]
- *gpu_extras* [GPU Utilities]
- *idprop.types* [ID Property Access]
- *imbuf* [Image Buffer]
- *mathutils* [Math Types & Utilities]
导入模块时出错,并且可能导致挂载组件时失败也不给出提示信息,原因是 bgui 模块未提供默认安装,可以在 Blender Python Console 中执行 pip 命令安装模块,以下命令会将控制台内容输出到日志,并使用系统默认编辑器打开 log.md 文件:
Python 模块手动安装,只需要将模块代码目录复制到 lib 目录下,模块初始化脚本位置应该如下所示:
UPBGE-0.30-windows-x86_64\3.0\python\lib\bgui\__init__.py
UPBGE 通过 `Game Object Properties` 向场景所有对象提供三种脚本属性设置,以及逻辑节点编辑器侧栏面板,也提供了一种全局数据存储功能:
1. `Game Objects` 继承自 `bge.types.KX_GameObject`,每对象挂载一个实例。
2. `Game Components` 继承自 `bge.types.KX_PythonComponent`,每对象挂载任意实例。
3. `Game Properties` 属性数据服务,来自 Blender 基础功能,每对象单独存储属性数据。
4. `GlobalDB` 逻辑节点树全局字典数据对象,在逻辑编辑器侧栏 Global 面板中添加的字典对象及数据。
游戏中的每个 `GameObject` 可以存储控逻辑组件的集合(Logic Bricks),可以组合逻辑块来执行用户定义的动作,这些动作决定游戏模拟的进度。
可以像脚本组件一样在 Python 脚本文件(模块)中定义多个 `GameObject`,并且挂载到场景中不同的对象上使用。但是每个对象只能挂载一个 `GameObject`,而不像脚本组件那样任意挂载多个组件。不同的游戏对象应该派生具体的类型,例如挂载到 Camera 对象上的 GameObject,就应该继承 `KX_Camera`。
UPBGE 整个编程环境中,逻辑块和脚本组件,以及游戏对象的类型层次结构之间的关系摘要如下,另外,逻辑节点编程模块基于 Python 脚本实现,另行分析:
1. `SCA_IObject` 是场景中的对象类型的接口;
2. `KX_GameObject` 游戏对象,也就是场景中的对象,是场景中的对象类型的基类;
3. `KX_Scene` 代表整个游戏场景,objects 集合引用所有游戏对象,`EXP_ListValue<KX_GameObject>`;
4. `KX_PythonComponent` 脚本组件类型是游戏对象上挂载的脚本的类定义,通过 object 属性引用其所有者;
5. `SCA_ILogicBrick` 是逻辑块的基类接口,它继承了**重双向循环链接**数据结构用于处理节点连接关系;
之所以称为为 python component,是因为脚本中的功能会以 Blender UI 的方式显现在 Game Object 属性面板下的组件栏目中,所有 `args` 有序字典中保存的入口参数都会有相应的 UI 设置界面。
Properties 数据服务器 Blender 提供的一种基础服务,在各种面板上,可以使用右键复制、粘贴属性数据。3D 视图对象模式可以通过 `Object -> Game` 菜单管理属性数据,`VIEW3D_MT_object_game(Menu)`。

可以对数据进行清理,一次操作删除对象上设置的 Game Properties 数据,`game_property_clear` 操作可以在多个源代码中,或者 bpy.ops.object API 文档找到相关的信息。
逻辑节点编辑器侧栏面板 Globals 可以添加全局字典对象,以存储数据供所有节点树使用。相关的设计时节点实现有 5 个类型,其中两个为插槽实现类型,剩下 3 个是节点,归类在 `Value -> Global`。所谓 Category 即一个全局的存储数据的 Dictionary 对象,这个字典对象存储在 `GlobalDB`。
使用 List Global Category 节点,可以读取 `GlobalDB` 对象上存储的数据。
使用 Get Property 节点,就可以指定要读取场景中那个对象上挂载的 Game Properties 数据。
除了使用脚本中的 `GlobalDB` 对象,还可以使用导出的 Python API 存储字典数据到文件:
`Objects -> Properties` 分类下和属性数据相关的 9 逻辑节点类型,其中一个为插槽。
在使用上,`KX_GameObject` 和 `KX_PythonComponent` 对象导出的脚本接口是很类似的,同样的参数结构,和执行方法。但是内部实现上,前者比后者复杂得多。脚本组件对象导出了一个 `object` 属性,它引用当前脚本组件所挂载的对象,如场景中的 Cube。
控制器对象从个父类 `SCA_ILogicBrick` 继承了一个 `owner` 属性,引用一个当前调用这个控制器的游戏对象 GameObject。因为一个控制器可以挂载到多个对象上,不同的控制器挂载到不同对像,所挂载的游戏对象也不相同。
在脚本中导入模块、符号时,应避免 * 通配式加载所有子模块、符号,可能导致的命名空间污染问题。
组件保存所在的脚本中,还可以编写由 Logic Bricks - bge.types.SCA_IController 运行的代码,这是两种不同的脚本运行方式。脚本组件由 Game Components 机制加载,可调用脚本内所有继承了 `KX_PythonComponent` 基类的类型对象。而通过逻辑块 Python 控制器运行,**Script** 模式下则是直接运行整个脚本,或者 **Module** 模式下运行外部脚本文件中的控制器方法,其函数参数接收一个控制器对象,即是当前调用控制器方法的 `SCA_IController`。
组件首次加载是在 bge 模块之前完成,此时bge 是一个伪模块,因为它只包 `KX_PythonComponent` 这个组件基类类型,也就是说,此时只导出了这个组件类型定义到 Python 脚本空间,以以避免 import 语句导入所有 bge 模块,此时只是加载过程中的一个阶段状态。源代码代码片段如下,`load_class()` 方法中只是导出了 `bge` 和 `bge.types` 模块符号,并没有实质性内容。此时设置了一个布尔值标识 `__component__`,可用于检测的 bge 的加载状态。但当用户想要使用 bge 模块中的其它函数或属性时,检测此标记值,避免直接调用未加载的库函数产生一些问题。
SystemError: bge.logic.getCurrentController(), this function is being run
outside the python controllers context, or blenders internal state is corrupt.
Blender 定义了一套 Python Proxy 函数,用来向脚本环境导入模块定义:
upbge-0.30\doc\python_api\rst\bge.logic.rst
upbge-0.30\doc\python_api\rst\bge_types\bge.types.KX_PythonComponent.rst
upbge-0.30\source\blender\blenkernel\intern\python_proxy.c
打开 Blender 脚本编辑器,可以从 `Templates - Python Component templates` 菜单找到脚本组件模板,以供学习。这些模板脚本涉及场景对象列表、音频播放、鼠标键盘输入、玩家控制器实现、相机镜头控制、时间轴动画、Minimap 纹理、渲染器调用、物理载具、AI 跟踪等等,源代码文件列表如下:
其中 **Python Component** 模板就是一个脚本组件的最基本结构。**Bpytypes** 演示了 bpy 模块所有的可用类型。
Python 脚本组件就是代码复用的一种概念,也是定义 UPBGE 引擎中可能的一种类型定义,规则要求:
1. 需要继承自 `KX_PythonComponent` 类型定义;
2. 包含一个 Python 有序字典对象作为组件要传入的参数名称、类型等属性说明 **args**;
3. 两个基本方法:**start()** 和 **update()**;
4. 另外,还有一个可选方法,**dispose()** 用于移除组件回收内存时执行清理工作;
脚本组件按 Python 模块化组织,基本的命名规则是 `module.Component`,自行指定模块名和类名。点击创建,Blender 脚本编辑器中有会出现相应的 `module.py` 模块脚本,可以保存为外部脚本文件。UPBGE 提供了一个属性面板用来添加脚本组件:`Game Object Properties -> Game Component` 提供了两个方法创建脚本组件:**Add** 和 **Create**,对应两个按钮。

设置好参数列表后并挂载脚本组件后,`Game Components` 面板就会添加对应参数列表的面板设置选项。在游戏运行时,`start()` 方法接收到的字典对象将包含这些来自属性面板配置过的参数值。
有序字典 OrderedDict 构造函数接收一个数组,元素类型是 Tuple,可以指定参数名称、默认值,或者指针类型,像指定 `str` 这种字节串类型将触发异常:**Unsupported pointer type**。
Python-3.10.2\Doc\library\collections.rst
Blender 中编写的 Python 脚本模块有两种基本存储方式:
1. 默认方式为 .blend 文件内嵌: `Text Editor -> File -> Make Internal`
2. 外部脚本文件: `Text Editor -> File -> Save As...`
模块是 Python 脚本的基本组织方式,它可以保存在独立的文件中,也可以保存在某种数据库中。而脚本模块只是 Python 众多支持模块中的一种,它还支持 C/C++ 编写的扩展模块等等。
比如,将 `module.py` 模块保存到 .blend 同级的 `scripts/module.py`,那么导入脚本模块就应该使用 `script.module.Component` 这样的点路径。
使用外部脚本的一个好处是可以利用外部编程工具,如 VS Code/Sublime Text 等等,还可以使用语言服务器 LSP 提供语法提示等辅助功能。
脚本组件命名使用大小写结合的驼峰拼写法(CamelCase),比如 `MypyComponent` 就会在面板中显示为 `Mypy Componnet`,可以使用全小写,但是最好大写开头表示一个类型定义,而不是变量。
注意:有几个小问题,如果已经创建了脚本模块文件,不能通过点击 **Create** 往现有模块添加新组件,会提示脚本模块已经存在。另外,不要使用二级占路径,UPBGE 不支持。模块名也不要与存在的目录同名,否则创建模块后,提示模块没有相应的组件属性,但脚本模块依然会创建,只是不能正确使用:
module 'xxx' has no attribute 'Component'
对于已经保存到外部文件的脚本,还可以使用 Blender 脚本编辑器重新找打开,但此时注意,外部脚本模块文件和 Blender 内部脚本模块名称可以不一致,因为它们此时是两个不同的脚本模块。但是在执行`Make Internal` 将脚本模块明确为内嵌模块之前,保存脚本内容还是保存会保存到外部脚本文件内。挂载时组件时,依然可以使用文件目录路径作为 Python 模块的点路径,同时又可以挂载内嵌模块。

如果脚本文件有语法错误,则加载时会提示,应该打开 Window -> Console 查看控制台输出信息:
No mudules named "script.module" or script error at loading.
`Game Object Properties -> Game Object` 面板可以创建游戏数据对象,可以和脚本组件保存在相同脚本模块文件中。但是加载时,不能在 Game Object 面板中添加组件,也不能在 Game Component 面板中添加 Game Object。
组件脚本文件可以和 .blend 文件同级目录存放,刷新已加载组件点击下拉菜单 `Relaod Component`。首次添加脚本组件就会执行 `start()` 初始化方法,刷新时不再执行,但是模块级代码会被执行。同一个脚本组件可以多次添加使用,也可以给场景中选中的多个对象添加。但是要移除已挂载的脚本组件,就需要单独对每个对象进行移除操作。