3
\$\begingroup\$

I'm building a type of Pinball game using Godot 4.0. I've got two types of components relevant to this question:

  • balls, represented as a RigidBody2D scene with Sprite and circular CollisionShape2D children
  • circular bumpers, represented as a StaticBody2D scene with Sprite and circular CollisionShape2D children

When a ball hits any object, that other object should be "in charge" (code-wise) of what happens; at least, to get some clean code. However, the body_exited signal is only available on the balls' node type, but not available on StaticBody2D.

I'd want to write (pseudo-code):

# In StaticBody2D BumperCircular.gd:
func _on_body_exited(body):
    if is_ball(body):
        # add bonus velocity because of the bump

I feel like I'm missing something, because it seems wrong to add body_exited on the Ball and then dispatch/delegate back to the other body for logic on what happens next.

So I feel like I have to write:

# In RigidBody2D Ball.gd:
func _on_body_exited(body):
    if is_bumper(body):
        # logic for bumper coded inside this script
    else if is_some_component(body):
        # logic for component type 2 coded inside this script
    else if is_some_component(body):
        # logic for component type 3 coded inside this script
    # etc.
        # etc.

Or at best:

# In RigidBody2D Ball.gd:
func _on_body_exited(body):
    if has_ball_collision_logic(body):
        body.on_ball_collision_logic(this) # dispatch to the other object for logic

But both seem to me the wrong way around.

What am I missing here? Why is there no body_exited on StaticBody2D?

PS. I've searched this Stack Exchange site and online in general, plus I've peered through the (inheritance tree of the) StaticBody2D documentation but found no relevant info yet.

\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Note: even thought this answer is written for 2D, equivalent approaches would work for 3D.


Solution 1: Use a RigidBody2D.

You can set the freeze property of your RigidBody2D to true, and its freeze_mode to FREEZE_MODE_STATIC which will make it not move just like an StaticBody2D.

However, apparently the signals won't be enabled unless you set it to FREEZE_MODE_KINEMATIC.


Solution 2: Duck type it.

This idea would almost work:

# In RigidBody2D Ball.gd:
func _on_body_exited(body):
    if has_ball_collision_logic(body):
        body.on_ball_collision_logic(this)

To make it work, you can check for the particular method you are going to call:

func _on_body_exited(body):
    if body.has_method(&"on_ball_collision_logic"):
        body.on_ball_collision_logic(this)

Solution 3: Give an Area2D to your StaticBody2D (or CharacterBody2D)

Your StaticBody2D (or CharacterBody2D) can't detect collisions, but an Area2D can. So you can add Area2D as a child of it, in such way that it has identical position and colliders, it can detect the collisions for the StaticBody2D (or CharacterBody2D).


Solution 4: Make the StaticBody2D (or CharacterBody2D) detect collisions.

We are going to add an _integrate_forces callback to the StaticBody2D (or CharacterBody2D) using body_set_force_integration_callback:

func _ready() -> void:
    PhysicsServer2D.body_set_force_integration_callback(
        get_rid(),
        _integrate_forces
    )

func _integrate_forces(PhysicsDirectBodyState2D state, userdata) -> void:
    pass

Next we will ask PhysicsDirectBodyState2D for contacts, but for that to work we need to set how many contacts we want to get:

func _ready() -> void:
    var body_rid := get_rid()
    PhysicsServer2D.body_set_force_integration_callback(
        body_rid,
        _integrate_forces
    )
    body_set_max_contacts_reported(body_rid, 32)

func _integrate_forces(PhysicsDirectBodyState2D state, userdata) -> void:
    for index in state.get_contact_count():
        var body := state.get_contact_collider_object(index) as PhysicsBody2D

And then we will keep track of them, so we can emit signals:

signal body_entered(body:Node)
signal body_exited(body:Node)

var colliding_bodies := {}

func _ready() -> void:
    var body_rid := get_rid()
    PhysicsServer2D.body_set_force_integration_callback(
        body_rid,
        _integrate_forces
    )
    body_set_max_contacts_reported(body_rid, 32)

func _integrate_forces(PhysicsDirectBodyState2D state, userdata) -> void:
    var new_colliding_bodies := {}
    for index in state.get_contact_count():
        var body := state.get_contact_collider_object(index) as PhysicsBody2D
        if not is_instance_valid(body):
            continue

        new_colliding_bodies[body] = body
        if not colliding_bodies.has(body):
            body_entered.emit(body)

    for body in colliding_bodies.keys():
        if not new_colliding_bodies.has(body):
            body_exited.emit(body)

    colliding_bodies = new_colliding_bodies

And there you have body_entered and body_exited added to your StaticBody2D (or CharacterBody2D).


Why is there no body_exited on StaticBody2D?

The engine does not need to put the work for the StaticBody2D body to detect collisions because the StaticBody2D won't react to collisions. So these are not offered out of the box.

\$\endgroup\$
4
  • \$\begingroup\$ Wow! Thanks for the comprehensive answer. Gives me something to work with 👍, plus taught me a more generic thing or two! \$\endgroup\$
    – Jeroen
    Mar 8, 2023 at 13:02
  • \$\begingroup\$ PS. I went with option 2 for the moment, but I did need to use self instead of this everywhere, or I'd get "Identifier 'this' not declared in the current scope." - perhaps a 4.0 thing? \$\endgroup\$
    – Jeroen
    Mar 8, 2023 at 13:12
  • \$\begingroup\$ @Jeroen no, it is self in GDScript for Godot 3.x or 4 (it would be this in C#, but this is not C#). I just assumed you had some variable called this. \$\endgroup\$
    – Theraot
    Mar 8, 2023 at 13:13
  • \$\begingroup\$ For solution 1 to work, I actually had to set freeze_mode to FREEZE_MODE_KINEMATIC to receive _on_body_entered and _on_body_exited signals. No signals received when freeze_mode is FREEZE_MODE_STATIC. \$\endgroup\$
    – zett42
    Mar 16, 2023 at 17:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .