溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何利用AngularJS開發(fā)2048游戲

發(fā)布時間:2021-11-17 13:39:52 來源:億速云 閱讀:111 作者:柒染 欄目:web開發(fā)

這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)如何利用AngularJS開發(fā)2048游戲,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

我頻繁地被問及到的一個問題之一,就是什么時候使用Angular框架是一個糟糕的選擇。我的默認(rèn)答復(fù)是編寫游戲的時候,盡管Angular有它自己的事件循環(huán)處理 ($digest循環(huán)) ,并且游戲通常需要很多底層DOM操作.如果說有Angular能支持很多類型的游戲,那這個說法可不準(zhǔn)確。即使游戲需要大量的DOM操作,這可能會用到angular框架處理靜態(tài)部分,如記錄最高分和游戲菜單。

如果你和我一樣迷上流行的2048 游戲. 游戲的目標(biāo)是用相同的值相加拼出值為2048的方塊。

我們會用AngularJS從頭到尾地創(chuàng)建一個副本, 并解釋創(chuàng)建app的全過程。由于這個app相對復(fù)雜,所以我也打算用這篇文章來描述如何創(chuàng)建復(fù)雜的AngularJS應(yīng)用。

這是我們要創(chuàng)建的 demo .

現(xiàn)在開始吧!

TL;DR: 這個app的源代碼也可下載,文章尾部有該app在github上的鏈接.

第一步:規(guī)劃app

如何利用AngularJS開發(fā)2048游戲

第一步我們要做的,就是給要創(chuàng)建的app做高層設(shè)計。無論是山寨別人的app,還是自己從零做起,這一步都與app的規(guī)模無關(guān)。

再來看看這個游戲,我們發(fā)現(xiàn)在游戲板的頂端有一堆瓦片。每個瓦片自身都可以作為一個位置,用來放置其他有編號的瓦片。我們可以根據(jù)這個事實,把任務(wù)移動瓦片的任務(wù)交給CSS3來處理,而不是依靠JavaScript,它需要知道移動瓦片的位置。當(dāng)游戲面板上有一個瓦片,我們只需要簡單地確保它放在頂部合適的位置即可。

使用CSS3來布局,能帶給我們CSS動畫效果,同時也默認(rèn)使用AngularJS行為來跟蹤游戲板的狀態(tài),瓦片和游戲邏輯。

因為我們只有一個單頁面(single page),我們還需要一個單控制器(single controller)來管理頁面。
因為應(yīng)用的生命周期內(nèi)只有一個游戲板,我們在GridService服務(wù)中的一個單一實例里包含所有的網(wǎng)格邏輯。由于服務(wù)是單例模式對象,所以這是一個存儲網(wǎng)格的恰當(dāng)位置。我們使用GridService來處理瓦片替換,移動,管理網(wǎng)格。

而把游戲的邏輯和處理放到一個叫做GameManager的服務(wù)中。它將負(fù)責(zé)游戲的狀態(tài),處理移動,維護(hù)分?jǐn)?shù)(當(dāng)前分?jǐn)?shù)和最高分)

最后,我們需要一個允許我們管理鍵盤的組件。我們需要一個叫做KeyboardService的服務(wù)。在這篇博文中,實現(xiàn)了應(yīng)用對桌面的處理,我們也可以復(fù)用這個服務(wù)來管理觸摸操作讓它在移動設(shè)備上運轉(zhuǎn)。

創(chuàng)建app

如何利用AngularJS開發(fā)2048游戲

為了創(chuàng)建app,我們先創(chuàng)建一個基本的 app (使用 yeoman angular 生成器生成app的結(jié)構(gòu), 這一步不是必須的. 我們只把它作為切入點,之后就迅速地從它的結(jié)構(gòu)上分開。).創(chuàng)建一個app目錄用來放置整個應(yīng)用。把test/目錄作為app/目錄的同級目錄.

The following instructions are for setting up the project using the yeoman tool. If you prefer to do it manually, you can skip installing the dependencies and move on to the next section.

因為在應(yīng)用中我們用了yeomanin工具, 我們首先要確保它已經(jīng)安裝好了. Yeoman安裝時基于NodeJS和npm.安裝NodeJS不是這篇教程所要講的,但你可以參看NodeJS.org 站點.

裝完npm后,我們就可以安裝yeoman工具yo和angular生成器(它由yo調(diào)用來創(chuàng)建Angular app):

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

安裝后,我們可以使用yeoman工具生成我們的應(yīng)用,如下:

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

該工具會詢問一些請求。我們都選yes即可,除了要選擇angular-cookies作為依賴外,我們不需要任何其他的依賴了。

Note that using the Angular generator, it will expect you have the compass gem installed along with a ruby environment. See the complete source for a way to get away without using ruby and compass below.

我們的angular 模塊

我們將創(chuàng)建scripts/app.js文件來放置我們的應(yīng)用?,F(xiàn)在就開始創(chuàng)建應(yīng)用吧:

angular.module('twentyfourtyeightApp', [])

模塊結(jié)構(gòu)

如何利用AngularJS開發(fā)2048游戲

布局angular應(yīng)用使用的結(jié)構(gòu)現(xiàn)在是根據(jù)函數(shù)推薦的,而不是類型。這就是說,不用把組件分成控制器,服務(wù),指令等,就可以在函數(shù)基礎(chǔ)上定義我們的模塊結(jié)構(gòu)。例如,在應(yīng)用中定義一個Game模塊和一個Keyboard模塊。

模塊結(jié)構(gòu)清晰地為我們分離出匹配文件結(jié)構(gòu)的職能域。這不僅方便我們創(chuàng)建大型,靈活性強(qiáng)的angular應(yīng)用,也方便我們共享app中的函數(shù)。

最后,我們搭建測試環(huán)境適應(yīng)文件目錄結(jié)構(gòu)。

視圖

應(yīng)用中最易切入的地方非視圖莫屬了。審視視圖自身,我們發(fā)現(xiàn)只有一個view/template.在這個應(yīng)用中,不需要多視圖,所以我們創(chuàng)建單一的<div>元素,用來放置應(yīng)用的內(nèi)容。

在我們的主文件app/index.html中,我們需要包含所有的依賴項(包括angular.js自身和JS文件,即scripts/app.js),如下:

<!-- index.html --> <doctype html> <html>   <head>     <title>2048</title>     <link rel="stylesheet" href="styles/main.css">   </head>   <body ng-app="twentyfourtyeightApp"     <!-- header -->     <div class="container" ng-include="'views/main.html'"></div>     <!-- script tags -->     <script src="bower_components/angular/angular.js"></script>     <script src="scripts/app.js"></script>   </body> </html>

Feel free to make a more complex version of the game with multiple views &ndash; please leave a comment below if you do. We&rsquo;d love to see what you create.

有了app/index.html文件集,我們需要在應(yīng)用視圖層面上,詳細(xì)地處理app/views/main.html中的視圖。當(dāng)需要在應(yīng)用中導(dǎo)入一個新
資源時,我們需要修改index.html文件。

打開app/views/main.html,我們要替換所有的游戲指定的視圖。使用controllerAs語法,我們可以在$scope中清楚地知道我們期待在哪里查詢數(shù)據(jù),哪個控制器負(fù)責(zé)哪個組件。

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

ThecontrollerAssyntax is a relatively new syntax that comes with version 1.2. It is useful when dealing with many controllers on the page as it allows us to be specific about the controllers where we expect functions and data to be defined.

在視圖中,我們要顯示以下一些項目:

  1. 游戲靜態(tài)頭

  2. 當(dāng)前游戲分?jǐn)?shù)和本地用戶最高分

  3. 游戲板

游戲靜態(tài)頭可以這樣來完成:

<!-- heading inside app/views/main.html --> <div id="content" ng-controller='GameController as ctrl'>   <div id="heading" class="row">     <h2 class="title">ng-2048</h2>     <div class="scores-container">       <div class="score-container">{{ ctrl.game.currentScore }}</div>       <div class="best-container">{{ ctrl.game.highScore }}</div>     </div>   </div>   <!-- ... --> </div>

注意到,當(dāng)在視圖中引用currentScore和highScroe時,我們也引用了GameController.controllerAs語法使得我們可以顯示地引用我們感興趣的控制器。

GameController

現(xiàn)在我們有了一個合理的項目結(jié)構(gòu),現(xiàn)在來創(chuàng)建GameController來放置我們要在視圖中顯示的值。在app/script/app.js中,我們可以在主模塊twentyfourtyeight.App中創(chuàng)建控制器:

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

在視圖中,我們引用了一個game對象,它將在GameController中設(shè)置。該game對象將引用主game對象。我們在一個新模塊中創(chuàng)建這個主游戲模塊,用來放置游戲中所有的引用。
因為這個模塊還沒有創(chuàng)建,app不會再瀏覽器中加載它。在控制器中,我們可以添加GameManager依賴

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

別忘了,我們正創(chuàng)建一個模塊級別的依賴,它是應(yīng)用中不同的部分,所以要確保它在應(yīng)用中正確地加載,我們需要將它列為angular模塊的一個依賴。為使Game模塊成為twentyfourtyeightApp的依賴,我們在定義該模塊的數(shù)組中列舉它。

我們整個的app/script/app.js文件應(yīng)該看起來像這樣:

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

Game

既然我們有視圖間部分相互連接了,那么就可以開始編寫游戲背后的邏輯了。為創(chuàng)建一個新游戲模塊,我們在app/scripts/目錄中把我們的模塊創(chuàng)建為app/scripts/game/game.js:

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

When building modules, we like to write them in their own directory named after the module. We&rsquo;ll implement the module initialization in a file by the name of the module. For instance, we&rsquo;re building a game module, so we&rsquo;ll build our game module inside theapp/scripts/gamedirectory in a file namedgame.js. This methodology has provided to be scalable and logical in production.

Game模塊將提供一個單核心組件:GameManager.

我們將來完成GameManager,使它能處理游戲的狀態(tài),用戶可以移動的不同方法,記錄分?jǐn)?shù)以及決定游戲何時結(jié)束和用戶是否打破最高分以及用戶是否輸局了。

開始開發(fā)應(yīng)用時,我們喜歡為我們用到的方法編寫stub方法,并編寫測試代碼然后填入要實現(xiàn)的地方。

For the purposes of this article, we&rsquo;ll run through this process for this module. When we write the next several modules, we&rsquo;ll only mention the core components we should be testing.

我們知道GameManager將支持以下特性:

  1. 建立新游戲

  2. 處理游戲循環(huán)/移動操作

  3. 更新分?jǐn)?shù)

  4. 跟蹤游戲是否結(jié)束

有了這些特性,我們可以創(chuàng)建GameManager服務(wù)的基本大綱,我們就可以對它進(jìn)行測試代碼的編寫:

angular.module('Game', [])  .service('GameManager', function() {    // Create a new game    this.newGame = function() {};    // Handle the move action    this.move = function() {};    // Update the score    this.updateScore = function(newScore) {};    // Are there moves left?    this.movesAvailable = function() {};  });

基本的功能實現(xiàn)完后,就來編寫測試代碼,使它定義GameManager需要支持的功能.

測試驅(qū)動開發(fā) (TDD)

如何利用AngularJS開發(fā)2048游戲

開始實現(xiàn)測試前,需要使用karma驅(qū)動測試。如果你對karma不熟悉,就把它當(dāng)做一個測試runner,它允許我們在終端和代碼中舒適高效地進(jìn)行前臺的自動化測試。
要使用Karma,我們需要確保它已安裝正確。使用Karma,我們要依賴NodeJS,因為它可以作為一個npm包。運行以下代碼,安裝Karma:

$ npm install -g karma

The-gflag tells npm to install the package globally. Without this flag, the package would only be installed locally in the current working directory.

如果你使用了yeoman angular生成器,你可以跳過下一部分。

要使用 karma, 我們需要編寫一個配置文件。盡管我們不會深入討論怎樣配置Karma(猛戳這里 ng-book ,查看配置Karma的詳細(xì)選項), 但是關(guān)鍵的部分還是要知道的,即設(shè)置Karma使它在測試中加載所有我們感興趣的文件。

要創(chuàng)建一個配置文件,我們可以使用karma init命令來創(chuàng)建一個基本的版本.

$ karma init karma.conf.js

該命令會詢問一些請求并創(chuàng)建karma.conf.js文件。從這里起,我們將改變兩個配置選項:files數(shù)組和要打開的autoWatch:

// ...  files: [    'app/bower_components/angular/angular.js',    'app/bower_components/angular-mocks/angular-mocks.js',    'app/bower_components/angular-cookies/angular-cookies.js',    'app/scripts/**/*.js',    'test/unit/**/*.js' ],  autoWatch: true,  // ...

建立完這個配置文件,我們可以隨時運行測試(它寫在test/unit/目錄下)
為運行測試,我們運行karma start命令,如下所示:

$ karma start karma.conf.js

編寫第一份測試

既然karma安裝和配置好了,我們就可以開始為GameManager編寫基本的測試。因為我們還不知道應(yīng)用的全部功能,我們只能進(jìn)行有限的測試

Often times, we find that our API changes as we develop the application, so rather than introduce a lot of work ahead of time that we&rsquo;ll likely change, we set up our tests to test basic functionality and fill them in deeper as we uncover the eventual API.

第一份測試的較好的備選方案,是它可以告訴我們有沒有可能向左移動。為測試是否可以向左移動,我們簡單地寫一個我們需要調(diào)用的stub方法,它測試應(yīng)用邏輯的行為并返回true/false.

我們將穿件一個文件---test/unit/game/game_spec.js,并開始創(chuàng)建我們的測試上下文:

describe('Game module', function() {    describe('GameManager', function() {      // Inject the Game module into this test      beforeEach(module('Game'));         // Our tests will go below here    });  });

In this test, we&rsquo;re using Jasmine syntax.

同其他單元測試一樣,我們需要創(chuàng)建GameManager對象的實例。我們可以沿襲常規(guī)(當(dāng)測試服務(wù)時),把它注入到我們測試中:

// ...  // Inject the Game module into this test  beforeEach(module('Game'));     var gameManager; // instance of the GameManager  beforeEach(inject(function(GameManager) {  gameManager = GameManager;  });     // ...

有了這個gameManager的實例,我們可以開始編寫movesAvailable()期望的功能.

我們將定義movesAvailable()函數(shù),它用來驗證是否有剩下可用的方塊以及驗證有沒有可能的合并。因為它是游戲是否結(jié)束的條件,我們把這個方法放到GameManager,但是在GridService中實現(xiàn)大部分功能,GridService將在下一步創(chuàng)建。

要看游戲板上是否有方塊移動,我們看兩個條件:

  1. 游戲板上有可用的位置

  2. 有可匹配的位置

有了這兩個條件,我們就可以編寫測試代碼來看是否滿足這兩個條件。

最基本的想法就是我們寫出測試代碼,然后滿足一個條件,它可以用來觀察單元測試在環(huán)境下的表現(xiàn)。由于依賴GridService來報告游戲板的條件,所以我們要在GameManager中改變條件來看邏輯是否正確。

Mock the GridService

要mock我們的GridService,我們通過重寫默認(rèn)的Angular行為來“提供”我們的mock后的服務(wù),而不是真正的服務(wù),所以我們可以在服務(wù)中建立可控的條件

用mocked方法創(chuàng)建一個fake對象,然后通過$provide服務(wù)處理它們并告訴Angular這些fake對象是真正的對象。

// ...  var _gridService;  beforeEach(module(function($provide) {  _gridService = {    anyCellsAvailable: angular.noop,    tileMatchesAvailable: angular.noop  };     // Switch out the real GridService for our  // fake version  $provide.value('GridService', _gridService);  }));  // ...

現(xiàn)在我們可以使用這個fake _gridService實例來建立我們的條件。
我們要確保當(dāng)有可用的方塊時,movesAvailable()函數(shù)返回true.現(xiàn)在就在GridService中mock anyCellsAvailable()方法。我們希望這個方法在GridService中報告是否有可用方塊。

// ...  describe('.movesAvailable', function() {    it('should report true if there are cells available', function() {      spyOn(_gridService, 'anyCellsAvailable').andReturn(true);      expect(gameManager.movesAvailable()).toBeTruthy();    });    // ...

既然基本原理弄清楚了,我們就可以設(shè)定第二個條件的期望值了。如果有可用的搭配,我們就要確保movesAvailable()函數(shù)返回true.同時我們確保對話返回true時,要是沒有可用的網(wǎng)格或搭配,就沒有可用的移動。

另兩個測試確保如下過程:

// ...  it('should report true if there are matches available', function() {    spyOn(_gridService, 'anyCellsAvailable').andReturn(false);    spyOn(_gridService, 'tileMatchesAvailable').andReturn(true);    expect(gameManager.movesAvailable()).toBeTruthy();  });  it('should report false if there are no cells nor matches available', function() {    spyOn(_gridService, 'anyCellsAvailable').andReturn(false);    spyOn(_gridService, 'tileMatchesAvailable').andReturn(false);    expect(gameManager.movesAvailable()).toBeFalsy();  });  // ...

我們已經(jīng)奠定基礎(chǔ)了,現(xiàn)在在實現(xiàn)期望的功能前可以編寫測試樣例了。

Although we aren&rsquo;t going to continue with TDD in this post, for the sake of overall completion, we suggest you should continue with it. Check out the full source code below for more tests.

回到GameManager

現(xiàn)在我們來實現(xiàn)movesAvailable函數(shù). 我們已經(jīng)測試代碼可以運行, 并且明確了執(zhí)行的條件, 這個函數(shù)實現(xiàn)起來就簡單了.

// ...  this.movesAvailable = function() {  return GridService.anyCellsAvailable() ||           GridService.tileMatchesAvailable();  };  // ...

打造game grid

GameManager已經(jīng)準(zhǔn)備妥當(dāng), 我們接下來就要創(chuàng)建GridService來管理游戲板.

回想一下我們用來描述游戲板的兩個數(shù)組變量grid和tiles, 我們用這兩個局部變量來設(shè)置GridService. 在app/scripts/grid/grid.js文件中, service的創(chuàng)建代碼如下:

angular.module('Grid', [])  .service('GridService', function() {    this.grid   = [];    this.tiles  = [];    // Size of the board    this.size   = 4;    // ...  });

當(dāng)我們想創(chuàng)建一個新游戲, 數(shù)組用null元素初始化. grid數(shù)組只包含在游戲板上用來放置方塊的固定數(shù)量的Dom元素, 因此grid可以理解為靜態(tài)的.

相比而言, tiles數(shù)組用來存放游戲中正在使用的瓦片, 則相對是動態(tài)變化的. 下來我們在頁面上創(chuàng)建grid, 看看如何通過使用這些變量來控制grid和瓦片的布局.

回到app/views/main.html中,我們需要開始布局網(wǎng)格。因為它是動態(tài)的,加上我們要把我們的邏輯處理放在網(wǎng)格內(nèi),我們僅僅只要把邏輯放到它自己的指令內(nèi)。使用指令,將清空主模板和在指令中的封裝的功能,同時主控制器也被清空。
在app/index.html中,我們把網(wǎng)格指令放到網(wǎng)格并在控制器中傳遞GameManager實例:

<!-- instructions --> <div id="game-container"> <div grid ng-model='ctrl.game' class="row"></div> <!-- ... -->

編寫這個指令,使它能包含在Grid模塊中。在app/scripts/grid/目錄下,我們創(chuàng)建一個grid_directives.js文件來放置grid指令。
在grid指令中,由于它的權(quán)限有限,不能封裝視圖,所以我們還需要一些變量。

這個指令需要一個GameManager實例(或者,至少一個包含grid和tiles的模型),這樣就可以根據(jù)指令的需要完成了一個自定義的指令。另外,我們不希望我們的指令干擾到頁面或者頁面中的GameManager實例,所以我們需要使用isolate來創(chuàng)建這個之類,用于限制它的使用范圍。

深入理解自定義指令可以參考: custom directives  ,或者查看 ng-book里面關(guān)于指令的內(nèi)容
angular.module('Grid')  .directive('grid', function() {    return {      restrict: 'A',      require: 'ngModel',      scope: {        ngModel: '='     },      templateUrl: 'scripts/grid/grid.html'   };  });
該指令的主要功能是建立網(wǎng)格視圖,所以我們不需要在指令里面使用自定義邏輯。
<div id="game">   <div class="grid-container">     <div class="grid-cell"         ng-repeat="cell in ngModel.grid track by $index">       </div>   </div>   <div class="tile-container">     <div tile         ng-model='tile'       ng-repeat='tile in ngModel.tiles track by $index'>     </div> </div> </div>

在指令的模板里面,我們使用兩次ngRepeat來遍歷展示grid和tiles數(shù)組,并且分別使用$index來跟蹤遍歷的結(jié)果。

可以看到第一個ng-repeat是一個非常簡單的遍歷,將ngModel.grid遍歷輸出到一個class為grid-cell的div里面。

在第二個ng-repeat里面,我們給每一個屏幕上的元素創(chuàng)建一個叫做tile的輔助的指令。這個tile指令將用于給每個tile元素創(chuàng)建直觀的頁面顯示效果。后面我們再來創(chuàng)建這個tile指令...

精明的讀者可能會看到,我們只使用了一個一維數(shù)組來展示一個二維網(wǎng)格。當(dāng)我們渲染視圖的時候,我們只獲取一列tiles,而不是一個格子。為了讓他們變成網(wǎng)格,我們需要使用CSS。

Enter SCSS

針對這個項目,我們使用SASS的一個現(xiàn)代變體:scss。scss不僅是一個更強(qiáng)大的CSS,我們將會以動態(tài)的方式來構(gòu)建我們的CSS。

這個app的視覺元素部分將使用CSS完成,包括動畫以及布局和視覺元素(瓷磚的顏色等)。

為了可以使用二維數(shù)組的方式創(chuàng)建面板,我們需要使用CSS3的transform關(guān)鍵字來將每個瓷磚放置在面板特定的位置上。

CSS3 transform 屬性

CSS3 transform 屬性向元素應(yīng)用 2D 或 3D 轉(zhuǎn)換。 該屬性允許我們對元素(當(dāng)然是可以動起來的元素)進(jìn)行移動、傾斜、旋轉(zhuǎn)、縮放,以及其它更多動作. 使用這個屬性,我們可以簡單地將方塊放到游戲板上,然后給元素應(yīng)用適當(dāng)?shù)膖ransform屬性。

例如,下面這個示例,我們有一個40px寬和40px高的box類:

.box {    width:40px;    height:40px;    background-color: blue;  }

如果我們應(yīng)用一個translateX(300px)屬性,我們將向左移動盒子300px,以下示例證明了這一點:

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

使用這個轉(zhuǎn)換屬性,我們能夠簡單地通過給我們的方塊應(yīng)用一個CSS類標(biāo)記在游戲板上移動它們?,F(xiàn)在,微秒的地方就是我們怎樣來構(gòu)建我們動態(tài)的類,如此,當(dāng)我們在頁面上定點時,它們使用CSS類來對應(yīng)一個合適的方格?

這就是SCSS發(fā)揮威力的地方。我們將設(shè)置一些變量(比如一行我們想要幾個方塊),并且在這些變量周圍構(gòu)建我們的SCSS,使用一些數(shù)學(xué)方法來為我們做計算。

讓我們看一看這些變量,我們需要正確的計算它們的在游戲板上的位置:

$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幫我們動態(tài)的計算這三個變量的位置。首先,我們需要計算每一個方塊的面積。這對SCSS變量來說是非常容易的:

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

現(xiàn)在我們可以為#game這個容器設(shè)置合適的寬高。同樣,我們在#game這個容器上設(shè)置位置參數(shù),這樣我們就可以在容器中準(zhǔn)確的定位到我們的子元素。我們會放置我們的.gird-container和.tile-container到#game這個容器對象中。

我們在這里只包含了與scss相關(guān)的部分。剩下的代碼可以在文章最后提供的github地址上找到。

#game {    position: relative;    width: $width;    height: $width; // The gameboard is a square       .grid-container {      position: absolute;   // the grid is absolutely positioned      z-index: 1;           // IMPORTANT to set the z-index for layering      margin: 0 auto;       // center         .grid-cell {        width: $tile-size;              // set the cell width        height: $tile-size;             // set the cell height        margin-bottom: $tile-padding;   // the padding between lower cells        margin-right: $tile-padding;    // the padding between the right cell        // ...      }    }    .tile-container {      position: absolute;      z-index: 2;         .tile {        width: $tile-size;        // tile width        height: $tile-size;       // tile height        // ...      }    }  }

需要注意的是為了將.tile-container置于.gird-container之上,我們必須為.tile-container設(shè)置更高的z-index值。否則,瀏覽器會將它們置于同等高度,這樣看上去就不美觀了。

通過這些設(shè)置,我們現(xiàn)在可以動態(tài)生成這些方塊的位置坐標(biāo)。我們需要的只是一個.position-[x}-{y}標(biāo)記類,將它附值給一個方塊,那樣瀏覽器就知道方塊的位置坐標(biāo),然后動態(tài)的將方塊放置到那個位置上去。因為我們要計算與這個格子容器相關(guān)的轉(zhuǎn)換屬性,我們將用0,0來做為第一個方塊的初始位置。

我們將迭代所有的方塊,然后基于我們計算的預(yù)期偏移值來動態(tài)地創(chuàng)建每一個類:

.tile {    // ...    // Dynamically create .position-#{x}-#{y} classes to mark    // where each tile will be placed    @for $x from 1 through $tile-count {      @for $y from 1 through $tile-count {        $zeroOffsetX: $x - 1;        $zeroOFfsetY: $y - 1;        $newX: ($tile-size) * ($zeroOffsetX) + ($tile-padding * $zeroOffsetX);        $newY: ($tile-size) * ($zeroOffsetY) + ($tile-padding * $zeroOffsetY);           &.position-#{$zeroOffsetX}-#{$zeroOffsetY} {          -webkit-transform: translate($newX, $newY);          transform: translate($newX, $newY);        }      }    }    // ...  }

需要注意我們必須以1為起始值來計算偏移,而不是以前的以0為起始值.這是SASS自身的一個局限。我們通過將索引減1來規(guī)避這個問題。

現(xiàn)在我們已經(jīng)創(chuàng)建了.position-#{x}-#{y}這個CSS標(biāo)記類,可以將我們的方塊布局到屏幕上了。

如何利用AngularJS開發(fā)2048游戲

為不同的方塊的著色

注意到每一個方塊出現(xiàn)在屏幕上時都有不同的顏色。這些不同的顏色表示每一個方塊自己擁有的數(shù)值。這是一種簡單地方法可以讓玩家知道這些方塊處在不同的狀態(tài)之下。使用我們迭代所有方塊時相同的手法來創(chuàng)建一個方塊的顏色方案。

為了完成顏色方案的創(chuàng)建,我們首先需要創(chuàng)建一個SCSS數(shù)組來保存我們將在屏幕上用到的每一種背景顏色。每一種顏色將

$colors:  #EEE4DA, // 2            #EAE0C8, // 4            #F59563, // 8            #3399ff, // 16            #ffa333, // 32            #cef030, // 64            #E8D8CE, // 128            #990303, // 256            #6BA5DE, // 512            #DCAD60, // 1024            #B60022; // 2048

單地迭代每一種顏色,并且動態(tài)地基于這個方塊的數(shù)值來創(chuàng)建一個類。也就是說,當(dāng)一個方塊的值是2時,我們將增加.tile-2這個CSS類,這個類的背景色是#EEE4DA。我們將使用SCSS技巧來幫助我們處理,而不是為每一個方塊進(jìn)行硬編碼。

@for $i from 1 through length($colors) {    &.tile-#{power(2, $i)} .tile-inner {      background: nth($colors, $i)    }  }

當(dāng)然了,我們需要定義power()這個混合函數(shù)。它像這樣定義:

@function power ($x, $n) {    $ret: 1;       @if $n >= 0 {      @for $i from 1 through $n {        $ret: $ret * $x;      }     } @else {      @for $i from $n to 0 {        $ret: $ret / $x;      }    }       @return $ret;  }

方塊指令

因為SASS的不懈的工作,我們可以回到我們的方塊指令,根據(jù)動態(tài)定位來展示每一個方塊,并且允許CSS能夠以它被設(shè)計的方式來工作,然后依序排列這些方塊。

因為tile指令是一個自定義視圖的容器,所以我們不需要讓它有太多的功能。我們需要用到元素負(fù)責(zé)顯示的特性。除此之外,這里沒有其它功能需要放進(jìn)去。下面這段代碼說明了一切:

angular.module('Grid')  .directive('tile', function() {    return {      restrict: 'A',      scope: {        ngModel: '='     },      templateUrl: 'scripts/grid/tile.html'   };  });

現(xiàn)在,tile指令有意思的地方在于我們?nèi)绻麆討B(tài)呈現(xiàn)。使用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>

使用這條基礎(chǔ)指令,我們幾乎就要把它顯示在屏幕上了。所有以x和y為坐標(biāo)的方塊,它們將自動被分配相應(yīng)的.position-#{x}-#{y}類,并且瀏覽器也將自動的將它們放置到期望的位置上。

這意味著我們的方塊對象將需要一個x,y和一個對指令運行來說可行的值。因此,我們需要為每一個即將布局對屏幕上的方塊創(chuàng)建一個新的對象。

TileModel服務(wù)

我們將創(chuàng)建一個智能地對象,它包含數(shù)據(jù)以及功能處理,而不是弄一個不能處理信息的普通對象。

因為我們希望可以利用Angular的依賴注入,我們將新建一個服務(wù)來管理我們的數(shù)據(jù)模型。我們將在Grid模塊中創(chuàng)建一個TileModel服務(wù),因為只有在涉及到游戲板時,使用低階的TileModel才有必要。

使用.factory這個方法,我們可以簡單地新建一個函數(shù),將之作為一個工場方法。不像service()這個函數(shù)假定我們使用來定義服務(wù)的函數(shù)就是那個服務(wù)的構(gòu)建函數(shù),factory()方法將函數(shù)的返回值作為服務(wù)對象。這樣,使用factory()方法我們能夠?qū)⑷魏螌ο笞鳛橐粋€服務(wù)來注入到我們的Angular應(yīng)用當(dāng)中。

在我們的app/scripts/grid/grid.js這個文件中,我們可以創(chuàng)建我們的TileModel工場方法:

angular.module('Grid')  .factory('TileModel', function() {    var Tile = function(pos, val) {      this.x = pos.x;      this.y = pos.y;      this.value = val || 2;    };       return Tile;  })  // ...

現(xiàn)在,在我們Angular應(yīng)用中的任何地方,我們可以注入TileMode服務(wù),并將它作為一個全局對象來使用。相當(dāng)棒,對不對?

不要忘記給我們放到TileModel里的功能寫測試用例。

我們的第一個方格

現(xiàn)在,我們有了TileModel這個服務(wù),可以開始放置TileModel的實例到tiles數(shù)組中,之后它們就會神奇的出現(xiàn)在格子中正確的地方。

讓我們嘗試在GridService服務(wù)里邊添加一些Tile實例到tiles數(shù)組中:

angular.module('Grid', [])  .factory('TileModel', function() {    // ...  })  .service('GridService', function(TileModel) {    this.tiles  = [];    this.tiles.push(new TileModel({x: 1, y: 1}, 2));    this.tiles.push(new TileModel({x: 1, y: 2}, 2));    // ...  });

游戲板準(zhǔn)備完畢

現(xiàn)在可以放置方塊到屏幕上了,我們需要在GridService里創(chuàng)建一個功能,這個功能將會為我們準(zhǔn)備好游戲板.當(dāng)我們第一次加載頁面時,我們希望可以創(chuàng)建一個空的游戲板。并且希望當(dāng)用戶在游戲區(qū)域點擊"New Game"或者"Try again"按鈕時觸發(fā)相同的動作。

為了清空游戲板,我們將在GameService中創(chuàng)建一個新的函數(shù),叫做buildEmptyGameBoard()。這個方法將會負(fù)責(zé)以空值來填充grid和tiles數(shù)組。

在我們寫代碼前,我們會寫測試來確保buildEmptyGameBoard()這個函數(shù)的正確性。正如我們在上面談到的那樣,我們不會討論過程,只關(guān)心結(jié)果。測試可以像這樣:

// In test/unit/grid/grid_spec.js  // ...  describe('.buildEmptyGameBoard', function() {    var nullArr;       beforeEach(function() {      nullArr = [];      for (var x = 0; x < 16; x++) {        nullArr.push(null);      }    })    it('should clear out the grid array with nulls', function() {      var grid = [];      for (var x = 0; x < 16; x++) {        grid.push(x);      }      gridService.grid = grid;      gridService.buildEmptyGameBoard();      expect(gridService.grid).toEqual(nullArr);    });    it('should clear out the tiles array with nulls', function() {      var tiles = [];      for (var x = 0; x < 16; x++) {        tiles.push(x);      }      gridService.tiles = tiles;      gridService.buildEmptyGameBoard();      expect(gridService.tiles).toEqual(nullArr);    });  });

有了測試,現(xiàn)在可以來實現(xiàn)我們的buildEmptyGameBoard()函數(shù)。

這個函數(shù)很簡單,代碼已經(jīng)充分解釋了它的作用。在app/scripts/grid/grid.js里邊

.service('GridService', function(TileModel) {    // ...    this.buildEmptyGameBoard = function() {      var self = this;      // Initialize our grid      for (var x = 0; x < service.size * service.size; x++) {        this.grid[x] = null;      }         // Initialize our tile array      // with a bunch of null objects      this.forEach(function(x,y) {        self.setCellAt({x:x,y:y}, null);      });    };    // ...

上面的代碼使用了一些功能清晰明了地輔助函數(shù)。這里列舉了一些我們在整個工程中使用的輔助函數(shù),它們都非常簡單明了:

// Run a method for each element in the tiles array  this.forEach = function(cb) {    var totalSize = this.size * this.size;    for (var i = 0; i < totalSize; i++) {      var pos = this._positionToCoordinates(i);      cb(pos.x, pos.y, this.tiles[i]);    }  };     // Set a cell at position  this.setCellAt = function(pos, tile) {    if (this.withinGrid(pos)) {      var xPos = this._coordinatesToPosition(pos);      this.tiles[xPos] = tile;    }  };     // Fetch a cell at a given position  this.getCellAt = function(pos) {    if (this.withinGrid(pos)) {      var x = this._coordinatesToPosition(pos);      return this.tiles[x];    } else {      return null;    }  };     // A small helper function to determine if a position is  // within the boundaries of our grid  this.withinGrid = function(cell) {    return cell.x >= 0 && cell.x < this.size &&            cell.y >= 0 && cell.y < this.size;  };
太不可思議了吧?!??

我們使用到的this._positionToCoordinates()和this._coordinatesToPosition()這倆個函數(shù)有什么用呢?

回想一下我們上面討論的,我們用到了一個一維數(shù)組來布局我們的方格。這從應(yīng)用的性能和處理復(fù)雜動畫來說都是一種更好的選擇。我們將以接下來探討動畫。暫且看來,我們只是得益于利用了一維數(shù)組來代表多維數(shù)組的復(fù)雜性。

一維數(shù)組中的多維數(shù)組

我們?nèi)绾卧谝粋€一維數(shù)組中表示一個多維數(shù)組?讓我們看看沒有顏色的網(wǎng)格表示的游戲板,和它們的格用值表示。在代碼中,這個多維數(shù)組分解為數(shù)組的數(shù)組:

如何利用AngularJS開發(fā)2048游戲如何利用AngularJS開發(fā)2048游戲

查看每個格的位置,當(dāng)我們從單個數(shù)組角度看時,會看到一個模式出現(xiàn):

如何利用AngularJS開發(fā)2048游戲

我們可以看到第一個格,(0,0)映射到格的0的位置。第二個數(shù)組位置 1 指向網(wǎng)格的 (1,0) 位置。移動到下一行,網(wǎng)格的 (0,1) 位置指向一維數(shù)組的第 4 個元素,而索引為 5 的元素指向 (1.1)。

推算出位置之間的關(guān)系,我們可以看出方程中出現(xiàn)兩個位置之間的關(guān)系。

i = x + ny

這里的 i 是格的索引,x 和 y 是在多維數(shù)組中的位置,n 是格每行/列的數(shù)量。

我們定義兩個轉(zhuǎn)換格位置為  x-y 坐標(biāo)系或 y-x 坐標(biāo)系的幫助函數(shù)。從概念上講,很容易將格位置處理為  x-y 坐標(biāo),但是函數(shù)上我們將設(shè)置我們的一維數(shù)組中的每個拼貼。

// Helper to convert x to x,y  this._positionToCoordinates = function(i) {    var x = i % service.size,        y = (i - x) / service.size;    return {      x: x,      y: y    };  };     // Helper to convert coordinates to position  this._coordinatesToPosition = function(pos) {    return (pos.y * service.size) + pos.x;  };

最初的游戲者位置

現(xiàn)在,開始一個新的游戲,我們將想要設(shè)置一些開始的塊。我們將隨便的為我們的游戲者在游戲面板中選擇這些開始的地方。

.service('GridService', function(TileModel) {    this.startingTileNumber = 2;    // ...    this.buildStartingPosition = function() {      for (var x = 0; x < this.startingTileNumber; x++) {        this.randomlyInsertNewTile();      }    };    // ...

建立一個開始位置相對簡單,因為只需要調(diào)用 randomlyInsertNewTile() 函數(shù)放置拼貼的數(shù)量。randomlyInsertNewTile() 函數(shù)需要我們知道所有可以隨便放置拼貼的位置。這在函數(shù)上很容易實現(xiàn),因為所有我們需要做的是走過唯一數(shù)組并跟蹤數(shù)組中沒有放置拼貼的位置。

.service('GridService', function(TileModel) {    // ...    // Get all the available tiles    this.availableCells = function() {      var cells = [],          self = this;         this.forEach(function(x,y) {        var foundTile = self.getCellAt({x:x, y:y});        if (!foundTile) {          cells.push({x:x,y:y});        }      });         return cells;    };    // ...

列出了游戲板上所有可用的坐標(biāo),我們可以簡單地從這個數(shù)組中選擇一個隨機(jī)的位置。我們的 randomAvailableCell() 函數(shù)將為我們處理這些。我們可以用幾種不同的方式來實現(xiàn)。這里顯示我們在2048中的實現(xiàn)。

.service('GridService', function(TileModel) {    // ...    this.randomAvailableCell = function() {      var cells = this.availableCells();      if (cells.length > 0) {        return cells[Math.floor(Math.random() * cells.length)];      }    };    // ...

在這里,我們可以簡單地創(chuàng)建一個新的TileModel實例并插入到我們的 this.tiles 數(shù)組中。

.service('GridService', function(TileModel) {    // ...    this.randomlyInsertNewTile = function() {      var cell = this.randomAvailableCell(),          tile = new TileModel(cell, 2);      this.insertTile(tile);    };       // Add a tile to the tiles array    this.insertTile = function(tile) {      var pos = this._coordinatesToPosition(tile);      this.tiles[pos] = tile;    };       // Remove a tile from the tiles array    this.removeTile = function(pos) {      var pos = this._coordinatesToPosition(tile);      delete this.tiles[pos];    }    // ...  });

現(xiàn)在,由于我們使用了 Angular ,我們的方塊在我們的視圖中將只是魔法般的顯示為游戲板上的拼貼。

”記住,下一步要做的是寫測試來測試我們關(guān)于函數(shù)的假設(shè)。我們在為這個項目寫測試時發(fā)現(xiàn)幾個bug,你也會發(fā)現(xiàn)。

鍵盤互鎖

好了,現(xiàn)在在游戲板上有了我們的拼貼塊。有趣的是一個游戲你不能玩?讓我們轉(zhuǎn)換注意力到在游戲里添加互動。

這篇文章的目的,我們只關(guān)注游戲板交互,把觸摸操作放在一邊。不過,添加觸摸動作應(yīng)該不難,特別是我們只對滑動感興趣,這是 ngTouch 提供的。我們不管這個先管實現(xiàn)。

游戲本身通過使用箭頭鍵(或a,w,s,d鍵)操作。在我們的游戲中,我們想要允許用戶簡單的在頁面上與游戲交互。與要求用戶關(guān)注游戲板元素(或任何其他頁面上的元素,就此而言)相反。這將允許用戶只關(guān)注文檔與游戲交互。

為了允許用戶的這種交互類型,添加一個事件監(jiān)聽器到文檔。在Angular中,我們將“綁定”我們的事件監(jiān)聽器和由Angular提供的 $document 服務(wù)。為了處理定義用戶交互,我們將在一個服務(wù)中封裝我們的鍵盤事件綁定。記住,我們在頁面中只需要一個鍵盤處理器,所以一個服務(wù)是最好的。

另外,我們也希望在我們檢測到用戶鍵盤操作時,設(shè)置自定義動作發(fā)生。使用一個服務(wù)將允許我們自然的添加它到我們的angular對象并根據(jù)用戶輸入產(chǎn)生動作。

首先,我們創(chuàng)建一個新的模塊(就像我們所做的基于模塊的開發(fā)),在 app/scripts/keyboard/keyboard.js 文件(如果之前不存在,我們需要創(chuàng)建它)中叫做 Keyboard。

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

對于我們創(chuàng)建的任何新的 JavaScript,我們需要在我們的 index.heml 文件中引用?,F(xiàn)在的 <script> 標(biāo)簽列表看起來像這樣:

<!-- body -->    <script src="scripts/app.js"></script>    <script src="scripts/grid/grid.js"></script>    <script src="scripts/grid/grid_directive.js"></script>    <script src="scripts/grid/tile_directive.js"></script>    <script src="scripts/keyboard/keyboard.js"></script>    <script src="scripts/game/game.js"></script>  </body>  </html>

而由于我們創(chuàng)建了一個新的模塊,我們也將需要告訴我們的Angular模塊,我們想把這個新模塊用作我們自己的應(yīng)用程序的依賴項:

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

這個鍵盤服務(wù)背后的意思是,我們將在$document上綁定keydown事件,來捕獲來自文檔中的用戶交互組件。在另外一端的我們的angular對象中,我們會將事件處理函數(shù)進(jìn)行注冊,而后它就可以在用戶交互發(fā)生時被調(diào)用.

讓我們開始吧.

// app/scripts/keyboard/keyboard.js  angular.module('Keyboard', [])  .service('KeyboardService', function($document) {       // Initialize the keyboard event binding    this.init = function() {    };       // Bind event handlers to get called    // when an event is fired    this.keyEventHandlers = [];    this.on = function(cb) {    };  });

init() 函數(shù)會讓 KeyboardService 開始偵聽鍵盤事件. 我們將會過濾掉不感興趣的鍵盤事件.

對于我們感興趣的任何事件觸發(fā),我們將會組織默認(rèn)動作的運行,并將該事件派發(fā)到我們的keyEventHandlers.

如何利用AngularJS開發(fā)2048游戲

我怎么知道什么事件是我們感興趣的呢?因為我們只對有限數(shù)量的鍵盤事件感興趣,所以我們可以通過用我們感性的其中一個鍵盤事件來進(jìn)行檢查確認(rèn).

當(dāng)箭頭按鍵被按下的時候,文檔對象會收到一個事件,這個事件帶上了被按下的鍵盤按鍵的按鍵編碼.

我們可以創(chuàng)建一個這些事件的映射表,然后檢查鍵盤動作在這個關(guān)注映射表中的存在.

// app/scripts/keyboard/keyboard.js  angular.module('Keyboard', [])  .service('KeyboardService', function($document) {       var UP    = 'up',        RIGHT = 'right',        DOWN  = 'down',        LEFT  = 'left';       var keyboardMap = {      37: LEFT,      38: UP,      39: RIGHT,      40: DOWN    };       // Initialize the keyboard event binding    this.init = function() {      var self = this;      this.keyEventHandlers = [];      $document.bind('keydown', function(evt) {        var key = keyboardMap[evt.which];           if (key) {          // An interesting key was pressed          evt.preventDefault();          self._handleKeyEvent(key, evt);        }      });    };    // ...  });

任何時候keyboardMap中的按鍵觸發(fā)了 keydown 事件,  KeyboardService 都會運行 this._handleKeyEvent 函數(shù).

這個函數(shù)的全部責(zé)任就是調(diào)用每個時間處理器中注冊了的每一個按鍵處理函數(shù). 它將會簡單的遍歷按鍵處理函數(shù)的數(shù)組,包括按鍵事件和原始的事件,每一個都調(diào)用一遍:

// ...  this._handleKeyEvent = function(key, evt) {    var callbacks = this.keyEventHandlers;    if (!callbacks) {      return;    }       evt.preventDefault();    if (callbacks) {      for (var x = 0; x < callbacks.length; x++) {        var cb = callbacks[x];        cb(key, evt);      }    }  };  // ...

另外,我們只需要將我們的處理器函數(shù)放到我們的處理器列表中就可以了.

// ...  this.on = function(cb) {    this.keyEventHandlers.push(cb);  };  // ...

使用Keyboard服務(wù)

現(xiàn)在我們已經(jīng)有能力觀察來自用戶的鍵盤事件, 我們需要在我們的應(yīng)用啟動時啟動它. 因為我們是把它作為服務(wù)創(chuàng)建的,我們可以簡單的在主控制器中做這些事情.

如何利用AngularJS開發(fā)2048游戲

首先,我們將需要調(diào)用init()函數(shù)啟動在鍵盤上的監(jiān)聽. 然后,我們將要把我們的處理器函數(shù)注冊到GameManager 對 move() 函數(shù)的調(diào)用上.

回到我們的GameController, 我們將新增 newGame() 和 startGame() 函數(shù). newGame() 函數(shù)將簡單的調(diào)用游戲服務(wù)來創(chuàng)建一個新的游戲,并啟動鍵盤事件處理程序.

然我們來看看代碼!我們需要為我們應(yīng)用程序注入作為新的模塊依賴的Keyboard模塊:

angular.module('twentyfourtyeightApp', ['Game', 'Keyboard'])  // ...

現(xiàn)在我們就可以吧 KeyboardService 注入到我們的 GameController 并在發(fā)生用戶交互時啟動. 首先是 newGame() 方法:

// ... (from above)  .controller('GameController', function(GameManager, KeyboardService) {    this.game = GameManager;       // Create a new game    this.newGame = function() {      KeyboardService.init();      this.game.newGame();      this.startGame();    };       // ...

我們還沒有在GameManager上定義newGame()方法, 很快我們就會充實它.

當(dāng)我們把新游戲創(chuàng)建好,我們會調(diào)用 startGame(). startGame() 函數(shù)將會設(shè)置鍵盤服務(wù)事件處理器:

.controller('GameController', function(GameManager, KeyboardService) {    // ...    this.startGame = function() {      var self = this;      KeyboardService.on(function(key) {        self.game.move(key);      });    };       // Create a new game on boot    this.newGame();  });

按下開始按鈕
 

我們已經(jīng)做了很多工作來讓自己達(dá)到這樣一個里程碑:開始游戲. 我們需要實現(xiàn)的最后一個方法就是GameManager里面的newGame()方法:

  1. 構(gòu)建一個空的游戲面板d

  2. 設(shè)置開始位置

  3. 初始化游戲

我們已經(jīng)在我們的GridService里面實現(xiàn)了這一邏輯, 因此現(xiàn)在只是要想辦法把它給掛上去了!

在我們的 app/scripts/game/game.js 文件中,讓我們來添加這個 newGame() 函數(shù). 這個函數(shù)將會把我們的游戲統(tǒng)計重設(shè)到預(yù)期的開始條件:

angular.module('Game', [])  .service('GameManager', function(GridService) {    // Create a new game    this.newGame = function() {      GridService.buildEmptyGameBoard();      GridService.buildStartingPosition();      this.reinit();    };       // Reset game state    this.reinit = function() {      this.gameOver = false;      this.win = false;      this.currentScore = 0;      this.highScore = 0; // we'll come back to this    };  });

在我們的瀏覽器匯總加載好這個頁面,我們將得到一個網(wǎng)格&hellip; 因為我們還沒有定義任何移動功能,所有現(xiàn)在看起來還相當(dāng)?shù)牧钊朔ξ?

如何利用AngularJS開發(fā)2048游戲

讓你的游戲動起來 (游戲主循環(huán))
 

現(xiàn)在讓我們來深入研究一下我們游戲的實際功能是怎么實現(xiàn)的. 當(dāng)用戶按下任何方向鍵, 我們會調(diào)用GridService上的move()函數(shù)(我們曾在GameController里面創(chuàng)建了這個函數(shù)).

為了構(gòu)建 move() 函數(shù), 我們將需要定義游戲約束. 即,我們需要定義在每一個動作上我們的游戲?qū)⑷绾畏磻?yīng).

對于每一個動作,我們需要:

  1. 確定用戶的方向鍵指示的向量.

  2. 為面板上的每一個小塊找到其所有的最遠(yuǎn)可能位置。同時,拿下一個位置的方塊比較看看我們是不是能夠把它們合并.

  3. 對于每一個方塊,我們將會想要確認(rèn)是否有下一個帶有相同值的方塊存在.

    1. 如果該方塊已經(jīng)是合并后的結(jié)果了,那我們就把它認(rèn)為是已經(jīng)用過了的,并跳過它.

    2. 如果方塊還沒有合并過,那么我就要把它認(rèn)為是可以合并的.

    3. 如果不存在下一個方塊,那么我們就只要將方塊移動到最遠(yuǎn)的位置上就行了. (這意味著是面板上的最遠(yuǎn)端).

    4. 如果存在下一個方塊:

    5. 并且下一個方塊的值是跟當(dāng)前方塊不同的值,那么我們就將方塊平鋪到最遠(yuǎn)的位置(下一個方塊的位置是當(dāng)前方塊移動的邊界).

    6. 并且下一個方塊的值是跟當(dāng)前方塊相同的值,那么我們就找到了一個可能的合并.

現(xiàn)在我們已經(jīng)把功能定義好了,我們就可以制定構(gòu)建move()函數(shù)的策略了.

angular.module('Game', [])  .service('GameManager', function(GridService) {    // ...    this.move = function(key) {      var self = this; // Hold a reference to the GameManager, for later      // define move here      if (self.win) { return false; }    };    // ...  });

對于移動有幾個條件需要考慮:如果游戲結(jié)束了,并且我們已經(jīng)以某種方式結(jié)束了游戲循環(huán),我們將簡單的返回并繼續(xù)循環(huán).

接下來我們將需要遍歷整個網(wǎng)格,找出所有可能的位置. 由于網(wǎng)格有責(zé)任了解那個位置是打開的, 我們將在GridService上創(chuàng)建一個新的函數(shù),以幫助我們找出所有可能的遍歷位置.

如何利用AngularJS開發(fā)2048游戲

為了找出方向,我們將需要挑選出用戶按鍵所指示的向量. 例如,如果用戶按下右方向鍵,那么將是想要往x軸增長的方向移動.

如果用戶按下了上方向鍵,那么用戶是想方塊往y軸減少的方向移動. 我們可以使用一個JavaScript對象將我們的向量映射到用戶所按下的鍵(我們可以從KeyboardService獲取到), 向下面這樣:

// In our `GridService` app/scripts/grid/grid.jsvar vectors = {    'left': { x: -1, y: 0 },    'right': { x: 1, y: 0 },    'up': { x: 0, y: -1 },    'down': { x: 0, y: 1 }};

現(xiàn)在我們將簡單的遍歷所有可能的位置,使用向量來決定我們想要遍歷潛在位置的方向:

.service('GridService', function(TileModel) {    // ...    this.traversalDirections = function(key) {      var vector = vectors[key];      var positions = {x: [], y: []};      for (var x = 0; x < this.size; x++) {        positions.x.push(x);        positions.y.push(x);      }      // Reorder if we're going right      if (vector.x > 0) {        positions.x = positions.x.reverse();      }      // Reorder the y positions if we're going down      if (vector.y > 0) {        positions.y = positions.y.reverse();      }      return positions;    };    // ...

現(xiàn)在隨著我們新的 traversalDirections() 被定義,我們可以遍歷move()函數(shù)中所有可能的移動了。回到我們的GameManager, 我們將使用這些潛在的為哈子讓網(wǎng)格里面的方塊跑起來.

// ...  this.move = function(key) {    var self = this;    // define move here    if (self.win) { return false; }    var positions = GridService.traversalDirections(key);       positions.x.forEach(function(x) {      positions.y.forEach(function(y) {        // For every position      });    });  };  // ...

現(xiàn)在在我們的位置循環(huán)中,我們將遍歷所有可能位置,并找出位置中現(xiàn)有的方塊。從這里開始我們將開始像功能的第二部分進(jìn)發(fā),找出從方塊出發(fā)所有更遠(yuǎn)處的位置:

// ...  // For every position  // save the tile's original position  var originalPosition = {x:x,y:y};  var tile = GridService.getCellAt(originalPosition);     if (tile) {    // if we have a tile here    var cell = GridService.calculateNextPosition(tile, key);    // ...  }

如何利用AngularJS開發(fā)2048游戲

如果我們找到了一個方塊,我們將開始從這個方塊開始尋找最遠(yuǎn)的可能位置. 為此,我們將一步一步遍歷網(wǎng)格的下一個位置,檢查下一個格子是否在網(wǎng)格的邊界以內(nèi),還有是否這個網(wǎng)格單元所在的位置是空的(也就是還沒有方塊).

如果這個網(wǎng)格單元是空的并且在網(wǎng)格的邊界之內(nèi),那么我們將繼續(xù)轉(zhuǎn)移到下一個網(wǎng)格單元并檢查同樣的條件.

如果這兩個條件有一個沒有滿足,那么我們就可能找到了網(wǎng)格的邊界,或者是找到了下一個單元. 我們將吧下一個位置作為新的位置newPosition保存,并且獲取到下一個單元(不管它是否存在).

由于這個過程設(shè)計到網(wǎng)格,所以我們將把這個函數(shù)放到GridService里面:

// in GridService  // ...  this.calculateNextPosition = function(cell, key) {    var vector = vectors[key];    var previous;       do {      previous = cell;      cell = {        x: previous.x + vector.x,        y: previous.y + vector.y      };    } while (this.withinGrid(cell) && this.cellAvailable(cell));       return {      newPosition: previous,      next: this.getCellAt(cell)    };  };

現(xiàn)在我們就可以為我們的 方塊計算下一個可能的位置了,我們還可以檢查潛在的合并.

合并被定義成一個方塊融入另一個值與之相同的方塊. 我們將檢查是否下一個位置有相同值的方塊,還有之前它是否已經(jīng)合并過.

// ...  // For every position  // save the tile's original position  var originalPosition = {x:x,y:y};  var tile = GridService.getCellAt(originalPosition);     if (tile) {    // if we have a tile here    var cell = GridService.calculateNextPosition(tile, key),        next = cell.next;       if (next &&        next.value === tile.value &&        !next.merged) {      // Handle merged    } else {      // Handle moving tile    }    // ...  }

現(xiàn)在,如果下一個位置不滿足條件,俺么我們就只要讓方塊從當(dāng)前位置向下一個位置進(jìn)行簡單的移動就行了(代碼中的else語句).

這是其中比較容易處理的情況,我們要做的就是將方塊移動到新的位置newPosition.

// ...  if (next &&      next.value === tile.value &&      !next.merged) {    // Handle merged  } else {    GridService.moveTile(tile, cell.newPosition);  }

移動方塊

如果可能會猜想到的,moveTile()方法是一個最有可能被定義在GridService中的操作.

移動方面就是簡單的更新方塊在數(shù)組中的位置,還有就是更新TileModel.

如我們已經(jīng)定義的,有兩個單獨的操作用于分開的兩個目的. 當(dāng)我們要:

移動數(shù)組中的方塊

GridService數(shù)組會從后端開始映射方塊的定位. 數(shù)組中方塊的位置并沒有被綁定到網(wǎng)格的位置上.

更新TileModel上的位置

我們會為前端放置方塊的CSS更新坐標(biāo).

簡而言之:為了保持對后端方塊的跟蹤,我們將需要更新GridService中的 this.tilesarray 并更新方塊對象的位置.

而moveTile() 就編程了簡單的兩步操作 :

// GridService  // ...  this.moveTile = function(tile, newPosition) {    var oldPos = {      x: tile.x,      y: tile.y    };       // Update array location    this.setCellAt(oldPos, null);    this.setCellAt(newPosition, tile);    // Update tile model    tile.updatePosition(newPosition);  };

現(xiàn)在我們將需要定義我們的 tile.updatePosition() 方法. 方法并不像它聽起來的那樣,它只是簡單的更新了模型自身的x和y坐標(biāo):

.factory('TileModel', function() {    // ...       Tile.prototype.updatePosition = function(newPos) {      this.x = newPos.x;      this.y = newPos.y;    };    // ...  });

回到我們的GridService, 我們可以簡單的調(diào)用 .moveTile() 來同時更新GridService.tiles 數(shù)組和方塊自身上面的位置.

合并方塊

現(xiàn)在我們已經(jīng)處理的較簡單的情況,而合并方塊也就成了接下來我們需要處理的問題。合并是這樣被定義的:

合并發(fā)生在某個方塊在下一個位置遇到值與之相同的另一個方塊的時候.

當(dāng)一個方塊被合并,它就移動的面板并更新當(dāng)前游戲的得分以及(在必要的時候)最高得分.

合并需要下面這幾步:

  1. 在最終的位置添加一個以合并數(shù)為其值的新方塊

  2. 移除舊的方塊

  3. 更新游戲的得分

  4. 檢查是否產(chǎn)生了獲勝的方塊值

分解下來,合并操作就成了一些需要處理的簡單操作.

// ...  var hasWon = false;  // ...  if (next &&      next.value === tile.value &&      !next.merged) {    // Handle merged    var newValue = tile.value * 2;    // Create a new tile    var mergedTile = GridService.newTile(tile, newValue);    mergedTile.merged = [tile, cell.next];       // Insert the new tile    GridService.insertTile(mergedTile);    // Remove the old tile    GridService.removeTile(tile);    // Move the location of the mergedTile into the next position    GridService.moveTile(merged, next);    // Update the score of the game    self.updateScore(self.currentScore + newValue);    // Check for the winning value    if (merged.value >= self.winningValue) {      hasWon = true;    }  } else {  // ...

因為我們只想支持每行一個單獨的方塊移動(那就是如果我們有兩個可能的合并,那么每行只會有一個合并會發(fā)生), 我們還需要保持對已經(jīng)合并的方塊的跟蹤. 我們?yōu)榇硕x了.merged標(biāo)識.

在我們放下對這個函數(shù)的關(guān)注之前,我們使用了兩個還有沒有定義好的函數(shù).

GridService.newTile() 方法創(chuàng)建了一個新的TileModel對象。我們在GridService中的這個操作只是簡單的包含我們所創(chuàng)建的新方塊的位置:

// GridService  this.newTile = function(pos, value) {    return new TileModel(pos, value);  };  // ...

我們將回到 self.updateScore() 方法一小會兒. 現(xiàn)在,我們有足夠多的信息知道它更新了游戲的分值(如方法名稱所示).

方塊移動之后

我們只想盡在做出一次有效的移動之后才添加新的方塊,因此我們將需要去檢查看看是否實際真的發(fā)生了任何從一個方塊到另一個方塊的移動.

var hasMoved = false;  // ...    hasMoved = true; // we moved with a merge  } else {    GridService.moveTile(tile, cell.newPosition);  }     if (!GridService.samePositions(originalPos, cell.newPosition)) {    hasMoved = true;  }  // ...

在所有的方塊都已經(jīng)移動(或者嘗試著要被移動)之后,我們將檢查游戲是否已經(jīng)被完成。如果游戲?qū)嶋H上已經(jīng)結(jié)束了,我們就將設(shè)置游戲上的self.win.

我們會在當(dāng)我們有一個方塊碰撞的時候移動,因此在合并的條件下,我們將簡單的把 hasMovedvariable 設(shè)置成 true.

最后,我們將會檢查面板上是否有任何的移動發(fā)生. 如果有,我們將:

  1. 想面板添加一個新的方塊

  2. 檢查我們是否需要顯示游戲結(jié)束 gameOver幀

if (!GridService.samePositions(originalPos, cell.newPosition)) {    hasMoved = true;  }     if (hasMoved) {    GridService.randomlyInsertNewTile();       if (self.win || !self.movesAvailable()) {      self.gameOver = true;    }  }  // ...

重設(shè)方塊

在我們運行任意一次主游戲循環(huán)之前,我們將需要重設(shè)每一個方塊,比如我們不在需要跟蹤他們的合并狀態(tài). 即,每次我們要做出單個的移動時,都要將之前的狀態(tài)清除,讓每一個方塊都能再次移動. 為此,在移動的循環(huán)開始處,我們將會調(diào)用:

GridService.prepareTiles();

GridService中的prepareTiles()方法簡單的遍歷了所有的方塊并重設(shè)了它們的狀態(tài):

this.prepareTiles = function() {    this.forEach(function(x,y,tile) {      if (tile) {        tile.reset();      }    });  };

保留分值

回到 updateScore() 方法 ; 游戲本身需要跟蹤兩個分值:

  1. 當(dāng)前游戲的得分

  2. 玩家的歷史最高分

當(dāng)前得分 currentScore 只是一個簡單的變量,我們將在每一次游戲的內(nèi)存中對它進(jìn)行跟蹤. 也就是說我們不需要任何特殊的方式來處理它.

歷史最高分 highScore, 是一個我們會持久化的變量. 我們有幾種方法來處理這個問題,使用本地存儲 localstorage, cookies, 或者是兩者的結(jié)合.

因為cookie是兩種方式中最簡單,也是在跨瀏覽器時最安全的一種方法, 因此我們也就采用把我們的最高分 highScore 設(shè)置到一個cookie中.

在Angular中訪問cookie的最簡單方式是使用 angular-cookies 模塊.

為了使用這個模塊,我們將需要從 angularjs.org 下載它,或者使用包管理器,比如bower,來安裝它.

$ bower install --save angular-cookies

像往常一樣,我們需要在index.html中引用腳本,并對應(yīng)用上的設(shè)置模塊級依賴 ofngCookies .

我們將向下面這樣更新我們的 app/index.html :

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

現(xiàn)在就是要把 ngCookies 模塊作為模塊級依賴添加進(jìn)去 (在我們將要引用cookie的 Game 模塊上):

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

設(shè)置好對ngCookies的依賴,我們就可以將 $cookieStore 服務(wù)注入到我們的 GameManager服務(wù)中去了. 而我們現(xiàn)在就可以在我們用戶的瀏覽器上獲取和設(shè)置cookie了.

例如,為了獲取用戶最近的最高得分,我們將編寫一個函數(shù)來為我們從用戶的cookie中獲取它:

this.getHighScore = function() {    return parseInt($cookieStore.get('highScore')) || 0;  }

回到GameManager類上的updateScore()方法, 我們將更新本地的當(dāng)前得分. 如果當(dāng)前得分比我們之前的最高得分還要高,那我們就將更新最高得分的cookie.

this.updateScore = function(newScore) {    this.currentScore = newScore;    if (this.currentScore > this.getHighScore()) {      this.highScore = newScore;      // Set on the cookie      $cookieStore.put('highScore', newScopre);    }  };

解決對方塊的跟蹤問題

現(xiàn)在我們可以讓方塊顯示在屏幕上了,但是屏幕上會出現(xiàn)一個問題,那就是一些奇怪的行為會讓我們得到重復(fù)的方塊. 此外,我們的方塊也會出現(xiàn)在不可預(yù)期的位置.

這個問題的原因是Angular只知道方塊是被賦予了一個唯一的標(biāo)識,然后被放在方塊數(shù)組中的. 我們在視圖中設(shè)置了這個唯一的標(biāo)識符,作為數(shù)組中方塊的 $index(也就是它在數(shù)組中的索引,或者說位置). 因為我們會在數(shù)組中到處移動方塊,所以$index不再能夠?qū)哂形ㄒ粯?biāo)識的方塊進(jìn)行跟蹤. 我們需要一個不同的跟蹤方案.

<div id="game">   <!-- grid-container -->   <div class="tile-container">     <div tile         ng-model='tile'       ng-repeat='tile in ngModel.tiles track by $index'></div>   </div> </div>

我們將會通過方塊自身唯一的uuid來對其進(jìn)行跟蹤,而不是依賴于數(shù)組來識別方塊的位置. 創(chuàng)建我們自己的唯一標(biāo)識將確保angular可以講方塊數(shù)組中的方塊作為它們自己的唯一對象進(jìn)行對待. Angular 將會把識別唯一的表示,并把方塊看做是它自身的對象, 只要保證方塊唯一的uuid沒有發(fā)生變化就行.

當(dāng)我們創(chuàng)建一個新的實體是,我們就能使用TileModel很容易的實現(xiàn)一個唯一的標(biāo)識方案. 我們也可以想出我們自己的創(chuàng)意來創(chuàng)建唯一的標(biāo)識.

只要我們創(chuàng)建的每一個 TileModel 實體都是唯一的,我們?nèi)绾紊晌ㄒ恍詉d都無所謂.

為了創(chuàng)建一個唯一的id,我們跳轉(zhuǎn)到 StackOverflow, 找到 rfc4122-compliant,一個全局的唯一標(biāo)識生成器,并用一個單獨的方法next()將這個算法封裝成一個工廠:

.factory('GenerateUniqueId', function() {    var generateUid = function() {      // http://www.ietf.org/rfc/rfc4122.txt      var d = new Date().getTime();      var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {        var r = (d + Math.random()*16)%16 | 0;        d = Math.floor(d/16);        return (c === 'x' ? r : (r&0x7|0x8)).toString(16);      });      return uuid;    };    return {      next: function() { return generateUid(); }    };  })

為了使用這個 GenerateUniqueId 工廠, 我們可以將它注入,并調(diào)用 GenerateUniqueId.next() 來創(chuàng)建新的uuid. 回到我們的 TileModel, 我們可以為(構(gòu)造器中的)實體創(chuàng)建一個唯一的id了:

// In app/scripts/grid/grid.js  // ...  .factory('TileModel', function(GenerateUniqueId) {    var Tile = function(pos, val) {      this.x      = pos.x;      this.y      = pos.y;      this.value  = val || 2;      // Generate a unique id for this tile      this.id = GenerateUniqueId.next();      this.merged = null;    };    // ...  });

現(xiàn)在我們的每一個方塊都有了一個唯一的標(biāo)識符, 我們可以告訴Angular通過這個id而不是 $index進(jìn)行跟蹤.

<!-- ... --> <div tile         ng-model='tile'       ng-repeat='tile in ngModel.tiles track by $id(tile.id)'></div> <!-- ... -->

這一方案只有一個問題。因為我們是(明確的)使用null初始化我們的數(shù)組的,并且我們會用null來重設(shè)數(shù)組(而不是 sort 或者 resize 這個數(shù)組), Angular就會不管不顧的將null作為對象來進(jìn)行跟蹤. 因為null值并沒有唯一的id,因此這就將會造成我們的瀏覽器拋出一個錯誤,而且瀏覽器它也沒有辦法處理重復(fù)的對象.

因此,我們可以使用一個內(nèi)置的angular工具來跟蹤唯一id,還有對象的$index位置(null 值對象可以用它們在數(shù)組中的位置進(jìn)行跟蹤,因為每一個位置只會有一個). 我們可以像下面這樣通過修改網(wǎng)格指令的視圖來計算出null對象:

<!-- ... --> <div tile         ng-model='tile'       ng-repeat='tile in ngModel.tiles track by $id(tile.id || $index)'></div> <!-- ... -->

這個問題可以通過依賴數(shù)據(jù)架構(gòu)的一種不同的實現(xiàn)來解決, 比如在一個迭代器中查找每一個TileModel的位置,而不是依賴于方塊數(shù)據(jù)的索引,或者是在每次發(fā)生變化(或者執(zhí)行了一次$digest())時都對數(shù)組重新組合一次. 為了簡單明了起見,我們已經(jīng)用數(shù)組對其進(jìn)行了實現(xiàn),而這是唯一一個我們需要針對這個實現(xiàn)進(jìn)行處理的副作用.

我們贏了嗎??。??游戲結(jié)束了

當(dāng)我們輸?shù)?048原作游戲時,一個游戲結(jié)束的提示框滑入屏幕,它允許我們重新開始游戲,并且在twitter上關(guān)注游戲的創(chuàng)建者。這不光是一個呈現(xiàn)給玩家的炫酷效果,它還介紹了一種中斷游戲運行的好方法。

我們可以用一些基本地angular技術(shù)輕易的創(chuàng)建這種效果。我們已經(jīng)在GameManager中用gamOver變量來記錄游戲是否結(jié)束。我們可以創(chuàng)建一個<div>標(biāo)簽來包含游戲結(jié)束提示框,并且在游戲方格中以絕對坐標(biāo)給它定位。這種技術(shù)(和Angular)的神奇的地方就在于簡單地就可以實現(xiàn)如此功能,并且還沒有任何的花招:

我們可以簡單地創(chuàng)建一個<div>元素來包含游戲結(jié)束或勝利時的消息,并且根據(jù)游戲的狀態(tài)呈現(xiàn)出來。舉個例子,游戲結(jié)束提示框像這樣:

<!-- ... --> <div id="game-container">   <div grid ng-model='ctrl.game' class="row"></div>     <div id="game-over"           ng-if="ctrl.game.gameOver"         class="row game-overlay">       Game over        <div class="lower">         <a class="retry-button" ng-click='ctrl.newGame()'>Try again</a>       </div>     </div>   <!-- ... -->

比較難的一部分是處理樣式。比較高效的做法是,我們只是將元素放置到游戲方格中的一個絕對位置上,然后由瀏覽器去完成布局的工作。這是與樣式(注意,完事的CSS樣式可以到下面的github鏈接中找到)相關(guān)的一部分:

.game-overlay {    width: $width;    height: $width;    background-color: rgba(255, 255, 255, 0.47);    position: absolute;    top: 0;    left: 0;    z-index: 10;    text-align: center;    padding-top: 35%;    overflow: hidden;    box-sizing: border-box;       .lower {      display: block;      margin-top: 29px;      font-size: 16px;    }  }

我們能夠以相同的方法來實現(xiàn)勝利時的提示框,只需要創(chuàng)建一個表示勝利的.game-overlay元素即可。

動畫

2048游戲原作中一個令人印象深刻的一點是方塊看上去神奇的從一個位置滑到下一個位置,并且游戲結(jié)束或者勝利時的提示框很自然的出現(xiàn)在了屏幕上。當(dāng)我們使用Angular時,我們可以免費實現(xiàn)幾乎一模一樣的效果(感謝CSS

實際上,我們已經(jīng)建立起了游戲,這樣我們創(chuàng)建滑動、顯現(xiàn)、展現(xiàn)等動畫效果就很容易實現(xiàn)。我們(幾乎)沒有用JavaScript來實現(xiàn)它們。

對 CSS 定位進(jìn)行動畫處理(即添加方塊滑動)

當(dāng)我們使用position-[x]-[y]類,通過CSS定位方格時,一旦在方格上設(shè)置了一個新位置,DOM元素將會添加一個新類position-[newX]-[newY],同時移除舊類position-[oldX]-[oldY]。在這種情況下,我們可以通過在.tile類上定義一個CSS過渡,簡單地定義默認(rèn)的滑動動作發(fā)生在CSS類本身。

相關(guān)的SCSS如下:

.tile {    @include border-radius($tile-radius);    @include transition($transition-time ease-in-out);    -webkit-transition-property: -webkit-transform;    -moz-transition-property: -moz-transform;    transition-property: transform;    z-index: 2;  }

定義好了CSS過渡,滑塊現(xiàn)在可以輕松地在一個位置和新位置之間滑動了。(是的,真的就是如此簡單。)

讓結(jié)束畫面動起來

現(xiàn)在,讓我們在動畫上找些 樂子,試試 ng-Animate 模塊。這是 angular 框架一個開箱即用的模塊。

在寫代碼前,需要首先安裝ng-Animate。有兩個方法,一是直接從 angularjs.org 下載,一是用包管理器(例如 bower)安裝。

$ bower install --save angular-animate

照例,要在我們的 HTML 文件中引用這個腳本,這樣瀏覽器才能載入模塊。修改 index.html 文件載入 angular-animate.js:

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

像任何其他 angular 模塊一樣,我們需要告訴 angular 框架我們的模塊需要依賴 angular-animate。 只需修改 app/app.js 文件的依賴數(shù)組即可:

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

ngAnimate模塊

盡管深入地討論ngAnimate超出了本文的范圍(查看ng-book來深入了解它如何工作),我們僅僅簡單地了解它如何工作,以便可以為我們的游戲?qū)崿F(xiàn)動畫效果。

ngAnimate作為一個獨立的模塊,angular任何時候在一個相關(guān)的指令中添加一個新的對象(到我們的游戲中),都將給它附值一個CSS類(免費)。我們可以使用這些類來為我們游戲中的不同組件添上動畫效果:

命令進(jìn)入類離開類
ng-repeatng-enterng-leave
ng-ifng-enterng-leave
ng-class[className]-add[className]-remove

當(dāng)一個元素被添加進(jìn)入ng-repeat命令的范圍,新的DOM元素將會自動地被附上ng-enter這個CSS類。然后,當(dāng)它真正被添加到視圖上去后,將添加上ng-enter-active這個CSS類。這是很重要的,因為它將允許我們在ng-enter類里構(gòu)建我們希望的動畫效果,并且在ng-enter-active類里設(shè)置動畫的樣式。這個功能和ng-leave在元素從ng-repeat迭代指令中移除時起到的效果一樣。

當(dāng)一個新的CSS類從一個DOM元素上被添加(或被移除)時,相應(yīng)的CSS類[classname]-add和[classname]-add-active將被添加到這個DOM元素上。這里我們再一次在相應(yīng)的類里設(shè)置我們的CSS動畫。

讓游戲結(jié)束的提示畫面動起來

我們可以使用ng-enter類讓游戲結(jié)束或者游戲勝利時的提示畫面以動畫效果呈現(xiàn)出來。記住,.game-overlay這個類被隱藏起來了,需要用ng-if指令來顯示它。當(dāng)ng-if條件改變時,ngAnimate將會在表達(dá)式值為真時添加.ng-enter和.ng-enter-active類(或者angular移除這個元素時添加.ng-leave和.ng-leave-active)。

我們將在.ng-enter類中構(gòu)建動畫,然后在.ng-enter-active類里面啟動它。相關(guān)的SCSS如下:

.game-overlay {    // ...    &.ng-enter {      @include transition(all 1000ms ease-in);      @include transform(translate(0, 100%));      opacity: 0;    }    &.ng-enter-active {      @include transform(translate(0, 0));      opacity: 1;    }    // ...  }

所有的SCSS都可以在文章最后的github鏈接中找到。

自定義場景

假如我們想要創(chuàng)建一個不同大小的游戲板。比如說,2048游戲原作是一個4x4的格子,那如果我們想要創(chuàng)建一個3x3或者6x6的游戲板呢?我們可以輕易地做到而不需要改動很多代碼。

游戲板本身被SCSS創(chuàng)建和放置,并且格子在.GridService中被管理。那樣,我們需要對這兩個地方做出修改來讓我們可以創(chuàng)建自定義的游戲板。

動態(tài) CSS

那好,我們不是真正需要用到動態(tài)CSS,而是創(chuàng)建一個我們真正需要的CSS類。我們能夠動態(tài)的創(chuàng)建DOM元素標(biāo)記,它允許動態(tài)地設(shè)置格子,而不是創(chuàng)建一個單獨的#game標(biāo)記。換句話說,我們創(chuàng)建一個3x3的游戲板,將它嵌套在一個ID為#game-3和ID為#game-6的DOM元素中。

我們能夠在已經(jīng)存在的動態(tài)SCSS外部創(chuàng)建一個混合類。通過簡單地找到#game這個樣式ID,然后將它封裝到mixin里面。例如:

@mixin game-board($tile-count: 4) {    $tile-size: ($width - $tile-padding * ($tile-count + 1)) / $tile-count;    #game-#{$tile-count} {       position: relative;      padding: $tile-padding;      cursor: default;      background: #bbaaa0;      // ...  }

現(xiàn)在我們可以包含這個game-board混合類來動態(tài)創(chuàng)建一個樣式表,它包含了多種版本的游戲板,每一種由它們相應(yīng)的#game-[n]標(biāo)記來區(qū)別。

為了構(gòu)建多版本的游戲板,我們可以簡單地迭代所有我們希望創(chuàng)建的游戲板,然后調(diào)用這個混合類。

$min-tile-count: 3;       // lowest tile count  $max-tile-count: 6;       // highest tile count  @for $i from $min-tile-count through $max-tile-count {    @include game-board($i);  }
動態(tài)GridService

現(xiàn)在我們有自己的CSS封裝類來創(chuàng)建多種大小的游戲板,我們需要修改我們的GridService,這樣我們能夠在程序啟動時設(shè)置方格的大小。

Angular 讓這個過程相當(dāng)容易。首先,我們需要讓我們的GridService成為一個provider,而不是一個直接的service。如果你不了解service和provider之間的差別,查看mg-book作深入的研究。簡單來說,一個provider允許我們在啟動前配置它。

另外,我們需要修改構(gòu)造函數(shù),在provider上設(shè)置為$get方法:

@mixin game-board($tile-count: 4) {    $tile-size: ($width - $tile-padding * ($tile-count + 1)) / $tile-count;    #game-#{$tile-count} {       position: relative;      padding: $tile-padding;      cursor: default;      background: #bbaaa0;      // ...  }

我們模塊上任何不在$get中方法在.config()函數(shù)中都可用。$get函數(shù)中的任何東西對于運行中的程度來說都是可用的,但在.config()里的就不可用。

這就是所有我們需要做的事來讓游戲板的大小成為動態(tài)的?,F(xiàn)在,讓我們創(chuàng)建一個6x6的游戲板,而不是默認(rèn)的4x4。在我們程式里的.config()函數(shù)中,我們能夠調(diào)用GridServiceProvider來設(shè)置大小:

angular  .module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard', 'ngAnimate', 'ngCookies'])  .config(function(GridServiceProvider) {    GridServiceProvider.setSize(4);  })

當(dāng)創(chuàng)建一個provider時,Angular動態(tài)地創(chuàng)建一個config-time模塊,它可以讓我們可以用:[serviceName]Provider為名字注入進(jìn)去。

演示地址

完整的例子地址如下: http://ng2048.github.io/.

結(jié)論

嘖嘖! 我們希望你享受整個使用Angular來創(chuàng)建2048游戲的過程. 在這個話題上有很多評論. 如果你也喜歡,請在下方留下評論. 如果你對Angular感興趣了, 看看我們的書Complete Book on AngularJS. 這本書涵蓋了所有你需要知道關(guān)于AngularJS的知識并且堅持不斷更新。

感謝

非常感謝 Gabriele Cirulli 發(fā)明了這了不起 (并且會上癮)的2048游戲以及 給這篇文章帶來的靈感。在這篇文章里面很多想法都是為圍繞著游戲本身以及如何構(gòu)建它來描述的。

完整的源碼

游戲完整的源碼可以從該地址獲取 http://d.pr/pNtX. 在本地構(gòu)建,只需要clone源碼并且運行

$ npm install   $ bower install  $ grunt serve

故障排除

如果你在構(gòu)建 npm install時候遇到麻煩, 先保證你有最新版本的node.js以及npm.

本文倉庫源碼測試運行在 nodev0.10.26 以及npm1.4.3.

這里有個好的方法去獲取一個最新的node版本是通過 n 節(jié)點版本管理:

$ sudo npm cache clean -f  $ sudo npm install -g n  $ sudo n stable

上述就是小編為大家分享的如何利用AngularJS開發(fā)2048游戲了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI