Practice and reinforce the concepts from Lesson 7
Create your first mobile game by:
Time Limit: 5 minutes
# Create new game project
npx create-expo-app BubblePopper --template blank
cd BubblePopper
# Install game dependencies
npx expo install expo-haptics
npx expo install expo-av
npx expo install @react-native-async-storage/async-storage
npx expo install react-native-svg
Time Limit: 5 minutes
Prepare game assets (we'll use emojis and simple shapes):
// Game constants
const GAME_CONFIG = {
BUBBLE_EMOJIS: ['🔴', '🟡', '🟢', '🔵', '🟣', '🟠'],
BUBBLE_SIZE: 60,
GAME_WIDTH: 350,
GAME_HEIGHT: 600,
GRAVITY: 0.5,
BUBBLE_SPEED: 2,
SPAWN_RATE: 60, // frames between spawns
};
const SOUNDS = {
pop: 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav',
gameOver: 'https://www.soundjay.com/misc/sounds/fail-buzzer-02.wav',
levelUp: 'https://www.soundjay.com/misc/sounds/beep-28.wav',
};
✅ Checkpoint: Project setup and basic assets ready!
Replace App.js
with the game foundation:
import React, { useState, useEffect, useRef } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Dimensions,
Alert,
Animated,
} from 'react-native';
import * as Haptics from 'expo-haptics';
import AsyncStorage from '@react-native-async-storage/async-storage';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const GAME_CONFIG = {
BUBBLE_EMOJIS: ['🔴', '🟡', '🟢', '🔵', '🟣', '🟠'],
BUBBLE_SIZE: 60,
GAME_WIDTH: screenWidth - 40,
GAME_HEIGHT: screenHeight - 200,
GRAVITY: 0.3,
BUBBLE_SPEED: 1.5,
SPAWN_RATE: 90,
};
export default function App() {
// Game state
const [gameState, setGameState] = useState('menu'); // menu, playing, paused, gameOver
const [score, setScore] = useState(0);
const [highScore, setHighScore] = useState(0);
const [level, setLevel] = useState(1);
const [lives, setLives] = useState(3);
const [bubbles, setBubbles] = useState([]);
const [timeLeft, setTimeLeft] = useState(60);
// Game loop
const gameLoopRef = useRef(null);
const frameCountRef = useRef(0);
const nextBubbleId = useRef(0);
// Load high score on mount
useEffect(() => {
loadHighScore();
}, []);
// Game loop effect
useEffect(() => {
if (gameState === 'playing') {
gameLoopRef.current = setInterval(gameLoop, 16); // ~60 FPS
return () => clearInterval(gameLoopRef.current);
} else {
clearInterval(gameLoopRef.current);
}
}, [gameState]);
const loadHighScore = async () => {
try {
const savedHighScore = await AsyncStorage.getItem('bubblePopperHighScore');
if (savedHighScore) {
setHighScore(parseInt(savedHighScore));
}
} catch (error) {
console.error('Error loading high score:', error);
}
};
const saveHighScore = async (newScore) => {
try {
if (newScore > highScore) {
setHighScore(newScore);
await AsyncStorage.setItem('bubblePopperHighScore', newScore.toString());
}
} catch (error) {
console.error('Error saving high score:', error);
}
};
const gameLoop = () => {
frameCountRef.current += 1;
// Spawn new bubbles
if (frameCountRef.current % GAME_CONFIG.SPAWN_RATE === 0) {
spawnBubble();
}
// Update bubble positions
setBubbles(prevBubbles =>
prevBubbles
.map(bubble => ({
...bubble,
y: bubble.y + bubble.speed,
}))
.filter(bubble => bubble.y < GAME_CONFIG.GAME_HEIGHT + 100) // Remove off-screen bubbles
);
// Check for bubbles that reached bottom (lose life)
setBubbles(prevBubbles => {
const bottomBubbles = prevBubbles.filter(bubble =>
bubble.y >= GAME_CONFIG.GAME_HEIGHT - GAME_CONFIG.BUBBLE_SIZE
);
if (bottomBubbles.length > 0) {
setLives(prev => {
const newLives = prev - bottomBubbles.length;
if (newLives <= 0) {
gameOver();
}
return Math.max(0, newLives);
});
return prevBubbles.filter(bubble =>
bubble.y < GAME_CONFIG.GAME_HEIGHT - GAME_CONFIG.BUBBLE_SIZE
);
}
return prevBubbles;
});
};
const spawnBubble = () => {
const newBubble = {
id: nextBubbleId.current++,
x: Math.random() * (GAME_CONFIG.GAME_WIDTH - GAME_CONFIG.BUBBLE_SIZE),
y: -GAME_CONFIG.BUBBLE_SIZE,
emoji: GAME_CONFIG.BUBBLE_EMOJIS[Math.floor(Math.random() * GAME_CONFIG.BUBBLE_EMOJIS.length)],
speed: GAME_CONFIG.BUBBLE_SPEED + (level - 1) * 0.5,
points: Math.floor(Math.random() * 3) + 1, // 1-3 points
};
setBubbles(prev => [...prev, newBubble]);
};
const popBubble = (bubbleId) => {
const bubble = bubbles.find(b => b.id === bubbleId);
if (!bubble) return;
// Remove bubble and add score
setBubbles(prev => prev.filter(b => b.id !== bubbleId));
setScore(prev => prev + bubble.points * level);
// Haptic feedback
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Check for level up
const newScore = score + bubble.points * level;
if (Math.floor(newScore / 100) > level - 1) {
setLevel(prev => prev + 1);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
};
const startGame = () => {
setGameState('playing');
setScore(0);
setLevel(1);
setLives(3);
setBubbles([]);
setTimeLeft(60);
frameCountRef.current = 0;
nextBubbleId.current = 0;
};
const pauseGame = () => {
setGameState('paused');
};
const resumeGame = () => {
setGameState('playing');
};
const gameOver = () => {
setGameState('gameOver');
saveHighScore(score);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
};
const resetGame = () => {
setGameState('menu');
setBubbles([]);
};
// Render functions
const renderBubble = (bubble) => (
<TouchableOpacity
key={bubble.id}
style={[
styles.bubble,
{
left: bubble.x,
top: bubble.y,
},
]}
onPress={() => popBubble(bubble.id)}
>
<Text style={styles.bubbleEmoji}>{bubble.emoji}</Text>
<Text style={styles.bubblePoints}>+{bubble.points}</Text>
</TouchableOpacity>
);
const renderGameScreen = () => (
<View style={styles.gameScreen}>
{/* Game Header */}
<View style={styles.gameHeader}>
<Text style={styles.scoreText}>Score: {score}</Text>
<Text style={styles.levelText}>Level: {level}</Text>
<View style={styles.livesContainer}>
{[...Array(3)].map((_, i) => (
<Text key={i} style={styles.lifeHeart}>
{i < lives ? '❤️' : '💔'}
</Text>
))}
</View>
<TouchableOpacity style={styles.pauseButton} onPress={pauseGame}>
<Text style={styles.pauseText}>⏸</Text>
</TouchableOpacity>
</View>
{/* Game Area */}
<View style={styles.gameArea}>
{bubbles.map(renderBubble)}
</View>
</View>
);
const renderMenu = () => (
<View style={styles.menuScreen}>
<Text style={styles.title}>Bubble Popper</Text>
<Text style={styles.subtitle}>Pop the bubbles before they reach the bottom!</Text>
<View style={styles.stats}>
<Text style={styles.statText}>High Score: {highScore}</Text>
</View>
<TouchableOpacity style={styles.startButton} onPress={startGame}>
<Text style={styles.startButtonText}>Start Game</Text>
</TouchableOpacity>
</View>
);
const renderPauseScreen = () => (
<View style={styles.overlayScreen}>
<Text style={styles.overlayTitle}>Game Paused</Text>
<TouchableOpacity style={styles.actionButton} onPress={resumeGame}>
<Text style={styles.actionButtonText}>Resume</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton} onPress={resetGame}>
<Text style={styles.actionButtonText}>Main Menu</Text>
</TouchableOpacity>
</View>
);
const renderGameOverScreen = () => (
<View style={styles.overlayScreen}>
<Text style={styles.overlayTitle}>Game Over!</Text>
<Text style={styles.finalScore}>Final Score: {score}</Text>
{score > highScore && (
<Text style={styles.newHighScore}>New High Score! 🏆</Text>
)}
<TouchableOpacity style={styles.actionButton} onPress={startGame}>
<Text style={styles.actionButtonText}>Play Again</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton} onPress={resetGame}>
<Text style={styles.actionButtonText}>Main Menu</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
{gameState === 'menu' && renderMenu()}
{gameState === 'playing' && renderGameScreen()}
{gameState === 'paused' && renderPauseScreen()}
{gameState === 'gameOver' && renderGameOverScreen()}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#87CEEB',
},
menuScreen: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 30,
},
title: {
fontSize: 42,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
textAlign: 'center',
},
subtitle: {
fontSize: 18,
color: '#34495e',
textAlign: 'center',
marginBottom: 40,
},
stats: {
marginBottom: 40,
},
statText: {
fontSize: 20,
color: '#2c3e50',
textAlign: 'center',
},
startButton: {
backgroundColor: '#3498db',
paddingHorizontal: 40,
paddingVertical: 15,
borderRadius: 25,
elevation: 3,
},
startButtonText: {
color: 'white',
fontSize: 22,
fontWeight: 'bold',
},
gameScreen: {
flex: 1,
paddingTop: 50,
},
gameHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: 'rgba(255,255,255,0.9)',
},
scoreText: {
fontSize: 18,
fontWeight: 'bold',
color: '#2c3e50',
},
levelText: {
fontSize: 18,
fontWeight: 'bold',
color: '#8e44ad',
},
livesContainer: {
flexDirection: 'row',
},
lifeHeart: {
fontSize: 20,
marginHorizontal: 2,
},
pauseButton: {
padding: 5,
},
pauseText: {
fontSize: 24,
color: '#2c3e50',
},
gameArea: {
flex: 1,
position: 'relative',
},
bubble: {
position: 'absolute',
width: GAME_CONFIG.BUBBLE_SIZE,
height: GAME_CONFIG.BUBBLE_SIZE,
borderRadius: GAME_CONFIG.BUBBLE_SIZE / 2,
backgroundColor: 'rgba(255,255,255,0.8)',
alignItems: 'center',
justifyContent: 'center',
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
bubbleEmoji: {
fontSize: 28,
},
bubblePoints: {
fontSize: 12,
color: '#2c3e50',
fontWeight: 'bold',
position: 'absolute',
bottom: 5,
},
overlayScreen: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.8)',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 30,
},
overlayTitle: {
fontSize: 32,
fontWeight: 'bold',
color: 'white',
marginBottom: 20,
},
finalScore: {
fontSize: 24,
color: 'white',
marginBottom: 20,
},
newHighScore: {
fontSize: 20,
color: '#f1c40f',
marginBottom: 20,
},
actionButton: {
backgroundColor: '#3498db',
paddingHorizontal: 30,
paddingVertical: 12,
borderRadius: 20,
marginVertical: 10,
},
actionButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});
Add power-ups and special effects:
// Add to GAME_CONFIG
POWERUPS: {
SLOW_TIME: { emoji: '⏰', color: '#9b59b6', effect: 'slowTime' },
MULTI_POINTS: { emoji: '💰', color: '#f39c12', effect: 'doublePoints' },
EXTRA_LIFE: { emoji: '❤️', color: '#e74c3c', effect: 'extraLife' },
CLEAR_SCREEN: { emoji: '💥', color: '#1abc9c', effect: 'clearScreen' },
},
// Add powerup states
const [activePowerups, setActivePowerups] = useState({});
// Enhanced bubble spawning with powerups
const spawnBubble = () => {
const isPowerup = Math.random() < 0.1; // 10% chance
let newBubble;
if (isPowerup) {
const powerupKeys = Object.keys(GAME_CONFIG.POWERUPS);
const randomPowerup = powerupKeys[Math.floor(Math.random() * powerupKeys.length)];
const powerup = GAME_CONFIG.POWERUPS[randomPowerup];
newBubble = {
id: nextBubbleId.current++,
x: Math.random() * (GAME_CONFIG.GAME_WIDTH - GAME_CONFIG.BUBBLE_SIZE),
y: -GAME_CONFIG.BUBBLE_SIZE,
emoji: powerup.emoji,
speed: GAME_CONFIG.BUBBLE_SPEED + (level - 1) * 0.5,
points: 5,
isPowerup: true,
powerupType: randomPowerup,
color: powerup.color,
};
} else {
newBubble = {
id: nextBubbleId.current++,
x: Math.random() * (GAME_CONFIG.GAME_WIDTH - GAME_CONFIG.BUBBLE_SIZE),
y: -GAME_CONFIG.BUBBLE_SIZE,
emoji: GAME_CONFIG.BUBBLE_EMOJIS[Math.floor(Math.random() * GAME_CONFIG.BUBBLE_EMOJIS.length)],
speed: GAME_CONFIG.BUBBLE_SPEED + (level - 1) * 0.5,
points: Math.floor(Math.random() * 3) + 1,
isPowerup: false,
};
}
setBubbles(prev => [...prev, newBubble]);
};
// Enhanced pop bubble with powerup handling
const popBubble = (bubbleId) => {
const bubble = bubbles.find(b => b.id === bubbleId);
if (!bubble) return;
if (bubble.isPowerup) {
activatePowerup(bubble.powerupType);
}
// Remove bubble and add score
setBubbles(prev => prev.filter(b => b.id !== bubbleId));
const pointMultiplier = activePowerups.doublePoints ? 2 : 1;
setScore(prev => prev + bubble.points * level * pointMultiplier);
// Haptic feedback
Haptics.impactAsync(
bubble.isPowerup
? Haptics.ImpactFeedbackStyle.Heavy
: Haptics.ImpactFeedbackStyle.Light
);
};
// Powerup activation
const activatePowerup = (powerupType) => {
switch (powerupType) {
case 'SLOW_TIME':
setActivePowerups(prev => ({ ...prev, slowTime: true }));
setTimeout(() => {
setActivePowerups(prev => ({ ...prev, slowTime: false }));
}, 5000);
break;
case 'MULTI_POINTS':
setActivePowerups(prev => ({ ...prev, doublePoints: true }));
setTimeout(() => {
setActivePowerups(prev => ({ ...prev, doublePoints: false }));
}, 10000);
break;
case 'EXTRA_LIFE':
setLives(prev => Math.min(5, prev + 1));
break;
case 'CLEAR_SCREEN':
setBubbles([]);
setScore(prev => prev + 50); // Bonus points
break;
}
};
// Enhanced bubble rendering
const renderBubble = (bubble) => {
const bubbleSpeed = activePowerups.slowTime ? 0.5 : 1;
return (
<TouchableOpacity
key={bubble.id}
style={[
styles.bubble,
bubble.isPowerup && { backgroundColor: bubble.color },
{
left: bubble.x,
top: bubble.y,
},
]}
onPress={() => popBubble(bubble.id)}
>
<Text style={[
styles.bubbleEmoji,
bubble.isPowerup && styles.powerupEmoji,
]}>
{bubble.emoji}
</Text>
<Text style={styles.bubblePoints}>+{bubble.points}</Text>
</TouchableOpacity>
);
};
// Add powerup indicator styles
powerupEmoji: {
fontSize: 32,
textShadowColor: 'white',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
},
// Active powerups display
const renderActivePowerups = () => (
<View style={styles.powerupsContainer}>
{activePowerups.slowTime && (
<Text style={styles.activePowerup}>⏰ Slow Time</Text>
)}
{activePowerups.doublePoints && (
<Text style={styles.activePowerup}>💰 2x Points</Text>
)}
</View>
);
Add animations and particle effects:
// Add particle effect for popped bubbles
const [particles, setParticles] = useState([]);
// Create particle explosion
const createParticles = (x, y, color = '#FFD700') => {
const newParticles = [];
for (let i = 0; i < 6; i++) {
newParticles.push({
id: Date.now() + i,
x: x + GAME_CONFIG.BUBBLE_SIZE / 2,
y: y + GAME_CONFIG.BUBBLE_SIZE / 2,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
life: 30,
maxLife: 30,
color,
});
}
setParticles(prev => [...prev, ...newParticles]);
};
// Update particles in game loop
const updateParticles = () => {
setParticles(prev =>
prev
.map(particle => ({
...particle,
x: particle.x + particle.vx,
y: particle.y + particle.vy,
life: particle.life - 1,
vy: particle.vy + 0.3, // gravity
}))
.filter(particle => particle.life > 0)
);
};
// Enhanced popBubble with particles
const popBubble = (bubbleId) => {
const bubble = bubbles.find(b => b.id === bubbleId);
if (!bubble) return;
// Create particle explosion
createParticles(bubble.x, bubble.y, bubble.isPowerup ? bubble.color : '#FFD700');
// Rest of the pop bubble logic...
};
// Particle rendering
const renderParticle = (particle) => {
const opacity = particle.life / particle.maxLife;
return (
<View
key={particle.id}
style={[
styles.particle,
{
left: particle.x,
top: particle.y,
backgroundColor: particle.color,
opacity,
},
]}
/>
);
};
// Add to game area
{particles.map(renderParticle)}
// Screen shake effect
const [screenShake, setScreenShake] = useState(0);
const shakeAnimation = useRef(new Animated.Value(0)).current;
const shakeScreen = () => {
setScreenShake(10);
Animated.sequence([
Animated.timing(shakeAnimation, {
toValue: 10,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -10,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 0,
duration: 100,
useNativeDriver: true,
}),
]).start(() => setScreenShake(0));
};
// Apply shake to game area
<Animated.View
style={[
styles.gameArea,
{
transform: [{ translateX: shakeAnimation }],
},
]}
>
{/* Game content */}
</Animated.View>
// Particle styles
particle: {
position: 'absolute',
width: 6,
height: 6,
borderRadius: 3,
},
powerupsContainer: {
position: 'absolute',
top: 80,
right: 20,
alignItems: 'flex-end',
},
activePowerup: {
backgroundColor: 'rgba(0,0,0,0.7)',
color: 'white',
padding: 5,
borderRadius: 10,
fontSize: 12,
marginVertical: 2,
},
Create multiple game types:
const GAME_MODES = {
classic: {
name: 'Classic',
description: 'Pop bubbles before they reach bottom',
timeLimit: false,
lives: 3,
},
timeAttack: {
name: 'Time Attack',
description: 'Score as much as possible in 60 seconds',
timeLimit: 60,
lives: 999,
},
survival: {
name: 'Survival',
description: 'How long can you survive?',
timeLimit: false,
lives: 1,
},
};
// Game mode selector
const renderModeSelector = () => (
<View style={styles.modeSelector}>
<Text style={styles.modeTitle}>Choose Game Mode:</Text>
{Object.entries(GAME_MODES).map(([key, mode]) => (
<TouchableOpacity
key={key}
style={styles.modeButton}
onPress={() => startGameWithMode(key)}
>
<Text style={styles.modeButtonText}>{mode.name}</Text>
<Text style={styles.modeDescription}>{mode.description}</Text>
</TouchableOpacity>
))}
</View>
);
Add special boss bubbles:
// Boss bubble system
const createBossBubble = () => {
return {
id: nextBubbleId.current++,
x: GAME_CONFIG.GAME_WIDTH / 2 - 50,
y: -100,
emoji: '👾',
speed: 0.5,
points: 50,
health: 5,
isBoss: true,
size: 100,
pattern: 'zigzag', // movement pattern
};
};
// Boss movement patterns
const updateBossMovement = (boss, frameCount) => {
switch (boss.pattern) {
case 'zigzag':
return {
...boss,
x: boss.x + Math.sin(frameCount * 0.1) * 2,
y: boss.y + boss.speed,
};
case 'circle':
const centerX = GAME_CONFIG.GAME_WIDTH / 2;
const radius = 50;
return {
...boss,
x: centerX + Math.cos(frameCount * 0.05) * radius - boss.size / 2,
y: boss.y + boss.speed,
};
default:
return { ...boss, y: boss.y + boss.speed };
}
};
// Boss health bar
const renderBossHealthBar = (boss) => (
<View style={styles.bossHealthContainer}>
<Text style={styles.bossLabel}>BOSS</Text>
<View style={styles.healthBar}>
<View
style={[
styles.healthBarFill,
{ width: `${(boss.health / 5) * 100}%` }
]}
/>
</View>
</View>
);
// Game statistics
const [gameStats, setGameStats] = useState({
totalBubbles: 0,
totalGames: 0,
bestStreak: 0,
currentStreak: 0,
powerupsUsed: 0,
});
// Update statistics
const updateStats = (stat, value) => {
setGameStats(prev => ({
...prev,
[stat]: prev[stat] + value,
}));
};
// Achievement system
const ACHIEVEMENTS = {
FIRST_POP: { name: 'First Pop!', description: 'Pop your first bubble' },
HUNDRED_SCORE: { name: 'Century!', description: 'Score 100 points' },
STREAK_MASTER: { name: 'Streak Master', description: 'Pop 10 bubbles in a row' },
POWERUP_COLLECTOR: { name: 'Collector', description: 'Use 5 powerups' },
};
const [unlockedAchievements, setUnlockedAchievements] = useState([]);
const checkAchievements = () => {
// Check and unlock achievements based on stats
if (score >= 100 && !unlockedAchievements.includes('HUNDRED_SCORE')) {
unlockAchievement('HUNDRED_SCORE');
}
};
const unlockAchievement = (achievementId) => {
setUnlockedAchievements(prev => [...prev, achievementId]);
Alert.alert('Achievement Unlocked!', ACHIEVEMENTS[achievementId].name);
};
// Leaderboard system
const [leaderboard, setLeaderboard] = useState([]);
const submitScore = async (playerName, score) => {
try {
const newEntry = {
id: Date.now(),
name: playerName,
score,
date: new Date().toLocaleDateString(),
};
const updatedLeaderboard = [...leaderboard, newEntry]
.sort((a, b) => b.score - a.score)
.slice(0, 10); // Top 10
setLeaderboard(updatedLeaderboard);
await AsyncStorage.setItem('gameLeaderboard', JSON.stringify(updatedLeaderboard));
} catch (error) {
console.error('Error submitting score:', error);
}
};
// Share score function
const shareScore = () => {
const message = `I just scored ${score} points in Bubble Popper! Can you beat it? 🎮`;
// Implement sharing logic
};
Game Successfully Built If:
Essential Game Loop:
Polish Elements:
Time Investment: 60 minutes total Difficulty Level: Intermediate Prerequisites: React Native basics, understanding of game loops Tools Needed: Physical device for testing (haptics work better on device)