技术博客

Area3D 的重叠检测与回调时机:body_entered 在物理帧的哪个阶段发出

第 73 / 149 篇 · 2025-07-27 · godot · 物理引擎信号系统源码分析

拾取范围、触发器、伤害判定区——Area3D 大概是使用频率仅次于 CharacterBody3D 的物理节点。它不求解碰撞(东西可以随便穿过),只负责回答一个问题:谁现在和我重叠了。但"谁进来了"这个信号什么时候发、以什么顺序发,直接影响你怎么写拾取和触发逻辑。这篇从源码里把 Area 的监控机制拆开。

重叠对的维护:body 侧与 area 侧双向记账

Area 检测重叠的机制建立在 broad phase 之上:Area 和 Body 一样有 AABB,被放进同一个 GodotHashGrid,pair 系统为每个"可能重叠的 body-area 对"创建 AreaPair3D(servers/physics_3d/godot_area_3d.cpp、godot_area_pair_3D.cpp)。窄相确认真正重叠后,这个 pair 进入两个对象各自的重叠列表:

  • body 维护 areas 列表(我压在哪些 area 上);
  • area 维护 body_map / area_map(我体内有哪些 body / area)。

两边都记是因为查询方向不同:body.get_overlapping_areas() 走前者,area.get_overlapping_bodies() 走后者。这份列表是增量维护的——pair 建立时插入,分离时删除,不存在"每帧全量扫一遍"的做法,所以一千个 area 叠在一起的成本主要在新 pair 的建立/销毁,不在持续重叠。

monitoring 和 monitorable 两个开关控制的是不同方向的"是否参与记账":monitoring=false 的 area 不再跟踪别人进来(不发信号、不维护 map),但别人仍然能看到它;monitorable=false 反过来,别的 body 的 get_overlapping_areas() 里找不到它,但它自己照常监控别人。文档里那句"Area can detect others but can't be detected"说的就是 monitorable=false。做"隐形的触发器"时两个开关的语义差别就是这个。

回调时机:为什么是 flush 阶段而不是 step 中

这是最容易写错的地方。直觉上"两个物体一重叠就应该立刻发信号",但引擎不会在 solver 迭代中间发信号——那会让脚本回调看到一个进行了一半的物理世界(第 8 篇说过 space 在步进中是锁定的)。

实际的时机在物理步进结束后的 flush queries 阶段(Space3D::flush_queries,每个物理步后由物理线程触发,脚本侧看到的效果是在 _physics_process 之后、渲染之前)。流程是:

物理步进(integrate → solve → island 解算)
  ↓ 此时 Area 只更新重叠列表,不发信号
flush_queries:
  遍历所有"状态变了"的 area pair
  对新增重叠:调 area 的 _body_in → emit body_entered
  对消失重叠:调 area 的 _body_out → emit body_exited

也就是说,body_entered 信号是在物理步进彻底完成后批量发的。同一个物理步内发生"进入又离开"(高速物体一帧穿过 area),列表净变化为零,你可能一个信号都收不到——这是采样问题,和 CCD(第 6 篇)同源:area 没有厚度,穿过时间小于一个物理步就漏检。解决办法是把 area 的形状做大、把物理帧率提高,或者给判定目标换用射线检测。

还有一个细节:body_entered 信号携带的参数是 body 的 RID,映射到节点在场景树层的 _bind 代码里转成 Node3D 指针发出去。如果你在回调里立刻查 overlaps_body(),拿到的列表是 flush 完成后的状态,自洽;但如果你给信号接了多个处理函数,它们的执行顺序按 connect 顺序,中间没有插入其他 flush 的机会——别在回调里假设"别人还没收到这个信号"。

使用模式的取舍

Area 的 gravity、linear_damp 等场效属性(area 作为"力场")是在 integrator 阶段被 body 读取的:body 积分时查自己 areas 列表里的场参数叠加进加速度。这意味着力场效果不需要信号——把 monitoring 关掉,场照样生效(monitoring 只管信号和查询)。做风区、水域、传送带时这是免费的优化。

回调 vs 轮询的选择也有讲究:少量 area、关心进入/离开的精确时机,用信号;每个物理步都要问"谁在我里面"且数量多(技能范围扫描),直接 get_overlapping_bodies() 轮询——信号路径每次进入/离开都有 emit + 脚本调用的开销,轮询只是读一次 HashMap,高频下更便宜。

一个物理步内 Area 的状态与信号时机 物理步进(solver 工作) integrate:body 叠加 area 力场 broad phase:新 AreaPair 建立 solve:Area 不参与响应 只更新重叠列表 body_map / area_map 增删 步进完 flush_queries(每物理步一次) 遍历状态变化的 pair 新增重叠 → emit body_entered 消失重叠 → emit body_exited 脚本侧看到的效果 信号在 _physics_process 之后到达 回调里查 overlaps_body 是自洽快照 一帧内进又出:净变化为零,信号丢失

本文基于 Godot 4.x servers/physics_3d/godot_area_3d.cpp、godot_area_pair_3d.cpp 与 space_3d_sw.cpp 的 flush_queries 流程。