接觸canvas 也只有一個多月,第一次完整實現一個遊戲流程,收穫還蠻大的。
射擊遊戲截圖
先上demo:https://littleyljy.github.io/demo/shootgame/
遊戲規則要求玩家控制飛機發射子彈,消滅會移動的怪獸,如果全部消滅了則遊戲成功,如果怪獸移動到底部則遊戲失敗。
遊戲分為幾個場景:
實現場景切換,其實是先把所有場景display: none , 然後透過js 控制data-status 分別為start 、playing 、failed 、success 、all-success 、stop 來實現對應場景display: block 。
HTML 和CSS 如下:
<div id=game data-status=start> <div class=game-panel> <section class=game-intro game-ui> <h1 class=section-title>射擊遊戲</h1> <p class=game- desc>這是一個令人欲罷不能的射擊遊戲,使用← 和→ 操作你的飛機,使用空格(space)進行射擊,使用回車(enter)暫停遊戲。一起來消滅宇宙怪獸吧! </p> <p class=game-level>目前Level: 1</p> <button class=js-play button>開始遊戲</button> </section> <section class=game-failed game-ui> <h1 class=section-title>遊戲結束</h1> <p class=game-info-text>最終得分: <span class=score></span></p> <button class=js-replay button>重新開始</button> </section> <section class=game-success game-ui> <h1 class=section-title>遊戲成功</h1> <p class=game-next- level game-info-text></p> <button class=js-next button>繼續遊戲</button> </section> <section class=game-all-success game-ui> <h1 class=section-title>通關成功</h1> <p class=game-next-level game-info-text>你已經成功地防禦了怪獸的所有攻擊。 </p> <button class=js-replay button>再玩一次</button> </section> <section class=game-stop game-ui> <h1 class=section-title>遊戲暫停</h1> < button class=js-stop button>遊戲繼續</button> </section> </div> <div class=game-info game-ui> <span class=title>分數:</span> <span class=score></span> </div> <canvas id=canvas width=700 height=600> <!-- 動畫畫板--> </canvas> < /div>
#game{ width: 700px; height: 600px; position: relative; left: 50%; top: 40px; margin: 0 0 0 -350px; background: linear-gradient(-180deg, #0400024 4000 97%);}.game-ui{ display: none; padding: 55px; box-sizing: border-box; height: 100%;}[data-status=start] .game-intro { display: block; padding-頂: 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;}物件導向
整個遊戲可以把怪獸(Enemy)、飛機(Plane)、子彈(Bullet)當作對象,另外還有配置對象(CONFIG)和控制遊戲邏輯的遊戲對象(GAME)。
遊戲相關配置/** * 遊戲相關配置* @type {Object} */var CONFIG = { status: 'start', // 遊戲開始預設為開始中level: 1, // 遊戲預設等級totalLevel: 6, // 總共6關numPerLine: 7, // 遊戲預設每行多少個怪獸canvasPadding: 30, // 預設畫布的間隔bulletSize: 10, // 預設子彈長度bulletSpeed: 10, // 預設子彈的移動速度enemySpeed: 2, // 預設敵人移動距離enemySize: 50, // 預設敵人的尺寸enemyGap: 10, // 預設敵人之間的間距enemyIcon: './img/enemy. png', // 怪獸的圖像enemyBoomIcon: './img/boom.png', // 怪獸死亡的圖像enemyDirection: 'right', // 預設敵人一開始往右移動planeSpeed: 5, // 預設飛機每一步移動的距離planeSize: { width: 60, height: 100 }, // 預設飛機的尺寸, planeIcon: './img/plane. png'};定義父類別
因為怪獸(Enemy)、飛機(Plane)、子彈(Bullet)都有相同的x, y, size, speed 屬性和move() 方法,所以可以定義一個父類別Element,透過子類別繼承父類別的方式實現。
/*父類:包含xy speed move() draw()*/var Element = function (opts) { this.opts = opts || {}; //設定座標、尺寸、速度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 inheritPrototype(subType, superType) { var proto = Object.create(superType.prototype); proto.constructor = subType; subType.prototype = proto;}
move(x, y) 方法根據傳入的(x, y) 值自疊加。
定義怪獸怪獸包含特有屬性:怪獸狀態、影像、控制爆炸狀態持續的boomCount ,和draw()、down()、direction()、booming() 方法。
/*敵人*/var Enemy = function (opts) { this.opts = opts || {}; //呼叫父類屬性Element.call(this, opts); //特有屬性狀態和圖像this.status = ' normal';//normal、booming、noomed this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon; this.boomCount = 0;};//繼承Element方法inheritPrototype(Enemy, Element);//方法:繪製敵人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.myBoomIoom; 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;};//方法:down 向下移動Enemy.prototype.down = function () { this.move(0, this.size); return this;};//方法:左右移動Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0); } else { this.move (-this.speed, 0); } return this;};//方法:敵人爆炸Enemy.prototype.booming = function () { this.status = 'booming'; this.boomCount += 1; if (this.boomCount > 4) { this.status = 'boomed'; } return this;}
子彈有fly() 、draw() 方法。
/*子彈*/var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts);};inheritPrototype(Bullet, Element);//方法:讓子彈飛Bullet. prototype.fly = function () { this.move(0, -this.speed); return this;};//方法:繪製子彈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;};
飛機物件包含獨特屬性:狀態、寬高、影像、橫座標最大最小值,有hasHit()、draw()、direction()、shoot()、drawBullets() 方法。
/*飛機*/var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //特有屬性狀態和圖像this.status = 'normal'; this.width = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; this.minX = opts.minX; this.maxX = opts.maxX; //子彈相關this.bullets = []; this.bulletSpeed = opts.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || CONFIG.bulletSize}; ;//繼承Element方法inheritPrototype(Plane, Element);//方法:子彈擊中目標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;};//方法:繪製飛機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;};//方法:飛機方向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;//方便鍊式呼叫};//方法:發射子彈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;};//方法:繪製子彈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(); }};
鍵盤事件有以下幾種狀態:
因為飛機需要按下左鍵(keyCode=37)右鍵(keyCode=39)時(keydown)一直移動,釋放時keyup 不移動。按下空格(keyCode=32)或上方向鍵(keyCode=38)時(keydown)發射子彈,釋放時keyup 停止發射。另外按下回車鍵(keyCode=13)暫停遊戲。所以,需要定義一個KeyBoard 物件監聽onkeydown 和onkeyup 是否按下或釋放某個按鍵。
因為左右鍵是矛盾的,為保險起見,按下左鍵時需要把右鍵設為false。右鍵同理。
//鍵盤事件var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this);};//KeyBoard物件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://空格-發射子彈this.pressedSpace = true; break; case 37 ://左邊方向鍵this.pressedLeft = true; this.heldLeft = true; this.pressedRight = false; this.heldRight = false; break; case 38://上方向鍵-發射子彈this.pressedUp = true; break; case 39://右方向鍵this.pressedLeft = false; this.heldLeft = false; this .pressedRight = true; this.heldRight = true; break; case 13://迴車鍵-暫停遊戲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)包含了整個遊戲的邏輯,包括init(初始化)、bindEvent(綁定按鈕)、setStatus(更新遊戲狀態)、play(遊戲中)、stop(暫停)、end(結束)等,在此不展開描述。也包含了生成怪獸、繪製遊戲元素等函數。
// 整個遊戲物件var GAME = { //一系列邏輯函數//遊戲元素函數}1、初始化
初始化函數主要是定義飛機初始座標、飛機移動範圍、怪獸移動範圍,以及初始化分數、怪獸數組,建立KeyBoard 對象,只執行一次。
/** * 初始化函數,這個函數只執行一次* @param {object} opts * @return {[type]} [description] */init: function (opts) { //設定opts var opts = Object.assign( {}, opts, CONFIG);//合併所有參數this.opts = opts; this.status = 'start'; //計算飛機物件初始座標this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; //飛機極限座標this.planeMinX = opts.canvasding; vasPadding; //飛機極限座標this.planeMinX = opts.canvasding; opts.planeSize.width; //計算敵人移動區域this.enemyMinX = opts.canvasPadding; this.enemyMaxX = canvasWidth - opts.canvasPadding - opts.enemySize; //分數設定為0 this.score = 0; this.enemies =enemySize; //分數設定為0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); this.bindEvent(); this.renderLevel(); },2、綁定按鈕事件
因為幾個遊戲場景包含開始遊戲(playBtn)、重新開始(replayBtn)、下一關遊戲(nextBtn)、暫停遊戲繼續(stopBtn)幾個按鈕。我們需要給不同按鈕執行不同事件。
首先定義var self = this; 的原因是this 的用法。在bindEvent 函數中, this 指向GAME 對象,而在playBtn.onclick = function () {}; 中this 指向了playBtn ,這顯然不是我們希望的,因為playBtn 沒有play() 事件,GAME 物件中才有。因此需要把GAME 物件賦值給一個變數self ,然後才能在playBtn.onclick = function () {}; 中呼叫play() 事件。
要注意的是replayBtn 按鈕在闖關失敗和通關場景都有出現,因此取得的是所有.js-replay 的集合。然後forEach 遍歷每個replayBtn 按鈕,重置關卡和分數,呼叫play() 事件。
bindEvent: function () { var self = this; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelector('. js-next'); var stopBtn = document.querySelector('.js-stop'); // 開始遊戲按鈕綁定playBtn.onclick = function () { self.play(); }; //重新開始遊戲按鈕綁定replayBtn.forEach(function (e ) { e.onclick = function () { self.opts.level = 1; self.play(); self.score = 0; totalScoreText.innerText = self.score; }; }); // 下一關遊戲按鈕綁定nextBtn.onclick = function () { self.opts.level += 1; self.play(); }; // 暫停遊戲繼續按鈕綁定stopBtn.onclick = function () { self.setStatus('playing'); self.updateElement(); }; },3.生成飛機
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, width: opts.planeSize.width, height: opts.planeSize.height, opts.planemin. : this.planeMinX, speed: opts.planeSpeed, maxX: this.planeMaxX, planeIcon: opts.planeIcon });}4.生成一組怪獸
因為怪獸都是成組出現的,每一關的怪獸數量也不同,兩個for 迴圈的作用就是產生一行怪獸,依照關數(level)增加level 行怪獸。或增加怪獸的速度(speed: speed + i,)來提高每一關難度等。
//產生敵人createEnemy: function (enemyType) { var opts = this.opts; var level = opts.level; var enemies = this.enemies; var numPerLine = opts.numPerLine; var padding = opts.canvasi; .enemyGap; var size = opts.enemySize; var speed = opts.enemySpeed; //每升級一關敵人增加一行for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //綜合元素的參數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、更新怪獸
取得怪獸數組的x 值,判斷是否到達畫布邊界,如果到達邊界則怪獸向下移動。同時也要監聽怪獸狀態,正常狀態下的怪獸是否被擊中,爆炸狀態下的怪獸,消失的怪獸要從陣列剔除,同時得分。
//更新敵人狀態updateEnemeis: function () { var opts = this.opts; var plane = this.plane; var enemies = this.enemies; var i = enemies.length; var isFall = false;//敵人下落 enemiesXvar enemiesXvar enemiesXvar enemiesXvar enemiesXvar enemiesXvar enemiesXvar enemiesXvar)。 = getHorizontalBoundary(enemies); if (enemiesX.minX < this.enemyMinX || enemiesX.maxX >= this.enemyMaxX) { console.log('enemiesX.minX', enemiesX.minX); console.log('enemiesX.maxX', enemiesX.maxX); console.log('enemiesX.maxX', enemiesX.maxX); opts.enetsDirection = opts. .enemyDirection === 'right' ? 'left' : 'right'; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true; } //循環更新敵人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; } } },
getHorizontalBoundary 函數的作用是遍歷數組每個元素的x 值,篩選出更大或更小的值,從而獲得數組最大和最小的x 值。
//取得陣列橫向邊界function getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else {max) { min = item.x; max = item.x; } else {max) { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x; } } }); return { minX: min, maxX: max }}6.更新鍵盤面板
按下回車鍵執行stop() 函數,按下左鍵執行飛機左移,按下右鍵執行飛機右移,按下空格執行飛機發射子彈,為了不讓子彈連成一條直線,在這裡設定keyBoard. pressedUp 和keyBoard.pressedSpace 為false。
updatePanel: function () { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); return; } if (keyBoard.pressedLeft || keyBoard.Left(); return; } if (keyBoard.pressedLeft || keyBoard.LedLeft) { pheldLeft. 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: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemy) { //console.log('draw:this.enemy',enemy); enemy. draw(); }); },8.更新所有元素
首先判斷怪獸數組長度是否為0 ,為0 且level 等於totalLevel 說明通關,否則顯示下一關遊戲準備畫面;如果怪獸數組y 座標大於飛機y 座標加怪獸高度,顯示遊戲失敗。
canvas 動畫的原理就是不斷繪製、更新、清除畫布。
遊戲暫停的原理就是阻止requestAnimationFrame() 函數執行,但不重置元素。因此判斷status 的狀態為stop 時跳出函數。
//更新所有元素狀態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; } //清理畫布ctx.clearRect(0, 0, canvasWidth, canvasHeight); //繪製畫布this.draw(); //更新元素狀態this.updatePanel(); this.updateEnemeis(); //不斷循環updateElement requestAnimationFrame(function () { if(self.status === 'stop'){ return; }else{ self.updateElement(self.updateElement(self ); } }); },寫在最後
透過以上幾個步驟,遊戲的基本功能就完成了,其他一些遊戲流程控制,包括開始、結束、得分計算等在此就不敘述了。
可以優化的地方:按住空白鍵的時候,可以連續發射子彈。但是,這時再按一下方向鍵,發現無法再發射子彈了。最好是能移動的時候,也能保持子彈的發射。
canvas 做遊戲還是比較有趣的,另外還可以把這款遊戲加以擴展,改成手機版,畫布尺寸透過取得螢幕寬高確定,鍵盤部分改成觸控事件(touchstart、touchmove、touchend),怪獸出現方式也可以改成從螢幕頂端隨機下落,怪獸增加血量(如射擊4次才消失)等。
下載網址:https://github.com/littleyljy/shoot
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。