[译] 如何使用 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索