Douglas ’ Blog

O que aprendi ao construir Asteroids pela terceira vez

~ 18 de mar. de 2024 ~


Fazer jogos era minha única motivação para entrar na carreira de programação. Depois de me distrair com desenvolvimento web, me cansei da web e decidi me dedicar ao desenvolvimento de jogos.

Cerca de dois anos atrás, assisti ao CS50 para desenvolvimento de jogos. O curso utilizou Lua e Love2D, e então, depois de assistir, tentei criar um clone de ‘Asteroids’. Consegui criar uma versão muito básica de ‘Asteroids’.

Em geral, foi fácil trabalhar com Lua e Love2D. Mas houve algumas desvantagens. O Love2D não tinha uma boa abstração para input. Felizmente, o curso forneceu uma abstração melhor. Além disso, a API do Love2D é estranha se comparada à do Raylib. Por exemplo, você precisa definir a cor antes de desenhar uma forma, em vez de passar a cor como argumento para a função de desenho. Além disso, a cor é composta por três argumentos separados, com uma faixa entre 0 e 1. A falta de tipos torna a refatoração inviável sem uma quantidade enorme de frustração.

Você pode ver a jogabilidade abaixo.

Eu recomendaria Lua e Love2D para criar jogos? Não. Raylib é uma opção muito melhor.

Também tentei converter para uma arquitetura ECS, mas foi difícil e não valeu a complexidade. Entendi que ECS não é uma bala de prata.


Um tempo depois, decidi aprender Godot. Então, recriei o jogo, mas com algumas melhorias. Adicionei partículas de fogo para a nave espacial, partículas para a explosão do asteroide e uma tremulação de tela quando você destrói um asteroide.

Como o Godot é uma engine, você pode arrastar e soltar para posicionar as imagens e textos. Também é fácil criar partículas. Existem muitos tutoriais disponíveis. Um deles era sobre a tremulação de tela. Eu apenas copiei o código e não me preocupei em entender o código.

Eu recomendaria fazer jogos no Godot? Sim. O tradeoff deve ser óbvio: desenvolvimento mais rápido, mas falta de controle. Além disso, você precisa aprender a usar a engine, não os fundamentos. Eu a usaria para um jogo futuro? Não, porque quero criar minhas próprias abstrações, mas o Godot me obriga a usar o sistema de abstração de Nodes.

Você pode jogar a versão web aqui.

Você pode ver a jogabilidade abaixo. Além disso, observe o bug no início.


Finalmente, fiz um com Raylib. Decidi fazer um jogo “lançável”. Algo que você poderia dizer que é um jogo polido.

Houve algumas coisas que aprendi das iterações anteriores. Mas principalmente, aprendi com esta última.

Seno e Cosseno.

Saber como usar o seno e o cosseno para rotação e direção é essencial para fazer a nave espacial virar e avançar.

Integrais

Saber integrais foi essencial para fazer a nave se mover como se estivesse no espaço. Também me ajudou a entender como aceleração, velocidade e posição funcionam juntas. No Godot, usei a física integrada, mas fazer a minha própria não foi difícil. Como bônus, tenho um ajuste mais refinado sobre o movimento da nave espacial.

Os timers sempre devem ser do tipo float e diminuir pelo tempo delta.

Dessa forma, você pode expressar o timer em segundos, o que é mais fácil de calcular e independente da taxa de quadros. Além disso, o tempo delta é crucial para garantir que vários sistemas, como animação, timers e física, permaneçam independentes da taxa de quadros.

Os eventos estão por toda parte.

Neste jogo simples, há muitos eventos e eventos que desencadeiam outros eventos. Por exemplo, quando a energia atinge zero, desencadeia o estado de ‘parada de impacto’. Em seguida, quando o temporizador termina, desencadeia o estado de ‘fim de jogo’. Isso dá tempo ao jogador para ver a colisão com o meteoro. Masahiro Sakurai tem um vídeo sobre isso.

Salvar e carregar dados do jogo é super fácil.

Às vezes, ao trabalhar com outras linguagens, esqueço que arquivos de texto não são a única maneira de armazenar dados em um arquivo. Para salvar a pontuação mais alta, armazeno os bytes em um arquivo. Para ler é o inverso, você converte para o tipo que precisa. Se os dados são uma struct, o processo seria o mesmo. Portanto, você elimina o custo de ler o arquivo e analisar os dados.

As partículas da explosão foram simples de fazer.

As partículas são essenciais em qualquer jogo, e criar partículas de explosão é muito mais fácil no Raylib do que no Godot. Basta gerar um Vector2 aleatório em todas as direções e ajustar a cor desvanecida baseada no temporizador da partícula. As partículas de fogo que criei para a propulsão da nave no Godot foram mais simples de implementar. Não consegui replicar o mesmo efeito no Raylib. Uma abordagem alternativa seria usar sprites para a animação do fogo.

O efeito de câmera lenta é simples de fazer.

Basta multiplicar o fator de câmera lenta por qualquer coisa que precise desacelerar. No Godot, você altera uma variável para que tenha efeito em todo o jogo.

Sem gerenciamento de memória.

Tudo é alocado na pilha, exceto pelas funções do Raylib que podem alocar memória internamente. Depois de fechar o jogo, o sistema operacional cuida da desalocação da memória. Para mim, em C, isso é incentivado pelo fato de não fornecer arrays dinâmicos. Por outro lado, o Godot te obriga a destruir e criar objetos. Na versão do Godot, eu destruo e crio novos objetos meteoro, mas na versão do Raylib, reutilizo os mesmos dados.

As flags do compilador são úteis.

Dois deles facilitaram a identificação de bugs, as flags -fstack-protector-all e -Wshadow. A primeira fornece um rastreamento da pilha do acesso a índices fora dos limites em um array. A segunda avisa sobre a redeclaração do mesmo nome de variável no mesmo escopo, especialmente ao lidar com loops aninhados.

Vários dados interconectados são difíceis de desacoplar e organizar em arquivos.

Por exemplo, a colisão da nave espacial com os meteoros está no arquivo principal, e vários dados são atualizados lá, como pontuação, temporizador de câmera lenta e tremulação da câmera. Não pensei muito nisso, mas talvez usar uma fila de mensagens centralizada seja uma solução. Uma possível desvantagem é que os dados são processados apenas no próximo quadro, a menos que você execute em uma thread separada. Também pode ser a abstração errada e desnecessária.

O ruído de gradiente é incrível!

A aleatoriedade é um aspecto crucial do desenvolvimento de jogos, no entanto, por si só, não é suficiente. Precisamos de uma forma de aleatoriedade ‘controlada’ para alcançar os resultados desejados. O ruído de Perlin fornece uma sensação mais orgânica de aleatoriedade. Eu utilizei o stb_perlin.h e traduzi o código do Godot para C para criar o efeito de tremulação da câmera. Além disso, vale ressaltar que as câmeras permanecem significativas mesmo em jogos estacionários.

Dispor elementos da interface do usuário manualmente é demorado.

Uma solução possível é criar um editor integrado.

As máquinas de estado governam o estado do jogo.

Os jogos operam em máquinas de estado, e devo pesquisar mais para descobrir como simplificar o código usando máquinas de estado e tornar o código mais fácil de entender.

Possíveis melhorias

Empacotar os arquivos com o binário.

Um único binário, facilmente portável em um pen drive, com a possível desvantagem de não ter suporte a mods.

Salvar as iniciais do jogador e a pontuação mais alta.

Assim como nos jogos antigos!

Lançar uma versão para Windows e web.

Aproveitar o suporte multiplataforma do Raylib.

Incorporar os cabeçalhos do Raylib e Raymath no meu repositório.

A melhor prática no desenvolvimento de jogos é incluir suas bibliotecas no seu código-fonte. Eu usei essa abordagem para o arquivo de cabeçalho stb_perlin. O benefício dessa abordagem é que ela garante uma versão fixa e torna as atualizações explícitas, simplificando o processo para outros executarem o código. Diferente do mundo JavaScript, não há necessidade de gerenciadores de pacotes.

Encontrar uma maneira melhor de organizar os arquivos.

Devo separar por entidade ou por tipo de arquivo?


Durante o desenvolvimento, houve alguns eventos fortuitos que acredito terem melhorado o design do jogo.

Quando adicionei a textura do planeta, posicionei-a no centro da tela, mas para minha surpresa, ficou assim.

screeshot of asteroids with a big planet on the background

Esqueci que a textura era grande, mas ela tem um apelo melhor do que ter um pequeno planeta distante como este.

screeshot of asteroids with a little planet on the background

Para testar se a pontuação estava sendo renderizada corretamente, aumentei a pontuação quando o jogador avançava. Então me lembrei que Sakurai falou sobre risco-recompensa. Ficar parado praticamente não oferece recompensa porque cada meteoro destruído vale 10 pontos, mas se mover adiciona 70 pontos por quadro! Quanto maior o risco, maior a recompensa! Eu encontrei uma maneira de meio que trapacear isso. Você consegue adivinhar como?

Confira o código aqui e a jogabilidade abaixo.

Conclusão

O desenvolvimento de jogos é desafiador, mas uma experiência divertida, diferente do desenvolvimento web, que geralmente apresenta os mesmos problemas todas as vezes. As vantagens incluem não precisar de reuniões, dailies, prazos ou pressão, além de evitar o tédio geral do ambiente de trabalho.

Como exercício mental, leia o código-fonte de cada implementação e tente imaginar a dificuldade de adicionar um segundo jogador. Acredito que esta seja uma maneira interessante de avaliar a complexidade de trabalhar com a base de código.