技术博客

碰撞层与掩码:32 位位运算的设计

第 66 / 149 篇 · 2025-08-01 · godot · 物理引擎位运算性能优化

这是物理分类里最"简单"的一个机制,也是误用率最高的。collision_layer 和 collision_mask 在编辑器里是 32 个复选框,在代码里就是两个 uint32_t。先把语义钉死,再谈它背后真正的性能账。

语义:我是谁 vs 我看谁

  • collision_layer:我是谁——body 占据的层,一位一个层;
  • collision_mask:我看谁——扫描哪些层上的对象。

两 body $A$、$B$ 发生碰撞,判定是:

$$ (A.\text{mask} \wedge B.\text{layer}) \neq 0 \;\;\wedge\;\; (B.\text{mask} \wedge A.\text{layer}) \neq 0 $$

双向都过才算碰撞。这个非对称结构是个被低估的特性:把 $A$ 的 mask 设为只包含 $B$ 的层、而 $B$ 的 mask 不含 $A$ 的层,就得到单向检测——子弹只打敌人不碰队友、探测 Area 只感知玩家而不被玩家感知。做单向门、触发区时这是标准手法,不用写任何特殊代码。

$Hitbox.set_collision_layer_value(4, true)   # 我占第 4 层
$Hitbox.set_collision_mask_value(3, true)     # 我看第 3 层(敌人)

Area3D 的监测(body_entered 等信号)走同一套判定;射线查询的 PhysicsRayQueryParameters3D.collision_mask 是单向版——只查你指定的层。

性能真相:AND 不要钱,候选对要钱

先说结论:这对 32 位 AND 加分支的判断本身约 1 纳秒,在 broad phase 逐 pair 调用上百万次也无感。layer/mask 的性能意义不在判断本身,而在它能决定哪些 pair 根本不被生成。

Broad phase 每帧产出的是"AABB 有交"的候选 pair,n 个物体、随机分布时期望候选数约为:

$$ E[\text{pairs}] \approx \binom{n}{2} \cdot p_{\text{overlap}}^2 $$

$p_{\text{overlap}}$ 是单对 AABB 交叠概率,随场景密度上升。注意这个数和分层一点关系都没有——只要 AABB 交了,候选就产生了,layer/mask 只是把它过滤掉。把 200 个静态环境物、50 个敌人全放在第 1 层,它们之间每帧互相产生 $\binom{250}{2} \approx 3.1$ 万次 AABB 交测试的候选;给静态物单独一层、敌人不再扫描它(多数敌人物理上确实不需要感知环境细节),候选数直接砍掉一大块。分层的收益是减候选,不是加速判断。

在 Jolt 下还有第二层收益:BroadPhaseLayer 映射(第 4 篇)把静态/动态分入不同的树,静态-静态 pair 在树遍历阶段就不生成,连候选都没有。Godot Physics 没有这层抽象,只能靠 layer/mask 在生成后过滤——这也是大规模静态场景 Jolt 领先的另一笔小账。

为什么是位,而不是标签或列表

热路径上的过滤器,设计约束是"每次判断必须零分配、可预测、可向量化"。位运算三个全占:两个寄存器一次 AND;而字符串标签要哈希查找,列表要遍历,pair 数上来就是实 hurt。此外位图可以无损映射进任何物理引擎的过滤体系——Jolt 的 ObjectLayer、PhysX 的 word0/word1 都是同样的 32 位思路,迁移时语义不丢。

代价是表达力:32 层封顶、层与层之间没有"继承"(第 4 层自动含第 1~3 层这种关系要手工展开成多个 mask 位)。对游戏场景的耦合分类(世界/玩家/敌人/触发区/投射物……),32 位绰绰有余;真不够用,说明分类该重构而不是加层。

一个实务分配模板

层 occupants 扫描(mask)
1 静态环境 只扫 2、3(玩家和敌人的落地需求)甚至不扫
2 玩家 1、3、4、5
3 敌人 1、2、4、5
4 投射物/打击盒 1、3
5 触发 Area 2

原则两条:静态物能不扫就不扫(它们是候选对的最大来源);投射物单向打(mask 只含目标层,省掉投射物互撞的求解)。

排查时对着这条看:物理帧耗时高 → profiler 里 broad phase / narrow phase 占比高 → 数场景里多少 body 共享同一个 layer——九成的大规模场景物理问题,最后都收在这张表里。

layer / mask:一对 32 位整数与双向 AND A.mask B.layer AND ≠ 0:单向通过,反向再查一次 collide(A, B) = (A.mask & B.layer) && (B.mask & A.layer) 单次判断约 1 ns,热路径无感 真成本在 broadphase 候选 pair 的数量 非对称用法:A 只检测 B、B 不检测 A——单向传感器、投射物互不碰撞 全挤第 1 层 = 候选对逼近 O(n²):分层减候选,Jolt 的 BroadPhaseLayer 让整棵子树不生成候选