Languages: HTML5, JavaScript
Code: https://github.com/straker/galaxian-canvas-game/tree/master/part2javascript
上節,咱們講述了背景滾動的實現,本節咱們來實現一下玩家飛船的移動和子彈的發射。css
首先讓咱們看一下本次教程的效果,若是界面沒有反應,確認您鼠標點擊了下面的界面。html
控制: 移動 – 上下左右箭頭
射擊 – 空格鍵html5
再次,咱們先看html頁面代碼:java
<!DOCTYPE html> <html> <head> <title>Space Shooter Demo</title> <style> canvas { position: absolute; top: 0px; left: 0px; background: transparent; } #background { z-index: -2; } #main { z-index: -1; } #ship { z-index: 0; } </style> </head> <body> <!-- The canvas for the panning background --> <canvas id="background" width="600" height="360"> Your browser does not support canvas. Please try again with a different browser. </canvas> <!-- The canvas for all enemy ships and bullets --> <canvas id="main" width="600" height="360"> </canvas> <!-- The canvas the ship uses (can only move up one forth the screen.) --> <canvas id="ship" width="600" height="360"> </canvas> <script src="space_shooter_part_two.js"></script> </body> </html>
與上一節的html文件相比,一些地方放生了變化,這裏咱們使用了3個畫布(canvas),一個用來畫背景,一個用來畫飛船,一個用來畫子彈和敵機,這樣作的好處是咱們不用每次都重繪遊戲的每一幀。經過css定義了這三個圖層的z序。git
首先,Drawable對象的init方法須要改變,代碼以下:github
function Drawable() { this.init = function(x, y, width, height) { // Defualt variables this.x = x; this.y = y; this.width = width; this.height = height; } }
由於咱們的圖像再也不充滿整個畫布,因此咱們補充了width, height 2個屬性。canvas
圖像倉庫對象,也有改動,代碼以下:數組
/** * Define an object to hold all our images for the game so images * are only ever created once. This type of object is known as a * singleton. */ var imageRepository = new function() { // Define images this.background = new Image(); this.spaceship = new Image(); this.bullet = new Image(); // Ensure all images have loaded before starting the game var numImages = 3; var numLoaded = 0; function imageLoaded() { numLoaded++; if (numLoaded === numImages) { window.init(); } } this.background.onload = function() { imageLoaded(); } this.spaceship.onload = function() { imageLoaded(); } this.bullet.onload = function() { imageLoaded(); } // Set images src this.background.src = "imgs/bg.png"; this.spaceship.src = "imgs/ship.png"; this.bullet.src = "imgs/bullet.png"; }
window.init();開始遊戲。加載三張圖片,背景,飛船,子彈。三張圖片都加載完成後,調用
接下來咱們將討論飛船的移動和子彈的發射。緩存
首先,咱們先了解一個新的數據結構,對象池。
對於一些可能須要被頻繁建立和銷燬的對象,咱們能夠將其緩存起來,重複使用,以減小系統的開銷,提升遊戲運行的速度,這裏咱們的子彈就屬於這種對象。
/** * Custom Pool object. Holds Bullet objects to be managed to prevent * garbage collection. */ function Pool(maxSize) { var size = maxSize; // Max bullets allowed in the pool var pool = []; /* * Populates the pool array with Bullet objects */ this.init = function() { for (var i = 0; i < size; i++) { // Initalize the bullet object var bullet = new Bullet(); bullet.init(0,0, imageRepository.bullet.width, imageRepository.bullet.height); pool[i] = bullet; } }; /* * Grabs the last item in the list and initializes it and * pushes it to the front of the array. */ this.get = function(x, y, speed) { if(!pool[size - 1].alive) { pool[size - 1].spawn(x, y, speed); pool.unshift(pool.pop()); } }; /* * Used for the ship to be able to get two bullets at once. If * only the get() function is used twice, the ship is able to * fire and only have 1 bullet spawn instead of 2. */ this.getTwo = function(x1, y1, speed1, x2, y2, speed2) { if(!pool[size - 1].alive && !pool[size - 2].alive) { this.get(x1, y1, speed1); this.get(x2, y2, speed2); } }; /* * Draws any in use Bullets. If a bullet goes off the screen, * clears it and pushes it to the front of the array. */ this.animate = function() { for (var i = 0; i < size; i++) { // Only draw until we find a bullet that is not alive if (pool[i].alive) { if (pool[i].draw()) { pool[i].clear(); pool.push((pool.splice(i,1))[0]); } } else break; } }; }
當對象池被初始化時,創建了一個給定個數的子彈對象數組,每當須要一個新的子彈的時候,對象池查看數組的最後一個元素,看該元素是否當前被佔用。若是被佔用,說明當前對象池已經滿了,沒法產生新子彈;若是沒被佔用,
取出最後一個子彈元素,放到數組的最前面。這樣使得對象池中老是後面部分存放着待使用的子彈,而前面存放着正在使用的子彈(屏幕上看到的)。
當對象池animates子彈的時候,判斷當前子彈是否還在屏幕中(draw方法返回false),若是draw方法返回true,說明該子彈已經脫離屏幕區域,能夠被回收供再利用)。
如今對象池對象已經準備好,咱們開始建立子彈對象,以下面代碼:
/** * Creates the Bullet object which the ship fires. The bullets are * drawn on the "main" canvas. */ function Bullet() { this.alive = false; // Is true if the bullet is currently in use /* * Sets the bullet values */ this.spawn = function(x, y, speed) { this.x = x; this.y = y; this.speed = speed; this.alive = true; }; /* * Uses a "drity rectangle" to erase the bullet and moves it. * Returns true if the bullet moved off the screen, indicating that * the bullet is ready to be cleared by the pool, otherwise draws * the bullet. */ this.draw = function() { this.context.clearRect(this.x, this.y, this.width, this.height); this.y -= this.speed; if (this.y <= 0 - this.height) { return true; } else { this.context.drawImage(imageRepository.bullet, this.x, this.y); } }; /* * Resets the bullet values */ this.clear = function() { this.x = 0; this.y = 0; this.speed = 0; this.alive = false; }; } Bullet.prototype = new Drawable();
子彈對象初始化狀態設置alive爲false,子彈對象包含三個方法spawn,draw和clear,仔細看draw方法能夠看到if (this.y <= 0 - this.height),表明子彈已經在屏幕中消失,這裏返回的true。
在剛纔的draw方法中,咱們使用了一個被稱做髒矩形的技術,咱們僅僅清除(clearRect
)子彈前一瞬間的矩形區域,而不是整個屏幕。若是子彈離開了屏幕,draw方法返回true,指示該子彈對象能夠被再回收;不然咱們重繪它。
髒矩形的使用是咱們開始建立3個畫布的緣由。若是咱們只有一個畫布,咱們不得不在每一幀中,重繪全部的對象,屏幕背景,子彈,飛船。若是咱們使用2個畫布,背景一個畫布,子彈和飛船共用一個,那麼子彈每一幀將被重繪,而飛船隻有移動的時候才被重繪,若是子彈和飛船發生重疊,子彈的區域的清除將使得飛船再也不完整,直到咱們移動飛船,這將大大地影響可視效果。因此這裏咱們採用3個畫布。
飛船對象
咱們最後要建立的是飛船對象,代碼以下:
/** * Create the Ship object that the player controls. The ship is * drawn on the "ship" canvas and uses dirty rectangles to move * around the screen. */ function Ship() { this.speed = 3; this.bulletPool = new Pool(30); this.bulletPool.init(); var fireRate = 15; var counter = 0; this.draw = function() { this.context.drawImage(imageRepository.spaceship, this.x, this.y); }; this.move = function() { counter++; // Determine if the action is move action if (KEY_STATUS.left || KEY_STATUS.right || KEY_STATUS.down || KEY_STATUS.up) { // The ship moved, so erase it's current image so it can // be redrawn in it's new location this.context.clearRect(this.x, this.y, this.width, this.height); // Update x and y according to the direction to move and // redraw the ship. Change the else if's to if statements // to have diagonal movement. if (KEY_STATUS.left) { this.x -= this.speed if (this.x <= 0) // Keep player within the screen this.x = 0; } else if (KEY_STATUS.right) { this.x += this.speed if (this.x >= this.canvasWidth - this.width) this.x = this.canvasWidth - this.width; } else if (KEY_STATUS.up) { this.y -= this.speed if (this.y <= this.canvasHeight/4*3) this.y = this.canvasHeight/4*3; } else if (KEY_STATUS.down) { this.y += this.speed if (this.y >= this.canvasHeight - this.height) this.y = this.canvasHeight - this.height; } // Finish by redrawing the ship this.draw(); } if (KEY_STATUS.space && counter >= fireRate) { this.fire(); counter = 0; } }; /* * Fires two bullets */ this.fire = function() { this.bulletPool.getTwo(this.x+6, this.y, 3, this.x+33, this.y, 3); }; } Ship.prototype = new Drawable();
飛船對象設置本身的移動速度爲3, 子彈對象池大小爲30,開火幀率爲15。
KEY_STATUS對象的代碼以下:
// The keycodes that will be mapped when a user presses a button. // Original code by Doug McInnes KEY_CODES = { 32: 'space', 37: 'left', 38: 'up', 39: 'right', 40: 'down', } // Creates the array to hold the KEY_CODES and sets all their values // to false. Checking true/flase is the quickest way to check status // of a key press and which one was pressed when determining // when to move and which direction. KEY_STATUS = {}; for (code in KEY_CODES) { KEY_STATUS[ KEY_CODES[ code ]] = false; } /** * Sets up the document to listen to onkeydown events (fired when * any key on the keyboard is pressed down). When a key is pressed, * it sets the appropriate direction to true to let us know which * key it was. */ document.onkeydown = function(e) { // Firefox and opera use charCode instead of keyCode to // return which key was pressed. var keyCode = (e.keyCode) ? e.keyCode : e.charCode; if (KEY_CODES[keyCode]) { e.preventDefault(); KEY_STATUS[KEY_CODES[keyCode]] = true; } } /** * Sets up the document to listen to ownkeyup events (fired when * any key on the keyboard is released). When a key is released, * it sets teh appropriate direction to false to let us know which * key it was. */ document.onkeyup = function(e) { var keyCode = (e.keyCode) ? e.keyCode : e.charCode; if (KEY_CODES[keyCode]) { e.preventDefault(); KEY_STATUS[KEY_CODES[keyCode]] = false; } }
最後的步驟
最後咱們須要更新遊戲對象以及動畫功能,代碼以下:
/** * Creates the Game object which will hold all objects and data for * the game. */ function Game() { /* * Gets canvas information and context and sets up all game * objects. * Returns true if the canvas is supported and false if it * is not. This is to stop the animation script from constantly * running on browsers that do not support the canvas. */ this.init = function() { // Get the canvas elements this.bgCanvas = document.getElementById('background'); this.shipCanvas = document.getElementById('ship'); this.mainCanvas = document.getElementById('main'); // Test to see if canvas is supported. Only need to // check one canvas if (this.bgCanvas.getContext) { this.bgContext = this.bgCanvas.getContext('2d'); this.shipContext = this.shipCanvas.getContext('2d'); this.mainContext = this.mainCanvas.getContext('2d'); // Initialize objects to contain their context and canvas // information Background.prototype.context = this.bgContext; Background.prototype.canvasWidth = this.bgCanvas.width; Background.prototype.canvasHeight = this.bgCanvas.height; Ship.prototype.context = this.shipContext; Ship.prototype.canvasWidth = this.shipCanvas.width; Ship.prototype.canvasHeight = this.shipCanvas.height; Bullet.prototype.context = this.mainContext; Bullet.prototype.canvasWidth = this.mainCanvas.width; Bullet.prototype.canvasHeight = this.mainCanvas.height; // Initialize the background object this.background = new Background(); this.background.init(0,0); // Set draw point to 0,0 // Initialize the ship object this.ship = new Ship(); // Set the ship to start near the bottom middle of the canvas var shipStartX = this.shipCanvas.width/2 - imageRepository.spaceship.width; var shipStartY = this.shipCanvas.height/4*3 + imageRepository.spaceship.height*2; this.ship.init(shipStartX, shipStartY, imageRepository.spaceship.width, imageRepository.spaceship.height); return true; } else { return false; } }; // Start the animation loop this.start = function() { this.ship.draw(); animate(); }; } /** * The animation loop. Calls the requestAnimationFrame shim to * optimize the game loop and draws all game objects. This * function must be a gobal function and cannot be within an * object. */ function animate() { requestAnimFrame( animate ); game.background.draw(); game.ship.move(); game.ship.bulletPool.animate(); }