PAYATHON 2026

Godot:如何检测完全位于现有 Area3D 内的新 Area3D?

排障赵师傅

结论

配置正常时,Area3D 可以检测到完全位于其内部的新建 Area3D,不需要让它先穿过边界。

如果只有靠近球形边界时才能检测到,建议先检查 FOV 的碰撞形状是否为 ConcavePolygonShape3D。这种形状即使看起来是封闭球体,在物理上也只有三角形表面,内部是空的。事件区域位于内部、没有接触表面时,就可能检测不到。Godot 对空心碰撞形状的说明

目前缺少实际场景和碰撞资源信息,还不能确定原因,可以先从这一项查起。

先检查碰撞形状

在编辑器中选中 FOV 下的 CollisionShape3D,查看 shape 的资源类型。球形 FOV 应优先使用 SphereShape3D。

尤其要检查通过 Create Trimesh Collision Sibling 或 Mesh.create_trimesh_shape() 生成的碰撞形状。显示用的球形网格与检测用的实心球形碰撞体并不相同。

可以用两个简单球形碰撞体做最小验证:

  1. 将 FOV 的碰撞资源替换为 SphereShape3D。
  2. 为 EventArea3D 使用一个较小的 SphereShape3D。
  3. 在 FOV 中心创建事件区域,让它保持存在,观察是否收到信号。

复杂的 FOV 可以考虑使用 ConvexPolygonShape3D,或组合多个凸形状。Godot 的 Area3D 文档也提醒,凹多边形碰撞体内部空心,可能导致异常检测结果。Area3D 碰撞形状说明

如果问题出在空心碰撞体上,等待更多物理帧、增加同类空心形状或反复缩放,都无法让内部变成实心。backface_collision = true 只会启用三角形背面的碰撞,也不会填满内部。

再检查监测开关和碰撞层

要让 FOV 检测到事件区域,需要满足以下条件:

  • FOV 的 monitoring = true。
  • EventArea3D 的 monitorable = true。
  • FOV 的 collision_mask 包含事件所在的 collision_layer。
  • 双方都有有效且未禁用的碰撞形状,并位于同一个 World3D 的物理空间中。

事件区域自身的 monitoring 可以关闭,其他区域仍能检测到它。双方也不需要互相设置检测掩码。Area3D 监测属性

例如,事件放在第 2 层时,FOV 的检测掩码可以这样设置:

# 第 2 层对应位掩码 2。
event_area.collision_layer = 1 << 1
fov.collision_mask = 1 << 1

这段代码会覆盖原有设置。如果需要保留其他检测层,可以使用 set_collision_mask_value(2, true)。碰撞层与掩码 API

Godot 4 的代码示例

两个 CollisionShape3D 都使用 SphereShape3D,场景结构如下:

NPC
└── FOV (Area3D)
    └── CollisionShape3D

EventArea3D (Area3D,运行时创建)
└── CollisionShape3D

将下面的脚本挂在 FOV 上,在事件生成前连接好信号:

extends Area3D

func _ready() -> void:
    monitoring = true
    collision_mask = 1 << 1
    area_entered.connect(_on_area_entered)


func _on_area_entered(area: Area3D) -> void:
    if area.is_in_group("observable_events"):
        # 在这里将事件数据交给 NPC 的记忆系统。
        print("观察到事件:", area.global_position)

下面的函数可以放在事件管理器中。event_parent 应是已经进入场景树的 Node3D:

func spawn_event(
    event_parent: Node3D,
    world_position: Vector3
) -> Area3D:
    var event_area := Area3D.new()
    event_area.name = "EventArea3D"
    event_area.collision_layer = 1 << 1
    event_area.collision_mask = 0
    event_area.monitorable = true
    event_area.monitoring = false
    event_area.add_to_group("observable_events")

    var sphere := SphereShape3D.new()
    sphere.radius = 0.25

    var collision := CollisionShape3D.new()
    collision.shape = sphere
    event_area.add_child(collision)

    # 加入场景前先设置相对于父节点的位置。
    event_area.position = event_parent.to_local(world_position)
    event_parent.add_child(event_area)

    return event_area

示例中的半径只是演示值。排查时,让节点及其父节点保持单位缩放,通过 SphereShape3D.radius 调整大小,避免非均匀缩放影响碰撞结果。CollisionShape3D 缩放注意事项

物理更新时机需要注意什么

get_overlapping_areas() 返回物理步骤更新后的重叠列表。创建或移动节点后立即调用,可能读到旧状态;反复调用也不会强制刷新检测。重叠列表的更新时间

排查时注意以下几点:

  • 让事件至少存活到物理系统有机会处理它,避免创建后立刻 queue_free()。
  • 提前连接信号。连接前已经发出的 area_entered 不会补发;如果需要初始化已有事件,应读取更新后的重叠列表。
  • 在碰撞回调中创建区域或修改形状时,如果出现物理查询刷新期间禁止修改的报错,应通过 call_deferred() 延后执行相关操作。

已经尝试等待物理帧,而且边界附近能够检测到时,应优先验证形状类型。继续增加等待次数对排查的帮助有限。

只关心事件位置时,可以直接判断距离

如果事件只代表一个位置,FOV 又是球形范围,可以在事件发生时直接检查它到球心的距离,无须为每个瞬时事件创建碰撞区域:

func is_event_in_range(
    event_position: Vector3,
    fov_center: Vector3,
    fov_radius: float
) -> bool:
    return event_position.distance_squared_to(fov_center) \
        <= fov_radius * fov_radius

球心应使用实际碰撞球的中心,半径应使用世界空间中的有效半径。

事件中心在范围内、球体重叠和事件球完全在范围内,对应不同的条件。设两个球心距离为 d,FOV 半径为 R,事件半径为 r:

判断目标条件
事件中心位于 FOV 内d <= R
两个球体重叠或相切d <= R + r
整个事件球完全位于 FOV 内d + r <= R

area_entered 表示建立了重叠关系,不代表整个事件区域已经完全进入。

其他可以尝试的方法

对于非球形、由实心碰撞形状组成的 FOV,可以使用 PhysicsDirectSpaceState3D.intersect_point() 查询事件位置落在哪些区域内。通过 PhysicsPointQueryParameters3D 显式设置 collide_with_areas = true,并让查询掩码包含 FOV 所在的碰撞层。这种查询只判断一个点,无法确认整个事件区域是否被包含,也无法解决凹三角网格内部空心的问题。点查询 API、查询参数

如果需要按事件体积查找重叠对象,可以考虑 intersect_shape()。直接物理空间查询应在 _physics_process() 中执行,并根据 NPC 数量设置合适的 max_results,避免结果被截断。形状查询 API、物理空间访问时机

如果使用两个简单球形碰撞体仍能稳定复现问题,再制作最小项目,记录 Godot 版本、3D 物理后端、节点变换和创建时机,继续排查引擎或项目配置。

球形范围只能判断事件是否在附近。要确定 NPC 是否看见事件,还需要按游戏规则检查视角,并用射线判断墙壁等障碍物是否遮挡。写入记忆时,建议保存事件数据,避免只保留随后可能被释放的 Area3D 节点引用。

备注:内容仅供参考。