├── .gitignore ├── .netlify └── state.json ├── App.js ├── LICENSE ├── README.md ├── app.json ├── assets ├── audio │ ├── hit.mp3 │ ├── point.mp3 │ └── wing.mp3 ├── icons │ ├── app-icon.png │ ├── loading-icon.png │ └── preview.jpeg └── spritesheet.png ├── babel.config.js ├── components ├── DisableBodyScrollingView.js ├── ExpoButton.js ├── GithubButton.js ├── KeyboardControlsView.js ├── Link.js ├── Link.web.js └── logyo.js ├── package.json ├── src ├── game.js ├── setupSpriteSheetAsync.js └── sprites.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | 64 | #expo 65 | web-build/ 66 | web-report/ 67 | .expo/ -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "80f353d9-8f0e-4034-91cd-cfd6886d5981" 3 | } -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { GLView } from 'expo'; 2 | import * as React from 'react'; 3 | import { Text, TouchableWithoutFeedback, View } from 'react-native'; 4 | 5 | import DisableBodyScrollingView from './components/DisableBodyScrollingView'; 6 | import ExpoButton from './components/ExpoButton'; 7 | import GithubButton from './components/GithubButton'; 8 | import KeyboardControlsView from './components/KeyboardControlsView'; 9 | import logyo from './components/logyo'; 10 | import Game from './src/game'; 11 | 12 | logyo('https://twitter.com/baconbrix'); 13 | export default class App extends React.Component { 14 | state = { 15 | score: 0, 16 | }; 17 | render() { 18 | const { style, ...props } = this.props; 19 | return ( 20 | 23 | 24 | { 26 | if (this.game) { 27 | if (code === 'Space') { 28 | this.game.onPress(); 29 | } 30 | } 31 | }} 32 | > 33 | { 35 | if (this.game) this.game.onPress(); 36 | }} 37 | > 38 | { 41 | this.game = new Game(context); 42 | this.game.onScore = score => this.setState({ score }); 43 | }} 44 | /> 45 | 46 | 47 | {this.state.score} 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | const Score = ({ children }) => ( 58 | 70 | {children} 71 | 72 | ); 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Evan Bacon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | Flappy Bird 🐦 8 | 9 | Universal Expo App 10 | 11 |

12 | 13 | Expo & PIXI.js 14 | 15 | Try it now: https://flappybacon.netlify.com 16 | 17 | [fb]: https://flappybacon.netlify.com 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Flappy Bird", 4 | "description": "A side-scroller where the player controls a bird, attempting to fly between columns of green pipes without hitting them.", 5 | "slug": "flappy-bird", 6 | "privacy": "unlisted", 7 | "sdkVersion": "32.0.0", 8 | "version": "1.0.0", 9 | "orientation": "portrait", 10 | "primaryColor": "#91DE78", 11 | "icon": "./assets/icons/app-icon.png", 12 | "splash": { 13 | "image": "./assets/icons/app-icon.png", 14 | "backgroundColor": "#91DE78" 15 | }, 16 | "packagerOpts": { 17 | "assetExts": ["ttf", "mp4", "otf", "xml"] 18 | }, 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "platforms": ["android", "ios", "web"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/audio/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/audio/hit.mp3 -------------------------------------------------------------------------------- /assets/audio/point.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/audio/point.mp3 -------------------------------------------------------------------------------- /assets/audio/wing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/audio/wing.mp3 -------------------------------------------------------------------------------- /assets/icons/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/icons/app-icon.png -------------------------------------------------------------------------------- /assets/icons/loading-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/icons/loading-icon.png -------------------------------------------------------------------------------- /assets/icons/preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/icons/preview.jpeg -------------------------------------------------------------------------------- /assets/spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-flappy-bird/6d9602d13dbaed4e6a0a10d852a31aa00e3575be/assets/spritesheet.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/DisableBodyScrollingView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import { View } from 'react-native'; 4 | 5 | const getElement = component => { 6 | try { 7 | return findDOMNode(component); 8 | } catch (e) { 9 | return component; 10 | } 11 | }; 12 | 13 | const freezeBody = e => { 14 | e.preventDefault(); 15 | }; 16 | 17 | class DisableBodyScrollingView extends React.PureComponent { 18 | componentWillUnmount() { 19 | if (this.view) { 20 | this.view.removeEventListener('touchstart', freezeBody, false); 21 | this.view.removeEventListener('touchmove', freezeBody, false); 22 | } 23 | } 24 | 25 | render() { 26 | const { style, ...props } = this.props; 27 | 28 | return ( 29 | { 33 | const nextView = getElement(view); 34 | if (nextView && nextView.addEventListener) { 35 | nextView.addEventListener('touchstart', freezeBody, false); 36 | nextView.addEventListener('touchmove', freezeBody, false); 37 | } 38 | if (this.view && this.view.removeEventListener) { 39 | this.view.removeEventListener('touchstart', freezeBody, false); 40 | this.view.removeEventListener('touchmove', freezeBody, false); 41 | } 42 | this.view = nextView; 43 | }} 44 | {...props} 45 | /> 46 | ); 47 | } 48 | } 49 | 50 | export default DisableBodyScrollingView; 51 | -------------------------------------------------------------------------------- /components/ExpoButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, Text, View } from 'react-native'; 3 | 4 | import Link from './Link'; 5 | 6 | export default () => ( 7 | 17 | 18 | 25 | Expo 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /components/GithubButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image } from 'react-native'; 3 | 4 | import Link from './Link'; 5 | 6 | export default () => ( 7 | 18 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /components/KeyboardControlsView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class KeyboardControlsView extends React.PureComponent { 4 | static defaultProps = { 5 | onKeyDown: () => {}, 6 | onKeyUp: () => {}, 7 | }; 8 | 9 | componentDidMount() { 10 | window.addEventListener('keydown', this.onKeyDown, false); 11 | window.addEventListener('keyup', this.onKeyUp, false); 12 | } 13 | componentWillUnmount() { 14 | window.removeEventListener('keydown', this.onKeyDown); 15 | window.removeEventListener('keyup', this.onKeyUp); 16 | } 17 | 18 | onKeyDown = e => { 19 | this.props.onKeyDown(e); 20 | }; 21 | 22 | onKeyUp = e => { 23 | this.props.onKeyUp(e); 24 | }; 25 | 26 | render() { 27 | return this.props.children; 28 | } 29 | } 30 | 31 | export default KeyboardControlsView; 32 | -------------------------------------------------------------------------------- /components/Link.js: -------------------------------------------------------------------------------- 1 | import { Linking } from 'expo'; 2 | import React from 'react'; 3 | import { TouchableOpacity } from 'react-native'; 4 | 5 | export default ({ url, onPress, ...props }) => ( 6 | { 8 | if (url) { 9 | Linking.openURL(url); 10 | } 11 | if (onPress) { 12 | onPress(); 13 | } 14 | }} 15 | {...props} 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /components/Link.web.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ url, ...props }) => ; 4 | -------------------------------------------------------------------------------- /components/logyo.js: -------------------------------------------------------------------------------- 1 | import { Constants } from 'expo'; 2 | 3 | let saidHello = false; 4 | 5 | // From PIXI.js 6 | export default function(type) { 7 | if (saidHello) { 8 | return; 9 | } 10 | 11 | if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) { 12 | console.log( 13 | '\n %c %c %c Expo Web ' + 14 | Constants.expoVersion + 15 | ' %c %c ' + 16 | type + 17 | ' %c %c \n\n', 18 | 'background: #4630EB; padding:5px 0;', 19 | 'background: #4630EB; padding:5px 0;', 20 | 'color: #ffffff; background: #030307; padding:5px 0;', 21 | 'background: #4630EB; padding:5px 0;', 22 | 'background: #4630EB; padding:5px 0;', 23 | 'background: #4630EB; padding:5px 0;', 24 | 'color: #4630EB; background: #fff; padding:5px 0;', 25 | 'color: #4630EB; background: #fff; padding:5px 0;', 26 | ); 27 | } else if (window.console) { 28 | console.log( 29 | 'Expo Web ' + Constants.expoVersion + ' - ' + type + ' - www.expo.io', 30 | ); 31 | } 32 | 33 | saidHello = true; 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flappy-bird", 3 | "version": "0.0.0", 4 | "description": "A side-scroller where the player controls a bird, attempting to fly between columns of green pipes without hitting them.", 5 | "author": "Evan Bacon", 6 | "private": true, 7 | "main": "node_modules/expo/AppEntry.js", 8 | "scripts": { 9 | "serve": "serve ./web-build", 10 | "prepublish:web": "expo build:web", 11 | "publish:web": "netlify deploy" 12 | }, 13 | "dependencies": { 14 | "expo": "^33.0.0-alpha.web.1", 15 | "expo-pixi": "^1.1.0", 16 | "react": "16.8.6", 17 | "react-dom": "^16.8.6", 18 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 19 | "react-native-web": "^0.11.2" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-expo": "^5.1.1", 23 | "serve": "^11.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | import { PIXI } from 'expo-pixi'; 2 | import { Container, extras, Sprite } from 'pixi.js'; 3 | import { AsyncStorage, PixelRatio } from 'react-native'; 4 | 5 | import source from '../assets/spritesheet.png'; 6 | import setupSpriteSheetAsync from './setupSpriteSheetAsync'; 7 | import sprites from './sprites'; 8 | 9 | const { TilingSprite, AnimatedSprite } = extras; 10 | 11 | const scale = PixelRatio.get(); 12 | 13 | const Settings = { 14 | playerFallSpeed: 8 * scale, 15 | playerHorizontalPosition: 100 * scale, 16 | playerVerticalPosition: 200 * scale, 17 | playerMaxVelocity: -3 * scale, 18 | pipeWidth: 80 * scale, 19 | groundHeight: 100 * scale, 20 | pipeHeight: 500 * scale, 21 | playerGravity: 0.4 * scale, 22 | minPipeHeight: 50 * scale, 23 | pipeVerticalGap: 190 * scale, //180 is pretty legit 24 | gameSpeed: 40 * 0.25, 25 | }; 26 | 27 | class FlappySprite extends Sprite { 28 | constructor(...args) { 29 | super(...args); 30 | this.scale.set(scale); 31 | } 32 | } 33 | 34 | class Ground extends TilingSprite { 35 | constructor(texture) { 36 | super(texture, Settings.width, Settings.groundHeight); 37 | this.tileScale.set(scale * 2); 38 | this.position.x = 0; 39 | this.position.y = Settings.skyHeight; 40 | } 41 | } 42 | 43 | class Background extends FlappySprite { 44 | constructor(texture) { 45 | super(texture); 46 | this.position.x = 0; 47 | this.position.y = 0; 48 | this.width = Settings.width; 49 | this.height = Settings.height; 50 | } 51 | } 52 | 53 | function boxesIntersect(a, b, paddingA = 0) { 54 | const ab = a.getBounds(); 55 | ab.x += paddingA; 56 | ab.width -= paddingA * 2; 57 | ab.y += paddingA; 58 | ab.height -= paddingA * 2; 59 | 60 | const bb = b.getBounds(); 61 | return ( 62 | ab.x + ab.width > bb.x && 63 | ab.x < bb.x + bb.width && 64 | ab.y + ab.height > bb.y && 65 | ab.y < bb.y + bb.height 66 | ); 67 | } 68 | class PipeContainer extends Container { 69 | pipes = []; 70 | pipeIndex = 0; 71 | 72 | constructor(pipeTexture) { 73 | super(); 74 | this.pipeTexture = pipeTexture; 75 | this.position.x = Settings.width + Settings.pipeWidth / 2; 76 | } 77 | 78 | tryAddingNewPipe = () => { 79 | if (!this.pipes.length) return; 80 | const { pipe } = this.pipes[this.pipes.length - 1]; 81 | if (-pipe.position.x >= Settings.pipeHorizontalGap) { 82 | this.addNewPipe(); 83 | } 84 | }; 85 | 86 | moveAll = () => { 87 | let score = 0; 88 | for (let index = 0; index < this.pipes.length; index++) { 89 | this.move(index); 90 | if (this.tryScoringPipe(index)) { 91 | score += 1; 92 | } 93 | } 94 | return score; 95 | }; 96 | 97 | tryScoringPipe = index => { 98 | const group = this.pipes[index]; 99 | 100 | if ( 101 | !group.scored && 102 | this.toGlobal(group.pipe.position).x < Settings.playerHorizontalPosition 103 | ) { 104 | group.scored = true; 105 | return true; 106 | } 107 | return false; 108 | }; 109 | 110 | move = index => { 111 | const { pipe, pipe2 } = this.pipes[index]; 112 | pipe.position.x -= Settings.gameSpeed; 113 | pipe2.position.x -= Settings.gameSpeed; 114 | }; 115 | 116 | addNewPipe = () => { 117 | const pipeGroup = {}; 118 | const pipe = new Pipe(this.pipeTexture); 119 | const pipe2 = new Pipe(this.pipeTexture); 120 | pipe.rotation = Math.PI; 121 | 122 | const maxPosition = 123 | Settings.skyHeight - 124 | Settings.minPipeHeight - 125 | Settings.pipeVerticalGap - 126 | pipe.height / 2; 127 | const minPosition = -(pipe.height / 2 - Settings.minPipeHeight); 128 | 129 | pipe.position.y = Math.floor( 130 | Math.random() * (maxPosition - minPosition + 1) + minPosition, 131 | ); 132 | 133 | pipe2.position.y = pipe.height + pipe.position.y + Settings.pipeVerticalGap; 134 | pipe.position.x = pipe2.position.x = 0; 135 | 136 | pipeGroup.upper = pipe.position.y + pipe.height / 2; 137 | pipeGroup.lower = pipeGroup.upper + Settings.pipeVerticalGap; 138 | pipeGroup.pipe = pipe; 139 | pipeGroup.pipe2 = pipe2; 140 | 141 | this.addChild(pipe); 142 | this.addChild(pipe2); 143 | this.pipes.push(pipeGroup); 144 | this.tryRemovingLastGroup(); 145 | }; 146 | 147 | tryRemovingLastGroup = () => { 148 | if ( 149 | this.pipes[0].pipe.position.x + Settings.pipeWidth / 2 > 150 | Settings.width 151 | ) { 152 | this.pipes.shift(); 153 | } 154 | }; 155 | 156 | setXforGroup = (index, x) => { 157 | const { pipe, pipe2 } = this.pipes[index]; 158 | pipe.position.x = x; 159 | pipe2.position.x = x; 160 | }; 161 | 162 | getX = index => { 163 | const { pipe } = this.pipes[index]; 164 | return this.toGlobal(pipe.position).x; 165 | }; 166 | 167 | restart = () => { 168 | this.pipeIndex = 0; 169 | this.pipes = []; 170 | this.children = []; 171 | }; 172 | } 173 | 174 | class Pipe extends FlappySprite { 175 | constructor(texture) { 176 | super(texture); 177 | this.width = Settings.pipeWidth; 178 | this.height = Settings.pipeHeight; 179 | this.anchor.set(0.5); 180 | } 181 | } 182 | 183 | class Bird extends AnimatedSprite { 184 | constructor(textures) { 185 | super(textures); 186 | this.animationSpeed = 0.2; 187 | this.anchor.set(0.5); 188 | this.width = 60 * scale; 189 | this.height = 48 * scale; 190 | 191 | this.speedY = Settings.playerFallSpeed; 192 | this.rate = Settings.playerGravity; 193 | 194 | this.restart(); 195 | } 196 | 197 | restart = () => { 198 | this.play(); 199 | this.rotation = 0; 200 | this.position.x = Settings.playerHorizontalPosition; 201 | this.position.y = Settings.playerVerticalPosition; 202 | }; 203 | 204 | updateGravity = () => { 205 | this.position.y -= this.speedY; 206 | this.speedY -= this.rate; 207 | 208 | const FLAP = 35; 209 | this.rotation = -Math.min( 210 | Math.PI / 4, 211 | Math.max(-Math.PI / 2, (FLAP + this.speedY) / FLAP), 212 | ); 213 | }; 214 | } 215 | 216 | class Game { 217 | stopAnimating = true; 218 | isStarted = false; 219 | isDead = false; 220 | score = 0; 221 | 222 | constructor(context) { 223 | // Sharp pixels 224 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 225 | 226 | this.app = new PIXI.Application({ 227 | context, 228 | autoResize: false, 229 | width: context.drawingBufferWidth / 1, 230 | height: context.drawingBufferHeight / 1, 231 | }); 232 | this.app.ticker.add(this.animate); 233 | /* 234 | this.app.stage.interactive = true; 235 | this.app.stage.buttonMode = true; 236 | this.app.stage.on('mousedown', this.beginGame); 237 | this.app.stage.on('tap', this.beginGame); 238 | */ 239 | 240 | Settings.width = this.app.renderer.width; 241 | Settings.pipeScorePosition = -( 242 | Settings.width - Settings.playerHorizontalPosition 243 | ); 244 | Settings.height = this.app.renderer.height; 245 | Settings.skyHeight = Settings.height - Settings.groundHeight; 246 | Settings.pipeHorizontalGap = Settings.pipeWidth * 5; 247 | this.loadAsync(); 248 | } 249 | 250 | // Resize function window 251 | resize = ({ width, height, scale }) => { 252 | const parent = this.app.view.parentNode; 253 | // Resize the renderer 254 | // this.app.renderer.resize(width * scale, height * scale); 255 | 256 | // if (Platform.OS === 'web') { 257 | // this.app.view.style.width = width; 258 | // this.app.view.style.height = height; 259 | // } 260 | }; 261 | 262 | loadAsync = async () => { 263 | this.textures = await setupSpriteSheetAsync(source, sprites); 264 | this.onAssetsLoaded(); 265 | }; 266 | 267 | onAssetsLoaded = () => { 268 | this.background = new Background(this.textures.background); 269 | this.pipeContainer = new PipeContainer(this.textures.pipe); 270 | this.ground = new Ground(this.textures.ground); 271 | 272 | this.bird = new Bird([ 273 | this.textures['bird_000'], 274 | this.textures['bird_001'], 275 | this.textures['bird_002'], 276 | this.textures['bird_001'], 277 | ]); 278 | 279 | [this.background, this.pipeContainer, this.ground, this.bird].map(child => 280 | this.app.stage.addChild(child), 281 | ); 282 | 283 | this.stopAnimating = false; 284 | }; 285 | 286 | onPress = () => { 287 | if (this.isDead) { 288 | this.restart(); 289 | } else { 290 | this.beginGame(); 291 | } 292 | }; 293 | 294 | beginGame = () => { 295 | if (!this.isStarted) { 296 | this.isStarted = true; 297 | this.score = 0; 298 | this.onScore(this.score); 299 | this.pipeContainer.addNewPipe(); 300 | } 301 | this.bird.speedY = Settings.playerFallSpeed; 302 | }; 303 | 304 | animate = () => { 305 | if (this.stopAnimating) { 306 | return; 307 | } 308 | 309 | if (!this.isDead) { 310 | if (Math.abs(this.ground.tilePosition.x) > this.ground.width) { 311 | this.ground.tilePosition.x = 0; 312 | } 313 | this.ground.tilePosition.x -= Settings.gameSpeed; 314 | } 315 | 316 | if (this.isStarted) { 317 | this.bird.updateGravity(); 318 | } 319 | 320 | if (this.isDead) { 321 | this.bird.rotation += Math.PI / 4; 322 | if ( 323 | this.bird.rotation > Math.PI / 2 && 324 | this.bird.position.y > Settings.skyHeight - this.bird.height / 2 325 | ) { 326 | saveHighScoreAsync(this.score); 327 | this.stopAnimating = true; 328 | } 329 | } else { 330 | if (this.bird.position.y + this.bird.height / 2 > Settings.skyHeight) { 331 | this.hitPipe(); 332 | } 333 | 334 | const points = this.pipeContainer.moveAll(); 335 | if (points) { 336 | this.score += points; 337 | this.onScore(this.score); 338 | } 339 | this.pipeContainer.tryAddingNewPipe(); 340 | 341 | const padding = 15; 342 | for (const group of this.pipeContainer.pipes) { 343 | const { pipe, pipe2, upper, lower } = group; 344 | if ( 345 | boxesIntersect(this.bird, pipe, padding) || 346 | boxesIntersect(this.bird, pipe2, padding) 347 | ) { 348 | this.hitPipe(); 349 | } 350 | } 351 | } 352 | }; 353 | 354 | restart = () => { 355 | this.isStarted = false; 356 | this.isDead = false; 357 | this.stopAnimating = false; 358 | this.score = 0; 359 | this.onScore(this.score); 360 | this.bird.restart(); 361 | this.pipeContainer.restart(); 362 | this.animate(); 363 | }; 364 | 365 | hitPipe = () => { 366 | this.bird.stop(); 367 | this.isDead = true; 368 | }; 369 | 370 | updateScore = () => { 371 | this.score += 1; 372 | this.onScore(this.score); 373 | // TODO: UPDATE UI 374 | }; 375 | } 376 | 377 | async function saveHighScoreAsync(score) { 378 | const highScore = await getHighScoreAsync(); 379 | if (score > highScore) { 380 | await AsyncStorage.setItem('hiscore', highScore); 381 | } 382 | return { 383 | score: Math.max(score, highScore), 384 | isBest: score > highScore, 385 | }; 386 | } 387 | 388 | async function getHighScoreAsync() { 389 | const score = await AsyncStorage.getItem('hiscore'); 390 | if (score) { 391 | return parseInt(score); 392 | } 393 | return 0; 394 | } 395 | 396 | export default Game; 397 | -------------------------------------------------------------------------------- /src/setupSpriteSheetAsync.js: -------------------------------------------------------------------------------- 1 | import { PIXI } from 'expo-pixi'; 2 | const { Rectangle, Texture } = PIXI; 3 | async function setupSpriteSheetAsync(resource, spriteSheet) { 4 | const texture = await Texture.fromExpoAsync(resource); 5 | 6 | let textures = {}; 7 | for (const sprite of spriteSheet) { 8 | const { name, x, y, width, height } = sprite; 9 | try { 10 | const frame = new Rectangle(x, y, width, height); 11 | textures[name] = new global.PIXI.Texture(texture.baseTexture, frame); 12 | } catch ({ message }) { 13 | console.error(message); 14 | } 15 | } 16 | return textures; 17 | } 18 | 19 | export default setupSpriteSheetAsync; 20 | -------------------------------------------------------------------------------- /src/sprites.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { name: 'background', x: 0, y: 0, width: 144, height: 192 }, 3 | { name: 'pipe', x: 145, y: 0, width: 26, height: 160 }, 4 | { name: 'bird_000', x: 145, y: 161, width: 17, height: 12 }, 5 | { name: 'bird_001', x: 145, y: 174, width: 17, height: 12 }, 6 | { name: 'bird_002', x: 172, y: 0, width: 17, height: 12 }, 7 | { name: 'ground', x: 172, y: 13, width: 16, height: 72 }, 8 | ]; 9 | --------------------------------------------------------------------------------