如何在Phaser中编写一个Sapper并运行HTML5开发人员测试任务

下午好,亲爱的同事们!

我的名字叫亚历山大(Alexander),我是HTML5游戏的开发商。

在我发送简历的公司之一中,我被要求完成测试任务。 我同意,并在1天后根据TOR HTML5开发了该游戏。



因为我正在接受游戏编程方面的培训,并且为了更有效地使用我的代码,所以我决定在完成的项目上写一篇培训文章会很有用。 由于完成的测试得到了积极的评估并导致了面试的邀请,因此我的决定可能有权存在,并且可能会在将来对某人有所帮助。

本文将介绍足以成功完成开发人员HTML5职位的平均测试任务的工作量。 想要熟悉Phaser框架的任何人也可能会感兴趣。 并且,如果您已经在使用Phaser并用JS编写-请参阅如何在TypeScript中开发项目。

因此,在cat下有很多TypeScript代码!

引言


我们对此问题作简要说明。

  1. 我们将开发一个简单的HTML5游戏-一个经典的工具。
  2. 作为主要工具,我们将使用相位器3,打字稿和webpack。
  3. 该游戏将为桌面设计,并在浏览器中运行。

我们提供了最终项目的链接。


如果突然有人忘记了游戏规则,请回想一下这位工兵的技巧。 但是由于这种情况不太可能发生,因此将规则放置在破坏者的下方:)

拍手规则
比赛场地由布置在桌子中的单元格组成。 默认情况下,当游戏开始时,所有单元格都关闭。 炸弹放置在某些牢房中。

在封闭的单元格上单击鼠标左键时,它将打开。 如果一个开孔中有炸弹,则游戏以失败告终。

如果该单元格中没有炸弹,则会在其中显示一个数字,表明相对于当前打开的位置,相邻单元格中的炸弹数量。 如果附近没有炸弹,则牢房看起来是空的。

右键单击封闭的单元格可以在其上设置一个标志。 玩家的任务是安排所有可用的标记,以便标记所有已开采的单元格。 放置所有标志后,玩家在一个打开的单元格上按下鼠标左键以检查他是否获胜。

接下来,我们直接进入手册本身。 所有材料都分为小步骤,每个步骤都描述了在短时间内完成特定任务的过程。 因此,逐步执行小目标,最终我们将创建一个完整的游戏。 如果决定快速转到特定步骤,请使用目录。


1.准备


1.1项目模板


下载默认的相位器项目模板 。 这是框架作者推荐的模板,它为我们提供了以下目录结构:
index.html启动游戏的HTML页面
webpack /base.js为测试环境构建配置
prod.js为生产构建配置
src /资产/游戏资产(精灵,声音,字体)
index.js入口点
对于我们的项目,我们不需要当前的index.js文件,因此将其删除。 然后创建目录/src/scripts/ ,并将空的index.ts文件放入其中。 我们将所有脚本添加到此文件夹中。
还需要记住的是,在构建用于生产的项目时,将在根目录中创建一个dist目录,该目录将放置发行版本。

1.2构建配置


我们将使用webpack进行组装。 由于我们的模板最初是为使用JavaScript而准备的,并且我们使用TypeScript编写,因此我们需要对收集器的配置进行一些小的更改。

webpack/base.js添加entry键(用于指示构建我们的项目时的进入点),以及用于描述构建TS脚本规则的ts-loader配置:

 // webpack/base.js //... module.exports = { entry: './src/scripts/index.ts', // ... resolve: { extensions: [ '.ts', '.tsx', '.js' ] }, module: { rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, //... 

我们还需要在项目根目录中创建tsconfig.json文件。 对我来说,它具有以下内容:

 { "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] } 

1.3安装模块


从package.json安装所有依赖项,并向其中添加typescript和ts-loader模块:

 npm i npm i typescript --save-dev npm i ts-loader --save-dev 

现在该项目已准备好开始开发。 我们可以使用2条命令,这些命令已经在package.json文件的scripts属性中定义。

  1. 建立一个调试项目并通过本地服务器在浏览器中打开

     npm start 
  2. 运行要出售的版本,并将发行版本放在dist /文件夹中

     npm run build 

1.4资产准备


该游戏的所有资产均已从OpenGameArt (版本61x61)进行了诚实下载,并拥有名为Feel free to use的最友好的许可证,随附的页面会仔细地告诉我们)。 顺便说一句,本文中提供的代码具有相同的许可证! ;)

我从下载的集合中删除了时钟图像,并重命名了其余文件,以便获得易于使用的帧名称。 名称和相应文件的列表显示在下面的屏幕上。

根据生成的精灵,我们将在TexturePacker程序中创建一个Phaser JSONArray格式地图集(免费版本已经足够了,我还没有得到工作),并将生成的spritesheet.pngspritesheet.json文件放在src/assets/ project目录中。



2.创建场景


2.1入口点


我们通过创建webpack配置中描述的入口点开始开发。

 // src/scripts/index.ts import * as Phaser from "phaser"; new Phaser.Game({ type: Phaser.AUTO, parent: "minesweeper", width: window.innerWidth, height: window.innerHeight, backgroundColor: "#F0FFFF", scene: [] }); 

由于我们拥有的游戏是为台式机设计的,并且将填满整个屏幕,因此我们将widthheight字段大胆地使用浏览器的整个widthheight
scene字段当前为空数组,我们将对其进行修复!

2.2起始场景


src/scripts/scenes/StartScene.ts创建第一个场景的类:

 export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } } 

为了有效继承Phaser.Scene我们将场景名称作为参数传递给父类的构造函数。

该场景将结合资源预加载和开始屏幕的功能,邀请用户加入游戏。

通常,在我的项目中,玩家按顺序依次经历两个场景,然后进入开始场景:

 Boot => Preload => Start 

但是在这种情况下,游戏是如此简单,并且资产太少,因此没有理由将预载放到一个单独的场景中,甚至没有更多的理由来做初始的单独的Boot loader。

我们将以preload方法加载所有资产。 为了将来能够使用创建的图集,我们需要执行两个步骤:

  1. 使用require获取pngjson地图集文件:

     // StartScene.ts const spritesheetPng = require("./../../assets/spritesheet.png"); const spritesheetJson = require("./../../assets/spritesheet.json"); // ... 

  2. 将它们preload到开始场景的preload方法中:

     // StartScene.ts // ... public preload(): void { this.load.atlas("spritesheet", spritesheetPng, spritesheetJson); } // ... 


2.3起始场景的文字


在开始的场景中,还有两件事要做:

  1. 告诉玩家如何开始游戏
  2. 在玩家的主动下开始游戏

为了满足第一点,我们首先在场景文件的开头创建两个枚举,以描述文本及其样式:

 // StartScene.js enum Texts { Title = 'Minesweeper HTML5', Message = 'Click anywhere to start' } enum Styles { Color = '#008080', Font = 'Arial' } //... 

然后在create方法中将两个文本都创建为对象。 让我提醒您,只有在将所有资源加载到preload方法中之后,才会调用Phaser中场景的create方法,这非常适合我们。

 // StartScene.js //... public create(): void { this.add.text( this.cameras.main.centerX, this.cameras.main.centerY - 100, Texts.Title, {font: `52px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); this.add.text( this.cameras.main.centerX, this.cameras.main.centerY + 100, Texts.Message, {font: `28px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); } //... 

在另一个更大的项目中,我们可以将文本和样式添加到json语言环境文件中或单独的配置中,但是鉴于我们现在只有2行,我认为这一步骤是多余的,在这种情况下,我建议不要使我们的生活变得复杂,将自己限制在场景文件开头的列表中。

2.4过渡到游戏级别


在继续进行之前,我们在此场景中要做的最后一件事是跟踪鼠标单击事件,以将玩家启动到游戏中:

 // StartScene.js //... public create(): void { //... this.input.once('pointerdown', () => { this.scene.start('Game'); }); } //... 

2.5级场景


通过传递给this.scene.start方法的"Game"参数判断this.scene.start您已经猜到是时候创建第二个场景了,该场景将处理主要的游戏逻辑。 创建src/scripts/scenes/GameScene.ts

 export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } } 

在此场景中,我们不需要preload方法,因为 我们已经在上一个场景中加载了所有必要的资源。

2.6在入口处设置场景


现在已经创建了两个场景,将它们添加到我们的入口点
src/scripts/index.ts

 //... import { StartScene } from "./scenes/StartScene"; import { GameScene } from "./scenes/GameScene"; //... new Phaser.Game({ // ... scene: [StartScene, GameScene] }); 

3.游戏对象


因此, GameScene类将实现游戏级逻辑。 我们对精工游戏水平有何期待? 在视觉上,我们希望看到一个封闭的运动场。 我们知道该字段是一个表,这意味着它具有给定数量的行和列,其中一些炸弹放置在舒适的位置。 因此,我们有足够的信息来创建描述比赛场地的单独实体。

3.1游戏板


创建src/scripts/models/Board.ts在其中放置Board类:

 import { Field } from "./Field"; export class Board extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _rows: number = 0; private _cols: number = 0; private _bombs: number = 0; private _fields: Field[] = []; constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { super(); this._scene = scene; this._rows = rows; this._cols = cols; this._bombs = bombs; this._fields = []; } public get cols(): number { return this._cols; } public get rows(): number { return this._rows; } } 

让我们让该类成为Phaser.Events.EventEmitter的后继者,以便访问将来用于注册和调用事件的接口。

Field类的对象数组将存储在_fields私有属性中。 我们稍后将实现此模型。

我们设置了专用数字属性_rows_cols以指示_cols的行数和列数。 创建用于读取_rows_cols公共_cols

_bombs字段告诉我们该_bombs需要生成的炸弹数量。 在_scene参数中_scene我们将引用传递给GameScene游戏场景的对象,在其中我们将创建Board类的实例。

值得注意的是,我们仅将场景对象转移到模型中,以便进一步传输到视图,而在此处仅将其用于显示视图。 事实是,移相器直接使用场景对象渲染子画面,因此在创建子画面预制件时,我们有义务提供到当前场景的链接,我们将在以后进行开发。 对于我们自己,我们将接受以下协议:将链接转移到场景仅用于进一步用作显示引擎,并同意我们不会在模型和视图中直接调用场景的自定义方法。

一旦我们确定了棋盘创建界面,我建议在关卡场景中对其进行初始化,以最终确定GameScene类:

  // GameScene.ts import { Board } from "../models/Board"; const Rows = 8; const Cols = 8; const Bombs = 8; export class GameScene extends Phaser.Scene { private _board: Board = null; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); } } 

我们在场景文件的开头将board参数带到常量,并在创建此类的实例时将其传递给Board构造函数。

3.2细胞模型


该板包含要在屏幕上显示的单元格。 每个单元格必须放置在由行和列确定的相应位置。

单元也被选择为单独的实体。 创建src/scripts/models/Field.ts在其中放置描述单元格的类:

 import { Board } from "./Board"; export class Field extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _board: Board = null; private _row: number = 0; private _col: number = 0; constructor(scene: Phaser.Scene, board: Board, row: number, col: number) { super(); this._init(scene, board, row, col); } public get col(): number { return this._col; } public get row(): number { return this._row; } public get board(): Board { return this._board; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { this._scene = scene; this._board = board; this._row = row; this._col = col; } } 

每个单元格应具有其所在的行和列指标。 我们设置参数_board_scene以设置指向板对象和场景的链接。 我们实现了用于读取_row_col_board吸气剂。

3.3单元格视图


创建了抽象单元格,现在我们要对其进行可视化。 要在屏幕上显示单元格,需要创建其视图。 创建src/scripts/views/FieldView.ts并将视图类放入其中:

 import { Field } from "../models/Field"; export class FieldView extends Phaser.GameObjects.Sprite { private _model: Field = null; constructor(scene: Phaser.Scene, model: Field) { super(scene, 0, 0, 'spritesheet', 'closed'); this._model = model; this._init(); this._create(); } private _init(): void { } private _create(): void { } } 

请注意,我们使此类成为Phaser.GameObjects.Sprite的后代。 用移相器来说,该类已成为Sprite预制件。 也就是说,我得到了精灵游戏对象的功能,我们将使用自己的方法进一步扩展该功能。

让我们看一下此类的构造函数。 在这里,首先,我们必须使用以下参数集调用父类的构造函数:

  • 链接到场景对象(正如我在3.1节中所警告的:相位器要求我们链接到当前场景以渲染精灵)
  • 画布上的xy坐标
  • 地图集可用的字符串键,我们在开始场景的preload方法中加载了该键
  • 您要选择以显示精灵的此地图集中的框架字符串键

在private _model属性中设置对模型的引用(即Field类的实例)。

我们还谨慎地启动了2个当前为空的_init_create ,我们将在稍后实现。

3.4在视图类中创建精灵


因此,创建了视图,但是她仍然不知道如何绘制精灵。 要将精灵和我们需要的框架放在画布上,您将需要修改我们自己的private _create方法:

 // FieldView.js //... private _create(): void { this.scene.add.existing(this); //      this.setOrigin(0.5); //  pivot point    } //... 

3.5精灵定位


此刻,所有创建的精灵将放置在画布的坐标(0,0)中。 我们还需要将每个单元格放在板上的相应位置。 即,对应于此单元格的行和列的位置。 为此,我们需要编写代码来计算FieldView类的每个实例的坐标。

_position属性添加到类中,该属性负责游戏场地上单元格的最终坐标:

 // FieldView.ts //... interface Vec2 {x: number, y: number}; export class FieldView extends Phaser.GameObjects.Sprite { private _position: Vec2 = {x: 0, y: 0}; //... 

由于我们要相对于屏幕的中心对齐板并相应地对齐其中的单元格,因此我们还需要_offset属性,指示此特定单元格相对于屏幕的左边缘和上边缘的偏移量。 使用私有获取器添加它:

 // FieldView.ts //... private get _offset(): Vec2 { return { x: (this.scene.cameras.main.width - this._model.board.cols * this.width) / 2, y: (this.scene.cameras.main.height - this._model.board.rows * this.height) / 2 }; } //... 

因此,我们:

  1. this._scene.cameras.main.width获得了总屏幕宽度。
  2. 我们通过将单元格的数量乘以一个单元格的宽度来获得木板的总宽度: this._board.cols * this.width
  3. 从屏幕的宽度上减去木板的宽度,我们在屏幕上得到了一个位置,而不是被木板占据。
  4. 将结果数字除以2,我们可以在电路板的左侧和右侧获得缩进值。
  5. 通过将每个单元格移动此缩进值,我们可以保证整个电路板沿x轴对齐。

我们执行绝对相似的动作以获得垂直位移。

仍然需要在_init方法中添加必要的代码:

 // FieldView.ts // ... private _init(): void { const offset = this._offset; this.x = this._position.x = offset.x + this.width * this._model.col + this.width / 2; this.y = this._position.y = offset.y + this.height * this._model.row + this.height / 2; } // ... 

这里的属性this.xthis.ythis.widththis.height是父类Phaser.GameObjects.Sprite的继承属性。 更改this.xthis.y的属性将导致精灵在画布上的正确定位。

3.6创建FieldView的实例


Field类中创建一个视图:

 // Field.ts // ... private _view: FieldView = null; public get view(): FieldView { return this._view; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { //... this._view = new FieldView(this._scene, this); } // ... 

3.7显示板字段。


让我们回到Board类,它实际上是Field对象的集合,并将创建单元格。

我们将把木板创建代码取出到一个单独的_create方法中,并从构造函数中调用此方法。 知道在_create方法中,我们不仅将创建单元格,还将提取用于在单独的_createFields方法中创建单元格的代码。

 // Board.ts constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { // ... this._create(); } private _create(): void { this._createFields(); } private _createFields(): void { } 

正是在这种方法中,我们将在嵌套循环中创建所需数量的单元格:

 // Board.ts // ... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { this._fields.push(new Field(this._scene, this, row, col)); } } } //... 

这是第一次使用该命令运行程序集进行调试

 npm start 

确保在屏幕中央,我们希望看到8行中的64个单元格。

3.8制造炸弹


之前,我报道了在Board类的_create方法中,我们将不仅创建字段。 还有什么 还将创建炸弹,并将创建的单元格设置为相邻炸弹的数量。 让我们从炸弹本身开始。

我们需要将N枚炸弹放在板上随机的单元格中。 我们用近似算法描述炸弹的创建过程:

                          

在循环的每次迭代中,我们将从this._fields属性中获得一个随机单元,直到创建的炸弹数量与this._bombs字段中指示的this._bombs 。 如果接收到的单元是空的,那么我们将在其中安装炸弹并更新生成炸弹所需的计数器。

要生成随机数,我们使用静态方法Phaser.Math.Between

 // Board.ts //... private _createBombs(): void { let count = this._bombs; //      while (count > 0) { //       let field = this._fields[Phaser.Math.Between(0, this._fields.length - 1)]; //    if (field.empty) { //     field.setBomb(); //     --count; //    } } } 

不要忘记在Board.ts文件中写对this._createBombs();Board.ts this._createBombs();_create方法的末尾

正如您已经注意到的,为了使此代码正确运行,您需要通过向其添加empty getter和setBomb方法来改进Field类。

将一个_value私有字段添加到Field _value ,这将控制单元格的内容。 我们接受以下协议。
_value === 0单元格为空,并且其中没有地雷或值
_value === -1牢房里有一个地雷
_value > 0单元中的是当前单元旁边的地雷数量

遵循这些规则,我们将在Field类中开发使用_value属性的方法:

 // Field.ts // ... private _value: number = 0; // ... public get value(): number { return this._value; } public set value(value) { this._value = value; } public get empty(): boolean { return this._value === 0; } public get mined(): boolean { return this._value === -1; } public get filled(): boolean { return this._value > 0; } public setBomb(): void { this._value = -1; } // ... 

3.9设定值


炸弹已经排列好了,现在我们拥有所有数据,以便在需要它的所有单元格中设置数值。

让我提醒您,根据工兵的规则,该牢房的数量必须与位于该牢房旁边的炸弹数量相对应。 根据此规则,我们编写相应的伪代码。

                    

Board类中,创建一个新方法并将指定的伪代码转换为真实代码:

 // Board.ts //... private _createValues() { //      this._fields.forEach(field => { //      if (field.mined) { //     field.getClosestFields().forEach(item => { //      if (item.value >= 0) { ++item.value; } }); } }); } //... 

让我们看看我们使用的哪些接口没有实现。 您需要添加getClosestFields方法来获取相邻的单元格。

如何识别相邻小区?

例如,考虑板子上任何不在边缘的单元,即不在极行和极列中的单元。 这样的像元具有最大数量的邻居:顶数为1,底数为1,左数为3,右数为3(包括对角线上的像元)。

因此,在每个相邻的单元格中,指示符_row_col不超过1。这意味着我们可以预先指定当前字段的参数_row_col之间的差异。 在文件的开头添加一个常量到类描述中:

 // Field.ts const Positions = [ {row : 0, col : 1}, //  {row : 0, col : -1}, //  {row : 1, col : 0}, //  {row : 1, col : 1}, //   {row : 1, col : -1}, //   {row : -1, col : 0}, //  {row : -1, col : 1}, //   {row : -1, col : -1} //   ]; //... 

现在我们可以添加缺少的方法,在其中循环遍历此数组:

 // Field.ts //... public getClosestFields(): Field[] { let results = []; //      Positions.forEach(position => { //      let field = this._board.getField(this._row + position.row, this._col + position.col); //       if (field) { //     results.push(field); } }); return results; }; //... 

不要忘记在每次迭代中检查field变量,因为板上的并非所有单元都有8个邻居。 例如,左上方的单元格在其左侧将没有邻居,依此类推。

仍然需要实现getField方法并将所有必需的调用添加到Board类中的_create方法

 // Board.ts //... public getField(row: number, col: number): Field { return this._fields.find(field => field.row === row && field.col === col); } //... private _create(): void { this._createFields(); this._createBombs(); this._createValues(); } //... 

4.


4.1


, , . .

. FieldView _create :

 // FielView.ts //... private _create(): void { // ... this.setInteractive(); } //... 

phaser Phaser.GameObjects . ( pointerdown ) , FieldView , Phaser.GameObjects.Sprite .

, , , . setInteractive , .

, , Board , Field , _createFields :

 // Board.ts //... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { const field = new Field(this._scene, this, row, col) field.view.on('pointerdown', this._onFieldClick.bind(this, field)); this._fields.push(field); } } } //... 

, _onFieldClick , . Board . , , GameScene . , , Board . :

 // Board.ts //... private _onFieldClick(field: Field, pointer: Phaser.Input.Pointer): void { if (pointer.leftButtonDown()) { this.emit(`left-click`, field); } else if (pointer.rightButtonDown()) { this.emit(`right-click`, field); } } //... 

, , , . , . , , , , , , Field .

GameScene _create , :

 // Board.ts //... import { Field } from "../models/Field"; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); this._board.on('left-click', this._onFieldClickLeft, this); this._board.on('right-click', this._onFieldClickRight, this); } private _onFieldClickLeft(field: Field): void { } private _onFieldClickRight(field: Field): void { } //... 

4.2.


. . . , , , :

  1. , , , ,
  2. , ,

:

                           

, . _onFieldClickLeft :

 // GameScene.ts //... private _onFieldClickLeft(field: Field): void { if (field.closed) { //    field.open(); //   if (field.mined) { //    field.exploded = true; this._onGameOver(false); //   } else if (field.empty) { //    this._board.openClosestFields(field); //   } } else if (field.opened) { //    if (this._board.completed) { //       this._onGameOver(true); //   } } } //... 

, , Field Board , , .

3 States , _state :

 // Field.ts enum States { Closed = 'closed', Opened = 'opened', Marked = 'flag' }; export class Field extends Phaser.Events.EventEmitter { private _state: string = States.Closed; //... public get marked(): boolean { return this._state === States.Marked; } public get closed(): boolean { return this._state === States.Closed; } public get opened(): boolean { return this._state === States.Opened; } //... 

, , , open , :

 // Field.ts //... public open(): void { this._setState(States.Opened); } private _setState(state: string): void { if (this._state !== state) { this._state = state; this.emit('change'); } } //... 

, . _setState , . , .

_exploded Field, :

 // Field.ts private _exploded: boolean = false; //... public set exploded(exploded: boolean) { this._exploded = exploded; this.emit('change'); } public get exploded(): boolean { return this._exploded; } //... 

Board openClosestFields . , .
:

  :                 

:

 // Board.ts //... public openClosestFields(field: Field): void { field.getClosestFields().forEach(item => {//     if (item.closed) {//    item.open();//   if (item.empty) {//    this.openClosestFields(item);//     } } }); } //... 

completed Board . ? .

 // Board.ts //... public get completed(): boolean { return this._fields.filter(field => field.completed).length === this._bombs; } //... 

_fields completed , . ( , completed Field ) _bombs ( ), true , , .
, . Board :

 // Board.ts //... public open(): void { this._fields.forEach(field => field.open()); } //... 

completed Field . ? , . :

 // Field.ts //... public get completed(): boolean { return this.marked && this.mined; } //... 

_onGameOver , . status .

 // GameScene.ts //... private _onGameOver(status: boolean) { this._board.off('left-click', this._onFieldClickLeft, this); this._board.off('right-click', this._onFieldClickRight, this); this._board.open(); } //... 

4.3


.

Field _setState , change . FieldView :

 // FieldView.ts //... private _init(): void { //... this._model.on('change', this._onStateChange, this); } private _onStateChange(): void { this._render(); } private _render(): void { this.setFrame(this._frameName); } //... 

_onStateChange . , , , _render .

. , setFrame , .

_frameName , . , .
条件
closed
flag
empty,
exploded
,
mined
, ,
1...9
1 9,

, . :

 // FieldView.ts const States = { 'closed': field => field.closed, 'flag': field => field.marked, 'empty': field => field.opened && !field.mined && !field.filled, 'exploded': field => field.opened && field.mined && field.exploded, 'mined': field => field.opened && field.mined && !field.exploded } //... 

, — , . ( ):

 // FieldView.ts //... private get _frameName(): string { for (let key in States) { if (States[key](this._model)) { return key; } } return this._model.value.toString(); } 

. , true , , key .

, _value , States .

, .

4.4


, . . .

  1. ,
  2. ,
  3. /

:

                                

, :

 // GameScene.ts private _flags: number = 0; //... private _onFieldClickRight(field: Field): void { if (field.closed && this._flags > 0) { //        field.addFlag(); //     } else if (field.marked) { //     field.removeFlag(); //   } this._flags = Bombs - this._board.countMarked; } //... public create(): void { this._flags = Bombs; //... } //... 

_flags , , . , , . Board countMarked :

 // Board.ts //... public get countMarked(): number { return this._fields.filter(field => field.marked).length; } //... 

Field , open :

 // Field.ts //... public addFlag(): void { this._setState(States.Marked); } public removeFlag(): void { this._setState(States.Closed); } //... 

, _setState change , , , .

, . , , :

 // GameScene.ts //... constructor() { super('Game'); //        document.querySelector("canvas").oncontextmenu = e => e.preventDefault(); } //... 

4.5 GameSceneView


UI GameSceneView src/scripts/views/GameSceneView.ts .

FieldView GameObjects .
:

  • (/)

UI GameSceneView .
.

 enum Styles { Color = '#008080', Font = 'Arial' } enum Texts { Flags = 'FLAGS: ', Exit = 'EXIT', Success = 'YOU WIN!', Failure = 'YOU LOOSE' }; export class GameSceneView { private _scene: Phaser.Scene = null; private _style: {font: string, fill: string}; constructor(scene: Phaser.Scene) { this._scene = scene; this._style = {font: `28px ${Styles.Font}`, fill: Styles.Color}; this._create(); } private _create(): void { } public render() { } } 

.

 // GameSceneView.ts //... private _txtFlags: Phaser.GameObjects.Text = null; //... private _createTxtFlags(): void { this._txtFlags = this._scene.add.text( 50, 50, Texts.Flags, this._style ).setOrigin(0, 1); } //... 

50px . , setOrigin pivot point (0, 1). , .

.

 // GameSceneView.ts //... private _txtStatus: Phaser.GameObjects.Text = null; //... private _createTxtStatus(): void { this._txtStatus = this._scene.add.text( this._scene.cameras.main.centerX, 50, Texts.Success, this._style ).setOrigin(0.5, 1); this._txtStatus.visible = false; } //... 

setOrigin 0.5 x. , , .

, .

 // GameSceneView.ts //... private _btnExit: Phaser.GameObjects.Text = null; //... private _createBtnExit(): void { this._btnExit = this._scene.add.text( this._scene.cameras.main.width - 50, 50, Texts.Exit, this._style ).setOrigin(1); this._btnExit.setInteractive(); this._btnExit.once('pointerdown', () => { this._scene.scene.start('Start'); }); } //... 

setOrigin , . , . .

render UI _create .

 // GameSceneView.ts //... private _create(): void { this._createTxtFlags(); this._createTxtStatus(); this._createBtnExit(); } public render(data: {flags?: number, status?: boolean}) { if (typeof data.flags !== 'undefined') { this._txtFlags.text = Texts.Flags + data.flags.toString(); } if (typeof data.status !== 'undefined') { this._txtStatus.text = data.status ? Texts.Success : Texts.Failure; this._txtStatus.visible = true; } } //... 

UI, .
GameScene _render , :

 // GameScene.ts //... import { GameSceneView } from "../views/GameSceneView"; //... export class GameScene extends Phaser.Scene { private _view: GameSceneView = null; //... private _onGameOver(status: boolean) { //... this._view.render({status}); } //... private _onFieldClickRight(field: Field): void { //... this._flags = Bombs - this._board.countMarked; this._view.render({flags: this._flags}); } //... public create(): void { //... this._view = new GameSceneView(this); this._view.render({flags: this._flags}); } //... } 

5.


, , ?! , phaser, . .

2 : . .

5.1




, . .

FiledView _create _animateShow :

 // FieldView.ts //... private _create(): void { //... this._animateShow(); } //... 

B . , , 2 :

  1. ,

 // FieldView.ts //... private _animateShow(): Promise<void> { this.x = -this.width; this.y = -this.height; const delay = this._model.row * 50 + this._model.col * 10; return this._moveTo(this._position, delay); } //... 

(0, 0), , , . .

_moveTo .

 // FieldView.ts //... private _moveTo(position: Vec2, delay: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, x: position.x, y: position.y, duration: 600, ease: 'Elastic', easeParams: [1, 1], delay, onComplete: () => { resolve(); } }); }); } //... 

tweens . add :

  • targets , . this , .
  • .
  • duration , — 600.
  • ease easeParams easing .
  • delay , . , . .
  • , onComplete , .

, , resolve , .

5.2




, . ?

_render . , , . , , .

 // FieldView.ts //... private _onStateChange(): void { if (this._model.opened) { this._animateFlip(); } else { this._render(); } } //... 

scale . x , , . , x , . _animateFlip .

 // FieldView.ts //... private _animateFlip(): void { this._scaleXTo(0).then(() => { this._render(); this._scaleXTo(1); }) } //... 

_moveTo _scaleTo :

 // FieldView.ts //... private _scaleXTo(scaleX: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, scaleX, ease: 'Elastic.easeInOut', easeParams: [1, 1], duration: 150, onComplete: () => { resolve() } }); }); } //... 

, . .

, ! :)

, !

结论


, , , . , phaser . !

. , . . , , , , tilemap, spine .
, , , , , ? , , . .

! !

Source: https://habr.com/ru/post/zh-CN476682/


All Articles