Como construir um bubble shooter arcade funcional
A maioria dos tutoriais sobre bubble shooter começa falando de gráficos bonitos e mecânicas simples. Na prática, o que mata esses jogos não é a lógica de disparo em si, mas a implementação do grid de bolhas. Passei uma semana inteira depurando um projeto em que as bolhas não se reconectavam corretamente quando uma linha inteira explodia. O resultado eram flutuações que pareciam presas no ar. A causa raiz era um erro na forma como calculava os índices dos vizinhos em um grid hexagonal empilhado. A correção foi simples, mas exigia entender como as linhas alternadas funcionam em coordenadas offset.
Entendendo o bubble shooter arcade
O gênero parece básico, mas a geometria por trás dele exige atenção. O grid é tipicamente organizado em colunas alternadas: uma linha desloca metade da largura de uma bolha em relação à anterior. Isso significa que cada bolha tem até seis vizinhos, não quatro. Se você usar um array bidimensional padrão como se fosse um grid quadrado, a física de queda vai falhar. Eu costumava representar isso com um array 1D onde os índices pares e ímpares se comportam de formas ligeiramente diferentes. Dá trabalho no início, mas evita bugs que aparecem horas depois, quando o jogador já está envolvido. O objetivo é claro: criar clusters de três ou mais bolhas da mesma cor para eliminá-los. Bolhas que ficam suspensas sem conexão com o teto caem. A mecânica de recuo, o ângulo de tiro, o sistema de pontuação — tudo isso é construído sobre essa base geométrica.
A estrutura do grid hexagonal
O coração do jogo é o grid. Vou explicar como eu monto o meu. Cada linha tem uma quantidade fixa de bolhas, geralmente começando com menos bolhas nas linhas superiores e crescendo conforme desce. Na minha implementação, uso coordenadas offset onde uma linha par tem os mesmos índices X que a anterior, mas uma linha ímpar está deslocada em 0.5 unidades. Isso se reflete nos cálculos de colisão e conectividade. Para verificar quais bolhas são vizinhas de uma dada posição, usei esta função:
Vizinhos em linha par: (row-1, col-1), (row-1, col), (row, col-1), (row, col+1), (row+1, col-1), (row+1, col) Vizinhos em linha ímpar: (row-1, col), (row-1, col+1), (row, col-1), (row, col+1), (row+1, col), (row+1, col+1)
Essa diferença sutil é o que causa a maioria dos bugs em implementações amadoras. Bolhas que deveriam se conectar simplesmente não se conectam. Ou pior, o algoritmo de queda deixa cair bolhas que estão sólidas.
👉 Clique no botão abaixo para saber mais sobre o assunto!
A lógica de disparo e colisão
O projétil se move em linha reta até colidir com uma bolha ou com o teto. A colisão com o teto é trivial — você posiciona o projétil na linha mais alta disponível e encaixa na coluna mais próxima. A colisão com outra bolha é mais complicada. Você precisa calcular o ponto exato de impacto, determinar a linha e coluna mais próximas, e inserir a bolha nessa posição. Eu enfrentei um problema específico com bolhas que quase se encaixavam mas causavam sobreposição visual. A solução foi verificar, antes de fixar a bolha no grid, se a posição alvo já estava ocupada. Se estiver, você arredonda para a célula vizinha mais vazia com base na direção de chegada do projétil. Isso evita sobreposições e mantém o grid limpo.
Encontrar clusters e detectar queda
Após cada disparo, duas verificações acontecem. Primeiro, você procura clusters de três ou mais bolhas da mesma cor conectadas. Uso uma busca em profundidade (DFS) a partir de cada bolha do cluster. Segundo, você identifica todas as bolhas que perderam conexão com o teto e as remove da simulação com uma animação de queda. O detalhe importante é a ordem. Sempre verifique os clusters primeiro. Se você remover bolhas suspensas antes de procurar clusters, pode eliminar bolhas que fariam parte de um combo válido. Isso já me custou pontos em testes de QA.
Otimizações que fazem diferença
Em projetos menores, você pode rodar uma varredura completa do grid a cada frame. Em bubble shooter arcade, isso raramente é necessário. O grid só muda quando o jogador dispara. Você pode processar eventos de forma assíncrona: animar o projétil, detectar colisão, atualizar o grid, verificar clusters e queda, e só então liberar o próximo tiro. Isso simplifica muito o fluxo e evita condições de corrida. Uma otimização útil é manter uma lista de bolhas críticas — aquelas que estão adjacentes a espaços vazios ou a clusters potencialmente grandes. Quando uma bolha é removida, você só precisa verificar essas vizinhas, não o grid inteiro. Reduz o tempo de processamento de clusters de O(n²) para algo muito mais enxuto na prática.
Pitfalls comuns
O primeiro erro que vejo em praticamente todo projeto iniciante é a falta de um limite superior para o grid. Se o jogador nunca eliminar bolhas o suficiente, o grid pode crescer até cobrir a tela inteira. Você precisa de uma condição de derrota clara: se uma bolha tocar a linha inferior, game over. E precisa gerar o grid inicial de forma que haja sempre pelo menos uma sequência viável de eliminação. O segundo erro é ignorar a aleatoriedade controlada das cores. Se o jogo gerar cores completamente aleatórias, é possível criar configurações onde não há clusters possíveis e o jogador fica travado. Uma abordagem melhor é garantir que, a cada novo lote de bolhas, pelo menos um cluster de três ou mais seja sempre formável nas primeiras linhas.
Downloads e referências
Se você quer começar do zero, um motor como Godot ou Unity oferece templates que já incluem movimentação de projéteis e detecção de colisão básica. O código do grid hexagonal que descrevi aqui é transferível para qualquer engine. Para quem prefere algo pronto, existem vários projetos open-source no GitHub que implementam bubble shooter arcade com variações interesantes, como power-ups, níveis com obstáculos e modos de jogo especiais. O que funciona na prática é construir devagar. Comece com um grid de dez linhas, três cores, e uma única mecânica de disparo. Faça funcionar. Só depois adicione animações, sons, pontuação e níveis. A maioria dos projetos que vi desandar veio tentando implementar tudo de uma vez.
Se der certo, o resultado é um jogo que funciona de forma consistente e é rápido de rodar em qualquer dispositivo. Se não der certo, você terá aprendido algo sobre geometria discreta e estruturas de dados. Ambas as opções são válidas.