translate lesson 6 to zh-tw

pull/206/head
CRTao 4 years ago
parent c55edb13c3
commit 4e0e067b4e

@ -0,0 +1,224 @@
# 建立太空遊戲 Part 1簡介
![影片](../../images/pewpew.gif)
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/29)
### 遊戲開發中的繼承(Inheritance)與組合(Composition)
在之前的課程中,因為專案較小的規模,我們不需要去擔憂應用程式的設計結構。然而,當你的應用程式規模越來越大時,結構的選擇就是一大課題。在 JavaScript 中,有兩種大方向來建立龐大的應用程式:*組合(Composition)*與*繼承(Inheritance)*。它們有各自的優缺點,我們會藉由遊戲內容來進行說明。
✅ 其中一本有名的程式設計用書是有關於[設計模式](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%9A%E5%8F%AF%E5%A4%8D%E7%94%A8%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%9F%BA%E7%A1%80)。
在遊戲中你會有`遊戲物件`,顯示在畫面中。這代表它們在笛卡爾座標系中有各自的位置,以 `x``y` 座標點定義。當你在開發遊戲時,你會注意到所有的遊戲物件都有一套標準的規範,和大多數的遊戲相似,通常會有這些元素:
- **適地性** 大多數遊戲元素都是建立在位置上的。這代表他們有各自的所在處,一組 `x``y`
- **可移動的** 這些物件可以移動到新的位置。典型來說有英雄、怪物或是 NPC(Non Player Character),但有些例外,好比是樹這種常駐物件。
- **可自毀的** 這些物件只能存在於一小段時間,接著它們就會自我刪除。通常這是`死亡`或是`被摧毀`的布林訊號傳遞給遊戲引擎,告知物件不再需要被描繪出來。
- **冷卻時間** 「冷卻時間」是存活週期短的典型物件屬性。好比是一段文字、爆炸的視覺特效,只能呈現數毫秒的時間。
✅ 想想看遊戲小精靈(Pac-Man)。你能辨別出符合上述清單的其中四種物件嗎?
### 行為表達
以上的敘述皆在表達遊戲物件所進行的行為。那我們該如何去編寫它們呢?我們可以使用方法(methods)連接 classes 或是物件(objects)來表達這些行為。
**Classes**
這個想法是結合 `classes` 與`繼承`的方式來在 class 中添加特定行為。
✅ 繼承是一個重要概念。在[有關繼承的 MDN 文章中](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)學習更多內容。
以程式碼來表達的話,一個遊戲物件通常會呈現這種形式:
```javascript
//設定 class GameObject
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
}
//這個 class 會繼承 GameObject 中 class 內容
class Movable extends GameObject {
constructor(x,y, type) {
super(x,y, type)
}
//這個可移動物件可以在畫面上移動
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//這是特定的 class 繼承 Movable class它能使用所有繼承到的屬性內容
class Hero extends Movable {
constructor(x,y) {
super(x,y, 'Hero')
}
}
//另一方面,這個 class 只繼承到 GameObject 的內容
class Tree extends GameObject {
constructor(x,y) {
super(x,y, 'Tree')
}
}
//英雄可以移動......
const hero = new Hero();
hero.moveTo(5,5);
//但樹木卻不能
const tree = new Tree();
```
✅ 花點時間重新構思小精靈(Pac-Man)的主角,或是 Inky、Pinky 與 Blinky 這幾隻鬼魂。它們該如何以 JavaScript 表現?
**組合**
另一種處理物件繼承的方式為*組合(Composition)*。物件以這種方式呈現它們的行為:
```javascript
//建立常數 gameObject
const gameObject = {
x: 0,
y: 0,
type: ''
};
//...與常數 movable
const movable = {
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//常數 movableObject 是 gameObject 與 movable 的組合
const movableObject = {...gameObject, ...movable};
//利用函式建立新的英雄,繼承 movableObject 的內容
function createHero(x, y) {
return {
...movableObject,
x,
y,
type: 'Hero'
}
}
//...與常駐物件只繼承 gameObject 的屬性
function createStatic(x, y, type) {
return {
...gameObject
x,
y,
type
}
}
//建立可以移動的英雄
const hero = createHero(10,10);
hero.moveTo(5,5);
//和建立只能佇立於此的樹木
const tree = createStatic(0,0, 'Tree');
```
**我該使用哪一種設計模式?**
這都取決於你選擇何種設計模式。JavaScript 支援這兩種範例。
--
另一種在遊戲開發中常見的設計模式負責處理玩家的遊戲表現與遊戲體驗。
## 發布訂閱設計模式
✅ Pub/Sub 全名為 'publish-subscribe'
這個設計模式將應用程式內不同的模組分開處理,讓彼此不知道彼此的行為。為何要這樣做?這讓我們總觀上更輕易地了解各個模組的行為。也可以在你想要時輕易地改變模組的行為模式。我們該如何實踐它呢?我們先建立這幾個概念:
- **訊息** 一個訊息通常會以文字字串與額外的負載(payload) ── 一組定義訊息內容的資料 ── 呈現。遊戲中典型的訊息可以是 `KEY_PRESSED_ENTER`
- **發布者** 這個元素*發布*訊息給所有的訂閱者。
- **訂閱者** 這個元素*監聽*特定的訊息,並藉由執行某些任務以作為訊息的回應,例如發射雷射光。
實踐方法雖小,但這是功能強大的設計方式。這是它的建立方式:
```javascript
//設定 EventEmitter class 容納監聽者
class EventEmitter {
constructor() {
this.listeners = {};
}
//當訊息接收時,讓監聽者處理它的負載
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
//當訊息發出時,附上負載發給監聽者
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach(l => l(message, payload))
}
}
}
```
利用上述程式我們建立一套小型實作內容:
```javascript
//設定訊息種類
const Messages = {
HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//調用你設定的 eventEmitter
const eventEmitter = new EventEmitter();
//設定英雄
const hero = createHero(0,0);
//讓 eventEmitter 監聽有關英雄往左移的訊息,並執行動作
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
//設定遊戲視窗來監聽鍵盤事件,當左方向鍵按壓時,發出英雄往左移的訊息
window.addEventListener('keyup', (evt) => {
if (evt.key === 'ArrowLeft') {
eventEmitter.emit(Messages.HERO_MOVE_LEFT)
}
});
```
我們連接了鍵盤事件 `ArrowLeft` 並傳遞 `HERO_MOVE_LEFT` 訊息。我們監聽該訊息並移動 `hero` 作為結果。這種開發方式讓事件監聽者與英雄區隔開來。你也可以將 `ArrowLeft` 換成 `A` 鍵。此外,我們能修改 eventEmitter 的 on 函式,讓 `ArrowLeft` 事件產生截然不同的行為。
```javascript
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
```
當遊戲越來越豐富、物件越來越複雜時,這套設計方式能維持程式碼的整潔。由衷建議善用這套設計模式。
---
## 🚀 挑戰
想想看發布訂閱模式可以如何增進一款遊戲。哪一個部份該發送事件,而遊戲又該如何回應事件?現在你有機會發揮你的創意,思考一款新遊戲和它運作的模組。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/30)
## 複習與自學
藉由[閱讀此連結](https://docs.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber?WT.mc_id=academic-13441-cxa)來認識更多關於發布與訂閱的設計模式。
## 作業
[建立遊戲雛形](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 建立遊戲雛形
## 簡介
使用本課程中的程式案例,編寫一款你喜歡的遊戲呈現方式。這是一款簡單小規模的遊戲,目的是要能以 class、組合模式與發布訂閱模式呈現遊戲的運作方式。發揮你的創意
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------- | -------------------------- | ---------------------------- |
| | 畫面上有三個元素且能被控制 | 畫面上有兩個元素且能被控制 | 畫面上只有一個元素且能被控制 |

@ -0,0 +1,216 @@
# 建立太空遊戲 Part 2在畫布上繪製英雄與怪物
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/31)
## Canvas
Canvas 是 HTML 中的元素,預設上不帶有任何內容,就如一塊白板。你需要自己彩繪上去。
✅ 在 MDN 上閱讀[更多關於 Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)。
這是它典型的宣告方式,位在頁面的 body 中:
```html
<canvas id="myCanvas" width="200" height="100"></canvas>
```
上面我們設定了 `id`、`width` 和 `height`
- `id`:讓你在處理物件時,能快速地取得參考位置。
- `width`:物件的寬度。
- `height`:物件的高度。
## 繪製簡單幾何圖樣
Canvas 使用了笛卡爾座標系繪製圖案。因此有 x 軸與 y 軸來表達物件的所在地點。座標點 `0,0` 位在畫布的左上方;而右下方則是我們定義畫布的寬度與高度。
![畫布網格](../canvas_grid.png)
> 圖片出自於 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes)
要在 Canvas 物件上繪製圖案,你需要執行下列步驟:
1. **取得 Canvas 物件的參考位置**。
1. **取得 Context 物件的參考位置**,定義在 Canvas 元素中。
1. 使用 context 元素**進行繪製動作**。
以程式碼表達上述步驟會呈現成:
```javascript
// 繪製紅色矩形
//1. 取得 canvas 參考點
canvas = document.getElementById("myCanvas");
//2. 設定 context 為 2D 以繪製基本圖形
ctx = canvas.getContext("2d");
//3. 填入色彩紅色
ctx.fillStyle = 'red';
//4. 利用這些參數決定位置與大小,繪製矩形
ctx.fillRect(0,0, 200, 200) // x,y,width, height
```
✅ Canvas API 主要是處理 2D 圖形,但你也可以在網頁中繪製 3D 圖形。要完成這個需求,你可以使用 [WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API)。
你可以使用 Canvas API 繪製出這些物件:
- **幾何圖形**,我們已經展示繪製矩形的流程,還有許多種形狀可以使用。
- **文字**,你可以繪製文字,決定你想要的字型及顏色。
- **圖片**,你可以依據圖片檔繪製圖案,舉例來說像是 .jpg 或是 .png 檔。
✅ 試試看!你知道如何繪製矩形,你能在頁面中繪製圓形嗎?看看在 CodePen 上有趣的 Canvas 塗鴉。這邊有一樣[特別令人驚豔的例子](https://codepen.io/dissimulate/pen/KrAwx)。
## 讀取並繪製圖片檔
建立 `Image` 物件並設定其 `src` 屬性,你可以讀取圖片檔。接著監聽 `load` 事件,了解圖片何時已經可以被使用。程式碼如下:
### 讀取檔案
```javascript
const img = new Image();
img.src = 'path/to/my/image.png';
img.onload = () => {
// 圖片載入完成,準備使用
}
```
### 讀取檔案之模式
建議上可以將上述程式打包起來,建立成完整的結構,判斷圖片是否載入完成,也方便未來的使用:
```javascript
function loadAsset(path) {
return new Promise((resolve) => {
const img = new Image();
img.src = path;
img.onload = () => {
// 圖片載入完成,準備使用
resolve(img);
}
})
}
// 實際用法
async function run() {
const heroImg = await loadAsset('hero.png')
const monsterImg = await loadAsset('monster.png')
}
```
要在畫面上繪製遊戲物件,你的程式碼會如下所示:
```javascript
async function run() {
const heroImg = await loadAsset('hero.png')
const monsterImg = await loadAsset('monster.png')
canvas = document.getElementById("myCanvas");
ctx = canvas.getContext("2d");
ctx.drawImage(heroImg, canvas.width/2,canvas.height/2);
ctx.drawImage(monsterImg, 0,0);
}
```
## 是時候來建立你的遊戲了
### 建立目標
你需要建立包含 Canvas 元素的網頁。它會是 `1024*768` 的黑色畫面。我們提供了兩張圖片:
- 英雄艦艇
![英雄艦艇](../solution/assets/player.png)
- 5*5 隻怪物
![敵軍艦艇](../solution/assets/enemyShip.png)
### 開始開發的建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
在 Visual Studio Code 中開啟這個資料夾的副本。你需要建立本地端的開發環境,建議為 Visual Studio Code 與安裝好的 NPM 與 Node。如果你的電腦中還沒設定好 `npm`[這是它的設定流程]](https://www.npmjs.com/get-npm)。
前往 `your_work` 資料夾,開始你的專案:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器,網址為 `http://localhost:5000`。開啟瀏覽器並輸入該網址。目前會是空白的頁面,但不久後就會不一樣了。
> 筆記:想觀察畫面的改變,請重新整理你的頁面。
### 加入程式碼
`your-work/app.js` 中加入程式碼以解決下列目標:
1. 在 Canvas **繪製**黑色背景
> 要點:在 `/app.js` 中,加入兩行程式在 TODO 下方:設定 `ctx` 元素為黑色,左上方座標點為 0,0 且大小與 Canvas 相等。
2. **讀取**材質
> 要點:使用 `await loadTexture` 導入圖片位置以新增玩家與敵軍圖片。你還沒辦法在畫面上看到它們!
3. 在畫面的正下方**繪製**英雄
> 要點:使用 `drawImage` API 來繪製 heroImg 到畫面上,設定位置為 `canvas.width / 2 - 45``canvas.height - canvas.height / 4)`
4. **繪製** 5*5 隻怪物
> 要點:現在移除註解,在畫面上繪製敵人。接著編輯函式 `createEnemies`
首先,設定幾個常數:
```javascript
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
```
接著,利用迴圈在畫面上繪製矩陣型態的怪物:
```javascript
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
ctx.drawImage(enemyImg, x, y);
}
}
```
## 結果
完成後的成果應該如下所示:
![黑畫面上有英雄與 5*5 隻怪物](../partI-solution.png)
## 解答
試著自己先完成程式碼,但如果你遭遇到困難,請參考[解答](../solution/app.js)。
---
## 🚀 挑戰
你已經學會繪製 2D 圖形的 Canvas API。看看 [WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API),試著繪製 3D 物件。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/32)
## 複習與自學
[閱讀更多資料](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API),學習更多有關 Canvas API 的用法。
## 作業
[把玩 Canvas API](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 把玩 Canvas API
## 簡介
挑選一款 Canvas API 上的元素,為它建立一些有趣的設定。你能利用重複的星星建立銀河嗎?你能建立有特殊材質的線條嗎?你可以觀察 CodePen 上的範本激發想法,但請不要抄襲。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------- | ---------------------------- | ------------ |
| | 程式碼呈現有趣的材質與圖案 | 有提交程式碼,但無法正常執行 | 未提交程式碼 |

@ -0,0 +1,388 @@
# 建立太空遊戲 Part 3加入動作
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/33)
有外星人在移動的遊戲才會好玩!在這款遊戲中,我們會建立兩種移動模式:
- **鍵盤滑鼠的移動**:當使用者控制鍵盤或滑鼠時,能移動畫面上的物件。
- **遊戲內建的移動**:遊戲能自動地在一定時間內,移動其中的物件。
那我們該如何移動畫面上的物件呢?這都取決於笛卡爾座標系:我們改變物件的座標 (x,y),並在畫面上重新繪製出來。
通常你需要下列流程來*移動*畫面上的物件:
1. **設定物件的新地點**,你才能察覺到物件有所移動。
2. **清除畫面**,每一次的繪製間都需要將畫面清除乾淨。我們可以繪製一張背景色的矩形來覆蓋畫面。
3. **在新地點重新繪製物件**,我們就能移動物件,從 A 點移動到 B 點。
合理的程式碼如下所示:
```javascript
// 設定英雄位置
hero.x += 5;
// 利用矩形清除英雄
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 重新繪製背景與英雄
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
```
✅ 你能了解為什麼在同一秒內多次重新繪製英雄會影響效能的原因嗎?閱讀[其他種同目的之設計模式](https://www.html5rocks.com/en/tutorials/canvas/performance/)。
## 處理鍵盤事件
連接特定事件到程式中,你就能處理遊戲事件。鍵盤事件可以在視窗被選擇時觸發,而滑鼠事件如 `click`,則要點擊特定的物件。我們會在這個專案中,使用鍵盤物件。
要處理一種事件,需要使用視窗的 `addEventListener()` 方法,並提供給它兩個參數。第一個參數是事件的名稱,例如: `keyup`。第二個參數是回應事件結果的被呼叫函式。
下列是一種例子:
```javascript
window.addEventListener('keyup', (evt) => {
// `evt.key` = 按鍵字串
if (evt.key === 'ArrowUp') {
// 做某事
}
})
```
鍵盤事件有兩個屬性來判別被按壓的按鍵:
- `key`,使用字串名稱表達該按鍵,例如: `ArrowUp`
- `keyCode`,使用數字呈現,例如 `37` 會對應到 `ArrowLeft`
✅ 除了遊戲開發以外,鍵盤事件也是十分實用的功能。你能想到其他使用相同技術的應用嗎?
### 特殊按鍵之限制
有許多*特殊*按鍵會影響視窗。這代表若我們正監聽著 `keyup` 事件,這個按鍵同時也會執行視窗的滾動行為。某些時候你會需要*關閉*這些瀏覽器中預設的行為,好比是建立這款遊戲時。你需要下列的程式:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // 方向鍵
case 32:
e.preventDefault();
break; // 空白鍵
default:
break; // 不阻止其他按鍵
}
};
window.addEventListener('keydown', onKeyDown);
```
上述的程式碼能確保方向鍵與空白鍵關閉*預設*的行為。這個*關閉*機制會在我們呼叫 `e.preventDefault()` 時觸發。
## 遊戲內建的移動
我們可以讓物件自己移動,利用計時器如 `setTimeout()` 或是 `setInterval()` 這兩個函式,隨著秒數間隔更新物件的位置。如下方呈現:
```javascript
let id = setInterval(() => {
// 在 y 軸上移動敵人
enemy.y += 10;
})
```
## 遊戲迴圈
遊戲迴圈是個重要概念,定期地呼叫必須執行的函式。之所以被稱作遊戲迴圈也是基於所有東西會在一個迴圈中呈現給玩家。遊戲迴圈會利用到所有的遊戲物件,並依據各個情況與理由決定是否要繪製出它們。舉例來說,當一個敵人被雷射擊中,爆炸了。他就不應該存在於現在的遊戲迴圈中。你會在後續的課程學到更多此概念。
這是一個遊戲迴圈的基本格式,以程式碼表達如下:
```javascript
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
```
上述的迴圈會每 `200` 毫秒重新繪製 Canvas。你能自由地判斷哪種時長更適合套用在你的遊戲中。
## 繼續我們的太空遊戲
你會利用現有的程式碼來擴增我們的專案。你可以使用你在 Part I 完成的程式,或是使用 [Part II - Starter](../your-work) 這包程式。
- **移動英雄**:你需要加入程式,確保你可以使用方向鍵來移動主角。
- **移動敵人**:你也需要加入程式,確保敵人能定期地由上往下移動。
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能呈現英雄以及所有的敵人,但它們還沒辦法移動!
### 加入程式碼
1. **加入特定物件** `hero`、`enemy` 和 `game object`,它們皆有 `x``y` 位置屬性。(記得課程[繼承與組合](../../README.zh-tw.md)中的片段)。
*提示* `game object` 要有 `x``y`,以及繪製到畫布上的能力。
>要點:開始建立 GameObject class ,結構如下所示,再繪製到畫布上:
```javascript
class GameObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.dead = false;
this.type = "";
this.width = 0;
this.height = 0;
this.img = undefined;
}
draw(ctx) {
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
```
現在,延伸 GameObject 來建立英雄與敵人。
```javascript
class Hero extends GameObject {
constructor(x, y) {
...it needs an x, y, type, and speed
}
}
```
```javascript
class Enemy extends GameObject {
constructor(x, y) {
super(x, y);
(this.width = 98), (this.height = 50);
this.type = "Enemy";
let id = setInterval(() => {
if (this.y < canvas.height - this.height) {
this.y += 5;
} else {
console.log('Stopped at', this.y)
clearInterval(id);
}
}, 300)
}
}
```
2. **加入鍵盤事件處理器**以處理鍵盤輸入(移動英雄的上下左右)
*記住* 這是笛卡爾座標系,左上方為 `0,0`。也請記得關閉鍵盤的*預設行為*
>要點:建立函式 onKeyDown 並連接到視窗中:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
...add the code from the lesson above to stop default behavior
}
};
window.addEventListener("keydown", onKeyDown);
```
這時候檢查你的瀏覽器命令欄,看看是否能偵測到鍵盤輸入。
3. **建立**[發布訂閱模式](../../README.zh-tw.md),這能讓剩下的程式段落保持乾淨。
要做到此步驟,你可以:
1. **建立視窗的事件監聽者**
```javascript
window.addEventListener("keyup", (evt) => {
if (evt.key === "ArrowUp") {
eventEmitter.emit(Messages.KEY_EVENT_UP);
} else if (evt.key === "ArrowDown") {
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
} else if (evt.key === "ArrowLeft") {
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
} else if (evt.key === "ArrowRight") {
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
}
});
```
1. **建立 EventEmitter class** 以發布及訂閱訊息:
```javascript
class EventEmitter {
constructor() {
this.listeners = {};
}
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach((l) => l(message, payload));
}
}
}
```
1. **建立常數**並設定 EventEmitter
```javascript
const Messages = {
KEY_EVENT_UP: "KEY_EVENT_UP",
KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};
let heroImg,
enemyImg,
laserImg,
canvas, ctx,
gameObjects = [],
hero,
eventEmitter = new EventEmitter();
```
1. **初始化遊戲**
```javascript
function initGame() {
gameObjects = [];
createEnemies();
createHero();
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
hero.y -=5 ;
})
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
hero.y += 5;
});
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
hero.x -= 5;
});
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
hero.x += 5;
});
}
```
1. **設定遊戲迴圈**
重構函式 window.onload 來初始化遊戲,設定遊戲迴圈的定時間隔。你還需要加入雷射光:
```javascript
window.onload = async () => {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
heroImg = await loadTexture("assets/player.png");
enemyImg = await loadTexture("assets/enemyShip.png");
laserImg = await loadTexture("assets/laserRed.png");
initGame();
let gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGameObjects(ctx);
}, 100)
};
```
5. **加入程式**來定期地移動敵人
重構函式 `createEnemies()` 以建立敵人們,接到 gameObjects 中:
```javascript
function createEnemies() {
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
const enemy = new Enemy(x, y);
enemy.img = enemyImg;
gameObjects.push(enemy);
}
}
}
```
新增函式 `createHero()` 來為英雄做相同的事情。
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
最後,建立函式 `drawGameObjects()` 以開始繪製:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
你的敵人開始會朝你的英雄艦艇前進!
---
## 🚀 挑戰
如你所見,在加入零零總總的函式、變數與 class 後,你的程式變成了「麵條式代碼(spaghetti code)」。你能有效的編排你的程式,讓它更容易被閱讀?勾劃出一個系統來組織你的程式碼,即使所有東西都在一個檔案中。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/34)
## 複習與自學
我們並沒有使用框架(frameworks)來編寫我們的遊戲,現在有許多 JavaScript 基底的 Canvas 框架,提供給遊戲開發使用。花點時間[閱讀這些框架](https://github.com/collections/javascript-game-engines)。
## 作業
[為你的程式做註解](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 為你的程式做註解
## 簡介
打開遊戲資料夾中目前的 /app.js 檔案,試著幫它做上註解並整理乾淨。程式碼很容易脫離掌控,現在是個好機會來確保你的程式是容易去閱讀的,在未來還可以被使用。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ----------------------------- | ----------------------- | ----------------------- |
| | `app.js` 完整地註解且分塊整理 | `app.js` 有做充分的註解 | `app.js` 凌亂且缺乏註解 |

@ -0,0 +1,297 @@
# 建立太空遊戲 Part 4加入雷射與碰撞偵測
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/35)
這堂課中,你會學會如何在 JavaScript 上發射雷射光!我們需要在遊戲中新增兩項東西:
- **雷射光**:這束雷射會從英雄艦艇垂直往上移動
- **碰撞偵測**,除了完成*射擊*這項能力,我們還會新增幾項遊戲規則:
- **雷射擊中敵人**:被雷射擊中的敵人會死亡
- **雷射擊中畫面最上方**:雷射擊中到畫面最上方會消失
- **敵人碰觸到英雄**:敵人與英雄在互相碰觸時皆會被摧毀
- **敵人碰觸到畫面最下方**:敵人碰觸到畫面最下方時,該敵人與英雄會被摧毀
簡單來說,你這位*英雄*需要在敵人到達畫面最下方之前,使用雷射擊毀它們。
✅ 做點關於第一款電腦遊戲的研究。它有哪些功能?
讓我們成為英雄吧!
## 碰撞偵測
我們該如何進行碰撞偵測呢?我們需要將遊戲物件視為移動中的矩形。你或許會問為什麼?這是因為,繪製遊戲物件的圖片皆為矩形:它有一組 `x``y`、`width` 與 `height`
若兩個矩形,舉例來說:英雄與敵人*相交*了,這就是一次碰撞。至於會發生什麼事取決於遊戲規則。要製作碰撞偵測,你需要這些步驟:
1. 取得表達遊戲物件為矩形的方法,好比是:
```javascript
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width
}
}
```
2. 一個比較函式,這個函式可以如下:
```javascript
function intersectRect(r1, r2) {
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
```
## 我們該如何摧毀物件
要摧毀遊戲物件,你需要讓遊戲知道,它不再需要在定期的遊戲迴圈中繪製該物件。一種方法是在情況發生時,我們可以標記遊戲物件為*死亡*,例如:
```javascript
// 碰撞發生
enemy.dead = true
```
接著,你在重新繪製畫面前,排除掉這些*死亡*的物件,例如:
```javascript
gameObjects = gameObject.filter(go => !go.dead);
```
## 我們該如何發射雷射
發射雷射可以被視作回應一件鍵盤事件,並建立往特定方向移動的物件。因此我們需要列出下列步驟:
1. **建立雷射物件**:從英雄艦艇的正上方,建立往畫面上方移動的物件。
2. **連接該程式到鍵盤事件**:我們需要在鍵盤中挑選一個按鍵,表達玩家發射雷射光。
3. 在按鍵按壓時,**建立看起來像雷射光的遊戲物件**。
## 雷射的冷卻時間
在每次按壓按鍵時,好比說*空白鍵*,雷射光都需要被發射出來。為了讓遊戲不要在短時間內發射太多組雷射光,我們需要修正它。修法為建立*冷卻時間* ── 一個計時器確保雷射在一定期間內只能被發射一次。你可以藉由下列方式建立:
```javascript
class Cooldown {
constructor(time) {
this.cool = false;
setTimeout(() => {
this.cool = true;
}, time)
}
}
class Weapon {
constructor {
}
fire() {
if (!this.cooldown || this.cooldown.cool) {
// 產生雷射光
this.cooldown = new Cooldown(500);
} else {
// 什麼事都不做,冷卻中。
}
}
}
```
✅ 根據太空遊戲系列課程的第一章,回想關於*冷卻時間*。
## 建立目標
你會利用上一堂課中現成的程式碼(你應該有整理並重構過)做延伸。使用來自 Part II 的檔案或是使用 [Part III - Starter](../your-work)。
> 要點:你需要使用的雷射光已經在資料夾中,並已匯入到程式碼中。
- **加入碰撞偵測**,建立下列規則給各個雷射碰觸到東西的情況:
1. **雷射擊中敵人**:被雷射擊中的敵人會死亡
2. **雷射擊中畫面最上方**:雷射擊中到畫面最上方會消失
3. **敵人碰觸到英雄**:敵人與英雄在互相碰觸時皆會被摧毀
4. **敵人碰觸到畫面最下方**:敵人碰觸到畫面最下方時,該敵人與英雄會被摧毀
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能呈現英雄以及所有的敵人,但它們還沒辦法移動!
### 建立程式碼
1. **設定表達遊戲物件為矩形的方法,以處理碰撞狀況** 下列的程式表達 `GameObject` 的矩形呈現方式。編輯你的 GameObject class
```javascript
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width,
};
}
```
2. **加入程式碼來檢查碰撞** 這會是新函式來測試兩矩形是否相交:
```javascript
function intersectRect(r1, r2) {
return !(
r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top
);
}
```
3. **加入雷射發射功能**
1. **加入鍵盤事件訊息**。 *空白鍵*要能在英雄艦艇上方建立雷射光。加入三個常數到 Messages 物件中:
```javascript
KEY_EVENT_SPACE: "KEY_EVENT_SPACE",
COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER",
COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO",
```
1. **處理空白鍵**。 編輯 `window.addEventListener` 的 keyup 函式來處理空白鍵:
```javascript
} else if(evt.keyCode === 32) {
eventEmitter.emit(Messages.KEY_EVENT_SPACE);
}
```
1. **加入監聽者**。 編輯函式 `initGame()` 來確保英雄可以在空白鍵按壓時,發射雷射光:
```javascript
eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
if (hero.canFire()) {
hero.fire();
}
```
建立新的函式 `eventEmitter.on()` 確保敵人碰觸到雷射光時,能更新死亡狀態:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
})
```
1. **移動物件**。 確保雷射逐步地向畫面上方移動。建立新的 class Laser 延伸自 `GameObject`,你應該有做過:
```javascript
class Laser extends GameObject {
constructor(x, y) {
super(x,y);
(this.width = 9), (this.height = 33);
this.type = 'Laser';
this.img = laserImg;
let id = setInterval(() => {
if (this.y > 0) {
this.y -= 15;
} else {
this.dead = true;
clearInterval(id);
}
}, 100)
}
}
```
1. **處理碰撞**。 建立雷射的碰撞規則。加入函式 `updateGameObjects()` 來確認被碰撞的物件。
```javascript
function updateGameObjects() {
const enemies = gameObjects.filter(go => go.type === 'Enemy');
const lasers = gameObjects.filter((go) => go.type === "Laser");
// 雷射擊中某物
lasers.forEach((l) => {
enemies.forEach((m) => {
if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
first: l,
second: m,
});
}
});
});
gameObjects = gameObjects.filter(go => !go.dead);
}
```
記得在 `window.onload` 裡的遊戲迴圈中加入 `updateGameObjects()`
4. **設定雷射的冷卻時間**,它只能在定期內發射一次。
最後,編輯 Hero class 來允許冷卻:
```javascript
class Hero extends GameObject {
constructor(x, y) {
super(x, y);
(this.width = 99), (this.height = 75);
this.type = "Hero";
this.speed = { x: 0, y: 0 };
this.cooldown = 0;
}
fire() {
gameObjects.push(new Laser(this.x + 45, this.y - 10));
this.cooldown = 500;
let id = setInterval(() => {
if (this.cooldown > 0) {
this.cooldown -= 100;
} else {
clearInterval(id);
}
}, 200);
}
canFire() {
return this.cooldown === 0;
}
}
```
到這裡,你的遊戲有了些功能!你可以測試方向鍵,使用空白鍵發射雷射。當你擊中敵人時它們會消失。幹得漂亮!
---
## 🚀 挑戰
加入爆炸特效! 看看 [Space Art Repo](../solution/spaceArt/readme.txt) 中的檔案,試著在雷射擊中外星人時,加入爆炸畫面。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/36)
## 複習與自學
對遊戲中的迴圈定時做點實驗。當你改變數值時,發生了什麼事?閱讀更多關於 [JavaScript 計時事件](https://www.freecodecamp.org/news/javascript-timing-events-settimeout-and-setinterval/)。
## 作業
[探索碰撞](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 探索碰撞
## 簡介
為了更了解碰撞是如何運作的,建立一款小遊戲包含物件的碰撞。利用鍵盤或是滑鼠來移動物件,當物件碰撞時執行某些行為。它可以像是彗星撞地球,或是碰碰車。發揮你的創意!
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------------------------------------------- | ------------------ | ---------- |
| | 建立出完整的程式:有在畫面上繪製物件、有基本的碰撞與對應的行為 | 程式有部分尚未完成 | 程式有瑕疵 |

@ -0,0 +1,189 @@
# 建立太空遊戲 Part 5分數與生命數
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/37)
在這堂課中,你會學習如何為遊戲加入計分功能與計算性命數。
## 在畫面上繪製文字
為了在畫面上顯示遊戲分數,你需要了解如何配置文字。答案是在 Canvas 物件上使用方法 `fillText()`。你也可以控制其他特徵,例如文字字型、文字顏色甚至文字對齊方向(左、右、置中)。下面是在畫面中繪製一些文字。
```javascript
ctx.font = "30px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "right";
ctx.fillText("show this on the screen", 0, 0);
```
✅ 閱讀更多關於[在畫布上建立文字](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text),你可以自由地豐富你的文字!
## 性命,一個遊戲概念
遊戲中的生命概念只是一個數字。在太空遊戲中,在你的船艦受到攻擊時,扣除生命值是一種常見的方式。如果能以圖像的方式顯示生命值也是很好的方式,例如船艦圖示、心臟圖像,而非單純使用數字。
## 建立目標
我們在遊戲中新增下列功能:
- **遊戲分數**:當每一艘敵軍艦艇被摧毀時,英雄應該取得一些獎勵分數,我們建議一艘敵艦一百分。遊戲總分應該顯示在畫面的左下角。
- **生命值**:你的船艦有三條性命。在每一次敵軍艦艇撞擊你時,你會損失一條性命。生命數應該顯示在畫面的右下角,以下列圖像顯示出來。 ![性命圖片](../solution/assets/life.png)
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能顯示出英雄與所有的敵人,在你操作方向鍵後,英雄能移動並擊落敵人。
### 加入程式碼
1. 從資料夾 `solution/assets/` **複製你需要的檔案** 到資料夾 `your-work` 中。你會加入檔案 `life.png`。在函式 window.onload 中加入 lifeImg
```javascript
lifeImg = await loadTexture("assets/life.png");
```
1. 在檔案清單中加入 `lifeImg`
```javascript
let heroImg,
...
lifeImg,
...
eventEmitter = new EventEmitter();
```
2. **新增變數**。 加入程式碼表達你的遊戲總分(0)和剩餘性命(3),並顯示在畫面上。
3. **擴增函式 `updateGameObjects()`**。 擴增函式 `updateGameObjects()` 來處理敵軍碰撞:
```javascript
enemies.forEach(enemy => {
const heroRect = hero.rectFromGameObject();
if (intersectRect(heroRect, enemy.rectFromGameObject())) {
eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
}
})
```
4. **加入 `life``points`**.
1. **初始化變數**。 在 `Hero` class 的 `this.cooldown = 0` 下方,設定性命與分數:
```javascript
this.life = 3;
this.points = 0;
```
1. **在畫面上顯示變數內容**。 在畫面上繪製這些數值:
```javascript
function drawLife() {
// TODO, 35, 27
const START_POS = canvas.width - 180;
for(let i=0; i < hero.life; i++ ) {
ctx.drawImage(
lifeImg,
START_POS + (45 * (i+1) ),
canvas.height - 37);
}
}
function drawPoints() {
ctx.font = "30px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "left";
drawText("Points: " + hero.points, 10, canvas.height-20);
}
function drawText(message, x, y) {
ctx.fillText(message, x, y);
}
```
1. **在遊戲迴圈中加入呼叫**。 請確保你加入這些函式到 `updateGameObjects()` 下方的 window.onload 內:
```javascript
drawPoints();
drawLife();
```
1. **制定遊戲規則**。 制定下列的遊戲規則:
1. **在英雄與敵人發生碰撞時**,扣除一條生命。
擴增 `Hero` class 來執行這段減法:
```javascript
decrementLife() {
this.life--;
if (this.life === 0) {
this.dead = true;
}
}
```
2. **當雷射擊中敵人時**,增加遊戲總分一百分。
擴增 Hero class 來執行這段加法:
```javascript
incrementPoints() {
this.points += 100;
}
```
加入這些函式到碰撞事件發送器中:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints();
})
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife();
});
```
✅ 做點研究,探索其他使用到 JavaScript 與 Canvas 的遊戲。他們有什麼共同特徵?
在這個工作的尾聲,你應該能在右下方看到「性命」小船;左下方看到遊戲總分。當你碰撞到敵人時會扣除生命;當你擊落敵人時會增加分數。做得好!你的遊戲就快完成了。
---
## 🚀 挑戰
你的程式就快完成了。你能預測到下一步嗎?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/38)
## 複習與自學
研究其他種增加與減少分數與生命數的方法。這邊有一些有趣的遊戲引擎,例如:[PlayFab](https://playfab.com)。使用這些引擎能如何增進你的遊戲?
## 作業
[建立計分遊戲](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 建立計分遊戲
## 簡介
建立一款遊戲,能有創意地顯示生命值與分數。建議上能在畫面的正下方,以心型圖示顯示生命值,或是斗大的數字顯示分數。看看這些[免費的遊戲資源](https://www.kenney.nl/)。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------- | ---------------- | ------------ |
| | 呈現出完整的遊戲 | 遊戲提供部分內容 | 遊戲存在問題 |

@ -0,0 +1,222 @@
# 建立太空遊戲 Part 6結束與重來
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/39)
有許多方式可以表達遊戲中的*結束狀態*。這都取決於你這位遊戲開發者,定義遊戲結束的理由。假設我們討論這款已經開發許久的太空遊戲,以下是遊戲結束的理由:
- **`N` 艘敵軍艦艇被擊毀**:如果你想將遊戲分成許多關卡,一種常見的方式是將每一關的破關門檻,定為擊毀 `N` 艘敵軍艦艇。
- **你的船艦已被擊毀**:一定有遊戲,只要你的船艦被擊毀一次時,便判定你輸了這場遊戲。另一種可行概念是加入生命值系統。每次你的船艦被擊毀時,會扣除一條生命。一但你損失了所有性命,你便輸了這場遊戲。
- **你已經取得 `N` 點分數**:另一種常見的結束狀態為分數門檻。取得分數的機制取決在你,常見的條件為摧毀敵艦、或是收集敵艦所*掉落*的道具。
- **完成關卡**:這或許會涉及到許多種狀態,好比說: `X` 艘艦艇已被擊毀、已取得 `Y` 點分數或是收集特定的道具。
## 重新遊戲
如果玩家很享受你的遊戲,他們會想再重新遊玩一次。一旦因任何原因結束遊戲時,你應該要提供重新遊戲的方法。
✅ 想想看,什麼條件下會結束一款遊戲,而它們又是如何提示你重新遊玩。
## 建立目標
你需要為你的遊戲新增這些規則:
1. **贏得遊戲**。 一旦所有敵軍艦艇被擊毀時,你便贏得這場遊戲。請額外地顯示勝利訊息。
1. **重新開始**。 一旦你損失了所有性命,或是贏得了勝利,你應該提供方法來重新遊戲。記住!你需要重新初始化你的遊戲,所有遊戲的歷史紀錄會被移除。
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| life.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址。你的遊戲應該能被遊玩。
> 要點: 要避免在 Visual Studio Code 裡出現警告訊息,編輯函式 `window.onload` 以 is而非 let 的方式呼叫 `gameLoopId`;並在檔案正上方獨立地宣告 gameLoopId `let gameLoopId;`
### 加入程式碼
1. **追蹤結束狀態**。 新增程式碼來追蹤敵人的數量,利用下列函式判斷英雄艦艇是否被擊毀:
```javascript
function isHeroDead() {
return hero.life <= 0;
}
function isEnemiesDead() {
const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead);
return enemies.length === 0;
}
```
1. **加入訊息處理器**。 編輯 `eventEmitter` 以處理這些狀態:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints();
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife();
if (isHeroDead()) {
eventEmitter.emit(Messages.GAME_END_LOSS);
return; // 遊戲失敗,提前結束
}
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.GAME_END_WIN, () => {
endGame(true);
});
eventEmitter.on(Messages.GAME_END_LOSS, () => {
endGame(false);
});
```
1. **加入新的訊息**。 新增這些訊息到 Messages 常數中:
```javascript
GAME_END_LOSS: "GAME_END_LOSS",
GAME_END_WIN: "GAME_END_WIN",
```
2. **加入重新開始的功能** 在按下特定按鈕後,程式會重新開始遊戲。
1. **監聽 `Enter` 按鈕之按壓**。 編輯視窗的 eventListener ,監聽按鍵的按壓:
```javascript
else if(evt.key === "Enter") {
eventEmitter.emit(Messages.KEY_EVENT_ENTER);
}
```
1. **加入重新遊戲的訊息**。 加入這段訊息到 Messages 常數中:
```javascript
KEY_EVENT_ENTER: "KEY_EVENT_ENTER",
```
1. **制定遊戲規則**。 編制下列的遊戲規則:
1. **玩家勝利條件**。 當所有敵軍艦艇被擊毀時,顯示勝利訊息。
1. 首先,建立函式 `displayMessage()`
```javascript
function displayMessage(message, color = "red") {
ctx.font = "30px Arial";
ctx.fillStyle = color;
ctx.textAlign = "center";
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
}
```
1. 建立函式 `endGame()`
```javascript
function endGame(win) {
clearInterval(gameLoopId);
// 設定延遲以確保所有圖像皆繪製完成
setTimeout(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (win) {
displayMessage(
"Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew",
"green"
);
} else {
displayMessage(
"You died !!! Press [Enter] to start a new game Captain Pew Pew"
);
}
}, 200)
}
```
1. **重新遊戲的邏輯**。 當玩家損失所有的性命,或是贏下這場遊戲,顯示遊戲重來的提示。此外,在*重新遊玩*按鍵被按壓時,重新遊戲(你可以自己決定任一個鍵盤按鍵)。
1. 建立函式 `resetGame()`
```javascript
function resetGame() {
if (gameLoopId) {
clearInterval(gameLoopId);
eventEmitter.clear();
initGame();
gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPoints();
drawLife();
updateGameObjects();
drawGameObjects(ctx);
}, 100);
}
}
```
1. 在 `initGame()` 內呼叫 `eventEmitter` 來重新設定遊戲:
```javascript
eventEmitter.on(Messages.KEY_EVENT_ENTER, () => {
resetGame();
});
```
1. 在 EventEmitter 加入函式 `clear()`
```javascript
clear() {
this.listeners = {};
}
```
👽 💥 🚀 恭喜你,艦長!你的遊戲已經完成了!幹得好! 🚀 💥 👽
---
## 🚀 挑戰
加入遊戲音效!你能加入音效來提升遊戲品質嗎?或許在雷射擊中敵人,或是在英雄死亡、勝利時發出音效。看看這套[沙盒](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_audio_play),了解如何使用 JavaScript 播放音效。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/40)
## 複習與自學
你的功課是建立一款新的小遊戲。去探索一些有趣的遊戲,決定你想建造的遊戲類型。
## 作業
[建立一款遊戲](assignment.zh-tw.md)

@ -0,0 +1,19 @@
# 建立一款遊戲
## 簡介
試著建立一款小遊戲,實際應用不同的終止狀態。做一些變化,如取得的分數、英雄血量或是被擊敗的怪物。可以是很簡單的類型,例如文字冒險遊戲。請參考下列的遊戲流程作為啟發:
```
英雄> 使用寶劍攻擊 - 獸人受到了 3 點傷害
獸人> 使用棍棒攻擊 - 英雄受到了 2 點傷害
英雄> 踢擊 - 獸人受到了 1 點傷害
遊戲訊息> 獸人已被擊敗 - 英雄取得 2 枚硬幣
遊戲訊息> ****已殲滅所有怪獸,你已佔領了邪惡堡壘****
```
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------- | ---------------- | ------------ |
| | 呈現出完整的遊戲 | 遊戲提供部分內容 | 遊戲存在問題 |

@ -0,0 +1,31 @@
# 建立一款太空遊戲
一款太空遊戲能教會你更多 JavaScript 的進階概念
這系列課程中,你會學習如何建立屬於自己的太空遊戲。如果你遊玩過遊戲「太空侵略者」,這款遊戲有相同的套路:操控太空船並擊落由上接近的怪物。這是遊戲完成後的模樣:
![遊戲成品](../images/pewpew.gif)
這六堂課程中,你會學習:
- **使用** Canvas 元素來在畫面上繪製物件
- **了解**笛卡爾座標系
- **學習**發布與訂閱設計模式,建立容易維護及擴增的遊戲結構
- **利用** Async/Await 來讀取遊戲資源
- **處理**鍵盤事件
## 總覽
- 理論
- [利用 JavaScript 設計遊戲](../1-introduction/translations/README.zh-tw.md)
- 實作
- [在畫布上繪製](../2-drawing-to-canvas/translations/README.zh-tw.md)
- [移動畫面上之物件](../3-moving-elements-around/translations/README.zh-tw.md)
- [碰撞偵測](../4-collision-detection/translations/README.zh-tw.md)
- [持續得分](../5-keeping-score/translations/README.zh-tw.md)
- [結束與重新遊戲](../6-end-condition/translations/README.zh-tw.md)
## 參與人員
遊戲資源皆來自於 https://www.kenney.nl/。
如果你熱愛設計遊戲,這邊有許多實用的資源,許多資源是免費的,有些則是付費內容。
Loading…
Cancel
Save