[翻譯]使用AngularJS開發(fā)2048

本文從 這里 翻譯過來的。

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

  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

不論程序規(guī)模大小,是復(fù)制別人的還是自己原創(chuàng)的,第一步要做的都是:高屋建瓴式的設(shè)計。

玩過2048的人應(yīng)該清楚,游戲有一個面板(board),上面是一些格子,每個格子就是一個位置,標(biāo)上數(shù)字的方塊可以在這些格子上移動,根據(jù)這個事實,可以不依賴于javascript,讓CSS3負(fù)責(zé)處理方塊在面板上的移動,


3d-board.png

由于只有一個頁面,所以只需要一個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)用程序,同時也更加容易得共享模塊功能。

scripts_dir.png
The view

最容易開始的地方就是寫頁面了,在這個應(yīng)用中不需要多個頁面,因此創(chuàng)建一個div元素裝載應(yīng)用程序的內(nèi)容就可以了。

在app/index.html文件中,需要包含所有依賴(包括angular.js以及我們自己編寫的javascript文件-到現(xiàn)在為止,只有一個script/app.js):

index.html

后續(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中,我們至少要包含下面幾項

  1. 游戲的標(biāo)題
  2. 當(dāng)前得分和最高得分
  3. 游戲面板

游戲的靜態(tài)頭部信息簡單就是下面這樣子:

游戲的靜態(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需要支持的功能就有:

  1. 創(chuàng)建新游戲
  2. 處理循環(huán)和移動操作
  3. 更新游戲得分
  4. 監(jiān)控游戲狀態(tài)

這樣GameManager的基本框架就有了

GameManager
Back to the GameManager

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


movesAvailable

Building the game grid

接下來創(chuàng)建GridService去管理游戲面板

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

GridService

當(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)的視圖。

grid_directive.js

這個指令只是負(fù)責(zé)創(chuàng)建游戲的grid視圖,其中不需要任何的邏輯。

grid.html

在指令模板中,有兩次ngRepeat的調(diào)用,分別顯示的是grid和tile數(shù)組的內(nèi)容。


main.html

第一個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,

transformX
.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)容可以在項目源碼中找到。

SCSS文件

注意一點就是:為了使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的位置

下面是計算過程:

.tile

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

Coloring the different tiles

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

$colors

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

.tile-x

The Tile directive

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

tile directive

一個有意思的事情是,方塊是如何動態(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

TileModel
Our first grid

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

gird service

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。

buildEmptyGameBoard()

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

工具函數(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可以就可以表示為下面的形式

位置、坐標(biāo)轉(zhuǎn)化
Initial player positions

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

buildStartingPosition函數(shù)

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

availableCells函數(shù)

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

randomAvailableCell函數(shù)

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

randomlyInsertNewTile函數(shù)

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)該包含下面的外部文件了

index.html

同時,每當(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)用。

KeyBoard Service

init函數(shù)中會將鍵盤監(jiān)聽的任務(wù)交給KeyboardService去處理,所有我們感興趣的keydown事件會被過濾出來進(jìn)行處理。

任何我們感興趣事件,我們都會去阻止其默認(rèn)行為,然后將其交給keyEventHandlers處理。

keyboard.png

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

keyboard

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

_handleKeyEvent

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

on

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)聽操作。

keyboard-sequence.png

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

GameController

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

startGame

Press the start button

最后一個需要實現(xiàn)的方法是newGame,位于GameManager中,這個方法做了下面幾件事情:

  1. 創(chuàng)建空的游戲面板
  2. 設(shè)置開始位置
  3. 初始化游戲

GridService已經(jīng)實現(xiàn)了上述的邏輯,現(xiàn)在要做的就是把他們串聯(lián)起來

1.png

Get your move on (the game loop)

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

game-1.png

在開始寫move方法前,我們需要先定義游戲的約束,也就是,每一次移動,游戲要如何處理。

  1. 獲取用戶方向鍵對應(yīng)的vector向量
  2. 為游戲面板上的每一個tile找到最遠(yuǎn)可能的位置,同時,判斷下一個位置中的tile是否可以合并。
  3. 對每一個tile,判斷下一個tile是否與其擁有相同的value
  4. 如果下一個tile不存在,只需將當(dāng)前的tile移動到最遠(yuǎn)可能的位置(這意味著最近的位置就是游戲面板的邊緣)
  5. 如果下一個tile存在,其value和當(dāng)前tile不同,只需移動當(dāng)前的tile到最遠(yuǎn)位置(當(dāng)前tile的下一個tile就是可移動的邊界)
  6. value相同,找到了一個可能的合并
    1. 如果是合并后得到的,跳過
    2. 如果還沒有合并,那么認(rèn)為這是一個合并

既然已經(jīng)定義出了功能,我們就能為構(gòu)造move函數(shù)設(shè)計出策略。

move函數(shù)

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

grid-vectors.gif

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

vector

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

traversalDirections

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

move

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

為了為一個tile尋找其最遠(yuǎn)可能的位置,需要走到其下一個位置檢查當(dāng)前格子是否是面板的邊沿并且該位置是空的。

如果該位置是空的,并且在格子的范圍內(nèi),接下來就繼續(xù),走到其下一個位置做相同的檢查。

如果上述的兩個檢查條件都失敗了,要么是走到格子的邊沿了,要么是找到了下一個格子了,我們將下一個位置設(shè)置為newPosition,并且記錄下一個cell

next-process.gif

將這個功能放到GridService中

calculateNextPosition

既然可以為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)。

moveTile

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

updatePosition()
Merging a tile

既然已經(jīng)處理了簡單的情況,方塊合并就成為下一個要處理的事情了,合并被定義為下面的操作:

一個方塊在下一個潛在的位置碰到了和自己相同value的另一個方塊

當(dāng)方塊被合并后,它就從面板上別移除掉了,同時會更新游戲的當(dāng)前得分和歷史最高分

合并包含幾個步驟:

  1. 添加一個新的方塊到最終的位置上,其value是合并過的值
  2. 移除原有的方塊
  3. 更新游戲得分
  4. 檢查是否獲勝
in move function

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

代碼中有兩個方法目前還沒有實現(xiàn),GridService.newTile()簡單創(chuàng)建一個新的TileModel對象

GridService.newTile

self.updateScore()方法稍后會講到,接下來要解決更新游戲得分的問題。

After tile movement

一次有效的移動過后需要給游戲內(nèi)新加入一個方塊,通過檢查移動前和移動后的位置是否相同判斷移動是否有效。

在所有方塊都被移動(或嘗試移動)過后,需要檢查游戲是否獲勝,如果獲勝,至此游戲就結(jié)束了,接著設(shè)置self.win標(biāo)志。

格子發(fā)生碰撞后必然需要移動,因此只需簡單設(shè)置hasMoved=true

最后需要檢查是否發(fā)生移動,如果確定移動過:

  1. 給游戲添加新的方塊
  2. 檢查是否需要顯示游戲結(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)

prepareTiles

Keeping the score

回到updateScore()方法,游戲中需要記錄兩個得分:

  1. 當(dāng)前得分
  2. 歷史最高得分

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中獲取它

getHighScore

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

updateScore
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()方法

uuid生成器

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

Tile

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

tile view

上面的方案還存在一個問題,由于tiles數(shù)組在游戲開始時將所有位置都設(shè)置為null,Angular也不管不顧的嘗試將null做為對象看待,由于null沒有id屬性,這樣導(dǎo)致瀏覽器拋出一個無法操作重復(fù)對象的錯誤

如何解決呢,可以告訴Angular,當(dāng)當(dāng)前位置為空的話使用$index,否則使用tile對象的id屬性,接下來修改一下tile.html文件,添加對于null值的支持:

tile.html

通過改變底層數(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代碼

.game-overlay

可以使用相同的技術(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代碼片段如下:

.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 不在本文的范圍內(nèi)(ng-book這本書中有關(guān)于其原理的深入探討),我們只是簡單的看看她是如何工作的,以便于能夠為我們的游戲添加動畫效果。

ngAnimate作為一個模塊級別的依賴被我們引入,任何時候,在Angular添加了一個新的對象后,一組相關(guān)的指令可以用來為其添加css的class

1.png

當(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代碼如下:

.game-overlay類

Demo demo

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

關(guān)于這個游戲所有的代碼可以在github上找到,地址在這里

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

build 2048
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容