Sushi schlägt Drop
Bewegen Sie Ihren Kopf, um fallendes Sushi in diesem Rhythmusspiel zu essen! Sammeln Sie Punkte, bilden Sie Combos und fangen Sie spezielles Sushi, um Belohnungen zu erhalten – ein unterhaltsames und fesselndes Erlebnis!

Gestalten Sie es mit dem Beispielprojekt ganz nach Ihren Wünschen.

Head Touch Input
This project demonstrates an input system for a game that utilizes head tilt controls and touchscreen controls to trigger events.
View sample project
Object Cloning and Detection
Cloning existing scene objects multiple times and making them fall. Using colliders and events to detect when sushi touches a plate.
View sample projectBehind the Build: Sushi Beat Drop

Written by Saul Pena Gamero & Mackenzie Li
May 8, 2025
Summary
In den letzten zwei Monaten hat unser Team ein Rhythmusspiel entwickelt, das traditionelles Rhythmus-Gameplay mit innovativen Augmented-Reality-Elementen (AR) kombiniert. Diese Fusion aus klassischer Mechanik und AR-Technologie schafft ein einzigartiges und fesselndes Erlebnis, das unser Spiel von anderen abhebt. Unser primäres Ziel war es, ein Spiel zu entwickeln, das sowohl Spaß macht als auch fesselnd ist und die Spieler dazu animiert, rhythmische Herausforderungen auf eine völlig neue Art und Weise zu genießen.
Dieses Projekt baut auf der Arbeit auf, die wir während des letzten Niantic-Wettbewerbs begonnen haben, bei dem wir kreative Anwendungen für AR-Gesichtsfilter erforscht haben. Unsere Inspiration kam von einem Beispielprojekt auf Niantic Studio, das die Mechanik von Kopfbewegungen in AR demonstrierte. Wir waren fasziniert davon, wie interaktiv und unterhaltsam dieses Konzept sein könnte, und beschlossen, es zu einem zentralen Spielelement unseres Spiels zu machen.
Um den besten Spielstil für dieses Spielelement zu finden, haben wir Ideen gesammelt und uns von Rhythmusspielen wie Taiko no Tatsujin und Osu inspirieren lassen. Als Fans des Genres hatten wir uns ein Spiel vorgestellt, das nicht nur Timing und Rhythmus, sondern auch physische Interaktion durch Kopfbewegungen beinhaltet. Durch die Verschmelzung dieser Elemente haben wir ein Spiel geschaffen, das sich durch sein einzigartiges Gameplay und seine physische Interaktion auszeichnet und den Spielern ein Rhythmusspielerlebnis wie kein anderes bietet.
Gameplay
Übersicht
Unser Rhythmusspiel basiert auf einem einfachen, aber fesselnden Konzept: Sushi fällt synchron zum Rhythmus der Musik von oben auf den Bildschirm. Das Ziel des Spielers ist es, das Sushi im richtigen Moment zu fangen, wenn es auf den Tellern darunter landet. Auf der linken und rechten Seite des Bildschirms befinden sich zwei Teller (
), auf die Sushi-Stücke im Rhythmus der Musik fallen. Wenn das Sushi auf einem Teller landet, muss der Spieler reagieren, um es zu "fangen", und erhält für präzises Timing Punkte.
Steuerung und Benutzereingaben
Die Spieler können mit dem Spiel über zwei verschiedene Steuerungsmethoden interagieren, die für Flexibilität sorgen und das Spielerlebnis insgesamt verbessern. Die primäre Steuerungsmethode nutzt Kopfbewegungen für ein freihändiges, immersives Spielerlebnis. Wenn Sushi auf den linken Teller fällt, neigt der Spieler seinen Kopf nach links, um es zu fangen; dasselbe gilt für den rechten Teller mit einer Neigung nach rechts. Dieser Steuerungsstil nutzt Augmented-Reality-Mechanismen, um eine zusätzliche Ebene der physischen Interaktion und des Spielspaßes zu schaffen.
Für Spieler, denen das Neigen des Kopfes über längere Zeiträume hinweg zu anstrengend oder unangenehm ist, haben wir außerdem eine Tap-to-Play-Option implementiert. In diesem Modus können Spieler auf die linke oder rechte Seite des Bildschirms tippen, um Sushi auf den entsprechenden Teller zu fangen. Diese Alternative sorgt dafür, dass das Spiel unterhaltsam und zugänglich bleibt und unterschiedliche Vorlieben und Komfortniveaus berücksichtigt.
Durch die Kombination von rhythmischem Gameplay mit intuitiven und anpassbaren Steuerelementen bietet das Spiel ein unterhaltsames und interaktives Erlebnis, das ein breites Publikum anspricht.
Project Structure
3D-Szenen-
Unsere Spielszene umfasst mehrere wesentliche Komponenten wie Skripte, 3D-Assets, den Gesichts-Tracker, die Kamera und die Beleuchtung. Während einige davon, wie die Kamera und die Beleuchtung, relativ intuitiv sind, konzentrieren wir uns in diesem Abschnitt auf die 3D-Assets und den Face Tracker.
Der Face Tracker spielt eine entscheidende Rolle bei der Erkennung der Kopfposition und der Bewegungen des Spielers. Dadurch kann das Spiel die Richtung und den Zeitpunkt von Kopfneigungen erkennen und so eine präzise Interaktion mit der Spielmechanik ermöglichen.
Die 3D-Assets umfassen die beiden Teller und die Sushi-Stücke, die beide für das Rhythmusspiel unerlässlich sind. Die Platten sind statische Objekte, die jeweils mit einem Physik-Kollisionskörper und einer statischen Rigidbody-Komponente ausgestattet sind. An jeder Platte ist ein Skript namens "ScoreArea" angehängt, das die Punktevergabe regelt, wenn Sushi darauf landet.
Die Sushi-Stücke hingegen sind dynamische Objekte. Jedes Sushi verfügt über einen Physik-Kollisionskörper und einen dynamischen starren Körper, sodass es beim Fallen auf natürliche Weise mit der Umgebung interagieren kann. Darüber hinaus ist jedes Sushi-Stück mit einem Sushi-Skript verknüpft, das sein Verhalten und seine Integration in das Gameplay steuert.
Die spezifischen Funktionen der Skripte werden in einem späteren Abschnitt ausführlich erläutert, aber diese Komponenten zusammen schaffen das interaktive und immersive Erlebnis unseres Rhythmusspiels.
Assets
Alle Assets in unserem Spiel sind vollständig handgezeichnet oder individuell erstellt und zeugen von der künstlerischen Arbeit und Liebe zum Detail, die in das Projekt gesteckt wurde. Die 3D-Modelle wurden mit Blender erstellt und verleihen dem Spiel eine persönliche und einzigartige Note.Die Assets umfassen eine Vielzahl visueller und interaktiver Elemente, darunter das Titelbild, ein GIF mit Anweisungen für die Spieler und Animationen für das Gacha-System. Darüber hinaus haben wir eine Glückwunschseite zur Feier der Erfolge der Spieler sowie zwei niedliche 3D-Modelle von Katzensushi erstellt, die als wichtige Spielelemente dienen.
Skripte
GameManager:- Der GameManager verwaltet die Zustandsübergänge des Spiels, darunter Start, Levelauswahl, Tutorial, Spielverlauf, Belohnungen und Endbildschirm. Es verwaltet dynamisch UI-Elemente, Hintergründe und Ereignis-Listener für jeden Zustand und sorgt so für reibungslose Übergänge und Benutzerinteraktionen während des gesamten Spiels.
- Die nächsten beiden Bilder zeigen Beispiele für verschiedene Zustände aus der definierten Zustandsmaschine.
- Einige wichtige Elemente, die zu beachten sind, sind die Ereignisse, die zu anderen Ereignissen überleiten sollen, sowie die Art und Weise, wie jeder Zustand seine eigene HTML-Benutzeroberfläche auf der Seite erstellt und entfernt.
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:
- Die FaceTracking-Komponente verwaltet Punktestände und Combo-Updates basierend auf Kopfbewegungen und Bildschirmberührungen, löst Ereignisse aus und aktualisiert die Benutzeroberfläche dynamisch. Wir steuern die Drehung der Köpfe, indem wir die Z-Achse des Kopfes im Tick verfolgen. Je nach Rotation führen wir unterschiedliche Kontrollen durch. Wir erwarten außerdem, dass der Kopf wieder in eine neutrale Position zurückkehrt, bevor die Überprüfungen erneut durchgeführt werden. Dadurch wird verhindert, dass ein Benutzer während des gesamten Spiels einfach nur den Kopf zur Seite hält.
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)
}
}
}
- Wir verfolgen auch, welcher Spieler den Bildschirm mit seinen Fingern berührt. Wir verwenden globale Ereignisse, um Ereignisse auszulösen, je nachdem, ob der Benutzer den Bildschirm auf der linken oder rechten Seite berührt. Wir implementieren einen Timer, damit der Benutzer seinen Finger anheben muss. Dies ist erforderlich, damit der Benutzer in eine neutrale Position zurückkehren kann.
// 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:
- Die Komponente "objectSpawner" übernimmt das Erzeugen von Sushi-Objekten zu vordefinierten Zeitpunkten, synchronisiert mit Audiospuren unterschiedlicher Geschwindigkeit. Es randomisiert Spawn-Orte und Sushi-Arten, richtet Ereignis-Listener für verschiedene Spielmodi ein und verwaltet die Audiowiedergabe, um ein dynamisches Spielerlebnis zu bieten, das von globalen "
"-Ereignissen gesteuert wird. - Um das Sushi zu spawnen, haben wir Referenzen zu beiden Arten in der Szene, die bereits über die entsprechenden Komponenten verfügen. Wir verwenden den folgenden Code, um sie zu duplizieren und dann nach unten fallen zu lassen.
- Wir erstellen eine neue Entität, die Zielentität, und kopieren dann alle Komponenten aus der Quelle in diese.
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
}
- Wir verwenden ein Zeitstempelsystem, um zu wissen, wann das Sushi serviert werden muss. Jeder Song verfügt über ein eigenes Array "
" mit Zeitstempeln, das wie folgt verwendet wird:
currentTimestampIndex++
if (currentTimestampIndex < timeStamps.length) {
const delay = (timeStamps[currentTimestampIndex] - timeStamps[currentTimestampIndex - 1]) * 1000
setTimeout(() => spawnSushi(world, objectToSpawn, objectToSpawnSuper, spawnY, spawnZ, timeStamps), delay)
}
- Ein Beispiel für einen Zeitstempel finden Sie unten:
// 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:
● Die Komponente "scoreArea" erkennt Kollisionen mit Sushi-Entitäten, aktualisiert die Punktzahl basierend auf der Sushi-Art und verwaltet den Status des Sushis innerhalb des Bereichs. Es ermöglicht die Zerstörung von Sushi-Entitäten, wenn diese gesammelt oder entfernt werden, während die visuelle Rückmeldung im Punktestandbereich dynamisch aktualisiert wird, um den Status widerzuspiegeln.
● So handhaben wir die Punktestandslogik, visuelle Aktualisierungen und Interaktionsverwaltung, wenn ein Sushi in den Punktestandbereich gelangt:
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:
● Die Sushi-Komponente definiert Sushi-Entitäten mit Attributen wie Bewegungsgeschwindigkeit, Typ und Status (in Bewegung oder stationär). Es verwaltet ihre Positionsaktualisierungen, entfernt automatisch Entitäten außerhalb des Bildschirms und enthält einen zeitgesteuerten Zerstörungsmechanismus für aktives Sushi, wodurch eine effiziente Spieledynamik gewährleistet wird.
● Wir überprüfen und stellen sicher, dass Sushi-Entitäten sich in der Spielwelt bewegen und entfernt werden, wenn sie den sichtbaren Bereich verlassen.
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:
● Die Komponente "UIRewardController" verwaltet das Belohnungsanzeigesystem, animiert und präsentiert Belohnungen wie S, SS und SSS mit entsprechenden GIFs und Punkten. Die "
" aktualisiert dynamisch die Benutzeroberfläche, verarbeitet ereignisbasierte Belohnungszusätze und löst die Belohnungssequenz "
" aus, wodurch ein fesselndes Spielerlebnis nach dem Spiel gewährleistet wird.
Belohnungen:
● Die "Rewards"-Komponente verwaltet das Belohnungssystem, zeigt Popups für Ereignisse wie "
" und "Combo3" an und löst Belohnungen für "Combo10" aus, darunter ein "Mega Bonus Gacha" mit zufälligen Belohnungen (S, SS oder SSS) aus "
". Es verfolgt Belohnungsdaten, überwacht globale Ereignisse und
liefert den Spielern durch visuelle und ereignisgesteuerte Belohnungen ansprechendes Feedback.
SushiKiller:
● Die SushiKiller-Komponente erkennt Kollisionen mit Sushi-Entitäten und löst bei einer Kollision ein globales
comboReset-Ereignis aus. Es verwaltet Statusdaten zur Verfolgung der Verfügbarkeit von Sushi und des Sammelstatus von "
" und stellt sicher, dass die Spielmechanik basierend auf den Interaktionen aktualisiert wird.
Implementation
GameStates (Zustandsmaschine):
Das Spiel wird durch verschiedene Spielzustände verwaltet, die jeweils für die Steuerung bestimmter Teile des Spielablaufs verantwortlich sind.
Jeder Status verwaltet seine eigenen UI-Elemente und sendet bei Bedarf Ereignisse an "
", um andere Komponenten zu steuern.

❖ Startspiel-
Übergänge zu: Levelauswahl
Beschreibung: Der Startbildschirm des Spiels, auf dem die Spieler ihre Reise beginnen.
UI-Elemente: Ein Hintergrund und eine Schaltfläche zum Wechseln zum Levelauswahlbildschirm "
".
❖ Levelauswahl "
" Wechselt zu: Tutorial, Hauptspiel "
" Beschreibung: Die Spieler können aus drei Songs auswählen, die jeweils einem bestimmten Schwierigkeitsgrad in "
" zugeordnet sind.
UI-Elemente: Ein Hintergrund, drei Schaltflächen für die verfügbaren Songs und
eine einzelne Schaltfläche zum Wechseln zum Tutorial.
❖ Tutorial "
" Wechselt zu: Levelauswahl "
" Beschreibung:
In diesem Status lernen die Spieler anhand von zwei Mechaniken, wie sie mit dem Spiel interagieren können:
■ Bewegen des Kopfes von links nach rechts, um fallendes Sushi auf einem Teller zu fangen.
■ Antippen des Bildschirms, wenn fallendes Sushi den Teller berührt.
Animierte GIFs veranschaulichen diese Aktionen, und über eine Schaltfläche können die Spieler
zum Levelauswahlbildschirm zurückkehren.
➢ UI-Elemente: Animierte GIFs zeigen die Spielmechanik und eine Schaltfläche zum
Zurückkehren zur Levelauswahl.
➢ Hauptspiel
Übergänge zu: Belohnungsbildschirm
Beschreibung: Der Kernbereich des Spiels, in dem die Spieler mit dem
Spiel interagieren:
● Die Spieler sehen sich selbst auf dem Bildschirm, während Sushi im Rhythmus der
Musik fällt.
● Das Ziel ist es, Sushi zu sammeln, um die Punktzahl und den Combo-Zähler
zu erhöhen.
● Höhere Combos bringen bessere Belohnungen im Gacha-System.
UI-Elemente: Live-Spieler-Feed, fallende Sushi-Grafiken, Punktestand und
Combo-Zähler.
➢ Belohnungsbildschirm
Übergänge zu: Endbildschirm
Beschreibung: Nach Abschluss eines Songs erhalten die Spieler Belohnungen basierend auf ihrer
Leistung:
● Der Bildschirm zeigt Animationen der Gachas, die sie verdient haben.
● Die Spieler sehen ihren Endpunktestand und alle Punktesteigerungen aus der
Sitzung.
● Sobald die Animationen beendet sind, wechselt das Spiel zum Endbildschirm
.
UI-Elemente:
Belohnungsanimationen, Punktestandübersicht und Übergang zum Bildschirm "
" (Spiel beenden).
➢ Bildschirm "End Game" (Spiel beenden)
Übergänge zu: Levelauswahl
Beschreibung: Der letzte Bildschirm der Spielsitzung, auf dem sich das Spiel bei den Spielern für ihre Teilnahme bedankt.
● Die Spieler werden aufgefordert, eine neue Runde zu starten, indem sie zum Bildschirm "
" (Levelauswahl) zurückkehren.
UI-Elemente: Eine Dankesmeldung und eine Schaltfläche zum Zurückkehren zum Bildschirm "Level
" (Levelauswahl).