我們前面花了一些時間討論關於 HTML 與 CSS 的相互操作,但目前網頁還沒有任何功能,因此我們現在就來開始做這件事情。而我希望透過一個簡單、但同時又可以讓各位快速看到網頁反映的內容來進行。幸運的是我偶然看到在 MDN 教學網站上有一個 2D Breakout Game,這難度剛好可以讓我們來寫遊戲的功能。但因為網站有提到需要具備一定 JavaSctipt 基礎,因此並不會從頭講述 JavaScript 語法,所以這次的內容我就會設計一下,讓內容更加容易上手一些。
這就要回到 1995 年的矽谷,那時的 Netscape 公司跟 IE 開打瀏覽器戰爭。當年的 Netscape 就有一個很狂的目標,就是要稱霸整個網路界,那也伴隨一個在當時也很狂的想法,就是把網頁增加互動成分,變得不再只是靜態的樣子。而那公司內有一位剛被僱用的年輕工程師 Brendan Eich,就被公司高層賦予了一個幾乎不可能完成的任務:「用 10 天的時間做一個程式語言讓網頁可以有互動功能」
Brendan Eich 當時面臨的最大難題是:如何在極短的時間內設計出一個功能足夠強大、又足夠簡單的語言?他從三個不同的程式語言中汲取靈感,然後再把這些結合在一起,做出了 Mocha 這語言。
當時 C 語言非常普及,很多程式設計師都熟悉它的語法。所以,Brendan 決定從 C 語言中拿來一些基本結構,比如說用 {}
大括號來表示程式區塊、for
和 while
迴圈來控制程式流程,還有 ;
來表示語句的結束。這樣,開發者一看就知道該怎麼寫,降低了學習門檻。
當年還有一個程式語言叫做 Scheme,經常被用來處理函數邏輯。Brendan 想要 JavaScript 也能處理複雜的函數邏輯,所以他加入了 Scheme 的一些特性,特別是「閉包(Closure)」這個概念。這是什麼意思呢?簡單來說,閉包讓程式可以「記住」一個函數內部的變數,即使這個函數已經結束執行了,你還可以保留這些變數。想像一下,假設你去餐廳點餐,通常在結束點餐並取餐後,沒有人會記得你點了什麼。但是,如果有 Closure 的機制,就像餐廳記得你上次點了什麼菜一樣,你下次來的時候,餐廳服務生還能告訴你上次點的菜是什麼,這樣你就可以直接選擇「同樣的餐點」。這種機制就非常適合處理網頁上的事件(例如按鈕點擊)之類的行為,讓 JavaScript 能夠記住使用者的操作,然後即時回應給使用者。
當時,Java 具有很明確的 物件導向(Object-Oriented) 的特性(如類別 Class 的概念),在當時相當流行。Brendan 從中設計了一個物件(Object)系統,讓開發者可以像 Java 那樣把功能和資料包裝成一個的「物件」,但他並沒有把 Java 的類別系統原封不動地導入進來,因為他認為 Java 的類別結構實在過於繁瑣,處理簡單網頁事件根本就是殺機用牛刀啊。而 Brendan 同時引入了一個更簡單的系統,叫做「原型(Prototype)繼承」。這有點像是「模仿」的概念。想像你創造了一個「模範生」,其他學生都可以把這個模範生當作「範本」來學習(繼承)他的特性,但你不需要定義一整套繁瑣的學習規則。也因此就保留了物件導向的好處,又避免程式碼便得過度複雜。
不久之後,Netscape 將 Mocha 這款「咖啡」改名為 LiveScript。然而,為了搭上 Java 這款「咖啡」語言的順風車,就與當時擁有 Java 版權的 Sun Microsystems 合作,將其定名為 JavaScript 了, 你一定要記得 Java 跟 JavaScript 還是很不一樣的。 而有時候你真的不知道、或難以想像到這一個商業行銷手段,就這樣摧生了一個你到哪都能看到的通用語言了。
以下我就不再講述 JavaScript 演進的歷史了,有興趣的各位可以從 EMCA 網站中找到相關的故事。
在進入網頁遊戲開發的世界時,我們最好的起點就是純 JavaScript。這就就像是你要蓋一棟大樓,你得先了解如何用鋼筋跟水泥才能打造地基與建築物骨架。而在你掌握了 JavaScript 之後,你就能輕鬆運用各式各樣的「框架」,來為你的遊戲專案、或是網頁專案有更不一樣的呈現方式,這也是我們後續要做的事情。
但是,你同時也要知道這些框架就像是為工匠進行量身打造的工具,幫助你完成一些重複而無聊的工作(例如處理 UI 元件、遊戲邏輯的設置),讓開發過程可以更為輕鬆。但如果一開始就完全依賴這些工具,直到當某個功能出現問題時,你可能會直接不知道發生了什麼事情,因為你不知道框架的「機械運作」究竟出了什麼毛病。所以囉,馬步要紮得好,後面練功才會輕鬆。我也還在蹲馬步的過程中,大家一起加油吧!
我們在進入 JS 之前,要先做一些事情。我們到先到 ./pages/game.html
中進行一些修改如下:
先找到我們放遊戲的區塊,也就是 <h2>Game Area</h2>
所在的 <header>
之下,加入以下這段。這個 Canvas 顧名思義,就是處理 JS 繪圖的「畫布」區塊。那我們就會用 JS 控制讓這區塊呈現出你畫出來的內容。但具體要怎麼畫呢?待會會說到。
<canvas id="game-canvas" width="800" height="600"></canvas>
看起來會像這樣。
...
<section id="game-area">
<header>
<h2>Game Area</h2>
<p>Difficulty: <span id="difficulty-level"></span></p>
</header>
**<canvas id="game-canvas" width="800" height="600"></canvas>**
</section>
...
而我們先在 HTML 底部附近加上 <script> </script>
的標記,像這樣:
<section id="game-controls">
<h3>Game Controls</h3>
...
</section>
</main>
**<script>
// We will write JS here.
</script>**
</body>
</html>
OK,這樣大致準備就緒了,就可以開始了
以下就針對我們會用到的 JS 基礎操作進行說明,各位有興趣的話,可以在這些網站多學習:
1. https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures-v8/
2. https://www.w3schools.com/js/default.asp
3. 其他的可以參考這裡:https://www.upwork.com/resources/top-sites-for-online-education-and-learning#codecademy
了解變數的定義方式
在 JavaScript 中,我們可以使用 let
或 const
關鍵字來定義變數。變數可以用來儲存數值、字串或物件等資料類型。例如
let x = 50; // Define a variable x with **initial** value 50.
let y = 100; // Define a variable y with **initial** value 100.
const brickX = 1; // Define a variable brickX with a **fixed** value 1.
這裡的 x
和 y
可以理解為我們 Canvas 上的座標,之後我們會使用這些來定位圖形的位置。
你可能會發現還有 var
,這主要差異在於「是否可以重複宣告」,主要是給函數或判斷式在 { }
外面也可以被存取。現在我們已經不推薦這個方式,都以 let
與 const
為主。
if (true){
var player1 = "Alice";
let player2 = "Bob";
const player3 = "Charlie";
}
console.log(player1); // show the player1 message: "Alice"
console.log(player2); // ReferenceError:player2 is not defined
console.log(player3); // ReferenceError:player3 is not defined
let 與 const 的差異主要在於 能不能重新賦值。簡單來說:
let x = 50;
x = x + 10;
console.log("x: ", x) // show the message of x: 60
如果寫成這樣就錯了:
const x = 50;
x = x + 10; // Error: Attempt to assign to const or read-only variable.
進行數值操作
我們在數值操作中,會使用「加等於」例如:x += dx
相當於 x = x + dx
,用來更新 x
的值。讓我們來看看這樣做的效果:
let dx = 2;
let dy = -2;
x += dx; // increase x by 2
y += dy; // decrease y by 2
更新後,x
會從 50 變成 52,y
則會從 100 變成 98。這樣的變化在動畫中會讓物體看起來像是往右上方移動。
函數的基本結構
在 JavaScript 中,我們可以使用 function
關鍵字來定義一個函數。函數是一組可重複執行的程式碼,主要用來處理一些執行程式的邏輯結構。例如:
function draw() {
// Logic of drawing on canvas
}
draw();
這個 draw()
函數目前還沒有執行任何動作,目前只是被定義出來,但如果我們之後調用這函數,例如寫上 draw();
,就會執行 function draw() { … }
之中的 {}
區塊內的程式碼。
條件判斷式(if
、else-if
、else
)
假設我們希望當 x
座標數值超過 600 或(||
)低於 0 時,就要觸發「反彈」大概就是球撞牆要反彈的意思。我們就可以這樣呈現:
if (x + dx > 600 || x + dx < 0) {
dx = -dx; // Reverse the movement by adding minus sign.
}
若希望 Fancy 一些,就是超出右邊邊界或左邊邊界時,會分別跳出訊息,這樣就可以用 else if 依序檢查多個條件進行判斷:
if (x + dx > 600) {
dx = -dx;
console.log("Hit the right wall");
} else if (x + dx < 0) {
dx = -dx;
console.log("Hit the left wall");
}
當所有 if
和 else if
條件都不符合時,我們可以使用 else
來處理所有其他情況:
if (x + dx > 600) {
dx = -dx;
console.log("Hit the right wall.");
} else if (x + dx < 0) {
dx = -dx;
console.log("Hit the left wall.");
} else {
console.log("Exception Occurs.")
}
執行重複邏輯 for
當你要重複執行一個程式碼達到一定次數,可以使用 for 迴圈進行變數控制,就是透過設定 index 初始值、條件判斷與 index 更新,for
迴圈就能夠控制執行順序,並根據設定好的規則來遍歷(Iterate)你的資料。
for (initial index value; condition; index counter) {
// Program to execute.
}
舉例來說,你被女友懲罰要打字說 Sorry 重複 100 次,我們可以這樣寫:
for (let i = 1; i <= 100; i++) {
console.log(`${i}. I'm sorry.`);
}
這裡我們使用「模板字串(Template Literal)」,簡單來說就是一種做成一個字串格式的方式,只需要用 反引號(`)來包住你的字串,而不是用一般的雙引號 "
或單引號 '
。這樣做可以讓 JS 將 ${}
中的變數替換為對應的值。
或是你現在要計算遊戲總分,你定義了一個陣列,可以順便學一下陣列操作:
let score = [30, 40, 15, 25, 36];
let total = 0;
for (let i = 0; i < scores.length; i++) {
total += scores[i];
}
console.log(`Total Score is ${total}.`);
其中 .length
是取出該物件的「長度」,像是以上 score.length
的值就會是 5。而在 for 迴圈內的陣列中的 i
是 index,你也可以換成 j
或 k
等你想要的變數。而 score[0]
會是 30、score[3]
是 25,你要記得這些程式語言是從 0 開始數的。
相對簡潔的 forEach
如果你要檢查每一個元素,且你不會在迴圈中途中斷,就可以使用 forEach 把整體變得較為簡潔。像是:
let score = [30, 40, 15, 25, 36];
let total = 0;
score.forEach(function(score) {
total += score;
});
console.log(`Total Score is ${total}.`); // Total Score is 146.
你會發現這結構不太一樣,這就跟前面提到的「物件」有很大的關係。forEach
之所以能用「方法(Method)」的形式來呼叫,是因為這本身就是 JavaScript 陣列物件(Array Object)的一部分,也就是說,你只有在處理陣列的時候才會用到 forEach
。結構如下:
array.forEach(function(element, index, array) {
// Program to execute.
});
let score = [30, 40, 15, 25, 36];
score.forEach(function(number) {
if (number > 35) {
// Escape from the function without interript the forEach loop.
return;
}
console.log(number); // Output: 30, 15, and 25
});
使用 getElementById
選取 HTML 元素
我們知道 JavaScript 就是要來控制網頁互動的,所以 JS 就會有一些能夠選取和操作 HTML 頁面上元素的方法。在這段程式碼中,我們選取了一個 <canvas>
元素:
const canvas = document.getElementById("game-canvas");
這行程式碼是根據 HTML 元素的 id
屬性(這裡是 "myCanvas"
)來選取畫布元素,並將它賦值給變數 canvas
。如果我們在 ./pages/game/html
中把 canvas 印出來,就會看到這樣的內容:
...
<header>
...
</header>
<canvas id="game-canvas" width="800" height="600"></canvas>
</section>
...
<script>
const canvas = document.getElementById("game-canvas");
console.log("canvas: \n", canvas)
</script>
</body>
</html>
你可以在瀏覽器的 Console 裡面看到 canvas 變數就是裝這些內容:
<canvas id="game-canvas" width="800" height="600"></canvas>
一個很重要的事情是,Canvas 的起始座標在左上角,意思是最左上角的座標是 (0, 0)
,而在最右下角則是 ({canvas.width}, {canvas.height})
。這跟你平常學的 Cartesian 座標有點差異,但還算好理解。
取得 2D 繪圖的上下文
canvas
元素本身只是網頁中的一個空白畫布,就像一張什麼都還沒畫的紙一樣。要在這張紙上畫圖,我們需要有一個「畫家」來進行操作;而「繪圖上下文(Context)」會提供這個「畫家」的工具與能力。
const ctx = canvas.getContext("2d");
在這行程式碼中,我們傳入了 "2d"
作為參數,這表示我們要獲取一個「2D 繪圖上下文」。而我們使用 const 的原因是因為這變數的 Reference 不能被改變,不然突然改掉上下文就會造成整組就大亂了(想像你在讀物理課本,結果突然被換成歷史課本)。而後這行程式碼會返回一個繪圖上下文物件 ctx
,這物件就有許多繪圖的方法,像是 arc()
和 fill()
等,我們接下來舊會開始介紹。
使用方法繪製圖形
這段程式碼中使用了 arc()
方法來繪製圓形:
ctx.arc(x, y, 10, 0, Math.PI * 2);
這個方法有五個參數:
x, y
:圓心的座標。10
:圓的半徑。0, Math.PI * 2
:繪製的起始角度和結束角度(這裡是完整的 360 度圓形)。操作屬性來更改繪圖顏色
接著,我們可以透過修改 fillStyle
屬性來改變填充顏色:
ctx.fillStyle = "#0095DD";
這行程式碼是把 ctx
的 fillStyle
屬性設為藍色(#0095DD),接著我們可以用 fill()
來填充這個顏色。
基本圖形繪製
在 draw()
函數中,我們首先會開始一個新的路徑,然後用 arc()
繪製圓形:
ctx.beginPath(); // Start the drawing path
ctx.arc(x, y, 10, 0, Math.PI * 2); // Draw an arc (circle).
ctx.fill(); // Fill the colour
ctx.closePath(); // Close the drawing path
這段程式碼會讓圓形出現在指定的 x
和 y
座標地方。同時要注意的是,調用 ctx 必須要先告訴電腦說要從哪裡「開始一個新的繪圖路徑」與「「結束當前繪圖路徑」,而中間就是畫出圖形所需要執行的步驟。因為這些 ctx 這個 Method 之中的所有 Function 都是被呼叫的,因此呼叫的順序就很重要了!
使用 setInterval
來控制動畫
動畫的核心原理就是不斷地重繪圖形,讓圖形在畫布上移動。這段程式碼中的 setInterval
會以 10 毫秒的間隔調用 draw()
函數,並根據 x
和 y
的變化來重新繪製圓形,達到動畫效果。
setInterval(draw(), 10);
使用變數更新座標位置
在每次調用 draw()
時,程式碼會更新 x
和 y
的值,這樣圓形就會在畫布上「移動」:
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
let x = canvas.width / 2; // The ball starts at the middle of the canva
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
function drawCircle() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
drawCircle();
// Determine the status
if (x + dx > canvas.width || x + dx < 0) {
dx = -dx;
}
if (y + dy > canvas.height || y + dy < 0) {
dy = -dy;
}
// Update the coordinate
x += dx;
y += dy;
}
setInterval(draw, 10);
這樣做的結果是,每次 x
和 y
的值變化後,圓形的位置也會跟著變化,這樣就達到動畫移動的效果囉!
總之,以上就是我們會用到的所有 JavaScript 的語法,接下來就是針對這些進行活用。下一章就會開始逐項進入遊戲元件、如磚頭、球…等的設計環節。我想整個專案會變得越來越有趣,畢竟我們可以看到一些會動的東西。然而,你就會發現這些語法就可以做很多事情了,我們常用的技術不外乎就是這些的應用。但一個設計網頁的高手是一直在提昇自己的 JavaScript 技術的,所以我們還得持續迭代自己的技術。當然你還可能會聽到比較「優雅」的 CoffeeScript、純函數的 PureScript、以及強型別的 TypeScript。這些都是 JavaScript 的親家,但理念都不太一樣。先預告一下,後續我會把這些東西融合 TypeScript,因為有時候隨著專案擴大時,就需要額外的機制來處理,這就留到後面再說了~