Moving from tile to tile in a grid like old RPGs

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

I’m trying to do what GDquest did in one of his videos: I’m trying to do grid-based movement. I’ve tried adapting his code, but I couldn’t get that to work. Then I tried the code of airvikar, but I couldn’t get that to work. Finally, I went to the official Discord channel and got the input of eons. He gave me some code and pointed me in a good direction, but I still can’t get this code to function properly.

extends KinematicBody2D

const MOTION_SPEED = 160

func _physics_process(delta):
    var motion_vector = Vector2()
    var le_input = false
    var moving = false
    var direction = Vector2()
    var step_size = 0
    var motion_amount = 0
    
    # Let's do some moving
    if (Input.is_action_pressed("ui_up")):
        motion_vector += Vector2( 0, -1)
    if (Input.is_action_pressed("ui_down")):
        motion_vector += Vector2( 0, 1)
    if (Input.is_action_pressed("ui_left")):
        motion_vector += Vector2( -1, 0)
    if (Input.is_action_pressed("ui_right")):
        motion_vector += Vector2( 1, 0)
    
    if motion_vector != Vector2() and !moving:
        direction = motion_vector.normalized()
        step_size = MOTION_SPEED
        moving = true
    if step_size >= 0:
        motion_amount = step_size * delta
        move_and_collide(direction * motion_amount)
    elif moving:
        step_size = 0
        moving = false

This code is on a player scene which is added to the main scene. I’m not entirely certain where the code isn’t properly moving the player sprite from tile to tile. The player sprite should just move from tile to tile, and not really have any intermediary movement (i.e. walking slowly in between tiles).

:bust_in_silhouette: Reply From: SIsilicon

I know the type of movement your talking about. And I believe I have a solution.

var moving = false

func _physics_process(delta):
    if !moving:
        var motion_vector = Vector2()
        if Input.is_action_pressed("ui_up"):
            motion_vector += Vector2( 0, -1)
        if Input.is_action_pressed("ui_down"):
            motion_vector += Vector2( 0, 1)
        if Input.is_action_pressed("ui_left"):
            motion_vector += Vector2( -1, 0)
        if Input.is_action_pressed("ui_right"):
            motion_vector += Vector2( 1, 0)

        if motion_vector != Vector2():
            #tile_size is the size of the tilemap in pixels.
            var new_position = position + motion_vector * tile_size
            #Yes. I'm assuming you have a Tween node as a child.
            $Tween.interpolate_property ( self, 'position', position, new_position, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
            #That last method's fifth property is how long it takes to go from one tile to the other in seconds.
            $Tween.start()
            moving = true

#This function is connected to the tween node's tween_completed signal.
func _on_Tween_tween_completed(object, key):
    moving = false

I know the answer looks like it has parts taking from your question. But that was intentional so that you could understand it.

SIsilicon | 2018-05-17 20:48

Thank you for your answer, Sisilicon.

Ertain | 2018-05-17 22:27

I tried the code, and it only moved my sprite once. Repeated key pressed did nothing.

Ertain | 2018-05-18 03:26

Are you sure? I put the exact same code in a test project and it works fine.
Have you connected that last function to the tween_completed signal?

SIsilicon | 2018-05-18 15:13

Thank you for the clarification. I’ll connect the signals (it pays to actually read the code comments).

Ertain | 2018-05-18 15:28

Okay, it’s working, albeit barely. My character is quite slow, and collisions don’t work. Well, at least it’s a start. :-/

Ertain | 2018-05-18 20:34

Slow character?
Remember this line??

$Tween.interpolate_property ( self, 'position', position, new_position, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
#That last method's fifth property is how long it takes to go from one tile to the other in seconds.

The fifth property is your character’s speed. Except the bigger it is, the longer it takes for them to go to the next tile.

SIsilicon | 2018-05-18 22:50

So a value of 1 means it takes 1 second to go to the next tile.
And a value of zero means you travel instantly.

SIsilicon | 2018-05-18 22:55

I eventually learned that. :stuck_out_tongue: As for collisions, well, that’s another monster.

Ertain | 2018-05-18 23:52

I gotcha covered there.

Assuming your player is a kinematic body, you should have access to the method test_move(Transform2D from, Vector2 rel_vec). Returns true if a collision occurs.

Which mean you will change this-

        #tile_size is the size of the tilemap in pixels.
        var new_position = position + motion_vector * tile_size
        #Yes. I'm assuming you have a Tween node as a child.
        $Tween.interpolate_property ( self, 'position', position, new_position, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
        #That last method's fifth property is how long it takes to go from one tile to the other in seconds.
        $Tween.start()
        moving = true

To this.

        #tile_size is the size of the tilemap in pixels.
        motion_vector *= tile_size
        if not test_move(global_transform, motion_vector):
            #Yes. I'm assuming you have a Tween node as a child.
            $Tween.interpolate_property ( self, 'position', position, position+motion_vector, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
            #That last method's fifth property is how long it takes to go from one tile to the other in seconds.
            $Tween.start()
            moving = true

Notice that the new_position variable was omitted and in the Tween’s function replaced with position+motion_vector.

SIsilicon | 2018-05-19 00:22

Thank you for that fix, Sisilicon.

Ertain | 2018-05-20 02:16

extends KinematicBody2D

var motion = Vector2()
var talking = false
var move_speed = 50
var facing = "down"


func _physics_process(delta):
	#<!-- CHECK TO SEE IF PLAYER IS TALKING --!>#
	if talking != true:
		#<!-- CHECK TO SEE IF PLAYER IS PRESSING A BUTTON --!>#
		if Input.is_action_pressed("ui_right"):
			facing = "right"
			$Sprite.play("walk_right")
			motion.y = 0
			motion.x = move_speed
		elif Input.is_action_pressed("ui_left"):
			$Sprite.play("walk_left")
			facing = "left"
			motion.y = 0
			motion.x = -move_speed
		elif Input.is_action_pressed("ui_up"):
			$Sprite.play("walk_up")
			facing = "up"
			motion.x = 0
			motion.y = -move_speed
		elif Input.is_action_pressed("ui_down"):
			$Sprite.play("walk_down")
			facing = "down"
			motion.x = 0
			motion.y = move_speed
		else:
			#<!-- IF THE USER IS NOT PRESSING THE BUTTONS SET PLAYER TO IDLE --!>#
			if facing == "right":
				$Sprite.play("idle_right")
			elif facing == "left":
				$Sprite.play("idle_left")
			elif facing == "up":
				$Sprite.play("idle_up")
			elif facing == "down":
				$Sprite.play("idle_down")
			motion.x = 0
			motion.y = 0
		
		#<!-- MOVE PLAYER ON THE X AND Y AXIS--!>#
		move_and_slide(motion)
	pass

jack the programmer | 2019-04-15 23:39

Thank you all so much for this, it has been very helpful. I too got the single movement the first time I ran the code. After that it just worked. My issue is that if I hold down an arrow key, it makes the first move, stops, and then continues on. How do I make sure that the motion is smooth throughout?

Edit:
Nevermind I’m an idiot. I was playing the wrong scene

jackatthekilns | 2021-01-13 19:07

:bust_in_silhouette: Reply From: Christian Tietze

Pixel-by-tile movement

The key to pixel-by-tile movement is to block input while the movement is going on.

The following code assumes

Then you can use move_and_collide physics and do not need to use tweens. The delta in _physics_process is a fraction of a second. You want to move X pixels per second (your speed, here: _step_size). So you accumulate (+) deltas until the _step_size is reached and then move a pixel.

This approach helps to not draw between-pixel or half-pixel positions and makes sure your big, chunky pixel sprites are moving on a proper pixel grid.

You don’t need to use KinematicBody2D and can make your player a simple Area2D, and set position directly in _process instead of using move_and_collide in _physics_process. Depending on the game you’re making that might be a lot simpler look just as good.

Animated demo

Code

extends KinematicBody2D

const TILE_SIZE = 16

var direction: Vector2 = Vector2.ZERO
var pixels_per_second: float setget _pixels_per_second_changed

var _step_size: float
func _pixels_per_second_changed(value: float) -> void:
    pixels_per_second = value
    _step_size = (1 / pixels_per_second)

# Accumulates delta, aka fractions of seconds, to time movement
var _step: float = 0
# Count movement in distinct integer steps
var _pixels_moved: int = 0 

func is_moving() -> bool:
    return self.direction.x != 0 or self.direction.y != 0

func _ready() -> void:
    self.pixels_per_second = 1 * TILE_SIZE

func _physics_process(delta: float) -> void:
    if not is_moving(): return
    # delta is measured in fractions of seconds, so for a speed of
    # 4 pixels_per_second, we need to accumulate _step until 
    # it reaches 0.25, and then we can move a pixel
    _step += delta
    if _step < _step_size: return

    # Move a pixel
    _step -= _step_size
    _pixels_moved += 1
    move_and_collide(direction)

    # Complete movement
    if _pixels_moved >= TILE_SIZE:
        direction = Vector2.ZERO
        _pixels_moved = 0
        _step = 0

func _input(event: InputEvent) -> void:
    if is_moving(): return
    if Input.is_action_pressed("ui_right"):
        direction = Vector2(1, 0)
    elif Input.is_action_pressed("ui_left"):
        direction = Vector2(-1, 0)
    elif Input.is_action_pressed("ui_down"):
        direction = Vector2(0, 1)
    elif Input.is_action_pressed("ui_up"):
        direction = Vector2(0, -1)

Thanks for the answer. But this question was solved years ago. I guess your answer could help out other would-be Godot developers.

Ertain | 2021-01-13 22:19