Beginner
This tutorial walks you through the process of creating one of the most common classic games you can attempt to recreate. There are a lot of variations on this game, this one features a snake that eats “food” and that only grows when it eats. This snake also crawls on a playfield that contains obstacles.
![]()
In this tutorial you’ll learn how to:
This tutorial is designed for the beginners, but if you are completely new to Defold and the game development, we recommend reading some of the introductory manuals first, especially about Defold’s Building Blocks and the Glossary. If you don’t have Defold downloaded yet, check the Installation manual. It’s also recommended to check the Editor’s overview, to quickly dive into the Editor itself, but we also provide here screenshots for each step.
Start Defold and:

Done!
We’ll start with defining the resolution of the game.
game.project file, on the left side, in the Assets pane. Double click on it to open.game.project file.Width and Height) to 768⨉768 or some other multiple of 16.
The reason why you want to do this is because the game will be drawn on a grid where each segment is going to be 16x16 pixels, and this way the game screen won’t cut off any partial segments. This game.project file contains all important settings of the projects - you can read about all of them in the Project Settings manual.
Done!
Very little is needed in terms of graphics for a minimalist Snake clone. One 16⨉16 green segment for the snake, one white block for the obstacles and one, smaller red block representing the food.
First, create a directory for the assets in the Defold Editor:
main folderNew Folder.assets and click Create Folder.
Done!
This image below is the only asset you need:


You can also read more details about importing assets here.
Done!
Defold provides a built-in Tilemap component that you will use to create the playfield consisting of the tiles aligned in a grid. A tilemap allows you to set and read individual tiles, which suits this game perfectly. Since tilemaps fetch their graphics from a Tilesource, you need to create one:
assets folder.New ▸ Tile Source in the “Resources” section.snake.tilesource).
The tilesource will open in a dedicated Tilesource Editor for this type of file, and you’ll be asked to provide an image for it, that is necessary. On the right side you can find a Properties pane:
Set the Image property to the graphics file you just imported.

The Width and Height properties should be kept at 16 (default value). This will split the 32⨉32 pixel image into 4 tiles, numbered 1–4.

Note that the Extrude Borders property is set to 2 pixels. This is to prevent visual artifacts around the tiles that have graphics all the way out to the edge.
If you make any changes to a file an asterisk mark * appears next to its name on its tab. Select File ▸ Save All or use shortcut `Ctrl+S (⌘Cmd + S on Mac) to save all files.
Done!
Now you have a tilesource ready for use, so it’s time to create the playfield tilemap component:
Right click the main folder and select New ▸ Tile Map in the “Components” section. Name the new file “grid” (the editor will save the file as “grid.tilemap”).

It will open in a Tilemap Editor, and highlight that it needs a Tile Source, so set the Tile Source property to the previously created “snake.tilesource”.

Done!
Defold only stores the area of the tilemap that is actually used so you need to add enough tiles to fill the boundaries of the screen.
layer1 layer in the Outline pane on the right side.Choose the menu option Edit ▸ Select Tile... or shortcut Space to display the tile palette, then click the tile you want to use when painting.


You will need a tilemap of size 48x48 tiles (because our display is 768 and we have 16px tiles, so 768/16 = 48) to fill our game screen.
Save the tilemap when you are done.
Done!
Now we need to add our tilemap to the game. If you are familiar with Defold Building Blocks, components are part of Game Objects and game objects can be defined in the Collections.
Open main.collection by double-clicking on it in the Assets pane. This is, in the Empty Project template by default, the bootstrap collection that is loaded on engine start.
Right click the root in the Outline and select Add Game Object which creates a new game object in the collection that is loaded when the game starts.

Right click the new game object and select Add Component File. Choose the file “grid.tilemap” that you just created.

Right now we have a tilemap in our game collection. It should be visible, when you run the game from the Editor.
Project ▸ Build or shortcut Ctrl + B (⌘Cmd + B on Mac).
Done!
Right click the folder main in the Assets browser and select New ▸ Script in the Scripts section. Name the new script file “snake” (it will be saved as “snake.script”). This file will hold all the logic for the game.

Go back to main.collection and right click the game object holding the tilemap. Select Add Component File and choose the file “snake.script”.

Now you have the tilemap component and the script in place.
Done!
The script you are going to write will drive all of the game. We will be adding features one by one.
The idea for how that is going to work is the following:
Open snake.script and locate the init() function. This function is called by the engine when the script is initialized on game start. Change the code to the following:
function init(self)
self.segments = { -- <1>
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0} -- <2>
self.speed = 7.0 -- <3>
self.time = 0 -- <4>
end
In this code we:
self.segments containing a list of tables, each holding a X and Y position for a segment.self.dir holding an X and Y direction.self.speed, expressed as tiles per second.self.time that will be used to keep track of movement speed.The script code above is written in the Lua language. There are a few things to note about the code, but if you won’t yet understand any of the below, don’t worry about it. Just tag along, experiment and give it time — you will get it eventually. For now, you can remember in init() we just initialized the variables that we will be using.
self. The self reference is used to store instance data.self reference can be used as a Lua table that you can store data in. Just use the dot notation as you would with any other table: self.data = "value". The reference is valid throughout the lifetime of the script, in this case from game start until you quit it.{}.{x = 10, y = 20}), nested Lua tables ({ {a = 1}, {b = 2} }) or other data types.Done!
The init() function is called exactly once, when the script component is instantiated into the running game. The function update(), however, is called once each frame, so 60 times a second by default. That makes the function ideal for real-time game logic.
The idea for the update is this: at some set interval do the following:

:::sidenote Keep in mind that our head of the snake is at the end of the table, and the tail is at the beginning. :::
update() function in snake.script and change the code to the following:function update(self, dt)
self.time = self.time + dt -- <1>
if self.time >= 1.0 / self.speed then -- <2>
local head = self.segments[#self.segments] -- <3>
local newhead = {
x = head.x + self.dir.x,
y = head.y + self.dir.y
} -- <4>
table.insert(self.segments, newhead) -- <5>
local tail = table.remove(self.segments, 1) -- <6>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0) -- <7>
for i, s in ipairs(self.segments) do -- <8>
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2) -- <9>
end
self.time = 0 -- <10>
end
end
In this code we:
update() was invoked — so-called “delta time”, or dt.# is the operator used to get the length of a table given that it is used as an array, which it is in our case — all the segments are table values with no key specified.self.dir).#grid has only 1 layer named layer1.i set to the position in the table (starting from 1) and s set to the current segment.If you run the game now you should see the 4-segment-long snake crawl from left to right over the play field.

Done!
Before you add a code to react to the player input, you need to set up the input connections.
input folder the file game.input_binding and double click to open it.
The input binding file maps actual user input (keys, mouse movements etc) to action names that are fed to scripts that have requested input.
Done!
With bindings in place, open snake.script and add the following line at the beginning of the init() function:
function init(self)
msg.post(".", "acquire_input_focus") -- <1>
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
end
The added line:
Then find on_input function and type the following code:
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then -- <1>
self.dir.x = 0 -- <2>
self.dir.y = 1
elseif action_id == hash("down") and action.pressed then
self.dir.x = 0
self.dir.y = -1
elseif action_id == hash("left") and action.pressed then
self.dir.x = -1
self.dir.y = 0
elseif action_id == hash("right") and action.pressed then
self.dir.x = 1
self.dir.y = 0
end
end
These if...elseif... branches do the following:
action table has the pressed field set to true (player pressed the key) then:Run the game again and check that you are able to steer the snake.
Done!
Now, notice that if you press two keys simultaneously, that will result in two calls to on_input(), one for each press. As the code is written above, only the call that happens last will have an effect on the snake’s direction since subsequent calls to on_input() will overwrite the values in self.dir.
Also note that if the snake moves left and you press the right key, the snake will steer into itself. The apparently obvious fix to this problem is by adding an additional condition to the if clauses in on_input():
if action_id == hash("up") and self.dir.y ~= -1 and action.pressed then
...
elseif action_id == hash("down") and self.dir.y ~= 1 and action.pressed then
...
However, if the snake is moving left and the player quickly presses first up, then right before the next movement step happens, only the right press will have an effect and the snake will move into itself. With the conditions added to the if clauses shown above, the input will be ignored. Not good!
A proper solution to this problem is to store the input in a queue and pull entries from that queue as the snake moves:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
self.dirqueue = {} -- <1>
end
This time, we:
self.dirqueue that is initialized as an empty table.In the update() function add:
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed then
local newdir = table.remove(self.dirqueue, 1) -- <1>
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y -- <2>
if not opposite then
self.dir = newdir -- <3>
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0)
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
newdir is not null) then check if newdir is pointing opposite to self.dir.And modify on_input to store current input in the queue instead:
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1}) -- <1>
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
self.dir directly.Start the game and check that it plays as expected.
Done!
The snake needs food on the map so it can grow long and fast. Let’s add that!
Above the init() function add a new function:
local function put_food(self) -- <1>
self.food = {x = math.random(2, 47), y = math.random(2, 47)} -- <2>
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3) -- <3>
end
In this function we:
put_food() that puts a piece of food on the map.self.food.Then call it at the end of the init() function:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
math.randomseed(socket.gettime()) -- <1>
put_food(self) -- <2>
end
math.random(), set the random seed, otherwise the same series of random values will be generated. This seed should only be set once.put_food() at game start so the player begins with a food item on the map.Done!
Now, detecting if the snake has collided with something is just a matter of looking at what’s on the tilemap where snake is heading and react.
Add a variable that keeps track of whether the snake is alive or not:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true -- <1>
math.randomseed(socket.gettime())
put_food(self)
end
Then add logic that tests for collision with wall/obstacle and food:
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then -- <1>
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y) -- <2>
if tile == 2 or tile == 4 then
self.alive = false -- <3>
elseif tile == 3 then
self.speed = self.speed + 1 -- <4>
put_food(self)
else
local tail = table.remove(self.segments, 1) -- <5>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
Now try the game and make sure it plays well!
This concludes the tutorial but please continue experimenting with the game and work through some of the exercises below!
Done!
Here is the complete script code for reference:
local function put_food(self)
self.food = {x = math.random(2, 47), y = math.random(2, 47)}
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3)
end
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true
math.randomseed(socket.gettime())
put_food(self)
end
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y)
if tile == 2 or tile == 4 then
self.alive = false
elseif tile == 3 then
self.speed = self.speed + 1
put_food(self)
else
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1})
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
It’s a good exercise to try implementing these improvements: