[Перевод] Делаем игру 2048 на AngularJS
Наверное, вам, как и многим коллегам, пришлась по вкусу игра »2048», в которой необходимо достичь плитки с числом 2048, собирая вместе плитки с одинаковыми числами.В этой статье мы вместе построим клон этой игры при помощи фреймворка AngularJS. По ссылке можно посмотреть демонстрацию конечного результата.
Первые шаги: планируем приложениеПервый шаг — разработка высокоуровневого дизайна приложения. Мы делаем это для любых приложений, неважно, клонируем ли существующее приложение или пишем своё с нуля.
В игре есть игровое поле с набором клеток. Каждая из клеток — место для расположения плитки с числом. Этим можно воспользоваться и переложить ответственность за размещение плиток на CSS3, а не делать это в скрипте. Когда плитка находится на игровом поле, мы просто должны убедиться, что она находится на нужном месте.
Использование CSS3 позволяет нам оставить анимацию для CSS, и использовать поведение AngularJS по умолчанию для отслеживания состояния доски, плиток и логики игры. Так как у нас одна страница, нам понадобится один контроллер.
Так как у нас одно игровое поле, вся логика сетки будет хранится в одном экземпляре сервиса GridService. Сервисы — это синглтоны, поэтому в них удобно хранить сетку. GridService будет размещать плитки, двигать их, отслеживать состояние сетки.
Игровая логика будет хранится и обрабатываться в другом сервисе под названием GameManager. Он будет в ответе за управление состоянием игры, обработку ходов и хранение очков (текущего достижения и таблицы рекордов).
И наконец нам понадобится компонент для обслуживания клавиатуры. Это будет сервис KeyboardService. В этой статье мы реализуем работу десктопного приложения, но его можно будет легко переделать для сенсорных экранов.
Построение приложения Создадим простое приложение (мы использовали генератор приоложений yeoman, но это необязательно). Создадим каталог приложения, в котором оно будет хранится. Каталог test/ будет находится рядом с каталогом app/.
Следующие инструкции относятся к настройке проекта через yeoman. Если вам удобнее делать это вручную, их можно пропустить.
Сначала убедимся, что yeoman установлен. Для этого должны быть установлены NodeJS и npm. После этого нужно установить утилиту yeoman под названием yo, и генератор для angular (который будет использован утилитой для создания приложения):
$ npm install -g yo $ npm install -g generator-angular После этого можно создавать приложение через утилиту yo:
$ cd ~/Development && mkdir 2048 $ yo angular twentyfourtyeight Нужно будет утвердительно ответить на все вопросы, кроме «select the angular-cookies as a dependency», поскольку нам они не нужны.
Наш модуль angular Создадим файл приложения scripts/app.js. Начнём приложение: angular.module ('twentyfourtyeightApp', []) Модульная структура Рекомендуется строить структуру приложения по функционалу, а не по типам. То есть, не разделять компоненты, как принято, по контроллерам, сервисам, директивам. К примеру, в нашем приложении мы определим модуль Game и модуль Keyboard.
Модульная структура даёт нам чёткое разделение ответственности, которое совпадает с файловой структурой. Это помогает не только строить большие и сложные приложения, но и делиться функционалом внутри самого приложения. Позже мы настроим окружение для тестирования, совпадающее со структурой каталога.
Вид Проще всего начать приложение с Вида. В данном случае у нас есть только один вид/шаблон. Поэтому мы создадим единственный
В файл app/views/main.html поместим все виды, относящиеся к игре. Через синтаксис controllerAs можно задать, где будут находится данные по нашему $scope, и какой контроллер отвечает за какой из компонентов.
1. Статический заголовок игры2. Текущий счёт и таблицу рекордов3. Игровое поле
Статический заголовок:
ng-2048
GameController Определив структуру приложения, можно создать GameController для хранения величин, которые появятся в Виде. Внутри app/scripts/app.js создадим контроллер на главном модуле twentyfourtyeightApp: angular .module ('twentyfourtyeightApp', []) .controller ('GameController', function () { }); В Виде мы упоминали объект game, который будет управляться контроллером GameController. Этот игровой объект будет создан в новом модуле, и будет содержать все необходимые ссылки на игру. Но пока его нет, приложение не запустится. В контроллере мы можем добавить зависимость от GameManager:
.controller ('GameController', function (GameManager) { this.game = GameManager; }); Помните, что мы создаём зависимость уровня модулей, поэтому, чтобы убедиться, что она загрузится приложением, её надо внести в список зависимостей модуля Angular. Чтобы сделать модуль Game зависимостью для twentyfourtyeightApp, его надо упомянуть в массиве, в котором мы определяем модуль.
Целиком файл app/scripts/app.js выглядит так:
angular .module ('twentyfourtyeightApp', ['Game']) .controller ('GameController', function (GameManager) { this.game = GameManager; }); Game Мы почти подключили Вид, теперь можем заняться и логикой игры. Создадим модуль игры как app/scripts/game/game.js: angular.module ('Game', []); Лучше создавать модуль в своём собственном каталоге с именем, совпадающим с именем модуля. Наш модуль Game обеспечивает один основной компонент: GameManager.
GameManager будет отвечать за хранение состояния игры, возможные ходы, текущий счёт, определение окончания игры и её результат. Я рекомендую test driven development, когда сначала пишется заглушка для нужного модуля, затем пишется тест для него, и потом уже заполняются пустые места.
В целях обучения мы подробно пройдём этот путь для текущего модуля, а в дальнейшем я просто упомяну, для каких компонентов необходимо написать тесты.
Итак, функции GameManager:
1. Создание игры2. Обновление счёта3. Отслеживание, не закончилась ли игра
Основная структура сервиса, для которой можно будет написать тесты, будет такой:
angular.module ('Game', []) .service ('GameManager', function () { // Создать новую игру this.newGame = function () {}; // Обработка хода this.move = function () {}; // Обновление очков this.updateScore = function (newScore) {}; // Остались ли ещё ходы? this.movesAvailable = function () {}; }); Test Driven Development (TDD) Перед созданием тестов необходимо установить модуль karma. Это окружение для запуска тестов, позволяющее автоматизировать тесты фронтенда, запуская их из терминала и кода.
Для установки наберите:
$ npm install -g karma Если вы создавали предложение через yeoman, то следующую часть можно пропустить.
Для использования karma нужно создать файл с настройками. Основная часть процесса настройки — загрузить все файлы, которые нужно тестировать.
Для создания файла конфигурации нужно выполнить команду
$ karma init karma.conf.js Затем отредактировать массив файлов и включить опцию 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.conf.js Пишем первые тесты Напишем тест проверки на оставшиеся ходы. В новом файле test/unit/game/game_spec.js запишем: describe ('Game module', function () { describe ('GameManager', function () { // Инъекция модуля Game в тест beforeEach (module ('Game'));
// Ниже пойдут наши тесты }); }); В этом тесте используется синтаксис Jasmine.jasmine.github.io/2.0/introduction.html
Как и для других юнит-тестов, нам надо создать экземпляр GameManager. Обычным способом для тестов является иньекция его в тест:
// … // Инъекция модуля Game в тест beforeEach (module ('Game'));
var gameManager; // instance of the GameManager beforeEach (inject (function (GameManager) { gameManager = GameManager; });
// … С этим экземпляром gameManager можно настроить ожидания нашей функции movesAvailable (). Определим её как метод, который проверяет, есть ли ещё пустые квадратики, а также возможны ли ещё какие-либо объединения плиток. Поскольку это является условием для окончания игры, мы оставим этот метод в GameManager, а все подробности реализуем в GridService, который мы создадим чуть позже.
Если на доске остались ходы, это значит, что:
1. Есть пустые места2. Есть возможности для объединения плиток
Идея в том, чтобы писать тесты этих условий так, что мы создаём необходимое условие и проверяем реакцию кода на него. Поскольку мы будем полагаться на отчёты о состоянии игрового поля только от GridService, мы сымитируем состояние, и проверим, что логика GameManager работает правильно.
Имитация GridService Для этого мы переопределим поведение Angular по умолчанию и вместо настоящего сервиса подсунем имитацию, что позволит нам контролировать условия. Для этого мы создадим ложный объект с имитирующими методами, и затем скажем Angular, что этот объект — настоящий, подменив его в сервисе $provide. // … var _gridService; beforeEach (module (function ($provide) { _gridService = { anyCellsAvailable: angular.noop, tileMatchesAvailable: angular.noop };
// Подменим настоящий GridService // ненастоящей версией $provide.value ('GridService', _gridService); })); // … Теперь поддельный _gridService можно использовать, чтобы задать нужные условия. Убедимся, что функция movesAvailable () выдаёт true, когда есть свободные ячейки. Сымитируем метод anyCellsAvailable () (который ещё не написан) в сервисе GridService. Метод должен сообщать о свободных ячейках в 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. Надо убедиться и в обратном — что невозможно сделать ход, если нет свободных ячеек и подходящих плиток.
Два другие теста, подтверждающие это:
// … it ('должна возвращать true, если есть подходящие ячейки и плитки», function () { spyOn (_gridService, 'anyCellsAvailable').andReturn (false); spyOn (_gridService, 'tileMatchesAvailable').andReturn (true); expect (gameManager.movesAvailable ()).toBeTruthy (); }); it ('должна возвращать false, если нет подходящих ячеек и плиток», function () { spyOn (_gridService, 'anyCellsAvailable').andReturn (false); spyOn (_gridService, 'tileMatchesAvailable').andReturn (false); expect (gameManager.movesAvailable ()).toBeFalsy (); }); // … Теперь можно писать тесты, несмотря на то, что основное поведение приложения ещё не реализовано.
Вернёмся к GameManager Теперь нам надо реализовать функцию movesAvailable (). Мы уже можем протестировать работу кода, и уже определили те условия, при которых он работает, поэтому написать функцию довольно легко: // … this.movesAvailable = function () { return GridService.anyCellsAvailable () || GridService.tileMatchesAvailable (); }; // … Строим игровую сетку После создания GameManager нам нужно создать GridService, который будет обрабатывать все состояния игрового поля. Мы хотели делать это с помощью двух массивов — основной сетки и плиток. Зададим GridService две локальные переменные в файле app/scripts/grid/grid.js: angular.module ('Grid', []) .service ('GridService', function () { this.grid = []; this.tiles = []; // Размер доски this.size = 4; // … }); Для старта игры нужно будет заполнить эти массивы нулями. Сетка статичная, а для заполнения её плитками используются только элементы DOM. Массив плиток, наоборот, динамический.
В файле app/views/main.html нужно отобразить решётку. Логично будет расположить её в отдельной директиве. Использование директивы позволяет не раздувать главный шаблон и инкапсулировать функциональность.
В файле app/index.html расположим директиву сетки и передадим ей экземпляр GameManager через контроллер:
Директива должна содержать экземпляр GameManager (или, как минимум, модель, содержащую массивы сетки и плиток), это будет обязательным условием. Кромет ого, директива не должна обращаться к остальной части страницы или к экземпляру GameManager на странице, поэтому мы создадим её в изолированной области видимости.
angular.module ('Grid') .directive ('grid', function () { return { restrict: 'A', require: 'ngModel', scope: { ngModel: '=' }, templateUrl: 'scripts/grid/grid.html' }; }); Основная задача директивы — настроить вид сетки, поэтому настраиваемая логика в ней нам не нужна.
grid.html В шаблоне директивы мы запустим два ngRepeat для показа сетки и плиток, и для отслеживания их через $index.
Второй ng-repeat создаёт вложенные директивы для каждого из элементов-плиток tile. Директивы tile отвечают за визуальное создание плиток. Мы к ним скоро вернёмся.
Внимательный читатель воскликнет, что мы используем одномерный массив для вывода двумерной сетки. Молодец! И действительно, когда мы отрендерим наш вид, мы получим столбец плиток, а не решётку. Чтобы превратить их в решётку, нам понадобится CSS.
Входит SCSS Для проекта мы будем использовать модерновый вариант SASS: scss. Это более мощный вариант стилей, к тому же мы сделаем наши CSS динамичными. Основная часть визуальной работы ляжет на CSS, включая анимации, раскладку и визуальные элементы (цвета и т.п.)Для создания двумерной доски мы используем ключевое слово CSS3 transform.
CSS3 transform property Свойство CSS3 transform — это свойство, позволяющее изменять элемент в 2D или 3D. Его можно двигать, корёжить, вращать, масштабировать и т.д. (и анимировать всё это дело). Мы можем просто разместить плитку на доске и применить к ней нужную трансформацию.К примеру, у нас есть квадрат ширины 40 px и высоты 40 px.
.box { width:40 px; height:40 px; background-color: blue; } Применяя свойство transform translateX (300 px) мы передвинем его на 300 px вправо.
.box.transformed { -webkit-transform: translateX (300 px); transform: translateX (300 px); } Свойство translate позволяет передвигать плитки по доске просто применяя к ним стили. Но как построить динамические классы в соответствии с расположением клеток на сетке, когда мы меняем положение элементов на странице?
И тут на белом коне въезжает SCSS. Зададим несколько переменных (сколько плиток в ряд, и т.д.) и построим наш SCSS вокруг них. Вот, какие переменные нам нужны:
$width: 400 px; // Ширина доски $tile-count: 4; // Количество плиток по горизонтали и вертикали $tile-padding: 15 px; // Отступы между ними С их помощью мы даём SCSS возможность динамически рассчитывать позицию элемента. Сперва рассчитаем размер, что довольно тривиально:
$tile-size: ($width — $tile-padding * ($tile-count + 1)) / $tile-count; Теперь настроим контейнер #game, задав ему нужную высоту и ширину. Также мы убедимся, что сможем задавать абсолютные позиции элементов внутри него. Элементы .grid-container и .tile-container уютно разместятся внутри объекта #game.
Здесь — только цитаты из scss, остальное можно найти в нашем проекте на github (ссылка в конце статьи).
#game { position: relative; width: $width; height: $width; // Доска квадратная
.grid-container { position: absolute; // позиционирование absolute z-index: 1; // важно задать z-index для корректной работы слоёв margin: 0 auto; // центрируем
.grid-cell { width: $tile-size; // ширина ячейки height: $tile-size; // высота ячейки margin-bottom: $tile-padding; // отступ снизу margin-right: $tile-padding; // отступ справа // … } } .tile-container { position: absolute; z-index: 2;
.tile { width: $tile-size; // ширина плитки height: $tile-size; // высота плитки // … } } } Чтобы .tile-container расположился над .grid-container, необходимо задать z-index выше, чем у .tile-container. Если этого не сделать, они будут сидеть на одном уровне и выглядеть это будет криво
Теперь можно динамически позиционировать плитки. Нам нужно назначить класс .position-{x}-{y} каждой плитке. Начальной позицией первой плитки будет 0,0.
.tile { // … // Обходим позиции и создаём классы.position-#{x}-#{y}, // располагая таким образом плитки @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 из индекса. Динамически раздав всем позиции, мы можем выводить плитки на экран.
Раскрашиваем различные плитки Цвета плиток зависят от содержащегося в них значения, это сделано для удобства игрока. Мы сделаем это в цикле, похожем на тот, что раздавал плиткам позиции, только теперь он будет назначать им цвета. Для этого создадим массив 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, и так далее. Вместо того, чтобы захардкодить эти классы, мы воспользуемся магией 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; } Директива Tile Т.к. директива — это контейнер для Вида, работы нужно немного. Надо будет настроить доступ к ячейке, которую она должна выводить. angular.module ('Grid') .directive ('tile', function () { return { restrict: 'A', scope: { ngModel: '=' }, templateUrl: 'scripts/grid/tile.html' }; }); Интересная часть директивы tile — как мы динамически располагаем сетку. Это делается в шаблоне через переменную ngModel, находящуюся в изолированной области видимости. Как видно выше, она ссылается на объект tile из массива tiles:
TileModel Создадим не просто какой-то тупой объект, а вовсе даже умный, содержащий не только данные, но и функциональность.Чтобы иметь возможность использовать инъекцию зависимостей Angular, мы создадим сервис, содержащий модель данных. Сервис TileModel будет в модуле Grid, так как он нужен только для низкого уровня работы игрового поля.
Через метод .factory мы можем создать функцию, которую мы назначим фабрике. В отличие от функции service (), которая предполагает, что используемая функция определяет сервис в конструкторе этого сервиса, метод factory () назначает сервисный объект возвращаемым значением функции. Поэтому через метод factory () мы можем назначить объект как сервис, чтобы вставлять его в наши приложения.
В файле 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; }) // … Теперь мы можем где угодно сделать инъекцию TileModel и использовать её, словно глобальный объект. Удобство и комфорт.
Не забудьте написать тесты для TileModel.
Наша первая сетка Теперь у нас есть TileModel, и мы можем размещать её экземпляры в массиве плиток, после чего они волшебным образом появятся на нужных местах решётки. 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. При первой загрузке страницы необходимо создать пустое игровое поля, и делать это после каждого нажатия кнопок «Новая игра» и «Попробовать снова». Для очистки поля используется функция buildEmptyGameBoard () в нашем GridService. Этот метод будет заполнять нулями сетку и массив плиток.Перед кодом давайте напишем тест проверки функции buildEmptyGameBoard ().
// В это время в файле 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('должен заполнить сетку нулями’, function() { var grid = []; for (var x = 0; x < 16; x++) { grid.push(x); } gridService.grid = grid; gridService.buildEmptyGameBoard(); expect(gridService.grid).toEqual(nullArr); }); it(''должен заполнить плитки нулями’, function() { var tiles = []; for (var x = 0; x < 16; x++) { tiles.push(x); } gridService.tiles = tiles; gridService.buildEmptyGameBoard(); expect(gridService.tiles).toEqual(nullArr); }); }); Сама функция будет небольшой, а располагаться будет в 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; }
// Инициализация массива кучкой нулевых объектов this.forEach (function (x, y) { self.setCellAt ({x: x, y: y}, null); }); }; // … Код использует несколько вспомогательных методов. Вот ещё несколько таких функций:
// Запускать для каждого элемента массива плиток 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]); } };
// Установить ячейку на позиции this.setCellAt = function (pos, tile) { if (this.withinGrid (pos)) { var xPos = this._coordinatesToPosition (pos); this.tiles[xPos] = tile; } };
// Запросить ячейку с позиции this.getCellAt = function (pos) { if (this.withinGrid (pos)) { var x = this._coordinatesToPosition (pos); return this.tiles[x]; } else { return null; } };
// Находится ли позиция в границах нашей сетки 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,0, добавляет 1 к позиции. И так далее.
Экстраполируя систему, мы можем видеть, что уравнение, описывающее связь между двумя системами координат, выглядит так:
i = x + ny i — индекс ячейки, x и y — координаты на двумерном игровом поле, n — количество ячеек в строке/столбце. Две вспомогательные функции занимаются преобразованием этих координат. Концептуально легче иметь дело с x и y, но в смысле реализации лучше использовать одномерный массив.
// Преобразование x в x, y this._positionToCoordinates = function (i) { var x = i % service.size, y = (i — x) / service.size; return { x: x, y: y }; };
// Преобразование координат в индекс 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() для тех плиток, которые мы желаем разместить. Функция предполагает, что мы знаем обо всех возможных местах положения для плиток. Она просто проходит по массиву и ведёт учёт тех мест, где ещё нет плитки.
.service ('GridService', function (TileModel) { // … // Получить все возможные плитки 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 (). Вот один из возможных вариантов реализации:
.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); };
// Добавить плитку в массив this.insertTile = function (tile) { var pos = this._coordinatesToPosition (tile); this.tiles[pos] = tile; };
// Убрать плитку из массива this.removeTile = function (pos) { var pos = this._coordinatesToPosition (tile); delete this.tiles[pos]; } // … }); Теперь, благодаря Angular, наши плитки волшебным образом появятся на игровом поле.
Не забудьте написать тесты для проверки функциональности.
Взаимодействие с клавиатурой В нашем проекте мы будем работать с клавиатурой и не коснёмся сенсорных экранов (каламбур). Но реализовать управление касаниями несложно.Игра управляется стрелками, или нажатиями w, s, d, a. Нам нужно, чтобы пользователь, находясь на странице, мог управлять игрой, без необходимости ставить фокус на каком-то элементе. Для этого необходимо назначить отслеживание событий на document. В Angular это будет сервис $document.
Использование сервиса позволит нам создать настраиваемые события, которые происходят по нажатию кнопок, которые мы вставим в объекты Angular.
Для начала создадим новый модуль Keyboard в файле app/scripts/keyboard/keyboard.js
// app/scripts/keyboard/keyboard.js angular.module ('Keyboard', []);
Как и для других скриптов, для этого нужно создать ссылку из index.html. Теперь наш список тегов script выглядит так: