Godot Engine 3.1 Grid-based Movement

Creating the TileMap

Create a new TileMap under the root node in our Scene Tree. Add a new TileSet and add the icon.png as an image. Create a Single Tile the size of the entire image (make sure it’s 64*64, use the snapping options in the Inspector if you must) and then create a collision over the entire tile. Under Selected Tile set the Modulate to be completely black.

Now we have some black walls to place around our scene.

Player collision

If we run the game now the player will be able to walk through the walls even though they have collision boxes. Add a RayCast2D to the player and set its position to (32, 32) so that the origin is in the middle of our sprite. Make sure you check the box next to Enabled. This is going to point in the direction in front of the player, and if there’s a collision, the player will not move.

At the top of the player script (right above the variables) we’re going to create an onready var that will let us call our RayCast2D more easily.

onready var ray = $RayCast2D

At the bottom of the get_movedir() function, we’re going to add two lines that will set ray’s cast to the edge of our player sprite.

func get_movedir():
	var LEFT = Input.is_action_pressed("ui_left")
	var RIGHT = Input.is_action_pressed("ui_right")
	var UP = Input.is_action_pressed("ui_up")
	var DOWN = Input.is_action_pressed("ui_down")
	
	movedir.x = -int(LEFT) + int(RIGHT) # if pressing both directions this will return 0
	movedir.y = -int(UP) + int(DOWN)
	
	if movedir.x != 0 && movedir.y != 0: # prevent diagonals
		movedir = Vector2.ZERO
	if movedir != Vector2.ZERO:
		ray.cast_to = movedir * tile_size / 2

Now we’re going to add a new if statement to our moving state and put the rest of it into an else statement.

	# MOVEMENT
	if ray.is_colliding():
		position = last_position
		target_position = last_position
	else:
		position += speed * movedir * delta
		
		if position.distance_to(last_position) >= tile_size: # if we've moved further than one space
			position = target_position # snap the player to the intended position

Now instead of just moving to the desired location as soon as we have a new target, it checks to see if there’s a wall in front. If so, it sets the target_position back to our last_position, the rest of the moving state isn’t called, and we go back to idle.

Conclusion

Here’s the full player.gd script:

extends Sprite

onready var ray = $RayCast2D

var speed = 256 # big number because it's multiplied by delta
var tile_size = 64 # size in pixels of tiles on the grid

var last_position = Vector2() # last idle position
var target_position = Vector2() # desired position to move towards
var movedir = Vector2() # move direction

func _ready():
	position = position.snapped(Vector2(tile_size, tile_size)) # make sure player is snapped to grid
	last_position = position
	target_position = position

func _process(delta):
	# MOVEMENT
	if ray.is_colliding():
		position = last_position
		target_position = last_position
	else:
		position += speed * movedir * delta
		
		if position.distance_to(last_position) >= tile_size: # if we've moved further than one space
			position = target_position # snap the player to the intended position
	
	# IDLE
	if position == target_position:
		get_movedir()
		last_position = position # record the player's current idle position
		target_position += movedir * tile_size # if key is pressed, get new target (also shifts to moving state)
	
	

# GET DIRECTION THE PLAYER WANTS TO MOVE
func get_movedir():
	var LEFT = Input.is_action_pressed("ui_left")
	var RIGHT = Input.is_action_pressed("ui_right")
	var UP = Input.is_action_pressed("ui_up")
	var DOWN = Input.is_action_pressed("ui_down")
	
	movedir.x = -int(LEFT) + int(RIGHT) # if pressing both directions this will return 0
	movedir.y = -int(UP) + int(DOWN)
	
	if movedir.x != 0 && movedir.y != 0: # prevent diagonals
		movedir = Vector2.ZERO
	if movedir != Vector2.ZERO:
		ray.cast_to = movedir * tile_size / 2

That’s it for this tutorial. I may come back to this and add animations in the future, but I think it stands on its own quite well. If you do want to see more, feel free to message me in my Discord or Twitter. If you want to support me, I have a Patreon which keeps these tutorials going. All of these links can be found on the sidebar to your right. See you next time!