Meu próprio Minecraft
2023-07-27
É estranho pensar que fazem mais de 10 anos que eu criei meu primeiro mundo no Minecraft. Depois desse tempo todo ainda não consigo descrever como era. Parecia algo grande de mais, e mesmo assim era uma experiência acolhedora.
Esse jogo foi, definitivamente, uma parte importante da minha vida. Não só pelos amigos que fiz ou pelas horas que passei, mas porque foi a primeira vez que programei. 1 E agora, quase completando minha graduação em Computação, eu decidi completar o ciclo.
Escrevi meu próprio Minecraft.
Matrizes
Eu amo Lua e amo LÖVE 2. É um framework extremamente versátil para joguinhos 2D. "Mas o minezinho é três-dê" você diz. Bom, 3D é apenas uma dimensão a mais projetada na sua tela. Pra isso, basta multiplicar umas matrizes e tá pronto.
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
vec4 position( mat4 transform_projection, vec4 vertexPosition )
{
return projectionMatrix * viewMatrix * modelMatrix * vertexPosition;
}
Cubos
Desenhar um cubo é fácil. O problema é desenhar milhares deles. Pra fazer isso, Minecraft divide o mundo em chunks, ou pedacinhos de 16x256x16. Isso não só alivia para a GPU desenhar o mundo, mas também facilita fazer as atualizações de cada frame.
Nossa versão não é diferente. Dividimos o mundo em pedaços de 16x48x16. Esses chunks são gerados aleatoriamente. Pra isso usamos ruído Perlin, que LOVE convenientemente fornece pra gente.
height = height + love.math.noise(x * frequency, z * frequency) * amplitude
max = max + amplitude
amplitude = amplitude / 2 frequency = frequency * 2
Depois que uma chunk é gerada, precisamos gerar uma mesh para ela. Uma mesh é simplesmente uma coleção de vértices que é enviado para a GPU para ser desenhado.
Existem vários algoritmos para meshing eficiente. Aqui só iteramos pelos blocos checando se cada face é visível (não está obstruída). Mas isso precisa ser feito 16 * 16 * 48 * 6 vezes por chunk. Isso é terrívelmente lento 😭. Então eu decidi apelar para threads. Dessa forma, a renderização não fica esperando o mundo ser gerado.
Durante esse processo, calculamos para cada vértice sua coordenada de textura, seu vetor normal (para iluminação) e o seu ambient occlusion 3. O resultado é um belo pedacinho de mundo.
Matar o dragão
Depois de gerar um mundo todo, parei por aí. Não queria um jogo pra matar meu tempo, só uma demonstração da parte visual. Adicionei apenas colisão (AABB) e raycasting para destruir e construir blocos. Confesso que não entendo muito bem a implementação de ambos.
Adicionei também algumas coisinhas que o Minecraft não tem, como sombras de verdade e dithering no lugar da transparência. O resultado é esse mundinho aí.
Lições Aprendidas
3D não é tão difícil assim. Recomendo dar uma pesquisada e tentar fazer a sua própria engine. Minecraft é um ótimo projeto pra testar isso. Fui um pouco mordido pela otimização prematura e o código ficou bem feio. Mas se quiser dar uma olhada pra ter uma referência, ou até mesmo testar, ta aqui:
Se der segfault
relaxa, faz parte da experiência 😎.