Skip to Content
React Native9. APIs natives9.3 Haptics & sons

Chapitre 9.3 — Haptics & sons : le feedback du jeu

Où on en est : Drill est fonctionnel mais muet et inerte. Ce chapitre ajoute la couche sensorielle — vibrations et sons — celle qui sépare « ça marche » de « c’est bon en main ».

⏱️ TL;DRexpo-haptics : impactAsync (Light/Medium/Heavy) pour les taps, notificationAsync (Success/Warning/Error) pour les verdicts. expo-audio : useAudioPlayer(require('…/correct.mp3')) + player.play() pour les jingles. Tout passe par lib/feedback.ts — UN module qui respecte les réglages (sons/vibrations désactivables) et centralise le vocabulaire sensoriel de l’app.

🎯 Objectifs

  • Choisir le bon haptic pour chaque moment (le vocabulaire Light/Medium/Success/Error).
  • Jouer des sons courts avec expo-audio sans latence ni fuite mémoire.
  • Centraliser le feedback dans lib/feedback.ts, branché sur les réglages.
  • Doser : la frontière entre feedback et sapin de Noël.

Le vocabulaire haptique

npx expo install expo-haptics
import * as Haptics from 'expo-haptics' // Les impacts — un contact physique : taps, sélections Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) // tap ordinaire Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) // action notable Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy) // rare, événement fort // Les notifications — un VERDICT : résultat d'une action Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) // bonne réponse ! Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) // mauvaise réponse Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning) // timer sous 3 s // La sélection — le tick discret des molettes/sliders Haptics.selectionAsync()

La grammaire de Drill (et une bonne grammaire par défaut pour vos apps) :

MomentHaptic
Tap sur une réponseimpact Light (accusé de réception)
Verdict bonne réponsenotification Success
Verdict mauvaise réponse / timeoutnotification Error
Timer passe sous 3 snotification Warning (une fois !)
Nouveau record (écran résultat)impact Heavy
Navigation, boutons ordinairesrien — le silence est le fond qui fait ressortir le reste

Les sons courts avec expo-audio

npx expo install expo-audio
// Le hook du SDK : charge le son, le libère au démontage import { useAudioPlayer } from 'expo-audio' function ResultScreen() { const fanfare = useAudioPlayer(require('../assets/sounds/record.mp3')) // … if (isNewRecord) fanfare.play() }

Pour les sons DE JEU (bonne/mauvaise réponse), le hook par écran ne suffit pas — on veut des players préchargés et globaux (latence zéro au moment du verdict). D’où le module :

// lib/feedback.ts — LE module sensoriel de Drill import * as Haptics from 'expo-haptics' import { createAudioPlayer, type AudioPlayer } from 'expo-audio' import { useSettings } from '../stores/settings' // Players créés une fois, préchargés (fichiers < 100 Ko, mp3) const sounds: Record<'correct' | 'wrong' | 'tick' | 'record', AudioPlayer> = { correct: createAudioPlayer(require('../assets/sounds/correct.mp3')), wrong: createAudioPlayer(require('../assets/sounds/wrong.mp3')), tick: createAudioPlayer(require('../assets/sounds/tick.mp3')), record: createAudioPlayer(require('../assets/sounds/record.mp3')), } function play(name: keyof typeof sounds) { if (!useSettings.getState().soundEnabled) return const p = sounds[name] p.seekTo(0) // un son de jeu peut se rejouer avant d'être fini p.play() } function haptic(fn: () => Promise<void>) { if (useSettings.getState().hapticsEnabled) fn() } // L'API que le reste de l'app consomme — le VOCABULAIRE, pas la technique export const feedback = { tap: () => haptic(() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)), correct: () => { haptic(() => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)) play('correct') }, wrong: () => { haptic(() => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)) play('wrong') }, lastSeconds: () => haptic(() => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning)), newRecord: () => { haptic(() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)) play('record') }, }

L’architecture à retenir : l’app appelle feedback.correct() — un verbe métier. Ni les écrans ni le store ne savent ce que ça déclenche (vibration ? son ? les deux ? rien si désactivé ?). Changer la grammaire sensorielle = un fichier. Et les réglages (ch. 6.4) sont respectés par construction, via getState() hors React.

Le branchement : l’action answer du store retourne le verdict (ch. 6.2 — anticipé précisément pour ça) :

onPress={() => { feedback.tap() const correct = answer(i, secondsLeft) correct ? feedback.correct() : feedback.wrong() }}

⚠️ Piège — Deux écueils symétriques. (1) La latence : charger le mp3 au moment de jouer = 100-300 ms de retard, le feedback arrive « à côté » de l’action — d’où le préchargement. (2) La surenchère : un haptic sur CHAQUE interaction transforme l’app en brosse à dents électrique. La grammaire ci-dessus tient en 5 verbes — si vous dépassez 8, vous décorez, vous ne communiquez plus.

🍏 Côté iOS — Même API, rendu différent : le Taptic Engine des iPhone est plus fin que la plupart des vibreurs Android (où Heavy et Medium peuvent se ressembler). Raison de plus pour tester sur VOTRE téléphone : le feedback se règle à l’oreille et à la main, pas en théorie.

Où trouver les sons

Des jingles de 0,3-1 s, libres : freesound.org  (filtres CC0), pixabay.com/sound-effects . Convention : mp3, < 100 Ko, volume normalisé (Audacity : Effect → Loudness Normalization, -14 LUFS) — un son deux fois plus fort que les autres ruine l’ensemble.

✏️ Exercices

1. Branchez la grammaire complète dans le quiz (tap, correct, wrong, lastSeconds au passage sous 3 s — le useRef de l’exercice 6.3, newRecord sur l’écran résultat). Jouez une partie son coupé, puis vibration coupée : les réglages sont-ils respectés partout ?

✅ Solution

Chaque appel passe par feedback.* → les deux Switch coupent tout d’un coup (getState dans play/haptic). Le test des réglages est trivial PARCE QUE le module est centralisé — la version « Haptics.impactAsync éparpillé dans 6 fichiers » aurait 6 endroits à vérifier.

2. Ajoutez un « tick » sonore par seconde sous 5 s — puis retirez-le après test. Pourquoi cette itération (ajouter → tester → retirer) est-elle la bonne méthode pour le feedback ?

✅ Solution

useEffect sur secondsLeft : if (secondsLeft <= 5 && phase.name === 'playing') play('tick'). Au test : le tick STRESSE plus qu’il n’informe (la ProgressBar + le Warning haptic suffisent) — il sort. Le feedback ne se juge que téléphone en main : la boucle ajouter/ressentir/trancher est la seule méthode, et « retirer » est un résultat de test réussi, pas un échec.

3. Votre future app Pomodoro (P19) : écrivez sa grammaire feedback (5 verbes max) en réutilisant la structure de lib/feedback.ts.

✅ Solution

start (impact Medium — engagement), pause (selection — discret), workDone (notification Success + son doux — la récompense), breakDone (impact Light + tick — on repart), abandoned (rien — ne pas punir). La structure (verbes métier + réglages respectés + players préchargés) se copie telle quelle ; seul le vocabulaire change. C’est ça, un pattern.

🧠 Quiz

1. impactAsync vs notificationAsync — la distinction sémantique ?

Réponse

Impact = un contact (tap, sélection — Light/Medium/Heavy). Notification = un verdict (Success/Warning/Error — résultat d’une action). Drill : Light au tap, Success/Error au verdict.

2. Pourquoi précharger les sons de jeu ?

Réponse

Charger à la demande = 100-300 ms de latence : le feedback arrive décalé de l’action, l’effet est ruiné. Players créés au démarrage (createAudioPlayer) + seekTo(0) pour rejouer.

3. Quel est le rôle architectural de lib/feedback.ts ?

Réponse

Exposer des verbes métier (correct, wrong, newRecord) qui encapsulent haptics + sons + respect des réglages — un seul endroit pour toute la grammaire sensorielle.

4. Comment les réglages sont-ils consultés hors composants ?

Réponse

useSettings.getState().soundEnabled — l’accès hors-React de Zustand (ch. 6.2), au moment exact de l’appel.

5. Quel est le signe d’un feedback sur-dosé ?

Réponse

Vibration/son sur les interactions ordinaires (navigation, chaque bouton) : le bruit de fond tue le signal. Une grammaire tient en ~5 verbes ; le silence est ce qui fait ressortir les moments qui comptent.


👉 Chapitre suivant : 9.4 — Partage & presse-papiers — le score qui sort de l’app.