Notas de desarrollo de "Machine" de xkcd
Idea inicial
- Después de darle vueltas a la idea hasta finales de marzo, se decidió a inicios de abril
- "¿Se podría hacer un dispositivo gigante en forma de mosaico como los GIF de blueballs hechos por usuarios de Something Awful? Que cada quien contribuya con un pequeño cuadrado"
- Al principio parecía que la idea ya estaba completamente formada, pero al conversar de verdad se dieron cuenta de que todavía había muchas decisiones por tomar
- Tenían ideas distintas sobre partes clave, como de dónde salen las bolas, si todos ven la misma máquina, cuál es el objetivo y cómo interactúan los jugadores
Lo que aprendieron de intentos anteriores
- Ya tenían experiencia creando cómics interactivos centrados en contenido creado por usuarios
- Lorenz: un exquisito cadáver donde los lectores escribían el texto de los paneles para desarrollar el chiste y la historia (fue muy divertido)
- Collector's Edition: un juego donde los lectores encontraban stickers ocultos en el archivo de xkcd y los pegaban permanentemente en un lienzo compartido (no dio el resultado esperado)
- Empezar con un mapa central vacío al inicio llevaba al caos
- Había pocos incentivos para colocar stickers, así que era difícil que las acciones individuales impulsaran la trama y solo aparecían patrones simples
- No había una historia general ni un objetivo, y tampoco estaba clara la relación entre los stickers
- Para que un lienzo colectivo funcione, hace falta enseñar con ejemplos qué se puede construir, además de contar con un contexto y un propósito compartidos
Diseño de restricciones
- Una vez que decidieron hacer una gran máquina de caída de canicas, se enfrentaron a demasiadas opciones
- Decidieron estructurarla como una cuadrícula de 100x100
- Simular 10 mil tiles en tiempo real en el cliente parecía arriesgado
- No estaba claro cómo podrían los jugadores construir subzonas de una máquina compleja sin comunicarse directamente, ni si tiles separados funcionarían al integrarse
- Tras varios experimentos mentales, establecieron 3 principios clave:
1. Maximizar la expresividad del jugador, incluso sacrificando exactitud
- ¿Qué tan predecible debía ser la máquina?
- También consideraron ejecutar todo en el servidor o validar tiles individuales, pero comprobaron en el editor prototipo que era fácil crear patrones caóticos de colisión entre bolas
- Si las bolas no se movían en línea recta sin interferencias, era fácil terminar con máquinas impredecibles
- Aumentar la predictibilidad de la máquina chocaba con la libertad del jugador
- El plazo de desarrollo ajustado también los llevó a preferir un enfoque con menos predicción/simulación
- Decidieron dar a los jugadores una libertad de creación muy flexible, incluso permitiendo máquinas extremadamente no deterministas o rotas
- Eso requería moderación activa para verificar que se cumplieran las restricciones y eliminar contenido inapropiado
2. Dar restricciones estrictas que fomenten máquinas compatibles e intercambiables
- La aceptación mediante moderación y las máquinas impredecibles de los jugadores hicieron que, paradójicamente, hiciera falta más orden
- Al principio pensaron en entradas y salidas totalmente libres, pero durante la moderación se dieron cuenta de que si era necesario reemplazar tiles iniciales podía causar fallas a gran escala
- Diseñaron restricciones lo bastante fuertes para que varios jugadores pudieran crear diseños compatibles dentro del mismo espacio de tile
- Aplicaron el principio de robustez: "sé conservador con los datos que envías y tolerante con los datos que recibes"
- Para imponer restricciones de entrada y salida, hacía falta un mapa completo de la máquina desde el inicio
- La generación del mapa también permitía ajustar la dificultad de la máquina (desde simples 1 entrada/1 salida hasta complejas fusiones de 4 entradas/4 salidas)
- Para dar retroalimentación en tiempo real, restringieron los tiles para que expulsaran bolas a una velocidad parecida a la que las recibían
- Se limitaron las máquinas que se tragaban bolas o las retrasaban
- Se sometían los tiles a pruebas de caos con velocidades de entrada aleatorias
- Establecieron el principio de "hacer correr la máquina un rato y verificar si, en promedio, cumple las restricciones"
3. La máquina debe llegar a un estado estable dentro de los primeros 30 segundos
- Surgió la pregunta de cuánto tiempo tendría que observarla una persona moderadora
- Calcularon el tiempo que tomaría moderar la máquina completa (83.3 horas para 10 mil tiles)
- Se decidió de forma algo arbitraria que debía entrar en estado estable dentro de 30 segundos
- Configuraron las bolas para que desaparecieran después de 30 segundos
- Al principio no había tiempo de expiración, así que mientras los jugadores aprendían el juego las bolas se acumulaban y llenaban la pantalla
- Al aumentar la cantidad de cuerpos rígidos activos, la simulación física se volvía más lenta
- Llegó un punto en que las bolas estorbaban más de lo que divertían
- La expiración de las bolas evitó que la máquina acumulara errores con el tiempo
- Con solo observar 30 segundos, la persona moderadora podía hacerse una buena idea de hacia dónde podían ir la mayoría de las bolas
Simulación y surrealismo
- Dos grandes desafíos de la arquitectura de Machine:
- ¿Funcionaría conectar tiles heterogéneos con las restricciones de diseño anteriores para formar una máquina completa?
- Lo verificaron generando y resolviendo algunos mapas pequeños
- Si no se puede ejecutar la máquina gigante en tiempo real ni en el servidor ni en el cliente, ¿cómo mostrarla?
El objetivo era hacer posible seguir una sola bola mientras se hace scroll
- Aunque no se simule toda la máquina, sí debía simularse el área alrededor de la región que ve el jugador
- Al principio probaron simular solo el área visible en un mapa infinito
- Funcionaba bastante bien, pero al hacer scroll los tiles entraban a la simulación en un estado inicial vacío, lo que generaba huecos en el flujo
- En vez de tiles vacíos, debían verse como si ya tuvieran actividad
Segundo desafío: tomar snapshots de los tiles solo después de que alcanzaran un estado estable, para que existan justo antes de entrar al área visible al hacer scroll
- En el cómic final, la vista con display clipping desactivado (
CSS overflow:hidden, contain:paint desactivado):
- ¿Notaste los snapshots? Si no pones mucha atención, es difícil darte cuenta
- Solo los tiles renderizados existen dentro de la simulación física
- Optimización de visualización: solo se ven las bolas dentro del área visible, pero la simulación ocurre en todo el rango del tile
- Para fingir la parte superior de la máquina, se generan y suministran bolas en la fila superior de la simulación (con base en la velocidad esperada por las restricciones de entrada)
- Integraron la generación de snapshots en la UI de moderación
- La persona moderadora debía esperar al menos 30 segundos antes de aprobar un tile
- Al hacer clic en el botón de aprobar, se generaba el snapshot
- A criterio de la persona moderadora, se podía esperar un poco más hasta que la máquina se viera bien
- El enfoque de snapshots funcionó mejor de lo esperado
- Dio el buen resultado de reiniciar los errores acumulados en la máquina
- La primera impresión de los tiles vistos al hacer scroll era un estado limpio y agradable que le gustó a quien moderó
- En la práctica, si se observara por mucho tiempo, muchas máquinas podrían romperse o fallar, pero como al seguir explorando se entra en snapshots nuevos, eso no llega a verse
- La máquina del cómic que se recorre con scroll no es real. Es surrealista
- No se simula completa al mismo tiempo, pero eso terminó dando un resultado aún mejor
Renderizar miles de bolas con React y el DOM
- Construido sobre el motor de física Rapier
- Su excelente documentación, una API limpia con primitivas útiles y su implementación en Rust (ejecutada como WASM en el navegador) dieron un rendimiento impresionante
- Al principio les atrajo la garantía de determinismo de Rapier, pero no hicieron simulación del lado del servidor
- Encima de Rapier escribieron un contexto personalizado de React,
<PhysicsContext>
- Crea objetos físicos de Rapier y los administra dentro del ciclo de vida de componentes de React
- Facilita desarrollar componentes "widget" para cada objeto colocable con física o superficies de colisión
- React actúa como un scene graph simple y medio improvisado
- Simplifica la carga/descarga de tiles al hacer scroll: cuando un tile se desmonta, se limpian toda su física y su DOM
- Como extra, hizo más fácil conectar hot reloading con fast refresh (muy útil para ajustar formas de colisión)
- Otra ventaja del enfoque con contexto de React:
- Si los hooks de física no están dentro de
<PhysicsContext>, se convierten en noop
- Lo usaron para renderizar vistas previas estáticas de tiles en la UI de moderación
- Les habría gustado usar componentes en lugar de hooks para crear objetos de Rapier (como hace react-three-rapier)
- Encaja mejor con el diffing de React (cuando cambian dependencias,
useEffect elimina la instancia anterior y luego la recrea)
- Machine se renderiza por completo usando el DOM
- Durante el desarrollo inicial les preocupaba topar con el límite de rendimiento del renderizado en DOM
- Si se volvía demasiado lento, esperaban cambiar a PixiJS o canvas, pero querían ver hasta dónde podían llevar el DOM
- Optimización del rendimiento de renderizado:
- El frame loop aplica estilos directamente a los widgets que tienen simulación física
- El diff de React solo corre cuando hay cambios estructurales en el scene graph
- Al principio, las bolas se renderizaban con React
1 comentarios
Comentarios en Hacker News
Al reunir varias opiniones, se puede resumir lo siguiente:
Rapier, aunque en ocasiones hubo cierres por errores recursivos