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 customTileSet
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 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 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 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 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 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 settings
There is more ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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
:
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.gdextends Node const TILE_SIZE: int = 24
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 ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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 movement
I added also a
Camera2D
to follow the player as it moves, and aSprite2D
(Shadow
) to display a shadow below the player sprite.Final 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
:
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 MapInput 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
):
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 node
We then add some code to shift the raycast towards the player's moving direction and check for collisions:
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 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 around
I also added some code inside the player script to update the animation based on the moving direction:
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 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.