炸弹人游戏开发系列(6):实现碰撞检测,设置移动步长

前言

上文中我们实现了“玩家控制炸弹人”的功能,本文将实现碰撞检测,让炸弹人不能穿过墙。在实现的过程中会发现炸弹人移动的问题,然后会通过设置移动步长来解决。

说明

名词解释

  • 具体状态类
    指应用于炸弹人移动状态的状态模式的ConcreState角色的类。这里具体包括WalkLeftState、WalkRightState、WalkUpState、WalkDownState、StandLeftState等类。

本文目的

实现碰撞检测

本文主要内容

回顾上文更新后的领域模型

查看大图

对领域模型进行思考

重构PlayerSprite

重构前代码

(function () {
    var PlayerSprite = YYC.Class({
        //供子类构造函数中调用
        Init: function (data) {
            this.x = data.x;
            this.y = data.y;

            this.minX = data.minX;
            this.maxX = data.maxX;
            this.minY = data.minY;
            this.maxY = data.maxY;

            this.defaultAnimId = data.defaultAnimId;
            this.anims = data.anims;

            this.walkSpeed = data.walkSpeed;

            this._context = new Context(this);
        },
        Private: {
            _context: null,

            _setCoordinate: function (deltaTime) {
                this.x = this.x + this.speedX * deltaTime;
                this.y = this.y + this.speedY * deltaTime;

                this._limitMove();
            },
            _limitMove: function () {
                this.x = Math.max(this.minX, Math.min(this.x, this.maxX));
                this.y = Math.max(this.minY, Math.min(this.y, this.maxY));
            },
            _getCurrentState: function () {
                var currentState = null;

                switch (this.defaultAnimId) {
                    case "stand_right":
                        currentState = Context.standRightState;
                        break;
                    case "stand_left":
                        currentState = Context.standLeftState;
                        break;
                    case "stand_down":
                        currentState = Context.standDownState;
                        break;
                    case "stand_up":
                        currentState = Context.standUpState;
                        break;
                    case "walk_down":
                        currentState = Context.walkDownState;
                        break;
                    case "walk_up":
                        currentState = Context.walkUpState;
                        break;
                    case "walk_right":
                        currentState = Context.walkRightState;
                        break;
                    case "walk_left":
                        currentState = Context.walkLeftState;
                        break;
                    default:
                        throw new Error("未知的状态");
                        break;
                };

                return currentState;
            }
        },
        Public: {
            //精灵的坐标
            x: 0,
            y: 0,
            //精灵的速度
            walkSpeed: 0,

            speedX: 0,
            speedY: 0,

            //精灵的坐标区间
            minX: 0,
            maxX: 9999,
            minY: 0,
            maxY: 9999,

            anims: null,
            //默认的Animation的Id , string类型
            defaultAnimId: null,
            //当前的Animation.
            currentAnim: null,

            init: function () {
                this._context.setPlayerState(this._getCurrentState());

                //设置当前Animation
                this.setAnim(this.defaultAnimId);
            },
            //重置当前帧
            resetCurrentFrame: function (index) {
                this.currentAnim && this.currentAnim.setCurrentFrame(index);
            },
            //设置当前Animation, 参数为Animation的id, String类型
            setAnim: function (animId) {
                this.currentAnim = this.anims[animId];
            },
            // 更新精灵当前状态.
            update: function (deltaTime) {
                //每次循环,改变一下绘制的坐标
                this._setCoordinate(deltaTime);

                if (this.currentAnim) {
                    this.currentAnim.update(deltaTime);
                }
            },
            draw: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight);
                }
            },
            clear: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                }
            },
            handleNext: function () {
                this._context.walkLeft();
                this._context.walkRight();
                this._context.walkUp();
                this._context.walkDown();
                this._context.stand();
            }
        }
    });

    window.PlayerSprite = PlayerSprite;
}());
View Code

  handleNext改名为changeDir

反思handleNext方法。从方法名来看,它的职责应该为处理本次循环的所有逻辑。然而,经过数次重构后,现在handleNext的职责只是调用状态类的方法,更具体的来说,它的职责为判断和设置炸弹人移动方向。

因此,应该将handleNext改名为changeDir,从而能够反映出它的职责。

  从update方法中分离出move方法

再来审视update方法,发现它有两个职责:

  • 更新坐标
  • 更新动画

进一步思考,此处“更新坐标”的职责更抽象地来说应该为"炸弹人移动“的职责。应该将其提出,形成move方法。然后去掉”__setCoordinate“方法,将其代码直接写到move方法中

  删除deltaTime

            _setCoordinate: function (deltaTime) {
                this.x = this.x + this.speedX * deltaTime;
                this.y = this.y + this.speedY * deltaTime;

                this._limitMove();
            },

这里deltaTime其实没有什么作用,因此将其删除。

  重构后相关代码

PlayerSprite

            update: function (deltaTime) {
                 if (this.currentAnim) {
                    this.currentAnim.update(deltaTime);
                }
            },
            draw: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight);
                }
            },
            clear: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                }
            },
            move: function () {
                this.x = this.x + this.speedX;
                this.y = this.y + this.speedY;

                this._limitMove();
            },
            changeDir: function () {
                this._context.walkLeft();
                this._context.walkRight();
                this._context.walkUp();
                this._context.walkDown();
                this._context.stand();
            }

要对应修改PlayerLayer

          __changeDir: function () {
                this.___iterator("changeDir");
            },
            ___move: function () {
                this.___iterator("move");
            },
...
            render: function () {
                if (this.P__isChange()) {
                    this.clear(this.P__context);
                    this.__changeDir();
                    this.___move();
                    this.___update();
                    this.draw(this.P__context);
                    this.P__setStateNormal();
                }
            }    

分离speedX/speedY属性的语义,提出“方向向量”概念dirX/dirY

状态类WalkLeftState

            walkLeft: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.A] === true) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = -sprite.walkSpeed;
                    sprite.speedY = 0;

                    sprite.setAnim("walk_left");
                }
            },

目前是通过在具体状态类中改变speedX/speedY的正负(如+sprite.walkSpeed或-sprite.walkSpeed),来实现炸弹人移动方向的改变。因此,我发现speedX/speedY属性实际上有两个语义:

  • 炸弹人移动速度
  • 炸弹人移动方向

这样会造成speed语义混淆,不便于阅读和维护。因此,将“炸弹人移动方向”提出来,形成新的属性dirX/dirY,而speedX/speedY则保留“炸弹人移动速度”语义。

  重构后相关代码

PlayerSprite

            dirX: 0,
            dirY: 0,
...
            move: function () {
                this.x = this.x + this.speedX * this.dirX;
                this.y = this.y + this.speedY * this.dirY;

                this._limitMove();
            },    

WalkLeftState(其它具体状态类也要做类似的修改)

            walkLeft: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.A] === true) {
                    sprite = this.P_context.sprite;
                    sprite.dirX = -1;
                    sprite.dirY = 0;

                    sprite.setAnim("walk_left");
                }
            },

开发策略

首先查阅相关资料,确定碰撞检测的方法,然后再实现炸弹人与地图砖墙的碰撞检测。

初步实现碰撞检测

提出“碰撞检测”的概念

在第2篇博文中提出了“碰撞检测”的概念:

用于检测炸弹人与砖墙、炸弹人与怪物等之间的碰撞。碰撞检测包括矩形碰撞、多边形碰撞等,一般使用矩形碰撞即可。

此处我采用矩形碰撞检测。

增加地形数据TerrainData

首先,我们需要一个存储地图中哪些区域能够通过,哪些区域不能通过的数据结构。

通过参考地图数据mapData,我决定数据结构选用二维数组,且地形数组与地图数组一一对应。

相关代码

地图数据MapData

(function () {
    var ground = bomberConfig.map.type.GROUND,
        wall = bomberConfig.map.type.WALL;

    var mapData = [
        [ground, wall, ground, ground],
        [ground, ground, ground, ground],
        [ground, wall, ground, ground],
        [ground, wall, ground, ground]
    ];

    window.mapData = mapData;
}());

地形数据TerrainData

//地形数据
(function () {
  //0表示可以通过,1表示不能通过
    var terrainData = [
        [0, 1, 0, 0],
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 1, 0, 0]
    ];

    window.terrainData = terrainData;
}());

重构TerrainData

受到MapData的启示,可以在Config中加入地形数据的枚举值(pass、stop),然后直接在TerrainData中使用枚举值。这样做有以下的好处:

  • 增强可读性
  • 枚举值放到Config中,方便统一管理

相关代码

Config

    map: {
...
        terrain: {
            pass: 0,
            stop: 1
        }
    },

TerrainData

//地形数据
(function () {
    var pass = bomberConfig.map.terrain.pass,
        stop = bomberConfig.map.terrain.stop;

    var terrainData = [
        [pass, stop, pass, pass],
        [pass, pass, pass, pass],
        [pass, stop, pass, pass],
        [pass, stop, pass, pass]
    ];

    window.terrainData = terrainData;
}());

在PlayerSprite中实现矩形碰撞检测

实现checkCollideWithMap方法:

            _checkCollideWithMap: function () {
                var i1 = Math.floor((this.y) / bomberConfig.HEIGHT),
                    i2 = Math.floor((this.y + bomberConfig.player.IMGHEIGHT - 1) / bomberConfig.HEIGHT),
                    j1 = Math.floor((this.x) / bomberConfig.WIDTH),
                    j2 = Math.floor((this.x + bomberConfig.player.IMGWIDTH - 1) / bomberConfig.WIDTH),
                    terrainData = window.terrainData,
                    pass = bomberConfig.map.terrain.pass,
                    stop = bomberConfig.map.terrain.stop;

                if (terrainData[i1][j1] === pass && terrainData[i1][j2] === pass
                    && terrainData[i2][j1] === pass && terrainData[i2][j2] === pass) {
                    return false;
                }
                else {
                    return true;
                }
            },

在move中判断:

move: function () {
    var origin_x = this.x,
        origin_y = this.y;

    this.x = this.x + this.speedX * this.dirX;
    this.y = this.y + this.speedY * this.dirY;

    this._limitMove();

    if (this._checkCollideWithMap()) {
        this.x = origin_x;
        this.y = origin_y;
    }
},

领域模型

 

设置移动步长

发现问题

如果炸弹人每次移动0.2个方格,炸弹人想通过两个障碍物之间的空地,则炸弹人所在矩形区域必须与空地区域平行时才能通过。这通常导致玩家需要调整多次才能顺利通过。

如图所示:

 

不能通过

 

可以通过

引入”移动步长“概念  

结合参考资料”html5游戏开发-零基础开发RPG游戏-开源讲座(二)-跑起来吧英雄“,这里可以引出“移动步长”的概念:

即炸弹人一次移动一个地图方格(炸弹人一次会移动多步)。即如果一个方格长为10px,而游戏每次主循环轮询时炸弹人移动2px,则炸弹人一次需要移动5步。在炸弹人的一个移动步长完成之前,玩家不能操作炸弹人,直到炸弹人完成一个移动步长(即移动了一个方格),玩家才能操作炸弹人。

实现移动步长

提出概念

这里先提出以下概念:

  • step

移动步数,炸弹人移动一个方格需要的步数

  • completeOneMove(该标志会在后面重构中被删除)

炸弹人完成一个移动步长的标志

  • moving

炸弹人正在移动的标志

  • moveIndex

炸弹人在一次移动步长中已经移动的次数

具体实现

首先在游戏开始时,计算一次炸弹人移动一个方格需要的步数;然后在移动前,先判断是否完成一次移动步长,如果正在移动且没有完成一次步长,则moveIndex加1;在移动后,判断该次移动是否完成移动步长,并相应更新移动标志和moveIndex。

重构

将“moveIndex加1”移到状态类中

具体状态类的职责为:负责本状态的逻辑以及决定状态过渡。“moveIndex加1”这个职责属于“本状态的逻辑”,因此应该将其移到具体状态类中,封装为addIndex方法。

将按键判断移到PlayerSprite中

 “按键判断”是状态转换事件的判断,这里因为炸弹人不同状态转换为同一状态的触发事件相同,所以可以将其移到上一层的客户端(调用具体状态类的地方)中,即移到PlayerSprite的changeDir方法中。具体分析详见Javascript设计模式之我见:状态模式中的“将触发状态的事件判断移到Warrior类中”。

相关代码

PlayerSprite

...      
             _computeCoordinate: function () {
                this.x = this.x + this.speedX * this.dirX;
                this.y = this.y + this.speedY * this.dirY;

                this._limitMove();

                //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3),
                //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍),
                //因此此处需要向下取整。
                
                if (this.completeOneMove) {
                    this.x -= this.x % bomberConfig.WIDTH;
                    this.y -= this.y % bomberConfig.HEIGHT;
                }
            },
            //计算移动次数
            _computeStep: function () {
                this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX);
                this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY);
            },
            _allKeyUp: function () {
                return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false
                     && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false;
            },
            _judgeCompleteOneMoveByIndex: function () {
                if (!this.moving) {
                    return;
                }

                 if (this.moveIndex_x >= this.stepX) {
                    this.moveIndex_x = 0;
                    this.completeOneMove = true;
                }
                else if (this.moveIndex_y >= this.stepY) {
                    this.moveIndex_y = 0;
                    this.completeOneMove = true;
                }
                else {
                    this.completeOneMove = false;
                }
            },
            _judgeAndSetDir: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this._context.walkLeft();
                }
                else if (window.keyState[keyCodeMap.D] === true) {
                    this._context.walkRight();
                }
                else if (window.keyState[keyCodeMap.W] === true) {
                    this._context.walkUp();
                }
                else if (window.keyState[keyCodeMap.S] === true) {
                    this._context.walkDown();
                }
            }
...

            //一次移动步长中的需要移动的次数
            stepX: 0,
            stepY: 0,

            //一次移动步长中已经移动的次数
            moveIndex_x: 0,
            moveIndex_y: 0,

            //是否正在移动标志
            moving: false,

            //完成一次移动标志
            completeOneMove: false,

            init: function () {
                this._context.setPlayerState(this._getCurrentState());
                this._computeStep();

                this.setAnim(this.defaultAnimId);
            },
...
            move: function () {
                this._judgeCompleteOneMoveByIndex();

                this._computeCoordinate();
            },
            changeDir: function () {
                if (!this.completeOneMove && this.moving) {
                    this._context.addIndex();
                    return;
                }

                if (this._allKeyUp()) {
                    this._context.stand();
                }
                else {
                    this._judgeAndSetDir();
                }
            }
...

Context

(function () {
    var Context = YYC.Class({
        Init: function (sprite) {
            this.sprite = sprite;
        },
        Private: {
            _state: null
        },
        Public: {
            sprite: null,

            setPlayerState: function (state) {
                this._state = state;
                this._state.setContext(this);
            },
            walkLeft: function () {
                this._state.walkLeft();
            },
            walkRight: function () {
                this._state.walkRight();
            },
            walkUp: function () {
                this._state.walkUp();
            },
            walkDown: function () {
                this._state.walkDown();
            },
            stand: function () {
                this._state.stand();
            },
            addIndex: function () {
                this._state.addIndex();
            }
        },
        Static: {
            walkLeftState: new WalkLeftState(),
            walkRightState: new WalkRightState(),
            walkUpState: new WalkUpState(),
            walkDownState: new WalkDownState(),
            standLeftState: new StandLeftState(),
            standRightState: new StandRightState(),
            standUpState: new StandUpState(),
            standDownState: new StandDownState()
        }
    });

    window.Context = Context;
}());

WalkLeftState(此处只举一个状态类说明,其它状态类与该类类似):

...            
        walkLeft: function () { var sprite = this.P_context.sprite; sprite.dirX = -1; sprite.dirY = 0; sprite.setAnim("walk_left"); sprite.moving = true; this.addIndex(); }, addIndex: function () { this.P_context.sprite.moveIndex_x += 1; }
...

继续完成碰撞检测

对地图障碍物检测进行了修改,并将碰撞检测和边界检测移到具体状态类中。

相关代码

WalkLeftState(此处只举一个状态类说明,其它状态类与该类类似)

...
walkLeft: function () {
    var sprite = this.P_context.sprite;
    sprite.setAnim("walk_left");

    if (!this.checkPassMap()) {
        sprite.moving = false;
        sprite.dirX = 0;
        return;
    }

    sprite.dirX = -1;
    sprite.dirY = 0;
    sprite.moving = true;

    this.addIndex();
},
...
//检测是否可通过该地图。可以通过返回true,不能通过返回false
checkPassMap: function () {
    return !this.checkCollideWithBarrier();
},
checkCollideWithBarrier: function () {
    var pass = bomberConfig.map.terrain.pass,
        stop = bomberConfig.map.terrain.stop;

    //计算目的地地形数组下标
    var target_x = this.P_context.sprite.x / bomberConfig.WIDTH - 1,
        target_y = this.P_context.sprite.y / bomberConfig.HEIGHT;

    //超出边界
    if (target_x >= terrainData.length || target_y >= terrainData[0].length) {
        return true;
    }

    if (target_x < 0) {
        return true;
    }
    //碰撞
    if (window.terrainData[target_y][target_x] === stop) {
        return true;
    }

    return false;
}
...

重构

重构PlayerSprite

将move移到状态类中

PlayerSprite的move方法负责炸弹人的移动,其应该属于具体状态类的职责(负责本状态的逻辑),故将PlayerSprite的move移到具体状态类中。

进一步分析

将PlayerSprite的move移到具体状态类中,从职责上来进一步分析,实质是将“炸弹人移动”的职责分散到各个具体状态类中了(如WalkLeftState、WalkRightState只负责X方向的移动,WalkUpState、WalkDownState只负责Y方向的移动)

优点

增加了细粒度的控制。可以控制各个具体状态类下炸弹人的移动。

缺点

不好统一管理。当想修改“炸弹人移动”的逻辑时,可能需要修改每个具体状态类的move。

不过这个缺点可以在后面的提取具体状态类的基类的重构中解决。因为该重构会将具体状态类中“炸弹人移动”的职责汇聚到基类中。

重构addIndex

现在PlayerSprite -> changeDir中不用调用addIndex方法了,可以直接在具体状态类的move方法中调用。

这样做的好处是具体状态类不用再公开addIndex方法了,而是将其私有化。

为什么把公有方法addIndex改为私有方法比较好?

这是因为改动一个类的私有成员时,只会影响到该类,而不会影响到与该类关联的其它类;而改动公有成员则可能会影响与之关联的其它类。特别当我们是在创建供别人使用的类库时,如果发布后再来修改公有成员,会对很多人造成影响!这也是符合“高内聚低耦合”的思想。

我们应该对公有权限保持警惕的态度,能设成私有的就私有,只公开必要的接口成员。

相关代码

PlayerSprite

           move: function () {
                this._context.move();
            },

WalkLeftState(WalkRightState与之类似)

            move: function () {
                if (this.P_context.sprite.moving) {
                    this.addIndex();
                }

                this.__judgeCompleteOneMoveByIndex();
                this.__computeCoordinate();
            },
            __addIndex: function(){
                this.P_context.sprite.moveIndex_x += 1;
            },
            __judgeCompleteOneMoveByIndex: function () {
                var sprite = this.P_context.sprite;

                if (!sprite.moving) {
                    return;
                }

                if (sprite.moveIndex_x >= sprite.stepX) {
                    sprite.moveIndex_x = 0;
                    sprite.completeOneMove = true;
                }
                else {
                    sprite.completeOneMove = false;
                }
            },
            __computeCoordinate: function () {
                var sprite = this.P_context.sprite;

                sprite.x = sprite.x + sprite.speedX * sprite.dirX;

                //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3),
                //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍),
                //因此此处需要向下取整。


                //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整)
                if (sprite.completeOneMove) {
                    sprite.x -= sprite.x % bomberConfig.WIDTH;
                }
            }

WalkUpState(WalkDownState与之类似)

            move: function () {
                if (this.P_context.sprite.moving) {
                    this.addIndex();
                }

                this.__judgeCompleteOneMoveByIndex();
                this.__computeCoordinate();
            },
            __addIndex: function(){
                this.P_context.sprite.moveIndex_y += 1;
            },
            __judgeCompleteOneMoveByIndex: function () {
                var sprite = this.P_context.sprite;

                if (!sprite.moving) {
                    return;
                }

                if (sprite.moveIndex_y >= sprite.stepY) {
                    sprite.moveIndex_y = 0;
                    sprite.completeOneMove = true;
                }
                else {
                    sprite.completeOneMove = false;
                }
            },
            __computeCoordinate: function () {
                var sprite = this.P_context.sprite;

                sprite.y = sprite.y + sprite.speedY * sprite.dirY;

                //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3),
                //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍),
                //因此此处需要向下取整。


                //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整)
                if (sprite.completeOneMove) {
                    sprite.y -= sprite.y % bomberConfig.HEIGHT;
                }
            }

重构状态模式

让我们来看看状态类。

思路

我发现具体状态类有很多重复的代码,有些方法有很多相似之处。这促使我提炼出一个高层的共同模式。具体的方法就是提炼出基类,然后用模板模式,在子类中实现不同点。

提炼出WalkState、StandState

因此,我从WalkLeftState,WalkRightState,WalkDownState,WalkUpState中提炼出基类WalkState,从StandLeftState、StandRightState、StandDownState、StandUpState中提炼出基类StandState。

提炼出WalkState_X、WalkState_Y

我发现在WalkLeftState,WalkRightState中和WalkDownState,WalkUpState中,它们分别有共同的模式,而这共同模式不能提到WalkState中。因此,我又从WalkLeftState,WalkRightState中提炼出WalkState_X,WalkDownState,WalkUpState中提炼出WalkState_Y,然后让WalkState_X和WalkState_Y继承于WalkState。

状态模式最新的领域模型

相关代码

PlayerState

(function () {
    var PlayerState = YYC.AClass({
        Protected: {
            P_context: null
        },
        Public: {
            setContext: function (context) {
                this.P_context = context;
            }
        },
        Abstract: {
            stand: function () { },
            walkLeft: function () { },
            walkRight: function () { },
            walkUp: function () { },
            walkDown: function () { },
            move: function () { }
        }
    });

    window.PlayerState = PlayerState;
}());
View Code

WalkState

(function () {
    var WalkState = YYC.AClass(PlayerState, {
        Protected: {
            //*子类可复用的代码

            P__checkMapAndSetDir: function () {
                var sprite = this.P_context.sprite;

                this.P__setDir();

                if (!this.__checkPassMap()) {
                    sprite.moving = false;
                    //sprite.dirX = 0;
                    this.P__stop();
                }
                else {
                    sprite.moving = true;
                }
            },
            Abstract: {
                P__setPlayerState: function () { },
                //计算并返回目的地地形数组下标
                P__computeTarget: function () { },
                //检测是否超出地图边界。
                //超出返回true,否则返回false
                P__checkBorder: function () { },
                //设置方向
                P__setDir: function () { },
                //停止
                P__stop: function () { }
            }
        },
        Private: {
            //检测是否可通过该地图。可以通过返回true,不能通过返回false
            __checkPassMap: function () {
                //计算目的地地形数组下标
                var target = this.P__computeTarget();

                if (this.P__checkBorder(target)) {
                    return false;
                }

                return !this.__checkCollideWithBarrier(target);
            },
            //地形障碍物碰撞检测
            __checkCollideWithBarrier: function (target) {
                var stop = bomberConfig.map.terrain.stop;

                //碰撞
                if (window.terrainData[target.y][target.x] === stop) {
                    return true;
                }

                return false;
            }
        },
        Public: {
            stand: function () {
                this.P__setPlayerState();
                this.P_context.stand();
                this.P_context.sprite.resetCurrentFrame(0);
                this.P_context.sprite.stand = true;
            },
            Virtual: {
                walkLeft: function () {
                    this.P_context.setPlayerState(Context.walkLeftState);
                    this.P_context.walkLeft();
                    this.P_context.sprite.resetCurrentFrame(0);
                },
                walkRight: function () {
                    this.P_context.setPlayerState(Context.walkRightState);
                    this.P_context.walkRight();
                    this.P_context.sprite.resetCurrentFrame(0);
                },
                walkUp: function () {
                    this.P_context.setPlayerState(Context.walkUpState);
                    this.P_context.walkUp();
                    this.P_context.sprite.resetCurrentFrame(0);
                },
                walkDown: function () {
                    this.P_context.setPlayerState(Context.walkDownState);
                    this.P_context.walkDown();
                    this.P_context.sprite.resetCurrentFrame(0);
                }
            }
        },
        Abstract: {
            move: function () {
            }
        }
    });

    window.WalkState = WalkState;
}());
View Code

WalkState_X

(function () {
    var WalkState_X = YYC.AClass(WalkState, {
        Protected: {
        },
        Private: {
            __judgeCompleteOneMoveByIndex: function () {
                var sprite = this.P_context.sprite;

                if (sprite.moveIndex_x >= sprite.stepX) {
                    sprite.moveIndex_x = 0;
                    sprite.moving = false;
                }
                else {
                    sprite.moving = true;
                }
            },
            __computeCoordinate: function () {
                var sprite = this.P_context.sprite;

                sprite.x = sprite.x + sprite.speedX * sprite.dirX;
            },
            __roundingDown: function () {
                this.P_context.sprite.x -= this.P_context.sprite.x % bomberConfig.WIDTH;
            }
        },
        Public: {
            move: function () {
                if (!this.P_context.sprite.moving) {
                    this.__roundingDown();
                    return;
                }

                this.P_context.sprite.moveIndex_x += 1;
                this.__judgeCompleteOneMoveByIndex();
                this.__computeCoordinate();
            }
        },
        Abstract: {
        }
    });

    window.WalkState_X = WalkState_X;
}());
View Code

WalkState_Y

(function () {
    var WalkState_Y = YYC.AClass(WalkState, {
        Protected: {
        },
        Private: {
            __judgeCompleteOneMoveByIndex: function () {
                var sprite = this.P_context.sprite;

                if (sprite.moveIndex_y >= sprite.stepY) {
                    sprite.moveIndex_y = 0;
                    sprite.moving = false;
                }
                else {
                    sprite.moving = true;
                }
            },
            __computeCoordinate: function () {
                var sprite = this.P_context.sprite;

                sprite.y = sprite.y + sprite.speedY * sprite.dirY;
            },
            __roundingDown: function () {
                this.P_context.sprite.y -= this.P_context.sprite.y % bomberConfig.WIDTH;
            }
        },
        Public: {
            move: function () {
                if (!this.P_context.sprite.moving) {
                    this.__roundingDown();
                    return;
                }

                this.P_context.sprite.moveIndex_y += 1;
                this.__judgeCompleteOneMoveByIndex();
                this.__computeCoordinate();
            }
        },
        Abstract: {
        }
    });

    window.WalkState_Y = WalkState_Y;
}());
View Code

WalkLeftState

(function () {
    var WalkLeftState = YYC.Class(WalkState_X, {
        Protected: {
            P__setPlayerState: function () {
                this.P_context.setPlayerState(Context.standLeftState);
            },
            P__computeTarget: function () {
                var sprite = this.P_context.sprite;

                return {
                    x: sprite.x / window.bomberConfig.WIDTH - 1,
                    y: sprite.y / window.bomberConfig.HEIGHT
                };
            },
            P__checkBorder: function (target) {
                if (target.x < 0) {
                    return true;
                }

                return false;
            },
            P__setDir: function () {
                var sprite = this.P_context.sprite;

                sprite.setAnim("walk_left");
                sprite.dirX = -1;
            },
            P__stop: function () {
                var sprite = this.P_context.sprite;

                sprite.dirX = 0;
            }
        },
        Public: {
            walkLeft: function () {
                this.P__checkMapAndSetDir();
            }
        }
    });

    window.WalkLeftState = WalkLeftState;
}());
View Code

WalkRightState

(function () {
    var WalkRightState = YYC.Class(WalkState_X, {
        Protected: {
            P__setPlayerState: function () {
                this.P_context.setPlayerState(Context.standRightState);
            },
            P__computeTarget: function () {
                var sprite = this.P_context.sprite;

                return {
                    x: sprite.x / window.bomberConfig.WIDTH + 1,
                    y: sprite.y / window.bomberConfig.HEIGHT
                };
            },
            P__checkBorder: function (target) {
                if (target.x >= window.terrainData[0].length) {
                    return true;
                }

                return false;
            },
            P__setDir: function () {
                var sprite = this.P_context.sprite;

                sprite.setAnim("walk_right");
                sprite.dirX = 1;
            },
            P__stop: function () {
                var sprite = this.P_context.sprite;

                sprite.dirX = 0;
            }
        },
        Public: {
            walkRight: function () {
                this.P__checkMapAndSetDir();
            }
        }
    });

    window.WalkRightState = WalkRightState;
}());
View Code

WalkDownState

(function () {
    var WalkDownState = YYC.Class(WalkState_Y, {
        Protected: {
            P__setPlayerState: function () {
                this.P_context.setPlayerState(Context.standDownState);
            },
            P__computeTarget: function () {
                var sprite = this.P_context.sprite;

                return {
                    x: sprite.x / window.bomberConfig.WIDTH,
                    y: sprite.y / window.bomberConfig.HEIGHT + 1
                };
            },
            P__checkBorder: function (target) {
                if (target.y >= window.terrainData.length) {
                    return true;
                }

                return false;
            },
            P__setDir: function () {
                var sprite = this.P_context.sprite;

                sprite.setAnim("walk_down");
                sprite.dirY = 1;
            },
            P__stop: function () {
                var sprite = this.P_context.sprite;

                sprite.dirY = 0;
            }
        },
        Private: {
        },
        Public: {
            walkDown: function () {
                this.P__checkMapAndSetDir();
            }
        }
    });

    window.WalkDownState = WalkDownState;
}());
View Code

WalkUpState

(function () {
    var WalkUpState = YYC.Class(WalkState_Y, {
        Protected: {
            P__setPlayerState: function () {
                this.P_context.setPlayerState(Context.standUpState);
            },
            P__computeTarget: function () {
                var sprite = this.P_context.sprite;

                return {
                    x: sprite.x / window.bomberConfig.WIDTH,
                    y: sprite.y / window.bomberConfig.HEIGHT - 1
                };
            },
            P__checkBorder: function (target) {
                if (target.y < 0) {
                    return true;
                }

                return false;
            },
            P__setDir: function () {
                var sprite = this.P_context.sprite;

                sprite.setAnim("walk_up");
                sprite.dirY = -1;
            },
            P__stop: function () {
                var sprite = this.P_context.sprite;

                sprite.dirY = 0;
            }
        },
        Public: {
            walkUp: function () {
                this.P__checkMapAndSetDir();
            }
        }
    });

    window.WalkUpState = WalkUpState;
}());
View Code

StandState

(function () {
    var StandState = YYC.AClass(PlayerState, {
        Protected: {
        },
        Public: {
            walkLeft: function () {
                this.P_context.sprite.resetCurrentFrame(0);
                this.P_context.setPlayerState(Context.walkLeftState);
                this.P_context.walkLeft();
            },
            walkRight: function () {
                this.P_context.sprite.resetCurrentFrame(0);
                this.P_context.setPlayerState(Context.walkRightState);
                this.P_context.walkRight();
            },
            walkUp: function () {
                this.P_context.sprite.resetCurrentFrame(0);
                this.P_context.setPlayerState(Context.walkUpState);
                this.P_context.walkUp();
            },
            walkDown: function () {
                this.P_context.sprite.resetCurrentFrame(0);
                this.P_context.setPlayerState(Context.walkDownState);
                this.P_context.walkDown();
            },
            move: function () {
            }
        },
        Abstract: {
        }
    });

    window.StandState = StandState;
}());

StandLeftState

(function () {
    var StandLeftState = YYC.Class(StandState, {
        Public: {
            stand: function () {
                var sprite = this.P_context.sprite;
                
                sprite.dirX = 0;
                sprite.setAnim("stand_left");
                sprite.moving = false;
            }
        }
    });

    window.StandLeftState = StandLeftState;
}());

StandRightState

(function () {
    var StandRightState = YYC.Class(StandState, {
        Public: {
            stand: function () {
                var sprite = this.P_context.sprite;
                
                sprite.dirX = 0;
                sprite.setAnim("stand_right");
                sprite.moving = false;
            }
        }
    });

    window.StandRightState = StandRightState;
}());

StandDownState

(function () {
    var StandDownState = YYC.Class(StandState, {
        Public: {
            stand: function () {
                var sprite = this.P_context.sprite;
                
                sprite.dirY = 0;
                sprite.setAnim("stand_down");
                sprite.moving = false;
            }
        }
    });

    window.StandDownState = StandDownState;
}());

StandUpState

(function () {
    var StandUpState = YYC.Class(StandState, {
        Public: {
            stand: function () {
                var sprite = this.P_context.sprite;
                
                sprite.dirY = 0;
                sprite.setAnim("stand_up");
                sprite.moving = false;
            }
        }
    });

    window.StandUpState = StandUpState;
}());

重构PlayerSprite

changeDir改名为setDir

该方法会在游戏主循环中调用,并不会每次轮询时都改变炸弹人移动方向,因此changDir这个方法名不合理,改为setDir更为合适。

删除completeOneMove

现在可以不需要completeOneMove标志了,故将其删除。 

重构后的PlayerSprite

(function () {
    var PlayerSprite = YYC.Class({
        Init: function (data) {
            //初始坐标
            this.x = data.x;
            this.y = data.y;

            this.speedX = data.speedX;
            this.speedY = data.speedY;

            //x/y坐标的最大值和最小值, 可用来限定移动范围.
            this.minX = data.minX;
            this.maxX = data.maxX;
            this.minY = data.minY;
            this.maxY = data.maxY;

            this.defaultAnimId = data.defaultAnimId;
            this.anims = data.anims;

            this.walkSpeed = data.walkSpeed;
            this.speedX = data.walkSpeed;
            this.speedY = data.walkSpeed;

            this._context = new Context(this);
        },
        Private: {
            //状态模式上下文类
            _context: null,

            //更新帧动画
            _updateFrame: function (deltaTime) {
                if (this.currentAnim) {
                    this.currentAnim.update(deltaTime);
                }
            },
            _computeCoordinate: function () {
                this.x = this.x + this.speedX * this.dirX;
                this.y = this.y + this.speedY * this.dirY;

                //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3),
                //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍),
                //因此此处需要向下取整。


                //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整)
                if (this.completeOneMove) {
                    this.x -= this.x % bomberConfig.WIDTH;
                    this.y -= this.y % bomberConfig.HEIGHT;
                }
            },
            _getCurrentState: function () {
                var currentState = null;

                switch (this.defaultAnimId) {
                    case "stand_right":
                        currentState = Context.standRightState;
                        break;
                    case "stand_left":
                        currentState = Context.standLeftState;
                        break;
                    case "stand_down":
                        currentState = Context.standDownState;
                        break;
                    case "stand_up":
                        currentState = Context.standUpState;
                        break;
                    case "walk_down":
                        currentState = Context.walkDownState;
                        break;
                    case "walk_up":
                        currentState = Context.walkUpState;
                        break;
                    case "walk_right":
                        currentState = Context.walkRightState;
                        break;
                    case "walk_left":
                        currentState = Context.walkLeftState;
                        break;
                    default:
                        throw new Error("未知的状态");
                        break;
                };

                return currentState;
            },
            //计算移动次数
            _computeStep: function () {
                this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX);
                this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY);
            },
            _allKeyUp: function () {
                return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false
                    && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false;
            },
            _judgeCompleteOneMoveByIndex: function () {
                if (!this.moving) {
                    return;
                }

                if (this.moveIndex_x >= this.stepX) {
                    this.moveIndex_x = 0;
                    this.completeOneMove = true;
                }
                else if (this.moveIndex_y >= this.stepY) {
                    this.moveIndex_y = 0;
                    this.completeOneMove = true;
                }
                else {
                    this.completeOneMove = false;
                }
            },
            _judgeAndSetDir: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this._context.walkLeft();
                }
                else if (window.keyState[keyCodeMap.D] === true) {
                    this._context.walkRight();
                }
                else if (window.keyState[keyCodeMap.W] === true) {
                    this._context.walkUp();
                }
                else if (window.keyState[keyCodeMap.S] === true) {
                    this._context.walkDown();
                }
            }
        },
        Public: {
            //精灵的坐标
            x: 0,
            y: 0,

            //精灵的速度
            speedX: 0,
            speedY: 0,

            //精灵的坐标区间
            minX: 0,
            maxX: 9999,
            minY: 0,
            maxY: 9999,
            //精灵包含的所有 Animation 集合. Object类型, 数据存放方式为" id : animation ".
            anims: null,
            //默认的Animation的Id , string类型
            defaultAnimId: null,

            //当前的Animation.
            currentAnim: null,

            //精灵的方向系数:
            //往下走dirY为正数,往上走dirY为负数;
            //往右走dirX为正数,往左走dirX为负数。
            dirX: 0,
            dirY: 0,

            //定义sprite走路速度的绝对值
            walkSpeed: 0,

            //一次移动步长中的需要移动的次数
            stepX: 0,
            stepY: 0,

            //一次移动步长中已经移动的次数
            moveIndex_x: 0,
            moveIndex_y: 0,

            //是否正在移动标志
            moving: false,

            //站立标志
            //用于解决调用WalkState.stand后,PlayerLayer.render中P__isChange返回false的问题
            //(不调用draw,从而仍会显示精灵类walk的帧(而不会刷新为更新状态后的精灵类stand的帧))。
            stand: false,

            //设置当前Animation, 参数为Animation的id, String类型
            setAnim: function (animId) {
                this.currentAnim = this.anims[animId];
            },
            //重置当前帧
            resetCurrentFrame: function (index) {
                this.currentAnim && this.currentAnim.setCurrentFrame(index);
            },
            init: function () {
                this._context.setPlayerState(this._getCurrentState());
                this._computeStep();

                //设置当前Animation
                this.setAnim(this.defaultAnimId);
            },

            // 更新精灵当前状态
            update: function (deltaTime) {
                this._updateFrame(deltaTime);
            },
            draw: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight);
                }
            },
            clear: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    //直接清空画布区域
                    context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                }
            },
            move: function () {

                this._context.move();
            },
            setDir: function () {
                if (this.moving) {
                    return;
                }

                if (this._allKeyUp()) {
                    this._context.stand();
                }
                else {
                    this._judgeAndSetDir();
                }
            }
        }
    });

    window.PlayerSprite = PlayerSprite;
}());
View Code

本文最终领域模型

查看大图

高层划分

与上文相同,没有增加新的包

层、包

对应领域模型

  • 辅助操作层
    • 控件包
      PreLoadImg
    • 配置包
      Config
  • 用户交互层
    • 入口包
      Main
  • 业务逻辑层
    • 辅助逻辑
      • 工厂包
        BitmapFactory、LayerFactory、SpriteFactory
      • 事件管理包
        KeyState、KeyEventManager
    • 游戏主逻辑
      • 主逻辑包
        Game
    • 层管理
      • 层管理实现包
        PlayerLayerManager、MapLayerManager
      • 层管理抽象包
      • LayerManager
      • 层实现包
        PlayerLayer、MapLayer
      • 层抽象包
        Layer
      • 集合包
        Collection
    • 精灵
      • 精灵包
        PlayerSprite、Context、PlayerState、WalkState、StandState、WalkState_X、WalkState_Y、StandLeftState、StandRightState、StandUpState、StandDownState、WalkLeftState、WalkRightState、WalkUpState、WalkDownState
      • 动画包
        Animation、GetSpriteData、SpriteData、GetFrames、FrameData
  • 数据操作层
    • 地图数据操作包
      MapDataOperate
    • 路径数据操作包
      GetPath
    • 图片数据操作包
      Bitmap
  • 数据层
    • 地图包
      MapData、TerrainData
    • 图片路径包
      ImgPathData

本文参考资料

html5游戏开发-零基础开发RPG游戏-开源讲座(二)-跑起来吧英雄

欢迎浏览上一篇博文:炸弹人游戏开发系列(5):控制炸弹人移动,引入状态模式

欢迎浏览下一篇博文:炸弹人游戏开发系列(7):加入敌人,使用A*算法寻路

posted @ 2013-10-20 11:25 Wonder-YYC 阅读(...) 评论(...) 编辑 收藏