// ------------------------------------------------------------------------ // Bubble Shooter Game Tutorial With HTML5 And JavaScript // Copyright (c) 2015 Rembound.com // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/. // // http://rembound.com/articles/bubble-shooter-game-tutorial-with-html5-and-javascript // ------------------------------------------------------------------------ // The function gets called when the window is fully loaded window.onload = function() { // Get the canvas and context var canvas = document.getElementById("viewport"); var context = canvas.getContext("2d"); // Timing and frames per second var lastframe = 0; var fpstime = 0; var framecount = 0; var fps = 0; var initialized = false; // Level var level = { x: 4, // X position y: 83, // Y position width: 0, // Width, gets calculated height: 0, // Height, gets calculated columns: 15, // Number of tile columns rows: 14, // Number of tile rows tilewidth: 40, // Visual width of a tile tileheight: 40, // Visual height of a tile rowheight: 34, // Height of a row radius: 20, // Bubble collision radius tiles: [] // The two-dimensional tile array }; // Define a tile class var Tile = function(x, y, type, shift) { this.x = x; this.y = y; this.type = type; this.removed = false; this.shift = shift; this.velocity = 0; this.alpha = 1; this.processed = false; }; // Player var player = { x: 0, y: 0, angle: 0, tiletype: 0, bubble: { x: 0, y: 0, angle: 0, speed: 1000, dropspeed: 900, tiletype: 0, visible: false }, nextbubble: { x: 0, y: 0, tiletype: 0 } }; // Neighbor offset table var neighborsoffsets = [[[1, 0], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1]], // Even row tiles [[1, 0], [1, 1], [0, 1], [-1, 0], [0, -1], [1, -1]]]; // Odd row tiles // Number of different colors var bubblecolors = 7; // Game states var gamestates = { init: 0, ready: 1, shootbubble: 2, removecluster: 3, gameover: 4 }; var gamestate = gamestates.init; // Score var score = 0; var turncounter = 0; var rowoffset = 0; // Animation variables var animationstate = 0; var animationtime = 0; // Clusters var showcluster = false; var cluster = []; var floatingclusters = []; // Images var images = []; var bubbleimage; // Image loading global variables var loadcount = 0; var loadtotal = 0; var preloaded = false; // Load images function loadImages(imagefiles) { // Initialize variables loadcount = 0; loadtotal = imagefiles.length; preloaded = false; // Load the images var loadedimages = []; for (var i=0; i= level.x + level.width) { // Right edge player.bubble.angle = 180 - player.bubble.angle; player.bubble.x = level.x + level.width - level.tilewidth; } // Collisions with the top of the level if (player.bubble.y <= level.y) { // Top collision player.bubble.y = level.y; snapBubble(); return; } // Collisions with other tiles for (var i=0; i 0) { // Setup drop animation for (var i=0; i= 0) { tilesleft = true; // Alpha animation tile.alpha -= dt * 15; if (tile.alpha < 0) { tile.alpha = 0; } if (tile.alpha == 0) { tile.type = -1; tile.alpha = 1; } } } // Drop bubbles for (var i=0; i= 0) { tilesleft = true; // Accelerate dropped tiles tile.velocity += dt * 700; tile.shift += dt * tile.velocity; // Alpha animation tile.alpha -= dt * 8; if (tile.alpha < 0) { tile.alpha = 0; } // Check if the bubbles are past the bottom of the level if (tile.alpha == 0 || (tile.y * level.rowheight + tile.shift > (level.rows - 1) * level.rowheight + level.tileheight)) { tile.type = -1; tile.shift = 0; tile.alpha = 1; } } } } if (!tilesleft) { // Next bubble nextBubble(); // Check for game over var tilefound = false for (var i=0; i= level.columns) { gridpos.x = level.columns - 1; } if (gridpos.y < 0) { gridpos.y = 0; } if (gridpos.y >= level.rows) { gridpos.y = level.rows - 1; } // Check if the tile is empty var addtile = false; if (level.tiles[gridpos.x][gridpos.y].type != -1) { // Tile is not empty, shift the new tile downwards for (var newrow=gridpos.y+1; newrow= 3) { // Remove the cluster setGameState(gamestates.removecluster); return; } } // No clusters found turncounter++; if (turncounter >= 5) { // Add a row of bubbles addBubbles(); turncounter = 0; rowoffset = (rowoffset + 1) % 2; if (checkGameOver()) { return; } } // Next bubble nextBubble(); setGameState(gamestates.ready); } function checkGameOver() { // Check for game over for (var i=0; i= 0) { if (!colortable[tile.type]) { colortable[tile.type] = true; foundcolors.push(tile.type); } } } } return foundcolors; } // Find cluster at the specified tile location function findCluster(tx, ty, matchtype, reset, skipremoved) { // Reset the processed flags if (reset) { resetProcessed(); } // Get the target tile. Tile coord must be valid. var targettile = level.tiles[tx][ty]; // Initialize the toprocess array with the specified tile var toprocess = [targettile]; targettile.processed = true; var foundcluster = []; while (toprocess.length > 0) { // Pop the last element from the array var currenttile = toprocess.pop(); // Skip processed and empty tiles if (currenttile.type == -1) { continue; } // Skip tiles with the removed flag if (skipremoved && currenttile.removed) { continue; } // Check if current tile has the right type, if matchtype is true if (!matchtype || (currenttile.type == targettile.type)) { // Add current tile to the cluster foundcluster.push(currenttile); // Get the neighbors of the current tile var neighbors = getNeighbors(currenttile); // Check the type of each neighbor for (var i=0; i= 0 && nx < level.columns && ny >= 0 && ny < level.rows) { neighbors.push(level.tiles[nx][ny]); } } return neighbors; } function updateFps(dt) { if (fpstime > 0.25) { // Calculate fps fps = Math.round(framecount / fpstime); // Reset time and framecount fpstime = 0; framecount = 0; } // Increase time and framecount fpstime += dt; framecount++; } // Draw text that is centered function drawCenterText(text, x, y, width) { var textdim = context.measureText(text); context.fillText(text, x + (width-textdim.width)/2, y); } // Render the game function render() { // Draw the frame around the game drawFrame(); var yoffset = level.tileheight/2; // Draw level background context.fillStyle = "#8c8c8c"; context.fillRect(level.x - 4, level.y - 4, level.width + 8, level.height + 4 - yoffset); // Render tiles renderTiles(); // Draw level bottom context.fillStyle = "#656565"; context.fillRect(level.x - 4, level.y - 4 + level.height + 4 - yoffset, level.width + 8, 2*level.tileheight + 3); // Draw score context.fillStyle = "#ffffff"; context.font = "18px Verdana"; var scorex = level.x + level.width - 150; var scorey = level.y+level.height + level.tileheight - yoffset - 8; drawCenterText("Score:", scorex, scorey, 150); context.font = "24px Verdana"; drawCenterText(score, scorex, scorey+30, 150); // Render cluster if (showcluster) { renderCluster(cluster, 255, 128, 128); for (var i=0; i= 0) { // Support transparency context.save(); context.globalAlpha = tile.alpha; // Draw the tile using the color drawBubble(coord.tilex, coord.tiley + shift, tile.type); context.restore(); } } } } // Render cluster function renderCluster(cluster, r, g, b) { for (var i=0; i= bubblecolors) return; // Draw the bubble sprite context.drawImage(bubbleimage, index * 40, 0, 40, 40, x, y, level.tilewidth, level.tileheight); } // Start a new game function newGame() { // Reset score score = 0; turncounter = 0; rowoffset = 0; // Set the gamestate to ready setGameState(gamestates.ready); // Create the level createLevel(); // Init the next bubble and set the current bubble nextBubble(); nextBubble(); } // Create a random level function createLevel() { // Create a level with random tiles for (var j=0; j= 2) { // Change the random tile var newtile = randRange(0, bubblecolors-1); // Make sure the new tile is different from the previous tile if (newtile == randomtile) { newtile = (newtile + 1) % bubblecolors; } randomtile = newtile; count = 0; } count++; if (j < level.rows/2) { level.tiles[i][j].type = randomtile; } else { level.tiles[i][j].type = -1; } } } } // Create a random bubble for the player function nextBubble() { // Set the current bubble player.tiletype = player.nextbubble.tiletype; player.bubble.tiletype = player.nextbubble.tiletype; player.bubble.x = player.x; player.bubble.y = player.y; player.bubble.visible = true; // Get a random type from the existing colors var nextcolor = getExistingColor(); // Set the next bubble player.nextbubble.tiletype = nextcolor; } // Get a random existing color function getExistingColor() { existingcolors = findColors(); var bubbletype = 0; if (existingcolors.length > 0) { bubbletype = existingcolors[randRange(0, existingcolors.length-1)]; } return bubbletype; } // Get a random int between low and high, inclusive function randRange(low, high) { return Math.floor(low + Math.random()*(high-low+1)); } // Shoot the bubble function shootBubble() { // Shoot the bubble in the direction of the mouse player.bubble.x = player.x; player.bubble.y = player.y; player.bubble.angle = player.angle; player.bubble.tiletype = player.tiletype; // Set the gamestate setGameState(gamestates.shootbubble); } // Check if two circles intersect function circleIntersection(x1, y1, r1, x2, y2, r2) { // Calculate the distance between the centers var dx = x1 - x2; var dy = y1 - y2; var len = Math.sqrt(dx * dx + dy * dy); if (len < r1 + r2) { // Circles intersect return true; } return false; } // Convert radians to degrees function radToDeg(angle) { return angle * (180 / Math.PI); } // Convert degrees to radians function degToRad(angle) { return angle * (Math.PI / 180); } // On mouse movement function onMouseMove(e) { // Get the mouse position var pos = getMousePos(canvas, e); // Get the mouse angle var mouseangle = radToDeg(Math.atan2((player.y+level.tileheight/2) - pos.y, pos.x - (player.x+level.tilewidth/2))); // Convert range to 0, 360 degrees if (mouseangle < 0) { mouseangle = 180 + (180 + mouseangle); } // Restrict angle to 8, 172 degrees var lbound = 8; var ubound = 172; if (mouseangle > 90 && mouseangle < 270) { // Left if (mouseangle > ubound) { mouseangle = ubound; } } else { // Right if (mouseangle < lbound || mouseangle >= 270) { mouseangle = lbound; } } // Set the player angle player.angle = mouseangle; } // On mouse button click function onMouseDown(e) { // Get the mouse position var pos = getMousePos(canvas, e); if (gamestate == gamestates.ready) { shootBubble(); } else if (gamestate == gamestates.gameover) { newGame(); } } // Get the mouse position function getMousePos(canvas, e) { var rect = canvas.getBoundingClientRect();file:///C:/Users/ASUS/Downloads/bubble/Bubble-Shooter-HTML5/bubble-shooter.html return { x: Math.round((e.clientX - rect.left)/(rect.right - rect.left)*canvas.width), y: Math.round((e.clientY - rect.top)/(rect.bottom - rect.top)*canvas.height) }; } // Call init to start the game init(); };