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 el paisaje infinito y generado mediante procedimientos de este juego casual de conducción.

Slow Roads es un juego de conducción informal que se enfoca en un entorno generado de manera continua, todo alojado en el navegador como una aplicación de WebGL. Para muchos, una experiencia tan intensiva puede parecer fuera de lugar en el contexto limitado del navegador y, de hecho, uno de mis objetivos con este proyecto es compensar esa actitud. En este artículo, desglosaré algunas de las técnicas que usé para superar los obstáculos del rendimiento en mi misión de resaltar el potencial de 3D, que a menudo se pasa por alto en la Web.

Desarrollo 3D en el navegador

Después del lanzamiento de Slow Roads, vi un comentario recurrente en los comentarios: “No sabía que esto era posible en el navegador”. Si compartes esta opinión, ciertamente no eres una minoría; según la encuesta Estado de JS de 2022, un 80% de los desarrolladores aún no han experimentado con WebGL. Para mí, es una lástima que se pierda tanto potencial, en especial cuando se trata de videojuegos basados en navegadores. Con Slow Roads, espero lograr que WebGL se destaque aún más y, tal vez, reducir la cantidad de desarrolladores que se oponen a la frase “motor de juegos 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 enormemente hasta convertirse en herramientas y bibliotecas altamente capaces y convenientes. Ahora es más fácil que nunca para los desarrolladores front-end incorporar la UX en 3D en su trabajo, incluso sin experiencia previa en gráficos por computadora. Three.js, la biblioteca líder de WebGL, sirve de base para muchas expansiones, como react-tres-fiber, que integra componentes 3D en el 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.

No obstante, a pesar de la notable utilidad de estas bibliotecas, los proyectos ambiciosos eventualmente se ven limitados por las limitaciones técnicas. Los escépticos sobre la idea de los juegos basados en el navegador podrían resaltar que JavaScript es de un solo subproceso y está restringido a los recursos. Sin embargo, al navegar por estas limitaciones, se 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 el navegador pueden comenzar a jugar con un solo clic, sin necesidad de instalar aplicaciones ni de acceder a los servicios. Sin mencionar que los desarrolladores disfrutan de la elegante comodidad de tener frameworks de frontend sólidos disponibles para compilar IU o controlar las herramientas de redes para modos multijugador. En mi opinión, estos valores hacen que el navegador sea una plataforma excelente tanto para jugadores como para desarrolladores. Además, 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 uniforme en rutas lentas

Dado que los elementos centrales de Slow Roads implican movimiento de alta velocidad y generación costosa de paisajes, la necesidad de un rendimiento uniforme subrayó todas mis decisiones de diseño. Mi principal estrategia fue comenzar con un diseño del juego de estilo reducido que permitiera tomar atajos contextuales dentro de la arquitectura del motor. La desventaja significa cambiar algunas funciones deseables en busca del minimalismo, pero da como resultado un sistema personalizado y altamente optimizado que se juega bien en diferentes navegadores y dispositivos.

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

Dar forma al motor del entorno en torno al juego

Como componente clave del juego, el motor de generación del entorno es inevitablemente costoso, lo que justifica tomar la mayor proporción de los presupuestos para 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 picos de rendimiento.

El entorno está compuesto por mosaicos geométricos, que difieren en tamaño y resolución (categorizados como "niveles de detalle" o LoD) en función de qué tan cerca aparecerán a la cámara. En los juegos típicos con una cámara libre, se deben cargar y descargar diferentes LoD de forma constante para detallar los alrededores del jugador, donde sea que elija ir. Esta puede ser una operación costosa y que genera pérdidas, en especial cuando el entorno en sí se genera de forma dinámica. Afortunadamente, esta convención puede subvertirse por completo en Slow Roads, gracias a la expectativa contextual de que el usuario debe permanecer en la ruta. En cambio, se puede reservar la geometría de alto detalle para el pasillo estrecho que flanquea directamente la ruta.

Un diagrama que muestra cómo generar la ruta con mucha anticipación puede permitir la programación proactiva y el almacenamiento en caché de la generación del entorno.
Es una vista de la geometría del entorno de Slow Roads renderizada como un esquema de página, que indica los pasillos de geometría de alta resolución que rodean la ruta. Las partes distantes del entorno, que nunca deberían verse de cerca, se renderizan con una resolución mucho más baja.

La línea media de la ruta se genera mucho antes de la llegada del jugador, lo que permite una predicción precisa de exactamente cuándo y dónde se necesitarán los detalles del entorno. El resultado es un sistema eficiente que puede programar de manera proactiva el trabajo costoso, lo que genera solo el mínimo necesario en cada momento y sin desperdiciar esfuerzo en detalles que no se verán. Esta técnica solo es posible porque la ruta es una única ruta que no se ramifica, un buen ejemplo de compensaciones de juego que se adaptan a los atajos arquitectónicos.

Un diagrama que muestra cómo generar la ruta con mucha anticipación puede permitir la programación proactiva y el almacenamiento en caché de la generación del entorno.
Cuando se observa una distancia determinada en la ruta, los fragmentos del entorno se pueden interrumpir y generar gradualmente justo antes de que sean necesarios. Además, todos los fragmentos que se revisarán en el futuro cercano pueden identificarse y almacenarse en caché para evitar una regeneración innecesaria.

Ser exigente con las leyes de la física

En segundo lugar, después de la demanda computacional del motor de entorno, está la simulación física. Slow Roads usa un motor físico personalizado y minimalista que toma todos los atajos disponibles.

El mayor ahorro aquí es evitar simular demasiados objetos en primer lugar, aprovechando el contexto zen mínimo, descontando elementos como colisiones dinámicas y objetos destructibles. La suposición de que el vehículo permanecerá en la ruta significa que es razonablemente ignorar las colisiones con objetos todoterreno. Además, la codificación de la ruta como una línea intermedia dispersa permite trucos elegantes para la detección rápida de colisiones con la superficie de la ruta y las barandillas protectoras, todo basado en una verificación de distancia hasta el centro de la ruta. La conducción todoterreno se vuelve más costosa, pero este es otro ejemplo de intercambio justo adecuado para el contexto del juego.

Cómo administrar el espacio en memoria

Como otro recurso restringido en el navegador, es importante administrar la memoria con cuidado, a pesar del hecho de que JavaScript tiene una recolección de elementos no utilizados. 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 probablemente esté realizando varias tareas a la vez, las grandes recolecciones de elementos no utilizados pueden tardar varios fotogramas en completarse, lo que provoca inestabilidades notorias. Para evitar esto, la memoria de bucle se puede asignar previamente en las variables de clase en la inicialización y reciclarse en cada fotograma.

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

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 infinito como Slow Roads, la mayor parte de la geometría existe en una especie de cinta de correr. Una vez que una pieza antigua se retrasa en la distancia, sus estructuras de datos se pueden almacenar y reciclar nuevamente para una próxima parte del mundo, un patrón de diseño conocido como reducción de objetos.

Estas prácticas ayudan a priorizar la ejecución eficiente, con el sacrificio de algo de simplicidad del código. En contextos de alto rendimiento, es importante tener en cuenta la forma en que las funciones prácticas a veces se prestan al cliente para el beneficio del desarrollador. Por ejemplo, los métodos como Object.keys() o Array.map() son muy útiles, pero es fácil pasar por alto que cada uno crea un array nuevo para el valor que se muestra. Comprender el funcionamiento interno de estas cajas negras puede ayudarte a ajustar tu código y evitar resultados engañosos en el rendimiento.

Reduce el tiempo de carga con recursos generados mediante procedimientos

Si bien el rendimiento del tiempo de ejecución debería ser la preocupación principal de los desarrolladores de juegos, los axiomas habituales relacionados con 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 manera intencional, pero los tiempos de carga prolongados pueden ser perjudiciales para la experiencia, si no la retención de usuarios. Los juegos suelen requerir grandes recursos en forma de texturas, sonidos y modelos 3D que, como mínimo, se deben comprimir con cuidado en cualquier lugar donde se puedan evitar los detalles.

De forma alternativa, en primer lugar, generar recursos en el cliente mediante un procedimiento puede evitar transferencias largas. Este es un gran beneficio para los usuarios con conexiones lentas y le brinda al desarrollador un control más directo sobre cómo se constituye su juego, no solo para el paso de carga inicial, sino también para adaptar los niveles de detalles a diferentes configuraciones de calidad.

Una comparación que ilustra cómo la calidad de la geometría generada mediante procedimientos en Slow Roads se puede adaptar dinámicamente a las necesidades de rendimiento del usuario.

La mayor parte de la geometría de Slow Roads se genera mediante procedimientos y es simple, con sombreadores personalizados que combinan varias texturas para aportar detalles. La desventaja es que estas texturas pueden ser elementos pesados, aunque existen más oportunidades de ahorro aquí, con métodos como el textura estocástico capaz de lograr un 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 de procedimiento, la generación del entorno inicial demora en promedio solo 3.2 segundos. Para aprovechar al máximo el pequeño tamaño de descarga inicial, una pantalla de presentación simple recibe a los visitantes nuevos y pospone la costosa inicialización de la escena hasta que se presiona un botón. Esto también actúa como un búfer conveniente para las sesiones rebotadas, lo que minimiza la transferencia desperdiciada de recursos cargados de forma dinámica.

Un histograma de los tiempos de carga que muestra un fuerte pico en los primeros tres segundos que representa a más del 60% de los usuarios, seguido de un descenso rápido. El histograma muestra que más del 97% de los usuarios ven tiempos de carga inferiores a 10 segundos.

Adopta un enfoque ágil para las optimizaciones tardías

Siempre pensé 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 que evoluciona rápidamente, puede ser difícil predecir dónde pueden ocurrir los cuellos de botella importantes. El enfoque debe centrarse en implementar las funciones deseadas con rapidez, en lugar de hacerlo de forma limpia, y luego trabajar en sentido inverso para optimizar los sistemas en los casos en que realmente sea importante. El generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome es invaluable para este paso y me ayudó a diagnosticar algunos problemas importantes de versiones anteriores del juego. Tu tiempo como desarrollador es valioso, así que asegúrate de no dedicar tiempo a deliberar problemas que puedan resultar insignificantes o redundantes.

Cómo supervisar la experiencia del usuario

Cuando implementas todos estos trucos, es importante asegurarse de que el juego funcione como se espera en el mundo real. Adaptar una variedad de capacidades de hardware es un aspecto fundamental del desarrollo de cualquier juego, pero los juegos web pueden orientarse a un espectro mucho más amplio que incluya computadoras de escritorio de gama alta y dispositivos móviles de hace una década. La forma más sencilla de abordar esto es ofrecer una configuración que adapte los cuellos de botella más probables en la base de código (para las tareas intensivas en GPU y CPU), tal como lo revela el generador de perfiles.

Sin embargo, crear perfiles en tu propia máquina solo puede abarcar esa parte, por lo que es valioso cerrar el ciclo de retroalimentación con tus usuarios de alguna manera. Para Slow Roads, ejecuto estadísticas sencillas que informan sobre el rendimiento junto con factores contextuales como la resolución de pantalla. Estas estadísticas se envían a un backend de Node básico a través de socket.io, junto con cualquier comentario escrito que el usuario envíe a través del formulario dentro del juego. En los primeros días, estas estadísticas detectaron muchos problemas importantes que se podrían mitigar con cambios simples en la UX, como destacar el menú de configuración cuando se detecta un FPS constantemente bajo o advertir que un usuario podría necesitar habilitar la aceleración de hardware si el rendimiento es particularmente deficiente.

Las rutas lentas que se aproximan

Incluso después de tomar todas estas medidas, queda una porción significativa de la base de jugadores que debe jugar en parámetros de configuración más bajos, principalmente aquellos que usan dispositivos ligeros que no tienen una GPU. Si bien el rango de parámetros de configuración de calidad disponibles conduce a una distribución del rendimiento relativamente uniforme, solo el 52% de los jugadores alcanza los 55 FPS.

Es una matriz definida por la configuración de distancia de vista en comparación con la configuración de detalles, que muestra el promedio de fotogramas por segundo alcanzado en diferentes vinculaciones. La distribución se distribuye de manera bastante uniforme entre 45 y 60, donde 60 es el objetivo para un buen rendimiento. Los usuarios con configuraciones bajas tienden a ver un valor de FPS más bajo que los de configuración alta, 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 a menudo causa un rendimiento artificialmente bajo.

Por suerte, aún quedan muchas oportunidades de ahorrar en el rendimiento. Además de agregar más trucos de renderización para reducir la demanda de GPU, espero experimentar con trabajadores web en paralelización de la generación del entorno a corto plazo y, con el tiempo, podría ver la necesidad de incorporar WASM o WebGPU a la base de código. Cualquier margen que pueda liberar permitirá entornos más enriquecidos y diversos, que será el objetivo perdurable del resto del proyecto.

A medida que avanzan los proyectos de pasatiempos, Slow Roads ha sido una forma abrumadoramente gratificante de demostrar lo sorprendentemente elaborado, el rendimiento y los juegos de navegador populares que pueden ser. Si logré despertar tu interés en WebGL, ten en cuenta que las rutas lentas en tecnología son un ejemplo bastante superficial de todas sus capacidades. Recomiendo a los lectores que exploren el catálogo de Three.js. Quienes estén interesados en el desarrollo de juegos web en particular podrán participar en la comunidad de webgamedev.com.