UPBGE Logic Nodes (源代码分析)
UPBGE 游戏开发系列教程:
# 🥚 UPBGE - Blender 游戏引擎继承者
## 🎨 UPBGE Python Scripting
## 🎨 Logic Nodes (源代码分析)
## 🎨 Script Lifecycle (源代码分析)
## 🎨 UPBGE Python API (源代码分析)
与逻辑块、脚本组件不同,UPBGE Logic Nodes 以插件的形式集成在 UPBGE 开发环境,完全以 Python 脚本实现。逻辑块和脚本组件则基于导出到脚本环境中的 C++ API 开发。
源代码位置对应如下,Logic Nodes 是一个独立插件项目,可以在 Github 上下载:
1. **Logic Nodes** scripts\addons\bge_netlogic\basicnodes\__init__.py
2. **Logic Bricks** upbge-0.30\source\gameengine\GameLogic
3. **Python Components** upbge-0.30\source\gameengine\Ketsji\KX_PythonComponent.h
4. Uchronian Logic: UPBGE Logic Nodes https://github.com/UPBGE/UPBGE-logicnodes
Blender Python Console 执行以下脚本可以获取逻辑节点分类与运行时类型对照表,Blender 环境不支持 stdout 重定义,只好将输出文件 nodes.md 设置到打印函数参数中:
注意,在脚本组件中,有些方法不能直接调用,比如只有在 Logic Bricks 下有效的当前控制器获取方法就不能在脚本组件中直接调用,除非是在 python controllers namespace:
SystemError: bge.logic.getCurrentController(), this function is being run outside the python controllers context, or blenders internal state is corrupt.
你应该知道脚本会用什么方式调用,至少有以下四种不同的运行方式:
1. Logic Bricks - Python Controller
2. Logic Nodes - Python - Run Python Code
3. Game Object Properties - Python Component
4. Game Object Properties - Game Object
Controllers 是脚本编程中的桥梁一样的对象,它上连传感器 Sensors,下连执行器 Actuators。在源代码中,控制器的实现代码很少量,因为它本身的逻辑不复杂。他的父类 `SCA_ILogicBrick`, 涉及整个 BGE 逻辑块的实现,逻辑复杂,代码也相对多。
upbge-0.30\doc\python_api\rst\bge.types.rst
upbge-0.30\doc\python_api\rst\bge.logic.rst
源文档:https://github.com/Jeangowhy/opendocs/blob/main/upbge.md
Logic Nodes 编辑器侧栏面板也可以操作逻辑节点代码生成:`Administration -> Compile All`,注意使用 `Apply As Bricks` 方式,如果使用 `Apply As Component` 方式下编译,生成代码则会内嵌在 Blender 文件,可使用自带的脚本编辑器查看。
工程 `bgelogic` 目录下生成代码中的逻辑节点树并不是一个具体的类型,它只是一个 Python 脚本文件,也是 Python 的脚本模块,这个模块中定义了:
1. 一个 `_initialize(owner)` 初始化函数;
2. 一个 `pulse_network(controller)` 控制器触发函数;
控制器对象 `owner` 属性引用一个当前调用这个控制器的游戏对象 GameObject。因为一个控制器可以挂载到多个对象上,不同的控制器挂载到不同对像,使用所用的游戏对象也不同。
一个名为 **NewTree.001** 的逻辑节点树,用 `On Init` -> `Once` -> `Print` 去执行
打印 `Get Property` 获取的属性数据。由代码生成工具 tree_code_generator.py 生成的逻辑
节点树代码如下:
代码中会导入 uplogic\nodes.py 模块,创建 `LogicNetwork` 实例,它代表了逻辑节点树,节点类型定义为称为 Cell。变量命名按 CON - Condition Nodes, ACT - Action Nodes 这样的规则。网络执行时,`pulse_network()` 被调用,传入控制器所携带的 owner 游戏对象给初始化方法,初始化好连接关系,再通过 `setup()` 方法配置所有连接的节点。
`StatefulValueProducer` 定义一个有状态节点,两个接口函数 `get_value()` `has_status()`。`LogicNetworkCell` 定义一个有运行控制方法的节点(Cell),定义多个接口函数,还有多个属性:
1. **_uid** 私有属性,节点标识;
2. **_status** 私有属性,存储节点状态;
3. **_value** 私有属性,存储节点值;
4. **_children** 私有属性,子节点集合;
5. **network** 所属节点网络;
6. **is_waiting** 是否处于等待状态;
接口方法包括了 `setup()` `stop()` `reset()` 以及 `evaluate()` `deactivate()` 等等。`LogicNetwork` 作为节点树的实现,它实现 `setup()` 方法设置树内连接的所有节点的 network 属性,以及调用其 `setup()` 方法完成配置,如果节点有配置需要。
LogicNetworkCell 接口定义了三个逻辑状态,初始状态就是 WAITING:
节点求值完成后,调用 `_set_ready()` 切换到 READY 状态,或者 `_set_status()` 直接设置状态。
NO_VALUE 在 On Value Changed To `ConditionValueTrigger` 这些需要通过求值结果来决定是否输出触发条件的节点上使用。求值方法通过判断最后一次更新的值来决定如何调用 `_set_value()`,初始状态下的 last_value 初始值就是 NO_VALUE。
`LogicNetwork` 作为逻辑树的实现,是逻辑节点编程的核心节点,也是逻辑节点的管理器。它继承了以上的状态之外,还要实现其它节点的功能,比如给 Ray Casts 节点提供 `rayCast()` 投射方法,所以逻辑树在实现上也是一个 Caster,这么多功能使用得它本身的结构有点大。射线投射方法还会在逻辑树上设置私有属性,`_NL_ray_cast_data` 用于缓存数据。
✨ 逻辑节点使用教程
UPBGE 屏幕空间大小标准化为 Vec(1,1),要获取窗口原像素单位大小,可以使用渲染器属性。相机对象 `KX_Camera` 提供了 API 用于获取场景中的 3D Vector 坐标,或者获取与指定视线方向相交的对象。又或者反过来,将指定对象的原点坐标转换为相应的屏幕坐标。这里涉及的变换矩阵可以通过相机 API 获取。
upbge-0.30\doc\python_api\rst\bge_types\bge.types.KX_GameObject.rst
upbge-0.30\doc\python_api\rst\bge_types\bge.types.KX_Camera.rst
相机对象的父类,`KX_GameObject` 提供两个更基础的方法,其中 `rayCast()` 在逻辑树中调用:从一个坐标点或物体观察另一个点或物体,在限定 **dist** 距离内找到具有 **prop** 属性匹配的,射线碰撞到的第一个物体。
1. **objto** 参数接收一个目标坐标或对象,逻辑树中使用 ray_target 变量。
2. **face** 参数用于确定返回法线的朝向,0 表示总是朝向射线原点,1 表示碰撞点的曲面法线。
3. **xray** 是否使用 X 光穿透,配合 **prop** 属性,默认 0 表示不穿透,有效目标被遮挡就不能拾取。
4. **poly** 根据不同的值 0/1/2 值返回 3 ~ 5 个数据,(KX_GameObject, hitpoint, hitnormal, KX_PolyProxy, hituv)。
5. **mask** 使用 16-bit 数据用于碰撞层的对象过滤,`collisionGroup & mask`,同层才进行检测。
用户在游戏世界中需要使用鼠标、屏幕等基础设备进行交互,即就是输入、输出的数据处理,当用于在屏幕上一点进行操作,而这个点输入时对应的时屏幕空间的二维坐标,根据引擎中的相机成像过程,进行逆运算才能得到相应的三维空间的坐标,`getScreenVect()`。反过来,3D 场景中的对象坐标要通过成像过程的数学运算,才得到相应的坐标,`getScreenPosition()`。
软件中的相机只是抽象的概念,本质上是数学关系,光线传播或者是光学成像原理。那么,求解 3D 空间坐标参照的相机位置就是一个关键信息,不同相机成像不同,也就是对应不同空间坐标中的物体、表面的信息。相当在屏幕各个像素发射一条射线,沿着相机的正前方传播,光线与物体碰撞时的坐标位置就是要求解的屏幕坐标对应的 3D Vector。
以下射线工具需要使用 `KX_Camera` 对象的 API,因此相机属性只能选择相机,不能使用其它对象替代。并且,射线可以设置一个有效距离,超过距离就不再进行碰撞检测。另外,待检测的物体可以设置特定的属性,**Game Properties** 面板中设置一个数据属性,然后射线节点 Property 属性中填写相同的名称,那些没有设置相应属性的对象就不会参与射线的碰撞检测。
参考 `ActionMousePick` 节点调用逻辑树定义的光线投射方法获取数据,**ray_origin** 就是光线传播的起点,留空表示默认使用相机成像平面发射光线,光线从起点向 **ray_target** 坐标传递,两个坐标向量相减得到的向量就表示光线传播方向。
raw_target = camera.worldPosition - camera.getScreenVect(x, y)
射线投射逻辑节点功能 Ray Casts:
1. **Mouse Ray** 按照指定相机视角,从光标位置发出射线,并获取射线首次碰撞到对象信息。
2. **Camera Ray** 按照指定相机视角,在相机坐标空间原点向 Aim 坐标投射射线,默认拾取屏幕中心目标对象。
3. **Ray** 直接指定射线起点 Origin 和目标点 Aim,拾取两点之间首个碰撞目标,Visualize 可查看射线。
4. **Projectile Ray** 按抛物线投射光线,Power 能量越大线条越直,可以激活 Visualize 查看效果。
射线投射逻辑节点设计时、运行时类型对照:
UPBGE-Docs\source\manual\logic\sensors\types\ray.rst
射线投身节点通用属性说明:
Distance 指定射线有效距离,在此距离内的物理对象才可能被拾取。
Property 指定一个名称,只有设置了相应 Game Properties 的对象才可能被拾取。
启用 X-Ray 可以拾取被遮挡的有效目标,即设置了 Property 中指定的属性的游戏对象。

**Camera Ray** 节点中的 Aim 坐标是以相机坐标空间计算的,也就是笛卡尔坐标系统,以屏幕中心为原点 (0,0)。这个原点与鼠标默认的坐标原点(屏幕左上角)不重叠,并且相机空间与模型使用相同的长度单位,而鼠标使用的是规范值,[1,1],所以鼠标的坐标数据需要根据窗口大小进行转换:
mouse_position - 0.5 * window_size
注意,Aim 输入可以是 Vec2 也可以是 Vec3,前者会触发屏幕空间转换,而 **Status** 节点获取的坐标数据已经将二维转换为三维向量,所以不会触发这个过程。同样 **Vectory XY** 也是输出 3 维。所以要么直接设置 Aim = [0,0] 拾取屏幕中心目标对象,要么自行处理目标坐标数据:
使用 **Get Resolution** 节点可以获取全屏幕的像素大小,但是不能直接用来将鼠标坐标转换成相机画面中的世界尺寸。画面的大小与相机 lens 和 fov 等参数密切相关。代码中乘 10 是一个放大系数。
在处理数据过程中,Math 运算节点可以对向量进行数值的运算,而 Vector Math 节点则进行向量运算,例如点积,叉积乖乖。其中 Vector XY,虽然只有两个分量,但其实它是三维的输出。
还有 RunPythonCode,虽然它可以运行脚本,但也只是调用函数,并且参数只能有一个,当然,可以通过字典对象传递多个值。
另外,Object -> Data -> Get Position 等节点可以获取游戏对象的空间坐标等信息,和专用的 Get Property 节点不同,它专用于 Game Properties 属性数据的获取,等价于游戏对象的 `get()`。
相比 **Mouse Ray** 以屏幕中心为 (0.5,0.5),这和鼠标输入的坐标系统原点重叠在左上角,
**Status** 节点获取到的光标坐标数据可以使用直接。
鼠标逻辑节点功能 Input -> Mouse:
01. **Look** 指定的 Main 对象视角跟随鼠标移动面转动,建议指定“头部”对象。
02. **Set Position** 设置光标位置,左上角到右下角 (0, 0) ~ (1,1),屏幕中心为 (0.5,0.5)。
03. **Cursor Visibility** 设置光标是否显示。
04. **Status** 获取光标的屏幕坐标、移动、滚轮数据。
05. **Button** 鼠标点击时触发,可以指定 L/M/R 按钮,以及是否每帧都触发。
06. **Moved** 鼠标移动时触发。
07. **Button Up** 鼠标按钮释放时触发,可以指定 L/M/R 按钮,以及是否每帧都触发。
08. **Button Over** 鼠标在物理体上悬停时并点击时触发,可以指定 L/M/R 按钮,非物理体没有效果。
09. **Wheel** 滚轮事件触发,Scroll Up/Down 或者 Up and Down 三种条件。
10. **Over** 鼠标与物体体产生的 Enter/Over/Exit 事件输出,还有相应的碰撞点以及法线。
注意,**Status** 获取的 Movement 是实时的鼠标移动距离数据,不移动就为 0 值,而且数值是标准化的大小 [1,1],表示整个屏幕空间,移动的像素距离换算成比例值。
**Look** 节点是使对象跟随鼠标移动的快速方法,可以指定“躯干”和“头部”对象,当鼠标偏移屏幕中心时,就根据屏幕空间的 X/Y 偏移量分别调整 Main Object 和 Head 的旋转角度,如果没有指定 Head 对象,则将两轴偏移量都应用到主体的旋转。勾选 Smooth 可以使用旋转动作的起止运动更平缓。可以指定敏感度 Sensitivity,这是一个乘数,设置为 0 则不会产生旋转量。
旋转角度可以控制在一个范围,使用 Vec2 表示:
1. Cap Left/Right 约束 local Z 旋转轴的角度范围,对应主体对象的偏转角,注意要求:x > y;
2. Cap Up/Down 约束 local X/Y axis 旋转轴的角度范围,对应头对象的俯仰角;
当前 UPBGE 0.3 版本源代码应该有逻辑错误,出现 use_cap_z 属性的重复,另一个应该是 use_cap_y。
鼠标逻辑节点设计时、运行时类型对照:
变换节点功能 Objects -> Transformation,Apply 类型施加物理参数,配合 Game Physics 物理系统属性使用,Bullet 物理引擎。节点激活 Local (蓝色) 和 Global 分别表示相对局部、全局坐标系统:
01. **Align Axis to Vector** 将物体指定的 Axis 轴向与指定的向量方向对齐。
02. **Apply Force** 向物理刚体施加力的作用,输入一个向量指定力度和方向。
03. **Apply Impulse** 施加冲量作用,两个向量输入分别是冲击点、冲量向量。
04. **Apply Movement** 施加移动量,输入向量指定偏移量。
05. **Apply Rotation** 施加旋转量,输入向量指定偏移量,使用角度为单位。
06. **Apply Torque** 施加扭矩,向量指定旋转轴及力度,比如 [0,0,1] 以 Z 轴为旋转中心,力度 1。
07. **Follow Path** 沿曲线路径移动对象。
08. **Move To** 向量 Target 目标点坐标匀速移动,受重力影响,无法向 Z 轴上方移动。
09. **Move To with Navmesh** 在指定导航网格上导航到指定目标点。
10. **Rotate To** 以 Rot Axis 为旋转轴,Front Axis 为正面旋转到目标角度。
11. **Translate** 以指定速度向目标位置平移,
**Rotate To** 仅在世界空间的单个轴和固定角度上应用旋转,可以瞬时或指定 Speed。如果旋转轴与正面同轴,则无法旋转(轴向锁定)。
冲量模拟的受力分析复杂,很容易出现旋转效果,特别是在冲击点不在物体的中心轴,越容易使用物体产生旋转。冲击点与冲量方向搭配不正确也影响模拟结果,比如物体已经在地面,使用 [0,0,-1] 这个冲量就可能不产生效果,因为冲击方向指向地面。冲击点可以超出物体几何空间,这相当旋转扭矩旋转 Torque。
冲量解算方法定义在 Bullet 引擎的移植代码中:`CcdPhysicsController::ApplyImpulse()`
upbge-0.30\source\gameengine\Physics\Bullet\CcdPhysicsController.cpp
https://upbge.org/docs/latest/manual/manual/logic_nodes/scene/objects/transformation/index.html
**Follow Path** 是复杂的变换逻辑节点,某些情况下似乎无法完全运行,可以用来模拟四处走动的 NPC。路径曲线使用 Nurbs Curve,在其中的点之间移动对象。仅与获取曲线点配合使用,Get Curve Points。
**Move To with Navmesh** 比较好用一点,只是使用起来需要构建 Navigation Mesh,好在 UPBGE 在场景属性面板提供了导航网格构建工具,只需要依据 Mesh 对象构建出 Navigation Mesh。前期工作就是创建网格空间结构。使用 Blender 的各种建模工具也很方便,以下提供一个参考思路:
1. 创建一个 `Curve -> Nurbs Path` 对象,按 Tab 进行编辑模式,按路径走向需要调整控制点;
2. 切换回对象模式,找到 Path 对象数据属性面板 `Geometry -> Bevel`;
3. 就使用 Round 倒角方式,将路径倒角出一个管道形状,Depth 指定深度,相当于控制管道半径;
4. 在对象模式下,找到菜单 `Object -> Convert -> Mesh` 将路径对象转换为网格体;
5. 切换到编辑模式,选择所有顶点,依次按 s z 0 将顶点沿 Z 轴缩放到 0 值,即压平网格体;
6. 选保持选中所有顶点,`Mesh -> Merge -> By Distance` 将顶点按就近距离合并以简化;
7. 找到场景属性面板 `Navigation Mesh -> Build Navigation Mesh` 按网格体生成导航路径;

导航网格只在寻路算法中表示 AI 角色可以触达的区域,像陡坡或直立物体所覆盖的区域都不算是游戏角色可触达的区域,可以根据导航网格面板中指定的参数设置,使用方法参考 Bullet 引擎的文档。当前算法可能会在转角位置产生卡住的不动的问题,可以适当调整一个稍大的 Cell Size,避免与障碍物接触。

生成导航网格后,直接在**Move To with Navmesh**中的 Navmesh Object 属性列表中选择指定,然后 Destination 指定导航目标位置,勾选 Visualize 可以运行时看到一条红线指示导航路径。变换逻辑节点设计时、运行时类型对照:
Animation 动画节点分为三类,Timeline、Armature、Constraints,功能参考:
**Animation Status** 获取指定对象上的动画播放状态,输出两个控制流和 Action Name/Frame 等。
**Play Animation** 在指定对象上播放指定时间轴动画数据,可以指定帧区间 Start/End,速度等等。
**Set Animation Frame** 将指定对象的动画播放状态移动到指定 Frame/Layer。
**Stop Animation** 停止在指定对象、以及指定动画层上播放动画。
**Set Animation Frame** 使用 Freeze 模式,要在停止状态下才有效,会将动画“冰冻”在指定帧位置,此时执行播放命令无效,需要先停止动画,解除冰冻状态才可以继续播放。

时间轴动画,就是记录在以帧为单位的属性数据的重放到指定对象上。例如,最简单的位移动画,数据记录的是 Position 属性在不同关键帧的数值。关键帧之间应该取什么什值,取决于插值算法生成的中间值。
Blender 编程模型中,基本单位是数据块,Datablocks。所有对象都具有数据,这些数据块包括 meshes, objects, materials, textures, node trees, scenes, texts, brushes ... 所有数据块都可以通过 Outliner -> blend-files 列表查看,删除,管理。
Datablocks https://docs.blender.org/manual/en/latest/files/data_blocks.html
而时间轴动画记录下来的数据就是 `Action` 数据块,打开 Timeline 编辑器,可以通过 Keying(关键设置工具)或直接按快捷键 I 来添加关键帧,将当前的状态数据记录下来。不同的属性在时间轴上显示为不同 Channels,选择需要记录的属性,就会产生相应的轨道记录。
关键键有不同的类型:
1. **Keyframe** (白/黄色菱形) 常规关键帧,如果之间有灰色块连接表示记录的状态数据相等,没变化。
2. **Breakdown** (青色小菱形) 间断状态,如用于不同关键姿态间的过渡。
3. **Move Hold** (深灰色/橙色菱形) 惯性延续,一个可以在一个保持姿势附近添加少量动作的关键帧。在动画摄影表中,它还会在它们之间显示一个条块。
4. **Extreme** (红色大菱形) 极端状态,或者其他需要的用途。
5. **Jitter** (绿色小菱形) 抖动,填充或烘焙关键帧,用于在其他帧上插帧,或用于其他所需目的。

`Keying -> Active Keying Set` 列表中可以选择当前活动的通道,然后点击带 + 号的钥匙图标就可以在相应的属性通道添加一个关键帧,不需要的关键帧也可以随时删除,可以直接在时间轴框选关键帧,移动它们到指定帧位置,或者直接删除它们。每个时间轴动画对应的数据块都有一个名字,比如 CubeAction 就表明这是 Cube 对象上的一个动画数据块。**Dope Sheet** 编辑器还可以切换为**Action Editor**以编辑时间轴动画数据,包括当前帧的插值,通过修改左侧显示在绿色背景中的插值数据,就只可以自动创建新的关键帧。

还可以使用 **Graph Editor** 曲线动画工具改变插值规律,通过给属性通道添加 F-Curves 函数曲线、修改器,用于设置关键帧间的插值函数。选择好关键帧,通过以下菜单操作就可以改变插值方式:
- 菜单 Key -> Interpolation Mode 选择插值函数类型;
- 侧栏面板 F-Curve - Active Keyframe - Interpolation 设置插值类型和缓动类型 Easing Type。
- 侧栏面板 Modifiers - Add Modifier 添加动画曲线修改器。

将动画数据重现(播放)到指定对象的属性,就可以还原关键帧记录下的状态,并且 animation layering 动画分层概念可以将多个动画在同一个对象上播放,结合 Blend 选项在不同动画层之间按权重计算重叠属性的数据,最终得到一个混合好的动画效果。
Animation 动画节点设计时、运行时类型对照:
骨骼动画涉及的内容比较多,以后再深入探讨,大概操作流程是:
1. 创建与模型相适应的骨骼系统,按关节位置连接骨骼;
2. 将模型与骨骼绑定:通过顶点组管理模型中的顶点与对应骨骼的权重值,以确定每块骨骼对指定顶点的影响程度;
3. 然后调整骨骼状态以改变模型的形态,因为有上一步的绑定操作,模型网格会按权重分配给对应的骷髅进行变形;
4. 蒙皮,将材质赋予模型,使用模型在姿态控制下呈现特定的动画效果;
✨ 逻辑节点原理
逻辑节点的连接关系固定在由生成器输出的逻辑树配置代码中,保存在项目的 bgelogic 目录下,运行游戏时或主动通过逻辑编辑侧栏面板 `Administration -> Compile All` 生成代码。比如,上面代码中的 `ACT001.condition = CON0000` 就是将一个条件节点连接到一个动作节点的 condition 端口上。
注意,在 Apply As Logic Bricks 模式下编译才会生成外部脚本模块,如果是 Component 模式则会内嵌在 Blender 文件,使用自带的脚本编辑器查看。
Blender 提供的节点编辑器最基础的两个组件就是:
1. `bpy.types.NodeSocket` 节点插槽基类,所有节点的输入、输出端口都是插槽类型的实例;
2. `bpy.types.Node` 节点基类,逻辑树中定义的节点之间,通过关联插槽类型到输入、输出端口连接;
UPBGE 逻辑节点实现插件,bge_netlogic 插件代码主要分成四块:
- **uplogic** 逻辑节点运行时的实现,由逻辑节点生成器根据逻辑节点树的节点连接信息生成的代码调用。
- **basicnodes** 逻辑节点编辑器中节点 UI 的实现,最终子类属于 bpy.types.Node 或 NodeSocket。
- **nodeutils** 节点编辑器中的节点分类目录,使用了 `nodeitems_utils` 插件模块。
- **ops** 包括代码生成器,操作组件,bpy.types.Operator,对应逻辑节点编辑器中的按钮等 UI。
每个节点每个端口的设计时代码,basicnodes 目录下定义,主要是提供绘制出相应的图形界面的逻辑。并且向生成的运行时节点类型实现提供连接关系信息数据。
而运行时的实现代码,uplogic 目录下定义的各种 Cell 类型对应逻辑节点,SubCell 对应插槽功能:
1. `ActionCell` 行为节点执行各种动作,比如 `ActionApplyRotation` 旋转指定游戏对象;
2. `ConditionCell` 条件节点根据求值函数输出逻辑条件,供行为节点的条件使用;
3. `ParameterCell` 参数节点主要是向其它节点提供数据;
4. `LogicNetworkSubCell` 插槽类型,也是唯一的运行时插槽类型实现;
`LogicNetworkSubCell` 插槽类型记录了上游节点(owner)和其数据读取 API,`get_value()`方法一般由父类 `get_socket_value()` 方法间接调用。Get Owner 这样的节点用来获取游戏对象,它的运行时实现 `ParamOwnerObject` 通过 `get_owner()` 获取逻辑树上的游戏对象输出给下游。
下游节点连接到一个输出端口,就可以根据端口 owner 属性获取引用上游节点,注意这个 owner 表示 Socket 对象归属的节点,并不是游戏对象。通常一个逻辑节点中 Object 选项有一个 **Use Owner** 图标,激活此选项就表示使用逻辑树当前挂载的游戏对象。

Get Property 这样的节点,运行时实现为 `ParameterObjectProperty`,它的输入端口可以指定场景中的对象,也就是 GameObject,根据不同设置,在生成逻辑树的代码有不同的属性值配置:
1. Object 属性留空,生成代码:`game_object = None`
2. Object 指定列表中的对象,比如场景中的 Plane 对象:`game_object = "NLO:Plane"`
3. Object 从其它节点输入,比如 Get Owner:`game_object = nodes.ParamOwnerObject()`
如果是第二种,可以直接在属性列表中看到这个指定对象的 Game Properties 数据属性列表。但是节点需要激活 **Free Edit** 模式才能自由指定需要访问的属性数据。而在生成代码中,Object 属性值中前缀 `NLO:` 表示它真正需要获取的是一个游戏对象。`get_socket_value()` 方法包含前缀值的处理,会将参数值截掉 'NLO:' 的部分作为对象名称匹配场景中的对象,Scene.objects 保存所有对象的引用。`get_value()` 则没有这个前缀的处理。
还有一种情况,属性值设置为 'NLO:U_O' 则返回 `LogicNetwork` 私有成员 `_owner` 引用的游戏对象,它在生成的逻辑树代码定义在初始化方法中,和传入节点的控制器引用相同的游戏对象。
在逻辑节点执行求值时,还会调用 `is_invalid()` 方法验证属性的名称的有效性,以及判断节点是否还处于等待状态,或者游戏对象本身已经定义了 invalid 属性并且值不为 False,这都是处于无效状态,会导致节点路过求值操作,或者说求值未完成:
编写 GameObject 对象时注意 `property_name not in game_object` 这个属性存在性判断条件。而这个 in 运算符的使用,就涉及多个魔术方法的定义与使用,才能决定一个属性数据是否存在。即就是说,即使用 GmaeObject `getPropertyNames()` 方法可以看到属性的定义,但是通过 not-in 运算符返回的值可能又是表明属性不存在。
Python 脚本中,`obj.attr` 和 `obj['attr']` 两种访问方法涉及了不同执行逻辑,它们大多数情况下都不是指向相同的数据。同时,UPBGE 引擎内还会整合 Game Properties 数据,以及逻辑节点树生成代码中设置 GameObject 属性数据,比如一个名为 **ArchitectureBasic** 的逻辑树就会在生成代码中包含以下游戏对象属性设置:
owner["IGNLTree_ArchitectureBasic"] = network
这些在不同执行阶段混入的数据,如果不清楚什么数据在什么时间可用,无疑会让程序逻辑变得非常复杂,并且可能导致一些怪异的现象。这些在不同执行阶段混入的数据,如果不清楚什么数据在什么时间可用,无疑会让程序逻辑变得非常复杂,并且可能导致一些怪异的现象。比如,在逻辑节点求值方法中,无法通过 `get()` 方法获取游戏对象属性以外的属性值,即那些属性 Python 原生导出的符号。
`getPropertyNames()` 函数导出自 C++ 方法 `PyGetPropertyNames()`,它只是从导出符号中获取列表,在脚本中可能访问不到真实的值。列表中存在的属性,也就是说,`KX_GameObject::sPyget()` 源代码中导出的这个方法是 Game Properties 属性数据获取的专用方法。根据其导出符号用途的宏定义规则,可以知道,这是一个由`EXP_PYMETHOD_VARARGS` 宏函数生成的方法,头文件中可找到相应定义。
Python 2.x 升级到 Python 3.x,所有类型的底层类型系统完全重新设计,旧版本的类型称为旧式类型,新版本的类型,只要是继承自 `object` 或者其子类型,那么就是新式类型,默认都是新式类。旧式类和新式类的区别,old-style vs. new-style,主要体现在多重继承、属性访问以及特殊方法等方面。
在新式类中,一个类可以直接继承自内置类型(比如 list、dict 等),同时也支持使用 super() 函数调用指定父类方法,例如 `super(A, obj).m` 或者 `super().__init__()`。还能够使用 slots 插槽属性限制实例属性的数量,以及使用 `getattribute()` 方法控制属性访问等高级特性。还涉及类型成员解释顺序算法 Method Resolution Order (MRO) 等等。
Python-3.10.2\Doc\library\functions.rst
获取属性数据时,不能在没有实现下标操作方法使用 `obj['attr']` 这样的索引法取值,可以实现`__getitem__` 魔术方法,或者使用 `getattr()` 内置函数。for-in 循环会优先使用 `__iter__` 魔术方法枚举数据,如果没有定义,才使用 `__getitem__`。
数据枚举完成,就应该发出停止检举信息,`StopIteration` 或者 `IndexError` 异常都可以停止循环。如果没有终止信息发出,这就是一个死循环。
另外,if-in 或 not-in 这种带 in 运算符的搭配会触发 `__getitem__` 枚举行为,但是 in 运算符优先使用 `__contains__` 方法,一个布尔值就能解决所查询的数据是否存在的问题。但是 `__getitem__` 方法则需要返回一个值,供 in 运算符进行比较,getitem 不能返回 True 或 False 决定数据是否存在。
Python 作为动态类型语言,它的类型标注只是给人看的,不对于编译器的编译处理,所以以下代码展示了一个代码一致性的反面示范,因为 `__getitem__` 会被多个方法调用,id 不一定是数值。和 `__getitem__` 方法配对的还有 `__setitem__`,用于下标索引方式的赋值操作。
所有设计时实现实现,名称上基本都使用 LN 前缀,而运行时实现则没有这样的前缀,这是代码约定,这是非常好的编码习惯,一方面逻辑更清晰,另外更方便定位。
`Python -> Dictionary -> Init Empty` 节点为例,它创建一个空字典以保存数据。在逻辑节点编辑器中添加此节点,就会执行 `init()` 初始化,添加输入、输出端口,以及相关的端口类型。同时,节点实现类型中还定义了三个方法,分别返回输入、输出端口对应的字段或变量名称,以及实现运行时的类型 `InitEmptyDict(ActionCell)`,所有节点运行时实现都是 `LogicNetworkCell` 子类。
节点树进行运行时,节点得到控件流执行,就会调用求值方法 `evaluate()`,并将数据暂存起来,等待相应的输出端口连接的下游节点调用已经为各个端口注册好的 API 获取暂存数据。这里就是指注册在输出端口 **Dictionary** 的接口函数,此端口对应的变量名是 `DICT`,此值对应节点类型的一个同名成员,也就是在 self.DICT 这个成员上注册的 API `get_dict()`,下游节点需要获取数据时就会根据以上信息调用 `get_dict()`。
Init Empty 与 Init From Item 节点的主要差别在于 `InitEmptyDict` 和 `InitNewDict`
两个实现类型的功能差别。不同节点的这个求值方法有所不同,前者直接创建空字典,`dict = {}`,后者则是根据输入的键值对数据来初始化一个字典,`dict = {str(key): value}`。求值完成后,父类定义的内部函数 `set_ready()` 被执行,告知节点树当前节点已经完成求值。
由于 UPBGE 逻辑节点编程设计思路与 Armory3D 完全不一样,在使用节点时的思维也几乎完全不一样。定时器 Time - Timer `ConditionTimeElapsed` 就是这样一个典型:When Elapsed 持续输出触发信号,当输入 Set Timer 条件时,定时器开始计时,停止输出触发信号。计时到达后,恢复信号。可以使用 `ConditionNot` 返回这个逻辑,即没有输入 Set Timer 时也不输出触发,有信号输入时就在计时这段时间持续输出触发信号。


