Recriando Flappy Bird com Lua e Love2D
Nesse artigo vamos criar o Flat Bird, que é um clone de Flappy Bird, mas com gráficos mais simplificados que o original, pois o foco será na mecânica do jogo.
Para isso utilizaremos a linguagem de programação Lua e o framework Love2D (também conhecido como LÖVE).
Lua
Lua é uma pequena e interessante linguagem de programação desenvolvida na universidade brasileira PUC-Rio e cuja razão de existir é basicamente simplificar a vida dos programadores que dependem das linguagens C ou C++. Portanto, foi projetado para ser facilmente integrado à linguagem C (também C++).
Se você nunca ouviu falar, não se preocupe pois a sintaxe é muito simples, especialmente para quem já programou em Python ou JavaScript. E não precisa saber C para usá-la!
Mas saiba que vários jogos famosos como Angry Birds e World of Warcraft utilizaram-na em partes do seu desenvolvimento. Uma lista mais completa de tais títulos pode ser encontrada aqui.
Love2D
É um framework para criar jogos 2D usando Lua.
A instalação é muito simples, basta baixar o arquivo correspondente à sua plataforma no site:
Game loop
No coração de praticamente todo jogo existe o chamado game loop. É basicamente um loop infinito, onde o jogo processa a entrada (e.g. teclas pressionadas e toques na tela), atualiza o estado das entidades do jogo (seja respondendo à entrada ou dando continuidade a uma simulação física) e, finalmente, mostra na tela o resultado.
E isso se repete várias vezes por segundo, geralmente há uma taxa constante com cerca de 30 ou 60 frames por segundo.
Com o LÖVE não é diferente. Mas, nesse caso, você não tem a responsabilidade de escrever o game loop e sim apenas certas callbacks que são funções especiais, as quais o framework chama na hora certa.
Por exemplo, um “Hello, world!” consiste simplesmente em implementar a callback love.draw:
Note que não foi preciso escrever código para criar a janela, nem para lidar diretamente com APIs gráficas como OpenGL ou Vulkan, o que facilita muito a vida de quem está começando na área de game dev.
Porém, ainda assim é possível ter um controle mais refinado de certos aspectos da aplicação como veremos mais adiante.
Para rodar o código acima, salve-o em um arquivo main.lua, abra um terminal, vá até o diretório em que você salvou o arquivo e execute “love main.lua”.
Isso assume que o programa love está acessível na variável de ambiente PATH. Se você usou o instalador, não vai ter problemas. Se escolheu a versão .zip, então você terá que adicionar manualmente o diretório na PATH.
Flat Bird
Se você esteve morando em uma caverna nos últimos anos e não conhece o viciante e irritante Flappy Bird, você pode jogá-lo aqui.
Feitas as apresentações, vamos ao nosso jogo.
Os elementos centrais são o pássaro, os canos e o chão. O jogador pontua ao passar pela abertura entre os dois canos na vertical e morre ao tocar nos canos ou no chão.
O controle consiste simplesmente na ação de fazer o pássaro subir um certa distância, lutando contra a gravidade. Pode ser implementado com o aperto de uma tecla, clique do mouse ou toque na tela, em caso de plataformas móveis.
Antes de chegar ao código do jogo vamos definir o tamanho da janela e o título da mesma. Para isso, crie um arquivo conf.lua e adicione o seguinte código:
Para mais informações sobre as configurações disponíveis, veja esse link.
O código do jogo ficará num único arquivo `main.lua` para evitar ter que mexer com módulos ou pacotes.
Para começar, escolhemos uma resolução para o canvas, que é onde iremos desenhar o jogo. Note que essa resolução não precisa ser a mesma da janela ou da tela onde o jogo vai rodar. No caso, eu estou assumindo que a janela e o canvas tem a mesma resolução para simplificar o código.
Pense na palavra-chave “local” como o “let” do JavaScript.
Aqui tem algumas cores para pintar os elementos do jogo. O LÖVE assume que as componentes RGBA de uma cor estão no intervalo [0, 1].
Uma constante que simula a aceleração induzida pela gravidade. Podemos pensar na unidade como pixels/segundo.
No LÖVE, a origem do sistema de coordenadas, o ponto (0, 0), é aquele mais próximo do canto superior esquerdo da tela. E o eixo y é invertido no sentido que ao aumentar a coordenada y, o objeto se move para baixo na tela.
Pode parecer estranho mas é uma convenção bem comum em frameworks e bibliotecas de desenvolvimento de jogos.
Pássaro
A seguir temos o código responsável pelo pássaro. Ele posiciona o pássaro no centro da tela na vertical e um pouco à esquerda do centro na horizontal. Também são definidas velocidades mínimas e máximas, para evitar ficar muito lento ou muito rápido. O `bump_height` é a altura que o pássaro se desloca na vertical toda vez que o jogador acionar o controle.
Na função birdUpdate nós alteramos a velocidade de acordo com a constante gravitacional que definimos acima e também alteramos a posição vertical do pássaro segundo sua velocidade. O que é o `dt`? Bem, é só a forma encurtada de "delta time" que é o tempo que se passou entre os dois últimos frames.
Isso é necessário pois esta função será chamada cerca de 60 vezes por segundo e como cada frame pode demorar um pouco mais ou um pouco menos do que o esperado, usar o `dt` ajuda a suavizar o movimento independentemente da máquina do jogador.
Canos
Em Lua só existe uma estrutura de dados, que é a tabela(hash). É parecido com o Object do JavaScript.
Para lidar com os canos, iremos usar uma tabela pipes contendo dois campos (.clock e .gen_rate) que funcionam como um relógio para determinar quando o próximo cano tem que ser criado. Também usaremos os campos numéricos da tabela `pipes` como uma estrutura de fila.
Pontuação
Aqui temos o código do sistema de pontuação. Nele guardamos a pontuação atual e a pontuação máxima (que persiste apenas enquanto o jogo estiver aberto). O critério para aumentar a pontuação é: se o centro do pássaro passou do centro de um cano, cujo campo behind_bird ainda é falso, para evitar contar várias vezes o mesmo ponto.
Piso
Aqui definimos o piso, ou chão, que é simplesmente um retângulo marrom na parte inferior da tela.
Funções auxiliares
Aqui definimos um relógio parecido com aquele usado para gerar os canos, mas com o objetivo de contar o tempo até reiniciar o jogo quando der “game over”.
A função gameReset transforma o estado do jogo para o estado inicial, modificando apenas os campos necessários. Note que scoreboardReset não zera a pontuação máxima.
A função hasIntersection detecta se há interseção entre dois retângulos, onde xj, yj representa o canto superior esquerdo do retângulo j e wj, hj representa a largura e altura do mesmo, respectivamente.
LÖVE callbacks
Esta callback é chamada apenas uma vez, durante a inicialização do jogo, por isso é um bom lugar para escrever códigos que carregam assets como a música do jogo e imagem de fundo.
Aqui definimos uma seed para o sistema gerador de números aleatórios, inicializamos as entidades do jogo e definimos a fonte a ser usada para todos os textos que forem mostrados na tela. Escolhi usar uma única fonte, no caso a mesma do scoreboard.
Aqui movemos o pássaro, os canos, checamos se há colisão (o que determina o fim do jogo) e caso não haja colisão, atualizamos a pontuação, se for o caso. Note que caso o pássaro esteja “morto”, não atualizamos o estado das entidades do jogo, mas exibimos uma mensagem de “game over” no lugar, que dura alguns segundos antes do jogo ser reiniciado.
Essa é a callback responsável por desenhar na tela (qualquer código como love.graphics.rect só funciona como esperado se chamado dentro dessa função).
Essa callback é chamada quando há um evento de “tecla pressionada”. No caso, precisamos fazer o pássaro subir um pouco e voltar a velocidade inicial quando a tecla espaço, seta pra cima ou W for pressionada. Mas só se o pássaro ainda estiver vivo, pois o modo zumbi não foi implementado.
Resultado
No final o jogo fica assim:
Para sua conveniência, o código também pode ser encontrado aqui.
Se você não gostou do visual minimalista do jogo, existem alguns pacotes de arte que foram disponibilizados gratuitamente pelos seus criadores como os seguintes:
- https://megacrash.itch.io/flappy-bird-assets
- Mais próximo do jogo original.
- https://ansimuz.itch.io/flappy-chicken
- Tem uma música legal e uns sprites para criar variações do jogo, como fazer o pássaro soltar ovos que explodem!
Ao invés de desenhar retângulos, agora é necessário carregar as imagens do disco para a memória com a função love.graphics.newImage e para desenhá-las na tela usa-se a função love.graphics.draw.
As imagens nesses pacotes são em uma resolução bem baixa, algo como 16x16 e 32x32. Para fazer o jogo numa resolução maior, você precisa alterar o tipo de filtro usado pelo LÖVE(o algoritmo usado para aumentar ou diminuir imagens), usando a seguinte função https://love2d.org/wiki/love.graphics.setDefaultFilter.
Os modos disponíveis são “linear” e “nearest”. O padrão é o “linear”, então para preservar o aspecto “pixelado” das imagens, sem introduzir borrões, você vai querer usar o modo “nearest”.
Para mais informações sobre Lua e LÖVE, recomendo os seguintes links:
- https://www.lua.org/manual/5.1/pt/
- https://love2d.org/wiki/love
- https://sheepolution.com/learn/book/contents
Muito obrigado! Desfrute o jogo!
A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.