JavaScript躲避行星游戲?qū)崿F(xiàn)全程
1. 游戲概述
顧名思義,躲避小行星游戲的目標是非常明顯的:當(dāng)小行星向你沖來時,讓火箭飛行和生存的時間盡可能長一些(如圖91所示)。如果你碰上某顆小行星,游戲?qū)⒔Y(jié)束,游戲的分數(shù)是通過火箭生存的時間來計算的。
躲避小行星游戲是一個“橫向卷軸式”游戲,或者說它至少類似于這樣的游戲,將會側(cè)重于動態(tài)場景。
2. 核心功能
在創(chuàng)建游戲之前,首先需要構(gòu)建一些基本框架。就創(chuàng)建躲避小行星游戲而言,這些框架就是基本的HTML、CSS以及JavaScript代碼(作為將來要添加的高級代碼的基礎(chǔ))。
2.1 構(gòu)建 HTML 代碼
在瀏覽器中創(chuàng)建游戲的優(yōu)點在于可以使用一些構(gòu)建網(wǎng)站的常用技術(shù)。也就是說,可以使用 HTML 語言來創(chuàng)建游戲的用戶界面(UI)?,F(xiàn)在的界面看上去不太美觀,這是因為我們還沒有使用 CSS 來設(shè)計用戶界面的樣式,但目前內(nèi)容的原始結(jié)構(gòu)是最重要的。
在你的計算機上為該游戲創(chuàng)建一個新目錄,新建一個index.html
文件,在其中加入以下代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Asteroid avoidance</title> <link rel="stylesheet" href="style.css" rel="external nofollow" rel="external nofollow" > <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="./main.js"></script> </head> <body> <div id="game"> <div id="game-ui"> <div id="game-intro"> <h1>Asteroid avoidance</h1> <p>Click play and then press any key to start.</p> <p> <a id="game-play" class="button" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Play</a> </p> </div> <div id="game-stats"> <p>Time: <span class="game-score"></span> </p> <p> <a class="game-reset" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Reset</a> </p> </div> <div id="game-complete"> <h1>Game over!</h1> <p>You survived for <span class="game-score"></span> seconds. </p> <p><a class="game-reset buyyon" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Play</a></p> </div> </div> <canvas id="game-canvas" width="800" height="600"> <!-- 在此處插入后備代碼 --> </canvas> </div> </body> </html>
我不打算過多解釋這些 HTML 代碼,因為它們比較簡單,但你只要知道這就是游戲所需的所有標記即可。
2.2 美化界面
創(chuàng)建一個名為 style.css
的新文件,并把它和 HTML 文件放在相同的目錄下。在該 CSS 文件中插入以下代碼:
* { margin: 0; padding: 0; } html, body { height: 100%; width: 100%; } canvas { display: block; } body { background-color: #000; color: #fff; font-family: Verdana, Arial, sans-serif; font-size: 18px; height: 100%; } h1 { font-size: 30px; } p { margin: 0 20px; } a { color: #fff; text-decoration: none; } a:hover { text-decoration: underline; } a.button { background-color: #185da8; border-radius: 5px; display: block; font-size: 30px; margin: 40px 0 0 270px; padding: 10px; width: 200px; } a.button:hover { background-color: #2488f5; color: #fff; text-decoration: none; } #game { height: 600px; left: 50%; margin: -300px 0 0 -400px; position: relative; top: 50%; width: 800px; } #game-canvas { background-color: #001022; } #game-ui { height: 600px; position: absolute; width: 800px; } #game-intro, #game-complete { background-color: rgba(0, 0, 0, .5); margin-top: 100px; padding: 40px 0; text-align: center; } #game-stats { font-size: 14px; margin: 20px 0; } #game-stats, .game-reset { margin: 20px 20px 0 0; position: absolute; right: 0; top: 0; }
2.3 編寫 JavaScript 代碼
在添加一些有趣的游戲邏輯之前,首先需要用JavaScript實現(xiàn)核心功能。創(chuàng)建一個名為 main.js
的新文件,并把它和 HTML 文件放在相同的目錄下。在該 js 文件中插入以下代碼:
$(document).ready(function () { const canvas = $('#game-canvas'); const context = canvas.get(0).getContext("2d"); // 畫布尺寸 const canvasWidth = canvas.width(); const canvasHeight = canvas.height(); // 游戲設(shè)置 let playGame; // 游戲UI const ui = $("#game-ui"); const uiIntro = $("#game-intro"); const uiStats = $("#game-stats"); const uiComplete = $("#game-complete"); const uiPlay = $("#game-play"); const uiReset = $(".game-reset"); const uiScore = $(".game-score"); // 重至和啟動游戲 function startGame() { // 重置游戲狀態(tài) uiScore.html("0"); uiStats.show(); // 初始游戲設(shè)置 playGame = false; // 開始動畫糖環(huán) animate(); } //初始化游戲環(huán)境 function init() { uiStats.hide(); uiComplete.hide(); uiPlay.click(function (e) { e.preventDefault(); uiIntro.hide(); startGame(); }); uiReset.click(function (e) { e.preventDefault(); uiComplete.hide(); startGame(); }); } // 動畫循環(huán),游戲的嫌味性就在這里 function animate() { // 清除 context.clearRect(0, 0, canvasWidth, canvasHeight); if (playGame) { setTimeout(animate, 33); } } init(); });
在你最喜歡的瀏覽器中運行該游戲,應(yīng)該會看到一個更加美觀的 UI。另外,你還可以單擊 Play 按鈕來顯示游戲的主窗口,盡管它看上去也許還有些單調(diào)。
3. 創(chuàng)建游戲?qū)ο?/h2>
躲避小行星游戲使用兩個主要對象:小行星和玩家使用的火箭。我們將使用 JavaScript 類來創(chuàng)建這些對象。你也許會覺得奇怪,既然玩家只有一個,為什么還要通過一個類來定義它呢?簡而言之,如果你以后需要在游戲中添加多個玩家,通過類創(chuàng)建玩家就會更容易一些。
3.1 創(chuàng)建小行星
通過類創(chuàng)建游戲?qū)ο笠馕吨憧梢栽谄渌螒蛑蟹浅7奖愕刂赜煤透淖兯鼈兊挠猛尽?/p>
第一步是聲明主要變量,我們將使用這些變量來存儲所有的小行星。同時,還需要聲明另外一個變量,用于計算游戲中應(yīng)該存在的小行星數(shù)目。在 JavaScript 代碼頂部的 playGame
變量下面添加以下代碼:
let asteroids; let numAsteroids;
稍后你將會為這些變量賦值,但現(xiàn)在我們只建立小行星類。在startGame
函數(shù)上面添加以下代碼:
function Asteroid(x, y, radius, vX) { this.x = x; this.y = y; this.radius = radius; this.vX = vX; }
這里存在一個速度屬性,這是因為小行星只需要從右向左運動,即只需要 x
速度。這里不需要 y
速度,所以就省略了。
在開始創(chuàng)建所有小行星之前,需要建立數(shù)組來存儲這些小行星,并聲明實際需要使用的小行星數(shù)目。在startGame
函數(shù)中的PlayGame
變量下面添加以下代碼:
asteroids = new Array(); numAsteroids = 10;
你也許認為 10 個小行星是一個很小的數(shù)目,但是當(dāng)這些小行星在屏幕上消失時,你將重復(fù)使用它們,所以在游戲中你實際看到的小行星數(shù)目可以有無窮多個。你可以把這里的小行星數(shù)目看做屏幕上某一時刻可能出現(xiàn)的小行星總數(shù)。
創(chuàng)建小行星的過程其實就是一個創(chuàng)建循環(huán)的過程,循環(huán)的次數(shù)就是你剛才聲明的小行星數(shù)目。在你剛才賦值的numAsteroids
變量下面添加以下代碼:
for (let i = 0; i < numAsteroids; i++) { const radius = 5 + (Math.random() * 10); const x = canvasWidth + radius + Math.floor(Math.random() * canvasWidth); const y = Math.floor(Math.random() * canvasHeight); const vx = -5 - (Math.random() * 5); asteroids.push(new Asteroid(x, y, radius, vX)); }
為了讓每顆小行星的外觀都與眾不同,并且使游戲看上去更有趣一些,可以把小行星的半徑設(shè)為一個介于 5 到 15 像素之間的隨機數(shù)( 5 加上一個介于 0 到 10 之間的隨機數(shù))。雖然 x
速度的值介于 -5 到 -10 之間,但你也可以采用同樣的方法來設(shè)置它( -5 減去一個 0 到 5 之間的數(shù))。因為你需要讓小行星按從右向左的方向運動,所以使用的是一個負的速度值,這說明 x
的位置將隨著時間的推移而減小。
計算每顆小行星的 x
位置看上去有些復(fù)雜,但其實非常簡單。在開始啟動游戲的時候,如果讓所有的小行星全部顯示在屏幕上,就讓人覺得有些太奇怪了。因此在游戲開始之前,最好把小行星放在屏幕的右側(cè),當(dāng)游戲開始時才讓它們按從右向左的順序穿過屏幕。
為此,首先需要把 x
位置設(shè)為 canvas
元素的寬度,然后加上小行星的半徑。這意味著如果你現(xiàn)在畫出小行星,那么它應(yīng)該位于屏幕的右側(cè)。如果僅僅這樣做,那么所有的小行星將會排成一行,所以下一步我們需要把 x
位置加上一個介于 0 到畫布寬度之間的隨機值。與 x
位置相比,y
位置簡單一些,它只是一個介于 0 到畫布高度之間的隨機值。
這樣可以產(chǎn)生一個與畫布尺寸相同的方框,方框中隨機分布著一些小行星。當(dāng)游戲開始時,這些小行星將穿過可見的畫布。
最后一步是把一個新的小行星推送到數(shù)組中,做好移動和繪制小行星的準備。
3.2 創(chuàng)建玩家使用的火箭
首先聲明用于建立玩家的初始化變量。在 JavaScript 頂部的 numAsteroids
變量下面添加以下代碼:
let player;
該變量將用于存儲玩家對象的引用,但現(xiàn)在我們還沒有定義玩家對象。在Asteroid
類下面添加以下代碼:
function Player(x, y) { this.x = x; this.y = y; this.width = 24; this.height = 24; this.halfWidth = this.width / 2; this.halfHeight = this.height / 2; this.vX = 0; this.vY = 0; }
你應(yīng)該熟悉以上代碼的某些部分,例如位置和速度屬性。其余屬性用于描述玩家使用的火箭的尺寸,包括整個尺寸和一半的尺寸。繪制火箭和執(zhí)行碰撞檢測時,你需要使用這些尺寸。
最后一步是創(chuàng)建一個新的玩家對象。為此,在 startGame
函數(shù)中的numAsteroids
變量下面添加以下對象:
player = new Player(150, canvasHeight / 2);
通過以上代碼,玩家的位置將垂直居中,并且距離畫布左邊界 150 像素。
現(xiàn)在還不能看到任何效果,稍后當(dāng)你開始著手移動所有的游戲?qū)ο髸r,將會從視覺上看到這種效果。
4. 檢測鍵盤輸入
本游戲?qū)⑹褂面I盤來控制游戲。更確切地說,你將使用方向鍵來四處移動玩家使用的火箭。如何才能實現(xiàn)這種控制呢?這比控制鼠標輸入更難嗎?不,其實非常簡單。下面我來教你怎么做。
4.1 鍵值
在處理鍵盤輸人時,首先需要知道哪一個按鍵被按下了。在 JavaScript 中,普通鍵盤上的每一個按鍵都分配了一個特定的鍵值(key code)。通過這些鍵值,可以唯一確定按下或釋放了哪個鍵。稍后你將學(xué)習(xí)如何使用鍵值,現(xiàn)在我們首先需要了解每個按鍵所對應(yīng)的數(shù)值。
例如,鍵 a
到 z
(無論在什么情況下)對應(yīng)的鍵值分別是從 65 到 90 。箭頭鍵對應(yīng)的鍵值是從 37 到 40,其中左箭頭的鍵值是 37、上箭頭的鍵值是 38、右箭頭的鍵值是 39、下箭頭的鍵值是 40。空格鍵的鍵值是 32。
在躲避小行星游戲中,你需要重點關(guān)注的是箭頭鍵,因此在 JavaScript 代碼頂部的 player
變量下面添加以下代碼:
const arrowUp = 38; const arrowRight = 39; const arrowDown = 40;
以上代碼為每個箭頭對應(yīng)的鍵值分別分配了一個變量。這種方法稱作枚舉(enumeration),它是對值進行命名的過程。這主要是為后面的工作提供方便,因為通過這些名稱你能很容易確定變量引用的是哪個箭頭鍵。
請注意,為什么沒有為左箭頭聲明一個變量呢?因為你不會手動地讓玩家向后移動。相反,當(dāng)玩家沒有按任何按鍵時,就會表現(xiàn)為向后移動的狀態(tài)。稍后你就會明白其中的道理。
4.2 鍵盤事件
在向游戲中添加鍵盤交互效果之前,首先需要確定玩家在何時按下或釋放某個按鍵。為此,需要使用 keydown
和 keyup
事件監(jiān)聽器。
在 startGame
函數(shù)中的 animate
函數(shù)調(diào)用上面(在創(chuàng)建所有小行星的循環(huán)下面)添加以下代碼:
$(window).keydown(e => { }); $(window).keyup(e => { });
按下某個按鍵時將觸發(fā)第一個監(jiān)聽器,釋放某個按鍵時將觸發(fā)第二個監(jiān)聽器。非常簡單。稍后我們將在這些事件監(jiān)聽器中添加一些有用的代碼,但首先需要在重新設(shè)置游戲時刪除這些監(jiān)聽器,這能防止玩家由于無意按下某個按鍵而啟動游戲。在 uiReset.click
事件監(jiān)聽器中的 startGame
調(diào)用上面添加以下代碼:
$(window).unbind('keyup'); $(window).unbind('keydown');
接下來,還需要添加一些在激活玩家后用到的屬性。在 Player
類的末尾添加以下代碼:
this.moveRight = false; this.moveUp = false; this.moveDown = false;
通過這些屬性,你可以知道玩家的移動方向,這些屬性值的設(shè)置取決于玩家按下了哪個按鍵?,F(xiàn)在你是不是已經(jīng)理解了其中的所有道理呢?
最后,需要向鍵盤事件監(jiān)聽器中添加一些邏輯。首先,在 keydown
事件監(jiān)聽器中添加以下代碼:
const keyCode = e.keyCode; if (!playGame) { playGame = true; animate(); } if (keyCode == arrowRight) { player.moveRight = true; } else if (keyCode == arrowUp) { player.moveUp = true; } else if (keyCode == arrowDown) { player.moveDown = true; }
并在 keyup
事件監(jiān)聽器中添加以下代碼:
const keyCode = e.keyCode; if (keyCode == arrowRight) { player.moveRight = false; } else if (keyCode == arrowUp) { player.moveUp = false; } else if (keyCode == arrowDown) { player.moveDown = false; }
以上代碼的作用非常明顯,但我還需要作一些說明。在兩個監(jiān)聽器中,第一行的作用都是把按鍵的鍵值賦給一個變量。然后在一組檢查語句中使用該鍵值來判斷是否按下了某個箭頭鍵,如果按下了箭頭鍵,判斷是哪個箭頭鍵。這樣,我們就可以啟動(如果按下了該鍵)或禁用(如果釋放了該鍵)玩家對象的對應(yīng)屬性。
例如,如果按下了向右的箭頭鍵,那么玩家對象的 moveRight
屬性將被設(shè)為 true
。如果釋放了該方向鍵,則 moveRight
屬性將被設(shè)為false
。
**注意:**如果玩家一直按住某個按鍵,那么將觸發(fā)多個 keydown
事件。因此,代碼要具備處理多個被觸發(fā)的 keydown
事件的能力,這一,點非常重要。在每個 keydown
事件之后不一定總是一個 keyup
事件,另外還要注意的是,在 keydown
事件監(jiān)聽器中是如何通過一個條件語句來查看游戲當(dāng)前是否正在進行的。如果玩家沒有做好游戲準備,該語句將阻止游戲運行。只有玩家按下鍵盤上的某個鍵時,才會啟動游戲。方法很簡單,但卻非常有效。
游戲中的鍵盤輸入非常多,我們不可能一一列舉。在下一節(jié)中,我們將通過這些輸入來控制玩家沿著正確的方向運動。
5. 讓對象運動起來
現(xiàn)在你已經(jīng)做好了實現(xiàn)游戲?qū)ο髣赢嫷乃袦蕚?。?dāng)你實際看到游戲效果時,這一切會變得更有趣。
第一步是更新所有游戲?qū)ο蟮奈恢?。我們從更新小行星對象的位置開始,在 animate
函數(shù)中
畫布的 clearRect
方法下面添加以下代碼:
const asteroidsLength = asteroids.length; for (let i = 0; i < asteroidsLength; i++) { const tmpAsteroid = asteroids[i]; tmpAsteroid.x += tmpAsteroid.vX; context.fillStyle = "rgb(255, 255, 255)"; context.beginPath(); context.arc(tmpAsteroid.x, tmpAsteroid.y, tmpAsteroid.radius, 0, Math.PI * 2, true); context.closePath(); context.fill(); }
這些代碼非常簡單。主要是遍歷每一顆小行星,并根據(jù)速度來更新它的位置,然后在畫布上繪制小行星。
刷新瀏覽器查看效果(記住按下某個按鍵啟動游戲)。應(yīng)該能夠看到某顆小行星帶穿越屏幕的場景。
注意它們是如何消失在屏幕左側(cè)的。下一節(jié)將學(xué)習(xí)如何在橫向滾動的屏幕上阻止它們的運動。
迄今為止,假設(shè)這些小行星都實現(xiàn)了我們的預(yù)期效果。接下來還需要更新并顯示玩家!
在 animate
函數(shù)中剛才添加小行星代碼的下面再添加以下代碼:
player.vX = 0; player.vY = 0; if (player.moveRight) { player.vX = 3; } if (player.moveUp) { player.vY = -3; } if (player.moveDown) { player.vY = 3; } player.x += player.vX; player.y += player.vY;
以上代碼將更新玩家的速度,并將速度設(shè)置為一個特定的值,該值由玩家移動的方向來確定。如果玩家需要向右移動,那么速度值為 x
軸上的 3像素。如果玩家需要向上移動,那么速度值為 y
軸上的 -3 像素。同樣,如果玩家需要向下移動,那么速度值即為y軸上的3像素。這非常簡單。另外,還需要注意如何在代碼的開始處重置速度值。如果玩家沒有按下任何按鍵,該語句將阻止玩家移動。
最后,還需要根據(jù)速度來更新玩家的 x
和 y
位置。現(xiàn)在你還看不到任何效果,但你已經(jīng)做好了在屏幕上繪制火箭的所有準備工作。
在剛才添加的代碼下面直接添加以下代碼:
context.fillStyle = 'rgb(255, 0, 0)'; context.beginPath(); context.moveTo(player.x + player.halfWidth, player.y); context.lineTo(player.x - player.halfWidth, player.y - player.halfHeight); context.lineTo(player.x - player.halfWidth, player.y + player.halfHeight) context.closePath(); context.fill();
你知道以上代碼的作用嗎?很明顯,你正在繪制一條填充路徑,但你能告訴我繪制的路徑是什么形狀嗎?是的,它只是一個三角形而已。
如果你仔細查看,會發(fā)現(xiàn)玩家對象的尺寸屬性的作用。知道了玩家對象的寬度值和高度值的一半,就可以構(gòu)建一個動態(tài)三角形,它能隨著尺寸值的變化變大或縮小。方法很簡單,但效果卻很好。
在瀏覽器中查看游戲的效果,應(yīng)該能夠看到玩家使用的火箭。
試著按下箭頭鍵??吹交鸺苿恿藛??現(xiàn)在的游戲效果已經(jīng)非常棒了。
這里可以只使用運動邏輯,但游戲會顯得有些單調(diào)。我們不妨在火箭上再添加一團閃動的火焰!在 Player
類的末尾添加以下代碼:
this.flameLength = 20;
以上代碼用于確定火焰的持續(xù)時間,稍后我們還需要添加更多代碼?,F(xiàn)在先在 animate
函數(shù)中繪制火箭的代碼前面添加以下代碼:
if (player.moveRight) { context.save(); context.translate(player.x - player.halfWidth, player.y); if (player.flameLength == 20) { player.flameLength = 15; } else { player.flameLength = 20; } context.fillStyle = "orange"; context.beginPath(); context.moveTo(0, -5); context.lineTo(-player.flameLength, 0); context.lineTo(0, 5); context.closePath(); context.fill(); context.restore(); }
條件語句用于確保只有當(dāng)玩家向右運動時才繪制火焰,因為如果在其他時間也能看到火焰,看上去就不符合常理了。
我們使用畫布的 translate
方法來繪制火焰,因為在后面調(diào)用 save
方法來保存畫布的繪圖狀態(tài)時,translate
方法可以節(jié)約一些時間。現(xiàn)在已經(jīng)存儲了繪圖上下文的原始狀態(tài),接下來就可以調(diào)用 translate
方法,并把 2D 繪圖上下文的原點移到玩家使用的火箭的左側(cè)。
現(xiàn)在已經(jīng)移動了畫布的原點,接下來的任務(wù)就非常簡單了。只需要對存儲在玩家對象的 flameLength
屬性中的值執(zhí)行循環(huán)(使火箭呈現(xiàn)閃爍效果),并把填充顏色改為橙色,然后從新的起點繪制一個長度與flameLength
屬性相同的三角形。最后還需要調(diào)用 restore
方法,將原始繪圖狀態(tài)恢復(fù)到畫布上。
刷新瀏覽器看看剛才的勞動成果。當(dāng)按下向右的箭頭鍵時,火箭上應(yīng)該出現(xiàn)了一團閃爍的火焰。
接下來需要做好準備,我們將使游戲產(chǎn)生一種逼真的橫向卷軸效果。
6. 假造橫向卷軸效果
雖然這個游戲看上去好像是橫向卷動的,但實際上你并沒有穿越在游戲世界中。相反,你將循環(huán)利用所有在屏幕上消失的對象,并讓它們重新顯示在屏幕的另一側(cè)。這樣就會產(chǎn)生一種始終穿越在永無止境的游戲世界中的效果。聽起來好像有些奇特,其實它只是一種橫向卷動效果而已。
6.1 循環(huán)利用小行星
讓游戲產(chǎn)生一種永無止境的穿越效果其實并不難。實際上非常簡單!在animate
函數(shù)中剛才繪制每顆小行星的代碼上面添加以下代碼:
if (tmpAsteroid.x + tmpAsteroid.radius < 0) { tmpAsteroid.radius = 5 + (Math.random() * 10); tmpAsteroid.x = canvasWidth + tmpAsteroid.radius; tmpAsteroid.y = Math.floor(Math.random() * canvasHeight); tmpAsteroid.vX = -5 - (Math.random() * 5); }
這點代碼就夠了。這段代碼的作用是檢查小行星是否移動到畫布的左邊界之外,如果是,則重置該小行星,并將它重新移回到畫布的右側(cè)。你已經(jīng)重新利用了該小行星,但它看上去卻像是一顆全新的小行星。
6.2 添加邊界
現(xiàn)在,玩家火箭可能會自由地在游戲中飛越,也可能會停止不動(試圖飛越畫布的右側(cè)時)。為了解決這個問題,需要在適當(dāng)?shù)奈恢迷O(shè)置一些邊界。在繪制火箭火焰的代碼上面(正好在設(shè)置新的玩家位置的代碼下面)添加以下代碼:
if (player.x - player.halfWidth < 20) { player.x = 20 + player.halfWidth; } else if (player.x + player.halfWidth > canvasWidth - 20) { player.x = canvasWidth - 20 - player.halfWidth; } if (player.y - player.halfHeight < 20) { player.y = 20 + player.halfHeight; } else if (player.y + player.halfHeight > canvasHeight - 20) { player.y = canvasHeight - 20 - player.halfHeight; }
你也許能猜出以上代碼的作用。它主要執(zhí)行一些標準的邊界檢查。這些檢查查看玩家是否位于畫布邊界 20 像素之內(nèi),如果是,則阻止它們沿著該方向進一步移動。我認為在畫布的邊界處預(yù)留 20 像素的空隙視覺效果更佳,但也可以把這個值再改小一點,以便玩家能夠向右移動到畫布的邊緣處。
6.3 讓玩家保持連續(xù)移動
目前,如果玩家沒有按下任何按鍵,火箭將停止移動。當(dāng)所有的小行星正在飄蕩時,火箭突然停止移動不太符合常理。因此可以在游戲中添加一些額外的運動,當(dāng)玩家不再向前移動時,可以讓它們繼續(xù)向后移動。
在 animate
函數(shù)中把改變玩家 vX
屬性的代碼段更換為以下代碼:
if (player.moveRight) { player.vX = 3; } else { player.vX = -3; }
這段代碼只是在條件語句中添加了一段額外代碼,即當(dāng)玩家不需要向右移動時,把玩家的 vX
屬性設(shè)為-3。你總結(jié)一下就會發(fā)現(xiàn),這與大部分游戲邏輯都是相同的。在瀏覽器中運行該游戲,現(xiàn)在的游戲看上去更逼真了!
7. 添加聲音
這也許是游戲中最酷的一部分。在游戲中添加一些簡單的聲音非常有趣,游戲也會變得更加引人入勝。你也許覺得在游戲中添加音頻是非常困難的,但使用 HTML5 音頻來實現(xiàn)卻是一件輕而易舉的事!下面我們來看看。
首先需要在游戲的 HTML 代碼中聲明所有的 HTML5 音頻元素。直接在index.html
文件中的 canvas
元素下面添加以下代碼:
<audio id="game-sound-background" loop> <source src="sounds/background.ogg"> <source src="sounds/background.mp3"> </audio> <audio id="game-sound-thrust" loop> <source src="sounds/thrust.ogg"> <source src="sounds/thrust.mp3"> </audio> <audio id="game-sound-death"> <source src="sounds/death.ogg"> <source src="sounds/death.mp3"> </audio>
如果你掌握了 HTML5 音頻部分的內(nèi)容,那么應(yīng)該對以上代碼非常熟悉了。如果你還沒有掌握該內(nèi)容,也不用著急,因為它非常簡單。這里聲明了 3 個獨立的 HTML5 audio
元素,并且為每個 audio
元素定義了一個唯一的 id
屬性,后面將用到這些 id
屬性。循環(huán)播放的聲音還需要定義一個 loop
屬性。
注意:并非所有的瀏覽器都支持 loop
屬性。由于它是規(guī)范的部分,因此越來越多的瀏覽器將會全面支持該屬性。如果需要采用一種變通的方案,可以在音頻播放結(jié)束時添加一個事件監(jiān)聽器,并再次播放。
這 3 種聲音都是背景音樂,火箭開始移動時使用推進器的聲音,最后玩家死亡時使用深沉的轟鳴聲。為了與大多數(shù)瀏覽器兼容,每種聲音都需要兩個版本的文件,因此也需要包含兩個 source
元素:一個是 mp3 版本,另一個是 ogg 版本。
在 HTML 文件中只需要完成這些任務(wù)就可以了,接下來我們回 JavaScript文件中,并在 JavaScript文件頂部的 uiScore
變量下面添加以下代碼:
const soundBackground = $("#game-sound-background").get(0); const soundThrust = $("#game-sound-thrust").get(0); const soundDeath = $("#game-sound-death").get(0);
這些變量使用 HTML 文件中聲明的 id
屬性來獲取每個audio
元素,這與在游戲中獲取 canvas
元素非常相似。接下來將使用這些變量訪問HTML5 音頻 API 并控制聲音。
這些內(nèi)容無需過多解釋,緊接著我們轉(zhuǎn)入 keydown
事件監(jiān)聽器中,在把playGame
設(shè)置為 true
的代碼后面添加以下代碼:
soundBackground.currentTime = 0; soundBackground.play();
現(xiàn)在,你已經(jīng)在游戲中添加了HTML5音頻,并且可以非常方便地控制它。很酷吧?以上代碼的作用是訪問與背景音樂相關(guān)的HTML5 audio
元素,并且可以通過HTML5音頻 API 直接控制它。因此,通過更改 currentTime
屬性,可以重置音頻文件播放的起始位置,另外,通過調(diào)用 play
方法,可以播放該音頻文件。真的很簡單!
載入并運行游戲,現(xiàn)在當(dāng)你開始移動火箭時,應(yīng)該能聽到一些美妙的背景音樂。
下一步是控制推進器的聲音(當(dāng)玩家移動火箭時)。我希望你已經(jīng)猜到了如何去實現(xiàn),其實這與實現(xiàn)背景音樂一樣簡單。
在 keydown
事件監(jiān)聽器中 player
對象的 moveRight
屬性設(shè)置代碼下面添加以下代碼:
if (soundThrust.paused) { soundThrust.currentTime = 0; soundThrust.play(); }
第一行代碼用于檢查是否正在播放推進器聲音,如果是,則禁止在游戲中再次播放它。這可以防止該聲音在播放的過程中被中途切斷,因為每秒鐘可能會觸發(fā)多次 keydown
事件,而你當(dāng)然也不希望每次觸發(fā) keydown
事件時都再次播放推進器聲音。
當(dāng)玩家停止移動時,你也許不希望推進器聲音繼續(xù)播放,為此,在 keyup
事件監(jiān)聽器中 player
對象的 moveRight
屬性設(shè)置代碼下面添加以下代碼:
soundThrust.pause();
就這么簡單,音頻 API 太方便了,通過它訪問和操縱音頻非常簡單。
在繼續(xù)下一步之前(下一節(jié)將添加死亡的聲音),我們還需要考慮一個問題:如果玩家重置游戲,我們需要如何確保停止播放聲音。為此,在 init
函數(shù)的 uiReset
,click
事件處理程序中的 startGame
調(diào)用上面添加以下代碼:
soundThrust.pause(); soundBackground.pause();
當(dāng)游戲重置時,以上兩行代碼可以確保停止播放推進器聲音和背景音樂。因為死亡的聲音不需要進行循環(huán),并且你希望在游戲結(jié)束時才播放它,所以暫時不需要考慮死亡的聲音。
8. 結(jié)束游戲
現(xiàn)在的游戲已經(jīng)逐漸成型了。實際上,它就快完成了。接下來唯一要做的就是實現(xiàn)某種計分系統(tǒng),并通過某種方法來結(jié)束游戲。首先解決計分系統(tǒng)問題,稍后介紹如何結(jié)束游戲。
8.1 計分系統(tǒng)
在游戲中,鑒于玩家試圖生存盡可能長的時間,所以把存活時間作為計分標準顯然是一個不錯想法。不是嗎?
我們需要通過某種方法來計算游戲從開始到現(xiàn)在所持續(xù)的時間。這正好是JavaScript 計時器的強項,但在構(gòu)建計時器之前需要聲明一些變量。在JavaScript 代碼頂部的 player
變量下面添加以下代碼:
let score; let scoreTimeout;
這些變量將用于存儲分數(shù)(已經(jīng)過去的秒數(shù))和對計時器操作的引用,以便根據(jù)需要來開始或停止計時器。
另外,在游戲開始或重置時也需要重新設(shè)置分數(shù)。為此,在 startGame
函數(shù)頂部的 numAsteroids
變量下面添加以下代碼:
score = 0;
為了便于管理得分計時器,我們創(chuàng)建一個名為 timer
的專用函數(shù)。在 animate
函數(shù)上面添加以下代碼:
function timer() { if (playGame) { scoreTimeout = setTimeout(() => { uiScore.html(++score); timer(); }, 1000); } }
以上代碼現(xiàn)在還不會起作用,但它會檢查游戲是否開始,如果游戲已經(jīng)開始,它就把計時器的時間間隔設(shè)置為 1 秒,并把該計時器賦給 scoreTimeout
變量。在計時器中,score
變量的值在增加,同時計分 UI 也在更新。然后,計時器自身將調(diào)用 timeout
函數(shù)來重復(fù)整個過程,這意味著游戲結(jié)束時計時器才會停止計時。
現(xiàn)在還沒有調(diào)用 timer
函數(shù),所以它還不會發(fā)揮作用。當(dāng)游戲開始時,需要調(diào)用該函數(shù),因此在 keydown
事件監(jiān)聽器中的 animate
函數(shù)調(diào)用下面添加以下代碼:
timer();
只要玩家開始游戲,以上代碼就會觸發(fā)計時器。在瀏覽器中查看效果,在游戲界面的左上角可以看到分數(shù)在不斷增加。
但遺憾的是,這里還存在一個問題——如果你重置游戲,分數(shù)有時候會顯示為 1 秒鐘。這是因為當(dāng)你重置游戲時,分數(shù)計時器仍然在運行,但它實際在你重置游戲之后才運行(將重置分數(shù)由 0 更改為 1 )。為了解決這個問題,需要在重置游戲時先清除計時器。幸運的是,JavaScript 有特定的函數(shù)可以實現(xiàn)該操作。
在 init
函數(shù)的 uiReset.click
事件監(jiān)聽器的 startGame
調(diào)用上面添加以下代碼:
clearTimeout(scoreTimeout);
顧名思義,以上代碼的作用顯而易見。通過這個獨立的函數(shù)可以獲取 scoreTime
變量中的分數(shù)計時器,并且阻止計時器的運行。再次運行游戲,你可以發(fā)現(xiàn)通過這行簡單的 JavaScript 代碼已經(jīng)成功解決了上面遇到的問題。
8.2 殺死玩家
如果小行星無法傷害你,那么躲避小行星就沒有任何意義了,因此我們需要添加一些功能來殺死玩家(當(dāng)玩家碰到小行星時)。
在這里發(fā)現(xiàn)明顯的問題了嗎?在火箭是三角形的情況下,你能執(zhí)行圓周碰撞檢測嗎?簡單地說,你不能執(zhí)行圓周碰撞檢測,或者說至少沒那么容易。但這里將忽略一些細節(jié)問題,也就是說,把玩家火箭的一小部分區(qū)域作為碰撞檢測區(qū)域。在實際中如果你幸運一些,這種檢測有助于躲避小行星。
為了簡化代碼,我認為這樣做是值得的。畢竟,這只是一個供娛樂的小游戲而已,因此不需要追求絕對的真實。
因此,只需在 animate
函數(shù)中繪制每顆小行星的代碼上面添加以下代碼即可:
const dx = player.x - tmpAsteroid.x; const dy = player.y - tmpAsteroid.y; const distance = Math.sqrt((dx * dx) + (dy * dy)); if (distance < player.halfWidth + tmpAsteroid.radius) { soundThrust.pause(); soundDeath.currentTime = 0; soundDeath.play(); // 游戲結(jié)束 playGame = false; clearTimeout(scoreTimeout); uiStats.hide(); uiComplete.show() soundBackground.pause(); $(window).unbind("keyup"); $(window).unbind("keydown"); }
你應(yīng)該能很快明白以上代碼中的距離計算方法。你將通過它們來計算玩家火箭與當(dāng)前循環(huán)中的小行星之間的像素距離。
下一步是判斷火箭是否與小行星發(fā)生碰撞,可以通過查看上面計算的像素距離是否小于小行星半徑加上火箭碰撞圓周的半徑之和。這里使用的火箭碰撞圓周的半徑是火箭寬度的一半,你也可以隨意改變它。
如果火箭與小行星發(fā)生碰撞,就要殺死玩家。殺死玩家并結(jié)束游戲的過程非常簡單,但我還需要逐行對它進行解釋。
前三行代碼停止播放推進器聲音,并重置和播放死亡的聲音。開始播放死亡的聲音時,需要把 playGame
設(shè)置為 false
來結(jié)束整個游戲,并通過前面已經(jīng)使用過的 clearTimeout
函數(shù)來停止計分計時器。
此時,所有的游戲邏輯都已經(jīng)停止,因此可以隱藏統(tǒng)計界面,并顯示游戲結(jié)束界面。
顯示游戲結(jié)束界面時,需要停止播放背景音樂,并最終釋放鍵盤事件處理程序,從而防止玩家由于無意按下某個按鍵而啟動游戲。
9. 增加游戲難度
好的,其實我們要創(chuàng)建的游戲還沒有完成。讓我們在游戲中再添加一些功能,即增加游戲的難度,玩家要想存活更長的時間就變得更加困難了。
我們還是直入主題吧,在 timer
函數(shù)中的 uiScore.html
下面添加以下代碼:
if (score % 5 === 0) { numAsteroids += 5; }
以上代碼看上去是否像一個普通的條件語句?其實不是。注意其中的百分比符號。它是求模運算符。求??梢杂嬎阋粋€數(shù)是否能被另一個數(shù)完全整除,它將返回兩個數(shù)相除所得的余數(shù)。
你可以通過求模計算來執(zhí)行周期性的操作,例如,每隔 5 秒發(fā)生一次。因為你可以把模 5 運算運用于某個數(shù),如果它返回的結(jié)果為 0,那么該數(shù)一定能夠被 5 整除。
在本游戲中,我們使用模 5 運算來確保某個代碼段每隔 5 秒鐘執(zhí)行一次。這個代碼段的作用是,每隔 5 秒鐘就向游戲中添加 5 顆小行星。實際上,這里并沒有增加小行星,增加的只是小行星的數(shù)目。
添加小行星很簡單,在 animate
函數(shù)中繪制玩家火箭的代碼下面添加以下代碼:
while (asteroids.length < numAsteroids) { const radius = 5 + (Math.random() * 10); const x = Math.floor(Math.random() * canvasWidth) + canvasWidth + radius; const y = Math.floor(Math.random() * canvasHeight); const vX = -5 - (Math.random() * 5); asteroids.push(new Asteroid(x, y, radius, vX)); }
以上代碼檢查每個循環(huán)中的小行星數(shù)目,如果數(shù)目沒有達到要求,它將繼續(xù)向游戲中添加新的小行星,直到小行星的數(shù)目達到要求為止。
再次在瀏覽器中啟動游戲,你會發(fā)現(xiàn),當(dāng)存活的時間越來越長時,游戲中的小行星就會越來越多?,F(xiàn)在,你已經(jīng)真正完成了游戲。
10. 完整源碼
下面給出該游戲的完整源碼:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Asteroid avoidance</title> <link rel="stylesheet" href="style.css" rel="external nofollow" rel="external nofollow" > <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="./main.js"></script> </head> <body> <div id="game"> <div id="game-ui"> <div id="game-intro"> <h1>Asteroid avoidance</h1> <p>Click play and then press any key to start.</p> <p> <a id="game-play" class="button" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Play</a> </p> </div> <div id="game-stats"> <p>Time:<span class="game-score"></span> </p> <p> <a class="game-reset" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Reset</a> </p> </div> <div id="game-complete"> <h1>Game over!</h1> <p>You survived for <span class="game-score"></span> seconds. </p> <p><a class="game-reset button" href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Play</a></p> </div> </div> <canvas id="game-canvas" width="800" height="600"> <!-- 在此處插入后備代碼 --> </canvas> <audio id="game-sound-background" loop> <source src="sounds/background.ogg"> <source src="sounds/background.mp3"> </audio> <audio id="game-sound-thrust" loop> <source src="sounds/thrust.ogg"> <source src="sounds/thrust.mp3"> </audio> <audio id="game-sound-death"> <source src="sounds/death.ogg"> <source src="sounds/death.mp3"> </audio> </div> </body> </html>
style.css
* { margin: 0; padding: 0; } html, body { height: 100%; width: 100%; } canvas { display: block; } body { background-color: #000; color: #fff; font-family: Verdana, Arial, sans-serif; font-size: 18px; height: 100%; } h1 { font-size: 30px; } p { margin: 0 20px; } a { color: #fff; text-decoration: none; } a:hover { text-decoration: underline; } a.button { background-color: #185da8; border-radius: 5px; display: block; font-size: 30px; margin: 40px 0 0 270px; padding: 10px; width: 200px; } a.button:hover { background-color: #2488f5; color: #fff; text-decoration: none; } #game { height: 600px; left: 50%; margin: -300px 0 0 -400px; position: relative; top: 50%; width: 800px; } #game-canvas { background-color: #001022; } #game-ui { height: 600px; position: absolute; width: 800px; } #game-intro, #game-complete { background-color: rgba(0, 0, 0, .5); margin-top: 100px; padding: 40px 0; text-align: center; } #game-stats { font-size: 14px; margin: 20px 0; } #game-stats, .game-reset { margin: 20px 20px 0 0; position: absolute; right: 0; top: 0; }
main.js
$(document).ready(function () { const canvas = $('#game-canvas'); const context = canvas.get(0).getContext("2d"); // 畫布尺寸 const canvasWidth = canvas.width(); const canvasHeight = canvas.height(); // 游戲設(shè)置 let playGame; let asteroids; let numAsteroids; let player; let score; let scoreTimeout; const arrowUp = 38; const arrowRight = 39; const arrowDown = 40; // 游戲UI const ui = $("#game-ui"); const uiIntro = $("#game-intro"); const uiStats = $("#game-stats"); const uiComplete = $("#game-complete"); const uiPlay = $("#game-play"); const uiReset = $(".game-reset"); const uiScore = $(".game-score"); const soundBackground = $("#game-sound-background").get(0); const soundThrust = $("#game-sound-thrust").get(0); const soundDeath = $("#game-sound-death").get(0); function Asteroid(x, y, radius, vX) { this.x = x; this.y = y; this.radius = radius; this.vX = vX; } function Player(x, y) { this.x = x; this.y = y; this.width = 24; this.height = 24; this.halfWidth = this.width / 2; this.halfHeight = this.height / 2; this.vX = 0; this.vY = 0; this.moveRight = false; this.moveUp = false; this.moveDown = false; this.flameLength = 20; } // 重至和啟動游戲 function startGame() { // 重置游戲狀態(tài) uiScore.html("0"); uiStats.show(); // 初始游戲設(shè)置 playGame = false; asteroids = new Array(); numAsteroids = 10; score = 0; player = new Player(150, canvasHeight / 2); for (let i = 0; i < numAsteroids; i++) { const radius = 5 + (Math.random() * 10); const x = canvasWidth + radius + Math.floor(Math.random() * canvasWidth); const y = Math.floor(Math.random() * canvasHeight); const vX = -5 - (Math.random() * 5); asteroids.push(new Asteroid(x, y, radius, vX)); } $(window).keydown(e => { const keyCode = e.keyCode; if (!playGame) { playGame = true; soundBackground.currentTime = 0; soundBackground.play(); animate(); timer(); } if (keyCode == arrowRight) { player.moveRight = true; if (soundThrust.paused) { soundThrust.currentTime = 0; soundThrust.play(); } } else if (keyCode == arrowUp) { player.moveUp = true; } else if (keyCode == arrowDown) { player.moveDown = true; } }); $(window).keyup(e => { const keyCode = e.keyCode; if (keyCode == arrowRight) { player.moveRight = false; soundThrust.pause(); } else if (keyCode == arrowUp) { player.moveUp = false; } else if (keyCode == arrowDown) { player.moveDown = false; } }); // 開始動畫糖環(huán) animate(); } //初始化游戲環(huán)境 function init() { uiStats.hide(); uiComplete.hide(); uiPlay.click(function (e) { e.preventDefault(); uiIntro.hide(); startGame(); }); uiReset.click(function (e) { e.preventDefault(); uiComplete.hide(); $(window).unbind('keyup'); $(window).unbind('keydown'); soundThrust.pause(); soundBackground.pause(); clearTimeout(scoreTimeout); startGame(); }); } function timer() { if (playGame) { scoreTimeout = setTimeout(() => { uiScore.html(++score); if (score % 5 === 0) { numAsteroids += 5; } timer(); }, 1000); } } // 動畫循環(huán),游戲的嫌味性就在這里 function animate() { // 清除 context.clearRect(0, 0, canvasWidth, canvasHeight); const asteroidsLength = asteroids.length; for (let i = 0; i < asteroidsLength; i++) { const tmpAsteroid = asteroids[i]; tmpAsteroid.x += tmpAsteroid.vX; if (tmpAsteroid.x + tmpAsteroid.radius < 0) { tmpAsteroid.radius = 5 + (Math.random() * 10); tmpAsteroid.x = canvasWidth + tmpAsteroid.radius; tmpAsteroid.y = Math.floor(Math.random() * canvasHeight); tmpAsteroid.vX = -5 - (Math.random() * 5); } const dx = player.x - tmpAsteroid.x; const dy = player.y - tmpAsteroid.y; const distance = Math.sqrt((dx * dx) + (dy * dy)); if (distance < player.halfWidth + tmpAsteroid.radius) { soundThrust.pause(); soundDeath.currentTime = 0; soundDeath.play(); // 游戲結(jié)束 playGame = false; clearTimeout(scoreTimeout); uiStats.hide(); uiComplete.show() soundBackground.pause(); $(window).unbind("keyup"); $(window).unbind("keydown"); } context.fillStyle = "rgb(255, 255, 255)"; context.beginPath(); context.arc(tmpAsteroid.x, tmpAsteroid.y, tmpAsteroid.radius, 0, Math.PI * 2, true); context.closePath(); context.fill(); } while (asteroids.length < numAsteroids) { const radius = 5 + (Math.random() * 10); const x = Math.floor(Math.random() * canvasWidth) + canvasWidth + radius; const y = Math.floor(Math.random() * canvasHeight); const vX = -5 - (Math.random() * 5); asteroids.push(new Asteroid(x, y, radius, vX)); } if (player.moveRight) { player.vX = 3; } else { player.vX = -3; } player.vY = 0; if (player.moveRight) { player.vX = 3; context.save(); context.translate(player.x - player.halfWidth, player.y); if (player.flameLength == 20) { player.flameLength = 15; } else { player.flameLength = 20; } context.fillStyle = "orange"; context.beginPath(); context.moveTo(0, -5); context.lineTo(-player.flameLength, 0); context.lineTo(0, 5); context.closePath(); context.fill(); context.restore(); } if (player.moveUp) { player.vY = -3; } if (player.moveDown) { player.vY = 3; } player.x += player.vX; player.y += player.vY; if (player.x - player.halfWidth < 20) { player.x = 20 + player.halfWidth; } else if (player.x + player.halfWidth > canvasWidth - 20) { player.x = canvasWidth - 20 - player.halfWidth; } if (player.y - player.halfHeight < 20) { player.y = 20 + player.halfHeight; } else if (player.y + player.halfHeight > canvasHeight - 20) { player.y = canvasHeight - 20 - player.halfHeight; } context.fillStyle = 'rgb(255, 0, 0)'; context.beginPath(); context.moveTo(player.x + player.halfWidth, player.y); context.lineTo(player.x - player.halfWidth, player.y - player.halfHeight); context.lineTo(player.x - player.halfWidth, player.y + player.halfHeight) context.closePath(); context.fill(); if (playGame) { setTimeout(animate, 33); } } init(); });
到此這篇關(guān)于JavaScript躲避行星游戲?qū)崿F(xiàn)全程的文章就介紹到這了,更多相關(guān)JS躲避行星內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
momentjs獲取上周、上月、前三個月的起始和結(jié)束時間(附完整代碼)
這篇文章主要給大家介紹了關(guān)于momentjs獲取上周、上月、前三個月的起始和結(jié)束時間的相關(guān)資料,在需要你進行日期處理的地方,必然少不了moment.js的使用,需要的朋友可以參考下2023-07-07