Player collisions not working as expected when destroying blocks

Godot Version

4.3

Question

Hello everyone,

I’m currently working on a platformer game using Godot 4.3 and I’ve encountered an issue with player collisions that I haven’t been able to resolve. I’d appreciate any help or suggestions you might have.

Problem Description: In my game, there’s a platform that breaks when the player collides with it at high speed. To implement this, I calculate collisions using get_slide_collision_count and get_slide_collision. When the player collides with the block, I check if the player is running. If they are, the block is destroyed. However, the player should destroy the block and pass through it. Instead, the player collides with the block and is pushed back, even though the block is destroyed.

Attempts to Solve the Issue

Using move_and_collide with test_only = true: I attempted to use move_and_collide with test_only = true to detect collisions without moving the player. However, this only returns the first collision, which fails when the player collides with both a block and the ground simultaneously.

Handling Multiple Collisions with move_and_slide: I tried using move_and_slide and iterating through collisions with get_slide_collision, but since get_slide_collision only returns collisions that have already occurred, it triggers the on_wall state, breaking the movement logic. Additionally, stopping the player and making them move again doesn’t seem like an optimal solution.

Using an Auxiliary Area2D: I also experimented with using an auxiliary Area2D to detect collisions. This method worked, but since Area2D isn’t synchronized with the physics, it had to be significantly larger to detect collisions before the player collided with the RigidBody2D. Determining the ideal size for the Area2D has proven challenging and could lead to incorrect results if not sized perfectly.

At this point, I’m running out of ideas on how to solve this problem effectively. If anyone has encountered a similar issue or has suggestions on how to approach this, I’d greatly appreciate your input.

Thank you in advance for your help!

Could you perhaps cache the velocity from before move_and_slide() and reapply it after the collision? It might not be ideal, but should be unnoticable when playing. You just need to add the checks for your “destroyable platforms”, etc, to reapply the velocity only in case the platform is destroyed.

extends CharacterBody2D
func _physics_process(delta: float) -> void:
	velocity += get_gravity() * delta
	var last_velocity = velocity
	move_and_slide()
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		(collision.get_collider() as Node).queue_free()
		velocity = last_velocity

1 Like

Thanks for the response!

In this case, I would still have issues because the on_wall method will return true, and there are other mechanics that use this value. I would need to reset not just the speed, but also on_wall and on_floor if the only collision is with the destructible block. I’m not sure if there’s a way to do this.

This is surprisingly harder than it seems at the beginning. We are missing something like “get collisions without moving” thingy for this to be very trivial.

But here’s my idea that could work:

  1. Similarly to your last idea - create a detection Area2D as a child of your Player, with the same shape and size as your Player.
  2. Put your Breakable Block to a separate Collision Layer - I’ll use Layer 2 in my example.
  3. Based on the current velocity of your player, turn on/off its Collision Mask to match the Breakable Block’s Collision Layer - I’ll use Mask 2 in my example. Also, enable/disable the monitoring of the Area2D.
  4. Use Area2D to detect and queue_free() Breakable Blocks that enter the area.

This is the code I used:

extends CharacterBody2D

var threshold_velocity: float = 500.0

func _ready() -> void:
	$Area2D.body_entered.connect(_on_body_entered)

func _physics_process(delta: float) -> void:
	velocity += get_gravity() * delta
	
	var can_break = velocity.length() >= threshold_velocity
	set_collision_mask_value(2, not can_break)
	$Area2D.monitoring = can_break
	
	move_and_slide()

func _on_body_entered(body: Node) -> void:
	body.queue_free()

This is the scene structure


Here’s a demo video, Player to the left doesn’t have enough velocity, so is stopped by the block, Player to the right has enough velocity, so “breaks” through the block.

1 Like

Thank you so much for your help!

I ended up adopting another approach:

var collision := KinematicCollision2D.new()
if test_move(transform, velocity * delta, collision, 0.1, true):
    if collision.get_collider().has_method('hit'):
        collision.get_collider().hit(self)

move_and_slide()

By using test_move, I can anticipate collisions and handle them before actually moving the character.

Your suggestion would also work, but there’s a detail I forgot to mention: the character breaks the block when running and hitting it head-on. However, if the character is running on top of the block, it doesn’t break and functions as a floor. Because of this, I couldn’t disable the collision layer.

Anyway, thanks a lot for your help!

1 Like

What happens in your new solution when you hit the ground and the block at the same time? Wouldn’t you run into the same problem as you described in your initial post?

1 Like

Good point! This way also works:

	var collision := move_and_collide(velocity * delta, true, 0.1, true)
	if collision:
		if collision.get_collider().has_method('hit'):
			collision.get_collider().hit(self)

Using move_and_collide with the recovery_as_collision parameter has the same result. The previous version didn’t work because I didn’t set this parameter to true. :face_in_clouds:

From what I understand, without recovery_as_collision, a new collision isn’t detected while the player remains in a previous collision. I believe this solution might still fail if the player collides with the ground and the block in the same frame (initiates the collision at the exact same moment), but I think I can leave this case open.

To be honest, I don’t fully understand it yet, but in the tests, all cases worked.

1 Like