• 炸弹人游戏开发系列(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):放炸弹

  • 相关阅读:
    转载__Java内部类
    Fragment之介绍(转)
    转载__Android-屏幕适配需要注意的地方
    转载__广播机制
    Activity的启动模式--总结
    图片_ _Android--加载大分辨率图片到内存
    转载—— android 瀑布流的实现详解,附源码
    转载_安卓性能优化
    C# Byte[] 数组操作
    C# 测算代码运行时间 Stopwatch
  • 原文地址:https://www.cnblogs.com/chaogex/p/3334223.html
Copyright © 2020-2023  润新知