本文從 這里 翻譯過(guò)來(lái)的郑气。
2048這個(gè)游戲有一段時(shí)間特別火,Github上有其原始版本腰池,游戲看起來(lái)很簡(jiǎn)單尾组,但是很耐玩,要玩通關(guān)卻也需要一番技巧與耐心示弓。
剛開(kāi)始學(xué)習(xí)Angular讳侨,本想著依葫蘆畫(huà)瓢實(shí)現(xiàn)ng2048,結(jié)果在github上搜了搜奏属,發(fā)現(xiàn)別人已經(jīng)有實(shí)現(xiàn)了跨跨,而且作者還將其開(kāi)發(fā)過(guò)程非常詳細(xì)的寫(xiě)出來(lái),這才有了下面硬著頭皮的翻譯囱皿。
注意 :強(qiáng)烈建議您參考原文試讀一下勇婴,譯者水平有限,要是惡心到了您嘱腥,這個(gè)實(shí)非我所愿耕渴。
本想著寫(xiě)一行大字,最好是要醒目的:"轉(zhuǎn)載請(qǐng)標(biāo)明出處"齿兔,后來(lái)看過(guò)coolshell上一篇文章后:互聯(lián)網(wǎng)之子 – Aaron Swartz 橱脸,覺(jué)得沒(méi)什么必要了.
翻譯的過(guò)程中的確碰到了讓人頭疼不已的事情:
- 有些詞語(yǔ)到底要不要翻譯呢?
我會(huì)根據(jù)自己的理解分苇,盡量避免不必要的翻譯添诉,對(duì)于不得不翻譯,但是感覺(jué)譯過(guò)之后似有不妥的詞组砚,我會(huì)補(bǔ)充到文章開(kāi)始吻商,以便作為提醒。 - 翻譯成什么樣更好呢糟红?
這個(gè)就因能力和精力而定了艾帐, - 是直譯還是意譯呢乌叶?
漢語(yǔ)博大精深,有些英語(yǔ)長(zhǎng)句要講明白的事情柒爸,漢語(yǔ)可以很簡(jiǎn)練的表達(dá)相同的意思准浴,同時(shí)并非人人都是文豪,文章可能會(huì)啰嗦捎稚,這時(shí)為了閱讀流暢乐横,我會(huì)有意去掉部分語(yǔ)句,當(dāng)然前提是不給讀者的理解帶來(lái)影響今野。
Game Board:游戲面板葡公,
grid:游戲面板上切分出來(lái)的格子,簡(jiǎn)稱格子
tile:在格子上移動(dòng)的方塊条霜,簡(jiǎn)稱方塊
我經(jīng)常被問(wèn)到的一個(gè)問(wèn)題是:什么情況下使用Angular會(huì)被認(rèn)為是一種很2的選擇催什,這個(gè)問(wèn)題的答案通常是:游戲制作,Angular有它自己的事件循環(huán)操作($digest 循環(huán))宰睡,通常游戲需要大量的底層DOM操作蒲凶。由于Angular能夠支持很多種類(lèi)型的游戲,上面的理由有些牽強(qiáng)拆内,即使需要大量DOM操作的游戲旋圆,Angular也是可以用來(lái)開(kāi)發(fā)游戲的靜態(tài)內(nèi)容,比如跟蹤分?jǐn)?shù)排行榜和創(chuàng)建游戲菜單麸恍。
如果你和我一樣灵巧,也癡迷于2048這個(gè)流行的游戲,這個(gè)游戲的目標(biāo)是通過(guò)消除相同得分的方塊最終獲得2048這個(gè)方塊抹沪。
在我的文章中孩等,我們準(zhǔn)備開(kāi)發(fā)一個(gè)AngularJS版本的2048,從開(kāi)始到結(jié)束采够,解釋開(kāi)發(fā)的整個(gè)過(guò)程,由于2048是一個(gè)相對(duì)復(fù)雜的應(yīng)用程序冰垄,所以本文也可以看做是教授如何使用AngularJS構(gòu)建復(fù)雜應(yīng)用程序的例子蹬癌。
文章太長(zhǎng)了,不讀了:
所有的源碼放在github上虹茶,點(diǎn)擊這里跳轉(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è)計(jì)蝴罪。
玩過(guò)2048的人應(yīng)該清楚董济,游戲有一個(gè)面板(board),上面是一些格子要门,每個(gè)格子就是一個(gè)位置虏肾,標(biāo)上數(shù)字的方塊可以在這些格子上移動(dòng)廓啊,根據(jù)這個(gè)事實(shí),可以不依賴于javascript封豪,讓CSS3負(fù)責(zé)處理方塊在面板上的移動(dòng)谴轮,
由于只有一個(gè)頁(yè)面,所以只需要一個(gè)controller管理頁(yè)面吹埠。
玩游戲期間只有一個(gè)游戲面板第步,我們會(huì)把相關(guān)操作grid的邏輯包含在一個(gè)GridService中,GridService services是單例對(duì)象缘琅,在它里面保存方塊是合適的粘都,GridService將會(huì)用來(lái)操作方塊的放置,移動(dòng)以及遍歷方塊尋找可能的位置刷袍。
我們會(huì)在GameManager sevice中存儲(chǔ)游戲的邏輯和處理程序翩隧,GameManager負(fù)責(zé)管理游戲的狀態(tài),操作格子的移動(dòng)做个,以及維護(hù)用戶得分(包括當(dāng)前得分以及最高得分)
最后鸽心,需要一個(gè)管理鍵盤(pán)的組件,我們叫她KeyboardService居暖,本文中我們僅實(shí)現(xiàn)PC端的操作顽频,但是也可以重用這個(gè)service去管理觸摸操作,以便在移動(dòng)設(shè)備上能夠正常工作太闺。
Building the app
先要使用yeoman angular generator生成應(yīng)用的文件結(jié)構(gòu)糯景,這個(gè)不是必須的,我們會(huì)放置一個(gè)test目錄省骂,這個(gè)目錄和app目錄是平級(jí)的蟀淮。
下面使用yuoman建立項(xiàng)目,如果你更愿意手工操作钞澳,可以跳過(guò)相應(yīng)的內(nèi)容怠惶。
首先確保安裝了yeoman,yeoman依賴于NodeJS和npm轧粟,安裝NodeJS超出了本文的范圍策治,你可以參考NodeJS官網(wǎng)的指導(dǎo)進(jìn)行安裝。
npm安裝好之后兰吟,就可以安裝yeoman工具yo和angular generator(yo會(huì)使用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í)行過(guò)程中會(huì)被問(wèn)一些問(wèn)題,除了選擇angular-cookies作為依賴這一項(xiàng)混蔼,其他只要回答yes就可以了履腋,
Our angular module
現(xiàn)在創(chuàng)建程序的入口文件scripts/app.js:
angular.module('twentyfourtyeightApp', [])
Modular structure
我們推薦Angular應(yīng)用的目錄結(jié)構(gòu)采用功能分類(lèi),而不是類(lèi)型分類(lèi),也就是說(shuō)不要依照controllers遵湖,services悔政,directives分割項(xiàng)目組件,而是應(yīng)該根據(jù)模塊功能定義模塊結(jié)構(gòu)奄侠,例如我們的應(yīng)用中定義了Game模塊和KeyBoard模塊卓箫。
模塊結(jié)構(gòu)體現(xiàn)出一個(gè)清晰的文件和職責(zé)對(duì)應(yīng)關(guān)系,這有助于構(gòu)建大型的復(fù)雜Angular應(yīng)用程序垄潮,同時(shí)也更加容易得共享模塊功能烹卒。
The view
最容易開(kāi)始的地方就是寫(xiě)頁(yè)面了,在這個(gè)應(yīng)用中不需要多個(gè)頁(yè)面弯洗,因此創(chuàng)建一個(gè)div元素裝載應(yīng)用程序的內(nèi)容就可以了旅急。
在app/index.html文件中,需要包含所有依賴(包括angular.js以及我們自己編寫(xiě)的javascript文件-到現(xiàn)在為止牡整,只有一個(gè)script/app.js):
后續(xù)我們只需要修改app/views/main.html文件就可以了藐吮,當(dāng)需要引入資源文件時(shí),才會(huì)去修改app/index.html文件
打開(kāi)app/views/main.html文件逃贝,其中將放置游戲需要的頁(yè)面元素谣辞,使用controllerAs語(yǔ)法,它告訴了$scope去哪里找到數(shù)據(jù)沐扳,哪個(gè)controller負(fù)責(zé)操作哪個(gè)component泥从。
<!-- app/views/main.html -->
<div id="content" ng-controller='GameController as ctrl'>
<!-- Now the variable: ctrl refers to the GameController -->
</div>
controllerAs語(yǔ)法來(lái)自1.2版本,當(dāng)在頁(yè)面上操作多個(gè)controller時(shí)使用它非常有用
view中沪摄,我們至少要包含下面幾項(xiàng)
- 游戲的標(biāo)題
- 當(dāng)前得分和最高得分
- 游戲面板
游戲的靜態(tài)頭部信息簡(jiǎn)單就是下面這樣子:
The GameController
項(xiàng)目框架搭起來(lái)了躯嫉,接下來(lái)創(chuàng)建GameController去裝載需要在頁(yè)面中展示的元素,在app/scripts/app.js文件中杨拐,使用下面的語(yǔ)句在twentyfourtyeightApp模塊上創(chuàng)建controller祈餐。
angular.module('twentyfourtyeightApp', [])
.controller('GameController', function() {
});
在頁(yè)面上引用了將要給GameController設(shè)置的game對(duì)象,game對(duì)象將會(huì)引用到模塊中的main game對(duì)象哄陶,main game對(duì)象將會(huì)在新的模塊中創(chuàng)建
.controller('GameController', function(GameManager) {
this.game = GameManager;
});
由于這個(gè)模塊還沒(méi)有被創(chuàng)建帆阳,所以我們的應(yīng)用還不能在瀏覽器中運(yùn)行,在Controller內(nèi)部我們添加GameManager依賴屋吨。
記住舱痘,應(yīng)用程序的不同模塊之間有依賴,為了確保依賴能夠被加載离赫,需要將依賴的模塊注入到Angular的應(yīng)用中,為了使Game模塊成為twentyfourtyeightApp的依賴塌碌,在模塊定義地方將其注入進(jìn)來(lái)渊胸。
我們整個(gè)app/scripts/app.js文件看起來(lái)應(yīng)該是下面這樣子
angular
.module('twentyfourtyeightApp', ['Game'])
.controller('GameController', function(GameManager) {
this.game = GameManager;
});
The Game
接下來(lái)開(kāi)發(fā)游戲自身的邏輯,創(chuàng)建app/scripts/game/game.js文件台妆,新建Game模塊
angular.module('Game', []);
Game模塊提供一個(gè)核心的組件:GameManager
GameManager 負(fù)責(zé)管理游戲的狀態(tài)翎猛,用戶發(fā)出的不同運(yùn)動(dòng)指令胖翰、跟蹤記錄游戲得分,判斷游戲是否結(jié)束以及用戶贏了還是輸了
GameManager需要支持的功能就有:
- 創(chuàng)建新游戲
- 處理循環(huán)和移動(dòng)操作
- 更新游戲得分
- 監(jiān)控游戲狀態(tài)
這樣GameManager的基本框架就有了
Back to the GameManager
movesAvailable()用于檢查是否還有可用的格子切厘,以及是否存在可以合并的方塊
Building the game grid
接下來(lái)創(chuàng)建GridService去管理游戲面板
回想一下萨咳,如何處理游戲面板呢,我們用到了兩個(gè)數(shù)組疫稿,grid和tile數(shù)組
在app/scripts/grid/grid.js文件培他,讓我們創(chuàng)建對(duì)應(yīng)的service
當(dāng)開(kāi)始新游戲時(shí),需要將grid和tile數(shù)組元素置為null遗座,grid數(shù)組是靜態(tài)的舀凛,它只用于DOM元素的占位使用。
tile數(shù)組是動(dòng)態(tài)的途蒋,它保存著前游戲中的方塊猛遍。
在app/views/main.html文件中添加grid指令,將controller上的GameManager對(duì)象實(shí)例傳遞到視圖里面
在app/scripts/grid/目錄里面添加文件grid_directive.js号坡,grid指令基本不需要什么變量懊烤,她的職責(zé)只是封裝對(duì)應(yīng)的視圖。
這個(gè)指令只是負(fù)責(zé)創(chuàng)建游戲的grid視圖宽堆,其中不需要任何的邏輯腌紧。
grid.html
在指令模板中,有兩次ngRepeat的調(diào)用日麸,分別顯示的是grid和tile數(shù)組的內(nèi)容寄啼。
第一個(gè)ng-repeat指令相當(dāng)直接,它簡(jiǎn)單迭代grid數(shù)組代箭,放置一個(gè)空的包含class為grid-cell的div元素
在第二個(gè)ng-repeat指令中墩划,我們?yōu)槊恳粋€(gè)展示在屏幕上被叫做tile的元素創(chuàng)建了第二個(gè)指令,tile指令負(fù)責(zé)元素可視化展示的創(chuàng)建嗡综,稍后我們會(huì)創(chuàng)建乙帮。
聰明的讀者會(huì)注意到,我們使用一維的數(shù)組去展示一個(gè)二維的方格极景,當(dāng)視圖被渲染時(shí)察净,我們就可以看到想要的形式了。
Enter SCSS
在項(xiàng)目中會(huì)使用SCSS盼樟,SCSS增強(qiáng)了CSS氢卡,提供了動(dòng)態(tài)創(chuàng)建CSS的能力。
為了創(chuàng)建二維的游戲面板晨缴,我們使用了CSS3中的transform译秦,使用它去將方塊定位到指定的位置上。
CSS3 transform property
transform屬性可以對(duì)元素使用2D和3D變換,例如:移動(dòng)元素筑悴,旋轉(zhuǎn)元素等等们拙。
看下面的demo,是一個(gè)寬度為40px的正方形盒子阁吝,通過(guò)使用transformX(300px)就可以使這個(gè)元素沿X軸移動(dòng)300px砚婆,
.box.transformed {
-webkit-transform: translateX(300px);
transform: translateX(300px);
}
可以簡(jiǎn)單的通過(guò)給元素添加class的形式達(dá)到移動(dòng)tiles方塊的目的,剩下來(lái)的工作就是:如何給游戲面板上的方塊創(chuàng)建class突勇。
這就是SCSS閃耀的地方装盯,我們首先創(chuàng)建一些變量,例如每一行有幾個(gè)格子与境,然后使用這些變量構(gòu)建SCSS验夯,下面這些變量可以用來(lái)給游戲面板定位:
$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
通過(guò)在SCSS使用這些變量就可以為我們計(jì)算位置了,首先需要計(jì)算每一個(gè)方塊的寬度:
$tile-size: ($width - $tile-padding * ($tile-count + 1)) / $tile-count;
接下來(lái)是為游戲面板設(shè)置合適的寬度和高度摔刁,我們?yōu)槠鋬?nèi)部的容器設(shè)置絕對(duì)定位挥转,這里貼出了部分SCSS文件的內(nèi)容,完整內(nèi)容可以在項(xiàng)目源碼中找到共屈。
注意一點(diǎn)就是:為了使tile-container放置到grid-container之上绑谣,必須給tile-container設(shè)置一個(gè)高于grid-container的z-index值,不然瀏覽器會(huì)認(rèn)為他們處于同一個(gè)z-index上拗引,這樣效果就不好看了借宵。
接下來(lái)是動(dòng)態(tài)生成方塊的定位,我們需要一個(gè).position-{x}-{y}類(lèi)矾削,x壤玫、y代表的是方塊在游戲面板中的坐標(biāo),例如使用0哼凯,0 表示第一個(gè)tile的位置
下面是計(jì)算過(guò)程:
現(xiàn)在生成了動(dòng)態(tài)的.position-#{x}-#{y}欲间,接下來(lái)就可以在屏幕上展示tile了
Coloring the different tiles
不同數(shù)值的tile擁有不同的顏色,使用上面用到的技術(shù)断部,通過(guò)迭代color變量猎贴,給tile生成一個(gè)顏色相關(guān)的class,下面的SCSS數(shù)組用于定義不同方塊擁有的顏色
迭代$color數(shù)組蝴光,給不同數(shù)值的方塊創(chuàng)建對(duì)應(yīng)的顏色類(lèi)她渴,例如擁有數(shù)值2的方塊,我們將添加一個(gè).tile-2的類(lèi)蔑祟,它將擁有背景色#EEE4DA趁耗,使用SCSS可以動(dòng)態(tài)完成這項(xiàng)工作。
The Tile directive
tile指令是視圖的容器疆虚,我們不期望它里面包含很多邏輯苛败,能夠訪問(wèn)到它所占據(jù)的格子就可以了右冻,除此之外,沒(méi)有其他功能需要添加到這個(gè)指令中了
一個(gè)有意思的事情是著拭,方塊是如何動(dòng)態(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>
有了這個(gè)基本的指令,我們幾乎可以在屏幕上顯示了暗赶,每一個(gè)tile都有x和y坐標(biāo)鄙币,x、y的值會(huì)被動(dòng)態(tài)的賦給類(lèi).position-#{x}-#{y}蹂随,瀏覽器在給元素應(yīng)用了這些class之后十嘿,元素就被定位到了期望的位置上。
這也意味值tile對(duì)象需要x岳锁、y和value三個(gè)屬性绩衷。
The TileModel
我們要用到Angular的依賴注入技術(shù),我們創(chuàng)建一個(gè)裝載數(shù)據(jù)的service TileModel service
Our first grid
有了TileModel激率,我們可以開(kāi)始給tile數(shù)組添加TileModel對(duì)象的實(shí)例了咳燕,然后他們就會(huì)魔法般的出現(xiàn)在正確的格子上。
The Board’s ready for the game
現(xiàn)在可以把tile畫(huà)到屏幕上了乒躺,在GridService中需要有一個(gè)功能去準(zhǔn)備游戲的面板招盲,當(dāng)?shù)谝淮渭虞d網(wǎng)頁(yè)的時(shí)候,創(chuàng)建一個(gè)空的游戲面板嘉冒,當(dāng)用戶點(diǎn)擊新建游戲或者重來(lái)一次的時(shí)候曹货,同樣需要?jiǎng)?chuàng)建新的游戲面板。
GridService中buildEmptyGameBoard()函數(shù)就有用來(lái)創(chuàng)建一個(gè)新的游戲面板的,這個(gè)方法負(fù)責(zé)把grid和tile數(shù)組元素置為null伟骨。
下面是一些用到的工具函數(shù)
Multi-dimensional array in one dimension
參考下面兩個(gè)圖垮庐,如何用一維數(shù)組去表示一個(gè)多維數(shù)組?
圖二中的(0蜕衡,0)映射到圖三中的0,(0,1)對(duì)應(yīng)的是4 设拟, (1慨仿,1)對(duì)應(yīng)的是5,因此得到了下面的公式
i = x + ny
其中i是一維數(shù)組元素的位置纳胧,x镰吆、y是二維數(shù)組的坐標(biāo),n每行擁有的元素的個(gè)數(shù)跑慕。
這樣万皿,位置到坐標(biāo)和坐標(biāo)到位置的轉(zhuǎn)換函數(shù)_positionToCoordinates和_coordinatesToPosition可以就可以表示為下面的形式
Initial player positions
游戲開(kāi)始時(shí),隨機(jī)選擇兩個(gè)位置插入tile
randomlyInsertNewTile()方法隨機(jī)選擇一個(gè)可以使用的位置牢硅,用來(lái)插入新生成的方塊對(duì)象蹬耘,但是首先需要知道有哪些位置可以使用。
簡(jiǎn)單使用Math.random隨機(jī)獲取一個(gè)可用位置坐標(biāo)
下面就是randomlyInsertNewTile函數(shù)的實(shí)現(xiàn)
Keyboard interaction
現(xiàn)在已經(jīng)可以將方塊添加到游戲面板上了减余,但是游戲還玩不了综苔,接下來(lái)需要把注意力切換到如何給游戲加入交互操作上了。
本文僅僅關(guān)注給游戲添加鍵盤(pán)交互動(dòng)作位岔,觸摸動(dòng)作不在本文中實(shí)現(xiàn)如筛,給游戲添加觸摸動(dòng)作也不是一件難事,我們關(guān)注的觸摸事件ngTouch已經(jīng)提供了抒抬,實(shí)現(xiàn)這個(gè)功能就交給你了杨刨。
使用方向鍵玩游戲(或者a, w, s, d 鍵),我們希望用戶通過(guò)簡(jiǎn)單的方式玩游戲擦剑,不要求用戶集中注意力在游戲面板上的元素(或網(wǎng)頁(yè)上的其他元素)妖胀,這樣用戶就只與聚焦的文檔進(jìn)行游戲互動(dòng)。
為此抓于,需要給document元素綁定事件監(jiān)聽(tīng)器做粤,在Angular中提供了$document服務(wù),我們就將監(jiān)聽(tīng)器綁定到$document上捉撮。為了處理定義好的用戶交互動(dòng)作怕品,我們將鍵盤(pán)事件包裝后綁定到一個(gè)服務(wù)中,頁(yè)面上我們只需要一個(gè)鍵盤(pán)事件處理器巾遭,因此選擇service是正確的肉康。
此外,無(wú)論何時(shí)檢測(cè)到用戶的鍵盤(pán)事件后灼舍,我們要觸發(fā)設(shè)置的自定義動(dòng)作吼和,使用service允許我們將其注入到Angular對(duì)象中,進(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)該包含下面的外部文件了
同時(shí)献丑,每當(dāng)創(chuàng)建新的模塊末捣,也需要告訴Angular,我們的應(yīng)用需要使用這個(gè)新模塊创橄,需要將其作為依賴注入到應(yīng)用中箩做。
.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard'])
KeyBoard Service的實(shí)現(xiàn)中,我們給$document綁定keydown事件妥畏,用來(lái)捕獲用戶交互邦邦,同時(shí)安吁,我們也會(huì)注冊(cè)一個(gè)處理函數(shù),當(dāng)有用戶交互的時(shí)候這個(gè)處理函數(shù)會(huì)被調(diào)用燃辖。
init函數(shù)中會(huì)將鍵盤(pán)監(jiān)聽(tīng)的任務(wù)交給KeyboardService去處理鬼店,所有我們感興趣的keydown事件會(huì)被過(guò)濾出來(lái)進(jìn)行處理。
任何我們感興趣事件黔龟,我們都會(huì)去阻止其默認(rèn)行為薪韩,然后將其交給keyEventHandlers處理。
如何知道觸發(fā)的事件是不是我們感興趣的呢捌锭?由于與我們游戲有關(guān)的也就是有限的鍵盤(pán)動(dòng)作,所以我們能夠去檢測(cè)觸發(fā)的事件是否是我們關(guān)心的特定的鍵盤(pán)動(dòng)作觸發(fā)的罗捎。
this._handleKeyEvent的職責(zé)是就是去調(diào)用已經(jīng)注冊(cè)的key handler
我們需要將處理函數(shù)添加到處理函數(shù)隊(duì)列中
Using the Keyboard service
現(xiàn)在我們有能力監(jiān)聽(tīng)用戶的鍵盤(pán)輸入了观谦,當(dāng)應(yīng)用啟動(dòng)之后,我們就得做這件事請(qǐng)
桨菜,由于監(jiān)聽(tīng)鍵盤(pán)輸入被封裝為service了豁状,我們只需要在GameController里面做這件事情就行了。
首先需要調(diào)用init()函數(shù)啟動(dòng)對(duì)鍵盤(pán)的監(jiān)聽(tīng)倒得,接著泻红,注冊(cè)處理函數(shù),用它去調(diào)用GameManager進(jìn)而調(diào)用move()函數(shù)霞掺。
回到GameController谊路,我們需要添加newGame()和startGame()函數(shù)newGame函數(shù)簡(jiǎn)單的調(diào)用game service創(chuàng)建新游戲,并且啟動(dòng)鍵盤(pán)事件監(jiān)聽(tīng)操作菩彬。
現(xiàn)在將KeyboardService注入到GameController中
一旦要?jiǎng)?chuàng)建新游戲缠劝,startGame就會(huì)被調(diào)用,startGame函數(shù)會(huì)設(shè)置鍵盤(pán)的事件操作函數(shù)
Press the start button
最后一個(gè)需要實(shí)現(xiàn)的方法是newGame骗灶,位于GameManager中惨恭,這個(gè)方法做了下面幾件事情:
- 創(chuàng)建空的游戲面板
- 設(shè)置開(kāi)始位置
- 初始化游戲
GridService已經(jīng)實(shí)現(xiàn)了上述的邏輯,現(xiàn)在要做的就是把他們串聯(lián)起來(lái)
Get your move on (the game loop)
現(xiàn)在將進(jìn)入游戲的核心部分耙旦,當(dāng)用戶按下鍵盤(pán)的方向鍵后脱羡,GridService的move方法就會(huì)被調(diào)用
在開(kāi)始寫(xiě)move方法前,我們需要先定義游戲的約束免都,也就是锉罐,每一次移動(dòng),游戲要如何處理琴昆。
- 獲取用戶方向鍵對(duì)應(yīng)的vector向量
- 為游戲面板上的每一個(gè)tile找到最遠(yuǎn)可能的位置氓鄙,同時(shí),判斷下一個(gè)位置中的tile是否可以合并业舍。
- 對(duì)每一個(gè)tile抖拦,判斷下一個(gè)tile是否與其擁有相同的value
- 如果下一個(gè)tile不存在升酣,只需將當(dāng)前的tile移動(dòng)到最遠(yuǎn)可能的位置(這意味著最近的位置就是游戲面板的邊緣)
- 如果下一個(gè)tile存在,其value和當(dāng)前tile不同态罪,只需移動(dòng)當(dāng)前的tile到最遠(yuǎn)位置(當(dāng)前tile的下一個(gè)tile就是可移動(dòng)的邊界)
- value相同噩茄,找到了一個(gè)可能的合并
- 如果是合并后得到的,跳過(guò)
- 如果還沒(méi)有合并复颈,那么認(rèn)為這是一個(gè)合并
既然已經(jīng)定義出了功能绩聘,我們就能為構(gòu)造move函數(shù)設(shè)計(jì)出策略。
下一步要遍歷Grid找到所有可能的位置耗啦,在GridService上創(chuàng)建一個(gè)新的函數(shù)幫助我們找到所有可能的位置
為了獲得移動(dòng)方向凿菩,需要有一個(gè)向量vector,用來(lái)描述用戶按鍵的信息帜讲,例如:當(dāng)用戶按下右方向衅谷,這表明用戶想要向右移動(dòng)增加x的位置,可以將這種關(guān)系使用javascript的對(duì)象來(lái)表示似将,就像下面這樣子:
接下來(lái)遍歷可能的位置获黔,使用vector向量判斷將要遍歷的方向
現(xiàn)在,traversalDirections()函數(shù)定義后在验,在move函數(shù)中玷氏,我們就能夠迭代可能的運(yùn)動(dòng),回到GameManager腋舌,我們將使用這些潛在的位置開(kāi)始遍歷grid
現(xiàn)在在position循環(huán)內(nèi)部盏触,我們將會(huì)迭代出可能的位置,尋找該位置存在的tiles块饺,
為了為一個(gè)tile尋找其最遠(yuǎn)可能的位置耻陕,需要走到其下一個(gè)位置檢查當(dāng)前格子是否是面板的邊沿并且該位置是空的。
如果該位置是空的刨沦,并且在格子的范圍內(nèi)诗宣,接下來(lái)就繼續(xù),走到其下一個(gè)位置做相同的檢查想诅。
如果上述的兩個(gè)檢查條件都失敗了召庞,要么是走到格子的邊沿了,要么是找到了下一個(gè)格子了来破,我們將下一個(gè)位置設(shè)置為newPosition篮灼,并且記錄下一個(gè)cell
將這個(gè)功能放到GridService中
既然可以為tiles計(jì)算下一個(gè)可能的位置,也就可以檢查潛在的合并了徘禁。
合并是這樣定義的:一個(gè)方塊碰到了和它值相同的另一個(gè)方塊诅诱,代碼中將檢查看下一個(gè)位置的方塊是否待移動(dòng)方塊有相同的值,并且之前沒(méi)有被合并過(guò)
現(xiàn)在送朱,如果下一個(gè)位置不滿足條件娘荡,那只需簡(jiǎn)單的將方塊從當(dāng)前的位置移動(dòng)到newPosition這個(gè)位置干旁。
Moving the tile
你可能已經(jīng)猜對(duì)了,將moveTile()放置到GridService中是再合適不過(guò)的了炮沐。
移動(dòng)方塊就是簡(jiǎn)單的更新一下其在一維數(shù)組中的位置争群,還有就是更新TileModel
Moving the tile in the array
GridService的數(shù)組反應(yīng)了在后端方塊定位在了哪里。方塊在數(shù)組中的位置沒(méi)有和其在格子中的位置進(jìn)行綁定
Updating the position on the TileModel
為了前端css放置格子大年,需要更新格子的坐標(biāo)换薄。
現(xiàn)在,定義tile.updatePosition() 方法翔试,方法做的事情正如其名字一樣轻要,簡(jiǎn)單的更新方塊自己的x、y坐標(biāo)
Merging a tile
既然已經(jīng)處理了簡(jiǎn)單的情況垦缅,方塊合并就成為下一個(gè)要處理的事情了伦腐,合并被定義為下面的操作:
一個(gè)方塊在下一個(gè)潛在的位置碰到了和自己相同value的另一個(gè)方塊
當(dāng)方塊被合并后,它就從面板上別移除掉了失都,同時(shí)會(huì)更新游戲的當(dāng)前得分和歷史最高分
合并包含幾個(gè)步驟:
- 添加一個(gè)新的方塊到最終的位置上,其value是合并過(guò)的值
- 移除原有的方塊
- 更新游戲得分
- 檢查是否獲勝
游戲僅僅支持單一的方塊移動(dòng)幸冻,也就是說(shuō)當(dāng)一行出現(xiàn)多個(gè)合并位置時(shí)粹庞,真正的合并只會(huì)發(fā)生一次,所以需要對(duì)已經(jīng)發(fā)生過(guò)的合并進(jìn)行標(biāo)記洽损,代碼中使用merged屬性來(lái)做這件事庞溜。
代碼中有兩個(gè)方法目前還沒(méi)有實(shí)現(xiàn),GridService.newTile()簡(jiǎn)單創(chuàng)建一個(gè)新的TileModel對(duì)象
self.updateScore()方法稍后會(huì)講到碑定,接下來(lái)要解決更新游戲得分的問(wèn)題流码。
After tile movement
一次有效的移動(dòng)過(guò)后需要給游戲內(nèi)新加入一個(gè)方塊,通過(guò)檢查移動(dòng)前和移動(dòng)后的位置是否相同判斷移動(dòng)是否有效延刘。
在所有方塊都被移動(dòng)(或嘗試移動(dòng))過(guò)后漫试,需要檢查游戲是否獲勝,如果獲勝碘赖,至此游戲就結(jié)束了驾荣,接著設(shè)置self.win標(biāo)志。
格子發(fā)生碰撞后必然需要移動(dòng)普泡,因此只需簡(jiǎn)單設(shè)置hasMoved=true
最后需要檢查是否發(fā)生移動(dòng)播掷,如果確定移動(dòng)過(guò):
- 給游戲添加新的方塊
- 檢查是否需要顯示游戲結(jié)束的界面
Reset the tiles
每一次move方法調(diào)用,需要重新設(shè)置方塊的merge狀態(tài)撼班,因?yàn)榇丝桃呀?jīng)不需要知道方塊的merge狀態(tài)了歧匈,將方塊的狀態(tài)擦除掉,認(rèn)為他們可以再運(yùn)行一次砰嘁,在move方法開(kāi)始運(yùn)行時(shí)件炉,執(zhí)行:
GridService.prepareTiles();
prepareTiles()方法簡(jiǎn)單迭代方塊并且重設(shè)其merge狀態(tài)
Keeping the score
回到updateScore()方法勘究,游戲中需要記錄兩個(gè)得分:
- 當(dāng)前得分
- 歷史最高得分
currentScore就是一個(gè)變量,每一次游戲只需將其值保存在內(nèi)存中妻率,不需要對(duì)她有任何操作
highScore也是一個(gè)變量乱顾,但是需要在所有游戲中維持這個(gè)變量,有多種方案處理它宫静,localstorage走净,cookies或者兩者的結(jié)合
考慮到cookies是最容易的并且瀏覽器支持友好,我們的代碼里面使用cookies保存highScore變量
在Angular中最容易使用cookies的方式就是使用angular-cookies模塊
為了使用這個(gè)模塊孤里,需要從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了
為了得到用戶最近的最高得分虏等,我們寫(xiě)一個(gè)函數(shù)從用戶cookie中獲取它
回到updateScore()方法弄唧,代碼將會(huì)更新游戲當(dāng)前得分,如果當(dāng)前得分比用戶歷史最高得分還高霍衫,就需要同時(shí)把它也一起更新了候引。
Wrath of track by
現(xiàn)在方塊可以在屏幕上輸出了,一個(gè)bug也隨之而來(lái)敦跌,結(jié)果就是澄干,方塊出現(xiàn)在我們未預(yù)料的位置上
bug的出現(xiàn)原因是:Angular根據(jù)唯一Id知道tiles數(shù)組中都有哪些方塊,我們?cè)趘iew中使用格子在數(shù)組中的位置作為唯一Id柠傍,由于格子在數(shù)組中被我們移來(lái)移去麸俘,$index就不能做為唯一Id來(lái)使用了,我們需要另外一種方案解決這個(gè)問(wèn)題惧笛。
使用方塊自身的uuid區(qū)分方塊而不依賴數(shù)組本身从媚,創(chuàng)建方塊自己的uuid將會(huì)保證Angular將tiles數(shù)組中的方塊做為唯一的對(duì)象對(duì)待,只要uuid不發(fā)生變化患整,Angular將會(huì)將每一個(gè)方塊做為uuid的對(duì)象在視圖中展示出來(lái)静檬。
對(duì)于如何為每一個(gè)格子創(chuàng)建uuid,可以轉(zhuǎn)到StackOverflow并级,其中有人實(shí)現(xiàn)了一個(gè)遵從rfc4122的guid生成器拂檩,我們代碼中將其包裝為一個(gè)factory,對(duì)外提供next()方法
回到TileModel中嘲碧,為每一個(gè)Tile對(duì)象創(chuàng)建屬性id
既然每一個(gè)tile對(duì)象擁有了uuid稻励,相應(yīng)的告訴Angular使用uuid去變量tiles數(shù)組,而不是$index.
上面的方案還存在一個(gè)問(wèn)題,由于tiles數(shù)組在游戲開(kāi)始時(shí)將所有位置都設(shè)置為null望抽,Angular也不管不顧的嘗試將null做為對(duì)象看待加矛,由于null沒(méi)有id屬性,這樣導(dǎo)致瀏覽器拋出一個(gè)無(wú)法操作重復(fù)對(duì)象的錯(cuò)誤
如何解決呢煤篙,可以告訴Angular斟览,當(dāng)當(dāng)前位置為空的話使用$index,否則使用tile對(duì)象的id屬性辑奈,接下來(lái)修改一下tile.html文件苛茂,添加對(duì)于null值的支持:
通過(guò)改變底層數(shù)據(jù)結(jié)構(gòu)的方式,這個(gè)問(wèn)題也可以解決鸠窗,例如使用迭代器查找tile的位置妓羊,而不依賴于tiles數(shù)組的索引,或者通過(guò)每次重排數(shù)組稍计,由于簡(jiǎn)單明了的原因躁绸,我們使用了數(shù)組作為其實(shí)現(xiàn),因而帶來(lái)了這個(gè)副作用臣嚣。
We won?!?? Game over
玩原版的游戲時(shí)净刮,如果失敗了,game over的界面會(huì)從游戲面板下方滑動(dòng)上來(lái)硅则,在這個(gè)界面上允許我們選擇重新開(kāi)始游戲并且可以follow作者的twitter淹父。游戲不僅僅給了玩者很酷的視覺(jué)效果,這也是一種中斷游戲的優(yōu)雅方式抢埋。
使用一些基礎(chǔ)的angular技術(shù),我們可以實(shí)現(xiàn)這個(gè)效果督暂,游戲中我們使用gameOver變量來(lái)跟蹤記錄游戲何時(shí)結(jié)束揪垄,簡(jiǎn)單的創(chuàng)建一個(gè)div元素包含我們的游戲結(jié)束界面,將其絕對(duì)定位到游戲面板的位置逻翁。
創(chuàng)建一個(gè)包含游戲結(jié)束或者闖關(guān)成功的div元素饥努,div中顯示的內(nèi)容是根據(jù)游戲的狀態(tài)確定的:
棘手的部分是為其寫(xiě)樣式,實(shí)際上我們僅僅是將其絕對(duì)定位到游戲面板的位置上八回,下面是部分css代碼
可以使用相同的技術(shù)創(chuàng)建闖關(guān)通過(guò)的界面酷愧,要做的僅僅是創(chuàng)建一個(gè)winning.game-overlay元素
Animation
原版2014讓人印象深刻的一個(gè)特點(diǎn)是:方塊魔法般的從一個(gè)格子滑動(dòng)到下一個(gè)格子,游戲勝利和結(jié)束畫(huà)面出現(xiàn)的是那么自然而不突兀缠诅,使用Angular溶浴,同樣也可以做到和原版近乎一致的效果。
實(shí)際上管引,我們希望我們的游戲可實(shí)現(xiàn)滑動(dòng)士败、展現(xiàn)等效果,動(dòng)畫(huà)是如此容易實(shí)現(xiàn),以至于我們根本不需要或者只需很少的javascript就可以實(shí)現(xiàn)谅将。
Animating the CSS positioning (aka adding sliding tiles)
我們的實(shí)現(xiàn)中漾狼,方塊的定位是通過(guò)為其添加class position-[x]-[y]來(lái)實(shí)現(xiàn)的,當(dāng)一個(gè)新的位置設(shè)置給了方塊后饥臂,其對(duì)應(yīng)的Dom元素就被添加上了新的定位class position-[newX]-[newY]逊躁,同時(shí)舊的position-[oldX]-[oldY]將會(huì)被移除,這種情況下隅熙,為.tile添加css的transition屬性就可以達(dá)到sliding的效果
SCSS代碼片段如下:
Animating the game over screen
如果想要從動(dòng)畫(huà)中獲得更多稽煤,可以使用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這本書(shū)中有關(guān)于其原理的深入探討),我們只是簡(jiǎn)單的看看她是如何工作的猛们,以便于能夠?yàn)槲覀兊挠螒蛱砑觿?dòng)畫(huà)效果念脯。
ngAnimate作為一個(gè)模塊級(jí)別的依賴被我們引入,任何時(shí)候弯淘,在Angular添加了一個(gè)新的對(duì)象后绿店,一組相關(guān)的指令可以用來(lái)為其添加css的class
當(dāng)一個(gè)元素被添加到ng-repeat的作用域后,新的元素將會(huì)被自動(dòng)的賦予ng-enter對(duì)應(yīng)的css class庐橙,接著當(dāng)其真正被添加到view中時(shí)假勿,ng-enter-active類(lèi)也會(huì)被自動(dòng)添加,這對(duì)我們?cè)趹?yīng)用中實(shí)現(xiàn)動(dòng)畫(huà)很重要态鳖,同樣ng-leave工作的模式和ng-enter是相同的
Animating the game over screen
在游戲獲勝和結(jié)束的畫(huà)面中转培,就可使用ng-enter來(lái)為其實(shí)現(xiàn)動(dòng)畫(huà)效果。記住一點(diǎn)浆竭,.game-overlay類(lèi)的隱藏和顯示使用ng-if指令控制浸须,當(dāng)ng-if的條件變化時(shí)(為真時(shí))ngAnimate將會(huì)為其添加.ng-enter和.ng-enter-active
相關(guān)的SCSS代碼如下:
Demo demo
完整的demo在這里http://ng2048.github.io/
關(guān)于這個(gè)游戲所有的代碼可以在github上找到,地址在這里
To build the game locally, clone the source and run: