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
Ce jeu est une variante du jeu d’association classique dans la lignée de Bejeweled et de Candy Crush. Le joueur relie par glissement des blocs de même couleur pour les supprimer, mais le but du jeu n’est pas de supprimer de longues séries de blocs de même couleur, de vider le plateau ou de récolter des points : il s’agit de réunir un ensemble de « blocs magiques » spéciaux dispersés sur le plateau pour qu’ils se connectent.
Ce tutoriel est un guide pas à pas dans lequel nous construisons le jeu à partir d’une conception complète. En réalité, trouver une conception qui fonctionne demande beaucoup de temps et d’efforts. Vous pouvez partir d’une idée centrale, puis chercher un moyen d’en faire un prototype pour mieux comprendre ce qu’elle peut apporter. Même un jeu simple comme « Magic Link » demande un travail de conception assez conséquent. Ce jeu a connu quelques itérations et expérimentations avant d’aboutir à sa forme et à ses règles définitives (encore loin d’être parfaites). Mais, pour ce tutoriel, nous allons passer cette étape et commencer à construire le jeu à partir de sa conception finale.
Vous devez commencer par créer un nouveau projet et importer le paquet de ressources :

À chaque manche, le plateau est rempli aléatoirement de blocs colorés et d’un ensemble de blocs magiques. Les blocs colorés suivent ces règles :
Les blocs magiques se comportent différemment, selon ces règles :
Le joueur interagit avec le jeu selon les règles suivantes :
Le niveau de difficulté détermine le nombre de blocs magiques placés sur le plateau.
Comme pour tout projet, nous devons définir les grandes lignes de notre approche de l’implémentation. Il existe de nombreuses façons de structurer et de construire ce jeu. Techniquement, nous pourrions, si nous le souhaitions, implémenter l’ensemble du jeu dans le système d’interface graphique. Cependant, construire le jeu avec des objets de jeu (game object) et des sprites, et utiliser les API GUI pour les éléments d’interface graphique et d’affichage tête haute à l’écran, est le plus souvent la manière naturelle de créer un jeu ; c’est donc la voie que nous allons suivre.
Comme nous prévoyons un nombre de fichiers assez limité, nous allons conserver une structure de dossiers très simple pour le projet :

Le fichier game.project conserve principalement les paramètres par défaut, mais certains choix restent à faire. Tout d’abord, nous devons choisir une résolution pour le jeu. Il est assez facile de changer la résolution par la suite et, pour un jeu finalisé, nous devrons faire en sorte qu’il ait un bel aspect, quelle que soit la résolution ou le rapport d’aspect de l’appareil cible.
Nous avons choisi une résolution de 640x960 pixels, qui est la résolution native de l’iPhone 4. C’est aussi une résolution qui tient sur de nombreux moniteurs, ce qui facilite les tests du jeu sur ordinateur. Si vous souhaitez travailler avec une autre résolution, vous n’aurez que quelques valeurs à ajuster différemment.

Nous allons aussi devoir augmenter le nombre maximal de sprites rendus. Si vous le souhaitez, vous pouvez passer à la section suivante et revenir ici lorsque la console vous indique que vous avez atteint la limite de sprites.

Nous pouvons calculer le nombre maximal de sprites nécessaires :
Supposons donc que nous ayons un maximum de 30 blocs magiques. Le plateau compte 63 blocs (sprites). Parmi eux, les 30 blocs magiques ajoutent chacun quatre sprites pour les effets spéciaux. Cela représente 120 sprites supplémentaires. Avec les éléments graphiques des liaisons (33 au maximum dans ce cas), nous devrons donc dessiner au moins 120 + 33 = 153 sprites à chaque image. La puissance de deux la plus proche est 256.
Cependant, fixer le maximum à 256 ne suffit pas. Chaque fois que nous vidons et réinitialisons le plateau, nous allons supprimer tous les objets de jeu actuels et en créer de nouveaux. Le nombre de sprites doit tenir compte de tous les objets qui existent au cours de l’image. Cela inclut les objets supprimés, car ils sont retirés à la fin de l’image. Fixer le nombre maximal de sprites à 512 suffira donc.
![]()
Toutes les ressources nécessaires au jeu ont été préparées à l’avance. Nous les ajoutons sous forme d’images de 512x512 pixels et laissons le moteur les réduire à la taille cible.
L’activation de hidpi dans les paramètres du projet fait passer le tampon arrière en haute résolution. De grandes images affichées à une échelle réduite paraîtront très nettes sur les écrans Retina.

En plus des blocs, une image de « connecteur » et des sprites d’effets sont fournis. Nous avons également deux images d’arrière-plan : l’une servira de fond au plateau de jeu et l’autre au menu principal. Ajoutez toutes les images au dossier images, puis créez un fichier d’atlas sprites.atlas. Ouvrez le fichier d’atlas et ajoutez-y toutes les images.

Un ensemble d’images d’interface graphique permet de créer des éléments tels que des boutons et des fenêtres contextuelles. Elles sont ajoutées à un atlas distinct nommé gui.atlas.
La première étape consiste à créer la logique du plateau. Le plateau disposera de sa propre collection, qui contiendra tout ce qui est affiché à l’écran pendant la partie. Pour le moment, seuls le composant (component) factory “blockfactory” et le script sont nécessaires. Plus tard, nous ajouterons une factory pour les liaisons, des composants d’interface graphique pour le menu principal et, enfin, des mécanismes de chargement pour démarrer une partie depuis le menu principal et un moyen de revenir au menu.
board.collection dans le dossier main. Veillez à la nommer “board” pour que nous puissions l’adresser plus tard. Si vous ajoutez le composant sprite de l’arrière-plan, veillez à fixer sa position Z à -1, sinon il ne sera pas dessiné derrière tous les blocs que nous créerons plus tard./main/board.collection pour faciliter les tests.

Le fichier de script board.script contiendra toute la logique du plateau lui-même et des blocs qu’il contient. Commencez par créer la fonction de construction du plateau et appelez-la (temporairement) depuis init(). Nous ajoutons également deux fonctions que nous n’utiliserons pas tout de suite, mais qui nous seront utiles plus tard :
filter()build_blocklist()Après la construction du plateau, nous utiliserons deux ensembles de données différents contenant tous les blocs, self.blocks et self.board :
-- board.script
go.property("timer", 0) -- Use to time events
local blocksize = 80 -- Distance between block centers
local edge = 40 -- Left and right edge.
local bottom_edge = 50 -- Bottom edge.
local boardwidth = 7 -- Number of columns
local boardheight = 9 -- Number of rows
local centeroff = vmath.vector3(8, -8, 0) -- Center offset for connector gfx since there's shadow below in the block img
local dropamount = 3 -- The number of blocks dropped on a "drop"
local colors = { hash("orange"), hash("pink"), hash("blue"), hash("yellow"), hash("green") }
--
-- filter(function, table)
-- e.g: filter(is_even, {1,2,3,4}) -> {2,4}
--
local function filter(func, tbl)
local new = {}
for i, v in pairs(tbl) do
if func(v) then
new[i] = v
end
end
return new
end
--
-- Build a list of blocks in 1 dimension for easy filtering
--
local function build_blocklist(self)
self.blocks = {}
for x, l in pairs(self.board) do
for y, b in pairs(self.board[x]) do
table.insert(self.blocks, { id = b.id, color = b.color, x = b.x, y = b.y })
end
end
end
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
msg.post(".", "acquire_input_focus")
msg.post("#", "start_level")
end
local function build_board(self)
math.randomseed(os.time())
local pos = vmath.vector3()
local c
local x = 0
local y = 0
for x = 0,boardwidth-1 do
pos.x = edge + blocksize / 2 + blocksize * x
self.board[x] = {}
for y = 0,boardheight-1 do
pos.y = bottom_edge + blocksize / 2 + blocksize * y
-- Calc z
pos.z = x * -0.1 + y * 0.01 -- <1>
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
self.board[x][y] = { id = id, color = c, x = x, y = y }
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
build_board(self)
end
end
La logique du plateau crée des objets de jeu “block” au moyen du composant factory “blockfactory”. Pour que cela fonctionne, nous devons créer l’objet de jeu du bloc. Le bloc possède un script et un sprite. Nous définissons l’animation par défaut du sprite sur l’un des blocs colorés de sprites.atlas, puis ajoutons du code à block.script pour que le bloc prenne la bonne couleur à sa création :

-- block.script
go.property("color", hash("none"))
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
Définissez la propriété Prototype du composant factory “blockfactory” sur le nouveau fichier d’objet de jeu block.go.

Vous devriez maintenant pouvoir lancer le jeu et voir le plateau rempli de blocs aux couleurs aléatoires :

Maintenant que nous avons un plateau, nous devons ajouter les interactions utilisateur. Définissons d’abord les associations d’entrées dans game.input_binding, dans le dossier input. Assurez-vous que les paramètres de game.project utilisent votre fichier d’associations d’entrées.

Nous n’avons besoin que d’une association et nous affectons MOUSE_BUTTON_LEFT au nom d’action “touch”. Ce jeu n’utilise pas le multitouch et, pour simplifier les choses, Defold convertit les entrées tactiles à un doigt en clics gauches de la souris.
Le traitement des entrées revient au plateau ; nous devons donc ajouter le code correspondant dans board.script :
-- board.script
function on_input(self, action_id, action)
if action_id == hash("touch") and action.value == 1 then
-- What block was touched or dragged over?
local x = math.floor((action.x - edge) / blocksize)
local y = math.floor((action.y - bottom_edge) / blocksize)
if x < 0 or x >= boardwidth or y < 0 or y >= boardheight or self.board[x][y] == nil then
-- outside board.
return
end
if action.pressed then
-- Player started touch
msg.post(self.board[x][y].id, "make_orange")
self.dragging = true
elseif self.dragging then
-- then drag
msg.post(self.board[x][y].id, "make_green")
end
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
end
end
Les messages make_orange et make_green sont seulement temporaires : ils fournissent un retour visuel qui confirme que le code fonctionne. Nous devons ajouter du code à block.script pour traiter ces messages :
-- block.script
function on_message(self, message_id, message, sender)
if message_id == hash("make_orange") then
sprite.play_flipbook("#sprite", hash("orange"))
elseif message_id == hash("make_green") then
sprite.play_flipbook("#sprite", hash("green"))
end
end
Les blocs recevront maintenant d’abord un message make_orange, puis des messages make_green tant que vous maintenez le contact tactile (ou le bouton de la souris enfoncé). Il est donc probable que les blocs ne fassent que clignoter en orange (voire pas du tout) avant de devenir verts. Mais nous savons bien quel bloc le joueur touche ! Si vous souhaitez suivre plus précisément le traitement des entrées, insérez des appels à print() ou à pprint() dans le code.
Nous avons maintenant besoin de ressources pour le marqueur qui indiquera que des blocs sont reliés par le joueur. L’idée consiste simplement à superposer un élément graphique à chaque bloc pour montrer qu’il est relié.
Nous devons créer un objet de jeu “connector” contenant l’image du sprite de connecteur, ainsi qu’un composant factory “connector factory” dans l’objet de jeu “board” :


Le script de cet objet de jeu est minimal : il doit seulement redimensionner l’image pour l’accorder au reste du jeu et définir correctement l’ordre Z.
-- connector.script
function init(self)
go.set_scale_xy(0.18) -- Scale in 2D without changing Z.
go.set(".", "position.z", 1) -- Put on top.
end
La fonction same_color_neighbors() renvoie une liste de blocs adjacents à un bloc donné (à la position x, y) et de même couleur. Elle utilise la fonction filter(), appliquée à la liste à plat complète des blocs dans self.blocks.
-- board.script
--
-- Returns a list of neighbor blocks of the same color as the
-- block on x, y
--
local function same_color_neighbors(self, x, y)
local f = function (v)
return (v.id ~= self.board[x][y].id) and
(v.x == x or v.x == x - 1 or v.x == x + 1) and
(v.y == y or v.y == y - 1 or v.y == y + 1) and
(v.color == self.board[x][y].color)
end
return filter(f, self.blocks)
end
Une fonction utilitaire in_blocklist() vérifie si un bloc existe dans une liste de blocs :
-- board.script
--
-- Does the block exist in the list of blocks?
--
local function in_blocklist(blocks, block)
for i, b in pairs(blocks) do
if b.id == block then
return true
end
end
return false
end
Nous utilisons ces fonctions lors des entrées tactiles et des glissements dans on_input() pour construire les chaînes de blocs touchés. Nous détectons et ignorons ici les blocs magiques, même s’il n’y en a pas encore :
-- board.script
function on_input(self, action_id, action)
...
-- If trying to manipulate magic blocks, ignore.
if self.board[x][y].color == hash("magic") then
return
end
if action.pressed then
-- List of neighbors of the same color as touched block
self.neighbors = same_color_neighbors(self, x, y)
self.chain = {}
table.insert(self.chain, self.board[x][y])
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
self.dragging = true
elseif self.dragging then
-- then drag
if in_blocklist(self.neighbors, self.board[x][y].id) and not in_blocklist(self.chain, self.board[x][y].id) then
-- dragging over a same-colored neighbor
table.insert(self.chain, self.board[x][y])
self.neighbors = same_color_neighbors(self, x, y)
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
end
end
Enfin, lorsque le contact tactile est relâché, supprimez visuellement tous les connecteurs des liaisons.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
end

La logique permettant de relier des blocs de même couleur est maintenant en place, et il est facile de supprimer les blocs reliés. Si nous définissons leur emplacement sur le plateau sur hash("removing") au lieu de simplement nil, c’est parce que, plus tard, lorsque nous implémenterons la logique des blocs magiques, nous devrons nous assurer qu’ils glissent uniquement vers les emplacements des blocs qui viennent d’être supprimés. Si nous définissons ici l’emplacement sur le plateau sur nil, nous ne pouvons pas distinguer les blocs qui viennent d’être supprimés de ceux qui l’ont été auparavant.
-- board.script
-- Remove the currently selected block-chain
--
local function remove_chain(self)
-- Delete all chained blocks
for i, c in ipairs(self.chain) do
self.board[c.x][c.y] = hash("removing")
go.delete(c.id)
end
self.chain = {}
end
Nous aurons aussi besoin d’une fonction pour supprimer réellement (définir sur nil) les emplacements du plateau qui ont été définis sur hash("removing") :
-- board.script
--
-- Set removed blocks to nil
--
local function nilremoved(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] == hash("removing") then
self.board[x][y] = nil
end
end
end
end
Nous créons également une fonction qui fait descendre les blocs restants lorsque les blocs situés sous eux sont supprimés (définis sur nil). Nous parcourons le plateau colonne par colonne, de gauche à droite, et chaque colonne de bas en haut. Si nous rencontrons un emplacement vide (nil), nous faisons descendre tous les blocs situés au-dessus.
-- board.script
--
-- Apply shift-down logic to all blocks.
--
local function slide_board(self)
-- Slide all remaining blocks down into blank spots.
-- Going column by column makes this easy.
local dy = 0
local pos = vmath.vector3()
for x = 0,boardwidth - 1 do
dy = 0
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil then
if dy > 0 then
-- Move down dy steps
self.board[x][y - dy] = self.board[x][y]
self.board[x][y] = nil
-- Calc new position
self.board[x][y - dy].y = self.board[x][y - dy].y - dy
go.animate(self.board[x][y-dy].id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * (y - dy), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y-dy].id, "position.z", x * -0.1 + (y-dy) * 0.01)
end
else
dy = dy + 1
end
end
end
-- blocklist needs updating
build_blocklist(self)
end

Nous pouvons maintenant simplement ajouter des appels à ces fonctions dans on_input() lorsque le contact tactile a été relâché et que self.chain contient des blocs.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and slide the remaining blocks down.
remove_chain(self)
nilremoved(self)
slide_board(self)
end
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Il est temps d’ajouter les blocs magiques. Tout d’abord, ajoutons la possibilité de transformer un bloc en bloc magique. Nous pourrons ainsi faire un passage distinct sur le plateau rempli et convertir les blocs de notre choix en blocs magiques. Pour donner un peu plus d’éclat aux blocs magiques, créons d’abord un effet magique animé sous la forme d’un objet de jeu magic_fx.go, que nous pourrons créer depuis le bloc magique.

Cet objet de jeu contient deux sprites. L’un représente la couleur “magic” (un sprite utilisant l’image magic-sphere_layer2.png) et l’autre est un effet “light” (un sprite utilisant l’image magic-sphere_layer3.png). L’objet est configuré pour tourner dès sa création, selon la valeur de la propriété direction. Nous faisons aussi en sorte que l’objet écoute deux messages : lights_on et lights_off, qui contrôlent le sprite de l’effet lumineux.
Créez un nouveau script et ajoutez-le comme composant script à magic_fx.go :
-- magic_fx.script
go.property("direction", hash("left"))
function init(self)
msg.post("#", "lights_off")
if self.direction == hash("left") then
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 3 + math.random())
else
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, -360, go.EASING_LINEAR, 2 + math.random())
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("lights_on") then
msg.post("#light", "enable")
elseif message_id == hash("lights_off") then
msg.post("#light", "disable")
end
end
Le bloc magique créera maintenant deux objets de jeu magic_fx à la réception du message make_magic. Chacun tournera dans une direction opposée, créant une jolie danse de couleurs à l’intérieur des blocs. Nous ajoutons également un sprite supplémentaire à block.go avec l’image magic-sphere_layer4.png. Cette image est placée à une coordonnée Z supérieure à celle de l’effet créé et dessine la coque, ou « enveloppe », de la sphère magique.

Notez que nous devons ajouter un composant Factory à l’objet de jeu du bloc et lui indiquer d’utiliser notre objet de jeu magic_fx.go comme Prototype. Le script du bloc doit également écouter les messages lights_on et lights_off et les transmettre aux objets créés. Notez que ces objets doivent être supprimés lorsque le bloc est supprimé. La fonction final() du bloc s’en charge. Tout cela se passe dans block.script.
-- block.script
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
self.fx1 = nil
self.fx2 = nil
msg.post("#cover", "disable")
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
function final(self)
if self.fx1 ~= nil then
go.delete(self.fx1)
end
if self.fx2 ~= nil then
go.delete(self.fx2)
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("make_magic") then
self.color = hash("magic")
msg.post("#cover", "enable")
msg.post("#sprite", "enable")
sprite.play_flipbook("#sprite", hash("magic-sphere_layer1"))
self.fx1 = factory.create("#fxfactory", p, nil, { direction = hash("left") })
self.fx2 = factory.create("#fxfactory", p, nil, { direction = hash("right") })
go.set_parent(self.fx1, go.get_id())
go.set_parent(self.fx2, go.get_id())
go.set(self.fx1, "position.z", 0.01)
go.set(self.fx1, "scale.xy", 1)
go.set(self.fx2, "position.z", 0.02)
go.set(self.fx2, "scale.xy", 1)
elseif message_id == hash("lights_on") or message_id == hash("lights_off") then
msg.post(self.fx1, message_id)
msg.post(self.fx2, message_id)
end
end
Nous pouvons maintenant créer des blocs magiques et les illuminer, un effet que nous utiliserons pour indiquer qu’un bloc magique se trouve à côté d’un autre.

Le code qui remplit le plateau de blocs doit maintenant être modifié pour y placer quelques blocs magiques :
-- board.script
local function build_board(self)
...
-- Distribute magic blocks.
local rand_x = 0
local rand_y
for y = 0, boardheight - 1, boardheight / self.num_magic do
local set = false
while not set do
rand_y = math.random(math.floor(y), math.min(boardheight - 1, math.floor(y + boardheight / self.num_magic)))
rand_x = math.random(0, boardwidth - 1)
if self.board[rand_x][rand_y].color ~= hash("magic") then
msg.post(self.board[rand_x][rand_y].id, "make_magic")
self.board[rand_x][rand_y].color = hash("magic")
set = true
end
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
La principale mécanique des blocs magiques est leur capacité à glisser latéralement lorsqu’un autre bloc disparaît à côté d’eux. Nous implémentons tous les détails de cette mécanique dans la fonction slide_magic_blocks() de board.script. L’algorithme est simple :
M de blocs magiques.M jusqu’à ce que la liste ne diminue plus. À chaque itération :
hash("removing"), retirez simplement le bloc magique de la liste M.hash("removing"), faites-le glisser dans ce trou, définissez son ancien emplacement sur hash("removing"), puis retirez-le de la liste M.-- board.script
-- Apply the shifting logic to magic blocks. Only slide to positions
-- marked for removal with hash("removing")
--
local function slide_magic_blocks(self)
-- Slide all magic blocks to the side that should slide first.
-- This works best going row by row!
local row_m
for y = 0,boardheight - 1 do
row_m = {}
-- Build list of magic blocks on this row.
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil and self.board[x][y] ~= hash("removing") and self.board[x][y].color == hash("magic") then
table.insert(row_m, self.board[x][y])
end
end
local mc = #row_m + 1
-- Go through list, slide and remove if possible. Reiterate until the list does not shrink.
while #row_m < mc do
mc = #row_m
for i, m in pairs(row_m) do
local x = m.x
if y > 0 and self.board[x][y-1] == hash("removing") then
-- Hole below, do nothing.
row_m[i] = nil
elseif x > 0 and self.board[x-1][y] == hash("removing") then
-- Hole to the left! Slide magic block there
self.board[x-1][y] = self.board[x][y]
self.board[x-1][y].x = x - 1
go.animate(self.board[x][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x - 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y].id, "position.z", (x - 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
elseif x < boardwidth - 1 and self.board[x + 1][y] == hash("removing") then
-- Hole to the right. Slide magic block there
self.board[x+1][y] = self.board[x][y]
self.board[x+1][y].x = x + 1
go.animate(self.board[x+1][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x + 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x+1][y].id, "position.z", (x + 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
end
end
end
end
end
Nous pouvons essayer cette mécanique en ajoutant un appel à la fonction dans on_input() :
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Nous voyons maintenant clairement pourquoi nous avons utilisé le marqueur intermédiaire hash("removing") sur les emplacements lors de leur suppression. Sans lui, les blocs magiques glisseraient d’un côté à l’autre dans n’importe quel emplacement vide adjacent. Une mécanique peut-être intéressante, mais qui ne correspond pas à celle prévue pour ce petit jeu.
Il nous faut maintenant une logique pour détecter si des blocs magiques sont connectés (situés à gauche, à droite, au-dessus ou au-dessous les uns des autres), et pour savoir si tous les blocs magiques du plateau sont connectés. L’algorithme utilisé est assez simple :
M de tous les blocs magiques du plateau.M :
region n’est définie pour le bloc, attribuez-lui le numéro de région R (initialement 1).R, puis poursuivez avec leurs voisins, les voisins de leurs voisins, et ainsi de suite.R de 1.
Voici l’implémentation de l’algorithme :
-- board.script
--
-- Build list of all current magic blocks.
--
local function magic_blocks(self)
local magic = {}
for x = 0,boardwidth - 1 do
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil and self.board[x][y].color == hash("magic") then
table.insert(magic, self.board[x][y])
end
end
end
return magic
end
--
-- Filter out adjacent magic blocks
--
local function adjacent_magic_blocks(blocks, block)
return filter(function (e)
return (block.x == e.x and math.abs(block.y - e.y) == 1) or
(block.y == e.y and math.abs(block.x - e.x) == 1)
end, blocks)
end
--
-- Spread region to neighbors
--
local function mark_neighbors(blocks, block, region)
local neighbors = adjacent_magic_blocks(blocks, block)
for i, m in pairs(neighbors) do
if m.region == nil then
m.region = region
mark_neighbors(blocks, m, region)
end
end
end
--
-- Mark all magic block regions
--
local function mark_magic_regions(self)
local m_blocks = magic_blocks(self)
-- 1. Clear all region marks and count neighbors
for i, m in pairs(m_blocks) do
m.region = nil
local n = 0
for _ in pairs(adjacent_magic_blocks(m_blocks, m)) do n = n + 1 end
m.neighbors = n
end
-- 2. Assign regions and spread them
local region = 1
for i, m in pairs(m_blocks) do
if m.region == nil then
m.region = region
mark_neighbors(m_blocks, m, region)
region = region + 1
end
end
return m_blocks
end
Nous créons aussi des fonctions permettant de compter le nombre de régions parmi les blocs magiques. S’il n’y a qu’une région, nous savons que tous les blocs magiques sont connectés. De plus, nous ajoutons une fonction qui éteint les lumières de tous les blocs magiques, et une autre qui active les effets lumineux des blocs magiques ayant des blocs magiques voisins :
-- board.script
--
-- Count the number of connected regions among the magic blocks.
--
local function count_magic_regions(blocks)
local maxr = 0
for i, m in pairs(blocks) do
if m.region > maxr then
maxr = m.region
end
end
return maxr
end
--
-- Shut off lights on all listed magic blocks
--
local function shutdown_lined_up_magic(self)
for i, m in ipairs(self.lined_up_magic) do
msg.post(m.id, "lights_off")
end
end
--
-- Set highlight for all magic blocks
--
local function highlight_magic(blocks)
for i, m in pairs(blocks) do
if m.neighbors > 0 then
msg.post(m.id, "lights_on")
else
msg.post(m.id, "lights_off")
end
end
end
Nous pouvons maintenant intégrer ces éléments de logique au déroulement général. Tout d’abord, comme la génération du plateau est aléatoire, il existe une faible probabilité qu’il soit gagnant dès le départ. Si cela arrive, nous supprimons simplement le plateau et le reconstruisons :
-- board.script
--
-- Clear the board
--
local function clear_board(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil then
go.delete(self.board[x][y].id)
self.board[x][y] = nil
end
end
end
end
local function build_board(self)
...
-- Build 1d list that we can easily filter.
build_blocklist(self)
local magic_blocks = mark_magic_regions(self)
if count_magic_regions(magic_blocks) == 1 then
-- "Win" from start. Make new board.
clear_board(self)
build_board(self)
end
highlight_magic(magic_blocks)
end
Le reste de la logique prend place dans on_input(). Il n’y a toujours pas de code pour traiter le message level_completed, mais cela convient pour le moment :
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and refill board.
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
local magic_blocks = mark_magic_regions(self)
-- Highlight adjacent magic blocks.
if count_magic_regions(magic_blocks) == 1 then
-- Win!
msg.post("#", "level_completed")
end
highlight_magic(magic_blocks)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Il est maintenant possible de jouer et d’atteindre la condition de victoire, même s’il ne se passe encore rien lorsque vous reliez tous les blocs magiques.

L’idée du « lâcher » est d’ajouter une mécanique de progression simple. Le joueur peut effectuer un nombre limité de lâchers en appuyant sur le bouton DROP : chaque lâcher fait simplement tomber quelques nouvelles pièces aléatoires sur le plateau. Le joueur commence avec un lâcher et en reçoit un supplémentaire chaque fois qu’il termine un niveau. Le code de cette mécanique tient dans deux fonctions : l’une renvoie la liste des emplacements où les blocs lâchés peuvent atterrir, et l’autre effectue le lâcher proprement dit, avec l’animation et tout le reste.
-- board.script
--
-- Find spots for a drop.
--
local function dropspots(self)
local spots = {}
for x = 0, boardwidth - 1 do
for y = 0, boardheight - 1 do
if self.board[x][y] == nil then
table.insert(spots, { x = x, y = y })
break
end
end
end
-- If more than dropamount, randomly remove a slot until dropamount
for c = 1, #spots - dropamount do
table.remove(spots, math.random(#spots))
end
return spots
end
--
-- Perform the drop
--
local function drop(self, spots)
for i, s in pairs(spots) do
local pos = vmath.vector3()
pos.x = edge + blocksize / 2 + blocksize * s.x
pos.y = 1000
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
go.animate(id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * s.y, go.EASING_OUTBOUNCE, 0.5)
-- Calc new z
go.set(id, "position.z", s.x * -0.1 + s.y * 0.01)
self.board[s.x][s.y] = { id = id, color = c, x = s.x, y = s.y }
end
-- Rebuild blocklist
build_blocklist(self)
end
Nous pouvons tester les lâchers en exécutant le code suivant, par exemple dans on_reload(), ou en l’associant à une action d’entrée temporaire :
s = dropspots(self)
if #s > 0 then
-- Do the drop
drop(self, s)
end

Il est temps de tout assembler. Commençons par créer un écran de démarrage et le séparer du plateau. La première étape consiste à créer main_menu.gui et à y placer un bouton Start (un nœud de texte et un nœud de boîte texturé), un nœud de texte pour le titre et quelques blocs décoratifs (des nœuds de boîte texturés). Le script main_menu.gui_script que nous attachons à l’interface graphique anime les blocs décoratifs dans init(). Il contient aussi une fonction on_input() qui envoie un message start_game à un script principal. Nous allons créer ce script dans un instant.

-- main_menu.gui_script
function init(self)
msg.post(".", "acquire_input_focus")
local bs = { "brick1", "brick2", "brick3", "brick4", "brick5", "brick6" }
for i, b in ipairs(bs) do
local n = gui.get_node(b)
local rt = (math.random() * 3) + 1
local a = math.random(-45, 45)
gui.set_color(n, vmath.vector4(1, 1, 1, 0))
gui.animate(n, "position.y", -100 - math.random(0, 50), gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "color.w", 1, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "rotation.z", a, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
end
gui.animate(gui.get_node("start"), "color.x", 1, gui.EASING_INOUTSINE, 1, 0, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local start = gui.get_node("start")
if gui.pick_node(start, action.x, action.y) then
msg.post("/main#script", "start_game")
end
end
end
Puisque le script du menu principal se chargera bientôt de démarrer le jeu, supprimez l’appel temporaire de création du plateau dans init(), dans board.script :
-- board.script
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
end
Le script principal conservera l’état général du jeu et démarrera la partie sur demande. Ici, nous voulons que main.collection contienne uniquement le minimum de ressources nécessaires à l’affichage au démarrage. Pour cela, main.collection contiendra un objet de jeu “main” regroupant l’interface graphique du menu principal, un composant script et, surtout, un composant Collection Proxy.
Le proxy de collection (collection proxy) nous permet de charger et de décharger dynamiquement des collections dans le jeu en cours d’exécution. Il agit au nom d’un fichier de collection donné : nous chargeons, initialisons, activons, désactivons et déchargeons la collection dynamique en envoyant des messages au proxy. Pour une description complète de leur utilisation, consultez la documentation des proxys de collection.
Dans notre cas, nous définissons la propriété Collection du composant proxy de collection sur board.collection, qui contient le « niveau ».

Nous devons maintenant ouvrir game.project et définir la collection bootstrap main_collection sur /main/main.collectionc.

Démarrer une partie consiste maintenant à envoyer des messages à notre proxy de collection pour charger, initialiser et activer le plateau, puis à désactiver le menu principal (pour le masquer). Le retour au menu principal effectue l’opération inverse (à condition que le proxy ait chargé la collection).
-- main.script
function init(self)
msg.post("#", "to_main_menu")
self.state = "MAIN_MENU"
end
function on_message(self, message_id, message, sender)
if message_id == hash("to_main_menu") then
if self.state ~= "MAIN_MENU" then
msg.post("#boardproxy", "unload")
end
msg.post("main:/main#menu", "enable") -- <1>
self.state = "MAIN_MENU"
elseif message_id == hash("start_game") then
msg.post("#boardproxy", "load")
msg.post("#menu", "disable")
elseif message_id == hash("proxy_loaded") then
-- Board collection has loaded...
msg.post(sender, "init")
msg.post("board:/board#script", "start_level", { difficulty = 1 }) -- <2>
msg.post(sender, "enable")
self.state = "GAME_RUNNING"
end
end
Avant d’ajouter la dernière partie de la logique au script du plateau, nous devons ajouter au plateau un ensemble d’éléments d’interface graphique. Nous plaçons d’abord un bouton RESTART et un bouton DROP au-dessus du plateau.

Le script de l’interface graphique du plateau envoie des messages à la boîte de dialogue de redémarrage lors d’un clic, et au script du plateau lui-même lorsque l’on clique sur DROP :
-- board.gui_script
function init(self)
msg.post("#", "show")
msg.post("/restart#gui", "hide")
msg.post("/level_complete#gui", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
msg.post("#", "enable")
elseif message_id == hash("set_drop_counter") then
local n = gui.get_node("drop_counter")
gui.set_text(n, message.drops .. " x")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local restart = gui.get_node("restart")
local drop = gui.get_node("drop")
if gui.pick_node(restart, action.x, action.y) then
-- Show the restart dialog box.
msg.post("/restart#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(drop, action.x, action.y) then
msg.post("/board#script", "drop")
end
end
end
La boîte de dialogue RESTART est simple. Nous la créons dans restart.gui et lui attachons un script simple qui ne fait rien si le joueur clique sur NO, envoie un message restart_level au script du plateau s’il clique sur YES, et un message to_main_menu au script principal s’il clique sur Quit to main menu :

-- restart.gui_script
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local yes = gui.get_node("yes")
local no = gui.get_node("no")
local quit = gui.get_node("quit")
if gui.pick_node(no, action.x, action.y) then
msg.post("#", "hide")
msg.post("/board#gui", "show")
elseif gui.pick_node(yes, action.x, action.y) then
msg.post("board:/board#script", "restart_level")
msg.post("/board#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(quit, action.x, action.y) then
msg.post("main:/main#script", "to_main_menu")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Nous créons aussi une boîte de dialogue simple de fin de niveau dans level_complete.gui, avec un script simple qui envoie un message next_level au script du plateau lorsque le joueur clique sur CONTINUE :

-- level_complete.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local continue = gui.get_node("continue")
if gui.pick_node(continue, action.x, action.y) then
msg.post("board#script", "next_level")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Une boîte de dialogue sert à présenter le niveau actuel, avec un script qui se limite à la masquer et à l’afficher. Lors de l’affichage, son message est défini de manière à inclure le niveau de difficulté actuel :

-- present_level.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
local n = gui.get_node("message")
gui.set_text(n, "Level " .. message.level)
msg.post("#", "enable")
end
end
Nous ajoutons également une boîte de dialogue qui s’affiche si le joueur tente d’effectuer un lâcher alors qu’il n’y a pas de place.

-- no_drop_room.gui_script
function init(self)
msg.post("#", "hide")
self.t = 0
end
function update(self, dt)
if self.t < 0 then
msg.post("#", "hide")
else
self.t = self.t - dt
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
self.t = 1
msg.post("#", "enable")
end
end
Enfin, nous ajoutons ces composants d’interface graphique à board.collection et le code nécessaire à board.script :

Nous devons écrire le code de traitement de tous les messages envoyés depuis et vers le plateau dans on_message().
start_levelgo.animate() comme temporisateur en animant la valeur de “timer”, qui n’est utilisée à aucune autre fin.restart_levellevel_completednext_level lorsque le joueur cliquera sur son bouton CONTINUE.next_levelstart_level avec le niveau de difficulté suivant.drop-- board.script
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
self.num_magic = message.difficulty + 1
build_board(self)
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post("present_level#gui", "show", { level = message.difficulty } )
-- Wait some...
go.animate("#", "timer", go.PLAYBACK_ONCE_FORWARD, 1, go.EASING_LINEAR, 2, 0, function ()
msg.post("present_level#gui", "hide")
msg.post(".", "acquire_input_focus")
end)
elseif message_id == hash("restart_level") then
clear_board(self)
build_board(self)
self.drops = 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post(".", "acquire_input_focus")
elseif message_id == hash("level_completed") then
-- turn off input
msg.post(".", "release_input_focus")
-- Animate the magic!
for i, m in ipairs(magic_blocks(self)) do
go.set_scale_xy(0.17, m.id)
go.animate(m.id, "scale.xy", go.PLAYBACK_LOOP_PINGPONG, 0.19, go.EASING_INSINE, 0.5, 0)
end
-- Show completion screen
msg.post("level_complete#gui", "show")
elseif message_id == hash("next_level") then
clear_board(self)
self.drops = self.drops + 1
-- Difficulty level is number of magic blocks - 1
msg.post("#", "start_level", { difficulty = self.num_magic })
elseif message_id == hash("drop") then
s = dropspots(self)
if #s == 0 then
-- Can't perform drop
msg.post("no_drop_room#gui", "show")
elseif self.drops > 0 then
-- Do the drop
drop(self, s)
self.drops = self.drops - 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
end
end
end
Et voilà ! Le jeu et ce tutoriel sont maintenant terminés ! Amusez-vous bien avec ce jeu !

Ce petit jeu présente des propriétés intéressantes et nous vous encourageons à expérimenter avec lui. Voici une liste d’exercices que vous pouvez réaliser pour vous familiariser davantage avec Defold :