[翻译]使用AngularJS开发2048

本文从 这里 翻译过来的。

2048这个游戏有一段时间特别火,Github上有其原始版本,游戏看起来很简单,但是很耐玩,要玩通关却也需要一番技巧与耐心。

刚开始学习Angular,本想着依葫芦画瓢实现ng2048,结果在github上搜了搜,发现别人已经有实现了,而且作者还将其开发过程非常详细的写出来,这才有了下面硬着头皮的翻译。

注意 :强烈建议您参考原文试读一下,译者水平有限,要是恶心到了您,这个实非我所愿。

本想着写一行大字,最好是要醒目的:"转载请标明出处",后来看过coolshell上一篇文章后:互联网之子 – Aaron Swartz ,觉得没什么必要了.

翻译的过程中的确碰到了让人头疼不已的事情:

  • 有些词语到底要不要翻译呢?
    我会根据自己的理解,尽量避免不必要的翻译,对于不得不翻译,但是感觉译过之后似有不妥的词,我会补充到文章开始,以便作为提醒。
  • 翻译成什么样更好呢?
    这个就因能力和精力而定了,
  • 是直译还是意译呢?
    汉语博大精深,有些英语长句要讲明白的事情,汉语可以很简练的表达相同的意思,同时并非人人都是文豪,文章可能会啰嗦,这时为了阅读流畅,我会有意去掉部分语句,当然前提是不给读者的理解带来影响。

Game Board:游戏面板,
grid:游戏面板上切分出来的格子,简称格子
tile:在格子上移动的方块,简称方块


我经常被问到的一个问题是:什么情况下使用Angular会被认为是一种很2的选择,这个问题的答案通常是:游戏制作,Angular有它自己的事件循环操作($digest 循环),通常游戏需要大量的底层DOM操作。由于Angular能够支持很多种类型的游戏,上面的理由有些牵强,即使需要大量DOM操作的游戏,Angular也是可以用来开发游戏的静态内容,比如跟踪分数排行榜和创建游戏菜单。

如果你和我一样,也痴迷于2048这个流行的游戏,这个游戏的目标是通过消除相同得分的方块最终获得2048这个方块。

在我的文章中,我们准备开发一个AngularJS版本的2048,从开始到结束,解释开发的整个过程,由于2048是一个相对复杂的应用程序,所以本文也可以看做是教授如何使用AngularJS构建复杂应用程序的例子。

文章太长了,不读了:
所有的源码放在github上,点击这里跳转

Index

  1. Planning the app
  2. Modular structure
  3. GameController
  4. Testing testing testing
  5. Building the grid
  6. SCSS to the rescue
  7. The tile directive
  8. The Boardgame
  9. Grid theory
  10. Gameplay (keyboard)
  11. Pressing the start button
  12. The game loop
  13. Keeping score
  14. Game over and win screens
  15. Animation
  16. Customization
  17. Demo

First steps: planning the app

不论程序规模大小,是复制别人的还是自己原创的,第一步要做的都是:高屋建瓴式的设计。

玩过2048的人应该清楚,游戏有一个面板(board),上面是一些格子,每个格子就是一个位置,标上数字的方块可以在这些格子上移动,根据这个事实,可以不依赖于javascript,让CSS3负责处理方块在面板上的移动,


3d-board.png

由于只有一个页面,所以只需要一个controller管理页面。

玩游戏期间只有一个游戏面板,我们会把相关操作grid的逻辑包含在一个GridService中,GridService services是单例对象,在它里面保存方块是合适的,GridService将会用来操作方块的放置,移动以及遍历方块寻找可能的位置。

我们会在GameManager sevice中存储游戏的逻辑和处理程序,GameManager负责管理游戏的状态,操作格子的移动,以及维护用户得分(包括当前得分以及最高得分)

最后,需要一个管理键盘的组件,我们叫她KeyboardService,本文中我们仅实现PC端的操作,但是也可以重用这个service去管理触摸操作,以便在移动设备上能够正常工作。

Building the app

先要使用yeoman angular generator生成应用的文件结构,这个不是必须的,我们会放置一个test目录,这个目录和app目录是平级的。

下面使用yuoman建立项目,如果你更愿意手工操作,可以跳过相应的内容。

首先确保安装了yeoman,yeoman依赖于NodeJS和npm,安装NodeJS超出了本文的范围,你可以参考NodeJS官网的指导进行安装。

npm安装好之后,就可以安装yeoman工具yo和angular generator(yo会使用generator去创建Angular app):

$ npm install -g yo
$ npm install -g generator-angular

安装好之后,就可以使用yeoman工具创建应用了:

$ cd ~/Development && mkdir 2048
$ yo angular twentyfourtyeight

执行过程中会被问一些问题,除了选择angular-cookies作为依赖这一项,其他只要回答yes就可以了,

Our angular module

现在创建程序的入口文件scripts/app.js:

angular.module('twentyfourtyeightApp', [])

Modular structure

我们推荐Angular应用的目录结构采用功能分类,而不是类型分类,也就是说不要依照controllers,services,directives分割项目组件,而是应该根据模块功能定义模块结构,例如我们的应用中定义了Game模块和KeyBoard模块。

模块结构体现出一个清晰的文件和职责对应关系,这有助于构建大型的复杂Angular应用程序,同时也更加容易得共享模块功能。

scripts_dir.png
The view

最容易开始的地方就是写页面了,在这个应用中不需要多个页面,因此创建一个div元素装载应用程序的内容就可以了。

在app/index.html文件中,需要包含所有依赖(包括angular.js以及我们自己编写的javascript文件-到现在为止,只有一个script/app.js):

index.html

后续我们只需要修改app/views/main.html文件就可以了,当需要引入资源文件时,才会去修改app/index.html文件

打开app/views/main.html文件,其中将放置游戏需要的页面元素,使用controllerAs语法,它告诉了$scope去哪里找到数据,哪个controller负责操作哪个component。

<!-- app/views/main.html -->
<div id="content" ng-controller='GameController as ctrl'>
  <!-- Now the variable: ctrl refers to the GameController -->
</div>

controllerAs语法来自1.2版本,当在页面上操作多个controller时使用它非常有用

view中,我们至少要包含下面几项

  1. 游戏的标题
  2. 当前得分和最高得分
  3. 游戏面板

游戏的静态头部信息简单就是下面这样子:

游戏的静态头部信息

The GameController

项目框架搭起来了,接下来创建GameController去装载需要在页面中展示的元素,在app/scripts/app.js文件中,使用下面的语句在twentyfourtyeightApp模块上创建controller。

angular.module('twentyfourtyeightApp', [])
.controller('GameController', function() {
});

在页面上引用了将要给GameController设置的game对象,game对象将会引用到模块中的main game对象,main game对象将会在新的模块中创建

.controller('GameController', function(GameManager) {
  this.game = GameManager;
});

由于这个模块还没有被创建,所以我们的应用还不能在浏览器中运行,在Controller内部我们添加GameManager依赖。

记住,应用程序的不同模块之间有依赖,为了确保依赖能够被加载,需要将依赖的模块注入到Angular的应用中,为了使Game模块成为twentyfourtyeightApp的依赖,在模块定义地方将其注入进来。

我们整个app/scripts/app.js文件看起来应该是下面这样子

angular
.module('twentyfourtyeightApp', ['Game'])
.controller('GameController', function(GameManager) {
  this.game = GameManager;
});
The Game

接下来开发游戏自身的逻辑,创建app/scripts/game/game.js文件,新建Game模块

angular.module('Game', []);

Game模块提供一个核心的组件:GameManager
GameManager 负责管理游戏的状态,用户发出的不同运动指令、跟踪记录游戏得分,判断游戏是否结束以及用户赢了还是输了

GameManager需要支持的功能就有:

  1. 创建新游戏
  2. 处理循环和移动操作
  3. 更新游戏得分
  4. 监控游戏状态

这样GameManager的基本框架就有了

GameManager
Back to the GameManager

movesAvailable()用于检查是否还有可用的格子,以及是否存在可以合并的方块


movesAvailable

Building the game grid

接下来创建GridService去管理游戏面板

回想一下,如何处理游戏面板呢,我们用到了两个数组,grid和tile数组
在app/scripts/grid/grid.js文件,让我们创建对应的service

GridService

当开始新游戏时,需要将grid和tile数组元素置为null,grid数组是静态的,它只用于DOM元素的占位使用。

tile数组是动态的,它保存着前游戏中的方块。

在app/views/main.html文件中添加grid指令,将controller上的GameManager对象实例传递到视图里面

在app/scripts/grid/目录里面添加文件grid_directive.js,grid指令基本不需要什么变量,她的职责只是封装对应的视图。

grid_directive.js

这个指令只是负责创建游戏的grid视图,其中不需要任何的逻辑。

grid.html

在指令模板中,有两次ngRepeat的调用,分别显示的是grid和tile数组的内容。


main.html

第一个ng-repeat指令相当直接,它简单迭代grid数组,放置一个空的包含class为grid-cell的div元素

在第二个ng-repeat指令中,我们为每一个展示在屏幕上被叫做tile的元素创建了第二个指令,tile指令负责元素可视化展示的创建,稍后我们会创建。

聪明的读者会注意到,我们使用一维的数组去展示一个二维的方格,当视图被渲染时,我们就可以看到想要的形式了。

Enter SCSS

在项目中会使用SCSS,SCSS增强了CSS,提供了动态创建CSS的能力。

为了创建二维的游戏面板,我们使用了CSS3中的transform,使用它去将方块定位到指定的位置上。

CSS3 transform property

transform属性可以对元素使用2D和3D变换,例如:移动元素,旋转元素等等。

看下面的demo,是一个宽度为40px的正方形盒子,通过使用transformX(300px)就可以使这个元素沿X轴移动300px,

transformX
.box.transformed {
  -webkit-transform: translateX(300px);
  transform: translateX(300px);
}

可以简单的通过给元素添加class的形式达到移动tiles方块的目的,剩下来的工作就是:如何给游戏面板上的方块创建class。

这就是SCSS闪耀的地方,我们首先创建一些变量,例如每一行有几个格子,然后使用这些变量构建SCSS,下面这些变量可以用来给游戏面板定位:

$width: 400px;          // The width of the whole board
$tile-count: 4;         // The number of tiles per row/column
$tile-padding: 15px;    // The padding between tiles

通过在SCSS使用这些变量就可以为我们计算位置了,首先需要计算每一个方块的宽度:

$tile-size: ($width - $tile-padding * ($tile-count + 1)) / $tile-count;

接下来是为游戏面板设置合适的宽度和高度,我们为其内部的容器设置绝对定位,这里贴出了部分SCSS文件的内容,完整内容可以在项目源码中找到。

SCSS文件

注意一点就是:为了使tile-container放置到grid-container之上,必须给tile-container设置一个高于grid-container的z-index值,不然浏览器会认为他们处于同一个z-index上,这样效果就不好看了。

接下来是动态生成方块的定位,我们需要一个.position-{x}-{y}类,x、y代表的是方块在游戏面板中的坐标,例如使用0,0 表示第一个tile的位置

下面是计算过程:

.tile

现在生成了动态的.position-#{x}-#{y},接下来就可以在屏幕上展示tile了

Coloring the different tiles

不同数值的tile拥有不同的颜色,使用上面用到的技术,通过迭代color变量,给tile生成一个颜色相关的class,下面的SCSS数组用于定义不同方块拥有的颜色

$colors

迭代$color数组,给不同数值的方块创建对应的颜色类,例如拥有数值2的方块,我们将添加一个.tile-2的类,它将拥有背景色#EEE4DA,使用SCSS可以动态完成这项工作。

.tile-x

The Tile directive

tile指令是视图的容器,我们不期望它里面包含很多逻辑,能够访问到它所占据的格子就可以了,除此之外,没有其他功能需要添加到这个指令中了

tile directive

一个有意思的事情是,方块是如何动态的放置到游戏面板上的,多亏了模板中的ngModel变量,ngModel指向的是tiles数组。

<div ng-if='ngModel' class="tile position-{{ ngModel.x }}-{{ ngModel.y }} tile-{{ ngModel.value }}">
  <div class="tile-inner">
    {{ ngModel.value }}
  </div>
</div>

有了这个基本的指令,我们几乎可以在屏幕上显示了,每一个tile都有x和y坐标,x、y的值会被动态的赋给类.position-#{x}-#{y},浏览器在给元素应用了这些class之后,元素就被定位到了期望的位置上。

这也意味值tile对象需要x、y和value三个属性。

The TileModel

我们要用到Angular的依赖注入技术,我们创建一个装载数据的service TileModel service

TileModel
Our first grid

有了TileModel,我们可以开始给tile数组添加TileModel对象的实例了,然后他们就会魔法般的出现在正确的格子上。

gird service

The Board’s ready for the game

现在可以把tile画到屏幕上了,在GridService中需要有一个功能去准备游戏的面板,当第一次加载网页的时候,创建一个空的游戏面板,当用户点击新建游戏或者重来一次的时候,同样需要创建新的游戏面板。

GridService中buildEmptyGameBoard()函数就有用来创建一个新的游戏面板的,这个方法负责把grid和tile数组元素置为null。

buildEmptyGameBoard()

下面是一些用到的工具函数

工具函数

Multi-dimensional array in one dimension

参考下面两个图,如何用一维数组去表示一个多维数组?

图一
图二
图三

图二中的(0,0)映射到图三中的0,(0,1)对应的是4 , (1,1)对应的是5,因此得到了下面的公式
i = x + ny
其中i是一维数组元素的位置,x、y是二维数组的坐标,n每行拥有的元素的个数。

这样,位置到坐标和坐标到位置的转换函数_positionToCoordinates和_coordinatesToPosition可以就可以表示为下面的形式

位置、坐标转化
Initial player positions

游戏开始时,随机选择两个位置插入tile

buildStartingPosition函数

randomlyInsertNewTile()方法随机选择一个可以使用的位置,用来插入新生成的方块对象,但是首先需要知道有哪些位置可以使用。

availableCells函数

简单使用Math.random随机获取一个可用位置坐标

randomAvailableCell函数

下面就是randomlyInsertNewTile函数的实现

randomlyInsertNewTile函数

Keyboard interaction

现在已经可以将方块添加到游戏面板上了,但是游戏还玩不了,接下来需要把注意力切换到如何给游戏加入交互操作上了。

本文仅仅关注给游戏添加键盘交互动作,触摸动作不在本文中实现,给游戏添加触摸动作也不是一件难事,我们关注的触摸事件ngTouch已经提供了,实现这个功能就交给你了。

使用方向键玩游戏(或者a, w, s, d 键),我们希望用户通过简单的方式玩游戏,不要求用户集中注意力在游戏面板上的元素(或网页上的其他元素),这样用户就只与聚焦的文档进行游戏互动。

为此,需要给document元素绑定事件监听器,在Angular中提供了$document服务,我们就将监听器绑定到$document上。为了处理定义好的用户交互动作,我们将键盘事件包装后绑定到一个服务中,页面上我们只需要一个键盘事件处理器,因此选择service是正确的。

此外,无论何时检测到用户的键盘事件后,我们要触发设置的自定义动作,使用service允许我们将其注入到Angular对象中,进而处理用户输入。

在app/scripts/keyboard/keyboard.js文件中创建Keyboard模块

// app/scripts/keyboard/keyboard.js
angular.module('Keyboard', []);

我们每创建新文件,都需要考虑将其引入到index.html文件中,现在index.html文件应该包含下面的外部文件了

index.html

同时,每当创建新的模块,也需要告诉Angular,我们的应用需要使用这个新模块,需要将其作为依赖注入到应用中。

.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard'])

KeyBoard Service的实现中,我们给$document绑定keydown事件,用来捕获用户交互,同时,我们也会注册一个处理函数,当有用户交互的时候这个处理函数会被调用。

KeyBoard Service

init函数中会将键盘监听的任务交给KeyboardService去处理,所有我们感兴趣的keydown事件会被过滤出来进行处理。

任何我们感兴趣事件,我们都会去阻止其默认行为,然后将其交给keyEventHandlers处理。

keyboard.png

如何知道触发的事件是不是我们感兴趣的呢?由于与我们游戏有关的也就是有限的键盘动作,所以我们能够去检测触发的事件是否是我们关心的特定的键盘动作触发的。

keyboard

this._handleKeyEvent的职责是就是去调用已经注册的key handler

_handleKeyEvent

我们需要将处理函数添加到处理函数队列中

on

Using the Keyboard service

现在我们有能力监听用户的键盘输入了,当应用启动之后,我们就得做这件事请
,由于监听键盘输入被封装为service了,我们只需要在GameController里面做这件事情就行了。

首先需要调用init()函数启动对键盘的监听,接着,注册处理函数,用它去调用GameManager进而调用move()函数。

回到GameController,我们需要添加newGame()和startGame()函数newGame函数简单的调用game service创建新游戏,并且启动键盘事件监听操作。

keyboard-sequence.png

现在将KeyboardService注入到GameController中

GameController

一旦要创建新游戏,startGame就会被调用,startGame函数会设置键盘的事件操作函数

startGame

Press the start button

最后一个需要实现的方法是newGame,位于GameManager中,这个方法做了下面几件事情:

  1. 创建空的游戏面板
  2. 设置开始位置
  3. 初始化游戏

GridService已经实现了上述的逻辑,现在要做的就是把他们串联起来

1.png

Get your move on (the game loop)

现在将进入游戏的核心部分,当用户按下键盘的方向键后,GridService的move方法就会被调用

game-1.png

在开始写move方法前,我们需要先定义游戏的约束,也就是,每一次移动,游戏要如何处理。

  1. 获取用户方向键对应的vector向量
  2. 为游戏面板上的每一个tile找到最远可能的位置,同时,判断下一个位置中的tile是否可以合并。
  3. 对每一个tile,判断下一个tile是否与其拥有相同的value
  4. 如果下一个tile不存在,只需将当前的tile移动到最远可能的位置(这意味着最近的位置就是游戏面板的边缘)
  5. 如果下一个tile存在,其value和当前tile不同,只需移动当前的tile到最远位置(当前tile的下一个tile就是可移动的边界)
  6. value相同,找到了一个可能的合并
    1. 如果是合并后得到的,跳过
    2. 如果还没有合并,那么认为这是一个合并

既然已经定义出了功能,我们就能为构造move函数设计出策略。

move函数

下一步要遍历Grid找到所有可能的位置,在GridService上创建一个新的函数帮助我们找到所有可能的位置

grid-vectors.gif

为了获得移动方向,需要有一个向量vector,用来描述用户按键的信息,例如:当用户按下右方向,这表明用户想要向右移动增加x的位置,可以将这种关系使用javascript的对象来表示,就像下面这样子:

vector

接下来遍历可能的位置,使用vector向量判断将要遍历的方向

traversalDirections

现在,traversalDirections()函数定义后,在move函数中,我们就能够迭代可能的运动,回到GameManager,我们将使用这些潜在的位置开始遍历grid

move

现在在position循环内部,我们将会迭代出可能的位置,寻找该位置存在的tiles,

为了为一个tile寻找其最远可能的位置,需要走到其下一个位置检查当前格子是否是面板的边沿并且该位置是空的。

如果该位置是空的,并且在格子的范围内,接下来就继续,走到其下一个位置做相同的检查。

如果上述的两个检查条件都失败了,要么是走到格子的边沿了,要么是找到了下一个格子了,我们将下一个位置设置为newPosition,并且记录下一个cell

next-process.gif

将这个功能放到GridService中

calculateNextPosition

既然可以为tiles计算下一个可能的位置,也就可以检查潜在的合并了。

合并是这样定义的:一个方块碰到了和它值相同的另一个方块,代码中将检查看下一个位置的方块是否待移动方块有相同的值,并且之前没有被合并过

现在,如果下一个位置不满足条件,那只需简单的将方块从当前的位置移动到newPosition这个位置。

Moving the tile

你可能已经猜对了,将moveTile()放置到GridService中是再合适不过的了。

移动方块就是简单的更新一下其在一维数组中的位置,还有就是更新TileModel

Moving the tile in the array

GridService的数组反应了在后端方块定位在了哪里。方块在数组中的位置没有和其在格子中的位置进行绑定

Updating the position on the TileModel

为了前端css放置格子,需要更新格子的坐标。

moveTile

现在,定义tile.updatePosition() 方法,方法做的事情正如其名字一样,简单的更新方块自己的x、y坐标

updatePosition()
Merging a tile

既然已经处理了简单的情况,方块合并就成为下一个要处理的事情了,合并被定义为下面的操作:

一个方块在下一个潜在的位置碰到了和自己相同value的另一个方块

当方块被合并后,它就从面板上别移除掉了,同时会更新游戏的当前得分和历史最高分

合并包含几个步骤:

  1. 添加一个新的方块到最终的位置上,其value是合并过的值
  2. 移除原有的方块
  3. 更新游戏得分
  4. 检查是否获胜
in move function

游戏仅仅支持单一的方块移动,也就是说当一行出现多个合并位置时,真正的合并只会发生一次,所以需要对已经发生过的合并进行标记,代码中使用merged属性来做这件事。

代码中有两个方法目前还没有实现,GridService.newTile()简单创建一个新的TileModel对象

GridService.newTile

self.updateScore()方法稍后会讲到,接下来要解决更新游戏得分的问题。

After tile movement

一次有效的移动过后需要给游戏内新加入一个方块,通过检查移动前和移动后的位置是否相同判断移动是否有效。

在所有方块都被移动(或尝试移动)过后,需要检查游戏是否获胜,如果获胜,至此游戏就结束了,接着设置self.win标志。

格子发生碰撞后必然需要移动,因此只需简单设置hasMoved=true

最后需要检查是否发生移动,如果确定移动过:

  1. 给游戏添加新的方块
  2. 检查是否需要显示游戏结束的界面
Reset the tiles

每一次move方法调用,需要重新设置方块的merge状态,因为此刻已经不需要知道方块的merge状态了,将方块的状态擦除掉,认为他们可以再运行一次,在move方法开始运行时,执行:

GridService.prepareTiles();

prepareTiles()方法简单迭代方块并且重设其merge状态

prepareTiles

Keeping the score

回到updateScore()方法,游戏中需要记录两个得分:

  1. 当前得分
  2. 历史最高得分

currentScore就是一个变量,每一次游戏只需将其值保存在内存中,不需要对她有任何操作

highScore也是一个变量,但是需要在所有游戏中维持这个变量,有多种方案处理它,localstorage,cookies或者两者的结合

考虑到cookies是最容易的并且浏览器支持友好,我们的代码里面使用cookies保存highScore变量

在Angular中最容易使用cookies的方式就是使用angular-cookies模块

为了使用这个模块,需要从angularjs.org官网或者包管理器中下载它,例如bower中可以这样安装它:

$ bower install --save angular-cookies

和往常一样,需要将其引入到index.html文件中,并且在我们的应用中设置依赖ngCookies

在app/index.html文件中添加:

<script src="bower_components/angular-cookies/angular-cookies.js"></script>

更新game.js文件

angular.module('Game', ['Grid', 'ngCookies'])

使用ngCookies作为依赖后,就可以将$cookieStore服务注入到GameManager服务中了,现在就可以在用户浏览器里面获取和设置cookies了

为了得到用户最近的最高得分,我们写一个函数从用户cookie中获取它

getHighScore

回到updateScore()方法,代码将会更新游戏当前得分,如果当前得分比用户历史最高得分还高,就需要同时把它也一起更新了。

updateScore
Wrath of track by

现在方块可以在屏幕上输出了,一个bug也随之而来,结果就是,方块出现在我们未预料的位置上

bug的出现原因是:Angular根据唯一Id知道tiles数组中都有哪些方块,我们在view中使用格子在数组中的位置作为唯一Id,由于格子在数组中被我们移来移去,$index就不能做为唯一Id来使用了,我们需要另外一种方案解决这个问题。

使用方块自身的uuid区分方块而不依赖数组本身,创建方块自己的uuid将会保证Angular将tiles数组中的方块做为唯一的对象对待,只要uuid不发生变化,Angular将会将每一个方块做为uuid的对象在视图中展示出来。

对于如何为每一个格子创建uuid,可以转到StackOverflow,其中有人实现了一个遵从rfc4122的guid生成器,我们代码中将其包装为一个factory,对外提供next()方法

uuid生成器

回到TileModel中,为每一个Tile对象创建属性id

Tile

既然每一个tile对象拥有了uuid,相应的告诉Angular使用uuid去变量tiles数组,而不是$index.

tile view

上面的方案还存在一个问题,由于tiles数组在游戏开始时将所有位置都设置为null,Angular也不管不顾的尝试将null做为对象看待,由于null没有id属性,这样导致浏览器抛出一个无法操作重复对象的错误

如何解决呢,可以告诉Angular,当当前位置为空的话使用$index,否则使用tile对象的id属性,接下来修改一下tile.html文件,添加对于null值的支持:

tile.html

通过改变底层数据结构的方式,这个问题也可以解决,例如使用迭代器查找tile的位置,而不依赖于tiles数组的索引,或者通过每次重排数组,由于简单明了的原因,我们使用了数组作为其实现,因而带来了这个副作用。

We won?!?? Game over

玩原版的游戏时,如果失败了,game over的界面会从游戏面板下方滑动上来,在这个界面上允许我们选择重新开始游戏并且可以follow作者的twitter。游戏不仅仅给了玩者很酷的视觉效果,这也是一种中断游戏的优雅方式。

使用一些基础的angular技术,我们可以实现这个效果,游戏中我们使用gameOver变量来跟踪记录游戏何时结束,简单的创建一个div元素包含我们的游戏结束界面,将其绝对定位到游戏面板的位置。

创建一个包含游戏结束或者闯关成功的div元素,div中显示的内容是根据游戏的状态确定的:

棘手的部分是为其写样式,实际上我们仅仅是将其绝对定位到游戏面板的位置上,下面是部分css代码

.game-overlay

可以使用相同的技术创建闯关通过的界面,要做的仅仅是创建一个winning.game-overlay元素

Animation

原版2014让人印象深刻的一个特点是:方块魔法般的从一个格子滑动到下一个格子,游戏胜利和结束画面出现的是那么自然而不突兀,使用Angular,同样也可以做到和原版近乎一致的效果。

实际上,我们希望我们的游戏可实现滑动、展现等效果,动画是如此容易实现,以至于我们根本不需要或者只需很少的javascript就可以实现。

Animating the CSS positioning (aka adding sliding tiles)

我们的实现中,方块的定位是通过为其添加class position-[x]-[y]来实现的,当一个新的位置设置给了方块后,其对应的Dom元素就被添加上了新的定位class position-[newX]-[newY],同时旧的position-[oldX]-[oldY]将会被移除,这种情况下,为.tile添加css的transition属性就可以达到sliding的效果

SCSS代码片段如下:

.tile
Animating the game over screen

如果想要从动画中获得更多,可以使用ngAnimate模块

首先是安装ngAnimate

$ bower install --save angular-animate

其次是给index.html文件引入

script src="bower_components/angular-animate/angular-animate.js"></script>

最后在app/app.js文件中将ngAnimate模块注入

angular.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard', 'ngAnimate', 'ngCookies'])
ngAnimate

尽管深入的讨论ngAnimate 不在本文的范围内(ng-book这本书中有关于其原理的深入探讨),我们只是简单的看看她是如何工作的,以便于能够为我们的游戏添加动画效果。

ngAnimate作为一个模块级别的依赖被我们引入,任何时候,在Angular添加了一个新的对象后,一组相关的指令可以用来为其添加css的class

1.png

当一个元素被添加到ng-repeat的作用域后,新的元素将会被自动的赋予ng-enter对应的css class,接着当其真正被添加到view中时,ng-enter-active类也会被自动添加,这对我们在应用中实现动画很重要,同样ng-leave工作的模式和ng-enter是相同的

Animating the game over screen

在游戏获胜和结束的画面中,就可使用ng-enter来为其实现动画效果。记住一点,.game-overlay类的隐藏和显示使用ng-if指令控制,当ng-if的条件变化时(为真时)ngAnimate将会为其添加.ng-enter和.ng-enter-active

相关的SCSS代码如下:

.game-overlay类

Demo demo

完整的demo在这里http://ng2048.github.io/

关于这个游戏所有的代码可以在github上找到,地址在这里

To build the game locally, clone the source and run:

build 2048

推荐阅读更多精彩内容