Search code examples
godotgdscript

Collision detection and overlapping detection in same node?


Is it possible to make a single node which collides like a RigidBody2D but at the same time when collision is disabled it works like an Area2D node and can detect overlapping?

for example:
I have an appendage and it behaves like an Area2D node, but when it is cut off it acts like it's own RigidBody2D node.

one approach I've considered is creating a new appendage of RigidBody2D node when cut and transferring the CollisionShape2D from the old appendage with the Area2D node.

but I don't know which method would be the least computation power consuming

is there a better method of achieving this?

Edit:
A perfect example would be a guillotine enter image description here

enter image description here

The blade and head being a RigidBody2D,

both initially have CollisionPolygon2D have disabled set to true and once the blade falls down and the Head detects overlapping exited, the disabled gets set to false and the head gets chopped off (seperated from parent node) and bounces away.


Solution

  • "FauxBody2D"

    We are going to make a custom Node which I'm calling FauxBody2D. It will work a RigidBody2D with a CollisionShape and as an Area2D with the same CollisionShape. And to archive this, we will use Physics2DServer.

    Even though the first common ancestor class of RigidBody2D and Area2D is CollisionObject2D, it is not convenient to extend CollisionObject2D. So FauxBody2D will be of type Node2D.

    So create a new script faux_body.gd. However, it is not intended to be used directly in the scene tree (you can, but you won't be able to extend its code), instead to use it add a Node2D with a new script and set it extends FauxBody2D.

    You would be able to the variables of FauxBody2D and mess with it in undesirable ways. In fact, even though I'm declaring setters, your script would bypass them if you don't use self. For example, don't set applied_force, set self.applied_force instead. By the way, some methods are left empty for you to override in your script (they are "virtual").

    These are our firsts lines of code in faux_body.gd:

    class_name FauxBody2D
    extends Node2D
    

    I will avoid repeating code.


    Mimic RigidBody2D

    I'm skipping rough, absorbent. Also In this answer I only show monitoring and signals with area. See the followup answer.

    We are going to create a body in _enter_tree and free it in _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
    

    There is no expression to get a zeroed RID. I will declare a _invalid_rid and never set it, so it is always zeroed.

    Also the body should be in the same space as the FauxBody2D:

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

    Mimic CollisionShape2D

    Next let us implement the logic for the 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)
    
    

    Here I'm using _invalid_rid when the shape is not valid. Notice that we are not responsible of freeing the shape RID.


    State

    With this done the body will work as a RigidBody2D but children of the FauxBody2D are not children of the body. We will take advantage of integrate forces, and while we are at it set the state of the 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)
    

    The body will start at the global_transform of our FauxBody2D, and when the body moves we get a callback in _body_moved where update the properties of the FauxBody2D to match the state of the body, including the global_transform of the FauxBody2D. Now you can add children to the FauxBody2D and they will move according to the body.

    However, when the FauxBody2D moves, it does not move the body. I will solve it with 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)
    

    By the way if _body.get_id() != 0: should be the same as if _body: but I prefer to be explicit.

    Now when the FauxBody2D moves (not when its transform is set) it will update the transform of the body.


    Parameters

    Next I will deal with body parameters:

    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)
    

    I believe the pattern is clear.

    By the way inertia is not exposed in RigidBody2D in Godot 3.x but it is in Godot 4.0, so went ahead and added it here.


    Continuos integration

    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)
    

    Force and torque

    We will accumulate these in applied_force and torque. I will take another page form Godot 4.0 and have a center_of_mass. In consequence I will not use body_add_force, instead I will do the equivalent body_add_center_force and body_add_torque calls, so I can compute the torque with the custom center_of_mass.

    Furthermore, Godot has a discrepancy between 2D and 3D that in 3D forces are reset every physics frame, but not in 2D. So I want it to be configurable. For that I'm adding a auto_reset_forces property.

    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
    

    By the way, I haven't really experimented with applying forces and torque to physics bodies before adding them to the scene tree (I don't know why I would do that). Yet, it makes sense to me that the applied forces and torque would be stored and applied when the body enters the scene tree. And by the way, I'm not erasing them when the body exits the scene tree.


    Collision exceptions

    And we run into a function that is not exposed to scripting: body_get_collision_exceptions. So we will have to keep inventory of the collision exceptions. This is fine, it means I can get away with storing them before creating the 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)
    

    Test motion

    This one is very simple:

    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)
    

    By the way, in case you want to pass the exclude parameter of body_test_motion, know that it wants RIDs. And just in case, I'll also mention that get_collision_exceptions is documented to return Nodes, and that is how I implemented it here.


    Axis velocity

    While I'm tempted to implement it like this:

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

    It is not really convenient. The reason being that I want to continue with the idea of storing the properties and apply them when the body enters the scene tree.

    For an alternative way to implement this, we should understand what it does: it changes the linear_velocity but only on the direction of axis_velocity, any perpendicular velocity would not be affected. In other words, we decompose linear_velocity in velocity along axis_velocity and velocity perpendicular to axis_velocity, and then we compute a new linear_velocity from the axis_velocity plus the component of the old linear_velocity that is perpendicular to axis_velocity.

    So, like this:

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

    By the way, the reason why the official documentation says that axis_velocity is useful for jumps is because it allows you to set the vertical velocity without affecting the horizontal velocity.


    Mimic Area2D

    I will not implement space overrides et.al. Hopefully you have by now a good idea of how to interact with Physics2DServer, suffice to say you would want to use the area_* methods instead of the body_* methods. So you can set the area parameters with area_set_param and the space override mode with area_set_space_override_mode.


    Next, let us create an area:

    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
        # …
    

    Note: I am also giving position to the area with area_set_transform.

    And let us attach the shape to the area:

    func _update_shape() -> void:
        # …
    
        if _shape.get_id() != 0:
            Physics2DServer.area_add_shape(_area, _shape, Transform2D.IDENTITY, disabled)
            # …
    

    Move the area

    We should also move the area when the body moves:

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

    Modes

    I want copy Godot 4.0 design and use freeze and freeze_mode instead of using mode. Then converting our Node2D will eventually be an extra freeze_mode. It could also be an extra mode if I do it more like Godot 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()
    

    Since I implemented _update_body_mode checking if freeze_mode is Kinematic, "Area" will behave as "Static", which is what we want. Well, almost, we will get to that.


    Input Pickable

    Sadly body_set_pickable is not exposed for scripting. So we will have to recreate this functionality.

    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 entered and exited

    There seems to be a bug that area_set_monitorable is needed to monitor too. So we cannot make an area that monitors but is not 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()
    

    Here I'm using area_set_monitor_callback and area_set_area_monitor_callback. The documentation claims that area_set_monitor_callback works for both areas and bodies. However that is nor correct. area_set_monitor_callback is only for bodies, and the undocumented area_set_area_monitor_callback is for areas.

    I need to keep track of each shape that enters and exists. Which is why I'm using dictionaries for overlapping_areas and overlapping_bodies. The keys will be the RIDs, and the values will be the number of shape overlaps.

    We are almost done.


    Collision layer and mask

    I want both area and body to share collision layer and mask. Except in "Area" mode, where I'll set the collision layer and mask of the body to 0 so it does not collide with anything.

    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()
    

    And while we are at it let us implement get_collision_layer_bit, get_collision_mask_bit, set_collision_layer_bit, and set_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)
    

    And add a call to _update_collision_layer_and_mask() in _update_body_mode:

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

    And we are done. I think.