├── 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 |
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 |
--------------------------------------------------------------------------------