Notas de desenvolvimento de "Machine" do xkcd
Ideia inicial
- A ideia foi pensada até o fim de março e decidida no começo de abril
- "Daria para criar um dispositivo gigante em mosaico, como os GIFs de blueball feitos por usuários do Something Awful? Todo mundo contribuiria com um pequeno quadrado"
- No começo, parecia que a ideia já estava totalmente formada, mas, ao conversar de fato sobre ela, perceberam que ainda havia muitas decisões a tomar
- Tinham visões diferentes sobre partes centrais, como de onde as bolas saem, se todos veem a mesma máquina, qual é o objetivo e como os jogadores interagem
Lições aprendidas com tentativas anteriores
- Já havia experiência na criação de quadrinhos interativos centrados em conteúdo feito por usuários
- Lorenz: um exquisite corpse em que leitores escreviam o texto dos painéis para desenvolver piadas e a história (foi muito divertido)
- Collector's Edition: um jogo em que leitores encontravam adesivos escondidos no arquivo do xkcd e os colavam permanentemente em uma tela compartilhada (não produziu o resultado esperado)
- Começar com um mapa central vazio no início levava ao caos
- Faltavam incentivos para posicionar os adesivos, então era difícil que ações individuais avançassem o enredo, e só surgiam padrões simples
- Não havia uma história geral nem um objetivo, e a relação entre os adesivos também era pouco clara
- Para uma tela coletiva dar certo, é preciso ensinar por exemplo o que pode ser criado e haver contexto e objetivo compartilhados
Projeto das restrições
- Depois de decidir criar uma grande máquina de queda de bolinhas, surgiram escolhas demais
- Decidiram usar uma grade de 100x100
- Parecia arriscado simular 10 mil tiles em tempo real no cliente
- Não havia certeza de como jogadores, sem comunicação direta, poderiam criar subseções de uma máquina complexa que funcionassem quando tiles separados fossem integrados
- Após vários experimentos mentais, definiram 3 princípios centrais:
1. Maximizar a expressividade dos jogadores, mesmo sacrificando a precisão
- Até que ponto a máquina deveria ser previsível?
- Consideraram executar tudo no servidor ou validar tiles individuais, mas confirmaram no editor de protótipo que era fácil criar padrões caóticos de colisão de bolinhas
- Se as bolas não se movessem em linha reta sem interferência, era fácil criar máquinas imprevisíveis
- Tornar a máquina mais previsível entrava em conflito com a liberdade dos jogadores
- O prazo apertado de desenvolvimento também favoreceu uma abordagem com menos previsão/simulação
- Decidiram dar aos jogadores uma liberdade de criação muito flexível, incluindo máquinas extremamente não determinísticas ou quebradas
- Isso exigia revisão ativa para verificar se as restrições eram cumpridas e remover conteúdo impróprio
2. Fornecer restrições rígidas que incentivem máquinas compatíveis e intercambiáveis
- A aceitação por revisão e as máquinas imprevisíveis dos jogadores acabaram exigindo ainda mais ordem
- No início, pensaram em entradas e saídas totalmente livres, mas perceberam no processo de revisão que, se fosse preciso substituir tiles iniciais, isso poderia causar grandes falhas em cadeia
- Projetaram restrições fortes o bastante para que vários jogadores pudessem criar designs compatíveis no mesmo espaço de tile
- Aplicaram o princípio da Robustez: "seja conservador no que envia, seja tolerante no que recebe"
- Para impor restrições de entrada e saída, precisavam de um mapa da máquina inteira desde o início
- A geração do mapa também permitia ajustar a dificuldade da máquina (de simples 1 entrada/1 saída até mesclas complexas de 4 entradas/4 saídas)
- Para dar feedback em tempo real, limitaram cada tile a expelir bolinhas numa velocidade parecida com a que recebe
- Restringiram máquinas que engolem ou atrasam bolinhas
- Submeteram os tiles a testes caóticos com velocidades de entrada aleatórias
- Estabeleceram o princípio de "rodar a máquina por um tempo e verificar se, em média, ela cumpre as restrições"
3. A máquina deve alcançar estado estável nos primeiros 30 segundos
- Surgiu a pergunta de quanto tempo um revisor precisaria observar
- Calcularam o tempo de revisão da máquina inteira (83,3 horas para 10 mil tiles)
- Decidiram arbitrariamente que ela deveria entrar em estado estável em até 30 segundos
- Configuraram as bolinhas para desaparecerem após 30 segundos
- No início, não havia tempo de expiração, então, enquanto os jogadores aprendiam o jogo, as bolinhas se acumulavam e enchiam a tela
- Com muitos corpos rígidos ativos, a simulação física ficava mais lenta
- As bolinhas acabavam mais atrapalhando do que divertindo
- A expiração das bolinhas impediu que a máquina acumulasse erros ao longo do tempo
- O revisor só precisava observar 30 segundos para entender, em grande parte, para onde as bolinhas poderiam ir
Simulação e surrealismo
- Dois grandes desafios da arquitetura de Machine:
- Será que, com essas restrições de projeto, tiles heterogêneos conectados em uma máquina inteira funcionariam?
- Validaram isso gerando e resolvendo alguns mapas pequenos
- Se não fosse possível executar a máquina gigante em tempo real nem no servidor nem no cliente, como exibi-la?
O objetivo era permitir rolar a tela acompanhando uma única bolinha
- Mesmo que a máquina inteira não fosse simulada, a área ao redor da região vista pelo jogador precisava ser simulada
- No começo, testaram simular apenas a área visível em um mapa infinito
- Funcionou bem, mas, ao rolar a tela, os tiles entravam na simulação em um estado inicial vazio, criando lacunas no fluxo
- Em vez de tiles vazios, eles precisavam parecer já estar em atividade
Segundo desafio: tirar snapshots dos tiles só depois de alcançarem estado estável, para que só existam pouco antes de aparecer com a rolagem
- Na versão final do quadrinho, a visualização com clipping de display desativado (CSS overflow:hidden, contain:paint desativado):
- Você percebeu os snapshots? Sem prestar muita atenção, é difícil notar
- Só os tiles renderizados existem na simulação física
- Otimização de display: só as bolinhas dentro da área de visualização aparecem, mas a simulação roda em toda a extensão do tile
- Para simular o topo da máquina, geravam e alimentavam bolinhas na linha mais alta da simulação (com base na velocidade esperada pelas restrições de entrada)
- Integraram a geração de snapshots à UI de revisão
- O revisor precisava esperar pelo menos 30 segundos antes de aprovar um tile
- Ao clicar no botão de aprovar, era gerado um snapshot
- O revisor também podia, a seu critério, esperar um pouco mais até a máquina parecer boa
- A abordagem com snapshots funcionou melhor do que o esperado
- Teve o efeito positivo de resetar erros acumulados na máquina
- A primeira impressão dos tiles vistos com a rolagem era um estado limpo e bonito que agradou ao revisor
- Na prática, se observadas por muito tempo, muitas máquinas poderiam falhar ou quebrar, mas isso nunca era visto porque, ao continuar explorando, entrava-se em novos snapshots
- A máquina que aparece rolando no quadrinho não é real. É surreal
- O conjunto não é simulado de uma vez só, mas isso acabou produzindo um resultado melhor
Renderizando milhares de bolinhas com React e DOM
- Construído com base no motor físico Rapier
- A boa documentação, a API limpa com componentes básicos úteis e a implementação em Rust (executada como WASM no navegador) entregaram um desempenho impressionante
- No início, a garantia de determinismo do Rapier chamou atenção, mas não houve simulação no lado do servidor
- Escreveram um contexto React personalizado
<PhysicsContext> sobre o Rapier
- Ele cria objetos físicos do Rapier e os gerencia dentro do ciclo de vida de componentes React
- Facilitou desenvolver componentes "widget" para cada objeto posicionável com física ou superfícies de colisão
- O React serviu como um scene graph simples e meio improvisado
- Também simplificou carregar/descarregar tiles ao rolar a visualização: ao desmontar um tile, toda a física e o DOM eram limpos
- Como bônus, ficou fácil conectar hot reloading ao fast refresh (ótimo para ajustar formas de colisão)
- Outra vantagem da abordagem com contexto React:
- Se um hook de física não estiver dentro de
<PhysicsContext>, ele vira noop
- Isso foi usado para renderizar prévias estáticas de tiles na UI de revisão
- Teria sido melhor usar componentes em vez de hooks para criar objetos Rapier (como faz o react-three-rapier)
- Isso se encaixa melhor no diffing do React (quando dependências mudam,
useEffect remove a instância anterior e cria outra)
- Machine é renderizado inteiramente com DOM
- No início do desenvolvimento, havia preocupação de atingir o limite de desempenho da renderização em DOM
- Se ficasse lento demais, a expectativa era migrar para PixiJS ou canvas, mas queriam ver até onde o DOM conseguia ir
- Otimização de desempenho de renderização:
- O loop de frames aplica estilos diretamente aos widgets que têm simulação física
- O diff do React só roda quando há mudanças estruturais no scene graph
- No início, as bolinhas eram renderizadas com React
1 comentários
Comentários do Hacker News
Reunindo vários comentários, o conteúdo pode ser resumido assim:
Rapier, mas também houve crashes causados por erros recursivos