[譯] 如何使用 Phaser 3 和 TypeScript 在瀏覽器中構建一個簡單的遊戲

照片由 Phil Botha 拍攝併發佈於 Unsplashhtml

我是個後端開發,個人前端開發專業知識相對較弱。前一段時間我想找點樂子 —— 在瀏覽器中製做遊戲;我選擇 Phaser 3 框架(它如今看起來很是流行)和 TypeScript 語言(由於我更喜歡靜態類型語言而不是動態類型語言)。事實證實,你須要作一些無聊的事情才能使它正常工做,因此我寫了這個教程來幫助像我這樣的其餘人更快地開始。前端

準備開發環境

IDE

選擇你的開發環境。若是你願意,你能夠隨時使用普通的舊記事本,但我建議你使用更有幫助的 IDE。至於我,我更喜歡在 Emacs 中開發拿手的項目,所以我安裝了 tide 並按照說明進行設置。node

Node

若是咱們使用 JavaScript 進行開發,那麼無需這些準備步驟就能夠開始編碼。可是,因爲咱們想要使用 TypeScript,咱們必須設置基礎架構以儘量快地進行將來的開發。所以咱們須要安裝 node 和 npm 。android

在我編寫本教程時,我使用 node 10.13.0npm 6.4.1。請注意,前端世界中的版本更新速度很是快,所以你只需使用最新的穩定版本。我強烈建議你使用 nvm 而不是手動安裝 node 和 npm,這會爲你節省大量的時間和精力。webpack

搭建項目

項目結構

咱們將使用 npm 來構建項目,所以要啓動項目,請轉到空文件夾並運行npm init。 npm 會問你關於項目屬性的幾個問題,而後建立一個package.json 文件。它看起來像這樣:ios

{
  "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"
}
複製代碼

軟件包

使用如下命令安裝咱們須要的軟件包:git

npm install -D typescript webpack webpack-cli ts-loader phaser live-server
複製代碼

-D 選項(完整寫法 --save-dev)使 npm 自動將這些包添加到 package.json 中的 devDependencies 列表中:github

"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 中,以便咱們能夠將它包含在頁面中。web

package.json 附近添加 webpack.config.jstypescript

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.jsonwebpack.config.jstsconfig.json,並添加 src/phaser.d.ts。在開始編寫代碼以前,咱們須要作的最後一件事是解釋 npm 與項目有什麼關係。咱們更新 package.jsonscripts 部分,以下所示:

"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 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索