同一节点中的碰撞检测和重叠检测?

Collision detection and overlapping detection in same node?

是否有可能使单个节点像 RigidBody2D 一样发生碰撞,但同时在禁用碰撞时它像 Area2D 节点一样工作并且可以检测重叠?

例如:
我有一个附属物,它的行为就像一个 Area2D 节点,但是当它被切断时,它就像它自己的 RigidBody2D 节点一样。

我考虑过的一种方法是在剪切时创建 RigidBody2D 节点的新附属物,并从具有 Area2D 节点的旧附属物转移 CollisionShape2D

但我不知道哪种方法耗电最少

有没有更好的方法来实现这个?

编辑:
一个完美的例子就是断头台

blade 和头部 RigidBody2D,

两者最初都将 CollisionPolygon2D 禁用设置为 true,一旦 blade 掉落并且头部检测到重叠退出,disabled 将设置为 false 并且头部被切碎关闭(与 parent 节点分开)并弹开。

“人造Body2D”

我们将定制一个 Node,我称之为 FauxBody2D。它将作为 RigidBody2DCollisionShape 一起工作,作为 Area2D 相同 CollisionShape。为了存档,我们将使用 Physics2DServer.

虽然RigidBody2DArea2D的第一个共同祖先class是CollisionObject2D,但不方便扩展CollisionObject2D。所以 FauxBody2D 将是 Node2D.

类型

因此创建一个新脚本faux_body.gd。然而,它并不打算直接在场景树中使用(你可以,但你不能扩展它的代码),而是使用它添加一个 Node2D 和一个新脚本并将它设置为 extends FauxBody2D.

您将能够使用 FauxBody2D 的变量并以不受欢迎的方式对其进行处理。事实上,即使我声明了 setter,如果您不使用 self,您的脚本也会绕过它们。例如,不要设置 applied_force,而是设置 self.applied_force。顺便说一句,有些方法留空供您在脚本中覆盖(它们是“虚拟的”)。

这些是我们在 faux_body.gd 中的第一行代码:

class_name FauxBody2D
extends Node2D

我会避免重复代码。


模仿RigidBody2D

我正在跳过 roughabsorbent。同样在这个答案中,我只显示了区域的监控和信号。见 the followup answer.

我们将在 _enter_tree 中创建一个 body 并在 _exit_tree 中释放它:

var _body:RID
var _invalid_rid:RID

func _enter_tree() -> void:
    _body = Physics2DServer.body_create()

func _exit_tree() -> void:
    Physics2DServer.free_rid(_body)
    _body = _invalid_rid

没有表达式可以清零 RID。我会声明一个 _invalid_rid 并且从不设置它,所以它总是归零。

此外 body 应该与 FauxBody2D 相同 space:

func _enter_tree() -> void:
    # …
    Physics2DServer.body_set_space(_body, get_world_2d().space)

模仿CollisionShape2D

接下来让我们实现 CollisionShape2D:

的逻辑
export var shape:Shape2D setget set_shape
export var disabled:bool setget set_disabled
export var one_way_collision:bool setget set_one_way_collision
export(float, 0.0, 128.0) var one_way_collision_margin:float setget set_one_way_collision_margin

var _shape:RID

func _enter_tree() -> void:
    # …
    _update_shape()

func _update_shape() -> void:
    var new_shape = _invalid_rid if shape == null else shape.get_rid()
    if new_shape == _shape:
        return

    if _shape.get_id() != 0:
        Physics2DServer.body_remove_shape(_body, 0)

    _shape = new_shape

    if _shape.get_id() != 0:
        Physics2DServer.body_add_shape(_body, _shape, Transform2D.IDENTITY, disabled)
        Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)

func set_shape(new_value:Shape2D) -> void:
    if shape == new_value:
        return

    shape = new_value
    if _body.get_id() == 0:
        return

    _update_shape()

func set_disabled(new_value:bool) -> void:
    if disabled == new_value:
        return

    disabled = new_value
    if _body.get_id() == 0:
        return

    if _shape.get_id() != 0:
        Physics2DServer.body_set_shape_disabled(_body, 0, disabled)

func set_one_way_collision(new_value:bool) -> void:
    if one_way_collision == new_value:
        return

    one_way_collision = new_value
    if _body.get_id() == 0:
        return

    if _shape.get_id() != 0:
        Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)

func set_one_way_collision_margin(new_value:float) -> void:
    if one_way_collision_margin == new_value:
        return

    one_way_collision_margin = new_value
    if _body.get_id() == 0:
        return

    if _shape.get_id() != 0:
        Physics2DServer.body_set_shape_as_one_way_collision(_body, 0, one_way_collision, one_way_collision_margin)

此处我在 shape 无效时使用 _invalid_rid。请注意,我们不负责释放形状 RID.


完成后 body 将作为 RigidBody2D 使用,但 FauxBody2D 的 children 不是 body 的 children .我们将利用集成力,同时设置 body.

的状态
signal sleeping_state_changed()

export var linear_velocity:Vector2 setget set_linear_velocity
export var angular_velocity:float setget set_angular_velocity
export var can_sleep:bool = true setget set_can_sleep
export var sleeping:bool setget set_sleeping
export var custom_integrator:bool setget set_custom_integrator

func _enter_tree() -> void:
    # …
    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_TRANSFORM, global_transform)
    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_ANGULAR_VELOCITY, angular_velocity)
    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_CAN_SLEEP, can_sleep)
    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_LINEAR_VELOCITY, linear_velocity)
    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_SLEEPING, sleeping)
    Physics2DServer.body_set_force_integration_callback(_body, self, "_body_moved", 0)
    Physics2DServer.body_set_omit_force_integration(_body, custom_integrator)

func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
    _integrate_forces(state)
    global_transform = state.transform
    angular_velocity = state.angular_velocity
    linear_velocity = state.linear_velocity
    if sleeping != state.sleeping:
        sleeping = state.sleeping
        emit_signal("sleeping_state_changed")

# warning-ignore:unused_argument
func _integrate_forces(state:Physics2DDirectBodyState) -> void:
    pass

func set_linear_velocity(new_value:Vector2) -> void:
    if linear_velocity == new_value:
        return

    linear_velocity = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_LINEAR_VELOCITY, linear_velocity)

func set_angular_velocity(new_value:float) -> void:
    if angular_velocity == new_value:
        return

    angular_velocity = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_ANGULAR_VELOCITY, angular_velocity)

func set_can_sleep(new_value:bool) -> void:
    if can_sleep == new_value:
        return

    can_sleep = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_CAN_SLEEP, can_sleep)

func set_sleeping(new_value:bool) -> void:
    if sleeping == new_value:
        return

    sleeping = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_SLEEPING, sleeping)

func set_custom_integrator(new_value:bool) -> void:
    if custom_integrator == new_value:
        return

    custom_integrator = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_omit_force_integration(_body, custom_integrator)

body 将从我们 FauxBody2Dglobal_transform 开始,当 body 移动时,我们在 _body_moved 中获得回调,更新FauxBody2D 的属性以匹配 body 的状态,包括 FauxBody2Dglobal_transform。现在您可以将 children 添加到 FauxBody2D 并且它们将根据 body.

移动

然而,当 FauxBody2D 移动时,它不会移动 body。我会用 NOTIFICATION_TRANSFORM_CHANGED:

来解决
func _enter_tree() -> void:
    # …
    set_notify_transform(true)

func _notification(what: int) -> void:
    if what == NOTIFICATION_TRANSFORM_CHANGED:
        if _body.get_id() != 0:
            Physics2DServer.body_set_state(_body, Physics2DServer.BODY_STATE_TRANSFORM, global_transform)

顺便说一句 if _body.get_id() != 0: 应该和 if _body: 一样,但我更喜欢明确一点。

现在,当 FauxBody2D 移动时(不是在设置变换时),它将更新 body 的变换。


参数

接下来我将处理body个参数:

export(float, EXP, 0.01, 65535.0) var mass:float = 1.0 setget set_mass
export(float, EXP, 0.0, 65535.0) var inertia:float = 1.0 setget set_inertia
export(float, 0.0, 1.0) var bounce:float = 0.0 setget set_bounce
export(float, 0.0, 1.0) var friction:float = 1.0 setget set_friction
export(float, -128.0, 128.0) var gravity_scale:float = 1.0 setget set_gravity_scale
export(float, -1.0, 100.0) var linear_damp:float = -1 setget set_linear_damp
export(float, -1.0, 100.0) var angular_damp:float = -1 setget set_angular_damp

func _enter_tree() -> void:
    # …
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_ANGULAR_DAMP, angular_damp)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_GRAVITY_SCALE, gravity_scale)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_INERTIA, inertia)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_LINEAR_DAMP, linear_damp)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_MASS, mass)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_BOUNCE, bounce)
    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_FRICTION, friction)
    # …

func set_mass(new_value:float) -> void:
    if mass == new_value:
        return

    mass = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_MASS, mass)

func set_inertia(new_value:float) -> void:
    if inertia == new_value:
        return

    inertia = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_INERTIA, inertia)

func set_bounce(new_value:float) -> void:
    if bounce == new_value:
        return

    bounce = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_BOUNCE, bounce)

func set_friction(new_value:float) -> void:
    if friction == new_value:
        return

    friction = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_FRICTION, friction)

func set_gravity_scale(new_value:float) -> void:
    if gravity_scale == new_value:
        return

    gravity_scale = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_GRAVITY_SCALE, gravity_scale)

func set_linear_damp(new_value:float) -> void:
    if linear_damp == new_value:
        return

    linear_damp = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_LINEAR_DAMP, linear_damp)

func set_angular_damp(new_value:float) -> void:
    if angular_damp == new_value:
        return

    angular_damp = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_param(_body, Physics2DServer.BODY_PARAM_ANGULAR_DAMP, angular_damp)

我相信模式很清楚。

顺便说一下,inertia 没有在 Godot 的 RigidBody2D 中公开 3.x 但它在 Godot 4.0 中,所以继续在这里添加它。


持续集成

export(int, "Disabled", "Cast Ray", "Cast Shape") var continuous_cd

func _enter_tree() -> void:
    # …
    Physics2DServer.body_set_continuous_collision_detection_mode(_body, continuous_cd)
    # …

func set_continuous_cd(new_value:int) -> void:
    if continuous_cd == new_value:
        return

    continuous_cd = new_value
    if _body.get_id() == 0:
        return

    Physics2DServer.body_set_continuous_collision_detection_mode(_body, continuous_cd)

力和扭矩

我们将在applied_forcetorque中积累这些。我将使用 Godot 4.0 的另一个页面并有一个 center_of_mass。因此,我不会使用 body_add_force,而是执行等效的 body_add_center_forcebody_add_torque 调用,因此我可以使用自定义 center_of_mass 计算 torque

此外,Godot 在 2D 和 3D 之间存在差异,在 3D 中每个物理帧都会重置力,但在 2D 中不会。所以我希望它是可配置的。为此,我添加了 auto_reset_forces 属性.

export var applied_force:Vector2 setget set_applied_force
export var applied_torque:float setget set_applied_torque
export var center_of_mass:Vector2
export var auto_reset_forces:bool

func _enter_tree() -> void:
    # …
    Physics2DServer.body_add_central_force(_body, applied_force)
    Physics2DServer.body_add_torque(_body, applied_torque)

func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
    # …
    if auto_reset_forces:
        Physics2DServer.body_add_central_force(_body, -applied_force)
        Physics2DServer.body_add_torque(_body, -applied_torque)
        applied_force = Vector2.ZERO
        applied_torque = 0

func add_central_force(force:Vector2) -> void:
    applied_force += force
    if _body.get_id() != 0:
        Physics2DServer.body_add_central_force(_body, force)

func add_force(force:Vector2, offset:Vector2) -> void:
    var torque := (offset - center_of_mass).cross(force)
    applied_force += force
    applied_torque += torque
    if _body.get_id() != 0:
        Physics2DServer.body_add_central_force(_body, force)
        Physics2DServer.body_add_torque(_body, torque)

func add_torque(torque:float) -> void:
    applied_torque += torque
    if _body.get_id() != 0:
        Physics2DServer.body_add_torque(_body, torque)

func apply_central_impulse(impulse:Vector2) -> void:
    if _body.get_id() != 0:
        Physics2DServer.body_apply_central_impulse(_body, impulse)

func apply_impulse(offset:Vector2, impulse:Vector2) -> void:
    if _body.get_id() != 0:
        Physics2DServer.body_apply_impulse(_body, offset, impulse)

func apply_torque_impulse(torque:float) -> void:
    if _body.get_id() != 0:
        Physics2DServer.body_apply_torque_impulse(_body, torque)

func set_applied_force(new_value:Vector2) -> void:
    if applied_force == new_value:
        return

    if _body.get_id() != 0:
        var difference := new_value - applied_force
        Physics2DServer.body_add_central_force(_body, difference)

    applied_force = new_value

func set_applied_torque(new_value:float) -> void:
    if applied_torque == new_value:
        return

    if _body.get_id() != 0:
        var difference := new_value - applied_torque
        Physics2DServer.body_add_torque(_body, difference)

    applied_torque = new_value

顺便说一句,在将物理体添加到场景树之前,我还没有真正尝试过对物理体施加力和扭矩(我不知道为什么要这样做)。然而,当 body 进入场景树时,施加的力和扭矩将被存储和应用对我来说是有意义的。顺便说一句,当 body 退出场景树时,我不会擦除它们。


碰撞异常

并且我们 运行 进入了一个未暴露给脚本的函数:body_get_collision_exceptions。所以我们必须保留碰撞异常的清单。这很好,这意味着我可以在创建 body.

之前存储它们
var collision_exceptions:Array

func add_collision_exception_with(body:Node) -> void:
    var collision_object := body as PhysicsBody2D
    if not is_instance_valid(collision_object):
        push_error( "Collision exception only works between two objects of PhysicsBody type.")
        return

    var rid = collision_object.get_rid()
    if rid.get_id() == 0:
        return

    if collision_exceptions.has(collision_object):
        return

    collision_exceptions.append(collision_object)
    if _body.get_id() != 0:
        Physics2DServer.body_add_collision_exception(_body, rid)

func get_collision_exceptions() -> Array:
    return collision_exceptions

func remove_collision_exception_with(body:Node) -> void:
    var collision_object := body as PhysicsBody2D
    if not is_instance_valid(collision_object):
        push_error( "Collision exception only works between two objects of PhysicsBody type.")
        return

    var rid = collision_object.get_rid()
    if rid.get_id() == 0:
        return

    if not collision_exceptions.has(collision_object):
        return

    collision_exceptions.erase(collision_object)
    if _body.get_id() != 0:
        Physics2DServer.body_remove_collision_exception(_body, rid)

测试动作

这个很简单:

func test_motion(motion:Vector2, infinite_inertia:bool = true, margin:float = 0.08, result:Physics2DTestMotionResult = null) -> bool:
    if _body.get_id() == 0:
        push_error("body is not inside the scene tree")
        return false

    return Physics2DServer.body_test_motion(_body, global_transform, motion, infinite_inertia, margin, result)

顺便说一句,如果你想传递body_test_motionexclude参数,知道它想要RIDs。为了以防万一,我还要提到 get_collision_exceptions 记录在 return Node 中,这就是我在这里实现它的方式。


轴速度

虽然我很想像这样实现它:

func set_axis_velocity(axis_velocity:Vector2) -> void:
    Physics2DServer.body_set_axis_velocity(_body, axis_velocity)

不是很方便。原因是我想继续存储属性的想法,并在 body 进入场景树时应用它们。

对于一个实现这个的替代方法,我们应该明白它做了什么:它改变了linear_velocity但只在axis_velocity的方向上,任何垂直速度都不会受到影响。换句话说,我们分解 linear_velocity 沿 axis_velocity 的速度和垂直 axis_velocity 的速度,然后我们从 axis_velocity 加上分量计算一个新的 linear_velocityaxis_velocity 垂直的旧 linear_velocity

所以,像这样:

func set_axis_velocity(axis_velocity:Vector2) -> void:
    self.linear_velocity = axis_velocity + linear_velocity.slide(axis_velocity.normalized())

对了,官方文档之所以说axis_velocity对跳跃有用,是因为它可以让你设置垂直速度,而不影响水平速度。


模仿Area2D

我不会实施 space 覆盖 et.al。希望您现在已经对如何与 Physics2DServer 交互有了一个很好的了解,只要说您想要使用 area_* 方法而不是 body_* 方法就够了。因此,您可以使用 area_set_param 设置区域参数,使用 area_set_space_override_mode 设置 space 覆盖模式。


接下来,让我们创建一个区域:

var _area:RID

func _enter_tree() -> void:
    _area = Physics2DServer.area_create()
    Physics2DServer.area_set_space(_area, get_world_2d().space)
    Physics2DServer.area_set_transform(_area, global_transform)
    # …

func _exit_tree() -> void:
    Physics2DServer.free_rid(_area)
    _area = _invalid_rid
    # …

注意:我也给area_set_transform的区域定位了。

然后让我们将形状附加到该区域:

func _update_shape() -> void:
    # …

    if _shape.get_id() != 0:
        Physics2DServer.area_add_shape(_area, _shape, Transform2D.IDENTITY, disabled)
        # …

移动区域

body移动时,我们也应该移动区域:

func _notification(what: int) -> void:
    if what == NOTIFICATION_TRANSFORM_CHANGED:
        # …
        if _area.get_id() != 0:
            Physics2DServer.area_set_transform(_area, global_transform)

模式

我想复制 Godot 4.0 设计并使用 freezefreeze_mode 而不是使用 mode。那么转换我们的Node2D最终会多出一个freeze_mode它也可能是一个额外的mode如果我做的更像戈多3.x。

export var lock_rotation:bool setget set_lock_rotation
export var freeze:bool setget set_freeze
export(int, "Static", "Kinematic", "Area") var freeze_mode:int setget set_freeze_mode

func _enter_tree() -> void:
    # …
    _update_body_mode()

func _update_body_mode() -> void:
    if freeze:
        if freeze_mode == 1:
            Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_KINEMATIC)
        else:
            Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_STATIC)
    else:
        if lock_rotation:
            Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_CHARACTER)
        else:
            Physics2DServer.body_set_mode(_body, Physics2DServer.BODY_MODE_RIGID)

func set_lock_rotation(new_value:bool) -> void:
    if lock_rotation == new_value:
        return

    lock_rotation = new_value
    if _body.get_id() == 0:
        return

    _update_body_mode()

func set_freeze(new_value:bool) -> void:
    if freeze == new_value:
        return

    freeze = new_value
    if _body.get_id() == 0:
        return

    _update_body_mode()

func set_freeze_mode(new_value:int) -> void:
    if freeze_mode == new_value:
        return

    freeze_mode = new_value
    if _body.get_id() == 0:
        return

    _update_body_mode()

自从我实施 _update_body_mode 检查 freeze_mode 是否为 Kinematic 后,"Area" 将表现得像 "Static",这就是我们想要的。 好吧,差不多了,我们会讲到的。


输入可拾取

遗憾的是 body_set_pickable 没有公开脚本。所以我们将不得不重新创建此功能。

signal input_event(viewport, event, shape_idx)
signal mouse_entered()
signal mouse_exited()

export var input_pickable:bool setget set_input_pickable

var _mouse_is_inside:bool

func _enter_tree() -> void:
    # …
    _update_pickable()
    # …

func _notification(what: int) -> void:
    # …
    if what == NOTIFICATION_VISIBILITY_CHANGED:
        _update_pickable()

func _update_pickable() -> void:
    set_process_input(input_pickable and _body.get_id() != 0 and is_visible_in_tree())

func _input(event: InputEvent) -> void:
    if (
        not (event is InputEventScreenDrag)
        and not (event is InputEventScreenTouch)
        and not (event is InputEventMouse)
    ):
        return

    var viewport := get_viewport()
    var position:Vector2 = viewport.get_canvas_transform().affine_inverse().xform(event.position)
    var objects := get_world_2d().direct_space_state.intersect_point(position, 32, [], 0x7FFFFFFF, false, true)
    var is_inside := false
    for object in objects:
        if object.rid == _area:
            is_inside = true
            break

    if is_inside:
        _input_event(viewport, event, 0)
        emit_signal("input_event", viewport, event, 0)

    if event is InputEventMouse and _mouse_is_inside != is_inside:
        _mouse_is_inside = is_inside
        if _mouse_is_inside:
            emit_signal("mouse_entered")
        else:
            emit_signal("mouse_exited")

# warning-ignore:unused_argument
# warning-ignore:unused_argument
# warning-ignore:unused_argument
func _input_event(viewport:Object, event:InputEvent, shape_idx:int) -> void:
    pass

func set_input_pickable(new_value:bool) -> void:
    if input_pickable == new_value:
        return

    input_pickable = new_value
    _update_pickable()

Body进入和退出

似乎还有一个错误需要 area_set_monitorable 进行监控。所以我们不能做一个可以监控但不可监控的区域。

signal body_entered(body)
signal body_exited(body)
signal body_shape_entered(body_rid, body, body_shape_index, local_shape_index)
signal body_shape_exited(body_rid, body, body_shape_index, local_shape_index)
signal area_entered(area)
signal area_exited(area)
signal area_shape_entered(area_rid, area, area_shape_index, local_shape_index)
signal area_shape_exited(area_rid, area, area_shape_index, local_shape_index)

export var monitorable:bool setget set_monitorable
export var monitoring:bool setget set_monitoring

var overlapping_body_instances:Dictionary
var overlapping_area_instances:Dictionary
var overlapping_bodies:Dictionary
var overlapping_areas:Dictionary

func _enter_tree() -> void:
    # …
    _update_monitoring()
    # …

func _update_monitoring() -> void:
    Physics2DServer.area_set_monitorable(_area, monitorable or monitoring)
    if monitoring:
        Physics2DServer.area_set_monitor_callback(_area, self, "_body_monitor")
        Physics2DServer.area_set_area_monitor_callback(_area, self, "_area_monitor")
    else:
        Physics2DServer.area_set_monitor_callback(_area, null, "")
        Physics2DServer.area_set_area_monitor_callback(_area, null, "")

func _body_monitor(status:int, body:RID, instance_id:int, body_shape_index:int, local_shape_index:int) -> void:
    if _body == body:
        return

    var instance := instance_from_id(instance_id)
    if status == 0:
        # entered
        if not overlapping_bodies.has(body):
            overlapping_bodies[body] = 0
            overlapping_body_instances[instance] = instance
            emit_signal("body_entered", instance)

        overlapping_bodies[body] += 1
        emit_signal("body_shape_entered", body, instance, body_shape_index, local_shape_index)
    else:
        # exited
        emit_signal("body_shape_exited", body, instance, body_shape_index, local_shape_index)
        overlapping_bodies[body] -= 1
        if overlapping_bodies[body] == 0:
            overlapping_bodies.erase(body)
            overlapping_body_instances.erase(instance)
            emit_signal("body_exited", instance)

func _area_monitor(status:int, area:RID, instance_id:int, area_shape_index:int, local_shape_index:int) -> void:
    var instance := instance_from_id(instance_id)
    if status == 0:
        # entered
        if not overlapping_areas.has(area):
            overlapping_areas[area] = 0
            overlapping_area_instances[instance] = instance
            emit_signal("area_entered", instance)

        overlapping_areas[area] += 1
        emit_signal("area_shape_entered", area, instance, area_shape_index, local_shape_index)
    else:
        # exited
        emit_signal("area_shape_exited", area, instance, area_shape_index, local_shape_index)
        overlapping_areas[area] -= 1
        if overlapping_areas[area] == 0:
            overlapping_areas.erase(area)
            overlapping_area_instances.erase(instance)
            emit_signal("area_exited", instance)

func get_overlapping_bodies() -> Array:
    if not monitoring:
        push_error("monitoring is false")
        return []

    return overlapping_body_instances.keys()

func get_overlapping_areas() -> Array:
    if not monitoring:
        push_error("monitoring is false")
        return []

    return overlapping_area_instances.keys()

func overlaps_body(body:Node) -> bool:
    if not monitoring:
        return false

    return overlapping_body_instances.has(body)

func overlaps_area(area:Node) -> bool:
    if not monitoring:
        return false

    return overlapping_area_instances.has(area)

func set_monitoring(new_value:bool) -> void:
    if monitoring == new_value:
        return

    monitoring = new_value
    if _area.get_id() == 0:
        return

    _update_monitoring()

func set_monitorable(new_value:bool) -> void:
    if monitorable == new_value:
        return

    monitorable = new_value
    if _area.get_id() == 0:
        return

    _update_monitoring()

我在这里使用 area_set_monitor_callbackarea_set_area_monitor_callback。该文档声称 area_set_monitor_callback 适用于区域和身体。但这也不正确。 area_set_monitor_callback只针对body,无证的area_set_area_monitor_callback针对area

我需要跟踪每个进入和存在的形状。这就是为什么我使用 overlapping_areasoverlapping_bodies 的字典。键将是 RIDs,值将是形状重叠的数量。

我们快完成了。


碰撞层和遮罩

我希望区域和 body 共享碰撞层和遮罩。除了 "Area" 模式,我会将 body 的碰撞层和遮罩设置为 0,这样它就不会与任何东西发生碰撞。

export(int, LAYERS_2D_PHYSICS) var collision_layer:int = 1 setget set_collision_layer
export(int, LAYERS_2D_PHYSICS) var collision_mask:int = 1 setget set_collision_mask

func _enter_tree() -> void:
    # …
    _update_collision_layer_and_mask()
    # …

func _update_collision_layer_and_mask() -> void:
    Physics2DServer.area_set_collision_layer(_area, collision_layer)
    Physics2DServer.body_set_collision_layer(_body, collision_layer if not freeze or freeze_mode != 2 else 0)
    Physics2DServer.area_set_collision_mask(_area, collision_mask)
    Physics2DServer.body_set_collision_mask(_body, collision_mask if not freeze or freeze_mode != 2 else 0)

func set_collision_layer(new_value:int) -> void:
    if collision_layer == new_value:
        return

    collision_layer = new_value
    if _body.get_id() == 0:
        return

    _update_collision_layer_and_mask()

func set_collision_mask(new_value:int) -> void:
    if collision_mask == new_value:
        return

    collision_mask = new_value
    if _body.get_id() == 0:
        return

    _update_collision_layer_and_mask()

同时让我们实现 get_collision_layer_bitget_collision_mask_bitset_collision_layer_bitset_collision_mask_bit:

func get_collision_layer_bit(bit:int) -> bool:
    if bit < 0 or bit > 31:
        push_error("Collision layer bit must be between 0 and 31 inclusive.")
        return false

    return collision_layer & (1 << bit) != 0

func get_collision_mask_bit(bit:int) -> bool:
    if bit < 0 or bit > 31:
        push_error("Collision mask bit must be between 0 and 31 inclusive.")
        return false

    return collision_mask & (1 << bit) != 0

func set_collision_layer_bit(bit:int, value:bool) -> void:
    if bit < 0 or bit > 31:
        push_error("Collision layer bit must be between 0 and 31 inclusive.")
        return

    if value:
        self.collision_layer = collision_layer | 1 << bit
    else:
        self.collision_layer = collision_layer & ~(1 << bit)

func set_collision_mask_bit(bit:int, value:bool) -> void:
    if bit < 0 or bit > 31:
        push_error("Collision mask bit must be between 0 and 31 inclusive.")
        return

    if value:
        self.collision_mask = collision_mask | 1 << bit
    else:
        self.collision_mask = collision_mask & ~(1 << bit)

并在_update_body_mode中添加对_update_collision_layer_and_mask()的调用:

func _update_body_mode() -> void:
    _update_collision_layer_and_mask()
    # …

我们完成了。 我觉得。