Rill Valley: un valle multijugador con WebGPU en el navegador, calculado por un lenguaje funcional
Un valle al atardecer que se recorre en el navegador: bosque, lago, cristales, armas, chat de voz y bots. El terreno, la física y la lógica del juego están escritos en Rill; el mismo archivo se compila a WebAssembly para el navegador y a un programa nativo para el servidor, y WebGPU dibuja la imagen.

Rill Valley es un valle al atardecer que se recorre en el navegador: bosques de pinos y álamos, un lago en el centro, cristales que brillan en las laderas y Saturno en el cielo. Varias personas pueden caminar por el mismo valle, hablar entre ellas y dispararse. El valle en sí, es decir, el terreno, dónde están los árboles y los cristales, caminar, caer, el vuelo de cada bala y las matrices de la cámara, se calcula en Rill, el lenguaje funcional que estoy escribiendo. La imagen la dibuja WebGPU.
Jugar: rill.baltavista.com/rillwebgpu. Hace falta un navegador con WebGPU (Chrome o Edge). Haz clic para capturar el ratón; WASD para caminar, espacio para saltar, dos veces espacio para el jetpack, shift para correr, C para agacharse, clic izquierdo para disparar, clic derecho para apuntar, 1-4 para cambiar de arma, R para recargar, M para la música.

Un archivo, dos compilaciones
Todo lo que define el valle está en un solo archivo: valley.rill, unas 8.800 líneas. Ese archivo se compila dos veces:
world.rillexpone 56 funciones sobre él y se compila a WebAssembly. Es el motor del juego en el navegador, 232 KB.server.rilllo envuelve en un servidor WebSocket y se compila a un programa nativo de Linux.
Ninguno de los dos decide nada sobre lo que es el valle. Esa es la única razón por la que el servidor y el navegador pueden ponerse de acuerdo sobre adónde fue una bala: los dos ejecutan el mismo código.
En el navegador, JavaScript solo hace lo que un módulo WebAssembly no puede: preparar el dispositivo WebGPU, cargar los shaders y ejecutar las pasadas de dibujo en orden. El sonido, el micrófono y la conexión de red también le tocan a él.
Cómo se hace el valle
El valle no se descarga, se calcula. El terreno es una suma de doce senos. El lugar de cada pino, álamo, cristal y mata de hierba se elige con un recorrido determinista sobre un hash: un candidato, un hash, una regla sobre el suelo, y lo que sobrevive se queda donde cayó. El servidor le dice a cada cliente solo tres números (cuántos pinos, cristales y matas) y cada uno construye el mismo valle por su cuenta.
Todo el valle se reconstruye en unos 4 milisegundos. Eso permite que el juego se ajuste a la máquina: empieza con un nivel de calidad elegido según la tarjeta gráfica, luego mira los tiempos de fotograma medidos y reconstruye el valle más denso o más ralo (árboles, hierba, luciérnagas, mapa de sombras, número de olas, densidad de píxeles) hasta gastar el presupuesto de fotograma de la pantalla. Una máquina lenta dibuja una malla de terreno más tosca pero pisa exactamente el mismo suelo, porque todas las preguntas de física se le hacen a la función de altura y no a la malla.

El dibujo
Con WebGPU, como una serie de pasadas, cada una en su propio archivo WGSL:
- Una pasada de sombras desde un sol fijo de hora dorada.
- Una pasada principal con MSAA 4x, cielo procedural y niebla.
- Pinos, álamos, cristales y hierba que se mece con el viento, dibujados con instancing. El viento mueve el agua, los pinos y la hierba con los mismos dos números, porque los tres leen los mismos valores que calcula Rill.
- Un lago de olas de Gerstner con espuma en la orilla y en las crestas. Lo que de verdad hace que parezca agua es un campo de ruido lento que lo recorre y escala todas las olas a la vez: hay zonas picadas y zonas casi de cristal.
- Un enjambre de luciérnagas en un compute shader.
En la máquina donde se tomaron las capturas (Apple serie M, calidad máxima), la escena tiene 342 mil triángulos a 60 fps y 4-9 ms de GPU por fotograma.
Todas las texturas, modelos, sonidos, shaders y el propio código de la página viajan en un solo archivo, valley.rpak: 103 archivos, 6,1 MB. El formato es propio de este proyecto. Lo que se reduce se comprime con LZ, y quien lo descomprime es el motor en WebAssembly, no el navegador. Un visitante descarga dos cosas: index.html y valley.rpak.
El juego
El juego empieza con unas órdenes: te han enviado a un planeta desconocido, tu misión en nombre de la compañía es destruir unos cristales que se consideran una amenaza para la Tierra, y puedes encontrarte con agentes de compañías rivales que tienen la misma misión.
- Cristales: hay tres colores y el color lo dice todo. El ámbar es el más común y al romperse cura al jugador un 3%, el cian un 6% y el violeta (el más raro) un 12%. Rareza y valor van en el mismo orden, así que un color visto a doscientos metros se convierte en un motivo para cruzar el valle. Los cristales además lanzan fuego: meteoros que vuelan, frenan, caen y dejan un cráter que quema a quien esté cerca.
- Armas: un fusil de cerrojo, un RPG-7, un AK y una MG3. La bala del fusil sale a 420 m/s; la gravedad y la resistencia del aire la frenan todo el camino, así que su caída crece más rápido que la parábola de los libros. Al apuntar la vista se acerca cinco veces y la mira ocupa la pantalla. El cerrojo se acciona en cuatro movimientos separados después de cada disparo y la vaina vacía sale volando por encima del hombro.
- Lanzacohetes: el cohete explota en lo primero sólido que toca y se lleva los cristales que haya a seis metros. El cohete cargado se ve en el tubo; desaparece al dispararlo y no vuelve hasta que se carga el siguiente.
- Gravedad: es un ajuste del servidor, no una constante. La del valle por defecto es de 26 m/s², unas dos veces y media la de la Tierra. Las piernas empujan con la misma fuerza en cualquier planeta; un planeta más débil no las hace más fuertes, deja que el mismo impulso llegue más lejos.


Con otras personas
El servidor es un strand por conexión y un strand central que es dueño del mundo. Solo el central toca el mundo y todo lo demás es un canal, así que no hay ningún cerrojo (lock) en el servidor. Un tick también es un mensaje en el mismo canal que las teclas, de modo que una entrada llega antes o después de un tick, nunca en medio.
- 20 ticks por segundo, protocolo binario. Una instantánea son 31 bytes más 37 por jugador. El protocolo fue de texto mientras se ajustaba; al pasar a binario conservó las letras como códigos de mensaje, así que un hexdump todavía se lee: una instantánea sigue empezando por
s. - El cliente también camina. El navegador se mueve a sí mismo con la misma función
walkque ejecuta el servidor, así que girar y caminar responden a la tecla y no al viaje de ida y vuelta. La posición que llega del servidor se ajusta a lo largo de unos fotogramas. La puntería nunca se corrige. - No se cree nada de lo que envía un cliente. Se comprueba que cada número sea finito (a un jugador en una posición
NaNno le da nada), la dirección de un disparo se convierte en un vector unitario y una bala solo puede ir a menos de 20 grados de donde mira su dueño. - Límites: 150 mensajes por segundo por conexión, 64 conexiones a la vez, como mucho 16 desde una misma dirección.
- Nombres: cada jugador recibe un token la primera vez que llega; el navegador lo guarda y el nombre que tomó no se le da a nadie más, ni siquiera cuando su dueño no está.
- Voz: los jugadores pueden hablar entre sí y el sonido no pasa por el servidor. Dos navegadores montan su propia conexión WebRTC; el servidor solo los presenta.
- Guardianes: por el valle también andan bots acorazados que no son personas; caminan por las laderas, brillan donde les da la luz y solo saltan para atacar.
Ahora mismo hay cuatro valles abiertos (kuzey, güney, doğu y batı: norte, sur, este y oeste); la página pregunta en cuál quieres entrar al abrirse. Cada uno corre en su propio contenedor Docker y usa unos 5 MB de memoria. Con ?solo la página abre un valle solo para ti, sin nadie más; no hay servidor y todo funciona en el navegador.

En cifras
valley.rill (el valle en sí) | ~8.800 líneas de Rill |
server.rill (el servidor multijugador) | ~2.300 líneas de Rill |
host.js (la escena WebGPU) | ~4.800 líneas de JavaScript |
| Shaders | un archivo WGSL por pasada |
world.wasm | 232 KB |
valley.rpak | 103 archivos, 6,1 MB |
| Memoria por servidor | ~5 MB |
Créditos
Las texturas, los modelos y los sonidos vienen de fuentes CC0: texturas de Poly Haven y ambientCG; el fusil de Poly Haven y el RPG-7 de OpenGameArt; la figura humana y sus animaciones de la Universal Animation Library de Quaternius. La música es The Field of Dreams de pauliuw (OpenGameArt, CC0). Nada se usa tal como venía: las texturas son fotografías de día y el valle está al atardecer, así que cada una se rehízo para la luz del valle.
Rill Valley sigue siendo una demo. Funciona sin problemas, pero no ha pasado por nada más duro que una tarde recibiendo disparos. Si pasas por allí y algo se rompe, avísame.