91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

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

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

如何利用AngularJS開發2048游戲

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

這期內容當中小編將會給大家帶來有關如何利用AngularJS開發2048游戲,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

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

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

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

這是我們要創建的 demo .

現在開始吧!

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

第一步:規劃app

如何利用AngularJS開發2048游戲

第一步我們要做的,就是給要創建的app做高層設計。無論是山寨別人的app,還是自己從零做起,這一步都與app的規模無關。

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

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

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

而把游戲的邏輯和處理放到一個叫做GameManager的服務中。它將負責游戲的狀態,處理移動,維護分數(當前分數和最高分)

最后,我們需要一個允許我們管理鍵盤的組件。我們需要一個叫做KeyboardService的服務。在這篇博文中,實現了應用對桌面的處理,我們也可以復用這個服務來管理觸摸操作讓它在移動設備上運轉。

創建app

如何利用AngularJS開發2048游戲

為了創建app,我們先創建一個基本的 app (使用 yeoman angular 生成器生成app的結構, 這一步不是必須的. 我們只把它作為切入點,之后就迅速地從它的結構上分開。).創建一個app目錄用來放置整個應用。把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.

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

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

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

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

$ 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 模塊

我們將創建scripts/app.js文件來放置我們的應用。現在就開始創建應用吧:

angular.module('twentyfourtyeightApp', [])

模塊結構

如何利用AngularJS開發2048游戲

布局angular應用使用的結構現在是根據函數推薦的,而不是類型。這就是說,不用把組件分成控制器,服務,指令等,就可以在函數基礎上定義我們的模塊結構。例如,在應用中定義一個Game模塊和一個Keyboard模塊。

模塊結構清晰地為我們分離出匹配文件結構的職能域。這不僅方便我們創建大型,靈活性強的angular應用,也方便我們共享app中的函數。

最后,我們搭建測試環境適應文件目錄結構。

視圖

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

在我們的主文件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文件集,我們需要在應用視圖層面上,詳細地處理app/views/main.html中的視圖。當需要在應用中導入一個新
資源時,我們需要修改index.html文件。

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

<!-- 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. 游戲靜態頭

  2. 當前游戲分數和本地用戶最高分

  3. 游戲板

游戲靜態頭可以這樣來完成:

<!-- 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>

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

GameController

現在我們有了一個合理的項目結構,現在來創建GameController來放置我們要在視圖中顯示的值。在app/script/app.js中,我們可以在主模塊twentyfourtyeight.App中創建控制器:

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

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

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

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

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

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

Game

既然我們有視圖間部分相互連接了,那么就可以開始編寫游戲背后的邏輯了。為創建一個新游戲模塊,我們在app/scripts/目錄中把我們的模塊創建為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,使它能處理游戲的狀態,用戶可以移動的不同方法,記錄分數以及決定游戲何時結束和用戶是否打破最高分以及用戶是否輸局了。

開始開發應用時,我們喜歡為我們用到的方法編寫stub方法,并編寫測試代碼然后填入要實現的地方。

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. 處理游戲循環/移動操作

  3. 更新分數

  4. 跟蹤游戲是否結束

有了這些特性,我們可以創建GameManager服務的基本大綱,我們就可以對它進行測試代碼的編寫:

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() {};  });

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

測試驅動開發 (TDD)

如何利用AngularJS開發2048游戲

開始實現測試前,需要使用karma驅動測試。如果你對karma不熟悉,就把它當做一個測試runner,它允許我們在終端和代碼中舒適高效地進行前臺的自動化測試。
要使用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的詳細選項), 但是關鍵的部分還是要知道的,即設置Karma使它在測試中加載所有我們感興趣的文件。

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

$ karma init karma.conf.js

該命令會詢問一些請求并創建karma.conf.js文件。從這里起,我們將改變兩個配置選項:files數組和要打開的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編寫基本的測試。因為我們還不知道應用的全部功能,我們只能進行有限的測試

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.

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

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

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.

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

// ...  // 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()函數,它用來驗證是否有剩下可用的方塊以及驗證有沒有可能的合并。因為它是游戲是否結束的條件,我們把這個方法放到GameManager,但是在GridService中實現大部分功能,GridService將在下一步創建。

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

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

  2. 有可匹配的位置

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

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

Mock the GridService

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

用mocked方法創建一個fake對象,然后通過$provide服務處理它們并告訴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);  }));  // ...

現在我們可以使用這個fake _gridService實例來建立我們的條件。
我們要確保當有可用的方塊時,movesAvailable()函數返回true.現在就在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();    });    // ...

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

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

// ...  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();  });  // ...

我們已經奠定基礎了,現在在實現期望的功能前可以編寫測試樣例了。

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

現在我們來實現movesAvailable函數. 我們已經測試代碼可以運行, 并且明確了執行的條件, 這個函數實現起來就簡單了.

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

打造game grid

GameManager已經準備妥當, 我們接下來就要創建GridService來管理游戲板.

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

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

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

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

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

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

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

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

深入理解自定義指令可以參考: custom directives  ,或者查看 ng-book里面關于指令的內容
angular.module('Grid')  .directive('grid', function() {    return {      restrict: 'A',      require: 'ngModel',      scope: {        ngModel: '='     },      templateUrl: 'scripts/grid/grid.html'   };  });
該指令的主要功能是建立網格視圖,所以我們不需要在指令里面使用自定義邏輯。
<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數組,并且分別使用$index來跟蹤遍歷的結果。

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

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

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

Enter SCSS

針對這個項目,我們使用SASS的一個現代變體:scss。scss不僅是一個更強大的CSS,我們將會以動態的方式來構建我們的CSS。

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

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

CSS3 transform 屬性

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

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

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

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

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

使用這個轉換屬性,我們能夠簡單地通過給我們的方塊應用一個CSS類標記在游戲板上移動它們。現在,微秒的地方就是我們怎樣來構建我們動態的類,如此,當我們在頁面上定點時,它們使用CSS類來對應一個合適的方格?

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

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

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

我們可以讓SCSS幫我們動態的計算這三個變量的位置。首先,我們需要計算每一個方塊的面積。這對SCSS變量來說是非常容易的:

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

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

我們在這里只包含了與scss相關的部分。剩下的代碼可以在文章最后提供的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設置更高的z-index值。否則,瀏覽器會將它們置于同等高度,這樣看上去就不美觀了。

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

我們將迭代所有的方塊,然后基于我們計算的預期偏移值來動態地創建每一個類:

.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來規避這個問題。

現在我們已經創建了.position-#{x}-#{y}這個CSS標記類,可以將我們的方塊布局到屏幕上了。

如何利用AngularJS開發2048游戲

為不同的方塊的著色

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

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

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

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

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

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

@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的不懈的工作,我們可以回到我們的方塊指令,根據動態定位來展示每一個方塊,并且允許CSS能夠以它被設計的方式來工作,然后依序排列這些方塊。

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

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

現在,tile指令有意思的地方在于我們如果動態呈現。使用ngModel這個在其它地方定義的變量,所有這些事都在模板中被搞定了。正好我們前面看到的一樣,它引用了我們tiles數組中的方塊對象。

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

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

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

TileModel服務

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

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

使用.factory這個方法,我們可以簡單地新建一個函數,將之作為一個工場方法。不像service()這個函數假定我們使用來定義服務的函數就是那個服務的構建函數,factory()方法將函數的返回值作為服務對象。這樣,使用factory()方法我們能夠將任何對象作為一個服務來注入到我們的Angular應用當中。

在我們的app/scripts/grid/grid.js這個文件中,我們可以創建我們的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;  })  // ...

現在,在我們Angular應用中的任何地方,我們可以注入TileMode服務,并將它作為一個全局對象來使用。相當棒,對不對?

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

我們的第一個方格

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

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

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));    // ...  });

游戲板準備完畢

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

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

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

// 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);    });  });

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

這個函數很簡單,代碼已經充分解釋了它的作用。在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);      });    };    // ...

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

// 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()這倆個函數有什么用呢?

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

一維數組中的多維數組

我們如何在一個一維數組中表示一個多維數組?讓我們看看沒有顏色的網格表示的游戲板,和它們的格用值表示。在代碼中,這個多維數組分解為數組的數組:

如何利用AngularJS開發2048游戲如何利用AngularJS開發2048游戲

查看每個格的位置,當我們從單個數組角度看時,會看到一個模式出現:

如何利用AngularJS開發2048游戲

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

推算出位置之間的關系,我們可以看出方程中出現兩個位置之間的關系。

i = x + ny

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

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

// 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;  };

最初的游戲者位置

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

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

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

.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;    };    // ...

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

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

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

.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];    }    // ...  });

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

”記住,下一步要做的是寫測試來測試我們關于函數的假設。我們在為這個項目寫測試時發現幾個bug,你也會發現。

鍵盤互鎖

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

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

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

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

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

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

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

對于我們創建的任何新的 JavaScript,我們需要在我們的 index.heml 文件中引用。現在的 <script> 標簽列表看起來像這樣:

<!-- 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>

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

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

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

讓我們開始吧.

// 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() 函數會讓 KeyboardService 開始偵聽鍵盤事件. 我們將會過濾掉不感興趣的鍵盤事件.

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

如何利用AngularJS開發2048游戲

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

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

我們可以創建一個這些事件的映射表,然后檢查鍵盤動作在這個關注映射表中的存在.

// 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中的按鍵觸發了 keydown 事件,  KeyboardService 都會運行 this._handleKeyEvent 函數.

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

// ...  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);      }    }  };  // ...

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

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

使用Keyboard服務

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

如何利用AngularJS開發2048游戲

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

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

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

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

現在我們就可以吧 KeyboardService 注入到我們的 GameController 并在發生用戶交互時啟動. 首先是 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()方法, 很快我們就會充實它.

當我們把新游戲創建好,我們會調用 startGame(). startGame() 函數將會設置鍵盤服務事件處理器:

.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();  });

按下開始按鈕
 

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

  1. 構建一個空的游戲面板d

  2. 設置開始位置

  3. 初始化游戲

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

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

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    };  });

在我們的瀏覽器匯總加載好這個頁面,我們將得到一個網格&hellip; 因為我們還沒有定義任何移動功能,所有現在看起來還相當的令人乏味.

如何利用AngularJS開發2048游戲

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

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

為了構建 move() 函數, 我們將需要定義游戲約束. 即,我們需要定義在每一個動作上我們的游戲將如何反應.

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

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

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

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

    1. 如果該方塊已經是合并后的結果了,那我們就把它認為是已經用過了的,并跳過它.

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

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

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

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

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

現在我們已經把功能定義好了,我們就可以制定構建move()函數的策略了.

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; }    };    // ...  });

對于移動有幾個條件需要考慮:如果游戲結束了,并且我們已經以某種方式結束了游戲循環,我們將簡單的返回并繼續循環.

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

如何利用AngularJS開發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 }};

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

.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;    };    // ...

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

// ...  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      });    });  };  // ...

現在在我們的位置循環中,我們將遍歷所有可能位置,并找出位置中現有的方塊。從這里開始我們將開始像功能的第二部分進發,找出從方塊出發所有更遠處的位置:

// ...  // 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開發2048游戲

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

如果這個網格單元是空的并且在網格的邊界之內,那么我們將繼續轉移到下一個網格單元并檢查同樣的條件.

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

由于這個過程設計到網格,所以我們將把這個函數放到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)    };  };

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

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

// ...  // 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    }    // ...  }

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

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

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

移動方塊

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

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

如我們已經定義的,有兩個單獨的操作用于分開的兩個目的. 當我們要:

移動數組中的方塊

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

更新TileModel上的位置

我們會為前端放置方塊的CSS更新坐標.

簡而言之:為了保持對后端方塊的跟蹤,我們將需要更新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);  };

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

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

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

合并方塊

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

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

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

合并需要下面這幾步:

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

  2. 移除舊的方塊

  3. 更新游戲的得分

  4. 檢查是否產生了獲勝的方塊值

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

// ...  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 {  // ...

因為我們只想支持每行一個單獨的方塊移動(那就是如果我們有兩個可能的合并,那么每行只會有一個合并會發生), 我們還需要保持對已經合并的方塊的跟蹤. 我們為此定義了.merged標識.

在我們放下對這個函數的關注之前,我們使用了兩個還有沒有定義好的函數.

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

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

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

方塊移動之后

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

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

在所有的方塊都已經移動(或者嘗試著要被移動)之后,我們將檢查游戲是否已經被完成。如果游戲實際上已經結束了,我們就將設置游戲上的self.win.

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

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

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

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

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

重設方塊

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

GridService.prepareTiles();

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

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

保留分值

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

  1. 當前游戲的得分

  2. 玩家的歷史最高分

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

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

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

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

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

$ bower install --save angular-cookies

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

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

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

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

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

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

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

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

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

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

解決對方塊的跟蹤問題

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

這個問題的原因是Angular只知道方塊是被賦予了一個唯一的標識,然后被放在方塊數組中的. 我們在視圖中設置了這個唯一的標識符,作為數組中方塊的 $index(也就是它在數組中的索引,或者說位置). 因為我們會在數組中到處移動方塊,所以$index不再能夠對具有唯一標識的方塊進行跟蹤. 我們需要一個不同的跟蹤方案.

<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來對其進行跟蹤,而不是依賴于數組來識別方塊的位置. 創建我們自己的唯一標識將確保angular可以講方塊數組中的方塊作為它們自己的唯一對象進行對待. Angular 將會把識別唯一的表示,并把方塊看做是它自身的對象, 只要保證方塊唯一的uuid沒有發生變化就行.

當我們創建一個新的實體是,我們就能使用TileModel很容易的實現一個唯一的標識方案. 我們也可以想出我們自己的創意來創建唯一的標識.

只要我們創建的每一個 TileModel 實體都是唯一的,我們如何生成唯一性id都無所謂.

為了創建一個唯一的id,我們跳轉到 StackOverflow, 找到 rfc4122-compliant,一個全局的唯一標識生成器,并用一個單獨的方法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 工廠, 我們可以將它注入,并調用 GenerateUniqueId.next() 來創建新的uuid. 回到我們的 TileModel, 我們可以為(構造器中的)實體創建一個唯一的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;    };    // ...  });

現在我們的每一個方塊都有了一個唯一的標識符, 我們可以告訴Angular通過這個id而不是 $index進行跟蹤.

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

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

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

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

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

我們贏了嗎?!??游戲結束了

當我們輸掉2048原作游戲時,一個游戲結束的提示框滑入屏幕,它允許我們重新開始游戲,并且在twitter上關注游戲的創建者。這不光是一個呈現給玩家的炫酷效果,它還介紹了一種中斷游戲運行的好方法。

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

我們可以簡單地創建一個<div>元素來包含游戲結束或勝利時的消息,并且根據游戲的狀態呈現出來。舉個例子,游戲結束提示框像這樣:

<!-- ... --> <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鏈接中找到)相關的一部分:

.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;    }  }

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

動畫

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

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

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

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

相關的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過渡,滑塊現在可以輕松地在一個位置和新位置之間滑動了。(是的,真的就是如此簡單。)

讓結束畫面動起來

現在,讓我們在動畫上找些 樂子,試試 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 文件的依賴數組即可:

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

ngAnimate模塊

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

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

命令進入類離開類
ng-repeatng-enterng-leave
ng-ifng-enterng-leave
ng-class[className]-add[className]-remove

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

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

讓游戲結束的提示畫面動起來

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

我們將在.ng-enter類中構建動畫,然后在.ng-enter-active類里面啟動它。相關的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鏈接中找到。

自定義場景

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

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

動態 CSS

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

我們能夠在已經存在的動態SCSS外部創建一個混合類。通過簡單地找到#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;      // ...  }

現在我們可以包含這個game-board混合類來動態創建一個樣式表,它包含了多種版本的游戲板,每一種由它們相應的#game-[n]標記來區別。

為了構建多版本的游戲板,我們可以簡單地迭代所有我們希望創建的游戲板,然后調用這個混合類。

$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);  }
動態GridService

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

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

另外,我們需要修改構造函數,在provider上設置為$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()函數中都可用。$get函數中的任何東西對于運行中的程度來說都是可用的,但在.config()里的就不可用。

這就是所有我們需要做的事來讓游戲板的大小成為動態的。現在,讓我們創建一個6x6的游戲板,而不是默認的4x4。在我們程式里的.config()函數中,我們能夠調用GridServiceProvider來設置大小:

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

當創建一個provider時,Angular動態地創建一個config-time模塊,它可以讓我們可以用:[serviceName]Provider為名字注入進去。

演示地址

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

結論

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

感謝

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

完整的源碼

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

$ npm install   $ bower install  $ grunt serve

故障排除

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

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

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

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

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

大冶市| 惠州市| 武威市| 金寨县| 钟山县| 定南县| 清徐县| 济源市| 绥德县| 永春县| 铜梁县| 内丘县| 宝清县| 雅江县| 额济纳旗| 托克逊县| 富裕县| 东台市| 兴化市| 淮安市| 文水县| 厦门市| 正镶白旗| 涿州市| 德钦县| 安岳县| 凭祥市| 龙游县| 彩票| 普定县| 长垣县| 彭泽县| 韩城市| 新宁县| 杭锦旗| 合山市| 花莲县| 延津县| 钦州市| 玛多县| 和林格尔县|