
UPBGE 游戏开发系列教程:
# 🥚 UPBGE - Blender 游戏引擎继承者
## 🎨 UPBGE Python Scripting
## 🎨 Logic Nodes (源代码分析)
## 🎨 Script Lifecycle (源代码分析)
## 🎨 UPBGE Python API (源代码分析)
源文档:https://github.com/Jeangowhy/opendocs/blob/main/upbge.md
内部模块导致的对象相关接口可以从 UPBGE API 文档查询,另一个信息获取途径则是直接参考源代码:
1. https://github.com/UPBGE/UPBGE-API 2. https://github.com/UPBGE/upbge/releases * Blender for Developers * Bledner Code Layout https://www.blender.org/bf/codelayout.jpg * Bledner Code Layout of modules https://download.blender.org/ftp/ideasman42/pics/code_layout.webp
API 文档本身是通过 doxygen 工具从源代码中内容生成的摘要信息,所以阅读源代码有不一样的效果,关键是找对阅读方法(心态)和代码阅读工具,高效定位文件、符号的工具即为好。阅读代码至少有两点实用主义的好处:一是学习引擎更底层的工作原理,二是为今后可能的深入开发打下一个基础。
源代码阅读工具配置参考:https://github.com/Jeangowhy/opendocs Code Map and Live Dependency Validation on Visual Studio https://learn.microsoft.com/en-us/visualstudio/modeling/map-dependencies-across-your-solutions
当然,除了生成的摘要信息外,官方还在文档中添加额外的资源,比如示范程序:
upbge-0.30\doc\python_api\examples\aud upbge-0.30\doc\python_api\examples\bpy upbge-0.30\doc\python_api\examples\gpu upbge-0.30\doc\python_api\examples\matah
另外,还可以使用 Python 内置的对象信息查询方法,如 `help('tuple')` 或者 `dir('tuple')`。
根据 Bledner Code Layout 文档显示,Blender 源代码主要分布在以下几个目录:
🟡 Modules only call lower level code
<---------------- Editor definitions, drawing, interaction ---------------->
<---------------------------- Editor utilities ---------------------------->
<---------------------------------- Tools --------------------------------->
blender/source/blender/editors/ 🟡
<-------------------------- Game & Render engine -------------------------->
blender/source/ blender/source/blender 🔵
<-------------------------- General Blender APIs -------------------------->
blender/source/blender/ 🟡
<----------------- Utility Libraries (in own development) ----------------->
blender/intern/ 🔵
<-------------- Utility Libraries (from external development) ------------->
blender/extern/ 🔵
对于 UPBGE 游戏引擎部分则集中在 gameengine 20 个子目录下的将近 500 个文件。就当下而言,将可以阅读重点聚焦到包含 `KX_GameObject` 和 `KX_PythonComponent` 两个类型的 Ketsji 这个子目录下:
> tree.exe -f UPBGE-0.30-windows-x86_64\upbge-0.30\source\gameengine
|-- gameengine\Ketsji
| |-- gameengine\Expressions\EXP_Python.h
| |-- gameengine\Expressions\EXP_PyObjectPlus.h
| |-- gameengine\Ketsji\KX_BlenderCanvas.cpp
| |-- gameengine\Ketsji\KX_BlenderCanvas.h
| |-- gameengine\Ketsji\KX_BlenderMaterial.cpp
| |-- gameengine\Ketsji\KX_BlenderMaterial.h
| |-- gameengine\Ketsji\KX_Camera.cpp
| |-- gameengine\Ketsji\KX_Camera.h
| |-- gameengine\Ketsji\KX_EmptyObject.cpp
| |-- gameengine\Ketsji\KX_EmptyObject.h
| |-- gameengine\Ketsji\KX_FontObject.cpp
| |-- gameengine\Ketsji\KX_FontObject.h
| |-- gameengine\Ketsji\KX_GameObject.cpp
| |-- gameengine\Ketsji\KX_GameObject.h
| |-- gameengine\Ketsji\KX_Globals.cpp
| |-- gameengine\Ketsji\KX_Globals.h
| |-- gameengine\Ketsji\KX_KetsjiEngine.cpp
| |-- gameengine\Ketsji\KX_KetsjiEngine.h
| |-- gameengine\Ketsji\KX_PythonInit.cpp
| |-- gameengine\Ketsji\KX_PythonInit.h
| |-- gameengine\Ketsji\KX_PythonInitTypes.cpp
| |-- gameengine\Ketsji\KX_PythonInitTypes.h
| |-- gameengine\Ketsji\KX_PythonMain.cpp
| |-- gameengine\Ketsji\KX_PythonMain.h
| |-- gameengine\Ketsji\KX_PythonProxy.cpp
| |-- gameengine\Ketsji\KX_PythonProxy.h
| |-- gameengine\Ketsji\KX_PythonProxyManager.cpp
| |-- gameengine\Ketsji\KX_PythonProxyManager.h
| |-- gameengine\Ketsji\KX_PythonComponent.cpp
| |-- gameengine\Ketsji\KX_PythonComponent.h
| |-- gameengine\Ketsji\KX_Scene.cpp
| |-- gameengine\Ketsji\KX_Scene.h
Python 模块开发接口中定义了一套接口类型,如 `PyObject`、`PyCFunction`、`PyMethodDef` 等等用于向脚本环境导出类型定义、函数符号、成员方法定义。Ketsji Engine 基于这些接口类型定义了一系列宏函数用来向 Python 模块导出各种符号定义。`EXP_PyObjectPlus` 这个抽象类代表了一个导出出 Python 脚本环境中的类型,它定义了一套通用方法,支撑起 UPBGE 脚本环境下的基类结构。
另一方面,Python 接口规则使用 `PyTypeObject` 来定义一个导出到脚本环境的类型定义结构,这个接口导出的类型,在脚本看来才是“真正”的类型定义。相对于 `EXP_PyObjectPlus`,它是 C++ 类型。
源代码条件编译使用 `WITH_PYTHON`,以上这些符号都可以用来搜索那些导出到脚本环境的 API,名称中带有 DOC 表示这是一个导出 API 帮助文档信息的宏定义:
// These macros are helpful when embedding Python routines
EXP_PYMETHOD(class_name, method_name)
EXP_PYMETHOD_VARARGS(class_name, method_name)
EXP_PYMETHOD_NOARGS(class_name, method_name)
EXP_PYMETHOD_O(class_name, method_name)
/// Method table macro (with doc).
EXP_PYMETHOD_DOC(class_name, method_name)
EXP_PYMETHOD_DOC_VARARGS(class_name, method_name)
EXP_PYMETHOD_DOC_O(class_name, method_name)
EXP_PYMETHOD_DOC_NOARGS(class_name, method_name)
/// Method table macro (with doc).
EXP_PYMETHODTABLE(class_name, method_name)
EXP_PYMETHODTABLE_O(class_name, method_name)
EXP_PYMETHODTABLE_NOARGS(class_name, method_name)
EXP_PYMETHODTABLE_KEYWORDS(class_name, method_name)
/// Function implementation macro.
EXP_PYMETHODDEF_DOC(class_name, method_name, doc_string)
EXP_PYMETHODDEF_DOC_VARARGS(class_name, method_name, doc_string)
EXP_PYMETHODDEF_DOC_O(class_name, method_name, doc_string)
EXP_PYMETHODDEF_DOC_NOARGS(class_name, method_name, doc_string)
KX_PythonInit.cpp 代码有一系列核心模块的导出,`initBGE()` 方法初始化的模块包括:
1. Application Data (bge.app)
2. Game Types (bge.types)
3. Physics Constraints (bge.constraints)
4. 游戏逻辑模块的方法导出 Game Logic (bge.logic)
5. 光栅化模块的方法导出 Rasterizer (bge.render)
6. Game Keys (bge.events)
7. Video Texture (bge.texture)
PyMODINIT_FUNC initBGE() { PyObject *modules = PyImport_GetModuleDict(); PyObject *mod = PyModule_Create(&BGE_module_def); addSubModule(modules, mod, initApplicationPythonBinding(), "bge.app"); addSubModule(modules, mod, initConstraintPythonBinding(), "bge.constraints"); addSubModule(modules, mod, initGameKeysPythonBinding(), "bge.events"); addSubModule(modules, mod, initGameLogicPythonBinding(), "bge.logic"); addSubModule(modules, mod, initRasterizerPythonBinding(), "bge.render"); addSubModule(modules, mod, initGameTypesPythonBinding(), "bge.types"); addSubModule(modules, mod, initVideoTexturePythonBinding(), "bge.texture"); return mod; }
引擎中定义了两个链表数据结构,它们都是双向循环链表,和双向链表 (Doubly Linked List) 不同,增加首尾衔接功能,`SG_DList` 搜索动作不会以有到尾端的情况出现。重双向循环链表 `SG_QList`,同时存储了两条 `SG_DList` 双向循环链表。链表是一种插入算法时间为常数的数据结构,因为链表中的节点使用指针记录前后节点的位置,所以只需要断开原链接,将新的数据链接前后节点即完成数据插入。但是数据搜索速度随着节点数量增加而下降,因为要逐节点线性查找。
以下两个类型定义本身作为链表中的节点使用,不同的是 QList 存储双链,DList 存储单链,每条循环链使用 m_current 指针记录当前位置:
1. `SG_DList` Double circular linked list
2. `SG_QList` Double-Double circular linked list. For storing an object is two lists simultaneously
链表的一种改进算法是跳表,以下用一个 [1,9] 的区间数据来解析 SkipList 结构,因为数据量少,只需要 2 级索引即可以实现二分法查找的效率:
Level 2 -> 🔴1 ------------------------> 🔴5
| |
Level 1 -> 🔴1 --------> 🔴3 ---------> 🔴5 --------> 🔴7
| | | |
🔴1 -> 🔴2 -> 🔴3 -> 🔴4 -> 🔴5 -> 🔴6 -> 🔴7 -> 🔴8 -> 🔴9
`SCA_ILogicBrick` 是逻辑块的基类,传感器、控制器、执行器等等都派生自此,它些类因为节点连接关系的处理,就需要使用链表这样的数据结构。
`SCA_IObject` 是游戏对象的父类,SCA 前缀表明这是一个场景相关类型,这个类形提供一些常用方法。
`KX_Scene` 场景对象可以访问到场景内的所有对象,它不仅继承了 Python Proxy,还从第二父类 `SCA_IScene` 继承一系统调试方法,C++/Python 支持多继承,但这一特性带来的复杂度多于便利。
多承继引入的一个问题就是同名方法查找问题:如果继承多个父类中,都定了相同的函数,那么选用哪一个?这就是 Python MRO - Method Resolution Order 机制中 C3 算法要做的事,C3 即三个约束条件。
C3 算法保证了三件事情:
1. 单调性:任意两个类的相对顺序和自己所有父类的 MRO 顺序一致,即父集与子集关系。
2. 一致性:任意两个类的顺序和继承图里所有直接继承自这两个类的声明顺序一致。
3. 有序性:如果两个类不具有直接的继承关系,那么找到两个类的最小公共子类,其多继承顺序靠前的分支上的类具有高优先级。
基本的树状数据结构搜索方法有两种:
- 【经典类】 Depth-First Search (DFS) 深度优先搜索算法;
- 【新式类】 Breadth First Search (BFS) 广度优先搜索算法;
Python 旧式类的算法使用从左往右(继承顺序),采用深度优先搜索(DFS)的算法,称为旧式类的 MRO。单纯使用这两种方法都不是最佳选择,Python 最后选择了 C3 算法。多继承必然会遇到棱形法则关系处理 Multiple Inheritance: The Diamond Rule,以下示意图摘自 Python 创始人的论文:
PEP 253 -- Subtyping Built-in Types by Guido van Rossum
class A:
^ ^ def save(self): ...
/ \
/ \
/ \
/ \
class B class C:
^ ^ def save(self): ...
\ /
\ /
\ /
\ /
class D
C3 算法又称为超类线性化 (superclass linearization)。Python 会计算出每一个类的 MRO 列表。一个类的 MRO 列表是一个包含了其继承链上所有基类的线性顺序列,并且继承列表中的每一项均保持唯一。当需要在继承链中寻找某个属性时,Python 会在 MRO 列表中从左到右开始查找各个基类,直到找到第一个匹配这个属性的类为止。
我们不必深究这个算法的数学原理,它实际上就是合并所有父类的 MRO 列表并遵循如下三条准则:
- 先子类、后父类的顺序进行符号匹配检查;
- 根据父类在列表中的从左到右顺序进行检查;
- 如果对下一个类存在两个合法的选择,选择第一个父类;
其实我们只需要知道 MRO 列表中类的顺序代表着类层次结构间的关系即可。使用类型的 `__mro__` 或者 `mro()` 可以获取其 MRO 解析结果数据。
class A:
def save(self): print("A::save()")
class B(A):
def save(self): print("B::save()")
class C(A):
def save(self): print("C::save()")
class BS(B, C): pass
class CS(C, B): pass
BS().save() # B::save()
CS().save() # C::save()
print(BS.mro()) # (<'BS'>, <'B'>, <'C'>, <'A'>, <'object'>)
`KX_PythonProxyManager` 是游戏对象的注册中心,负责调用所有游戏对象的 `Update()` 方法。
+==========================================+
| KX_PythonProxyManager |
+==========================================+
| std::vector<KX_GameObject *> m_objects |
| bool m_objects_changed = false |
| void Register(KX_GameObject *gameobj) |
| void Unregister(KX_GameObject *gameobj) |
| void Update() |
+==========================================+
upbge-0.30\source\gameengine\GameLogic\SCA_LogicManager.h
upbge-0.30\source\gameengine\GameLogic\SCA_LogicManager.cpp
+==========================================+
| SCA_LogicManager |
+==========================================+
void RegisterEventManager (SCA_EventManager *eventmgr);
void RegisterToSensor (SCA_IController *controller, class SCA_ISensor *sensor);
void RegisterToActuator (SCA_IController *controller, class SCA_IActuator *actuator);
void BeginFrame (double curtime, double fixedtime);
void UpdateFrame (double curtime);
void EndFrame();
void AddActiveActuator (SCA_IActuator *actua, bool event)
upbge-0.30\source\gameengine\GameLogic\SCA_ISensor.h
upbge-0.30\source\gameengine\GameLogic\SCA_ISensor.cpp
+==================================================================+
| SCA_ISensor (SCA_IObject *gameobj, SCA_EventManager *eventmgr) |
+==================================================================+
virtual void ReParent (SCA_IObject *parent);
void Activate (SCA_LogicManager *logicmgr);
virtual bool Evaluate () = 0;
virtual bool IsPositiveTrigger ();
virtual void Init ();
void SetPulseMode (bool posmode, bool negmode, int skippedticks);
void SetInvert (bool inv);
void SetLevel (bool lvl);
void SetTap (bool tap);
virtual void RegisterToManager ();
virtual void UnregisterToManager ();
void Replace_EventManager (SCA_LogicManager *logicmgr);
void LinkToController (SCA_IController *controller);
void UnlinkController (SCA_IController *controller);
void UnlinkAllControllers ();
void ActivateControllers (SCA_LogicManager *logicmgr);
virtual void ProcessReplica ();
virtual double GetNumber ();
virtual sensortype GetSensorType ();
void Suspend ();
bool IsSuspended ();
bool GetState ();
bool GetPrevState ();
int GetPosTicks ();
int GetNegTicks ();
void Resume ();
void ClrLink ();
void IncLink ();
void DecLink ();
bool IsNoLink () const;
以下是 KetsjiEngine 一些主要类型关系示意图:
*------------------*
| EXP_PyObjectPlus |
*------------------*
| .-------------------------.
+-->| EXP_Value |-----.
| '-------------------------' |
| | .----------------------. |
| +-->| KX_BlenderMaterial | | .-----------------.
| | '----------------------' +-->| KX_PythonProxy |
| | .----------------------. | '-----------------'
| +-->| KX_CharacterWrapper | | | .-------------.
| | '----------------------' | +-->| SCA_IObject |
| | .----------------------. | | '-------------'
| +-->| KX_ConstraintWrapper | | | | .---------------.
| | '----------------------' | | `-->| KX_GameObject |
| | .----------------------. | | '---------------'
| +-->| KX_LodManager | | | .--------------------.
| | '----------------------' | +-->| KX_PythonComponent |
| | .----------------------. | | '--------------------'
| +-->| KX_VehicleWrapper | | | .-------------.
| | '----------------------' | `-->| KX_Scene | (SCA_IScene)
| | .----------------------. | '-------------'
| +-->| SCA_InputEvent | |
| | '----------------------' | .-----------------.
| | .----------------------. ·-->| SCA_ILogicBrick | (SG_QList)
| `-->| SCA_PythonJoystick | '-----------------'
| '----------------------' | .-----------------.
| .-------------------------. +-->| SCA_IActuator |
+-->| KX_2DFilterManager | | '-----------------'
| '-------------------------' | | .---------------------.
| .-------------------------. | +-->| BL_ArmatureActuator |
+-->| KX_LibLoadStatus | | | '---------------------'
| '-------------------------' | +-->| SCA_ActionActuator |
| .-------------------------. | | '---------------------'
+-->| KX_LodLevel | | `-->| SCA_TrackToActuator |
| '-------------------------' | '---------------------'
| .-------------------------. | .-----------------.
+-->| SCA_PythonKeyboard | +-->| SCA_IController |
| '-------------------------' | '-----------------'
| +-------------------------+ | | .----------------------.
`-->| SCA_PythonMouse | | `-->| SCA_PythonController |
+-------------------------+ | '----------------------'
| .-------------.
`-->| SCA_ISensor |
'-------------'
.-------------------.
| KX_GameObject |
'-------------------'
| .-------------------.
+-->| BL_ArmatureObject |
| '-------------------'
+-->| KX_Camera |
| '-------------------'
+-->| KX_EmptyObject |
| '-------------------'
+-->| KX_FontObject |
| '-------------------'
+-->| KX_LightObject |
| '-------------------'
`-->| KX_NavMeshObject |
'-------------------'
*------------------------------------------------------*
| SCA_ILogicBrick : public EXP_Value, public SG_QList |
*------------------------------------------------------*
| .-----------------. | .-----------------.
+-->| SCA_ISensor | +-->| SCA_IActuator |
| '-----------------' '-----------------'
| |--> SCA_ActuatorSensor |--> BL_ArmatureActuator
| |--> SCA_AlwaysSensor |--> SCA_2DFilterActuator
| |--> SCA_ArmatureSensor |--> SCA_ActionActuator
| |--> SCA_CollisionSensor |--> SCA_AddObjectActuator
| |--> SCA_DelaySensor |--> SCA_CameraActuator
| |--> SCA_JoystickSensor |--> SCA_CollectionActuator
| |--> SCA_KeyboardSensor |--> SCA_ConstraintActuator
| |--> SCA_MouseSensor |--> SCA_DynamicActuator
| |--> SCA_MovementSensor |--> SCA_EndObjectActuator
| |--> SCA_NetworkMessageSensor |--> SCA_GameActuator
| |--> SCA_PropertySensor |--> SCA_MouseActuator
| |--> SCA_RandomSensor |--> SCA_NetworkMessageActuator
| `--> SCA_RaySensor |--> SCA_ObjectActuator
| .-----------------. |--> SCA_ParentActuator
`-->| SCA_IController | |--> SCA_PropertyActuator
'-----------------' |--> SCA_RandomActuator
|--> SCA_ANDController |--> SCA_ReplaceMeshActuator
|--> SCA_ExpressionController |--> SCA_SceneActuator
|--> SCA_IController |--> SCA_SoundActuator
|--> SCA_NANDController |--> SCA_StateActuator
|--> SCA_NORController |--> SCA_SteeringActuator
|--> SCA_ORController |--> SCA_TrackToActuator
|--> SCA_PythonController |--> SCA_VibrationActuator
|--> SCA_XNORController `--> SCA_VisibilityActuator
`--> SCA_XORController
EXP_PyObjectPlus
|--> BL_ArmatureBone : public EXP_PyObjectPlus
KX_2DFilterManager : public RAS_2DFilterManager, public EXP_PyObjectPlus
EXP_Expression : public CM_RefCount<EXP_Expression>
|--> EXP_ConstExpr : public EXP_Expression
|--> EXP_IdentifierExpr : public EXP_Expression
|--> EXP_Operator2Expr : public EXP_Expression
EXP_Value : public EXP_PyObjectPlus, public CM_RefCount<EXP_Value>
|--> BL_ArmatureChannel : public EXP_Value
|--> BL_ArmatureConstraint : public EXP_Value
|--> BL_Shader : public EXP_Value, public virtual RAS_Shader
|--> BL_Texture : public EXP_Value, public RAS_Texture
|--> EXP_ListWrapper : public EXP_Value
|--> EXP_PropValue : public EXP_Value
|--> EXP_BaseListValue : public EXP_PropValue
|--> EXP_EmptyValue : public EXP_PropValue
|--> EXP_ErrorValue : public EXP_PropValue
|--> EXP_FloatValue : public EXP_PropValue
|--> EXP_StringValue : public EXP_PropValue
|--> KX_2DFilterFrameBuffer : public EXP_Value, public RAS_2DFilterFrameBuffer
|--> KX_BlenderMaterial : public EXP_Value, public RAS_IPolyMaterial
|--> KX_CharacterWrapper : public EXP_Value
|--> KX_CollisionContactPoint : public EXP_Value
|--> KX_ConstraintWrapper : public EXP_Value
|--> KX_LodManager : public EXP_Value
|--> KX_MeshProxy : public EXP_Value
|--> KX_PolyProxy : public EXP_Value
|--> KX_PythonProxy : public EXP_Value
|--> KX_VehicleWrapper : public EXP_Value
|--> KX_VertexProxy : public EXP_Value
|--> SCA_InputEvent : public EXP_Value
|--> SCA_PythonJoystick : public EXP_Value
|--> Texture : public EXP_Value
RAS_2DFilter : public virtual RAS_Shader
SCA_TimeEventManager : public SCA_EventManager