All Projects → f111fei → 2048egret

f111fei / 2048egret

egret+puremvc

Programming Languages

javascript
184084 projects - #8 most used programming language

写在前面

本游戏使用Egret的GUI进行开发,不涉及Egret基础部分内容。本实例强烈建议结合GUIExample和深入浅出EGRET GUI系列教程来学习。

要了解Egret基础部分的内容请参考下面:

Egret GUI 相关教程:


2048是最近很火的一个小游戏,原版 就是用JavaScript写的。恰巧最近Egret PublicBeta,观望和学习了一阵后,发现egret正好适合开发这类游戏。Egret使用TypeScript作为开发语言,最终编译为JavaScript,正好和原始版本PK一下。

游戏预览:

2048egret

PieceOfPie

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文件就行了。

在项目初始化时,使用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。找到官方GUIExample中的这两个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&lt;string&gt; = ["addLabel","scoreLabel","highScoreLabel","resetButton"];

public get skinParts():Array&lt;string&gt;{
    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;
        egret.Tween.get(this.addLabel).to({y:0},300).call(this.moveEffect_effectEndHandler , this);
	}

}

在构造函数里面赋值skinName传入皮肤的类引用,这个解析过程就是SkinAdapter完成的。 可以看到主机组件有一个partAdded进行皮肤组件的配对。这个时候我们就可以获取到对应的皮肤组件,来进行操作了,

②制作游戏区域--------了解九宫格和容器布局

再次来观察这个游戏的主界面。有一张纯色的背景图,还有4x4个空白的格子,随着游戏的进行会多出带数字的格子,游戏结束了还会出现胜利的界面。

先来看看第一张图。也许你认为这个背景一张4x4的图片不就搞定了么。不过我们这个2048单元格的数量可是可以任意调整的,可以是5x5,或者6x6甚至更多,这样才具备灵活性。你只需要改变CommandData的size属性就可以了(PS:游戏胜利的条件默认是达成2048,也可以通过修改CommandData的winValue属性来修改条件比如8192时胜利或者像上面那样32的时候胜利,想不输都难)。

我们继承SkinnableContainer建立一个MainGameUI的类作为容器来显示上面的界面,当然同时还需要一个皮肤MainGameUISkin。先来确定skinParts,如下:

/**
 * 和主机组件匹配的皮肤部件
 */
private static _skinParts:Array&lt;string&gt; = [
    "tileGroup","contentGroup"
];

public get skinParts():Array&lt;string&gt;{
    return MainGameUISkin._skinParts;
}

/**
 * 游戏底背景
 */
private backUIAsset:egret.gui.UIAsset;

/**
 * 背景格子容器
 */
private backGroundGroup:egret.gui.Group;

/**
 * 格子容器
 */
public tileGroup:egret.gui.Group;

/**
 * 内容
 */
public contentGroup:egret.gui.Group;

游戏游戏的底背景backUIAsset和背景格子容器backGroundGroup由于逻辑组件MainGameUI不需要关心所有这里不将其设置为skinParts。tileGroup是放置单元格的容器,contentGroup是SkinnableContainer的皮肤部件,SkinnableContainer的addElement方法实际上是添加到这个里面,换言之如果皮肤缺少这个contentGroup那么调用MainGameUI的addElement是看不到你要添加的子项的。 然后override这个createChildren方法将这些组件加入到显示列表。

public createChildren():void
{
    super.createChildren;
    this.backUIAsset = new egret.gui.UIAsset();
    this.backUIAsset.source = "source.background";
    //使用九宫格
    this.backUIAsset.scale9Grid = new egret.gui.Rectangle(20, 20, 65, 65);
    this.backUIAsset.width = CommonData.size*(TileUI.size + this.gap) + this.gap;
    this.backUIAsset.height = this.backUIAsset.width;
    this.addElement(this.backUIAsset);

    //使用格子布局
    var layout:egret.gui.TileLayout = new egret.gui.TileLayout();
    layout.columnCount = layout.rowCount = CommonData.size;
    layout.horizontalGap = layout.verticalGap = this.gap;
    this.backGroundGroup = new egret.gui.Group();
    this.backGroundGroup.x = this.backGroundGroup.y = this.gap;
    this.backGroundGroup.layout = layout;
    this.addElement(this.backGroundGroup);
    this.initBackGround(CommonData.size);

    this.tileGroup = new egret.gui.Group();
    this.tileGroup.x = this.tileGroup.y = this.gap;
    this.addElement(this.tileGroup);

    this.contentGroup = new egret.gui.Group();
    this.contentGroup.percentHeight = this.contentGroup.percentWidth = 100;
    this.contentGroup.touchEnabled = false;
    this.addElement(this.contentGroup);
}

先看backUIAsset,由于图片素材只是一个小的纯色圆角矩形,使用scale9Grid属性来设置九宫格缩放,这样设置了宽高就不会变形了。这里backGroundGroup设置了一个layout来确定布局。设置好间距以及行列数,向容器里面添加子项时就自动设置了位置了,不需要设置子项的x,y属性。TileLayout会自动布局。

private initBackGround(size:number):void{
        //背景格子
        var tile:egret.gui.UIAsset;
        var totalNum:number = size * size;
        for(var i:number = 0;i &lt; totalNum ; i++)
        {
            tile = new egret.gui.UIAsset();
            tile.width = tile.height = TileUI.size;
            tile.source = "source.backtile";
            this.backGroundGroup.addElement(tile);
        }
}

③制作单元格--------自定义属性

单元格使用TileUI来定义。单元格有一个很重要的属性就是单元格的数据,这里使用value属性来表示。

private valueChanged:boolean;
private _value:number;
/**
 * 格子的数字
 */
public get value():number{
    return this._value;
}

public set value(value:number){
    if(value == this._value){
        return;
    }
    this.valueChanged = true;
    this._value = value;
    this.invalidateProperties();
}

public commitProperties():void{
    if(this.valueChanged){
        this.valueChanged = false;
        this.updateValue();
    }
}

private updateValue():void{
    var mi:number = Math.log(this._value)/Math.log(2);
    this.source = "number.number_"+mi;
}

这里使用了invalidateProperties和commitProperties来完成属性的失效验证。当设置value的时候调用invalidateProperties,失效属性,GUI框架在下一次渲染的时候会调用commitProperties来完成属性的提交。失效验证简单说就是一种延迟应用改变的措施,这种失效验证在egret的GUI组件内部随处可见。这种自定义属性的例子只是其中之一,当组件的commitProperties方法被调用时组件已经准备完毕,所有的皮肤部件也匹配上了。这样也不用担心应用属性的时候报空的情况。这里通过设置TileUI的source属性来改变数字。还记得一开始的resource.json加载的number.json么。

{
"frames": {
	"number_1": {"x":106, "y":212, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_10": {"x":0, "y":318, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_11": {"x":0, "y":212, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_12": {"x":0, "y":106, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_13": {"x":0, "y":0, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_2": {"x":212, "y":318, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_3": {"x":212, "y":212, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_4": {"x":212, "y":106, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_5": {"x":212, "y":0, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_6": {"x":106, "y":318, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_7": {"x":318, "y":0, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_8": {"x":106, "y":106, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105},
	"number_9": {"x":106, "y":0, "w":105, "h":105, "offX":0,"offY":0,"sourceW":105,"sourceH":105}

},
"file": "number.png"
}

这个sheet记录的每一张图也有一个名称比如2这个数字就是number_1加上number这个sheet名称使用"."符号连接,所以获取"2"这个数字的图片就可以这样写this.source = "number.number_1"。根据值的不同取2的对数得到相应的下标数字。

④制作胜负界面--------自定义组件状态

游戏结束之后会出现胜负的界面。但是胜负界面应该是两套不同的素材,那么我们是不是可以制作两个皮肤来根据胜负来切换?这样当然可以。但是,还有一种更简单的办法就是使用自定义状态,只需要一个皮肤类就可以完成两种视图的切换。

新建一个ResultWindow类继承自SkinnableComponent,然后新建ResultWindowSkin。在ResultWindowSkin的构造函数中定义两个状态win和failed。

public constructor(){
    super();
    this.states = ["win","failed"];
}

然后加入两个皮肤部件button和resultUI

private static _skinParts:Array<string> = ["button","resultUI"];

public get skinParts():Array<string>{
    return ResultWindowSkin._skinParts;
}

/**
 * 按钮
 */
public button:egret.gui.Button;

/**
 * 结果文本
 */
public resultUI:egret.gui.UIAsset;

在createChildren里面将皮肤部件布局好位置。最后重写commitCurrentState方法来根据对应的状态来改变部件的skin。

public commitCurrentState():void {
    super.commitCurrentState();
    if(this.currentState == "win")
    {
        this.resultUI.source = "source.result_sucess";
        this.button.skinName = ContinueButtonSkin;
    }
    else
    {
        this.resultUI.source = "source.result_failed";
        this.button.skinName = ResetButtonSkin;
    }
}

这样还没有完,我们需要在主机组件里面定义何时是何种状态,在ResultWindow中重写getCurrentSkinState来定义皮肤状态。

private _win:boolean = false;
public get win():boolean{
    return this._win;
}

public set win(value:boolean){
    if(value == this._win)
        return;
    this._win = value;
    this.invalidateSkinState();
}

public getCurrentSkinState():string {
    return this.win?"win":"failed";
}

当外界设置win的值时调用invalidateSkinState来失效皮肤状态,在框架下次渲染的时候,调用SkinnableComponent的validateSkinState方法同时通过getCurrentSkinState来获取皮肤状态,通知皮肤去改变视图。这又是失效验证机制的一次完美使用。我们只需要调用失效,然后重写对应的验证方法就行了。事实上,按钮的up,down,disable之类的状态也是这样实现的。

egret的GUI库,集合了Flex和FlexLite的核心思想。实现了自动布局,皮肤分离,组件的三层失效验证机制。快来膜拜作者DOM大神吧。

6.优化游戏

①使用对象池

在游戏里面随着游戏的进行,每一次移动都有一个格子组件TileUI的创建,当游戏久了会造成巨大的内存开销。这里使用对象池技术。当一个对象使用完毕时,放入对象池,下次需要使用时取出来,这样避免了对象的重复创建,节约了内存。具体的实现参考ObjectPool这个类。具体使用在MainGameUI里面,如下:

/**
 * 创建一个格子
 */
public createTile(tileVO:TileVO):void{
    var tile:TileUI = &lt;TileUI&gt;(ObjectPool.getPool("game.TileUI").borrowObject());  //从对象池创建
   ......
   ......
}

当对象池里面没有TileUI时使用会new一个出来,否则直接从对象池获取。

/**
 * 清除一个格子
 */
public removeTile(tileVO:TileVO):void{
    var tileUI:TileUI = this.getTileUI(tileVO.x , tileVO.y);
    if(tileUI){
        this.tileGroup.removeElement(tileUI);
        ObjectPool.getPool("game.TileUI").returnObject(tileUI);
    }
}

当格子销毁时,回收到对象池,以备下次使用。

另外由于js没有提供弱引用特性,在对象池里面的对象要彻底销毁就需要手动了。

②针对不同设备

游戏在PC我们使用键盘上的方向键操控游戏,但是在移动设备上就需要使用手势来操控了,通过egret.MainContext.deviceType这个值来获取平台 。具体实现在ApplicationMediator中:

//为PC和移动端设置不同的移动策略
if(egret.MainContext.deviceType != egret.MainContext.DEVICE_MOBILE)
{
    var self = this;
    document.addEventListener("keydown",function(event:KeyboardEvent){
        switch (event.keyCode) {
            ....省略键盘事件......
        }
    });
}
else
{
    this.main.addEventListener(egret.TouchEvent.TOUCH_BEGIN , this.mouseDownHandle , this)
}

由于egret目前没有提供手势的API,这里我们自己实现手势。监听TOUCH_BEGIN,TOUCH_MOVE,TOUCH_END和LEAVE_STAGE这四个事件。具体实现如下:

private downPoint:egret.Point;
private movePoint:egret.Point;
private mouseDownHandle(event:egret.TouchEvent):void
{
    egret.UIGlobals.stage.addEventListener(egret.TouchEvent.TOUCH_MOVE,this.stage_mouseMoveHandler,this);
    egret.UIGlobals.stage.addEventListener(egret.TouchEvent.TOUCH_END,this.stage_mouseUpHandler,this);
    egret.UIGlobals.stage.addEventListener(egret.Event.LEAVE_STAGE,this.stage_mouseUpHandler,this);

    this.downPoint = this.main.globalToLocal(event.stageX, event.stageY);
}

private needMove:boolean;
private stage_mouseMoveHandler(event:egret.TouchEvent):void{
    if(!this.movePoint)
        this.movePoint = new egret.Point();
    this.movePoint.x = event.stageX;
    this.movePoint.y = event.stageY;
    if (this.needMove)
        return;
    this.needMove = true;
}

public stage_mouseUpHandler(event:egret.Event):void{
    egret.UIGlobals.stage.removeEventListener(egret.TouchEvent.TOUCH_MOVE,
        this.stage_mouseMoveHandler,
        this);
    egret.UIGlobals.stage.removeEventListener(egret.TouchEvent.TOUCH_END,
        this.stage_mouseUpHandler,
        this);
    egret.UIGlobals.stage.addEventListener(egret.Event.LEAVE_STAGE,
        this.stage_mouseUpHandler,
        this);
    if(this.needMove){
        this.updateWhenMouseUp();
        this.needMove = false;
    }
}

/**
 * 移动设备上,判断移动方向
 */
private updateWhenMouseUp():void
{
    var p:egret.Point = this.main.globalToLocal(this.movePoint.x, this.movePoint.y ,egret.Point.identity);
    var offSetX:number = p.x - this.downPoint.x;
    var offSetY:number = p.y - this.downPoint.y;

    if(offSetY&lt;0 &amp;&amp; Math.abs(offSetY)&gt;Math.abs(offSetX))  //上
    {
        this.doMove(0);
    }
    else if(offSetX&gt;0 &amp;&amp; offSetX&gt;Math.abs(offSetY))  //右
    {
        this.doMove(1);
    }
    else if(offSetY&gt;0 &amp;&amp; offSetY&gt;Math.abs(offSetX))  //下
    {
        this.doMove(2);
    }
    else if(offSetX&lt;0 &amp;&amp; Math.abs(offSetX)&gt;Math.abs(offSetY))  //左
    {
        this.doMove(3);
    }
}

③动画效果

游戏里面格子的创建,移动,合并,都需要一个缓动效果来支持。这里面最大的问题就是,缓动效果是持续时间的,而往往数据的改变是一瞬间。比如:一个操作合并了两个格子,其中一个需要移动,并且移动完成后要移除掉,此时新的格子也要出现。但是往往数据层把这些数据是同时发过来的,所以我们需要调节时机来让效果更好。比如格子的创建,我们延迟100毫秒显示在舞台上,因为在创建的同时其他格子会移动,所以等其他格子移动完成了后显示出来比较好。再比如格子的合并,实际上就是一个格子移动,移动完成后消失,目标位置格子改变数字并且出现缩放效果。这样要注意的就是移动之前调整将移动的格子的层级显示在最下面,避免移动的时候挡在目标位置格子的上面。

7.性能

egret的工作流还是很高效的,工具也不少,架构设计集各家所长。最后发布游戏在PC上运行是满帧运行,比原版甚至更快。在手机上运行也不错,体验超过了某些原生语言开发的2048。

最后交出源代码:点我传送

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].