Carrera Doty

Doty Run es un juego endless runner con jugabilidad dinámica, generación procedural y mecánicas innovadoras. Guiados por el aventurero Doty, los jugadores recorren un sendero helado lleno de monedas, obstáculos y curvas cerradas, todo ello mientras persiguen puntuaciones altas. Doty Run pone de relieve la capacidad de Niantic Studio para crear juegos web complejos e interactivos.

Hazlo tuyo con los proyectos de ejemplo 

player controller

Controlador del jugador checkmark bullet

Aprende a implementar un controlador de jugador que responda a acciones de entrada y gestos táctiles.

Ver ejemplo de proyecto
Gyroscope Controller

Controlador del giroscopio checkmark bullet

Domina los controles de inclinación basados en giroscopio para disfrutar de un juego intuitivo y envolvente.

Ver ejemplo de proyecto

Behind the Build: Doty Run

Written by Camilo Medina

December 4, 2024


Introduction

Doty Run es un emocionante juego de carrera sin fin en el que los jugadores guían a Doty, un intrépido aventurero que recorre un camino helado para recoger las preciadas Monedas Doty. ¿El objetivo? Sobrevive todo el tiempo que puedas, llega lo más lejos posible y recoge todas las monedas que puedas para superar tu puntuación más alta.


Controla a Doty inclinando el teléfono hacia la izquierda o la derecha para moverte hacia los lados, y desliza el dedo hacia arriba, abajo, izquierda o derecha para saltar, rodar o girar. Para los jugadores de PC, el movimiento se controla con las teclas de flecha. Navega por el camino de hielo, evita los obstáculos y sigue el rastro para que la aventura continúe. Cada carrera guarda tu mejor puntuación: ¡cada aventura es una oportunidad
para superarla! ¿Hasta dónde puedes llegar?

Project Structure

Escena 3D

  • GameManager: Contiene el script game-manager responsable del estado central del juego y la puntuación.
  • GameUI: Contiene las entidades responsables de la interfaz de usuario
    • Start:
    • Contiene el script ui-start
    • Gameplay:
    • Contiene el script ui-gameplay
    • GameOver: Contiene el script ui-game-over
  • Entorno: Modelos 3D del entorno del juego
  • Fog: Contiene el script fog-controller para gestionar los efectos de niebla en la escena
  • Sfx: Contiene el componente de audio para la música de fondo y el script sfx-manager para los efectos de sonido
  • PathRoot: Es el padre raíz de los elementos de ruta generados durante el tiempo de ejecución, contiene el script de movimiento de ruta
  • PathPool: Contiene los scripts path-manager y object-spawner para crear y generar dinámicamente objetos path a partir de los elementos hijos disponibles durante el juego
    • Base: Modelo 3D de mosaico base en el que el jugador puede moverse en tres posiciones: izquierda, centro y derecha. Contiene una malla y un colisionador de física
    • Parcial: Modelo 3D de mosaico parcial en el que el jugador sólo puede moverse a una posición (izquierda, centro o derecha). Contiene una malla y un colisionador físico.
    • Corner: Modelo 3D de azulejo de esquina donde el camino cambia de dirección. Contiene una malla y un colisionador de física
    • Arc: Modelo 3D de obstáculo en forma de arco. Contiene una malla y un colisionador de física
    • SmallObstacle: Modelo 3D de obstáculo pequeño, condicionalmente activo. Contiene una malla y un colisionador físico
    • LargeObstacle: Modelo 3D de obstáculo grande, condicionalmente activo. Contiene una malla y un colisionador físico.
    • Coin: Modelo 3D de una moneda. Contiene una malla y un colisionador de física configurado sólo para eventos
  • Player: Contiene un colisionador de física y todos los scripts relacionados con el jugador: controlador de jugador, colisión de jugador, gestor de entrada, controlador giroscópico, animador de jugador y controlador de entrada táctil
  • Doty: Modelo 3D de Doty, contiene una malla y animaciones para todas las acciones del jugador.
  • Cámara: Contiene la cámara del juego
  • Luz ambiental: Contiene la luz ambiental del juego
  • Directional Light: Contiene la luz direccional del juego

Activos

Modelos 3D

  • Entorno
    • env-1.glb: Un modelo 3D del entorno principal
  • Objetos
    • moneda_1.glb: Un modelo 3D de una moneda, utilizada como coleccionable.
    • doty_coin_gold_v2.glb: Versión alternativa del modelo de moneda.
    • hielo_grande.glb: Modelo 3D de un gran obstáculo de hielo.
    • ice_small.glb: Modelo 3D de un obstáculo de hielo pequeño.
  • Trayectoria
    • arco.glb: Un modelo 3D de una estructura de arco.
    • base_v1.glb: Un modelo 3D de la baldosa base.
    • base_v2.glb: Modelo 3D alternativo de la baldosa base.
    • esquina.glb: Modelo 3D de la baldosa de esquina.
    • parcial.glb: Un modelo 3D de la loseta parcial base.

Jugador

  • doty_opti_web_anim.glb: Un modelo 3D altamente optimizado del personaje del jugador, con animaciones diseñadas para el rendimiento web.

Efectos de sonido (SFX)

  • coin.mp3: Efecto de sonido reproducido cuando se recoge una moneda.
  • Fast_Lane_Fun.mp3: Música de fondo para secuencias de juego rápidas.

INTERFAZ DE USUARIO

  • Fin de partida
    • game_over_background.jpg: Imagen de fondo mostrada en la pantalla de fin de partida.
    • restart_button.png: Imagen del botón utilizado para reiniciar el juego.
  • Juego
    • coin_icon.png: Icono de moneda para la interfaz de usuario.
  • Inicio
    • start_background.jpg: Imagen de fondo que aparece en la pantalla de inicio.
    • start_button.png: Imagen del botón utilizado para iniciar el juego.

Scripts

Este es un resumen rápido de los scripts de este proyecto

.

app.js: Este archivo .js sirve como punto de entrada principal para la aplicación web y se puede utilizar para implementar la funcionalidad principal. Aquí, importa estilos CSS globales y modulares(utilities.css, start.css, gameplay.css, y game-over.css) para definir la apariencia visual y el diseño de la interfaz de usuario del juego. Además, inyecta la fuente que utilizará la interfaz de usuario para lograr un estilo coherente.

game-manager.ts: Gestiona los estados principales del juego (inicio, juego, gameover), maneja los cálculos de puntuación de los jugadores, y gestiona el almacenamiento y la recuperación de
la puntuación más alta

.

utilidades.js: Implementa mecánicas básicas que requieren acceso a la biblioteca THREE, como la función para solicitar acceso al giroscopio. Estas utilidades se mantienen separadas de los scripts de Studio para mantener la organización y evitar conflictos con la biblioteca ECS.

Cámara

camara-seguir-jugador.ts: Implementa un sencillo sistema de cámara en tercera persona. En el esquema se requiere una referencia al jugador. La cámara sigue la posición y rotación del jugador, siguiendo la rotación y posición a lo largo del eje Y.

Esta mecánica se consigue creando una entidad vacía que sigue al jugador, y luego haciendo la cámara hija de esta entidad. La cámara se sitúa en su posición actual creando un desplazamiento respecto al jugador. Este enfoque evita la necesidad de calcular matrices de transformación para ajustar dinámicamente la posición de la cámara con respecto al jugador.

El script puede ampliarse para seguir también al jugador a lo largo del eje X. Por razones de jugabilidad, esta función no está activada, pero puede activarse descomentando la sección correspondiente del código.

CSS

game-over.css: Maneja los estilos de la interfaz de usuario para la pantalla de finalización del juego. Incluye la apariencia del fondo del juego, la tabla de puntuaciones y el botón de reinicio.

gameplay.css: Define los estilos de interfaz de usuario para el estado del juego. Incluye indicadores estáticos y dinámicos que se muestran de izquierda a derecha: cantidad de monedas, distancia y puntos.

start.css: Gestiona los estilos de interfaz de usuario para la pantalla de inicio. Define el aspecto del fondo de inicio y del botón de inicio.

utilities.css: Proporciona clases de utilidad para los elementos de la interfaz de usuario, como clases para ocultar elementos y añadir animaciones.

Entradas

gyro-controller.ts: Este componente implementa la mecánica de entrada utilizando el giroscopio del teléfono para detectar la rotación alrededor del eje Y (inclinación izquierda-derecha) basado en una propiedad de esquema configurable, angleLimit. Acceder al giroscopio en algunos dispositivos requiere un permiso explícito del usuario, que debe solicitarlo mediante una interacción directa, como pulsar un botón. El componente puede modificarse fácilmente para detectar otros tipos de rotación, como la rotación alrededor del eje Z (dirección de la brújula) o del eje X (inclinación delante-detrás).

input-manager.ts: Este componente gestiona la implementación del sistema de entrada de Studio. Actualmente, Studio sólo ofrece la posibilidad de comprobar si una entrada está activa mediante input.getAction(), sin ofrecer información sobre cuándo se activa o finaliza la entrada. Este script implementa su propio sistema, enviando eventos para ambos casos on-input-action-triggered y on-input-action-ended.

touch-input-controller.ts: Este componente gestiona las interacciones táctiles en la pantalla, disparando el evento on-input-action-triggered cuando un dedo se desliza hacia arriba, abajo, izquierda o derecha por la pantalla.

Generador de rutas

fog-controller.ts: Este componente configura y aplica un efecto de niebla a la escena de Studio accediendo a la escena three.js a través de world.three.scene. El esquema permite personalizar el color de la niebla (utilizando valores RGB), así como sus distancias cercana y lejana.

object-spawner.ts: Este componente posiciona objetos en baldosas en una nueva sección del camino basándose en una configuración generada procedimentalmente. La configuración determina qué objetos -como monedas, obstáculos o potenciadores- se colocan en cada baldosa.

path-manager.ts: Este componente gestiona una mecánica central del juego: la generación dinámica de nuevos tramos de camino. Coloca las baldosas basándose en una configuración generada procedimentalmente, determinando la longitud de la sección, la dirección y el posicionamiento parcial de las baldosas.

path-movement.ts: Este componente mueve en dirección hacia delante las baldosas actuales de
en la sección de camino, creando la ilusión de movimiento del jugador a través de la escena de juego de
.

Jugador

jugador-animador.ts: Controla las animaciones del jugador respondiendo a eventos del juego como saltar, rodar, caer y colisiones. Cambia entre animaciones como Ralentí, Correr, Saltar y Caer en función de las acciones del jugador o los eventos del juego. El componente también se encarga de la sincronización de animaciones específicas, como volver a Ejecutar después de una tirada o pausar las animaciones durante la finalización del juego. También comprueba las colisiones con el obstáculo del arco de una manera simple y directa: verificando si la animación actual es Roll. Este enfoque asegura que el jugador pulsó el botón de bajada en el momento correcto sin complicar demasiado el código.

player-collision.ts: Maneja todas las colisiones del jugador. El juego implementa dos tipos principales de colisiones: una para baldosas y otra para objetos. Envía eventos correspondientes a cada colisión para desencadenar acciones en otros scripts. Dado que Studio no dispone actualmente de un sistema de etiquetado o nomenclatura de entidades, el filtrado de colisiones se realiza utilizando el nombre objectThree, que puede obtenerse o establecerse a través de world.three.entityToObject.get(entity). Tenga en cuenta que este enfoque puede causar un comportamiento extraño al probar el juego en Live Preview.

player-controller.ts: Gestiona el movimiento del jugador, incluyendo saltar, rodar y girar, basándose en los eventos de entrada de gyro-controller.ts, input-manager.ts y touch-input-controller.ts. Activa las animaciones correspondientes a través de player-animator.ts y comprueba las condiciones de fin de juego, como caídas o colisiones con obstáculos, enviando el evento on-player-lost-game cuando es necesario. Además, se adapta al método de entrada: en ordenadores de sobremesa o dispositivos sin giroscopio, permite el desplazamiento lateral mediante gestos de teclado o swipe respectivamente .

SFX

sfx-manager.ts: Controla la reproducción de música de fondo, activando el audio cuando el evento de inicio inicia el juego.

INTERFAZ DE USUARIO

ui-game-over.ts: Crea divs individuales para contener cada elemento de la pantalla overUI del juego, incluyendo el fondo, el botón de reinicio, y el panel que muestra la puntuación del juego. También gestiona el evento que se dispara cuando se pulsa el botón de reinicio.

ui-gameplay.ts: Crea divs individuales para contener cada elemento de la pantalla de la interfaz de usuario del juego, representando las tres estadísticas del juego: monedas, distancia y puntos. Realiza un seguimiento de la distancia recorrida por el jugador y de la puntuación del juego durante la partida.

ui-start.ts: Crea divs individuales para contener cada elemento de la pantalla de inicio del juego, incluyendo el fondo y el botón de inicio. También gestiona el evento que se desencadena cuando se pulsa el botón de inicio.

Implementation

A la hora de desarrollar un juego de carrera sin fin, hay que tener en cuenta un detalle particular: el personaje del jugador no se mueve; es el camino el que tiene que avanzar hacia el jugador, creando la ilusión de que éste se mueve siempre hacia delante.

image1-3

¿Por qué?

Como el juego no sabe lo lejos que puede llegar el jugador, alejarse demasiado del origen global podría provocar errores en los cálculos matemáticos debido a las limitaciones de precisión en coma flotante. Además, mantener al jugador inmóvil en la posición (0,0,0) simplifica el desarrollo de ciertas mecánicas de juego, como la detección de colisiones y el movimiento de la cámara.


Entonces, ¿qué hace qué?

  • Jugador: Se mueve sólo a lo largo de los ejes X e Y, realizando acciones como saltar,
    rodar, moverse lateralmente y rotar alrededor del eje Y al girar.
  • Camino: Se mueve a lo largo del eje Z hacia el jugador, creando la ilusión de movimiento hacia delante
    .
Interfaz de

usuario

Todo juego requiere una interfaz de usuario, y Studio ofrece un sistema de interfaz de usuario integrado. Sin embargo, dado que este juego está diseñado para ejecutarse en un navegador web, la IU de Studio puede mejorarse utilizando CSS, lo que permite diseños más elegantes y una mayor personalización.

He aquí un sencillo flujo de trabajo para implementar una interfaz de usuario con TypeScript y CSS en Studio:

  1. Cree un componente ECS: por ejemplo, my-component.ts
  2. Añada el componente a una entidad de escena: Adjunta el componente personalizado a una entidad de tu escena.
  3. Instanciar el elemento de interfaz de usuario: Cree un nuevo elemento div y asígnele un ID único. Ejemplo: const myUiElement = document.createElement('div') myUiElement.id = 'my-ui-element'
  4. Añade el elemento UI al documento: Añade el elemento al cuerpo del documento. p. ej., document.body.append(myUiElement)
  5. Crear un archivo CSS: p. ej., my-css-style.css
  6. Definir estilos para el nuevo elemento de interfaz de usuario: Utilice el ID único para apuntar al elemento en su archivo CSS. e.g., #my-ui-element {/_ Add styles here _/}
  7. Importa el archivo CSS en la aplicación: Importa el archivo CSS recién creado en tu app.js. por ejemplo, import './my-css-style.css'

Sugerencia: El archivo app.js debe crearse manualmente; no se incluye por defecto en un proyecto de Studio.

¡Et voilà! Usando este flujo de trabajo, es posible implementar cualquier elemento de UI.

Este juego cuenta con tres diferentes pantallas de interfaz de usuario, cada uno implementado siguiendo los pasos anteriores:

  • Pantalla de inicio: Muestra una imagen de fondo, el título del juego, y un botón de inicio para comenzar el juego.
  • Pantalla de juego: Proporciona actualizaciones en tiempo real, como la puntuación y el estado de los jugadores.
  • Pantalla de fin de partida: Muestra la puntuación final e incluye un botón de reinicio para volver a jugar.
image2-2

 

Como el juego requiere acceso al giroscopio como entrada para controlar al jugador, al pulsar el botón de inicio se activa la función requestGyroscopePermission del script utilities.js. Esto hace que el navegador solicite los permisos necesarios al usuario antes de que comience el juego.

Controlador del jugador

El script player-controller.ts está diseñado para manejar entradas de tres scripts separados: gyro-controller.ts, input-manager.ts, y touch-input-controller.ts. Cada guión proporciona datos de entrada específicos que el controlador del jugador utiliza para mover o animar al personaje.

image5

El script gyro-controller.ts envía el evento on-device-orientation-changed, incluyendo el deviceOrientationState como dato. Este estado representa la orientación del dispositivo (izquierda, centro o derecha). El player-controller. ts escucha este evento y llama a la función movePlayerSideways(side), ajustando la posición del jugador en consecuencia.

Tanto input-manager.ts como touch-input-controller.ts envían el evento on-input-action-triggered, con actionName como dato. Representa la acción desencadenada (arriba, izquierda, derecha o abajo). El player-controller. ts escucha este evento y llama a rotateAndMovePlayer(actionName), que se encarga tanto de la rotación como del movimiento en función de la acción especificada.

Para cada acción ejecutada por el jugador, el script player-controller.ts envía el evento de animación correspondiente para garantizar que se activa la animación adecuada.

Creador de caminos

Como ya se ha mencionado, el personaje del jugador no se mueve; en su lugar, el camino se mueve hacia el jugador, creando la ilusión de movimiento hacia delante. Para conseguirlo, el juego implementa un camino que consta de secciones, y cada sección está formada por fichas. Existen diferentes tipos de baldosas.

image6
  1. Baldosa base: La baldosa estándar y completa. En una loseta base, el jugador puede moverse lateralmente entre tres posiciones: izquierda, centro y derecha.
  2. Loseta Parcial: Una loseta fina, que cubre sólo un tercio de la anchura de la loseta base. Aquí, el jugador sólo puede moverse en una posición lateral.
  3. Corner Tile: Indica un cambio en la dirección del camino y marca el comienzo de una nueva sección. Cada baldosa de esquina da paso a una nueva sección del camino. En esta baldosa se permite al jugador girar para continuar en la dirección del camino.


Cuando el juego comienza y al colocar la esquina, path-manager.ts computa la configuración de una nueva sección usando la función computeNewSectionPathConfig(includeCornerTile, includePartialTile).

image9
         
// TODO: convert image to code

      

Esta función genera una matriz de cadenas que representan la secuencia de fichas de la sección.

Por ejemplo:

['baldosa-base', 'baldosa-parcial', 'baldosa-parcial', 'baldosa-parcial', 'baldosa-parcial', 'baldosa-base', 'baldosa-base', 'baldosa-base', 'baldosa-esquina'].

Cada cadena describe el tipo de baldosa que debe aparecer en la sección. Pasando false a los parámetros includeCorner o includeVariants, el juego puede excluir esquinas o baldosas parciales según sea necesario.

El path-manager. ts escucha el evento on-tile-changed enviado por el script player-collision.ts. Este evento se dispara cada vez que el jugador colisiona con una nueva loseta. La función de devolución de llamada handleTileChange(event) gestiona este evento eliminando la baldosa anterior y añadiendo una nueva para mantener continuamente la ruta.

Cuando se añade una nueva baldosa a la sección, se adjunta como hija de la entidad PathRoot. El script path-movement.ts mueve todas las fichas hijo hacia delante en la dirección del jugador, creando la ilusión de que el jugador siempre se mueve hacia delante.

image4-3
         
// TODO: convert image to code

      

La colocación de objetos, como monedas, obstáculos o potenciadores, sigue un proceso similar al de la colocación de fichas. El script object-spawner.ts utiliza la función computeObjectConfigInSectionTiles(addCoins, addObstacles, addPowerUps) para generar configuraciones para objetos en la nueva sección.

Esta función es activada por el evento on-new-path-section-computed enviado por el path-manager.ts.

Por ejemplo { 0: [], 1: [], 2: ['obstáculo'], 3: ['moneda', 'obstáculo'], 4: ['powerUp'], 5: [], 6: ['esquina'] }

Cada clave corresponde a un índice de baldosa, y el valor es un array de objetos a colocar en esa baldosa. Este sistema está diseñado para ser flexible y ampliable, lo que permite añadir nuevos tipos de objetos.

Para mejorar la estética del juego, en Studio se ha implementado un efecto de niebla que da al jugador la ilusión de que el camino es interminable. Esto evita que el jugador se dé cuenta de cómo se van añadiendo las fichas una a una.

Juego

Durante el juego, el número actual de monedas recogidas se muestra en la parte izquierda de la pantalla, mientras que en la parte derecha se muestran los puntos actuales. Una vez que el jugador alcanza cada 250 metros, una actualización de estado muestra la distancia actual.

image3-1

Cuando el jugador pierde, ya sea por caerse o chocar con un obstáculo, el script game-manager.js calcula la nueva puntuación. Si la nueva puntuación es superior a la anterior, guarda y muestra la nueva puntuación más alta con un mensaje exclusivo; de lo contrario, simplemente muestra la puntuación. Estas dos funciones permiten guardar y recuperar datos.

image7
         
// TODO: convert image to code