- 游戏开发
- 图形学
【免费下载链接】turbulenz_engine
Turbulenz is a modular 3D and 2D game framework for making HTML5 powered games for browsers, desktops and mobile devices.
导读:本文围绕 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):
- 计算相对线速度
deltaV与角速度偏置angBias(按各形状的SHAPE_SWEEP_FACTOR加权); - 迭代循环中,按当前 alpha 推进
_sweepIntegrate,更新形状世界数据,调用_distance求分离距离sep; - 若
sep < SWEEP_LIMIT(0.0005m精度阈值)即判定碰撞;若分离速度方向(proj)不再分离或穿透过深(sep < HALF_LIMIT)则终止; - 否则按
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_EPSILON | 1e-6/1e-12 | 点包含测试的边界浮点容差 |
COLLINEAR_EPSILON/COLLINEAR_SQ_EPSILON | 1e-5/1e-10 | 射线与边共线退化判定(射线测试跳过条件) |
SWEEP_LIMIT | 0.0005 m | 扫掠求交的分离距离精度阈值 |
SWEEP_SLOP | 0.05 m | 连续碰撞允许的穿透容差(负半径) |
MINIMUM_SWEEP_ADVANCE | 1e-6 | 保守推进的最小步进分数 |
MAX_SWEEP_ITER | 50 | 单次扫掠最大子迭代数 |
EQUAL_SQ_VEL/ZERO_ANG_BIAS | 0.2 (m/s)²/0.02 m·rad/s | 动态扫掠中"相对速度可忽略"对的过滤条件 |
性能上值得注意两点:
- 无状态复用:
signedDistance、sweepTest复用同一个_toi内部缓冲,调用方需要自行保存返回值与输出数组,避免在两次调用之间持有内部数据引用; - 无需 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.
相关推荐
Turbulenz 引擎 Broadphase 对象完全指南:从 Sweep-and-Prune 到 BoxTree 的宽阶段碰撞检测
Turbulenz 引擎 Broadphase 对象完全指南:从 Sweep and Prune 到 BoxTree 的宽阶段碰撞检测 Broadphase(宽
游戏开发图形学Turbulenz 引擎 OcclusionQuery 对象详解:GPU 遮挡查询原理与延迟渲染光源裁剪实战
Turbulenz 引擎 OcclusionQuery 对象详解:GPU 遮挡查询原理与延迟渲染光源裁剪实战 OcclusionQuery(遮挡查询)是 Tur
游戏开发图形学Box2D物理引擎中的碰撞检测系统详解
Box2D物理引擎中的碰撞检测系统详解 前言 Box2D作为一款优秀的2D物理引擎,其核心功能之一就是高效的碰撞检测系统。本文将深入解析Box2D中的碰撞检测机
物理引擎游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考