Link

首页

Tweet

书单

about me

Command

友情链接

Category

游戏控制


第6章 游戏控制

6.1 触摸事件

6.1.1 触摸事件类型

在PC或MAC设备中,我们使用鼠标点击事件。而移动设备中我们使用的是触摸事件。Egret引擎将触摸事件全部封装到egret.TouchEvent中,你可以在该事件中看到如下几种触摸事件类型。

其中,除了TOUCH_CANCEL事件,其他事件都是属于常用事件类型。下面来详细介绍一下各个事件类型的不同。

TOUCH_BEGIN表示手指触摸到一个显示对象,类似PC中的鼠标按下。

TOUCH_END表示手指抬起结束触摸,类似PC中的鼠标抬起。

TOUCH_MOVE表示手指在显示对象上移动。移动操作一定是先按下,然后发生移动。

TOUCH_RELEASE_OUTSIDE表示手指移动过程中,移出了显示对象区域。

TOUCH_TAP表示一次触摸点击,类似PC中的鼠标点击。他是由TOUCH_BEGINTOUCH_END连续完成的。

6.1.2 开启触摸事件

触摸事件本质上是引擎内部通过对canvas的触摸事件拦截实现的。无论我们画面中绘制了多少个显示对象,对于浏览器来说当前只有一个canvas对象(canvas对象实现了WebGL的上下文环境)。Egret引擎通接收到对canvas对象的触摸事件,得到触摸的坐标位置。然后在引擎中对显示列表进行碰撞查找,寻找当前触摸的位置点和哪个显示对象的范围发生了碰撞,则认为对应显示对象被点击。

由于触摸事件本质上是对显示列表(多叉树)的遍历搜索,为了优化性能,Egret引擎在每个显示对象上设定了一个名称为touchEnabled的属性。该属性用来标记当前显示对象是否参与触摸查找算法,如果它为false,那么在算法中直接跳过该显示节点,不对齐自身以及子节点进行触摸的碰撞检测。

所以当你需要一个显示对象可被点击的时候,需要先设置其touchEnabled属性为true。

6.1.3 触摸事件属性

本小结只介绍触摸事件中最常用的4个属性:localX、localY、stageX和stageY。至于touchPointID只有在多点触摸中才会用到,多点触摸将在下个小结中讲解。

在第2章的时候,我们讲解过坐标系。其中最为特殊且重要的两个坐标系是全局坐标系和物体坐标系。在触摸事件中,localX和localY对应着被触摸物体的物体坐标系(Egret引擎中将物体坐标系统一称为“局部坐标系”)。而stageX和stageY则代表着当前触摸点的全局坐标系。

知道这两个坐标系的值非常有用,我们经常会在游戏的不同层级切换,通过它们可以进行坐标转换。

6.1.4 多点触摸

多点触摸在PC或MAC平台无法测试,你需要将代码写好然后移动设备中机型测试查看效果。

每个触摸事件中都会携带一个名为touchPointID的属性。当一个手指触摸到屏幕时,系统会为这个手指触摸所发送的事件分配一个ID。当第二个手指开始触摸的时候,系统会为它分配另外一个ID。当手指抬起或者移除屏幕后,所分配的ID才会回收。所以我们可以通过对touchPointID属性的判断来区分当前触摸事件来源于哪个手指。下面的示例中,我们将实现一个支持两个触摸操作的例子,可以通过双手指的捏合动作缩放显示对象。同时还可以通过两个手指旋转显示对象,代码如下:

/**
 * @copyright www.egret.com
 * @author A闪
 * @desc 需要在移动设备中预览,两个手指可控制DisplayObject缩放和旋转
 *      。
 */

class Main extends egret.DisplayObjectContainer {

    public constructor()
    {
        super();
        this.once( egret.Event.ADDED_TO_STAGE, this.onAddToStage, this );
    }

    private onAddToStage(event:egret.Event)
    {
        var imgLoader:egret.ImageLoader = new egret.ImageLoader;
        imgLoader.once( egret.Event.COMPLETE, this.imgLoadHandler, this );
        imgLoader.load( "resource/cartoon-egret_00_small.png" );
    }

    private _bird:egret.Bitmap;                        //舞台中唯一显示对象


    //image load complete ( event callback )
    private imgLoadHandler( evt:egret.Event ):void
    {

        this._bird = new egret.Bitmap( evt.currentTarget.data );
        this._bird.anchorOffsetX = this._bird.width/2;
        this._bird.anchorOffsetY = this._bird.height/2;
        this._bird.x = this.stage.stageWidth/2;
        this._bird.y = this.stage.stageHeight/2;
        this.addChild( this._bird );

        this.drawText();

        this.stage.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.mouseDown, this);
        this.stage.addEventListener(egret.TouchEvent.TOUCH_END, this.mouseUp, this);
        this.stage.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.mouseMove, this);
    }

    private touchPoints:Object = {names:[]}; //{touchid:touch local,names:[ID1,ID2]};
    private distance:number = 0;
    private defAngle:number = 0;
    private touchCon:number = 0;
    private _currentBirdRotation:number = 0;
    private mouseDown(evt:egret.TouchEvent)
    {
        egret.log("touch begin:"+evt.touchPointID);
        if(this.touchPoints[evt.touchPointID]==null)
        {
            this.touchPoints[evt.touchPointID] = new egret.Point(evt.stageX,evt.stageY);
            this.touchPoints["names"].push(evt.touchPointID);
        }
        this.touchCon++;

        if(this.touchCon==2)
        {
            this.distance = this.getTouchDistance();
            egret.log("distance:"+this.distance);

            this.defAngle = this.getTouchAngle();
            egret.log("touch angle:"+this.defAngle);
            egret.log("bird angle:"+this._bird.rotation);
        }

    }

    private mouseMove(evt:egret.TouchEvent)
    {
        //egret.log("touch move:"+evt.touchPointID);

        this.touchPoints[evt.touchPointID].x = evt.stageX;
        this.touchPoints[evt.touchPointID].y = evt.stageY;
        if(this.touchCon==2)
        {
            var newdistance = this.getTouchDistance();
            this._bird.scaleX = newdistance/this.distance;
            this._bird.scaleY = this._bird.scaleX;

            var newangle = this.getTouchAngle();
            this._bird.rotation = this._currentBirdRotation + newangle - this.defAngle;
        }
    }

    private mouseUp(evt:egret.TouchEvent)
    {
        egret.log("touch end:"+evt.touchPointID);
        delete  this.touchPoints[evt.touchPointID];
        this.touchCon--;
        //
        this._bird.width *= this._bird.scaleX;
        this._bird.height *= this._bird.scaleY;
        this._bird.scaleX = 1;
        this._bird.scaleY = 1;
        this._bird.anchorOffsetX = this._bird.width/2;
        this._bird.anchorOffsetY = this._bird.height/2;
        this._currentBirdRotation = this._bird.rotation;

        egret.log("bird size [wdith:"+this._bird.width.toFixed(1) +", height:"+this._bird.height.toFixed(1)+"]");
        egret.log("bird angle:"+this._bird.rotation);
    }

    private getTouchDistance():number
    {
        var _distance:number = 0;
        var names = this.touchPoints["names"];
        _distance = egret.Point.distance( this.touchPoints[names[names.length-1]],
            this.touchPoints[names[names.length-2]]);
        return _distance;
    }

    private c:number = 0.017453292; //2PI/360
    private getTouchAngle():number
    {
        var ang:number = 0;
        var names = this.touchPoints["names"];
        var p1:egret.Point = this.touchPoints[names[names.length-1]];
        var p2:egret.Point = this.touchPoints[names[names.length-2]];

        ang = Math.atan2((p1.y-p2.y),(p1.x-p2.x)) / this.c;
        return ang;
    }

    private _txInfo:egret.TextField;
    private _bgInfo:egret.Shape;
    private drawText()
    {
        /// 提示信息
        this._txInfo = new egret.TextField;
        this.addChild( this._txInfo );

        this._txInfo.size = 28;
        this._txInfo.x = 50;
        this._txInfo.y = 50;
        this._txInfo.textAlign = egret.HorizontalAlign.LEFT;
        this._txInfo.textColor = 0x000000;
        this._txInfo.type = egret.TextFieldType.DYNAMIC;
        this._txInfo.lineSpacing = 6;
        this._txInfo.multiline = true;

        this._txInfo.text =
            "该示例需在移动端体验,使用双手指做捏合动作\n可缩放显示对象。双手指可旋转显示对象";

        this._bgInfo = new egret.Shape;
        this.addChildAt( this._bgInfo, this.numChildren - 1 );

        this._bgInfo.x = this._txInfo.x;
        this._bgInfo.y = this._txInfo.y;
        this._bgInfo.graphics.clear();
        this._bgInfo.graphics.beginFill( 0xffffff, .5 );
        this._bgInfo.graphics.drawRect( 0, 0, this._txInfo.width, this._txInfo.height );
        this._bgInfo.graphics.endFill();
    }
}

6.2 简单的按钮实现

按钮是最为常见也是最简单的控制器,在Egret引擎中我们可以编写一个简单的按钮用来控制游戏中的内容。无论是控制人物移动还是在一些操作面板中,都少不了按钮的身影。在编写按钮的时候,我们需要注意的是 touchEnabled 属性。默认情况下,显示对象是无法接受触摸事件的。需要将 touchEnabled 属性设置为true,允许显示对象接受触摸来实现点击下过。

一个按钮,在移动设备中应该具有以下三种状态。

三种状态一般来说,都是使用三张不同图片来说表现状态不同。

说明
在移动设备中,没有鼠标点击事件。你所能使用的都是触摸事件。与此同时,也不存在鼠标滑过事件。

我们先来实现一个只有抬起状态的按钮。核心代码如下:

清单0.1 代码清单6.1

private createGameScene() {
    let btn:egret.Bitmap = new egret.Bitmap();
    let texture: egret.Texture = RES.getRes('btnQuit_up');
    btn.texture = texture;
    this.addChild(btn);

    btn.touchEnabled = true;
    btn.addEventListener(egret.TouchEvent.TOUCH_TAP, this.click, this);
}

private click(e: egret.TouchEvent): void {
    console.log('点击');
}

新建一个位图,其纹理为预先定义好的按钮抬起状态图片,然后开启其 touchEnabled 属性。最后绑定触摸事件,egret.TouchEvent.TOUCH_TAP。

运行效果如图。

当我们点击按钮时,在控制台可以打印出“点击”。

接下来我们要实现按钮的抬起和按下两种状态。原理并不复杂,当发生触摸时需要将按钮的纹理设置为按下图片,当触摸结束时将按钮的纹理设置为抬起的图片。这样就实现了最基本的按钮两种状态切换。

添加对 egret.TouchEvent.TOUCH_BEGIN 和 egret.TouchEvent.TOUCH_END 时间的侦听。

btn.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.down, this);
btn.addEventListener(egret.TouchEvent.TOUCH_END, this.up, this);

egret.TouchEvent.TOUCH_BEGIN 表示刚刚触摸到按钮上,egret.TouchEvent.TOUCH_END 表示触摸结束,也就是手指抬起状态。相应函数如下。

清单0.1 代码清单6.2

private up(e: egret.TouchEvent): void {
    this.btn.texture = RES.getRes('btnQuit_up');
}

private down(e: egret.TouchEvent): void {
    this.btn.texture = RES.getRes('btnQuit_down');
}

当触发不同状态事件时,来切换不同的纹理。编译调试,点击当前按钮可以看到按钮状态在切换。

注意
egret.TouchEvent.TOUCH_TAP 事件不会和另外两个事件发生冲突。当触摸开始并且结束后,才算一次点击完成。

你可能会觉得这样便完成了一个按钮的制作,但仔细测试会发现一个问题。当我们在按钮上按下后,手指移出按钮范围。此后按钮就无法收到 egret.TouchEvent.TOUCH_END 事件。画面中的按钮将永远停留在按下时候的状态。这是一个错误的状态,我们需要增加一种事件,来避免这种情况的发生。

egret.TouchEvent.TOUCH_RELEASE_OUTSIDE 事件表示当用户在启用触摸设备上的已启动接触的不同 DisplayObject 实例上抬起接触点时(例如,按住屏幕上的某个对象,然后从它上面挪开后再松开手指)调度。我们在代码中继续添加如下语句。

btn.addEventListener(egret.TouchEvent.TOUCH_RELEASE_OUTSIDE, this.up, this);

编译调试,在按钮上点击,然后将鼠标或手指移出按钮范围后抬起。可以按钮安装的状态又回到抬起状态。

按钮类的封装

为了方便按钮的使用,需要将按钮的功能进行封装,下面示例我们封装一个简单的按钮。

清单0.1 代码清单6.3 Button.ts

class Button extends egret.Bitmap {

    private _upSkin: egret.Texture;
    private _downSkin: egret.Texture;

    public constructor(upSkin: string, downSkin: string) {
        super();
        this._upSkin = RES.getRes(upSkin);
        this._downSkin = RES.getRes(downSkin);
        this.texture = this._upSkin;
        this.touchEnabled = true;
        this.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.down, this);
        this.addEventListener(egret.TouchEvent.TOUCH_END, this.up, this);
        this.addEventListener(egret.TouchEvent.TOUCH_RELEASE_OUTSIDE, this.up, this);
    }

    private up(e: egret.TouchEvent): void {
        this.texture = this._upSkin;
    }

    private down(e: egret.TouchEvent): void {
        this.texture = this._downSkin;
    }
}

在入口类中,使用代码如下。

let btn: Button = new Button('btnQuit_up', 'btnQuit_down');
this.addChild(btn);

6.3 简单手势

在移动游戏中,有时候我们会有左滑右滑来完成控制操作。对于简单的四方向或者八方向滑动操作处理也相对简单。

如下图,当我们手指触摸到屏幕时,记录点击的位置(点A)。当手指抬起时,记录其抬起的位置(点B)。此时得到两个点,将两个点连为一条线段,可计算出线段的角度。如果是四方向,可以将一周切分为四份,判断当前线段所在象限,即可得出方向。图中示例,此时应该向右移动。

注意:在判定方向前,我们需要检查线段的长度,当线段长度大于一定值时,才认为此次滑动生效。这是为了防止一些极端情况下产生的误触而导致的“灵敏度”过高的问题。

也许你会产生疑问,为什么图中判定方向的辅助线其交叉点不在坐标原点上,而是与点A重合?下面来详细讲解这个问题。

由于线段没有方向,所以此时线段AB所计算出的方向既可以是右,也可以是左。为了解决这个问题,我们需要让在线段上加上方向,又回到了向量。

向量表示的是大小与方向,在笛卡尔坐标系中并不表示位置,所以此时我们需要将点A作为一个全新坐标系的原点,而点B则变为向量B。

从全局坐标系到新坐标系的转换不需要多复杂,只需要将B的x,y属性减去A的x,y属性,此时B就存在于新坐标系中了。你也可以将其想象为旧坐标系发生位置偏移。

四个方向所对应的额角度范围:

既然是向量,那么借助已有的代码就可以轻松实现四方向的手势识别了,计算方向的代码如下:

private directions: string[] = [
    SimpleGestureEvent.RIGHT
    , SimpleGestureEvent.DOWN
    , SimpleGestureEvent.DOWN
    , SimpleGestureEvent.LEFT
    , SimpleGestureEvent.RIGHT
    , SimpleGestureEvent.UP
    , SimpleGestureEvent.UP
    , SimpleGestureEvent.LEFT];

private minDistance: number = 10;
private directionVector: Vector = Vector.zero();

private direction(): string {
    let x: number = this.touchEndPoint.x - this.touchBeginPoint.x;
    let y: number = this.touchEndPoint.y - this.touchBeginPoint.y;
    this.directionVector.setTo(x, y);
    if (this.directionVector.norm >= this.minDistance) {
        let index: number = Math.floor(Vector.angle(Vector.right(), this.directionVector) / 45);
        index = this.directionVector.y < 0 ? index += 4 : index;
        return this.directions[index];
    }
    return SimpleGestureEvent.NONE;
}

direction 函数中通过运算得到向量的x,y值,然后判断directionVector是否大于最小长度,如果大于表示此次滑动手势有效,进而判定其方向。

在判定方式时,我们可以通过switch语句实现,但此处使用另外一种方法。

let index: number = Math.floor(Vector.angle(Vector.right(), this.directionVector) / 45);

将向量的角度除以45,得到的值向下取整。这样我们便知道此角度中包含多少个45°,再去预定义好的数组中查找对应的方向定义。

index = this.directionVector.y < 0 ? index += 4 : index;

作用是用来判定向量方向是向上还是向下的,因为向量角度计算后得到的是一个0~180°的数值,故需要特殊处理。对应的角度和数组下标如下图:

完整代码如下:

Matin.ts

class Main extends egret.DisplayObjectContainer {
    public constructor() {
        super();
        this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);
    }

    private onAddToStage(event: egret.Event) {
        this.draw();
    }

    private draw() {
        let bg: MoveShape = new MoveShape();
        bg.graphics.beginFill(0xcccccc);
        bg.graphics.drawRect(0, 0, this.stage.stageWidth, this.stage.stageHeight);
        bg.graphics.endFill();
        this.addChild(bg);
        bg.touchEnabled = true;

        let gesture: SimpleGesture = new SimpleGesture(bg);
        gesture.addEventListener(SimpleGestureEvent.LEFT, this.left, this);
        gesture.addEventListener(SimpleGestureEvent.RIGHT, this.right, this);
        gesture.addEventListener(SimpleGestureEvent.UP, this.up, this);
        gesture.addEventListener(SimpleGestureEvent.DOWN, this.down, this);
    }

    private left(e: SimpleGestureEvent) {
        console.log('left');
    }

    private right(e: SimpleGestureEvent) {
        console.log('right');
    }

    private up(e: SimpleGestureEvent) {
        console.log('up');
    }

    private down(e: SimpleGestureEvent) {
        console.log('down');
    }

}

SimpleGesture.ts

class SimpleGesture extends egret.EventDispatcher {

    private touchTrigger: egret.DisplayObject;
    public constructor(trigger: egret.DisplayObject) {
        super();
        this.touchTrigger = trigger;
        this.addlistener();
    }
    private addlistener(): void {
        this.touchTrigger.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.touchBegin, this);
    }

    private touchBeginPoint: egret.Point = new egret.Point();
    private touchEndPoint: egret.Point = new egret.Point();
    private touchBegin(e: egret.TouchEvent): void {
        this.touchBeginPoint.setTo(e.stageX, e.stageY);
        this.touchTrigger.addEventListener(egret.TouchEvent.TOUCH_END, this.touchEnd, this);
    }

    private touchEnd(e: egret.TouchEvent): void {
        this.touchTrigger.removeEventListener(egret.TouchEvent.TOUCH_END, this.touchEnd, this);
        this.touchEndPoint.setTo(e.stageX, e.stageY);
        let dir: string = this.direction();
        if (dir !== SimpleGestureEvent.NONE) {
            this.dispatchEvent(new SimpleGestureEvent(dir));
        }
    }

    private directions: string[] = [
        SimpleGestureEvent.RIGHT
        , SimpleGestureEvent.DOWN
        , SimpleGestureEvent.DOWN
        , SimpleGestureEvent.LEFT
        , SimpleGestureEvent.RIGHT
        , SimpleGestureEvent.UP
        , SimpleGestureEvent.UP
        , SimpleGestureEvent.LEFT];

    private minDistance: number = 10;
    private directionVector: Vector = Vector.zero();

    private direction(): string {
        let x: number = this.touchEndPoint.x - this.touchBeginPoint.x;
        let y: number = this.touchEndPoint.y - this.touchBeginPoint.y;
        this.directionVector.setTo(x, y);
        if (this.directionVector.norm >= this.minDistance) {
            let index: number = Math.floor(Vector.angle(Vector.right(), this.directionVector) / 45);
            index = this.directionVector.y < 0 ? index += 4 : index;
            return this.directions[index];
        }
        return SimpleGestureEvent.NONE;
    }
}

class SimpleGestureEvent extends egret.Event {
    public static LEFT = "LEFT";
    public static RIGHT = "RIGHT";
    public static UP = "UP";
    public static DOWN = "DOWN";
    public static NONE = "NONE";

    public v: Vector = null;
    public constructor(type: string, bubbles: boolean = false, cancelable: boolean = false) {
        super(type, bubbles, cancelable);
    }
}

6.4 实例:贪吃蛇

本节我们来开发第一个游戏实例。贪吃蛇是一个非常简单的小游戏。在游戏控制中我们可使用刚刚学习的简单手势来实现方向控制。与此同时,在绘图方面,我们将使用矢量图来进行画面绘制,方便学习。

在一开始,我们需要先初始化一些游戏数据。由于不同手机的尺寸不同,其宽高比也不同,所以在初始阶段,我们需要先来计算一些必要的数据。建立一个名称为SceneData的类,用来存储这些数据。代码如下:

class SceneData {

    private static _stageWidth: number = 0;
    private static _stageHeight: number = 0;

    private static _blockSize: number = 0;

    private static _row: number = 0;
    private static _col: number = 20;

    public static setStageSize(width: number, height: number): void {
        SceneData._stageWidth = width;
        SceneData._stageHeight = height;
        SceneData._blockSize = SceneData._stageWidth / SceneData._col;
        SceneData._row = Math.floor(SceneData._stageHeight / SceneData._blockSize);
    }

    public static get Row(): number {
        return SceneData._row;
    }

    public static get Col(): number {
        return SceneData._col;
    }

    public static get BlockSize(): number {
        return SceneData._blockSize;
    }

    public static get stageWidth(): number {
        return SceneData._stageWidth;
    }

    public static get stageHeight(): number {
        return SceneData._stageHeight;
    }

    private static _pos: Pos = new Pos();
    public static getPos(index: number): Pos {
        SceneData._pos.Col = index % SceneData._col;
        SceneData._pos.Row = Math.floor(index / SceneData._col);
        return SceneData._pos;
    }

    public static outMapBound(index: number, dir: Direction): boolean {
        switch (dir) {
            case Direction.UP:
                return index < SceneData.Col ? true : false;
            case Direction.DOWN:
                return (index + SceneData.Col) >= SceneData.Row * SceneData.Col ? true : false;
            case Direction.LEFT:
                return index % SceneData.Col === 0 ? true : false;
            case Direction.RIGHT:
                return (index + 1) % SceneData.Col === 0 ? true : false;
        }
        return false;
    }
}

这段代码中,_stageWidth和_stageHeight用来存储当前的画面尺寸。通过画面尺寸来计算其他的数值。

_col变量用来表示画面地图中每一行有20个格子,也就是存在20列。有了无他的宽度和格子数量,我们可以计算出每一个格子的边长。由于格子是正方形的,所以边长记录在_blockSize变量中。

通过边长,我们可以计算出当前地图中一共有多少行。这里将行数存储在_row变量中。当舞台的高度越大,那么行数也就越大。在代码中需要注意第15行代码。

SceneData._row = Math.floor(SceneData._stageHeight / SceneData._blockSize);

计算行数一定要向下取整,以防止最后一行超出屏幕导致画面出现错误。这些基础数据我们需要在游戏一开始便初始化,所以在Main类入口中,先调用如下代码来进行数据初始化。

SceneData.setStageSize(this.stage.stageWidth, this.stage.stageHeight);

有了数据之后,先来绘制背景图。在贪吃蛇这个游戏中,背景图也是游戏的地图。通过graphics方法来绘制地图。为了让游戏更加有复古的感觉,我们可以从网上寻找一些参考图片,并通过绘图软件进行取色。背景色中,主要涉及到两个颜色,其色值分别为0x94A27D和0x8D9A75。你也可以尝试其他颜色来绘制出不同的效果。新建一个Background类,用来绘制背景,代码如下:

class Background {

    private readonly bgColor: number = 0x94A27D;
    private readonly color: number = 0x8D9A75;
    private _row: number = 0;
    private _col: number = 0;

    constructor(row: number, column: number) {
        this._row = row;
        this._col = column;
    }

    public draw(width: number, height: number): egret.DisplayObjectContainer {
        let con: egret.DisplayObjectContainer = new egret.DisplayObjectContainer();

        let bg: egret.Shape = new egret.Shape();
        bg.graphics.beginFill(this.bgColor);
        bg.graphics.drawRect(0, 0, width, height);
        bg.graphics.endFill();
        con.addChild(bg);
        bg.touchEnabled = true;

        let b: egret.DisplayObject = null;
        for (let r: number = 0; r < this._row; r++) {
            for (let c: number = 0; c < this._col; c++) {
                b = Block.getBlock(this.color);
                b.x = b.width * c;
                b.y = b.height * r;
                con.addChild(b);
            }
        }
        return con;
    }
}

我们将整个背景图都会知道一个Shape对象中,然后将其返回。主要的绘图逻辑在draw函数中。在Main类中,调用一下代码将背景放到显示列表中。

let bg: Background = new Background(SceneData.Row, SceneData.Col);
this.addChild(bg.draw(SceneData.stageWidth, SceneData.stageHeight));

Background类中涉及到一个名为Block的类,下面将进行详细讲解。

在这个贪吃蛇游戏中,无论是蛇还是背景图中的方块,其实都是有同一种大小相同的方块构成的。其不同点在于颜色会略有差异。这是老旧的点阵显示屏的显示效果。为此,我们将这个方块的绘制方法进行封装,单独放到Block类中,其代码如下:

class Block {

    public static getBlock(color: number = 0x06050E): egret.DisplayObject {
        let b: egret.Shape = new egret.Shape();
        b.graphics.lineStyle(2, color);
        b.graphics.drawRect(1, 1, SceneData.BlockSize - 2, SceneData.BlockSize - 2);
        b.graphics.endFill();
        b.graphics.beginFill(color);
        b.graphics.drawRect(5, 5, SceneData.BlockSize - 10, SceneData.BlockSize - 10);
        b.graphics.endFill();
        return b;
    }

}

注意其中的尺寸问题,每个方块都拥有一个边框,这个边框也是占用尺寸的。如果你忘记减去边框尺寸,放么方块的尺寸可能和设想的完全不一样。最后导致画面内方块的位置都出现了偏移。

接下来我们定义两个非常重要的东西一个用来表示位置,另外一个用来表示方向。

Pos类用来表示位置,代码如下:

class Pos {
    public Row:number = 0;
    public Col:number = 0;
}

Direction类用来表示方向,代码如下:

enum Direction {
    UP = -1,
    DOWN = 1,
    LEFT = -2,
    RIGHT = 2
}

需要说明的是为什么Direction枚举中我们要使用数字来表示方向,而不是使用字符串。由于贪吃蛇游戏中一个设计因素导致我们这样设计在计算上比较方便。当蛇向右移动时,我们可以让蛇向上或者向下转动方向,但不可向左。也就是说蛇的运行方向不可以是当前方向的反方向。在做此判断是,使用数字做一次加法运算即可判定出当前方向与用户控制方向是否相反。只要两个方向相加为0,即手势操作无效。该逻辑在Main类中changeDir函数有实现,代码如下:

private changeDir(e: SimpleGestureEvent) {
    this.dir = (this.dir + e.dir) != 0 ? e.dir : this.dir;
}

接下来新建一个Snake类,主要实现蛇的逻辑,代码如下:

class Snake extends egret.EventDispatcher {
    private data: Array<number> = [40, 20, 0];
    private con: egret.DisplayObjectContainer;

    public draw(): egret.DisplayObjectContainer {
        if (!this.con) {
            this.con = new egret.DisplayObjectContainer();
            this.update();
        }
        return this.con;
    }

    private update(): void {
        this.con.removeChildren();
        let b: egret.DisplayObject = null;
        let p: Pos;
        for (let i: number = 0; i < this.data.length; i++) {
            b = Block.getBlock();
            p = SceneData.getPos(this.data[i]);
            b.x = b.width * p.Col;
            b.y = b.height * p.Row;
            this.con.addChild(b);
        }
    }

    public up(): void {
        let curIndex: number = this.data[0];
        let nextIndex = curIndex - SceneData.Col;
        this.move(curIndex, nextIndex, Direction.UP);
    }

    public down(): void {
        let curIndex: number = this.data[0];
        let nextIndex = curIndex + SceneData.Col;
        this.move(curIndex, nextIndex, Direction.DOWN);
    }

    public left(): void {
        let curIndex: number = this.data[0];
        let nextIndex = curIndex - 1;
        this.move(curIndex, nextIndex, Direction.LEFT);
    }

    public right(): void {
        let curIndex: number = this.data[0];
        let nextIndex = curIndex + 1;
        this.move(curIndex, nextIndex, Direction.RIGHT);
    }

    private move(curIndex: number, nextIndex: number, dir: Direction): void {
        if (this.eat(nextIndex)) {
            this.data.unshift(nextIndex);
            this.dispatchEvent(new SnakeEvent(SnakeEvent.EAT));
            this.update();
        }

        if (this.collision(curIndex, nextIndex, dir)) {
            //发生碰撞了!
            this.dispatchEvent(new SnakeEvent(SnakeEvent.GAME_OVER));
            return;
        }
        this.data.pop();
        this.data.unshift(nextIndex);
        this.update();
    }

    private eat(index: number): boolean {
        return index === Food.data ? true : false;
    }

    private collision(curIndex: number, nextIndex: number, dir: Direction): boolean {
        if (this.collisionSelf(nextIndex)) {
            return true;
        }
        return SceneData.outMapBound(curIndex, dir);
    }

    public collisionSelf(index: number): boolean {
        for (let i: number = 0; i < this.data.length; i++) {
            if (this.data[i] === index) {
                return true;
            }
        }
        return false;
    }
}

class SnakeEvent extends egret.Event {
    public static EAT = "eat";
    public static GAME_OVER = "GAME_OVER";
    public constructor(type: string, bubbles: boolean = false, cancelable: boolean = false) {
        super(type, bubbles, cancelable);
    }
}

data变量中,存储了蛇身体所占的格子位置。每一个格子都有一个编号,第一个格子编号为0。从左上角开始,按照中文阅读顺序依次递增。默认蛇身体占3个格子,分别为0,20,40。在左上角纵向排列。

Snake类实现了5个功能,你需要仔细阅读其中的public权限接口就能明白他们的用途。draw函数,负责绘制蛇的画面。up,down,left,right四个函数分别控制蛇的上下左右四个方向的移动。当调用left函数时,蛇就向左移动一个格子。每一个函数中,仅仅是对蛇的数据进行计算变化,移动操作则统一放到了move函数中进行处理。

在move函数中,首先要进行吃东西的计算,来判断当前的蛇是否和地图中生成的食物发生了碰撞,如果碰撞,则蛇的身体增加一个长度。接下来再对边界和自身进行碰撞检测,当放生碰撞时,则游戏结束。

当蛇吃到食物或者发生碰撞时,则发送自定义事件,事件类型分别为SnakeEvent.EAT和SnakeEvent.GAME_OVER。

最后一个重要元素则是食物。我们将食物的逻辑放到Food类中,代码如下:

class Food {

    public static data: number = 0;
    public static create(): number {
        Food.data = Math.floor(Math.random() * SceneData.Row * SceneData.Col);
        return Food.data;
    }

    private con: egret.DisplayObjectContainer;
    public draw(): egret.DisplayObjectContainer {
        if (!this.con) {
            this.con = new egret.DisplayObjectContainer();
            this.update();
        }
        return this.con;
    }
    public update(): void {
        this.con.removeChildren();
        let b: egret.DisplayObject = Block.getBlock();
        let p: Pos = SceneData.getPos(Food.data);
        b.x = b.width * p.Col;
        b.y = b.height * p.Row;
        this.con.addChild(b);
    }

}

每一次食物的生成都是随机的,但有一点需要注意,食物所生成的位置不应该是蛇身体所在的位置。

最后我们实现Main类,代码如下:

class Main extends egret.DisplayObjectContainer {

    public constructor() {
        super();
        this.once(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);
    }
    private snake: Snake = new Snake();
    private food: Food = new Food();
    private onAddToStage(event: egret.Event) {

        SceneData.setStageSize(this.stage.stageWidth, this.stage.stageHeight);

        let bg: Background = new Background(SceneData.Row, SceneData.Col);
        this.addChild(bg.draw(SceneData.stageWidth, SceneData.stageHeight));

        this.addChild(this.snake.draw());
        this.snake.addEventListener(SnakeEvent.EAT, this.eat, this);
        this.snake.addEventListener(SnakeEvent.GAME_OVER, this.gameOver, this);

        let collision: boolean = true;
        while (collision) {
            collision = this.snake.collisionSelf(Food.create());
        }

        this.addChild(this.food.draw());

        let t: egret.Timer = new egret.Timer(500);
        t.addEventListener(egret.TimerEvent.TIMER, this.moven, this);
        t.start();

        let gesture: SimpleGesture = new SimpleGesture(this);
        gesture.addEventListener(SimpleGestureEvent.CHANGE_DIR, this.changeDir, this);
    }

    private dir: number = Direction.DOWN;
    private changeDir(e: SimpleGestureEvent) {
        this.dir = (this.dir + e.dir) != 0 ? e.dir : this.dir;
    }

    private moven(): void {
        switch (this.dir) {
            case Direction.UP:
                this.snake.up();
                break;
            case Direction.DOWN:
                this.snake.down();
                break;
            case Direction.LEFT:
                this.snake.left();
                break;
            case Direction.RIGHT:
                this.snake.right();
                break;
        }
    }

    private eat(e: SnakeEvent): void {
        let collision: boolean = true;
        while (collision) {
            collision = this.snake.collisionSelf(Food.create());
        }
        this.food.update();
    }

    private gameOver(e: SnakeEvent): void {
        console.log('游戏结束');
    }
}

该游戏还有SimpleGesture类和Vector类没有讲解。这两个类一个负责手势另外一个是向量封装,我们在前面的章节中已讲解过,此处不再赘述。你可以查看附件中的源码。

最终游戏运行效果如图:

6.5 虚拟摇杆

很多手游都采用虚拟摇杆的方式来实现控制人物移动或者瞄准镜的移动。其实现原理非常简单,只要细心思考所有人都能够想到一个实现方式。在很多教程或者博客中,最常见的一种方式是通过计算摇杆所产生的角度(借助三角函数实现)来设置人物的移动方向。更近一步的话,在移动时,乘以一个系数用来控制移动速度。这种方式确实可以实现摇杆效果,但如果使用向量的话,会使代码变得更加简单。

前面我们使用向量来控制物体的移动,在制作虚拟摇杆的时候只要使摇杆产生我们所需要的向量即可。

为方便说明原理,我们看下面的图。

在摇杆中,包含两层图片。下面一层是一个范围框,表示摇杆可以拖拽的范围区域。而上面一层是我们可以拖拽的摇杆主体部分。默认情况下,摇杆主体部分的圆形与外层范围框的圆形重合,也就是出于正中央的位置。当用户拖拽的时候,摇杆主体能移动到的最远距离恰巧是主体边际与范围框重合。

当前假设圆心位于坐标系的原点,摇杆范围半径的R,摇杆主体半径为r的话,那么摇杆主体能够移动的范围半径是R-r像素。如下图,斜线区域为摇杆可移动区域。

新建一个名为Joysticks的类,该类实现虚拟摇杆功能,根据上面的描述,在Joysticks初始化时,我们需要设定摇杆最大移动范围半径R和摇杆主体半径r。并根据这两个参数来绘制基本图形,绘图代码如下:

private R: number = 0;
private r: number = 0;

private background: egret.Shape;
private joystick: egret.Shape;

private draw(): void {
    let bg: egret.Shape = new egret.Shape();
    bg.graphics.lineStyle(1);
    bg.graphics.drawCircle(0, 0, this.R);
    bg.graphics.endFill();
    this.background = bg;

    let joys: egret.Shape = new egret.Shape();
    joys.graphics.beginFill(0xff0000);
    joys.graphics.drawCircle(0, 0, this.r);
    joys.graphics.endFill();
    this.joystick = joys;

    this.addChild(this.background);
    this.addChild(this.joystick);
}

绘图完成后,需要监听Touch触摸动作,当手指在屏幕上滑动时,需要改变摇杆主体的位置,并根据摇杆所处的位置计算出一个向量。该向量就是人物移动时所需要的向量。来看下产生移动时的处理函数,也是模拟摇杆中最为核心的代码:

private joystickV: Vector = Vector.zero();
private moveJoyStick() {
    if (!this.joystick) {
        return;
    }
    this.joystickV.setTo(this.touchPoint.x, this.touchPoint.y);
    let norm: number = this.joystickV.norm;

    let normScale: number = 1;
    if (norm > this.scopeR) {
        normScale = this.scopeR / norm;
    }
    this.joystickV.multiply(normScale);
    this.joystick.x = this.joystickV.x;
    this.joystick.y = this.joystickV.y;

    this.dispatchMoveEvent();
}

函数一开始需要安全检查,如果发现摇杆并没有被创建,则直接结束掉当前函数。

touchPoint这个变量代表用户手指所触摸的位置,我们需要将其中的x,y赋值给joystickV

joystickV变量主要是用来计算摇杆在本次移动后所计算出的向量。当用户移动手指后,其位置可能会在摇杆范围内,也可能超出摇杆范围。而我们要做的是当触摸位置超出摇杆范围后,需要改变其长度,也就是joystickV的模。

norm变量是joystickV当前的模,判断其值是否大于scopeR(该值为R-r)。如果超出,则表示手指已经超出摇杆范围,我们需要计算出normScale缩放系数。该系数在不改变joystickV方向的前提下,修改其大小。

如果手指触摸的位置没有超出摇杆范围,则normScale缩放系数始终为1。

借助前面的代码,将normScale缩放系数乘以joystickV,这里用到的是标量与向量相乘。其结果就是我们摇杆的位置。

处理了摇杆位置后,接下来需要将新的移动向量以事件的方式发送出去,在dispatchMoveEvent函数中发送一个自定义事件。事件中包含计算后的向量值,代码如下:

private eventVector: Vector = Vector.zero();
private dispatchMoveEvent() {
    this.eventVector.copy(this.joystickV);
    let scale: number = this.eventVector.norm / this.scopeR;
    this.eventVector.normalize();
    this.eventVector.multiply(scale * this.maxSpeed);

    let event: JoystickEvent = new JoystickEvent(JoystickEvent.CHANGE);
    event.v = this.eventVector;
    this.dispatchEvent(event);
}

上面的函数中,需要先计算要发送的向量值。eventVector值设置与joystickV值相同,scale是速度(向量的大小)的缩放系数,它的值等于joystickV的模除以scopeR最大移动半径。

得到缩放系数后,乘以最大速度maxSpeed,得到的即是此次移动的速度大小。为了得到正确的向量,需要将eventVector进行归一化处理,然后乘以大小。

向量的归一化,即不改变向量的方向,使其模为1。实现代码如下:

Vector.ts

public normalize(): void {
    if (this.norm === 0) {
        this.setTo(0, 0);
        return;
    }
    this.multiply(1 / this.norm);
}

需要注意的一点是,向量的模可能为0。在除法公式中,0不可作为除数,无数学意义,故需要针对模为0的情况进行特殊处理。

最后我们在Main.ts中实例化摇杆,并用它来控制另外一个方块的移动。

Main.ts

class Main extends egret.DisplayObjectContainer {
    public constructor() {
        super();
        this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);
    }

    private onAddToStage(event: egret.Event) {
        this.draw();
    }

    private rect: MoveShape = null;
    private speed: Vector = Vector.zero();
    private draw() {
        let bg: MoveShape = new MoveShape();
        bg.graphics.beginFill(0xcccccc);
        bg.graphics.drawRect(0, 0, this.stage.stageWidth, this.stage.stageHeight);
        bg.graphics.endFill();
        this.addChild(bg);
        bg.touchEnabled = true;

        let shp: MoveShape = new MoveShape();
        shp.graphics.beginFill(0x00ff00);
        shp.graphics.drawRect(0, 0, 50, 50);
        shp.graphics.endFill();
        this.addChild(shp);
        this.rect = shp;

        this.touchEnabled = true;
        this.touchChildren = true;
        let joysticks: Joysticks = new Joysticks(150, 60, bg);
        joysticks.x = joysticks.y = 300;
        this.addChild(joysticks);
        joysticks.addEventListener(JoystickEvent.START, this.startHandle, this);
        joysticks.addEventListener(JoystickEvent.CHANGE, this.changeHandle, this);
        joysticks.addEventListener(JoystickEvent.END, this.endHandle, this);
    }

    private startHandle(e: JoystickEvent): void {
        this.addEventListener(egret.Event.ENTER_FRAME, this.move, this);
    }

    private endHandle(e: JoystickEvent): void {
        this.removeEventListener(egret.Event.ENTER_FRAME, this.move, this);
    }

    private changeHandle(e: JoystickEvent): void {
        this.speed.copy(e.v);
    }

    private move(e: egret.Event): void {
        this.rect.move(this.speed);
    }
}

Joysticks.ts

class Joysticks extends egret.DisplayObjectContainer {

    private R: number = 0;
    private r: number = 0;
    private scopeR: number = 0;
    private maxSpeed: number = 0;

    private background: egret.Shape;
    private joystick: egret.Shape;
    private touchTrigger: egret.DisplayObject;

    public constructor(R: number, r: number, trigger: egret.DisplayObject, maxSpeed: number = 10) {
        super();
        this.R = R;
        this.r = r;
        this.scopeR = R - r;
        this.touchTrigger = trigger;
        this.maxSpeed = maxSpeed;
        this.draw();
        this.addlistener();
    }

    private draw(): void {
        let bg: egret.Shape = new egret.Shape();
        bg.graphics.lineStyle(1);
        bg.graphics.drawCircle(0, 0, this.R);
        bg.graphics.endFill();
        this.background = bg;

        let joys: egret.Shape = new egret.Shape();
        joys.graphics.beginFill(0xff0000);
        joys.graphics.drawCircle(0, 0, this.r);
        joys.graphics.endFill();
        this.joystick = joys;

        this.addChild(this.background);
        this.addChild(this.joystick);
    }

    private addlistener() {
        this.touchTrigger.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.touchBegin, this);
    }

    private touchPoint: egret.Point = new egret.Point();
    private touchBegin(e: egret.TouchEvent) {
        this.dispatchStartEvent();
        this.globalToLocal(e.stageX, e.stageY, this.touchPoint);
        this.moveJoyStick();
        this.touchTrigger.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.touchMove, this);
        this.touchTrigger.addEventListener(egret.TouchEvent.TOUCH_END, this.touchEnd, this);
    }

    private touchMove(e: egret.TouchEvent) {
        this.globalToLocal(e.stageX, e.stageY, this.touchPoint);
        this.moveJoyStick();
    }

    private touchEnd(e: egret.TouchEvent) {
        this.touchTrigger.removeEventListener(egret.TouchEvent.TOUCH_MOVE, this.touchMove, this);
        this.touchTrigger.removeEventListener(egret.TouchEvent.TOUCH_END, this.touchEnd, this);
        this.touchPoint.x = 0;
        this.touchPoint.y = 0;
        this.moveJoyStick();
        this.dispatchEndEvent();
    }

    private joystickV: Vector = Vector.zero();
    private moveJoyStick() {
        if (!this.joystick) {
            return;
        }
        this.joystickV.setTo(this.touchPoint.x, this.touchPoint.y);
        let norm: number = this.joystickV.norm;

        let normScale: number = 1;
        if (norm > this.scopeR) {
            normScale = this.scopeR / norm;
        }
        this.joystickV.multiply(normScale);
        this.joystick.x = this.joystickV.x;
        this.joystick.y = this.joystickV.y;

        this.dispatchMoveEvent();
    }

    private eventVector: Vector = Vector.zero();
    private dispatchMoveEvent() {
        this.eventVector.copy(this.joystickV);
        let scale: number = this.eventVector.norm / this.scopeR;
        this.eventVector.normalize();
        this.eventVector.multiply(scale * this.maxSpeed);

        let event: JoystickEvent = new JoystickEvent(JoystickEvent.CHANGE);
        event.v = this.eventVector;
        this.dispatchEvent(event);
    }

    private dispatchStartEvent() {
        let event: JoystickEvent = new JoystickEvent(JoystickEvent.START);
        this.dispatchEvent(event);
    }

    private dispatchEndEvent() {
        let event: JoystickEvent = new JoystickEvent(JoystickEvent.END);
        this.dispatchEvent(event);
    }
}

JoystickEvent.ts

class JoystickEvent extends egret.Event {
    public static START: string = "JOYSTICK_START";
    public static CHANGE: string = "JOYSTICK_MOVE";
    public static END: string = "JOYSTICK_END";

    public v: Vector = null;
    public constructor(type: string, bubbles: boolean = false, cancelable: boolean = false) {
        super(type, bubbles, cancelable);
    }
}

在该实例中,调用了Vector类的copysetTo方法,这两个方法的实现如下:

/**
 * 复制另外一个向量的值
 */
public copy(v: Vector): void {
    this.x = v.x;
    this.y = v.y;
}

/**
 * 设置向量的值
 */
public setTo(x: number, y: number): void {
    this.x = x;
    this.y = y;
}

6.6 复杂手势

这个算法是我在传媒大学授课时候给学生们讲过的一种识别算法。由于它的原理非常简单,计算也并不复杂非常适合学习练手。与此同时,在终端手写识别算法中,我所讲解的算法在计算上也非常节约性能,但识别精度却不是很高。

由于是游戏中使用,其实我们不需要对复杂的汉字,或者文字字符进行识别,有的仅仅是非常简单的符号。下图中所示的符号,则是游戏中出现的一些常见符号。

我们可以将这些基本的符号总结为“|—V^ZC”等。这对于我们学习手写识别的算法试验也更加简单一些。我们来看一下具体的实现思路与方法(实例中的代码采用Egret编写)。

第一步:设置一个八方向坐标

第二步:将八个风向平均分为八个区域

第三步:将八个区域进行编号

第四步:对应每个编号,计算出它的旋转角度范围

第五步:采集用户对于屏幕触摸时候的数据

代码如下:

//注册侦听
egret.MainContext.instance.stage.addEventListener(egret.TouchEvent.TOUCH_BEGIN,this.mouseDown,this);
egret.MainContext.instance.stage.addEventListener(egret.TouchEvent.TOUCH_END,this.mouseUp,this);
egret.MainContext.instance.stage.addEventListener(egret.TouchEvent.TOUCH_MOVE,this.mouseMove,this);

//响应函数
private mouseDown(evt:egret.TouchEvent)
{
    this._layer.graphics.clear();
    this._mouseDatas = [];
    var p:egret.Point = new egret.Point(evt.stageX,evt.stageY);
    this._mouseDatas.push(p);
    this._currentPoint = p;
}
private mouseMove(evt:egret.TouchEvent)
{
    var p:egret.Point = new egret.Point(evt.stageX,evt.stageY);
    this._mouseDatas.push(p);
    this._layer.graphics.lineStyle(5,0) ;
    this._layer.graphics.moveTo(this._currentPoint.x,this._currentPoint.y);
    this._layer.graphics.lineTo(p.x,p.y);
    this._layer.graphics.endFill();
    this._currentPoint = p;
}
private mouseUp(evt:egret.TouchEvent)
{
    var p:egret.Point = new egret.Point(evt.stageX,evt.stageY);
    this._mouseDatas.push(p);
    this._layer.graphics.clear();
    this.motion();
}

第六步:数据降噪操作

我们看到,不同的手机屏幕响应和游戏FPS都会影响到我们采样的数据。现在我们拥有了很多坐标点信息,这些坐标点都是用户手指划过的地方。我们需要对一些较为密集的点进行降噪操作。具体的数值需要进行反复试验。我这里将两个采样数据点的间距设置为大于30px。

private motion()
{
    var _arr:egret.Point[] = [];
    var currentIndex:number = 0;
    var len:number = this._mouseDatas.length;
    _arr.push(this._mouseDatas[currentIndex]);
    for(var i:number=0; i<len; i++)
    {
        if( egret.Point.distance(this._mouseDatas[currentIndex], this._mouseDatas[i])>30 )
        {
            currentIndex = i;
            _arr.push(this._mouseDatas[currentIndex]);
        }
    }
    this._mouseDatas = _arr;
    this.parseDirection();
}

第七步:将采样数据转换为方向序列

两个点练成一条线段肯定会有方向,也会有角度。在第四步中,我们已经计算出每个角度范围内所代表的方向编号。现在我们需要将这个方向编号序列计算出来

private parseDirection()
{
    this._dirsArr = [];
    var len:number = this._mouseDatas.length;
    for(var i:number=0; i<len; i++)
    {
        if( this._mouseDatas[i+1])
        {
            var p1:egret.Point = this._mouseDatas[i];
            var p2:egret.Point = this._mouseDatas[i+1];
            var a:number = p1.y - p2.y;
            var b:number = egret.Point.distance(p1,p2);
            var rad:number = Math.asin( a/b );
            var ang:number = rad * 57.2957800; // rad * 180/Math.PI 直接求常量,优化
            var quad:number = this.quadrant(p1,p2);
            var dir:number = this.getDirByAngQuad(ang, quad);
            this._dirsArr.push(dir);
        }
    }
}
/*
    根据所在象限与角度计算出方向编号。
    方向编号,以第一象限0度为基础,按照顺时针方向,将圆等分为8份。
     */
    private getDirByAngQuad(ang:number,quad:number):number
    {
        switch(quad)
        {
            case 1:
                if( ang<=22.5 && ang>= 0 )
                {
                    return 1;
                }
                else if( ang<= 67.5 && ang> 22.5 )
                {
                    return 8;
                }
                else
                {
                    return 7;
                }
                break;
            case 2:
                if( ang<=22.5 && ang>=0 )
                {
                    return 5;
                }
                else if( ang<= 67.5 && ang> 22.5 )
                {
                    return 6;
                }
                else
                {
                    return 7;
                }
                break;
            case 3:
                if( ang<= -67.5 && ang>= -90 )
                {
                    return 3;
                }
                else if( ang<=-22.5 && ang> -67.5 )
                {
                    return 4;
                }
                else{
                    return 5;
                }
                break;
            case 4:
                if( ang<=-67.5 && ang>= -90 )
                {
                    return 3;
                }
                else if( ang<=-22.5 && ang>-67.5)
                {
                    return 2;
                }
                else{
                    return 1;
                }
                break;
        }
    }
/*
计算两点关系所形成的象限
以P1 作为坐标原点,P2为设定点,判断P2相对于P1时所在象限
 */
private quadrant(p1:egret.Point,p2:egret.Point):number
{
    if(p2.x>=p1.x)
    {
        if( p2.y <= p1.y )
        {
            return 1;
        }
        else
        {
            return 4;
        }
    }
    else
    {
        if( p2.y <= p1.y )
        {
            return 2;
        }
        else
        {
            return 3;
        }

第八步:方向数据去重操作

我们在第七步生成了方向序列,但是这个数据中可能会出现这样的现象。例如:2,3,1,4,5,3,3,3,3。

不难发现,最后是4个3相连。对于我们后续的步骤来说,用户所绘制的线路方向,每两个相邻的方向都应不同。所以这里我们需要做去重操作。代码如下:

/*
对比去重
 */
private repDiff(data:number[]):string
{
    var str:string = "";
    var len:number = data.length;
    var currentType:number = 0;
    for(var i:number=0; i<len; i++)
    {
        if( currentType != data[i])
        {
            currentType = data[i];
            str += data[i];
        }
    }
    return str;
}

第九步:识别对比

第八步中我们得到了一个字符串,事实上字符串内容也就是我们所解析出来的方向数据。这一步我们要和已经定义好的数据进行对比。最终会得出一个相似率。如果相似率超过一定百分比,我们就认为用户书写是正确的。

对于两个字符串比较相似度,我们直接使用Levenshtein Distance算法。

下面是我们预先定义的答案的数据。需要注意的是笔顺有正有反,两种都要考虑到。

V:"28","46"
| :"3","7"
^ :"82","64"
- :"5","1"
Z:"141","585"

对比代码如下:

private sweep( str:string ):number
{
    var maxType:number = -1;
    var max:number = -1;
    var len:number = this._symbol.length;
    for(var i:number=0; i<len; i++)
    {
        var val:number = this.Levenshtein_Distance_Percent(this._symbol[i], str);
        if(val>max)
        {
            max = val;
            maxType = this._symbolG[i];
        }
    }
    if(max<0.4)
    {
        maxType = -1;
    }
    return maxType;
}
private Levenshtein_Distance(s,t)
{
    var n=s.length;// length of s
    var m=t.length;// length of t
    var d=[];// matrix
    var i;// iterates through s
    var j;// iterates through t
    var s_i;// ith character of s
    var t_j;// jth character of t
    var cost;// cost
    // Step 1
    if (n == 0) return m;
    if (m == 0) return n;
    // Step 2
    for (i = 0; i <= n; i++) {
        d[i]=[];
        d[i][0] = i;
    }
    for (j = 0; j <= m; j++) {
        d[0][j] = j;
    }
    // Step 3
    for (i = 1; i <= n; i++) {
        s_i = s.charAt (i - 1);
        // Step 4
        for (j = 1; j <= m; j++) {
            t_j = t.charAt (j - 1);
            // Step 5
            if (s_i == t_j) {
                cost = 0;
            }else{
                cost = 1;
            }
            // Step 6
            d[i][j] = this.Minimum (d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1] + cost);
        }
    }
    // Step 7
    return d[n][m];
}
private Levenshtein_Distance_Percent(s,t):number{
    var l=s.length>t.length?s.length:t.length;
    var d=this.Levenshtein_Distance(s,t);
    return (1-d/l);//.toFixed(4);
}
private Minimum(a,b,c){
    return a<b?(a<c?a:c):(b<c?b:c);
}

我将对比数据设置为40%,当相似率大于40%的时候,就认为用户书写正确。通过这种方式来调整游戏的难易度。