前言
上文中我们实现了炸弹人与墙的碰撞检测,以及设置移动步长来解决发现的问题。本文会加入1个AI敌人,敌人使用A*算法追踪炸弹人。
本文目的
加入敌人,追踪炸弹人
本文主要内容
- 开发策略
- 加入敌人
- 实现 寻路算法
- 重构
- 本文最终领域模型
- 高层划分
- 演示
- 本文参考资料
回顾上文更新后的领域模型
查看大图
开发策略
首先实现“加入敌人”功能。通过参考“炸弹人游戏开发系列(4):炸弹人显示与移动“中的实现,可以初步分析出需要加入敌人图片、敌人帧数据和精灵数据、敌人精灵类EnemySprite、敌人层EnemyLayer和敌人层管理类EnemyLayerManager。
然后实现“追踪炸弹人”功能。需要新建一个算法类FindPath,负责使用A*算法计算并返回路径数据。
敌人精灵类与算法类的交互关系:
并行开发
可以并行开发“加入敌人”和“追踪炸弹人”。
先定义一个FindPath类的的接口,指定findPath方法输入参数和返回参数的格式。
实现“加入敌人”功能时,可以按照接口指定的格式使用假的路径数据来测试EnemySprite类;实现“追踪炸弹人”功能时,按照接口指定格式使用假的坐标数据来测试FindPath类。
在EnemySprit和FindPath都实现后,再集成在一起测试。因为两者接口一致,因此集成时不会有什么困难。
加入敌人
扩大地图
现在地图大小为4*4,太小了。
加入一个敌人后:
- 可玩性太低
很快游戏就结束了;玩家操作炸弹人躲避敌人的空间太小了。 - 不方便演示和测试游戏
由于游戏很快就结束,因此不方便演示和测试游戏。
因此,将地图扩大为20*20。
要实现这个功能,只需要修改MapData和TerrainData即可。
相关代码
MapData


//地图数据 (function () {var ground = bomberConfig.map.type.GROUND,wall = bomberConfig.map.type.WALL;var mapData = [[wall, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, wall, wall, wall,wall, wall, wall, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, wall, wall, ground,ground, ground, ground, wall, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, wall, wall, wall,ground, wall, ground, wall, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, wall, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground],[wall, ground, ground, ground, wall,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground,ground, ground, ground, ground, ground]];window.mapData = mapData; }());
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; }());
加入敌人图片和数据
首先加入敌人的精灵图片
然后加入敌人帧动画数据类


(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; }());
然后加入敌人精灵类数据
(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;//设置当前Animationthis.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; }());
增加敌人精灵类工厂
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(); //创建OpenListvar close_list = new Array(); //创建CloseListvar 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];elsearr_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;}elsereturn 14;}/***********************************************************************function setSuccessorNode(bestNodeNum,map_w)*功能:把现节点的周围8个节点放入预先准备好的临时arr中以备检察*参数:现节点的编号0351 6247周围八个点的排序***********************************************************************/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(); //创建OpenListclose_list = new Array(); //创建CloseListtmp = new Array(); //存放当前节点的八个方向的节点 };var findPath = {aCompute: function (terrainData, begin, end) {_reset();return aCompute(terrainData, begin, end);}};window.findPath = findPath; }());
重构
重构状态模式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; }());
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; }());
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; }());
增加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 () {//设置当前Animationthis.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; }());
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; }());
将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的实例。其中,地图的一个单元格就是一个地图元素精灵类。
为什么增加?
1、可以在创建MapLayer元素时,元素由bitmap改为精灵类,这样x、y属性就可以从bitmap移到精灵类中了.
2、精灵类包含动画,方便后期增加动态地图。
它的父类为Sprite还是MoveSprite?
因为MapElementSprite不属于“移动”语义,且它与MoveSprite没有相同的模式,所以它应该继承于Sprite。
领域模型
相关代码
MapElementSprite
(function () {var MapElementSprite = YYC.Class(Sprite, { Init: function (data, bitmap) {this.base(data, bitmap);},Protected: {},Private: {},Public: {draw: function (context) {context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height); },clear: function (context) {//直接清空画布区域context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); }}});window.MapElementSprite = MapElementSprite; }());
MapLayer


(function () {var MapLayer = YYC.Class(Layer, {Init: function () {},Private: {___canvasBuffer: null,___contextBuffer: null,___getCanvasBuffer: function () {//缓冲的canvas也要在html中创建并设置width、height!this.___canvasBuffer = document.getElementById("mapLayerCanvas_buffer");},___getContextBuffer: function () {this.___contextBuffer = this.___canvasBuffer.getContext("2d");},___drawBuffer: function () {this.P__iterator("draw", this.___contextBuffer);}},Public: {setCanvas: function () {this.P__canvas = document.getElementById("mapLayerCanvas");var css = {"position": "absolute","top": bomberConfig.canvas.TOP,"left": bomberConfig.canvas.LEFT,"border": "1px solid blue","z-index": 0};$("#mapLayerCanvas").css(css);//缓冲canvas的css也要设置!$("#mapLayerCanvas_buffer").css(css);},init: function(){//*双缓冲//获得缓冲canvasthis.___getCanvasBuffer();//获得缓冲contextthis.___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; }());
重构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为横向widthy = 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; }());
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为横向widthy = 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; }());
本文最终领域模型
查看大图
高层划分
新增包
- 算法包
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星算法