├── src ├── components │ ├── start-game.tsx │ ├── start-coins.tsx │ ├── background.tsx │ ├── game-over.tsx │ ├── get-ready.tsx │ ├── pause-button.tsx │ ├── bird.tsx │ ├── score.tsx │ └── obstacles.tsx ├── assets │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── base.png │ ├── pause.png │ ├── play.png │ ├── gameover.png │ ├── message.png │ ├── pipe-red.png │ ├── audio │ │ ├── die.wav │ │ ├── hit.wav │ │ ├── wing.wav │ │ ├── point.wav │ │ └── swoosh.wav │ ├── gold-coin.png │ ├── pipe-green.png │ ├── star-coin.png │ ├── background-day.png │ ├── bluebird-upflap.png │ ├── pipe-green-down.png │ ├── redbird-midflap.png │ ├── redbird-upflap.png │ ├── background-night.png │ ├── bluebird-downflap.png │ ├── bluebird-midflap.png │ ├── redbird-downflap.png │ ├── yellowbird-midflap.png │ ├── yellowbird-upflap.png │ └── yellowbird-downflap.png ├── helpers │ └── sound.ts ├── hooks │ ├── useGameOverEffect.ts │ ├── useGameStateEffect.ts │ └── useGameOver.ts └── store │ ├── game-state.ts │ └── bird.ts ├── types.d.ts ├── tsconfig.json ├── README.md ├── babel.config.js ├── app.config.js ├── package.json ├── .gitignore └── App.tsx /src/components/start-game.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@env" { 2 | export const SENTRY_DSN: string; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "expo/tsconfig.base" 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/0.png -------------------------------------------------------------------------------- /src/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/1.png -------------------------------------------------------------------------------- /src/assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/2.png -------------------------------------------------------------------------------- /src/assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/3.png -------------------------------------------------------------------------------- /src/assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/4.png -------------------------------------------------------------------------------- /src/assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/5.png -------------------------------------------------------------------------------- /src/assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/6.png -------------------------------------------------------------------------------- /src/assets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/7.png -------------------------------------------------------------------------------- /src/assets/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/8.png -------------------------------------------------------------------------------- /src/assets/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/9.png -------------------------------------------------------------------------------- /src/assets/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/base.png -------------------------------------------------------------------------------- /src/assets/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/pause.png -------------------------------------------------------------------------------- /src/assets/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/play.png -------------------------------------------------------------------------------- /src/assets/gameover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/gameover.png -------------------------------------------------------------------------------- /src/assets/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/message.png -------------------------------------------------------------------------------- /src/assets/pipe-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/pipe-red.png -------------------------------------------------------------------------------- /src/assets/audio/die.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/audio/die.wav -------------------------------------------------------------------------------- /src/assets/audio/hit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/audio/hit.wav -------------------------------------------------------------------------------- /src/assets/audio/wing.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/audio/wing.wav -------------------------------------------------------------------------------- /src/assets/gold-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/gold-coin.png -------------------------------------------------------------------------------- /src/assets/pipe-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/pipe-green.png -------------------------------------------------------------------------------- /src/assets/star-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/star-coin.png -------------------------------------------------------------------------------- /src/assets/audio/point.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/audio/point.wav -------------------------------------------------------------------------------- /src/assets/audio/swoosh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/audio/swoosh.wav -------------------------------------------------------------------------------- /src/assets/background-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/background-day.png -------------------------------------------------------------------------------- /src/assets/bluebird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/bluebird-upflap.png -------------------------------------------------------------------------------- /src/assets/pipe-green-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/pipe-green-down.png -------------------------------------------------------------------------------- /src/assets/redbird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/redbird-midflap.png -------------------------------------------------------------------------------- /src/assets/redbird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/redbird-upflap.png -------------------------------------------------------------------------------- /src/assets/background-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/background-night.png -------------------------------------------------------------------------------- /src/assets/bluebird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/bluebird-downflap.png -------------------------------------------------------------------------------- /src/assets/bluebird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/bluebird-midflap.png -------------------------------------------------------------------------------- /src/assets/redbird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/redbird-downflap.png -------------------------------------------------------------------------------- /src/assets/yellowbird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/yellowbird-midflap.png -------------------------------------------------------------------------------- /src/assets/yellowbird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/yellowbird-upflap.png -------------------------------------------------------------------------------- /src/assets/yellowbird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponikar/try-flappy-bird-2D/HEAD/src/assets/yellowbird-downflap.png -------------------------------------------------------------------------------- /src/components/start-coins.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const StarCoins = () => { 4 | return null; 5 | }; 6 | 7 | const StarCoin = () => {}; 8 | -------------------------------------------------------------------------------- /src/helpers/sound.ts: -------------------------------------------------------------------------------- 1 | import { Audio } from "expo-av"; 2 | 3 | export const playSound = async (url: any) => { 4 | const soundObject = new Audio.Sound(); 5 | try { 6 | await soundObject.loadAsync(url); 7 | soundObject.playAsync(); 8 | } catch (error) { 9 | console.log("Error playing sound", error); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flappy bird game built in react native skia. 2 | 3 | ### Things to do 4 | 5 | - ✅ Add Score functionalities 6 | - ✅ Pause functionality 7 | - ✅ GameOver 8 | - ✅ Remove the obstacles that are off the screens 9 | - ✅ Sound 10 | - Audit the app 11 | - React bug [fix](https://github.com/facebook/react/issues/18178#issuecomment-595846312) 12 | -------------------------------------------------------------------------------- /src/hooks/useGameOverEffect.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useGameState } from "../store/game-state"; 3 | 4 | export const useGameOverEffect = (cb: () => void) => { 5 | const state = useGameState(); 6 | 7 | useEffect(() => { 8 | // reset changes when game is restarting 9 | if (state === "ideal") { 10 | cb(); 11 | } 12 | }, [state]); 13 | }; 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | "react-native-reanimated/plugin", 7 | [ 8 | "module:react-native-dotenv", 9 | { 10 | moduleName: "@env", 11 | path: ".env", 12 | blacklist: null, 13 | whitelist: null, 14 | safe: false, 15 | allowUndefined: true, 16 | }, 17 | ], 18 | ], 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/background.tsx: -------------------------------------------------------------------------------- 1 | import { Image, useImage } from "@shopify/react-native-skia"; 2 | import React from "react"; 3 | import { Dimensions } from "react-native"; 4 | 5 | const { width, height } = Dimensions.get("window"); 6 | 7 | export const GameBackground = () => { 8 | const imageBackground = useImage(require("../assets/background-day.png")); 9 | 10 | if (!imageBackground) return null; 11 | return ( 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/game-over.tsx: -------------------------------------------------------------------------------- 1 | import { useImage, Image } from "@shopify/react-native-skia"; 2 | import { Dimensions } from "react-native"; 3 | import { useGameState } from "../store/game-state"; 4 | 5 | const { width, height } = Dimensions.get("screen"); 6 | export const GameOver = () => { 7 | const image = useImage(require("../assets/gameover.png")); 8 | 9 | const state = useGameState(); 10 | 11 | if (!image) return null; 12 | if (state !== "game-over") return; 13 | return ( 14 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app.config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config({ 3 | path: "./.env", 4 | }); 5 | 6 | console.log("CONFIGURED DOT ENV"); 7 | console.log(process.env.SENTRY_AUTH_TOKEN); 8 | console.log(process.env.SENTRY_PROJECT); 9 | console.log(process.env.SENTRY_ORG); 10 | module.exports = { 11 | expo: { 12 | name: "Flappy Bird", 13 | slug: "flappy-bird", 14 | plugins: ["sentry-expo"], 15 | version: "0.0.1", 16 | hooks: { 17 | postPublish: [ 18 | { 19 | file: "sentry-expo/upload-sourcemaps", 20 | config: { 21 | organization: process.env.SENTRY_ORG, 22 | project: process.env.SENTRY_PROJECT, 23 | authToken: process.env.SENTRY_AUTH_TOKEN, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/hooks/useGameStateEffect.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { useGameState } from "../store/game-state"; 3 | 4 | export const useGameStateEffect = ( 5 | callback: () => void, 6 | timer: number = 500 7 | ) => { 8 | const gameState = useGameState(); 9 | const timeInterval = useRef(); 10 | 11 | useEffect(() => { 12 | if (gameState === "running" || gameState === "resumed") { 13 | timeInterval.current = setInterval(callback, timer); 14 | } else if (gameState === "paused") { 15 | clearInterval(timeInterval.current); 16 | } else if (gameState === "game-over") { 17 | clearInterval(timeInterval.current); 18 | // reset the game 19 | } 20 | return () => clearInterval(timeInterval.current); 21 | }, [gameState]); 22 | 23 | return timeInterval; 24 | }; 25 | -------------------------------------------------------------------------------- /src/store/game-state.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface GameState { 4 | state: "paused" | "game-over" | "ideal" | "resumed" | "running"; 5 | actions: { 6 | gameOver: () => void; 7 | gamePaused: () => void; 8 | gameResumed: () => void; 9 | gameStarted: () => void; 10 | gameRestarted: () => void; 11 | }; 12 | } 13 | 14 | const useGameStore = create((set) => ({ 15 | state: "ideal", 16 | actions: { 17 | gameOver: () => set({ state: "game-over" }), 18 | gamePaused: () => set({ state: "paused" }), 19 | gameResumed: () => { 20 | set({ state: "resumed" }); 21 | setTimeout(() => set({ state: "running" }), 1); 22 | }, 23 | gameStarted: () => set({ state: "running" }), 24 | gameRestarted: () => set({ state: "ideal" }), 25 | }, 26 | })); 27 | 28 | export const useGameState = () => useGameStore((state) => state.state); 29 | 30 | export const useGameActions = () => useGameStore((state) => state.actions); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@sentry/react-native": "4.9.0", 4 | "@shopify/react-native-skia": "0.1.157", 5 | "expo": "^47.0.0", 6 | "expo-application": "~5.0.1", 7 | "expo-av": "~13.0.3", 8 | "expo-constants": "~14.0.2", 9 | "expo-device": "~5.0.0", 10 | "expo-updates": "~0.15.6", 11 | "react": "18.1.0", 12 | "react-dom": "18.1.0", 13 | "react-native": "0.70.5", 14 | "react-native-dotenv": "^3.4.8", 15 | "react-native-reanimated": "~2.12.0", 16 | "react-native-web": "~0.18.7", 17 | "sentry-expo": "~6.0.0", 18 | "zustand": "^4.3.4" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.19.3", 22 | "@types/react": "~18.0.24", 23 | "@types/react-native": "~0.70.6", 24 | "dotenv": "^16.0.3", 25 | "typescript": "^4.6.3" 26 | }, 27 | "scripts": { 28 | "start": "expo start", 29 | "android": "expo start --android", 30 | "ios": "expo start --ios", 31 | "web": "expo start --web" 32 | }, 33 | "version": "1.0.0", 34 | "private": true, 35 | "name": "try-flappy-bird" 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | 34 | # node.js 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | yarn-error.log 39 | 40 | # BUCK 41 | buck-out/ 42 | \.buckd/ 43 | *.keystore 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/ 51 | 52 | */fastlane/report.xml 53 | */fastlane/Preview.html 54 | */fastlane/screenshots 55 | 56 | # Bundle artifacts 57 | *.jsbundle 58 | 59 | # CocoaPods 60 | /ios/Pods/ 61 | 62 | # Expo 63 | .expo/* 64 | web-build/ 65 | .env -------------------------------------------------------------------------------- /src/store/bird.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { playSound } from "../helpers/sound"; 3 | 4 | interface Bird { 5 | state: { 6 | y: number; 7 | x: number; 8 | width: number; 9 | height: number; 10 | }; 11 | actions: { 12 | jump: () => void; 13 | keepFalling: () => void; 14 | resetBird: () => void; 15 | }; 16 | } 17 | 18 | const BIRD_INITIAL_STATE: Bird["state"] = { 19 | y: 0, 20 | x: 150, 21 | width: 50, 22 | height: 50, 23 | }; 24 | 25 | const useBirdStore = create((set) => ({ 26 | state: BIRD_INITIAL_STATE, 27 | actions: { 28 | jump: () => { 29 | playSound(require("../assets/audio/wing.wav")); 30 | set((data) => { 31 | if (data.state.y <= 0) return data; 32 | return { ...data, state: { ...data.state, y: data.state.y - 40 } }; 33 | }); 34 | }, 35 | keepFalling: () => 36 | set((data) => { 37 | return { ...data, state: { ...data.state, y: data.state.y + 40 } }; 38 | }), 39 | resetBird: () => set({ state: BIRD_INITIAL_STATE }), 40 | }, 41 | })); 42 | 43 | export const useBird = () => useBirdStore((state) => state.state); 44 | export const useBirdActions = () => useBirdStore((state) => state.actions); 45 | -------------------------------------------------------------------------------- /src/components/get-ready.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Image, useImage } from "@shopify/react-native-skia"; 2 | import React from "react"; 3 | import { Dimensions, Pressable } from "react-native"; 4 | import { useGameActions, useGameState } from "../store/game-state"; 5 | 6 | const { width, height } = Dimensions.get("screen"); 7 | 8 | export const GetReady = () => { 9 | const image = useImage(require("../assets/message.png")); 10 | const state = useGameState(); 11 | if (!image || state !== "ideal") return null; 12 | return ( 13 | 21 | ); 22 | }; 23 | 24 | export const GetReadyClickArea = () => { 25 | const actions = useGameActions(); 26 | 27 | const state = useGameState(); 28 | 29 | const onPress = () => { 30 | actions.gameStarted(); 31 | }; 32 | 33 | if (state !== "ideal") return null; 34 | return ( 35 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/pause-button.tsx: -------------------------------------------------------------------------------- 1 | import { useImage, Image } from "@shopify/react-native-skia"; 2 | import React from "react"; 3 | import { Dimensions, Pressable, TouchableOpacity } from "react-native"; 4 | import { useGameActions, useGameState } from "../store/game-state"; 5 | 6 | const { width: screenWidth } = Dimensions.get("screen"); 7 | export const Pause = () => { 8 | const pause = useImage(require("../assets/pause.png")); 9 | const play = useImage(require("../assets/play.png")); 10 | 11 | const state = useGameState(); 12 | if (!pause || !play || state === "ideal") return null; 13 | 14 | if (state === "running") 15 | return ( 16 | 24 | ); 25 | 26 | return ( 27 | 35 | ); 36 | }; 37 | 38 | export const PauseButtonArea = () => { 39 | const gameActions = useGameActions(); 40 | const state = useGameState(); 41 | 42 | const onPress = () => { 43 | if (state === "paused") { 44 | gameActions.gameResumed(); 45 | } else { 46 | gameActions.gamePaused(); 47 | } 48 | }; 49 | 50 | if (state === "ideal") return null; 51 | 52 | return ( 53 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/bird.tsx: -------------------------------------------------------------------------------- 1 | import { Image, useImage } from "@shopify/react-native-skia"; 2 | import React, { FC, useEffect, useRef, useState } from "react"; 3 | import { useGameOverEffect } from "../hooks/useGameOverEffect"; 4 | import { useBird, useBirdActions } from "../store/bird"; 5 | 6 | interface BirdProps {} 7 | 8 | export const Bird: FC = () => { 9 | const { y, width, height, x } = useBird(); 10 | const ideal = useImage(require("../assets/bluebird-midflap.png")); 11 | const goingdown = useImage(require("../assets/bluebird-downflap.png")); 12 | const goingup = useImage(require("../assets/bluebird-upflap.png")); 13 | 14 | const [birdState, setBirdState] = useState<"ideal" | "goingup" | "goingdown">( 15 | "ideal" 16 | ); 17 | 18 | const { resetBird } = useBirdActions(); 19 | 20 | useGameOverEffect(resetBird); 21 | 22 | const posYRef = useRef(0); 23 | 24 | useEffect(() => { 25 | if (y < posYRef.current) { 26 | setBirdState("goingup"); 27 | } else if (y > posYRef.current) { 28 | setBirdState("goingdown"); 29 | } else { 30 | setBirdState("ideal"); 31 | } 32 | posYRef.current = y; 33 | }, [y]); 34 | 35 | if (!ideal) return null; 36 | 37 | if (birdState === "goingup") { 38 | return ( 39 | 47 | ); 48 | } 49 | 50 | if (birdState === "goingdown") { 51 | return ( 52 | 60 | ); 61 | } 62 | 63 | return ( 64 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/hooks/useGameOver.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { Dimensions } from "react-native"; 3 | import { playSound } from "../helpers/sound"; 4 | import { useBird } from "../store/bird"; 5 | import { useGameActions } from "../store/game-state"; 6 | 7 | interface Object { 8 | x: number; 9 | y: number; 10 | width: number; 11 | height: number; 12 | } 13 | 14 | const checkCollision = (obj1: Object, obj2: Object) => { 15 | // Get the bounding box coordinates for obj1 16 | let obj1_left = obj1.x; 17 | let obj1_top = obj1.y; 18 | let obj1_right = obj1.x + obj1.width; 19 | let obj1_bottom = obj1.y + obj1.height; 20 | 21 | // Get the bounding box coordinates for obj2 22 | let obj2_left = obj2.x; 23 | let obj2_top = obj2.y; 24 | let obj2_right = obj2.x + obj2.width; 25 | let obj2_bottom = obj2.y + obj2.height; 26 | 27 | // Check for overlap in the x-dimension 28 | let x_overlap = false; 29 | if (obj1_left < obj2_right && obj1_right > obj2_left) { 30 | x_overlap = true; 31 | } 32 | 33 | // Check for overlap in the y-dimension 34 | let y_overlap = false; 35 | if (obj1_top < obj2_bottom && obj1_bottom > obj2_top) { 36 | y_overlap = true; 37 | } 38 | 39 | // Return true if there is overlap in both dimensions 40 | if (x_overlap && y_overlap) { 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | 47 | const { height } = Dimensions.get("screen"); 48 | 49 | export const useGameOver = (pipe: Object) => { 50 | const bird = useBird(); 51 | const { gameOver } = useGameActions(); 52 | const isGameOver = checkCollision(bird, pipe); 53 | 54 | const isSoundPlayed = useRef(false); 55 | 56 | const isTouchingGround = bird.y >= height; 57 | 58 | if (isGameOver || isTouchingGround) { 59 | if (!isSoundPlayed.current) { 60 | playSound(require("../assets/audio/hit.wav")); 61 | setTimeout(() => playSound(require("../assets/audio/die.wav")), 1000); 62 | isSoundPlayed.current = true; 63 | } 64 | 65 | gameOver(); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/score.tsx: -------------------------------------------------------------------------------- 1 | import { Image, SkImage, useImage } from "@shopify/react-native-skia"; 2 | import React, { FC, memo } from "react"; 3 | import { useGameOverEffect } from "../hooks/useGameOverEffect"; 4 | import { useGameStateEffect } from "../hooks/useGameStateEffect"; 5 | 6 | const CacheImages: Record = {}; 7 | 8 | export const Score = () => { 9 | const [currentScore, setCurrentScore] = React.useState(0); 10 | 11 | useGameStateEffect(() => setCurrentScore((oldScore) => oldScore + 20)); 12 | 13 | useGameOverEffect(() => { 14 | setCurrentScore((score) => { 15 | // store the score somewhere 16 | 17 | return 0; 18 | }); 19 | }); 20 | 21 | const digits = currentScore 22 | .toString() 23 | .split("") 24 | .map((digit) => Number(digit)); 25 | 26 | return ( 27 | <> 28 | {digits.map((digit, index) => { 29 | if (CacheImages[digit]) { 30 | return ( 31 | 40 | ); 41 | } 42 | 43 | return ; 44 | })} 45 | 46 | ); 47 | }; 48 | 49 | const DigitImages: Record = { 50 | 0: require("../assets/0.png"), 51 | 1: require("../assets/1.png"), 52 | 2: require("../assets/2.png"), 53 | 3: require("../assets/3.png"), 54 | 4: require("../assets/4.png"), 55 | 5: require("../assets/5.png"), 56 | 6: require("../assets/6.png"), 57 | 7: require("../assets/7.png"), 58 | 8: require("../assets/8.png"), 59 | 9: require("../assets/9.png"), 60 | }; 61 | 62 | const Digit: FC<{ digit: number; index: number }> = memo(({ digit, index }) => { 63 | const image = useImage(DigitImages[digit]); 64 | 65 | if (!image) return null; 66 | 67 | CacheImages[digit] = image; 68 | 69 | return ( 70 | 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@shopify/react-native-skia"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { Pressable, StyleSheet } from "react-native"; 4 | import { GameBackground } from "./src/components/background"; 5 | import { Bird } from "./src/components/bird"; 6 | import { GameOver } from "./src/components/game-over"; 7 | import { GetReady, GetReadyClickArea } from "./src/components/get-ready"; 8 | import { Obstacles } from "./src/components/obstacles"; 9 | import { Pause, PauseButtonArea } from "./src/components/pause-button"; 10 | import { Score } from "./src/components/score"; 11 | import { useGameStateEffect } from "./src/hooks/useGameStateEffect"; 12 | import { useBirdActions } from "./src/store/bird"; 13 | import { useGameActions, useGameState } from "./src/store/game-state"; 14 | import { SENTRY_DSN } from "@env"; 15 | import * as Sentry from "sentry-expo"; 16 | 17 | if (!__DEV__) { 18 | Sentry.init({ 19 | dsn: SENTRY_DSN, 20 | enableInExpoDevelopment: true, 21 | debug: true, 22 | }); 23 | } 24 | const BIRD_FALLING_SPEED = 300; 25 | export default function App() { 26 | const { keepFalling, jump } = useBirdActions(); 27 | 28 | const timeout = useRef(null); 29 | 30 | const state = useGameState(); 31 | 32 | const timer = useGameStateEffect(keepFalling, BIRD_FALLING_SPEED); 33 | const gameActions = useGameActions(); 34 | 35 | useEffect(() => { 36 | return () => { 37 | clearInterval(timer.current); 38 | clearTimeout(timeout.current); 39 | }; 40 | }, []); 41 | 42 | const handlePress = () => { 43 | if (state === "paused") return; 44 | 45 | if (state === "game-over") { 46 | return gameActions.gameRestarted(); 47 | } 48 | 49 | clearTimeout(timeout.current); 50 | clearInterval(timer.current); 51 | jump(); 52 | 53 | timeout.current = setTimeout(() => { 54 | timer.current = setInterval(() => { 55 | keepFalling(); 56 | }, BIRD_FALLING_SPEED); 57 | }, 200); 58 | }; 59 | return ( 60 | <> 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | 78 | const styles = StyleSheet.create({ 79 | container: { 80 | flex: 1, 81 | backgroundColor: "#fff", 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/obstacles.tsx: -------------------------------------------------------------------------------- 1 | import { useImage, Image } from "@shopify/react-native-skia"; 2 | import React, { FC } from "react"; 3 | import { Dimensions } from "react-native"; 4 | import { useGameOver } from "../hooks/useGameOver"; 5 | import { useGameOverEffect } from "../hooks/useGameOverEffect"; 6 | import { useGameStateEffect } from "../hooks/useGameStateEffect"; 7 | import { useGameActions } from "../store/game-state"; 8 | 9 | const currentObstacleXPosition: Record = {}; 10 | 11 | const MAX_HEIGHT = 400; 12 | const MIN_HEIGHT = 300; 13 | 14 | const MAX_WIDTH = 75; 15 | const MIN_WIDTH = 50; 16 | 17 | interface Obstacle { 18 | id: number; 19 | x: number; 20 | y: number; 21 | width: number; 22 | height: number; 23 | 24 | type: "light" | "dark"; 25 | } 26 | 27 | const { width: screenWidth, height: screenHeight } = Dimensions.get("window"); 28 | 29 | export const Obstacles = () => { 30 | const [obstacles, setObstacles] = React.useState([]); 31 | 32 | const removingObstacle = React.useRef(false); 33 | const generateObstacles = () => { 34 | if (removingObstacle.current) { 35 | console.log("CAN'T GENERATE OBSTACLES WHILE REMOVING ONE"); 36 | return; 37 | } 38 | 39 | console.log("ADDING OBSTACLE"); 40 | 41 | setObstacles((o) => { 42 | const height = Math.random() * (MAX_HEIGHT - MIN_HEIGHT) + MIN_HEIGHT; 43 | return [ 44 | ...o, 45 | { 46 | id: new Date().getTime(), 47 | y: Math.floor(Math.random() * 2) === 1 ? 0 : screenHeight - height, 48 | x: screenWidth + 150, 49 | width: Math.random() * (MAX_WIDTH - MIN_WIDTH) + MIN_WIDTH, 50 | height, 51 | type: "light", 52 | }, 53 | ]; 54 | }); 55 | }; 56 | useGameStateEffect(generateObstacles, 1500); 57 | 58 | console.log("SO FAR LENGTH", obstacles.length); 59 | 60 | useGameStateEffect(() => { 61 | console.log("REMOVING OBSTACLE"); 62 | removingObstacle.current = true; 63 | setObstacles((o) => { 64 | const updatedObs = o.map((obstacle) => { 65 | if (currentObstacleXPosition[obstacle.id] <= -obstacle.width) { 66 | return null; 67 | } 68 | return { ...obstacle, x: currentObstacleXPosition[obstacle.id] }; 69 | }); 70 | return updatedObs.filter((obstacle) => obstacle !== null); 71 | }); 72 | 73 | setTimeout(() => (removingObstacle.current = false), 500); 74 | }, 6000); 75 | useGameOverEffect(() => setObstacles([])); 76 | 77 | return ( 78 | <> 79 | {obstacles.map((o) => ( 80 | 81 | ))} 82 | 83 | ); 84 | }; 85 | 86 | const Obstacle: FC<{ 87 | object: Obstacle; 88 | }> = ({ object }) => { 89 | const pipeUp = useImage(require("../assets/pipe-green.png")); 90 | const pipeDown = useImage(require("../assets/pipe-green-down.png")); 91 | const [x, setX] = React.useState(object.x); 92 | 93 | useGameOver({ 94 | x, 95 | y: object.y, 96 | width: object.width, 97 | height: object.height, 98 | }); 99 | 100 | const isUnMounted = React.useRef(false); 101 | 102 | useGameStateEffect(() => { 103 | if (!isUnMounted.current) { 104 | setX((x) => { 105 | currentObstacleXPosition[object.id] = x - 10; 106 | return x - 10; 107 | }); 108 | } 109 | }, 100); 110 | 111 | React.useEffect(() => { 112 | return () => { 113 | isUnMounted.current = true; 114 | }; 115 | }, []); 116 | 117 | if (!pipeDown || !pipeUp) return null; 118 | 119 | return ( 120 | 128 | ); 129 | }; 130 | --------------------------------------------------------------------------------