hrxdev
Português

#3 Construa uma prisão com o mouse, e salas que se encontram sozinhas

· Nick

O segundo marco está concluído e, pela primeira vez, há algo para ver. O cliente em Godot agora fica por cima do núcleo da simulação, e dá para construir com o mouse uma prisão de um andar: colocar pisos, traçar paredes, posicionar portas, marcar salas, demolir coisas. Tudo é cubo simples e cor chapada, e ainda não há ninguém lá dentro. Mas é um lugar, e dá para passear com a câmera por ele.

O que dá para fazer agora

Você começa um jogo novo, escolhe o tamanho do mapa (64, 128 ou 256 blocos de lado) e uma semente opcional, e recebe uma grade vazia. A câmera é ortográfica, com inclinação fixa. Ela gira em passos rígidos de 90°, dá zoom em direção ao cursor, desloca-se com as teclas ou com o botão do meio e tem um modo visto de cima para construir. Uma câmera que girasse livremente deixaria as paredes esconderem as salas, então não gira.

Pisos e demolições recebem um retângulo. Paredes seguem um contorno (ou uma linha reta se você segurar Shift). Uma porta é um clique, e ela escolhe a direção pelas paredes vizinhas, com uma tecla para girá-la. Enquanto você arrasta, a área sob o cursor aparece como um fantasma: blocos verdes serão construídos, vermelhos serão pulados, e um rótulo diz quantos e por quê. Nada é enviado ao mundo até você soltar, e se o núcleo disser que nenhum bloco mudaria, nada é enviado. Clique direito ou Escape cancelam.

O modelo de mundo tem vários andares, então há um seletor de andar, e os andares acima do atual ficam ocultos. As salas têm sua própria ferramenta, explicada abaixo. Uma camada de depuração mostra as bordas dos chunks, os blocos transitáveis, o bloco sob o cursor e um painel de tempos. Todas as palavras da interface estão em inglês.

A prévia é a coisa de verdade

Um fantasma que discorda do resultado é pior do que nenhum fantasma. Por isso o cliente não adivinha o que um comando de construção vai fazer. O núcleo sabe responder “o que aconteceria se você executasse isto” para a mesma área, bloco a bloco, com um motivo para cada bloco que ele recusaria. A verificação que preenche a prévia é a mesma regra que o comando usa quando roda de verdade, então as duas não têm como se separar. O cliente só desenha a resposta. Ele só pergunta de novo ao núcleo quando algo que afeta a resposta mudou: a ferramenta, a área arrastada, o andar ou o próprio mapa.

O mesmo princípio molda o resto do cliente: ele lê o estado do mundo e envia comandos, e não tem lógica de simulação própria. Por enquanto o núcleo roda seus ticks na thread principal, e um comando vindo de um clique é aplicado no início do próximo quadro, antes de o mundo dar seu tick. Se os tempos de tick começarem a disparar quando o mundo estiver cheio de gente, o plano é mover o núcleo para a própria thread, com um limite medido para saber quando fazer isso.

Salas que se encontram sozinhas

Olhei duas maneiras de criar salas. O Prison Architect deixa você desenhar uma zona, que passa a ser um objeto com seus próprios blocos. É fácil de entender, mas dá errado numa prisão: um bloco de celas inteiro seria uma única zona, e depois de reconstruir uma parede você tem de repintá-la. A outra maneira é encontrar áreas fechadas automaticamente, mas aí você não consegue marcar um pátio sem paredes em volta.

O que escolhi é uma mistura. A única coisa guardada é um tipo de sala em cada bloco: cela, refeitório, banheiros, enfermaria, sala de visitas, pátio de exercícios e assim por diante. Todo o resto é deduzido das paredes. Um espaço é um grupo de blocos abertos ligados pelos lados, num mesmo nível, e é fechado se não tocar a borda do mapa nem um vazio. Uma sala é o conjunto de blocos de um mesmo tipo dentro de um mesmo espaço. Paredes e portas são divisas, e um tipo pintado sob uma parede é mantido, mas ignorado, então volta se você tirar a parede.

Isso dá um comportamento que parece certo sem precisar ser explicado. Passe uma parede no meio de uma sala e ela vira duas salas do mesmo tipo. Tire a parede e elas se fundem. Ponha uma porta onde havia uma parede e nada muda. Uma sala inválida não é apagada, porque isso seria o oposto de “sem dor”; ela aparece em vermelho com os motivos: não está fechada, divide o espaço com outro tipo, é pequena demais ou não tem porta. Você pode pintar um retângulo, preencher um espaço fechado inteiro com um clique ou limpá-lo.

A lista de salas é um dado derivado, então não é salva nem entra no hash do mundo; só o tipo de cada bloco. Ela é refeita depois dos comandos e depois dos sistemas, e verificar um mundo sem mudanças não custa nada. Neste marco, o nível que mudou é recalculado inteiro, o que leva cerca de meio milissegundo num mapa de 256×256; fazer isso chunk por chunk pode esperar até que as regiões da busca de caminho precisem da mesma estrutura. A regra que exijo dele é que uma reconstrução incremental deve dar o mesmo resultado que uma completa.

Reconstruir só o que mudou, cortar andares de graça

O mapa é desenhado como uma malha por chunk por nível, e não como um objeto por parede. Cada chunk tem um contador de versão no núcleo, e o renderizador lembra qual versão desenhou por último. Quando o mundo muda, só os chunks com versão mais nova entram numa fila, e a fila é processada com um orçamento de tempo por quadro, de modo que uma mudança enorme se espalha por alguns quadros em vez de travar um só. Pintar um tipo de sala não sobe a versão do mapa, então nunca acorda o construtor de malhas.

O corte por andar segue a mesma ideia de fazer menos. Os andares acima do atual simplesmente ficam ocultos. As paredes do andar atual são achatadas para um metro por um shader, com um material compartilhado e um único valor de altura, então trocar de andar ou alternar paredes inteiras não reconstrói nada. Passar por quatro níveis dispara zero reconstruções de malha.

Uma fresta de dez centímetros

Quando testei a primeira prisão de um andar na verificação final, havia um piso no nível de cima, e de lado aparecia uma linha clara e fina entre o topo de uma parede e a laje por cima dela. Em um canto dava para ver o piso pairando no ar. Suspeitei do corte por andar, depois do z-fighting, depois da suavização.

Nenhum deles. O piso era um único quadrilátero plano, sem espessura, desenhado na altura do seu nível mais dez centímetros. As paredes do nível de baixo subiam até exatamente três metros. O piso do nível seguinte começava em três metros e dez. A fresta tinha dez centímetros de largura, e por ela se via a face superior iluminada da parede. Na horizontal tudo se alinhava perfeitamente, e é por isso que demorei a perceber.

A correção é que o piso agora é uma laje com espessura real, que desce até o início do seu nível. O topo fica na mesma altura, então a seleção de blocos com o mouse e todas as camadas continuam iguais. As faces laterais são puladas onde um piso ou uma parede se encostam dentro do mesmo chunk e desenhadas nas bordas dos chunks, de modo que a malha de um chunk nunca depende dos vizinhos. Isso deixa uma borda escura de dez centímetros na ponta da laje. Olhei capturas de tela de antes e depois e decidi manter. Ela se lê como a borda de um piso, que é o que é.

Será que aguenta

A última tarefa do marco era construir uma prisão de um andar com o mouse e ver se doía. Na maior parte, não doeu. O que a rodada achou além da fresta acima foi pequeno e chato: os botões de ferramenta não perdiam o destaque quando você escolhia outra ferramenta com o mouse, e não havia como sair da janela de Novo Jogo com Escape. Os dois estão corrigidos. Um menu de pausa está na lista de ideias para depois.

O que vem a seguir

O próximo é o M2, movimento e rotina diária. O mundo ganha regiões e alcançabilidade, busca de caminho que funciona entre andares e campos de fluxo para multidões. O núcleo finalmente terá trabalho de verdade em cada tick, então o critério do benchmark vai parar de dormir. Veja o roteiro.

← #2 · Todas as entradas do devlog