Mamun Labs Learn
JourneyGlossary
esc
  • Try “map”, “flexbox” or “state”.
Log in
Start learning
Phase 7 · 5 / 5Sound and saving the best score
Phase 7Games
5 topics · 3–4 wks
  1. 1The game loop
  2. 2Touch input
  3. 3Simple animation
  4. 4Collisions and score
  5. 5Sound and saving the best score
JourneyPhase 7Sound and saving the best score
01 / 09Overview

Sound and saving the best score

You'll learn to play a short sound effect when the game ends, and to save the best score on the phone so it is still there tomorrow.

Why it mattersA small sound tells the player something happened, and a saved best score gives them a reason to play again.
about 60 min·needs: Collisions and score, Saving data on the phone
02 / 09

Watch & read

Start with the required ones. The others are there when you need them.

Docs
Expo Audio (expo-audio)Expo docs · official · 10 min · English
MMamun's noteRead "Installation" and "Playing sounds" only. The example has a Replay button that calls seekTo(0) and then play(), which is exactly what our game does. Skip recording and background audio.
Docs
Async Storage usageAsync Storage docs · official · 10 min · English
MMamun's noteRead "Storing string value" and "Reading string value". See how getItem can give back null. This is the version Expo installs for you today, so the code works in Snack and Expo Go.
DocsOptional
Store dataExpo docs · official · 5 min · English
MMamun's noteA short page with every way to save data in an Expo app. Read the "Async Storage" part. It is right for small things like a best score; SecureStore is for passwords and SQLite is for big lists.
ToolOptional
Karabiner click (the sound in our game)Wikimedia Commons · 2 min · English
MMamun's noteThe click our game plays. It is about one second long and free to use (CC0). If you pick your own sound, choose a short click or thud, and check that it is free to use.
03 / 09

Explanation

You want two things when the game ends: a short click, so the player feels the hit, and a best score that does not reset to 0 every time the app is closed.

The sound. expo-audio plays sound files. useAudioPlayer loads one when the screen appears, and you play it when you need it:

const hitSound = useAudioPlayer({ uri: HIT_SOUND_URL });
hitSound.seekTo(0); // back to the start, in case it played before
hitSound.play();

Use a short, plain sound effect: a click, a tap or a thud, less than a second long. Older tutorials use expo-av. That package was removed from recent versions of Expo, so use expo-audio.

The best score. State is forgotten when the app closes. AsyncStorage is a small notebook on the phone that remembers text under a name, called a key. You met it when saving data for your habit tracker. Two things to remember:

  1. It only stores strings. Save with String(best), and turn it back with Number(saved).
  2. The first time, nothing is saved yet, so getItem gives back null.
function newBest(savedText, score) {
const best = savedText === null ? 0 : Number(savedText);
return Math.max(best, score);
}

When the game ends, read the saved best, compare it with score, and save the bigger one.

People often compare the saved text straight away. "12" is text, not a number, and null is not 0. So Math.max or > can give a strange answer, or the best score shows as empty. Turn it into a number first.

getItem and setItem are asynchronous, like fetch: they give back a promise. Use .then or await, and set state when the value arrives.

When you read storage code, look for the key ("best"), the turn into a string when saving, and the turn back into a number when loading.

Words to recognise
expo-audiouseAudioPlayersound effectAsyncStoragegetItemsetItemnull
04 / 09

Code tour

Click a line number with a ring to read the note for that line.

App.jsx5 notes
1const [best, setBest] = useState(0);
const hitSound = useAudioPlayer(HIT_SOUND);
Mentor note · line 2
useAudioPlayer loads the click once, when the screen appears. It does not play yet.
3useEffect(() => {
AsyncStorage.getItem("best").then((saved) => setBest(newBest(saved, 0)));
5}, []);
6
7useEffect(() => {
if (!game.gameOver) return;
hitSound.seekTo(0);
10 hitSound.play();
11 AsyncStorage.getItem("best").then((saved) => {
12 const b = newBest(saved, game.score);
13 setBest(b);
AsyncStorage.setItem("best", String(b));
15 });
16}, [game.gameOver]);
05 / 09

Practice

Short drills. Take your time and read the code before you answer.

DrillPredict1 / 3
1// Last time you played, the app saved "15" under "best".
2let best = 0;
3
4AsyncStorage.getItem("best").then((saved) => {
5 best = Number(saved);
6});
7
8console.log(best);
What is printed?
DrillFind the bug2 / 3
newBest(savedText, score) gets what AsyncStorage.getItem gave back (a string like "12", or null the first time) and this game's score. It should return the best score as a number. Right now the best score comes back as text, or as null. Fix it.
1
2
3
4
5
Try 1 of 3 · solution unlocks after 3
DrillThe full tap-to-jump game3 / 3
Here is the whole phase project: jump, score, game over, a short click when you hit, and a best score that is saved. Play a few games, then reload the Snack. Your best score is still there.
1import { useEffect, useRef, useState } from "react";
2import { Animated, Pressable, StyleSheet, Text, View } from "react-native";
3import { useAudioPlayer } from "expo-audio";
4import AsyncStorage from "@react-native-async-storage/async-storage";
5
6const SPEED = 10, SCREEN_WIDTH = 400, PLAYER_X = 50, JUMP_HEIGHT = 150;

Runs in the browser. Choose “My Device” in Snack to try it on your phone with Expo Go.

Working whenRun into the obstacle. You hear one short click and see game over.Score more than your best. Best changes to your new score.Reload the Snack (or close and open the app in Expo Go). Best still shows your score.Save the best under a new key, like "bestScore", in all three places. After a reload, best starts again at 0, because nothing is saved under the new key yet.
Check each goal yourself, then mark it.
06 / 09

Quick check

3 questions. Each one explains itself after you answer.

  1. 1 / 3The app opens for the very first time. What does AsyncStorage.getItem("best") give back?
  2. 2 / 3Why do we save the best score with String(best)?
  3. 3 / 3Why call hitSound.seekTo(0) before hitSound.play()?
07 / 09

Explain it back

Only you can see this. Log in to keep it.0 words
08 / 09

How confident are you with Sound and saving the best score?

09 / 09

Done

You can now
Read code that loads and saves a value with AsyncStorage and say what it shows the first time the app opens.
Next: Icons, names and screenshots Back to Phase 7
On this page
  1. 01Sound and saving the best score
  2. 02Watch & read
  3. 03Explanation
  4. 04Code tour
  5. 05Practice
  6. 06Quick check
  7. 07Explain it back
  8. 08Confidence
  9. 09Done
Up next · 02 / 09Watch & read
Mamun Labs LearnFound a mistake on this page? Tell Mamun.
Mamun Labs Learn
JourneyGlossary
esc
  • Try “map”, “flexbox” or “state”.
Log in
Start learning
Log in to save your progress. Everything here works without an account.Log in to save progressLog in
Phase 7Games
5 topics · 3–4 wks
  1. 1The game loop
  2. 2Touch input
  3. 3Simple animation
  4. 4Collisions and score
  5. 5Sound and saving the best score
JourneyPhase 7Sound and saving the best score
01 / 09Overview

Sound and saving the best score

You'll learn to play a short sound effect when the game ends, and to save the best score on the phone so it is still there tomorrow.

Why it mattersA small sound tells the player something happened, and a saved best score gives them a reason to play again.
about 60 min·needs: Collisions and score, Saving data on the phone
02 / 09

Watch & read

Start with the required ones. The others are there when you need them.

Docs
Expo Audio (expo-audio)Expo docs · official · 10 min · English
MMamun's noteRead "Installation" and "Playing sounds" only. The example has a Replay button that calls seekTo(0) and then play(), which is exactly what our game does. Skip recording and background audio.
Docs
Async Storage usageAsync Storage docs · official · 10 min · English
MMamun's noteRead "Storing string value" and "Reading string value". See how getItem can give back null. This is the version Expo installs for you today, so the code works in Snack and Expo Go.
DocsOptional
Store dataExpo docs · official · 5 min · English
MMamun's noteA short page with every way to save data in an Expo app. Read the "Async Storage" part. It is right for small things like a best score; SecureStore is for passwords and SQLite is for big lists.
ToolOptional
Karabiner click (the sound in our game)Wikimedia Commons · 2 min · English
MMamun's noteThe click our game plays. It is about one second long and free to use (CC0). If you pick your own sound, choose a short click or thud, and check that it is free to use.
03 / 09

Explanation

You want two things when the game ends: a short click, so the player feels the hit, and a best score that does not reset to 0 every time the app is closed.

The sound. expo-audio plays sound files. useAudioPlayer loads one when the screen appears, and you play it when you need it:

const hitSound = useAudioPlayer({ uri: HIT_SOUND_URL });
hitSound.seekTo(0); // back to the start, in case it played before
hitSound.play();

Use a short, plain sound effect: a click, a tap or a thud, less than a second long. Older tutorials use expo-av. That package was removed from recent versions of Expo, so use expo-audio.

The best score. State is forgotten when the app closes. AsyncStorage is a small notebook on the phone that remembers text under a name, called a key. You met it when saving data for your habit tracker. Two things to remember:

  1. It only stores strings. Save with String(best), and turn it back with Number(saved).
  2. The first time, nothing is saved yet, so getItem gives back null.
function newBest(savedText, score) {
const best = savedText === null ? 0 : Number(savedText);
return Math.max(best, score);
}

When the game ends, read the saved best, compare it with score, and save the bigger one.

People often compare the saved text straight away. "12" is text, not a number, and null is not 0. So Math.max or > can give a strange answer, or the best score shows as empty. Turn it into a number first.

getItem and setItem are asynchronous, like fetch: they give back a promise. Use .then or await, and set state when the value arrives.

When you read storage code, look for the key ("best"), the turn into a string when saving, and the turn back into a number when loading.

Words to recognise
expo-audiouseAudioPlayersound effectAsyncStoragegetItemsetItemnull
04 / 09

Code tour

Click a line number with a ring to read the note for that line.

App.jsx5 notes
1const [best, setBest] = useState(0);
const hitSound = useAudioPlayer(HIT_SOUND);
Mentor note · line 2
useAudioPlayer loads the click once, when the screen appears. It does not play yet.
3useEffect(() => {
AsyncStorage.getItem("best").then((saved) => setBest(newBest(saved, 0)));
5}, []);
6
7useEffect(() => {
if (!game.gameOver) return;
hitSound.seekTo(0);
10 hitSound.play();
11 AsyncStorage.getItem("best").then((saved) => {
12 const b = newBest(saved, game.score);
13 setBest(b);
AsyncStorage.setItem("best", String(b));
15 });
16}, [game.gameOver]);
05 / 09

Practice

Short drills. Take your time and read the code before you answer.

DrillPredict1 / 3
1// Last time you played, the app saved "15" under "best".
2let best = 0;
3
4AsyncStorage.getItem("best").then((saved) => {
5 best = Number(saved);
6});
7
8console.log(best);
What is printed?
DrillFind the bug2 / 3
newBest(savedText, score) gets what AsyncStorage.getItem gave back (a string like "12", or null the first time) and this game's score. It should return the best score as a number. Right now the best score comes back as text, or as null. Fix it.
1
2
3
4
5
Try 1 of 3 · solution unlocks after 3
DrillThe full tap-to-jump game3 / 3
Here is the whole phase project: jump, score, game over, a short click when you hit, and a best score that is saved. Play a few games, then reload the Snack. Your best score is still there.
1import { useEffect, useRef, useState } from "react";
2import { Animated, Pressable, StyleSheet, Text, View } from "react-native";
3import { useAudioPlayer } from "expo-audio";
4import AsyncStorage from "@react-native-async-storage/async-storage";
5
6const SPEED = 10, SCREEN_WIDTH = 400, PLAYER_X = 50, JUMP_HEIGHT = 150;

Runs in the browser. Choose “My Device” in Snack to try it on your phone with Expo Go.

Working whenRun into the obstacle. You hear one short click and see game over.Score more than your best. Best changes to your new score.Reload the Snack (or close and open the app in Expo Go). Best still shows your score.Save the best under a new key, like "bestScore", in all three places. After a reload, best starts again at 0, because nothing is saved under the new key yet.
Check each goal yourself, then mark it.
06 / 09

Quick check

3 questions. Each one explains itself after you answer.

  1. 1 / 3The app opens for the very first time. What does AsyncStorage.getItem("best") give back?
  2. 2 / 3Why do we save the best score with String(best)?
  3. 3 / 3Why call hitSound.seekTo(0) before hitSound.play()?
07 / 09

Explain it back

Only you can see this. Log in to keep it.0 words
08 / 09

How confident are you with Sound and saving the best score?

09 / 09

Done

You can now
Read code that loads and saves a value with AsyncStorage and say what it shows the first time the app opens.
Next: Icons, names and screenshots Back to Phase 7
On this page
  1. 01Sound and saving the best score
  2. 02Watch & read
  3. 03Explanation
  4. 04Code tour
  5. 05Practice
  6. 06Quick check
  7. 07Explain it back
  8. 08Confidence
  9. 09Done
Up next · 02 / 09Watch & read

You want two things when the game ends: a short click, so the player feels the hit, and a best score that does not reset to 0 every time the app is closed.

The sound. expo-audio plays sound files. useAudioPlayer loads one when the screen appears, and you play it when you need it:

const hitSound = useAudioPlayer({ uri: HIT_SOUND_URL });
hitSound.seekTo(0); // back to the start, in case it played before
hitSound.play();

Use a short, plain sound effect: a click, a tap or a thud, less than a second long. Older tutorials use expo-av. That package was removed from recent versions of Expo, so use expo-audio.

The best score. State is forgotten when the app closes. AsyncStorage is a small notebook on the phone that remembers text under a name, called a key. You met it when saving data for your habit tracker. Two things to remember:

  1. It only stores strings. Save with String(best), and turn it back with Number(saved).
  2. The first time, nothing is saved yet, so getItem gives back null.
function newBest(savedText, score) {
const best = savedText === null ? 0 : Number(savedText);
return Math.max(best, score);
}

When the game ends, read the saved best, compare it with score, and save the bigger one.

People often compare the saved text straight away. "12" is text, not a number, and null is not 0. So Math.max or > can give a strange answer, or the best score shows as empty. Turn it into a number first.

getItem and setItem are asynchronous, like fetch: they give back a promise. Use .then or await, and set state when the value arrives.

When you read storage code, look for the key ("best"), the turn into a string when saving, and the turn back into a number when loading.

Words to recognise
expo-audiouseAudioPlayersound effectAsyncStoragegetItemsetItemnull

You want two things when the game ends: a short click, so the player feels the hit, and a best score that does not reset to 0 every time the app is closed.

The sound. expo-audio plays sound files. useAudioPlayer loads one when the screen appears, and you play it when you need it:

const hitSound = useAudioPlayer({ uri: HIT_SOUND_URL });
hitSound.seekTo(0); // back to the start, in case it played before
hitSound.play();

Use a short, plain sound effect: a click, a tap or a thud, less than a second long. Older tutorials use expo-av. That package was removed from recent versions of Expo, so use expo-audio.

The best score. State is forgotten when the app closes. AsyncStorage is a small notebook on the phone that remembers text under a name, called a key. You met it when saving data for your habit tracker. Two things to remember:

  1. It only stores strings. Save with String(best), and turn it back with Number(saved).
  2. The first time, nothing is saved yet, so getItem gives back null.
function newBest(savedText, score) {
const best = savedText === null ? 0 : Number(savedText);
return Math.max(best, score);
}

When the game ends, read the saved best, compare it with score, and save the bigger one.

People often compare the saved text straight away. "12" is text, not a number, and null is not 0. So Math.max or > can give a strange answer, or the best score shows as empty. Turn it into a number first.

getItem and setItem are asynchronous, like fetch: they give back a promise. Use .then or await, and set state when the value arrives.

When you read storage code, look for the key ("best"), the turn into a string when saving, and the turn back into a number when loading.

Words to recognise
expo-audiouseAudioPlayersound effectAsyncStoragegetItemsetItemnull