├── img └── sky.png ├── css └── main.css ├── server ├── server.js └── package.json ├── index.html ├── README.md └── js ├── imageLoader.js ├── util.js ├── gameLoop.js └── game.js /img/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/augustoclaro/my-flappy-bird/HEAD/img/sky.png -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin:0; 3 | padding:0; 4 | } 5 | 6 | #gameCanvas{ 7 | border:1px solid black; 8 | } 9 | #game-container{ 10 | width:800px; 11 | height:600px; 12 | margin:0 auto; 13 | } 14 | 15 | h2{ 16 | text-align:center; 17 | } 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'); 2 | var serveStatic = require('serve-static'); 3 | var path = require('path'); 4 | var port = 8080; 5 | 6 | connect().use(serveStatic(path.join(__dirname, '..'))).listen(port, function(){ 7 | console.log('Servidor rodando na porta ' + port + '.'); 8 | }); 9 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "dependencies": { 7 | "connect": "^3.4.0", 8 | "serve-static": "^1.10.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "start": "node server.js" 14 | }, 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flappy bird game 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 |
19 |

20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #my-flappy-bird 2 | Flappy bird like html 5 and javascript game, for fun and learn. 3 | 4 | All the code is commented and free to use anyway you want. 5 | 6 | #Setup game 7 | Make sure you have node and npm installed. 8 | 9 | After checking out the project, on command-line, navigate to the server folder and run 10 | 11 | ```sh 12 | npm install 13 | ``` 14 | After downloading and installing, run the server file: 15 | 16 | ```sh 17 | node server.js 18 | ``` 19 | 20 | Now, you can go to your browser and check the game out at http://localhost:8080 21 | 22 | My website article (portuguese): http://augustoclaro.com.br/jogo-inspirado-em-flappy-bird-feito-em-html-5-e-javascript-puro/ 23 | 24 | Demo: http://www.augustoclaro.com.br/flappy-game/ 25 | 26 | Thanks for reading :D 27 | -------------------------------------------------------------------------------- /js/imageLoader.js: -------------------------------------------------------------------------------- 1 | var ImageLoader = (function(){ 2 | //Image loader helper 3 | var _results = {}; 4 | var _loadImages = function(images, cb){ 5 | for (var key in images){ 6 | //Create new image 7 | var img = new Image(); 8 | //Store key on alt attribute 9 | img.alt = key; 10 | img.onload = function(){ 11 | //Set the loaded image to the results object using the stored key 12 | _results[this.alt] = this; 13 | //if all images are loaded, execute callback, if exists 14 | if (Object.keys(_results).length === Object.keys(images).length 15 | && typeof cb === 'function') 16 | cb(_results); 17 | }; 18 | img.src = images[key]; 19 | } 20 | }; 21 | return { 22 | loadImages: _loadImages 23 | }; 24 | })(); 25 | -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | var Util = (function(){ 2 | return { 3 | clearCanvas: function(canvas){ 4 | //clear canvas helper 5 | var context = canvas.getContext('2d'); 6 | context.clearRect(0, 0, canvas.width, canvas.height); 7 | }, 8 | transferCanvas: function(canvasFrom, canvasTo){ 9 | //transfer data between canvas helper. Used to buffer the image data before showing 10 | var sourceContext = canvasFrom.getContext('2d'); 11 | var destContext = canvasTo.getContext('2d'); 12 | var imgData = sourceContext.getImageData(0, 0, canvasFrom.width, canvasFrom.height); 13 | destContext.putImageData(imgData, 0, 0, 0, 0, canvasFrom.width, canvasFrom.height); 14 | }, 15 | random: function(min, max){ 16 | //generate random number between range 17 | return Math.floor(Math.random() * (max - min + 1)) + min; 18 | }, 19 | checkBoxCollision: function(box1, box2){ 20 | //check collision between boxes 21 | return box1.x < box2.x + box2.w && //onde o player começa menor que onde o item termina - horizontal 22 | box1.x + box1.w > box2.x && //onde o player termina maior que onde o item começa - horizontal 23 | box1.y < box2.y + box2.h && //onde o player começa menor que onde o item termina - vertical 24 | box1.y + box1.h > box2.y; //onde o player termina maior que onde o item começa - vertical 25 | } 26 | }; 27 | })(); 28 | -------------------------------------------------------------------------------- /js/gameLoop.js: -------------------------------------------------------------------------------- 1 | var GameLoop = (function(){ 2 | //function to verify if given object is a function 3 | var _isFunction = function(obj){ return typeof obj === 'function'; }; 4 | 5 | var _gameLoopData = { 6 | //default FPS 7 | FPS: 20, 8 | //initialize some loop vars 9 | lastTime: new Date().getTime(), 10 | currentTime: 0, 11 | interval: 0, 12 | delta: 0, 13 | action: function(){} 14 | }; 15 | 16 | var GameLoop = function(o){ 17 | //check parameter to accept loop action directly or options to override FPSS 18 | //and set the options 19 | if (_isFunction(o)) 20 | _gameLoopData.action = o; 21 | else{ 22 | if (_isFunction(o.action)) _gameLoopData.action = o.action; 23 | if (o.FPS) _gameLoopData.FPS = o.FPS; 24 | } 25 | //main loop function 26 | var _mainAction = function(){ 27 | //check for paused state 28 | if (_gameLoopData.mode === 'running') 29 | window.requestAnimationFrame(_mainAction); 30 | //initialize current time 31 | _gameLoopData.currentTime = new Date().getTime(); 32 | //set time variation between actions 33 | _gameLoopData.delta = _gameLoopData.currentTime - _gameLoopData.lastTime; 34 | //set ms interval 35 | _gameLoopData.interval = 1000 / _gameLoopData.FPS 36 | //ensure enough time has passed to execute loop action 37 | if (_gameLoopData.delta > _gameLoopData.interval){ 38 | //run loop action 39 | _gameLoopData.action(_gameLoopData); 40 | //seting last time compensating the time overpassed 41 | _gameLoopData.lastTime = _gameLoopData.currentTime - _gameLoopData.delta % _gameLoopData.interval; 42 | } 43 | }; 44 | var _start = function(){ 45 | //set mode to running and call main loop 46 | _gameLoopData.mode = 'running'; 47 | _mainAction(); 48 | return GameLoop; 49 | }; 50 | var _pause = function(){ 51 | //set mode to pause forcing the loop to stop 52 | _gameLoopData.mode = 'pause'; 53 | return GameLoop; 54 | }; 55 | return { 56 | start: _start, 57 | pause: _pause 58 | }; 59 | }; 60 | return GameLoop; 61 | })(); 62 | -------------------------------------------------------------------------------- /js/game.js: -------------------------------------------------------------------------------- 1 | var flapGame = (function(imageLoader, util){ 2 | //Main game object 3 | var gameObj = { 4 | //Default player values 5 | defaults: { 6 | player: { 7 | x: 50, 8 | y: 50, 9 | gravity: 17 //gravity is the amount of pixels to sum to player Y every loop 10 | } 11 | }, 12 | stage: 'start', //initial stage 13 | log: function(msg){ 14 | //Log function. Using h2 to log messages 15 | document.getElementById('msg').innerText = msg; 16 | }, 17 | inputData: { 18 | keyPressed: 0, //key code 19 | mouseClick: 0, //1=left click,2=right click 20 | clear: function(){ 21 | gameObj.inputData.keyPressed = 22 | gameObj.inputData.mouseClick = 0; 23 | } 24 | }, 25 | player: { 26 | //Player object 27 | points: 0, 28 | x: undefined, 29 | y: undefined, 30 | gravity: undefined, 31 | size: { 32 | width: 50, 33 | height: 50 34 | }, 35 | renderToCanvas: function(){ 36 | var ctx = gameObj.bufferCanvas.getContext('2d'); 37 | //Draw bird's body 38 | ctx.fillStyle = 'red'; 39 | ctx.fillRect(gameObj.player.x, gameObj.player.y, gameObj.player.size.width, gameObj.player.size.height); 40 | //Draw bird's beak 41 | ctx.fillStyle = 'yellow'; 42 | ctx.fillRect(gameObj.player.x + gameObj.player.size.width - 5, gameObj.player.y + 12, 15, 10); 43 | //Draw bird's wing 44 | ctx.beginPath(); 45 | ctx.moveTo(gameObj.player.x + 5, gameObj.player.y + 25); 46 | ctx.lineTo(gameObj.player.x + 40, gameObj.player.y + 25); 47 | ctx.lineTo(gameObj.player.x + 22.5, gameObj.player.y + (gameObj.player.wingUp ? 13 : 37)); 48 | ctx.fill(); 49 | } 50 | }, 51 | gamePhysics: { 52 | //game physics config 53 | jumpGravity: 30, 54 | jumpLoops: 10, 55 | treeDistance: 350, 56 | treeSpeed: 10 57 | }, 58 | createRandomTreePair: function(){ 59 | //create tree pair. the x pos is the 60 | //game width or, if not the first one, 61 | //some pixels after the last one. 62 | //the y pos is a percentage of the canvas height 63 | //that represents the bottom of the upper tree. the 64 | //down tree is calculated through this value 65 | return { 66 | x: gameObj.treePairs.length 67 | ? (gameObj.treePairs[gameObj.treePairs.length - 1].x + gameObj.gamePhysics.treeDistance) 68 | : gameObj.gameSize.width, 69 | y: util.random(20, 60) 70 | }; 71 | }, 72 | //Game size configs 73 | gameSize: { 74 | width: 800, 75 | height: 600 76 | }, 77 | //Init some vars 78 | gameLoop: undefined, 79 | gameCanvas: undefined, 80 | bufferCanvas: undefined, 81 | treePairs: undefined, 82 | //Tree size 83 | treeSize: { 84 | width: 82, 85 | height: 381 86 | }, 87 | //Game image resources 88 | resources: { 89 | sky: 'img/sky.png' 90 | }, 91 | createCanvas: function(){ 92 | //Create canvas from game settings 93 | var el = document.createElement('canvas'); 94 | el.setAttribute('width', gameObj.gameSize.width); 95 | el.setAttribute('height', gameObj.gameSize.height); 96 | return el; 97 | }, 98 | detectInput: function(){ 99 | //Set document event handlers to fill up inputData 100 | document.onkeypress = function(e){ 101 | //Set key pressed code 102 | var key = e.keyCode || e.which; 103 | gameObj.inputData.keyPressed = key; 104 | }; 105 | 106 | gameObj.gameCanvas.onclick = function(){ 107 | //Set mouseClick to left click code 108 | gameObj.inputData.mouseClick = 1; 109 | }; 110 | }, 111 | startRound: function(){ 112 | //Set initial values to new round 113 | gameObj.treePairs = []; 114 | //Set player data 115 | gameObj.player.x = gameObj.defaults.player.x; 116 | gameObj.player.y = gameObj.defaults.player.y; 117 | gameObj.player.gravity = gameObj.defaults.player.gravity; 118 | gameObj.player.points = 0; 119 | //Generate starting trees. ony 5 trees. The other ones will be 120 | //generated as the existing trees disapears 121 | for (var i = 1; i <= 5; i++) 122 | gameObj.treePairs.push(gameObj.createRandomTreePair()) 123 | }, 124 | init: function(parentElId){ 125 | window.onload = function(){ 126 | gameObj.log('Starting game and loading objects.'); 127 | //Create game and buffer canvas. Set game canvas ID and append to parent elementgameObj 128 | gameObj.gameCanvas = gameObj.createCanvas(); 129 | gameObj.bufferCanvas = gameObj.createCanvas(); 130 | gameObj.gameCanvas.setAttribute('id', 'gameCanvas'); 131 | var parentEl = document.getElementById(parentElId); 132 | parentEl.appendChild(gameObj.gameCanvas); 133 | //Start first round 134 | gameObj.startRound(); 135 | //Load resources 136 | imageLoader.loadImages(gameObj.resources, function(loadedResources){ 137 | gameObj.resources = loadedResources; 138 | //Start browser event handlers 139 | gameObj.detectInput(); 140 | //Fire gameloop 141 | gameObj.startGameLoop(); 142 | }); 143 | }; 144 | }, 145 | startGameLoop: function(){ 146 | //Start game loop 147 | gameObj.gameLoop = GameLoop(gameObj.loopAction).start(); 148 | }, 149 | loopAction: function(){ 150 | //Update game entries 151 | gameObj.update(); 152 | //Render game objects 153 | gameObj.render(); 154 | }, 155 | update: function(){ 156 | //Detect player action through enter key, space bar and mouse left click 157 | var playerAction = gameObj.inputData.keyPressed === 32 || 158 | gameObj.inputData.keyPressed === 13 || 159 | gameObj.inputData.mouseClick === 1; 160 | 161 | if (gameObj.stage === 'start'){ 162 | //On start stage, just show the message and 163 | //handle player action to start the game 164 | gameObj.log('All Ready! Press space, enter or click the mouse to start!'); 165 | if (playerAction) 166 | gameObj.stage = 'play'; 167 | } 168 | else if (gameObj.stage === 'pause'){ 169 | //On pause stage, just show the message and 170 | //handle player action to resume the game 171 | gameObj.log('Game paused! Press space, enter or click the mouse to resume!'); 172 | if (playerAction) 173 | gameObj.stage = 'play'; 174 | } 175 | else if (gameObj.stage === 'gameover'){ 176 | //On gameover stage, just show the message and 177 | //handle player action to restart the game 178 | gameObj.log('Game over! Press space, enter or click the mouse to play again!'); 179 | if (playerAction){ 180 | //reset round data 181 | gameObj.startRound(); 182 | gameObj.stage = 'play'; 183 | } 184 | } 185 | else{ 186 | //On play stage, show the pause message, 187 | //handle pause action and all game events 188 | gameObj.log('Press P to pause!'); 189 | var _player = gameObj.player; 190 | //check for player action 191 | if (playerAction){ 192 | //set negative gravity to make the bird jump up 193 | _player.gravity = gameObj.defaults.player.gravity - gameObj.gamePhysics.jumpGravity; 194 | }else if (gameObj.inputData.keyPressed === 112){ 195 | //handle P key to pause the game 196 | gameObj.stage = 'pause'; 197 | } 198 | else if(_player.gravity !== gameObj.defaults.player.gravity){ 199 | //gradually restore the original gravity 200 | _player.gravity += gameObj.gamePhysics.jumpGravity / gameObj.gamePhysics.jumpLoops; 201 | } 202 | //Apply gravity 203 | _player.y += _player.gravity; 204 | //Wing up and down 205 | gameObj.player.frameCount = gameObj.player.frameCount || 0; 206 | gameObj.player.frameCount = gameObj.player.frameCount === 5 ? 0 : gameObj.player.frameCount + 1; 207 | if (!gameObj.player.frameCount) 208 | gameObj.player.wingUp = !gameObj.player.wingUp; 209 | //Move all tree pairs 210 | for (var i = 0; i < gameObj.treePairs.length; i++){ 211 | var treePair = gameObj.treePairs[i]; 212 | //Move the tree 213 | treePair.x -= gameObj.gamePhysics.treeSpeed; 214 | //Check if tree is already gone 215 | if (treePair.x < gameObj.treeSize.width * -1){ 216 | //Remove tree from array 217 | gameObj.treePairs.splice(i, 1); 218 | //Replace the deleted tree with new one on the end of the quere 219 | gameObj.treePairs.push(gameObj.createRandomTreePair()); 220 | //Sum one point to the player 221 | gameObj.player.points++; 222 | } 223 | } 224 | //Check for gameover 225 | if (gameObj.hasLost()){ 226 | gameObj.stage = 'gameover'; 227 | } 228 | } 229 | //Clear input data 230 | gameObj.inputData.clear(); 231 | }, 232 | hasLost: function(){ 233 | //Check if the bird has hit the ground or if there was any collision with trees 234 | return (gameObj.player.y > (gameObj.gameSize.height - gameObj.player.size.height)) 235 | || gameObj.checkTreeCollision(); 236 | }, 237 | checkTreeCollision: function(){ 238 | for (var i = 0; i < gameObj.treePairs.length; i++){ 239 | //For every tree, check if the collision data is set 240 | //and if so, if any of them collides with the bird 241 | var treePair = gameObj.treePairs[i]; 242 | if (treePair.collisions && treePair.collisions.some(function(item){ 243 | return util.checkBoxCollision({ 244 | x: gameObj.player.x, 245 | y: gameObj.player.y, 246 | w: gameObj.player.size.width, 247 | h: gameObj.player.size.height 248 | }, item); 249 | })) return true; 250 | } 251 | return false; 252 | }, 253 | render: function(){ 254 | //Clear buffer canvas 255 | util.clearCanvas(gameObj.bufferCanvas); 256 | //Render background 257 | var ctx = gameObj.bufferCanvas.getContext('2d'); 258 | var bgRes = gameObj.resources.sky; 259 | ctx.drawImage(bgRes, 0, 0, bgRes.width, bgRes.height, 0, 0, gameObj.gameSize.width, gameObj.gameSize.height); 260 | //Render all tree pairs 261 | for (var i = 0; i < gameObj.treePairs.length; i++) 262 | gameObj.drawTreePair(gameObj.treePairs[i]); 263 | //Render bird 264 | gameObj.player.renderToCanvas(); 265 | //Render points 266 | document.getElementById('pontos').innerText = 'Score: ' + gameObj.player.points + ' point(s).'; 267 | //Transfer buffer image data to game canvas 268 | //This avoid the main canvas to blink when updating 269 | util.transferCanvas(gameObj.bufferCanvas, gameObj.gameCanvas); 270 | }, 271 | drawTreePair: function(treePair){ 272 | var ctx = gameObj.bufferCanvas.getContext('2d'); 273 | //Determine the upper tree bottom and de down tree top position 274 | var upCorner = treePair.y * gameObj.gameSize.height / 100; 275 | var downCorner = upCorner + gameObj.player.size.height * 3; 276 | //Calculate the y coord for both trees 277 | var treeDownY = upCorner - gameObj.treeSize.height; 278 | var treeUpY = downCorner; 279 | //Set collision data for this tree pair 280 | treePair.collisions = [{x: treePair.x, y: treeDownY, w: gameObj.treeSize.width, h: gameObj.treeSize.height}, 281 | {x: treePair.x, y: treeUpY, w: gameObj.treeSize.width, h: gameObj.treeSize.height}]; 282 | //Draw the tree pair rects 283 | ctx.fillStyle = 'brown'; 284 | ctx.fillRect(treePair.x, treeDownY, gameObj.treeSize.width, gameObj.treeSize.height); 285 | ctx.fillRect(treePair.x, treeUpY, gameObj.treeSize.width, gameObj.treeSize.height); 286 | } 287 | }; 288 | 289 | return gameObj; 290 | })(ImageLoader, Util); 291 | --------------------------------------------------------------------------------