仿微信「跳一跳」- 4. 碰撞检测

上篇文章留了个问题:蚂蚁就算没有跳到下一个盒子上,游戏也会照常进行,这样明显不合理。下面就来解决这个问题。

1. 碰撞检测

要想判断蚂蚁是否成功落到了小盒子上,要用到碰撞检测。

Tiny.js 已经提供了简单的碰撞检测方法,可以检测两个无旋转矩形是否发生了碰撞。此外,还提供了一种像素检测碰撞的方法。对于简单的业务开发,直接使用这两种方法就可以了。

但是我们的游戏是斜 45 度地图,碰撞盒需要进行旋转。而且盒子的图片是包含阴影的,如果蚂蚁跳到阴影上显然不能算作成功,因此需要自定义一个不规则的多边形来作为碰撞盒。因此,我们需要寻找一个合适的碰撞检测算法。

通常的碰撞检测算法包括光线投射、分离轴等等,想了解的同学可以看下这篇文章:https://aotu.io/notes/2017/02/16/2d-collision-detection/

算法需要根据业务的实际需要来选择,因此我们重新梳理一下要解决的问题:

蚂蚁的脚算作一个点,如果蚂蚁跳到小盒子可以着力的位置(一个不规则的多边形),就算成功,否则失败。

这个问题可以抽象为一个点是否在一个不规则多边形内部的问题,因此我们直接使用了 @substack 大神的开源库 point-in-polygon

2. 自定义碰撞盒

2.1 蚂蚁的碰撞盒

蚂蚁的碰撞盒实际上是它的脚底的一个点。因此确定这个点的坐标就好。注意还要考虑整个窗口的偏移,然后进行微调。

getAntRegion() {
  var { x, y, width, height } = getCollisionRegion(this.ant);
  x -= this.position.x;
  y -= this.position.y;

  const realX = x + width / 2 + 10; // 底部中点然后再微调下
  const realY = y + height;

  // 画出来看看
  // this._drawAntRegion(realX, realY);

  return [realX, realY];
}

我们封装了一个方法 getCollisionRegion,来获取用于碰撞盒计算的坐标跟宽高。因为碰撞的物体可能在不同的组中,所以需要使用全局坐标来计算,然后还要考虑缩放比率和中心点。

/**
 * 获取 obj 用于碰撞盒计算的坐标跟宽高
 * @param {Tiny.Sprite} obj
 */
export function getCollisionRegion(obj) {
  // 使用全局坐标来计算
  let { x, y } = obj.getGlobalPosition();

  // 还要考虑缩放比率,不同配置中拿到的宽度是不同的
  const winW = Tiny.config.fixSize ? Tiny.config.width : window.innerWidth;
  const rate = winW / Tiny.WIN_SIZE.width;

  // 还要考虑中心点
  const pivot = obj.getPivot();

  return {
    x: x / rate - pivot.x,
    y: y / rate - pivot.y,
    width: obj.width,
    height: obj.height,
  };
}

为了看下碰撞盒是否符合期望,可以使用这个辅助方法在页面中实际绘制一下,供测试使用。

_drawAntRegion(x, y) {
  var mask = new Tiny.Graphics();
  mask.lineStyle(4, 0x66FF33, 1);
  mask.drawCircle(x, y, 1);
  mask.endFill();

  this.addChild(mask);
}

2.2 小盒子的碰撞盒

同样,可以绘制小盒子的碰撞盒。我们期望能够完全自定义这个多边形的形状,因此对其边界坐标进行了一些微调。同样要使用 getCollisionRegion 来得到小盒子的坐标和宽高,然后减去整个窗口的偏移。同样可以在页面中将碰撞盒绘制出来,看是否符合预期。

getBoxRegion() {
  var {x, y, width} = getCollisionRegion(this.targetBox);
  x -= this.position.x;
  y -= this.position.y;
  var delta = 10; // 离边距需要隔一点距离

  var result = [
    [x + 133, y + delta], // 上
    [x + width - delta, y + 49], // 右
    [x + 133, y + 89 - delta], // 下
    [x + 54 + delta, y + 48], // 左
  ];

  this._drawBoxRegion(result);

  return result;
}

_drawBoxRegion(result) {
  var path = [];
  result.forEach((r) => {
    path = path.concat(r);
  });
  path = path.concat(result[0]);

  var mask = new Tiny.Graphics();
  mask.lineStyle(4, 0x66FF33, 1);
  mask.drawPolygon(path);
  mask.endFill();

  this.addChild(mask);
}

2.3 碰撞检测

使用 point-in-polygon 的 inside 方法判断即可。

jumpResult() {
  const antRegion = this.getAntRegion();
  const boxRegion = this.getBoxRegion();

  return inside(antRegion, boxRegion);
}

2.4 根据碰撞检测结果判断是否成功

const jumpSuccess = this.jumpResult();

if (jumpSuccess) {
  this._jumpSuccess();
} else {
  this._jumpFail();
}

看看效果:

3. 跳跃失败后

如果用户的跳跃失误了,我们期望可以自动清空已有的游戏进程,然后用户可以立刻重新开始游戏。

_jumpFail() {
  this.reset();
}

reset 中,我们做完所有清理工作,然后重启游戏。

reset() {
  // 清空所有元素
  while (this.children.length > 0) {
    const b = this.children[this.children.length - 1];
    b.parent.removeChild(b);
  }

  // 恢复屏幕坐标
  this.setPosition(0, 0);

  // 重新加载初始元素
  this.init();
}

因为游戏状态全部进行了重置,要特别注意是否有内存泄露问题。

看下现在的游戏效果:

4. 总结

基本的游戏已经成型了,本篇文章的源码可见: https://github.com/stonelee/jump/tree/feat-4