I have only been exposed to canvas for more than a month. It is the first time to completely implement a game process, and the harvest is quite big.
Shooting game screenshots
Go to the demo first: https://littleyljy.github.io/demo/shootgame/
game rulesThe player is required to control the aircraft to fire bullets and destroy the moving monsters. If all the monsters are destroyed, the game is successful. If the monsters move to the bottom, the game fails.
The game is divided into several scenes:
To achieve scene switching, you actually need to first display: none for all scenes, and then control the data-status through js to start, playing, failed, success, all-success, and stop to implement the corresponding scene display: block.
The HTML and CSS are as follows:
<div id=game data-status=start> <div class=game-panel> <section class=game-intro game-ui> <h1 class=section-title>Shooting game</h1> <p class=game- desc>This is an addictive shooting game. Use ← and → to operate your plane, use space to shoot, and use enter to pause the game. Let’s destroy the space monsters together! </p> <p class=game-level>Current Level: 1</p> <button class=js-play button>Start game</button> </section> <section class=game-failed game-ui> <h1 class=section-title>Game over</h1> <p class=game-info-text>Final score: <span class=score></span></p> <button class=js-replay button> Start again</button> </section> <section class=game-success game-ui> <h1 class=section-title>Game success</h1> <p class=game-next-level game-info-text></p> <button class=js-next button>Continue game</button> </section> <section class=game-all-success game-ui> <h1 class=section-title>Passed successfully</h1> <p class=game- next-level game-info-text>You have successfully defended against all monster attacks. </p> <button class=js-replay button>Play again</button> </section> <section class=game-stop game-ui> <h1 class=section-title>Game pause</h1> < button class=js-stop button>Game continues</button> </section> </div> <div class=game-info game-ui> <span class=title>Score:</span> <span class=score ></span> </div> <canvas id=canvas width=700 height=600> <!-- Animation drawing board --> </canvas> </div>
#game{ width: 700px; height: 600px; position: relative; left: 50%; top: 40px; margin: 0 0 0 -350px; background: linear-gradient(-180deg, #040024 0%, #07165C 97% );}.game-ui{ display: none; padding: 55px; box-sizing: border-box; height: 100%;}[data-status=start] .game-intro { display: block; padding-top: 180px; background: url(./img/bg.png) no-repeat 430px 180px; background-size: 200px;}[data-status=playing] .game-info { display: block; position: absolute; top:0; left:0; padding:20px;}[data-status=failed] .game-failed,[data-status=success] .game-success,[data-status=all-success] .game-all-success,[ data-status=stop] .game-stop{ display: block; padding-top: 180px; background: url(./img/bg-end.png) no-repeat 380px 190px; background-size: 250px;}object-oriented
The entire game can treat monsters (Enemy), planes (Plane), and bullets (Bullets) as objects, as well as configuration objects (CONFIG) and game objects (GAME) that control game logic.
Game related configuration/** * Game related configuration* @type {Object} */var CONFIG = { status: 'start', // The game starts by default as starting level: 1, // The game default level totalLevel: 6, // A total of 6 Off numPerLine: 7, // The game's default number of monsters per line canvasPadding: 30, // The default canvas interval bulletSize: 10, // The default bullet length bulletSpeed: 10, // Default bullet movement speed enemySpeed: 2, // Default enemy movement distance enemySize: 50, // Default enemy size enemyGap: 10, // Default distance between enemies enemyIcon: './img/enemy.png', / / The image of the monster enemyBoomIcon: './img/boom.png', // The image of the monster's death enemyDirection: 'right', // The default enemy moves to the right at the beginning planeSpeed: 5, // The default distance the plane moves at each step planeSize: { width: 60, height: 100 }, // The default size of the plane, planeIcon: './img/plane.png' };Define parent class
Because monsters (Enemy), planes (Plane), and bullets (Bullet) all have the same x, y, size, speed attributes and move() method, you can define a parent class Element and implement it by inheriting the parent class from the subclass .
/*Parent class: Contains xy speed move() draw()*/var Element = function (opts) { this.opts = opts || {}; //Set coordinates, size, speed this.x = opts.x; this.y = opts.y; this.size = opts.size; this.speed = opts.speed;};Element.prototype.move = function (x, y) { var addX = x || 0; var addY = y || 0; this.x += addX; this.y += addY;};//Function that inherits the prototype function inheritPrototype(subType, superType) { var proto = Object.create(superType.prototype); proto .constructor = subType; subType.prototype = proto;}
The move(x, y) method stacks itself based on the (x, y) value passed in.
Define monsterMonsters include unique attributes: monster status, image, boomCount that controls the duration of the explosion state, and draw(), down(), direction(), and booming() methods.
/*Enemy*/var Enemy = function (opts) { this.opts = opts || {}; //Call the parent class attribute Element.call(this, opts); //Special attribute status and image this.status = ' normal';//normal, booming, noomed this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon; this.boomCount = 0;};//Inherit the Element method inheritPrototype(Enemy, Element);//Method: Draw the enemy Enemy.prototype.draw = function () { if (this.enemyIcon && this.enemyBoomIcon) { switch (this.status) { case 'normal': var enemyIcon = new Image(); enemyIcon.src = this.enemyIcon; ctx.drawImage(enemyIcon, this.x, this.y, this.size, this.size); break; case 'booming': var enemyBoomIcon = new Image(); enemyBoomIcon.src = this.enemyBoomIcon; ctx.drawImage(enemyBoomIcon, this.x, this.y, this.size, this.size); break; case 'boomed': ctx.clearRect(this.x, this.y, this.size, this.size); break; default: break; } } return this;};//Method: down Move down Enemy.prototype.down = function () { this.move(0, this.size); return this; };//Method: Move left or right Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0); } else { this.move(-this.speed, 0); } return this;};//Method: Enemy explodes Enemy.prototype.booming = function () { this.status = 'booming'; this.boomCount += 1; if (this.boomCount > 4) { this.status = 'boomed'; } return this;}
Bullets have fly() and draw() methods.
/*Bullet*/var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts);};inheritPrototype(Bullet, Element);//Method: Let the bullet fly Bullet. prototype.fly = function () { this.move(0, -this.speed); return this;};//Method: Draw a bullet Bullet.prototype.draw = function () { ctx.beginPath(); ctx.strokeStyle = '#fff'; ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y - CONFIG.bulletSize); ctx.closePath() ; ctx.stroke(); return this;};
The aircraft object contains unique attributes: status, width and height, image, maximum and minimum abscissa values, and methods haveHit(), draw(), direction(), shoot(), and drawBullets().
/*Plane*/var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //Unique attribute status and image this.status = 'normal'; this.width = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; this.minX = opts.minX; this.maxX = opts.maxX; //Bullets related this.bullets = []; this.bulletSpeed = opts.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || CONFIG.bulletSize;};//Inherit the Element method inheritPrototype(Plane, Element) ;//Method: The bullet hits the target Plane.prototype.hasHit = function (enemy) { var bullets = this.bullets; for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; var isHitPosX = (enemy.x < bullet.x) && (bullet.x < (enemy.x + enemy.size)); var isHitPosY = (enemy.y < bullet.y) && (bullet.y < (enemy.y + enemy.size)); if (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } } return false;};//Method: Draw the plane Plane.prototype.draw = function () { this.drawBullets(); var planeIcon = new Image(); planeIcon.src = this.planeIcon; ctx.drawImage(planeIcon, this.x, this.y, this.width, this.height); return this;};//Method: Plane direction Plane.prototype.direction = function (direction) { var speed = this.speed; var planeSpeed; if (direction === 'left' ) { planeSpeed = this.x < this.minX ? 0 : -speed; } else { planeSpeed = this.x > this.maxX ? 0 : speed; } console.log('planeSpeed:', planeSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX); console.log('this .maxX:', this.maxX); this.move(planeSpeed, 0); return this;//Convenient chain call};//Method: launch bullets Plane.prototype.shoot = function () { var bulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x: bulletPosX, y: this.y, size: this.bulletSize, speed: this.bulletSpeed })); return this; };//Method: draw bullets Plane.prototype.drawBullets = function () { var bullets = this.bullets; var i = bullets.length; while (i--) { var bullet = bullets[i]; bullet.fly(); if (bullet.y <= 0) { bullets.splice(i, 1); } bullet.draw(); }};
Keyboard events have the following states:
Because the aircraft needs to keep moving when the left button (keyCode=37) and the right button (keyCode=39) are pressed (keydown), and the keyup does not move when released. When the space (keyCode=32) or up arrow key (keyCode=38) is pressed (keydown), bullets are fired, and when released, keyup stops firing. Also press the Enter key (keyCode=13) to pause the game. Therefore, you need to define a KeyBoard object to monitor whether onkeydown and onkeyup are pressing or releasing a key.
Because the left and right keys are contradictory, to be on the safe side, you need to set the right key to false when pressing the left key. The same goes for right-clicking.
//Keyboard event var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this);}; //KeyBoard object KeyBoard.prototype = { pressedLeft: false, pressedRight: false, pressedUp: false, heldLeft: false, heldRight: false, pressedSpace: false, pressedEnter: false, keydown: function (e) { var key = e.keyCode; switch (key) { case 32://Space - fire bullets this.pressedSpace = true; break; case 37://Left arrow key this.pressedLeft = true; this.heldLeft = true; this.pressedRight = false; this.heldRight = false; break; case 38://Up arrow key - launch bullets this.pressedUp = true; break; case 39://Right arrow key this.pressedLeft = false; this.heldLeft = false; this.pressedRight = true; this.heldRight = true; break; case 13://Enter key - pause the game this.pressedEnter = true; break; } }, keyup: function (e) { var key = e .keyCode; switch (key) { case 32: this.pressedSpace = false; break; case 37: this.heldLeft = false; this.pressedLeft = false; break; case 38: this.pressedUp = false; break; case 39: this.heldRight = false; this.pressedRight = false; break; case 13: this.pressedEnter = false; break; } }};Game logic
The game object (GAME) contains the logic of the entire game, including init (initialization), bindEvent (binding button), setStatus (updating game status), play (in game), stop (pause), end (end), etc., in This does not expand the description. It also includes functions such as generating monsters and drawing game elements.
//The entire game object var GAME = { //A series of logical functions // Game element functions}1. Initialization
The initialization function mainly defines the initial coordinates of the aircraft, the aircraft movement range, the monster movement range, initializes the score, the monster array, creates the KeyBoard object, and executes it only once.
/** * Initialization function, this function is executed only once * @param {object} opts * @return {[type]} [description] */init: function (opts) { //Set opts var opts = Object.assign( {}, opts, CONFIG);//Merge all parameters this.opts = opts; this.status = 'start'; //Calculate the initial coordinates of the aircraft object this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; //Plane limit coordinates this.planeMinX = opts.canvasPadding; this.planeMaxX = canvasWidth - opts.canvasPadding - opts.planeSize. width; //Calculate enemy movement area this.enemyMinX = opts.canvasPadding; this.enemyMaxX = canvasWidth - opts.canvasPadding - opts.enemySize; //The score is set to 0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); this.bindEvent(); this.renderLevel(); },2. Bind button events
Because several game scenes include buttons to start the game (playBtn), restart (replayBtn), next level of the game (nextBtn), and pause the game to continue (stopBtn). We need to perform different events for different buttons.
The reason for defining var self = this; in the first place is the usage of this. In the bindEvent function, this points to the GAME object, and in playBtn.onclick = function () {}; this points to playBtn. This is obviously not what we want, because playBtn does not have a play() event, only the GAME object has it. Therefore, the GAME object needs to be assigned to a variable self, and then the play() event can be called in playBtn.onclick = function () {};.
It should be noted that the replayBtn button appears in both failure and clearance scenarios, so what is obtained is the collection of all .js-replay. Then forEach iterates through each replayBtn button, resets the level and score, and calls the play() event.
bindEvent: function () { var self = this; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelector('. js-next'); var stopBtn = document.querySelector('.js-stop'); // Start game button binding playBtn.onclick = function () { self.play(); }; //Restart game button binding replayBtn.forEach(function (e) { e.onclick = function () { self.opts. level = 1; self.play(); self.score = 0; totalScoreText.innerText = self.score; }; }); // Next level game button binding nextBtn.onclick = function () { self.opts.level += 1; self.play(); }; // Pause the game and continue button binding stopBtn.onclick = function () { self. setStatus('playing'); self.updateElement(); }; },3. Generate aircraft
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, width: opts.planeSize.width, height: opts.planeSize.height, minX : this.planeMinX, speed: opts.planeSpeed, maxX: this.planeMaxX, planeIcon: opts.planeIcon });}4. Generate a group of monsters
Because monsters appear in groups, and the number of monsters in each level is also different, the function of the two for loops is to generate a row of monsters, and increase the level row of monsters according to the number of levels. Or increase the speed of the monster (speed: speed + i,) to increase the difficulty of each level, etc.
//Generate enemies createEnemy: function (enemyType) { var opts = this.opts; var level = opts.level; var enemies = this.enemies; var numPerLine = opts.numPerLine; var padding = opts.canvasPadding; var gap = opts .enemyGap; var size = opts.enemySize; var speed = opts.enemySpeed; //Add a line for each level of enemy level for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //Parameters of comprehensive elements var initOpt = { x: padding + j * (size + gap), y: padding + i * (size + gap), size: size, speed: speed, status: enemyType, enemyIcon: opts.enemyIcon, enemyBoomIcon: opts.enemyBoomIcon }; enemies.push(new Enemy(initOpt)); } } return enemies; },5. Update monsters
Get the x value of the monster array and determine whether it reaches the border of the canvas. If it reaches the border, the monster moves downward. At the same time, the status of monsters must also be monitored, whether monsters in normal status have been hit, monsters in explosive status, and monsters that have disappeared must be removed from the array and scored at the same time.
//Update enemy status updateEnemeis: function () { var opts = this.opts; var plane = this.plane; var enemies = this.enemies; var i = enemies.length; var isFall = false;//The whereabouts of enemies var enemiesX = getHorizontalBoundary(enemies); if (enemiesX.minX < this.enemyMinX || enemiesX.maxX >= this.enemyMaxX) { console.log('enemiesX.minX', enemiesX.minX); console.log('enemiesX.maxX', enemiesX.maxX); opts.enemyDirection = opts.enemyDirection === 'right' ? 'left' : 'right '; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true; } //Loop update enemy while (i--) { var enemy = enemies[i]; if (isFall) { enemy.down(); } enemy.direction(opts.enemyDirection); switch (enemy.status) { case 'normal': if (plane.hasHit(enemy)) { enemy .booming(); } break; case 'booming': enemy.booming(); break; case 'boomed': enemies.splice(i, 1); this.score += 1; break; default: break; } } },
The function of getHorizontalBoundary function is to traverse the x value of each element of the array, filter out larger or smaller values, and obtain the maximum and minimum x value of the array.
//Get the horizontal boundary function of the array getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x; } } }); return { minX: min, maxX: max }}6. Update keyboard panel
Press the Enter key to execute the stop() function, press the left button to move the aircraft left, press the right button to move the aircraft right, and press the space button to execute the aircraft firing bullets. In order to prevent the bullets from connecting in a straight line, set the keyBoard here. pressedUp and keyBoard.pressedSpace are false.
updatePanel: function () { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); return; } if (keyBoard.pressedLeft || keyBoard.heldLeft) { plane. direction('left'); } if (keyBoard.pressedRight || keyBoard.heldRight) { plane.direction('right'); } if (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = false; keyBoard.pressedSpace = false; plane.shoot(); } },7. Draw all elements
draw: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemy) { //console.log('draw:this.enemy',enemy); enemy. draw(); }); },8. Update all elements
First, determine whether the length of the monster array is 0. If it is 0 and the level is equal to totalLevel, it means the level is passed. Otherwise, the game preparation screen for the next level will be displayed; if the y coordinate of the monster array is greater than the y coordinate of the aircraft plus the height of the monster, it will indicate that the game has failed.
The principle of canvas animation is to continuously draw, update, and clear the canvas.
The principle of game pause is to prevent the requestAnimationFrame() function from executing, but not to reset the element. Therefore, when the status of status is judged to be stop, the function will be jumped out.
//Update the status of all elements updateElement: function () { var self = this; var opts = this.opts; var enemies = this.enemies; if (enemies.length === 0) { if (opts.level === opts.totalLevel) { this.end('all-success'); } else { this.end('success'); } return; } if (enemies[enemies.length - 1].y >= this.planePosY - opts.enemySize) { this.end('failed'); return; } //Clear the canvas ctx.clearRect(0, 0, canvasWidth, canvasHeight); //Draw the canvas this .draw(); //Update element status this.updatePanel(); this.updateEnemeis(); //Continuously loop updateElement requestAnimationFrame(function () { if(self.status === 'stop'){ return; }else{ self.updateElement(); } }); },write at the end
Through the above steps, the basic functions of the game are completed. Other game process controls, including start, end, score calculation, etc., will not be described here.
What can be optimized: When holding down the space bar, bullets can be fired continuously. However, when I pressed the direction key again, I found that I could no longer fire bullets. It's best to be able to keep firing bullets when you can move.
It is quite interesting to play games with canvas. In addition, this game can be expanded and changed into a mobile version. The canvas size is determined by obtaining the screen width and height. The keyboard part is changed to touch events (touchstart, touchmove, touchend). The appearance of monsters can also be changed. Change it to randomly falling from the top of the screen, and the monster will increase its health (for example, it will disappear after shooting 4 times), etc.
Download address: https://github.com/littleyljy/shoot
The above is the entire content of this article. I hope it will be helpful to everyone’s study. I also hope everyone will support VeVb Wulin Network.