zoukankan      html  css  js  c++  java
  • 炸弹人游戏开发系列(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*算法寻路

  • 相关阅读:
    python 字符串内建函数之开头与结尾判断
    python 字符串内建函数之查找、替换
    python 字符串内建函数之大小写
    python 字符串切片
    python for循环
    python if语句
    python input( )
    python 变量命名规则
    DllMain
    静态库lib和动态dll的区别及使用方法
  • 原文地址:https://www.cnblogs.com/chaogex/p/3327097.html
Copyright © 2011-2022 走看看