Collision detection and overlapping detection in same node?
Asked Answered
L

1

3

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.

Lupien answered 24/3, 2022 at 19:32 Comment(11)
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 Nodes altogether and talking to Physics2DServer to be better. I might write an answer later.Invidious
@Invidious 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 beginnersLupien
Yeah, it isn't for beginners, I still don't know all its ins and outs. But the short version is that Nodes 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 Nodes or I can make a bunch of Physics2DServer calls that accomplish the same thing, but without creating Nodes, which means less overhead. Thus if your goal is performance, using Physics2DServer should be better.Invidious
@Invidious 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?Lupien
Yes, No and yes. You don't need Nodes 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 Nodes. 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.Invidious
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 Nodes per bullet, instead it talks to Physics2DServer and VisualServer. So adding more bullets does not mean allocating more Nodes, which means less overhead, which means more performance, which means the game can have more bullets in play.Invidious
@Invidious 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 you can see the limbs becoming rigid bodies after blowing upLupien
@Invidious 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 enabledLupien
StackOverflow says Body is limited to 30000 characters; you entered 37369. Ha!Invidious
@Invidious desperate times call for desperate measures ¯_(ツ)_/¯Lupien
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.Invidious
I
6

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

Invidious answered 25/3, 2022 at 19:47 Comment(16)
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 sort of custom dot connect polygon feature like in CollisionPolygon2D?Lupien
@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.Invidious
what is body_set_state()? , what exactly is state suppose to be? what is the difference between ID and RID? I made this bare minimum code and added some comments, could you see if my understanding is accurate?Lupien
Also how do I detect and send signals when the body has collided?Lupien
@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.Invidious
@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 Resources, 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.Invidious
@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.Invidious
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?Lupien
@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.Invidious
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 :)Lupien
@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?Invidious
here's the continuation questionLupien
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]Lupien
@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()).Invidious
How do i get the signals in _body_monitor() to send the node which entered as well?Lupien
@cakelover I believe the code in the answer does that already, is it not working?Invidious

© 2022 - 2024 — McMap. All rights reserved.