Learning Godot 4 Tilemaps via Snake
With Godot 4 now in beta, I’ve been starting to mess around with the new kit. And although there’s a lot of really exciting 3d features coming to Godot 4, the 2d space is a bit more in my wheelhouse for now, as I continue to learn the engine.
One new feature I am looking forward to using in Godot 4 2d is the new TileMap
node updates. To get myself farmiliar with the new TileMap
api, I wanted a project that would let me dip my toes into this node, but I also wanted a project that had a small scope and ideally I wouldn’t have to think up rules or functionality from scratch. So, I chose to remake Snake, using a TileMap
as my main playarea!
Concept
While this isn’t really going to be using many of the new features of TileMap
, most of it is new to me anyway as a Godot noob. The idea is to use the TileMap
as a grid for the play space. The snake the player controls is made up of a series of grid squares, and every x
seconds, the snake moves one grid square in the direction the snake is pointing, which is determined by input that is buffered between the movement timer’s timeout duration.
On the grid
The main scene contains a TileMap
and a Timer
node, underneath the parent Node2d
, which has a script attached called GameManager.gd
. The TileMap
being the play area, and the Timer
being the amount of time between each movement of the snake, which I currently have set to .5 seconds.
Using the TileMap
node’s Layer system, I am able to split the background layer from the snake. This way, when I’m modifying grid squares of the TileMap
to show the snake move, I don’t need to worry about redrawing the background.
I created a Snake
scene and added a Snake.gd
script to the root StaticBody2d
node, as well as adding a Sprite2d
and CollisionShape2d
of course.
Here’s what that script looks like at the moment for just covering the movement.
1# Snake.gd
2
3extends StaticBody2D
4
5@onready var manager = get_parent()
6
7var input_buffer := Vector2.ZERO
8
9var grid_coords := [Vector2(21, 22), Vector2(22,22), Vector2(23,22), Vector2(24,22)]
10var body_segments: Array
11
12func _process(_delta):
13 if Input.get_vector("left", "right", "up", "down") != Vector2.ZERO:
14 input_buffer = Input.get_vector("left", "right", "up", "down")
It’s a little different from a basic 2d game set up, since I’m not moving the player kinematically, hence the StaticBody2d
node. I’m just storing the vector from Input.get_vector
, and using that to influce the direction the snake will move on the timer’s timeout duration. The grid_coords
is an array that will be storing the position of each snake segment position on the TileMap
. Which leads me to the SnakeTail
scene…. which is actually pretty much identical to the Snake
scene at the moment, just without a script and a different sprite. For now it is just serving the purpose of being an instanced scene.
Now to tie it all together in the GameManager.gd
script.
1# GameManager.gd
2
3extends Node2D
4
5@onready var grid = $TileMap;
6@onready var timer = $Timer;
7@onready var snake_head = preload("res://Snake/Snake.tscn");
8@onready var snake_segment = preload("res://Snake/SnakeTail.tscn");
9
10var snake
11
12func _ready():
13 timer.timeout.connect(_timer_timeout)
14 snake = snake_head.instantiate()
15 for s in range(snake.grid_coords.size()):
16 if s == 0:
17 add_child(snake)
18 snake.position = grid.map_to_local(snake.grid_coords[s])
19 snake.body_segments.push_front(snake)
20 else:
21 var current_segment = snake_segment.instantiate()
22 add_child(current_segment)
23 current_segment.position = grid.map_to_local(snake.grid_coords[s])
24 snake.body_segments.append(current_segment)
25 timer.start()
26
27func _timer_timeout():
28 var segment_next
29 var segment_previous
30 for s in range(snake.grid_coords.size()):
31 if s == 0:
32 segment_next = snake.grid_coords[s]
33 snake.grid_coords[s] = snake.grid_coords[s] + snake.input_buffer
34 snake.position = grid.map_to_local(snake.grid_coords[s])
35 else:
36 grid.erase_cell(1, snake.grid_coords[s])
37 segment_previous = snake.grid_coords[s]
38 snake.grid_coords[s] = segment_next
39 snake.body_segments[s].position = grid.map_to_local(snake.grid_coords[s])
40 segment_next = segment_previous
41 timer.start()
Basically, in the _ready
func we set up the snake by instancing all of the body parts from the Vector2
’s that are stored in the Snake
scene’s grid_coords
array. To make things easy, the snake’s head is always position 0
of the array, and the rest of the snake tail segments are added in sequential order. For both the snake head and body segments, instances of each scene are created for each of the Vector2
’s in grid_coords
, and pushed to the Snake
scene’s body_segments
array. Since they are in sequential order, we know that grid_coords[2]
is the position of the instanced scene stored in body_segments[2]
. Ain’t that fun.
Here you can see some of the TileMap
api coming into play, utilizing the map_to_local(Vector2i)
func, which takes a Vector2
and returns the cenetered position of the cell in the TileMap
’s local coordinate space. We do this to prvent the snake’s sprites being off center on the grid, which would make things confusing when we start moving things around and adding pick up items to the TileMap
.
Then, to actually move this bad boy, we connect the timer’s timeout signal. When the Timer
times out, the signal will call the _timer_timeout()
func, and start to shift the position of the snake’s head stored in grid_coord
by adding the input_vector
to it’s current position Vector2
. Then we just use the previous position of the snake’s head as the position of the next object in grid_coords
, and do the same for each body segment.
And that’s it for basic movement! Next up is adding some basic state management, and adding the apple pickup item so you can grow up to be a nice big snake.