Pushing blocks in a 2d Platformer

Godot Version

v4.2.2.stable.flathub [15073afe3]

Question

I’m trying to have some boxes that I can move around deterministically in a 2d platformer, since there may be puzzles that involve them. I basically want boxes to be movable with no rotation, and fall when nothing is supporting them. What I have working is close:

However, you can see that at certain points I’m unable to push a block when another box is above it, and you may also notice some subtle shifting of boxes up/down.

Does anyone have a fix for this, or perhaps an easier way to accomplish what I want? It seems quite simple, but I haven’t been able to figure it out. Most tutorials for pushing blocks use Godot’s rigidbody physics, but it is non-deterministic and can get quite wonky at times.

Here is my code for the block:

class_name PushBlock
extends CharacterBody2D

var push_speed = 25

var gravity = 100

func _physics_process(delta):
	velocity.y += gravity * delta
	
	move_and_slide()
	reset()

func push(direction):
	if is_on_floor(): velocity.x = push_speed * direction
	
func reset():
	velocity.x = 0

and here is the code for the player character:

extends CharacterBody2D

const SPEED = 150.0
const JUMP_VELOCITY = -250.0

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var push_obj = null
var direction = 0

@onready var on_floor_label = $OnFloorLabel
@onready var push_right_collider = $PushRightCollider
@onready var push_left_collider = $PushLeftCollider

func _physics_process(delta):
	# Add the gravity.
	if not is_on_floor():
		velocity.y += gravity * delta

	# Handle jump.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	direction = Input.get_axis("left", "right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)
	
	# logic to push block
	var pushblock = is_pushing_block()
	if pushblock and is_on_floor():
		pushblock.push(direction)

	move_and_slide()

# returns the pushblock body if player is pushing a block
func is_pushing_block():
	var bodies = null
	if direction == 0:
		return null
	elif direction == 1: bodies = push_right_collider.get_overlapping_bodies()
	else: bodies = push_left_collider.get_overlapping_bodies()
		
	for i in range(bodies.size()):
		if bodies[i].is_in_group('pushblock'):
			return bodies[i]
	return null
1 Like