首先附上原文地址:Building the 2048 game in AngularJS
我们被问到的其中一个最频繁的问题是:作为一个框架,Angular在何时被认为是一个并不好的选择。我们默认的答案通常是当要去写一款游戏时,因为Angular有它自己的事件循环处理($digest循环)并且游戏通常都需要很多底层的DOM操作。这其实是一个不准确的描述,因为许多种类的游戏Angular是可以支持的。甚至是那些需要大量DOM操作的游戏,也可以使用Angular框架来做静态的部分,比如追踪高分和游戏菜单等。
如果你像我一样,你可能正痴迷于那个受欢迎的2048游戏。这个游戏的目标是将等值的“瓦片”匹配在一起从而得到一个值为2048的“瓦片”。
在这个系列中(译者注:原文为“在今天这个帖子中”),我们将用AngularJS来写一个它的克隆版,从始至终解释整个构建app的处理过程。由于这个app差不多可以算是一个复杂的应用,我们也打算通过这个系列来描述如何来构建复杂的AngularJS应用。
这里这个我们将要用Angular来构建应用的demo的地址链接。
准备好,我们开始了!
这边附上该项目的Github托管地址
目录:
(翻译后的结构和原文稍有不同,目录在翻译过程中会逐渐完善)
一:规划应用
第一步我们将要做的是对我们要构建的应用进行一个高层次的设计。如果我们是克隆其他的应用或者是从头开始创建一个应用,无论这个应用有多大,我们都会这样做。
看这游戏,我们可以发现有一堆的“瓦片”堆在一个游戏板上。每一个“瓦片”都作为一个用于可被放置标有数字的“瓦片”的位置。我们可以用这个事实将放置“瓦片”的工作交给CSS3,而不是依靠Javascript来确认“瓦片”放置在哪里。当我们在板上有一个“瓦片”的时候,我们将简单地确定“瓦片”是被放置在合适位置的顶层。
使用CSS3来对游戏板布局让我们可以将制作动画的工作移交给CSS,但也同时使用AngularJS来追踪游戏板的状态、瓦片和游戏逻辑。
既然我们只有一个页面,我们将只使用一个控制器来管理页面。
另外既然我们这个应用只用到了一个游戏板,我们将把所有栅格逻辑(grid logic)放在一个GridService的服务中。既然服务都是一些单一的对象,存放栅格逻辑就很合适。我们将用GridService去处理放置“瓦片”、移动“瓦片”、将“瓦片”穿过栅格移动到合适的位置和管理栅格。
我们将把游戏逻辑和处理内部其他服务的内容放到GameManager中。GameManager将负责管理游戏状态,处理移动,和维护分数(包括当前分数和最高分)。
最后,我们需要一个组件来让我们可以管理键盘,我们叫它KeyboardService。这篇文章将实现桌面应用的处理,但我们可以重用一些服务,让它们可以同样工作在移动设备上。
二:构建应用
要构建我们的应用,我们将先创建一个基本应用(我们使用Yeoman的Angular生成器来生成我们应用的基本结构,但这并不是必要的。我们只是用它作为起点,但很快就会偏离这个结构)。我们将创建一个app目录在放我们这个的应用代码,我们把test目录创建在app目录的同一级,用来存放测试代码。
既然我们在用于中使用yeoman,我们将首先需要确保它已经被安装。Yeoman是基于Node.js和npm的,Node.js的安装超出的本篇文章的范围,可以通过Node.js官网了解更多。
在npm被安装之后,我们可以安装Yeoman的工具yo和Angular生成器(这个生成器使用yo工具来创建Angular应用):
$ npm install -g yo
$ npm install -g generator-angular
安装完成后,我们可以使用Yeoman工具来创建我们的应用:
$ cd ~/Development && mkdir 2048
$ yo angular twentyfourtyeight
这个工具会问许多问题,除了要选依赖(Dependencies)的时候,我们只选angular-cookies,其他我们全部选yes。
我们将创建scripts/app.js文件来放置我们的应用。让我为这个应用开个头:
angular.module('twentyfourtyeightApp', [])
三:模块化结构
我们推荐的Angular应用布局是使用功能而不是类型。我们不把我们的组件分为控制器、服务、指令等,而是基于功能来定义我们的模块结构。比如,我们将定义一个Game模块和一个Keyboard模块。
模块化结构让我们有一个清楚的责任分配,文件结构也将与之匹配。这不仅仅有助于我们构建大型的复杂Angular应用,它也能帮助我们进行跨应用的功能重用。
之后,我们将为我们的测试环境搭建相匹配的文件目录结构。
我们的应用最易入手的地方应该是视图了。看这视图本身,我们只有一个视图模板。这个应用不需要多个视图,所以我们创建单个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>
有了app/index.html文件,我们只需要一个app/views/main.html作为应用级视图保存视图的细节。当我们需要在应用中导入新的资源的时候,我们只需要修改index.html文件就可以了。
打开app/views/main.html文件,我们将放一些针对游戏的视图。使用controller as语法,我们可以清楚地找到放置数据的$scope,以及知道哪个控制器在处理哪个组件。
<!-- app/views/main.html -->
<div id="content" ng-controller='GameController as ctrl'>
<!-- Now the variable: ctrl refers to the GameController -->
</div>
在我们的视图中,我们最少会放置这几个东西:
- 我们游戏的静态头部
- 我们当前游戏的分数和本地用户的最高分
- 游戏板
<!-- heading inside app/views/main.html -->
<div id="content" ng-controller='GameController as ctrl'>
<div id="heading" class="row">
<h1 class="title">ng-2048</h1>
<div class="scores-container">
<div class="score-container">{{ ctrl.game.currentScore }}</div>
<div class="best-container">{{ ctrl.game.highScore }}</div>
</div>
</div>
<!-- ... -->
</div>
可以发现当我们在视图中引用CurrentScore和highScore的时候我们引用了GameController。这个controller as语法让我们可以清楚的获取我们感兴趣的控制器引用。(译者注:controller as语法是在Angular的版本1.2加入的,在处理一个页面中的多个控制器时比较有用,其实就是取个别名,方便调用)