这是物理分类里最"简单"的一个机制,也是误用率最高的。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——九成的大规模场景物理问题,最后都收在这张表里。