godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.15k stars 97 forks source link

Add a `force_area_update()` method to Area2D/3D to immediately update colliding areas and bodies #3111

Open fire-forge opened 3 years ago

fire-forge commented 3 years ago

Describe the project you are working on

A 2D tower-defense game

Describe the problem or limitation you are having in your project

I am using an Area2D to detect if a tower would be colliding with something before allowing the player to place a tower there. In order to detect collision objects in an Area2D right after moving it, you have to wait one physics process frame for it to update.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

A new function for Area2D and Area3D called force_area_update, which would update immediately update the collision areas and bodies without waiting until the next physics frame (similar to RayCast2D.force_raycast_update)

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

It will work like RayCast2D.force_raycast_update: when you call the function, colliding areas and bodies are calculated immediately, making get_overlapping_areas and get_overlapping_bodies return accurate information when called within the same physics frame.

Example: area.position = Vector2() Change area position print(area.get_overlapping_bodies()) Prints bodies that were colliding with the Area2D at its previous position area.force_area_update() Force Area2D to update without waiting until the next physics frame print(area.get_overlapping_bodies()) Prints bodies that are colliding with the Area2D at its new position

If this enhancement will not be used often, can it be worked around with a few lines of script?

Waiting until the next physics frame to use get_overlapping_areas/bodies works, but is not always desirable.

Is there a reason why this should be core and not an add-on in the asset library?

It is not possible to modify the behavior of built-in nodes without modifying the engine code, so it couldn't be made as an add-on.

Xrayez commented 3 years ago

Since you mentioned RayCast2D, alternatively this could be done with ShapeCast2D, see my proposal #710.

For reference, I've already implemented ShapeCast2D in Goost builds (not officially maintained by Godot core developers).

wareya commented 3 years ago

I second this. Lacking the ability to use Area2Ds with no latency causes a lot of headaches when trying to make trigger areas, probing areas ("is there a wall of this particular type three pixels to my left/right?") (no, checking previously-hit collision surfaces isn't the same as this), etc. work properly on a frame-by-frame basis, and I ran into problems that this would've solved for pretty much all of the last four to five small game projects I've worked on (and imagine what a really big game project has to deal with).

pouleyKetchoupp commented 3 years ago

Yes, adding the possibly to get immediate overlapping results from a shape seems reasonable.

It would be pretty hacky to add to Area2D, but as @Xrayez suggested, implementing #710 would be perfect to solve this problem.

fire-forge commented 3 years ago

Adding shapecasting would solve most use cases of this proposal. If shapecasting was to be added, using a shapecast with a length of 0 and calling force_shapecast_update would essentially be the same as using an Area2D + CollisionShape2D.

DataPlusProgram commented 2 years ago

I have a use case that wouldn't be solved neatly by shapecasts:

This is an image

All these Area2D's are created at runtime and I want to check each one to see which ones overlap which before the first physics tick.

If I wanted to use shapecasts I would need to go over each area and make a duplicate cast for it. Which adds to the complexity of both the implementation and to the scene. It might be even more complex since I think the CollisionPolygon2D uses vertices directly and not a shape like ConcavePolygonShape2D.

Calinou commented 2 years ago

@DataPlusProgram Since these areas are effectively all 2D polygons, wouldn't the Geometry2D singleton be able to check for overlaps without relying on the physics server?

DataPlusProgram commented 2 years ago

Geometry class can check if a given polygon is in another. I could iterate through each polygon and check it with every other polygon but that is n2 complexity getting exponentially slower with each polygon in the scene.

I don't know exactly how Godot checks overlaps but I'm guessing it at least does some sort AABB. I could try and hand write these optimizations into my GDscript but it would take time and research to end up with a solution that would be worse than Godots.

Also if I rely on Godots internal physics system any future optimizations made in it would be automatically reflected in my script.

nonunknown commented 2 years ago

this should be implemented for Area3D also area.force_area_update() its a feature I missed since I've started using godot, as a workaround I've implemented my custom Area system, thanks to AABB class box overlaping method helped a lot!

fire-forge commented 2 years ago

@nonunknown I agree that there should be parity between Area3D and Area2D. I'll update the proposal to include Area3D too.

Also, I think this could be done in scripts with the PhysicsServer2D/3D API (which I didn't know about at the time when I wrote this proposal).

nonunknown commented 2 years ago

thanks, also can you provide the workaround to force update? this will help me a lot and maybe help me implement this in the core!

fire-forge commented 2 years ago

@nonunknown I don't have the exact code for it, I just think it could be done using the PhysicsServer2D/3D functions in a similar way to RayCast2D/3D but with a collision shape instead of a ray.

jitspoe commented 1 year ago

I just ran into a case where this would be nice. I'm trying to do splash damage for a rocket launcher and want to just recycle one Area3D for projectiles so I move it to where the rocket exploded and find overlapping objects. Unfortunately, since it doesn't update right away, I'd either have to do some more complex PhysicsServer stuff or delay the check, which would be problematic if multiple explosions happen in the same frame. Also have to ensure that the projectile/weapon/etc. stick around and don't get freed before the splash damage check.

jitspoe commented 1 year ago

Ok, I've dug into this a little more and discovered that it's possible to force the position to update immediately on any Node3D using force_update_transform(), HOWEVER, this does not update the list of overlapping bodies immediately because that gets updated in the Area3D::_body_inout() callback function which is called from GodotPhysicsServer3D::flush_queries()

This seems to suggest that even attempting to do this directly with the PhysicsServer3D would not work, as the callbacks would not be executed until the next physics frame.

jitspoe commented 1 year ago

This seems to be the least painful way to work around the issue for now. Throw this in an Autoload / global singleton script. I have one I call "Physics".

@onready var global_world : World3D = get_tree().current_scene.get_world_3d()
@onready var space_state := global_world.direct_space_state

func _ready():
    sphere_query.shape = SphereShape3D.new()

# returns dictionary containing "position", "normal", "collider", "shape"
# returns empty dictionary if nothing hit.
func cast_ray(start : Vector3, end : Vector3, mask : int = 0x7FFFFFFF, ignore_objects := []) -> Dictionary:
    ray_cast_query.from = start
    ray_cast_query.to = end
    ray_cast_query.exclude = ignore_objects
    ray_cast_query.collision_mask = mask
    return space_state.intersect_ray(ray_cast_query)

# Returns array of dictionaries containing "rid", "collider_id", "collider", and "shape"
func sphere_test(position : Vector3, radius : float, mask : int = 0x7FFFFFFF, ignore_objects := []) -> Array[Dictionary]:
    sphere_query.collision_mask = mask
    sphere_query.exclude = ignore_objects
    sphere_query.shape.radius = radius
    sphere_query.transform = Transform3D(Basis(), position)
    return space_state.intersect_shape(sphere_query)

(Threw in the ray cast as a bonus)

Then I can just do something like:

    var sphere_overlaps := Physics.sphere_test(position, splash_radius, Physics.PHYS_COMBINED_MASK_SPLASH_DAMAGE)
    for overlapped_info in sphere_overlaps:
        if (deal_damage(overlapped_info["collider"], position, weapon_owner, weapon, damage_amount, impulse)):
            damage_dealt = true
    return damage_dealt

I do really think functions like this should just be built into the engine as doing simple raycasts and shapecasts are commonplace in game logic.

nonunknown commented 1 year ago

@jitspoe doesnt the shapecast in GD4 solve this workaround?

jitspoe commented 1 year ago

@jitspoe doesnt the shapecast in GD4 solve this workaround?

Looks like that would also work, yes.

PaulMag commented 1 year ago

ShapeCast2D solves this problem as a replacement for Area2D (except in DataPlusProgram's special case where he already has a bunch of Area2Ds).

It doesn't directly give you a list of the overlapping areas/bodies like Area2D though, so you have to make a small iteration to get the equivalent of Area2D's get_overlapping_areas/bodies. Here is an example of how to do it:

func _ready():
    $ShapeCast2D.target_position = Vector2.ZERO
    $ShapeCast2D.collide_with_areas = true   # You probably want of these as true
    $ShapeCast2D.collide_with_bodies = true  # and the other as false.

func get_nodes_from_shapecast() -> Array:
    $ShapeCast2D.position = Vector2(position_x, position_y)  # Where you want your box to be.
    $ShapeCast2D.shape.size = Vector2(size_x, size_y)        # How big you want your box to be.
    $ShapeCast2D.force_shapecast_update()  # The magic sauce.

    var number_of_nodes = $ShapeCast2D.get_collision_count()  # Necessary to know how many times to iterate.
    var nodes_in_shapecast = []

    for i in range(number_of_nodes):
        var node = $ShapeCast2D.get_collider(i)  # Here you get one node at a time from the ShapeCast2D
        nodes_in_shapecast.append(node)          # and put them in a list.

    return nodes_in_shapecast

The stuff I put in the _ready function here you could just do in the inspector instead. "Target Position" must be set to [0, 0] for the ShapeCast2D to behave like Area2D's get_overlapping_areas/bodies, otherwise it behaves more like a RayCast2D with a direction. You can set "Collide With" to either "Areas" or "Bodies" or both, depending on what you need.

Here I am using a RectangleShape2D as ShapeCast2D's shape. If you use f.ex. a CircleShape2D instead you set $ShapeCast2D.shape.radius instead of $ShapeCast2D.shape.size.

I haven't tested in 3D, but I imagine everything would work the same there.