How to implement Top-down Grid Movement in Godot

Sandro Maglione

Sandro Maglione

Games

A classical top-down game constrains the player to move in 4 directions inside a grid. This is the strategy used in the first Pokemon games, where you can move the player only up, down, left, and right.

In this post, we learn how to implement a grid-based movement for a top-down game in Godot:

  • Setup a Godot project for pixel art sprites and tiles
  • Create and setup a TileMap with a custom TileSet to paint the levels
  • Create a player scene with animations, camera, grid movement, and styling

After reading this post you will have your own Godot game Pokemon-style movement ๐Ÿ‘‡

Final result, with the Player free to move in 4 directions in a TileMap levelFinal result, with the Player free to move in 4 directions in a TileMap level


Tilemap

Each level is implemented using Tilemap:

Tilemaps use a TileSet which contain a list of tiles used to create grid-based maps.

The movement of the player will be tight to the tiles in the Tilemap.

A Tilemap contains a Tileset, which is an image containing all the tiles organized in a grid:

A tileset image, with tiles of all the same sizes organized in a gridA tileset image, with tiles of all the same sizes organized in a grid

Using a Tilemap requires selecting the tile size and then attaching a Tileset to the Tilemap. Godot will split the image into tiles based on the tile size. You can then select the tiles and paint them on the map:

Example of a complete setup of Tilemap and Tileset in Godot, with a tile size of 24 pixelsExample of a complete setup of Tilemap and Tileset in Godot, with a tile size of 24 pixels

A Tilemap is all you need to create all the levels.

Later we are going to add physics and collisions (tiles where the player cannot move).

Tilemap in Godot has also many other features: layers, navigation, different grid shapes, and more.

Player scene

The player is built using AnimatedSprite2D. This node allows to create animations from a sequence of frames:

Example of AnimatedSprite2D with a list of animations for the playerExample of AnimatedSprite2D with a list of animations for the player

The player sprite is also an image that contains all the frames for each animation:

Image used for the player sprite animations in the example. The image is made of a series of 24x24 frames for each animation that the player will perform in the game (idle and walking). This image was created using Asprite, a software for creating pixel art and pixel art animationsImage used for the player sprite animations in the example. The image is made of a series of 24x24 frames for each animation that the player will perform in the game (idle and walking). This image was created using Asprite, a software for creating pixel art and pixel art animations

Important โ˜๏ธ: Make sure that the resolution of your character is the same of the resolution of each tile in the Tilemap.

This is required to make the player moving inside the grid, one tile at the time.

For a top-down grid-based movement we do not need to attach any collision shape or physics node to the player.

We are going instead to control the player movement by changing its position (available in every Node2D, therefore also in AnimatedSprite2D).

To avoid blur on a pixel art sprite with a low resolution you need to update the Texture Filter to "Nearest" inside Project > Project Settings > Textures: Low resolution sprites (24px in the example) require to update the settingsLow resolution sprites (24px in the example) require to update the settings

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.

Grid movement component

Moving a node in the grid using position can be applied to every element in the scene. Therefore, we are going to implement a generic component that we are then going to attach to the player.

Creating a separate component responsible for movement allows to use the same component for all moving NPCs and objects.

We create a new scene containing a Node2D, and we attach a new script to the node.

The script requires a reference to the node to move (any Node2D), as well as a value for the speed:

extends Node2D
 
@export var self_node: Node2D
@export var speed: float = 0.35

speed in this example represents the time it takes for the player to move by one step, in seconds

We then implement a single move function which moves the self_node of 1 tile in the given direction:

grid_movement.gd
var moving_direction: Vector2 = Vector2.ZERO
 
func move(direction: Vector2) -> void:
	if moving_direction.length() == 0 && direction.length() > 0:
		var movement = Vector2.DOWN
		if direction.y > 0: movement = Vector2.DOWN
		elif direction.y < 0: movement = Vector2.UP
		elif direction.x > 0: movement = Vector2.RIGHT
		elif direction.x < 0: movement = Vector2.LEFT
		
        moving_direction = movement
        
        var new_position = self_node.global_position + (moving_direction * Constants.TILE_SIZE)
        
        var tween = create_tween()
        tween.tween_property(self_node, "position", new_position, speed).set_trans(Tween.TRANS_LINEAR)
        tween.tween_callback(func(): moving_direction = Vector2.ZERO)

moving_direction stores the direction in which the node is currently moving. If its length() is not 0, it means that the node is already moving.

We first compute the direction of the movement by converting the given direction to another Vector2 (movement) with length 1 (DOWN, UP, LEFT, RIGHT).

The script gives priority to Y movement: if the user is clicking both X and Y inputs, the player will only move in the Y axis.

You can change the order of the if/elif to modify this behavior.

No diagonal movement is allowed, only 4 directions.

We then assign movement to moving_direction, which activates the moving process and blocks any further input (which requires moving_direction.length() == 0).

new_position is computed from the current global_position position of the player moved by 1 tile in the given direction.

Constants.TILE_SIZE represent the global tile size. This value is contained inside an auto-loaded script (Singleton):

constants.gd
extends Node
 
const TILE_SIZE: int = 24

Go to Project > Project Settings > Autoload and add the constants.gd script as a global script (autoload)Go to Project > Project Settings > Autoload and add the constants.gd script as a global script (autoload)

The last step is using a Tween to animate the movement from the current position to new_position.

At the end of the movement, we use tween_callback to reset moving_direction and unlock the next move.

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.

Moving the player

Now we have a new scene containing a Node2D with a script that provides a generic move function (called GridMovement in this example ๐Ÿ‘‡).

We add the movement scene to the player and assign the player's main node as self_node in the movement script.

Assign the Player as "Self Node" to enable movementAssign the Player as "Self Node" to enable movement

I added also a Camera2D to follow the player as it moves, and a Sprite2D (Shadow) to display a shadow below the player sprite.

Final setup for the Player node, with camera, shadow, movement, and animationFinal setup for the Player node, with camera, shadow, movement, and animation

The last step is adding a script to the player. Since all the movement logic is implemented in a separate node, the player script is responsible only for getting the input and call move:

player.gd
extends AnimatedSprite2D
 
func _process(_delta):
	var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
	$GridMovement.move(input_direction)

You can define the input keys for "move_left", "move_right", "move_up", and "move_down" inside Project > Project Settings > Input Map

Input Map in Godot's project settings. Make sure to reference the same names that you assigned in this settings in the player's scriptInput Map in Godot's project settings. Make sure to reference the same names that you assigned in this settings in the player's script

One last adjustment is snapping the player to the correct grid coordinates when the game starts.

The initial position should be exactly in the center of a tile (based on TILE_SIZE):

player.gd
extends AnimatedSprite2D
 
func _ready():
	position = position.snapped(Vector2.ONE * Constants.TILE_SIZE)
	position -= Vector2.ONE * (Constants.TILE_SIZE / 2)
 
func _process(_delta):
	var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
	$GridMovement.move(input_direction)

Inside _ready() we snap the position to the closest multiple of TILE_SIZE (snapped()), and then we move it back by TILE_SIZE / 2 to make sure its position is in the center of the tile.

Grid-based collisions

We do not need a RigidBody2D or any other built-in physics collision on the player scene.

Since the player is restricted to move in 4 directions inside a grid, we can simply block the movement if the next tile contains an obstacle or a wall.

We attach a RayCast2D to the movement node:

Final setup for the GridMovement nodeFinal setup for the GridMovement node

We then add some code to shift the raycast towards the player's moving direction and check for collisions:

grid_movement.gd
extends Node2D
 
@export var self_node: Node2D
@export var speed: float = 0.25
 
var moving_direction: Vector2 = Vector2.ZERO
 
func _ready():
	# Set movement direction as DOWN by default
	$RayCast2D.target_position = Vector2.DOWN * Constants.TILE_SIZE
 
func move(direction: Vector2) -> void:
	if moving_direction.length() == 0 && direction.length() > 0:
		var movement = Vector2.DOWN
		if direction.y > 0: movement = Vector2.DOWN
		elif direction.y < 0: movement = Vector2.UP
		elif direction.x > 0: movement = Vector2.RIGHT
		elif direction.x < 0: movement = Vector2.LEFT
		
		$RayCast2D.target_position = movement * Constants.TILE_SIZE
		$RayCast2D.force_raycast_update() # Update the `target_position` immediately
		
		# Allow movement only if no collision in next tile
		if !$RayCast2D.is_colliding():
			moving_direction = movement
			
			var new_position = self_node.global_position + (moving_direction * Constants.TILE_SIZE)
			
			var tween = create_tween()
			tween.tween_property(self_node, "position", new_position, speed).set_trans(Tween.TRANS_LINEAR)
			tween.tween_callback(func(): moving_direction = Vector2.ZERO)

If we now add a Physic layer to the TileMap, the player will not be allowed to move to the tiles with physics:

Inside the TileSet add a Physics Layer and then paint all the tiles that should cause a collisionInside the TileSet add a Physics Layer and then paint all the tiles that should cause a collision

Note: The tile must have the physics layer painted to its full size (since there is no sub-tile movement).

Completed grid movement level

That's it! Now you can paint your levels using TileMap, add the player in, and start moving it inside the grid. All you need inside a level is the TileMap and the Player:

Build all your levels using TileMap and add the Player to move aroundBuild all your levels using TileMap and add the Player to move around

I also added some code inside the player script to update the animation based on the moving direction:

player.gd
extends AnimatedSprite2D
 
func _ready():
	position = position.snapped(Vector2.ONE * Constants.TILE_SIZE)
	position -= Vector2.ONE * (Constants.TILE_SIZE / 2)
	play("idle_down")
 
func _process(_delta):
	var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
	$GridMovement.move(input_direction)
	
	moving_animation(input_direction)
			
func moving_animation(input_direction: Vector2) -> void:
	var animation_state: StringName = animation
	var moving_direction: Vector2 = $GridMovement.moving_direction
	var vectorDirection = vector2Direction(moving_direction)
	
	if moving_direction.length() > 0:
		animation_state = "walk_" + vectorDirection
	else:
		if input_direction.length() > 0:
			vectorDirection = vector2Direction(input_direction)
			animation_state = "idle_" + vectorDirection
		else:
			vectorDirection = animation_state.get_slice("_", 1)
			animation_state = "idle_" + vectorDirection
			
	play(animation_state)
 
func vector2Direction(vec: Vector2) -> String:
	var direction = "down"
	if vec.y > 0: direction = "down"
	elif vec.y < 0: direction = "up"
	elif vec.x > 0:
		flip_h = false
		direction = "right"
	elif vec.x < 0:
		# Horizontal flip since we have one animation for both left and right walking and idle
		flip_h = true
		direction = "right"
		
	return direction

Done! Here is an example of a complete level:

The Player free to move inside the TileMapThe Player free to move inside the TileMap


That's it!

This setup allows to move any player and NPC with a unified component that can be included in any scene.

We can add many more features starting from this basic setup: object interactions, NPC dialogues, long grass, and more.

You can subscribe to the newsletter here below for more updates on future tutorials ๐Ÿ‘‡

You can expect more step by step guides on how to implement interesting features found in the most popular games ๐Ÿ•น๏ธ

Thanks for reading.

๐Ÿ‘‹ใƒปInterested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.