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
Este juego es una variación del clásico juego de combinar al estilo de Bejeweled y Candy Crush. El jugador arrastra y enlaza bloques del mismo color para eliminarlos, pero el objetivo del juego no es eliminar largas series de bloques del mismo color, limpiar el tablero o acumular puntos, sino lograr que un conjunto de “magic blocks” especiales repartidos por el tablero se conecten.
Este tutorial está escrito como una guía paso a paso en la que construimos el juego sobre un diseño completo. En realidad, encontrar un diseño que funcione toma mucho tiempo y esfuerzo. Podrías empezar con una idea central y luego buscar una forma de prototiparla para comprender mejor qué puede aportar esa idea. Incluso un juego simple como “Magic Link” requiere bastante trabajo de diseño. Este juego pasó por un par de iteraciones y algo de experimentación hasta llegar a su forma final (y todavía lejos de ser perfecta) y su conjunto de reglas. Pero para este tutorial, vamos a saltarnos ese proceso y empezar a construir sobre el diseño final.
Necesitas empezar creando un nuevo proyecto e importando el paquete de assets:

El tablero se llena aleatoriamente con bloques de colores y un conjunto de magic blocks en cada ronda. Los bloques de colores siguen estas reglas:
Los magic blocks se comportan de otra manera, según estas reglas:
El jugador interactúa con el juego según las siguientes reglas:
El nivel de dificultad gobierna el número de magic blocks que se colocan en el tablero.
Como con todos los proyectos, necesitamos idear un plan general sobre cómo abordar la implementación. Hay muchas maneras en que el juego podría estructurarse y construirse. Técnicamente podríamos implementar todo el juego en el sistema GUI si quisiéramos. Sin embargo, construir el juego con objetos de juego y sprites, y usar las API de GUI para la GUI en pantalla y los elementos de heads-up display, suele ser la forma natural de construir un juego, así que tomaremos ese camino.
Como esperamos que el número de archivos se mantenga bastante bajo, mantendremos la estructura de carpetas del proyecto muy simple:

El archivo game.project se mantiene principalmente con la configuración predeterminada, pero hay algunos ajustes por decidir. En primer lugar, necesitamos seleccionar una resolución para el juego. Es bastante fácil cambiar la resolución en una etapa posterior, y para un juego final necesitaremos hacer algo de trabajo para que el juego se vea bien independientemente de la resolución o relación de aspecto del dispositivo objetivo.
Elegimos definir la resolución en 640x960 pixels, que es la resolución nativa del iPhone 4. También es una resolución que cabe en muchos monitores, así que el playtesting en la computadora se vuelve fluido. Si quieres trabajar en una resolución diferente, solo tendrás que ajustar algunos valores de otra manera.

También vamos a necesitar aumentar el número máximo de sprites renderizados. Si quieres, puedes saltar a la siguiente sección y volver aquí cuando se te notifique en la consola que alcanzaste el límite de sprites.

Podemos calcular un número máximo de sprites necesarios:
Entonces, supongamos que tenemos un máximo de 30 magic blocks. El tablero tiene 63 bloques (sprites). De estos, los 30 magic blocks agregan 4 sprites para efectos especiales. Eso son 120 sprites adicionales. Así que, con los gráficos de enlace (que son máximo 33 en este caso), necesitaremos dibujar al menos 120 + 33 = 153 sprites por frame. La potencia de dos más cercana es 256.
Sin embargo, definir el máximo en 256 no es suficiente. Cada vez que limpiemos y reiniciemos el tablero vamos a eliminar todos los objetos de juego actuales y generar nuevos. El contador de sprites tendrá que cubrir todos los objetos que están vivos durante el frame. Eso incluye cualquier objeto eliminado, porque se eliminan al final del frame. Por lo tanto, definir el número máximo de sprites en 512 será suficiente.
![]()
Todos los assets necesarios para el juego se prepararon de antemano. Los agregaremos como imágenes de 512x512 pixels y dejaremos que el motor las escale al tamaño objetivo.
Habilitar hidpi en la configuración del proyecto significa que el backbuffer se vuelve de alta resolución. Al dibujar imágenes grandes escaladas hacia abajo, se verán muy nítidas en pantallas retina.

Además de los bloques, se incluye una imagen “connector” más sprites de efectos. También tenemos dos imágenes de fondo. Una se usará como fondo del tablero de juego y otra se usará para el menú principal. Agrega todas las imágenes a la carpeta images, luego crea un archivo de atlas sprites.atlas. Abre el archivo de atlas y agrega todas las imágenes.

Hay un conjunto de imágenes GUI que se usan para crear elementos GUI, como botones y popups. Estas se agregan a un atlas separado llamado gui.atlas.
El primer paso es construir la lógica del tablero. El tablero residirá en su propia colección, que contendrá todo lo que aparece en pantalla durante el gameplay. Por ahora, lo único necesario es el componente factory “blockfactory” y el script. Más adelante agregaremos una factory para conexiones, componentes GUI del menú principal y finalmente mecánicas de carga para iniciar el gameplay desde el menú principal y una forma de salir al menú.
board.collection en la carpeta main. Asegúrate de nombrarla “board” para que podamos direccionarla más adelante. Si agregas el componente sprite de fondo, asegúrate de definir su posición Z en -1, o no se dibujará detrás de todos los bloques que generaremos luego./main/board.collection para poder probar fácilmente.

El archivo script board.script contendrá toda la lógica del tablero en sí y de los bloques del tablero. Empieza creando la función que construye el tablero e invócala (temporalmente) desde init(). También agregaremos dos funciones que no usaremos ahora pero que serán útiles más adelante:
filter()build_blocklist()Después de construir el tablero, usaremos dos conjuntos de datos diferentes que contienen todos los bloques, self.blocks y self.board:
-- board.script
go.property("timer", 0) -- Se usa para temporizar eventos
local blocksize = 80 -- Distancia entre centros de bloques
local edge = 40 -- Borde izquierdo y derecho.
local bottom_edge = 50 -- Borde inferior.
local boardwidth = 7 -- Número de columnas
local boardheight = 9 -- Número de filas
local centeroff = vmath.vector3(8, -8, 0) -- Offset central para el gfx del connector porque hay sombra debajo en la img del bloque
local dropamount = 3 -- El número de bloques que caen en un "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
--
-- Construye una lista de bloques en 1 dimensión para facilitar el filtrado
--
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 = {} -- Contiene la estructura del tablero
self.blocks = {} -- Lista de todos los bloques. Usada para facilitar el filtrado en la selección.
self.chain = {} -- Cadena de selección actual
self.connectors = {} -- Elementos connector para marcar la cadena de selección
self.num_magic = 3 -- Número de magic blocks en el tablero
self.drops = 1 -- Número de drops disponibles
self.magic_blocks = {} -- Magic blocks que están alineados
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
-- Calcula z
pos.z = x * -0.1 + y * 0.01 -- <1>
c = colors[math.random(#colors)] -- Elige un color aleatorio
local id = factory.create("#blockfactory", pos, null, { color = c })
self.board[x][y] = { id = id, color = c, x = x, y = y }
end
end
-- Construye lista 1d que podemos filtrar fácilmente.
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 lógica del tablero genera objetos de juego “block” mediante el componente factory “blockfactory”. Necesitamos construir el objeto de juego block para que esto funcione. El bloque tiene un script y un sprite. Definimos la animación predeterminada del sprite como cualquiera de los bloques de color en sprites.atlas, luego agregamos código a block.script para hacer que el bloque adopte el color correcto cuando se genera:

-- block.script
go.property("color", hash("none"))
function init(self)
go.set_scale(0.18) -- renderizado escalado hacia abajo
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
Define la propiedad Prototype del componente factory “blockfactory” al nuevo archivo gameobject block.go.

Ahora deberías poder ejecutar el juego y ver el tablero lleno de bloques de colores aleatorios:

Ahora que tenemos un tablero, debemos agregar interacción de usuario. Primero definimos los input bindings en game.input_binding en la carpeta input. Asegúrate de que la configuración de game.project use tu archivo de input bindings.

Solo necesitamos un binding y asignamos MOUSE_BUTTON_LEFT al nombre de acción “touch”. Este juego no usa multi touch y, por comodidad, Defold traduce la entrada de un dedo en clicks de mouse izquierdo.
El trabajo de tratar con el input recae en el tablero, así que necesitamos agregar código para eso en board.script:
-- board.script
function on_input(self, action_id, action)
if action_id == hash("touch") and action.value == 1 then
-- ¿Qué bloque fue tocado o atravesado al arrastrar?
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
-- fuera del tablero.
return
end
if action.pressed then
-- El jugador empezó el toque
msg.post(self.board[x][y].id, "make_orange")
self.dragging = true
elseif self.dragging then
-- luego arrastra
msg.post(self.board[x][y].id, "make_green")
end
elseif action_id == hash("touch") and action.released then
-- El jugador soltó el toque.
self.dragging = false
end
end
Los mensajes make_orange y make_green son solo temporales para obtener feedback visual de que el código funciona. Necesitamos agregar código a block.script para manejar estos mensajes:
-- 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
Ahora los bloques se rociarán primero con un mensaje make_orange, luego con mensajes make_green mientras mantengas el toque (o la pulsación del mouse), así que lo más probable es que los bloques solo parpadeen en naranja (si es que eso) antes de volverse verdes. ¡Pero sí sabemos qué bloque toca el jugador! Si quieres rastrear cómo se maneja el input con más detalle, inserta llamadas print() o pprint() en el código.
Ahora necesitamos assets para el marcador que se usará para indicar cuándo los bloques están enlazados por el jugador. La idea es simplemente superponer un gráfico en cada bloque para mostrar que está enlazado.
Necesitamos crear un objeto de juego “connector”, que contenga la imagen sprite connector y también un componente factory “connector factory” en el objeto de juego “board”:


El script de este objeto de juego es mínimo; solo necesita escalar los gráficos para que coincidan con el resto del juego y definir correctamente el orden Z.
-- connector.script
function init(self)
go.set_scale(0.18) -- Define la escala de este objeto de juego.
go.set(".", "position.z", 1) -- Colócalo encima.
end
La función same_color_neighbors() devuelve una lista de bloques que son adyacentes a un bloque particular (en la posición x, y) y del mismo color. Esta función usa la función filter() que se aplica a la lista plana completa de bloques en self.blocks.
-- board.script
--
-- Devuelve una lista de bloques vecinos del mismo color que el
-- bloque en 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
Una función auxiliar in_blocklist() comprueba si un bloque existe en una lista de bloques:
-- board.script
--
-- ¿Existe el bloque en la lista de bloques?
--
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
Usamos estas funciones durante el input de toque y arrastre en on_input() para construir los enlaces tocados de bloques. Aquí probaremos e ignoraremos magic blocks aunque todavía no haya ninguno:
-- board.script
function on_input(self, action_id, action)
...
-- Si intenta manipular magic blocks, ignorar.
if self.board[x][y].color == hash("magic") then
return
end
if action.pressed then
-- Lista de vecinos del mismo color que el bloque tocado
self.neighbors = same_color_neighbors(self, x, y)
self.chain = {}
table.insert(self.chain, self.board[x][y])
-- Marca bloque.
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
-- luego arrastra
if in_blocklist(self.neighbors, self.board[x][y].id) and not in_blocklist(self.chain, self.board[x][y].id) then
-- arrastrando sobre un vecino del mismo color
table.insert(self.chain, self.board[x][y])
self.neighbors = same_color_neighbors(self, x, y)
-- Marca bloque.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
end
end
Y finalmente, al soltar el toque, elimina visualmente todos los link connectors.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- El jugador soltó el toque.
self.dragging = false
-- Vacía la cadena de gráficos connector.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
end

Ahora tenemos la lógica en su lugar para permitir enlazar bloques de los mismos colores, y simplemente eliminar los bloques enlazados es fácil. La razón por la que definimos la posición en el tablero como hash("removing") en lugar de simplemente nil es porque más adelante, cuando hagamos la lógica de magic blocks, necesitamos asegurarnos de que los magic blocks se deslicen solo hacia bloques recién eliminados. Si definimos la posición en el tablero como nil aquí, no tenemos forma de distinguir entre bloques recién eliminados y bloques que fueron eliminados previamente.
-- board.script
-- Elimina la cadena de bloques actualmente seleccionada
--
local function remove_chain(self)
-- Elimina todos los bloques encadenados
for i, c in ipairs(self.chain) do
self.board[c.x][c.y] = hash("removing")
go.delete(c.id)
end
self.chain = {}
end
También necesitaremos una función para eliminar realmente (definir como nil) las posiciones en el tablero que se han definido como hash("removing"):
-- board.script
--
-- Define bloques eliminados como 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
También creamos una función que desliza los bloques restantes hacia abajo a medida que los bloques debajo de ellos se eliminan (se definen como nil). Iteramos sobre el tablero columna por columna de izquierda a derecha y recorremos cada columna de abajo hacia arriba. Si encontramos una posición vacía (nil), deslizamos hacia abajo todos los bloques encima de esa posición.
-- board.script
--
-- Aplica lógica de desplazamiento hacia abajo a todos los bloques.
--
local function slide_board(self)
-- Desliza todos los bloques restantes hacia abajo a los espacios vacíos.
-- Ir columna por columna hace esto fácil.
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
-- Mover hacia abajo dy pasos
self.board[x][y - dy] = self.board[x][y]
self.board[x][y] = nil
-- Calcula nueva posición
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)
-- Calcula nueva 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 necesita actualizarse
build_blocklist(self)
end

Ahora simplemente podemos agregar llamadas a estas funciones en on_input() cuando se haya soltado el toque y haya bloques en self.chain.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- El jugador soltó el toque.
self.dragging = false
if #self.chain > 1 then
-- Hay una cadena de bloques. Elimínala del tablero y desliza hacia abajo los bloques restantes.
remove_chain(self)
nilremoved(self)
slide_board(self)
end
-- Vacía la cadena de gráficos connector.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Ahora es momento de agregar los magic blocks a la mezcla. Primero, agreguemos la capacidad de que un bloque se convierta en magic block. De esa manera podemos hacer un paso separado sobre el tablero lleno y convertir los bloques que queremos en magic. Para darle algo de sabor a los magic blocks, primero creemos un efecto magic animado en forma de un objeto de juego magic_fx.go que podamos generar desde el magic block.

Este objeto de juego contiene dos sprites. Uno es el color “magic” (un sprite que usa la imagen magic-sphere_layer2.png) y el otro es un efecto “light” (un sprite que usa la imagen magic-sphere_layer3.png). El objeto está configurado para rotar cuando se genera, según el valor de la propiedad direction. También hacemos que el objeto escuche dos mensajes: lights_on y lights_off, que controlan el sprite del efecto de luz.
Crea un nuevo script y agrégalo como componente script a 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
Ahora, el magic block generará dos objetos de juego magic_fx al recibir el mensaje make_magic. Cada uno rotará en dirección opuesta, creando una bonita danza de color dentro de los bloques. También agregamos un sprite adicional a block.go con la imagen magic-sphere_layer4.png. Esta imagen se coloca en una Z más alta que el efecto generado y dibuja la cáscara o “cover” de la esfera mágica.

Ten en cuenta que debemos agregar un componente Factory al objeto de juego block y decirle que use nuestro objeto de juego magic_fx.go como Prototype. El script block también necesita escuchar los mensajes lights_on y lights_off y propagarlos hacia abajo a los objetos generados. Ten en cuenta que los objetos generados deben eliminarse cuando se elimina el bloque. Esto se encarga en la función final() del bloque. Todo esto ocurre en block.script.
-- block.script
function init(self)
go.set_scale(0.18) -- renderizado escalado hacia abajo
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", 1)
go.set(self.fx2, "position.z", 0.02)
go.set(self.fx2, "scale", 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
Ahora podemos crear magic blocks y también encenderlos, un efecto que usaremos para indicar que un magic block está junto a otro magic block.

El código que llena el tablero con bloques ahora necesita modificarse para que tengamos algunos magic blocks allí:
-- board.script
local function build_board(self)
...
-- Distribuye 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
-- Construye lista 1d que podemos filtrar fácilmente.
build_blocklist(self)
end
La mecánica principal de los magic blocks es su capacidad de deslizarse hacia los lados cuando otro bloque desaparece junto a ellos. Reflejamos todos los detalles de esa mecánica en la función slide_magic_blocks() en board.script. El algoritmo es simple:
M de magic blocks.M hasta que no se reduzca. En cada iteración:
hash("removing") debajo, simplemente elimínalo de la lista M.hash("removing"), deslízalo ahí, define su posición anterior como hash("removing") y luego elimínalo de la lista M.-- board.script
-- Aplica la lógica de desplazamiento a magic blocks. Solo se desliza a posiciones
-- marcadas para eliminación con hash("removing")
--
local function slide_magic_blocks(self)
-- Desliza todos los magic blocks hacia el lado que debe deslizar primero.
-- ¡Esto funciona mejor yendo fila por fila!
local row_m
for y = 0,boardheight - 1 do
row_m = {}
-- Construye lista de magic blocks en esta fila.
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
-- Recorre la lista, desliza y elimina si es posible. Reitera hasta que la lista no se reduzca.
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
-- Hueco debajo, no hacer nada.
row_m[i] = nil
elseif x > 0 and self.board[x-1][y] == hash("removing") then
-- ¡Hueco a la izquierda! Desliza magic block ahí
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)
-- Calcula nueva z
go.set(self.board[x][y].id, "position.z", (x - 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Se convertirá en nil luego
row_m[i] = nil
elseif x < boardwidth - 1 and self.board[x + 1][y] == hash("removing") then
-- Hueco a la derecha. Desliza magic block ahí
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)
-- Calcula nueva z
go.set(self.board[x+1][y].id, "position.z", (x + 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Se convertirá en nil luego
row_m[i] = nil
end
end
end
end
end
Podemos probar la mecánica agregando una llamada a la función en on_input():
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- El jugador soltó el toque.
self.dragging = false
if #self.chain > 1 then
-- Hay una cadena de bloques. Elimínala del tablero
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Desliza hacia abajo los bloques restantes.
slide_board(self)
end
self.chain = {}
-- Cadena vacía limpia los gráficos connector.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Ahora vemos claramente por qué usamos la “etiqueta” intermedia hash("removing") en posiciones al eliminarlas. Sin ella, los magic blocks se deslizarían de un lado a otro hacia cualquier posición vacía al costado. Quizá una mecánica interesante, pero no la pensada para este pequeño juego.
Ahora necesitamos lógica para detectar si los magic blocks están conectados (sentados a la izquierda, derecha, arriba o abajo unos de otros), y necesitamos saber si todos los magic blocks del tablero están conectados. El algoritmo usado es bastante directo:
M de todos los magic blocks del tablero.M:
region definida, asígnale el número de región R (inicialmente 1).R e itera hacia sus vecinos, los vecinos de sus vecinos, etc.R en 1.
Esta es la implementación del algoritmo:
-- board.script
--
-- Construye lista de todos los magic blocks actuales.
--
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
--
-- Filtra magic blocks adyacentes
--
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
--
-- Propaga región a vecinos
--
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
--
-- Marca todas las regiones de magic blocks
--
local function mark_magic_regions(self)
local m_blocks = magic_blocks(self)
-- 1. Limpia todas las marcas de región y cuenta vecinos
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. Asigna regiones y las propaga
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
También creamos funciones que nos permiten contar el número de regiones entre los magic blocks. Si el número de regiones es 1, sabemos que todos los magic blocks están conectados. Además, agregamos una función que apaga las luces en todos los magic blocks y otra que enciende los efectos de luz en los magic blocks que tienen magic blocks vecinos:
-- board.script
--
-- Cuenta el número de regiones conectadas entre los 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
--
-- Apaga luces en todos los magic blocks listados
--
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
--
-- Define highlight para todos los 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
Ahora podemos insertar estas partes de lógica en el flujo general. Primero, como la generación del tablero es aleatoria, hay una pequeña probabilidad de que empiece en estado ganador. Si eso ocurre, simplemente descartamos el tablero y lo construimos otra vez:
-- board.script
--
-- Limpia el tablero
--
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)
...
-- Construye lista 1d que podemos filtrar fácilmente.
build_blocklist(self)
local magic_blocks = mark_magic_regions(self)
if count_magic_regions(magic_blocks) == 1 then
-- "Victoria" desde el inicio. Crear tablero nuevo.
clear_board(self)
build_board(self)
end
highlight_magic(magic_blocks)
end
El resto de la lógica encaja en on_input(). Todavía no hay código para tratar con el mensaje level_completed, pero eso está bien por ahora:
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- El jugador soltó el toque.
self.dragging = false
if #self.chain > 1 then
-- Hay una cadena de bloques. Elimínala del tablero y rellena el tablero.
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Desliza hacia abajo los bloques restantes.
slide_board(self)
local magic_blocks = mark_magic_regions(self)
-- Resalta magic blocks adyacentes.
if count_magic_regions(magic_blocks) == 1 then
-- ¡Victoria!
msg.post("#", "level_completed")
end
highlight_magic(magic_blocks)
end
self.chain = {}
-- Cadena vacía limpia los gráficos connector.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Ahora es posible jugar y alcanzar el estado ganador, aunque todavía no pasa nada cuando enlazas todos los magic blocks.

La idea del “drop” es agregar una mecánica de progresión simple. El jugador puede realizar un número limitado de “drop”, que simplemente deja caer un par de piezas aleatorias nuevas sobre el tablero, presionando el botón DROP. El jugador empieza con un drop y cada vez que se limpia un nivel se concede un drop adicional. El código para la mecánica de drop encaja en dos funciones. Una devuelve una lista de posibles lugares donde pueden terminar los drops, y otra realiza el drop real con animación y todo.
-- board.script
--
-- Encuentra lugares para un 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
-- Si hay más que dropamount, elimina aleatoriamente un slot hasta dropamount
for c = 1, #spots - dropamount do
table.remove(spots, math.random(#spots))
end
return spots
end
--
-- Realiza el 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)] -- Elige un color aleatorio
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)
-- Calcula nueva 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
-- Reconstruye blocklist
build_blocklist(self)
end
Podemos probar drops ejecutando lo siguiente, por ejemplo en on_reload(), o vinculándolo a una acción de input temporal:
s = dropspots(self)
if #s > 0 then
-- Realiza el drop
drop(self, s)
end

Ahora es momento de unirlo todo. Primero, creemos una pantalla inicial y separémosla del tablero. El paso 1 es crear un main_menu.gui y configurarlo con un botón Start (un nodo de texto y un nodo caja texturizado), un nodo de texto de título y algunos bloques decorativos (nodos caja texturizados). El script main_menu.gui_script que adjuntamos a la GUI anima los bloques decorativos en init(). También contiene un on_input() que envía un mensaje start_game a un script principal. Crearemos ese script en un minuto.

-- 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
Como el trabajo de iniciar el juego pronto lo hará el script del menú principal, elimina la llamada temporal de configuración del tablero en init() en board.script:
-- board.script
--
-- INIT
--
function init(self)
self.board = {} -- Contiene la estructura del tablero
self.blocks = {} -- Lista de todos los bloques. Usada para facilitar el filtrado en la selección.
self.chain = {} -- Cadena de selección actual
self.connectors = {} -- Elementos connector para marcar la cadena de selección
self.num_magic = 3 -- Número de magic blocks en el tablero
self.drops = 1 -- Número de drops disponibles
self.magic_blocks = {} -- Magic blocks que están alineados
self.dragging = false -- Drag touch input
end
El script principal mantendrá el estado general del juego e iniciará el juego cuando se le solicite. Lo que queremos hacer aquí es hacer que main.collection contenga solo la cantidad mínima de assets que necesitamos mostrar al iniciar. Lo hacemos dejando que main.collection contenga un objeto de juego “main” que sostiene la GUI del menú principal, un componente script y, lo más importante, un componente Collection Proxy.
El proxy de colección nos permite cargar y descargar dinámicamente colecciones en el juego en ejecución. Actúa en nombre de un archivo de colección especificado y cargamos, inicializamos, habilitamos, deshabilitamos y descargamos la colección dinámica enviando mensajes al proxy. Para una descripción completa de cómo usarlos, consulta la documentación de Collection Proxy.
En nuestro caso definimos la propiedad Collection del componente collection proxy como board.collection, que contiene el “level”.

Ahora debemos abrir game.project y cambiar el bootstrap main_collection a /main/main.collectionc.

Ahora, iniciar un juego significa enviar mensajes a nuestro collection proxy para cargar, inicializar y habilitar el tablero, y luego deshabilitar el menú principal (para que no se muestre). Volver al menú principal hace lo contrario (dado que el proxy ha cargado la colección).
-- 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
Antes de agregar la pieza final de lógica al script del tablero, debemos agregar un conjunto de elementos GUI al tablero. Primero, en la parte superior del tablero, agregamos un botón RESTART y un botón DROP.

El script de la GUI del tablero envía mensajes al elemento de diálogo GUI de reinicio al hacer click y de vuelta al propio script del tablero al hacer click en 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
-- Muestra el cuadro de diálogo de reinicio.
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
El diálogo RESTART es simple. Lo construimos como restart.gui y adjuntamos un script simple que no hace nada si el jugador hace click en NO, envía un mensaje restart_level al script del tablero si el jugador hace click en YES y un mensaje to_main_menu al script principal si el jugador hace click en 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 todo el input hasta que desaparezcamos.
return true
end
También construimos un diálogo GUI simple para completar nivel en level_complete.gui con un script simple que envía un mensaje next_level al script del tablero cuando el jugador hace click en 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 todo el input hasta que desaparezcamos.
return true
end
Un diálogo que se usa para presentar el nivel actual, con un script que solo incluye ocultar y mostrar el diálogo. Al mostrar, el mensaje del diálogo se define como un mensaje que incluye el nivel de dificultad actual:

-- 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
También agregamos un diálogo que se muestra si el jugador intenta hacer un drop pero no hay espacio para él.

-- 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
Finalmente, agregamos estos componentes GUI a board.collection y agregamos el código necesario a board.script:

Necesitamos código para todos los mensajes que se envían hacia y desde el tablero en on_message().
start_levelgo.animate() como temporizador animando el valor de “timer”, que no se usa para nada más.restart_levellevel_completednext_level cuando el jugador haga click en el botón CONTINUE del diálogo.next_levelstart_level con el siguiente nivel de dificultad definido.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 } )
-- Espera un poco...
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
-- apaga input
msg.post(".", "release_input_focus")
-- ¡Anima la magia!
for i, m in ipairs(magic_blocks(self)) do
go.set_scale(0.17, m.id)
go.animate(m.id, "scale", go.PLAYBACK_LOOP_PINGPONG, 0.19, go.EASING_INSINE, 0.5, 0)
end
-- Muestra pantalla de completado
msg.post("level_complete#gui", "show")
elseif message_id == hash("next_level") then
clear_board(self)
self.drops = self.drops + 1
-- El nivel de dificultad es número de magic blocks - 1
msg.post("#", "start_level", { difficulty = self.num_magic })
elseif message_id == hash("drop") then
s = dropspots(self)
if #s == 0 then
-- No se puede realizar drop
msg.post("no_drop_room#gui", "show")
elseif self.drops > 0 then
-- Realiza el drop
drop(self, s)
self.drops = self.drops - 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
end
end
end
¡Ahí lo tienes! ¡El juego, y este tutorial, ahora están completos! ¡Disfruta jugando este juego!

Este pequeño juego tiene algunas propiedades interesantes y te animamos a experimentar con él. Aquí hay una lista de ejercicios que puedes hacer para familiarizarte más con Defold: