下午好,亲爱的同事们!
我的名字叫亚历山大(Alexander),我是HTML5游戏的开发商。
在我发送简历的公司之一中,我被要求完成测试任务。 我同意,并在1天后根据TOR HTML5开发了该游戏。

因为我正在接受游戏编程方面的培训,并且为了更有效地使用我的代码,所以我决定在完成的项目上写一篇培训文章会很有用。 由于完成的测试得到了积极的评估并导致了面试的邀请,因此我的决定可能有权存在,并且可能会在将来对某人有所帮助。
本文将介绍足以成功完成开发人员HTML5职位的平均测试任务的工作量。 想要熟悉Phaser框架的任何人也可能会感兴趣。 并且,如果您已经在使用Phaser并用JS编写-请参阅如何在TypeScript中开发项目。
因此,在cat下有很多TypeScript代码!
引言
我们对此问题作简要说明。
- 我们将开发一个简单的HTML5游戏-一个经典的工具。
- 作为主要工具,我们将使用相位器3,打字稿和webpack。
- 该游戏将为桌面设计,并在浏览器中运行。
我们提供了最终项目的链接。
如果突然有人忘记了游戏规则,请回想一下这位工兵的技巧。 但是由于这种情况不太可能发生,因此将规则放置在破坏者的下方:)
拍手规则比赛场地由布置在桌子中的单元格组成。 默认情况下,当游戏开始时,所有单元格都关闭。 炸弹放置在某些牢房中。
在封闭的单元格上单击鼠标左键时,它将打开。 如果一个开孔中有炸弹,则游戏以失败告终。
如果该单元格中没有炸弹,则会在其中显示一个数字,表明相对于当前打开的位置,相邻单元格中的炸弹数量。 如果附近没有炸弹,则牢房看起来是空的。
右键单击封闭的单元格可以在其上设置一个标志。 玩家的任务是安排所有可用的标记,以便标记所有已开采的单元格。 放置所有标志后,玩家在一个打开的单元格上按下鼠标左键以检查他是否获胜。
接下来,我们直接进入手册本身。 所有材料都分为小步骤,每个步骤都描述了在短时间内完成特定任务的过程。 因此,逐步执行小目标,最终我们将创建一个完整的游戏。 如果决定快速转到特定步骤,请使用目录。
1.准备
1.1项目模板
下载
默认的相位器项目模板 。 这是框架作者推荐的模板,它为我们提供了以下目录结构:
对于我们的项目,我们不需要当前的
index.js
文件,因此将其删除。 然后创建目录
/src/scripts/
,并将空的
index.ts
文件放入其中。 我们将所有脚本添加到此文件夹中。
还需要记住的是,在构建用于生产的项目时,将在根目录中创建一个
dist
目录,该目录将放置发行版本。
1.2构建配置
我们将使用webpack进行组装。 由于我们的模板最初是为使用JavaScript而准备的,并且我们使用TypeScript编写,因此我们需要对收集器的配置进行一些小的更改。
在
webpack/base.js
添加
entry
键(用于指示构建我们的项目时的进入点),以及用于描述构建TS脚本规则的
ts-loader
配置:
我们还需要在项目根目录中创建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
属性中定义。
- 建立一个调试项目并通过本地服务器在浏览器中打开
npm start
- 运行要出售的版本,并将发行版本放在dist /文件夹中
npm run build
1.4资产准备
该游戏的所有资产均已从
OpenGameArt (版本61x61)进行了诚实下载,并拥有名为
Feel free to use的最友好的许可证,随附的页面会仔细地告诉我们)。 顺便说一句,本文中提供的代码具有相同的许可证! ;)
我从下载的集合中删除了时钟图像,并重命名了其余文件,以便获得易于使用的帧名称。 名称和相应文件的列表显示在下面的屏幕上。
根据生成的精灵,我们将在
TexturePacker程序中创建一个
Phaser JSONArray
格式地图集(免费版本已经足够了,我还没有得到工作),并将生成的
spritesheet.png
和
spritesheet.json
文件放在
src/assets/
project目录中。

2.创建场景
2.1入口点
我们通过创建webpack配置中描述的入口点开始开发。
由于我们拥有的游戏是为台式机设计的,并且将填满整个屏幕,因此我们将
width
和
height
字段大胆地使用浏览器的整个
width
和
height
。
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
方法加载所有资产。 为了将来能够使用创建的图集,我们需要执行两个步骤:
- 使用
require
获取png
和json
地图集文件:
- 将它们
preload
到开始场景的preload
方法中:
2.3起始场景的文字
在开始的场景中,还有两件事要做:
- 告诉玩家如何开始游戏
- 在玩家的主动下开始游戏
为了满足第一点,我们首先在场景文件的开头创建两个枚举,以描述文本及其样式:
然后在
create
方法中将两个文本都创建为对象。 让我提醒您,只有在将所有资源加载到
preload
方法中之后,才会调用
Phaser
中场景的
create
方法,这非常适合我们。
在另一个更大的项目中,我们可以将文本和样式添加到json语言环境文件中或单独的配置中,但是鉴于我们现在只有2行,我认为这一步骤是多余的,在这种情况下,我建议不要使我们的生活变得复杂,将自己限制在场景文件开头的列表中。
2.4过渡到游戏级别
在继续进行之前,我们在此场景中要做的最后一件事是跟踪鼠标单击事件,以将玩家启动到游戏中:
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
:
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
类:
我们在场景文件的开头将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节中所警告的:相位器要求我们链接到当前场景以渲染精灵)
- 画布上的
x
和y
坐标 - 地图集可用的字符串键,我们在开始场景的
preload
方法中加载了该键 - 您要选择以显示精灵的此地图集中的框架字符串键
在private
_model
属性中设置对模型的引用(即
Field
类的实例)。
我们还谨慎地启动了2个当前为空的
_init
和
_create
,我们将在稍后实现。
3.4在视图类中创建精灵
因此,创建了视图,但是她仍然不知道如何绘制精灵。 要将精灵和我们需要的框架放在画布上,您将需要修改我们自己的private
_create
方法:
3.5精灵定位
此刻,所有创建的精灵将放置在画布的坐标(0,0)中。 我们还需要将每个单元格放在板上的相应位置。 即,对应于此单元格的行和列的位置。 为此,我们需要编写代码来计算
FieldView
类的每个实例的坐标。
将
_position
属性添加到类中,该属性负责游戏场地上单元格的最终坐标:
由于我们要相对于屏幕的中心对齐板并相应地对齐其中的单元格,因此我们还需要
_offset
属性,指示此特定单元格相对于屏幕的左边缘和上边缘的偏移量。 使用私有获取器添加它:
因此,我们:
- 在
this._scene.cameras.main.width
获得了总屏幕宽度。 - 我们通过将单元格的数量乘以一个单元格的宽度来获得木板的总宽度:
this._board.cols * this.width
。 - 从屏幕的宽度上减去木板的宽度,我们在屏幕上得到了一个位置,而不是被木板占据。
- 将结果数字除以2,我们可以在电路板的左侧和右侧获得缩进值。
- 通过将每个单元格移动此缩进值,我们可以保证整个电路板沿
x
轴对齐。
我们执行绝对相似的动作以获得垂直位移。
仍然需要在
_init
方法中添加必要的代码:
这里的属性
this.x
,
this.y
,
this.width
和
this.height
是父类
Phaser.GameObjects.Sprite
的继承属性。 更改
this.x
和
this.y
的属性将导致精灵在画布上的正确定位。
3.6创建FieldView的实例
在
Field
类中创建一个视图:
3.7显示板字段。
让我们回到
Board
类,它实际上是
Field
对象的集合,并将创建单元格。
我们将把木板创建代码取出到一个单独的
_create
方法中,并从构造函数中调用此方法。 知道在
_create
方法中,我们不仅将创建单元格,还将提取用于在单独的
_createFields
方法中创建单元格的代码。
正是在这种方法中,我们将在嵌套循环中创建所需数量的单元格:
这是第一次使用该命令运行程序集进行调试
npm start
确保在屏幕中央,我们希望看到8行中的64个单元格。
3.8制造炸弹
之前,我报道了在
Board
类的
_create
方法中,我们将不仅创建字段。 还有什么 还将创建炸弹,并将创建的单元格设置为相邻炸弹的数量。 让我们从炸弹本身开始。
我们需要将N枚炸弹放在板上随机的单元格中。 我们用近似算法描述炸弹的创建过程:
在循环的每次迭代中,我们将从
this._fields
属性中获得一个随机单元,直到创建的炸弹数量与
this._bombs
字段中指示的
this._bombs
。 如果接收到的单元是空的,那么我们将在其中安装炸弹并更新生成炸弹所需的计数器。
要生成随机数,我们使用静态方法
Phaser.Math.Between
。
不要忘记在
Board.ts
文件中写对
this._createBombs();
的
Board.ts
this._createBombs();
在
_create
方法的末尾
正如您已经注意到的,为了使此代码正确运行,您需要通过向其添加
empty
getter和
setBomb
方法来改进
Field
类。
将一个
_value
私有字段添加到Field
_value
,这将控制单元格的内容。 我们接受以下协议。
遵循这些规则,我们将在
Field
类中开发使用
_value
属性的方法:
3.9设定值
炸弹已经排列好了,现在我们拥有所有数据,以便在需要它的所有单元格中设置数值。
让我提醒您,根据工兵的规则,该牢房的数量必须与位于该牢房旁边的炸弹数量相对应。 根据此规则,我们编写相应的伪代码。
在
Board
类中,创建一个新方法并将指定的伪代码转换为真实代码:
让我们看看我们使用的哪些接口没有实现。 您需要添加
getClosestFields
方法来获取相邻的单元格。
如何识别相邻小区?
例如,考虑板子上任何不在边缘的单元,即不在极行和极列中的单元。 这样的像元具有最大数量的邻居:顶数为1,底数为1,左数为3,右数为3(包括对角线上的像元)。
因此,在每个相邻的单元格中,指示符
_row
和
_col
不超过1。这意味着我们可以预先指定当前字段的参数
_row
和
_col
之间的差异。 在文件的开头添加一个常量到类描述中:
现在我们可以添加缺少的方法,在其中循环遍历此数组:
不要忘记在每次迭代中检查
field
变量,因为板上的并非所有单元都有8个邻居。 例如,左上方的单元格在其左侧将没有邻居,依此类推。
仍然需要实现
getField
方法并将所有必需的调用添加到
Board
类中的
_create
方法
4.
4.1
, , . .
.
FieldView
_create
:
phaser
Phaser.GameObjects
. (
pointerdown
) ,
FieldView
,
Phaser.GameObjects.Sprite
.
, , , .
setInteractive
, .
, ,
Board
,
Field
,
_createFields
:
,
_onFieldClick
, .
Board
. , ,
GameScene
. , ,
Board
. :
, , , . , . , , , , , ,
Field
.
GameScene
_create
, :
4.2.
. . . , , , :
- —
- , , , ,
- , ,
:
, .
_onFieldClickLeft
:
, ,
Field
Board
, , .
3
States
,
_state
:
, , ,
open
, :
, .
_setState
, . , .
_exploded
Field, :
Board
openClosestFields
. , .
:
:
:
completed
Board
. ? .
_fields
completed
, . ( ,
completed
Field
)
_bombs
( ),
true
, , .
, .
Board
:
completed
Field
. ? , . :
_onGameOver
, .
status
.
4.3
.
Field
_setState
,
change
.
FieldView
:
_onStateChange
. , , ,
_render
.
. ,
setFrame
, .
_frameName
, . , .
, . :
, — , . ( ):
. ,
true
, ,
key
.
,
_value
,
States
.
, .
4.4
, . . .
- ,
- ,
- /
:
, :
_flags
, , . , , .
Board
countMarked
:
—
Field
,
open
:
,
_setState
change
, , , .
, . , , :
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() { } }
.
50px . ,
setOrigin
pivot point (0, 1). , .
.
setOrigin
0.5 x. , , .
, .
setOrigin
, . , . .
render
UI
_create
.
UI, .
GameScene _render , :
5.
, , ?! , phaser, . .
2 : . .
5.1

, . .
FiledView
_create
_animateShow
:
B . , , 2 :
- ,
(0, 0), , , . .
_moveTo
.
tweens
.
add
:
targets
, . this
, .- .
duration
, — 600.ease
easeParams
easing .- delay , . , . .
- ,
onComplete
, .
, ,
resolve
, .
5.2

, . ?
_render
. , , . , , .
scale
.
x
, , . ,
x
, .
_animateFlip
.
_moveTo
_scaleTo
:
, . .
, ! :)
, !
结论
, , , . , phaser . !
. , . . , , , , tilemap, spine .
, , , , , ? , , . .
! !