In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-03-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
In this issue, the editor will bring you about how to use AngularJS to develop 2048 games. The article is rich in content and analyzes and narrates it from a professional point of view. I hope you can get something after reading this article.
One of the questions I am frequently asked is when to use the Angular framework is a bad choice. My default answer is to write games, although Angular has its own event loop handling ($digest loop), and games usually require a lot of underlying DOM operations. If there is an Angular that can support many types of games, this is not accurate. Even if the game requires a lot of DOM operations, this may use the angular framework to handle static parts, such as recording the highest score and the game menu.
If you are as obsessed with the popular 2048 games as I am. The goal of the game is to add a square with a value of 2048 with the same value.
We will use AngularJS to create a copy from beginning to end and explain the whole process of creating app. Because this app is relatively complex, I also intend to use this article to describe how to create complex AngularJS applications.
This is the demo we are going to create.
Let's start now!
TL;DR: the source code for this app is also available for download, and there is a link to the app on github at the end of the article.
Step 1: plan app
The first step we need to do is to do a high-level design for the app to be created. Whether it's copying someone else's app or starting from scratch, this step has nothing to do with the size of the app.
Let's take a look at this game, we found a pile of tiles at the top of the game board. Each tile itself can be used as a location for other numbered tiles. Based on this fact, we can leave the task of moving tiles to CSS3 instead of relying on JavaScript, which needs to know where to move tiles. When there is a tile on the game panel, we simply need to make sure it is in the right place at the top.
Use CSS3 to layout, can bring us CSS animation effects, but also default to use AngularJS behavior to track the state of the game board, tiles and game logic.
Because we only have a single single page, we also need a single controller (single controller) to manage the page.
Because there is only one game board in the life cycle of the application, we contain all the grid logic in a single instance of the GridService service. Because the service is a singleton pattern object, this is an appropriate place to store the grid. We use GridService to handle tile replacement, move, and manage the grid.
Instead, put the logic and processing of the game into a service called GameManager. It will be responsible for the status of the game, handling movements, and maintaining scores (current and highest scores)
Finally, we need a component that allows us to manage the keyboard. We need a service called KeyboardService. In this blog post, we have implemented the application's processing of the desktop, and we can also reuse this service to manage touch operations and make it work on mobile devices.
Create app
To create an app, we first create a basic app (use the yeoman angular generator to generate the structure of the app, which is not necessary. We only use it as an entry point, and then quickly separate from its structure.) . Create an app directory to place the entire application. Use the test/ directory as a sibling of the app/ directory.
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.
Because we use the yeomanin tool in the application, we first need to make sure it is installed. Yeoman installation is based on NodeJS and npm. Installing NodeJS is not what this tutorial is about, but you can refer to the NodeJS.org site.
After installing npm, we can install the yeoman tool yo and the angular generator (which is called by yo to create the Angular app):
$npm install-g yo $npm install-g generator-angular
After installation, we can use the yeoman tool to generate our application, as follows:
$cd ~ / Development & & mkdir 2048$ yo angular twentyfourtyeight
The tool asks for some requests. We can all choose yes, and we don't need any dependencies other than angular-cookies as a dependency.
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 away to get away without using ruby and compass below.
Our angular module
We will create a scripts/app.js file to place our application. Start creating applications now:
Angular.module ('twentyfourtyeightApp', []) module structure
The structure used by layout angular applications is now recommended based on functions, not types. That is to say, without dividing the components into controllers, services, instructions, etc., we can define our module structure on a functional basis. For example, define a Game module and a Keyboard module in the application.
The module structure clearly separates the functional domains that match the file structure for us. This not only facilitates us to create large and flexible angular applications, but also facilitates us to share functions in app.
Finally, we set up a test environment to adapt to the file directory structure.
View
The easiest place to cut into the application is the view. Looking at the view itself, we find that there is only one view/template. In this application, there is no need for multiple views, so we create a single element to place the content of the application.
In our master file, app/index.html, we need to include all the dependencies (including angular.js itself and the JS file, that is, scripts/app.js), as follows:
2048 = 0 {@ for $I from 1 through $n {$ret: $ret * $x;}} @ else {@ for $I from $n to 0 {$ret: $ret / $x;} @ return $ret;} Block instruction
Because of SASS's tireless work, we can go back to our box instructions, display each square according to dynamic positioning, and allow CSS to work in the way it is designed, and then arrange the squares in order.
Because the tile directive is a container for custom views, we don't need to make it functional. We need to use the features that the element is responsible for displaying. Other than that, there are no other features to put in. The following code speaks for itself:
Angular.module ('Grid') .directive (' tile', function () {return {restrict: 'Aids, scope: {ngModel:' ='}, templateUrl: 'scripts/grid/tile.html'};})
Now, what's interesting about tile instructions is that if we present them dynamically. Using ngModel, a variable defined elsewhere, all of this is done in the template. Just as we saw earlier, it refers to the square object in our tiles array.
{{ngModel.value}}
With this basic instruction, we almost put it on the screen. All squares with x and y coordinates will automatically be assigned the corresponding .position-# {x}-# {y} class, and the browser will automatically place them in the desired location.
This means that our box object will need an xPowery and a value that is feasible for the instruction to run. Therefore, we need to create a new object for each square on the screen that is about to be laid out.
TileModel service
We will create an intelligent object that contains data and functional processing, rather than a normal object that cannot process information.
Because we want to take advantage of Angular's dependency injection, we will create a new service to manage our data model. We will create a TileModel service in the Grid module because it is only necessary to use a low-level TileModel when it comes to the game board.
Using the .factory method, we can simply create a new function as a workshop method. Unlike the service () function that assumes that the function we use to define the service is the service's builder, the factory () method takes the return value of the function as the service object. In this way, using the factory () method, we can inject any object into our Angular application as a service.
In our app/scripts/grid/grid.js file, we can create our TileModel workshop method:
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;}) / /.
Now, anywhere in our Angular application, we can inject the TileMode service and use it as a global object. It's pretty good, isn't it?
Don't forget to write test cases for the features we put in TileModel.
Our first square.
Now that we have the TileModel service, we can start putting instances of TileModel into the tiles array, and then they will magically appear in the right place in the grid.
Let's try to add some Tile instances to the tiles array in the GridService service:
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)); / /...}); the game board is ready
Now that we can put squares on the screen, we need to create a function in GridService that will prepare the game board for us. When we first load the page, we want to be able to create an empty game board. And you want to trigger the same action when the user clicks the "New Game" or "Try again" button in the game area.
To empty the game board, we will create a new function in GameService called buildEmptyGameBoard (). This method will be responsible for populating the grid and tiles arrays with null values.
Before we write the code, we write tests to make sure that the function buildEmptyGameBoard () is correct. As we mentioned above, we don't talk about the process, we only care about the results. The test can be like this:
/ / 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()这俩个函数有什么用呢? 回想一下我们上面讨论的,我们用到了一个一维数组来布局我们的方格。这从应用的性能和处理复杂动画来说都是一种更好的选择。我们将以接下来探讨动画。暂且看来,我们只是得益于利用了一维数组来代表多维数组的复杂性。 一维数组中的多维数组 我们如何在一个一维数组中表示一个多维数组?让我们看看没有颜色的网格表示的游戏板,和它们的格用值表示。在代码中,这个多维数组分解为数组的数组: 查看每个格的位置,当我们从单个数组角度看时,会看到一个模式出现: 我们可以看到第一个格,(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)];}}; / /.
Here, we can simply create a new TileModel instance and insert it into our this.tiles array.
.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];} / /.})
Now, because we use Angular, our squares will just magically appear as collages on the game board in our view.
"remember, the next step is to write tests to test our assumptions about functions. We found several bug when we wrote tests for this project, and you will also find it.
Keyboard interlock
All right, now we have our collage piece on the game board. The funny thing is that there's a game you can't play? Let's shift our attention to adding interaction to the game.
The purpose of this article is to focus only on board interaction and put touch operations aside. However, it should not be difficult to add touch actions, especially since we are only interested in sliding, which is provided by ngTouch. We don't care about the implementation.
The game itself is operated by using the arrow keys (or the afield wdirection s, d key). In our game, we want to allow users to simply interact with the game on the page. As opposed to requiring users to pay attention to the elements of the game board (or any other elements on the page, for this purpose). This will allow users to focus only on the document and the game interaction.
To allow this type of user interaction, add an event listener to the document. In Angular, we will "bind" our event listeners and the $document service provided by Angular. To handle defining user interactions, we will encapsulate our keyboard event bindings in a service. Remember, we only need a keyboard processor in the page, so a service is the best.
In addition, we also want to set a custom action to occur when we detect a user keyboard action. Using a service will allow us to naturally add it to our angular object and generate actions based on user input.
First, we create a new module (like the module-based development we did), which is called Keyboard in the app/scripts/keyboard/keyboard.js file (if it doesn't exist before, we need to create it).
/ / app/scripts/keyboard/keyboard.js angular.module ('Keyboard', [])
For any new JavaScript we create, we need to reference it in our index.heml file. The list of tags now looks like this:
And since we have created a new module, we will also need to tell our Angular module that we want to use this new module as a dependency for our own application:
.module ('twentyfourtyeightApp', [' Game', 'Grid',' Keyboard'])
The meaning behind this keyboard service is that we will bind the keydown event on $document to capture the user interaction from the document. In our angular object at the other end, we register the event handler so that it can be called when user interaction occurs.
Let's get started.
/ / 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) {};})
The init () function causes KeyboardService to start listening for keyboard events. We will filter out keyboard events that are not of interest.
For any event trigger that we are interested in, we will organize the default action to run and dispatch the event to our keyEventHandlers.
How do I know what events we are interested in? Because we are only interested in a limited number of keyboard events, we can check and confirm with one of our perceptual keyboard events.
When the arrow key is pressed, the document object receives an event with the key code of the pressed keyboard key.
We can create a mapping table of these events and then check the existence of keyboard actions in this concern mapping table.
/ / 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);}}) }; /.})
Whenever a key in keyboardMap triggers a keydown event, KeyboardService runs the this._handleKeyEvent function.
The full responsibility of this function is to call every key handler registered in each time processor. It will simply iterate through an array of key handlers, including keystroke events and original events, each called once:
/ /... 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服务 现在我们已经有能力观察来自用户的键盘事件, 我们需要在我们的应用启动时启动它. 因为我们是把它作为服务创建的,我们可以简单的在主控制器中做这些事情. 首先,我们将需要调用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()方法: 构建一个空的游戏面板d 设置开始位置 初始化游戏 我们已经在我们的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 }; }); 在我们的浏览器汇总加载好这个页面,我们将得到一个网格… 因为我们还没有定义任何移动功能,所有现在看起来还相当的令人乏味. 让你的游戏动起来 (游戏主循环) 现在让我们来深入研究一下我们游戏的实际功能是怎么实现的. 当用户按下任何方向键, 我们会调用GridService上的move()函数(我们曾在GameController里面创建了这个函数). 为了构建 move() 函数, 我们将需要定义游戏约束. 即,我们需要定义在每一个动作上我们的游戏将如何反应. 对于每一个动作,我们需要: 确定用户的方向键指示的向量. 为面板上的每一个小块找到其所有的最远可能位置。同时,拿下一个位置的方块比较看看我们是不是能够把它们合并. 对于每一个方块,我们将会想要确认是否有下一个带有相同值的方块存在. 如果该方块已经是合并后的结果了,那我们就把它认为是已经用过了的,并跳过它. 如果方块还没有合并过,那么我就要把它认为是可以合并的. 如果不存在下一个方块,那么我们就只要将方块移动到最远的位置上就行了. (这意味着是面板上的最远端). 如果存在下一个方块: 并且下一个方块的值是跟当前方块不同的值,那么我们就将方块平铺到最远的位置(下一个方块的位置是当前方块移动的边界). 并且下一个方块的值是跟当前方块相同的值,那么我们就找到了一个可能的合并. 现在我们已经把功能定义好了,我们就可以制定构建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上创建一个新的函数,以帮助我们找出所有可能的遍历位置. 为了找出方向,我们将需要挑选出用户按键所指示的向量. 例如,如果用户按下右方向键,那么将是想要往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;}; / /.
Now that our new traversalDirections () is defined, we can iterate through all the possible movements in the move () function. Going back to our GameManager, we will use these potential hairs to get the squares in the grid running.
/ /... 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});};}; / /.
Now in our location loop, we will iterate through all the possible locations and find the existing squares in the location. From here we will start to move forward like the second part of the function, finding all the further locations from the square:
/ /... / / For every position / / save the tile's original position var originalPosition = {xburex _ diary _ y}; var tile = GridService.getCellAt (originalPosition); if (tile) {/ / if we have a tile here var cell = GridService.calculateNextPosition (tile, key); / /.}
If we find a square, we will start looking for the farthest possible location from this square. To do this, we will step through the next location of the grid to check whether the next grid is within the boundary of the grid, and whether the location of the grid cell is empty (that is, there is no square yet).
If the grid cell is empty and within the boundary of the grid, then we will move on to the next grid cell and check the same conditions.
If one of these two conditions is not satisfied, then we may find the boundary of the grid, or find the next cell. We save the next location as a new location newPosition and get the next unit (whether it exists or not).
Since this process is designed to the grid, we will put this function in 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)};}
Now we can calculate the next possible location for our box, and we can check for potential mergers.
The merge is defined as a square that fits into another square with the same value. We will check to see if there is a square with the same value in the next location, and whether it has been merged before.
/ /... / / For every position / / save the tile's original position var originalPosition = {xburex _ diary _ 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} / /...}
Now, if the next location does not meet the condition, we can simply move the square from the current position to the next location (the else statement in the code).
This is one of the easier cases to deal with, all we have to do is move the square to the new location newPosition.
/ /... If (next & & next.value = = tile.value & &! next.merged) {/ / Handle merged} else {GridService.moveTile (tile, cell.newPosition);} move the square
As you might guess, the moveTile () method is the one most likely to be defined in GridService.
The mobile aspect is simply to update the position of the square in the array, and to update the TileModel.
As we have defined, there are two separate operations for two separate purposes. When we want to:
Move squares in an array
The GridService array maps the location of the square from the back end. The position of the square in the array is not bound to the location of the grid.
Update locations on TileModel
We will update the coordinates for the CSS where the square is placed at the front end.
In short: to keep track of the backend box, we will need to update the this.tilesarray in GridService and update the location of the box object.
MoveTile (), on the other hand, programs two simple steps:
/ / 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);}
Now we will need to define our tile.updatePosition () method. Method is not what it sounds like, it simply updates the x and y coordinates of the model itself
.factory ('TileModel', function () {/ /... Tile.prototype.updatePosition = function (newPos) {this.x = newPos.x; this.y = newPos.y;}; / /...})
Going back to our GridService, we can simply call .moveTile () to update the location above both the GridService.tiles array and the box itself.
Merge squares
Now that we have dealt with a simpler case, merging squares is the next problem we need to deal with. The merge is defined as follows:
Merging occurs when a square encounters another square with the same value at the next location.
When a square is merged, it moves the panel and updates the current game score and (if necessary) the highest score.
Merging requires the following steps:
Add a new square with the number of merges as its value at the final location
Remove old squares
Update the score of the game
Check to see if the winning square value is generated
Decomposed, the merge operation becomes some simple operations that need to be dealt with.
/ /... 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 {/ /.
Because we only want to support a separate square move per row (that is, if we have two possible merges, then only one merge will occur per row), we also need to keep track of the merged squares. We define the .marker logo for this purpose.
Before we drop our focus on this function, we use two undefined functions.
The GridService.newTile () method creates a new TileModel object. Our operation in GridService simply contains the location of the new box we created:
/ / GridService this.newTile = function (pos, value) {return new TileModel (pos, value);}; / /.
We'll go back to the self.updateScore () method for a little while. Now, we have enough information to know that it updates the game's score (as shown by the method name).
After the square moves
We only want to add new squares after making an effective move, so we will need to check to see if any movement from one square to another has actually taken place.
Var hasMoved = false; / /... HasMoved = true; / / we moved with a merge} else {GridService.moveTile (tile, cell.newPosition);} if (! GridService.samePositions (originalPos, cell.newPosition)) {hasMoved = true;} / /.
After all the squares have been moved (or trying to be moved), we will check to see if the game has been completed. If the game is actually over, we will set the self.win on the game.
We will move when we have a square collision, so in the case of merge, we will simply set hasMovedvariable to true.
Finally, we will check to see if any movement has occurred on the panel. If so, we will:
Add a new square to the panel
Check to see if we need to display the game end gameOver frame
If (! GridService.samePositions (originalPos, cell.newPosition)) {hasMoved = true;} if (hasMoved) {GridService.randomlyInsertNewTile (); if (self.win | |! self.movesAvailable ()) {self.gameOver = true;}} / /. Reset Square
Before we can run any of the main game loops, we will need to reset each square, for example, we no longer need to track their merge status. That is, every time we make a single move, we have to clear the previous state so that each square can move again. To do this, at the beginning of the moving loop, we will call:
GridService.prepareTiles ()
The prepareTiles () method in GridService simply iterates through all the squares and resets their states:
This.prepareTiles = function () {this.forEach (function) {if (tile) {tile.reset ();}});}; reserved score
Go back to the updateScore () method; the game itself needs to track two scores:
The score of the current game
The player's highest score in history
The current score currentScore is a simple variable and we will track it in the memory of each game. In other words, we don't need any special way to deal with it.
The highest score in history, highScore, is a variable that we will persist. We have several ways to deal with this problem, using local storage localstorage, cookies, or a combination of both.
Because cookie is the simplest of the two and the most secure when cross-browsers, we also set our highest score highScore to a cookie.
The easiest way to access cookie in Angular is to use the angular-cookies module.
To use this module, we will need to download it from angularjs.org or use a package manager, such as bower, to install it.
$bower install-save angular-cookies
As usual, we need to reference scripts in index.html and rely on ofngCookies. Com at the module level for settings on the application.
We will update our app/index.html as follows:
Now it's time to add the ngCookies module as a module-level dependency (on the Game module that we're going to reference cookie):
Angular.module ('Game', [' Grid', 'ngCookies'])
By setting up the dependency on ngCookies, we can inject $cookieStore service into our GameManager service. We can now get and set cookie on our users' browsers.
For example, to get the user's highest recent score, we will write a function to get it from the user's cookie for us:
This.getHighScore = function () {return parseInt ($cookieStore.get ('highScore')) | | 0;}
Going back to the updateScore () method on the GameManager class, we will update the local current score. If the current score is higher than our previous highest score, then we will update the cookie of the highest score.
This.updateScore = function (newScore) {this.currentScore = newScore; if (this.currentScore > this.getHighScore ()) {this.highScore = newScore; / / Set on the cookie $cookieStore.put ('highScore', newScopre);}}; solve the problem of tracking squares
Now we can make squares appear on the screen, but there is a problem on the screen, that is, some strange behavior will give us repeated squares. In addition, our squares will also appear in unexpected locations.
The reason for this problem is that Angular only knows that the square is given a unique identity and then placed in the square array. We set this unique identifier in the view as the $index of the square in the array (that is, its index, or position in the array). Because we move squares around in the array, $index can no longer track squares with unique identifiers. We need a different tracking scheme.
We will track the square through its own unique uuid, rather than relying on the array to identify the location of the square. Creating our own unique identity will ensure that angular can treat squares in an array of squares as their own unique objects. Angular will recognize the unique representation and treat the cube as its own object, as long as the unique uuid of the cube has not changed.
When we create a new entity, we can easily implement a unique identity scheme using TileModel. We can also come up with our own ideas to create unique logos.
As long as every TileModel entity we create is unique, it doesn't matter how we generate unique id.
To create a unique id, we jump to StackOverflow, find rfc4122-compliant, a global unique identity generator, and encapsulate the algorithm into a factory with a separate method 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) | 0; d = Math.floor (dcord 16) Return (c = ='x'? R: (r&0x7 | 0x8) .toString (16);}); return uuid;}; return {next: function () {return generateUid ();}};})
To use this GenerateUniqueId factory, we can inject it and call GenerateUniqueId.next () to create a new uuid. Back in our TileModel, we can create a unique id for the entity (in the constructor):
/ / 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;}; / /.})
Now that each of our squares has a unique identifier, we can tell Angular to track through this id instead of $index.
There is only one problem with this plan. Because we (explicitly) use null to initialize our array, and we use null to reset the array (instead of sort or resize), Angular regardless of null as an object to track. Because null values do not have a unique id, this will cause our browser to throw an error, and the browser will not be able to handle duplicate objects.
Therefore, we can use a built-in angular tool to track the unique id, as well as the $index position of the object (null-valued objects can be tracked with their position in the array, because there will only be one for each location). We can calculate the null object by modifying the view of the grid instruction as follows:
This problem can be solved by relying on a different implementation of the data architecture, such as finding the location of each TileModel in an iterator, rather than relying on the index of square data, or reassembling the array every time it changes (or executes $digest ()). For simplicity, we have implemented it in an array, and this is the only side effect that we need to deal with this implementation.
Did we win? The game is over.
When we lose 2048 of the original game, a prompt box for the end of the game slides into the screen, which allows us to restart the game and follow the creator of the game on twitter. This is not only a cool effect for players, it also introduces a good way to interrupt the operation of the game.
We can easily create this effect with some basic native angular techniques. We have used the gamOver variable in GameManager to record whether the game is over. We can create a tag to contain the end of the game prompt box and position it in absolute coordinates in the game box. The magic of this technology (and Angular) is that it can be done simply, without any tricks:
We can simply create an element to contain the message of the end or victory of the game and present it according to the state of the game. For example, the game end prompt box looks like this:
Game over Try again
The hard part is dealing with styles. In a more efficient way, we just put the elements in an absolute position in the game box, and then the browser does the layout. This is part of the style (note that the finished CSS style can be found in the github link below):
.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;}}
We can implement the prompt box for victory in the same way, simply by creating a. Game-overlay element that represents victory.
Animation
One of the impressive things about the 2048 original game is that the box seems to magically slide from one position to the next, and the prompt box at the end of the game or victory naturally appears on the screen. When we use Angular, we can achieve almost exactly the same effect for free (thanks to CSS)
In fact, we have built a game so that we can easily create animation effects such as sliding, rendering, presentation, and so on. We (almost) don't use JavaScript to implement them.
Animate CSS positioning (that is, add square slides)
When we use the position- [x]-[y] class to locate the box through CSS, once a new location is set on the box, the DOM element will add a new class position- [newX]-[newY] and remove the old class position- [oldX]-[oldY]. In this case, we can simply define that the default sliding action occurs in the CSS class itself by defining a CSS transition on the .tile class.
The relevant SCSS is as follows:
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;}
With the CSS transition defined, the slider can now easily slide between one position and the new one. Yes, it's really that simple. )
Let the end picture move.
Now, let's have some fun in animation and try the ng-Animate module. This is an out-of-the-box module of the angular framework.
Before you can write code, you need to install ng-Animate. There are two ways, one is to download directly from angularjs.org, and the other is to install it with a package manager such as bower.
$bower install-save angular-animate
As usual, the script is referenced in our HTML file so that the browser can load the module. Modify the index.html file to load the angular-animate.js:
Like any other angular module, we need to tell the angular framework that our module depends on angular-animate. Simply modify the dependent array of the app/app.js file:
Angular .module ('twentyfourtyeightApp', [' Game', 'Grid',' Keyboard', 'ngAnimate',' ngCookies']) / /. NgAnimate module
Although an in-depth discussion of ngAnimate is beyond the scope of this article (see ng-book to learn more about how it works), we simply understand how it works so that we can animate our game.
NgAnimate as a stand-alone module, any time angular adds a new object to a related instruction (to our game), it will be given a CSS class (free). We can use these classes to animate different components of our game:
Command to enter the class and leave the class ng-repeatng-enterng-leaveng-ifng-enterng-leaveng-class [className]-add [classname]-remove
When an element is added to the scope of the ng-repeat command, the new DOM element is automatically appended to the ng-enter CSS class. Then, when it is actually added to the view, the CSS class ng-enter-active will be added. This is important because it will allow us to build the animation we want in the ng-enter class and style the animation in the ng-enter-active class. This function is the same as ng-leave does when an element is removed from the ng-repeat iteration instruction.
When a new CSS class is added (or removed) from a DOM element, the corresponding CSS classes [classname]-add and [classname]-add-active are added to the DOM element. Here we once again animate our CSS in the corresponding class.
Let the prompt screen of the end of the game move.
We can use the ng-enter class to animate the prompt screen when the game ends or wins. Remember, the. game-overlay class is hidden and needs to be displayed with the ng-if instruction. When the ng-if condition changes, ngAnimate will add the. ng-enter and. Ng-enter-active classes when the expression value is true (or. Ng-leave and. Ng-leave-active when angular removes this element).
We will build the animation in the. ng-enter class, and then start it in the. ng-enter-active class The relevant SCSS is as follows:
.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;} / /.}
All SCSS can be found in the github link at the end of the article.
Customize the scen
Suppose we want to create a game board of different sizes. For example, the original 2048 game is a 4x4 grid, what if we want to create a 3x3 or 6x6 game board? We can do this easily without changing a lot of code.
The game board itself is created and placed by SCSS, and the grid is managed in .GridService. In that case, we need to make changes to these two places so that we can create a custom game board.
Dynamic CSS
Well, we don't really need to use dynamic CSS, but to create a CSS class that we really need. We can create DOM element tags dynamically, which allows you to set the grid dynamically instead of creating a separate # game tag. In other words, we create a 3x3 board and nest it in a DOM element with an ID of # game-3 and an ID of # game-6.
We can create a hybrid class outside of the existing dynamic SCSS. Simply find the style ID of # game and encapsulate it in mixin. For example:
@ 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; / /.}
Now we can include this game-board hybrid class to dynamically create a stylesheet that contains multiple versions of the game board, each distinguished by their corresponding # game- [n] tag.
In order to build multiple versions of the game board, we can simply iterate through all the game boards we want to create, and then call the mixed class.
$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);} dynamic GridService
Now that we have our own CSS wrapper class to create game boards of various sizes, we need to modify our GridService so that we can set the size of the box when the program starts.
Angular makes this process quite easy. First, we need to make our GridService a provider rather than a direct service. If you don't understand the difference between service and provider, check out mg-book for in-depth research. To put it simply, a provider allows us to configure it before startup.
In addition, we need to modify the constructor to set it to the $get method on provider:
@ 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; / /.}
Any method on our module that is not in $get is available in the .config () function. Anything in the $get function is available for the degree of running, but not in .config ().
This is all we need to do to make the size of the game board dynamic. Now, let's create a 6x6 game board instead of the default 4x4. In the .config () function in our program, we can call GridServiceProvider to set the size:
Angular .module ('twentyfourtyeightApp', [' Game', 'Grid',' Keyboard', 'ngAnimate',' ngCookies']) .config (function (GridServiceProvider) {GridServiceProvider.setSize (4);})
When creating a provider, Angular dynamically creates a config-time module that allows us to inject names with: [serviceName] Provider.
Demo address
The complete example address is as follows: http://ng2048.github.io/.
Conclusion
Tut-tut! We hope you enjoy the whole process of using Angular to create 2048 games. There are a lot of comments on this topic. If you like it, please leave a comment below. If you are interested in Angular, check out our book Complete Book on AngularJS. This book covers everything you need to know about AngularJS and keeps it up to date.
Thank you.
Thank you very much to Gabriele Cirulli for inventing this amazing (and addictive) 2048 game and the inspiration for this article. Many of the ideas in this article are described around the game itself and how to build it.
Complete source code
The complete source code of the game can be obtained http://d.pr/pNtX. from this address. Build locally, only need the clone source code and run
Troubleshooting $npm install $bower install $grunt serve
If you have trouble building npm install, make sure you have the latest version of node.js and npm.
In this paper, the warehouse source code test runs on nodev0.10.26 and npm1.4.3.
Here is a good way to get the latest node version through n-node version management:
$sudo npm cache clean-f $sudo npm install-g n $sudo n stable above is the editor's share of how to develop 2048 games using AngularJS. If you happen to have similar doubts, please refer to the above analysis to understand. If you want to know more about it, you are welcome to follow the industry information channel.
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.