Cómo Slow Roads intriga a los gamers y a los desarrolladores por igual, ya que destaca las sorprendentes capacidades del 3D en el navegador

Descubre el potencial de WebGL con los paisajes infinitos generados de forma procedural de este juego de conducción informal.

Slow Roads es un juego de conducción informal con énfasis en los paisajes generados de forma procedimental sin fin, todo alojado en el navegador como una aplicación WebGL. Para muchos, una experiencia tan intensiva puede parecer fuera de lugar en el contexto limitado del navegador. De hecho, corregir esa actitud fue uno de mis objetivos con este proyecto. En este artículo, analizaré algunas de las técnicas que usé para superar el obstáculo de rendimiento en mi misión de destacar el potencial a menudo pasado por alto del 3D en la Web.

Desarrollo en 3D en el navegador

Después de lanzar la función Rutas lentas, noté un comentario recurrente en los comentarios: "No sabía que esto era posible en el navegador". Si compartes este sentimiento, no eres una minoría. Según la encuesta Estado de JS de 2022, alrededor del 80% de los desarrolladores aún no experimentan con WebGL. Para mí, es una pena que se pierda tanto potencial, sobre todo cuando se trata de juegos basados en navegadores. Con Slow Roads, espero que WebGL ocupe un lugar más destacado y, quizás, que disminuya la cantidad de desarrolladores que rechazan la frase "motor de juego de JavaScript de alto rendimiento".

WebGL puede parecer misterioso y complejo para muchos, pero en los últimos años, sus ecosistemas de desarrollo han madurado mucho y se han convertido en herramientas y bibliotecas muy capaces y convenientes. Ahora es más fácil que nunca para los desarrolladores de frontend incorporar UX en 3D a su trabajo, incluso sin experiencia previa en gráficos por computadora. Three.js, la biblioteca principal de WebGL, sirve como base para muchas expansiones, incluida react-three-fiber, que agrega componentes 3D al framework de React. Ahora también hay editores de juegos integrales basados en la Web, como Babylon.js o PlayCanvas, que ofrecen una interfaz familiar y cadenas de herramientas integradas.

Sin embargo, a pesar de la utilidad notable de estas bibliotecas, los proyectos ambiciosos terminan por verse limitados por limitaciones técnicas. Quienes dudan de la idea de los juegos basados en el navegador podrían destacar que JavaScript es de subproceso único y tiene limitaciones de recursos. Sin embargo, navegar por estas limitaciones desbloquea el valor oculto: ninguna otra plataforma ofrece la misma accesibilidad instantánea y compatibilidad masiva que habilita el navegador. Los usuarios de cualquier sistema compatible con navegadores pueden comenzar a reproducir contenido con un clic, sin necesidad de instalar aplicaciones ni acceder a servicios. Sin mencionar que los desarrolladores disfrutan de la elegante conveniencia de tener frameworks de frontend sólidos disponibles para compilar la IU o controlar las redes para los modos multijugador. En mi opinión, estos valores son los que hacen que el navegador sea una plataforma tan excelente para jugadores y desarrolladores por igual. Y, como lo demuestra Slow Roads, las limitaciones técnicas a menudo se pueden reducir a un problema de diseño.

Cómo lograr un rendimiento fluido en rutas lentas

Dado que los elementos principales de Slow Roads implican movimiento a alta velocidad y generación de paisajes costosos, la necesidad de un rendimiento fluido subrayó cada una de mis decisiones de diseño. Mi estrategia principal fue comenzar con un diseño de juego reducido que permitiera tomar atajos contextuales dentro de la arquitectura del motor. Por el lado negativo, esto significa sacrificar algunas funciones que son útiles en la búsqueda del minimalismo, pero genera un sistema personalizado y súper optimizado que funciona bien en diferentes navegadores y dispositivos.

A continuación, se muestra un desglose de los componentes clave que mantienen la agilidad de Slow Roads.

Cómo dar forma al motor de entorno en función del juego

Como componente clave del juego, el motor de generación de entornos es inevitablemente costoso y, con razón, ocupa la mayor proporción de los presupuestos de memoria y procesamiento. El truco que se usa aquí es programar y distribuir el procesamiento intensivo durante un período para no interrumpir la velocidad de fotogramas con aumentos repentinos del rendimiento.

El entorno se compone de mosaicos de geometría, que difieren en tamaño y resolución (categorizados como "niveles de detalle" o LoD) según qué tan cerca aparezcan de la cámara. En los juegos típicos con una cámara de libre recorrido, se deben cargar y descargar constantemente diferentes LoD para detallar el entorno del jugador dondequiera que decida ir. Esta puede ser una operación costosa y desperdiciada, especialmente cuando el entorno en sí se genera de forma dinámica. Afortunadamente, esta convención se puede subvertir por completo en Rutas lentas gracias a la expectativa contextual de que el usuario debe permanecer en la ruta. En su lugar, se puede reservar la geometría de alto detalle para el corredor estrecho que bordea directamente la ruta.

Diagrama que muestra cómo generar la ruta con mucha anticipación puede permitir la programación y el almacenamiento en caché proactivos de la generación del entorno.
Vista de la geometría del entorno en rutas lentas renderizada como un esquema de página, que indica corredores de geometría de alta resolución que flanquean la ruta. Las partes distantes del entorno, que nunca deben verse de cerca, se renderizan con una resolución mucho más baja.

La línea central de la ruta se genera mucho antes de la llegada del jugador, lo que permite predecir con precisión cuándo y dónde se necesitarán los detalles del entorno. El resultado es un sistema ágil que puede programar de forma proactiva el trabajo costoso, generar solo lo mínimo necesario en cada momento y no desperdiciar esfuerzos en detalles que no se verán. Esta técnica solo es posible porque la ruta es única y no se ramifica, un buen ejemplo de cómo hacer compensaciones de juego que se adapten a atajos arquitectónicos.

Diagrama que muestra cómo generar la ruta con mucha anticipación puede permitir la programación y el almacenamiento en caché proactivos de la generación del entorno.
Si se observa una cierta distancia a lo largo de la ruta, los fragmentos de entorno se pueden anticipar y generar gradualmente justo antes de que se necesiten. Además, se pueden identificar y almacenar en caché los fragmentos que se volverán a visitar en un futuro cercano para evitar la regeneración innecesaria.

Ser exigente con las leyes de la física

Después de la demanda computacional del motor de entorno, la simulación de física es la segunda. Slow Roads usa un motor de física personalizado y minimalista que toma todos los atajos disponibles.

El mayor ahorro aquí es evitar simular demasiados objetos en primer lugar, y aprovechar el contexto minimalista y zen descartando elementos como las colisiones dinámicas y los objetos destructibles. La suposición de que el vehículo permanecerá en la ruta significa que se pueden ignorar de manera razonable las colisiones con objetos fuera de la ruta. Además, la codificación de la ruta como una línea central escasa permite trucos elegantes para la detección rápida de colisiones con la superficie de la ruta y los rieles de protección, todo basado en una verificación de distancia al centro de la ruta. La conducción fuera de la carretera se vuelve más costosa, pero este es otro ejemplo de una compensación justa adecuada al contexto del juego.

Cómo administrar el espacio en memoria

Como otro recurso restringido por el navegador, es importante administrar la memoria con cuidado, a pesar de que JavaScript se recoge como basura. Puede ser fácil pasar por alto, pero declarar incluso pequeñas cantidades de memoria nueva dentro de un bucle de juego puede generar problemas significativos cuando se ejecuta a 60 Hz. Además de consumir los recursos del usuario en un contexto en el que es probable que esté realizando varias tareas, las grandes reconstrucciones de elementos no utilizados pueden tardar varios fotogramas en completarse, lo que causa interrupciones notables. Para evitar esto, la memoria del bucle se puede asignar previamente en las variables de clase durante la inicialización y reciclarse en cada fotograma.

Vista del perfil de memoria antes y después de la optimización de la base de código de Slow Roads, que indica ahorros significativos y una reducción en la tasa de recolección de elementos no utilizados.
Si bien el uso general de la memoria apenas cambia, la asignación previa y el reciclaje de la memoria del bucle pueden reducir en gran medida el impacto de las reconstrucciones de elementos no utilizados costosas.

También es muy importante que las estructuras de datos más pesadas, como las geometrías y sus búferes de datos asociados, se administren de forma económica. En un juego generado de forma infinita, como Slow Roads, la mayor parte de la geometría existe en una especie de cinta de correr: una vez que un elemento antiguo queda atrás en la distancia, sus estructuras de datos se pueden almacenar y reciclar para un próximo elemento del mundo, un patrón de diseño conocido como agrupación de objetos.

Estas prácticas ayudan a priorizar la ejecución ágil, con el sacrificio de cierta simplicidad del código. En contextos de alto rendimiento, es importante tener en cuenta cómo las funciones de conveniencia a veces toman prestado del cliente para beneficiar al desarrollador. Por ejemplo, métodos como Object.keys() o Array.map() son increíblemente útiles, pero es fácil pasar por alto que cada uno crea un array nuevo para su valor que se muestra. Comprender el funcionamiento interno de estas cajas negras puede ayudarte a ajustar tu código y evitar golpes de rendimiento furtivos.

Cómo reducir el tiempo de carga con recursos generados de forma procedural

Si bien el rendimiento del entorno de ejecución debe ser la principal preocupación de los desarrolladores de juegos, los axiomas habituales sobre el tiempo de carga inicial de la página web siguen siendo válidos. Es posible que los usuarios sean más tolerantes cuando acceden a contenido pesado de forma consciente, pero los tiempos de carga largos pueden ser perjudiciales para la experiencia, si no para la retención de usuarios. Los juegos suelen requerir recursos grandes en forma de texturas, sonidos y modelos 3D, y, como mínimo, deben comprimirse con cuidado siempre que se pueda prescindir de los detalles.

Como alternativa, la generación de recursos de forma procedimental en el cliente puede evitar transferencias largas en primer lugar. Esto es un gran beneficio para los usuarios con conexiones lentas y le brinda al desarrollador un control más directo sobre la configuración de su juego, no solo para el paso de carga inicial, sino también cuando se trata de adaptar los niveles de detalles para diferentes parámetros de configuración de calidad.

Una comparación que ilustra cómo la calidad de la geometría generada de forma procedural en rutas lentas se puede adaptar de forma dinámica a las necesidades de rendimiento del usuario.

La mayor parte de la geometría de los caminos lentos se genera de forma procedimental y es simplista, con sombreadores personalizados que combinan varias texturas para brindar detalles. El inconveniente es que estas texturas pueden ser recursos pesados, aunque hay más oportunidades de ahorro aquí, con métodos como la texturización estocástica que pueden lograr mayor detalle a partir de texturas de origen pequeñas. Y, en un nivel extremo, también es posible generar texturas por completo en el cliente con herramientas como texgen.js. Lo mismo sucede con el audio, ya que la API de Web Audio permite la generación de sonido con nodos de audio.

Con el beneficio de los recursos procedimentales, la generación del entorno inicial tarda solo 3.2 segundos en promedio. Para aprovechar al máximo el tamaño de descarga inicial pequeño, una pantalla de presentación simple saluda a los visitantes nuevos y aplaza la inicialización costosa de la escena hasta después de que se presione un botón afirmativo. Esto también actúa como un búfer conveniente para las sesiones que rebotan, lo que minimiza la transferencia desperdiciada de recursos cargados de forma dinámica.

Un histograma de los tiempos de carga que muestra un pico pronunciado en los primeros tres segundos, que representa más del 60% de los usuarios, seguido de una disminución rápida. El histograma muestra que más del 97% de los usuarios ven tiempos de carga de menos de 10 segundos.

Cómo adoptar un enfoque ágil para la optimización tardía

Siempre consideré que la base de código de Slow Roads era experimental y, por lo tanto, adopté un enfoque muy ágil para el desarrollo. Cuando se trabaja con una arquitectura de sistema compleja y en rápida evolución, puede ser difícil predecir dónde pueden ocurrir los cuellos de botella importantes. El enfoque debe estar en implementar las funciones deseadas con rapidez, en lugar de hacerlo de forma ordenada, y luego trabajar hacia atrás para optimizar los sistemas donde realmente importa. El generador de perfiles de rendimiento en las Herramientas para desarrolladores de Chrome es invaluable para este paso y me ayudó a diagnosticar algunos problemas importantes con versiones anteriores del juego. Tu tiempo como desarrollador es valioso, así que asegúrate de no dedicarlo a deliberar sobre problemas que pueden resultar insignificantes o redundantes.

Cómo supervisar la experiencia del usuario

Mientras implementas todos estos trucos, es importante asegurarte de que el juego funcione como se espera en condiciones reales. Acomodar una variedad de capacidades de hardware es un aspecto básico de cualquier desarrollo de juegos, pero los juegos web pueden segmentarse para un espectro mucho más amplio que incluye computadoras de escritorio de alta gama y dispositivos móviles de hace una década a la vez. La forma más sencilla de abordar esto es ofrecer parámetros de configuración para adaptar los cuellos de botella más probables en tu base de código, tanto para tareas intensivas de GPU como de CPU, como lo revela tu generador de perfiles.

Sin embargo, la generación de perfiles en tu propia máquina solo puede abarcar una cantidad limitada de información, por lo que es valioso cerrar el ciclo de reacción con tus usuarios de alguna manera. En el caso de las rutas lentas, ejecuto estadísticas simples que informan sobre el rendimiento junto con factores contextuales, como la resolución de la pantalla. Estas estadísticas se envían a un backend de Node básico con socket.io, junto con cualquier comentario escrito que el usuario envíe a través del formulario del juego. En los primeros días, estas estadísticas detectaron muchos problemas importantes que se podían mitigar con cambios simples en la UX, como destacar el menú de configuración cuando se detectaba un FPS bajo de forma constante o advertir que un usuario podría necesitar habilitar la aceleración de hardware si el rendimiento era particularmente bajo.

Las rutas lentas que se aproximan

Incluso después de tomar todas estas medidas, queda una parte significativa de la base de jugadores que necesita jugar con parámetros de configuración más bajos, principalmente aquellos que usan dispositivos ligeros que no tienen una GPU. Si bien el rango de configuración de calidad disponible genera una distribución de rendimiento bastante uniforme, solo el 52% de los jugadores logra más de 55 FPS.

Es una matriz definida por la configuración de la distancia de visualización en comparación con la configuración de detalles, que muestra el promedio de fotogramas por segundo obtenidos en diferentes vinculaciones. La distribución se distribuye de manera bastante uniforme entre 45 y 60, y 60 es el objetivo para obtener un buen rendimiento. Los usuarios con parámetros de configuración bajos suelen ver una menor cantidad de FPS que los que tienen parámetros de configuración altos, lo que destaca las diferencias en la capacidad del hardware del cliente.
Ten en cuenta que estos datos están sesgados por los usuarios que ejecutan su navegador con la aceleración de hardware inhabilitada, lo que suele causar un rendimiento artificialmente bajo.

Afortunadamente, aún hay muchas oportunidades para ahorrar en el rendimiento. Además de agregar más trucos de renderización para reducir la demanda de la GPU, espero experimentar con trabajadores web para paralelizar la generación de entornos en un futuro cercano y, con el tiempo, es posible que deba incorporar WASM o WebGPU en la base de código. Cualquier margen que pueda liberar permitirá entornos más ricos y diversos, que será el objetivo duradero para el resto del proyecto.

En cuanto a los proyectos de pasatiempo, Slow Roads ha sido una forma abrumadoramente satisfactoria de demostrar lo sorprendentemente elaborados, eficaces y populares que pueden ser los juegos para navegadores. Si desperté tu interés en WebGL, ten en cuenta que, tecnológicamente, Slow Roads es un ejemplo bastante superficial de sus capacidades completas. Recomiendo a los lectores que exploren la exhibición de Three.js y que, en particular, quienes estén interesados en el desarrollo de juegos web se unan a la comunidad en webgamedev.com.