Abenteuer Gefrorene Münzjagd
Begib dich mit Kapitän Doty auf ein neues Abenteuer in der Gefrorenen Welt. Sammle goldene Münzen und entkomme den Eispilzen!

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

Studio: Münzstreuer
Dieses Projekt zeigt dir, wie du mit der Komponente von Niantic Studio Münzen nach dem Zufallsprinzip in einem bestimmten Radius verteilen kannst.
Beispielprojekt ansehen
Studio: Doty Snowball Party
Dieses Projekt zeigt dir, wie du ein Minispiel mit der Niantic Studio Physics Engine erstellst.
Beispielprojekt ansehenBehind the Build: Frozen Coin Hunt Adventure

Written by Alex di Guida
May 16, 2025
Introduction
Dieses Erlebnis ist ein WebAR-Spiel, das mit Niantic Studio Beta, Oktober 2024, erstellt wurde.
Die Hauptfigur ist der berühmte Captain Doty. Er muss sich drei verschiedenen Kapiteln stellen:
The Tiny Mushroom
- Sammle 20 Goldmünzen, während du dem kleinen Pilz ausweichst, der dich verfolgt.
The Angry Mushroom
- Collect 20 gold coins while avoiding the angry mushroom trying to catch you. Die Münzen sind alle versteckt. Bewege dich mit der magischen Schneeflocke, die in der Nähe schwebt, um die Münzen freizulegen.
Der Königspilz
-In tseine letzte Herausforderung, beginne eine Schneeballschlacht mit dem Königspilz, um zu gewinnen. Sammle Schneebälle vom Boden und wirf sie auf den König, um ihn zu besiegen.
Du hast drei Herzen, die deine Leben darstellen.
Project Structure
3D-Szene
- Basis-Elemente: Enthält die perspektivische AR-Kamera und die Umgebungs-/Richtungslichter.
- UI-Entitäten: Umfasst alle Elemente der Benutzeroberfläche, die auf dem Bildschirm angezeigt werden und dem Spieler Informationen und Feedback liefern. Zwischen den einzelnen Aufgaben wird ein Tutorial gezeigt.
Assets
- Enthält alle 3D-Modelle, Audiodateien und Bilder, die im Spiel verwendet werden.
Scripts
Der Ordner scripts ist in drei verschachtelte Ordner unterteilt:
- Komponenten:
Enthält die Hauptlogik des Spiels. Dazu gehören die Bildschirme der Benutzeroberfläche, die Spielerbewegung, die Logik für das Ablegen von Münzen auf dem gefrorenen Boden, das Verhalten von Pilzen und die Folgelogik sowie der Spielmanager, der Ereignisse während der Spielsitzung verarbeitet.
- Helfer:
Enthält verschiedene Hilfsfunktionen, die in den Komponenten verwendet werden und die Spiellogik vereinfachen und organisieren.
- Klassen:
Enthält das Shader-Material, das für die Verwaltung des Partikelsystems verwendet wird, mit dem der Schnee auf dem Spielplatz gerendert wird.
Implementation
Ein detaillierter Fokus auf die wichtigsten Skripte des Spiels, die die gesamte Kernlogik dieser Erfahrung verwalten.
Jede Komponente kann verschiedene Hörer für verschiedene Zwecke haben:
- game-start: Wird ausgelöst, wenn das Spiel beginnt oder nach einer Pause fortgesetzt wird.
- reset: Behandelt den Reset-Zustand der Komponente beim Übergang zur nächsten Herausforderung.
- pause: Hält Animationen und Überprüfungen an, z.
Münze
coin.ts
Dies ist die Münzkomponente, die die Erstellung einer neuen Münze in der Szene übernimmt
ecs.GltfModel.set(world, eid, {url: coinAsset})
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, (result) => {
// @ts-ignore
const {data: {model}} = result
storeCoinMaterial(model, eid)
applyRandomPositionToCoin(world, eid)
})
- Legen Sie das Münzmodell mithilfe des coinAssets fest, das die URL des Münzmodells enthält.
- Sobald das Modell geladen ist, speichern Sie das Münzmaterial für die spätere Verwendung, um seine Deckkraft zu verwalten.
- Weisen Sie der Münze bei der Erstellung eine zufällige Position innerhalb eines bestimmten Radius zu.
spread-coins.ts
Die Funktion spreadCoins ist die Funktion, die bei add aufgerufen wird und für die Erstellung der Münzen in der Szene verantwortlich ist:
const spreadCoins = (world, component) => {
const {eid, schema: {totalCoins}} = component
for (let i = 0; i < totalCoins; i++) {
const coinEntity = world.createEntity()
// @ts-ignore
const floatingSpeed = THREE.MathUtils.randFloat(0.0005, 0.0015)
// @ts-ignore
const floatingAmplitude = THREE.MathUtils.randFloat(0.5, 2)
Coin.set(world, coinEntity, {floatingSpeed, floatingAmplitude})
world.setParent(coinEntity, eid) // Set coin created as a children of Coins group
coins.push(coinEntity)
}
}
Dann gibt es noch checkSnowflakeProximity und checkPlayerProximity, die in der Tick-Funktion bei jedem Frame in der Komponente aufgerufen werden:
Die Funktion checkPlayerProximity ist für die Berechnung der Entfernung zwischen dem Spieler und einer Münze zuständig. Wenn sich der Spieler der Münze nähert, wird die Münze eingesammelt und aus der Szene ausgeblendet, wobei ein Reset abgewartet wird, bevor die nächste Herausforderung gestartet wird
.
const checkPlayerProximity = (world, component) => {
coins.forEach((coin) => {
......
const distanceToPlayer = vecCoinPosition.distanceTo(vecPlayerPosition)
if (!collecting && distanceToPlayer <= collectionDistance) {
collecting = true
world.events.dispatch(eid, 'coin-collect') // Updated UI score
coins = coins.filter(c => c !== coin) // Clear collected coin from the coins array
disableCoin(world, coin)
collecting = false
}
})
}
Die Funktion checkSnowflakeProximity ist eine ähnliche Funktion. Je näher der Spieler der Münze kommt, desto undurchsichtiger wird das Material der Münze. Wenn der Spieler nahe genug an eine Münze herankommt, wird diese eingesammelt und schließlich vom Spielfeld entfernt, da sie in der letzten Phase nicht mehr benötigt wird.
Player
player-controller.ts
Dies ist die Player-Komponente:
const handleGameStart = () => {
HoldDrag.set(world, eid, {factor: 12, lag: 0.25, distanceThreshold: 0.05, cone, help, timer})
}
const handleReset = () => {
HoldDrag.remove(world, eid)
const position = ecs.Position.cursor(world, eid)
position.x = 0
position.y = 0
position.z = 0
const quaternion = ecs.Quaternion.cursor(world, eid)
quaternion.x = 0
quaternion.y = 0
quaternion.z = 0
quaternion.w = 0
}
world.events.addListener(eid, 'reset', handleReset)
world.events.addListener(eid, 'game-start', handleGameStart)
Die HoldDrag-Komponente wird beim Start gesetzt und ermöglicht es, Doty zu bewegen
Die handleReset-Funktion bringt Doty in seine Ausgangsposition zurück und dreht ihn, wenn er sich zur nächsten Herausforderung bewegt
hold-drag.ts
Behandelt die Halte- und Ziehfingergesten und bewegt Doty durch 3 Hauptzuhörer:
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_START, handleStart)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_MOVE, handleMove)
world.events.addListener(eid, ecs.input.SCREEN_TOUCH_END, handleEnd)
Diese Komponente enthält auch die "Walk"- und "Idle"-Animationen für Doty, die ausgelöst werden, wenn Doty sich bewegt bzw.
Projectile
projectile.ts
Dies ist die Projektilkomponente, die die Erstellung einer neuen Münze in der Szene übernimmt. Es stellt einen Schneeball dar, der auf den Königspilz geworfen wird.
Ein einmal geladenes Projektil wird mit einer Skala und einem timeStart initialisiert, die es ermöglichen, seinen Lebenszyklus später in der Tick-Funktion zu behandeln
// Wait for the Projectile's model to be fully loaded
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, initializeProjectile)
Ein Kollisionswarner prüft, ob der Königspilz von einem Schneeball getroffen wird, und in diesem Fall wird seine Skala gesenkt, wenn seine Skala einen bestimmten Wert erreicht, ist das Spiel gewonnen, der Spieler hat den Königspilz besiegt
// Collision event listener
world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, (event) => {
const {data: {other}} = event
// Check if there is a collision with the target "Tree"
if (target && target.toString() === other.toString()) {
const {x, y, z} = ecs.Scale.get(world, target)
const toX = x - 0.2
const toY = y - 0.2
const toZ = z - 0.2
ecs.ScaleAnimation.set(
world,
target, {
fromX: x, fromY: y, fromZ: z, toX, toY, toZ, duration: 100, loop: false,
}
)
if (toX <= 1) { console.log('GAME WIN!')
ecs.GltfModel.set(world, target, {
animationClip: 'Death',
loop: false,
paused: false,
})
world.events.dispatch(eid, 'game-win')
}
}
})
projectile-spawner.ts
Der Projektilspawner ist eine Entität innerhalb von Doty, eine Box um seine Körperebene, von der aus die Schneeball-"Projektile" gespawnt und zum König geworfen werden.
Wenn der Schneeballknopf in der UI gedrückt wird, wird ein Ereignis namens "spawn-projectile" ausgelöst. Dieses Ereignis löst die Projektil-Spawner-Komponente aus, die dann ein neues Projektil in der Szene erzeugt.
world.events.addListener(eid, 'spawn-projectile', () => {
spawnProjectile()
})
Die Funktion spawnProjectile erzeugt und startet ein neues Projektil in der Szene. Setzt die anfängliche Position und Ausrichtung des Projektils basierend auf der Weltposition und -rotation des Spawners. Die Funktion berechnet die Flugbahn des Geschosses, einschließlich seiner Vorwärtsrichtung und einer vertikalen Komponente für eine parabolische Bewegung. Außerdem werden die physikalischen Eigenschaften des Geschosses wie Masse, Radius und Abprallverhalten konfiguriert, bevor die Geschwindigkeit und die Richtung des Abschusses festgelegt werden.
Schneebälle
snowball.ts
Dies ist die Schneeballkomponente, die für die Erstellung eines neuen Schneeballs auf dem gefrorenen Boden innerhalb der Szene zuständig ist
Sobald ein Schneeball geladen ist, erhält er einen Maßstab und eine zufällige Position innerhalb eines bestimmten Radius
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, () => {
applyRandomPosition(world, eid)
const snowballScale = ecs.Scale.cursor(world, eid)
snowballScale.x = scale
snowballScale.y = scale
snowballScale.z = scale
})
spread-snowballs.ts
Diese Komponente ähnelt der Komponente spread-coins.ts mit 2 Hauptfunktionen:
- Die Funktion checkPlayerProximity berechnet die Entfernung zwischen dem Spieler und einem Schneeball und bestimmt, ob der Schneeball eingesammelt werden kann.
- Eine Funktion zum Verteilen von Schneebällen, die jeden Schneeball um den gefrorenen Boden herum erzeugt.
UI - Screens
Jede UI-Komponente in diesem Ordner stellt einen Bildschirm oder nur ein UI-Element wie Symbole oder eine einzelne Schaltfläche dar, die während der Spielsitzung angezeigt werden. Sie haben alle eine setupCss-Funktion, die ihr spezifisches CSS innerhalb des <style> -Tags des <head>hinzufügt.
game-over-screen.ts
Es zeigt das Game-Over-Logo
Es pausiert den gesamten Ton des Spiels
Schließlich spielt es den Game-Over-Ton ab
landing-screen.ts
Dies ist der Bildschirm, der beim Starten des Spiels zuerst angezeigt wird
Hier kann der Benutzer den Schwierigkeitsgrad auswählen, den er ausprobieren möchte. Es wird ein Dropdown-Element mit einem Zuhörer erstellt, der den Schwierigkeitsgrad bei Auswahl einer Option ändert:
// DIFFICULTY DROPDOWN
......
const dropdown = document.createElement('select')
......
const options = ['[ Game Difficulty ]', 'easy', 'normal', 'hard']
options.forEach((option) => {
const optionElement = document.createElement('option')
optionElement.value = option
optionElement.textContent = option
......
dropdown.appendChild(optionElement)
})
// Add an event listener to get the selected value when it changes
dropdown.addEventListener('change', (event) => {
// @ts-ignore
const {target: {value}} = event
// Update mode
updateMode(value)
// Handle option title selection assigned normal as default selection in that case
if (value === options[0]) defaultSelectedValue.selected = true
})
Schließlich ist dieser Bildschirm auch für das Tippen auf die Schaltfläche "START" zuständig, um das erste Spieltutorial zu starten:
// ENTER BUTTON
const button = document.createElement('button')
button.id = 'enter-btn'
button.innerText = 'ENTER'
button.addEventListener('click', () => {
......
world.events.dispatch(tutorial, 'show-game-tutorial', {speed, mode})
})
tutorial-screen.ts
- Dieser Bildschirm wird beim ersten Start des Spiels und zwischen den einzelnen Kapiteln verwendet, um die neuen Anweisungen und Spielmechanismen zu zeigen
- Beim Drücken der Schaltfläche "OK" werden Ereignisse ausgelöst, um die verschiedenen Komponenten zu starten, die für den Ablauf der Spielsitzung verantwortlich sind:
// Handle on press start game
button.addEventListener('click', () => {
const dataAttr = dataAttribute.cursor(eid)
world.events.dispatch(hearts, 'game-start', {})
world.events.dispatch(timer, 'game-start', {mode: dataAttr.mode})
......
Es werden die UI-Elemente wie der Schieberegler und 2 Navigationspfeile erstellt.
win-screen.ts
- Es zeigt das Logo des Spiels
- Es pausiert den gesamten Ton des Spiels
- Schließlich spielt es den Ton des Spiels ab
UI - Elemente
Die übrigen Elemente sind hauptsächlich einzelne Schaltflächen oder Symbole, die während einer Spielsitzung angezeigt werden. Sie alle haben 2 verschiedene Ereignisse, die es ermöglichen, ihren Inhalt für den Benutzer ein- oder auszublenden:
world.events.addListener(eid, 'hide', () => handleHide(world, eid)) // hide
world.events.addListener(eid, 'game-start', () => handleShow(world, eid)) // show
hearts.ts
Erstellt die 3 Herz-Symbole in der unteren linken Ecke des Bildschirms, die die 3 Leben der Doty darstellen
help.ts
Es handelt sich um eine Schaltfläche, die das "?"Icon, das der Benutzer während einer Spielsitzung drücken kann, um bei Bedarf das Tutorial anzuzeigen
Während das Tutorial angezeigt wird, ist das Spiel in Pause, so dass der Benutzer die Anweisungen sicher noch einmal lesen kann
score.ts
Diese Komponente wurde speziell für die Score-UI entwickelt und hilft dabei, den Score ein- oder auszublenden
snowball-button.ts
Dies ist eine Schaltfläche, die es dem Benutzer ermöglicht, ein Ereignis auszulösen, um während des letzten Spielteils einen Schneeball zu werfen:
Während das Tutorial angezeigt wird, ist das Spiel in Pause, so dass der Benutzer die Anweisungen sicher noch einmal lesen kann
score.ts
Diese Komponente wurde speziell für die Score-UI entwickelt und hilft dabei, den Score ein- oder auszublenden
snowball-button.ts
Dies ist eine Schaltfläche, die es dem Benutzer ermöglicht, ein Ereignis auszulösen, um während des letzten Spielteils einen Schneeball zu werfen:
world.events.dispatch(spawner, 'spawn-projectile')
timer.ts
Diese Komponente ist für die Erstellung der Timer-Oberfläche zuständig, die während einer aktiven Spielsitzung läuft
Dieser Timer kann angehalten und wieder aufgenommen werden
Andere
Die verbleibenden Komponenten sind der Game-Manager, der Pilz, die Schneepartikel und die Schneeflocke.
game-manager.ts
Der Game-Manager ist die zentrale Komponente des Codes, die Ereignisse während der Spielsitzung verbindet, um sicherzustellen, dass die Spiellogik reibungslos abläuft und es dem Spieler ermöglicht, die verschiedenen Spielkapitel zu durchlaufen.
/**
* STATES
*/
ecs.defineState('default').initial().onEnter(() => {
// When entering the 'default' state, add listeners for the required events
world.events.addListener(world.events.globalId,
'coin-collect', coinCollect)
world.events.addListener(world.events.globalId,
'heart-lost', heartLost)
world.events.addListener(world.events.globalId,
'game-win', gameWinState)
world.events.addListener(world.events.globalId,
'game-over', gameOverState)
world.events.addListener(world.events.globalId,
'game-pause', gamePauseState)
}).onExit(() => {
// When exiting the 'default' state, remove the listeners
world.events.removeListener(world.events.globalId,
'coin-collect', coinCollect)
world.events.removeListener(world.events.globalId,
'heart-lost', heartLost)
world.events.removeListener(world.events.globalId,
'game-win', gameWinState)
world.events.removeListener(world.events.globalId,
'game-over', gameOverState)
world.events.removeListener(world.events.globalId,
'game-pause', gamePauseState)
})
mushroom.ts
Die Komponente mushroom ist die gegnerische Entität des Spiels. Wie bereits erwähnt, gibt es 3 verschiedene Arten von Pilzen: den kleinen, den wütenden und den Königspilz.
Die wichtigsten Teile der Logik in dieser Komponente sind:
- growMushroom-Funktion: Sie ist für die Vergrößerung des Pilzes zuständig, wenn sich der Spieler von seinem Ausgangspunkt wegbewegt und das Spiel beginnt
- followPlayer-Funktion: Sie ist für die Festlegung der nächsten Position und Drehung des Pilzes zuständig, um den Bewegungen des Spielers zu folgen
export const followPlayer = (world, component, ecsMushroomPosition) => {
const {eid, data: {speed}} = component
// Smoothly move mushroom towards player using lerp
const lerpFactor = speed
ecsMushroomPosition.x = lerp(ecsMushroomPosition.x, vecPlayerPosition.x, lerpFactor)
ecsMushroomPosition.y = lerp(ecsMushroomPosition.y, vecPlayerPosition.y, lerpFactor)
ecsMushroomPosition.z = lerp(ecsMushroomPosition.z, vecPlayerPosition.z, lerpFactor)
// Update mushroom's position
vecMushroomPosition = vec3.xyz(ecsMushroomPosition.x, ecsMushroomPosition.y, ecsMushroomPosition.z)
// Make the mushroom face the player, but only rotate on the Y axis
const direction = vecPlayerPosition.minus(vecMushroomPosition) // Define the direction from mushroom to player
// Calculate the angle based on the X and Z axes of the player position
const angle = Math.atan2(direction.x, direction.z)
// Set the Y rotation of the mushroom to face the player
const rotationQuat = quat.yRadians(angle)
// Directly set the new quaternion in ECS
const currentQuaternion = getQuaternion(world, eid)
currentQuaternion.x = rotationQuat.x
currentQuaternion.y = rotationQuat.y
currentQuaternion.z = rotationQuat.z
currentQuaternion.w = rotationQuat.w
}
checkPlayerProximity: prüft jeden Frame, ob der Pilz nahe genug am Spieler ist, in diesem Fall wird ein Herz entfernt und das Herzverlust-Ereignis ausgelöst
const dispatchLosingHeart = (world, mushroomId, trigger = 500) => {
if (hearts.length <= 0) return
const currentTime = Date.now() // Get the current time in milliseconds
if (currentTime - lastExecutionTime >= trigger) { // Check if 500 second has passed
lastExecutionTime = currentTime // Update the last execution time
world.events.dispatch(mushroomId, 'heart-lost', {mushroomId})
}
}
export const checkPlayerProximity = (world, component, distanceToPlayer) => {
const {eid, schema: {distanceCatch}} = component
// Check if the distance is less than distanceCatch
if (distanceToPlayer <= distanceCatch) {
dispatchLosingHeart(world, eid)
}
}
Die Funktion dispatchLosingHeart verfügt über eine Drosselungslogik, die sicherstellt, dass die Funktion nicht öfter als im Auslöseintervall ausgeführt wird. Um zu vermeiden, dass z.B. alle 3 Herzen in einem Frame verloren gehen
snow-particles.ts
Ist die Komponente, die für die Erstellung der Schneepartikel zuständig ist, die auf jeder Stufe herunterfallen
Sie verwendet das SnowParticles-Material, das unter scripts/classes/SnowParticles zu finden ist
snowflake.ts
Ist die 3D-Entität, die in Kapitel 2 um Doty herumschwebt
Diese Komponente ist dafür zuständig, die Komponente ein- oder auszublenden, da sie während der gesamten Spielsitzung nicht benötigt wird
Es gibt eine
Hilfedatei
Diese Hilfedateien sind eine Reihe von Dateien, die eine Aufschlüsselung der Logik bieten, die an verschiedenen Stellen des Codes aufgerufen wird, hier die wichtigste, die ich durchgehen möchte:
data.ts
In dieser Datei findest du eine Menge an Konfigurationen und Einstellungen, die im gesamten Code verwendet werden, wie z.B. die Geschwindigkeitswerte, die Pilzdaten (abzuspielende Clips, ihre Größe, URLs).