Sushi Beats Drop

Bougez votre tête pour manger les sushis qui tombent dans ce jeu rythmique ! Gagnez des points, réalisez des combos et attrapez des sushis spéciaux pour obtenir des récompenses dans une expérience amusante et captivante !

sushibeatsdropcover

Personnalisez-le grâce au projet exemple

headtouchinput

Head Touch Input checkmark bullet

This project demonstrates an input system for a game that utilizes head tilt controls and touchscreen controls to trigger events.

View sample project
objectcloneanddetection

Object Cloning and Detection checkmark bullet

Cloning existing scene objects multiple times and making them fall. Using colliders and events to detect when sushi touches a plate.

View sample project

Behind the Build: Sushi Beat Drop

Written by Saul Pena Gamero & Mackenzie Li

May 8, 2025


Summary

Au cours des deux derniers mois, notre équipe a développé un jeu musical qui combine un gameplay rythmique traditionnel avec des éléments innovants de réalité augmentée (RA). Cette fusion entre les mécanismes classiques et la technologie RA crée une expérience unique et captivante qui distingue notre jeu. Notre objectif principal était de concevoir un jeu à la fois amusant et immersif, qui encourage les joueurs à relever des défis rythmiques d'une manière totalement nouvelle. 
Ce projet s'appuie sur le travail que nous avons commencé lors du dernier concours Niantic, où nous avons exploré des applications créatives des filtres faciaux en réalité augmentée. Nous nous sommes inspirés d'un exemple de projet présenté sur Niantic Studio, qui mettait en avant les mécanismes de mouvement de la tête en réalité augmentée. Nous avons été fascinés par le potentiel interactif et divertissant de ce concept, et nous avons décidé d'en faire un élément central de notre jeu. 

Afin de déterminer le meilleur style de gameplay pour ce mécanisme, nous avons réfléchi à différentes idées et nous nous sommes inspirés de jeux rythmiques tels que Taiko no Tatsujin et Osu. En tant que fans du genre, nous avons imaginé un jeu qui intégrerait non seulement le timing et le rythme, mais aussi l'interaction physique à travers les mouvements de la tête. En fusionnant ces éléments, nous avons créé un jeu qui se distingue par son gameplay unique et son engagement physique, offrant aux joueurs une expérience de jeu rythmique sans pareille.

Gameplay

 
générales Notre jeu rythmique repose sur un concept simple mais captivant : des sushis tombent du haut de l'écran en synchronisation avec le rythme de la musique. Le but du joueur est d'attraper les sushis au bon moment lorsqu'ils atterrissent sur les assiettes en dessous.  
Deux assiettes sont placées à gauche et à droite de l'écran, dans la partie inférieure, et des morceaux de sushi tombent vers chaque assiette au rythme de la musique. Lorsque le sushi s'aligne avec une assiette, le joueur doit agir pour « l'attraper » et gagner des points s'il le fait au bon moment. 

Commandes et actions de l'utilisateur 
Les joueurs peuvent interagir avec le jeu à l'aide de deux méthodes de commande différentes, conçues pour offrir plus de flexibilité et améliorer l'expérience globale. La méthode de contrôle principale utilise l'inclinaison de la tête pour une expérience immersive et mains libres. Si le sushi tombe vers l'assiette de gauche, le joueur penche la tête vers la gauche pour l'attraper ; il en va de même pour l'assiette de droite avec une inclinaison vers la droite. Ce style de contrôle exploite les mécanismes de la réalité augmentée pour ajouter une dimension physique et ludique. 
Pour les joueurs qui pourraient trouver les mouvements de tête difficiles ou inconfortables lors de longues sessions, nous avons également mis en place une option « tap-to-play » (jouer en touchant l'écran). Dans ce mode, les joueurs peuvent appuyer sur le côté gauche ou droit de l'écran pour attraper les sushis sur l'assiette correspondante. Cette alternative garantit que le jeu reste agréable et accessible, répondant ainsi à différentes préférences et niveaux de confort. 

En combinant un gameplay basé sur le rythme avec des commandes intuitives et personnalisables, le jeu offre une expérience amusante et interactive qui séduit un large public.

 

Project Structure

de la scène 3D
Notre scène de jeu comprend plusieurs composants essentiels tels que des scripts, des ressources 3D, le suivi du visage, la caméra et l'éclairage. Si certains d'entre eux, comme la caméra et l'éclairage, sont relativement intuitifs, nous nous concentrerons dans cette section sur les ressources 3D et le suivi du visage. 

Le suivi du visage joue un rôle essentiel dans la détection de la position et des mouvements de la tête du joueur. Cela permet au jeu de déterminer la direction et le moment où la tête est inclinée, ce qui permet une interaction précise avec les mécanismes du jeu. 
Les ressources 3D comprennent les deux assiettes et les morceaux de sushi, qui sont tous deux essentiels à l'expérience du jeu rythmique. Les plaques sont des objets statiques, chacune équipée d'un collisionneur physique et d'un composant rigide statique. Un script, ScoreArea, est associé à chaque assiette afin de gérer le mécanisme de comptage lorsque les sushis y sont déposés. 

Les sushis, quant à eux, sont des objets dynamiques. Chaque sushi est doté d'un collisionneur physique et d'un corps rigide dynamique, ce qui lui permet d'interagir naturellement avec l'environnement lorsqu'il tombe. De plus, chaque morceau de sushi est associé à un script Sushi qui régit son comportement et son intégration dans le gameplay. 

Les fonctionnalités spécifiques des scripts seront abordées en détail dans une section ultérieure, mais ces composants créent ensemble l'expérience interactive et immersive de notre jeu rythmique. 

Ressources
Toutes les ressources de notre jeu sont entièrement dessinées à la main ou créées sur mesure, ce qui témoigne de l'effort artistique et de l'attention portée aux détails dans le cadre de ce projet. Les modèles 3D ont été créés à l'aide de Blender, ajoutant une touche personnelle et unique au jeu. 
Les ressources comprennent divers éléments visuels et interactifs, tels que la page de couverture, un GIF explicatif pour guider les joueurs et des animations pour le système gacha. De plus, nous avons créé une page de félicitations pour célébrer les exploits des joueurs, ainsi que deux adorables modèles 3D de chats sushis qui constituent des éléments clés du gameplay.
Scripts
d'

GameManager : 

  • Le GameManager gère les transitions d'état du jeu, notamment le démarrage, la sélection du niveau, le tutoriel, le jeu, les récompenses et l'écran de fin. Il gère dynamiquement les éléments de l'interface utilisateur, les arrière-plans et les écouteurs d'événements pour chaque état, garantissant ainsi des transitions fluides et des interactions utilisateur tout au long du jeu.
  • Les deux images suivantes montrent des exemples d'états différents de la machine à états en cours de définition.
  • Certains éléments clés à noter sont les événements qui sont susceptibles de transitionner vers d'autres événements, ainsi que la manière dont chaque état crée et supprime sa propre interface utilisateur HTML de la page. 
         
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' },
    ]
  })

      
 
FaceTracking : 
  • Le composant faceTracking permet de gérer les mises à jour des scores et des combos en fonction des mouvements de la tête et des touches sur l'écran, en déclenchant des événements et en mettant à jour l'interface utilisateur de manière dynamique. Nous gérons la rotation des têtes en suivant l'axe z de la tête dans le tick. En fonction de la rotation, nous effectuons différents contrôles. Nous attendons également que la tête revienne en position neutre avant de refaire les vérifications. Cela empêche un utilisateur de simplement garder la tête sur le côté pendant toute la partie.
         
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)
    }
  }
}

      
 
  • Nous suivons également les joueurs qui touchent l'écran avec leurs doigts. Nous utilisons des événements globaux pour déclencher des événements selon que l'utilisateur touche l'écran à gauche ou à droite. Nous mettons en place un minuteur afin que l'utilisateur doive lever le doigt. Ceci est nécessaire pour que l'utilisateur puisse revenir à une position neutre.
         
// 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 :

  • Le composant objectSpawner gère la génération d'objets sushi à des moments prédéfinis, synchronisés avec des pistes audio de différentes vitesses. Il randomise les emplacements d'apparition et les types de sushis, configure des écouteurs d'événements pour différents modes de jeu et gère la lecture audio afin d'offrir une expérience de jeu dynamique guidée par des événements d'
    globale.
  • Afin de faire apparaître les sushis, nous avons des références aux deux types dans la scène qui disposent déjà des composants appropriés. Nous utilisons le code suivant pour les dupliquer, puis les faire tomber.
  • Nous créons une nouvelle entité, l'entité cible, puis nous y copions tous les composants de la source.
         
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
}

      
 
  • Nous utilisons un système d'horodatage pour savoir quand faire apparaître les sushis. Chaque chanson possède son propre tableau d'
    s de timestamps qui s'utilise comme suit :
         
currentTimestampIndex++

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

      
 
  • Exemple d'horodatage ci-dessous :
         
// 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 :
● Le composant scoreArea détecte les collisions avec les entités sushi, met à jour le score en fonction du type de sushi et gère l'état des sushis dans la zone. Cela permet de détruire les sushis lorsqu'ils sont collectés ou retirés, tout en mettant à jour dynamiquement le retour visuel de la zone de score afin de refléter son état.
● Voici comment nous gérons la logique de score, les mises à jour visuelles et la gestion des interactions lorsqu'un sushi entre dans la zone de score :
         
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 :
● Le composant Sushi définit les entités sushi avec des attributs tels que la vitesse de déplacement, le type et l'état (en mouvement ou immobile). Il gère les mises à jour de leur position, supprime automatiquement les entités hors écran et inclut un mécanisme de destruction temporisée pour les sushis actifs, garantissant ainsi une dynamique de jeu efficace.
● Nous vérifions et nous assurons que les entités sushi se déplacent dans le monde du jeu et sont nettoyées lorsqu'elles quittent la zone 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 :
● Le composant UIRewardController gère le système d'affichage des récompenses, en animant et en mettant en
les récompenses telles que S, SS et SSS avec les GIF et les points correspondants. Il
met à jour dynamiquement l'interface utilisateur, gère les ajouts de récompenses basés sur des événements et déclenche la séquence de récompenses d'
, garantissant ainsi une expérience post-jeu captivante.

Récompenses :
● Le composant Récompenses gère le système de récompenses, affichant des fenêtres contextuelles pour des événements tels que
Combo3 et déclenchant des récompenses pour Combo10, y compris un Mega Bonus Gacha avec
des récompenses aléatoires (S, SS ou SSS). Il suit les données relatives aux récompenses, détecte les événements globaux et
fournit des commentaires intéressants aux joueurs grâce à des récompenses visuelles et basées sur les événements.

SushiKiller :
● Le composant sushiKiller détecte les collisions avec les entités sushi et déclenche un événement global
comboReset en cas de collision. Il gère les données d'état permettant de suivre la présence des sushis et le statut de collecte de l'
, garantissant ainsi que les mécanismes du jeu sont mis à jour en fonction des interactions.

Implementation

GameStates (machine à états) :
Le jeu est géré par différents états, chacun étant responsable du contrôle de parties spécifiques de l'expérience de jeu
. Chaque état gère ses propres éléments d'interface utilisateur et envoie des événements à d'autres composants à l'
, selon les besoins.

Screenshot 2025-05-09 at 1.37.36 PM

❖ Démarrer le jeu
Transitions vers : Sélection du niveau
Description : Écran initial du jeu où les joueurs commencent leur aventure.
Éléments de l'interface utilisateur : Un arrière-plan et un bouton pour passer à l'écran de sélection du niveau
.

❖ Sélection du niveau
Transitions vers : Tutoriel, gameplay principal
Description : Les joueurs peuvent choisir parmi trois chansons, chacune associée à un niveau de difficulté spécifique
.
Éléments de l'interface utilisateur : Un arrière-plan, trois boutons représentant les chansons disponibles et
un seul bouton pour passer à l'état Tutoriel.

❖ Tutoriel
Transitions vers : Sélection du niveau

Description :

Cet état apprend aux joueurs comment interagir avec le jeu à travers deux mécanismes :

■ Déplacer la tête de gauche à droite pour attraper les sushis qui tombent sur une assiette.
■ Appuyer sur l'écran lorsque les sushis tombent sur l'assiette.
Des GIF animés illustrent ces actions, et un bouton permet aux joueurs de revenir à l'écran de sélection des niveaux.
➢ Éléments de l'interface utilisateur : GIF animés montrant les mécanismes du jeu et un bouton pour revenir à la sélection des niveaux.

➢ Gameplay principal
Transitions vers : Écran de récompense
Description : Section principale du gameplay où les joueurs interagissent avec le jeu :

● Les joueurs se voient à l'écran sous la forme de sushis tombant au rythme de la musique.

● L'objectif est de collecter des sushis pour augmenter son score et son compteur de combos.

● Les combos plus élevés permettent d'obtenir de meilleures récompenses dans le système Gacha.
Éléments de l'interface utilisateur : Flux des joueurs en direct, visuels de sushis qui tombent, compteur de score et
compteur de combos.
➢ Écran de récompenses
Transitions vers : Écran de fin de partie
Description : Une fois une chanson terminée, les joueurs reçoivent des récompenses en fonction de leurs performances
:
● L'écran affiche des animations des Gachas qu'ils ont gagnés.
● Les joueurs voient leur score final et toute augmentation de score depuis la session
.
● Une fois les animations terminées, le jeu passe à l'écran de fin de partie
.

Éléments de l'interface utilisateur : Animations de récompense, résumé du score et déclenchement de la transition vers l'écran « Fin de partie »
.

➢ Écran « Fin de partie »
Transitions vers : Sélection du niveau
Description : dernier écran de la session de jeu, remerciant les joueurs d'avoir joué
.
● Les joueurs sont invités à recommencer une partie en retournant à l'écran « Sélection du niveau »
.

Éléments de l'interface utilisateur : message de remerciement et bouton permettant de retourner à l'écran « Sélection du niveau »
.

Your cool escaped html goes here.