炸弹人游戏开发系列(6):实现碰撞检测,设置移动步长
前言
上文中我们实现了“玩家控制炸弹人”的功能,本文将实现碰撞检测,让炸弹人不能穿过墙。在实现的过程中会发现炸弹人移动的问题,然后会通过设置移动步长来解决。
说明
名词解释
- 具体状态类
指应用于炸弹人移动状态的状态模式的ConcreState角色的类。这里具体包括WalkLeftState、WalkRightState、WalkUpState、WalkDownState、StandLeftState等类。
本文目的
实现碰撞检测
本文主要内容
回顾上文更新后的领域模型
对领域模型进行思考
重构PlayerSprite
重构前代码
(function () { var PlayerSprite = YYC.Class({ //供子类构造函数中调用 Init: function (data) { this.x = data.x; this.y = data.y; this.minX = data.minX; this.maxX = data.maxX; this.minY = data.minY; this.maxY = data.maxY; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; this.walkSpeed = data.walkSpeed; this._context = new Context(this); }, Private: { _context: null, _setCoordinate: function (deltaTime) { this.x = this.x + this.speedX * deltaTime; this.y = this.y + this.speedY * deltaTime; this._limitMove(); }, _limitMove: function () { this.x = Math.max(this.minX, Math.min(this.x, this.maxX)); this.y = Math.max(this.minY, Math.min(this.y, this.maxY)); }, _getCurrentState: function () { var currentState = null; switch (this.defaultAnimId) { case "stand_right": currentState = Context.standRightState; break; case "stand_left": currentState = Context.standLeftState; break; case "stand_down": currentState = Context.standDownState; break; case "stand_up": currentState = Context.standUpState; break; case "walk_down": currentState = Context.walkDownState; break; case "walk_up": currentState = Context.walkUpState; break; case "walk_right": currentState = Context.walkRightState; break; case "walk_left": currentState = Context.walkLeftState; break; default: throw new Error("未知的状态"); break; }; return currentState; } }, Public: { //精灵的坐标 x: 0, y: 0, //精灵的速度 walkSpeed: 0, speedX: 0, speedY: 0, //精灵的坐标区间 minX: 0, maxX: 9999, minY: 0, maxY: 9999, anims: null, //默认的Animation的Id , string类型 defaultAnimId: null, //当前的Animation. currentAnim: null, init: function () { this._context.setPlayerState(this._getCurrentState()); //设置当前Animation this.setAnim(this.defaultAnimId); }, //重置当前帧 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, //设置当前Animation, 参数为Animation的id, String类型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, // 更新精灵当前状态. update: function (deltaTime) { //每次循环,改变一下绘制的坐标 this._setCoordinate(deltaTime); if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, handleNext: function () { this._context.walkLeft(); this._context.walkRight(); this._context.walkUp(); this._context.walkDown(); this._context.stand(); } } }); window.PlayerSprite = PlayerSprite; }());
handleNext改名为changeDir
反思handleNext方法。从方法名来看,它的职责应该为处理本次循环的所有逻辑。然而,经过数次重构后,现在handleNext的职责只是调用状态类的方法,更具体的来说,它的职责为判断和设置炸弹人移动方向。
因此,应该将handleNext改名为changeDir,从而能够反映出它的职责。
从update方法中分离出move方法
再来审视update方法,发现它有两个职责:
- 更新坐标
- 更新动画
进一步思考,此处“更新坐标”的职责更抽象地来说应该为"炸弹人移动“的职责。应该将其提出,形成move方法。然后去掉”__setCoordinate“方法,将其代码直接写到move方法中
删除deltaTime
_setCoordinate: function (deltaTime) { this.x = this.x + this.speedX * deltaTime; this.y = this.y + this.speedY * deltaTime; this._limitMove(); },
这里deltaTime其实没有什么作用,因此将其删除。
重构后相关代码
PlayerSprite
update: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, move: function () { this.x = this.x + this.speedX; this.y = this.y + this.speedY; this._limitMove(); }, changeDir: function () { this._context.walkLeft(); this._context.walkRight(); this._context.walkUp(); this._context.walkDown(); this._context.stand(); }
要对应修改PlayerLayer
__changeDir: function () { this.___iterator("changeDir"); }, ___move: function () { this.___iterator("move"); }, ... render: function () { if (this.P__isChange()) { this.clear(this.P__context); this.__changeDir(); this.___move(); this.___update(); this.draw(this.P__context); this.P__setStateNormal(); } }
分离speedX/speedY属性的语义,提出“方向向量”概念dirX/dirY
状态类WalkLeftState
walkLeft: function () { var sprite = null; if (window.keyState[keyCodeMap.A] === true) { sprite = this.P_context.sprite; sprite.speedX = -sprite.walkSpeed; sprite.speedY = 0; sprite.setAnim("walk_left"); } },
目前是通过在具体状态类中改变speedX/speedY的正负(如+sprite.walkSpeed或-sprite.walkSpeed),来实现炸弹人移动方向的改变。因此,我发现speedX/speedY属性实际上有两个语义:
- 炸弹人移动速度
- 炸弹人移动方向
这样会造成speed语义混淆,不便于阅读和维护。因此,将“炸弹人移动方向”提出来,形成新的属性dirX/dirY,而speedX/speedY则保留“炸弹人移动速度”语义。
重构后相关代码
PlayerSprite
dirX: 0, dirY: 0, ... move: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); },
WalkLeftState(其它具体状态类也要做类似的修改)
walkLeft: function () { var sprite = null; if (window.keyState[keyCodeMap.A] === true) { sprite = this.P_context.sprite; sprite.dirX = -1; sprite.dirY = 0; sprite.setAnim("walk_left"); } },
开发策略
首先查阅相关资料,确定碰撞检测的方法,然后再实现炸弹人与地图砖墙的碰撞检测。
初步实现碰撞检测
提出“碰撞检测”的概念
在第2篇博文中提出了“碰撞检测”的概念:
用于检测炸弹人与砖墙、炸弹人与怪物等之间的碰撞。碰撞检测包括矩形碰撞、多边形碰撞等,一般使用矩形碰撞即可。
此处我采用矩形碰撞检测。
增加地形数据TerrainData
首先,我们需要一个存储地图中哪些区域能够通过,哪些区域不能通过的数据结构。
通过参考地图数据mapData,我决定数据结构选用二维数组,且地形数组与地图数组一一对应。
相关代码
地图数据MapData
(function () { var ground = bomberConfig.map.type.GROUND, wall = bomberConfig.map.type.WALL; var mapData = [ [ground, wall, ground, ground], [ground, ground, ground, ground], [ground, wall, ground, ground], [ground, wall, ground, ground] ]; window.mapData = mapData; }());
地形数据TerrainData
//地形数据 (function () { //0表示可以通过,1表示不能通过 var terrainData = [ [0, 1, 0, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0] ]; window.terrainData = terrainData; }());
重构TerrainData
受到MapData的启示,可以在Config中加入地形数据的枚举值(pass、stop),然后直接在TerrainData中使用枚举值。这样做有以下的好处:
- 增强可读性
- 枚举值放到Config中,方便统一管理
相关代码
Config
map: { ... terrain: { pass: 0, stop: 1 } },
TerrainData
//地形数据 (function () { var pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; var terrainData = [ [pass, stop, pass, pass], [pass, pass, pass, pass], [pass, stop, pass, pass], [pass, stop, pass, pass] ]; window.terrainData = terrainData; }());
在PlayerSprite中实现矩形碰撞检测
实现checkCollideWithMap方法:
_checkCollideWithMap: function () { var i1 = Math.floor((this.y) / bomberConfig.HEIGHT), i2 = Math.floor((this.y + bomberConfig.player.IMGHEIGHT - 1) / bomberConfig.HEIGHT), j1 = Math.floor((this.x) / bomberConfig.WIDTH), j2 = Math.floor((this.x + bomberConfig.player.IMGWIDTH - 1) / bomberConfig.WIDTH), terrainData = window.terrainData, pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; if (terrainData[i1][j1] === pass && terrainData[i1][j2] === pass && terrainData[i2][j1] === pass && terrainData[i2][j2] === pass) { return false; } else { return true; } },
在move中判断:
move: function () { var origin_x = this.x, origin_y = this.y; this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); if (this._checkCollideWithMap()) { this.x = origin_x; this.y = origin_y; } },
领域模型
设置移动步长
发现问题
如果炸弹人每次移动0.2个方格,炸弹人想通过两个障碍物之间的空地,则炸弹人所在矩形区域必须与空地区域平行时才能通过。这通常导致玩家需要调整多次才能顺利通过。
如图所示:
不能通过
可以通过
引入”移动步长“概念
结合参考资料”html5游戏开发-零基础开发RPG游戏-开源讲座(二)-跑起来吧英雄“,这里可以引出“移动步长”的概念:
即炸弹人一次移动一个地图方格(炸弹人一次会移动多步)。即如果一个方格长为10px,而游戏每次主循环轮询时炸弹人移动2px,则炸弹人一次需要移动5步。在炸弹人的一个移动步长完成之前,玩家不能操作炸弹人,直到炸弹人完成一个移动步长(即移动了一个方格),玩家才能操作炸弹人。
实现移动步长
提出概念
这里先提出以下概念:
- step
移动步数,炸弹人移动一个方格需要的步数
- completeOneMove(该标志会在后面重构中被删除)
炸弹人完成一个移动步长的标志
- moving
炸弹人正在移动的标志
- moveIndex
炸弹人在一次移动步长中已经移动的次数
具体实现
首先在游戏开始时,计算一次炸弹人移动一个方格需要的步数;然后在移动前,先判断是否完成一次移动步长,如果正在移动且没有完成一次步长,则moveIndex加1;在移动后,判断该次移动是否完成移动步长,并相应更新移动标志和moveIndex。
重构
将“moveIndex加1”移到状态类中
具体状态类的职责为:负责本状态的逻辑以及决定状态过渡。“moveIndex加1”这个职责属于“本状态的逻辑”,因此应该将其移到具体状态类中,封装为addIndex方法。
将按键判断移到PlayerSprite中
“按键判断”是状态转换事件的判断,这里因为炸弹人不同状态转换为同一状态的触发事件相同,所以可以将其移到上一层的客户端(调用具体状态类的地方)中,即移到PlayerSprite的changeDir方法中。具体分析详见Javascript设计模式之我见:状态模式中的“将触发状态的事件判断移到Warrior类中”。
相关代码
PlayerSprite
... _computeCoordinate: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; this._limitMove(); //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3), //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍), //因此此处需要向下取整。 if (this.completeOneMove) { this.x -= this.x % bomberConfig.WIDTH; this.y -= this.y % bomberConfig.HEIGHT; } }, //计算移动次数 _computeStep: function () { this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX); this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY); }, _allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, _judgeCompleteOneMoveByIndex: function () { if (!this.moving) { return; } if (this.moveIndex_x >= this.stepX) { this.moveIndex_x = 0; this.completeOneMove = true; } else if (this.moveIndex_y >= this.stepY) { this.moveIndex_y = 0; this.completeOneMove = true; } else { this.completeOneMove = false; } }, _judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this._context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this._context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this._context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this._context.walkDown(); } } ... //一次移动步长中的需要移动的次数 stepX: 0, stepY: 0, //一次移动步长中已经移动的次数 moveIndex_x: 0, moveIndex_y: 0, //是否正在移动标志 moving: false, //完成一次移动标志 completeOneMove: false, init: function () { this._context.setPlayerState(this._getCurrentState()); this._computeStep(); this.setAnim(this.defaultAnimId); }, ... move: function () { this._judgeCompleteOneMoveByIndex(); this._computeCoordinate(); }, changeDir: function () { if (!this.completeOneMove && this.moving) { this._context.addIndex(); return; } if (this._allKeyUp()) { this._context.stand(); } else { this._judgeAndSetDir(); } }
...
Context
(function () { var Context = YYC.Class({ Init: function (sprite) { this.sprite = sprite; }, Private: { _state: null }, Public: { sprite: null, setPlayerState: function (state) { this._state = state; this._state.setContext(this); }, walkLeft: function () { this._state.walkLeft(); }, walkRight: function () { this._state.walkRight(); }, walkUp: function () { this._state.walkUp(); }, walkDown: function () { this._state.walkDown(); }, stand: function () { this._state.stand(); }, addIndex: function () { this._state.addIndex(); } }, Static: { walkLeftState: new WalkLeftState(), walkRightState: new WalkRightState(), walkUpState: new WalkUpState(), walkDownState: new WalkDownState(), standLeftState: new StandLeftState(), standRightState: new StandRightState(), standUpState: new StandUpState(), standDownState: new StandDownState() } }); window.Context = Context; }());
WalkLeftState(此处只举一个状态类说明,其它状态类与该类类似):
...
walkLeft: function () { var sprite = this.P_context.sprite; sprite.dirX = -1; sprite.dirY = 0; sprite.setAnim("walk_left"); sprite.moving = true; this.addIndex(); }, addIndex: function () { this.P_context.sprite.moveIndex_x += 1; }
...
继续完成碰撞检测
对地图障碍物检测进行了修改,并将碰撞检测和边界检测移到具体状态类中。
相关代码
WalkLeftState(此处只举一个状态类说明,其它状态类与该类类似)
... walkLeft: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_left"); if (!this.checkPassMap()) { sprite.moving = false; sprite.dirX = 0; return; } sprite.dirX = -1; sprite.dirY = 0; sprite.moving = true; this.addIndex(); }, ... //检测是否可通过该地图。可以通过返回true,不能通过返回false checkPassMap: function () { return !this.checkCollideWithBarrier(); }, checkCollideWithBarrier: function () { var pass = bomberConfig.map.terrain.pass, stop = bomberConfig.map.terrain.stop; //计算目的地地形数组下标 var target_x = this.P_context.sprite.x / bomberConfig.WIDTH - 1, target_y = this.P_context.sprite.y / bomberConfig.HEIGHT; //超出边界 if (target_x >= terrainData.length || target_y >= terrainData[0].length) { return true; } if (target_x < 0) { return true; } //碰撞 if (window.terrainData[target_y][target_x] === stop) { return true; } return false; } ...
重构
重构PlayerSprite
将move移到状态类中
PlayerSprite的move方法负责炸弹人的移动,其应该属于具体状态类的职责(负责本状态的逻辑),故将PlayerSprite的move移到具体状态类中。
进一步分析
将PlayerSprite的move移到具体状态类中,从职责上来进一步分析,实质是将“炸弹人移动”的职责分散到各个具体状态类中了(如WalkLeftState、WalkRightState只负责X方向的移动,WalkUpState、WalkDownState只负责Y方向的移动)
优点
增加了细粒度的控制。可以控制各个具体状态类下炸弹人的移动。
缺点
不好统一管理。当想修改“炸弹人移动”的逻辑时,可能需要修改每个具体状态类的move。
不过这个缺点可以在后面的提取具体状态类的基类的重构中解决。因为该重构会将具体状态类中“炸弹人移动”的职责汇聚到基类中。
重构addIndex
现在PlayerSprite -> changeDir中不用调用addIndex方法了,可以直接在具体状态类的move方法中调用。
这样做的好处是具体状态类不用再公开addIndex方法了,而是将其私有化。
为什么把公有方法addIndex改为私有方法比较好?
这是因为改动一个类的私有成员时,只会影响到该类,而不会影响到与该类关联的其它类;而改动公有成员则可能会影响与之关联的其它类。特别当我们是在创建供别人使用的类库时,如果发布后再来修改公有成员,会对很多人造成影响!这也是符合“高内聚低耦合”的思想。
我们应该对公有权限保持警惕的态度,能设成私有的就私有,只公开必要的接口成员。
相关代码
PlayerSprite
move: function () { this._context.move(); },
WalkLeftState(WalkRightState与之类似)
move: function () { if (this.P_context.sprite.moving) { this.addIndex(); } this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); }, __addIndex: function(){ this.P_context.sprite.moveIndex_x += 1; }, __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (!sprite.moving) { return; } if (sprite.moveIndex_x >= sprite.stepX) { sprite.moveIndex_x = 0; sprite.completeOneMove = true; } else { sprite.completeOneMove = false; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.x = sprite.x + sprite.speedX * sprite.dirX; //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3), //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍), //因此此处需要向下取整。 //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整) if (sprite.completeOneMove) { sprite.x -= sprite.x % bomberConfig.WIDTH; } }
WalkUpState(WalkDownState与之类似)
move: function () { if (this.P_context.sprite.moving) { this.addIndex(); } this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); }, __addIndex: function(){ this.P_context.sprite.moveIndex_y += 1; }, __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (!sprite.moving) { return; } if (sprite.moveIndex_y >= sprite.stepY) { sprite.moveIndex_y = 0; sprite.completeOneMove = true; } else { sprite.completeOneMove = false; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.y = sprite.y + sprite.speedY * sprite.dirY; //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3), //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍), //因此此处需要向下取整。 //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整) if (sprite.completeOneMove) { sprite.y -= sprite.y % bomberConfig.HEIGHT; } }
重构状态模式
让我们来看看状态类。
思路
我发现具体状态类有很多重复的代码,有些方法有很多相似之处。这促使我提炼出一个高层的共同模式。具体的方法就是提炼出基类,然后用模板模式,在子类中实现不同点。
提炼出WalkState、StandState
因此,我从WalkLeftState,WalkRightState,WalkDownState,WalkUpState中提炼出基类WalkState,从StandLeftState、StandRightState、StandDownState、StandUpState中提炼出基类StandState。
提炼出WalkState_X、WalkState_Y
我发现在WalkLeftState,WalkRightState中和WalkDownState,WalkUpState中,它们分别有共同的模式,而这共同模式不能提到WalkState中。因此,我又从WalkLeftState,WalkRightState中提炼出WalkState_X,WalkDownState,WalkUpState中提炼出WalkState_Y,然后让WalkState_X和WalkState_Y继承于WalkState。
状态模式最新的领域模型
相关代码
PlayerState
(function () { var PlayerState = YYC.AClass({ Protected: { P_context: null }, Public: { setContext: function (context) { this.P_context = context; } }, Abstract: { stand: function () { }, walkLeft: function () { }, walkRight: function () { }, walkUp: function () { }, walkDown: function () { }, move: function () { } } }); window.PlayerState = PlayerState; }());
WalkState
(function () { var WalkState = YYC.AClass(PlayerState, { Protected: { //*子类可复用的代码 P__checkMapAndSetDir: function () { var sprite = this.P_context.sprite; this.P__setDir(); if (!this.__checkPassMap()) { sprite.moving = false; //sprite.dirX = 0; this.P__stop(); } else { sprite.moving = true; } }, Abstract: { P__setPlayerState: function () { }, //计算并返回目的地地形数组下标 P__computeTarget: function () { }, //检测是否超出地图边界。 //超出返回true,否则返回false P__checkBorder: function () { }, //设置方向 P__setDir: function () { }, //停止 P__stop: function () { } } }, Private: { //检测是否可通过该地图。可以通过返回true,不能通过返回false __checkPassMap: function () { //计算目的地地形数组下标 var target = this.P__computeTarget(); if (this.P__checkBorder(target)) { return false; } return !this.__checkCollideWithBarrier(target); }, //地形障碍物碰撞检测 __checkCollideWithBarrier: function (target) { var stop = bomberConfig.map.terrain.stop; //碰撞 if (window.terrainData[target.y][target.x] === stop) { return true; } return false; } }, Public: { stand: function () { this.P__setPlayerState(); this.P_context.stand(); this.P_context.sprite.resetCurrentFrame(0); this.P_context.sprite.stand = true; }, Virtual: { walkLeft: function () { this.P_context.setPlayerState(Context.walkLeftState); this.P_context.walkLeft(); this.P_context.sprite.resetCurrentFrame(0); }, walkRight: function () { this.P_context.setPlayerState(Context.walkRightState); this.P_context.walkRight(); this.P_context.sprite.resetCurrentFrame(0); }, walkUp: function () { this.P_context.setPlayerState(Context.walkUpState); this.P_context.walkUp(); this.P_context.sprite.resetCurrentFrame(0); }, walkDown: function () { this.P_context.setPlayerState(Context.walkDownState); this.P_context.walkDown(); this.P_context.sprite.resetCurrentFrame(0); } } }, Abstract: { move: function () { } } }); window.WalkState = WalkState; }());
WalkState_X
(function () { var WalkState_X = YYC.AClass(WalkState, { Protected: { }, Private: { __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (sprite.moveIndex_x >= sprite.stepX) { sprite.moveIndex_x = 0; sprite.moving = false; } else { sprite.moving = true; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.x = sprite.x + sprite.speedX * sprite.dirX; }, __roundingDown: function () { this.P_context.sprite.x -= this.P_context.sprite.x % bomberConfig.WIDTH; } }, Public: { move: function () { if (!this.P_context.sprite.moving) { this.__roundingDown(); return; } this.P_context.sprite.moveIndex_x += 1; this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); } }, Abstract: { } }); window.WalkState_X = WalkState_X; }());
WalkState_Y
(function () { var WalkState_Y = YYC.AClass(WalkState, { Protected: { }, Private: { __judgeCompleteOneMoveByIndex: function () { var sprite = this.P_context.sprite; if (sprite.moveIndex_y >= sprite.stepY) { sprite.moveIndex_y = 0; sprite.moving = false; } else { sprite.moving = true; } }, __computeCoordinate: function () { var sprite = this.P_context.sprite; sprite.y = sprite.y + sprite.speedY * sprite.dirY; }, __roundingDown: function () { this.P_context.sprite.y -= this.P_context.sprite.y % bomberConfig.WIDTH; } }, Public: { move: function () { if (!this.P_context.sprite.moving) { this.__roundingDown(); return; } this.P_context.sprite.moveIndex_y += 1; this.__judgeCompleteOneMoveByIndex(); this.__computeCoordinate(); } }, Abstract: { } }); window.WalkState_Y = WalkState_Y; }());
WalkLeftState
(function () { var WalkLeftState = YYC.Class(WalkState_X, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standLeftState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH - 1, y: sprite.y / window.bomberConfig.HEIGHT }; }, P__checkBorder: function (target) { if (target.x < 0) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_left"); sprite.dirX = -1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; } }, Public: { walkLeft: function () { this.P__checkMapAndSetDir(); } } }); window.WalkLeftState = WalkLeftState; }());
WalkRightState
(function () { var WalkRightState = YYC.Class(WalkState_X, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standRightState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH + 1, y: sprite.y / window.bomberConfig.HEIGHT }; }, P__checkBorder: function (target) { if (target.x >= window.terrainData[0].length) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_right"); sprite.dirX = 1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; } }, Public: { walkRight: function () { this.P__checkMapAndSetDir(); } } }); window.WalkRightState = WalkRightState; }());
WalkDownState
(function () { var WalkDownState = YYC.Class(WalkState_Y, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standDownState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH, y: sprite.y / window.bomberConfig.HEIGHT + 1 }; }, P__checkBorder: function (target) { if (target.y >= window.terrainData.length) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_down"); sprite.dirY = 1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; } }, Private: { }, Public: { walkDown: function () { this.P__checkMapAndSetDir(); } } }); window.WalkDownState = WalkDownState; }());
WalkUpState
(function () { var WalkUpState = YYC.Class(WalkState_Y, { Protected: { P__setPlayerState: function () { this.P_context.setPlayerState(Context.standUpState); }, P__computeTarget: function () { var sprite = this.P_context.sprite; return { x: sprite.x / window.bomberConfig.WIDTH, y: sprite.y / window.bomberConfig.HEIGHT - 1 }; }, P__checkBorder: function (target) { if (target.y < 0) { return true; } return false; }, P__setDir: function () { var sprite = this.P_context.sprite; sprite.setAnim("walk_up"); sprite.dirY = -1; }, P__stop: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; } }, Public: { walkUp: function () { this.P__checkMapAndSetDir(); } } }); window.WalkUpState = WalkUpState; }());
StandState
(function () { var StandState = YYC.AClass(PlayerState, { Protected: { }, Public: { walkLeft: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkLeftState); this.P_context.walkLeft(); }, walkRight: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkRightState); this.P_context.walkRight(); }, walkUp: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkUpState); this.P_context.walkUp(); }, walkDown: function () { this.P_context.sprite.resetCurrentFrame(0); this.P_context.setPlayerState(Context.walkDownState); this.P_context.walkDown(); }, move: function () { } }, Abstract: { } }); window.StandState = StandState; }());
StandLeftState
(function () { var StandLeftState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; sprite.setAnim("stand_left"); sprite.moving = false; } } }); window.StandLeftState = StandLeftState; }());
StandRightState
(function () { var StandRightState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirX = 0; sprite.setAnim("stand_right"); sprite.moving = false; } } }); window.StandRightState = StandRightState; }());
StandDownState
(function () { var StandDownState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; sprite.setAnim("stand_down"); sprite.moving = false; } } }); window.StandDownState = StandDownState; }());
StandUpState
(function () { var StandUpState = YYC.Class(StandState, { Public: { stand: function () { var sprite = this.P_context.sprite; sprite.dirY = 0; sprite.setAnim("stand_up"); sprite.moving = false; } } }); window.StandUpState = StandUpState; }());
重构PlayerSprite
changeDir改名为setDir
该方法会在游戏主循环中调用,并不会每次轮询时都改变炸弹人移动方向,因此changDir这个方法名不合理,改为setDir更为合适。
删除completeOneMove
现在可以不需要completeOneMove标志了,故将其删除。
重构后的PlayerSprite
(function () { var PlayerSprite = YYC.Class({ Init: function (data) { //初始坐标 this.x = data.x; this.y = data.y; this.speedX = data.speedX; this.speedY = data.speedY; //x/y坐标的最大值和最小值, 可用来限定移动范围. this.minX = data.minX; this.maxX = data.maxX; this.minY = data.minY; this.maxY = data.maxY; this.defaultAnimId = data.defaultAnimId; this.anims = data.anims; this.walkSpeed = data.walkSpeed; this.speedX = data.walkSpeed; this.speedY = data.walkSpeed; this._context = new Context(this); }, Private: { //状态模式上下文类 _context: null, //更新帧动画 _updateFrame: function (deltaTime) { if (this.currentAnim) { this.currentAnim.update(deltaTime); } }, _computeCoordinate: function () { this.x = this.x + this.speedX * this.dirX; this.y = this.y + this.speedY * this.dirY; //因为移动次数是向上取整,可能会造成移动次数偏多(如stepX为2.5,取整则stepX为3), //坐标可能会偏大(大于bomberConfig.WIDTH / bomberConfig.HEIGHT的整数倍), //因此此处需要向下取整。 //x、y为bomberConfig.WIDTH/bomberConfig.HEIGHT的整数倍(向下取整) if (this.completeOneMove) { this.x -= this.x % bomberConfig.WIDTH; this.y -= this.y % bomberConfig.HEIGHT; } }, _getCurrentState: function () { var currentState = null; switch (this.defaultAnimId) { case "stand_right": currentState = Context.standRightState; break; case "stand_left": currentState = Context.standLeftState; break; case "stand_down": currentState = Context.standDownState; break; case "stand_up": currentState = Context.standUpState; break; case "walk_down": currentState = Context.walkDownState; break; case "walk_up": currentState = Context.walkUpState; break; case "walk_right": currentState = Context.walkRightState; break; case "walk_left": currentState = Context.walkLeftState; break; default: throw new Error("未知的状态"); break; }; return currentState; }, //计算移动次数 _computeStep: function () { this.stepX = Math.ceil(bomberConfig.WIDTH / this.speedX); this.stepY = Math.ceil(bomberConfig.HEIGHT / this.speedY); }, _allKeyUp: function () { return window.keyState[keyCodeMap.A] === false && window.keyState[keyCodeMap.D] === false && window.keyState[keyCodeMap.W] === false && window.keyState[keyCodeMap.S] === false; }, _judgeCompleteOneMoveByIndex: function () { if (!this.moving) { return; } if (this.moveIndex_x >= this.stepX) { this.moveIndex_x = 0; this.completeOneMove = true; } else if (this.moveIndex_y >= this.stepY) { this.moveIndex_y = 0; this.completeOneMove = true; } else { this.completeOneMove = false; } }, _judgeAndSetDir: function () { if (window.keyState[keyCodeMap.A] === true) { this._context.walkLeft(); } else if (window.keyState[keyCodeMap.D] === true) { this._context.walkRight(); } else if (window.keyState[keyCodeMap.W] === true) { this._context.walkUp(); } else if (window.keyState[keyCodeMap.S] === true) { this._context.walkDown(); } } }, Public: { //精灵的坐标 x: 0, y: 0, //精灵的速度 speedX: 0, speedY: 0, //精灵的坐标区间 minX: 0, maxX: 9999, minY: 0, maxY: 9999, //精灵包含的所有 Animation 集合. Object类型, 数据存放方式为" id : animation ". anims: null, //默认的Animation的Id , string类型 defaultAnimId: null, //当前的Animation. currentAnim: null, //精灵的方向系数: //往下走dirY为正数,往上走dirY为负数; //往右走dirX为正数,往左走dirX为负数。 dirX: 0, dirY: 0, //定义sprite走路速度的绝对值 walkSpeed: 0, //一次移动步长中的需要移动的次数 stepX: 0, stepY: 0, //一次移动步长中已经移动的次数 moveIndex_x: 0, moveIndex_y: 0, //是否正在移动标志 moving: false, //站立标志 //用于解决调用WalkState.stand后,PlayerLayer.render中P__isChange返回false的问题 //(不调用draw,从而仍会显示精灵类walk的帧(而不会刷新为更新状态后的精灵类stand的帧))。 stand: false, //设置当前Animation, 参数为Animation的id, String类型 setAnim: function (animId) { this.currentAnim = this.anims[animId]; }, //重置当前帧 resetCurrentFrame: function (index) { this.currentAnim && this.currentAnim.setCurrentFrame(index); }, init: function () { this._context.setPlayerState(this._getCurrentState()); this._computeStep(); //设置当前Animation this.setAnim(this.defaultAnimId); }, // 更新精灵当前状态 update: function (deltaTime) { this._updateFrame(deltaTime); }, draw: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight); } }, clear: function (context) { var frame = null; if (this.currentAnim) { frame = this.currentAnim.getCurrentFrame(); //直接清空画布区域 context.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT); } }, move: function () { this._context.move(); }, setDir: function () { if (this.moving) { return; } if (this._allKeyUp()) { this._context.stand(); } else { this._judgeAndSetDir(); } } } }); window.PlayerSprite = PlayerSprite; }());
本文最终领域模型
高层划分
与上文相同,没有增加新的包
层、包
对应领域模型
- 辅助操作层
- 控件包
PreLoadImg - 配置包
Config
- 控件包
- 用户交互层
- 入口包
Main
- 入口包
- 业务逻辑层
- 辅助逻辑
- 工厂包
BitmapFactory、LayerFactory、SpriteFactory - 事件管理包
KeyState、KeyEventManager
- 工厂包
- 游戏主逻辑
- 主逻辑包
Game
- 主逻辑包
- 层管理
- 层管理实现包
PlayerLayerManager、MapLayerManager - 层管理抽象包
- LayerManager
- 层管理实现包
- 层
- 层实现包
PlayerLayer、MapLayer - 层抽象包
Layer - 集合包
Collection
- 层实现包
- 精灵
- 精灵包
PlayerSprite、Context、PlayerState、WalkState、StandState、WalkState_X、WalkState_Y、StandLeftState、StandRightState、StandUpState、StandDownState、WalkLeftState、WalkRightState、WalkUpState、WalkDownState - 动画包
Animation、GetSpriteData、SpriteData、GetFrames、FrameData
- 精灵包
- 辅助逻辑
- 数据操作层
- 地图数据操作包
MapDataOperate - 路径数据操作包
GetPath - 图片数据操作包
Bitmap
- 地图数据操作包
- 数据层
- 地图包
MapData、TerrainData - 图片路径包
ImgPathData
- 地图包
本文参考资料
html5游戏开发-零基础开发RPG游戏-开源讲座(二)-跑起来吧英雄