Search code examples
godotgdscriptgodot3

Area2D not detecting bodies when moved along with sync_to_physics enabled KinematicBody2D


A continuation of this question,

The solution provided there by @Theraot works for _draw() but not for Area2D as you can see in this example:

enter image description here

script for player:

extends KinematicBody2D

var velocity: Vector2

func _physics_process(_delta):
    velocity.y+=30
    move_and_slide(velocity)

script for Holder:

extends Node2D

onready var item = $Item
onready var item_area = $ItemArea
onready var item_inital_offset = item.global_position - global_position
onready var area_inital_offset = item_area.global_position - item.global_position

var velocity: Vector2

func _body_entered(body):
    print("body_entered=>", body)

func _body_exited(body):
    print("body_exited=>", body)

func _physics_process(delta):
    velocity.y+=30
    global_position+=velocity * delta
    
    item.global_position = global_position + item_inital_offset
    item_area.global_position = item.global_position + area_inital_offset

func _ready():
    item_area.connect("body_entered", self,"_body_entered")
    item_area.connect("body_exited", self,"_body_exited")

when the Player (green) falls into the ItemArea (blue) and stops when it hits Item (red), the body_entered signal is not emitted even though you can clearly see that it entered the area

enter image description here

I have checked that the Area2D node works by initially placing another body in it and it emits a signal just fine

my best guess is that maybe since both Holder & Player are both falling at the same speed they technically never intersect but since Item is synced_with_physics there's a visual lag which makes it drag back and not take the ItemArea along with it


Edit

I tried using intersect_shape directly within Holder but it still didn't work:

extends Node2D

onready var item = $Item
onready var item_inital_offset = item.global_position - global_position
const SHAPE_OFFSET = Vector2(0, -52)

export(Shape2D) var intersect_shape: Shape2D

var intersecting_bodies := {}
var velocity: Vector2

func _body_entered(body):
    print("body_entered=>", body)
func _body_exited(body):
    print("body_exited=>", body)
func _physics_process(delta):
    velocity.y+=30
    global_position+=velocity * delta
    
    item.global_position = global_position + item_inital_offset
    
    check_area_process(delta)
    
    update()
func check_area_process(_delta):
    var space := get_world_2d().direct_space_state
    var query := Physics2DShapeQueryParameters.new()
    
    query.set_shape(intersect_shape)
    query.transform = item.global_transform
    query.transform.origin = item.global_position + SHAPE_OFFSET
    
    var currently_intersecting_nodes:Array = space.intersect_shape(query)
    var new_intersecting_bodies := {}
    
    # check for newly entered objects
    for node in currently_intersecting_nodes:
        node=node.collider
        new_intersecting_bodies[node]=null
        
        if(!intersecting_bodies.has(node)):
            _body_entered(node)
    
    # check for exited objects
    for node in intersecting_bodies:
        if(!new_intersecting_bodies.has(node)):
            _body_exited(node)
    
    intersecting_bodies=new_intersecting_bodies
func _draw():
    if(get_tree().debug_collisions_hint):
        draw_set_transform(to_local(item.global_position + SHAPE_OFFSET), 0, Vector2.ONE)
        intersect_shape.draw(get_canvas_item(), Color(1, 1, 1, 0.5))


Solution

  • I remember an issue about areas not detecting bodies when they were moved. However, I have been unable to find it.

    We can use force_update_transform on the objects that moved (the body and the area). But - sadly - there isn't an equivalent to force_raycast_update for the area.


    So, I'll go ahead and give you the solution without area.

    We will work on your holder script, because there is where you want to use the area...

    For ease of use, I'll say have these exports:

    export(int, LAYERS_2D_PHYSICS) var collision_mask:int
    export(Shape2D) var shape_2d:Shape2D
    

    Then, in _physics_process you a Physics2DDirectSpaceState like this:

    var space := get_world_2d().direct_space_state
    

    Then you need to create a Physics2DShapeQueryParameters like this:

    if shape_2d == null:
        return
    
    var space := get_world_2d().direct_space_state
    var query := Physics2DShapeQueryParameters.new()
    query.collision_layer = collision_mask
    query.set_shape(shape_2d)
    query.transform = global_transform
    

    Addendum: I realize that you have the area offset from the holder, so you might want to compute an offset transformation for that. On a similar note, I remind you that the exported variables are for ease of use, but you can define those values in code if that makes more sense to you.

    And then we can query what bodies are overlapping the shape:

    if shape_2d == null:
        return
    
    var space := get_world_2d().direct_space_state
    var query := Physics2DShapeQueryParameters.new()
    query.collision_layer = collision_mask
    query.set_shape(shape_2d)
    query.transform = global_transform
    currently_overlapping_bodies:Array = space.intersect_shape(query)
    

    We can figure out what entered and what left... For that we need to add a variable on top (outside of _physics_process) to keep track of the overlapping bodies:

    var overlapping_bodies:Array = []
    

    And now, in _physics_process we can do this:

    if shape_2d == null:
        return
    
    var space := get_world_2d().direct_space_state
    var query := Physics2DShapeQueryParameters.new()
    query.collision_layer = collision_mask
    query.set_shape(shape_2d)
    query.transform = global_transform
    
    currently_overlapping_bodies:Array = space.intersect_shape(query)
    exited:Array = []
    entered:Array = []
    for body in overlapping_bodies:
        if not body in currently_overlapping_bodies:
            exited.append(body)
    
    for body in currently_overlapping_bodies:
        if not body in overlapping_bodies:
            entered.append(body)
    
    overlapping_bodies = currently_overlapping_bodies
    

    Be aware that since you wouldn't be using area anymore, it would not show up in the editor, nor while debugging with Visible Collision Shapes enabled. Thus, you need to use _draw to have some representation of it. Be aware that Shape2D has a draw method that you can use for this:

    func _draw() -> void:
        if shape_2d == null or not get_tree().debug_collisions_hint:
            return
    
        shape_2d.draw(get_canvas_item())
    

    Addendum: Since you might want to draw the shape offset, you might want to use draw_set_transform.

    And, of course, remember to call update.