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