资讯专栏INFORMATION COLUMN

Chrome 小恐龙游戏源码探究九 -- 游戏碰撞检测

cpupro / 1196人阅读

摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究八奔跑的小恐龙实现了小恐龙的绘制以及键盘对小恐龙的控制,这一篇文章中将实现游戏的碰撞检测。

文章首发于我的 GitHub 博客
前言

上一篇文章:《Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙》实现了小恐龙的绘制以及键盘对小恐龙的控制,这一篇文章中将实现游戏的碰撞检测。

碰撞检测原理

这个游戏采用的检测方法是盒子碰撞,这种检测方法最大的好处就是简单,但是缺点是不够精确。

首先,如果将小恐龙和障碍物分别看作两个大的盒子,那么进行碰撞检测的效果如下:

可以看出,两个盒子虽然有重叠部分,但是实际小恐龙并没有和障碍物相撞。

所以想要进行更精确的检测,需要把物体拆分成多个较小的盒子。例如:

但是拆分的时候也不能过于细致,否则运算的时候,很影响性能。

这个游戏中所进行的必要拆分如图所示:

这里值得一提的是,当小恐龙俯身时,只需要将其拆成一个大的盒子。因为当小恐龙俯身时,可以产生碰撞的部分只有前面,而在小恐龙前面碰撞一定会碰到它的头部。毕竟现在这个游戏中还没有那么矮小的障碍物,以至于刚好碰到小恐龙俯身时的下巴。

这就提示我们,如果想要对游戏进行扩展,添加新的障碍物,就要考虑到小恐龙当前的碰撞盒子是否需要进行调整,要确保当前的碰撞盒子可以正确检测出所有情况。

生成碰撞盒子

游戏中使用 CollisionBox 类来生成碰撞盒子:

/**
 * 用于生成碰撞盒子
 * @param {Number} x X 坐标
 * @param {Number} y Y坐标
 * @param {Number} w 宽度
 * @param {Number} h 高度
 */
function CollisionBox(x, y, w, h) {
  this.x = x;
  this.y = y;
  this.width = w;
  this.height = h;
};

小恐龙的碰撞盒子如下:

// 小恐龙的碰撞盒子
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 = [{
  type: "CACTUS_SMALL",  // 小仙人掌
  width: 17,
  height: 35,
  yPos: 105,             // 在 canvas 上的 y 坐标
  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 ], // y 坐标不固定
  multipleSpeed: 999,
  minSpeed: 8.5,
  minGap: 150,
  numFrames: 2,          // 两个动画帧  
  frameRate: 1000 / 6,   // 帧率(一帧的时间)
  speedOffset: 0.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),
+ ],
}];
添加碰撞盒子

Obstacle 类上添加属性:

function Obstacle(canvas, type, spriteImgPos, dimensions,
  gapCoefficient, speed, opt_xOffset) {
  //...

+ this.collisionBoxes = []; // 存储碰撞盒子
  
  // ...
}

添加方法,用于拷贝障碍物的碰撞盒子:

Obstacle.prototype = {
  // 复制碰撞盒子
  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);
    }
  },
};

然后,调用这个方法来初始化障碍物的碰撞盒子:

Obstacle.prototype = {
  init: function () {
+   this.cloneCollisionBoxes(); 

    // ...
  },
};

这里需要对仙人掌中间的碰撞盒子进行调整:

Obstacle.prototype = {
  init: function () {
    // ...

+   // 调整中间的碰撞盒子的大小
+   //      ____        ______        ________
+   //    _|   |-|    _|     |-|    _|       |-|
+   //   | |<->| |   | |<--->| |   | |<----->| |
+   //   | | 1 | |   | |  2  | |   | |   3   | |
+   //   |_|___|_|   |_|_____|_|   |_|_______|_|
+   //
+   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;
+   }

    // ...
  },
};
碰撞检测

首先,检测矩形四个边的相对位置,来判断两个矩形是否相交:

/**
 * 比较两个矩形是否相交
 * @param {CollisionBox} tRexBox 小恐龙的碰撞盒子
 * @param {CollisionBox} obstacleBox 障碍物的碰撞盒子
 */
function boxCompare(tRexBox, obstacleBox) {
  var crashed = false;

  // 两个矩形相交
  if (tRexBox.x < obstacleBox.x + obstacleBox.width &&
      tRexBox.x + tRexBox.width > obstacleBox.x &&
      tRexBox.y < obstacleBox.y + obstacleBox.height &&
      tRexBox.height + tRexBox.y > obstacleBox.y) {
    crashed = true;
  }

  return crashed;
};

然后调用这个方法,判断小恐龙和障碍物是否碰撞的逻辑如下:

/**
 * 检测盒子是否碰撞
 * @param {Object} obstacle 障碍物
 * @param {Object} tRex 小恐龙
 * @param {HTMLCanvasContext} opt_canvasCtx 画布上下文
 */
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
  // 调整碰撞盒子的边界,因为小恐龙和障碍物有 1 像素的白边
  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++) {
        // 调整碰撞盒子的实际位置(除去小恐龙和障碍物上 1 像素的白边)
        var adjTrexBox =
            createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
        var adjObstacleBox =
            createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
        var crashed = boxCompare(adjTrexBox, adjObstacleBox);

        // 绘制调试边框
        if (opt_canvasCtx) {
          drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
        }

        if (crashed) {
          return [adjTrexBox, adjObstacleBox];
        }
      }
    }
  }
  return false;
};

/**
 * 调整碰撞盒子
 * @param {!CollisionBox} box 原始的盒子
 * @param {!CollisionBox} adjustment 要调整成的盒子
 * @return {CollisionBox} 被调整的盒子对象
 */
function createAdjustedCollisionBox(box, adjustment) {
  return new CollisionBox(
    box.x + adjustment.x,
    box.y + adjustment.y,
    box.width,
    box.height);
};

/**
 * 绘制碰撞盒子的边框
 * @param {HTMLCanvasContext} canvasCtx canvas 上下文
 * @param {CollisionBox} tRexBox 小恐龙的碰撞盒子
 * @param {CollisionBox} obstacleBox 障碍物的碰撞盒子
 */
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
  canvasCtx.save();
  canvasCtx.strokeStyle = "#f00";
  canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);

  canvasCtx.strokeStyle = "#0f0";
  canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
      obstacleBox.width, obstacleBox.height);
  canvasCtx.restore();
};

其中 drawCollisionBoxes 方法是 debug 时用的,用于显示碰撞盒子的边框。

上面的代码中,对碰撞检测的计算进行了优化:首先判断小恐龙和障碍物最外层的盒子有没有碰撞,当它们最外层的盒子碰撞后,再计算里面的小盒子是否碰撞。这样和直接计算所有盒子是否碰撞比起来,性能要好很多。

然后,调用 checkForCollision 方法:

Runner.prototype = {
  update: function () {
    // ...

    if (this.playing) {
      // ...

+     // 碰撞检测
+     var collision = hasObstacles &&
+       checkForCollision(this.horizon.obstacles[0], this.tRex, this.ctx);

      // ...
    }

    // ...
  },
};

效果如下:

可以看到碰撞检测是实现了,但是小恐龙遮住了显示出来的碰撞盒子,这是因为更新画布时,绘制小恐龙的方法在碰撞检测后面调用。所以为了演示,我们把碰撞检测的调用代码调整一下位置:

Runner.prototype = {
  update: function () {
    // ...

    // 游戏变为开始状态或小恐龙还没有眨三次眼
    if (this.playing || (!this.activated &&
      this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
      this.tRex.update(deltaTime);

+     // 碰撞检测
+     var collision = hasObstacles &&
+       checkForCollision(this.horizon.obstacles[0], this.tRex, this.ctx);

      // 进行下一次更新
      this.scheduleNextUpdate();
    }
  },
};

这样就可以看到显示出的碰撞盒子,效果如下:

到此就实现了碰撞检测。至于检测出碰撞后,结束游戏的相关逻辑,放到下一章来实现。

查看添加或修改的代码,戳这里

Demo 体验地址:https://liuyib.github.io/blog/demo/game/google-dino/collision-detection/

上一篇 下一篇
Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙 Chrome 小恐龙游戏源码探究完 -- 游戏结束和其他要素

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103911.html

相关文章

  • Chrome 恐龙游戏源码探究一 -- 绘制静态地面

    摘要:首先是绘制静态的地面。上一篇下一篇无小恐龙游戏源码探究二让地面动起来 文章首发于我的 GitHub 博客 目录 Chrome 小恐龙游戏源码探究一 -- 绘制静态地面 Chrome 小恐龙游戏源码探究二 -- 让地面动起来 Chrome 小恐龙游戏源码探究三 -- 进入街机模式 Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵 Chrome 小恐龙游戏源码探究五 -- 随机绘...

    lixiang 评论0 收藏0
  • Chrome 恐龙游戏源码探究八 -- 奔跑的恐龙

    摘要:例如,将函数修改为小恐龙眨眼这样小恐龙会不停的眨眼睛。小恐龙的开场动画下面来实现小恐龙对键盘按键的响应。接下来还需要更新动画帧才能实现小恐龙的奔跑动画。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替》实现了游戏昼夜模式的交替,这一篇文章中,将实现:1、小恐龙的绘制 2、键盘对小恐龙的控制 3、页面失焦后,重新聚焦会重置...

    paulquei 评论0 收藏0
  • Chrome 恐龙游戏源码探究五 -- 随机绘制障碍

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究四随机绘制云朵实现了云朵的随机绘制,这一篇文章中将实现仙人掌翼龙障碍物的绘制游戏速度的改变障碍物的类型有两种仙人掌和翼龙。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵》 实现了云朵的随机绘制,这一篇文章中将实现:1、仙人掌、翼龙障碍物的绘制 2、游戏速度的改变 障碍物...

    tomorrowwu 评论0 收藏0
  • Chrome 恐龙游戏源码探究三 -- 进入街机模式

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究二让地面动起来实现了地面的移动。街机模式的效果就是游戏开始后,进入全屏模式。例如可以看到,进入街机模式之前,有一段开场动画。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究二 -- 让地面动起来》 实现了地面的移动。这一篇文章中,将实现效果:1、浏览器失焦时游戏暂停,聚焦游戏继续。 2、开场动...

    yeooo 评论0 收藏0
  • Chrome 恐龙游戏源码探究六 -- 记录游戏分数

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究五随机绘制障碍实现了障碍物仙人掌和翼龙的绘制。在游戏中,小恐龙移动的距离就是游戏的分数。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍》 实现了障碍物仙人掌和翼龙的绘制。这一篇将实现当前分数、最高分数的记录和绘制。 在游戏中,小恐龙移动的距离就是游戏的分数。分数每达 1...

    Jingbin_ 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<