0、演示在此:http://runjs.cn/detail/y8w993ifcss
一、框架搭建html
爲了方便演示,我搭建了一個簡單的遊戲「框架」,框架包含描述遊戲狀態的部分及邏輯部分。
canvas
一個彈幕遊戲至少應當包含「自機」及「彈幕」。首先,遊戲頁面中應當包含一個用於繪製實際圖形的canvas、一個用於演示的描述自機及彈幕形狀的canvas。此外,爲了便於更換圖形,我還建立了兩個canvas用於繪製自機和子彈。
數組
不包含JS代碼時,頁面以下:框架
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>.</title> </head> <style type="text/css"> #stage{ display:block; float:left; width:400px; height:300px; box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.3), 2px 2px 4px rgba(0, 0, 0, 0.6); } #hitck{ display:block; float:left; width:400px; height:300px; box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.3), 2px 2px 4px rgba(0, 0, 0, 0.6); } #plane{ display:block; float:left; width:48px; height:48px; box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.3), 2px 2px 4px rgba(0, 0, 0, 0.6); } #ammo{ display:block; float:left; width:12px; height:12px; box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.3), 2px 2px 4px rgba(0, 0, 0, 0.6); } </style> <body> <canvas id="stage"></canvas> <canvas id="hitck"></canvas> <canvas id="plane"></canvas> <canvas id="ammo"></canvas> </body> </html>
正如前文所說的,整個頁面就只有四個canvas,從方便的角度出發,咱們把舞臺的大小寫死爲800*600。
dom
二、一個子彈函數
在開始探究碰撞檢測前,咱們須要編寫彈幕生成的代碼,由於沒有實際的彈幕就沒法理解碰撞。
工具
子彈理所固然是一個物體,在此,咱們假設每一個子彈都是一個對象(注意,這麼作的性價比很低),一個基本的子彈定義以下:
性能
DirectAmmo = function(x, y, vx, vy, ax, ay){ this.x = x || 400; // 橫座標 this.y = y || 300; // 縱座標 this.vx = vx || 2; // 橫向速度 this.vy = vy || 2; // 縱向速度 this.ax = ax || 0; // 橫向加速度 this.ay = ay || 0; // 縱向加速度 this.die = false; // 子彈是否已消亡 }
假設每一個子彈的運動都是獨立的,那麼能夠寫一個函數來描述每個時間片中子彈的移動:
this
DirectAmmo.prototype.move = function(){ this.vx += this.ax; this.vy += this.ay; this.x += this.vx; this.y += this.vy; if(this.x < 0){ this.die = true; } if(this.y < 0){ this.die = true; } if(this.x > 800){ this.die = true; } if(this.y > 600){ this.die = true; } if(this.vx == 0 && this.vy == 0 && this.ax == 0 && this.ay == 0){ this.die = true; } return this; }
當子彈超出屏幕範圍以後,子彈便會消亡,將其die屬性置爲true。
一個遊戲不可能只有一顆子彈,同一個屏幕上可能同時出現不少的子彈,這些子彈按照各自的軌跡獨立運動,這構成了彈幕遊戲華麗的基礎,所以,咱們須要一個彈幕隊列來管理這些子彈。
var Queue = { ammo : [], initAmmo : function(maxAmmoCount){ maxAmmoCount = maxAmmoCount || 512; for(var i = 0; i < maxAmmoCount; i ++){ Queue.ammo[i] = null; } }, init : function(){ Queue.initAmmo(); } }
在initAmmo方法中,定義了一個maxAmmoCount變量,彈幕隊列中之多隻能包含maxAmmoCount個子彈對象實例。早期的彈幕遊戲一般用此方法控制性能。
而後,咱們須要爲子彈添加一個方法,用於將子彈自身加入到彈幕隊列中:從頭開始掃描隊列,當找到空位時把本身裝進去空位置中。空位的定義是null或者是指向已死亡的子彈的位置:
DirectAmmo.prototype.queue = function(){ var i = 0; while(i < Queue.ammo.length){ if(Queue.ammo[i] == null || Queue.ammo[i].die){ Queue.ammo[i] = this; return true; } i ++; } return false; }
至此,子彈自己就實現了。
三、一組子彈
彈幕遊戲中,敵機一般都是一次性傾瀉出一堆子彈——一堆有規律的子彈,譬如以下圖的從圓心開始往外擴散的子彈:
天然不可能逐個子彈去生成、去描述,咱們須要定義一個方法來生成這樣的一組子彈,這樣的一個總體比起單個子彈來多了兩個屬性:數量、速度。
RoundDirectAmmo = function(x, y, count, speed, ax, ay){ x = x || 400; y = y || 300; count = count || 32; speed = speed || 2; ax = ax || 0; ay = ay || 0; var offset = 0.00001; var step = Math.PI * 2 / count; var ammos = []; var i, vx, vy, da; for(i = 0; i < count; i++){ vx = speed * Math.cos(offset); vy = speed * Math.sin(offset); da = new DirectAmmo(x, y, vx, vy, ax, ay); offset += step; ammos[ammos.length] = da; } this.ammos = ammos; }
RoundDirectAmmo這個對象實際上包含了一組子彈,在生成這組子彈時按照每個子彈的擴散方向賦予不一樣的初速度,環狀彈幕就這樣實現了。
而後給RoundDirectAmmo寫一個將子彈加入到隊列的函數:
RoundDirectAmmo.prototype.queue = function(){ var i = 0, j = 0; while(i < Queue.ammo.length && j < this.ammos.length){ if(Queue.ammo[i] == null || Queue.ammo[i].die){ Queue.ammo[i] = this.ammos[j]; j ++; } i ++; } return this; }
這裏至關於對每一個子彈直接調用DirectAmmo的queue方法,至於爲何沒直接調用queue,我也忘記當時寫這段代碼時的目的了。
環狀彈幕的「散開」,除了上述這種同步散開外,還有螺旋狀散開的例子:
要實現這種彈幕,只須要對RoundDirectAmmo的加入隊列的方式進行一些修改,來添加一個queue_delay方法:
RoundCircleAmmo.prototype.queue_delay = function(n, i){ n = n || 10; i = i || 0; if(this.ammos[i]){ this.ammos[i].queue(); var qr = this; setTimeout(function(){qr.queue_delay(n, i + 1)}, n); } return this; }
如上,只要添加每一個子彈加入到隊列的時間間隔,就能夠實現螺旋散開的彈幕。
四、繪製對象
對子彈的講解到此爲止,接下來是實際的繪製過程。
在上面咱們建立了四個canvas,這裏咱們建立幾個變量用來指向這幾個canvas:
var cvs_plane, // 自機 ctx_plane, cvs_ammo, // 子彈 ctx_ammo, cvs, // 舞臺 ctx, cvs_ck, // 碰撞演示 ctx_ck;
爲了偷懶,灰機和子彈都畫一個圓來表示,實際上程序寫完以後只要替換一下這個繪製過程,不管什麼形狀的自機和子彈都沒問題:
cvs_plane = document.getElementById('plane'); cvs_plane.height = 48; cvs_plane.width = 48; ctx_plane = cvs_plane.getContext('2d'); ctx_plane.fillStyle = 'rgba(0, 192, 248, 1)'; ctx_plane.beginPath(); ctx_plane.arc(24,24,24,0,Math.PI*2,true); ctx_plane.closePath(); ctx_plane.fill(); var plane_data = ctx_plane.getImageData(0, 0, 48, 48).data; for(var i = 0; i < 48 * 48; i ++){ var dot = i * 4; if(plane_data[dot] > 0 || plane_data[dot + 1] > 0 || plane_data[dot + 2] > 0){ Matrix.plane[i] = 1; }else{ Matrix.plane[i] = 0; } } cvs_ammo = document.getElementById('ammo'); cvs_ammo.height = 12; cvs_ammo.width = 12; ctx_ammo = cvs_ammo.getContext('2d'); ctx_ammo.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx_ammo.beginPath(); ctx_ammo.arc(6,6,6,0,Math.PI*2,true); ctx_ammo.closePath(); ctx_ammo.fill(); var ammo_data = ctx_ammo.getImageData(0, 0, 12, 12).data; for(var i = 0; i < 12 * 12; i ++){ var dot = i * 4; if(ammo_data[dot] > 0 || ammo_data[dot + 1] > 0 || ammo_data[dot + 2] > 0){ Matrix.ammo[i] = 1; }else{ Matrix.ammo[i] = 0; } } cvs = document.getElementById('stage'); cvs.height = 600; cvs.width = 800; ctx = cvs.getContext('2d'); ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, 800, 600); cvs_ck = document.getElementById('hitck'); cvs_ck.height = 600; cvs_ck.width = 800; ctx_ck = cvs_ck.getContext('2d'); ctx_ck.fillStyle = '#000000'; ctx_ck.fillRect(0, 0, 800, 600);
上面這段代碼中牽扯到了一個Matrix對象,這個對象,咱們姑且稱之爲「碰撞檢測矩陣」。
五、碰撞檢測
碰撞檢測的方法不少,最容易實現的是基於顏色。
就如同咱們肉眼看到子彈撞上飛機同樣,只要設想,在飛機所處的位置上出現了子彈,就認爲二者發生了碰撞。
彈幕遊戲都有着「擦彈」的設定。面對形狀多變的子彈和飛機,咱們不能簡單的對子彈的座標寬高與飛機的座標寬高進行比對來判定是否發生了碰撞。譬如上面繪製的圓形子彈,其畫板面積是一個矩形,但實際能參與到碰撞的只有包含圖形的一個圓形部分,自機同理。
如是,很容易得出一個理論:只有自機、子彈畫板中不透明的部分發生了重疊,纔是真碰撞。
屏幕上的圖形有不少的顏色,而在檢測碰撞時咱們只須要兩種顏色:透明、不透明。
所以,咱們來定義幾個大小等於畫板寬度乘以畫板高度的數組,用來單純地繪製形狀,此外,咱們再定義幾個方法,組成Matrix對象:
var Matrix = { /** * 自機形狀 * @type {Array} */ plane : [], /** * 子彈形狀 * @type {Array} */ ammo : [], /** * 舞臺MAP * @type {Array} */ stage : [], /** * 碰撞位置 * @type {Array} */ hited : [], /** * 重置舞臺 */ init : function(){ var i, n = 800 * 600; for(i = 0; i < n; i ++){ Matrix.stage[i] = 0; } }, drawAmmo : function(x, y, w, h){ }, drawPlane : function(x, y, w, h){ } }
在繪製對象這一步中,咱們已經把子彈形狀跟飛機形狀繪製到了Matrix.ammo和Matrix.plane數組中。固然對於後期擴展來講,咱們也能夠不斷修改這兩個數組,但在本次演示中,二者的形狀已經固定了。
Matrix.drawAmmo的任務是把彈幕隊列中的全部子彈繪製到Matrix.stage中:
drawAmmo : function(x, y, w, h){ w = w || 12; h = h || 12; x = parseInt(x); y = parseInt(y); var i = 0, j = 0, k = 0, l = 0, m = 0, n = 0; for(i = 0; i < w; i ++){ for(j = 0; j < h; j ++){ m = parseInt(j * w + i); if(Matrix.ammo[m] > 0){ n = parseInt((j + y) * 800 + (i + x)); Matrix.stage[n] = 1; } } } }
Matrix.stage中,每一項表示一個像素點。設值爲0時,這個像素點中沒有內容,是虛空;值爲1時,這個像素點中有子彈;值爲2時,這個像素點中有子彈且有飛機;值爲3時,這個像素點中有飛機。
在每個時間片中,咱們先調用Matrix.drawAmmo繪製全部的子彈,而後調用Matrix.drawPlane繪製自機。
那麼,Matrix.drawPlane方法實際上包含了碰撞檢測的流程:
drawPlane : function(x, y, w, h){ w = w || 48; h = h || 48; x = parseInt(x); y = parseInt(y); var i = 0, j = 0, k = 0, l = 0, m = 0, n = 0, hit = false; Matrix.hited.length = 0; for(i = 0; i < w; i ++){ for(j = 0; j < h; j ++){ m = parseInt(j * w + i); if(Matrix.plane[m] > 0){ n = parseInt((j + y) * 800 + (i + x)); if(Matrix.stage[n] == 1){ Matrix.stage[n] = 2; hit = true; Matrix.hited.push(n); }else{ Matrix.stage[n] = 3; } } } } return hit; }
當有碰撞發生時,Matrix.drawPlane返回true。
接下來,定義一個plane對象來保存飛機的位置速度信息:
var plane = { x : 0, y : 0, speed : 2 }
至此,咱們已經能夠着手寫遊戲的主線程了。
在主線程中,咱們在stage畫板中繪製實際的遊戲舞臺,包括實際的子彈和飛機;此外,咱們還在hitck畫板中繪製子彈、飛機的形狀及碰撞狀態,不妨用高對比的顏色來分別標示這些像素,譬如用紅色、綠色、藍色分別表明子彈、飛機、碰撞。
那麼,基本的主線程以下:
function run(){ Matrix.init(); ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, 800, 600); ctx.fillStyle = '#00ff00'; ctx.drawImage(cvs_plane, plane.x, plane.y); var i, j; for(i = 0; i < Queue.ammo.length; i ++){ if(Queue.ammo[i] != null && !Queue.ammo[i].die){ Queue.ammo[i].move(); ctx.drawImage(cvs_ammo, Queue.ammo[i].x, Queue.ammo[i].y); Matrix.drawAmmo(Queue.ammo[i].x, Queue.ammo[i].y); } } if(Matrix.drawPlane(plane.x, plane.y)){ //碰撞發生時執行 } // 繪製碰撞演示內容 var ck_data = ctx_ck.createImageData(800, 600); for(i = 0, j = 0; i < 480000; i++, j += 4){ switch(Matrix.stage[i]){ case 1: ck_data.data[j] = 255; ck_data.data[j + 1] = 0; ck_data.data[j + 2] = 0; ck_data.data[j + 3] = 255; break; case 2: ck_data.data[j] = 0; ck_data.data[j + 1] = 0; ck_data.data[j + 2] = 255; ck_data.data[j + 3] = 255; break; case 3: ck_data.data[j] = 0; ck_data.data[j + 1] = 255; ck_data.data[j + 2] = 0; ck_data.data[j + 3] = 255; break; default: ck_data.data[j] = 0; ck_data.data[j + 1] = 0; ck_data.data[j + 2] = 0; ck_data.data[j + 3] = 255; break; } } ctx_ck.putImageData(ck_data, 0, 0); setTimeout(function(){run()}, 20); }
做爲一個遊戲,咱們還須要記錄遊戲狀態,譬如是否暫停,來引入一個對象:
var Game = { playing : true }
再次,咱們須要一個對象來記錄鍵盤狀態,以便讓玩家操做飛機移動,在此咱們定義移動的方向鍵爲WASD:
var Keyboard = { // 上下左右的鍵盤碼 UP : 87, DOWN : 83, LEFT : 65, RIGHT : 68, // 上下左右四個鍵是否被按下 up : false, down : false, left : false, right : false }
而後,在主線程run函數中加入以下語句對遊戲狀態及鍵盤進行響應:
if(!Game.playing){ setTimeout(function(){run()}, 20); return; } if(Keyboard.up && plane.y > 0){ plane.y -= plane.speed; } if(Keyboard.down && plane.y < 600){ plane.y += plane.speed; } if(Keyboard.left && plane.x > 0){ plane.x -= plane.speed; } if(Keyboard.right && plane.x < 800){ plane.x += plane.speed; }
要讓遊戲在飛機與子彈發生碰撞時暫停,在run對Matrix.drawPlane的調用中做修改:
if(Matrix.drawPlane(plane.x, plane.y)){ //碰撞發生時執行 Game.playing = false; }
而後咱們須要產生子彈——定義兩種子彈產生的流程:一是定時生成螺旋散開的彈幕,二是鼠標點擊畫面後在點擊處生成均勻散開的彈幕。
function test_ammo(){ var rca = new RoundCircleAmmo(Math.random() * 400 + 100, Math.random() * 300 + 100, 512); rca.queue_delay(10); setTimeout(function(){test_ammo()}, 8000); } test_ammo();
這裏會每八秒鐘生成一個隨機位置出現、包含512顆子彈的螺旋彈幕。
要讓鼠標點擊生成彈幕,先來引入一個Mouse對象來記錄鼠標狀態:
var Mouse = { downX : 0, downY : 0 }
而後引入兩個方法用於判斷鼠標點擊位置是否在畫板範圍內以及相對畫板的偏移:
/** * 工具庫 * @type {Object} */ var Util = { /** * 是否在畫板範圍內 * @param {canvas} canvas * @param {float} x * @param {float} y * @return {bool} */ canvasInScope : function(canvas, x, y){ x = x || Mouse.downX; y = y || Mouse.downY; return ( x > canvas.offsetLeft && x < canvas.offsetLeft + canvas.clientWidth && y > canvas.offsetTop && y < canvas.offsetTop + canvas.clientHeight ); }, /** * 位置在畫板上的座標偏移 * @param {canvas} canvas * @param {float} x * @param {float} y * @return {Vect} */ offsetOnCanvas : function(canvas, x, y){ x = x || Mouse.downX; y = y || Mouse.downY; var ratioX = canvas.width / canvas.clientWidth; var ratioY = canvas.height / canvas.clientHeight; var offsetX = (x - canvas.offsetLeft) * ratioX; var offsetY = (y - canvas.offsetTop) * ratioY; return new Vect(offsetX, offsetY); } } Vect = function(x, y){ this.x = x || 0; this.y = y || 0; }
而後能夠給window添加事件了:
window.onkeydown = function(e){ switch(e.keyCode){ case Keyboard.UP: Keyboard.up = true; break; case Keyboard.DOWN: Keyboard.down = true; break; case Keyboard.LEFT: Keyboard.left = true break; case Keyboard.RIGHT: Keyboard.right = true; break; default: break; } } window.onkeyup = function(e){ switch(e.keyCode){ case Keyboard.UP: Keyboard.up = false; break; case Keyboard.DOWN: Keyboard.down = false; break; case Keyboard.LEFT: Keyboard.left = false; break; case Keyboard.RIGHT: Keyboard.right = false; break; default: break; } } window.onclick = function(e){ Game.playing = true; Mouse.downX = e.x || e.clientX; Mouse.downY = e.y || e.clientY; if(Util.canvasInScope(cvs)){ var ammo2v = Util.offsetOnCanvas(cvs); new RoundDirectAmmo(ammo2v.x, ammo2v.y).queue(); } if(Util.canvasInScope(cvs_ck)){ var ammo2v = Util.offsetOnCanvas(cvs_ck); new RoundDirectAmmo(ammo2v.x, ammo2v.y).queue(); } }
至此,程序算是完成了。
從左到右依次爲:stage、hitck、plane、ammo