2

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.

cak3_lover
  • 1,440
  • 5
  • 26
  • I'm unaware of any clever solution for this, and you seem have an idea of how to go about it, did you run into trouble with it? - Anyway, if the goal is to optimize, then I'd expect bypassing `Node`s altogether and talking to `Physics2DServer` to be better. I might write an answer later. – Theraot Mar 24 '22 at 20:12
  • @Theraot I'm just very recently getting into the physics side of things and what the *heck* is `Physics2DServer`? The documentations don't seem to have any tutorial that can help with beginners – cak3_lover Mar 24 '22 at 20:20
  • Yeah, it isn't for beginners, I still don't know all its ins and outs. But the short version is that `Node`s are a convenient way to do scenes, but they are actually using "servers" behind the scenes. `Physics2DServer` is the one responsible for physics in 2D, `VisualServer` is responsible for graphics, and such. So, for example, If I need to create and `Area2D` I can use `Node`s or I can make a bunch of `Physics2DServer` calls that accomplish the same thing, but without creating `Node`s, which means less overhead. Thus if your goal is performance, using `Physics2DServer` should be better. – Theraot Mar 24 '22 at 23:06
  • @Theraot so basically `Physics2DServer` handles all the physics without having to create a node? That sounds a little counter intuitive, do you mean like I can create a `Position2D` node and somehow apply `Physics2DServer` to make it behave like a `RigidBody2D` and/or `Area2D`? – cak3_lover Mar 25 '22 at 07:08
  • Yes, No and yes. You don't need `Node`s to have physics, you don't exactly apply `Physics2DServer` to a `Node`, and yes you could make a `Position2D` behave like a `RigidBody2D` or an `Area2D`. `Physics2DServer` is a level or abstraction below `Node`s. I'm currently writing the code for the answer, but it is long code (I don't know what exactly you need of `RigidBody2D`, so I'm making it all - it is fine, it might help as tutorial for other people, the documentation is sacarse anyway), it will take a while. – Theraot Mar 25 '22 at 07:21
  • By the way, you would really see the benefits if you stop thinking of a `Node` equals a physics thing. A `Node` could be many physics things. Imagine a `BulletHell` custom `Node` that has an `add_bullet` method, and and emits a signal when any of the bullets collide. But does not use `Node`s per bullet, instead it talks to `Physics2DServer` and `VisualServer`. So adding more bullets does not mean allocating more `Node`s, which means less overhead, which means more performance, which means the game can have more bullets in play. – Theraot Mar 25 '22 at 07:32
  • @Theraot take your time, your answer will probably serve as future reference for many beginners. and to further elaborate what I'm trying to do: suppose you have an arm which behaves the usual way with overlapping detection,picking up stuff, etc. but once it is cut off from the body, the arm will becomes a `RigidBody2D` i.e. like in this [zombie dismemberment game](https://gamaverse.com/zombie-buster-game/) you can see the limbs becoming rigid bodies after blowing up – cak3_lover Mar 25 '22 at 09:01
  • @Theraot I understood your bullet hell example but like you said you need atleast 1 base node, similarly I'm trying to create a custom base node which behaves like an `Area2D` when the inner node `CollisionPolygon2D`'s collision is disabled & behaves like a `RigidBody2D` when collision is enabled – cak3_lover Mar 25 '22 at 09:04
  • 1
    StackOverflow says `Body is limited to 30000 characters; you entered 37369.` Ha! – Theraot Mar 25 '22 at 19:11
  • @Theraot desperate times call for desperate measures ¯\_(ツ)_/¯ – cak3_lover Mar 25 '22 at 19:12
  • I'm not convinced the guillotine is the perfect example, after all the `RigidBody2D` can detect collisions. And an `Area2D` would report a body entered as soon as the guillotine touches it too. Edit: Well, that or my answer is not what you need. – Theraot Mar 25 '22 at 19:57

1 Answers1

3

"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.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • feel like I just found *the holy grail*, I have been reading this from an hour now & I have so many questions. Let's start with: is it possible to add [this](https://i.ibb.co/pwC3PW4/image.png) sort of custom dot connect polygon feature like in `CollisionPolygon2D`? – cak3_lover Mar 26 '22 at 14:29
  • @cakelover You need a `Shape2D`, there are two options: `ConcavePolygonShape2D` and `ConvexPolygonShape2D`. The `CollisionPolygon2D` is equivalent to either depending on the `build_mode` property. To create them you need the points that make up the polygon. You might also be interested in the `Geometry` class. I'm aware that the `FauxBody2D` does not give you any widgets to edit the `Shape2D` in the editor, implementing those is their own challenge. – Theraot Mar 26 '22 at 18:05
  • what is `body_set_state()`? , what exactly is state suppose to be? what is the difference between [ID](https://docs.godotengine.org/en/stable/classes/class_rid.html#class-rid-method-get-id) and [RID](https://docs.godotengine.org/en/stable/classes/class_resource.html#class-resource-method-get-rid)? I made [this](https://pastebin.com/R8WHt7KX) bare minimum code and added some comments, could you see if my understanding is accurate? – cak3_lover Mar 27 '22 at 05:36
  • Also how do I detect and send signals when the body has collided? – cak3_lover Mar 27 '22 at 06:47
  • @cakelover `body_set_states` as the name suggests sets attributes of the body, that the physics engine can change, such as the position of the body or the velocity of the body. On the other hand `body_set_param` sets values that the physics body will not change. – Theraot Mar 27 '22 at 06:47
  • @cakelover - The RID is a wrapper for the handle for the resource. RID = Resource ID. The handle is not more than an `int` that works as index for the resource inside Godot. So, by wrapping the `int` in a `RID` and only allowing creating `RID` from `Resource`s, they ensure you are not passing any arbitrary `int`. However, you can still read the `int` with `get_id()` which sometimes is useful for debugging, or in my case to check if I have `0` which is always an invalid `RID`. – Theraot Mar 27 '22 at 06:47
  • @cakelover If you are only using the body, to know about collisions you need to use the callback you set with `body_set_omit_force_integration`, the `Physics2DDirectBodyState` you get has information about the collisions. So you keep track of the collisions and if you see a body that was not there before you emit a signal. And if there was a body but no longer is, you emit the other signal. If you are using an area instead, the example is in the answer. Edit: I cut handling body entered and exited with body because the answer was too long and I was going to do it with the area anyway. – Theraot Mar 27 '22 at 06:50
  • The custom node is not able to be added as a node in `PinJoint2D`, I'm guessing we'll have to extend `PhysicsBody2D` for that? – cak3_lover Mar 27 '22 at 06:52
  • @cakelover Correct. And it would not make much sense if it worked with an arbitrary custom node either. As usual, `PinJoint2D` also wraps around `Physics2DServer` stuff. You can start with `pin_joint_create` et. al. and make your own joints. – Theraot Mar 27 '22 at 06:55
  • I'm not sure I follow what you mean by "use the callback you set with `body_set_omit_force_integration`" could you add it to the answer? I don't mind the answer being lengthy as long as it's detailed :) – cak3_lover Mar 27 '22 at 07:10
  • @cakelover Ah, that is on me, I copied the wrong function name. It is `body_set_force_integration_callback`. The problem is not that the answer is too long, is that the answer is too long *for StackOverflow*. How about you ask it as another question? – Theraot Mar 27 '22 at 07:13
  • here's the [continuation question](https://stackoverflow.com/questions/71634409/collision-detection-and-overlapping-detection-in-same-node-part-2) – cak3_lover Mar 27 '22 at 07:24
  • A minor issue: I created an `Area2D` node & attached a `body_shape_entered()` signal, Now when a `FauxBody2D` body passes through the Area2D it prints the FauxBody2D node as `[Object:null]` – cak3_lover Mar 30 '22 at 18:41
  • 1
    @cakelover I think that is what `body_attach_object_instance_id` is for. Try `Physics2DServer.body_attach_object_instance_id(_body, self.get_instance_id())`. – Theraot Mar 30 '22 at 19:14
  • How do i get the signals in `_body_monitor()` to send the node which entered as well? – cak3_lover Apr 08 '22 at 14:18
  • 1
    @cakelover I believe the code in the answer does that already, is it not working? – Theraot Apr 08 '22 at 14:26