Notas de desenvolvimento de “Machine”, do xkcd
(chromakode.com)- Machine, do xkcd, é um enorme jogo de máquina de bolinhas no estilo Rube Goldberg, formado pela junção de dispositivos em tiles criados pelos leitores; a equipe transformou a ideia em um quadrinho interativo real em 3 semanas
- A experiência anterior com projetos participativos levou ao critério de design de que uma tela compartilhada precisa de contexto e propósito comuns para funcionar bem
- Para preservar a expressividade dos jogadores, mas garantir a compatibilidade entre tiles, foram impostas fortes restrições de entrada e saída, além de limitar cada dispositivo para que chegasse a um estado estável em até 30 segundos
- Em vez de simular toda a máquina em tempo real, apenas a área visível roda no Rapier; snapshots do momento da aprovação fazem os dispositivos parecerem já estar em funcionamento
- A operação do fluxo de aprovação e publicação dos envios combina React e renderização via DOM, backend em Haskell, Redis, OpenAPI, TanStack Query e uma UI de moderação
O ponto de partida de Machine
- O xkcd publicou Machine em 5 de abril
- Machine é um enorme construtor de máquina de Rube Goldberg, no estilo do jogo clássico The Incredible Machine
- A máquina inteira é composta pela junção de pequenos tiles de dispositivos criados por leitores individuais do xkcd
- A equipe criou Machine em 3 semanas, e a ideia partiu de Blue Ball Machine, um GIF colaborativo de 2005
- As perguntas centrais do brainstorming inicial eram de onde viriam as bolinhas, se a máquina vista por todos seria a mesma, qual seria o propósito da máquina, como os jogadores interagiriam e qual seria a motivação para participar
Lições de xkcd participativos
- Entre os quadrinhos interativos anteriores do xkcd centrados em conteúdo gerado por usuários, Lorenz permitia que leitores escrevessem textos dos painéis para desenvolver piadas e histórias, e acabou sendo uma boa experiência
- Em 2020, Collector’s Edition fazia os jogadores procurarem stickers no arquivo do xkcd e colá-los uma vez em uma tela global compartilhada, mas não funcionou tão bem quanto o esperado
- Todos os jogadores começavam no centro de um mapa vazio, e uma tela caótica logo se tornou a primeira impressão
- Faltavam incentivos para escolher cuidadosamente a posição dos stickers, e era difícil fazer a narrativa avançar apenas com ações individuais
- Sem uma história ou objetivo compartilhado, não ficava claro como cada sticker se conectava aos outros elementos da página
- Para que uma tela coletiva funcione bem, os usuários precisam conseguir aprender por exemplos o que seria interessante criar
- Para direcionar os resultados criativos em uma mesma direção, é necessário um contexto e propósito compartilhados que sincronizem o que criar
Design de restrições: expressividade, compatibilidade e estado estável em 30 segundos
- Mesmo depois de decidir criar um grande mecanismo colaborativo de queda de bolinhas, ainda restavam problemas como o tamanho total da máquina, o método de simulação e a forma de integrar os tiles
- Assumindo uma máquina de tamanho 100x100, a meta de rodar 10.000 tiles em tempo real no cliente e processar dezenas de bolinhas em cada tile foi considerada arriscada
-
Priorizar expressividade em vez de precisão
- Foram consideradas opções como rodar a máquina inteira no servidor ou simular tiles individuais para validação
- Quando padrões caóticos de colisão entre bolinhas se mostraram fáceis de criar no editor protótipo, a equipe concluiu que exigir uma máquina previsível reduziria a liberdade dos jogadores
- O design final priorizou a flexibilidade do jogador, permitindo até dispositivos muito não determinísticos ou quebrados
- Por causa dessa escolha, tornou-se necessária uma moderação ativa para verificar se os tiles atendiam às restrições e remover conteúdo ofensivo
-
Restrições de entrada e saída para compatibilidade entre tiles
- Inicialmente, foi considerada uma abordagem em que o próximo jogador expandiria livremente a partir da posição de saída do tile anterior
- Porém, se um tile colocado no início precisasse ser substituído depois, uma grande área que dependia dele poderia quebrar
- Por isso, foram impostas restrições fortes de entrada e saída para que vários jogadores pudessem criar designs compatíveis dentro do mesmo espaço de tile
- Essa abordagem se alinha ao princípio da Robustness principle: “seja conservador no que envia e liberal no que aceita”
- O gerador de mapas de Kevin começa com puzzles simples de 1 entrada e 1 saída, fica mais complexo com uma fusão intermediária de 4 entradas e 4 saídas e, no fim, retorna a 2 saídas por tile
- O editor oferece feedback em tempo real enquanto o jogador cria um tile
- Em média, o tile deve liberar bolinhas em uma velocidade parecida com a que recebe
- A ideia era reduzir dispositivos que engolissem bolinhas ou criassem grandes atrasos
- Para refletir variações nas entradas a montante, foi aplicado chaos testing, randomizando a velocidade das bolinhas que entram no editor
-
Era preciso chegar a um estado estável em até 30 segundos
- Para reduzir por quanto tempo os moderadores precisariam observar, foi definido um critério arbitrário: o dispositivo deveria entrar em estado estável em 30 segundos
- A base para o critério foi o cálculo de que observar 10.000 tiles por 30 segundos levaria cerca de 83,3 horas de moderação no total
- As bolinhas também foram alteradas para expirar após 30 segundos
- Sem expiração, a primeira experiência de jogadores iniciantes era ver bolinhas se acumulando na tela
- O número de rigid bodies ativos crescia, o que também deixava a simulação física mais lenta
- A expiração das bolinhas evita que erros se acumulem com o tempo e simplifica a moderação, permitindo ver, com apenas 30 segundos de observação, onde a maioria das bolinhas chega
Como não rodar a máquina inteira em tempo real
- A primeira grande premissa da arquitetura de Machine é que, se as restrições acima fossem respeitadas, seria possível juntar tiles diferentes e fazê-los parecer uma máquina completa
- A equipe confirmou essa premissa gerando e resolvendo alguns mapas pequenos
- Como não era possível rodar a máquina inteira em tempo real no servidor nem no cliente, era necessário simular apenas a região ao redor da área vista pelo usuário
- O objetivo era permitir acompanhar uma única bolinha do topo até a base da máquina
-
Um mundo físico onde só a área visível existe
- O visualizador inicial do mapa simulava apenas a área visível, mas, ao rolar a página, os novos tiles que entravam começavam vazios, criando lacunas no fluxo
- Para fazer com que eles parecessem já estar em atividade em vez de vazios, a equipe decidiu salvar snapshots dos tiles depois que alcançavam um estado estável e carregá-los imediatamente antes de entrarem na tela
- No quadrinho final, apenas os tiles renderizados existem de fato na simulação física
- Para dar a impressão de que há mais máquina acima da tela, a linha superior de tiles da simulação gera e fornece bolinhas na velocidade esperada pelas restrições de entrada
-
Snapshot no momento da aprovação
- A geração de snapshots foi conectada à UI de moderação
- Antes de aprovar um tile, o moderador precisa esperar pelo menos 30 segundos; o estado no momento em que o botão de aprovação é pressionado é salvo como snapshot
- O moderador também tem a liberdade de esperar um pouco mais até que o dispositivo esteja em um estado visualmente bom
- A abordagem de snapshots tem o efeito de reiniciar erros acumulados: quando o usuário rola a página e vê um novo tile pela primeira vez, ele encontra um estado limpo que o moderador considerou bom
- Se observado por muito tempo, muitos dispositivos podem parar ou ficar quebrados, mas, ao continuar explorando, o usuário encontra novos snapshots
- A máquina inteira não é simulada de forma completa e, como resultado, sua estrutura se aproxima de hyperreality
Estrutura de renderização com React, DOM e Rapier
- Machine foi construído sobre o motor físico Rapier
- A equipe usou o Rapier por suas vantagens em documentação, API, elementos básicos úteis e desempenho em navegador via WASM graças à implementação em Rust
- No início, a garantia de determinismo do Rapier também era interessante, mas, no fim, não houve simulação no lado do servidor
- Sobre o Rapier, foi criado um React context customizado chamado
<PhysicsContext>- Ele cria e gerencia objetos físicos do Rapier dentro do ciclo de vida dos componentes React
- Isso tornou mais fácil desenvolver cada objeto posicionável e superfície de colisão como componentes “widget”
- O React funciona como um scene graph rápido e grosseiro
- Quando um tile é desmontado, os objetos físicos e o DOM relacionados são limpos
- O hot reloading via atualização rápida facilitou ajustar os formatos de colisão
- Os hooks de física foram feitos para não funcionar fora de
<PhysicsContext>, o que foi aproveitado nas pré-visualizações estáticas da UI de moderação - Depois, a equipe concluiu que teria sido melhor criar objetos do Rapier como componentes, não como hooks
- react-three-rapier usa essa abordagem, que se encaixa melhor ao diffing do React
- A abordagem baseada em
useEffectremove a instância anterior e cria uma nova quando as dependências mudam
-
Renderização apenas com DOM
- Machine é renderizado inteiramente com DOM
- No início, a equipe achava que poderia migrar para PixiJS ou canvas se encontrasse limites de desempenho, mas levou a abordagem DOM o mais longe possível por exigir menos coisas a construir
- Para melhorar o desempenho de renderização, o loop de frames aplica diretamente os estilos dos widgets sujeitos à simulação física
- O diff do React só é executado quando a estrutura do scene graph muda
- No começo, as bolinhas também eram renderizadas com React, mas a criação e remoção frequentes aumentavam o custo de diff; por isso, foi criado um renderizador otimizado separado
- Foi aplicado draw culling em bolinhas e widgets fora da tela
- Essa abordagem funcionou bem com 4.000 bolinhas em simulação e centenas de bolinhas na tela, confirmando a renderização exclusivamente via DOM
API, moderação e operação dos envios
- O backend foi escrito em Haskell por davean e Kevin, usando Redis como armazenamento
- Para compartilhar tipos entre bases de código, foram usados OpenAPI e OpenAPI fetch
- Houve algum atrito inicial para alinhar com os tipos de Haskell
- Isso ajudou a coordenar mudanças de API na reta final
- TanStack Query foi útil para lidar com cache e atualização automática sem server push
-
UI de moderação e prioridades
- A UI de moderação projetada por Ed White era o gargalo pelo qual todos os envios passavam antes de serem publicados
- Para um tile específico, moderadores podiam ter que escolher entre centenas de designs candidatos
- A prioridade da fila foi definida atribuindo um interestingness score por tipo de widget e contando cada instância para ordenar os tiles candidatos
- Essa abordagem favorece soluções com muitos elementos, mas os moderadores compensavam revisando o meio da lista em busca de soluções mais minimalistas
- A grande diferença entre o número de designs enviados e o número de designs publicados na máquina real ficou como um ponto lamentável
- Antes do lançamento, a equipe procurou formas de publicar mais do backlog, mas não encontrou um bom compromisso dentro das restrições de tempo de moderação
- Depois que os envios ao vivo terminassem, eles queriam encontrar uma forma de compartilhar mais do dataset de envios
-
Cooldown de aprovação e controle de velocidade
- Como a qualidade dos snapshots dos tiles era importante, o botão de aprovação dos moderadores ficava desativado até a simulação rodar por pelo menos 30 segundos
- Esse cooldown permitia criar um snapshot em estado estável e verificar se a saída recebia bolinhas na velocidade esperada
- No início, a equipe esperava que isso fosse incômodo para os moderadores, mas a medida foi bem recebida por evitar decisões precipitadas
- Após o lançamento, foi adicionado um slider para que moderadores pudessem rodar a simulação muito mais rápido que em tempo real
- Com isso, passou a ser possível ver os primeiros 30 segundos de um envio em menos de 5 segundos, e também ficou mais fácil revisar comportamentos por períodos mais longos
Interações não intencionais entre tiles
- “Jamslunt Interfoggle” foi um dispositivo enviado nas primeiras horas após a publicação e usa um mecanismo baseado no alcance estreito de um ventilador
- O dispositivo acumula bolinhas azuis em um corredor e, quando o peso fica suficiente, elas transbordam para os dois lados
- “Bouncy”, posicionado acima, é um motor caótico que lança bolinhas por um caminho de interseção em três direções
- Às vezes, Bouncy envia uma bolinha verde para a saída errada, e essa bolinha rompe a pilha parada de bolinhas azuis, criando um fluxo em cadeia no Interfoggle
- No editor, para facilitar o entendimento das entradas, eram fornecidas apenas bolinhas da cor correta; por isso, o Interfoggle não poderia ter sido projetado levando esse comportamento da bolinha verde em conta
- Essas combinações não intencionais se tornaram uma das grandes diversões do projeto: ver como as pessoas usam ferramentas de forma criativa em uma tela compartilhada
Código e experimentos restantes
- O código-fonte de Machine está disponível no repositório no GitHub
- Uma implementação que simule completamente a máquina inteira de forma global continua sendo um desafio de hacking interessante
- O link para adicionar designs diretamente a Machine está em xkcd 2916
1 comentários
Opiniões no Hacker News
O que achei engraçado ao ler este texto é que, na época, eu não fazia ideia de que isso estava acontecendo.
Não parecia haver nenhuma explicação do que estava acontecendo, eu não sabia que era uma experiência coletiva, e só senti que várias coisas aleatórias estavam acontecendo de forma caótica.
Completei alguns tiles e enviei, achando que era assim que se passava para a “próxima fase”, e dei nomes idiotas como “test 1b”. Isso porque eu achava que era single-player, então pensei que só eu veria os nomes.
Depois de criar alguns, enjoei; fiquei explorando e vi coisas complexas, mas não percebi que eram envios de outras pessoas e achei que fossem apenas pontos de partida para resolver fases. No fim, eu é que caí na pegadinha de 1º de abril.
Talvez tenha sido porque nunca joguei aquele jogo da máquina original que serviu de inspiração :-)
Acho que matei o rapier ao adicionar um monte de elementos de “bonk”.
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rustat jt (rapier_wasm2d_bg.js:4836:11)at 4ea5626ea4b1e4145572.module.wasm:0xf061cat 4ea5626ea4b1e4145572.module.wasm:0xf0638at 4ea5626ea4b1e4145572.module.wasm:0xb5e7bat H.remove (rapier_wasm2d_bg.js:1051:14)at l.remove (collider_set.js:87:18)at y.removeCollider (world.js:343:28)at PhysicsContext.tsx:258:15Mesmo assim, é muito divertido, e é uma pena eu não ter sabido quando estava aberto em tempo real. Seria muito bom se desse para criar links permanentes para as máquinas individuais feitas pelas pessoas.
Entendo que possa haver questões de armazenamento, mas não daria para codificar o JSON em base64 e colocá-lo em um parâmetro de URL? Quero criar mapas estranhos e compartilhar com as pessoas.
As máquinas que entraram na versão pública completa podem ter links permanentes, mas criações individuais que não foram escolhidas na fila de curadoria não têm links permanentes.
Isso foi uma decisão deliberada para evitar o risco de hospedar conteúdo gerado por usuários sem moderação no domínio dos quadrinhos.
Para referência, esse assunto também apareceu no HN em 6 de abril, com 14 comentários.
https://news.ycombinator.com/item?id=39953514
“Não havia incentivo para pensar com cuidado sobre onde colocar os adesivos. Os jogadores não tinham agência suficiente, por meio de ações individuais, para fazer a trama avançar. Por isso, a criatividade ficou limitada a padrões simples, como repetir adesivos parecidos em forma de tiles ou criar linhas.”
Ah, então o jogo virou vida corporativa em uma grande empresa.
Participei quando foi lançado. Acho que passei cerca de uma hora tentando fazer com que as bolas certas fossem para as saídas certas da forma mais confiável possível.
Depois de enviar, atualizei a página e havia o dispositivo de outra pessoa no lugar. Reconheço que era mais bonito, mas tinha menos confiabilidade.
Eu gostaria que tivessem avisado mais cedo que funcionava assim. E parece que eu não fui o único que não sabia que a lista de blocos de construção era rolável.
Agora não tenho mais energia para voltar e conferir :(
Uau, eu e um amigo tivemos a mesma ideia em 2014 e a implementamos para o Ludum Dare. https://nickfa.ro/wiki/CoinSlot
É muito legal ver essa ideia aparecer de uma forma mais refinada e que funciona bem.
Isso me fez lembrar de quando eu era jovem. Desperdicei muitas horas com muito prazer.
https://www.myabandonware.com/game/the-incredible-machine-1m...
Acho que deixei passar alguma coisa: por que certos elementos parecem afetar apenas bolas de determinadas cores dentro da máquina?
Imagino que seja um mecanismo para impedir que as cores se misturem completamente, mas acho que isso não foi explicado no texto.
A bola amarela é leve e tem grande resistência do ar; a verde é pesada; a vermelha quica muito.
Assim, dá para projetar classificadores físicos.
Seria bom se houvesse uma forma fácil de verificar se alguma das máquinas que criei entrou na versão final.
No próximo design, talvez fosse interessante salvar os títulos dos envios anteriores em algo como armazenamento local e mostrar uma notificação.