├── .gitignore ├── LICENSE ├── README.md ├── css └── app.css ├── img ├── sprites.png ├── sprites.psd └── terrain.png ├── index.html ├── js ├── app.js ├── input.js ├── resources.js └── sprite.js └── tutorial.md /.gitignore: -------------------------------------------------------------------------------- 1 | .#* 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, James Long 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | A simple starting point for writing 2d games. See tutorial.md for more information. -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #151515; 5 | } 6 | 7 | canvas { 8 | display: block; 9 | margin: auto; 10 | 11 | position: absolute; 12 | top: 0; 13 | bottom: 0; 14 | left: 0; 15 | right: 0; 16 | } 17 | 18 | .wrapper { 19 | width: 512px; 20 | margin: 0 auto; 21 | margin-top: 2em; 22 | } 23 | 24 | #instructions { 25 | float: left; 26 | font-family: sans-serif; 27 | color: #757575; 28 | } 29 | 30 | #score { 31 | float: right; 32 | color: white; 33 | font-size: 2em; 34 | } 35 | 36 | .key { 37 | color: #aaffdd; 38 | } 39 | 40 | #game-over, #game-over-overlay { 41 | margin: auto; 42 | width: 512px; 43 | height: 480px; 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | right: 0; 48 | bottom: 0; 49 | z-index: 1; 50 | display: none; 51 | } 52 | 53 | #game-over-overlay { 54 | background-color: black; 55 | opacity: .5; 56 | } 57 | 58 | #game-over { 59 | height: 200px; 60 | text-align: center; 61 | color: white; 62 | } 63 | 64 | #game-over h1 { 65 | font-size: 3em; 66 | font-family: sans-serif; 67 | } 68 | 69 | #game-over button { 70 | font-size: 1.5em; 71 | } -------------------------------------------------------------------------------- /img/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlongster/canvas-game-bootstrap/a878158f39a91b19725f726675c752683c9e1c08/img/sprites.png -------------------------------------------------------------------------------- /img/sprites.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlongster/canvas-game-bootstrap/a878158f39a91b19725f726675c752683c9e1c08/img/sprites.psd -------------------------------------------------------------------------------- /img/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlongster/canvas-game-bootstrap/a878158f39a91b19725f726675c752683c9e1c08/img/terrain.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |

GAME OVER

18 | 19 |
20 | 21 |
22 |
23 |
24 | move with arrows or wasd 25 |
26 |
27 | shoot with space 28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | 2 | // A cross-browser requestAnimationFrame 3 | // See https://hacks.mozilla.org/2011/08/animating-with-javascript-from-setinterval-to-requestanimationframe/ 4 | var requestAnimFrame = (function(){ 5 | return window.requestAnimationFrame || 6 | window.webkitRequestAnimationFrame || 7 | window.mozRequestAnimationFrame || 8 | window.oRequestAnimationFrame || 9 | window.msRequestAnimationFrame || 10 | function(callback){ 11 | window.setTimeout(callback, 1000 / 60); 12 | }; 13 | })(); 14 | 15 | // Create the canvas 16 | var canvas = document.createElement("canvas"); 17 | var ctx = canvas.getContext("2d"); 18 | canvas.width = 512; 19 | canvas.height = 480; 20 | document.body.appendChild(canvas); 21 | 22 | // The main game loop 23 | var lastTime; 24 | function main() { 25 | var now = Date.now(); 26 | var dt = (now - lastTime) / 1000.0; 27 | 28 | update(dt); 29 | render(); 30 | 31 | lastTime = now; 32 | requestAnimFrame(main); 33 | }; 34 | 35 | function init() { 36 | terrainPattern = ctx.createPattern(resources.get('img/terrain.png'), 'repeat'); 37 | 38 | document.getElementById('play-again').addEventListener('click', function() { 39 | reset(); 40 | }); 41 | 42 | reset(); 43 | lastTime = Date.now(); 44 | main(); 45 | } 46 | 47 | resources.load([ 48 | 'img/sprites.png', 49 | 'img/terrain.png' 50 | ]); 51 | resources.onReady(init); 52 | 53 | // Game state 54 | var player = { 55 | pos: [0, 0], 56 | sprite: new Sprite('img/sprites.png', [0, 0], [39, 39], 16, [0, 1]) 57 | }; 58 | 59 | var bullets = []; 60 | var enemies = []; 61 | var explosions = []; 62 | 63 | var lastFire = Date.now(); 64 | var gameTime = 0; 65 | var isGameOver; 66 | var terrainPattern; 67 | 68 | var score = 0; 69 | var scoreEl = document.getElementById('score'); 70 | 71 | // Speed in pixels per second 72 | var playerSpeed = 200; 73 | var bulletSpeed = 500; 74 | var enemySpeed = 100; 75 | 76 | // Update game objects 77 | function update(dt) { 78 | gameTime += dt; 79 | 80 | handleInput(dt); 81 | updateEntities(dt); 82 | 83 | // It gets harder over time by adding enemies using this 84 | // equation: 1-.993^gameTime 85 | if(Math.random() < 1 - Math.pow(.993, gameTime)) { 86 | enemies.push({ 87 | pos: [canvas.width, 88 | Math.random() * (canvas.height - 39)], 89 | sprite: new Sprite('img/sprites.png', [0, 78], [80, 39], 90 | 6, [0, 1, 2, 3, 2, 1]) 91 | }); 92 | } 93 | 94 | checkCollisions(); 95 | 96 | scoreEl.innerHTML = score; 97 | }; 98 | 99 | function handleInput(dt) { 100 | if(input.isDown('DOWN') || input.isDown('s')) { 101 | player.pos[1] += playerSpeed * dt; 102 | } 103 | 104 | if(input.isDown('UP') || input.isDown('w')) { 105 | player.pos[1] -= playerSpeed * dt; 106 | } 107 | 108 | if(input.isDown('LEFT') || input.isDown('a')) { 109 | player.pos[0] -= playerSpeed * dt; 110 | } 111 | 112 | if(input.isDown('RIGHT') || input.isDown('d')) { 113 | player.pos[0] += playerSpeed * dt; 114 | } 115 | 116 | if(input.isDown('SPACE') && 117 | !isGameOver && 118 | Date.now() - lastFire > 100) { 119 | var x = player.pos[0] + player.sprite.size[0] / 2; 120 | var y = player.pos[1] + player.sprite.size[1] / 2; 121 | 122 | bullets.push({ pos: [x, y], 123 | dir: 'forward', 124 | sprite: new Sprite('img/sprites.png', [0, 39], [18, 8]) }); 125 | bullets.push({ pos: [x, y], 126 | dir: 'up', 127 | sprite: new Sprite('img/sprites.png', [0, 50], [9, 5]) }); 128 | bullets.push({ pos: [x, y], 129 | dir: 'down', 130 | sprite: new Sprite('img/sprites.png', [0, 60], [9, 5]) }); 131 | 132 | lastFire = Date.now(); 133 | } 134 | } 135 | 136 | function updateEntities(dt) { 137 | // Update the player sprite animation 138 | player.sprite.update(dt); 139 | 140 | // Update all the bullets 141 | for(var i=0; i canvas.height || 153 | bullet.pos[0] > canvas.width) { 154 | bullets.splice(i, 1); 155 | i--; 156 | } 157 | } 158 | 159 | // Update all the enemies 160 | for(var i=0; i r2 || 187 | b <= y2 || y > b2); 188 | } 189 | 190 | function boxCollides(pos, size, pos2, size2) { 191 | return collides(pos[0], pos[1], 192 | pos[0] + size[0], pos[1] + size[1], 193 | pos2[0], pos2[1], 194 | pos2[0] + size2[0], pos2[1] + size2[1]); 195 | } 196 | 197 | function checkCollisions() { 198 | checkPlayerBounds(); 199 | 200 | // Run collision detection for all enemies and bullets 201 | for(var i=0; i canvas.width - player.sprite.size[0]) { 247 | player.pos[0] = canvas.width - player.sprite.size[0]; 248 | } 249 | 250 | if(player.pos[1] < 0) { 251 | player.pos[1] = 0; 252 | } 253 | else if(player.pos[1] > canvas.height - player.sprite.size[1]) { 254 | player.pos[1] = canvas.height - player.sprite.size[1]; 255 | } 256 | } 257 | 258 | // Draw everything 259 | function render() { 260 | ctx.fillStyle = terrainPattern; 261 | ctx.fillRect(0, 0, canvas.width, canvas.height); 262 | 263 | // Render the player if the game isn't over 264 | if(!isGameOver) { 265 | renderEntity(player); 266 | } 267 | 268 | renderEntities(bullets); 269 | renderEntities(enemies); 270 | renderEntities(explosions); 271 | }; 272 | 273 | function renderEntities(list) { 274 | for(var i=0; i 0) { 23 | var max = this.frames.length; 24 | var idx = Math.floor(this._index); 25 | frame = this.frames[idx % max]; 26 | 27 | if(this.once && idx >= max) { 28 | this.done = true; 29 | return; 30 | } 31 | } 32 | else { 33 | frame = 0; 34 | } 35 | 36 | 37 | var x = this.pos[0]; 38 | var y = this.pos[1]; 39 | 40 | if(this.dir == 'vertical') { 41 | y += frame * this.size[1]; 42 | } 43 | else { 44 | x += frame * this.size[0]; 45 | } 46 | 47 | ctx.drawImage(resources.get(this.url), 48 | x, y, 49 | this.size[0], this.size[1], 50 | 0, 0, 51 | this.size[0], this.size[1]); 52 | } 53 | }; 54 | 55 | window.Sprite = Sprite; 56 | })(); -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | 2 | # Making Sprite-based Games with Canvas 3 | 4 | The [canvas element](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html) was introduced with [HTML5](http://en.wikipedia.org/wiki/HTML5) and provides an API for rendering on the web. The API is simple, but if you've never done graphics work before it might take some getting used to. It has great [cross-browser support](http://caniuse.com/#feat=canvas) at this point, and it makes the web a viable platform for games. 5 | 6 | Using canvas is simple: just create a `` tag, create a rendering context from it in javascript, and use methods like `fillRect` and `drawImage` on the context to render shapes and images. The [API](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html) has a lot of methods for rendering arbitrary paths, applying transformations, and more. 7 | 8 | In this article, we're going to create a 2d game with canvas; a real game with sprites, animations, collision detection, and of course, explosions! What's a game without explosions? 9 | 10 | This is the game we're going to make ([play it here](http://jlongster.github.com/canvas-game-bootstrap/)). I wrapped this up into a [game bootstrap](https://github.com/jlongster/canvas-game-bootstrap) project that you can use to quickly get started. I recomend checking out the source and running it locally by opening `index.html`. 11 | 12 | [![](http://jlongster.com/s/canvas-tutorial/screenshot.png)](http://jlongster.github.com/canvas-game-bootstrap/) 13 | 14 | ## Gearing Up 15 | 16 | The game might look complex, but it really just boils down to a few technical components. I've always been amazed how far you can go with canvas, simple collision detection, some sprites, and a game loop. 17 | 18 | However, in order to focus on the game components, I'm not going to fully explain every single line of code and API call. This tutorial is going to be somewhat advanced, but I hope that it's clear enough so that people of all skill levels can follow along. It's meant to explain basic game concepts, with a few more advanced techniques like sprite animations mixed in. 19 | 20 | For a more basic tutorial, check out [How to make a simple HTML5 Canvas Game](http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/) and [HTML5 Snake source code walkthrough](http://jdstraughan.com/2013/03/05/html5-snake-with-source-code-walkthrough/). 21 | 22 | ### Using Free Graphics 23 | 24 | It really sucks to try to make games for the first few times, because there's a lot to learn and you don't have any time to make cool graphics. I highly recommend using a free set of graphics until you have time to make your own. 25 | 26 | [HasGraphics](http://hasgraphics.com/) is an awesome place to find free 2d graphics. I'm using the [Hard Vacuum](http://www.lostgarden.com/2005/03/game-post-mortem-hard-vacuum.html) set for this example game. 27 | 28 | You also learn how real graphics are stored and how to work with them. In this tutorial I will show how I integrated them. 29 | 30 | ## Creating the Canvas 31 | 32 | Let's start by digging into the code. Most of the game is in [app.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/app.js) in the `js` directory. 33 | 34 | The very first thing we do is create the canvas tag, set the width and height, and add it to the `body` tag. We do this dynamically to keep everything in javascript, but you could add a `canvas` tag in the HTML file and use something like `getElementById` to get it too. 35 | 36 | ```js 37 | // Create the canvas 38 | var canvas = document.createElement("canvas"); 39 | var ctx = canvas.getContext("2d"); 40 | canvas.width = 512; 41 | canvas.height = 480; 42 | document.body.appendChild(canvas); 43 | ``` 44 | 45 | The `canvas` element has a `getContext` method which is what you use to get the rendering context. You can also pass `webgl` if you want a [WebGL](https://developer.mozilla.org/en-US/docs/WebGL) context for 3d scenes. 46 | 47 | From here on, we will be using the `ctx` variable to render everything. 48 | 49 | ## Game Loop 50 | 51 | You need a game loop that continually updates and renders the game. [Here's what it looks like](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/app.js#L22): 52 | 53 | ```js 54 | // The main game loop 55 | var lastTime; 56 | function main() { 57 | var now = Date.now(); 58 | var dt = (now - lastTime) / 1000.0; 59 | 60 | update(dt); 61 | render(); 62 | 63 | lastTime = now; 64 | requestAnimFrame(main); 65 | }; 66 | ``` 67 | 68 | You update and render the scene, and then use [requestAnimationFrame](http://paulirish.com/2011/requestanimationframe-for-smart-animating/) to queue up the next loop. It's basically a smarter way of saying `setTimeout(main, 1000 / 60)`, which attempts to render a 60 frames/second. At the [very top](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/app.js#L2) of app.js we shim rAF as the `requestAnimFrame` function since [not all browsers](http://caniuse.com/#feat=requestanimationframe) support it yet. 69 | 70 | **Never *ever*** use `setTimeout(main, 1000 / 60)`, as it's less accurate and also wastes a lot of cycles by rendering when unnecessary. 71 | 72 | The `update` function takes the time that has changed since the last update. **Never** update your scene with constant values per frame (like `x += 5;`). Your game will run wildly different on various computers and platforms, so you need to update your scene indepently of framerate. 73 | 74 | This is achieved by calculating the time since last update (in seconds), and expressing all movements in pixels/second units. Movement then becomes `x += 50 * dt`, or "50 pixels per second". 75 | 76 | ## Loading Resources and Starting the Game 77 | 78 | The next section of code initializes the game and loads all resources. This uses one of the few separate utility classes that I wrote, [resources.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/resources.js). It's a very simple library that loads images and fires an event when they are all loaded. 79 | 80 | Games require a lot of assets, like images, scene data, and so on. For 2d games, most or all of the assets are images. You need to load all your assets before starting the game so that they can be immediately used. 81 | 82 | It's easy to load an image in javascript and do something when it's available: 83 | 84 | ```js 85 | var img = new Image(); 86 | img.onload = function() { 87 | startGame(); 88 | }; 89 | img.src = url; 90 | ``` 91 | 92 | This gets really tedious though if you have several images to load. You need to make a bunch of global variables, and in each `onload` check if all of them are loaded. I wrote a basic resource loader to handle all of this automatically. Get ready for some code! 93 | 94 | ```js 95 | 96 | (function() { 97 | var resourceCache = {}; 98 | var loading = []; 99 | var readyCallbacks = []; 100 | 101 | // Load an image url or an array of image urls 102 | function load(urlOrArr) { 103 | if(urlOrArr instanceof Array) { 104 | urlOrArr.forEach(function(url) { 105 | _load(url); 106 | }); 107 | } 108 | else { 109 | _load(urlOrArr); 110 | } 111 | } 112 | 113 | function _load(url) { 114 | if(resourceCache[url]) { 115 | return resourceCache[url]; 116 | } 117 | else { 118 | var img = new Image(); 119 | img.onload = function() { 120 | resourceCache[url] = img; 121 | 122 | if(isReady()) { 123 | readyCallbacks.forEach(function(func) { func(); }); 124 | } 125 | }; 126 | resourceCache[url] = false; 127 | img.src = url; 128 | } 129 | } 130 | 131 | function get(url) { 132 | return resourceCache[url]; 133 | } 134 | 135 | function isReady() { 136 | var ready = true; 137 | for(var k in resourceCache) { 138 | if(resourceCache.hasOwnProperty(k) && 139 | !resourceCache[k]) { 140 | ready = false; 141 | } 142 | } 143 | return ready; 144 | } 145 | 146 | function onReady(func) { 147 | readyCallbacks.push(func); 148 | } 149 | 150 | window.resources = { 151 | load: load, 152 | get: get, 153 | onReady: onReady, 154 | isReady: isReady 155 | }; 156 | })(); 157 | ``` 158 | 159 | The way this works is your game calls `resources.load` with all the images to load, and then calls `resources.onReady` to register a callback for when everything is loaded. This assumes that you won't call `resources.load` later in the game; it only works at startup. 160 | 161 | It keeps a cache of images in `resourceCache`, and when the image loads it checks to see if all the requested images have loaded, and if so calls all the registered callbacks. Now we can just do this in our game: 162 | 163 | ```js 164 | resources.load([ 165 | 'img/sprites.png', 166 | 'img/terrain.png' 167 | ]); 168 | resources.onReady(init); 169 | ``` 170 | 171 | To get an image once the game starts, we just do `resources.get('img/sprites.png')`. Easy! 172 | 173 | You can manually load images and start the game or use something like [resources.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/resources.js) to make it easier. 174 | 175 | In the above code, [`init`](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/app.js#L35) is called when all the images are loaded, which creates the background pattern, hooks up the "Play Again" button, resets the game state, and starts the game. 176 | 177 | ```js 178 | function init() { 179 | terrainPattern = ctx.createPattern(resources.get('img/terrain.png'), 'repeat'); 180 | 181 | document.getElementById('play-again').addEventListener('click', function() { 182 | reset(); 183 | }); 184 | 185 | reset(); 186 | lastTime = Date.now(); 187 | main(); 188 | } 189 | ``` 190 | 191 | ## Game State 192 | 193 | Now we're rolling! Let's start implementing some game logic. At the core of every game is *game state*. This is data that represents the current state: a list of objects in the scene with position and other info, current score, time since the player last fired, and anything else. 194 | 195 | This is all of the game state for our game: 196 | 197 | ```js 198 | // Game state 199 | var player = { 200 | pos: [0, 0], 201 | sprite: new Sprite('img/sprites.png', [0, 0], [39, 39], 16, [0, 1]) 202 | }; 203 | 204 | var bullets = []; 205 | var enemies = []; 206 | var explosions = []; 207 | 208 | var lastFire = Date.now(); 209 | var gameTime = 0; 210 | var isGameOver; 211 | var terrainPattern; 212 | 213 | // The score 214 | var score = 0; 215 | var scoreEl = document.getElementById('score'); 216 | 217 | ``` 218 | 219 | This looks like a lot, but it's not that complicated. Most of it is keeping track of when the player last fired a bullet (`lastFire`), how long the game has been playing (`gameTime`), if the game is over (`isGameOver`), the terrain pattern image (`terrainPattern`), and the score (`score`). There's also a list of objects in the scene: bullets, enemies, and explosions. 220 | 221 | There's also the `player` entity, which keeps track of where the player is and the sprite state. Before we move on in the code, let's talk about entities and sprites. 222 | 223 | ## Entities & Sprites 224 | 225 | ### Entities 226 | 227 | An "entity" is an object in the scene. Anything from a ship to a bullet to an explosion is an entity. 228 | 229 | Entities in this system are simple javascript objects which keep track of where they are in the scene and more. This is a pretty simple system where we are manually handling each entity type, so there isn't any enforced structure. Each of our entities has `pos` and `sprite` field, and possibly more. For example, if we wanted to add an enemy to our scene, we would do: 230 | 231 | ```js 232 | enemies.push({ 233 | pos: [100, 50], 234 | sprite: new Sprite(/* sprite parameters */) 235 | }); 236 | ``` 237 | 238 | This adds an enemy at x=100 and y=50 with the specified sprite. 239 | 240 | 241 | ### Sprites & Animation 242 | 243 | A "sprite" is an image that is rendered to represent an entity. Sprites are more complex because we want to animate them. Without animation, sprites could be simple images that are rendered with [`ctx.drawImage`](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage). 244 | 245 | Animations are critical to a fun experience though, so it's really important that we implement it. 246 | 247 | We can implement animations by loading several images and flipping through them over time. This is called keyframe animation. 248 | 249 | ![](http://jlongster.com/s/canvas-tutorial/frames.png) 250 | 251 | If we alternate through these images back and forth, this is what it looks like: 252 | 253 | ![](http://jlongster.com/s/canvas-tutorial/test.gif) 254 | 255 | In order to make it easier to edit each keyframe and load them, these images are commonly put all into one image, called a sprite map. You may already be familiar with this technique [in CSS](http://davidwalsh.name/firefox-animation). In fact, many times *several* different sprite animations are contained in a single sprite map. Here's the sprite map for our example game (with a transparent background): 256 | 257 | ![](http://jlongster.com/s/canvas-tutorial/sprites.png) 258 | 259 | Like I mentioned at the beginning of this tutorial, I'm using the [Hard Vacuum](http://www.lostgarden.com/2005/03/game-post-mortem-hard-vacuum.html) set of graphics. The set is a just bunch of `bmp` files, so I copied the individual graphics I needed and pasted them into a single sprite sheet. You'll need a simple graphics editor to do this (anything that can move pixels around should work). If you don't have one, you'll have to work with the existing formats and possibly change how the sprite animations load them. You could also find a different set or work with simple colored rectangles. 260 | 261 | It would be difficult to manage all of these animations manually, however. This is where the second utility class comes in: [sprite.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/sprite.js). This is a small file that wraps up the animation logic into a reusable type. Let's dig into it. 262 | 263 | ```js 264 | function Sprite(url, pos, size, speed, frames, dir, once) { 265 | this.pos = pos; 266 | this.size = size; 267 | this.speed = typeof speed === 'number' ? speed : 0; 268 | this.frames = frames; 269 | this._index = 0; 270 | this.url = url; 271 | this.dir = dir || 'horizontal'; 272 | this.once = once; 273 | }; 274 | ``` 275 | 276 | This is the constructor for the `Sprite` class. It takes quite a number of arguments, but not all of them are required. Let's go through them one by one: 277 | 278 | * `url`: the path to the image for this sprite 279 | * `pos`: the x and y coordinate in the image for this sprite 280 | * `size`: size of the sprite (just one keyframe) 281 | * `speed`: speed in frames/sec for animating 282 | * `frames`: an array of frame indexes for animating: [0, 1, 2, 1] 283 | * `dir`: which direction to move in the sprite map when animating: 'horizontal' (default) or 'vertical' 284 | * `once`: `true` to only run the animation once, defaults to `false` 285 | 286 | The `frames` argument might need more explanation. The system assumes that all frames of an animation are the same size (the size passed in above). When animating, the system simply walks through the sprite map either horizontally or vertically (depending on `dir`) by starting at `pos` and incrementing by the x or y value of `size`. You need to specify `frames` to tell it *how* to walk, though, and each number references the frame to use. So `[0, 1, 2, 3, 2, 1]` would animate to the end and then reverse to the beginning. 287 | 288 | Only `url`, `pos`, and `size` are required, since you might not need animation. 289 | 290 | Every `Sprite` object has an [`update`](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/sprite.js#L15) method for updating the animation, and it takes the length of time since last update just like our global update. Every sprite needs to be updated each frame. 291 | 292 | ```js 293 | Sprite.prototype.update = function(dt) { 294 | this._index += this.speed*dt; 295 | } 296 | ``` 297 | 298 | Every `Sprite` object also has a [`render`](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/sprite.js#L19) method for actually drawing itself. This is where most of the animation logic lives. It checks to see which frame it should render, calculates the coordinates within the sprite map, and calls [`ctx.drawImage`](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage) to draw it. 299 | 300 | ```js 301 | Sprite.prototype.render = function(ctx) { 302 | var frame; 303 | 304 | if(this.speed > 0) { 305 | var max = this.frames.length; 306 | var idx = Math.floor(this._index); 307 | frame = this.frames[idx % max]; 308 | 309 | if(this.once && idx >= max) { 310 | this.done = true; 311 | return; 312 | } 313 | } 314 | else { 315 | frame = 0; 316 | } 317 | 318 | 319 | var x = this.pos[0]; 320 | var y = this.pos[1]; 321 | 322 | if(this.dir == 'vertical') { 323 | y += frame * this.size[1]; 324 | } 325 | else { 326 | x += frame * this.size[0]; 327 | } 328 | 329 | ctx.drawImage(resources.get(this.url), 330 | x, y, 331 | this.size[0], this.size[1], 332 | 0, 0, 333 | this.size[0], this.size[1]); 334 | } 335 | ``` 336 | 337 | We use the 3rd form of [`drawImage`](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage) which lets us specify an offset and size for the sprite and the destination separately. 338 | 339 | ## Updating the Scene 340 | 341 | Remember back in our game loop when we called `update(dt)` every frame? We need to define that function now, which needs to handle input, update all the sprites, update the positions of entities, and handle collision. 342 | 343 | ```js 344 | function update(dt) { 345 | gameTime += dt; 346 | 347 | handleInput(dt); 348 | updateEntities(dt); 349 | 350 | // It gets harder over time by adding enemies using this 351 | // equation: 1-.993^gameTime 352 | if(Math.random() < 1 - Math.pow(.993, gameTime)) { 353 | enemies.push({ 354 | pos: [canvas.width, 355 | Math.random() * (canvas.height - 39)], 356 | sprite: new Sprite('img/sprites.png', [0, 78], [80, 39], 357 | 6, [0, 1, 2, 3, 2, 1]) 358 | }); 359 | } 360 | 361 | checkCollisions(); 362 | 363 | scoreEl.innerHTML = score; 364 | }; 365 | ``` 366 | 367 | Note how we add new enemies to the scene. We add an enemy if a random value is lower then a threshold, and it's added at the right side of the game just outside the view. It is randomly placed on the y axis by multiplying a random value by the canvas height minus the height of the enemy, so that the bottom doesn't cut any off. The value `39` is hardcoded because we know that's the height of the sprite. This code is simpler for the sake of the tutorial. 368 | 369 | The threshold increases over time with the function [`1 - Math.pow(.993, gameTime)`](https://www.google.com/search?hl=en&site=&source=hp&q=1-.993^x&oq=1-.993^x&gs_l=hp.3...1436.6047.0.6566.8.8.0.0.0.0.264.874.4j3j1.8.0.les%3B..0.0...1c.1.5.hp.xQBpEcL-gyQ). `Math.random` returns a value between 0 and 1, and that function is about .13 at 20 seconds into the game, and slowly increases according to an exponential curve. The game is probably way too hard but it's illustrative. 370 | 371 | ### Input 372 | 373 | To handle input, I create one more utility library: [input.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/input.js). This is very small library that simply keeps the state of the currently pressed keys by adding keydown and keyup event handlers to the document. I don't think it's worth posting here, but please go [check it out](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/input.js) on github. 374 | 375 | The input library exports one single function, `input.isDown`, which takes a character such as `'a'` and returns true if it's currently pressed. You can also pass `'SPACE'`, `'LEFT'`, `'UP'` , `'RIGHT'`, and `'DOWN'`. 376 | 377 | Now we can handle input like this: 378 | 379 | ```js 380 | function handleInput(dt) { 381 | if(input.isDown('DOWN') || input.isDown('s')) { 382 | player.pos[1] += playerSpeed * dt; 383 | } 384 | 385 | if(input.isDown('UP') || input.isDown('w')) { 386 | player.pos[1] -= playerSpeed * dt; 387 | } 388 | 389 | if(input.isDown('LEFT') || input.isDown('a')) { 390 | player.pos[0] -= playerSpeed * dt; 391 | } 392 | 393 | if(input.isDown('RIGHT') || input.isDown('d')) { 394 | player.pos[0] += playerSpeed * dt; 395 | } 396 | 397 | if(input.isDown('SPACE') && 398 | !isGameOver && 399 | Date.now() - lastFire > 100) { 400 | var x = player.pos[0] + player.sprite.size[0] / 2; 401 | var y = player.pos[1] + player.sprite.size[1] / 2; 402 | 403 | bullets.push({ pos: [x, y], 404 | dir: 'forward', 405 | sprite: new Sprite('img/sprites.png', [0, 39], [18, 8]) }); 406 | bullets.push({ pos: [x, y], 407 | dir: 'up', 408 | sprite: new Sprite('img/sprites.png', [0, 50], [9, 5]) }); 409 | bullets.push({ pos: [x, y], 410 | dir: 'down', 411 | sprite: new Sprite('img/sprites.png', [0, 60], [9, 5]) }); 412 | 413 | 414 | lastFire = Date.now(); 415 | } 416 | } 417 | ``` 418 | 419 | If the user presses the down arrow or the 's' key, we move the player up the y axis. The canvas coordinate system places (0, 0) at the top left, so moving up the y axis moves the object down the screen. We do the same for the up, left, and right keys. 420 | 421 | Notice that we defined the `playerSpeed` variable at the [top of app.js](https://github.com/jlongster/canvas-game-bootstrap/blob/master/js/app.js#L71). Here are the speeds we defined: 422 | 423 | ```js 424 | // Speed in pixels per second 425 | var playerSpeed = 200; 426 | var bulletSpeed = 500; 427 | var enemySpeed = 100; 428 | ``` 429 | 430 | By multiplying `playerSpeed` by the `dt` parameter, we calculate the correct amount of pixels to move for that frame. If 1 second has passed since that last update (which is the `dt` parameter), the player would move 200 pixels. If .5 seconds have passed, he would move 100 pixels. This shows a constant rate of movement independant of framerate. 431 | 432 | The last thing we do is fire a bullet if the space button is pressed, the game isn't over, and it's been more than 100 milliseconds since the last bullet was fired. `lastFire` is a global variable that is part of the game state. It helps us control the rate of fire; otherwise the player could fire a bullet *every frame*! That's a little too easy, right? 433 | 434 | ```js 435 | var x = player.pos[0] + player.sprite.size[0] / 2; 436 | var y = player.pos[1] + player.sprite.size[1] / 2; 437 | 438 | bullets.push({ pos: [x, y], 439 | dir: 'forward', 440 | sprite: new Sprite('img/sprites.png', [0, 39], [18, 8]) }); 441 | bullets.push({ pos: [x, y], 442 | dir: 'up', 443 | sprite: new Sprite('img/sprites.png', [0, 50], [9, 5]) }); 444 | bullets.push({ pos: [x, y], 445 | dir: 'down', 446 | sprite: new Sprite('img/sprites.png', [0, 60], [9, 5]) }); 447 | 448 | lastFire = Date.now(); 449 | 450 | ``` 451 | 452 | If we are firing a bullet, we add 3 bullet entities to the scene. The `bullets` array keeps track of all the bullets to the scene, so it's as simple as pushing them on there. 453 | 454 | We calculate the position of the new bullets in the `x` and `y` variables. We add them at the position of the player, plus half the width and height of the player so that they shoot from the center of the ship. 455 | 456 | ![](http://jlongster.com/s/canvas-tutorial/ship-bullets.png) 457 | 458 | We add 3 bullets because they shoot out from different directions. This makes the game easier because the player can't get "trapped" horizontally when they are lots of ships. To differentiate the types of bullets, we add a `dir` property to the entity with a value of `'forward'`, `'up'`, or `'down'`. 459 | 460 | ### Entities 461 | 462 | All of the entities need to be updated. We have the single player entity and 3 arrays for bullets, enemies, and explosions. 463 | 464 | ```js 465 | function updateEntities(dt) { 466 | // Update the player sprite animation 467 | player.sprite.update(dt); 468 | 469 | // Update all the bullets 470 | for(var i=0; i canvas.height || 482 | bullet.pos[0] > canvas.width) { 483 | bullets.splice(i, 1); 484 | i--; 485 | } 486 | } 487 | 488 | // Update all the enemies 489 | for(var i=0; i canvas.height || 533 | bullet.pos[0] > canvas.width) { 534 | bullets.splice(i, 1); 535 | i--; 536 | } 537 | ``` 538 | 539 | Then we check to see if we should remove the bullet. The position is checked against the top, right, and bottom sides because bullets move up, right, and down. We don't make sure the whole bullet sprite is offscreen because the bullets move fast enough not to master. Simply checking the position (which is the top-left corner of the sprite) is enough. 540 | 541 | To remove the bullet, we destructively modify the `bullets` array and decrement the `i` in the loop since the array has been modified. If we didn't do this, the next bullet would be skipped. 542 | 543 | (Note: I wouldn't consider this a "best practice"; it's best to mark which ones need to be removed and remove them later. This fixes a lot of potential bugs with syncing issues, but for this tutorial we are keeping it simple.) 544 | 545 | ### Collision Detection 546 | 547 | Now for something that everybody fears: collision detection! It's actually not as hard as it sounds, at least for our 2d game. It would be hard if we had to *resolve* collisions. 548 | 549 | Resolving a collision means that you move one or both objects so that they are not colliding anymore. You need this for platformers where the player can run into a wall and the wall pushes back to stop him. For our game, we just need to make things explode! 550 | 551 | There are 3 types of collisions we need to check: 552 | 553 | 1. Enemies hit by bullets 554 | 2. The player hit by an enemy 555 | 3. The player hits the edge of the screen 556 | 557 | Detecting 2d collisions is simple: 558 | 559 | ```js 560 | function collides(x, y, r, b, x2, y2, r2, b2) { 561 | return !(r <= x2 || x > r2 || 562 | b <= y2 || y > b2); 563 | } 564 | 565 | function boxCollides(pos, size, pos2, size2) { 566 | return collides(pos[0], pos[1], 567 | pos[0] + size[0], pos[1] + size[1], 568 | pos2[0], pos2[1], 569 | pos2[0] + size2[0], pos2[1] + size2[1]); 570 | } 571 | ``` 572 | 573 | These two functions could be consolidated into one, but I find it easier to read this way. `collides` takes the coordinates for the top/left and bottom/right corners for both boxes and checks to see if there are any gaps. 574 | 575 | Think of it this way: if `r` is the x coordinate of the right side of box A and `x2` is the x coordinate of the left side of box B, if `r <= x2` is true then there's a gap and no matter any other positions or sizes of the boxes there cannot be an overlap, so there is no collision. There are 4 checks: one for each sides of the boxes. If there's ever a gap: no collision. 576 | 577 | Illustrations might help here, but this article isn't focusing on collision detection. You should be able to use the above functions without worrying too much about how it works. (Note: technically this is a very simplified form of the [separating axis theorem](http://www.sevenson.com.au/actionscript/sat/)). If you google "2d box collision" you will find this algorithm in several forms. 578 | 579 | The `boxCollides` function is a wrapper around `collides` so that we can pass our native vector objects into it (which are simple 2-element arrays). It also converts our `size` values into absolute coordinates so it can compare the values. 580 | 581 | And here's the motherload of code that actually runs the collision detection: 582 | 583 | ```js 584 | function checkCollisions() { 585 | checkPlayerBounds(); 586 | 587 | // Run collision detection for all enemies and bullets 588 | for(var i=0; i canvas.width - player.sprite.size[0]) { 662 | player.pos[0] = canvas.width - player.sprite.size[0]; 663 | } 664 | 665 | if(player.pos[1] < 0) { 666 | player.pos[1] = 0; 667 | } 668 | else if(player.pos[1] > canvas.height - player.sprite.size[1]) { 669 | player.pos[1] = canvas.height - player.sprite.size[1]; 670 | } 671 | } 672 | ``` 673 | 674 | It simply keeps the player inside the boundaries of the game by forcing it's x and y coordinates to be between 0 and `canvas.width`/`canvas.height`. 675 | 676 | ## Rendering 677 | 678 | We are almost done! We just need to define the `render` function which is called by our game loop to render the scene each frame. Here's what it looks like: 679 | 680 | ```js 681 | // Draw everything 682 | function render() { 683 | ctx.fillStyle = terrainPattern; 684 | ctx.fillRect(0, 0, canvas.width, canvas.height); 685 | 686 | // Render the player if the game isn't over 687 | if(!isGameOver) { 688 | renderEntity(player); 689 | } 690 | 691 | renderEntities(bullets); 692 | renderEntities(enemies); 693 | renderEntities(explosions); 694 | }; 695 | 696 | function renderEntities(list) { 697 | for(var i=0; i