/** * Asteroids HTML5 Canvas Game * Scenes for CanvasMark Rendering Benchmark - March 2013 * * @email kevtoast at yahoo dot com * @twitter kevinroast * * (C) 2013 Kevin Roast * * Please see: license.txt * You are welcome to use this code, but I would appreciate an email or tweet * if you do anything interesting with it! */ // Globals var BITMAPS = true; var GLOWEFFECT = false; var g_asteroidImg1 = new Image(); var g_asteroidImg2 = new Image(); var g_asteroidImg3 = new Image(); var g_asteroidImg4 = new Image(); var g_shieldImg = new Image(); var g_backgroundImg = new Image(); var g_playerImg = new Image(); var g_enemyshipImg = new Image(); /** * Asteroids root namespace. * * @namespace Asteroids */ if (typeof Asteroids == "undefined" || !Asteroids) { var Asteroids = {}; } /** * Asteroids benchmark test class. * * @namespace Asteroids * @class Asteroids.Test */ (function() { Asteroids.Test = function(benchmark, loader) { // get the image graphics loading loader.addImage(g_backgroundImg, './images/bg3_1.jpg'); loader.addImage(g_playerImg, './images/player.png'); loader.addImage(g_asteroidImg1, './images/asteroid1.png'); loader.addImage(g_asteroidImg2, './images/asteroid2.png'); loader.addImage(g_asteroidImg3, './images/asteroid3.png'); loader.addImage(g_asteroidImg4, './images/asteroid4.png'); loader.addImage(g_enemyshipImg, './images/enemyship1.png'); // generate the single player actor - available across all scenes this.player = new Asteroids.Player(new Vector(GameHandler.width / 2, GameHandler.height / 2), new Vector(0.0, 0.0), 0.0); // add the Asteroid game benchmark scenes for (var level, i=0, t=benchmark.scenes.length; i<4; i++) { level = new Asteroids.BenchMarkScene(this, t+i, i+1);// NOTE: asteroids indexes feature from 1... benchmark.addBenchmarkScene(level); } }; Asteroids.Test.prototype = { /** * Reference to the single game player actor */ player: null, /** * Lives count (only used to render overlay graphics during benchmark mode) */ lives: 3 }; })(); /** * Asteroids Benchmark scene class. * * @namespace Asteroids * @class Asteroids.BenchMarkScene */ (function() { Asteroids.BenchMarkScene = function(game, test, feature) { this.game = game; this.test = test; this.feature = feature; this.player = game.player; var msg = "Test " + test + " - Asteroids - "; switch (feature) { case 1: msg += "Bitmaps"; break; case 2: msg += "Vectors"; break; case 3: msg += "Bitmaps, shapes, text"; break; case 4: msg += "Shapes, shadows, blending"; break; } var interval = new Game.Interval(msg, this.intervalRenderer); Asteroids.BenchMarkScene.superclass.constructor.call(this, true, interval); // generate background starfield for (var star, i=0; i GameHandler.height || s.prevy > GameHandler.width) { s.init(); } } }, /** * Scene init event handler */ onInitScene: function onInitScene() { // generate the actors and add the actor sub-lists to the main actor list this.actors = []; this.enemies = []; this.actors.push(this.enemies); this.actors.push(this.playerBullets = []); this.actors.push(this.enemyBullets = []); this.actors.push(this.effects = []); this.actors.push([this.player]); // reset the player position with (this.player) { position.x = GameHandler.width / 2; position.y = GameHandler.height / 2; vector.x = 0.0; vector.y = 0.0; heading = 0.0; } // tests 1-2 display asteroids in various modes switch (this.feature) { case 1: { // start with 10 asteroids - more will be added if framerate is acceptable for (var i=0; i<10; i++) { this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1)); } this.testScore = 10; break; } case 2: { // start with 10 asteroids - more will be added if framerate is acceptable for (var i=0; i<10; i++) { this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1)); } this.testScore = 20; break; } case 3: { // test 3 generates lots of enemy ships that fire for (var i=0; i<10; i++) { this.enemies.push(new Asteroids.EnemyShip(this, i%2)); } this.testScore = 10; break; } case 4: { this.testScore = 25; break; } } // tests 2 in wireframe, all others are bitmaps BITMAPS = !(this.feature === 2); // reset interval flag this.interval.reset(); }, /** * Scene before rendering event handler */ onBeforeRenderScene: function onBeforeRenderScene(benchmark) { // add items to the test if (benchmark) { switch (this.feature) { case 1: case 2: { var count; switch (this.feature) { case 1: count = 10; break; case 2: count = 5; break; } for (var i=0; i this.testState) { this.testState += 20; for (var i=0; i<2; i++) { this.enemies.push(new Asteroids.EnemyShip(this, i%2)); } this.enemies[0].hit(); this.destroyEnemy(this.enemies[0], new Vector(0, 1)); } break; } case 4: { if (Date.now() - this.sceneStartTime > this.testState) { this.testState += 25; // spray forward guns for (var i=0; i<=~~(this.testState/500); i++) { h = this.player.heading - 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading + 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); } // side firing guns also h = this.player.heading - 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); h = this.player.heading + 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); // update player heading to rotate this.player.heading += 8; } break; } } } // update all actors using their current vector this.updateActors(); }, /** * Scene rendering event handler */ onRenderScene: function onRenderScene(ctx) { // setup canvas for a render pass and apply background if (BITMAPS) { // draw a scrolling background image ctx.drawImage(g_backgroundImg, this.backgroundX++, 0, GameHandler.width, GameHandler.height, 0, 0, GameHandler.width, GameHandler.height); if (this.backgroundX == (g_backgroundImg.width / 2)) { this.backgroundX = 0; } ctx.shadowBlur = 0; } else { // clear the background to black ctx.fillStyle = "black"; ctx.fillRect(0, 0, GameHandler.width, GameHandler.height); // glowing vector effect shadow ctx.shadowBlur = GLOWEFFECT ? 8 : 0; // update and render background starfield effect this.updateStarfield(ctx); } // render the game actors this.renderActors(ctx); // render info overlay graphics this.renderOverlay(ctx); }, /** * Randomly generate a new large asteroid. Ensures the asteroid is not generated * too close to the player position! * * @param speedFactor {number} Speed multiplier factor to apply to asteroid vector */ generateAsteroid: function generateAsteroid(speedFactor, size) { while (true) { // perform a test to check it is not too close to the player var apos = new Vector(Math.random()*GameHandler.width, Math.random()*GameHandler.height); if (this.player.position.distance(apos) > 125) { var vec = new Vector( ((Math.random()*2)-1)*speedFactor, ((Math.random()*2)-1)*speedFactor ); var asteroid = new Asteroids.Asteroid( apos, vec, size ? size : 4); return asteroid; } } }, /** * Update the actors position based on current vectors and expiration. */ updateActors: function updateActors() { for (var i = 0, j = this.actors.length; i < j; i++) { var actorList = this.actors[i]; for (var n = 0; n < actorList.length; n++) { var actor = actorList[n]; // call onUpdate() event for each actor actor.onUpdate(this); // expiration test first if (actor.expired()) { actorList.splice(n, 1); } else { // update actor using its current vector actor.position.add(actor.vector); // handle traversing out of the coordinate space and back again if (actor.position.x >= GameHandler.width) { actor.position.x = 0; } else if (actor.position.x < 0) { actor.position.x = GameHandler.width - 1; } if (actor.position.y >= GameHandler.height) { actor.position.y = 0; } else if (actor.position.y < 0) { actor.position.y = GameHandler.height - 1; } } } } }, /** * Blow up an enemy. * * An asteroid may generate new baby asteroids and leave an explosion * in the wake. * * Also applies the score for the destroyed item. * * @param enemy {Game.Actor} The enemy to destory and add score for * @param parentVector {Vector} The vector of the item that hit the enemy * @param player {boolean} If true, the player was the destroyed */ destroyEnemy: function destroyEnemy(enemy, parentVector) { if (enemy instanceof Asteroids.Asteroid) { // generate baby asteroids this.generateBabyAsteroids(enemy, parentVector); // add an explosion actor at the asteriod position and vector var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), enemy.size); this.effects.push(boom); // generate a score effect indicator at the destroyed enemy position var vec = new Vector(0, -(Math.random()*2 + 0.5)); var effect = new Asteroids.ScoreIndicator( new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100))); this.effects.push(effect); } else if (enemy instanceof Asteroids.EnemyShip) { // add an explosion actor at the asteriod position and vector var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), 4); this.effects.push(boom); // generate a score text effect indicator at the destroyed enemy position var vec = new Vector(0, -(Math.random()*2 + 0.5)); var effect = new Asteroids.ScoreIndicator( new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100))); this.effects.push(effect); } }, /** * Generate a number of baby asteroids from a detonated parent asteroid. The number * and size of the generated asteroids are based on the parent size. Some of the * momentum of the parent vector (e.g. impacting bullet) is applied to the new asteroids. * * @param asteroid {Asteroids.Asteroid} The parent asteroid that has been destroyed * @param parentVector {Vector} Vector of the impacting object e.g. a bullet */ generateBabyAsteroids: function generateBabyAsteroids(asteroid, parentVector) { // generate some baby asteroid(s) if bigger than the minimum size if (asteroid.size > 1) { for (var x=0, xc=Math.floor(asteroid.size/2); x= 0; n--) { actorList[n].onRender(ctx); } } }, /** * Render player information HUD overlay graphics. * * @param ctx {object} Canvas rendering context */ renderOverlay: function renderOverlay(ctx) { ctx.save(); // energy bar (100 pixels across, scaled down from player energy max) ctx.strokeStyle = "rgb(50,50,255)"; ctx.strokeRect(4, 4, 101, 6); ctx.fillStyle = "rgb(100,100,255)"; var energy = this.player.energy; if (energy > this.player.ENERGY_INIT) { // the shield is on for "free" briefly when he player respawns energy = this.player.ENERGY_INIT; } ctx.fillRect(5, 5, (energy / (this.player.ENERGY_INIT / 100)), 5); // lives indicator graphics for (var i=0; i