JavaScript躲避行星游戲?qū)崿F(xiàn)全程
1. 游戲概述
顧名思義,躲避小行星游戲的目標(biāo)是非常明顯的:當(dāng)小行星向你沖來(lái)時(shí),讓火箭飛行和生存的時(shí)間盡可能長(zhǎng)一些(如圖91所示)。如果你碰上某顆小行星,游戲?qū)⒔Y(jié)束,游戲的分?jǐn)?shù)是通過(guò)火箭生存的時(shí)間來(lái)計(jì)算的。
躲避小行星游戲是一個(gè)“橫向卷軸式”游戲,或者說(shuō)它至少類似于這樣的游戲,將會(huì)側(cè)重于動(dòng)態(tài)場(chǎng)景。
2. 核心功能
在創(chuàng)建游戲之前,首先需要構(gòu)建一些基本框架。就創(chuàng)建躲避小行星游戲而言,這些框架就是基本的HTML、CSS以及JavaScript代碼(作為將來(lái)要添加的高級(jí)代碼的基礎(chǔ))。
2.1 構(gòu)建 HTML 代碼
在瀏覽器中創(chuàng)建游戲的優(yōu)點(diǎn)在于可以使用一些構(gòu)建網(wǎng)站的常用技術(shù)。也就是說(shuō),可以使用 HTML 語(yǔ)言來(lái)創(chuàng)建游戲的用戶界面(UI)?,F(xiàn)在的界面看上去不太美觀,這是因?yàn)槲覀冞€沒(méi)有使用 CSS 來(lái)設(shè)計(jì)用戶界面的樣式,但目前內(nèi)容的原始結(jié)構(gòu)是最重要的。
在你的計(jì)算機(jī)上為該游戲創(chuàng)建一個(gè)新目錄,新建一個(gè)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>
我不打算過(guò)多解釋這些 HTML 代碼,因?yàn)樗鼈儽容^簡(jiǎn)單,但你只要知道這就是游戲所需的所有標(biāo)記即可。

2.2 美化界面
創(chuàng)建一個(gè)名為 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 編寫(xiě) JavaScript 代碼
在添加一些有趣的游戲邏輯之前,首先需要用JavaScript實(shí)現(xiàn)核心功能。創(chuàng)建一個(gè)名為 main.js 的新文件,并把它和 HTML 文件放在相同的目錄下。在該 js 文件中插入以下代碼:
$(document).ready(function () {
const canvas = $('#game-canvas');
const context = canvas.get(0).getContext("2d");
// 畫(huà)布尺寸
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");
// 重至和啟動(dòng)游戲
function startGame() {
// 重置游戲狀態(tài)
uiScore.html("0");
uiStats.show();
// 初始游戲設(shè)置
playGame = false;
// 開(kāi)始動(dòng)畫(huà)糖環(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();
});
}
// 動(dòng)畫(huà)循環(huán),游戲的嫌味性就在這里
function animate() {
// 清除
context.clearRect(0, 0, canvasWidth, canvasHeight);
if (playGame) {
setTimeout(animate, 33);
}
}
init();
});在你最喜歡的瀏覽器中運(yùn)行該游戲,應(yīng)該會(huì)看到一個(gè)更加美觀的 UI。另外,你還可以單擊 Play 按鈕來(lái)顯示游戲的主窗口,盡管它看上去也許還有些單調(diào)。


3. 創(chuàng)建游戲?qū)ο?/h2>
躲避小行星游戲使用兩個(gè)主要對(duì)象:小行星和玩家使用的火箭。我們將使用 JavaScript 類來(lái)創(chuàng)建這些對(duì)象。你也許會(huì)覺(jué)得奇怪,既然玩家只有一個(gè),為什么還要通過(guò)一個(gè)類來(lái)定義它呢?簡(jiǎn)而言之,如果你以后需要在游戲中添加多個(gè)玩家,通過(guò)類創(chuàng)建玩家就會(huì)更容易一些。
3.1 創(chuàng)建小行星
通過(guò)類創(chuàng)建游戲?qū)ο笠馕吨憧梢栽谄渌螒蛑蟹浅7奖愕刂赜煤透淖兯鼈兊挠猛尽?/p>
第一步是聲明主要變量,我們將使用這些變量來(lái)存儲(chǔ)所有的小行星。同時(shí),還需要聲明另外一個(gè)變量,用于計(jì)算游戲中應(yīng)該存在的小行星數(shù)目。在 JavaScript 代碼頂部的 playGame 變量下面添加以下代碼:
let asteroids; let numAsteroids;
稍后你將會(huì)為這些變量賦值,但現(xiàn)在我們只建立小行星類。在startGame函數(shù)上面添加以下代碼:
function Asteroid(x, y, radius, vX) {
this.x = x;
this.y = y;
this.radius = radius;
this.vX = vX;
}
這里存在一個(gè)速度屬性,這是因?yàn)樾⌒行侵恍枰獜挠蚁蜃筮\(yùn)動(dòng),即只需要 x 速度。這里不需要 y 速度,所以就省略了。
在開(kāi)始創(chuàng)建所有小行星之前,需要建立數(shù)組來(lái)存儲(chǔ)這些小行星,并聲明實(shí)際需要使用的小行星數(shù)目。在startGame函數(shù)中的PlayGame變量下面添加以下代碼:
asteroids = new Array(); numAsteroids = 10;
你也許認(rèn)為 10 個(gè)小行星是一個(gè)很小的數(shù)目,但是當(dāng)這些小行星在屏幕上消失時(shí),你將重復(fù)使用它們,所以在游戲中你實(shí)際看到的小行星數(shù)目可以有無(wú)窮多個(gè)。你可以把這里的小行星數(shù)目看做屏幕上某一時(shí)刻可能出現(xiàn)的小行星總數(shù)。
創(chuàng)建小行星的過(guò)程其實(shí)就是一個(gè)創(chuàng)建循環(huán)的過(guò)程,循環(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è)為一個(gè)介于 5 到 15 像素之間的隨機(jī)數(shù)( 5 加上一個(gè)介于 0 到 10 之間的隨機(jī)數(shù))。雖然 x 速度的值介于 -5 到 -10 之間,但你也可以采用同樣的方法來(lái)設(shè)置它( -5 減去一個(gè) 0 到 5 之間的數(shù))。因?yàn)槟阈枰屝⌒行前磸挠蚁蜃蟮姆较蜻\(yùn)動(dòng),所以使用的是一個(gè)負(fù)的速度值,這說(shuō)明 x 的位置將隨著時(shí)間的推移而減小。
計(jì)算每顆小行星的 x 位置看上去有些復(fù)雜,但其實(shí)非常簡(jiǎn)單。在開(kāi)始啟動(dòng)游戲的時(shí)候,如果讓所有的小行星全部顯示在屏幕上,就讓人覺(jué)得有些太奇怪了。因此在游戲開(kāi)始之前,最好把小行星放在屏幕的右側(cè),當(dāng)游戲開(kāi)始時(shí)才讓它們按從右向左的順序穿過(guò)屏幕。
為此,首先需要把 x 位置設(shè)為 canvas 元素的寬度,然后加上小行星的半徑。這意味著如果你現(xiàn)在畫(huà)出小行星,那么它應(yīng)該位于屏幕的右側(cè)。如果僅僅這樣做,那么所有的小行星將會(huì)排成一行,所以下一步我們需要把 x 位置加上一個(gè)介于 0 到畫(huà)布寬度之間的隨機(jī)值。與 x 位置相比,y 位置簡(jiǎn)單一些,它只是一個(gè)介于 0 到畫(huà)布高度之間的隨機(jī)值。
這樣可以產(chǎn)生一個(gè)與畫(huà)布尺寸相同的方框,方框中隨機(jī)分布著一些小行星。當(dāng)游戲開(kāi)始時(shí),這些小行星將穿過(guò)可見(jiàn)的畫(huà)布。
最后一步是把一個(gè)新的小行星推送到數(shù)組中,做好移動(dòng)和繪制小行星的準(zhǔn)備。
3.2 創(chuàng)建玩家使用的火箭
首先聲明用于建立玩家的初始化變量。在 JavaScript 頂部的 numAsteroids 變量下面添加以下代碼:
let player;
該變量將用于存儲(chǔ)玩家對(duì)象的引用,但現(xiàn)在我們還沒(méi)有定義玩家對(duì)象。在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)該熟悉以上代碼的某些部分,例如位置和速度屬性。其余屬性用于描述玩家使用的火箭的尺寸,包括整個(gè)尺寸和一半的尺寸。繪制火箭和執(zhí)行碰撞檢測(cè)時(shí),你需要使用這些尺寸。
最后一步是創(chuàng)建一個(gè)新的玩家對(duì)象。為此,在 startGame 函數(shù)中的numAsteroids變量下面添加以下對(duì)象:
player = new Player(150, canvasHeight / 2);
通過(guò)以上代碼,玩家的位置將垂直居中,并且距離畫(huà)布左邊界 150 像素。
現(xiàn)在還不能看到任何效果,稍后當(dāng)你開(kāi)始著手移動(dòng)所有的游戲?qū)ο髸r(shí),將會(huì)從視覺(jué)上看到這種效果。
4. 檢測(cè)鍵盤(pán)輸入
本游戲?qū)⑹褂面I盤(pán)來(lái)控制游戲。更確切地說(shuō),你將使用方向鍵來(lái)四處移動(dòng)玩家使用的火箭。如何才能實(shí)現(xiàn)這種控制呢?這比控制鼠標(biāo)輸入更難嗎?不,其實(shí)非常簡(jiǎn)單。下面我來(lái)教你怎么做。
4.1 鍵值
在處理鍵盤(pán)輸人時(shí),首先需要知道哪一個(gè)按鍵被按下了。在 JavaScript 中,普通鍵盤(pán)上的每一個(gè)按鍵都分配了一個(gè)特定的鍵值(key code)。通過(guò)這些鍵值,可以唯一確定按下或釋放了哪個(gè)鍵。稍后你將學(xué)習(xí)如何使用鍵值,現(xiàn)在我們首先需要了解每個(gè)按鍵所對(duì)應(yīng)的數(shù)值。
例如,鍵 a 到 z (無(wú)論在什么情況下)對(duì)應(yīng)的鍵值分別是從 65 到 90 。箭頭鍵對(duì)應(yīng)的鍵值是從 37 到 40,其中左箭頭的鍵值是 37、上箭頭的鍵值是 38、右箭頭的鍵值是 39、下箭頭的鍵值是 40??崭矜I的鍵值是 32。
在躲避小行星游戲中,你需要重點(diǎn)關(guān)注的是箭頭鍵,因此在 JavaScript 代碼頂部的 player 變量下面添加以下代碼:
const arrowUp = 38; const arrowRight = 39; const arrowDown = 40;
以上代碼為每個(gè)箭頭對(duì)應(yīng)的鍵值分別分配了一個(gè)變量。這種方法稱作枚舉(enumeration),它是對(duì)值進(jìn)行命名的過(guò)程。這主要是為后面的工作提供方便,因?yàn)橥ㄟ^(guò)這些名稱你能很容易確定變量引用的是哪個(gè)箭頭鍵。
請(qǐng)注意,為什么沒(méi)有為左箭頭聲明一個(gè)變量呢?因?yàn)槟悴粫?huì)手動(dòng)地讓玩家向后移動(dòng)。相反,當(dāng)玩家沒(méi)有按任何按鍵時(shí),就會(huì)表現(xiàn)為向后移動(dòng)的狀態(tài)。稍后你就會(huì)明白其中的道理。
4.2 鍵盤(pán)事件
在向游戲中添加鍵盤(pán)交互效果之前,首先需要確定玩家在何時(shí)按下或釋放某個(gè)按鍵。為此,需要使用 keydown 和 keyup 事件監(jiān)聽(tīng)器。
在 startGame 函數(shù)中的 animate 函數(shù)調(diào)用上面(在創(chuàng)建所有小行星的循環(huán)下面)添加以下代碼:
$(window).keydown(e => {
});
$(window).keyup(e => {
});按下某個(gè)按鍵時(shí)將觸發(fā)第一個(gè)監(jiān)聽(tīng)器,釋放某個(gè)按鍵時(shí)將觸發(fā)第二個(gè)監(jiān)聽(tīng)器。非常簡(jiǎn)單。稍后我們將在這些事件監(jiān)聽(tīng)器中添加一些有用的代碼,但首先需要在重新設(shè)置游戲時(shí)刪除這些監(jiān)聽(tīng)器,這能防止玩家由于無(wú)意按下某個(gè)按鍵而啟動(dòng)游戲。在 uiReset.click 事件監(jiān)聽(tīng)器中的 startGame 調(diào)用上面添加以下代碼:
$(window).unbind('keyup');
$(window).unbind('keydown');
接下來(lái),還需要添加一些在激活玩家后用到的屬性。在 Player 類的末尾添加以下代碼:
this.moveRight = false; this.moveUp = false; this.moveDown = false;
通過(guò)這些屬性,你可以知道玩家的移動(dòng)方向,這些屬性值的設(shè)置取決于玩家按下了哪個(gè)按鍵。現(xiàn)在你是不是已經(jīng)理解了其中的所有道理呢?
最后,需要向鍵盤(pán)事件監(jiān)聽(tīng)器中添加一些邏輯。首先,在 keydown 事件監(jiān)聽(tīng)器中添加以下代碼:
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)聽(tīng)器中添加以下代碼:
const keyCode = e.keyCode;
if (keyCode == arrowRight) {
player.moveRight = false;
} else if (keyCode == arrowUp) {
player.moveUp = false;
} else if (keyCode == arrowDown) {
player.moveDown = false;
}
以上代碼的作用非常明顯,但我還需要作一些說(shuō)明。在兩個(gè)監(jiān)聽(tīng)器中,第一行的作用都是把按鍵的鍵值賦給一個(gè)變量。然后在一組檢查語(yǔ)句中使用該鍵值來(lái)判斷是否按下了某個(gè)箭頭鍵,如果按下了箭頭鍵,判斷是哪個(gè)箭頭鍵。這樣,我們就可以啟動(dòng)(如果按下了該鍵)或禁用(如果釋放了該鍵)玩家對(duì)象的對(duì)應(yīng)屬性。
例如,如果按下了向右的箭頭鍵,那么玩家對(duì)象的 moveRight 屬性將被設(shè)為 true。如果釋放了該方向鍵,則 moveRight 屬性將被設(shè)為false。
**注意:**如果玩家一直按住某個(gè)按鍵,那么將觸發(fā)多個(gè) keydown 事件。因此,代碼要具備處理多個(gè)被觸發(fā)的 keydown 事件的能力,這一,點(diǎn)非常重要。在每個(gè) keydown 事件之后不一定總是一個(gè) keyup 事件,另外還要注意的是,在 keydown 事件監(jiān)聽(tīng)器中是如何通過(guò)一個(gè)條件語(yǔ)句來(lái)查看游戲當(dāng)前是否正在進(jìn)行的。如果玩家沒(méi)有做好游戲準(zhǔn)備,該語(yǔ)句將阻止游戲運(yùn)行。只有玩家按下鍵盤(pán)上的某個(gè)鍵時(shí),才會(huì)啟動(dòng)游戲。方法很簡(jiǎn)單,但卻非常有效。
游戲中的鍵盤(pán)輸入非常多,我們不可能一一列舉。在下一節(jié)中,我們將通過(guò)這些輸入來(lái)控制玩家沿著正確的方向運(yùn)動(dòng)。
5. 讓對(duì)象運(yùn)動(dòng)起來(lái)
現(xiàn)在你已經(jīng)做好了實(shí)現(xiàn)游戲?qū)ο髣?dòng)畫(huà)的所有準(zhǔn)備。當(dāng)你實(shí)際看到游戲效果時(shí),這一切會(huì)變得更有趣。
第一步是更新所有游戲?qū)ο蟮奈恢?。我們從更新小行星?duì)象的位置開(kāi)始,在 animate 函數(shù)中
畫(huà)布的 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();
}
這些代碼非常簡(jiǎn)單。主要是遍歷每一顆小行星,并根據(jù)速度來(lái)更新它的位置,然后在畫(huà)布上繪制小行星。
刷新瀏覽器查看效果(記住按下某個(gè)按鍵啟動(dòng)游戲)。應(yīng)該能夠看到某顆小行星帶穿越屏幕的場(chǎng)景。

注意它們是如何消失在屏幕左側(cè)的。下一節(jié)將學(xué)習(xí)如何在橫向滾動(dòng)的屏幕上阻止它們的運(yùn)動(dòng)。
迄今為止,假設(shè)這些小行星都實(shí)現(xiàn)了我們的預(yù)期效果。接下來(lái)還需要更新并顯示玩家!
在 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è)置為一個(gè)特定的值,該值由玩家移動(dòng)的方向來(lái)確定。如果玩家需要向右移動(dòng),那么速度值為 x 軸上的 3像素。如果玩家需要向上移動(dòng),那么速度值為 y 軸上的 -3 像素。同樣,如果玩家需要向下移動(dòng),那么速度值即為y軸上的3像素。這非常簡(jiǎn)單。另外,還需要注意如何在代碼的開(kāi)始處重置速度值。如果玩家沒(méi)有按下任何按鍵,該語(yǔ)句將阻止玩家移動(dòng)。
最后,還需要根據(jù)速度來(lái)更新玩家的 x 和 y 位置?,F(xiàn)在你還看不到任何效果,但你已經(jīng)做好了在屏幕上繪制火箭的所有準(zhǔn)備工作。
在剛才添加的代碼下面直接添加以下代碼:
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();
你知道以上代碼的作用嗎?很明顯,你正在繪制一條填充路徑,但你能告訴我繪制的路徑是什么形狀嗎?是的,它只是一個(gè)三角形而已。
如果你仔細(xì)查看,會(huì)發(fā)現(xiàn)玩家對(duì)象的尺寸屬性的作用。知道了玩家對(duì)象的寬度值和高度值的一半,就可以構(gòu)建一個(gè)動(dòng)態(tài)三角形,它能隨著尺寸值的變化變大或縮小。方法很簡(jiǎn)單,但效果卻很好。
在瀏覽器中查看游戲的效果,應(yīng)該能夠看到玩家使用的火箭。

試著按下箭頭鍵??吹交鸺苿?dòng)了嗎?現(xiàn)在的游戲效果已經(jīng)非常棒了。
這里可以只使用運(yùn)動(dòng)邏輯,但游戲會(huì)顯得有些單調(diào)。我們不妨在火箭上再添加一團(tuán)閃動(dòng)的火焰!在 Player 類的末尾添加以下代碼:
this.flameLength = 20;
以上代碼用于確定火焰的持續(xù)時(shí)間,稍后我們還需要添加更多代碼?,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();
}條件語(yǔ)句用于確保只有當(dāng)玩家向右運(yùn)動(dòng)時(shí)才繪制火焰,因?yàn)槿绻谄渌麜r(shí)間也能看到火焰,看上去就不符合常理了。
我們使用畫(huà)布的 translate 方法來(lái)繪制火焰,因?yàn)樵诤竺嬲{(diào)用 save 方法來(lái)保存畫(huà)布的繪圖狀態(tài)時(shí),translate 方法可以節(jié)約一些時(shí)間?,F(xiàn)在已經(jīng)存儲(chǔ)了繪圖上下文的原始狀態(tài),接下來(lái)就可以調(diào)用 translate 方法,并把 2D 繪圖上下文的原點(diǎn)移到玩家使用的火箭的左側(cè)。
現(xiàn)在已經(jīng)移動(dòng)了畫(huà)布的原點(diǎn),接下來(lái)的任務(wù)就非常簡(jiǎn)單了。只需要對(duì)存儲(chǔ)在玩家對(duì)象的 flameLength 屬性中的值執(zhí)行循環(huán)(使火箭呈現(xiàn)閃爍效果),并把填充顏色改為橙色,然后從新的起點(diǎn)繪制一個(gè)長(zhǎng)度與flameLength屬性相同的三角形。最后還需要調(diào)用 restore 方法,將原始繪圖狀態(tài)恢復(fù)到畫(huà)布上。
刷新瀏覽器看看剛才的勞動(dòng)成果。當(dāng)按下向右的箭頭鍵時(shí),火箭上應(yīng)該出現(xiàn)了一團(tuán)閃爍的火焰。

接下來(lái)需要做好準(zhǔn)備,我們將使游戲產(chǎn)生一種逼真的橫向卷軸效果。
6. 假造橫向卷軸效果
雖然這個(gè)游戲看上去好像是橫向卷動(dòng)的,但實(shí)際上你并沒(méi)有穿越在游戲世界中。相反,你將循環(huán)利用所有在屏幕上消失的對(duì)象,并讓它們重新顯示在屏幕的另一側(cè)。這樣就會(huì)產(chǎn)生一種始終穿越在永無(wú)止境的游戲世界中的效果。聽(tīng)起來(lái)好像有些奇特,其實(shí)它只是一種橫向卷動(dòng)效果而已。
6.1 循環(huán)利用小行星
讓游戲產(chǎn)生一種永無(wú)止境的穿越效果其實(shí)并不難。實(shí)際上非常簡(jiǎ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);
}這點(diǎn)代碼就夠了。這段代碼的作用是檢查小行星是否移動(dòng)到畫(huà)布的左邊界之外,如果是,則重置該小行星,并將它重新移回到畫(huà)布的右側(cè)。你已經(jīng)重新利用了該小行星,但它看上去卻像是一顆全新的小行星。
6.2 添加邊界
現(xiàn)在,玩家火箭可能會(huì)自由地在游戲中飛越,也可能會(huì)停止不動(dòng)(試圖飛越畫(huà)布的右側(cè)時(shí))。為了解決這個(gè)問(wèn)題,需要在適當(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í)行一些標(biāo)準(zhǔn)的邊界檢查。這些檢查查看玩家是否位于畫(huà)布邊界 20 像素之內(nèi),如果是,則阻止它們沿著該方向進(jìn)一步移動(dòng)。我認(rèn)為在畫(huà)布的邊界處預(yù)留 20 像素的空隙視覺(jué)效果更佳,但也可以把這個(gè)值再改小一點(diǎn),以便玩家能夠向右移動(dòng)到畫(huà)布的邊緣處。
6.3 讓玩家保持連續(xù)移動(dòng)
目前,如果玩家沒(méi)有按下任何按鍵,火箭將停止移動(dòng)。當(dāng)所有的小行星正在飄蕩時(shí),火箭突然停止移動(dòng)不太符合常理。因此可以在游戲中添加一些額外的運(yùn)動(dòng),當(dāng)玩家不再向前移動(dòng)時(shí),可以讓它們繼續(xù)向后移動(dòng)。
在 animate 函數(shù)中把改變玩家 vX 屬性的代碼段更換為以下代碼:
if (player.moveRight) {
player.vX = 3;
} else {
player.vX = -3;
}這段代碼只是在條件語(yǔ)句中添加了一段額外代碼,即當(dāng)玩家不需要向右移動(dòng)時(shí),把玩家的 vX 屬性設(shè)為-3。你總結(jié)一下就會(huì)發(fā)現(xiàn),這與大部分游戲邏輯都是相同的。在瀏覽器中運(yùn)行該游戲,現(xiàn)在的游戲看上去更逼真了!
7. 添加聲音
這也許是游戲中最酷的一部分。在游戲中添加一些簡(jiǎn)單的聲音非常有趣,游戲也會(huì)變得更加引人入勝。你也許覺(jué)得在游戲中添加音頻是非常困難的,但使用 HTML5 音頻來(lái)實(shí)現(xiàn)卻是一件輕而易舉的事!下面我們來(lái)看看。
首先需要在游戲的 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)該對(duì)以上代碼非常熟悉了。如果你還沒(méi)有掌握該內(nèi)容,也不用著急,因?yàn)樗浅:?jiǎn)單。這里聲明了 3 個(gè)獨(dú)立的 HTML5 audio 元素,并且為每個(gè) audio 元素定義了一個(gè)唯一的 id 屬性,后面將用到這些 id 屬性。循環(huán)播放的聲音還需要定義一個(gè) loop 屬性。
注意:并非所有的瀏覽器都支持 loop 屬性。由于它是規(guī)范的部分,因此越來(lái)越多的瀏覽器將會(huì)全面支持該屬性。如果需要采用一種變通的方案,可以在音頻播放結(jié)束時(shí)添加一個(gè)事件監(jiān)聽(tīng)器,并再次播放。
這 3 種聲音都是背景音樂(lè),火箭開(kāi)始移動(dòng)時(shí)使用推進(jìn)器的聲音,最后玩家死亡時(shí)使用深沉的轟鳴聲。為了與大多數(shù)瀏覽器兼容,每種聲音都需要兩個(gè)版本的文件,因此也需要包含兩個(gè) source 元素:一個(gè)是 mp3 版本,另一個(gè)是 ogg 版本。
在 HTML 文件中只需要完成這些任務(wù)就可以了,接下來(lái)我們回 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 屬性來(lái)獲取每個(gè)audio元素,這與在游戲中獲取 canvas 元素非常相似。接下來(lái)將使用這些變量訪問(wèn)HTML5 音頻 API 并控制聲音。
這些內(nèi)容無(wú)需過(guò)多解釋,緊接著我們轉(zhuǎn)入 keydown 事件監(jiān)聽(tīng)器中,在把playGame設(shè)置為 true 的代碼后面添加以下代碼:
soundBackground.currentTime = 0; soundBackground.play();
現(xiàn)在,你已經(jīng)在游戲中添加了HTML5音頻,并且可以非常方便地控制它。很酷吧?以上代碼的作用是訪問(wèn)與背景音樂(lè)相關(guān)的HTML5 audio元素,并且可以通過(guò)HTML5音頻 API 直接控制它。因此,通過(guò)更改 currentTime 屬性,可以重置音頻文件播放的起始位置,另外,通過(guò)調(diào)用 play 方法,可以播放該音頻文件。真的很簡(jiǎn)單!
載入并運(yùn)行游戲,現(xiàn)在當(dāng)你開(kāi)始移動(dòng)火箭時(shí),應(yīng)該能聽(tīng)到一些美妙的背景音樂(lè)。
下一步是控制推進(jìn)器的聲音(當(dāng)玩家移動(dòng)火箭時(shí))。我希望你已經(jīng)猜到了如何去實(shí)現(xiàn),其實(shí)這與實(shí)現(xiàn)背景音樂(lè)一樣簡(jiǎn)單。
在 keydown 事件監(jiān)聽(tīng)器中 player 對(duì)象的 moveRight 屬性設(shè)置代碼下面添加以下代碼:
if (soundThrust.paused) {
soundThrust.currentTime = 0;
soundThrust.play();
}
第一行代碼用于檢查是否正在播放推進(jìn)器聲音,如果是,則禁止在游戲中再次播放它。這可以防止該聲音在播放的過(guò)程中被中途切斷,因?yàn)槊棵腌娍赡軙?huì)觸發(fā)多次 keydown 事件,而你當(dāng)然也不希望每次觸發(fā) keydown 事件時(shí)都再次播放推進(jìn)器聲音。
當(dāng)玩家停止移動(dòng)時(shí),你也許不希望推進(jìn)器聲音繼續(xù)播放,為此,在 keyup 事件監(jiān)聽(tīng)器中 player 對(duì)象的 moveRight 屬性設(shè)置代碼下面添加以下代碼:
soundThrust.pause();
就這么簡(jiǎn)單,音頻 API 太方便了,通過(guò)它訪問(wèn)和操縱音頻非常簡(jiǎn)單。
在繼續(xù)下一步之前(下一節(jié)將添加死亡的聲音),我們還需要考慮一個(gè)問(wèn)題:如果玩家重置游戲,我們需要如何確保停止播放聲音。為此,在 init 函數(shù)的 uiReset,click 事件處理程序中的 startGame 調(diào)用上面添加以下代碼:
soundThrust.pause(); soundBackground.pause();
當(dāng)游戲重置時(shí),以上兩行代碼可以確保停止播放推進(jìn)器聲音和背景音樂(lè)。因?yàn)樗劳龅穆曇舨恍枰M(jìn)行循環(huán),并且你希望在游戲結(jié)束時(shí)才播放它,所以暫時(shí)不需要考慮死亡的聲音。
8. 結(jié)束游戲
現(xiàn)在的游戲已經(jīng)逐漸成型了。實(shí)際上,它就快完成了。接下來(lái)唯一要做的就是實(shí)現(xiàn)某種計(jì)分系統(tǒng),并通過(guò)某種方法來(lái)結(jié)束游戲。首先解決計(jì)分系統(tǒng)問(wèn)題,稍后介紹如何結(jié)束游戲。
8.1 計(jì)分系統(tǒng)
在游戲中,鑒于玩家試圖生存盡可能長(zhǎng)的時(shí)間,所以把存活時(shí)間作為計(jì)分標(biāo)準(zhǔn)顯然是一個(gè)不錯(cuò)想法。不是嗎?
我們需要通過(guò)某種方法來(lái)計(jì)算游戲從開(kāi)始到現(xiàn)在所持續(xù)的時(shí)間。這正好是JavaScript 計(jì)時(shí)器的強(qiáng)項(xiàng),但在構(gòu)建計(jì)時(shí)器之前需要聲明一些變量。在JavaScript 代碼頂部的 player 變量下面添加以下代碼:
let score; let scoreTimeout;
這些變量將用于存儲(chǔ)分?jǐn)?shù)(已經(jīng)過(guò)去的秒數(shù))和對(duì)計(jì)時(shí)器操作的引用,以便根據(jù)需要來(lái)開(kāi)始或停止計(jì)時(shí)器。
另外,在游戲開(kāi)始或重置時(shí)也需要重新設(shè)置分?jǐn)?shù)。為此,在 startGame 函數(shù)頂部的 numAsteroids 變量下面添加以下代碼:
score = 0;
為了便于管理得分計(jì)時(shí)器,我們創(chuàng)建一個(gè)名為 timer 的專用函數(shù)。在 animate 函數(shù)上面添加以下代碼:
function timer() {
if (playGame) {
scoreTimeout = setTimeout(() => {
uiScore.html(++score);
timer();
}, 1000);
}
}
以上代碼現(xiàn)在還不會(huì)起作用,但它會(huì)檢查游戲是否開(kāi)始,如果游戲已經(jīng)開(kāi)始,它就把計(jì)時(shí)器的時(shí)間間隔設(shè)置為 1 秒,并把該計(jì)時(shí)器賦給 scoreTimeout 變量。在計(jì)時(shí)器中,score 變量的值在增加,同時(shí)計(jì)分 UI 也在更新。然后,計(jì)時(shí)器自身將調(diào)用 timeout 函數(shù)來(lái)重復(fù)整個(gè)過(guò)程,這意味著游戲結(jié)束時(shí)計(jì)時(shí)器才會(huì)停止計(jì)時(shí)。
現(xiàn)在還沒(méi)有調(diào)用 timer 函數(shù),所以它還不會(huì)發(fā)揮作用。當(dāng)游戲開(kāi)始時(shí),需要調(diào)用該函數(shù),因此在 keydown 事件監(jiān)聽(tīng)器中的 animate 函數(shù)調(diào)用下面添加以下代碼:
timer();
只要玩家開(kāi)始游戲,以上代碼就會(huì)觸發(fā)計(jì)時(shí)器。在瀏覽器中查看效果,在游戲界面的左上角可以看到分?jǐn)?shù)在不斷增加。
但遺憾的是,這里還存在一個(gè)問(wèn)題——如果你重置游戲,分?jǐn)?shù)有時(shí)候會(huì)顯示為 1 秒鐘。這是因?yàn)楫?dāng)你重置游戲時(shí),分?jǐn)?shù)計(jì)時(shí)器仍然在運(yùn)行,但它實(shí)際在你重置游戲之后才運(yùn)行(將重置分?jǐn)?shù)由 0 更改為 1 )。為了解決這個(gè)問(wèn)題,需要在重置游戲時(shí)先清除計(jì)時(shí)器。幸運(yùn)的是,JavaScript 有特定的函數(shù)可以實(shí)現(xiàn)該操作。
在 init 函數(shù)的 uiReset.click 事件監(jiān)聽(tīng)器的 startGame 調(diào)用上面添加以下代碼:
clearTimeout(scoreTimeout);
顧名思義,以上代碼的作用顯而易見(jiàn)。通過(guò)這個(gè)獨(dú)立的函數(shù)可以獲取 scoreTime 變量中的分?jǐn)?shù)計(jì)時(shí)器,并且阻止計(jì)時(shí)器的運(yùn)行。再次運(yùn)行游戲,你可以發(fā)現(xiàn)通過(guò)這行簡(jiǎn)單的 JavaScript 代碼已經(jīng)成功解決了上面遇到的問(wèn)題。
8.2 殺死玩家
如果小行星無(wú)法傷害你,那么躲避小行星就沒(méi)有任何意義了,因此我們需要添加一些功能來(lái)殺死玩家(當(dāng)玩家碰到小行星時(shí))。
在這里發(fā)現(xiàn)明顯的問(wèn)題了嗎?在火箭是三角形的情況下,你能執(zhí)行圓周碰撞檢測(cè)嗎?簡(jiǎn)單地說(shuō),你不能執(zhí)行圓周碰撞檢測(cè),或者說(shuō)至少?zèng)]那么容易。但這里將忽略一些細(xì)節(jié)問(wèn)題,也就是說(shuō),把玩家火箭的一小部分區(qū)域作為碰撞檢測(cè)區(qū)域。在實(shí)際中如果你幸運(yùn)一些,這種檢測(cè)有助于躲避小行星。
為了簡(jiǎn)化代碼,我認(rèn)為這樣做是值得的。畢竟,這只是一個(gè)供娛樂(lè)的小游戲而已,因此不需要追求絕對(duì)的真實(shí)。
因此,只需在 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)該能很快明白以上代碼中的距離計(jì)算方法。你將通過(guò)它們來(lái)計(jì)算玩家火箭與當(dāng)前循環(huán)中的小行星之間的像素距離。
下一步是判斷火箭是否與小行星發(fā)生碰撞,可以通過(guò)查看上面計(jì)算的像素距離是否小于小行星半徑加上火箭碰撞圓周的半徑之和。這里使用的火箭碰撞圓周的半徑是火箭寬度的一半,你也可以隨意改變它。
如果火箭與小行星發(fā)生碰撞,就要?dú)⑺劳婕?。殺死玩家并結(jié)束游戲的過(guò)程非常簡(jiǎn)單,但我還需要逐行對(duì)它進(jìn)行解釋。
前三行代碼停止播放推進(jìn)器聲音,并重置和播放死亡的聲音。開(kāi)始播放死亡的聲音時(shí),需要把 playGame 設(shè)置為 false 來(lái)結(jié)束整個(gè)游戲,并通過(guò)前面已經(jīng)使用過(guò)的 clearTimeout 函數(shù)來(lái)停止計(jì)分計(jì)時(shí)器。
此時(shí),所有的游戲邏輯都已經(jīng)停止,因此可以隱藏統(tǒng)計(jì)界面,并顯示游戲結(jié)束界面。
顯示游戲結(jié)束界面時(shí),需要停止播放背景音樂(lè),并最終釋放鍵盤(pán)事件處理程序,從而防止玩家由于無(wú)意按下某個(gè)按鍵而啟動(dòng)游戲。

9. 增加游戲難度
好的,其實(shí)我們要?jiǎng)?chuàng)建的游戲還沒(méi)有完成。讓我們?cè)谟螒蛑性偬砑右恍┕δ?,即增加游戲的難度,玩家要想存活更長(zhǎng)的時(shí)間就變得更加困難了。
我們還是直入主題吧,在 timer 函數(shù)中的 uiScore.html下面添加以下代碼:
if (score % 5 === 0) {
numAsteroids += 5;
}
以上代碼看上去是否像一個(gè)普通的條件語(yǔ)句?其實(shí)不是。注意其中的百分比符號(hào)。它是求模運(yùn)算符。求??梢杂?jì)算一個(gè)數(shù)是否能被另一個(gè)數(shù)完全整除,它將返回兩個(gè)數(shù)相除所得的余數(shù)。
你可以通過(guò)求模計(jì)算來(lái)執(zhí)行周期性的操作,例如,每隔 5 秒發(fā)生一次。因?yàn)槟憧梢园涯?5 運(yùn)算運(yùn)用于某個(gè)數(shù),如果它返回的結(jié)果為 0,那么該數(shù)一定能夠被 5 整除。
在本游戲中,我們使用模 5 運(yùn)算來(lái)確保某個(gè)代碼段每隔 5 秒鐘執(zhí)行一次。這個(gè)代碼段的作用是,每隔 5 秒鐘就向游戲中添加 5 顆小行星。實(shí)際上,這里并沒(méi)有增加小行星,增加的只是小行星的數(shù)目。
添加小行星很簡(jiǎn)單,在 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));
}
以上代碼檢查每個(gè)循環(huán)中的小行星數(shù)目,如果數(shù)目沒(méi)有達(dá)到要求,它將繼續(xù)向游戲中添加新的小行星,直到小行星的數(shù)目達(dá)到要求為止。
再次在瀏覽器中啟動(dòng)游戲,你會(huì)發(fā)現(xiàn),當(dāng)存活的時(shí)間越來(lái)越長(zhǎng)時(shí),游戲中的小行星就會(huì)越來(lái)越多?,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");
// 畫(huà)布尺寸
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;
}
// 重至和啟動(dòng)游戲
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;
}
});
// 開(kāi)始動(dòng)畫(huà)糖環(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);
}
}
// 動(dòng)畫(huà)循環(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript中的16進(jìn)制字符(改進(jìn))
后來(lái)經(jīng)過(guò)自己的測(cè)試,發(fā)現(xiàn)將字符轉(zhuǎn)換為十六進(jìn)制的方法不完善。2011-11-11
Three.js實(shí)現(xiàn)簡(jiǎn)單3D房間布局
這篇文章主要為大家詳細(xì)介紹了Three.js實(shí)現(xiàn)簡(jiǎn)單3D房間布局的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
簡(jiǎn)單談?wù)刯avascript高級(jí)特性
看過(guò)很多關(guān)于js高級(jí)特性介紹的文章,本文是個(gè)人感覺(jué)最通俗易懂的,這里分享給大家,希望大家能夠喜歡2019-09-09
JavaScript表單焦點(diǎn)自動(dòng)切換代碼
這篇文章主要介紹了JavaScript表單焦點(diǎn)自動(dòng)切換代碼的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
原生JS和jQuery操作DOM對(duì)比總結(jié)
這篇文章主要給大家介紹了原生JS和jQuery操作DOM的一些對(duì)比總結(jié),文中總結(jié)了很多的對(duì)比,相信對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-01-01
javascript模擬實(shí)現(xiàn)C# String.format函數(shù)功能代碼
這篇文章主要介紹了javascript模擬實(shí)現(xiàn)C# String.format函數(shù)功能,相信大家可以用的到2013-11-11
用云開(kāi)發(fā)Cloudbase實(shí)現(xiàn)小程序多圖片內(nèi)容安全監(jiān)測(cè)的代碼詳解
這篇文章主要介紹了用云開(kāi)發(fā)Cloudbase實(shí)現(xiàn)小程序多圖片內(nèi)容安全監(jiān)測(cè),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
webpack中使用Eslint的實(shí)現(xiàn)
本文主要介紹了webpack中使用Eslint的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07

