炸彈人遊戲開發系列(7):加入敵人,使用A*算法尋路

前言css

上文中咱們實現了炸彈人與牆的碰撞檢測,以及設置移動步長來解決發現的問題。本文會加入1個AI敵人,敵人使用A*算法追蹤炸彈人。html

本文目的

加入敵人,追蹤炸彈人算法

本文主要內容

回顧上文更新後的領域模型

查看大圖canvas

開發策略數組

首先實現「加入敵人」功能。經過參考「炸彈人遊戲開發系列(4):炸彈人顯示與移動「中的實現,能夠初步分析出須要加入敵人圖片、敵人幀數據和精靈數據、敵人精靈類EnemySprite、敵人層EnemyLayer和敵人層管理類EnemyLayerManager。app

而後實現「追蹤炸彈人」功能。須要新建一個算法類FindPath,負責使用A*算法計算並返回路徑數據。ide

敵人精靈類與算法類的交互關係:函數

並行開發

能夠並行開發「加入敵人」和「追蹤炸彈人」。oop

先定義一個FindPath類的的接口,指定findPath方法輸入參數和返回參數的格式。post

實現「加入敵人」功能時,能夠按照接口指定的格式使用假的路徑數據來測試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, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                    ]
                },
                //向左站立
                stand_left: {
                    img: window.imgLoader.get("enemy"),
                    frames: [
                        { x: offset.x, y: offset.y + height, width: 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, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                    ]
                },
                //向下站立
                stand_down: {
                    img: window.imgLoader.get("enemy"),
                    frames: [
                        { x: offset.x, y: offset.y, width: 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, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + width, y: offset.y + 3 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 2 * width, y: offset.y + 3 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 3 * width, y: offset.y + 3 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                    ]
                },
                //向下走
                walk_down: {
                    img: window.imgLoader.get("enemy"),
                    frames: [
                        { x: offset.x, y: offset.y, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + width, y: offset.y, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 2 * width, y: offset.y, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 3 * width, y: offset.y, width: 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, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + width, y: offset.y + 2 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 2 * width, y: offset.y + 2 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 3 * width, y: offset.y + 2 * height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 }
                    ]
                },
                //向左走
                walk_left: {
                    img: window.imgLoader.get("enemy"),
                    frames: [
                        { x: offset.x, y: offset.y + height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + width, y: offset.y + height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 2 * width, y: offset.y + height, width: sw, height: sh, imgWidth: imgWidth, imgHeight: imgHeight, duration: 100 },
                        { x: offset.x + 3 * width, y: offset.y + height, width: 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"), width: bomberConfig.player.IMGWIDTH, height: bomberConfig.player.IMGHEIGHT }));
        },
        createEnemy: function () {
            return new EnemySprite(getSpriteData("enemy"), bitmapFactory.createBitmap({ img: window.imgLoader.get("enemy"), width: 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的實例。其中,地圖的一個單元格就是一個地圖元素精靈類。

爲何增長?

一、能夠在建立MapLayer元素時,元素由bitmap改成精靈類,這樣x、y屬性就能夠從bitmap移到精靈類中了.
二、精靈類包含動畫,方便後期增長動態地圖。

它的父類爲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,
            width: 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, width: 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, width: 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):放炸彈

相關文章
相關標籤/搜索