本文從 這里 翻譯過來的。
2048這個游戲有一段時間特別火,Github上有其原始版本,游戲看起來很簡單,但是很耐玩,要玩通關(guān)卻也需要一番技巧與耐心。
剛開始學(xué)習(xí)Angular,本想著依葫蘆畫瓢實現(xiàn)ng2048,結(jié)果在github上搜了搜,發(fā)現(xiàn)別人已經(jīng)有實現(xiàn)了,而且作者還將其開發(fā)過程非常詳細(xì)的寫出來,這才有了下面硬著頭皮的翻譯。
注意 :強烈建議您參考原文試讀一下,譯者水平有限,要是惡心到了您,這個實非我所愿。
本想著寫一行大字,最好是要醒目的:"轉(zhuǎn)載請標(biāo)明出處",后來看過coolshell上一篇文章后:互聯(lián)網(wǎng)之子 – Aaron Swartz ,覺得沒什么必要了.
翻譯的過程中的確碰到了讓人頭疼不已的事情:
- 有些詞語到底要不要翻譯呢?
我會根據(jù)自己的理解,盡量避免不必要的翻譯,對于不得不翻譯,但是感覺譯過之后似有不妥的詞,我會補充到文章開始,以便作為提醒。 - 翻譯成什么樣更好呢?
這個就因能力和精力而定了, - 是直譯還是意譯呢?
漢語博大精深,有些英語長句要講明白的事情,漢語可以很簡練的表達(dá)相同的意思,同時并非人人都是文豪,文章可能會啰嗦,這時為了閱讀流暢,我會有意去掉部分語句,當(dāng)然前提是不給讀者的理解帶來影響。
Game Board:游戲面板,
grid:游戲面板上切分出來的格子,簡稱格子
tile:在格子上移動的方塊,簡稱方塊
我經(jīng)常被問到的一個問題是:什么情況下使用Angular會被認(rèn)為是一種很2的選擇,這個問題的答案通常是:游戲制作,Angular有它自己的事件循環(huán)操作($digest 循環(huán)),通常游戲需要大量的底層DOM操作。由于Angular能夠支持很多種類型的游戲,上面的理由有些牽強,即使需要大量DOM操作的游戲,Angular也是可以用來開發(fā)游戲的靜態(tài)內(nèi)容,比如跟蹤分?jǐn)?shù)排行榜和創(chuàng)建游戲菜單。
如果你和我一樣,也癡迷于2048這個流行的游戲,這個游戲的目標(biāo)是通過消除相同得分的方塊最終獲得2048這個方塊。
在我的文章中,我們準(zhǔn)備開發(fā)一個AngularJS版本的2048,從開始到結(jié)束,解釋開發(fā)的整個過程,由于2048是一個相對復(fù)雜的應(yīng)用程序,所以本文也可以看做是教授如何使用AngularJS構(gòu)建復(fù)雜應(yīng)用程序的例子。
文章太長了,不讀了:
所有的源碼放在github上,點擊這里跳轉(zhuǎn)
Index
- Planning the app
- Modular structure
- GameController
- Testing testing testing
- Building the grid
- SCSS to the rescue
- The tile directive
- The Boardgame
- Grid theory
- Gameplay (keyboard)
- Pressing the start button
- The game loop
- Keeping score
- Game over and win screens
- Animation
- Customization
- Demo
First steps: planning the app
不論程序規(guī)模大小,是復(fù)制別人的還是自己原創(chuàng)的,第一步要做的都是:高屋建瓴式的設(shè)計。
玩過2048的人應(yīng)該清楚,游戲有一個面板(board),上面是一些格子,每個格子就是一個位置,標(biāo)上數(shù)字的方塊可以在這些格子上移動,根據(jù)這個事實,可以不依賴于javascript,讓CSS3負(fù)責(zé)處理方塊在面板上的移動,

由于只有一個頁面,所以只需要一個controller管理頁面。
玩游戲期間只有一個游戲面板,我們會把相關(guān)操作grid的邏輯包含在一個GridService中,GridService services是單例對象,在它里面保存方塊是合適的,GridService將會用來操作方塊的放置,移動以及遍歷方塊尋找可能的位置。
我們會在GameManager sevice中存儲游戲的邏輯和處理程序,GameManager負(fù)責(zé)管理游戲的狀態(tài),操作格子的移動,以及維護(hù)用戶得分(包括當(dāng)前得分以及最高得分)
最后,需要一個管理鍵盤的組件,我們叫她KeyboardService,本文中我們僅實現(xiàn)PC端的操作,但是也可以重用這個service去管理觸摸操作,以便在移動設(shè)備上能夠正常工作。
Building the app
先要使用yeoman angular generator生成應(yīng)用的文件結(jié)構(gòu),這個不是必須的,我們會放置一個test目錄,這個目錄和app目錄是平級的。
下面使用yuoman建立項目,如果你更愿意手工操作,可以跳過相應(yīng)的內(nèi)容。
首先確保安裝了yeoman,yeoman依賴于NodeJS和npm,安裝NodeJS超出了本文的范圍,你可以參考NodeJS官網(wǎng)的指導(dǎo)進(jìn)行安裝。
npm安裝好之后,就可以安裝yeoman工具yo和angular generator(yo會使用generator去創(chuàng)建Angular app):
$ npm install -g yo
$ npm install -g generator-angular
安裝好之后,就可以使用yeoman工具創(chuàng)建應(yīng)用了:
$ cd ~/Development && mkdir 2048
$ yo angular twentyfourtyeight
執(zhí)行過程中會被問一些問題,除了選擇angular-cookies作為依賴這一項,其他只要回答yes就可以了,
Our angular module
現(xiàn)在創(chuàng)建程序的入口文件scripts/app.js:
angular.module('twentyfourtyeightApp', [])
Modular structure
我們推薦Angular應(yīng)用的目錄結(jié)構(gòu)采用功能分類,而不是類型分類,也就是說不要依照controllers,services,directives分割項目組件,而是應(yīng)該根據(jù)模塊功能定義模塊結(jié)構(gòu),例如我們的應(yīng)用中定義了Game模塊和KeyBoard模塊。
模塊結(jié)構(gòu)體現(xiàn)出一個清晰的文件和職責(zé)對應(yīng)關(guān)系,這有助于構(gòu)建大型的復(fù)雜Angular應(yīng)用程序,同時也更加容易得共享模塊功能。

The view
最容易開始的地方就是寫頁面了,在這個應(yīng)用中不需要多個頁面,因此創(chuàng)建一個div元素裝載應(yīng)用程序的內(nèi)容就可以了。
在app/index.html文件中,需要包含所有依賴(包括angular.js以及我們自己編寫的javascript文件-到現(xiàn)在為止,只有一個script/app.js):

后續(xù)我們只需要修改app/views/main.html文件就可以了,當(dāng)需要引入資源文件時,才會去修改app/index.html文件
打開app/views/main.html文件,其中將放置游戲需要的頁面元素,使用controllerAs語法,它告訴了$scope去哪里找到數(shù)據(jù),哪個controller負(fù)責(zé)操作哪個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版本,當(dāng)在頁面上操作多個controller時使用它非常有用
view中,我們至少要包含下面幾項
- 游戲的標(biāo)題
- 當(dāng)前得分和最高得分
- 游戲面板
游戲的靜態(tài)頭部信息簡單就是下面這樣子:

The GameController
項目框架搭起來了,接下來創(chuàng)建GameController去裝載需要在頁面中展示的元素,在app/scripts/app.js文件中,使用下面的語句在twentyfourtyeightApp模塊上創(chuàng)建controller。
angular.module('twentyfourtyeightApp', [])
.controller('GameController', function() {
});
在頁面上引用了將要給GameController設(shè)置的game對象,game對象將會引用到模塊中的main game對象,main game對象將會在新的模塊中創(chuàng)建
.controller('GameController', function(GameManager) {
this.game = GameManager;
});
由于這個模塊還沒有被創(chuàng)建,所以我們的應(yīng)用還不能在瀏覽器中運行,在Controller內(nèi)部我們添加GameManager依賴。
記住,應(yīng)用程序的不同模塊之間有依賴,為了確保依賴能夠被加載,需要將依賴的模塊注入到Angular的應(yīng)用中,為了使Game模塊成為twentyfourtyeightApp的依賴,在模塊定義地方將其注入進(jìn)來。
我們整個app/scripts/app.js文件看起來應(yīng)該是下面這樣子
angular
.module('twentyfourtyeightApp', ['Game'])
.controller('GameController', function(GameManager) {
this.game = GameManager;
});
The Game
接下來開發(fā)游戲自身的邏輯,創(chuàng)建app/scripts/game/game.js文件,新建Game模塊
angular.module('Game', []);
Game模塊提供一個核心的組件:GameManager
GameManager 負(fù)責(zé)管理游戲的狀態(tài),用戶發(fā)出的不同運動指令、跟蹤記錄游戲得分,判斷游戲是否結(jié)束以及用戶贏了還是輸了
GameManager需要支持的功能就有:
- 創(chuàng)建新游戲
- 處理循環(huán)和移動操作
- 更新游戲得分
- 監(jiān)控游戲狀態(tài)
這樣GameManager的基本框架就有了

Back to the GameManager
movesAvailable()用于檢查是否還有可用的格子,以及是否存在可以合并的方塊

Building the game grid
接下來創(chuàng)建GridService去管理游戲面板
回想一下,如何處理游戲面板呢,我們用到了兩個數(shù)組,grid和tile數(shù)組
在app/scripts/grid/grid.js文件,讓我們創(chuàng)建對應(yīng)的service

當(dāng)開始新游戲時,需要將grid和tile數(shù)組元素置為null,grid數(shù)組是靜態(tài)的,它只用于DOM元素的占位使用。
tile數(shù)組是動態(tài)的,它保存著前游戲中的方塊。
在app/views/main.html文件中添加grid指令,將controller上的GameManager對象實例傳遞到視圖里面
在app/scripts/grid/目錄里面添加文件grid_directive.js,grid指令基本不需要什么變量,她的職責(zé)只是封裝對應(yīng)的視圖。

這個指令只是負(fù)責(zé)創(chuàng)建游戲的grid視圖,其中不需要任何的邏輯。
grid.html
在指令模板中,有兩次ngRepeat的調(diào)用,分別顯示的是grid和tile數(shù)組的內(nèi)容。

第一個ng-repeat指令相當(dāng)直接,它簡單迭代grid數(shù)組,放置一個空的包含class為grid-cell的div元素
在第二個ng-repeat指令中,我們?yōu)槊恳粋€展示在屏幕上被叫做tile的元素創(chuàng)建了第二個指令,tile指令負(fù)責(zé)元素可視化展示的創(chuàng)建,稍后我們會創(chuàng)建。
聰明的讀者會注意到,我們使用一維的數(shù)組去展示一個二維的方格,當(dāng)視圖被渲染時,我們就可以看到想要的形式了。
Enter SCSS
在項目中會使用SCSS,SCSS增強了CSS,提供了動態(tài)創(chuàng)建CSS的能力。
為了創(chuàng)建二維的游戲面板,我們使用了CSS3中的transform,使用它去將方塊定位到指定的位置上。
CSS3 transform property
transform屬性可以對元素使用2D和3D變換,例如:移動元素,旋轉(zhuǎn)元素等等。
看下面的demo,是一個寬度為40px的正方形盒子,通過使用transformX(300px)就可以使這個元素沿X軸移動300px,

.box.transformed {
-webkit-transform: translateX(300px);
transform: translateX(300px);
}
可以簡單的通過給元素添加class的形式達(dá)到移動tiles方塊的目的,剩下來的工作就是:如何給游戲面板上的方塊創(chuàng)建class。
這就是SCSS閃耀的地方,我們首先創(chuàng)建一些變量,例如每一行有幾個格子,然后使用這些變量構(gòu)建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;
接下來是為游戲面板設(shè)置合適的寬度和高度,我們?yōu)槠鋬?nèi)部的容器設(shè)置絕對定位,這里貼出了部分SCSS文件的內(nèi)容,完整內(nèi)容可以在項目源碼中找到。

注意一點就是:為了使tile-container放置到grid-container之上,必須給tile-container設(shè)置一個高于grid-container的z-index值,不然瀏覽器會認(rèn)為他們處于同一個z-index上,這樣效果就不好看了。
接下來是動態(tài)生成方塊的定位,我們需要一個.position-{x}-{y}類,x、y代表的是方塊在游戲面板中的坐標(biāo),例如使用0,0 表示第一個tile的位置
下面是計算過程:

現(xiàn)在生成了動態(tài)的.position-#{x}-#{y},接下來就可以在屏幕上展示tile了

Coloring the different tiles
不同數(shù)值的tile擁有不同的顏色,使用上面用到的技術(shù),通過迭代color變量,給tile生成一個顏色相關(guān)的class,下面的SCSS數(shù)組用于定義不同方塊擁有的顏色

迭代$color數(shù)組,給不同數(shù)值的方塊創(chuàng)建對應(yīng)的顏色類,例如擁有數(shù)值2的方塊,我們將添加一個.tile-2的類,它將擁有背景色#EEE4DA,使用SCSS可以動態(tài)完成這項工作。

The Tile directive
tile指令是視圖的容器,我們不期望它里面包含很多邏輯,能夠訪問到它所占據(jù)的格子就可以了,除此之外,沒有其他功能需要添加到這個指令中了

一個有意思的事情是,方塊是如何動態(tài)的放置到游戲面板上的,多虧了模板中的ngModel變量,ngModel指向的是tiles數(shù)組。
<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坐標(biāo),x、y的值會被動態(tài)的賦給類.position-#{x}-#{y},瀏覽器在給元素應(yīng)用了這些class之后,元素就被定位到了期望的位置上。
這也意味值tile對象需要x、y和value三個屬性。
The TileModel
我們要用到Angular的依賴注入技術(shù),我們創(chuàng)建一個裝載數(shù)據(jù)的service TileModel service

Our first grid
有了TileModel,我們可以開始給tile數(shù)組添加TileModel對象的實例了,然后他們就會魔法般的出現(xiàn)在正確的格子上。

The Board’s ready for the game
現(xiàn)在可以把tile畫到屏幕上了,在GridService中需要有一個功能去準(zhǔn)備游戲的面板,當(dāng)?shù)谝淮渭虞d網(wǎng)頁的時候,創(chuàng)建一個空的游戲面板,當(dāng)用戶點擊新建游戲或者重來一次的時候,同樣需要創(chuàng)建新的游戲面板。
GridService中buildEmptyGameBoard()函數(shù)就有用來創(chuàng)建一個新的游戲面板的,這個方法負(fù)責(zé)把grid和tile數(shù)組元素置為null。

下面是一些用到的工具函數(shù)

Multi-dimensional array in one dimension
參考下面兩個圖,如何用一維數(shù)組去表示一個多維數(shù)組?



圖二中的(0,0)映射到圖三中的0,(0,1)對應(yīng)的是4 , (1,1)對應(yīng)的是5,因此得到了下面的公式
i = x + ny
其中i是一維數(shù)組元素的位置,x、y是二維數(shù)組的坐標(biāo),n每行擁有的元素的個數(shù)。
這樣,位置到坐標(biāo)和坐標(biāo)到位置的轉(zhuǎn)換函數(shù)_positionToCoordinates和_coordinatesToPosition可以就可以表示為下面的形式

Initial player positions
游戲開始時,隨機選擇兩個位置插入tile

randomlyInsertNewTile()方法隨機選擇一個可以使用的位置,用來插入新生成的方塊對象,但是首先需要知道有哪些位置可以使用。

簡單使用Math.random隨機獲取一個可用位置坐標(biāo)

下面就是randomlyInsertNewTile函數(shù)的實現(xiàn)

Keyboard interaction
現(xiàn)在已經(jīng)可以將方塊添加到游戲面板上了,但是游戲還玩不了,接下來需要把注意力切換到如何給游戲加入交互操作上了。
本文僅僅關(guān)注給游戲添加鍵盤交互動作,觸摸動作不在本文中實現(xiàn),給游戲添加觸摸動作也不是一件難事,我們關(guān)注的觸摸事件ngTouch已經(jīng)提供了,實現(xiàn)這個功能就交給你了。
使用方向鍵玩游戲(或者a, w, s, d 鍵),我們希望用戶通過簡單的方式玩游戲,不要求用戶集中注意力在游戲面板上的元素(或網(wǎng)頁上的其他元素),這樣用戶就只與聚焦的文檔進(jìn)行游戲互動。
為此,需要給document元素綁定事件監(jiān)聽器,在Angular中提供了$document服務(wù),我們就將監(jiān)聽器綁定到$document上。為了處理定義好的用戶交互動作,我們將鍵盤事件包裝后綁定到一個服務(wù)中,頁面上我們只需要一個鍵盤事件處理器,因此選擇service是正確的。
此外,無論何時檢測到用戶的鍵盤事件后,我們要觸發(fā)設(shè)置的自定義動作,使用service允許我們將其注入到Angular對象中,進(jìn)而處理用戶輸入。
在app/scripts/keyboard/keyboard.js文件中創(chuàng)建Keyboard模塊
// app/scripts/keyboard/keyboard.js
angular.module('Keyboard', []);
我們每創(chuàng)建新文件,都需要考慮將其引入到index.html文件中,現(xiàn)在index.html文件應(yīng)該包含下面的外部文件了

同時,每當(dāng)創(chuàng)建新的模塊,也需要告訴Angular,我們的應(yīng)用需要使用這個新模塊,需要將其作為依賴注入到應(yīng)用中。
.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard'])
KeyBoard Service的實現(xiàn)中,我們給$document綁定keydown事件,用來捕獲用戶交互,同時,我們也會注冊一個處理函數(shù),當(dāng)有用戶交互的時候這個處理函數(shù)會被調(diào)用。

init函數(shù)中會將鍵盤監(jiān)聽的任務(wù)交給KeyboardService去處理,所有我們感興趣的keydown事件會被過濾出來進(jìn)行處理。
任何我們感興趣事件,我們都會去阻止其默認(rèn)行為,然后將其交給keyEventHandlers處理。

如何知道觸發(fā)的事件是不是我們感興趣的呢?由于與我們游戲有關(guān)的也就是有限的鍵盤動作,所以我們能夠去檢測觸發(fā)的事件是否是我們關(guān)心的特定的鍵盤動作觸發(fā)的。

this._handleKeyEvent的職責(zé)是就是去調(diào)用已經(jīng)注冊的key handler

我們需要將處理函數(shù)添加到處理函數(shù)隊列中

Using the Keyboard service
現(xiàn)在我們有能力監(jiān)聽用戶的鍵盤輸入了,當(dāng)應(yīng)用啟動之后,我們就得做這件事請
,由于監(jiān)聽鍵盤輸入被封裝為service了,我們只需要在GameController里面做這件事情就行了。
首先需要調(diào)用init()函數(shù)啟動對鍵盤的監(jiān)聽,接著,注冊處理函數(shù),用它去調(diào)用GameManager進(jìn)而調(diào)用move()函數(shù)。
回到GameController,我們需要添加newGame()和startGame()函數(shù)newGame函數(shù)簡單的調(diào)用game service創(chuàng)建新游戲,并且啟動鍵盤事件監(jiān)聽操作。

現(xiàn)在將KeyboardService注入到GameController中

一旦要創(chuàng)建新游戲,startGame就會被調(diào)用,startGame函數(shù)會設(shè)置鍵盤的事件操作函數(shù)

Press the start button
最后一個需要實現(xiàn)的方法是newGame,位于GameManager中,這個方法做了下面幾件事情:
- 創(chuàng)建空的游戲面板
- 設(shè)置開始位置
- 初始化游戲
GridService已經(jīng)實現(xiàn)了上述的邏輯,現(xiàn)在要做的就是把他們串聯(lián)起來

Get your move on (the game loop)
現(xiàn)在將進(jìn)入游戲的核心部分,當(dāng)用戶按下鍵盤的方向鍵后,GridService的move方法就會被調(diào)用

在開始寫move方法前,我們需要先定義游戲的約束,也就是,每一次移動,游戲要如何處理。
- 獲取用戶方向鍵對應(yīng)的vector向量
- 為游戲面板上的每一個tile找到最遠(yuǎn)可能的位置,同時,判斷下一個位置中的tile是否可以合并。
- 對每一個tile,判斷下一個tile是否與其擁有相同的value
- 如果下一個tile不存在,只需將當(dāng)前的tile移動到最遠(yuǎn)可能的位置(這意味著最近的位置就是游戲面板的邊緣)
- 如果下一個tile存在,其value和當(dāng)前tile不同,只需移動當(dāng)前的tile到最遠(yuǎn)位置(當(dāng)前tile的下一個tile就是可移動的邊界)
- value相同,找到了一個可能的合并
- 如果是合并后得到的,跳過
- 如果還沒有合并,那么認(rèn)為這是一個合并
既然已經(jīng)定義出了功能,我們就能為構(gòu)造move函數(shù)設(shè)計出策略。

下一步要遍歷Grid找到所有可能的位置,在GridService上創(chuàng)建一個新的函數(shù)幫助我們找到所有可能的位置

為了獲得移動方向,需要有一個向量vector,用來描述用戶按鍵的信息,例如:當(dāng)用戶按下右方向,這表明用戶想要向右移動增加x的位置,可以將這種關(guān)系使用javascript的對象來表示,就像下面這樣子:

接下來遍歷可能的位置,使用vector向量判斷將要遍歷的方向

現(xiàn)在,traversalDirections()函數(shù)定義后,在move函數(shù)中,我們就能夠迭代可能的運動,回到GameManager,我們將使用這些潛在的位置開始遍歷grid

現(xiàn)在在position循環(huán)內(nèi)部,我們將會迭代出可能的位置,尋找該位置存在的tiles,

為了為一個tile尋找其最遠(yuǎn)可能的位置,需要走到其下一個位置檢查當(dāng)前格子是否是面板的邊沿并且該位置是空的。
如果該位置是空的,并且在格子的范圍內(nèi),接下來就繼續(xù),走到其下一個位置做相同的檢查。
如果上述的兩個檢查條件都失敗了,要么是走到格子的邊沿了,要么是找到了下一個格子了,我們將下一個位置設(shè)置為newPosition,并且記錄下一個cell

將這個功能放到GridService中

既然可以為tiles計算下一個可能的位置,也就可以檢查潛在的合并了。
合并是這樣定義的:一個方塊碰到了和它值相同的另一個方塊,代碼中將檢查看下一個位置的方塊是否待移動方塊有相同的值,并且之前沒有被合并過

現(xiàn)在,如果下一個位置不滿足條件,那只需簡單的將方塊從當(dāng)前的位置移動到newPosition這個位置。

Moving the tile
你可能已經(jīng)猜對了,將moveTile()放置到GridService中是再合適不過的了。
移動方塊就是簡單的更新一下其在一維數(shù)組中的位置,還有就是更新TileModel
Moving the tile in the array
GridService的數(shù)組反應(yīng)了在后端方塊定位在了哪里。方塊在數(shù)組中的位置沒有和其在格子中的位置進(jìn)行綁定
Updating the position on the TileModel
為了前端css放置格子,需要更新格子的坐標(biāo)。

現(xiàn)在,定義tile.updatePosition() 方法,方法做的事情正如其名字一樣,簡單的更新方塊自己的x、y坐標(biāo)

Merging a tile
既然已經(jīng)處理了簡單的情況,方塊合并就成為下一個要處理的事情了,合并被定義為下面的操作:
一個方塊在下一個潛在的位置碰到了和自己相同value的另一個方塊
當(dāng)方塊被合并后,它就從面板上別移除掉了,同時會更新游戲的當(dāng)前得分和歷史最高分
合并包含幾個步驟:
- 添加一個新的方塊到最終的位置上,其value是合并過的值
- 移除原有的方塊
- 更新游戲得分
- 檢查是否獲勝

游戲僅僅支持單一的方塊移動,也就是說當(dāng)一行出現(xiàn)多個合并位置時,真正的合并只會發(fā)生一次,所以需要對已經(jīng)發(fā)生過的合并進(jìn)行標(biāo)記,代碼中使用merged屬性來做這件事。
代碼中有兩個方法目前還沒有實現(xiàn),GridService.newTile()簡單創(chuàng)建一個新的TileModel對象

self.updateScore()方法稍后會講到,接下來要解決更新游戲得分的問題。
After tile movement
一次有效的移動過后需要給游戲內(nèi)新加入一個方塊,通過檢查移動前和移動后的位置是否相同判斷移動是否有效。

在所有方塊都被移動(或嘗試移動)過后,需要檢查游戲是否獲勝,如果獲勝,至此游戲就結(jié)束了,接著設(shè)置self.win標(biāo)志。
格子發(fā)生碰撞后必然需要移動,因此只需簡單設(shè)置hasMoved=true
最后需要檢查是否發(fā)生移動,如果確定移動過:
- 給游戲添加新的方塊
- 檢查是否需要顯示游戲結(jié)束的界面

Reset the tiles
每一次move方法調(diào)用,需要重新設(shè)置方塊的merge狀態(tài),因為此刻已經(jīng)不需要知道方塊的merge狀態(tài)了,將方塊的狀態(tài)擦除掉,認(rèn)為他們可以再運行一次,在move方法開始運行時,執(zhí)行:
GridService.prepareTiles();
prepareTiles()方法簡單迭代方塊并且重設(shè)其merge狀態(tài)

Keeping the score
回到updateScore()方法,游戲中需要記錄兩個得分:
- 當(dāng)前得分
- 歷史最高得分
currentScore就是一個變量,每一次游戲只需將其值保存在內(nèi)存中,不需要對她有任何操作
highScore也是一個變量,但是需要在所有游戲中維持這個變量,有多種方案處理它,localstorage,cookies或者兩者的結(jié)合
考慮到cookies是最容易的并且瀏覽器支持友好,我們的代碼里面使用cookies保存highScore變量
在Angular中最容易使用cookies的方式就是使用angular-cookies模塊
為了使用這個模塊,需要從angularjs.org官網(wǎng)或者包管理器中下載它,例如bower中可以這樣安裝它:
$ bower install --save angular-cookies
和往常一樣,需要將其引入到index.html文件中,并且在我們的應(yīng)用中設(shè)置依賴ngCookies
在app/index.html文件中添加:
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
更新game.js文件
angular.module('Game', ['Grid', 'ngCookies'])
使用ngCookies作為依賴后,就可以將$cookieStore服務(wù)注入到GameManager服務(wù)中了,現(xiàn)在就可以在用戶瀏覽器里面獲取和設(shè)置cookies了
為了得到用戶最近的最高得分,我們寫一個函數(shù)從用戶cookie中獲取它

回到updateScore()方法,代碼將會更新游戲當(dāng)前得分,如果當(dāng)前得分比用戶歷史最高得分還高,就需要同時把它也一起更新了。

Wrath of track by
現(xiàn)在方塊可以在屏幕上輸出了,一個bug也隨之而來,結(jié)果就是,方塊出現(xiàn)在我們未預(yù)料的位置上
bug的出現(xiàn)原因是:Angular根據(jù)唯一Id知道tiles數(shù)組中都有哪些方塊,我們在view中使用格子在數(shù)組中的位置作為唯一Id,由于格子在數(shù)組中被我們移來移去,$index就不能做為唯一Id來使用了,我們需要另外一種方案解決這個問題。

使用方塊自身的uuid區(qū)分方塊而不依賴數(shù)組本身,創(chuàng)建方塊自己的uuid將會保證Angular將tiles數(shù)組中的方塊做為唯一的對象對待,只要uuid不發(fā)生變化,Angular將會將每一個方塊做為uuid的對象在視圖中展示出來。
對于如何為每一個格子創(chuàng)建uuid,可以轉(zhuǎn)到StackOverflow,其中有人實現(xiàn)了一個遵從rfc4122的guid生成器,我們代碼中將其包裝為一個factory,對外提供next()方法

回到TileModel中,為每一個Tile對象創(chuàng)建屬性id

既然每一個tile對象擁有了uuid,相應(yīng)的告訴Angular使用uuid去變量tiles數(shù)組,而不是$index.

上面的方案還存在一個問題,由于tiles數(shù)組在游戲開始時將所有位置都設(shè)置為null,Angular也不管不顧的嘗試將null做為對象看待,由于null沒有id屬性,這樣導(dǎo)致瀏覽器拋出一個無法操作重復(fù)對象的錯誤
如何解決呢,可以告訴Angular,當(dāng)當(dāng)前位置為空的話使用$index,否則使用tile對象的id屬性,接下來修改一下tile.html文件,添加對于null值的支持:

通過改變底層數(shù)據(jù)結(jié)構(gòu)的方式,這個問題也可以解決,例如使用迭代器查找tile的位置,而不依賴于tiles數(shù)組的索引,或者通過每次重排數(shù)組,由于簡單明了的原因,我們使用了數(shù)組作為其實現(xiàn),因而帶來了這個副作用。
We won?!?? Game over
玩原版的游戲時,如果失敗了,game over的界面會從游戲面板下方滑動上來,在這個界面上允許我們選擇重新開始游戲并且可以follow作者的twitter。游戲不僅僅給了玩者很酷的視覺效果,這也是一種中斷游戲的優(yōu)雅方式。
使用一些基礎(chǔ)的angular技術(shù),我們可以實現(xiàn)這個效果,游戲中我們使用gameOver變量來跟蹤記錄游戲何時結(jié)束,簡單的創(chuàng)建一個div元素包含我們的游戲結(jié)束界面,將其絕對定位到游戲面板的位置。
創(chuàng)建一個包含游戲結(jié)束或者闖關(guān)成功的div元素,div中顯示的內(nèi)容是根據(jù)游戲的狀態(tài)確定的:

棘手的部分是為其寫樣式,實際上我們僅僅是將其絕對定位到游戲面板的位置上,下面是部分css代碼

可以使用相同的技術(shù)創(chuàng)建闖關(guān)通過的界面,要做的僅僅是創(chuàng)建一個winning.game-overlay元素
Animation
原版2014讓人印象深刻的一個特點是:方塊魔法般的從一個格子滑動到下一個格子,游戲勝利和結(jié)束畫面出現(xiàn)的是那么自然而不突兀,使用Angular,同樣也可以做到和原版近乎一致的效果。
實際上,我們希望我們的游戲可實現(xiàn)滑動、展現(xiàn)等效果,動畫是如此容易實現(xiàn),以至于我們根本不需要或者只需很少的javascript就可以實現(xiàn)。
Animating the CSS positioning (aka adding sliding tiles)
我們的實現(xiàn)中,方塊的定位是通過為其添加class position-[x]-[y]來實現(xiàn)的,當(dāng)一個新的位置設(shè)置給了方塊后,其對應(yīng)的Dom元素就被添加上了新的定位class position-[newX]-[newY],同時舊的position-[oldX]-[oldY]將會被移除,這種情況下,為.tile添加css的transition屬性就可以達(dá)到sliding的效果
SCSS代碼片段如下:

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 不在本文的范圍內(nèi)(ng-book這本書中有關(guān)于其原理的深入探討),我們只是簡單的看看她是如何工作的,以便于能夠為我們的游戲添加動畫效果。
ngAnimate作為一個模塊級別的依賴被我們引入,任何時候,在Angular添加了一個新的對象后,一組相關(guān)的指令可以用來為其添加css的class

當(dāng)一個元素被添加到ng-repeat的作用域后,新的元素將會被自動的賦予ng-enter對應(yīng)的css class,接著當(dāng)其真正被添加到view中時,ng-enter-active類也會被自動添加,這對我們在應(yīng)用中實現(xiàn)動畫很重要,同樣ng-leave工作的模式和ng-enter是相同的
Animating the game over screen
在游戲獲勝和結(jié)束的畫面中,就可使用ng-enter來為其實現(xiàn)動畫效果。記住一點,.game-overlay類的隱藏和顯示使用ng-if指令控制,當(dāng)ng-if的條件變化時(為真時)ngAnimate將會為其添加.ng-enter和.ng-enter-active
相關(guān)的SCSS代碼如下:

Demo demo
完整的demo在這里http://ng2048.github.io/
關(guān)于這個游戲所有的代碼可以在github上找到,地址在這里
To build the game locally, clone the source and run:
