0

A continuation of the previous question

How exactly do we detect collision from body_set_force_integration_callback?

cak3_lover
  • 1,440
  • 5
  • 26

1 Answers1

1

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

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • for some reason the contacts are not being detected, [sample code](https://pastebin.com/dSdrCgkL) – cak3_lover Mar 27 '22 at 10:29
  • @cakelover Ah, I forgot we need to call `body_set_max_contacts_reported`. – Theraot Mar 27 '22 at 10:31
  • also I think you forgot `global_transform = state.transform` in `_body_moved`, isn't that responsible for actually changing the position? – cak3_lover Mar 27 '22 at 10:38
  • @cakelover The position of the `Node2D` yes, but not the body. Godot moves the body, it reports it in the callback and then we update the position of the `Node2D`. To be fair, in this answer I don't even go over adding a shape, and there would be other setup to do (i.e. `body_set_state` and `body_set_param`, edit: and `body_set_collision_layer` and `body_set_collision_mask`) but the other prior answer covers that. – Theraot Mar 27 '22 at 10:44
  • I made this [custom code](https://pastebin.com/VrnZ7xGC) [image](https://i.ibb.co/Bcz9j2J/image.png) and the blue `RigidBody2D` falls faster than the green custom Box (collision disabled) and I connected a signal to print when blue box passes through the green box, but it doesn't work – cak3_lover Mar 27 '22 at 12:31
  • 1
    @cakelover About the speed, set `inertia` to `1`. For the other issue, comes in four parts 1. you need this line `Physics2DServer.area_add_shape(_area, _shape, Transform2D.IDENTITY, disabled)` in `_update_shape`, which also means you need to make the `_shape` first. 2. I did overlook that we need to call `Physics2DServer.area_set_transform(_area, global_transform)` on `_body_moved`. 3. Apparently we need to set monitorable too? 4. I'm using instance wrong, I already have an errata there. I should rewrite that part of the answer, but it needs a different solution I haven't figured out. – Theraot Mar 27 '22 at 13:45
  • @cakelover About the instance problem, the issue is that it should refer to the `PhysicsBody2D` but first I should be indexing by the `RID`, and second I was expecting it to be the `Node`, but I get an instance id, and I don't have a way to go from the instance id to the `PhysicsBody2D` (there is a class called `ObjectDB` that does it, but it is not exposed to GDScript). Edit: I just found `instance_from_id`, I try to do it with that, if it works, I'll update the answers. – Theraot Mar 27 '22 at 13:48
  • @cakelover I have updated the answers. – Theraot Mar 27 '22 at 14:29
  • I have my script [Only Area.gd](https://pastebin.com/wnLgfn7a) and my layout like [this](https://i.ibb.co/xjtcJZW/image.png) were my `RigidBody2D`(blue) is falling faster than `Only Area` (white) in such a case the signal `body_entered()` is sent BUT when my layout is like [this](https://i.ibb.co/2dDWTYL/image.png) and the `Only Area` is falling faster than `RigidBody2D` the signal `body_entered()` is NOT sent, relatively speaking the `RigidBody2D` IS entering `Only Area` so how do I fix that? – cak3_lover Mar 29 '22 at 03:44
  • @cakelover Is the callback working? I see you use `collision_disabled` in `_update_monitoring` but you don't call `_update_monitoring` from `set_disabled`. If the callback is working you should be getting `the "body_shape_entered"` signal. I guess I could have done something dumb with the code, just reading it now I see that `"body_exited"` is wrong, because there is no check that all the shapes exited (which is not a problem if there is only one shape per body, but still wrong). But I don't know why `"body_entered"` would be failing. – Theraot Mar 29 '22 at 04:01
  • Well Im guessing it has something to do with the direction because technically the `RigidBody2D` isn't going towards the `Only Area` in the second case but instead the area is coming towards it, I don't know enough to inspect the source code – cak3_lover Mar 29 '22 at 04:37
  • 1
    @cakelover I think I figured out the issue: You are calling `area_set_transform` on `_enter_tree` and no where else. So the body is moving but the area is not. Call `area_set_transform` on `_body_moved` as I said above and/or call it form `_notification`. Otherwise the area is being left behind by the body. – Theraot Mar 29 '22 at 04:47
  • *oof* How did I miss that! Now it works great, thanks! also one more thing, I added `Physics2DServer.area_set_transform(_area, global_transform)` in `_body_moved` will it reduce performance? could you take a look at this [final code](https://pastebin.com/iTs3sAft) and tell me if there's any performance killer or something that might slow down as I expand my project? – cak3_lover Mar 29 '22 at 06:37
  • @cakelover Reduce performance compared to what? We need an alternative to compare with. So, I experimented with `body_attach_object_instance_id` with no result. The other idea is using `_physics_process` to do the same job that `_notification` and `_body_moved` is doing now. That would mean losing the ability to move the node independently (and for what I was doing, you cannot write a `_physics_process` in the script that extends `FauxBody2D`), but perhaps it performs better, I'm not sure. – Theraot Mar 29 '22 at 10:27
  • @cakelover What does your profiler say takes more time? – Theraot Mar 29 '22 at 10:52
  • well I don't have a frame of reference, I was just curious if there was an even more efficient method, if not then it's still fine – cak3_lover Mar 29 '22 at 16:23
  • 1
    @cakelover Similar code but in C++ is the first thing to comes to mind. Are you handling many of these objects? You could handle them in the same `Node2D`, I suppose afterward you could try threading. Also, if I recall correctly you were doing some complex animations, I don't know if you want to hard-code all that and use `VisualServer` (no, it does not do animations, that is why you would have to hard code them, then again, it is probably better to use C++). – Theraot Mar 29 '22 at 20:09
  • Well as you can see by all the previous questions I've asked, I'm making some really complex animations and movements but they can all be simplified into basic P `position`,R `rotation` & S `scale` so currently I'm setting up tools which will help me create these complex animations and then translate them into simpler P.R.S. so that the user's computer will not have to take much load – cak3_lover Mar 30 '22 at 10:10
  • @cakelover I just remembered something: you could make the animations in GPU. If you make a vertex shader that performs the right transform over time, it would take no CPU time (which is great for crowd simulations). If the animation is complex enough, you can store the animation in a texture. For example the x of the texture is time, and store the transform array in a few pixels vertically, perhaps have an vertical offset for each vertex so you can do deformations too. You of course need some process that goes over the animations and encode them, which is like what you are doing already. – Theraot Mar 30 '22 at 17:39