- 原文地址:How to build a simple game in the browser with Phaser 3 and TypeScript
- 原文作者:Mariya Davydova
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:iceytea
- 校對者:wznonstop、BYChoo
照片由 Phil Botha 拍攝併釋出於 Unsplash
我是個後端開發,我的前端開發專業知識相對較弱。前一段時間我想找點樂子 —— 在瀏覽器中製作遊戲;我選擇 Phaser 3 框架(它現在看起來非常流行)和 TypeScript 語言(因為我更喜歡靜態型別語言而不是動態型別語言)。事實證明,你需要做一些無聊的事情才能使它正常工作,所以我寫了這個教程來幫助像我這樣的其他人更快地開始。
準備開發環境
IDE
選擇你的開發環境。如果你願意,你可以隨時使用普通的舊記事本,但我建議你使用更有幫助的 IDE。至於我,我更喜歡在 Emacs 中開發拿手的專案,因此我安裝了 tide 並按照說明進行設定。
Node
如果我們使用 JavaScript 進行開發,那麼無需這些準備步驟就可以開始編碼。但是,由於我們想要使用 TypeScript,我們必須設定基礎架構以儘可能快地進行未來的開發。因此我們需要安裝 node 和 npm 。
在我編寫本教程時,我使用 node 10.13.0 和 npm 6.4.1。請注意,前端世界中的版本更新速度非常快,因此你只需使用最新的穩定版本。我強烈建議你使用 nvm 而不是手動安裝 node 和 npm,這會為你節省大量的時間和精力。
搭建專案
專案結構
我們將使用 npm 來構建專案,因此要啟動專案,請轉到空資料夾並執行npm init
。 npm 會問你關於專案屬性的幾個問題,然後建立一個package.json
檔案。它看起來像這樣:
{
"name": "Starfall",
"version": "0.1.0",
"description": "Starfall game (Phaser 3 + TypeScript)",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Mariya Davydova",
"license": "MIT"
}
複製程式碼
軟體包
使用以下命令安裝我們需要的軟體包:
npm install -D typescript webpack webpack-cli ts-loader phaser live-server
複製程式碼
-D
選項(完整寫法 --save-dev
)使 npm 自動將這些包新增到 package.json
中的 devDependencies 列表中:
"devDependencies": {
"live-server": "^1.2.1",
"phaser": "^3.15.1",
"ts-loader": "^5.3.0",
"typescript": "^3.1.6",
"webpack": "^4.26.0",
"webpack-cli": "^3.1.2"
}
複製程式碼
Webpack
Webpack 將執行 TypeScript 編譯器,並將一堆生成的 JS 檔案以及庫收集到一個壓縮過的 JS 中,以便我們可以將它包含在頁面中。
在 package.json
附近新增 webpack.config.js
:
const path = require('path');
module.exports = {
entry: './src/app.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.ts', '.tsx', '.js' ]
},
output: {
filename: 'app.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development'
};
複製程式碼
在這裡我們看到 webpack 必須從 src/app.ts
開始獲取原始碼(我們將很快新增)並收集 dist/app.js
檔案中的所有內容。
TypeScript
我們還需要一個用於 TypeScript 編譯器的小配置(tsconfig.json
),其中我們描述了希望將原始碼編譯到哪個 JS 版本,以及在哪裡找到這些原始碼:
{
"compilerOptions": {
"target": "es5"
},
"include": [
"src/*"
]
}
複製程式碼
TypeScript 定義
TypeScript 是一種靜態型別語言。因此,它需要編譯的型別定義(.d.ts)。在編寫本教程時,Phaser 3 的定義尚未作為 npm 包提供,因此您可能需要從官方儲存庫中 下載它們,並將檔案放在專案的 src
子目錄中。
Scripts
我們幾乎完成了專案的設定。此時你應該建立 package.json
、webpack.config.js
和 tsconfig.json
,並新增 src/phaser.d.ts
。在開始編寫程式碼之前,我們需要做的最後一件事是解釋 npm 與專案有什麼關係。我們更新 package.json
的 scripts
部分,如下所示:
"scripts": {
"build": "webpack",
"start": "webpack --watch & live-server --port=8085"
}
複製程式碼
執行 npm build
時,webpack 將根據配置構建 app.js
檔案。當你執行 npm start
時,你不必費心去構建過程,只要對任何更新進行了儲存操作,webpack 就會重建應用程式;而 live-server 將在預設瀏覽器中重新載入它。該應用程式將託管在 http://127.0.0.1:8085/ 。
入門
既然我們已經建立了基礎設施(開始一個專案時我感到厭惡的環節),我們終於可以開始編碼了。在這一步中,我們將做一件簡單的事情:在瀏覽器視窗中繪製一個深藍色矩形。使用一個大型的遊戲開發框架是有點……嗯……太過分了。不過,我們還會在接下來的步驟中使用它。
讓我簡要解釋一下 Phaser 3 的主要概念。遊戲是 Phaser.Game
類(或其後代)的一個例項。每個遊戲都包含一個或多個 Phaser.Game
後代的例項。每個場景包含幾個物件(靜態或動態物件),並代表遊戲的邏輯部分。例如,我們瑣碎的遊戲將有三個場景:歡迎螢幕,遊戲本身和分數螢幕。
讓我們開始編碼吧。
首先,為遊戲建立一個簡單的 HTML 容器。建立一個 index.html
檔案,其中包含以下程式碼:
<!DOCTYPE html>
<html>
<head>
<title>Starfall</title>
<script src="dist/app.js"></script>
</head>
<body>
<div id="game"></div>
</body>
</html>
複製程式碼
這裡只有兩個基本部分:第一個是 script
標籤,表示我們將在這裡使用我們構建的檔案;第二個是 div
標籤,它將成為遊戲容器。
現在建立 src/app.ts
檔案並新增以下程式碼:
import "phaser";
const config: GameConfig = {
title: "Starfall",
width: 800,
height: 600,
parent: "game"
backgroundColor: "#18216D"
};
export class StarfallGame extends Phaser.Game {
constructor(config: GameConfig) {
super(config);
}
}
window.onload = () => {
var game = new StarfallGame(config);
};
複製程式碼
這段程式碼一目瞭然。GameConfig 有很多不同的屬性,你可以檢視 這裡 .
現在你終於可以執行 npm start
了。如果在此步驟和之前的步驟中完成所有操作,您應該在瀏覽器中看到一些簡單的內容:
讓星辰墜落吧
我們建立了一個基本應用程式。現在是時候新增一個會發生某些事情的場景。我們的遊戲很簡單:星星會掉到地上,目標就是捕捉儘可能多的星星。
為了實現這個目標,建立一個新檔案 gameScene.ts
,並新增以下程式碼:
import "phaser";
export class GameScene extends Phaser.Scene {
constructor() {
super({
key: "GameScene"
});
}
init(params): void {
// TODO
}
preload(): void {
// TODO
}
create(): void {
// TODO
}
update(time): void {
// TODO
}
};
複製程式碼
這裡的建構函式包含一個 key ,其他場景可以在其下呼叫此場景。
你在這裡看到四種方法的插樁。讓我簡要解釋一下它們之間的區別:
-
init([params])
在場景開始時被呼叫。這個函式可以通過呼叫scene.start(key, [params])
來接受從其他場景或遊戲傳遞的引數。 -
preload()
在建立場景物件之前被呼叫,它包含載入資源;這些資源將被快取,因此當重新啟動場景時,不會重新載入它們。 -
create()
在載入資源時被呼叫,並且通常包含主要遊戲物件(背景,玩家,障礙物,敵人等)的建立。 -
update([time])
在每個 tick 中被呼叫幷包含場景的動態部分(移動,閃爍等)的所有內容。
為了確保我們以後不會忘記這些,讓我們在 game.ts
中快速新增以下行:
import "phaser";
import { GameScene } from "./gameScene";
const config: GameConfig = {
title: "Starfall",
width: 800,
height: 600,
parent: "game",
scene: [GameScene],
physics: {
default: "arcade",
arcade: {
debug: false
}
},
backgroundColor: "#000033"
};
...
複製程式碼
我們的遊戲現在知道遊戲場景。如果遊戲配置包含一個場景列表,然後第一個場景開始時,遊戲開始。所有其他場景都被建立,但直到明確呼叫才開始。
我們還在這裡新增了 arcade physics(一種物理模型,這裡有一些例子),這裡需要用它使我們的星星下降。
現在我們可以把內容放在我們遊戲場景的骨架上。
首先,我們宣告一些必要的屬性和物件:
export class GameScene extends Phaser.Scene {
delta: number;
lastStarTime: number;
starsCaught: number;
starsFallen: number;
sand: Phaser.Physics.Arcade.StaticGroup;
info: Phaser.GameObjects.Text;
...
複製程式碼
然後,我們初始化數字:
init(/*params: any*/): void {
this.delta = 1000;
this.lastStarTime = 0;
this.starsCaught = 0;
this.starsFallen = 0;
}
複製程式碼
現在,我們載入幾個圖片:
preload(): void {
this.load.setBaseURL(
"https://raw.githubusercontent.com/mariyadavydova/" +
"starfall-phaser3-typescript/master/");
this.load.image("star", "assets/star.png");
this.load.image("sand", "assets/sand.jpg");
}
複製程式碼
在這之後,我們可以準備我們的靜態元件。我們將創造地球元件,星星將落在那裡,文字通知我們目前的分數:
create(): void {
this.sand = this.physics.add.staticGroup({
key: 'sand',
frameQuantity: 20
});
Phaser.Actions.PlaceOnLine(this.sand.getChildren(),
new Phaser.Geom.Line(20, 580, 820, 580));
this.sand.refresh();
this.info = this.add.text(10, 10, '',
{ font: '24px Arial Bold', fill: '#FBFBAC' });
}
複製程式碼
Phaser 3 中的一個組是一種建立一組您想要一起控制的物件的方法。有兩種型別的物件:靜態和動態。正如你可能猜到的那樣,靜態物體(地面,牆壁,各種障礙物)不會移動,動態物體(馬里奧,艦船,導彈)可以移動。
我們建立了一個靜態的地面組。那些碎片沿著線放置。請注意,該線分為 20 個相等的部分(不是您可能預期的 19 個),並且地磚位於左端的每個部分,瓷磚中心位於該點(我希望這些能讓你明白那些數字的意思)。我們還必須呼叫 refresh()
來更新組邊界框,否則將根據預設位置(場景的左上角)檢查衝突。
如果您現在在瀏覽器中檢視應用程式,您應該會看到如下內容:
我們終於達到了這個場景中最具活力的部分 —— update()
函式,其中星星落下。此函式在 60ms 內呼叫一次。我們希望每秒發出一顆新的流星。我們不會為此使用動態組,因為每個星的生命週期都很短:它會被使用者點選或與地面碰撞而被摧毀。因此,在 emitStar()
函式中,我們建立一個新的星並新增兩個事件的處理:onClick()
和onCollision()
。
update(time: number): void {
var diff: number = time - this.lastStarTime;
if (diff > this.delta) {
this.lastStarTime = time;
if (this.delta > 500) {
this.delta -= 20;
}
this.emitStar();
}
this.info.text =
this.starsCaught + " caught - " +
this.starsFallen + " fallen (max 3)";
}
private onClick(star: Phaser.Physics.Arcade.Image): () => void {
return function () {
star.setTint(0x00ff00);
star.setVelocity(0, 0);
this.starsCaught += 1;
this.time.delayedCall(100, function (star) {
star.destroy();
}, [star], this);
}
}
private onFall(star: Phaser.Physics.Arcade.Image): () => void {
return function () {
star.setTint(0xff0000);
this.starsFallen += 1;
this.time.delayedCall(100, function (star) {
star.destroy();
}, [star], this);
}
}
private emitStar(): void {
var star: Phaser.Physics.Arcade.Image;
var x = Phaser.Math.Between(25, 775);
var y = 26;
star = this.physics.add.image(x, y, "star");
star.setDisplaySize(50, 50);
star.setVelocity(0, 200);
star.setInteractive();
star.on('pointerdown', this.onClick(star), this);
this.physics.add.collider(star, this.sand,
this.onFall(star), null, this);
}
複製程式碼
最後,我們有了一個遊戲!但是它還沒有勝利條件。我們將在教程的最後部分新增它。
把它全部包裝好
通常,遊戲由幾個場景組成。即使遊戲很簡單,你也需要一個開始場景(至少包含 Play 按鈕)和一個結束場景(顯示遊戲會話的結果,如得分或達到的最高等級)。讓我們將這些場景新增到我們的應用程式中。
在我們的例子中,它們將非常相似,因為我不想過多關注遊戲的圖形設計。畢竟,這是一個程式設計教程。
歡迎場景將在 welcomeScene.ts
中包含以下程式碼。請注意,當使用者點選此場景中的某個位置時,將顯示遊戲場景。
import "phaser";
export class WelcomeScene extends Phaser.Scene {
title: Phaser.GameObjects.Text;
hint: Phaser.GameObjects.Text;
constructor() {
super({
key: "WelcomeScene"
});
}
create(): void {
var titleText: string = "Starfall";
this.title = this.add.text(150, 200, titleText,
{ font: '128px Arial Bold', fill: '#FBFBAC' });
var hintText: string = "Click to start";
this.hint = this.add.text(300, 350, hintText,
{ font: '24px Arial Bold', fill: '#FBFBAC' });
this.input.on('pointerdown', function (/*pointer*/) {
this.scene.start("GameScene");
}, this);
}
};
複製程式碼
得分場景看起來幾乎相同,點選( scoreScene.ts
)後引導到歡迎場景。
import "phaser";
export class ScoreScene extends Phaser.Scene {
score: number;
result: Phaser.GameObjects.Text;
hint: Phaser.GameObjects.Text;
constructor() {
super({
key: "ScoreScene"
});
}
init(params: any): void {
this.score = params.starsCaught;
}
create(): void {
var resultText: string = 'Your score is ' + this.score + '!';
this.result = this.add.text(200, 250, resultText,
{ font: '48px Arial Bold', fill: '#FBFBAC' });
var hintText: string = "Click to restart";
this.hint = this.add.text(300, 350, hintText,
{ font: '24px Arial Bold', fill: '#FBFBAC' });
this.input.on('pointerdown', function (/*pointer*/) {
this.scene.start("WelcomeScene");
}, this);
}
};
複製程式碼
我們現在需要更新我們的主應用程式檔案:新增這些場景並使 WelcomeScene
成為列表中的第一個(譯者注:第一個位置會首先執行,類似於小程式的 page 列表):
import "phaser";
import { WelcomeScene } from "./welcomeScene";
import { GameScene } from "./gameScene";
import { ScoreScene } from "./scoreScene";
const config: GameConfig = {
...
scene: [WelcomeScene, GameScene, ScoreScene],
...
複製程式碼
你有沒有發現遺漏了什麼?是的,我們還沒有從任何地方呼叫 ScoreScene
!當玩家錯過第三顆星時(此時遊戲結束),我們來呼叫它:
private onFall(star: Phaser.Physics.Arcade.Image): () => void {
return function () {
star.setTint(0xff0000);
this.starsFallen += 1;
this.time.delayedCall(100, function (star) {
star.destroy();
if (this.starsFallen > 2) {
this.scene.start("ScoreScene",
{ starsCaught: this.starsCaught });
}
}, [star], this);
}
}
複製程式碼
最後,我們的 Starfall 遊戲看起來像一個真正的遊戲了 - 它可以開始、結束,甚至有一個分數排行榜(你可以捕獲多少顆星?)。
我希望這個教程對你來說和我寫的時候一樣有用?,任何反饋都非常感謝!
你可以在 這裡 找到本教程的原始碼。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。