Search code examples
godotgdscript

Collision detection and overlapping detection in same node? [part 2]


A continuation of the previous question

How exactly do we detect collision from body_set_force_integration_callback?


Solution

  • For context, we have a body RID:

    var _body:RID
    

    And we set a callback with body_set_force_integration_callback:

    Physics2DServer.body_set_force_integration_callback(_body, self, "_body_moved", 0)
    
    func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
        pass
    

    Before going further, I want to point out that the the final parameter of body_set_force_integration_callback is what we get in _user_data. But, if I set it to null Godot will not pass two arguments to the call, in which case I should define _body_moved with only the state parameter.

    Godot will be calling our _body_moved every physics frame if the state of the body is active (i.e. not sleeping).


    Note: We need to call body_set_max_contacts_reported and set how many contacts we want reported, for example:

    Physics2DServer.body_set_max_contacts_reported(_body, 32)
    

    Now, in the Physics2DDirectBodyState we get contacts, and we can ask what a few things about each contact, including the body:

    func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
        for index in state.get_contact_count():
            var body:RID = state.get_contact_collider(index)
            var instance:Object = state.get_contact_collider_object(index) 
    

    If it is the body of a PhysicsBody2D, then instance will have it.

    If we want to implement body_entered and body_exited, we need to keep track of the bodies. I'll keep a dictionary of the instances (i.e. PhysicsBody2D) and I'll use it to report get_colliding_bodies too.

    Then we need to keep track of shapes for body_shape_entered and body_shape_exited, not only bodies. We can find them out like this:

    func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
        for index in state.get_contact_count():
            var body:RID = state.get_contact_collider(index)
            var instance:Object = state.get_contact_collider_object(index) 
            var body_shape_index:int = state.get_contact_collider_shape(index)
            var local_shape_index:int = state.get_contact_local_shape(index)
    

    Notice they are not RID. They are the position of the shape in the body (so 0 is the first shape of the body, 1 is the second one, and so on). This means that we cannot keep track of the shapes separately from the bodies, because they shape indexes do not make sense without knowing to what body they belong. That means we cannot simple use two arrays of bodies like we did before.

    Also, if we only have one shape - which was the case of the prior answer - we could ignore local_shape_index because it is always 0. In which case I only need a Dictionary indexed by body:RID of body_shape_index:int.

    If I don't take that assumption, I struggle to decide the data structure.

    • I could use a Dictionary indexed by body:RID of Dictionary indexed by body_shape_index:int of local_shape_index:int, in which case I want helper methods to deal with it, which pushes me to make a class for it.
    • I could use a Dictionary indexed by body:RID of tuples of body_shape_index:int and local_shape_index:int. Except there is no tuple type, so I would cheat and use Vector2.

    You know what? I'll cheat and use the Vector2.

    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)
    
    var colliding_instances:Dictionary = {}
    var colliding_shapes:Dictionary = {}
    
    func _body_moved(state:Physics2DDirectBodyState, _user_data) -> void:
        var old_colliding_shapes:Dictionary = colliding_shapes
        var new_colliding_shapes:Dictionary = {}
        colliding_shapes = {}
        var instances:Dictionary = {}
        for index in state.get_contact_count():
            # get contact information
            var body:RID = state.get_contact_collider(index)
            var instance:Object = state.get_contact_collider_object(index) 
            var body_shape_index:int = state.get_contact_collider_shape(index)
            var local_shape_index:int = state.get_contact_local_shape(index)
            var vector := Vector2(body_shape_index, local_shape_index)
    
            # add to instances
            instances[body] = instance
    
            # add to colliding_shapes
            if not colliding_shapes.had(body):
                colliding_shapes[body] = [vector]
            else:
                colliding_shapes[body].append(vector)
    
            # remove from old_colliding_shapes or add to new_colliding_shapes
            # there is room for optimization here
            if (
                old_colliding_shapes.has(body)
                and old_colliding_shapes[body].has(vector)
            ):
                old_colliding_shapes[body].erase(vector)
                if old_colliding_shapes[body].size() == 0:
                    old_colliding_shapes.erase(body)
            else:
                if not new_colliding_shapes.had(body):
                    new_colliding_shapes[body] = [vector]
                else:
                    new_colliding_shapes[body].append(vector)    
    
        for body in old_colliding_shapes.keys():
            # get instance from old dictionary
            var instance:Object = colliding_instances[body]
            # emit
            if not instances.has(body):
                emit_signal("body_exited", body)
    
            for vector in old_colliding_shapes[body]:
                emit_signal(
                    "body_shape_exited",
                    body,
                    instance,
                    vector.x,
                    vector.y
                )
    
        for body in new_colliding_shapes.keys():
            # get instance from new dictionary
            var instance:Object = instances[body]
            # emit
            for vector in old_colliding_shapes[body]:
                emit_signal(
                    "body_shape_entered",
                    body,
                    colliders[body],
                    vector.x,
                    vector.y
                )
    
            if not colliding_instances.has(body):
                emit_signal("body_entered", body)
    
        # swap instance dictionaries
        colliding_instances = instances
    
    func get_colliding_bodies() -> Array:
        return colliding_instances.values()
    

    The variable old_colliding_shapes begins with the shapes already known to be colliding, and in in the iteration we are removing each one we see. So at the end, it has the shapes that were colliding but no longer are.

    The variable new_colliding_bodies begins empty, and in the iteration we are adding each shape we didn't remove from old_colliding_shapes, so at the end it has the shapes that are colliding that we didn't know about before.

    Notice that old_colliding_shapes and new_colliding_bodies are mutually exclusive. If a body is in one it is not in the other because we only add the body to new_colliding_bodies when it is not in old_colliding_shapes. But since they have shapes and not bodies a body can appear in both. This why I need an extra check to emit "body_exited" and "body_entered".