Chasse aux pièces de monnaie surgelées
Participez à une nouvelle aventure avec le capitaine Doty et traversez le monde gelé. Collectez des pièces d'or et échappez aux champignons glacés !

Personnalisez-le grâce au projet exemple

Studio : Coin Scatter
Ce projet vous guide dans la diffusion aléatoire de pièces dans un rayon donné à l'aide du composant de Niantic Studio.
Voir un exemple de projet
Studio : Doty Snowball Party
Ce projet vous guide dans la création d'un mini jeu à l'aide du moteur physique de Niantic Studio.
Voir un exemple de projetBehind the Build: Frozen Coin Hunt Adventure

Written by Alex di Guida
May 16, 2025
Introduction
Cette expérience est un jeu WebAR créé à l'aide de Niantic Studio Beta, octobre 2024.
Le personnage principal est le célèbre Capitaine Doty. Il est confronté à trois chapitres différents :
Le petit champignon
- Collectez 20 pièces d'or tout en évitant le petit champignon qui vous poursuit.
Le champignon en colère
- Collecter 20 pièces d'or tout en évitant le champignon en colère qui tente de vous attraper. Les pièces sont toutes cachées. Déplacez-vous avec le flocon de neige magique flottant à proximité pour révéler les pièces.
Le Roi Champignon
- Dansson dernier défi, commencez une bataille de boules de neige avec le Roi Champignon pour gagner. Ramassez des boules de neige sur leterrain et lancez-les sur le roi pour le vaincre.
Vous disposez de trois cœurs représentant vos vies.
Project Structure
Scène 3D
- Entités de base: Inclut la caméra Perspective AR et les lumières ambiantes/directionnelles.
- Entités d'interface utilisateur: Comprend tous les éléments de l'interface utilisateur affichés à l'écran, fournissant des informations et un retour d'information au joueur. Un tutoriel est présenté entre chaque défi.
Actifs
- Ce dossier contient tous les modèles 3D, les fichiers audio et les images utilisés dans le jeu.
Scripts
Le dossier des scripts est divisé en trois dossiers imbriqués :
- Components :
Contient la logique principale du jeu. Il s'agit des écrans de l'interface utilisateur, du mouvement des joueurs, de la logique d'apparition des pièces sur le sol gelé, des comportements des champignons et de la logique de suivi, ainsi que du gestionnaire de jeu, qui gère les événements au cours de la session de jeu.
- Helper :
Comprend diverses fonctions utilitaires utilisées dans les composants, permettant de simplifier et d'organiser la logique du jeu.
- Classes :
Contient le matériau d'ombrage utilisé pour gérer le système de particules, qui rend la neige tombant sur le terrain de jeu.
Implementation
Une attention particulière est portée aux principaux scripts du jeu, qui gèrent toute la logique de base de cette expérience.
Chaque composant peut avoir différentes clés d'écoute pour différents objectifs :
- game-start : Déclenché lorsque le jeu démarre ou reprend après avoir été mis en pause.
- reset : Gère l'état de réinitialisation du composant lors du passage au défi suivant.
- pause : Arrête les animations et les vérifications, telles que la proximité entre le champignon et le joueur ou entre une pièce et le joueur.
- hide : Utilisé pour les éléments de l'interface utilisateur, cachant l'interface lorsque le jeu est en pause ou lorsque l'interface utilisateur n'est pas nécessaire.
Les listeners sont envoyés depuis le composant game-manager.ts.
Coin
coin.ts
Il s'agit du composant coin qui gère la création d'une nouvelle pièce dans la scène.
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)
})
- Définissez le modèle de pièce à l'aide de coinAsset, qui contient l'URL du modèle de pièce.
- Une fois le modèle chargé, stockez le matériau de la pièce en vue d'une utilisation ultérieure pour gérer son opacité.
- Attribuez une position aléatoire à la pièce dans un rayon donné lors de sa création.
spread-coins.ts
La fonction spreadCoins est la fonction appelée sur add qui est chargée de créer les pièces dans la scène :
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)
}
}
Ensuite, nous avons les fonctions checkSnowflakeProximity et checkPlayerProximity qui sont appelées dans la fonction tick à chaque image du composant :
La fonction checkPlayerProximity est chargée de calculer la distance entre le joueur et une pièce de monnaie. Au fur et à mesure que le joueur se rapproche de la pièce, celle-ci est ramassée et cachée de la scène, attendant une réinitialisation avant de commencer le défi suivant
Le booléen de ramassage évite d'appeler la fonction de façon répétée pendant que la pièce est ramassée.
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
}
})
}
La fonction checkSnowflakeProximity est une fonction similaire. Au fur et à mesure que le joueur se rapproche de la pièce, l'opacité du matériau de la pièce augmente progressivement. Si le joueur s'approche suffisamment d'une pièce, celle-ci est ramassée et finalement retirée de la scène, car elle ne sera pas utilisée dans la phase finale.
Lecteur
player-controller.ts
Il s'agit du composant du lecteur :
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)
Le composant HoldDrag est activé au démarrage, ce qui permet de déplacer Doty
La fonction handleReset ramène Doty à sa position et à sa rotation initiales lors du passage au défi suivant
hold-drag.ts
Gère le geste de maintien et de déplacement du doigt, en déplaçant Doty par le biais de 3 auditeurs principaux :
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)
Ce composant comprend également les animations "Walk" et "Idle" pour Doty, qui sont déclenchées lorsque Doty se déplace ou reste immobile, respectivement.
Projectile
projectile.ts
Il s'agit du composant projectile qui gère la création d'une nouvelle pièce dans la scène. Il représente une boule de neige lancée vers le champignon royal.
Une fois chargé, un projectile est initialisé avec une échelle et un timeStart, ce qui permet de gérer son cycle de vie dans la fonction tick ultérieurement.
// Wait for the Projectile's model to be fully loaded
world.events.addListener(eid, ecs.events.GLTF_MODEL_LOADED, initializeProjectile)
Un détecteur de collision est chargé de vérifier si le champignon royal est touché par une boule de neige et, dans ce cas, son échelle est abaissée.
// 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
Le projectile spawner est une entité au sein de Doty, une boîte autour de son corps à partir de laquelle les "projectiles" de boule de neige sont générés et lancés vers le roi.
Lorsque le bouton "boule de neige" de l'interface utilisateur est pressé, un événement appelé "spawn-projectile" est déclenché. Cet événement déclenche le composant de création de projectile, qui crée alors un nouveau projectile dans la scène.
world.events.addListener(eid, 'spawn-projectile', () => {
spawnProjectile()
})
La fonction spawnProjectile crée et lance une nouvelle entité projectile dans la scène. Il définit la position et l'orientation initiales du projectile en fonction de la position et de la rotation du monde. La fonction calcule la trajectoire du projectile, y compris sa direction vers l'avant et une composante verticale pour un mouvement parabolique. Il configure également les propriétés physiques du projectile, telles que la masse, le rayon et la capacité de rebond, avant de définir sa vitesse et sa direction de lancement.
Boules de neige
snowball.ts
Il s'agit du composant boule de neige qui gère la création d'une nouvelle boule de neige sur le sol gelé de la scène
Une fois chargée, une boule de neige reçoit une échelle et une position aléatoire dans un rayon donné.
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
Ce composant est similaire au composant spread-coins.ts avec 2 fonctions principales :
- La fonction checkPlayerProximity calcule la distance entre le joueur et une boule de neige, déterminant si la boule de neige peut être collectée.
- Une fonction de répartition des boules de neige qui crée chaque boule de neige autour du sol gelé.
UI - Screens
Chaque composant UI contenu dans ce dossier représente un écran ou simplement un élément UI tel que des icônes ou un bouton unique affiché tout au long de la session de jeu. Ils ont tous une fonction setupCss qui ajoute leur CSS spécifique dans la balise <style> de la balise <head>.
game-over-screen.ts
Il affiche le logo du jeu
Il met en pause tout le son du jeu
Enfin, il joue le son du jeu
landing-screen.ts
C'est l'écran qui s'affiche au début lorsque le jeu est lancé
C'est là que l'utilisateur peut sélectionner le niveau de difficulté qu'il souhaite expérimenter. Un élément déroulant est créé avec un auditeur qui modifiera le niveau de difficulté en sélectionnant une option :
// 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
})
Enfin, cet écran est également chargé d'appuyer sur le bouton "START" pour déclencher le premier tutoriel du jeu :
// 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
- Cet écran est utilisé lors du premier lancement du jeu et entre chaque chapitre pour montrer les nouvelles instructions et les mécanismes de jeu
- En appuyant sur le bouton "OK", il envoie des événements pour démarrer les différents composants chargés de faire fonctionner la session de jeu :
// 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})
......
Il y a la création des éléments de l'interface utilisateur tels que le curseur et les deux boutons de navigation.
win-screen.ts
- Il affiche le logo win du jeu
- Il met en pause toute l'audio du jeu
- Enfin, il joue l'audio win du jeu
UI - Éléments
Les éléments restants sont principalement des boutons simples ou des icônes affichés au cours d'une session de jeu. Ils ont tous deux événements différents permettant de cacher ou d'afficher leur contenu à l'utilisateur :
world.events.addListener(eid, 'hide', () => handleHide(world, eid)) // hide
world.events.addListener(eid, 'game-start', () => handleShow(world, eid)) // show
hearts.ts
Crée les 3 icônes de cœurs UI dans le coin inférieur gauche de l'écran représentant les 3 vies de doty
help.ts
Il s'agit d'un bouton affichant le " ?"L'utilisateur peut appuyer sur cette icône pendant une session de jeu pour afficher le tutoriel si nécessaire.
Pendant que le tutoriel est affiché, le jeu est en pause, de sorte que l'utilisateur peut relire les instructions en toute sécurité.
score.ts
Ce composant est spécialement conçu pour l'interface utilisateur du score et permet de cacher ou d'afficher le score.
snowball-button.ts
Il s'agit d'un bouton qui permet à l'utilisateur de déclencher un événement pour lancer une boule de neige pendant la dernière partie du jeu :
Pendant que le tutoriel est affiché, le jeu est en pause, de sorte que l'utilisateur peut relire les instructions en toute sécurité.
score.ts
Ce composant est spécialement conçu pour l'interface utilisateur du score et permet de cacher ou d'afficher le score.
snowball-button.ts
Il s'agit d'un bouton qui permet à l'utilisateur de déclencher un événement pour lancer une boule de neige pendant la dernière partie du jeu :
world.events.dispatch(spawner, 'spawn-projectile')
timer.ts
Ce composant est chargé de créer l'interface utilisateur du timer qui s'exécute pendant une session de jeu active
Ce timer peut être mis en pause et repris.
Autres
Les composants restants sont le gestionnaire de jeu, le champignon, les particules de neige et le flocon de neige.
game-manager.ts
Le gestionnaire de jeu est le composant central du code qui relie les événements au cours de la session de jeu, en veillant à ce que la logique du jeu se déroule sans heurts et en permettant au joueur de progresser dans les différents chapitres du jeu.
/**
* 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
Le composant champignon est l'entité ennemie du jeu. Comme indiqué précédemment, il existe 3 types de champignons différents : le champignon minuscule, le champignon en colère et le champignon royal
Les principaux éléments logiques de ce composant sont les suivants :
- fonction growMushroom : qui est chargée d'agrandir le champignon lorsque le joueur quitte son emplacement d'origine et que le jeu commence
- fonction followPlayer : chargée de définir la prochaine position et la prochaine rotation du champignon afin de suivre les mouvements du joueur.
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 : vérifie à chaque image si le champignon est suffisamment proche du joueur, dans ce cas un coeur est retiré et l'événement heart-lost est déclenché.
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)
}
}
La fonction dispatchLosingHeart est dotée d'une logique d'étranglement, qui garantit que la fonction ne s'exécute pas plus souvent que l'intervalle de déclenchement. Pour éviter de perdre les 3 coeurs en 1 image par exemple
snow-particles.ts
Est le composant chargé de créer les particules de neige qui tombent sur chaque scène
Il utilise le matériau SnowParticles qui peut être trouvé sous scripts/classes/SnowParticles
snowflake.ts
Est l'entité 3D flottant autour de Doty dans le chapitre 2
Ce composant est chargé d'afficher ou de cacher le composant, car il n'est pas nécessaire pendant la session de jeu complète
Il y a un
Helpers
Ces helpers sont un ensemble de fichiers fournissant une décomposition de la logique appelée à différents endroits du code, voici le principal que je veux passer en revue :
data.ts
Dans ce fichier, vous trouverez un grand nombre de configurations et de paramètres utilisés dans le code, tels que les valeurs de vitesse, les données des champignons (clip à jouer, leur taille, les URL). Toutes les données des didacticiels telles que les titres, les instructions textuelles.
store.ts
C'est dans ce fichier que vous trouverez les fonctions storeTimer et getAllTimers. Ils sont chargés de mémoriser la valeur du temps et le mode de difficulté que le joueur a utilisé lorsqu'il a atteint la fin du jeu