├── screenshot.png
├── bubble-sprites.png
├── bubble-shooter.html
├── README.md
├── LICENSE
└── bubble-shooter-example.js
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rembound/Bubble-Shooter-HTML5/HEAD/screenshot.png
--------------------------------------------------------------------------------
/bubble-sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rembound/Bubble-Shooter-HTML5/HEAD/bubble-sprites.png
--------------------------------------------------------------------------------
/bubble-shooter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rembound.com Example
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bubble Shooter HTML5
2 | A Bubble Shooter game like Bust-A-Move or Puzzle Bobble with HTML5 Canvas and JavaScript.
3 |
4 | This is a code example that belongs to the article: [Bubble Shooter Game Tutorial With HTML5 And JavaScript](http://rembound.com/articles/bubble-shooter-game-tutorial-with-html5-and-javascript)
5 |
6 | [](http://rembound.com/articles/bubble-shooter-game-tutorial-with-html5-and-javascript)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Rembound.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/bubble-shooter-example.js:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------
2 | // Bubble Shooter Game Tutorial With HTML5 And JavaScript
3 | // Copyright (c) 2015 Rembound.com
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see http://www.gnu.org/licenses/.
17 | //
18 | // http://rembound.com/articles/bubble-shooter-game-tutorial-with-html5-and-javascript
19 | // ------------------------------------------------------------------------
20 |
21 | // The function gets called when the window is fully loaded
22 | window.onload = function() {
23 | // Get the canvas and context
24 | var canvas = document.getElementById("viewport");
25 | var context = canvas.getContext("2d");
26 |
27 | // Timing and frames per second
28 | var lastframe = 0;
29 | var fpstime = 0;
30 | var framecount = 0;
31 | var fps = 0;
32 |
33 | var initialized = false;
34 |
35 | // Level
36 | var level = {
37 | x: 4, // X position
38 | y: 83, // Y position
39 | width: 0, // Width, gets calculated
40 | height: 0, // Height, gets calculated
41 | columns: 15, // Number of tile columns
42 | rows: 14, // Number of tile rows
43 | tilewidth: 40, // Visual width of a tile
44 | tileheight: 40, // Visual height of a tile
45 | rowheight: 34, // Height of a row
46 | radius: 20, // Bubble collision radius
47 | tiles: [] // The two-dimensional tile array
48 | };
49 |
50 | // Define a tile class
51 | var Tile = function(x, y, type, shift) {
52 | this.x = x;
53 | this.y = y;
54 | this.type = type;
55 | this.removed = false;
56 | this.shift = shift;
57 | this.velocity = 0;
58 | this.alpha = 1;
59 | this.processed = false;
60 | };
61 |
62 | // Player
63 | var player = {
64 | x: 0,
65 | y: 0,
66 | angle: 0,
67 | tiletype: 0,
68 | bubble: {
69 | x: 0,
70 | y: 0,
71 | angle: 0,
72 | speed: 1000,
73 | dropspeed: 900,
74 | tiletype: 0,
75 | visible: false
76 | },
77 | nextbubble: {
78 | x: 0,
79 | y: 0,
80 | tiletype: 0
81 | }
82 | };
83 |
84 | // Neighbor offset table
85 | var neighborsoffsets = [[[1, 0], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1]], // Even row tiles
86 | [[1, 0], [1, 1], [0, 1], [-1, 0], [0, -1], [1, -1]]]; // Odd row tiles
87 |
88 | // Number of different colors
89 | var bubblecolors = 7;
90 |
91 | // Game states
92 | var gamestates = { init: 0, ready: 1, shootbubble: 2, removecluster: 3, gameover: 4 };
93 | var gamestate = gamestates.init;
94 |
95 | // Score
96 | var score = 0;
97 |
98 | var turncounter = 0;
99 | var rowoffset = 0;
100 |
101 | // Animation variables
102 | var animationstate = 0;
103 | var animationtime = 0;
104 |
105 | // Clusters
106 | var showcluster = false;
107 | var cluster = [];
108 | var floatingclusters = [];
109 |
110 | // Images
111 | var images = [];
112 | var bubbleimage;
113 |
114 | // Image loading global variables
115 | var loadcount = 0;
116 | var loadtotal = 0;
117 | var preloaded = false;
118 |
119 | // Load images
120 | function loadImages(imagefiles) {
121 | // Initialize variables
122 | loadcount = 0;
123 | loadtotal = imagefiles.length;
124 | preloaded = false;
125 |
126 | // Load the images
127 | var loadedimages = [];
128 | for (var i=0; i= level.x + level.width) {
268 | // Right edge
269 | player.bubble.angle = 180 - player.bubble.angle;
270 | player.bubble.x = level.x + level.width - level.tilewidth;
271 | }
272 |
273 | // Collisions with the top of the level
274 | if (player.bubble.y <= level.y) {
275 | // Top collision
276 | player.bubble.y = level.y;
277 | snapBubble();
278 | return;
279 | }
280 |
281 | // Collisions with other tiles
282 | for (var i=0; i 0) {
325 | // Setup drop animation
326 | for (var i=0; i= 0) {
348 | tilesleft = true;
349 |
350 | // Alpha animation
351 | tile.alpha -= dt * 15;
352 | if (tile.alpha < 0) {
353 | tile.alpha = 0;
354 | }
355 |
356 | if (tile.alpha == 0) {
357 | tile.type = -1;
358 | tile.alpha = 1;
359 | }
360 | }
361 | }
362 |
363 | // Drop bubbles
364 | for (var i=0; i= 0) {
369 | tilesleft = true;
370 |
371 | // Accelerate dropped tiles
372 | tile.velocity += dt * 700;
373 | tile.shift += dt * tile.velocity;
374 |
375 | // Alpha animation
376 | tile.alpha -= dt * 8;
377 | if (tile.alpha < 0) {
378 | tile.alpha = 0;
379 | }
380 |
381 | // Check if the bubbles are past the bottom of the level
382 | if (tile.alpha == 0 || (tile.y * level.rowheight + tile.shift > (level.rows - 1) * level.rowheight + level.tileheight)) {
383 | tile.type = -1;
384 | tile.shift = 0;
385 | tile.alpha = 1;
386 | }
387 | }
388 |
389 | }
390 | }
391 |
392 | if (!tilesleft) {
393 | // Next bubble
394 | nextBubble();
395 |
396 | // Check for game over
397 | var tilefound = false
398 | for (var i=0; i= level.columns) {
430 | gridpos.x = level.columns - 1;
431 | }
432 |
433 | if (gridpos.y < 0) {
434 | gridpos.y = 0;
435 | }
436 |
437 | if (gridpos.y >= level.rows) {
438 | gridpos.y = level.rows - 1;
439 | }
440 |
441 | // Check if the tile is empty
442 | var addtile = false;
443 | if (level.tiles[gridpos.x][gridpos.y].type != -1) {
444 | // Tile is not empty, shift the new tile downwards
445 | for (var newrow=gridpos.y+1; newrow= 3) {
473 | // Remove the cluster
474 | setGameState(gamestates.removecluster);
475 | return;
476 | }
477 | }
478 |
479 | // No clusters found
480 | turncounter++;
481 | if (turncounter >= 5) {
482 | // Add a row of bubbles
483 | addBubbles();
484 | turncounter = 0;
485 | rowoffset = (rowoffset + 1) % 2;
486 |
487 | if (checkGameOver()) {
488 | return;
489 | }
490 | }
491 |
492 | // Next bubble
493 | nextBubble();
494 | setGameState(gamestates.ready);
495 | }
496 |
497 | function checkGameOver() {
498 | // Check for game over
499 | for (var i=0; i= 0) {
540 | if (!colortable[tile.type]) {
541 | colortable[tile.type] = true;
542 | foundcolors.push(tile.type);
543 | }
544 | }
545 | }
546 | }
547 |
548 | return foundcolors;
549 | }
550 |
551 | // Find cluster at the specified tile location
552 | function findCluster(tx, ty, matchtype, reset, skipremoved) {
553 | // Reset the processed flags
554 | if (reset) {
555 | resetProcessed();
556 | }
557 |
558 | // Get the target tile. Tile coord must be valid.
559 | var targettile = level.tiles[tx][ty];
560 |
561 | // Initialize the toprocess array with the specified tile
562 | var toprocess = [targettile];
563 | targettile.processed = true;
564 | var foundcluster = [];
565 |
566 | while (toprocess.length > 0) {
567 | // Pop the last element from the array
568 | var currenttile = toprocess.pop();
569 |
570 | // Skip processed and empty tiles
571 | if (currenttile.type == -1) {
572 | continue;
573 | }
574 |
575 | // Skip tiles with the removed flag
576 | if (skipremoved && currenttile.removed) {
577 | continue;
578 | }
579 |
580 | // Check if current tile has the right type, if matchtype is true
581 | if (!matchtype || (currenttile.type == targettile.type)) {
582 | // Add current tile to the cluster
583 | foundcluster.push(currenttile);
584 |
585 | // Get the neighbors of the current tile
586 | var neighbors = getNeighbors(currenttile);
587 |
588 | // Check the type of each neighbor
589 | for (var i=0; i= 0 && nx < level.columns && ny >= 0 && ny < level.rows) {
678 | neighbors.push(level.tiles[nx][ny]);
679 | }
680 | }
681 |
682 | return neighbors;
683 | }
684 |
685 | function updateFps(dt) {
686 | if (fpstime > 0.25) {
687 | // Calculate fps
688 | fps = Math.round(framecount / fpstime);
689 |
690 | // Reset time and framecount
691 | fpstime = 0;
692 | framecount = 0;
693 | }
694 |
695 | // Increase time and framecount
696 | fpstime += dt;
697 | framecount++;
698 | }
699 |
700 | // Draw text that is centered
701 | function drawCenterText(text, x, y, width) {
702 | var textdim = context.measureText(text);
703 | context.fillText(text, x + (width-textdim.width)/2, y);
704 | }
705 |
706 | // Render the game
707 | function render() {
708 | // Draw the frame around the game
709 | drawFrame();
710 |
711 | var yoffset = level.tileheight/2;
712 |
713 | // Draw level background
714 | context.fillStyle = "#8c8c8c";
715 | context.fillRect(level.x - 4, level.y - 4, level.width + 8, level.height + 4 - yoffset);
716 |
717 | // Render tiles
718 | renderTiles();
719 |
720 | // Draw level bottom
721 | context.fillStyle = "#656565";
722 | context.fillRect(level.x - 4, level.y - 4 + level.height + 4 - yoffset, level.width + 8, 2*level.tileheight + 3);
723 |
724 | // Draw score
725 | context.fillStyle = "#ffffff";
726 | context.font = "18px Verdana";
727 | var scorex = level.x + level.width - 150;
728 | var scorey = level.y+level.height + level.tileheight - yoffset - 8;
729 | drawCenterText("Score:", scorex, scorey, 150);
730 | context.font = "24px Verdana";
731 | drawCenterText(score, scorex, scorey+30, 150);
732 |
733 | // Render cluster
734 | if (showcluster) {
735 | renderCluster(cluster, 255, 128, 128);
736 |
737 | for (var i=0; i= 0) {
796 | // Support transparency
797 | context.save();
798 | context.globalAlpha = tile.alpha;
799 |
800 | // Draw the tile using the color
801 | drawBubble(coord.tilex, coord.tiley + shift, tile.type);
802 |
803 | context.restore();
804 | }
805 | }
806 | }
807 | }
808 |
809 | // Render cluster
810 | function renderCluster(cluster, r, g, b) {
811 | for (var i=0; i= bubblecolors)
884 | return;
885 |
886 | // Draw the bubble sprite
887 | context.drawImage(bubbleimage, index * 40, 0, 40, 40, x, y, level.tilewidth, level.tileheight);
888 | }
889 |
890 | // Start a new game
891 | function newGame() {
892 | // Reset score
893 | score = 0;
894 |
895 | turncounter = 0;
896 | rowoffset = 0;
897 |
898 | // Set the gamestate to ready
899 | setGameState(gamestates.ready);
900 |
901 | // Create the level
902 | createLevel();
903 |
904 | // Init the next bubble and set the current bubble
905 | nextBubble();
906 | nextBubble();
907 | }
908 |
909 | // Create a random level
910 | function createLevel() {
911 | // Create a level with random tiles
912 | for (var j=0; j= 2) {
917 | // Change the random tile
918 | var newtile = randRange(0, bubblecolors-1);
919 |
920 | // Make sure the new tile is different from the previous tile
921 | if (newtile == randomtile) {
922 | newtile = (newtile + 1) % bubblecolors;
923 | }
924 | randomtile = newtile;
925 | count = 0;
926 | }
927 | count++;
928 |
929 | if (j < level.rows/2) {
930 | level.tiles[i][j].type = randomtile;
931 | } else {
932 | level.tiles[i][j].type = -1;
933 | }
934 | }
935 | }
936 | }
937 |
938 | // Create a random bubble for the player
939 | function nextBubble() {
940 | // Set the current bubble
941 | player.tiletype = player.nextbubble.tiletype;
942 | player.bubble.tiletype = player.nextbubble.tiletype;
943 | player.bubble.x = player.x;
944 | player.bubble.y = player.y;
945 | player.bubble.visible = true;
946 |
947 | // Get a random type from the existing colors
948 | var nextcolor = getExistingColor();
949 |
950 | // Set the next bubble
951 | player.nextbubble.tiletype = nextcolor;
952 | }
953 |
954 | // Get a random existing color
955 | function getExistingColor() {
956 | existingcolors = findColors();
957 |
958 | var bubbletype = 0;
959 | if (existingcolors.length > 0) {
960 | bubbletype = existingcolors[randRange(0, existingcolors.length-1)];
961 | }
962 |
963 | return bubbletype;
964 | }
965 |
966 | // Get a random int between low and high, inclusive
967 | function randRange(low, high) {
968 | return Math.floor(low + Math.random()*(high-low+1));
969 | }
970 |
971 | // Shoot the bubble
972 | function shootBubble() {
973 | // Shoot the bubble in the direction of the mouse
974 | player.bubble.x = player.x;
975 | player.bubble.y = player.y;
976 | player.bubble.angle = player.angle;
977 | player.bubble.tiletype = player.tiletype;
978 |
979 | // Set the gamestate
980 | setGameState(gamestates.shootbubble);
981 | }
982 |
983 | // Check if two circles intersect
984 | function circleIntersection(x1, y1, r1, x2, y2, r2) {
985 | // Calculate the distance between the centers
986 | var dx = x1 - x2;
987 | var dy = y1 - y2;
988 | var len = Math.sqrt(dx * dx + dy * dy);
989 |
990 | if (len < r1 + r2) {
991 | // Circles intersect
992 | return true;
993 | }
994 |
995 | return false;
996 | }
997 |
998 | // Convert radians to degrees
999 | function radToDeg(angle) {
1000 | return angle * (180 / Math.PI);
1001 | }
1002 |
1003 | // Convert degrees to radians
1004 | function degToRad(angle) {
1005 | return angle * (Math.PI / 180);
1006 | }
1007 |
1008 | // On mouse movement
1009 | function onMouseMove(e) {
1010 | // Get the mouse position
1011 | var pos = getMousePos(canvas, e);
1012 |
1013 | // Get the mouse angle
1014 | var mouseangle = radToDeg(Math.atan2((player.y+level.tileheight/2) - pos.y, pos.x - (player.x+level.tilewidth/2)));
1015 |
1016 | // Convert range to 0, 360 degrees
1017 | if (mouseangle < 0) {
1018 | mouseangle = 180 + (180 + mouseangle);
1019 | }
1020 |
1021 | // Restrict angle to 8, 172 degrees
1022 | var lbound = 8;
1023 | var ubound = 172;
1024 | if (mouseangle > 90 && mouseangle < 270) {
1025 | // Left
1026 | if (mouseangle > ubound) {
1027 | mouseangle = ubound;
1028 | }
1029 | } else {
1030 | // Right
1031 | if (mouseangle < lbound || mouseangle >= 270) {
1032 | mouseangle = lbound;
1033 | }
1034 | }
1035 |
1036 | // Set the player angle
1037 | player.angle = mouseangle;
1038 | }
1039 |
1040 | // On mouse button click
1041 | function onMouseDown(e) {
1042 | // Get the mouse position
1043 | var pos = getMousePos(canvas, e);
1044 |
1045 | if (gamestate == gamestates.ready) {
1046 | shootBubble();
1047 | } else if (gamestate == gamestates.gameover) {
1048 | newGame();
1049 | }
1050 | }
1051 |
1052 | // Get the mouse position
1053 | function getMousePos(canvas, e) {
1054 | var rect = canvas.getBoundingClientRect();
1055 | return {
1056 | x: Math.round((e.clientX - rect.left)/(rect.right - rect.left)*canvas.width),
1057 | y: Math.round((e.clientY - rect.top)/(rect.bottom - rect.top)*canvas.height)
1058 | };
1059 | }
1060 |
1061 | // Call init to start the game
1062 | init();
1063 | };
--------------------------------------------------------------------------------