How do I implement box pulling in a Sokoban clone?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By KV

Hi, I’m currently having trouble implementing a mechanic into my first game, which is a Sokoban clone. I want it so that the player can pull boxes when they hold down the z key in addition to pushing boxes naturally.

I’ve implemented a basic version of the mechanic, however its very buggy, like you are unable to pull boxes if they are in contact with a wall, and the player can also get themselves stuck inside walls when trying to pull.

What I basically want to happen is for the player to pull the box when they press any key that isn’t in the box’s direction. However, if they do press it in the box’s direction, they’ll just push it like if they weren’t holding down the z key to pull. How can I go about implementing this mechanic?

Here’s my player code:

extends KinematicBody2D

onready var ray = $RayCast2D
onready var runTimer = $RunTimer
var grid_size = 8
var runDelay: float  = 0.5

var inputs = {
	'ui_up' : Vector2(0,-1),
	'ui_down' : Vector2(0,1),
	'ui_left' : Vector2(-1,0),
	'ui_right' : Vector2(1,0)
}

func _unhandled_input(event):
	for dir in inputs.keys():
		if event.is_action(dir) and runTimer.is_stopped():
			runTimer.stop()
			if $Tween.is_active() == false:
				move(dir)
		if event.is_action_pressed(dir):
			runTimer.start(runDelay)
			if $Tween.is_active() == false:
				move(dir)

	if event.is_action_pressed("reset"):
		get_tree().reload_current_scene()

func move(dir):
	var game = get_parent()
	var vector_pos = inputs[dir] * grid_size
	ray.cast_to = vector_pos
	if Input.is_key_pressed(KEY_Z):
		ray.cast_to = -vector_pos
	ray.force_raycast_update()
	$Tween.interpolate_property(self, "position", position, position + vector_pos, 0.1, Tween.TRANS_SINE, Tween.EASE_IN_OUT)
	if !ray.is_colliding():
		$Tween.start()
		game.moves += 1
	else:
		var collider = ray.get_collider()
		if collider.is_in_group('box'):
			if collider.move(dir):
				$Tween.start()
				game.moves += 1

func _on_Tween_tween_all_completed():
	get_parent().check_end()

And my box code since the two objects work in tandem to work right (I based my game off of a YouTube tutorial if you couldn’t tell already):

extends KinematicBody2D

onready var ray = $RayCast2D
var grid_size = 8
var inputs = {
	'ui_up' : Vector2.UP,
	'ui_down' : Vector2.DOWN,
	'ui_left' : Vector2.LEFT,
	'ui_right' : Vector2.RIGHT
}

func move(dir):
	var vector_pos = inputs[dir] * grid_size
	ray.cast_to = vector_pos
	if Input.is_key_pressed(KEY_Z):
		ray.cast_to = -vector_pos
	ray.force_raycast_update()
	$Tween.interpolate_property(
		self, "position", 
		position, position + vector_pos, 0.1, 
		Tween.TRANS_SINE, 
		Tween.EASE_IN_OUT
	)
	if !ray.is_colliding():
		$Tween.start()
		return true
	return false

If your game is grid-based, you don’t need raycasts at all.You should make two arrays: one is map of player, walls and boxes, and second array is map of target positions. Then you just check that player can move to desired location. If this location has box, you check that player can move this box (free space in moving direction). After that you check box positions with target positions. And you add animations between player moves.
And pulling is simpler - you check that there is box nearby and player can move in desired location. Then you update map accordingly to movement direction.

AlexTheRegent | 2021-01-07 23:42

:bust_in_silhouette: Reply From: Andrea

the original Sokoban movement is instant (with animation in between tile movement), the is no sliding or colliding, therefore the kinematic object and the physic process is not necessary, you can switch everything to animated sprites and restric movement by checking if the area around the player is free or occupied by wall or boxes.

otherwise, you can create a “physic version” of the game, since you declared both the box and the player to be kinematic bodies, i’m expecting you have the walls as static bodies which both boxes and player collide on.

in this case you can let the physic engine manage the collision, but you might want to switch the box to a RigidBody. Doing so the pushing will come automatically, and for the pulling you can simply call

func _input(event):
 if event.is_action_pressed("pulling") and box_distance<whatEverValue:
   box.apply_central_impulse(vector_toward_player)

although you will lose some fine control of the box, it is probably easier than making 2 kinematic object interact.