欢迎光临散文网 会员登陆 & 注册

UPBGE Logic Nodes (源代码分析)

2023-04-24 10:03 作者:紧果呗  | 我要投稿

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

射线投身节点通用属性说明:

  1. Distance 指定射线有效距离,在此距离内的物理对象才可能被拾取。

  2. Property 指定一个名称,只有设置了相应 Game Properties 的对象才可能被拾取。

  3. 启用 X-Ray 可以拾取被遮挡的有效目标,即设置了 Property 中指定的属性的游戏对象。

Game Properties and Logic Nodes


**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** 节点可以获取全屏幕的像素大小,但是不能直接用来将鼠标坐标转换成相机画面中的世界尺寸。画面的大小与相机 lensfov 等参数密切相关。代码中乘 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 ObjectHead 的旋转角度,如果没有指定 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 -> TransformationApply 类型施加物理参数,配合 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` 按网格体生成导航路径;

Nurbs Path - Convert to Mesh 间接创建导航网格


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

Move To with Navmesh 导航逻辑节点演示


生成导航网格后,直接在**Move To with Navmesh**中的 Navmesh Object 属性列表中选择指定,然后 Destination 指定导航目标位置,勾选 Visualize 可以运行时看到一条红线指示导航路径。变换逻辑节点设计时、运行时类型对照:


Animation 动画节点分为三类,Timeline、Armature、Constraints,功能参考:

  1. **Animation Status** 获取指定对象上的动画播放状态,输出两个控制流和 Action Name/Frame 等。

  2. **Play Animation** 在指定对象上播放指定时间轴动画数据,可以指定帧区间 Start/End,速度等等。

  3. **Set Animation Frame** 将指定对象的动画播放状态移动到指定 Frame/Layer。

  4. **Stop Animation** 停止在指定对象、以及指定动画层上播放动画。

**Set Animation Frame** 使用 Freeze 模式,要在停止状态下才有效,会将动画“冰冻”在指定帧位置,此时执行播放命令无效,需要先停止动画,解除冰冻状态才可以继续播放。

Timeline Animation

时间轴动画,就是记录在以帧为单位的属性数据的重放到指定对象上。例如,最简单的位移动画,数据记录的是 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 (关键设置工具)

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

Dope Sheet 动画编辑器


还可以使用 **Graph Editor** 曲线动画工具改变插值规律,通过给属性通道添加 F-Curves 函数曲线、修改器,用于设置关键帧间的插值函数。选择好关键帧,通过以下菜单操作就可以改变插值方式:

- 菜单 Key -> Interpolation Mode 选择插值函数类型;

- 侧栏面板 F-Curve - Active Keyframe - Interpolation 设置插值类型和缓动类型 Easing Type。

- 侧栏面板 Modifiers - Add Modifier 添加动画曲线修改器。

F-Curve 曲线动画工具

将动画数据重现(播放)到指定对象的属性,就可以还原关键帧记录下的状态,并且 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** 图标,激活此选项就表示使用逻辑树当前挂载的游戏对象。

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 EmptyInit From Item 节点的主要差别在于 `InitEmptyDict` 和 `InitNewDict`

两个实现类型的功能差别。不同节点的这个求值方法有所不同,前者直接创建空字典,`dict = {}`,后者则是根据输入的键值对数据来初始化一个字典,`dict = {str(key): value}`。求值完成后,父类定义的内部函数 `set_ready()` 被执行,告知节点树当前节点已经完成求值。


由于 UPBGE 逻辑节点编程设计思路与 Armory3D 完全不一样,在使用节点时的思维也几乎完全不一样。定时器 Time - Timer `ConditionTimeElapsed` 就是这样一个典型:When Elapsed 持续输出触发信号,当输入 Set Timer 条件时,定时器开始计时,停止输出触发信号。计时到达后,恢复信号。可以使用 `ConditionNot` 返回这个逻辑,即没有输入 Set Timer 时也不输出触发,有信号输入时就在计时这段时间持续输出触发信号。


UPBGE Logic Nodes (源代码分析)的评论 (共 条)

分享到微博请遵守国家法律