Construyendo un mundo abierto en el navegador, parte 29: Un controlador, cualquier cuerpo
Por Oleg Sidorkin, CTO y cofundador de Cinevva
¿Acabas de llegar? Consulta la guía de la serie. Explica qué es un spike y enlaza todas las partes.
La parte 28 trató sobre la hierba y la oclusión. Veintiocho partes construyeron un mundo en el que estar: terreno que puedes interpretar, agua en la que puedes nadar, vegetación que se mantiene convincente hasta el horizonte y un servidor que recuerda lo que cambiaste. Esta parte trata sobre aquello que se mueve por todo ese mundo, y es la culminación de todo el motor, porque el objetivo no es crear un controlador para el jugador. Es crear un controlador al que no le importe qué cuerpo controla, de dónde proceden sus animaciones ni si quien está al mando es una persona o una IA. El spike 58 construye el movimiento como una pila de comportamientos conectables sobre un motor de físicas que no sabe nada sobre locomoción y después demuestra la arquitectura ejecutando tres cuerpos diferentes con una instancia del mismo motor. El spike 59 acopla un avatar real con animaciones retargeteadas a ese controlador sin cambiar ni una sola línea. El spike 60 acopla un paquete de animaciones distinto que no necesita ningún retargeting y extrae la lógica de selección de clips para convertirla en algo que realmente se puede probar.
Un motor de físicas que no sabe nada sobre caminar
Abrir el spike 58 en una pestaña nueva ↗ · Ver código fuente
La regla de diseño es estricta, y ese es precisamente el objetivo: el motor de cápsulas no posee ninguna lógica de locomoción. Ni caminar, ni correr, ni saltar, ni fricción, ni límite de velocidad máxima, ni siquiera gravedad. Integra una cápsula cinemática contra el terreno y nada más. Cada comportamiento de locomoción —caminar, deslizarse, planear, escalar, nadar, agacharse o gestionar la resistencia— vive en un controlador autocontenido registrado en el motor. En cada fotograma, el motor actualiza todos los controladores, pregunta a cada uno si quiere tomar el control y permite que el candidato con mayor prioridad escriba la velocidad. Caminar no tiene ningún estatus especial. Es simplemente el controlador de menor prioridad que siempre responde que sí, por lo que actúa como comportamiento predeterminado cuando no se activa ningún otro. Un controlador consta de seis funciones pequeñas: un tick que actualiza su propio estado interno en cada fotograma aunque no esté activo; un predicado puro wantsControl que reclama el fotograma; un applyForces, ejecutado únicamente por el ganador, que escribe la velocidad y aplica su propia gravedad si la necesita; además de los opcionales onEnter, onExit y stateName. El controlador de natación devuelve ownsCollision: true desde applyForces para hacerse cargo de la gestión del terreno, porque, de lo contrario, su resorte de flotabilidad entraría en conflicto con el ajuste de los pies al suelo del motor.
Incluso las cosas que no son locomoción terminan filtrándose a través de ese contrato, y eso fue lo que obligó a introducir la siguiente idea: los canales. Un diseño anterior de un solo canal pasaba todo por un único arbitraje, lo que significaba que un monitor de resistencia o una postura agachada tenían que fingir que eran locomoción y después rechazar el control mediante un truco en el que wantsControl devolvía falso, solo para poder realizar su contabilidad interna. La solución divide los controladores en canales con nombre que arbitran de forma independiente y se aplican en un orden fijo: recursos, después postura y, por último, locomoción. Los recursos se ejecutan primero porque sus escrituras, como la reducción de resistencia, son leídas por los demás. La postura se ejecuta en segundo lugar porque la reducción de altura de la cápsula al agacharse debe aplicarse antes de que el controlador de marcha la lea para limitar la velocidad máxima. La locomoción se ejecuta al final y controla la escritura de velocidad de cada fotograma. Así, un observador de resistencia, un modificador de agachado y un controlador de natación activo pueden coexistir limpiamente, cada uno en su propio canal, sin que ningún controlador tenga que mentir sobre lo que es. La demo visualiza todo esto mediante una cápsula básica que cambia de color según el controlador activo, para que puedas observar cómo se produce el arbitraje: el verde de caminar cambia a naranja en una pendiente pronunciada cuando toma el control el deslizamiento, a cian en el lago cuando gana la natación y a crema en el aire cuando pulsas la tecla de planeo.
Un motor, tres cuerpos
La verdadera prueba de que «no posee ninguna lógica de locomoción» no es el jugador. Es comprobar si el mismo motor, sin modificar, puede controlar algo que ni siquiera sea un jugador. createCapsuleEngine es una fábrica pura sin estado a nivel de módulo, sin singletons y sin efectos secundarios por instancia, por lo que el spike la instancia tres veces. El jugador es una instancia con el conjunto completo de controladores. Un caballo montable es una segunda instancia que solo registra un controlador de marcha, lo que convierte la idea completa en algo literal: el repertorio de movimientos de un cuerpo viene determinado únicamente por los controladores que registraste. Por eso, el caballo es más rápido en terreno llano y físicamente no puede escalar, nadar ni planear, porque esos controladores nunca se añadieron. Un PNJ autónomo es una tercera instancia, actualizada en cada fotograma junto con el jugador y dirigida por un controlador de deambulación que genera sus propias entradas para que el cuerpo se desplace por sí mismo sin manos sobre el teclado. El motor nunca llega a saber que uno de sus cuerpos es un caballo ni que otro está controlado por IA. Todos son el mismo integrador de cápsulas con listas de controladores diferentes.
Montar es la única pieza que vive deliberadamente fuera del motor. Intercambiar el control entre el jugador y el caballo implica coordinar dos motores, y un controlador se ejecuta dentro de un único motor y no puede ver más allá de ese límite. Por tanto, la lógica para montar reside en el nivel del anfitrión: es una pequeña máquina de estados que decide qué motor avanza en este fotograma, congela el otro, desplaza suavemente al jinete hasta la silla mediante un smoothstep de tres cuartos de segundo e indica a la cámara qué cuerpo debe seguir. La fábrica del motor nunca se entera de nada de esto. Esa es la frontera que la arquitectura establece y respeta. Los comportamientos que pertenecen a un cuerpo son controladores; la coordinación entre cuerpos es responsabilidad del anfitrión. Mantener ambos conceptos separados es la razón por la que añadir un cuarto o un cuadragésimo cuerpo no requeriría nada nuevo.
Probado sin navegador
Como el motor no accede a window, document ni Three.js, todo puede ejecutarse sin interfaz gráfica. El spike incluye un arnés de pruebas para Node que simula la interfaz del terreno, hace avanzar el motor fotograma a fotograma con entradas programadas y comprueba el estado resultante. Así, las regresiones que resultan penosas de detectar jugando se capturan mediante un script: la transición de salto y aterrizaje, los umbrales de entrada y salida de la natación, la desactivación automática de la escalada cuando una superficie se nivela y las tasas de consumo de resistencia. Alimentar el motor dos veces con la misma secuencia de entradas e intervalos de tiempo, y comprobar que el estado de salida sea idéntico byte a byte, fija el motor como determinista, que es la propiedad de la que dependerá en el futuro una versión en red. Merece la pena conservar una regresión detectada por el arnés: al pasar de terreno transitable a una pendiente demasiado pronunciada para caminar, antes se activaba el controlador de deslizamiento y el jugador rebotaba cuesta arriba. La solución fue una condición de descenso. El deslizamiento solo comienza si la cápsula está cayendo realmente sobre la pendiente, por lo que caminar horizontalmente contra una superficie inclinada ahora bloquea limpiamente al jugador en lugar de hacerlo deslizarse. La prueba que comprueba que «caminar contra una pendiente no transitable bloquea al jugador» garantiza que siga siendo así.
Conectar un avatar real sin tocar los controladores
Abrir el spike 59 en una pestaña nueva ↗ · Ver código fuente
El spike 59 comprueba si la capa de controladores está realmente desacoplada del cuerpo. Sustituye la cápsula de colores por un personaje real con esqueleto, el rig FBX 3MIKE con clips de Quaternius Universal Animation Library retargeteados sobre él, y los controladores no cambian en absoluto. El punto de unión es una única cadena. Cada controlador ya comunica un nombre de estado mediante stateName: idle, walk, run, jump, fall, land, slide, glide, swim y swimIdle. La capa del avatar asigna ese nombre a un clip retargeteado mediante una tabla de alias y realiza una transición por fundido al cambiar. El controlador de marcha elige dinámicamente su propio subestado a partir del contacto con el suelo, la velocidad vertical y la velocidad horizontal. De este modo, un único controlador de marcha gestiona los estados de reposo, caminar, correr, saltar, caer y aterrizar, y el avatar se limita a seguir el nombre comunicado. Como este spike es para un solo jugador, el avatar elimina la indirección mediante enteros de red y la abstracción de múltiples personajes de los spikes de red anteriores, y asigna el nombre del estado directamente a un clip. La prueba es que toda la mejora visual, desde una cápsula hasta un humano con rig, no modificó ni una sola línea del código de locomoción, que es exactamente la ventaja que debe ofrecer un controlador conectable.
Un paquete que no necesita retargeting y un selector que puedes probar
Abrir el spike 60 en una pestaña nueva ↗ · Ver código fuente
El spike 60 conecta un tercer cuerpo, el paquete POLYGON Base Locomotion de Synty, y el cargador es casi inexistente. Cada clip se distribuye como un FBX independiente que contiene una copia integrada del mismo esqueleto de Synty junto con una animación horneada. Como el rig del personaje y todos los clips utilizan nombres de huesos idénticos, basta con tomar el clip de fbx.animations[0] y reproducirlo directamente en el mezclador del personaje, sin que intervenga ninguna biblioteca de retargeting. Three.js resuelve los destinos de las pistas de animación por el nombre del hueso, no por la identidad del objeto, así que un paquete del estilo de Synty o Mixamo creado para el rig correspondiente funciona directamente. Ese es el contraste deliberado con la ruta de UAL del spike anterior, que requiere un retargeting pesado porque los clips de origen y el rig de destino se crearon para esqueletos diferentes. El mismo controlador, el mismo punto de unión mediante nombres de estado y dos pipelines de animación completamente distintos detrás.
La otra mitad del spike 60 consiste en hacer comprobable la lógica de selección de clips. Elegir qué clip reproducir está lleno de umbrales subjetivos, y esa lógica estaba enterrada dentro de la capa del avatar, junto a FBXLoader y el DOM, donde no se podía probar. El spike extrae el selector a funciones puras que no acceden ni a Three.js ni a la ventana: reciben un registro simple del jugador —velocidad, velocidad horizontal, orientación, contacto con el suelo, normal del terreno y velocidad de impacto— y un sustituto de la acción del clip, y devuelven una cadena con el alias del clip. Esto permite que los umbrales existan como constantes con nombre y verificadas mediante aserciones. El salto se divide en variantes caminando, corriendo y esprintando mediante intervalos de velocidad alineados con el umbral real de esprintar del controlador de marcha; los aterrizajes se dividen en suaves, medios y fuertes según la velocidad de impacto; las variantes de clips cuesta arriba y cuesta abajo se activan a partir del producto escalar de la proyección sobre la pendiente, con una zona muerta que usa el clip para terreno llano por debajo de unos siete grados; una transición de reposo a locomoción siempre se resuelve hacia delante para que arrancar desde parado nunca reproduzca un clip hacia atrás; y una transición de parada está condicionada por un tiempo mínimo dentro del bucle, porque los clips de parada según la fase de los pies de Synty contienen aproximadamente un segundo de desaceleración creada de antemano que resulta ridícula si se añade a un paso que el jugador apenas llegó a dar. Un pequeño estabilizador de estados retrasa el estado de caída durante unos fotogramas para que una pérdida de contacto con el suelo de un solo fotograma al cruzar una junta no haga parpadear la animación. Extraer todo esto de la envoltura de renderizado permite probar las reglas con tests unitarios sin GPU, aplicando la misma disciplina de ejecución sin interfaz gráfica que recibió el propio motor en el spike 58. Esa es la diferencia entre ajustar las sensaciones de locomoción a base de conjeturas y poder definirlas con precisión.
Tecnología mencionada en este capítulo
Un motor de físicas sin locomoción. El motor de cápsulas integra un cuerpo cinemático contra el terreno y no posee lógica para caminar, correr, saltar, aplicar fricción, limitar la velocidad ni aplicar gravedad. Cada comportamiento es un controlador registrado que expone tick, wantsControl, applyForces y, opcionalmente, onEnter/onExit/stateName. Caminar es simplemente el comportamiento predeterminado de menor prioridad que siempre responde que sí, y un controlador puede devolver ownsCollision: true para hacerse cargo de la gestión del terreno. El controlador de natación lo hace para que su resorte de flotabilidad no entre en conflicto con el ajuste de los pies al suelo.
Canales de arbitraje independientes. Los controladores se registran en canales con nombre —recursos, postura y locomoción— que arbitran por separado y se aplican en un orden fijo. Así, observadores como la resistencia y modificadores como agacharse coexisten con la locomoción activa en lugar de fingir que toman el control para después rechazarlo. Las escrituras de recursos, como la resistencia, son leídas por la postura y la locomoción; las escrituras de postura, como la altura de la cápsula al agacharse, son leídas por el límite de velocidad de la locomoción; la locomoción se ejecuta al final y controla la escritura de velocidad. Una fábrica, muchos cuerpos. createCapsuleEngine es una fábrica pura sin singletons, por lo que el mismo motor controla al jugador, un caballo montable que solo puede caminar y un PNJ con dirección autónoma alimentado mediante entradas sintéticas. El repertorio de movimientos de un cuerpo viene determinado exactamente por el conjunto de controladores registrados, así que el caballo no puede trepar ni nadar porque esos controladores nunca se añadieron. La lógica para montar reside en el nivel del host, no en un controlador, porque coordina dos motores que no pueden acceder el uno al otro. Consulta LOD controlado por GPU.
Pruebas deterministas sin interfaz gráfica. El motor no accede a window, document ni Three.js, por lo que un entorno de pruebas de Node lo controla fotograma a fotograma y comprueba el salto y aterrizaje, los umbrales de natación, la desactivación automática de la escalada y el consumo de resistencia. Reproducir dos veces una entrada idéntica y comprobar que la salida también sea idéntica demuestra el determinismo, y una prueba de regresión fija el umbral de descenso que impide que un desplazamiento horizontal contra una pendiente pronunciada haga que el jugador se deslice hacia atrás.
Vinculación de avatares independiente del cuerpo con un selector verificable. Los controladores devuelven una cadena con el nombre del estado y la capa visual la asigna a un clip, por lo que sustituir una cápsula por un avatar 3MIKE + UAL con retargeting no requiere modificar el código de locomoción. Los clips de Synty POLYGON comparten los nombres de los huesos con el rig y se reproducen sin retargeting (Three.js vincula las pistas por el nombre del hueso), a diferencia del costoso retargeting de la ruta UAL. El selector de clips se extrae en funciones puras con constantes con nombre para los intervalos de velocidad de salto, la intensidad del aterrizaje, las variantes de proyección sobre pendientes, una transición desde reposo siempre hacia delante, una duración mínima de protección para la transición de parada y un filtro antirrebote del estado de caída, de modo que las sensaciones de locomoción puedan probarse mediante tests unitarios en lugar de ajustarse a ojo.
Parte 29 de 30. Anterior: Parte 28 - Hierba hasta el horizonte y un terreno que se oculta a sí mismo Siguiente: Parte 30 - Una cámara que respeta las paredes Guía de la serie: /blog/2026-02-25-open-world-browser-series-guide