├── README.md ├── assets ├── birdie.png ├── clouds.png ├── fence.png ├── finger.png ├── flap.wav ├── hurt.wav ├── icon-120.png ├── icon.png └── score.wav ├── index.html ├── main.js ├── phaser.min.js └── screenshots.png /README.md: -------------------------------------------------------------------------------- 1 | # Don't Touch My Birdie 2 | 3 | A clone of the widely-popular game [Flappy Bird](http://en.wikipedia.org/wiki/Flappy_Bird) created using the [Phaser framework](http://phaser.io/). 4 | 5 | [Open in your mobile browser](https://marksteve.com/dtmb) 6 | 7 | ![Screenshots](screenshots.png) 8 | 9 | ## Note 10 | 11 | This was only tested on an iPhone 5. Expect it to hilariously fail on other devices. 12 | 13 | ## License 14 | 15 | This work is licensed under a [Creative Commons Attribution-NonCommercial 4.0 International License](http://creativecommons.org/licenses/by-nc/4.0/). 16 | -------------------------------------------------------------------------------- /assets/birdie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/birdie.png -------------------------------------------------------------------------------- /assets/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/clouds.png -------------------------------------------------------------------------------- /assets/fence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/fence.png -------------------------------------------------------------------------------- /assets/finger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/finger.png -------------------------------------------------------------------------------- /assets/flap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/flap.wav -------------------------------------------------------------------------------- /assets/hurt.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/hurt.wav -------------------------------------------------------------------------------- /assets/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/icon-120.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/icon.png -------------------------------------------------------------------------------- /assets/score.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/assets/score.wav -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DTMB 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var DEBUG = false; 2 | var SPEED = 180; 3 | var GRAVITY = 18; 4 | var FLAP = 420; 5 | var SPAWN_RATE = 1 / 1.2; 6 | var OPENING = 144; 7 | 8 | function init(parent) { 9 | 10 | var state = { 11 | preload: preload, 12 | create: create, 13 | update: update, 14 | render: render 15 | }; 16 | 17 | var game = new Phaser.Game( 18 | 480, 19 | 700, 20 | Phaser.CANVAS, 21 | parent, 22 | state, 23 | false, 24 | false 25 | ); 26 | 27 | function preload() { 28 | var assets = { 29 | spritesheet: { 30 | birdie: ['assets/birdie.png', 48, 24], 31 | clouds: ['assets/clouds.png', 128, 64] 32 | }, 33 | image: { 34 | finger: ['assets/finger.png'], 35 | fence: ['assets/fence.png'] 36 | }, 37 | audio: { 38 | flap: ['assets/flap.wav'], 39 | score: ['assets/score.wav'], 40 | hurt: ['assets/hurt.wav'] 41 | } 42 | }; 43 | Object.keys(assets).forEach(function(type) { 44 | Object.keys(assets[type]).forEach(function(id) { 45 | game.load[type].apply(game.load, [id].concat(assets[type][id])); 46 | }); 47 | }); 48 | } 49 | 50 | var gameStarted, 51 | gameOver, 52 | score, 53 | bg, 54 | credits, 55 | clouds, 56 | fingers, 57 | invs, 58 | birdie, 59 | fence, 60 | scoreText, 61 | instText, 62 | gameOverText, 63 | flapSnd, 64 | scoreSnd, 65 | hurtSnd, 66 | fingersTimer, 67 | cloudsTimer, 68 | cobraMode = 0, 69 | gameOvers = 0; 70 | 71 | function create() { 72 | game.stage.scaleMode = Phaser.StageScaleMode.SHOW_ALL; 73 | game.stage.scale.setScreenSize(true); 74 | // Draw bg 75 | bg = game.add.graphics(0, 0); 76 | bg.beginFill(0xDDEEFF, 1); 77 | bg.drawRect(0, 0, game.world.width, game.world.height); 78 | bg.endFill(); 79 | // Credits 'yo 80 | credits = game.add.text( 81 | game.world.width / 2, 82 | 10, 83 | 'marksteve.com/dtmb\n@themarksteve', 84 | { 85 | font: '8px "Press Start 2P"', 86 | fill: '#fff', 87 | align: 'center' 88 | } 89 | ); 90 | credits.anchor.x = 0.5; 91 | // Add clouds group 92 | clouds = game.add.group(); 93 | // Add fingers 94 | fingers = game.add.group(); 95 | // Add invisible thingies 96 | invs = game.add.group(); 97 | // Add birdie 98 | birdie = game.add.sprite(0, 0, 'birdie'); 99 | birdie.anchor.setTo(0.5, 0.5); 100 | birdie.animations.add('fly', [0, 1, 2, 3], 10, true); 101 | birdie.animations.add('cobra', [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 60, false); 102 | birdie.inputEnabled = true; 103 | birdie.body.collideWorldBounds = true; 104 | birdie.body.gravity.y = GRAVITY; 105 | // Add fence 106 | fence = game.add.tileSprite(0, game.world.height - 32, game.world.width, 32, 'fence'); 107 | fence.tileScale.setTo(2, 2); 108 | // Add score text 109 | scoreText = game.add.text( 110 | game.world.width / 2, 111 | game.world.height / 4, 112 | "", 113 | { 114 | font: '16px "Press Start 2P"', 115 | fill: '#fff', 116 | stroke: '#430', 117 | strokeThickness: 4, 118 | align: 'center' 119 | } 120 | ); 121 | scoreText.anchor.setTo(0.5, 0.5); 122 | // Add instructions text 123 | instText = game.add.text( 124 | game.world.width / 2, 125 | game.world.height - game.world.height / 4, 126 | "", 127 | { 128 | font: '8px "Press Start 2P"', 129 | fill: '#fff', 130 | stroke: '#430', 131 | strokeThickness: 4, 132 | align: 'center' 133 | } 134 | ); 135 | instText.anchor.setTo(0.5, 0.5); 136 | // Add game over text 137 | gameOverText = game.add.text( 138 | game.world.width / 2, 139 | game.world.height / 2, 140 | "", 141 | { 142 | font: '16px "Press Start 2P"', 143 | fill: '#fff', 144 | stroke: '#430', 145 | strokeThickness: 4, 146 | align: 'center' 147 | } 148 | ); 149 | gameOverText.anchor.setTo(0.5, 0.5); 150 | gameOverText.scale.setTo(2, 2); 151 | // Add sounds 152 | flapSnd = game.add.audio('flap'); 153 | scoreSnd = game.add.audio('score'); 154 | hurtSnd = game.add.audio('hurt'); 155 | // Add controls 156 | game.input.onDown.add(flap); 157 | game.input.keyboard.addCallbacks(game, onKeyDown, onKeyUp); 158 | // Start clouds timer 159 | cloudsTimer = new Phaser.Timer(game); 160 | cloudsTimer.onEvent.add(spawnCloud); 161 | cloudsTimer.start(); 162 | cloudsTimer.add(Math.random()); 163 | // RESET! 164 | reset(); 165 | } 166 | 167 | function reset() { 168 | gameStarted = false; 169 | gameOver = false; 170 | score = 0; 171 | credits.renderable = true; 172 | scoreText.setText("DON'T\nTOUCH\nMY\nBIRDIE"); 173 | instText.setText("TOUCH TO FLAP\nBIRDIE WINGS"); 174 | gameOverText.renderable = false; 175 | birdie.body.allowGravity = false; 176 | birdie.angle = 0; 177 | birdie.reset(game.world.width / 4, game.world.height / 2); 178 | birdie.scale.setTo(1.5, 1.5); 179 | birdie.animations.play('fly'); 180 | fingers.removeAll(); 181 | invs.removeAll(); 182 | } 183 | 184 | function start() { 185 | credits.renderable = false; 186 | birdie.body.allowGravity = true; 187 | // SPAWN FINGERS! 188 | fingersTimer = new Phaser.Timer(game); 189 | fingersTimer.onEvent.add(spawnFingers); 190 | fingersTimer.start(); 191 | fingersTimer.add(2); 192 | // Show score 193 | scoreText.setText(score); 194 | instText.renderable = false; 195 | // START! 196 | gameStarted = true; 197 | } 198 | 199 | function flap() { 200 | if (!gameStarted) { 201 | start(); 202 | } 203 | if (!gameOver) { 204 | birdie.body.velocity.y = -FLAP; 205 | flapSnd.play(); 206 | } 207 | } 208 | 209 | function spawnCloud() { 210 | cloudsTimer.stop(); 211 | 212 | var cloudY = Math.random() * game.height / 2; 213 | var cloud = clouds.create( 214 | game.width, 215 | cloudY, 216 | 'clouds', 217 | Math.floor(4 * Math.random()) 218 | ); 219 | var cloudScale = 2 + 2 * Math.random(); 220 | cloud.alpha = 2 / cloudScale; 221 | cloud.scale.setTo(cloudScale, cloudScale); 222 | cloud.body.allowGravity = false; 223 | cloud.body.velocity.x = -SPEED / cloudScale; 224 | cloud.anchor.y = 0; 225 | 226 | cloudsTimer.start(); 227 | cloudsTimer.add(4 * Math.random()); 228 | } 229 | 230 | function o() { 231 | return OPENING + 60 * ((score > 50 ? 50 : 50 - score) / 50); 232 | } 233 | 234 | function spawnFinger(fingerY, flipped) { 235 | var finger = fingers.create( 236 | game.width, 237 | fingerY + (flipped ? -o() : o()) / 2, 238 | 'finger' 239 | ); 240 | finger.body.allowGravity = false; 241 | 242 | // Flip finger! *GASP* 243 | finger.scale.setTo(1.5, flipped ? -1.5 : 1.5); 244 | finger.body.offset.y = flipped ? -finger.body.height * 1.5 : 0; 245 | 246 | // Move to the left 247 | finger.body.velocity.x = -SPEED; 248 | 249 | return finger; 250 | } 251 | 252 | function spawnFingers() { 253 | fingersTimer.stop(); 254 | 255 | var fingerY = ((game.height - 16 - o() / 2) / 2) + (Math.random() > 0.5 ? -1 : 1) * Math.random() * game.height / 6; 256 | // Bottom finger 257 | var botFinger = spawnFinger(fingerY); 258 | // Top finger (flipped) 259 | var topFinger = spawnFinger(fingerY, true); 260 | 261 | // Add invisible thingy 262 | var inv = invs.create(topFinger.x + topFinger.width, 0); 263 | inv.width = 2; 264 | inv.height = game.world.height; 265 | inv.body.allowGravity = false; 266 | inv.body.velocity.x = -SPEED; 267 | 268 | fingersTimer.start(); 269 | fingersTimer.add(1 / SPAWN_RATE); 270 | } 271 | 272 | function addScore(_, inv) { 273 | invs.remove(inv); 274 | score += 1; 275 | scoreText.setText(score); 276 | scoreSnd.play(); 277 | } 278 | 279 | function setGameOver() { 280 | gameOver = true; 281 | instText.setText("TOUCH BIRDIE\nTO TRY AGAIN"); 282 | instText.renderable = true; 283 | var hiscore = window.localStorage.getItem('hiscore'); 284 | hiscore = hiscore ? hiscore : score; 285 | hiscore = score > parseInt(hiscore, 10) ? score : hiscore; 286 | window.localStorage.setItem('hiscore', hiscore); 287 | gameOverText.setText("GAME OVER"); 288 | gameOverText.renderable = true; 289 | // Stop all fingers 290 | fingers.forEachAlive(function(finger) { 291 | finger.body.velocity.x = 0; 292 | }); 293 | invs.forEach(function(inv) { 294 | inv.body.velocity.x = 0; 295 | }); 296 | // Stop spawning fingers 297 | fingersTimer.stop(); 298 | // Make birdie reset the game 299 | birdie.events.onInputDown.addOnce(reset); 300 | hurtSnd.play(); 301 | gameOvers++; 302 | } 303 | 304 | function update() { 305 | if (gameStarted) { 306 | // Make birdie dive 307 | var dvy = FLAP + birdie.body.velocity.y; 308 | birdie.angle = (90 * dvy / FLAP) - 180; 309 | if (birdie.angle < -30) { 310 | birdie.angle = -30; 311 | } 312 | if ( 313 | gameOver || 314 | birdie.angle > 90 || 315 | birdie.angle < -90 316 | ) { 317 | birdie.angle = 90; 318 | birdie.animations.stop(); 319 | birdie.frame = 3; 320 | } else { 321 | birdie.animations.play(cobraMode > 0 ? 'cobra' : 'fly'); 322 | } 323 | // Birdie is DEAD! 324 | if (gameOver) { 325 | if (birdie.scale.x < 4) { 326 | birdie.scale.setTo( 327 | birdie.scale.x * 1.2, 328 | birdie.scale.y * 1.2 329 | ); 330 | } 331 | // Shake game over text 332 | gameOverText.angle = Math.random() * 5 * Math.cos(game.time.now / 100); 333 | } else { 334 | // Check game over 335 | if (cobraMode < 1) { 336 | game.physics.overlap(birdie, fingers, setGameOver); 337 | if (!gameOver && birdie.body.bottom >= game.world.bounds.bottom) { 338 | setGameOver(); 339 | } 340 | } 341 | // Add score 342 | game.physics.overlap(birdie, invs, addScore); 343 | } 344 | // Remove offscreen fingers 345 | fingers.forEachAlive(function(finger) { 346 | if (finger.x + finger.width < game.world.bounds.left) { 347 | finger.kill(); 348 | } 349 | }); 350 | // Update finger timer 351 | fingersTimer.update(); 352 | } else { 353 | birdie.y = (game.world.height / 2) + 8 * Math.cos(game.time.now / 200); 354 | } 355 | if (!gameStarted || gameOver) { 356 | // Shake instructions text 357 | instText.scale.setTo( 358 | 2 + 0.1 * Math.sin(game.time.now / 100), 359 | 2 + 0.1 * Math.cos(game.time.now / 100) 360 | ); 361 | } 362 | // Shake score text 363 | scoreText.scale.setTo( 364 | 2 + 0.1 * Math.cos(game.time.now / 100), 365 | 2 + 0.1 * Math.sin(game.time.now / 100) 366 | ); 367 | // Update clouds timer 368 | cloudsTimer.update(); 369 | // Remove offscreen clouds 370 | clouds.forEachAlive(function(cloud) { 371 | if (cloud.x + cloud.width < game.world.bounds.left) { 372 | cloud.kill(); 373 | } 374 | }); 375 | // Scroll fence 376 | if (!gameOver) { 377 | fence.tilePosition.x -= game.time.physicsElapsed * SPEED / 2; 378 | } 379 | // Decrease cobra mode 380 | cobraMode -= game.time.physicsElapsed * SPEED * 5; 381 | } 382 | 383 | function render() { 384 | if (DEBUG) { 385 | game.debug.renderSpriteBody(birdie); 386 | fingers.forEachAlive(function(finger) { 387 | game.debug.renderSpriteBody(finger); 388 | }); 389 | invs.forEach(function(inv) { 390 | game.debug.renderSpriteBody(inv); 391 | }); 392 | } 393 | } 394 | 395 | function onKeyDown(e) { } 396 | 397 | var pressTime = 0; 398 | function onKeyUp(e) { 399 | if (Phaser.Keyboard.SPACEBAR == e.keyCode) { 400 | if (game.time.now - pressTime < 200) { 401 | cobraMode = 1000; 402 | } else { 403 | flap(); 404 | } 405 | pressTime = game.time.now; 406 | } 407 | } 408 | 409 | }; 410 | -------------------------------------------------------------------------------- /screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksteve/dtmb/39331cc21daf524a322ff71cb143fb84d98ef782/screenshots.png --------------------------------------------------------------------------------