拾取范围、触发器、伤害判定区——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,高频下更便宜。
本文基于 Godot 4.x servers/physics_3d/godot_area_3d.cpp、godot_area_pair_3d.cpp 与 space_3d_sw.cpp 的 flush_queries 流程。