zoukankan      html  css  js  c++  java
  • 炸弹人游戏开发系列(7):加入敌人,使用A*算法寻路

    前言

    上文中我们实现了炸弹人与墙的碰撞检测,以及设置移动步长来解决发现的问题。本文会加入1个AI敌人,敌人使用A*算法追踪炸弹人。

    本文目的

    加入敌人,追踪炸弹人

    本文主要内容

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

    查看大图

    开发策略

    首先实现“加入敌人”功能。通过参考“炸弹人游戏开发系列(4):炸弹人显示与移动“中的实现,可以初步分析出需要加入敌人图片、敌人帧数据和精灵数据、敌人精灵类EnemySprite、敌人层EnemyLayer和敌人层管理类EnemyLayerManager。

    然后实现“追踪炸弹人”功能。需要新建一个算法类FindPath,负责使用A*算法计算并返回路径数据。

    敌人精灵类与算法类的交互关系:

    并行开发

    可以并行开发“加入敌人”和“追踪炸弹人”。

    先定义一个FindPath类的的接口,指定findPath方法输入参数和返回参数的格式。

    实现“加入敌人”功能时,可以按照接口指定的格式使用假的路径数据来测试EnemySprite类;实现“追踪炸弹人”功能时,按照接口指定格式使用假的坐标数据来测试FindPath类。

    在EnemySprit和FindPath都实现后,再集成在一起测试。因为两者接口一致,因此集成时不会有什么困难。

    加入敌人

    扩大地图

    现在地图大小为4*4,太小了。

    加入一个敌人后:

    • 可玩性太低
      很快游戏就结束了;玩家操作炸弹人躲避敌人的空间太小了。
    • 不方便演示和测试游戏
      由于游戏很快就结束,因此不方便演示和测试游戏。

    因此,将地图扩大为20*20。

    要实现这个功能,只需要修改MapData和TerrainData即可。

    相关代码

    MapData

    //地图数据
    (function () {
        var ground = bomberConfig.map.type.GROUND,
            wall = bomberConfig.map.type.WALL;
    
        var mapData = [
            [
                wall, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, wall, wall, wall,
                wall, wall, wall, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, wall, wall, ground,
                ground, ground, ground, wall, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, wall, wall, wall,
                ground, wall, ground, wall, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
    
    
    
            [
                wall, ground, wall, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
    
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
    
    
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ],
            [
                wall, ground, ground, ground, wall,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground,
                ground, ground, ground, ground, ground
            ]
        ];
    
        window.mapData = mapData;
    }());
    View Code

    TerrainData

    (function () {
        var pass = bomberConfig.map.terrain.pass,
            stop = bomberConfig.map.terrain.stop;
    
        var terrainData = [
            [
                stop, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, stop, stop, stop,
                stop, stop, stop, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, stop, stop, pass,
                pass, pass, pass, stop, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, stop, stop, stop,
                pass, stop, pass, stop, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
    
    
    
            [
                stop, pass, stop, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
    
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
    
    
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ],
            [
                stop, pass, pass, pass, stop,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass,
                pass, pass, pass, pass, pass
            ]
        ];
    
        window.terrainData = terrainData;
    }());
    View Code

    加入敌人图片和数据

    首先加入敌人的精灵图片

    然后加入敌人帧动画数据类

    (function () {
        var getEnemyFrames = (function () {
            //一个动作在图片中的宽度
            var width = bomberConfig.enemy.WIDTH,
                //一个动作在图片中的高度
                height = bomberConfig.enemy.HEIGHT,
                //一个动作的偏移量
                offset = {
                    x: bomberConfig.enemy.offset.X,
                    y: bomberConfig.enemy.offset.Y
                },
                //一个动作横向截取的长度
                sw = bomberConfig.enemy.SW,
                //一个动作纵向截取的长度
                sh = bomberConfig.enemy.SH,
                //一个动作图片在canvas中的宽度
                imgWidth = bomberConfig.enemy.IMGWIDTH,
                //一个动作图片在canvas中的高度
                imgHeight = bomberConfig.enemy.IMGHEIGHT;
    
            //帧数据
            var frames = function () {
                return {
                    //向右站立
                    stand_right: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + 2 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向左站立
                    stand_left: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向上站立
                    stand_up: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + 3 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向下站立
                    stand_down: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向上走
                    walk_up: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + 3 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + width, y: offset.y + 3 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 2 * width, y: offset.y + 3 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 3 * width, y: offset.y + 3 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向下走
                    walk_down: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + width, y: offset.y,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 2 * width, y: offset.y,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 3 * width, y: offset.y,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向右走
                    walk_right: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + 2 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + width, y: offset.y + 2 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 2 * width, y: offset.y + 2 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 3 * width, y: offset.y + 2 * height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    },
                    //向左走
                    walk_left: {
                        img: window.imgLoader.get("enemy"),
                        frames: [
                            { x: offset.x, y: offset.y + height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + width, y: offset.y + height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 2 * width, y: offset.y + height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                            { x: offset.x + 3 * width, y: offset.y + height,  sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                        ]
                    }
                }
            }
    
            return function (animName) {
                return frames()[animName];
            };
        }());
    
        window.getEnemyFrames = getEnemyFrames;
    }());
    View Code

    然后加入敌人精灵类数据

    (function () {
        var getSpriteData = (function () {
            var data = function(){
                return {
    ...
                    //敌人精灵类
                    enemy: {
                        //初始坐标
                        x: bomberConfig.WIDTH * 10,
                        //x: 0,
                        y: bomberConfig.HEIGHT * 3,
                        //定义sprite走路速度的绝对值
                        walkSpeed: bomberConfig.enemy.speed.NORMAL,
    
                        //速度
                        speedX: 1,
                        speedY: 1,
    
                        //注意坐标起始点为图片左上点!
    
                        minX: 0,
                        maxX: bomberConfig.canvas.WIDTH - bomberConfig.player.IMGWIDTH,
                        //maxX: 500,
                        minY: 0,
                        maxY: bomberConfig.canvas.HEIGHT - bomberConfig.player.IMGHEIGHT,
    
    
    
                        defaultAnimId: "stand_left",
    
                        anims: {
                            "stand_right": new Animation(getEnemyFrames("stand_right")),
                            "stand_left": new Animation(getEnemyFrames("stand_left")),
                            "stand_down": new Animation(getEnemyFrames("stand_down")),
                            "stand_up": new Animation(getEnemyFrames("stand_up")),
                            "walk_up": new Animation(getEnemyFrames("walk_up")),
                            "walk_down": new Animation(getEnemyFrames("walk_down")),
                            "walk_right": new Animation(getEnemyFrames("walk_right")),
                            "walk_left": new Animation(getEnemyFrames("walk_left"))
                        }
                    }
                }
            };
    
            return function (spriteName) {
                return data()[spriteName];
            };
        }());
    
        window.getSpriteData = getSpriteData;
    }());

    加入EnemySprite类

    增加敌人精灵类。

    创建假的A*算法类FindPath类

    创建返回假数据的FindPath类,用于测试EnemySprite类。

    相关代码

    FindPath

    (function () {
        //构造假数据
        var findPath = {
            aCompute: function (terrainData, begin, target) {
                return {
                    path: [{ x: 8, y: 0 }, { x: 7, y: 0 }, { x: 6, y: 0 }, { x: 5, y: 0 }, { x: 4, y: 0 }],
                    time: 0.1
                };
            }
        };
    
        window.findPath = findPath;
    }());

    EnemySprite

    (function () {
        var EnemySprite = 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 = new StandRightState();
                            break;
                        case "stand_left":
                            currentState = new StandLeftState;
                            break;
                        case "stand_down":
                            currentState = new StandDownState;
                            break;
                        case "stand_up":
                            currentState = new StandUpState;
                            break;
                        case "walk_down":
                            currentState = new WalkDownState;
                            break;
                        case "walk_up":
                            currentState = new WalkUpState;
                            break;
                        case "walk_right":
                            currentState = new WalkRightState;
                            break;
                        case "walk_left":
                            currentState = new 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);
                }
            },
            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,
    
                //寻找的路径
                path: null,
    
                //设置当前Animation, 参数为Animation的id, String类型
                setAnim: function (animId) {
                    this.currentAnim = this.anims[animId];
                },
                init: function () {
                    this.__context.setPlayerState(this.__getCurrentState());
                    this.__computeStep();
    
                    this.path = window.findPath.aCompute().path;
    
                    //设置当前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;
                    }
    
                    //返回并移除要移动到的坐标
                    var target = this.path.shift();
    
                    //当前坐标
                    var now = {
                        x: self.x / bomberConfig.WIDTH,
                        y: self.y / bomberConfig.HEIGHT
                    };
    
                    //判断要移动的方向,调用相应的方法
                    if (target.x > now.x) {
                        this.__context.walkRight();
                    }
                    else if (target.x < now.x) {
                        this.__context.walkLeft();
                    }
                    else if (target.y > now.y) {
                        this.__context.walkDown();
                    }
                    else if (target.y < now.y) {
                        this.__context.walkUp();
                    }
                    else {
                        this.__context.stand();
                    }
                }
            }
        });
    
        window.EnemySprite = EnemySprite;
    }());
    View Code

    增加敌人精灵类工厂

    SpriteFactory新增工厂方法createEnemy,用于创建EnemySprite实例。

    SpriteFactory

            createEnemy: function () {
                return new EnemySprite(getSpriteData("enemy"));
            }

    新增EnemyLayer

    增加敌人画布,该画布于地图画布之上,与玩家画布的zIndex相同。同时增加对应的敌人层类EnemyLayer,它的集合元素为EnemySprite类的实例。

    EnemyLayer

    (function () {
        var EnemyLayer = YYC.Class(Layer, {
            Init: function (deltaTime) {
                this.___deltaTime = deltaTime;
            },
            Private: {
                ___deltaTime: 0,
    
                ___iterator: function (handler) {
                    var args = Array.prototype.slice.call(arguments, 1),
                        nextElement = null;
    
                    while (this.hasNext()) {
                        nextElement = this.next();
                        nextElement[handler].apply(nextElement, args);  //要指向nextElement
                    }
                    this.resetCursor();
                },
                ___update: function (deltaTime) {
                    this.___iterator("update", deltaTime);
                },
                __setDir: function () {
                    this.___iterator("setDir");
                },
                ___move: function (deltaTime) {
                    this.___iterator("move", deltaTime);
                }
            },
            Public: {
                setCanvas: function () {
                    this.P__canvas = document.getElementById("enemyLayerCanvas");
    
                    $("#enemyLayerCanvas").css({
                        "position": "absolute",
                        "top": bomberConfig.canvas.TOP,
                        "left": bomberConfig.canvas.LEFT,
                        "border": "1px solid black",
                        "z-index": 1    //z-index与playerLayer相同
                    });
                },
                draw: function (context) {
                    this.___iterator("draw", context);
                },
                clear: function (context) {
                    this.___iterator("clear", context);
                },
                render: function () {
                    this.__setDir();
                    this.___move(this.___deltaTime);
    
                    //判断__state是否为change状态,如果是则调用canvas绘制精灵。
                    if (this.P__isChange()) {
                        this.clear(this.P__context);
                        this.___update(this.___deltaTime);
                        this.draw(this.P__context);
                        this.P__setStateNormal();
                    }
                }
            }
        });
    
        window.EnemyLayer = EnemyLayer;
    }());

    增加敌人层类工厂

    LayerFactory新增工厂方法createEnemy,用于创建EnemyLayer实例

    LayerFactory

            createEnemy: function (deltaTime) {
                return new EnemyLayer(deltaTime);
            }

    新增EnemyLayerManager

    增加EnemyLayer的管理类EnemyLayerManager

    var EnemyLayerManager = YYC.Class(LayerManager, {
        Init: function (layer) {
            this.base(layer);
        },
        Public: {
            createElement: function () {
                var element = [],
                     enemy = spriteFactory.createEnemy();
    
                enemy.init();
                element.push(enemy);
    
                return element;
            },
            change: function () {
                this.layer.change();
            }
        }
    });

    领域模型

    实现寻路算法

    游戏中的敌人采用A*算法寻路。参考资料:A星算法

    敌人寻路模式

    游戏开始时,敌人以它的当前位置为起始点,炸弹人的位置为终点寻路。如果敌人到达终点后,没有碰撞到炸弹人,则再一次以它的当前位置为起始点,炸弹人的位置为终点寻路。

    敌人寻路流程

    实现FindPath类

    领域模型

    相关代码

    FindPath

    (function () {
        var map_w, beginx, beginy, endx, endy;
        var arr_path_out = new Array();
    
        var pass = bomberConfig.map.terrain.pass,
            stop = bomberConfig.map.terrain.stop;
    
    
        var arr_map = new Array();
        var open_list = new Array(); //创建OpenList
        var close_list = new Array(); //创建CloseList
        var tmp = new Array(); //存放当前节点的八个方向的节点
        var arr_map_tmp = window.mapData; //存储从游戏中读入的地图数据
        var map_w = arr_map_tmp.length;
    
        function aCompute(mapData, begin, end) {
            //计算运行时间
            var startTime, endTime;
            var d = new Date();
            var time;
            startTime = d.getTime();
    
    
            var arr_path = new Array();
            var stopn = 0;
    
            /********************函数主体部分*************************/
    
            arr_map = setMap(mapData);
    
            map_w = mapData.length;
            beginx = begin.x;
            beginy = map_w - 1 - begin.y;
            endx = end.x;
            endy = map_w - 1 - end.y;
            var startNodeNum = tile_num(beginx, beginy);
            var targetNodeNum = tile_num(endx, endy);
    
            if (arr_map[targetNodeNum] && arr_map[targetNodeNum][0] == 0) {
                showError("目的地无法到达!");
                time = getTime(startTime);
                return { path: [], time: time };
            }
            if (arr_map[startNodeNum][0] == 0) {
                showError("起始点不可用!");
                time = getTime(startTime);
                return { path: [], time: time };
            }
    
            if (arr_map[targetNodeNum] && arr_map[targetNodeNum][0] * arr_map[startNodeNum][0] == 1) {
                arr_map[startNodeNum] = [arr_map[startNodeNum][0], startNodeNum, arr_map[startNodeNum][2], arr_map[startNodeNum][3], arr_map[startNodeNum][4]];//起始点的父节点为自己
                setH(targetNodeNum);
                setOpenList(startNodeNum); //把开始节点加入到openlist中
                //就要开始那个令人发指的循环了,==!!A*算法主体
    
                while (open_list.length != 0) {
                    var bestNodeNum = selectFmin(open_list);
                    stopn = 0;
                    open_list.shift();
                    setCloseList(bestNodeNum);
    
                    if (bestNodeNum == targetNodeNum) {
                        showPath(close_list, arr_path);
                        break;
                    }
                    var i = 0, j = 0;
                    //当目标为孤岛时的判断
                    var tmp0 = new Array();
                    var k;
                    tmp0 = setSuccessorNode(targetNodeNum, map_w);
                    for (j; j < 9; j++) {
                        if (j == 8) {
                            k = 0;
                            break;
                        }
                        if (tmp0[j][0] == 1) {
                            k = 1;
                            break;
                        }
                    }
                    //当目标为孤岛时的判断语句结束
                    if (k == 0) {
                        showError("目标成孤岛!");
                        time = getTime(startTime);
                        return { path: [], time: time };
                    }
                    else {
                        tmp = setSuccessorNode(bestNodeNum, map_w);
                        for (i; i < 8; i++) {
                            if ((tmp[i][0] == 0) || (findInCloseList(tmp[i][4]))) continue;
    
                            if (findInOpenList(tmp[i][4]) == 1) {
                                if (tmp[i][2] >= (arr_map[bestNodeNum][2] + cost(tmp[i], bestNodeNum))) {
                                    setG(tmp[i][4], bestNodeNum); //算g值,修改arr_map中[2]的值
                                    arr_map[tmp[i][4]] = tmp[i] = [arr_map[tmp[i][4]][0], bestNodeNum, arr_map[tmp[i][4]][2], arr_map[tmp[i][4]][3], arr_map[tmp[i][4]][4]]; //修改tmp和arr_map中父节点的值,并修改tmp中g值,是之和arr_map中对应节点的值统一
                                }
                            }
                            if (findInOpenList(tmp[i][4]) == 0) {
                                setG(tmp[i][4], bestNodeNum); //算g值,修改arr_map中[2]的值
                                arr_map[tmp[i][4]] = tmp[i] = [arr_map[tmp[i][4]][0], bestNodeNum, arr_map[tmp[i][4]][2], arr_map[tmp[i][4]][3], arr_map[tmp[i][4]][4]]; //修改tmp和arr_map中父节点的值,并修改tmp中g值,是之和arr_map中对应节点的值统一
                                setOpenList(tmp[i][4]); //存进openlist中
    
                            }
                        }
                    }
    
                    stopn++;
                    //if (stopn == map_w * map_w - 1) {     //2013.5.27修改
                    if (stopn == map_w * map_w * 1000) {
                        showError("找不到路径!");
                        time = getTime(startTime);
                        return { path: [], time: time };
    
                        //                break;
                    }
                }
    
    
                if (open_list.length == 0 && bestNodeNum != targetNodeNum) {
                    showError("没有找到路径!!");   //对于那种找不到路径的点的处理
                    time = getTime(startTime);
                    return { path: [], time: time };
                }
            }
    
            time = getTime(startTime);
    
            return { path: arr_path_out, time: time };
    
        }
    
        function getTime(startTime) {
            /***显示运行时间********/
            var endTime = new Date().getTime();
            return (endTime - startTime) / 1000;
        };
    
    
        function showError(error) {
            console.log(error);
        };
    
    
        /**********************************************************************
         *function setMap(n)
         *功能:把外部的地图数据抽象成该算法中可操作数组的形式来输入算法
         *参数:n为地图的宽度,生成方阵地图
         ************************************************************************/
        function setMap(mapData) {
    
            map_w = mapData.length;
            var m = map_w * map_w;
    
            var arr_map0 = new Array(); //该函数对地图数据转换的操作数组
            var a = m - 1;
            for (a; a >= 0; a--) {
                var xTmp = tile_x(a); //把ID 编号值转换为x坐标值,用来对应读入地图数据
                var yTmp = map_w - 1 - tile_y(a); //把ID 编号值转换为y坐标值,用来对应读入地图数据,对应数组标号和我自定义xoy坐标位置
    
                //[cost,parent,g,h,id]
                if (mapData[yTmp][xTmp] == pass)
                    arr_map0[a] = [1, 0, 0, 0, a];
                else
                    arr_map0[a] = [0, 0, 0, 0, a];
    
    
            }
    
            return arr_map0;
        }
    
        /*********************************************************************
         *以下三个函数是地图上的编号与数组索引转换
         *function tile_num(x,y)
         *功能:将 x,y 坐标转换为地图上块的编号
         *function tile_x(n)
         *功能:由块编号得出 x 坐标
         *function tile_y(n)
         *功能:由块编号得出 y 坐标
         ******************************************************************/
        function tile_num(x, y) {
            return ((y) * map_w + (x));
        }
    
        function tile_x(n) {
            return (parseInt((n) % map_w));
        }
    
        function tile_y(n) {
            return (parseInt((n) / map_w));
        }
    
        /*********************************************************************
         *function setH(targetNode)
         *功能:初始化所有点H的值
         *参数:targetNode目标节点
         **********************************************************************/
        function setH(targetNode) {
    
            var x0 = tile_x(targetNode);
            var y0 = tile_y(targetNode);
            var i = 0;
            for (i; i < arr_map.length; i++) {
                var x1 = tile_x(i);
                var y1 = tile_y(i);
                /*****欧几里德距离********************************/
                // var h = (Math.sqrt((parseInt(x0) - parseInt(x1)) * (parseInt(x0) - parseInt(x1))) + Math.sqrt((parseInt(y0) - parseInt(y1)) * (parseInt(y0) - parseInt(y1))));
                /*****对角线距离********************************/
                var h = Math.max(Math.abs(parseInt(x0) - parseInt(x1)), Math.abs(parseInt(y0) - parseInt(y1)));
                /*****曼哈顿距离********************************/
                    // var h=Math.abs(parseInt(x0) - parseInt(x1))+Math.abs(parseInt(y0) - parseInt(y1));
                arr_map[i][3] = h * parseInt(10);
            }
        }
    
        /*********************************************************************
         *function setG(nowNode,bestNode)
         *功能:计算现节点G的值
         *参数:nowNode现节点,bestNode其父节点
         **********************************************************************/
        function setG(nowNode, bestNode) {
            var x0 = tile_x(bestNode);
            var y0 = tile_y(bestNode);
            var x1 = tile_x(nowNode);
            var y1 = tile_y(nowNode);
            if (((x0 - x1) == 0) || ((y0 - y1) == 0)) {
                arr_map[nowNode] = [arr_map[nowNode][0], arr_map[nowNode][1], arr_map[nowNode][2] + parseInt(10), arr_map[nowNode][3], arr_map[nowNode][4]];
    
            }
            else {
    
                arr_map[nowNode] = [arr_map[nowNode][0], arr_map[nowNode][1], arr_map[nowNode][2] + parseInt(14), arr_map[nowNode][3], arr_map[nowNode][4]];
            }
        }
    
        /*********************************************************************
         *function selectFmin(open_list)
         *功能:在openlist中对f值进行排序(冒泡排序),并选择一个f值最小的节点返回
         *参数:openlist
         ***********************************************************************/
        function selectFmin(open_list) {
            var i, j, min, temp;
            for (i = 0; i < open_list.length; i++) {
                for (j = i + 1; j < open_list.length; j++) {
                    if ((open_list[i][2] + open_list[i][3]) > (open_list[j][2] + open_list[j][3])) {
                        temp = open_list[i];
                        open_list[i] = open_list[j];
                        open_list[j] = temp;
                    }
                }
            }
            var min = open_list[0];
            return min[4];
        }
    
        /***********************************************************************
         *function setOpenList(NodeNum)
         *功能:把节点加入open表中
         *参数:待加入openlist的节点的编号
         ************************************************************************/
        function setOpenList(NodeNum) {
            var n = open_list.length;
            open_list[n] = [arr_map[NodeNum][0], arr_map[NodeNum][1], arr_map[NodeNum][2], arr_map[NodeNum][3], arr_map[NodeNum][4]];
        }
    
        /***********************************************************************
         *function setCloseList(NodeNum)
         *功能:把节点加入close表中
         *参数:待加入closelist的节点的编号
         ************************************************************************/
        function setCloseList(NodeNum) {
            var n = close_list.length;
            close_list[n] = [arr_map[NodeNum][0], arr_map[NodeNum][1], arr_map[NodeNum][2], arr_map[NodeNum][3], arr_map[NodeNum][4]];
        }
    
        /***********************************************************************
         *function findInOpenList(nowNodeNum)
         *功能:查询当前节点是否在openlist中,找到返回1,找不到返回0
         *参数:待查询的节点的编号
         ************************************************************************/
        function findInOpenList(nowNodeNum) {
            var i;
            for (i = 0; i < open_list.length; i++) {
    
                if (open_list[i][4] == nowNodeNum)
                    return 1;
            }
            return 0;
        }
    
        /***********************************************************************
         *function findInCloseList(nowNodeNum)
         *功能:查询当前节点是否在closelist中,找到返回1,找不到返回0
         *参数:待查询的节点的编号
         ************************************************************************/
        function findInCloseList(nowNodeNum) {
            var i;
            for (i = 0; i < close_list.length; i++) {
                if (close_list[i][4] == nowNodeNum)
                    return 1;
                else return 0;
            }
        }
    
        /***********************************************************************
         *function cost(SuccessorNodeNum,bestNodeNum)
         *功能:现节点到达周围节点的代价
         *参数:SuccessorNodeNum周围节点编号,bestNodeNum现节点
         ************************************************************************/
        function cost(SuccessorNodeNum, bestNodeNum) {
            var x0 = tile_x(bestNodeNum);
            var y0 = tile_y(bestNodeNum);
            var x1 = tile_x(SuccessorNodeNum);
            var y1 = tile_y(SuccessorNodeNum);
            if (((x0 - x1) == 0) || ((y0 - y1) == 0)) {
                return 10;
            }
            else
                return 14;
        }
    
        /**********************************************************************
         *function setSuccessorNode(bestNodeNum,map_w)
         *功能:把现节点的周围8个节点放入预先准备好的临时arr中以备检察
         *参数:现节点的编号
         035
         1 6
         247
         周围八个点的排序
         ***********************************************************************/
        function setSuccessorNode(bestNodeNum, n) {
            var x0 = tile_x(bestNodeNum);
            var y0 = tile_y(bestNodeNum);
            var m = n - 1;
            if ((x0 - 1) >= 0 && (y0) >= 0 && (x0 - 1) <= m && (y0) <= m) tmp[1] = [arr_map[tile_num(x0 - 1, y0)][0], arr_map[tile_num(x0 - 1, y0)][1], arr_map[tile_num(x0 - 1, y0)][2], arr_map[tile_num(x0 - 1, y0)][3], arr_map[tile_num(x0 - 1, y0)][4]]; else {
                tmp[1] = [0, 0, 0, 0, 0];
            }
            if ((x0) >= 0 && (y0 + 1) >= 0 && (x0) <= m && (y0 + 1) <= m) tmp[3] = [arr_map[tile_num(x0, y0 + 1)][0], arr_map[tile_num(x0, y0 + 1)][1], arr_map[tile_num(x0, y0 + 1)][2], arr_map[tile_num(x0, y0 + 1)][3], arr_map[tile_num(x0, y0 + 1)][4]]; else {
                tmp[3] = [0, 0, 0, 0, 0];
            }
            if ((x0) >= 0 && (y0 - 1) >= 0 && (x0) <= m && (y0 - 1) <= m) tmp[4] = [arr_map[tile_num(x0, y0 - 1)][0], arr_map[tile_num(x0, y0 - 1)][1], arr_map[tile_num(x0, y0 - 1)][2], arr_map[tile_num(x0, y0 - 1)][3], arr_map[tile_num(x0, y0 - 1)][4]]; else {
                tmp[4] = [0, 0, 0, 0, 0];
            }
            if ((x0 + 1) >= 0 && (y0) >= 0 && (x0 + 1) <= m && (y0) <= m) tmp[6] = [arr_map[tile_num(x0 + 1, y0)][0], arr_map[tile_num(x0 + 1, y0)][1], arr_map[tile_num(x0 + 1, y0)][2], arr_map[tile_num(x0 + 1, y0)][3], arr_map[tile_num(x0 + 1, y0)][4]]; else {
                tmp[6] = [0, 0, 0, 0, 0];
            }
            if (bomberConfig.algorithm.DIRECTION == 8) {
                if ((x0 - 1) >= 0 && (y0 + 1) >= 0 && (x0 - 1) <= m && (y0 + 1) <= m) tmp[0] = [arr_map[tile_num(x0 - 1, y0 + 1)][0], arr_map[tile_num(x0 - 1, y0 + 1)][1], arr_map[tile_num(x0 - 1, y0 + 1)][2], arr_map[tile_num(x0 - 1, y0 + 1)][3], arr_map[tile_num(x0 - 1, y0 + 1)][4]]; else {
                    tmp[0] = [0, 0, 0, 0, 0];
                }
    
                if ((x0 - 1) >= 0 && (y0 - 1) >= 0 && (x0 - 1) <= m && (y0 - 1) <= m) tmp[2] = [arr_map[tile_num(x0 - 1, y0 - 1)][0], arr_map[tile_num(x0 - 1, y0 - 1)][1], arr_map[tile_num(x0 - 1, y0 - 1)][2], arr_map[tile_num(x0 - 1, y0 - 1)][3], arr_map[tile_num(x0 - 1, y0 - 1)][4]]; else {
                    tmp[2] = [0, 0, 0, 0, 0];
                }
    
                if ((x0 + 1) >= 0 && (y0 + 1) >= 0 && (x0 + 1) <= m && (y0 + 1) <= m) tmp[5] = [arr_map[tile_num(x0 + 1, y0 + 1)][0], arr_map[tile_num(x0 + 1, y0 + 1)][1], arr_map[tile_num(x0 + 1, y0 + 1)][2], arr_map[tile_num(x0 + 1, y0 + 1)][3], arr_map[tile_num(x0 + 1, y0 + 1)][4]]; else {
                    tmp[5] = [0, 0, 0, 0, 0];
                }
    
                if ((x0 + 1) >= 0 && (y0 - 1) >= 0 && (x0 + 1) <= m && (y0 - 1) <= m) tmp[7] = [arr_map[tile_num(x0 + 1, y0 - 1)][0], arr_map[tile_num(x0 + 1, y0 - 1)][1], arr_map[tile_num(x0 + 1, y0 - 1)][2], arr_map[tile_num(x0 + 1, y0 - 1)][3], arr_map[tile_num(x0 + 1, y0 - 1)][4]]; else {
                    tmp[7] = [0, 0, 0, 0, 0];
                }
    
            }
            if (bomberConfig.algorithm.DIRECTION == 4) {
                tmp[0] = [0, 0, 0, 0, 0];
                tmp[2] = [0, 0, 0, 0, 0];
                tmp[5] = [0, 0, 0, 0, 0];
                tmp[7] = [0, 0, 0, 0, 0];
            }
    
            return tmp;
        }
    
        /*******************************************************************
         *function showPath(close_list)
         *功能:把结果路径存入arr_path输出
         *参数:close_list
         ********************************************************************/
        function showPath(close_list, arr_path) {
            var n = close_list.length;
            var i = n - 1;
            var ii = 0;
            var nn = 0;
            var mm = 0;
    
    
            var arr_path_tmp = new Array();
            var target = null;
    
            /**********把close_list中有用的点存入arr_path_tmp中*************/
    
            for (ii; ; ii++) {
                arr_path_tmp[ii] = close_list[n - 1][4];
                if (close_list[n - 1][1] == close_list[i][4]) {
                    break;
                }
                for (i = n - 1; i >= 0; i--) {
                    if (close_list[i][4] == close_list[n - 1][1]) {
                        n = i + 1;
                        break;
                    }
                }
            }
    
            var w = arr_path_tmp.length - 1;
            var j = 0;
            for (var i = w; i >= 0; i--) {
                arr_path[j] = arr_path_tmp[i];
                j++;
            }
            for (var k = 0; k <= w; k++) {
                target = {
                    x: tile_x(arr_path[k]),
                    y: map_w - 1 - tile_y(arr_path[k])
                };
                arr_path_out.push(target);
            }
            arr_path_out.shift();
        }
    
        function _reset() {
            arr_path_out = new Array();
            arr_map = new Array();
            arr_map_tmp = window.mapData;
            map_w = arr_map_tmp.length;
    
            open_list = new Array(); //创建OpenList
            close_list = new Array(); //创建CloseList
            tmp = new Array(); //存放当前节点的八个方向的节点
        };
    
    
        var findPath = {
            aCompute: function (terrainData, begin, end) {
                _reset();
                return aCompute(terrainData, begin, end);
            }
        };
    
        window.findPath = findPath;
    }());
    View Code

    重构

    重构状态模式Context类

    重构前:

    (function () {
        var Context = YYC.Class({
            Init: function (sprite) {
                this.sprite = sprite;
            },
    ...
            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;
    }());

    删除Context的静态实例,改为在构造函数中创建具体状态类实例

    原因:

    因为EnemySprite和PlayerSprite都要使用Context的实例。如果为静态实例的话,EnemySprite中的Context类实例与PlayerSprite中的Context类实例会共享静态实例(具体状态类实例)!会造成互相干扰!

    重构后:

    (function () {
        var Context = YYC.Class({
            Init: function (sprite) {
                this.sprite = sprite;
    
                this.walkLeftState = new WalkLeftState();
                this.walkRightState = new WalkRightState();
                this.walkUpState = new WalkUpState();
                this.walkDownState = new WalkDownState();
                this.standLeftState = new StandLeftState();
                this.standRightState = new StandRightState();
                this.standUpState = new StandUpState();
                this.standDownState = new StandDownState();
            },
    ...
            Static: {
            }
        });
    
        window.Context = Context;
    }());

    提出基类Sprite

    为什么要提出

    • EnemySprite与PlayerSpite有很多相同的代码
    • 从概念上来说,玩家精灵类与敌人精灵类都属于精灵类的概念

    因此,提出EnemySprite、PlayerSprite基类Sprite。

    修改碰撞检测

    Sprite 增加getCollideRect获得碰撞面积。EnemySprite增加collideWidthOther。

    领域模型

    相关的代码

    Sprite

    (function () {
        var Sprite = YYC.AClass({
            Init: function (data) {
                this.x = data.x;
                this.y = data.y;
    
                this.speedX = data.speedX;
                this.speedY = data.speedY;
    
                this.minX  = data.minX;
                this.maxX  = data.maxX;
                this.minY  = data.minY;
                this.maxY  = data.maxY;
    
                this.defaultAnimId = data.defaultAnimId;
                this.anims = data.anims;
            },
            Private: {
                //更新帧动画
                _updateFrame: function (deltaTime) {
                    if (this.currentAnim) {
                        this.currentAnim.update(deltaTime);
                    }
                }
            },
            Public: {
                //在一个移动步长中已经移动的次数
                moveIndex: 0,
    
                //精灵的坐标
                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,
    
                //设置当前Animation, 参数为Animation的id, String类型
                setAnim: function (animId) {
                    this.currentAnim = this.anims[animId];
                },
                //重置当前帧
                resetCurrentFrame: function (index) {
                    this.currentAnim && this.currentAnim.setCurrentFrame(index);
                },
                //取得精灵的碰撞区域,
                getCollideRect: function () {
                    if (this.currentAnim) {
                        var f = this.currentAnim.getCurrentFrame();
                        return {
                            x1: this.x,
                            y1: this.y,
                            x2: this.x + f.imgWidth,
                            y2: this.y + f.imgHeight
                        }
                    }
                },
                Virtual: {
                    //初始化方法
                    init: function () {
                        this.setAnim(this.defaultAnimId);
                    },
                    // 更新精灵当前状态.
                    update: function (deltaTime) {
                        this._updateFrame(deltaTime);
                    }
                }
            },
            Abstract: {
                draw: function (context) { },
                clear: function (context) { },
                move: function () { },
                setDir: function () { }
            }
        });
    
        window.Sprite = Sprite;
    }());
    View Code

    EnemySprite

    (function () {
        var EnemySprite = YYC.Class(Sprite, {
            Init: function (data) {
                this.base(data);
    
                this.walkSpeed = data.walkSpeed;
                this.speedX = data.walkSpeed;
                this.speedY = data.walkSpeed;
    
                this.__context = new Context(this);
            },
            Private: {
                //状态模式上下文类
                __context: null,
    
                __getCurrentState: function () {
                    var currentState = null;
    
                    switch (this.defaultAnimId) {
                        case "stand_right":
                            currentState = new StandRightState();
                            break;
                        case "stand_left":
                            currentState = new StandLeftState;
                            break;
                        case "stand_down":
                            currentState = new StandDownState;
                            break;
                        case "stand_up":
                            currentState = new StandUpState;
                            break;
                        case "walk_down":
                            currentState = new WalkDownState;
                            break;
                        case "walk_up":
                            currentState = new WalkUpState;
                            break;
                        case "walk_right":
                            currentState = new WalkRightState;
                            break;
                        case "walk_left":
                            currentState = new 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);
                }
            },
            Public: {
                //精灵的方向系数:
                //往下走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,
    
                //寻找的路径
                path: [],
    
                playerSprite: null,
    
                init: function () {
                    this.__context.setPlayerState(this.__getCurrentState());
                    this.__computeStep();
    
                    this.base();
                },
                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);
                    }
                },
                //判断是否和另外一个精灵碰撞
                collideWidthOther: function (sprite2) {
                    var rect1 = this.getCollideRect();
                    var rect2 = sprite2.getCollideRect();
    
                    //如果碰撞,则抛出异常
                    if (rect1 && rect2 && !(rect1.x1 >= rect2.x2 || rect1.y1 >= rect2.y2 || rect1.x2 <= rect2.x1 || rect1.y2 <= rect2.y1)) {
                        throw new Error();
                    }
                },
                setPlayerSprite: function (sprite) {
                    this.playerSprite = sprite;
                },
                __computePath: function () {
                    //playerSprite的坐标要向下取整
                    var x = (this.playerSprite.x - this.playerSprite.x % window.bomberConfig.WIDTH) / window.bomberConfig.WIDTH;
                    var y = (this.playerSprite.y - this.playerSprite.y % window.bomberConfig.HEIGHT) / window.bomberConfig.HEIGHT;
    
                    return  window.findPath.aCompute(window.terrainData, 
                        { x: this.x / window.bomberConfig.WIDTH, y: this.y / window.bomberConfig.HEIGHT },
                        { x: x, y: y }).path
                },
                move: function () {
                    this.__context.move();
                },
                setDir: function () {
                    var target, now;
    
                    if (this.moving) {
                        return;
                    }
                    //特殊情况,如寻找不到路径
                    if (this.path === false) {
                        return;
                    }
    
                    if (this.path.length == 0) {
                        this.path = this.__computePath();
                    }
    
                    //返回并移除要移动到的坐标
                    target = this.path.shift();
    
                    //当前坐标
                    now = {
                        x: self.x / bomberConfig.WIDTH,
                        y: self.y / bomberConfig.HEIGHT
                    };
    
                    //判断要移动的方向,调用相应的方法
                    if (target.x > now.x) {
                        this.__context.walkRight();
                    }
                    else if (target.x < now.x) {
                        this.__context.walkLeft();
                    }
                    else if (target.y > now.y) {
                        this.__context.walkDown();
                    }
                    else if (target.y < now.y) {
                        this.__context.walkUp();
                    }
                    else {
                        this.__context.stand();
                    }
                }
            }
        });
    
        window.EnemySprite = EnemySprite;
    }());
    View Code

    PlayerSprite

    (function () {
        var PlayerSprite = YYC.Class(Sprite, {
            Init: function (data) {
                this.base(data);
    
                this.walkSpeed = data.walkSpeed;
                this.speedX = data.walkSpeed;
                this.speedY = data.walkSpeed;
    
                this.__context = new Context(this);
            },
            Private: {
                //状态模式上下文类
                __context: null,
                
                __getCurrentState: function () {
                    var currentState = null;
    
                    switch (this.defaultAnimId) {
                        case "stand_right":
                            currentState = new StandRightState();
                            break;
                        case "stand_left":
                            currentState = new StandLeftState;
                            break;
                        case "stand_down":
                            currentState = new StandDownState;
                            break;
                        case "stand_up":
                            currentState = new StandUpState;
                            break;
                        case "walk_down":
                            currentState = new WalkDownState;
                            break;
                        case "walk_up":
                            currentState = new WalkUpState;
                            break;
                        case "walk_right":
                            currentState = new WalkRightState;
                            break;
                        case "walk_left":
                            currentState = new 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;
                },
                __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: {
                //精灵的方向系数:
                //往下走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,
    
                init: function () {
                    this.__context.setPlayerState(this.__getCurrentState());
                    this.__computeStep();
    
                    this.base();
                },
                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

    增加CharacterLayer类

    • PlayerLayer、EnemyLayer有相似的模式
    • 从语义上来看,PlayerLayer、EnemyLayer都是属于”人物“的语义

    因此,提出CharacterLayer父类

    领域模型

    相关代码

    Layer

    (function () {
        var Layer = YYC.AClass(Collection, {
            Init: function () {
            },
            Private: {
                __state: bomberConfig.layer.state.CHANGE,   //默认为change
    
                __getContext: function () {
                    this.P__context = this.P__canvas.getContext("2d");
                }
            },
            Protected: {
                //*共用的变量(可读、写)
    
                P__canvas: null,
                P__context: null,
    
                //*共用的方法(可读)
    
                P__isChange: function(){
                    return this.__state === bomberConfig.layer.state.CHANGE;
                },
                P__isNormal: function () {
                    return this.__state === bomberConfig.layer.state.NORMAL;
                },
                P__setStateNormal: function () {
                    this.__state = bomberConfig.layer.state.NORMAL;
                },
                P__setStateChange: function () {
                    this.__state = bomberConfig.layer.state.CHANGE;
                },
                P__iterator: function (handler) {
                    var args = Array.prototype.slice.call(arguments, 1),
                        nextElement = null;
    
                    while (this.hasNext()) {
                        nextElement = this.next();
                        nextElement[handler].apply(nextElement, args);  //要指向nextElement
                    }
                    this.resetCursor();
                }
            },
            Public: {
                addElements: function(elements){
                    this.appendChilds(elements);
                },
                Virtual: {
                    init: function () {
                        this.__getContext();
                    },
                    change: function () {
                        this.__state = bomberConfig.layer.state.CHANGE;
                    }
                }
            },
            Abstract: {
                setCanvas: function () {
                },
                clear: function () {
                },
                //统一绘制
                draw: function () { },
                //游戏主线程调用的函数
                render: function () { }
            }
        });
    
        window.Layer = Layer;
    }());

    CharacterLayer

    (function () {
        var CharacterLayer = YYC.AClass(Layer, {
            Init: function (deltaTime) {
                this.___deltaTime = deltaTime;
            },
            Private: {
                ___deltaTime: 0,
    
                ___update: function (deltaTime) {
                    this.P__iterator("update", deltaTime);
                },
                ___setDir: function () {
                    this.P__iterator("setDir");
                },
                ___move: function () {
                    this.P__iterator("move");
                }
            },
            Public: {
                draw: function () {
                    this.P__iterator("draw", this.P__context);
                },
                clear: function () {
                    this.P__iterator("clear", this.P__context);
                },
                render: function () {
                    this.___setDir();
                    this.___move();
                    
                    if (this.P__isChange()) {
                        this.clear();
                        this.___update(this.___deltaTime);
                        this.draw();
                        this.P__setStateNormal();
                    }
                }
            }
        });
    
        window.CharacterLayer = CharacterLayer;
    }());

    PlayerLayer

    (function () {
        var PlayerLayer = YYC.Class(CharacterLayer, {
            Init: function (deltaTime) {
                this.base(deltaTime);
            },
            Private: {
                ___keyDown: function () {
                    if (keyState[keyCodeMap.A] === true || keyState[keyCodeMap.D] === true
                        || keyState[keyCodeMap.W] === true || keyState[keyCodeMap.S] === true) {
                        return true;
                    }
                    else {
                        return false;
                    }
                },
                ___spriteMoving: function () {
                    return this.getChildAt(0).moving
                },
                ___spriteStand: function () {
                    if (this.getChildAt(0).stand) {
                        this.getChildAt(0).stand = false;
                        return true;
                    }
                    else {
                        return false;
                    }
                }
            },
            Public: {
                setCanvas: function () {
                    this.P__canvas = document.getElementById("playerLayerCanvas");
    
                    $("#playerLayerCanvas").css({
                        "position": "absolute",
                        "top": bomberConfig.canvas.TOP,
                        "left": bomberConfig.canvas.LEFT,
                        "border": "1px solid red",
                        "z-index": 1
                    });
                },
                change: function () {
                    if (this.___keyDown() || this.___spriteMoving() || this.___spriteStand()) {
                        this.base();
                    }
                }
            }
        });
    
        window.PlayerLayer = PlayerLayer;
    }());

    EnemyLayer

    (function () {
        var EnemyLayer = YYC.Class(CharacterLayer, {
            Init: function (deltaTime) {
                this.base(deltaTime);
            },
            Private: {
                __getPath: function () {
                    this.P__iterator("getPath");
                }
            },
            Public: {
                playerLayer: null,
    
                init: function () {
                    this.base();
                },
                setCanvas: function () {
                    this.P__canvas = document.getElementById("enemyLayerCanvas");
    
                    $("#enemyLayerCanvas").css({
                        "position": "absolute",
                        "top": bomberConfig.canvas.TOP,
                        "left": bomberConfig.canvas.LEFT,
                        "border": "1px solid black",
                        "z-index": 1
                    });
                },
                getPlayer: function (playerLayer) {
                    this.playerLayer = playerLayer;
                    this.P__iterator("setPlayerSprite", playerLayer.getChildAt(0));
                },
                collideWidthPlayer: function () {
                    try{
                        this.P__iterator("collideWidthPlayer", this.playerLayer.getChildAt(0));
                        return false;
                    }
                    catch(e){
                        return true;
                    }
                },
    
                render: function () {
                    this.__getPath();
    
                    this.base();
                }
            }
        });
    
        window.EnemyLayer = EnemyLayer;
    }());

    提出父类MoveSprite

    • PlayerSprite、EnemySprite有相似的模式
    • 从语义上来看,PlayerSprite、EnemySprite都是能够移动的精灵类

    因此,提出父类MoveSprite

    为什么不把PlayerSprite、EnemySprite的相似的模式直接提到Sprite中?

    • 抽象层次不同

    因为我提取的语义是“移动的精灵类”,而Sprite的语义是“精灵类”,属于更抽象的概念

    为什么不叫CharacterSprite?

    • 因为关注的语义不同。

    在提取CharacterLayer类时,关注的是PlayerLayer、EnemyLayer中“人物”语义;而在提取MoveSprite类时,关注的是PlayerSprite、EnemySprite中”移动“语义。因此,凡是属于”人物“这个语义的Layer类,都可以考虑继承于CharacterLayer;而凡是有”移动“这个特点的Sprite类,都可以考虑继承于MoveSprite。

    领域模型

    相关代码

    Sprite

    (function () {
        var Sprite = YYC.AClass({
            Init: function (data, bitmap) {
                this.bitmap = bitmap;
                this.x = data.x;
                this.y = data.y;
                this.defaultAnimId = data.defaultAnimId;
                this.anims = data.anims;
            },
            Private: {
                _updateFrame: function (deltaTime) {
                    if (this.currentAnim) {
                        this.currentAnim.update(deltaTime);
                    }
                }
            },
            Public: {
                bitmap: null,
    
                //精灵的坐标
                x: 0,
                y: 0,
    
                //精灵包含的所有 Animation 集合. Object类型, 数据存放方式为" id : animation ".
                anims: null,
                //默认的Animation的Id , string类型
                defaultAnimId: null,
    
                //当前的Animation.
                currentAnim: null,
    
                setAnim: function (animId) {
                    this.currentAnim = this.anims[animId];
                },
                resetCurrentFrame: function (index) {
                    this.currentAnim && this.currentAnim.setCurrentFrame(index);
                },
                getCollideRect: function () {
                    return {
                        x1: this.x,
                        y1: this.y,
                        x2: this.x + this.bitmap.width,
                        y2: this.y + this.bitmap.height
                    }
                },
                Virtual: {
                    init: function () {
                        //设置当前Animation
                        this.setAnim(this.defaultAnimId);
                    },
                    // 更新精灵当前状态.
                    update: function (deltaTime) {
                        this._updateFrame(deltaTime);
                    }
                }
            },
            Abstract: {
                draw: function (context) { },
                clear: function (context) { }
            }
        });
    
        window.Sprite = Sprite;
    }());

    MoveSprite

    (function () {
        var MoveSprite = YYC.AClass(Sprite, {
            Init: function (data, bitmap) {
                this.base(data, bitmap);
                this.minX = data.minX;
                this.maxX = data.maxX;
                this.minY = data.minY;
                this.maxY = data.maxY;
    
                this.walkSpeed = data.walkSpeed;
                this.speedX = data.walkSpeed;
                this.speedY = data.walkSpeed;
            },
            Protected: {
                //状态模式上下文类
                P__context: null
            },
            Private: {
                __getCurrentState: function () {
                    var currentState = null;
    
                    switch (this.defaultAnimId) {
                        case "stand_right":
                            currentState = new StandRightState();
                            break;
                        case "stand_left":
                            currentState = new StandLeftState;
                            break;
                        case "stand_down":
                            currentState = new StandDownState;
                            break;
                        case "stand_up":
                            currentState = new StandUpState;
                            break;
                        case "walk_down":
                            currentState = new WalkDownState;
                            break;
                        case "walk_up":
                            currentState = new WalkUpState;
                            break;
                        case "walk_right":
                            currentState = new WalkRightState;
                            break;
                        case "walk_left":
                            currentState = new 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);
                },
                __isMoving: function(){
                    return this.x % bomberConfig.WIDTH !== 0 || this.y % bomberConfig.HEIGHT !== 0
                }
            },
            Public: {
                //精灵的速度
                speedX: 0,
                speedY: 0,
    
                //精灵的坐标区间
                minX: 0,
                maxX: 9999,
                minY: 0,
                maxY: 9999,
    
                //精灵的方向系数:
                //往下走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,
    
                init: function () {
                    this.P__context.setPlayerState(this.__getCurrentState());
                    this.__computeStep();
    
                    this.base();
                },
    
                draw: function (context) {
                    var frame = null;
    
                    if (this.currentAnim) {
                        frame = this.currentAnim.getCurrentFrame();
                        context.drawImage(this.bitmap.img, frame.x, frame.y, frame.width, frame.height, this.x, this.y, this.bitmap.width, this.bitmap.height);
                    }
                },
                clear: function (context) {
                    var frame = null;
    
                    if (this.currentAnim) {
                        frame = this.currentAnim.getCurrentFrame();
                        //直接清空画布区域
                        context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                    }
                },
                //获得当前坐标对应的方格坐标
                getCurrentCellPosition: function () {
                    if (this.__isMoving()) {
                        throw new Error("精灵正在移动且未完成一个移动步长");
                    }
                    return {
                        x: this.x / bomberConfig.WIDTH,
                        y: this.y / bomberConfig.HEIGHT
                    }
                }
            },
            Abstract: {
                move: function () { },
                setDir: function () { }
            }
        });
    
        window.MoveSprite = MoveSprite;
    }());
    View Code

    PlayerSprite

    (function () {
        var PlayerSprite = YYC.Class(MoveSprite, {
            Init: function (data, bitmap) {
                this.base(data, bitmap);
    
                this.P__context = new Context(this);
            },
            Private: {
                __allKeyUp: function () {
                    return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false
                        && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false;
                },
                __judgeAndSetDir: function () {
                    if (window.keyState[keyCodeMap.A] === true) {
                        this.P__context.walkLeft();
                    }
                    else if (window.keyState[keyCodeMap.D] === true) {
                        this.P__context.walkRight();
                    }
                    else if (window.keyState[keyCodeMap.W] === true) {
                        this.P__context.walkUp();
                    }
                    else if (window.keyState[keyCodeMap.S] === true) {
                        this.P__context.walkDown();
                    }
                }
            },
            Public: {
                move: function () {
                    this.P__context.move();
                },
                setDir: function () {
                    if (this.moving) {
                        return;
                    }
    
                    if (this.__allKeyUp()) {
                        this.P__context.stand();
                    }
                    else {
                        this.__judgeAndSetDir();
                    }
                }
            }
        });
    
        window.PlayerSprite = PlayerSprite;
    }());

    EnemySprite

    (function () {
        var EnemySprite = YYC.Class(MoveSprite, {
            Init: function (data, bitmap) {
                this.base(data, bitmap);
    
                this.P__context = new Context(this);
            },
            Private: {
                ___findPath: function () {
                    return window.findPath.aCompute(window.terrainData, this.___computeCurrentCoordinate(),
                        this.___computePlayerCoordinate()).path
                },
                ___computeCurrentCoordinate: function () {
                    if (this.x % window.bomberConfig.WIDTH || this.y % window.bomberConfig.HEIGHT) {
                        throw new Error("当前坐标应该为方格尺寸的整数倍!");
                    }
    
                    return {
                        x: this.x / window.bomberConfig.WIDTH,
                        y: this.y / window.bomberConfig.HEIGHT
                    };
                },
                ___computePlayerCoordinate: function () {
                    return {
                        x: Math.floor(this.playerSprite.x / window.bomberConfig.WIDTH),
                        y: Math.floor(this.playerSprite.y / window.bomberConfig.HEIGHT)
                    };
                },
                ___getAndRemoveTarget: function () {
                    return this.path.shift();
                },
                ___judgeAndSetDir: function (target) {
                    //当前坐标
                    var current = this.___computeCurrentCoordinate();
    
                    //判断要移动的方向,调用相应的方法
                    if (target.x > current.x) {
                        this.P__context.walkRight();
                    }
                    else if (target.x < current.x) {
                        this.P__context.walkLeft();
                    }
                    else if (target.y > current.y) {
                        this.P__context.walkDown();
                    }
                    else if (target.y < current.y) {
                        this.P__context.walkUp();
                    }
                    else {
                        this.P__context.stand();
                    }
                }
            },
            Public: {
                //寻找的路径
                path: [],
                playerSprite: null,
    
                collideWidthPlayer : function(sprite2){
                    var rect1=this.getCollideRect();
                    var rect2=sprite2.getCollideRect();
    
                    //如果碰撞,则抛出异常
                    if (rect1 && rect2 && !(rect1.x1 >= rect2.x2 || rect1.y1 >= rect2.y2 || rect1.x2 <= rect2.x1 || rect1.y2 <= rect2.y1)) {
                        throw new Error();
                    }
                },
                setPlayerSprite: function (sprite) {
                    this.playerSprite = sprite;
                },
                move: function () {
                    this.P__context.move();
                },
                setDir: function () {
    
                    //如果正在移动或者找不到路径,则返回
                    if (this.moving || this.path === false) {
                        return;
                    }
    
                    this.___judgeAndSetDir(this.___getAndRemoveTarget());
                },
                getPath: function () {
                    if (this.moving) {
                        return;
                    }
    
                    if (this.path.length == 0) {
                        this.path = this.___findPath();
                    }
                }
            }
        });
    
        window.EnemySprite = EnemySprite;
    }());
    View Code

    将Bitmap注入到Sprite中

    反思Sprite类,发现在getCollideRect方法中,使用了图片的宽度和高度:

              getCollideRect: function () {
                    if (this.currentAnim) {
                        var f = this.currentAnim.getCurrentFrame();
                        return {
                            x1: this.x,
                            y1: this.y,
                            x2: this.x + f.imgWidth,
                            y2: this.y + f.imgHeight
                        }
                    }
                },

    此处图片的宽度和高度是从FrameData中读取的:

        var getFrames = (function () {
    ...
                imgWidth = bomberConfig.player.IMGWIDTH,
                imgHeight = bomberConfig.player.IMGHEIGHT;
    ...

    图片的宽度和高度属于图片信息,应该都放到Bitmap类中!

    在创建精灵实例时,将图片的宽度和高度包装到Bitmap中,并注入到精灵类中:

    SpriteFactory

    (function () {
        var spriteFactory = {
            createPlayer: function () {
                return new PlayerSprite(getSpriteData("player"), bitmapFactory.createBitmap({ img: window.imgLoader.get("player"),  bomberConfig.player.IMGWIDTH, height: bomberConfig.player.IMGHEIGHT }));
            },
            createEnemy: function () {
                return new EnemySprite(getSpriteData("enemy"), bitmapFactory.createBitmap({ img: window.imgLoader.get("enemy"),  bomberConfig.player.IMGWIDTH, height: bomberConfig.player.IMGHEIGHT }));
            }
        }
    
        window.spriteFactory = spriteFactory;
    }());

    然后在getCollideRect方法中改为读取Bitmap实例引用的宽度和高度:

    Sprite

    Init: function (data, bitmap) {
        this.bitmap = bitmap;
    ...
    },
    ...
    bitmap: null,
    ...
    getCollideRect: function () {
        return {
            x1: this.x,
            y1: this.y,
            x2: this.x + this.bitmap.width,
            y2: this.y + this.bitmap.height
        }
    },

    领域模型 

    删除data -> frames.js中的imgWidth、imgHeight

    现在FrameData中的imgWidth、imgHeight是多余的了,应该将其删除。

    增加MapElementSprite

    增加地图元素精灵类,它拥有图片Bitmap的实例。其中,地图的一个单元格就是一个地图元素精灵类。

    为什么增加?

    1、可以在创建MapLayer元素时,元素由bitmap改为精灵类,这样x、y属性就可以从bitmap移到精灵类中了.
    2、精灵类包含动画,方便后期增加动态地图。

    它的父类为Sprite还是MoveSprite?

    因为MapElementSprite不属于“移动”语义,且它与MoveSprite没有相同的模式,所以它应该继承于Sprite。

    领域模型

    相关代码

    MapElementSprite

    (function () {
        var MapElementSprite = YYC.Class(Sprite, {
            Init: function (data, bitmap) {
                this.base(data, bitmap);
            },
            Protected: {
            },
            Private: {
            },
            Public: {
                draw: function (context) {
                    context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height);
                },
                clear: function (context) {//直接清空画布区域
                    context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                }
            }
        });
    
        window.MapElementSprite = MapElementSprite;
    }());

    MapLayer

    (function () {
        var MapLayer = YYC.Class(Layer, {
            Init: function () {
            },
            Private: {
                ___canvasBuffer: null,
                ___contextBuffer: null,
    
                ___getCanvasBuffer: function () {
                    //缓冲的canvas也要在html中创建并设置width、height!
                    this.___canvasBuffer = document.getElementById("mapLayerCanvas_buffer");
                },
                ___getContextBuffer: function () {
                    this.___contextBuffer = this.___canvasBuffer.getContext("2d");
                },
                ___drawBuffer: function () {
                    this.P__iterator("draw", this.___contextBuffer);
                }
            },
            Public: {
                setCanvas: function () {
                    this.P__canvas = document.getElementById("mapLayerCanvas");
                    
                    var css = {
                        "position": "absolute",
                        "top": bomberConfig.canvas.TOP,
                        "left": bomberConfig.canvas.LEFT,
                        "border": "1px solid blue",
                        "z-index": 0
                    };
    
                    $("#mapLayerCanvas").css(css);
                    //缓冲canvas的css也要设置!
                    $("#mapLayerCanvas_buffer").css(css);
                },
                init: function(){
                    //*双缓冲
    
                    //获得缓冲canvas
                    this.___getCanvasBuffer();
                    //获得缓冲context
                    this.___getContextBuffer();
    
                    this.base();
                },
    
    
                draw: function () {
                    this.___drawBuffer();
    
                    this.P__context.drawImage(this.___canvasBuffer, 0, 0);
    
                },
                clear: function () {
                    this.___contextBuffer.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                    this.P__context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                },
                render: function () {
                    if (this.P__isChange()) {
                        this.clear();
                        this.draw();
                        this.P__setStateNormal();
                    }
                }
            }
        });
    
        window.MapLayer = MapLayer;
    }());
    View Code

    重构Bitmap

    删除Bitmap的x、y属性。

    现在Bitmap的x、y属性用于保存地图图片的坐标。现在坐标保存在地图精灵类中了,故Bitmap中多余的x、y属性。

    相关代码

    Bitmap

    (function () {
        var Bitmap = YYC.Class({
            Init: function (data) {
                this.img = data.img;
                this.width = data.width;
                this.height = data.height;
            },
            Private: {
            },
            Public: {
                img: null,
                 0,
                height: 0
            }
        });
    
        window.Bitmap = Bitmap;
    }());

    重构LayerManager

    LayerManager本来的职责为“负责层的逻辑”,但是我认为LayerManager的职责应该为“负责层的统一操作”,它应该为一个键-值集合类,它的元素应该为Layer的实例。

    因此对LayerManager进行重构:

    • 将change的判断移到具体的Layer中

    由Layer类应该负责自己状态的维护。

    • 将createElement放到Game中

    创建层内元素createElement这个职责应该放到调用LayerManager的客户端,即Game类中。在Game中还要负责创建LayerManager、创建Layer。

    • 增加Hash

    增加一个Hash类,它实现键-值集合的通用操作,然后让LayerManager继承于Hash,使之成为集合类。

    为什么使用Hash类,而不是使用Collection类(数组集合类)

    功能上分析:

    因为LayerManager集合的元素为Layer实例,而每一个层的实例都是唯一的,即如PlayerLayer实例只有一个,不会有二个PlayerLayer实例。因此,使用Hash结构,可以通过key获得LayerManager集合中的每个元素。

    Hash的优势:

    Hash结构不需要知道LayerManager集合装入Layer实例的顺序,通过key值就可以唯一获得元素;而Collection结构(数组结构)需要知道装入顺序。

    领域模型

    相关代码

    LayerManager、PlayerLayerManager、EnemyLayerManager、MapLayerManager重构前:

    (function () {
    
        //* 父类
    
        var LayerManager = YYC.AClass({
            Init: function (layer) {
                this.layer = layer;
            },
            Private: {
            },
            Public: {
                layer: null,
    
                addElement: function (element) {
                    var i = 0,
                        len = 0;
    
                    for (i = 0, len = element.length; i < len; i++) {
                        this.layer.appendChild(element[i]);
                    }
                },
                render: function () {
                    this.layer.render();
                },
                Virtual: {
                    initLayer: function () {
                        this.layer.setCanvas();
                        this.layer.init();
                        this.layer.change();
                    }
                }
            },
            Abstract: {
                createElement: function () { },
                change: function () { }
            }
        });
    
    
        //*子类
    
        var MapLayerManager = YYC.Class(LayerManager, {
            Init: function (layer) {
                this.base(layer);
            },
            Private: {
                __getMapImg: function (i, j, mapData) {
                    var img = null;
    
                    switch (mapData[i][j]) {
                        case 1:
                            img = window.imgLoader.get("ground");
                            break;
                        case 2:
                            img = window.imgLoader.get("wall");
                            break;
                        default:
                            break
                    }
    
                    return img;
                }
            },
            Public: {
                //创建并设置每个地图单元bitmap,加入到元素数组中并返回。
                createElement: function () {
                    var i = 0,
                        j = 0,
                        x = 0,
                        y = 0,
                        row = bomberConfig.map.ROW,
                        col = bomberConfig.map.COL,
                        element = [],
                        mapData = mapDataOperate.getMapData(),
                        img = null;
    
                    for (i = 0; i < row; i++) {
                        //注意!
                        //y为纵向height,x为横向width
                        y = i * bomberConfig.HEIGHT;
    
                        for (j = 0; j < col; j++) {
                            x = j * bomberConfig.WIDTH;
                            img = this.__getMapImg(i, j, mapData);
    
                            element.push(spriteFactory.createMapElement({ x: x, y: y }, bitmapFactory.createBitmap({ img: img,  bomberConfig.WIDTH, height: bomberConfig.HEIGHT })));
                        }
                    }
    
                    return element;
                },
                change: function () {
                }
            }
        });
    
    
        var PlayerLayerManager = YYC.Class(LayerManager, {
            Init: function (layer) {
                this.base(layer);
            },
            Private: {
                __keyDown: function () {
                    if (keyState[keyCodeMap.A] === true || keyState[keyCodeMap.D] === true
                        || keyState[keyCodeMap.W] === true || keyState[keyCodeMap.S] === true) {
                        return true;
                    }
                    else {
                        return false;
                    }
                },
                __spriteMoving: function () {
                    return this.layer.getChildAt(0).moving
                },
                __spriteStand: function () {
                    if (this.layer.getChildAt(0).stand) {
                        this.layer.getChildAt(0).stand = false;
                        return true;
                    }
                    else {
                        return false;
                    }
                }
            },
            Public: {
                createElement: function () {
                    var element = [],
                        player = spriteFactory.createPlayer();
    
                    player.init();
                    element.push(player);
    
                    return element;
                },
                change: function () {
                    if (this.__keyDown() || this.__spriteMoving() || this.__spriteStand()) {
                        this.layer.change();
                    }
                }
            }
        });
    
    
        var EnemyLayerManager = YYC.Class(LayerManager, {
            Init: function (layer) {
                this.base(layer);
            },
            Private: {
            },
            Public: {
                initLayer: function (playerLayerManager) {
                    this.layer.setCanvas();
                    this.layer.init();
                    this.layer.getPlayer(playerLayerManager.layer);
                    this.layer.change();
                },
                createElement: function () {
                    var element = [],
                        enemy = spriteFactory.createEnemy();
    
                    enemy.init();
                    element.push(enemy);
    
                    return element;
                },
    
                collideWidthPlayer: function () {
                    return this.layer.collideWidthPlayer();
                },
                change: function () {
                    this.layer.change();
                }
            }
        });
    
    
        window.LayerManager = LayerManager;     //用于测试
    
        window.MapLayerManager = MapLayerManager;
        window.PlayerLayerManager = PlayerLayerManager;
        window.EnemyLayerManager = EnemyLayerManager;
    }());
    View Code

    LayerManager重构后:

    (function () {
    
        var LayerManager = YYC.Class(Hash, {
            Private: {
                __iterator: function (handler) {
                    var args = Array.prototype.slice.call(arguments, 1),
                        i = null,
                        layers = this.getChilds();
    
                    for (i in layers) {
                        if (layers.hasOwnProperty(i)) {
                            layers[i][handler].apply(layers[i], args);
                        }
                    }
                }
            },
            Public: {
                addLayer: function (name, layer) {
                    this.add(name, layer);
                    return this;
                },
                getLayer: function (name) {
                    return this.getValue(name);
                },
                initLayer: function () {
                    this.__iterator("setCanvas");
                    this.__iterator("init");
                },
                render: function () {
                    this.__iterator("render");
                },
                change: function () {
                    this.__iterator("change");
                }
            }
        });
    
        window.LayerManager = LayerManager;
    }());

    Hash

    (function () {
        var Hash = YYC.AClass({
            Private: {
                //容器
                _childs: {}
            },
            Public: {
                getChilds: function () {
                    return YYC.Tool.extend.extend({}, this._childs);
                },
                getValue: function (key) {
                    return this._childs[key];
                },
                add: function (key, value) {
                    this._childs[key] = value;
                    return this;
                }
            }
        });
    
        window.Hash = Hash;
    }());

    Game

    (function () {
        var Game = YYC.Class({
            Init: function () {
            },
            Private: {
                _pattern: null,
                _ground: null,
                _layerManager: null,
    
                _createLayerManager: function () {
                    this._layerManager = new LayerManager();
                    this._layerManager.addLayer("mapLayer", layerFactory.createMap());
                    this._layerManager.addLayer("playerLayer", layerFactory.createPlayer(this.sleep));
                    this._layerManager.addLayer("enemyLayer", layerFactory.createEnemy(this.sleep));
                },
                _addElements: function () {
                    var mapLayerElements = this._createMapLayerElement(),
                        playerLayerElements = this._createPlayerLayerElement(),
                        enemyLayerElements = this._createEnemyLayerElement();
    
                    this._layerManager.addElements("mapLayer", mapLayerElements);
                    this._layerManager.addElements("playerLayer", playerLayerElements);
                    this._layerManager.addElements("enemyLayer", enemyLayerElements);
                },
                //创建并设置每个地图方格精灵,加入到元素数组中并返回。
                _createMapLayerElement: function () {
                    var i = 0,
                        j = 0,
                        x = 0,
                        y = 0,
                        row = bomberConfig.map.ROW,
                        col = bomberConfig.map.COL,
                        element = [],
                        mapData = mapDataOperate.getMapData(),
                        img = null;
    
                    for (i = 0; i < row; i++) {
                        //注意!
                        //y为纵向height,x为横向width
                        y = i * bomberConfig.HEIGHT;
    
                        for (j = 0; j < col; j++) {
                            x = j * bomberConfig.WIDTH;
                            img = this._getMapImg(i, j, mapData);
                            element.push(spriteFactory.createMapElement({ x: x, y: y }, bitmapFactory.createBitmap({ img: img,  bomberConfig.WIDTH, height: bomberConfig.HEIGHT })));
                        }
                    }
    
                    return element;
                },
                _getMapImg: function (i, j, mapData) {
                    var img = null;
    
                    switch (mapData[i][j]) {
                        case 1:
                            img = window.imgLoader.get("ground");
                            break;
                        case 2:
                            img = window.imgLoader.get("wall");
                            break;
                        default:
                            break
                    }
    
                    return img;
                },
                _createPlayerLayerElement: function () {
                    var element = [],
                        player = spriteFactory.createPlayer();
    
                    player.init();
                    element.push(player);
    
                    return element;
                },
                _createEnemyLayerElement: function () {
                    var element = [],
                        enemy = spriteFactory.createEnemy();
    
                    enemy.init();
                    element.push(enemy);
    
                    return element;
                },
                _initLayer: function () {
                    this._layerManager.initLayer();
                    this._layerManager.getLayer("enemyLayer").getPlayer(this._layerManager.getLayer("playerLayer"));
                },
                _initEvent: function () {
                    //监听整个document的keydown,keyup事件
                    keyEventManager.addKeyDown();
                    keyEventManager.addKeyUp();
                }
            },
            Public: {
                context: null,
                sleep: 0,
                x: 0,
                y: 0,
                mainLoop: null,
    
                init: function () {
                    this.sleep = Math.floor(1000 / bomberConfig.FPS);
                    this._createLayerManager();
                    this._addElements();
                    this._initLayer();
                    this._initEvent();
                },
                start: function () {
                    var self = this;
    
                    this.mainLoop = window.setInterval(function () {
                        self.run();
                    }, this.sleep);
                },
                run: function () {
                    if (this._layerManager.getLayer("enemyLayer").collideWidthPlayer()) {
                        clearInterval(this.mainLoop);
                        alert("Game Over!");
                        return;
                    }
                    this._layerManager.render();
                    this._layerManager.change();
                }
            }
        });
    
        window.Game = Game;
    }());
    View Code

    本文最终领域模型

    查看大图

    高层划分

    新增包

    • 算法包
      FindPath
    • 精灵抽象包
      Sprite
    • 哈希集合包
      Hash

    删除包

    • 层管理实现包
      经过本文重构后,去掉了PlayerLayerManager等子类,只保留了LayerManager类。因此去掉层管理实现包。

    层、包

    重构

    将集合包重命名为数组集合包

    “层”这个层级的集合包的命名太广泛了,应该具体化为数组集合包,这样才不至于与哈希集合包混淆。

    将哈希集合包和数组集合包合并为集合包

    哈希集合包与数组集合包都属于集合,因此应该合并为集合包,然后将集合包放到辅助逻辑层。

    提出抽象包

      分析

    封闭性:

    层抽象包、精灵抽象包位于同一个层面(抽象层面),会对同一种性质的变化共同封闭。

    如精灵抽象类(精灵抽象包)发生变化,可能会引起层抽象类的变化。

    重用性:

    两者相互关联。

    两者为抽象类,具有通用性。

    可以一起被重用。

       结论

    因此,将层抽象包、精灵抽象包合并成抽象包,并放到辅助逻辑层。

    将集合包也合并到抽象包中

    抽象包与集合包有依赖关系,但实际上只是抽象包中的层抽象类与集合包有依赖,精灵抽象类与集合包没有管理,因此违反了共用重用原则CRP。

    考虑到集合包也具有的通用性,将其也合并到抽象包中:

    提出人物包、地图包

    层和精灵都包含人物(精灵中为移动)、地图的概念,人物层与移动精灵、地图层与地图元素精灵联系紧密。

      分析

    封闭性:

    层实现包与精灵实现包违反了共同封闭原则CCP。如地图发生变化时,只会引起地图层和地图元素精灵的变化,而不会引起人物层和移动精灵的变化。

    重用性:

    层实现包与精灵实现包中,人物层与地图层、移动精灵与地图元素精灵没有关联,因此违反了共同重用原则CRP。

    因此,分离出人物包、地图包,并将层、精灵这两个层合并为实现层:

      状态类放到哪

    状态类与属于人物包的玩家精灵类和敌人精灵类紧密关联,因此应该放到人物包中。

      MoveSprite、CharacterLayer应不应该放到抽象包中

    MoveSprite、CharacterLayer是抽象类,但是它们与具体的人物实现密切相关(因为它们是从人物实现类PlayerSprite和EnemySprite、PlayerLayer和EnemyLayer提取共同模式而形成的父类)。因此,它们应该放到人物实现包中。

    本文最终层、包

    对应领域模型

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

    演示地址

    演示地址

    本文参考资料

    A星算法

    欢迎浏览上一篇博文:炸弹人游戏开发系列(6):实现碰撞检测,设置移动步长

    欢迎浏览下一篇博文:炸弹人游戏开发系列(8):放炸弹

  • 相关阅读:
    leetcode 131. Palindrome Partitioning
    leetcode 526. Beautiful Arrangement
    poj 1852 Ants
    leetcode 1219. Path with Maximum Gold
    leetcode 66. Plus One
    leetcode 43. Multiply Strings
    pytorch中torch.narrow()函数
    pytorch中的torch.repeat()函数与numpy.tile()
    leetcode 1051. Height Checker
    leetcode 561. Array Partition I
  • 原文地址:https://www.cnblogs.com/chaogex/p/3334223.html
Copyright © 2011-2022 走看看