首页 > 代码库 > Chrome自带恐龙小游戏的源码研究(七)

Chrome自带恐龙小游戏的源码研究(七)

  在上一篇《Chrome自带恐龙小游戏的源码研究(六)》中研究了恐龙的跳跃过程,这一篇研究恐龙与障碍物之间的碰撞检测。

碰撞盒子

  游戏中采用的是矩形(非旋转矩形)碰撞。这类碰撞优点是计算比较简单,缺点是对不规则物体的检测不够精确。如果不做更为精细的处理,结果会像下图:

 技术分享

如图所示,两个盒子虽然有重叠部分,但实际情况是恐龙和仙人掌之间并未发生碰撞。为了解决这个问题,需要建立多个碰撞盒子:

技术分享

不过这样还是有问题,观察图片,恐龙和仙人掌都有四个碰撞盒子,如果每次Game Loop里都对这些盒子进行碰撞检测,那么结果是每次需要进行4X4=16次计算,如果物体或者盒子很多,就会导致运算量大大增加,造成严重的性能问题。为改进这一点,只需要先检测两个大盒子之间是否碰撞,如果没有,则略去里面小盒子的碰撞检测。反之则对里面的小盒子做碰撞检测。游戏中使用CollisionBox构造函数创建碰撞盒子:

/*** 碰撞盒子* @param x    {number} 盒子x坐标* @param y    {number} 盒子y坐标* @param w    {number} 盒子宽度* @param h    {number} 盒子高度*/function CollisionBox(x, y, w, h) {    this.x = x;    this.y = y;    this.width = w;    this.height = h;}

使用boxCompare方法检测两个盒子是否发生碰撞:

 1 /** 2 * 碰撞检测 3 * @param tRexBox {Object} 霸王龙的碰撞盒子 4 * @param obstacleBox {Object} 障碍物的碰撞盒子 5 */ 6 function boxCompare(tRexBox, obstacleBox) { 7     var tRexBoxX = tRexBox.x, 8         tRexBoxY = tRexBox.y, 9         obstacleBoxX = obstacleBox.x,10         obstacleBoxY = obstacleBox.y;11 12     return tRexBoxX < obstacleBoxX + obstacleBox.width && tRexBoxX + tRexBox.width > obstacleBoxX && tRexBoxY < obstacleBoxY + obstacleBox.height && tRexBox.height + tRexBoxY > obstacleBoxY;13 }

 

建立碰撞盒子

  接下来要为恐龙和障碍物建立碰撞盒子。游戏中为恐龙建立了6个碰撞盒子,分布在头、躯干和脚,同时它还有闪避状态:

        Trex.collisionBoxes = {            DUCKING:[                new CollisionBox(1,18,55,25)            ],            RUNNING: [                new CollisionBox(22, 0, 17, 16),                new CollisionBox(1, 18, 30, 9),                new CollisionBox(10, 35, 14, 8),                new CollisionBox(1, 24, 29, 5),                new CollisionBox(5, 30, 21, 4),                new CollisionBox(9, 34, 15, 4)            ]        };

障碍物的碰撞盒子定义在Obstacle.types中:

技术分享
 1 Obstacle.types = [{ 2     type: ‘CACTUS_SMALL‘, 3     width: 17, 4     height: 35, 5     yPos: 105, 6     multipleSpeed: 4, 7     minGap: 120, 8     minSpeed: 0, 9     collisionBoxes: [new CollisionBox(0, 7, 5, 27), new CollisionBox(4, 0, 6, 34), new CollisionBox(10, 4, 7, 14)]10 },11 {12     type: ‘CACTUS_LARGE‘,13     width: 25,14     height: 50,15     yPos: 90,16     multipleSpeed: 7,17     minGap: 120,18     minSpeed: 0,19     collisionBoxes: [new CollisionBox(0, 12, 7, 38), new CollisionBox(8, 0, 7, 49), new CollisionBox(13, 10, 10, 38)]20 },21 {22     type: ‘PTERODACTYL‘,23     width: 46,24     height: 40,25     yPos: [100, 75, 50],26     // Variable height mobile.27     multipleSpeed: 999,28     minSpeed: 8.5,29     minGap: 150,30     collisionBoxes: [new CollisionBox(15, 15, 16, 5), new CollisionBox(18, 21, 24, 6), new CollisionBox(2, 14, 4, 3), new CollisionBox(6, 10, 4, 7), new CollisionBox(10, 8, 6, 9)],31     numFrames: 2,32     frameRate: 1000 / 6,33     speedOffset: .834 }];
View Code

不过这只是定义了障碍物数量为1的情况,复数的障碍物需要在创建时修正碰撞盒子:

1 if (this.size > 1) {//只针对仙人掌2     this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - this.collisionBoxes[2].width;3     this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;4 }

下图分别为单数和复数的盒子。

技术分享技术分享

最后执行碰撞检测:

技术分享
 1 function checkForCollision(obstacle, tRex) { 2         //创建最外层的大盒子 3     var tRexBox = new CollisionBox(tRex.xPos + 1, tRex.yPos + 1, tRex.config.WIDTH - 2, tRex.config.HEIGHT - 2); 4     var obstacleBox = new CollisionBox(obstacle.xPos + 1, obstacle.yPos + 1, obstacle.typeConfig.width * obstacle.size - 2, obstacle.typeConfig.height - 2); 5  6     } 7     if (boxCompare(tRexBox, obstacleBox)) { 8         var collisionBoxes = obstacle.collisionBoxes; 9         var tRexCollisionBoxes = tRex.ducking ? Trex.collisionBoxes.DUCKING: Trex.collisionBoxes.RUNNING;10 11         for (var t = 0; t < tRexCollisionBoxes.length; t++) {12             for (var i = 0; i < collisionBoxes.length; i++) {13                 //修正盒子14                 var adjTrexBox = createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);15                 var adjObstacleBox = createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);16                 var crashed = boxCompare(adjTrexBox, adjObstacleBox);17 18                 if (crashed) {19                     return [adjTrexBox, adjObstacleBox];20                 }21             }22         }23     }24     return false;25 }
View Code
技术分享
//修正盒子,将相对坐标转为画布坐标function createAdjustedCollisionBox(box, adjustment) {    return new CollisionBox(box.x + adjustment.x, box.y + adjustment.y, box.width, box.height);}
View Code

以下是最终运行效果,打开控制台就能看到碰撞输出:

 

<script type="text/javascript">// this.bumpThreshold ? this.dimensions.WIDTH : 0; }, draw:function() { this.ctx.drawImage(imgSprite, this.sourceXPos[0], this.spritePos.y, this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[0],this.yPos, this.dimensions.WIDTH,this.dimensions.HEIGHT); this.ctx.drawImage(imgSprite, this.sourceXPos[1], this.spritePos.y, this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[1],this.yPos, this.dimensions.WIDTH,this.dimensions.HEIGHT); }, updateXPos:function(pos,increment) { var line1 = pos, line2 = pos === 0 ? 1 : 0; this.xPos[line1] -= increment; this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; if(this.xPos[line1] <= -this.dimensions.WIDTH) { this.xPos[line1] += this.dimensions.WIDTH * 2; this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; } }, update:function(deltaTime,speed) { var increment = Math.floor(speed * (FPS / 1000) * deltaTime); if(this.xPos[0] <= 0) { this.updateXPos(0, increment); } else { this.updateXPos(1, increment); } this.draw(); }, reset:function() { this.xPos[0] = 0; this.xPos[1] = this.dimensions.WIDTH; } }; //endregion //region utils function getRandomNum(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function getTimeStamp() { return performance.now(); } //endregion //region Cloud Cloud.config = { HEIGHT:14, //云朵sprite的高度 MAX_CLOUD_GAP:400, //两朵云之间的最大间隙 MAX_SKY_LEVEL:30, //云朵的最大高度 MIN_CLOUD_GAP:100, //两朵云之间的最小间隙 MIN_SKY_LEVEL:71, //云朵的最小高度 WIDTH:46, //云朵sprite的宽度 MAX_CLOUDS:6,//最大云朵数量 CLOUD_FREQUENCY:.5 //云朵出现频率 }; Cloud.clouds = []; function Cloud(canvas,spritePos,containerWidth) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.spritePos = spritePos; this.containerWidth = containerWidth; this.xPos = containerWidth; //云朵初始x坐标在屏幕外 this.yPos = 0; //云朵初始高度 this.remove = false; //是否移除 //云间隙 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,Cloud.config.MAX_CLOUD_GAP); this.init(); } Cloud.prototype = { init:function () { //云朵高度随机 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,Cloud.config.MIN_SKY_LEVEL); this.draw(); }, draw:function () { this.ctx.save(); var sourceWidth = Cloud.config.WIDTH, sourceHeight = Cloud.config.HEIGHT; this.ctx.drawImage(imgSprite, this.spritePos.x,this.spritePos.y, sourceWidth,sourceHeight, this.xPos,this.yPos, sourceWidth,sourceHeight); this.ctx.restore(); }, updateClouds:function(speed) { var numClouds = Cloud.clouds.length; if(numClouds) { for(var i = numClouds - 1; i >= 0; i--) { Cloud.clouds[i].update(speed); } var lastCloud = Cloud.clouds[numClouds - 1]; if(numClouds < Cloud.config.MAX_CLOUDS && (DEFAULT_WIDTH - lastCloud.xPos) > lastCloud.cloudGap && Cloud.config.CLOUD_FREQUENCY > Math.random()) { this.addCloud(); } Cloud.clouds = Cloud.clouds.filter(function(obj){ return !obj.remove; }); } else { this.addCloud(); } }, update:function(speed) { if(!this.remove) { //向左移动 this.xPos -= Math.ceil(speed); this.draw(); if(!this.isVisible()) { this.remove = true; } } }, //判断云朵是否移出屏幕外 isVisible:function() { return this.xPos + Cloud.config.WIDTH > 0; }, addCloud:function () { var cloud = new Cloud(this.canvas,spriteDefinition.CLOUD,DEFAULT_WIDTH); Cloud.clouds.push(cloud); } }; //endregion //region NightMode NightMode.config = { FADE_SPEED: 0.035, //淡入淡出速度 HEIGHT: 40, //月亮高度 MOON_SPEED: 0.25, //月亮移动速度 NUM_STARS: 2, //星星数量 STAR_SIZE: 9, //星星宽度 STAR_SPEED: 0.3,//星星速度 STAR_MAX_Y: 70, //星星在画布上出现的位置 WIDTH: 20 //半个月度宽度 }; //月亮在不同时期有不同的位置 NightMode.phases = [140,120,100,60,40,20,0]; NightMode.invertTimer = 0; NightMode.inverted = false; NightMode.invertTrigger = false; NightMode.INVERT_FADE_DURATION = 5000; function NightMode(canvas,spritePos,containerWidth) { this.spritePos = spritePos; this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.containerWidth = containerWidth; this.xPos = containerWidth - 50; //月亮的x坐标 this.yPos = 30; //月亮的y坐标 this.currentPhase = 0; this.opacity = 0; this.stars = []; //用于存储星星 this.drawStars = false; //是否绘制星星 this.placeStars(); //放置星星 } NightMode.prototype = { update:function(activated) { //若夜晚模式处于激活状态且opacity为0时 //对月亮周期进行更新 if(activated && this.opacity == 0) { this.currentPhase++; if(this.currentPhase >= NightMode.phases.length) { this.currentPhase = 0; } } //淡入 if(activated && (this.opacity < 1 || this.opacity == 0)) { this.opacity += NightMode.config.FADE_SPEED; } else if(this.opacity > 0) {//淡出 this.opacity -= NightMode.config.FADE_SPEED; } //当opacity大于0时移动月亮位置 if(this.opacity > 0) { this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED); //移动星星 if(this.drawStars) { for (var i = 0; i < NightMode.config.NUM_STARS; i++) { this.stars[i].x = this.updateXPos(this.stars[i].x,NightMode.config.STAR_SPEED); } } this.draw(); } else { this.opacity = 0; this.placeStars(); } this.drawStars = true; }, updateXPos: function(currentPos, speed) { if (currentPos < -NightMode.config.WIDTH) { currentPos = this.containerWidth; } else { currentPos -= speed; } return currentPos; }, draw:function() { //周期为3时画满月 var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 : NightMode.config.WIDTH; var moonSourceHeight = NightMode.config.HEIGHT; //从雪碧图上获取月亮正确的形状 var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]; var moonOutputWidth = moonSourceWidth; var starSize = NightMode.config.STAR_SIZE; var starSourceX = spriteDefinition.STAR.x; this.ctx.save(); //画布透明度也随之变化 this.ctx.globalAlpha = this.opacity; if (this.drawStars) { for (var i = 0; i < NightMode.config.NUM_STARS; i++) { this.ctx.drawImage(imgSprite, starSourceX, this.stars[i].sourceY, starSize, starSize, Math.round(this.stars[i].x), this.stars[i].y, NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE); } } this.ctx.drawImage(imgSprite, moonSourceX, this.spritePos.y, moonSourceWidth, moonSourceHeight, Math.round(this.xPos), this.yPos, moonOutputWidth, NightMode.config.HEIGHT); this.ctx.globalAlpha = 1; this.ctx.restore(); }, placeStars:function() { //将画布分为若干组 var segmentSize = Math.round(this.containerWidth /NightMode.config.NUM_STARS); for (var i = 0; i < NightMode.config.NUM_STARS; i++) { this.stars[i] = {}; //每组星星位置随机 this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)); this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y); this.stars[i].sourceY = spriteDefinition.STAR.y + NightMode.config.STAR_SIZE * i; } }, invert:function(deltaTime) { this.update(NightMode.inverted); //黑夜持续时间5秒 if(NightMode.invertTimer > NightMode.INVERT_FADE_DURATION) { NightMode.invertTimer = 0; NightMode.invertTrigger = false; NightMode.inverted = document.body.classList.toggle(‘inverted‘,NightMode.invertTrigger); } else if(NightMode.invertTimer) { NightMode.invertTimer += deltaTime; } else { //每500帧触发黑夜,这里只是为了模拟效果,完整游戏中是每700米触发一次黑夜 NightMode.invertTrigger = !(gameFrame % 500); if(NightMode.invertTrigger && NightMode.invertTimer === 0) { NightMode.invertTimer += deltaTime; NightMode.inverted = document.body.classList.toggle(‘inverted‘,NightMode.invertTrigger); } } }, reset: function() { this.currentPhase = 0; this.opacity = 0; this.update(false); } }; //endregion //region Obstacle Obstacle.MAX_GAP_COEFFICIENT = 1.5; //障碍物最大间距系数 Obstacle.MAX_OBSTACLE_LENGTH = 3; //障碍物的最大数量 Obstacle.obstacles = []; //存储障碍物的数组 Obstacle.obstacleHistory = []; //记录障碍物数组中障碍物的类型 Obstacle.MAX_OBSTACLE_DUPLICATION = 2; //障碍物的最大重复数量 //障碍物类型的相关配置 Obstacle.types = [ { type: ‘CACTUS_SMALL‘, //小仙人掌 width: 17, height: 35, yPos: 105, multipleSpeed: 4, minGap: 120, //最小间距 minSpeed: 0, //最低速度 collisionBoxes: [ new CollisionBox(0, 7, 5, 27), new CollisionBox(4, 0, 6, 34), new CollisionBox(10, 4, 7, 14) ] }, { type: ‘CACTUS_LARGE‘, //大仙人掌 width: 25, height: 50, yPos: 90, multipleSpeed: 7, minGap: 120, minSpeed: 0, collisionBoxes: [ new CollisionBox(0, 12, 7, 38), new CollisionBox(8, 0, 7, 49), new CollisionBox(13, 10, 10, 38) ] }, { type: ‘PTERODACTYL‘, //翼龙 width: 46, height: 40, yPos: [ 100, 75, 50 ], //有高、中、低三种高度 multipleSpeed: 999, minSpeed: 8.5, //最小速度 minGap: 150, numFrames: 2, //有两个动画帧 frameRate: 1000/6, //动画帧的切换速率 speedOffset: .8, //速度修正 collisionBoxes: [ new CollisionBox(15, 15, 16, 5), new CollisionBox(18, 21, 24, 6), new CollisionBox(2, 14, 4, 3), new CollisionBox(6, 10, 4, 7), new CollisionBox(10, 8, 6, 9) ] } ]; /** * 绘制障碍物构造函数 * @param canvas * @param type 障碍物的类型 * @param spriteImgPos 雪碧图坐标 * @param dimensions 屏幕尺寸 * @param gapCoefficient 障碍物间隙 * @param speed 障碍物移动速度 * @param opt_xOffset 障碍物水平偏移量 * @constructor */ function Obstacle(canvas,type,spriteImgPos,dimensions,gapCoefficient,speed,opt_xOffset) { this.ctx = canvas.getContext(‘2d‘); this.spritePos = spriteImgPos; //障碍物类型(仙人掌、翼龙) this.typeConfig = type; this.gapCoefficient = gapCoefficient; //每个障碍物的数量(1~3) this.size = getRandomNum(1,Obstacle.MAX_OBSTACLE_LENGTH); this.dimensions = dimensions; //表示该障碍物是否可以被移除 this.remove = false; //水平坐标 this.xPos = dimensions.WIDTH + (opt_xOffset || 0); this.yPos = 0; this.width = 0; this.collisionBoxes = []; this.gap = 0; this.speedOffset = 0; //速度修正 //障碍物的动画帧 this.currentFrame = 0; //动画帧切换的计时器 this.timer = 0; this.init(speed); } Obstacle.prototype = { init:function(speed) { this.cloneCollisionBoxes(); //如果随机障碍物是翼龙,则只出现一只 //翼龙的multipleSpeed是999,远大于speed if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { this.size = 1; } //障碍物的总宽度等于单个障碍物的宽度乘以个数 this.width = this.typeConfig.width * this.size; //若障碍物的纵坐标是一个数组 //则随机选取一个 if (Array.isArray(this.typeConfig.yPos)) { var yPosConfig = this.typeConfig.yPos; this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; } else { this.yPos = this.typeConfig.yPos; } this.draw(); //修正盒子 if (this.size > 1) { this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - this.collisionBoxes[2].width; this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; } //对翼龙的速度进行修正,让它看起来有的飞得快一些,有些飞得慢一些 if (this.typeConfig.speedOffset) { this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : -this.typeConfig.speedOffset; } //障碍物之间的间隙,与游戏速度有关 this.gap = this.getGap(this.gapCoefficient, speed); }, cloneCollisionBoxes: function() { var collisionBoxes = this.typeConfig.collisionBoxes; for (var i = collisionBoxes.length - 1; i >= 0; i--) { this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x, collisionBoxes[i].y, collisionBoxes[i].width, collisionBoxes[i].height); } }, //障碍物之间的间隔,gapCoefficient为间隔系数 getGap: function(gapCoefficient, speed) { var minGap = Math.round(this.width * speed + this.typeConfig.minGap * gapCoefficient); var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); return getRandomNum(minGap, maxGap); }, //判断障碍物是否移出屏幕外 isVisible: function() { return this.xPos + this.width > 0; }, draw:function() { //障碍物宽高 var sourceWidth = this.typeConfig.width; var sourceHeight = this.typeConfig.height; //根据障碍物数量计算障碍物在雪碧图上的x坐标 //this.size的取值范围是1~3 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + this.spritePos.x; // 如果当前动画帧大于0,说明障碍物类型是翼龙 // 更新翼龙的雪碧图x坐标使其匹配第二帧动画 if (this.currentFrame > 0) { sourceX += sourceWidth * this.currentFrame; } this.ctx.drawImage(imgSprite, sourceX, this.spritePos.y, sourceWidth * this.size, sourceHeight, this.xPos, this.yPos, sourceWidth * this.size, sourceHeight); }, //单个障碍物的移动 update:function(deltaTime, speed) { //如果障碍物还没有移出屏幕外 if (!this.remove) { //如果有速度修正则修正速度 if (this.typeConfig.speedOffset) { speed += this.speedOffset; } //更新x坐标 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); // Update frame if (this.typeConfig.numFrames) { this.timer += deltaTime; if (this.timer >= this.typeConfig.frameRate) { //在两个动画帧之间来回切换以达到动画效果 this.currentFrame = this.currentFrame == this.typeConfig.numFrames - 1 ? 0 : this.currentFrame + 1; this.timer = 0; } } this.draw(); if (!this.isVisible()) { this.remove = true; } } }, //管理多个障碍物移动 updateObstacles: function(deltaTime, currentSpeed) { //保存一个障碍物列表的副本 var updatedObstacles = Obstacle.obstacles.slice(0); for (var i = 0; i < Obstacle.obstacles.length; i++) { var obstacle = Obstacle.obstacles[i]; obstacle.update(deltaTime, currentSpeed); //移除被标记为删除的障碍物 if (obstacle.remove) { updatedObstacles.shift(); } } Obstacle.obstacles = updatedObstacles; if(Obstacle.obstacles.length > 0) { //获取障碍物列表中的最后一个障碍物 var lastObstacle = Obstacle.obstacles[Obstacle.obstacles.length - 1]; //若满足条件则添加障碍物 if (lastObstacle && lastObstacle.isVisible() && (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < this.dimensions.WIDTH) { this.addNewObstacle(currentSpeed); } } else {//若障碍物列表中没有障碍物则立即添加 this.addNewObstacle(currentSpeed); } }, //随机添加障碍 addNewObstacle:function (currentSpeed) { //随机选取一种类型的障碍 var obstacleTypeIndex = getRandomNum(0,Obstacle.types.length - 1); var obstacleType = Obstacle.types[obstacleTypeIndex]; //检查随机取到的障碍物类型是否与前两个重复 //或者检查其速度是否合法,这样可以保证游戏在低速时不出现翼龙 //如果检查不通过,则重新再选一次直到通过为止 if(this.duplicateObstacleCheck(obstacleType.type) || currentSpeed < obstacleType.minSpeed) { this.addNewObstacle(currentSpeed); } else { //检查通过后,获取其雪碧图中的坐标 var obstacleSpritePos = this.spritePos[obstacleType.type]; //生成新的障碍物并存入数组 Obstacle.obstacles.push(new Obstacle(c,obstacleType,obstacleSpritePos,this.dimensions, this.gapCoefficient,currentSpeed,obstacleType.width)); //同时将障碍物的类型存入history数组 Obstacle.obstacleHistory.unshift(obstacleType.type); } //若history数组的长度大于1,则清空最前面的两个 if (Obstacle.obstacleHistory.length > 1) { Obstacle.obstacleHistory.splice(Obstacle.MAX_OBSTACLE_DUPLICATION); } }, //检查障碍物是否超过允许的最大重复数 duplicateObstacleCheck:function(nextObstacleType) { var duplicateCount = 0; //与history数组中的障碍物类型比较,最大只允许重得两次 for(var i = 0; i < Obstacle.obstacleHistory.length; i++) { duplicateCount = Obstacle.obstacleHistory[i] === nextObstacleType ? duplicateCount + 1 : 0; } return duplicateCount >= Obstacle.MAX_OBSTACLE_DUPLICATION; } }; //endregion //region Trex var keycode = { JUMP: {‘38‘: 1, ‘32‘: 1}, // Up, spacebar DUCK: {‘40‘: 1} //Down }; document.addEventListener(‘keydown‘,onKeyDown); document.addEventListener(‘keyup‘,onKeyUp); function onKeyDown(e) { if(keycode.JUMP[e.keyCode]) { e.preventDefault(); if(!trex.jumping && !trex.ducking) { trex.startJump(6); } } if(keycode.DUCK[e.keyCode]) { e.preventDefault(); if(trex.jumping) { trex.setSpeedDrop(); } else if(!trex.jumping && !trex.ducking) { trex.setDuck(true); } } } function onKeyUp(e) { if(keycode.JUMP[e.keyCode]) { trex.endJump(); } if(keycode.DUCK[e.keyCode]) { trex.speedDrop = false; trex.setDuck(false); } } //todo Trex.config = { BLINK_TIMING:3000, //眨眼间隔 WIDTH: 44, //站立时宽度 WIDTH_DUCK: 59, //闪避时宽度 HEIGHT: 47, //站立时高度 BOTTOM_PAD: 10, GRAVITY: 0.6, //重力 INIITAL_JUMP_VELOCITY: -10,//初始起跳速度 DROP_VELOCITY: -5, //下落速度 SPEED_DROP_COEFFICIENT:3, //加速下降系数 MIN_JUMP_HEIGHT: 30, //最小起跳高度 MAX_JUMP_HEIGHT: 30 //最大起跳高度 }; //状态 Trex.status = { CRASHED: ‘CRASHED‘, //与障碍物发生碰撞 DUCKING: ‘DUCKING‘, //闪避 JUMPING: ‘JUMPING‘, //跳跃 RUNNING: ‘RUNNING‘, //跑动 WAITING: ‘WAITING‘ //待机 }; //建立多个碰撞盒子 Trex.collisionBoxes = { DUCKING:[ new CollisionBox(1,18,55,25) ], RUNNING: [ new CollisionBox(22, 0, 17, 16), new CollisionBox(1, 18, 30, 9), new CollisionBox(10, 35, 14, 8), new CollisionBox(1, 24, 29, 5), new CollisionBox(5, 30, 21, 4), new CollisionBox(9, 34, 15, 4) ] }; //元数据(metadata),记录各个状态的动画帧和帧率 Trex.animFrames = { WAITING: {//待机状态 frames: [44, 0],//动画帧x坐标在44和0之间切换,由于在雪碧图中的y坐标是0所以不用记录 msPerFrame: 1000 / 3 //一秒3帧 }, RUNNING: { frames: [88, 132], msPerFrame: 1000 / 12 }, CRASHED: { frames: [220], msPerFrame: 1000 / 60 }, JUMPING: { frames: [0], msPerFrame: 1000 / 60 }, DUCKING: { frames: [262, 321], msPerFrame: 1000 / 8 } }; function Trex(canvas,spritePos){ this.canvas = canvas; this.ctx = canvas.getContext(‘2d‘); this.spritePos = spritePos; //在雪碧图中的位置 this.xPos = 0; //在画布中的x坐标 this.yPos = 0; //在画布中的y坐标 this.groundYPos = 0; //初始化地面的高度 this.currentFrame = 0; //初始化动画帧 this.currentAnimFrames = []; //记录当前状态的动画帧 this.blinkDelay = 0; //眨眼延迟(随机) this.animStartTime = 0; //动画开始的时间 this.timer = 0; //计时器 this.msPerFrame = 1000 / FPS; //默认帧率 this.config = Trex.config; //拷贝一个配置的副本方便以后使用 this.jumpVelocity = 0; //跳跃的初始速度 this.status = Trex.status.WAITING; //初始化默认状态为待机状态 //为各种状态建立标识 this.jumping = false; //角色是否处于跳跃中 this.ducking = false; //角色是否处于闪避中 this.reachedMinHeight = false; //是否到达最小跳跃高度 this.speedDrop = false; //是否加速降落 this.jumpCount = 0; //跳跃次数 this.init(); } Trex.prototype = { init:function() { this.groundYPos = DEFAULT_HEIGHT - this.config.HEIGHT - this.config.BOTTOM_PAD; this.yPos = this.groundYPos; //计算出最小起跳高度 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; this.draw(0,0); this.update(0,Trex.status.RUNNING); }, setBlinkDelay:function () { //设置随机眨眼间隔时间 this.blinkDelay = Math.ceil(Math.random() * Trex.config.BLINK_TIMING); }, setDuck:function (isDucking) { if (isDucking && this.status !== Trex.status.DUCKING) { this.update(0, Trex.status.DUCKING); this.ducking = true; } else if (this.status === Trex.status.DUCKING) { this.update(0, Trex.status.RUNNING); this.ducking = false; } }, update:function (deltaTime,opt_status) { this.timer += deltaTime; if(opt_status) { this.status = opt_status; this.currentFrame = 0; //得到对应状态的帧率 e.g. WAITING 1000ms / 3fps = 333ms/fps this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; //对应状态的动画帧 e.g. WAITING [44,0] this.currentAnimFrames = Trex.animFrames[opt_status].frames; if(opt_status === Trex.status.WAITING) { //开始计时 this.animStartTime = getTimeStamp(); //设置延时 this.setBlinkDelay(); } } //待机状态 if(this.status === Trex.status.WAITING) { //执行眨眼动作 this.blink(getTimeStamp()); } else { this.draw(this.currentAnimFrames[this.currentFrame], 0); } //计时器超过一帧的运行时间,切换到下一帧 if (this.timer >= this.msPerFrame) { this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; this.timer = 0; } if (this.speedDrop && this.yPos === this.groundYPos) { this.speedDrop = false; this.setDuck(true); } }, //开始跳跃 startJump:function (speed) { if(!this.jumping) { //切换到jump状态 this.update(0,Trex.status.JUMPING); //设置跳跃速度 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10); this.jumping = true; this.reachedMinHeight = false; this.speedDrop = false; } }, updateJump:function (deltaTime, speed) { //帧切换速率 var msPerFrame = Trex.animFrames[this.status].msPerFrame; //经过的帧数 var framesElapsed = deltaTime / msPerFrame; //更新y轴坐标 if(this.speedDrop) { this.yPos += Math.round(this.jumpVelocity * this.config.SPEED_DROP_COEFFICIENT * framesElapsed); } else { this.yPos += Math.round(this.jumpVelocity * framesElapsed); } //由于速度受重力影响,需要对速度进行修正 this.jumpVelocity += this.config.GRAVITY * framesElapsed; //达到最小跳跃高度 if (this.yPos < this.minJumpHeight || this.speedDrop) { this.reachedMinHeight = true; } //达到最大高度后停止跳跃 if(this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { this.endJump(); } if(this.yPos > this.groundYPos) { this.reset(); this.jumpCount++; } this.update(deltaTime); }, endJump: function() { if (this.reachedMinHeight && this.jumpVelocity < this.config.DROP_VELOCITY) { this.jumpVelocity = this.config.DROP_VELOCITY; } }, setSpeedDrop:function () { this.speedDrop = true; this.jumpVelocity = 1; }, reset:function () { this.yPos = this.groundYPos; this.jumpVelocity = 0; this.jumping = false; this.jumpCount = 0; this.ducking = false; this.update(0, Trex.status.RUNNING); this.speedDrop = false; }, blink:function (time) { var deltaTime = time - this.animStartTime; if(deltaTime >= this.blinkDelay) { this.draw(this.currentAnimFrames[this.currentFrame],0); if (this.currentFrame === 1) {//0闭眼 1开眼 //设置新的眨眼间隔时间 this.setBlinkDelay(); this.animStartTime = time; } } }, draw:function (x,y) { var sourceX = x; var sourceY = y; var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? this.config.WIDTH_DUCK : this.config.WIDTH; var sourceHeight = this.config.HEIGHT; sourceX += this.spritePos.x; sourceY += this.spritePos.y; this.ctx.drawImage(imgSprite, sourceX, sourceY, sourceWidth, sourceHeight, this.xPos, this.yPos, this.ducking ? this.config.WIDTH_DUCK : this.config.WIDTH, this.config.HEIGHT); } }; //endregion //region CheckCollision /** * 碰撞盒子 * @param x {number} x坐标 * @param y {number} y坐标 * @param w {number} 宽度 * @param h {number} 高度 */ function CollisionBox(x,y,w,h) { this.x = x; this.y = y; this.width = w; this.height = h; } /** * 碰撞检测 * @param tRexBox {Object} 霸王龙的碰撞盒子 * @param obstacleBox {Object} 障碍物的碰撞盒子 */ function boxCompare(tRexBox, obstacleBox) { var tRexBoxX = tRexBox.x, tRexBoxY = tRexBox.y, obstacleBoxX = obstacleBox.x, obstacleBoxY = obstacleBox.y; return tRexBoxX < obstacleBoxX + obstacleBox.width && tRexBoxX + tRexBox.width > obstacleBoxX && tRexBoxY < obstacleBoxY + obstacleBox.height && tRexBox.height + tRexBoxY > obstacleBoxY; } function createAdjustedCollisionBox(box, adjustment) { return new CollisionBox( box.x + adjustment.x, box.y + adjustment.y, box.width, box.height); } function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { canvasCtx.save(); canvasCtx.lineWidth = 0.5; canvasCtx.strokeStyle = ‘#f00‘; canvasCtx.strokeRect(tRexBox.x+0.5, tRexBox.y+0.5, tRexBox.width, tRexBox.height); canvasCtx.strokeStyle = ‘#0f0‘; canvasCtx.strokeRect(obstacleBox.x+0.5, obstacleBox.y+0.5, obstacleBox.width, obstacleBox.height); canvasCtx.restore(); } function checkForCollision(obstacle, tRex, opt_canvasCtx) { var tRexBox = new CollisionBox( tRex.xPos + 1, tRex.yPos + 1, tRex.config.WIDTH - 2, tRex.config.HEIGHT - 2); var obstacleBox = new CollisionBox( obstacle.xPos + 1, obstacle.yPos + 1, obstacle.typeConfig.width * obstacle.size - 2, obstacle.typeConfig.height - 2); if (opt_canvasCtx) { drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox); } if (boxCompare(tRexBox, obstacleBox)) { var collisionBoxes = obstacle.collisionBoxes; var tRexCollisionBoxes = tRex.ducking ? Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING; for (var t = 0; t < tRexCollisionBoxes.length; t++) { for (var i = 0; i < collisionBoxes.length; i++) { // Adjust the box to actual positions. var adjTrexBox = createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox); var adjObstacleBox = createAdjustedCollisionBox(collisionBoxes[i], obstacleBox); var crashed = boxCompare(adjTrexBox, adjObstacleBox); // Draw boxes for debug. if (opt_canvasCtx) { drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox); } if (crashed) { return [adjTrexBox, adjObstacleBox]; } } } } return false; } //endregion window.onload = function () { var h = new HorizonLine(c,spriteDefinition.HORIZON); var cloud = new Cloud(c,spriteDefinition.CLOUD,DEFAULT_WIDTH); var night = new NightMode(c,spriteDefinition.MOON,DEFAULT_WIDTH); var obstacle = new Obstacle(c,Obstacle.types[0],spriteDefinition,{WIDTH:600},0.6,1); trex = new Trex(c,spriteDefinition.TREX); var startTime = 0; var deltaTime; var speed = 3; (function draw(time) { gameFrame++; /*if(speed < 13.5) { speed += 0.01; }*/ //ctx.clearRect(0,0,600,150); time = time || 0; deltaTime = time - startTime; /*cloud.updateClouds(0.2); night.invert(deltaTime);*/ if(trex.jumping) { ctx.clearRect(0,0,600,150); trex.updateJump(deltaTime); h.update(deltaTime,speed); obstacle.updateObstacles(deltaTime,speed); var collision = checkForCollision(Obstacle.obstacles[0], trex,ctx); if(collision) console.log(‘碰撞‘); } else { ctx.clearRect(0,0,600,150); h.update(deltaTime,speed); trex.update(deltaTime); obstacle.updateObstacles(deltaTime,speed); var collision = checkForCollision(Obstacle.obstacles[0], trex,ctx); if(collision) console.log(‘碰撞‘); } startTime = time; requestAnimationFrame(draw,c); })(); };// ]]></script>

后记

  通过建立碰撞盒子进行碰撞检测在应用上非常广泛,著名的街机游戏《街霸》和《拳皇》就是采用了这种方式:

技术分享

 技术分享

可以看到游戏中对人物建立了多个碰撞盒子,红色代表攻击区域,蓝色代表可以被攻击的区域,绿色区域之间不能重叠,用来推挤对手。

Chrome自带恐龙小游戏的源码研究(七)