news 2026/10/23 8:49:09

Turbulenz 引擎 CollisionUtils 对象详解:2D 碰撞检测查询与源码级原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Turbulenz 引擎 CollisionUtils 对象详解:2D 碰撞检测查询与源码级原理
  • 游戏开发
  • 图形学

【免费下载链接】turbulenz_engine

Turbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.

项目地址:https://gitcode.com/gh_mirrors/tu/turbulenz_engine
点击查看免费下载

导读:本文围绕 Turbulenz 开源 3D/2D 游戏引擎的CollisionUtils对象展开,它是 Physics2D 模块提供的独立碰撞检测工具接口,可在不创建物理世界的情况下直接完成点包含测试、形状相交判定、有向距离查询、射线投射与扫掠求交(Time of Impact)等几何查询。读完本文,你将掌握containsPoint、signedDistance、intersects、rayTest、sweepTest五个方法的完整用法、参数语义与返回值约定,并通过 tslib/physics2ddevice.ts 的源码理解其底层算法(分离轴测试、Voronoi 区域判定、保守推进法)与精度阈值配置。

一、CollisionUtils 是什么

CollisionUtils对象是 Turbulenz 引擎 Physics2D 模块对碰撞检测例程(collision detection routines)的统一封装接口。根据官方 API 文档 physics2d_collisionutils_api.rst 的定位说明:

"The CollisionUtils object provides an interface to the Physics2D collision detection routines, this object can be constructed seperately and used without any physics."

即:它可以被单独构造,并在完全不使用物理仿真(World、约束、重力)的情况下独立运行。这意味着你可以在 UI 点击判定、编辑器拾取、寻路预处理等场景中,仅借助几何形状与刚体姿态完成高效的碰撞查询,而不必付出物理模拟的开销。

在源码中,该对象对应 tslib/physics2ddevice.ts 中class Physics2DCollisionUtils(定义于第 12550 行附近,注释为 "Physics2D internal collision detection routines.")。它内部持有一个Physics2DTOIEvent(TOI 即 Time of Impact)事件缓冲,用于在距离查询与扫掠求交时复用临时数据,避免每次查询都产生垃圾回收压力。

构造方式

CollisionUtils对象通过Physics2DDevice.createCollisionUtils()创建,对应源码:

// tslib/physics2ddevice.ts createCollisionUtils(): Physics2DCollisionUtils { return Physics2DCollisionUtils.create(); }

create()内部为实例分配并初始化了一个Physics2DTOIEvent(见Physics2DCollisionUtils.create()的静态工厂实现)。典型用法:

var phys2D = ...; // 已初始化并获取的 Physics2DDevice var collisionUtils = phys2D.createCollisionUtils();

方法总览

方法功能返回值
containsPoint(shape, point)判断世界坐标点是否在形状内boolean
signedDistance(shapeA, shapeB, witnessA, witnessB, axis)两形状间的有向距离、见证点与分离轴number(有符号距离)
intersects(shapeA, shapeB)判断两形状是否相交boolean
rayTest(shape, ray, normal, ignoreInnerSurfaces)参数化射线与形状求交number(factor)或undefined
sweepTest(shapeA, shapeB, deltaTime, point, normal)扫掠求时间、碰撞点与法线number(TOI)或undefined

二、使用前的准备:RigidBody 与 Shape

文档中所有方法的shape参数都强调同一个前提:

"This shape must be assigned to a RigidBody object to define it's position and rotation in world coordinates."

这是因为碰撞检测需要形状在世界坐标系中的姿态,而这一姿态由所属刚体(RigidBody)的位置与旋转决定。源码中每个公开方法的第一步都是shape.body._update()(或shapeA.body._update()、shapeB.body._update()),用于同步刚体姿态到形状的世界数据(如圆形的世界圆心CIRCLE_WORLD、多边形的世界法线与世界顶点POLY_WNORMAL、POLY_WORLD等),随后才执行底层几何测试。

形状 Shape 的构造参数

Shape 对象通过Physics2DDevice.createCircleShape()与Physics2DDevice.createPolygonShape()创建,常用参数如下(完整说明见 physics2d_shape_api.rst):

{ sensor : false, // 可选,true 时只做碰撞检测、不生成接触点 material : mat, // 可选,材质对象,默认 getDefaultMaterial() group : 1, // 可选,分组位掩码,默认 1 mask : 0xffffffff, // 可选,掩码位掩码,默认 0xffffffff userData : null // 可选,任意自定义数据 }

两形状是否产生交互遵循(a.group & b.mask !== 0) && (b.group & a.mask !== 0)的规则。

刚体 RigidBody 的三种类型

RigidBody 分为三类(详见 physics2d_body_api.rst):

  • 'static':加入 World 后不可再变更(位置、形状等)的静态刚体;
  • 'kinematic':允许移动,但应通过修改速度来驱动(直接改位置等价于瞬移);
  • 'dynamic':具有非无限质量/惯量,受重力、碰撞与约束冲量影响。

即便你的场景只用 CollisionUtils 做纯几何查询,也仍需为每个形状关联一个 RigidBody 以承载其世界变换。

坐标与单位约定

Physics2D 采用 SI 单位(米 m、弧度 rad、秒 s、千克 kg、牛顿 N),坐标系 y 轴向下。官方建议最小物体尺寸不低于0.001m = 1mm,且为稳定性考虑,位置接触允许0.01m = 1cm的 slop(间隙容差),因此推荐物体至少1cm大小;若游戏以像素(px)为单位,可采用约60px对应1m的缩放系数(见 physics2ddevice_api.rst 顶部说明)。这些量纲约束同样适用于 CollisionUtils 的查询结果。

三、containsPoint:点包含测试

Summary:判断给定世界坐标点是否包含在给定 Shape 内。

Syntax:

var result = collisionUtils.containsPoint(shape, point);
  • shape:要测试包含关系的形状,必须已关联 RigidBody 以定义其世界坐标下的位置与旋转;
  • point:要测试的点(二维数组形式,如[x, y])。

返回true表示点在形状内(含边界),否则为false。

源码实现(tslib/physics2ddevice.ts 第 12557 行起):

containsPoint(shape: Physics2DShape, point: any /*v2*/): boolean { shape.body._update(); return this._contains(shape, point[0], point[1]); }

底层_contains按形状类型分派:

  • 圆形:直接比较点到圆心的距离平方与半径平方,即(dx*dx + dy*dy - rad*rad) <= CONTAINS_SQ_EPSILON,其中CONTAINS_SQ_EPSILON = 1e-12,允许边界上的浮点误差;
  • 多边形:对每条边计算世界法线与点的投影proj = wnormal · p - wproj,只要任意一条边满足proj > CONTAINS_EPSILON(1e-6)即判定点在多边形外侧;全部通过则点在内部。这正是"点在所有半平面内侧"的凸多边形判定。

从源码注释可知,_contains不执行 AABB 粗测("no AABB check is performed"),因此对单个形状的点测试是精确的细分几何测试。

典型应用:鼠标/触摸点击拾取(点击坐标落在某个可交互物体上)、编辑器中拖拽框选命中检测。

四、signedDistance:有向距离与最小平移向量

Summary:确定两个 Shape 之间的有向距离,同时输出两形状上的见证点(witness points)与分离轴(axis)。

Syntax:

var witnessA = []; var witnessB = []; var axis = []; var distance = collisionUtils.signedDistance(shapeA, shapeB, witnessA, witnessB, axis);
  • shapeA/shapeB:参与距离查询的两个形状,均须关联 RigidBody;
  • witnessA:输出数组,存储 shapeA 上见证点的世界坐标;
  • witnessB:输出数组,存储 shapeB 上见证点的世界坐标;
  • axis:输出数组,存储两形状间的轴(世界坐标),该轴长度恒为单位长度。

返回值为有符号距离:

  • 正值(分离状态):见证点即两形状的最近点,axis为从 shapeA 指向 shapeB 的分离轴;
  • 负值(穿透状态):见证点与轴对应于两形状的 MTV(Minimum Translational Vector,最小平移向量),方向同样为从 shapeA 到 shapeB。此时以该轴乘以距离绝对值平移其中一个形状即可分离两者。

源码(第 12563 行起)先更新两形状的刚体姿态(若两形状同属一个刚体则只更新一次),再调用this._distance(shapeA, shapeB, data)将结果写入 TOI 事件的数据缓冲,最后把轴与两个见证点从内部缓冲拷贝到调用方传入的数组中。

底层_distance按形状组合分派四种实现(tslib/physics2ddevice.ts 第 13075 行起):

  • 圆-圆:连线方向归一化得轴,两端沿轴缩进各自半径得见证点,距离 = 圆心距 - 半径和;若圆心重合则退化处理为轴(1, 0);
  • 圆-多边形:先沿多边形各边世界法线求"法线投影 - 半径 - 边支撑距离"的最大值,再用Voronoi 区域(边 Voronoi region)判定判断圆心投影落在边的哪一区——落在边投影区间内时取边法线作轴,否则退化为顶点-圆心检查;
  • 多边形-多边形:凸多边形间的最近距离算法,逐边计算支撑投影并维护最小距离;
  • 圆-多边形(多边形在前时):交换两侧结果后反转轴、交换见证点,保证结果始终以 shapeA 为基准方向。

典型应用:碰撞分辨率前的穿透深度计算、AI 避障中探测与障碍物的间隙、物理编辑器中对齐辅助。

五、intersects:相交判定

Summary:判断两个 Shape 是否相交。

Syntax:

var result = collisionUtils.intersects(shapeA, shapeB);
  • shapeA/shapeB:要测试相交的两个形状,均须关联 RigidBody。

返回true表示相交(含恰好接触),否则为false。

源码(第 12584 行起)同样先同步两形状姿态(同一刚体只同步一次),随后调用this._test(shapeA, shapeB)。底层_test(第 13967 行起)实现经典分离轴定理(SAT):

  • 圆-圆:比较圆心距平方与半径和平方,(dx*dx + dy*dy) <= (rSum*rSum);
  • 圆-多边形:逐边做半平面分离测试(任一dist > 0立即返回 false),再对最大穿透边做 Voronoi 区域与顶点距离检查;
  • 多边形-多边形:对 shapeA 的每条边法线,将 shapeB 的全部顶点投影到该轴上取最小值,只要存在一条分离轴(min 投影大于阈值)即判定不相交。

该测试本身不执行 AABB 粗测,但在 World 场景中,broadphase 层(如 BoxTreeBroadphase)会先用包围盒粗筛,再对候选对调用_test(见 tslib/physics2ddevice.ts 第 10455 行附近this._collisions._test(sa, sb)的调用链),因此性能可控。

典型应用:触发器(sensor)区域重叠检测、碰撞后逻辑判定(不同于物理响应,只关心"是否重叠")。

六、rayTest:参数化射线求交

Summary:确定给定 Shape 与参数化射线的交点。

Syntax:

var normal = []; var ray = { origin : [-1, 0], direction : [10, 0], maxFactor : 1 }; var ignoreInnerSurfaces = false; var factor = collisionUtils.rayTest(shape, ray, normal, ignoreInnerSurfaces);
  • shape:与射线求交的形状,须关联 RigidBody;
  • ray:参数化射线对象,包含三个字段:
    • origin:射线起点(二维数组);
    • direction:射线方向(二维数组);
    • maxFactor:射线投掷的最大参数上限,射线不会超过 maxFactor 继续投射;
  • normal:输出数组,存储交点处的法线;
  • ignoreInnerSurfaces(可选):为true时不与形状内表面求交;默认false。

返回值:

  • 未发生相交时返回undefined;
  • 发生相交时返回交点处的factor(参数 t),可用如下公式还原交点坐标:
mathDevice.v2AddScalarMul(ray.origin, ray.direction, factor);

其中v2AddScalarMul(a, b, c)计算a + b*c(实现见 tslib/vmath.ts 第 546 行及 tslib/webgl/mathdevice.ts 第 274 行的设备封装),也即intersection = origin + direction * factor。

源码分派(第 12595 行起):rayTest更新形状姿态后调用私有_rayTest,按形状类型选择_rayTestPolygon(第 12660 行起)或_rayTestCircle(第 12720 行起):

  • 多边形射线测试:遍历多边形每条边,利用世界法线 n 计算den = n·direction;当den >= 0 && noInner(射线背向该边法线且要求忽略内表面)或|den|² < COLLINEAR_SQ_EPSILON(共线退化,COLLINEAR_EPSILON = 1e-5)时跳过该边;否则计算t = (wproj - n·origin) / den,再通过dproj与POLY_CROSS1/POLY_CROSS2判断命中点是否落在边的投影区间内,取满足0 <= t < maxFactor的最小 t 为结果,法线方向由命中的是外表面(inner = den >= 0时的反号)决定;
  • 圆形射线测试:将射线方程代入圆的二次方程at² + bt + c = 0(a = |d|²、b = 2·(oc·d)、c = |oc|² - r²),判别式小于 0 返回undefined;否则取最近根distance = (-b - sqrt(D)) / (2a),若最近根为负(射线起点在圆内)且noInner为 true 则返回undefined,否则回退到远根并把法线反向,最终满足0 <= distance < maxFactor时返回 distance 并把圆心指向命中点的单位向量写入 normal。

典型应用:射击游戏的激光/弹道判定、塔防瞄准线阻挡检测、地形鼠标悬停拾取(结合physics2ddevice_api.rst中 World 级rayCast对 broadphase 的封装,可一次性对候选形状批量投射,见 tslib/physics2ddevice.ts 第 10168 行起)。

七、sweepTest:扫掠求时间(Time of Impact)

Summary:确定两个 Shape 在给定时间内扫掠移动时的碰撞时刻,同时输出碰撞点与碰撞法线。

Syntax:

var point = []; var normal = []; var timeOfImpact = collisionUtils.sweepTest(shapeA, shapeB, deltaTime, point, normal);
  • shapeA/shapeB:参与扫掠的两个形状,须关联 RigidBody,扫掠运动由各自刚体的速度决定(刚体的线速度与角速度会被用于推进扫掠);
  • deltaTime:扫掠的时间跨度(秒),必须严格为正;超过该时间仍未碰撞则判定失败返回;
  • point:输出数组,存储世界坐标系下的碰撞点;
  • normal:输出数组,存储世界坐标系下的碰撞法线,恒为单位长度,且方向从 shapeA 指向 shapeB。

返回值:

  • 给定时间帧内未发生碰撞时返回undefined;
  • 否则返回time of impact(碰撞时刻),即[0, deltaTime]内的具体时间值。

源码实现(第 12601 行起):

sweepTest(shapeA: Physics2DShape, shapeB: Physics2DShape, deltaTime: number, point: any /*v2*/, normal: any /*v2*/): number { var toi = this._toi; toi.shapeA = shapeA; toi.shapeB = shapeB; // ... 记录刚体速度与角速度到 BODY_SWEEP_* 数据槽 var ret = this._dynamicSweep(toi, deltaTime, 0, true); // ... 扫掠结束后将刚体恢复到初始位姿 if (ret < 0) { return undefined; } // 碰撞点取两见证点的中点,法线取 TOI 轴 return (ret * deltaTime); }

底层_dynamicSweep(第 12829 行起)实现保守推进法(Conservative Advancement):

  1. 计算相对线速度deltaV与角速度偏置angBias(按各形状的SHAPE_SWEEP_FACTOR加权);
  2. 迭代循环中,按当前 alpha 推进_sweepIntegrate,更新形状世界数据,调用_distance求分离距离sep;
  3. 若sep < SWEEP_LIMIT(0.0005m精度阈值)即判定碰撞;若分离速度方向(proj)不再分离或穿透过深(sep < HALF_LIMIT)则终止;
  4. 否则按delta = sep / (angBias - dot)计算推进量,并用MINIMUM_SWEEP_ADVANCE(1e-6)保证最小推进、以MAX_SWEEP_ITER(50)限制最大迭代次数,防止旋转中的边平行退化导致死循环或隧穿。

World 的连续碰撞检测(CCD)正是复用同一套例程:动态-静态对走_staticSweep(第 12967 行起),动态-动态对走_dynamicSweep(第 10819 行起),并传入Physics2DConfig.SWEEP_SLOP(0.05m)作为负半径容差。也就是说,sweepTest 暴露的正是物理仿真内部用于防隧穿的同一套 TOI 算法。

典型应用:高速子弹与薄墙壁的防穿透判定、角色移动中的落地时间预估、物理游戏中对"下一帧是否会撞上"的预判逻辑。

八、精度阈值与性能特征(源码级补充)

Physics2DCollisionUtils的全部查询都依赖Physics2DConfig中集中定义的数值阈值(见 tslib/physics2ddevice.ts 顶部配置区),了解它们有助于把握结果精度与适用范围:

配置常量默认值作用
CONTAINS_EPSILON/CONTAINS_SQ_EPSILON1e-6/1e-12点包含测试的边界浮点容差
COLLINEAR_EPSILON/COLLINEAR_SQ_EPSILON1e-5/1e-10射线与边共线退化判定(射线测试跳过条件)
SWEEP_LIMIT0.0005 m扫掠求交的分离距离精度阈值
SWEEP_SLOP0.05 m连续碰撞允许的穿透容差(负半径)
MINIMUM_SWEEP_ADVANCE1e-6保守推进的最小步进分数
MAX_SWEEP_ITER50单次扫掠最大子迭代数
EQUAL_SQ_VEL/ZERO_ANG_BIAS0.2 (m/s)²/0.02 m·rad/s动态扫掠中"相对速度可忽略"对的过滤条件

性能上值得注意两点:

  1. 无状态复用:signedDistance、sweepTest复用同一个_toi内部缓冲,调用方需要自行保存返回值与输出数组,避免在两次调用之间持有内部数据引用;
  2. 无需 World 即可运行:所有方法都只依赖形状与刚体的数据,不经过 broadphase 粗筛——这既是优势(轻量、精确),也意味着对"多形状批量查询"场景,应优先使用 World/Device 级 API(如rayCast、convexCast、采样器),让 BoxTreeBroadphase 先做 AABB 粗筛。

九、典型组合用法示例

以"点击拾取 + 射击判定"综合场景为例,串联本文五个方法:

// 1. 构造 CollisionUtils(可脱离 World 使用) var collisionUtils = phys2D.createCollisionUtils(); // 2. 点击点包含测试:判断屏幕坐标是否落在角色形状内 var hit = collisionUtils.containsPoint(playerShape, [mouseWorldX, mouseWorldY]); // 3. 相交判定:技能范围圈与目标是否重叠 var inRange = collisionUtils.intersects(rangeCircleShape, targetPolygonShape); // 4. 有向距离:目标与障碍物之间还有多少间隙(正值=间隙,负值=穿透深度) var witnessA = [], witnessB = [], axis = []; var dist = collisionUtils.signedDistance(targetShape, obstacleShape, witnessA, witnessB, axis); // 5. 射线求交:子弹路径上是否命中敌人,并取交点 var ray = { origin : [gunX, gunY], direction : [aimX, aimY], maxFactor : 100 }; var normal = []; var factor = collisionUtils.rayTest(enemyShape, ray, normal, false); if (factor !== undefined) { var hitPoint = mathDevice.v2AddScalarMul(ray.origin, ray.direction, factor); } // 6. 扫掠求交:预判高速物体在 deltaTime 内是否撞击 var point = [], normal = []; var toi = collisionUtils.sweepTest(bulletShape, wallShape, dt, point, normal); if (toi !== undefined) { // toi 即为 [0, dt] 内的碰撞时刻,point 为碰撞点 }

十、总结

CollisionUtils是 Turbulenz Physics2D 模块对外暴露的纯碰撞查询工具箱:它把点包含、相交判定、有向距离/MTV、射线求交与扫掠求交五类几何运算从物理仿真中解耦出来,让开发者可以在不搭建 World、不运行模拟的情况下直接获得引擎级的精确判定结果。从源码(tslib/physics2ddevice.ts 的Physics2DCollisionUtils类)可以看出,其底层实现与物理世界内部使用的例程完全一致——SAT 分离轴测试、Voronoi 区域判定、保守推进扫掠——并共享同一套Physics2DConfig精度阈值。因此,无论用于点击拾取、射线攻击,还是连续碰撞预判,它都能提供与物理仿真同源、可独立部署的高质量碰撞检测能力。更多相关对象(Shape、RigidBody、World、Broadphase)的完整说明可继续查阅 physics2d 系列 API 文档 目录下的对应章节。

  • 游戏开发
  • 图形学

【免费下载链接】turbulenz_engine

Turbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.

项目地址:https://gitcode.com/gh_mirrors/tu/turbulenz_engine
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 8:48:29

Python 3.10模式匹配实战:从基础语法到高级应用

1. Python模式匹配入门指南第一次在Python 3.10的更新日志里看到match-case语法时&#xff0c;我的反应和大多数Python老手一样&#xff1a;"这不就是其他语言里的switch-case吗&#xff1f;"但当我真正开始使用后才发现&#xff0c;Python的模式匹配远不止简单的条件…

作者头像 李华
网站建设 2026/10/22 11:08:10

Java读取Word并上传数据库:解析、存储与避坑指南

简介&#xff1a;面向需要实现 Word 文档内容自动入库的 Java 开发者&#xff0c;资源围绕 Apache POI 解析 Word 与 JDBC 批量写入数据库展开&#xff0c;覆盖 XWPFDocument 读取文本和表格、数据转换、PreparedStatement 防注入及事务处理等关键环节&#xff0c;适合有一定 J…

作者头像 李华
网站建设 2026/10/23 8:48:29

STM32普通IO口模拟串口实战:定时器中断实现软UART

简介&#xff1a;针对项目串口外设数量不足、又需要多路UART通信的嵌入式场景&#xff0c;这套基于STM32F407VET6的工程资源给出了普通IO口模拟串口的完整实现方案。初始化部分由CubeMx自动生成&#xff0c;作者独立完成核心功能逻辑&#xff1a;两个定时器分别用于发送延时和产…

作者头像 李华
网站建设 2026/10/22 9:49:19

FANUC 0i-MF操作手册核心要点:从安全须知到固定循环与故障排查

简介&#xff1a;FANUC 0i-MF操作手册是面向数控机床操作人员、PLC与智能制造领域从业者的加工中心操作指南&#xff0c;覆盖FANUC Series 0i-MODEL F系统的安全须知、接口与传输、NC程序编程、控制面板操作及日常维护等核心模块。文档特别强调安全分级概念&#xff0c;清晰区分…

作者头像 李华
网站建设 2026/10/22 16:32:20

YOLOv5斗地主牌面识别:数据增强、训练调优与安卓NCNN部署全攻略

简介&#xff1a;面向具备一定深度学习基础、希望快速落地扑克牌识别功能的开发者&#xff0c;这套斗地主牌面识别方案提供了从YOLOv5模型训练到安卓端NCNN部署的完整实现。方案核心是基于YOLOv5训练的高精度检测模型&#xff0c;可自动定位游戏截图中的每张牌面并识别数字和花…

作者头像 李华
网站建设 2026/10/22 18:27:07

Railway上部署小龙虾-OpenClaw:Docker与配置避坑指南

部署开源智能助理这事&#xff0c;我一直觉得最烦人的不是代码&#xff0c;而是环境。好好的项目&#xff0c;本地跑得飞快&#xff0c;一上云端就各种崩&#xff0c;数据库连不上、镜像拉不下来、日志刷屏看半天不知道错在哪。所以当我看到“小龙虾-OpenClaw”这个项目的时候&…

作者头像 李华