Inconsistent and "squishy" behavior with Area2Ds for fighting game pushboxes

Godot Version

4.3

Question

I’m attempting to recreate the behavior of player collision from 2D fighting games through a pushbox system. Pushboxes allow players to push each other while preventing them from standing on one another. My implementation is an Area2D that separates the characters based on the overlap of their collision shapes.

extends Area2D
class_name Pushbox

@onready var CollisionShape : Shape2D = $"Pushbox Shape".get_shape()

func _physics_process(_delta: float) -> void:
	var overlapping_areas : Array = get_overlapping_areas()
	for i in overlapping_areas.size():
		var overlap = overlapping_areas[i]
		if overlap is Pushbox:
			var colliding_pushbox : Pushbox = overlap
			var opposing_player : CharacterBody2D = colliding_pushbox.get_parent()
			
			# Defines the left and right extents of the player's pushbox collision shape
			# and the opposing player's pushbox collision shape.
			# Player pushbox left/right
			var self_pushbox_left : float = global_position.x - (CollisionShape.size.x / 2)
			var self_pushbox_right : float = global_position.x + (CollisionShape.size.x / 2)
			# Opposing player pushbox left/right
			var colliding_pushbox_left : float = colliding_pushbox.global_position.x - (colliding_pushbox.CollisionShape.size.x / 2)
			var colliding_pushbox_right : float = colliding_pushbox.global_position.x + (colliding_pushbox.CollisionShape.size.x / 2)
			# Finds the overlap between the two collision shapes.
			var right_overlap : float = abs(self_pushbox_right - colliding_pushbox_left)
			var left_overlap : float = abs(colliding_pushbox_right - self_pushbox_left)
			# The position adjustment will be used to push the players apart.
			var position_adjustment : float
			# Whichever overlap is the smallest out of the two will be used as the position adjustment.
			if (right_overlap <= left_overlap):
				position_adjustment = right_overlap
			elif (left_overlap <= right_overlap):
				position_adjustment = -left_overlap
			# Finally, the position adjustment is halfed.
			position_adjustment *= 0.5

			opposing_player.global_position.x += position_adjustment

It works somewhat as intended; the players do indeed push each other apart, preventing them from standing on each other’s heads. However there is an inconsistent and I guess squishy(for lack of a better word) effect to this:


Notice how only one pushbox is able to squeeze the other into clipping through the wall, also notice how they marginally overlap, which is not intentional. See also:

Ideally, there would be no overlapping like this. I’ve tried a lot to fix this but I can’t seem to wrap my head around it, and I’d really appreciate some help.

The players should be CharacterBodies if you do not want them to overlap. (seems like a 2D game but you’ve tagged it 3d ?)

You could get collisions from move_and_slide() like so, and apply the “remaining” distance as a push.

for i in get_slide_collision_count():
    var collision := get_slide_collision(i)
    var collider = collision.get_collider()
    if collider is Player:
        push_player(collider, collision.get_remainder())

Thank you for replying, it is 2D, so apologies for that.
The players are character bodies, just with an Area2D acting as the pushbox, along with a separate world collision box (the grey collision shape).
After attempting to your solution, it only solves the problem when the characters are on the wall. Now they’re able to stand on each other, which I don’t want, furthermore the pushing is stuttery.


(Player movement code is placeholder while I make the pushbox)

extends CharacterBody2D

@export_range(1, 2) var player_ID : int = 1

#"P%s_move_Left" % [player_ID]

func _physics_process(delta: float) -> void:
	var direction = Input.get_axis("P%s_move_Left" % [player_ID], "P%s_move_Right" % [player_ID])
	velocity.x = direction * 50
	
	if not is_on_floor():
		velocity.y += 10
	else:
		if Input.is_action_just_pressed("P%s_move_Jump" % [player_ID]):
			velocity.y -= 250
	
	for i in get_slide_collision_count():
		var collision := get_slide_collision(i)
		var collider = collision.get_collider()
		if collider is CharacterBody2D:
			push_player(collider, collision.get_remainder())
	
	move_and_slide()

func push_player(collider, remainder):
	collider.global_position += Vector2(remainder.x, 0)

Cool! The stutter is probably because the player needs to keep moving if they pushed another player, maybe using half the remainder to simulate weight/inertia

func push_player(collider, remainder):
	collider.global_position.x += remainder.x / 2
	self.position.x += remainder.x / 2

Not standing on heads gets a little tricky, the box shape makes for a lot of flat “head” to stand on. Detecting is easy enough, we could tell by the remainder.y or collision.get_normal().y being much greater than it’s x component. But the resolution is up to you, picking which direction to push could be based on the direction between positions pretty accurately, or collision.get_position(). I’d change to a capsule collision shape, with just move_and_slide players should slide off eachothers heads.