Cómo crear un Puzzle Bobble: Space Puzzle 8 en Lua

May 14

Cómo crear un Puzzle Bobble: Space Puzzle 8 en Lua

DATOS DE CADA BURBUJA

Para crear un puzzle bubble necesitamos almacenar los datos de cada burbuja de la escena en un lugar de la memoria, como buenos programadores hemos de analizar las estructuras de datos que hagan más fácil las comprobaciones de estado del juego para actualizar estas burbujas.

En mi caso hice una investigación en Internet para ver qué estructuras de datos estaban usando otros programadores, encontré la web de Dashiell Gough, que explica un poco por encima cómo funciona su juego, Cocos2D y las propiedades del motor físico, utilizando las colisiones, así es como organiza su tablero o matriz de burbujas.

En este caso se utiliza un vector de la STL, algoritmos floodfill y pilas, como el lenguaje que vamos a usar es Lua, todo se simplifica y usamos tablas (guardan punteros).

CASO DE USO: ELIGIENDO LA ESTRUCTURA DE DATOS SIN TENER UNA VISIÓN GLOBAL DEL PROBLEMA:

se trata de una serie de punteros a las burbujas hermanas, en las 6 direcciones: izquierda, derecha, arriba izquierda, arriba derecha, abajo izquierda y abajo derecha. Algo parecido a lo que se explica aquí: How to make a matrix for puzzle bubble. En Lua se usan variables locales a clases para guardar punteros. Esta opción de estructura de datos no nos sirve porque se complica demasiado a la hora de actualizar y comprobar estados generales de conjunto e individualmente, por ejemplo, para poder llegar a cualquier burbuja desde una burbuja dada la única manera sería usar un algoritmo recursivo con bastantes condiciones de ruptura.

El problema pues se reduce a un problema NP-completo sin solución trivial, es decir, tenemos que apañarnos con algún truco intermedio y hacer un poco el burro? o quizás optar por comernos un poco el coco y dar con una solución matemática que nos resuelva la papeleta eficientemente para casos muy grandes?…

ELEGIR CORRECTAMENTE LA ESTRUCTURA DE DATOS:

En nuestro caso vamos a optar por una solución por fuerza bruta ya que el número de elementos es muy pequeño y resolver un problema NP-completo se sale del objetivo de nuestro juego. Lo que hacemos es construir una matriz bidimensional de burbujas, estas son clases que guardan el color, métodos para realizar cierto tipo de acciones en función de su estado local y alguna cosa más. Así, recorremos la matriz y el orden de eficiencia como mucho será de O (n^2) y si tenemos un tablero de 7×8 tampoco es demasiado grande por lo que es altamente eficiente meter comprobaciones anidadas si las necesitamos.

Para dibujar la burbuja lo tenemos fácil, usamos el radio, podemos pegar una textura justo después de pintar en pantalla una circunferencia para darle más detalle a los gráficos, algún efecto ,etc. En el caso de la estructura de datos con punteros con hermanos y hermanas además tenemos un método para pintar qué puntero está siendo utilizado de modo que (ver vídeo) sabemos para cualquiera, qué burbuja tiene conectada a cualquier otra.

Ok, ahora vamos a hacerlo bien…éste es el resultado: Space Puzzle 8

Pasos en que se genera el juego Space Puzzle Bubble 8:

  1. Comprueba en el bucle principal en qué estado se encuentra el juego, estos los diseñamos nosotros, en este caso son:
    menú
    juego (puzzle)
    créditos
    en función del estado llamamos a la clase que se encargue de cada estado (menu, puzzle)
  2. la parte del menú es sencilla, se repite el dibujado de las opciones del menú y se comprueba la entrada para seleccionar una opción (interacción del jugador)
  3. los créditos es otra sección sencilla, apenas se trataría de replicar la opción anterior
  4. el juego, la clase puzzle se encarga de manejar toda la lógica del juego en sí, tiene una serie de estados que ayudan a saber qué hacer en cada momento del mismo

Para cambiar el estado del juego podemos hacer una función sencilla como esta:

function change_Game_Mode(new_Game_Mode)
    Game_Mode = new_Game_Mode
    currentColors = {}
    if new_Game_Mode == GAME_MODE_MENU then
        puzzle        = nil
        menu          = Menu()
    elseif new_Game_Mode == GAME_MODE_XIX then
        menu          = nil  
    elseif new_Game_Mode == GAME_MODE_PLAY then
        level         = 9 -- start level
        score         = 0 -- user score
        menu          = nil
        puzzle        = Puzzle(8,12,false) -- width, height, load level
        OrientAngle   = 0
    end
end

Fijaros en que algunas clases las ponemos como nil para liberar memoria.
Cada estado pertenece a uno de las siguientes categorías:

  1. Jugando ( PUZZLE_PLAYING = 1 )
  2. Cargando nivel ( PUZZLE_LOADING = 2 )
  3. Pasando al siguiente nivel ( PUZZLE_NEXT = 3 )
  4. Puzzle terminado ( PUZZLE_END = 4 )
  5. Nivel de jefe final ( PUZZLE_BOSS = 5 )
  6. Juego completado con éxito ( PUZZLE_WIN = 6 )

Un nivel se define en una lista doble de colores de burbujas, la primera lista, es decir:

levels = {
 { -- Level 1 
    -- row 1
    {BUBBLE_RED,BUBBLE_RED,BUBBLE_YELLOW,BUBBLE_YELLOW,
      BUBBLE_BLUE,BUBBLE_BLUE,BUBBLE_GREEN,
       BUBBLE_GREEN},
    -- row 2
    {BUBBLE_RED,BUBBLE_RED,BUBBLE_YELLOW,BUBBLE_YELLOW,BUBBLE_
     BLUE,BUBBLE_BLUE,BUBBLE_GREEN},
    -- row 3
    {BUBBLE_BLUE,BUBBLE_BLUE,BUBBLE_GREEN,BUBBLE_GREEN,
     BUBBLE_RED,BUBBLE_RED,BUBBLE_YELLOW,
       BUBBLE_YELLOW},
    -- row 4
   {BUBBLE_BLUE,BUBBLE_BLUE,BUBBLE_GREEN,BUBBLE_GREEN,
    BUBBLE_RED,BUBBLE_RED,BUBBLE_YELLOW}
  },
--etc.

evidentemente, los nombres de los colores representan un número de modelo para la clase Bubble, para inicializar una burbuja en dicha clase haremos algo así:

function Bubble:init(position,size,mycolor)
    self.position = position
    self.size     = size
    self.explored = false
    self.connected= false -- new algorithm 1/2/2012
    self.state    = BUBBLE_CREATED
    self.angle    = 0
    self.rotate   = 1
    self.image    = puzzle.bubbleSprite
    --etc.
end

y para cargar los datos del nivel que inicializan una a una las burbujas que hemos especificado:

function Puzzle:loadPuzzle()
    -- generar un modelo de gráficos aleatorio para las burbujas
    self.bubbleSprite = generate_random_bubble_sprite(nil)
    self.state        = PUZZLE_LOADING
    -- contadores a cero:
    local n = 0
    local k = 0
    -- guardar ancho y alto en una variable local, más eficiente
    local w = self.width
    local h = self.height 
    -- bucle para leer todo el array de niveles anterior
    for i=1, h do
     -- crear la fila
     self.bubbles[i] = {}
     n = 0
     if levels[level][i]~=nil then
         n = #(levels[level][i])
     end
     local aux = 0
     -- truco para saber si es una fila par o impar 
     -- (posicionado distinto)
     if math.fmod(i,2)==0 then
        aux = (self.bubbleSize/2) + 16
     end
     local y = self.initHeight+self.tileSize*self.height - 
               self.bubbleSize - (i-1)*self.tileSize
     for j=1,self.width do
        local c,state = nil,BUBBLE_EMPTY
        if nil==levels[level][i] or nil == levels[level][i][j] 
        then 
            c = nil
            if aux>0 and j==self.width then
                state = nil
            end
        else
            c = levels[level][i][j]
            state = BUBBLE_ATTACHED
            self:addCurrentColor(c,currentColors)
        end
        -- posicionar la burbuja en función del número en la matriz:
        self.bubbles[i][j] = Bubble(
            vec2(
             aux+self.initWidth+self.tileSize *
             j - self.bubbleSize,
            y),
            self.tileSize, -- bubble radius size
            c --color
         ) 
        self.bubbles[i][j].state = state 
      end 
    end
    self.state = PUZZLE_PLAYING
end

Una vez cargado el nivel ya podemos empezar a generar burbujas que lanzar hacia la palestra!, para eso, la función a continuación utiliza el acelerómetro

function Puzzle:launchBubble()
    -- si estamos en medio de una animación o no hay 
    --  ninguna nueva burbuja preparada, salir
    if self.animating or self.nextBubble == nil then return end
    -- con una burbuja lista para lanzar, cambiamos su estado:
    if self.nextBubble.state == BUBBLE_CREATED then
        self.nextBubble.state = BUBBLE_FLYING
        -- la velocidad en x depende de la orientación del iPad
        self.nextBubble.velocity = vec2(
            Gravity.x*12+math.tan(OrientAngle)*12,
            self.shootSpeed*DeltaTime*69*23
        )
    end
end

Ahora que la burbuja está en estado “volando”, desde la función de dibujado, actualizaremos su posición añadiendo la velocidad en x e y, comprobando en cada frame si entra en colisión con alguna parte del puzzle, esto es: el escenario o alguna de las burbujas que ya están colocadas. Esto se hace con la función “checkNextBubbleColliding”:

function Puzzle:checkNextBubbleColliding()
    local r = {}
    local aux = nil
    local nb  = self.nextBubble
    local k,t = 0,0
    local r   = self.tileSize-10
    local q   = 0
    local p   = nb.position
    local d   = 99999
 
    for i=1,self.height do
     for j=1,self.width do
       local b = self.bubbles[i][j]
       if b.state == BUBBLE_ATTACHED then
          -- comprobamos la colisión por el perímetro de 
          -- la circunferencia:
          q = math.sqrt( (b.position.x-p.x)*(b.position.x-p.x) +
                       (b.position.y-p.y)*(b.position.y-p.y) )
 
          if q<r and q<d then
            if aux == nil then
                aux = b
                k   = i
                t   = j
            elseif aux.position.y < b.position.y then
                aux = b
                k   = i
                t   = j
                break
            end
            if aux ~= nil then break end
            d = q
        end
      end -- end if bubble attached
     end -- for j,h
    end -- for i,w
    -- ha encontrado una burbuja con la que ha colisionado 
    -- a la distancia mínima
    -- ahora debemos conectar la burbuja a ella o la 
    -- que sea mejor candidata vecina (adyacente)
    if aux ~= nil then
        local xn,yn = nb.position.x,nb.position.y,
        local xa,ya = aux.position.x,aux.position.y
        -- debemos recorrer las 6 posibilidades
        if xn <= xa and yn <= ya then
            self:attachBubble(k,t,BUBBLE_DOWN_LEFT)
        elseif xn <= xa and yn>= (ya+r) then
            self:attachBubble(k,t,BUBBLE_UP_LEFT)
        elseif xn <= xa and yn >= ya then
            self:attachBubble(k,t,BUBBLE_LEFT)
        elseif xn >= xa and yn <= ya then
            self:attachBubble(k,t,BUBBLE_DOWN_RIGHT)
        elseif xn >= xa and yn >= (ya+r) then
            self:attachBubble(k,t,BUBBLE_UP_RIGHT) 
        else
            self:attachBubble(k,t,BUBBLE_RIGHT)
        end
        return true
    end
    return false
end

la función recursiva attachBubble comprobará la burbuja con la que ha chocado y cuando encuentre el mejor candidato a la solución asignará la burbuja generada que estaba en estado lanzada a enganchada añadiéndola en su lugar de la matriz, después buscará soluciones con el algoritmo floodfill adaptado en la función

function Puzzle:findSolutions(i,j)
    self:clearBubbleExplorations()
    self.animaBubbs = {}
    self:findFallingBubblesFrom(i,j)
    if ((#self.animaBubbs)>2) then
        self:findAdjacentAnimaFallingBubbles()
        self.animating = true
        self.animaTime = 6
        sound(SOUND_EXPLODE, 46131)
    else
        sound(SOUND_HIT, 20242)
    end
end
function Puzzle:findFallingBubblesFrom(i,j)
    local bubble = self.bubbles[i][j]
    if bubble.explored then return 0 end
    local b = nil -- base case
    local h = self.height
    local w = self.width
 
    table.insert(self.animaBubbs, table.maxn(self.animaBubbs)+1, vec2(i,j))
    bubble.explored = true
    local a = math.fmod(i,2)==0
    if a then
        -- up left
        if i>1 and self.bubbles[i-1][j].state==BUBBLE_ATTACHED and 
        self.bubbles[i-1][j].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i-1,j)
        end
        -- up right
        if i>1 and j<w and self.bubbles[i-1][j+1].state==BUBBLE_ATTACHED and 
        self.bubbles[i-1][j+1].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i-1,j+1)
        end
        -- down right
        if i<h and j<w and self.bubbles[i+1][j+1].state==BUBBLE_ATTACHED and 
        self.bubbles[i+1][j+1].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i+1,j+1)
        end
        -- down left
        if i<h and j>1 and self.bubbles[i+1][j].state==BUBBLE_ATTACHED and 
        self.bubbles[i+1][j].mycolor == bubble.mycolor then
         self:findFallingBubblesFrom(i+1,j)
        end
    else
        -- up left
        if i>1 and j>1 and self.bubbles[i-1][j-1].state==BUBBLE_ATTACHED and 
        self.bubbles[i-1][j-1].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i-1,j-1)
        end
        -- up right
        if i>1 and self.bubbles[i-1][j].state==BUBBLE_ATTACHED and 
        self.bubbles[i-1][j].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i-1,j)
        end
        -- down right
        if i<h and self.bubbles[i+1][j].state==BUBBLE_ATTACHED and 
        self.bubbles[i+1][j].mycolor == bubble.mycolor then
             self:findFallingBubblesFrom(i+1,j)
        end
        -- down left
        if i<h and j>1 and self.bubbles[i+1][j-1].state==BUBBLE_ATTACHED and 
        self.bubbles[i+1][j-1].mycolor == bubble.mycolor then
         self:findFallingBubblesFrom(i+1,j-1)
        end
    end
 
 
    -- left
    if j>1 and self.bubbles[i][j-1].state==BUBBLE_ATTACHED and 
        self.bubbles[i][j-1].mycolor == bubble.mycolor then
         self:findFallingBubblesFrom(i,j-1)
    end
 
 
    -- right 
    if j<w and self.bubbles[i][j+1].state==BUBBLE_ATTACHED and 
        self.bubbles[i][j+1].mycolor == bubble.mycolor then
         self:findFallingBubblesFrom(i,j+1)
    end
 
 
 
end

la función recorre el puzzle encontrando las burbujas adyacentes en las 6 direcciones, marcándolas si son del mismo color, es decir, añadiéndolas a un grupo, después buscará todas las que se encuentren bajo ellas y no estén pegadas a ninguna otra burbuja que no pertenezca al grupo inicial de la solución.
Para eso, lo que hacemos es recorrer la primera fila e ir hacia abajo, por cada burbuja del techo descendemos por las que tiene pegadas a esta y marcamos como enganchada la que encontremos que no pertenezca al grupo de la solución.

function Puzzle:findAdjacentAnimaFallingBubbles()
    --self:clearBubbleExplorations()
    self.adyAnimBbs = {}
    local mini,minj,maxi,maxj = self.height,self.width,0,0
    for i,b in ipairs(self.animaBubbs) do
        --if b.x < mini then mini = b.x end
       -- if b.x > maxi then maxi = b.x end
        --if b.y < minj then minj = b.y end
       -- if b.y > maxj then maxj = b.y end
        table.insert(self.fas, FontAnim({
            text=(3*i),
            x=self.bubbles[b.x][b.y].position.x,
            y=self.bubbles[b.x][b.y].position.y,
            color = self.bubbles[b.x][b.y].bcolor
        }))
        score = score + (3*i)
    end 
    -- explore adjacents to find attached falling bubbles 
 
 -- new algorithm- 1/2/2012
    self:exploreNonAdjscentFallingBubblesFromFirstLine()
 
    self:addNotConnectedBubblesAdjacentsToAnima()
end
 
function Puzzle:addNotConnectedBubblesAdjacentsToAnima()
    for i=1,self.height do
     local m = self.width
 
     if math.fmod(i,2) == 0 then
        m = m - 1
     end
     k = 1
     for j=1,m do
        if self.bubbles[i][j].connected == false and 
            self.bubbles[i][j].state== BUBBLE_ATTACHED and 
            not self:isBubbleInGroup(vec2(i,j),self.animaBubbs)
        then
            table.insert(self.animaBubbs, vec2(i,j))
            table.insert(self.fas, FontAnim({
                text=(6*k),
                x=self.bubbles[i][j].position.x,
                y=self.bubbles[i][j].position.y,
                color = self.bubbles[i][j].bcolor
            }))
            score = score + (6*k)
            k = k + 1
        end
     end
    end
end

y con eso ya tendríamos hecha la parte más difícil del juego ,que es la lógica de enganches de burbujas, encontrar grupos conectados del mismo color y las que se tienen que eliminar y básicamente lo que nos falta es comprobar condiciones como, si la altura de la última burbuja por debajo está en colisión con el límite por lo que el juego acabaría.
Para generar los colores de burbujas nuevas,ya que no se pueden poner todas sino sólo las que haya actualmente en la pantalla para que se pueda acabar el juego, se utiliza un array de colores usados actualmente, que utiliza los algoritmos de comprobación de estado de solución para almacenar los colores de las burbujas actualmente en la palestra de nuestro puzzle.
Problemas solucionados:

  • Generación de nuevas burbujas (colores actuales)
  • Comprobación de condiciones de estado del juego
  • Búsqueda de soluciones en tiempo real y animar los cambios de estado

Una vez solucionado el tema del juego normal, faltaría programar el nivel de los jefes finales.
Decidí crear 3 naves ,dos similares que serían los ayudantes de la nave principal que es más dura y sus disparos quitan más vida. La barra vida del jugador está a la derecha del todo, es un porcentaje que va bajando según nos alcanzan los disparos de las naves enemigas. Estos disparos se pueden bloquear si les disparamos asteroides, hélices o minas, que al chocar explotan ambas…hay animaciones cambiantes para todos los elementos, acompañados de sonidos, aquí, cada uno debe crear los que considere oportunos…el baile de los enemigos sigue una lógica de movimiento en cruz, así, tendremos que ir moviendo el control para atacar a los jefes, que no paran de dispararnos bolas de energía y haces de láser, estos debemos eliminarlos disparando contra ellos, una vez que alcanzamos a cualquiera de los enemigos encontraremos que su vida es reducida y podemos verlo en los indicadores que tenemos a la derecha para cada uno de ellos, cuando la vida baja hasta casi cero reemplazamos el sprite de cada nave por el de la nave rota y cuando llega a cero reemplazamos este sprite por una animación de una explosión a nuestro gusto.
No hay que decir, que debemos acompañar todas estas acciones por sonidos acorde a ellas jeje.

Una vez completada la fase del boss debemos enlazarla con la condición de la clase puzzle, cuando llegue al último nivel debemos hacer la transición con un título bonito y luego cargar el nivel del jefe final.
Al terminar cada nivel debemos también guardar los puntos…algo sencillo…

 
function Puzzle:drawWin()
    noTint()
    background(183, 185, 189, 255)
    --self:drawBgRocks()
    self.st:draw()
    self.animaTime = self.animaTime + 0.1
    if self.animaTime>=66 then
        score = score + self.bosses_score
        if highscore<score then
            saveLocalData("highscore",score)
            highscore = score
        end
        change_Game_Mode(GAME_MODE_MENU)
        return
    end
    pushStyle()
    font("Futura-CondensedExtraBold")
    fontSize(133)
    pushMatrix()
 
    fill(math.random(66,255),255,math.random(66,255),255)
    translate(WIDTH/2+3,HEIGHT/2+66)
    rotate(math.random(-6,6))
    text("WINNER ")
    fill(math.random(66,255),255,math.random(66,255),255)
    popMatrix()
    fontSize(33)
    text("Thank you for playing!. xixgames.com ",WIDTH/2,(HEIGHT/2)-66)
    text("Score:"..(score+self.bosses_score),WIDTH/2,(HEIGHT/2) - 166)
    if highscore<(score+self.bosses_score) then
        text("Congrats!!, you did a highscore!!",WIDTH/2,(HEIGHT/2) - 190)
    end
    popStyle()
end

Algo parecido se haría con el Game Over…

 
function Puzzle:drawGameOver()
    noTint()
    background(0, 53, 255, 255)
    --sprite("Tyrian Remastered:Arrow Right",WIDTH/2,self.initHeight+43,31*2,66)
    self.st:draw()
    self.animaTime = self.animaTime + 0.1
    if self.animaTime>=6 then
        self:init(self.width,self.height,true)
        --self.state = PUZZLE_PLAYING
    end
 
    pushStyle()
    font("Futura-CondensedExtraBold")
    fontSize(166)
    pushMatrix()
    fill(math.random(1,66), math.random(255), math.random(255),232)
 
    translate(WIDTH/2,HEIGHT/2)
 
    text("TRY AGAIN")
 
    popMatrix()
    popStyle()
end

Una vez terminado el juego toca probarlo, aquí es mejor comentarlo a los amigos, conocidos a todo el mundo y luego entrar en fase de mejora, arreglar bugs, etc.
La fase de producción quizás es la más difícil porque es cuando encontraremos verdaderas dificultades, debemos construir el juego adaptado para cada plataforma, añadirle las redes de puntuaciones, game centers, compras online, etc.
Pero esto, se queda fuera de esta pequña guía para crear un clon de Puzzle Bubble 🙂
A disfrutarlo!


Juego sacado de arkatia.com/arcade/puzzle-bobble/

Artículos relacionados

3 contribuciones

  1. iriz /

    Hola, realmente está muy interesante,me gustaría aprender más…me gustaría que me mandaras algunos tutoriales…saluditos

    • Pues pásate por vimeo.com/channels/natureofcode/ y hazte todos los ejercicios, cuando acabes vuelves por más 😉

  2. visstaralax /

    Hola! Estaba buscando información como esta, muchas gracias! Voy a ver tus tutoriales

Exprésate dejando un comentario:

Introduce el captcha

Por favor escriba los caracteres de la imagen captcha en el cuadro de entrada