仿微信「跳一跳」- 3. 斜 45 度地图

前面实现的游戏有个问题,就是盒子只能落到右方,而微信的「跳一跳」完全不是这样,它的坐标轴看起来就像倾斜了 45 度一样,可以向左上方 / 右上方来跳,这样显得比较有立体感(当然它本来就是使用 Three.js 这种 3D 框架实现的),那么 2D Canvas 能否实现类似的效果呢?

1. Isometric Game 实现

这种斜 45 度的伪 3D 效果,在 2D 开发中有个术语,叫「Isometric Game」,有感兴趣的同学可以看下这篇文章:《Isometric Game 及译法漫谈 》,有专门的工具和框架来处理这个问题。我们这个比较简单,自己计算一下元素绘制的方向就好了。

首先可以定义需要的方向

export const direction = {
  left: -1,
  right: 1,
};

然后封装一个方法 getTargetBoxPos,按照方向和要移动的距离,来计算倾斜后的坐标。下面的代码中将倾斜角度固定为 30°,然后利用三角函数来计算坐标。

/**
 * 从 originPos 按照指定方向 direction,移动距离 delta,返回新的坐标
 * @param {object} originPos
 * @param {number} delta
 * @param {number} direction
 */
export function getTargetBoxPos(originPos, delta, direction) {
  const deg = 30;

  const x = originPos.x + direction * delta * Math.cos(Tiny.deg2radian(deg));
  const y = originPos.y - delta * Math.sin(Tiny.deg2radian(deg)); // 只能向上运动
  return { x, y };
}

然后在绘制盒子时,调用 getTargetBoxPos 来确定盒子的目标位置

const targetPos = getTargetBoxPos(this.currentBox.position, this.targetBoxDelta, this.targetBoxDirection);

const deltaY = 100; // 从 deltaY 开始掉落
box.setPosition(targetPos.x, targetPos.y - deltaY); // 设置初始位置

屏幕滚动方法中,也调用 getTargetBoxPos 来确定屏幕的新位置

const targetPos = getTargetBoxPos(this.position, this.targetBoxDelta, this.targetBoxDirection);
const action = Tiny.MoveBy(500, {
  x: this.position.x - targetPos.x,
  y: this.position.y - targetPos.y,
});

在蚂蚁跳跃方法中,也调用 getTargetBoxPos 来确定蚂蚁的目标位置

const ant = this.ant;
const originX = ant.position.x;
const originY = ant.position.y;
const targetPos = getTargetBoxPos(this.ant.position, targetDelta, direction);
const deltaX = targetPos.x - originX;

const tween = new Tiny.TWEEN.Tween({ // 起始值
  rotation: 0,
  x: originX,
  y: originY,
}).to({ // 结束值
  rotation: [180 * direction, 320 * direction, 360 * direction], // 旋转 1 周
  x: [originX + deltaX * 0.5, originX + deltaX * 0.8, originX + deltaX],
  y: [targetPos.y - maxHeight * 0.5, targetPos.y - maxHeight * 0.2, targetPos.y],
}, 1000)

整体效果如下:

2. 加入随机

游戏要加入随机性才会比较好玩,因此下面来加几个试试。

2.1 盒子生成方向随机

我们期望在一个方向上可以连续生成 1 ~ 3 个盒子,然后自动更换方向。这里设计了两个属性:targetBoxDirection 为下一个盒子的方向,numInOneDirection 为在一个方向上的连续盒子数量,小于等于 0 时就更换方向。

this.numInOneDirection -= 1;
if (this.numInOneDirection <= 0) {
  this.targetBoxDirection = -this.targetBoxDirection; // 反向
  this.numInOneDirection = Tiny.randomInt(1, 3);
}

2.2 盒子生成距离随机

将随机距离放到属性 targetBoxDelta

this.targetBoxDelta = Tiny.randomInt(150, 300);

2.3 执行

然后在生成盒子之前,执行一下上面两个方法即可。

3. 发现问题

现在可以向右上方 / 左上方生成小盒子了,但是玩了一下发现了问题,有些小盒子莫名的消失了。

原因很简单,前面写的销毁盒子的判断逻辑有问题,之前的策略是移到屏幕左侧之外的小盒子就不会再出现了,现在玩法发生了变化,当向左上方生成盒子后,由于屏幕整体向右进行了移动,原本屏幕左侧之外的盒子又进入了视野中,因此还得需要进行绘制。所以销毁策略需要进行修改,改成销毁移到屏幕下方之外的盒子。

for (var i = this.boxes.length - 1; i >= 0; i--) {
  const box = this.boxes[i];

  // 屏幕下方的 box 不会再出现了,所以可以删掉,防止内存泄露
  if ((box.y - box.height + scenePostion.y) > Tiny.WIN_SIZE.height) {
    box.parent.removeChild(box);
    this.boxes.splice(i, 1);
  }
}

4. 自由控制蚂蚁跳跃的距离

现在无论怎么操作,蚂蚁每次都能稳稳地跳到下一个小盒子上,一点挑战性都没有,应该按照用户的操作,来控制蚂蚁每次跳的距离。 这个距离可以通过按住鼠标的时间长短来控制。按住时间越长,蚂蚁聚力越多,所以跳的应该越远。

下面的代码中,使用 pressTime 来记录用户按住的时间,然后计算得到要跳跃的距离。

canvas.addEventListener(supportTouch ? 'touchstart' : 'mousedown', () => {
  this.pressTime = +Date.now();
  ...
});

canvas.addEventListener(supportTouch ? 'touchend' : 'mouseup', () => {
  const deltaTime = +Date.now() - this.pressTime;

  var targetBoxDelta = 0.8 * deltaTime;
  const maxTargetDelta = 600; // 最大跳跃距离
  if (targetBoxDelta > maxTargetDelta) {
    targetBoxDelta = maxTargetDelta;
  }

  this.jump(targetBoxDelta, this.targetBoxDirection, () => {
  ...
});

效果如下:

5. 总结

游戏可以玩了,但是明显还有问题:就算没有跳到下一个盒子上,游戏还是可以继续运行。还要进行一下碰撞检测哦,留到下篇文章再说吧,本篇文章的源码可见: https://github.com/stonelee/jump/tree/feat-3