2048egret
egret+puremvc
Install / Use
/learn @f111fei/2048egretREADME
写在前面
本游戏使用Egret的GUI进行开发,不涉及Egret基础部分内容。本实例强烈建议结合GUIExample和深入浅出EGRET GUI系列教程来学习。
要了解Egret基础部分的内容请参考下面:
Egret GUI 相关教程:
2048是最近很火的一个小游戏,原版 就是用JavaScript写的。恰巧最近Egret PublicBeta,观望和学习了一阵后,发现egret正好适合开发这类游戏。Egret使用TypeScript作为开发语言,最终编译为JavaScript,正好和原始版本PK一下。
游戏预览:
1.准备开始
在开始之前,我们需要学习一下TypeScript和阅读官方的教程从Egret开发环境的部署到创建,编译,发布项目,以及Egret相关工具。在安装好开发环境后,在工作空间目录下使用命令行,创建2048egret新项目
egret create 2048egret
2.准备素材
每一个游戏都离不开美术资源,我们需要做的就是把美术资源打包,然后加载进来使用。这一方面egret有一套完整的工作流。
①资源打包
这里我们用到的资源主要有按钮,背景,文字以及数字这些图片。我们选择把这些图片都打包在一起合成一张大图就像 这样 和 这样 这样做可以减少URL请求数,还能减少资源的体积,把一些具有相同特征的图片放在一起便于管理。在egret里面这种类型的资源就是sheet。只有图片是不够的,还需要一个json描述文件来说明这张图每一张小图片的位置和大小。目前已经有成熟的工具来生成sheet和json。这里我用到的是 ShoeBox 配合这个 插件 来生成egret能识别的json。安装好插件后, 将每一张图片命名,然后将这些图片选中拖入Sprites Sheet中然后配置好生成的文件名点击save就能得到一张大图和一个json了,将图片和json放入"resource/assets/"文件夹下以备使用。此外ShoeBox还能读取swf将MovieClip导出为这种大图,按每一帧自动命名,这里的number.png就是这样导出的,下面有原始素材下载地址。
②资源加载
接下来我们需要生成一个资源描述文件resource.json,在游戏开始之前读取这个json来加载对应的文件。egret的资源加载机制可以参考 这里 现在已经有 工具 能自动生成这个resource.json了。按照下图配置。注意:虽然我们的资源有图片,但是对应的json文件已经记录了图片的位置,所以在这个工具中我们不需要添加对应的图片只添加json文件就行了。
<p align="center"><image src="http://xzper.qiniudn.com/2014/06/ResTool.png" /></p>在项目初始化时,使用RES加载资源,简单明了。
private onAddToStage(event:egret.Event){
........
........
//初始化Resource资源加载库
RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this);
RES.loadConfig("resource/resource.json","resource/");
}
/**
*配置文件加载完成,开始预加载preload资源组。
*/
private onConfigComplete(event:RES.ResourceEvent):void{
RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this);
RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE,this.onResourceLoadComplete,this);
RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS,this.onResourceProgress,this);
RES.loadGroup("preload");
}
③资源使用
在项目中我们可以使用RES来使用资源,参照对应的API。对于没有写进配置文件的资源使用RES.getResByUrl方法来异步获取。开发人员能使用极其少量的代码来完成各类资源的加载。
3.更改模板生成的代码
①修改细节
默认的文档类是GameApp。我觉得还是叫Main比较亲切,修改类名称,然后修改项目目录下的egretProperties.json文件,将document_class的值改为Main
{
"document_class" : "Main",
"native": {
"path_ignore": [
"libs"
]
}
}
默认生成的html的背景是黑色的,这里全部改成白色。将index.html里面的背景替换成#ffffff。
默认尺寸是480x800的尺寸。由于我们使用的部分图片宽度大于500,以及部分PC的分辨率太小为了不出现垂直滚动条影响体验,将尺寸换成520x650。这个不影响移动设备上的尺寸,移动设备默认是自适应宽度的。
index.html中
<div style="display:inline-block;width:100%; height:100%;margin: 0 auto; background: #ffffff; position:relative;" id="gameDiv">
<canvas id="gameCanvas" width="520" height="650" style="background-color: #ffffff"></canvas>
</div>
egret_loader.js中
//设置屏幕适配策略
egret.StageDelegate.getInstance().setDesignSize(520, 650);
context.stage = new egret.Stage();
var scaleMode = egret.MainContext.deviceType == egret.MainContext.DEVICE_MOBILE ? egret.StageScaleMode.SHOW_ALL : egret.StageScaleMode.NO_SCALE;
context.stage.scaleMode = scaleMode;
③引入第三方库pureMVC
这次我们要使用到一个mvc开发框架-pureMVC,熟悉as3的朋友一定也对这个框架不陌生吧。不熟悉的也没关系,这个框架不是这次的主角。我们从 这里 下载pureMVC的TypeScript版本。得到puremvc-typescript-standard-1.0.d.ts 和 puremvc-typescript-standard-1.0.js这两个文件,其实.d.ts就类似于c++里面的.h头文件,只有空方法和空属性,真正的实现是在js文件或者ts文件里面。在项目里面的src文件夹下建立一个puremvc的文件夹,将这个js文件和d.ts文件放进去。然后在项目根目录下建立一个puremvc.json的文件内容如下
{
"name": "puremvc",
"source":"src/puremvc/",
"file_list": [
"puremvc-typescript-standard-1.0.js",
"puremvc-typescript-standard-1.0.d.ts"
]
}
这样就表示配置了一个第三方模块。之后在编译器编译时会把相应的模块对应的js文件夹编译进libs文件夹下。项目里面我们还使用了gui模块,这些模块的配置是在egretProperties.json中,部分代码如下
"modules": [
{
"name": "core"
},
{
"name": "gui"
},
{
"name": "puremvc","path":"."
}
],
④注入AssetAdapter和SkinAdapter
我们这次的主角是egret的GUI。找到官方<a href="https://github.com/egret-labs/egret-examples" target="_blank">GUIExample</a>中的这两个ts文件复制到项目的src文件夹下面,由于这个项目没有用到默认皮肤,删除ShinAdapter里面getDefaultSkin方法的默认皮肤。最后不要忘了一点,在引擎初始化的时候注入这两个Adapter。
private onAddToStage(event:egret.Event){
//注入自定义的素材解析器
egret.Injector.mapClass("egret.gui.IAssetAdapter",AssetAdapter);
//注入自定义的皮肤解析器
egret.Injector.mapClass("egret.gui.ISkinAdapter",SkinAdapter);
......
......
}
这两个的Adapter的作用至关重要,AssetAdapter负责解释UIAsset的source属性 ,SkinAdapter负责解释SkinnableCompent的skinName属性。这里官方提供了两个默认已经写好了的,当然我们可以自己扩展。 没有他们,UIAsset素材包装器的source属性 和 可设置皮肤的GUI组件的skinName属性毫无作用。而这两种组件是今后使用最多的。不信可以往下看 。
⑤修改createGameScene方法
在生成的模板中,文档类Main在经过一系列的前期准备工作之后,终于轮到GUI组件的老大UIStage上场了。UIStage类似于Flex里面的SystemManager,内置弹出窗口层,工具提示层和鼠标样式层,所有的GUI组件都应该添加到他的下面,并且UIStage全局唯一。 这里我们实现了一个AppContainer继承自UIStage。 同时在这里pureMVC框架正式启动,开始运作。
/**
* 创建游戏场景
*/
private createGameScene():void {
var appContainer:game.AppContainer = new game.AppContainer();
this.addChild(appContainer);
game.ApplicationFacade.getInstance().startUp(appContainer);
}
4.pureMVC
①Mediator
Mediator(中介器)是连接视图也就是egret的GUI和pureMVC的桥梁。Mediator受到消息时(handleNotification)调用GUI组件的方法和设置属性,来改变视图。或者视图发生改变时通知Mediator由其发送消息到pureMVC(sendNotification)。
ApplicationMediator 监听键盘事件或者手势发送消息到GameCommand通知移动
MainGameMediator 接收消息,调用MainGameUI的方法处理格子的移动,添加,删除,重置,以及接收游戏结果,显示结果面板
MainMenuMediator 接收更新分数的消息,调用MainGameUI的方法更新分数与重置
ResultWindowMediator 发送游戏重置的消息,以及自销毁。
②Command
command属于控制器。负责收发消息和处理简单的事务。在StartupCommand中使用ControllerPrepCommand,ModelPrepCommand,ViewPrepCommand三个子任务。分别注册控制器,数据和视图。
GameCommand 处理各类事务。比如 玩家按下了方向键,收到消息调用GridProxy的移动方法改变数据,比如GridProxy移动格子分数改变了,通知GameCommand 调用GameProxy的更新分数方法改变分数,比如处理重置游戏的事务,通知各个数据模块重置数据
③Proxy
处理数据,提供公共方法供Command调用以改变数据。改变数据了然后sendNotification通知Mediator改变视图。
GameProxy 处理游戏数据,比如更新分数,处理游戏结果
GridProxy 这个游戏的核心数据,操作每一个格子的数据,通知视图格子的移动,添加,删除,重置。这里包含2048这个游戏的精髓。有兴趣的可以研究下,源码里面有详细注释,这篇文章不做重点讲解。
5.egret的GUI
①制作菜单-------认识皮肤部件
先来看看菜单长什么样子

我们会发现这个菜单。有些是静态文本,是一直不变的,我偷懒直接用了一张图片代替了,图片可以用egret.gui.UIAsset。
还有当前得分和最高分已经那个向上飘的数字是动态的,可以选用egret.gui.Label这个组件。
一个重试按钮,既然已经说了是按钮了我们就用egret.gui.Button好了。
接下来我们要做到皮肤和组件分离。那几个需要参与逻辑的组件自然就成了皮肤部件了。来看看MainMenuUISkin:
/**
* 和主机组件匹配的皮肤部件
*/
private static _skinParts:Array<string> = ["addLabel","scoreLabel","highScoreLabel","resetButton"];
public get skinParts():Array<string>{
return MainMenuUISkin._skinParts;
}
/**
* 加分文本
*/
public addLabel:egret.gui.Label;
/**
* 总分文本
*/
public scoreLabel:egret.gui.Label;
/**
* 最高分文本
*/
public highScoreLabel:egret.gui.Label;
/**
* 重置按钮
*/
public resetButton:egret.gui.Button;
public createChildren():void
{
super.createChildren;
var uiAsset:egret.gui.UIAsset = new egret.gui.UIAsset();
uiAsset.source = "source.menu";
this.addElement(uiAsset);
this.resetButton = new egret.gui.Button();
this.resetButton.skinName = ResetButtonSkin;
this.resetButton.right = 10;
this.resetButton.top = 80;
this.resetButton.label = "重置游戏";
this.addElement(this.resetButton);
this.highScoreLabel = new egret.gui.Label();
...省略若干代码
this.scoreLabel = new egret.gui.Label();
...省略若干代码
this.addLabel = new egret.gui.Label();
...省略若干代码
}
篇幅有限,省略了createChildren方法里面的子组件布局。**skin的createChildren方法是在皮肤和主机组件匹配的时候被调用的。皮肤和主机组件匹配是在主机组件被添加到显示列表的时候完成的。所以只要主机组件hostComponent还没有添加到显示舞台,获取hostComponent的皮肤部件都是无效的。**这也是为什么我将Mediator的注册放在GUI组件的createComplete后。以防Mediator访问出现空对象的情况。
再来看看主机组件MainMenuUI是怎么写的。
export class MainMenuUI extends egret.gui.SkinnableComponent{
public addLabel:egret.gui.Label;
public scoreLabel:egret.gui.Label;
public highScoreLabel:egret.gui.Label;
public resetButton:egret.gui.Button;
public constructor(){
super();
this.skinName = MainMenuUISkin;
this.addEventListener(egret.gui.UIEvent.CREATION_COMPLETE , this.createCompleteEvent, this);
}
public createCompleteEvent(event:egret.gui.UIEvent):void{
this.removeEventListener(egret.gui.UIEvent.CREATION_COMPLETE , this.createCompleteEvent, this);
ApplicationFacade.getInstance().registerMediator( new MainMenuMediator(this) );
}
public partAdded(partName:string, instance:any):void{
super.partAdded(partName , instance);
if(this.addLabel == instance){
this.addLabel.visible = false;
}
}
private moveEffect_effectEndHandler():void
{
this.addLabel.visible = false;
}
/**
* 加分效果
*/
public playScoreEffect(addScore:number):void{
this.addLabel.visible = true;
this.addLabel.text = "+".concat(addScore.toString());
egret.Tween.removeTweens(this.addLabel);
this.addLabel.y = 25;
