This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
Beginner
Ce tutoriel vous accompagne dans la création de l’un des jeux classiques les plus courants que vous pouvez tenter de recréer. Ce jeu connaît de nombreuses variantes ; celle-ci met en scène un serpent qui mange de la « nourriture » et qui ne grandit que lorsqu’il mange. Ce serpent se déplace également sur un terrain de jeu contenant des obstacles.
![]()
Dans ce tutoriel, vous apprendrez à :
Ce tutoriel s’adresse aux débutants, mais si vous découvrez à la fois Defold et le développement de jeux, nous vous recommandons de commencer par quelques manuels d’introduction, notamment ceux consacrés aux éléments fondamentaux de Defold et au glossaire. Si vous n’avez pas encore téléchargé Defold, consultez le manuel d’installation. Nous vous recommandons également de consulter la présentation de l’éditeur pour prendre rapidement en main l’éditeur lui-même, mais nous fournissons aussi ici des captures d’écran pour chaque étape.
Lancez Defold, puis :

Terminé !
Nous allons commencer par définir la résolution du jeu.
game.project sur la gauche, dans le panneau Assets. Double-cliquez dessus pour l’ouvrir.game.project.Width et Height) sur 768⨉768 ou sur un autre multiple de 16.
Le jeu sera dessiné sur une grille dont chaque segment mesurera 16x16 pixels ; ce réglage évite donc que l’écran du jeu coupe des segments. Le fichier game.project contient tous les paramètres importants du projet : vous pouvez les découvrir dans le manuel des paramètres du projet.
Terminé !
Un clone minimaliste de Snake nécessite très peu de graphismes : un segment vert de 16⨉16 pour le serpent, un bloc blanc pour les obstacles et un bloc rouge plus petit pour représenter la nourriture.
Commencez par créer un répertoire pour les ressources dans l’éditeur Defold :
mainNew Folder.assets et cliquez sur Create Folder.
Terminé !
L’image ci-dessous est la seule ressource dont vous avez besoin :


Vous pouvez également en savoir plus sur l’importation de ressources ici.
Terminé !
Defold fournit un composant (component) tilemap intégré que vous utiliserez pour créer le terrain de jeu, constitué de tuiles alignées sur une grille. Une tilemap permet de définir et de lire chaque tuile individuellement, ce qui convient parfaitement à ce jeu. Comme les tilemaps tirent leurs graphismes d’une source de tuiles, vous devez en créer une :
assets.New ▸ Tile Source dans la section « Resources ».snake.tilesource).
La source de tuiles s’ouvre dans un éditeur de sources de tuiles dédié à ce type de fichier, et vous devez lui fournir une image pour qu’elle puisse fonctionner. Le panneau Properties se trouve sur la droite :
Définissez la propriété Image sur le fichier graphique que vous venez d’importer.

Les propriétés Width et Height doivent rester à 16 (valeur par défaut). L’image de 32⨉32 pixels sera ainsi découpée en quatre tuiles numérotées de 1 à 4.

Remarquez que la propriété Extrude Borders est définie sur 2 pixels. Cela évite les artefacts visuels autour des tuiles dont les graphismes s’étendent jusqu’au bord.
Si vous modifiez un fichier, un astérisque * apparaît à côté de son nom dans son onglet. Sélectionnez File ▸ Save All ou utilisez le raccourci Ctrl+S (⌘Cmd + S sur Mac) pour enregistrer tous les fichiers.
Terminé !
La source de tuiles est maintenant prête à l’emploi : il est temps de créer le composant tilemap du terrain de jeu :
Faites un clic droit sur le dossier main et sélectionnez New ▸ Tile Map dans la section « Components ». Nommez le nouveau fichier « grid » (l’éditeur l’enregistrera sous le nom « grid.tilemap »).

Le fichier s’ouvre dans un éditeur de tilemaps qui signale qu’une Tile Source est nécessaire : définissez donc la propriété Tile Source sur le fichier « snake.tilesource » créé précédemment.

Terminé !
Defold ne stocke que la zone de la tilemap réellement utilisée ; vous devez donc ajouter suffisamment de tuiles pour couvrir toute la surface de l’écran.
layer1 dans le panneau Outline sur la droite.Choisissez l’option de menu Edit ▸ Select Tile... ou le raccourci Space pour afficher la palette de tuiles, puis cliquez sur la tuile que vous souhaitez utiliser pour peindre.


Vous aurez besoin d’une tilemap de 48x48 tuiles (car notre affichage mesure 768 et nos tuiles font 16px, donc 768/16 = 48) pour remplir l’écran du jeu.
Enregistrez la tilemap lorsque vous avez terminé.
Terminé !
Nous devons maintenant ajouter notre tilemap au jeu. Si vous connaissez les éléments fondamentaux de Defold, vous savez que les composants font partie des objets de jeu (game object) et que les objets de jeu peuvent être définis dans les collections.
Ouvrez main.collection en double-cliquant dessus dans le panneau Assets. Dans le modèle Empty Project, il s’agit par défaut de la collection bootstrap chargée au démarrage du moteur.
Faites un clic droit sur la racine dans Outline et sélectionnez Add Game Object pour créer un nouvel objet de jeu dans la collection chargée au démarrage du jeu.

Faites un clic droit sur le nouvel objet de jeu et sélectionnez Add Component File. Choisissez le fichier « grid.tilemap » que vous venez de créer.

Notre collection de jeu contient maintenant une tilemap. Elle devrait être visible lorsque vous lancez le jeu depuis l’éditeur.
Project ▸ Build ou utilisez le raccourci Ctrl + B (⌘Cmd + B sur Mac).
Terminé !
Faites un clic droit sur le dossier main dans le navigateur Assets et sélectionnez New ▸ Script dans la section Scripts. Nommez le nouveau fichier de script « snake » (il sera enregistré sous le nom « snake.script »). Ce fichier contiendra toute la logique du jeu.

Revenez à main.collection et faites un clic droit sur l’objet de jeu qui contient la tilemap. Sélectionnez Add Component File et choisissez le fichier « snake.script ».

Le composant tilemap et le script sont maintenant en place.
Terminé !
Le script que vous allez écrire pilotera l’ensemble du jeu. Nous allons ajouter les fonctionnalités une par une.
Voici le principe de fonctionnement :
Ouvrez snake.script et repérez la fonction init(). Le moteur appelle cette fonction lors de l’initialisation du script au démarrage du jeu. Remplacez le code par ce qui suit :
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
Dans ce code, nous :
self.segments, contenant une liste de tables dont chacune contient une position X et Y pour un segment.self.dir contenant une direction X et Y.self.speed, exprimée en tuiles par seconde.self.time la valeur d’un chronomètre qui servira à gérer la vitesse de déplacement.Le code du script ci-dessus est écrit en langage Lua. Quelques points méritent votre attention, mais si vous ne comprenez pas encore ce qui suit, ne vous inquiétez pas. Continuez, expérimentez et prenez le temps nécessaire : vous finirez par comprendre. Pour l’instant, retenez simplement que dans init(), nous avons initialisé les variables que nous allons utiliser.
self. La référence self sert à stocker les données de l’instance.self peut être utilisée comme une table Lua dans laquelle vous pouvez stocker des données. Utilisez simplement la notation pointée comme avec n’importe quelle autre table : self.data = "value". La référence reste valide pendant toute la durée de vie du script, dans ce cas du démarrage du jeu jusqu’à ce que vous le quittiez.{}.{x = 10, y = 20}), des tables Lua imbriquées ({ {a = 1}, {b = 2} }) ou d’autres types de données.Terminé !
La fonction init() est appelée exactement une fois, lorsque le composant script est instancié dans le jeu en cours d’exécution. En revanche, la fonction update() est appelée une fois à chaque image. Elle est donc idéale pour la logique du jeu en temps réel.
Le principe de la mise à jour est le suivant : à un intervalle défini, effectuez les opérations suivantes :

:::sidenote Gardez à l’esprit que la tête de notre serpent se trouve à la fin de la table, et sa queue au début. :::
update() dans snake.script et remplacez le code par ce qui suit :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
Dans ce code, nous :
update() : ce que l’on appelle le « delta de temps », ou dt.# est l’opérateur qui permet d’obtenir la longueur d’une table lorsqu’elle est utilisée comme un tableau, ce qui est le cas ici : tous les segments sont des valeurs de la table sans clé spécifiée.self.dir).#grid ne contient qu’une couche nommée layer1.i contient la position dans la table (à partir de 1) et s contient le segment actuel.Si vous lancez le jeu maintenant, vous devriez voir le serpent de quatre segments se déplacer de gauche à droite sur le terrain de jeu.

Terminé !
Avant d’ajouter du code pour réagir aux entrées du joueur, vous devez configurer les liaisons d’entrée.
input, repérez le fichier game.input_binding et faites un double-clic dessus pour l’ouvrir.
Le fichier de liaisons d’entrée associe les entrées réelles de l’utilisateur (touches, mouvements de la souris, etc.) à des noms d’actions transmis aux scripts qui ont demandé à recevoir les entrées.
Terminé !
Une fois les liaisons en place, ouvrez snake.script et ajoutez la ligne suivante au début de la fonction init() :
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
La ligne ajoutée :
Repérez ensuite la fonction on_input et saisissez le code suivant :
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
Ces branches if...elseif... effectuent les opérations suivantes :
action a son champ pressed défini sur true (le joueur a appuyé sur la touche), alors :Relancez le jeu et vérifiez que vous pouvez diriger le serpent.
Terminé !
Remarquez maintenant que si vous appuyez simultanément sur deux touches, cela provoque deux appels à on_input(), un pour chaque appui. Avec le code ci-dessus, seul le dernier appel a un effet sur la direction du serpent, car les appels successifs à on_input() écrasent les valeurs de self.dir.
Remarquez également que si le serpent se déplace vers la gauche et que vous appuyez sur la touche right, il se retourne sur lui-même. La correction apparemment évidente de ce problème consiste à ajouter une condition supplémentaire aux clauses if de 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
...
Cependant, si le serpent se déplace vers la gauche et que le joueur appuie rapidement d’abord sur up, puis sur right avant le déplacement suivant, seul l’appui sur right aura un effet et le serpent se retournera sur lui-même. Avec les conditions ajoutées aux clauses if ci-dessus, l’entrée sera ignorée. Ce n’est pas satisfaisant !
Pour résoudre correctement ce problème, il faut stocker les entrées dans une file d’attente et en retirer les éléments au fur et à mesure que le serpent avance :
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
Cette fois-ci, nous :
self.dirqueue, initialisée comme une table vide.Dans la fonction update(), ajoutez :
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 n’est pas nul), vérifiez si newdir pointe dans la direction opposée à self.dir.Et modifiez on_input pour stocker l’entrée actuelle dans la file d’attente :
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.Lancez le jeu et vérifiez qu’il fonctionne comme prévu.
Terminé !
Le serpent a besoin de nourriture sur la carte pour grandir et accélérer. Ajoutons-en !
Au-dessus de la fonction init(), ajoutez une nouvelle fonction :
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
Dans cette fonction, nous :
put_food() qui place un morceau de nourriture sur la carte.self.food.Appelez-la ensuite à la fin de la fonction init() :
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(), définissez la graine aléatoire ; sinon, la même série de valeurs aléatoires sera générée. Cette graine ne doit être définie qu’une seule fois.put_food() au démarrage du jeu pour que le joueur commence avec un morceau de nourriture sur la carte.Terminé !
Pour détecter si le serpent est entré en collision avec quelque chose, il suffit maintenant de regarder ce qui se trouve sur la tilemap à l’endroit vers lequel il se dirige et de réagir en conséquence.
Ajoutez une variable qui indique si le serpent est vivant ou non :
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
Ajoutez ensuite la logique qui teste les collisions avec les murs, les obstacles et la nourriture :
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
Essayez maintenant le jeu et assurez-vous qu’il fonctionne bien !
Ce tutoriel est terminé, mais continuez à expérimenter avec le jeu et essayez de réaliser quelques-uns des exercices ci-dessous !
Terminé !
Voici le code complet du script pour référence :
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
Essayer de mettre en œuvre ces améliorations constitue un bon exercice :