├── .github └── FUNDING.yml ├── README.md ├── assets ├── boss.png ├── bullet.png ├── death-ray.png ├── enemy-blue-bullet.png ├── enemy-blue.png ├── enemy-green.png ├── explode.png ├── player.png ├── spacefont │ ├── spacefont.png │ └── spacefont.xml └── starfield.png ├── game.js └── index.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jschomay # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHASER GAME DEMO 2 | ================ 3 | The commits in this repo go step by step through the process of building a polished space shooter game with the [Phaser.js](http://phaser.io) HTML5 game framework. 4 | 5 | ![screenshot](https://31.media.tumblr.com/f75844fe624ff1126b14e03b33c4cc8a/tumblr_inline_nfm51gCKmu1sbxzuw.png) 6 | 7 | You can follow through a written tutorial of this game with more commentary at [on my blog](http://jschomay.tumblr.com/post/103568304133/html5-space-shooter-game-tutorial-with-phaser-js). 8 | 9 | 10 | Credits 11 | ------- 12 | - Many of the assets and the base for the game come from the [invaders phaser examples](http://examples.phaser.io/_site/view_full.html?d=games&f=invaders.js&t=invaders). 13 | - Cool enemy ship graphics from http://opengameart.org/users/skorpio 14 | - Nice enemy lasers from http://opengameart.org/users/bonsaiheldin 15 | - Font face is http://www.fontspace.com/nimavisual/trench 16 | 17 | 18 | Very brief introduction to game dev concepts 19 | -------------------------------------------- 20 | Games are all about interactivity. A game is an experience that unfolds over time through a collaboration between the game creator(s) and the player. 21 | 22 | ### 3 facets to game design 23 | 24 | 1. Production values - graphics, sounds, special effects, polish 25 | 2. Content - story, theme, concept, characters, artwork 26 | 3. Gameplay - mechanics, challenge, pacing, fun, controls, "feel" 27 | 28 | ### 4 basic parts of game dev 29 | 30 | 1. Game loop - means to change and display state over time (usually for animation (at 60fps) but could be turn based) 31 | 2. Input - get input from the player (otherwise it is a simulation, not a game) 32 | 3. Update - change the game state based on internal logic and values and responding to player input 33 | 4. Render - redraw the visual representation of the game state at that time 34 | 35 | 36 | Resources 37 | --------- 38 | #### Phaser: 39 | - Examples - http://examples.phaser.io/ 40 | - API documentation - http://docs.phaser.io/ 41 | - Starting templates - https://github.com/photonstorm/phaser/tree/master/resources/Project%20Templates 42 | - Forum - http://www.html5gamedevs.com/forum/14-phaser/ 43 | - Interactive mechanics examples - http://gamemechanicexplorer.com/ 44 | - Tons of online tutorials - http://www.lessmilk.com/phaser-tutorial/ 45 | 46 | #### Game assets: 47 | - Sound effects (sfx) generator - http://www.superflashbros.net/as3sfxr/ 48 | - Custom bitmap font generator - http://kvazars.com/littera/ 49 | - Free art and sound database - http://opengameart.org 50 | - Links to other more assets - http://letsmakegames.org/resources/art-assets-for-game-developers/ 51 | - Assets on redit - http://www.reddit.com/r/GameAssets 52 | 53 | 54 | Additional topics not covered 55 | ----------------------------- 56 | - Best practices for more maintainable code and smaller files 57 | - Modular development (commonJS, etc) 58 | - Custom classes inheriting from phaser classes 59 | - Build tools (browserify, gulp, etc) 60 | - yoaman for scaffolding 61 | - Scaling modes 62 | - Optimization / profiling / debugging / testing 63 | - Mobile 64 | - Wrapping for native (CocoonJS, Cordova/PhoneGap) 65 | - Communicating with server / multiplayer 66 | - Tilemaps and other game types 67 | - Marketing / distribution / monetization / 3rd party APIs 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /assets/boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/boss.png -------------------------------------------------------------------------------- /assets/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/bullet.png -------------------------------------------------------------------------------- /assets/death-ray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/death-ray.png -------------------------------------------------------------------------------- /assets/enemy-blue-bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/enemy-blue-bullet.png -------------------------------------------------------------------------------- /assets/enemy-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/enemy-blue.png -------------------------------------------------------------------------------- /assets/enemy-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/enemy-green.png -------------------------------------------------------------------------------- /assets/explode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/explode.png -------------------------------------------------------------------------------- /assets/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/player.png -------------------------------------------------------------------------------- /assets/spacefont/spacefont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/spacefont/spacefont.png -------------------------------------------------------------------------------- /assets/spacefont/spacefont.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /assets/starfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschomay/phaser-demo-game/7a9fbe559a7f8043df1a178dc74019fd3cec6e94/assets/starfield.png -------------------------------------------------------------------------------- /game.js: -------------------------------------------------------------------------------- 1 | var game = new Phaser.Game(800,600, Phaser.AUTO, 'phaser-demo', {preload: preload, create: create, update: update, render: render}); 2 | 3 | var player; 4 | var greenEnemies; 5 | var blueEnemies; 6 | var enemyBullets; 7 | var starfield; 8 | var cursors; 9 | var bank; 10 | var shipTrail; 11 | var explosions; 12 | var playerDeath; 13 | var bullets; 14 | var fireButton; 15 | var bulletTimer = 0; 16 | var shields; 17 | var score = 0; 18 | var scoreText; 19 | var greenEnemyLaunchTimer; 20 | var greenEnemySpacing = 1000; 21 | var blueEnemyLaunchTimer; 22 | var blueEnemyLaunched = false; 23 | var blueEnemySpacing = 2500; 24 | var bossLaunchTimer; 25 | var bossLaunched = false; 26 | var bossSpacing = 20000; 27 | var bossBulletTimer = 0; 28 | var bossYdirection = -1; 29 | var gameOver; 30 | 31 | var ACCLERATION = 600; 32 | var DRAG = 400; 33 | var MAXSPEED = 400; 34 | 35 | function preload() { 36 | game.load.image('starfield', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/starfield.png'); 37 | game.load.image('ship', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/player.png'); 38 | game.load.image('bullet', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/bullet.png'); 39 | game.load.image('enemy-green', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/enemy-green.png'); 40 | game.load.image('enemy-blue', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/enemy-blue.png'); 41 | game.load.image('blueEnemyBullet', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/enemy-blue-bullet.png'); 42 | game.load.spritesheet('explosion', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/explode.png', 128, 128); 43 | game.load.bitmapFont('spacefont', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/spacefont/spacefont.png', 'https://rawgit.com/jschomay/phaser-demo-game/master/assets/spacefont/spacefont.xml'); 44 | game.load.image('boss', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/boss.png'); 45 | game.load.image('deathRay', 'https://raw.githubusercontent.com/jschomay/phaser-demo-game/master/assets/death-ray.png'); 46 | } 47 | 48 | function create() { 49 | // The scrolling starfield background 50 | starfield = game.add.tileSprite(0, 0, 800, 600, 'starfield'); 51 | 52 | // Our bullet group 53 | bullets = game.add.group(); 54 | bullets.enableBody = true; 55 | bullets.physicsBodyType = Phaser.Physics.ARCADE; 56 | bullets.createMultiple(30, 'bullet'); 57 | bullets.setAll('anchor.x', 0.5); 58 | bullets.setAll('anchor.y', 1); 59 | bullets.setAll('outOfBoundsKill', true); 60 | bullets.setAll('checkWorldBounds', true); 61 | 62 | // The hero! 63 | player = game.add.sprite(400, 500, 'ship'); 64 | player.health = 100; 65 | player.anchor.setTo(0.5, 0.5); 66 | game.physics.enable(player, Phaser.Physics.ARCADE); 67 | player.body.maxVelocity.setTo(MAXSPEED, MAXSPEED); 68 | player.body.drag.setTo(DRAG, DRAG); 69 | player.weaponLevel = 1 70 | player.events.onKilled.add(function(){ 71 | shipTrail.kill(); 72 | }); 73 | player.events.onRevived.add(function(){ 74 | shipTrail.start(false, 5000, 10); 75 | }); 76 | 77 | // The baddies! 78 | greenEnemies = game.add.group(); 79 | greenEnemies.enableBody = true; 80 | greenEnemies.physicsBodyType = Phaser.Physics.ARCADE; 81 | greenEnemies.createMultiple(5, 'enemy-green'); 82 | greenEnemies.setAll('anchor.x', 0.5); 83 | greenEnemies.setAll('anchor.y', 0.5); 84 | greenEnemies.setAll('scale.x', 0.5); 85 | greenEnemies.setAll('scale.y', 0.5); 86 | greenEnemies.setAll('angle', 180); 87 | greenEnemies.forEach(function(enemy){ 88 | addEnemyEmitterTrail(enemy); 89 | enemy.body.setSize(enemy.width * 3 / 4, enemy.height * 3 / 4); 90 | enemy.damageAmount = 20; 91 | enemy.events.onKilled.add(function(){ 92 | enemy.trail.kill(); 93 | }); 94 | }); 95 | 96 | game.time.events.add(1000, launchGreenEnemy); 97 | 98 | // Blue enemy's bullets 99 | blueEnemyBullets = game.add.group(); 100 | blueEnemyBullets.enableBody = true; 101 | blueEnemyBullets.physicsBodyType = Phaser.Physics.ARCADE; 102 | blueEnemyBullets.createMultiple(30, 'blueEnemyBullet'); 103 | blueEnemyBullets.callAll('crop', null, {x: 90, y: 0, width: 90, height: 70}); 104 | blueEnemyBullets.setAll('alpha', 0.9); 105 | blueEnemyBullets.setAll('anchor.x', 0.5); 106 | blueEnemyBullets.setAll('anchor.y', 0.5); 107 | blueEnemyBullets.setAll('outOfBoundsKill', true); 108 | blueEnemyBullets.setAll('checkWorldBounds', true); 109 | blueEnemyBullets.forEach(function(enemy){ 110 | enemy.body.setSize(20, 20); 111 | }); 112 | 113 | // More baddies! 114 | blueEnemies = game.add.group(); 115 | blueEnemies.enableBody = true; 116 | blueEnemies.physicsBodyType = Phaser.Physics.ARCADE; 117 | blueEnemies.createMultiple(30, 'enemy-blue'); 118 | blueEnemies.setAll('anchor.x', 0.5); 119 | blueEnemies.setAll('anchor.y', 0.5); 120 | blueEnemies.setAll('scale.x', 0.5); 121 | blueEnemies.setAll('scale.y', 0.5); 122 | blueEnemies.setAll('angle', 180); 123 | blueEnemies.forEach(function(enemy){ 124 | enemy.damageAmount = 40; 125 | }); 126 | 127 | // The boss 128 | boss = game.add.sprite(0, 0, 'boss'); 129 | boss.exists = false; 130 | boss.alive = false; 131 | boss.anchor.setTo(0.5, 0.5); 132 | boss.damageAmount = 50; 133 | boss.angle = 180; 134 | boss.scale.x = 0.6; 135 | boss.scale.y = 0.6; 136 | game.physics.enable(boss, Phaser.Physics.ARCADE); 137 | boss.body.maxVelocity.setTo(100, 80); 138 | boss.dying = false; 139 | boss.finishOff = function() { 140 | if (!boss.dying) { 141 | boss.dying = true; 142 | bossDeath.x = boss.x; 143 | bossDeath.y = boss.y; 144 | bossDeath.start(false, 1000, 50, 20); 145 | // kill boss after explotions 146 | game.time.events.add(1000, function(){ 147 | var explosion = explosions.getFirstExists(false); 148 | var beforeScaleX = explosions.scale.x; 149 | var beforeScaleY = explosions.scale.y; 150 | var beforeAlpha = explosions.alpha; 151 | explosion.reset(boss.body.x + boss.body.halfWidth, boss.body.y + boss.body.halfHeight); 152 | explosion.alpha = 0.4; 153 | explosion.scale.x = 3; 154 | explosion.scale.y = 3; 155 | var animation = explosion.play('explosion', 30, false, true); 156 | animation.onComplete.addOnce(function(){ 157 | explosion.scale.x = beforeScaleX; 158 | explosion.scale.y = beforeScaleY; 159 | explosion.alpha = beforeAlpha; 160 | }); 161 | boss.kill(); 162 | booster.kill(); 163 | boss.dying = false; 164 | bossDeath.on = false; 165 | // queue next boss 166 | bossLaunchTimer = game.time.events.add(game.rnd.integerInRange(bossSpacing, bossSpacing + 5000), launchBoss); 167 | }); 168 | 169 | // reset pacing for other enemies 170 | blueEnemySpacing = 2500; 171 | greenEnemySpacing = 1000; 172 | 173 | // give some bonus health 174 | player.health = Math.min(100, player.health + 40); 175 | shields.render(); 176 | } 177 | }; 178 | 179 | // Boss death ray 180 | function addRay(leftRight) { 181 | var ray = game.add.sprite(leftRight * boss.width * 0.75, 0, 'deathRay'); 182 | ray.alive = false; 183 | ray.visible = false; 184 | boss.addChild(ray); 185 | ray.crop({x: 0, y: 0, width: 40, height: 40}); 186 | ray.anchor.x = 0.5; 187 | ray.anchor.y = 0.5; 188 | ray.scale.x = 2.5; 189 | ray.damageAmount = boss.damageAmount; 190 | game.physics.enable(ray, Phaser.Physics.ARCADE); 191 | ray.body.setSize(ray.width / 5, ray.height / 4); 192 | ray.update = function() { 193 | this.alpha = game.rnd.realInRange(0.6, 1); 194 | }; 195 | boss['ray' + (leftRight > 0 ? 'Right' : 'Left')] = ray; 196 | } 197 | addRay(1); 198 | addRay(-1); 199 | // need to add the ship texture to the group so it renders over the rays 200 | var ship = game.add.sprite(0, 0, 'boss'); 201 | ship.anchor = {x: 0.5, y: 0.5}; 202 | boss.addChild(ship); 203 | 204 | boss.fire = function() { 205 | if (game.time.now > bossBulletTimer) { 206 | var raySpacing = 3000; 207 | var chargeTime = 1500; 208 | var rayTime = 1500; 209 | 210 | function chargeAndShoot(side) { 211 | ray = boss['ray' + side]; 212 | ray.name = side 213 | ray.revive(); 214 | ray.y = 80; 215 | ray.alpha = 0; 216 | ray.scale.y = 13; 217 | game.add.tween(ray).to({alpha: 1}, chargeTime, Phaser.Easing.Linear.In, true).onComplete.add(function(ray){ 218 | ray.scale.y = 150; 219 | game.add.tween(ray).to({y: -1500}, rayTime, Phaser.Easing.Linear.In, true).onComplete.add(function(ray){ 220 | ray.kill(); 221 | }); 222 | }); 223 | } 224 | chargeAndShoot('Right'); 225 | chargeAndShoot('Left'); 226 | 227 | bossBulletTimer = game.time.now + raySpacing; 228 | } 229 | }; 230 | 231 | boss.update = function() { 232 | if (!boss.alive) return; 233 | 234 | boss.rayLeft.update(); 235 | boss.rayRight.update(); 236 | 237 | if (boss.y > 140) { 238 | boss.body.acceleration.y = -50; 239 | } 240 | if (boss.y < 140) { 241 | boss.body.acceleration.y = 50; 242 | } 243 | if (boss.x > player.x + 50) { 244 | boss.body.acceleration.x = -50; 245 | } else if (boss.x < player.x - 50) { 246 | boss.body.acceleration.x = 50; 247 | } else { 248 | boss.body.acceleration.x = 0; 249 | } 250 | 251 | // Squish and rotate boss for illusion of "banking" 252 | var bank = boss.body.velocity.x / MAXSPEED; 253 | boss.scale.x = 0.6 - Math.abs(bank) / 3; 254 | boss.angle = 180 - bank * 20; 255 | 256 | booster.x = boss.x + -5 * bank; 257 | booster.y = boss.y + 10 * Math.abs(bank) - boss.height / 2; 258 | 259 | // fire if player is in target 260 | var angleToPlayer = game.math.radToDeg(game.physics.arcade.angleBetween(boss, player)) - 90; 261 | var anglePointing = 180 - Math.abs(boss.angle); 262 | if (anglePointing - angleToPlayer < 18) { 263 | boss.fire(); 264 | } 265 | } 266 | 267 | // boss's boosters 268 | booster = game.add.emitter(boss.body.x, boss.body.y - boss.height / 2); 269 | booster.width = 0; 270 | booster.makeParticles('blueEnemyBullet'); 271 | booster.forEach(function(p){ 272 | p.crop({x: 120, y: 0, width: 45, height: 50}); 273 | // clever way of making 2 exhaust trails by shifing particles randomly left or right 274 | p.anchor.x = game.rnd.pick([1,-1]) * 0.95 + 0.5; 275 | p.anchor.y = 0.75; 276 | }); 277 | booster.setXSpeed(0, 0); 278 | booster.setRotation(0,0); 279 | booster.setYSpeed(-30, -50); 280 | booster.gravity = 0; 281 | booster.setAlpha(1, 0.1, 400); 282 | booster.setScale(0.3, 0, 0.7, 0, 5000, Phaser.Easing.Quadratic.Out); 283 | boss.bringToTop(); 284 | 285 | // And some controls to play the game with 286 | cursors = game.input.keyboard.createCursorKeys(); 287 | fireButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); 288 | 289 | // Add an emitter for the ship's trail 290 | shipTrail = game.add.emitter(player.x, player.y + 10, 400); 291 | shipTrail.width = 10; 292 | shipTrail.makeParticles('bullet'); 293 | shipTrail.setXSpeed(30, -30); 294 | shipTrail.setYSpeed(200, 180); 295 | shipTrail.setRotation(50,-50); 296 | shipTrail.setAlpha(1, 0.01, 800); 297 | shipTrail.setScale(0.05, 0.4, 0.05, 0.4, 2000, Phaser.Easing.Quintic.Out); 298 | shipTrail.start(false, 5000, 10); 299 | 300 | // An explosion pool 301 | explosions = game.add.group(); 302 | explosions.enableBody = true; 303 | explosions.physicsBodyType = Phaser.Physics.ARCADE; 304 | explosions.createMultiple(30, 'explosion'); 305 | explosions.setAll('anchor.x', 0.5); 306 | explosions.setAll('anchor.y', 0.5); 307 | explosions.forEach( function(explosion) { 308 | explosion.animations.add('explosion'); 309 | }); 310 | 311 | // Big explosion 312 | playerDeath = game.add.emitter(player.x, player.y); 313 | playerDeath.width = 50; 314 | playerDeath.height = 50; 315 | playerDeath.makeParticles('explosion', [0,1,2,3,4,5,6,7], 10); 316 | playerDeath.setAlpha(0.9, 0, 800); 317 | playerDeath.setScale(0.1, 0.6, 0.1, 0.6, 1000, Phaser.Easing.Quintic.Out); 318 | 319 | // Big explosion for boss 320 | bossDeath = game.add.emitter(boss.x, boss.y); 321 | bossDeath.width = boss.width / 2; 322 | bossDeath.height = boss.height / 2; 323 | bossDeath.makeParticles('explosion', [0,1,2,3,4,5,6,7], 20); 324 | bossDeath.setAlpha(0.9, 0, 900); 325 | bossDeath.setScale(0.3, 1.0, 0.3, 1.0, 1000, Phaser.Easing.Quintic.Out); 326 | 327 | // Shields stat 328 | shields = game.add.bitmapText(game.world.width - 250, 10, 'spacefont', '' + player.health +'%', 50); 329 | shields.render = function () { 330 | shields.text = 'Shields: ' + Math.max(player.health, 0) +'%'; 331 | }; 332 | shields.render(); 333 | 334 | // Score 335 | scoreText = game.add.bitmapText(10, 10, 'spacefont', '', 50); 336 | scoreText.render = function () { 337 | scoreText.text = 'Score: ' + score; 338 | }; 339 | scoreText.render(); 340 | 341 | // Game over text 342 | gameOver = game.add.bitmapText(game.world.centerX, game.world.centerY, 'spacefont', 'GAME OVER!', 110); 343 | gameOver.x = gameOver.x - gameOver.textWidth / 2; 344 | gameOver.y = gameOver.y - gameOver.textHeight / 3; 345 | gameOver.visible = false; 346 | } 347 | 348 | function update() { 349 | // Scroll the background 350 | starfield.tilePosition.y += 2; 351 | 352 | // Reset the player, then check for movement keys 353 | player.body.acceleration.x = 0; 354 | 355 | if (cursors.left.isDown) 356 | { 357 | player.body.acceleration.x = -ACCLERATION; 358 | } 359 | else if (cursors.right.isDown) 360 | { 361 | player.body.acceleration.x = ACCLERATION; 362 | } 363 | 364 | // Stop at screen edges 365 | if (player.x > game.width - 50) { 366 | player.x = game.width - 50; 367 | player.body.acceleration.x = 0; 368 | } 369 | if (player.x < 50) { 370 | player.x = 50; 371 | player.body.acceleration.x = 0; 372 | } 373 | 374 | // Fire bullet 375 | if (player.alive && (fireButton.isDown || game.input.activePointer.isDown)) { 376 | fireBullet(); 377 | } 378 | 379 | // Move ship towards mouse pointer 380 | if (game.input.x < game.width - 20 && 381 | game.input.x > 20 && 382 | game.input.y > 20 && 383 | game.input.y < game.height - 20) { 384 | var minDist = 200; 385 | var dist = game.input.x - player.x; 386 | player.body.velocity.x = MAXSPEED * game.math.clamp(dist / minDist, -1, 1); 387 | } 388 | 389 | // Squish and rotate ship for illusion of "banking" 390 | bank = player.body.velocity.x / MAXSPEED; 391 | player.scale.x = 1 - Math.abs(bank) / 2; 392 | player.angle = bank * 30; 393 | 394 | // Keep the shipTrail lined up with the ship 395 | shipTrail.x = player.x; 396 | 397 | // Check collisions 398 | game.physics.arcade.overlap(player, greenEnemies, shipCollide, null, this); 399 | game.physics.arcade.overlap(greenEnemies, bullets, hitEnemy, null, this); 400 | 401 | game.physics.arcade.overlap(player, blueEnemies, shipCollide, null, this); 402 | game.physics.arcade.overlap(blueEnemies, bullets, hitEnemy, null, this); 403 | 404 | game.physics.arcade.overlap(boss, bullets, hitEnemy, bossHitTest, this); 405 | game.physics.arcade.overlap(player, boss.rayLeft, enemyHitsPlayer, null, this); 406 | game.physics.arcade.overlap(player, boss.rayRight, enemyHitsPlayer, null, this); 407 | 408 | game.physics.arcade.overlap(blueEnemyBullets, player, enemyHitsPlayer, null, this); 409 | 410 | // Game over? 411 | if (! player.alive && gameOver.visible === false) { 412 | gameOver.visible = true; 413 | gameOver.alpha = 0; 414 | var fadeInGameOver = game.add.tween(gameOver); 415 | fadeInGameOver.to({alpha: 1}, 1000, Phaser.Easing.Quintic.Out); 416 | fadeInGameOver.onComplete.add(setResetHandlers); 417 | fadeInGameOver.start(); 418 | function setResetHandlers() { 419 | // The "click to restart" handler 420 | tapRestart = game.input.onTap.addOnce(_restart,this); 421 | spaceRestart = fireButton.onDown.addOnce(_restart,this); 422 | function _restart() { 423 | tapRestart.detach(); 424 | spaceRestart.detach(); 425 | restart(); 426 | } 427 | } 428 | } 429 | } 430 | 431 | function render() { 432 | // for (var i = 0; i < greenEnemies.length; i++) 433 | // { 434 | // game.debug.body(greenEnemies.children[i]); 435 | // } 436 | // game.debug.body(player); 437 | } 438 | 439 | function fireBullet() { 440 | switch (player.weaponLevel) { 441 | case 1: 442 | // To avoid them being allowed to fire too fast we set a time limit 443 | if (game.time.now > bulletTimer) 444 | { 445 | var BULLET_SPEED = 400; 446 | var BULLET_SPACING = 250; 447 | // Grab the first bullet we can from the pool 448 | var bullet = bullets.getFirstExists(false); 449 | 450 | if (bullet) 451 | { 452 | // And fire it 453 | // Make bullet come out of tip of ship with right angle 454 | var bulletOffset = 20 * Math.sin(game.math.degToRad(player.angle)); 455 | bullet.reset(player.x + bulletOffset, player.y); 456 | bullet.angle = player.angle; 457 | game.physics.arcade.velocityFromAngle(bullet.angle - 90, BULLET_SPEED, bullet.body.velocity); 458 | bullet.body.velocity.x += player.body.velocity.x; 459 | 460 | bulletTimer = game.time.now + BULLET_SPACING; 461 | } 462 | } 463 | break; 464 | 465 | case 2: 466 | if (game.time.now > bulletTimer) { 467 | var BULLET_SPEED = 400; 468 | var BULLET_SPACING = 550; 469 | 470 | 471 | for (var i = 0; i < 3; i++) { 472 | var bullet = bullets.getFirstExists(false); 473 | if (bullet) { 474 | // Make bullet come out of tip of ship with right angle 475 | var bulletOffset = 20 * Math.sin(game.math.degToRad(player.angle)); 476 | bullet.reset(player.x + bulletOffset, player.y); 477 | // "Spread" angle of 1st and 3rd bullets 478 | var spreadAngle; 479 | if (i === 0) spreadAngle = -20; 480 | if (i === 1) spreadAngle = 0; 481 | if (i === 2) spreadAngle = 20; 482 | bullet.angle = player.angle + spreadAngle; 483 | game.physics.arcade.velocityFromAngle(spreadAngle - 90, BULLET_SPEED, bullet.body.velocity); 484 | bullet.body.velocity.x += player.body.velocity.x; 485 | } 486 | bulletTimer = game.time.now + BULLET_SPACING; 487 | } 488 | } 489 | } 490 | } 491 | 492 | 493 | function launchGreenEnemy() { 494 | var ENEMY_SPEED = 300; 495 | 496 | var enemy = greenEnemies.getFirstExists(false); 497 | if (enemy) { 498 | enemy.reset(game.rnd.integerInRange(0, game.width), -20); 499 | enemy.body.velocity.x = game.rnd.integerInRange(-300, 300); 500 | enemy.body.velocity.y = ENEMY_SPEED; 501 | enemy.body.drag.x = 100; 502 | 503 | enemy.trail.start(false, 800, 1); 504 | 505 | // Update function for each enemy ship to update rotation etc 506 | enemy.update = function(){ 507 | enemy.angle = 180 - game.math.radToDeg(Math.atan2(enemy.body.velocity.x, enemy.body.velocity.y)); 508 | 509 | enemy.trail.x = enemy.x; 510 | enemy.trail.y = enemy.y -10; 511 | 512 | // Kill enemies once they go off screen 513 | if (enemy.y > game.height + 200) { 514 | enemy.kill(); 515 | enemy.y = -20; 516 | } 517 | } 518 | } 519 | 520 | // Send another enemy soon 521 | greenEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(greenEnemySpacing, greenEnemySpacing + 1000), launchGreenEnemy); 522 | } 523 | 524 | function launchBlueEnemy() { 525 | var startingX = game.rnd.integerInRange(100, game.width - 100); 526 | var verticalSpeed = 180; 527 | var spread = 60; 528 | var frequency = 70; 529 | var verticalSpacing = 70; 530 | var numEnemiesInWave = 5; 531 | 532 | // Launch wave 533 | for (var i =0; i < numEnemiesInWave; i++) { 534 | var enemy = blueEnemies.getFirstExists(false); 535 | if (enemy) { 536 | enemy.startingX = startingX; 537 | enemy.reset(game.width / 2, -verticalSpacing * i); 538 | enemy.body.velocity.y = verticalSpeed; 539 | 540 | // Set up firing 541 | var bulletSpeed = 400; 542 | var firingDelay = 2000; 543 | enemy.bullets = 1; 544 | enemy.lastShot = 0; 545 | 546 | // Update function for each enemy 547 | enemy.update = function(){ 548 | // Wave movement 549 | this.body.x = this.startingX + Math.sin((this.y) / frequency) * spread; 550 | 551 | // Squish and rotate ship for illusion of "banking" 552 | bank = Math.cos((this.y + 60) / frequency) 553 | this.scale.x = 0.5 - Math.abs(bank) / 8; 554 | this.angle = 180 - bank * 2; 555 | 556 | // Fire 557 | enemyBullet = blueEnemyBullets.getFirstExists(false); 558 | if (enemyBullet && 559 | this.alive && 560 | this.bullets && 561 | this.y > game.width / 8 && 562 | game.time.now > firingDelay + this.lastShot) { 563 | this.lastShot = game.time.now; 564 | this.bullets--; 565 | enemyBullet.reset(this.x, this.y + this.height / 2); 566 | enemyBullet.damageAmount = this.damageAmount; 567 | var angle = game.physics.arcade.moveToObject(enemyBullet, player, bulletSpeed); 568 | enemyBullet.angle = game.math.radToDeg(angle); 569 | } 570 | 571 | // Kill enemies once they go off screen 572 | if (this.y > game.height + 200) { 573 | this.kill(); 574 | this.y = -20; 575 | } 576 | }; 577 | } 578 | } 579 | 580 | // Send another wave soon 581 | blueEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(blueEnemySpacing, blueEnemySpacing + 4000), launchBlueEnemy); 582 | } 583 | 584 | function launchBoss() { 585 | boss.reset(game.width / 2, -boss.height); 586 | booster.start(false, 1000, 10); 587 | boss.health = 501; 588 | bossBulletTimer = game.time.now + 5000; 589 | } 590 | 591 | function addEnemyEmitterTrail(enemy) { 592 | var enemyTrail = game.add.emitter(enemy.x, player.y - 10, 100); 593 | enemyTrail.width = 10; 594 | enemyTrail.makeParticles('explosion', [1,2,3,4,5]); 595 | enemyTrail.setXSpeed(20, -20); 596 | enemyTrail.setRotation(50,-50); 597 | enemyTrail.setAlpha(0.4, 0, 800); 598 | enemyTrail.setScale(0.01, 0.1, 0.01, 0.1, 1000, Phaser.Easing.Quintic.Out); 599 | enemy.trail = enemyTrail; 600 | } 601 | 602 | 603 | function shipCollide(player, enemy) { 604 | enemy.kill(); 605 | 606 | player.damage(enemy.damageAmount); 607 | shields.render(); 608 | 609 | if (player.alive) { 610 | var explosion = explosions.getFirstExists(false); 611 | explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight); 612 | explosion.alpha = 0.7; 613 | explosion.play('explosion', 30, false, true); 614 | } else { 615 | playerDeath.x = player.x; 616 | playerDeath.y = player.y; 617 | playerDeath.start(false, 1000, 10, 10); 618 | } 619 | } 620 | 621 | 622 | function hitEnemy(enemy, bullet) { 623 | var explosion = explosions.getFirstExists(false); 624 | explosion.reset(bullet.body.x + bullet.body.halfWidth, bullet.body.y + bullet.body.halfHeight); 625 | explosion.body.velocity.y = enemy.body.velocity.y; 626 | explosion.alpha = 0.7; 627 | explosion.play('explosion', 30, false, true); 628 | if (enemy.finishOff && enemy.health < 5) { 629 | enemy.finishOff(); 630 | } else { 631 | enemy.damage(enemy.damageAmount); 632 | } 633 | bullet.kill(); 634 | 635 | // Increase score 636 | score += enemy.damageAmount * 10; 637 | scoreText.render(); 638 | 639 | // Pacing 640 | 641 | // Enemies come quicker as score increases 642 | greenEnemySpacing *= 0.9; 643 | 644 | // Blue enemies come in after a score of 1000 645 | if (!blueEnemyLaunched && score > 1000) { 646 | blueEnemyLaunched = true; 647 | launchBlueEnemy(); 648 | // Slow green enemies down now that there are other enemies 649 | greenEnemySpacing *= 2; 650 | } 651 | 652 | // Launch boss 653 | if (!bossLaunched && score > 15000) { 654 | greenEnemySpacing = 5000; 655 | blueEnemySpacing = 12000; 656 | // dramatic pause before boss 657 | game.time.events.add(2000, function(){ 658 | bossLaunched = true; 659 | launchBoss(); 660 | }); 661 | } 662 | 663 | // Weapon upgrade 664 | if (score > 5000 && player.weaponLevel < 2) { 665 | player.weaponLevel = 2; 666 | } 667 | } 668 | 669 | // Don't count a hit in the lower right and left quarants to aproximate better collisions 670 | function bossHitTest(boss, bullet) { 671 | if ((bullet.x > boss.x + boss.width / 5 && 672 | bullet.y > boss.y) || 673 | (bullet.x < boss.x - boss.width / 5 && 674 | bullet.y > boss.y)) { 675 | return false; 676 | } else { 677 | return true; 678 | } 679 | } 680 | 681 | function enemyHitsPlayer (player, bullet) { 682 | bullet.kill(); 683 | 684 | player.damage(bullet.damageAmount); 685 | shields.render() 686 | 687 | if (player.alive) { 688 | var explosion = explosions.getFirstExists(false); 689 | explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight); 690 | explosion.alpha = 0.7; 691 | explosion.play('explosion', 30, false, true); 692 | } else { 693 | playerDeath.x = player.x; 694 | playerDeath.y = player.y; 695 | playerDeath.start(false, 1000, 10, 10); 696 | } 697 | } 698 | 699 | 700 | function restart () { 701 | // Reset the enemies 702 | greenEnemies.callAll('kill'); 703 | game.time.events.remove(greenEnemyLaunchTimer); 704 | game.time.events.add(1000, launchGreenEnemy); 705 | blueEnemies.callAll('kill'); 706 | blueEnemyBullets.callAll('kill'); 707 | game.time.events.remove(blueEnemyLaunchTimer); 708 | boss.kill(); 709 | booster.kill(); 710 | game.time.events.remove(bossLaunchTimer); 711 | 712 | blueEnemies.callAll('kill'); 713 | game.time.events.remove(blueEnemyLaunchTimer); 714 | // Revive the player 715 | player.weaponLevel = 1; 716 | player.revive(); 717 | player.health = 100; 718 | shields.render(); 719 | score = 0; 720 | scoreText.render(); 721 | 722 | // Hide the text 723 | gameOver.visible = false; 724 | 725 | // Reset pacing 726 | greenEnemySpacing = 1000; 727 | blueEnemyLaunched = false; 728 | bossLaunched = false; 729 | } 730 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Phaser.js game demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------