Sushi Beats Drop

¡Mueve la cabeza para comer el sushi que cae en este juego de ritmo! Gana puntos, crea combos y captura sushi especial para obtener recompensas en una experiencia divertida y atractiva.

sushibeatsdropcover

Hazlo tuyo con el proyecto de ejemplo

headtouchinput

Entrada Head Touch checkmark bullet

Este proyecto demuestra un sistema de entrada para un juego que utiliza controles de inclinación de la cabeza y controles de pantalla táctil para activar eventos.

Ver proyecto de muestra
objectcloneanddetection

Clonación y detección de objetos checkmark bullet

Clonar varias veces objetos de escena existentes y hacerlos caer. Uso de colisionadores y eventos para detectar cuándo el sushi toca un plato.

Ver proyecto de muestra

Behind the Build: Sushi Beat Drop

Written by Saul Pena Gamero & Mackenzie Li

May 8, 2025


Summary

Durante los dos últimos meses, nuestro equipo ha desarrollado un juego rítmico que combina la jugabilidad rítmica tradicional con elementos innovadores de realidad aumentada (RA). Esta fusión de mecánica clásica con tecnología de realidad aumentada crea una experiencia única y atractiva que distingue a nuestro juego. Nuestro objetivo principal era diseñar un juego divertido y envolvente que animara a los jugadores a disfrutar de los desafíos rítmicos de una forma totalmente nueva. 
Este proyecto se basa en el trabajo que iniciamos durante el último concurso de Niantic, en el que exploramos aplicaciones creativas de los filtros faciales de realidad aumentada. Nos inspiramos en un proyecto de muestra de Niantic Studio, que mostraba la mecánica del movimiento de la cabeza en la realidad aumentada. Nos fascinó lo interactivo y entretenido que podía ser este concepto, y decidimos convertirlo en una mecánica central de nuestro juego. 

Para determinar el mejor estilo de juego para esta mecánica, hicimos una lluvia de ideas y nos inspiramos en juegos rítmicos como Taiko no Tatsujin y Osu. Como aficionados al género, imaginamos un juego que incorporara no sólo sincronización y ritmo, sino también interacción física mediante movimientos de la cabeza. Al fusionar estos elementos, hemos creado un juego que destaca por su jugabilidad única y su compromiso físico, ofreciendo a los jugadores una experiencia de juego rítmica sin igual.

Gameplay

Visión general 
Nuestro juego de ritmo se centra en un concepto simple pero atractivo: el sushi cae desde la parte superior de la pantalla en sincronía con el ritmo de la música. El objetivo del jugador es atrapar el sushi en el momento justo mientras cae sobre los platos de abajo. Hay dos platos situados a la izquierda y a la derecha 
de la pantalla, y las piezas de sushi caerán hacia cada plato al ritmo de la música. Cuando el sushi se alinea con un plato, el jugador debe actuar para "atraparlo", ganando puntos por sincronización precisa. 

Controles y entradas de usuario 
Los jugadores pueden interactuar con el juego utilizando dos métodos de control diferentes, diseñados para ofrecer flexibilidad y mejorar la experiencia general. El método de control principal utiliza la inclinación de la cabeza para jugar sin manos y de forma envolvente. Si el sushi cae hacia el plato izquierdo, el jugador inclina la cabeza hacia la izquierda para cogerlo; lo mismo ocurre con el plato derecho con una inclinación hacia la derecha. Este estilo de control aprovecha la mecánica de realidad aumentada para añadir una capa de compromiso físico y diversión. 
Para aquellos jugadores a los que inclinar la cabeza les resulte difícil o incómodo durante largas sesiones, también hemos implementado la opción de tocar para jugar. En este modo, los jugadores pueden tocar el lado izquierdo o derecho de la pantalla para coger sushi en el plato correspondiente. Esta alternativa garantiza que el juego siga siendo ameno y accesible, atendiendo a distintas preferencias y niveles de comodidad. 

Al combinar una jugabilidad basada en el ritmo con controles intuitivos y personalizables, el juego ofrece una experiencia divertida e interactiva que atrae a un público amplio.

 

Project Structure

Escena 3D

Nuestra escena de juego incluye varios componentes esenciales, como scripts, activos 3D, el rastreador facial, la cámara y la iluminación. Aunque algunos de ellos, como la cámara y la iluminación, son relativamente intuitivos, en esta sección nos centraremos en los activos 3D y el rastreador facial. 

El rastreador facial desempeña un papel fundamental en la detección de la posición y los movimientos de la cabeza del jugador. Permite al juego determinar la dirección y el momento en que se inclina la cabeza, lo que posibilita una interacción precisa con la mecánica del juego. 
Los activos 3D incluyen los dos platos y las piezas de sushi, ambos esenciales para la experiencia del juego rítmico. Las placas son objetos estáticos, cada uno equipado con un colisionador físico y un componente estático de cuerpo rígido. Un script, ScoreArea, se adjunta a cada plato para manejar la mecánica de puntuación cuando el sushi cae sobre ellos. 

Las piezas de sushi, por su parte, son objetos dinámicos. Cada sushi tiene un colisionador físico y un cuerpo rígido dinámico, lo que les permite interactuar con el entorno de forma natural mientras caen. Además, cada pieza de sushi está asociada a un script Sushi que rige su comportamiento e integración en el juego. 

La funcionalidad específica de los scripts se tratará en detalle en una sección posterior, pero estos componentes crean colectivamente la experiencia interactiva e inmersiva de nuestro juego rítmico.

 

Activos
Todos los activos de nuestro juego están dibujados a mano o creados a medida, lo que demuestra el esfuerzo artístico y la atención al detalle que se ha puesto en el proyecto. Los modelos 3D se crearon con Blender, lo que añade un toque personal y único al juego. 
Los activos incluyen diversos elementos visuales e interactivos, como la portada, un GIF instructivo para guiar a los jugadores y animaciones para el sistema gacha. Además, creamos una página de felicitaciones para celebrar los logros de los jugadores, así como los dos adorables modelos 3D de sushi de gato que sirven como elementos clave del juego.

Scripts
GameManager: 

  • El gameManager se encarga de las transiciones de estado del juego, incluyendo la pantalla de inicio, selección de nivel, tutorial, dentro del juego, recompensas y final. Gestiona dinámicamente los elementos de la interfaz de usuario, los fondos y los escuchadores de eventos para cada estado, garantizando transiciones e interacciones fluidas con el usuario a lo largo del juego.
  • Las dos imágenes siguientes muestran ejemplos de diferentes estados de la máquina de estados que se está definiendo.
  • Algunos elementos clave a observar son los eventos que se espera que hagan transición a otros eventos, así como la forma en que cada estado crea y elimina su propia interfaz de usuario HTML de la página.
  •  
         
ecs.registerComponent({
  name: 'gameManager',
  stateMachine: ({ world, eid }) => {
    let startButton = null
    let levelButtons = []
    let endButton = null

    // Function to clean up the start button
    const removeStartButton = () => {}

    // Function to clean up level selection buttons
    const removeLevelButtons = () => {}

    // Function to clean up the end button
    const removeEndButton = () => {}

    ecs.defineState('startGame')
      .initial()
      .onEvent('interact', 'levelSelection', { target: world.events.globalId })
      .onEnter(() => {
        if (activeGameManagerEid !== null && activeGameManagerEid !== eid) {
          return
        }

        activeGameManagerEid = eid

        // Create the background and image
        createBackground(world)
      })
  }
})

      
         
ecs.defineState('levelSelection')
  .onEvent('levelSelected2', 'inGame', { target: world.events.globalId })
  .onEvent('showTutorial', 'tutorial', { target: world.events.globalId })
  .onEnter(() => {
    const levels = [
      { label: 'Slow', event: 'gameStartedSlow' },
      { label: 'Mid', event: 'gameStartedMid' },
      { label: 'Fast', event: 'gameStartedFast' },
    ]
  })

      
 
Seguimiento facial: 
  • El componente faceTracking para gestionar las actualizaciones de puntuación y combo basadas en movimientos de cabeza y toques de pantalla, activando eventos y actualizando la IU dinámicamente. Manejamos la rotación de las cabezas siguiendo el eje z de la cabeza en el tick. Dependiendo de la rotación, hacemos diferentes comprobaciones. También esperamos que la cabeza vuelva a una posición neutra antes de volver a realizar los controles. Así se evita que el usuario mantenga la cabeza de lado durante toda la partida.
         
tick: (world, component) => {
  const { touchTimerLeft, touchTimerRight } = component.data
  const rotation = ecs.quaternion.get(world, component.eid)

  if (component.data.touchTriggeredLeft && world.time.elapsed > touchTimerLeft) {
    component.data.touchTriggeredLeft = false
  }

  if (component.data.touchTriggeredRight && world.time.elapsed > touchTimerRight) {
    component.data.touchTriggeredRight = false
  }

  if (rotation) {
    const z = rotation.z

    // Handle right-side logic
    if (z > 0.20) {
      component.data.hitLeft = false

      if (!component.data.hitRight) {
        component.data.hitRight = true
        component.data.canHitRight = true
      }

      if (component.data.hitRight && component.data.canHitRight) {
        handleRightSide(world, component)
      }
    } else if (z < -0.20) {
      // Handle left-side logic
      component.data.hitRight = false

      if (!component.data.hitLeft) {
        component.data.hitLeft = true
        component.data.canHitLeft = true
      }

      if (component.data.hitLeft && component.data.canHitLeft) {
        handleLeftSide(world, component)
      }
    } else {
      // Reset state when head returns to neutral
      resetHeadState(component)
    }
  }
}

      
 
  • También hacemos un seguimiento del jugador que toca la pantalla con los dedos. Utilizamos eventos globales para disparar eventos dependiendo de si el usuario toca la pantalla en el lado izquierdo o derecho de la pantalla. Implementamos un temporizador para que el usuario tenga que levantar el dedo. Esto es necesario para que el usuario vuelva a una posición neutra.
         
// Dispatch global events on touch
world.events.addListener(world.events.globalId, ecs.input.SCREEN_TOUCH_START, (event) => {
  const touchX = event.data?.position?.x
  if (touchX < 0.5) {
    world.events.dispatch(eid, 'touchLeft')
  } else {
    world.events.dispatch(world.events.globalId, 'touchRight')
  }
})

// Listen for global touch events within this component
world.events.addListener(eid, 'touchLeft', () => {
  const data = dataAttribute.cursor(eid)
  data.touchTriggeredLeft = true
  data.touchTimerLeft = world.time.elapsed + 1000
})

world.events.addListener(world.events.globalId, 'touchRight', () => {
  const data = dataAttribute.cursor(eid)
  data.touchTriggeredRight = true
  data.touchTimerRight = world.time.elapsed + 1000
})

      
 
ObjectSpawner:

  • El componente objectSpawner se encarga de generar objetos sushi en intervalos de tiempo predefinidos, sincronizados con pistas de audio de velocidades variables. Aleatoriza las ubicaciones de spawn y los tipos de sushi, establece escuchas de eventos para diferentes modos de juego y gestiona la reproducción de audio para ofrecer una experiencia de juego dinámica impulsada por eventos globales
    .
  • Para spawn el sushi tenemos referencias a ambos tipos en la escena que ya tienen los componentes apropiados. Usamos el siguiente código para duplicarlos y hacer que caigan.
  • Creamos una nueva entidad, la entidad de destino, y luego copiamos todos los componentes de la fuente en ella.
         
function spawnSushi(world, objectToSpawn, objectToSpawnSuper, spawnY, spawnZ, timeStamps) {
  if (currentTimestampIndex >= timeStamps.length)
    return

  const sushiType = randomizeSushi()
  const newEid = world.createEntity()
  const spawnX = randomizeSpawnLocation()

  const clonedSuccessfully = sushiType === "regular"
    ? cloneComponents(objectToSpawn, newEid, world)
    : cloneComponents(objectToSpawnSuper, newEid, world)

  if (!clonedSuccessfully) {
    world.deleteEntity(newEid)
    return
  }
}

// Clone components from the source to the target entity
const cloneComponents = (sourceEid, targetEid, world) => {
  const componentsToClone = [
    Position, Quaternion, Scale, Shadow, BoxGeometry, Material,
    ecs.PositionAnimation, ecs.RotateAnimation, ecs.GltfModel,
    ecs.Collider, ecs.Audio, Sushi
  ]

  let clonedAnyComponent = false

  componentsToClone.forEach((component) => {
    if (component && component.has(world, sourceEid)) {
      const properties = component.get(world, sourceEid)
      component.set(world, targetEid, { ...properties })
      clonedAnyComponent = true
    }
  })

  return clonedAnyComponent
}

      
 
  • Utilizamos un sistema de marca de tiempo para saber cuándo desovar el sushi. Cada canción tiene su propia matriz
    de marcas de tiempo que se utiliza así:
         
currentTimestampIndex++

if (currentTimestampIndex < timeStamps.length) {
  const delay = (timeStamps[currentTimestampIndex] - timeStamps[currentTimestampIndex - 1]) * 1000
  setTimeout(() => spawnSushi(world, objectToSpawn, objectToSpawnSuper, spawnY, spawnZ, timeStamps), delay)
}

      
 
  • A continuación se muestra un ejemplo de sello de tiempo:
         
// Slow - One
const timeStampsSlow = [
  0.22, 1.31, 2.5, 3.8, 5.08, 6.37, 7.66, 8.95, 10.23, 11.53, 12.76, 14.1, 15.39,
  16.68, 17.97, 19.26, 20.55, 21.85, 23.14, 24.43, 25.72, 27.01, 28.3, 29.59,
  30.88, 32.16, 33.45, 34.74, 36.03, 37.32, 38.61, 39.9, 41.19, 42.49, 43.79,
  45.07, 46.36, 47.65, 48.94, 50.23, 51.52, 52.79, 54.1, 55.39, 56.68, 57.97,
  59.26, 60.55, 61.85, 63.14, 64.43, 65.72, 67.01, 68.3, 69.59, 70.88, 72.17,
  73.66, 75.01, 76.18, 77.59, 78.7, 79.93, 81.19, 82.49, 83.78, 85.07, 86.36,
  87.65, 88.94, 90.23, 91.52, 92.81, 94.1, 95.39, 96.68, 97.97, 99.26, 100.55,
  101.83, 103.14, 104.43, 105.72, 107.01, 108.3, 109.59, 110.88, 112.17, 115.52
]

      
 
ScoreArea:
● El componente scoreArea detecta colisiones con entidades sushi, actualiza la puntuación en función del tipo de sushi y gestiona el estado del sushi dentro del área. Permite la destrucción de entidades sushi cuando se recogen o eliminan, al tiempo que actualiza dinámicamente la retroalimentación visual del área de puntuación para reflejar su estado.
● Así es como manejamos la lógica de puntuación, las actualizaciones visuales y la gestión de la interacción cuando un sushi entra en el área de puntuación:
         
const handleCollisionStart = (e) => {
  if (Sushi.has(world, e.data.other)) {
    const areadata = component.schemaAttribute.get(eid)
    areadata.hasSushi = true
    areadata.sushiEntity = e.data.other

    ecs.Material.set(world, eid, { r: 225, g: 225, b: 0 })
    const rightScoreAreaData = Sushi.get(world, areadata.sushiEntity)

    if (rightScoreAreaData.type === "regular") {
      areadata.score = Math.floor(Math.random() * 3) + 1
    } else if (rightScoreAreaData.type === "super") {
      areadata.score = 10
    } else {
      areadata.score = -1
    }
  }
}

      
 
Sushi:
● El componente Sushi define entidades sushi con atributos como velocidad de movimiento, tipo y estado (en movimiento o inmóvil). Gestiona sus actualizaciones de posición, elimina automáticamente las entidades fuera de la pantalla e incluye un mecanismo de destrucción temporizada para el sushi activo, lo que garantiza una dinámica de juego eficiente.
● Comprobamos y nos aseguramos de que las entidades sushi se mueven en el mundo del juego y se limpian cuando salen del área visible.
         
if (ecs.Position.has(world, eid)) {
  const currentPosition = ecs.Position.get(world, eid)

  if (currentPosition) {
    currentPosition.y -= sushiData.speed

    if (currentPosition.y < -5.0) {
      // console.log(`Sushi (${eid}) went off-screen, deleting entity.`)
      world.deleteEntity(eid)
    } else {
      ecs.Position.set(world, eid, currentPosition)
    }
  }
}

      
 
UIRewardController:
● El componente UIRewardController gestiona el sistema de visualización de recompensas, animando y
mostrando recompensas como S, SS y SSS con los GIF y puntos correspondientes.
actualiza dinámicamente la interfaz de usuario, gestiona las adiciones de recompensas basadas en eventos y activa la secuencia de recompensas de
, lo que garantiza una atractiva experiencia tras el juego.

Recompensas:
● El componente Recompensas gestiona el sistema de recompensas, mostrando ventanas emergentes para eventos como
Combo3 y activando recompensas para Combo10, incluido un Mega Bonus Gacha con
recompensas aleatorias (S, SS o SSS). Realiza un seguimiento de los datos de recompensa, escucha eventos globales y
proporciona información atractiva a los jugadores a través de recompensas visuales y basadas en eventos.

SushiKiller:
● El componente sushiKiller detecta colisiones con entidades sushi y envía un evento global
comboReset en caso de colisión. Gestiona los datos de estado para rastrear la presencia de sushi y el estado de recogida de
, garantizando que la mecánica del juego se actualiza en función de las interacciones.

Implementation

GameStates (State Machine):
El juego está gestionado por distintos estados de juego, cada uno responsable de controlar partes específicas de
la experiencia de juego. Cada estado gestiona sus propios elementos de interfaz de usuario y envía eventos a
para otros componentes según sea necesario.

Screenshot 2025-05-09 at 1.37.36 PM

❖ Iniciar Juego
Transiciones a: Selección de Nivel
Descripción: La pantalla inicial del juego donde los jugadores comienzan su viaje.
Elementos UI: Un fondo y un botón para pasar a la pantalla de Selección de Nivel
.

❖ Selección de Nivel
Transiciones a: Tutorial, Juego principal
Descripción: Los jugadores pueden elegir entre tres canciones, cada una asociada a un
nivel de dificultad específico.
Elementos de la IU: Un fondo, tres botones que representan las canciones disponibles y
un único botón para pasar al estado Tutorial.

❖ Tutorial
Transiciones a: Selección de Nivel

Descripción: Este estado enseña a los jugadores a interactuar con el juego a través de
dos mecánicas:
■ Mover la cabeza de izquierda a derecha para capturar el sushi que cae en un plato.
■ Tocar la pantalla cuando el sushi que cae toca el plato.
GIF animados demuestran estas acciones, y un botón permite a los jugadores
volver a la pantalla de Selección de Nivel.

➢ Elementos UI: GIF animados que muestran la mecánica del juego y un botón para
volver a la pantalla de selección de nivel.
➢ Juego principal
Transiciones a: Pantalla de recompensas
Descripción: La sección principal del juego donde los jugadores interactúan con el juego
:
● Los jugadores se ven a sí mismos en la pantalla mientras el sushi cae al ritmo de
la música.
● El objetivo es recoger sushi para aumentar su puntuación y su contador de combos
.
● Los combos más altos otorgan mejores recompensas en el sistema Gacha.
Elementos de la IU: Información del jugador en directo, imágenes de sushi cayendo, contador de puntuaciones y
contador de combos.
➢ Pantalla de recompensas
Transiciones a: Pantalla de fin de juego
Descripción: Después de completar una canción, los jugadores reciben recompensas basadas en su
rendimiento:
● La pantalla muestra animaciones de las Gachas que han ganado.
● Los jugadores ven su puntuación final y cualquier aumento de puntuación de la
sesión.
● Una vez que las animaciones terminan, el juego transiciona a la Pantalla de Fin de Juego
.

Elementos UI: Animaciones de recompensa, resumen de puntuación y activador de transición a
la pantalla de fin de juego.

➢ Pantalla de fin de juego
Transiciones a: Selección de Nivel
Descripción: La pantalla final de la sesión de juego, en la que se da las gracias a los jugadores por
jugar.
● Se pide a los jugadores que empiecen otra ronda volviendo a la pantalla
Selección de nivel.

Elementos de IU: Un mensaje de agradecimiento y un botón para volver a la pantalla de Selección de Nivel
.

Your cool escaped html goes here.