前面实现的游戏有个问题,就是盒子只能落到右方,而微信的「跳一跳」完全不是这样,它的坐标轴看起来就像倾斜了 45 度一样,可以向左上方 / 右上方来跳,这样显得比较有立体感(当然它本来就是使用 Three.js 这种 3D 框架实现的),那么 2D Canvas 能否实现类似的效果呢?
这种斜 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)
整体效果如下:
游戏要加入随机性才会比较好玩,因此下面来加几个试试。
我们期望在一个方向上可以连续生成 1 ~ 3 个盒子,然后自动更换方向。这里设计了两个属性:targetBoxDirection
为下一个盒子的方向,numInOneDirection
为在一个方向上的连续盒子数量,小于等于 0 时就更换方向。
this.numInOneDirection -= 1;
if (this.numInOneDirection <= 0) {
this.targetBoxDirection = -this.targetBoxDirection; // 反向
this.numInOneDirection = Tiny.randomInt(1, 3);
}
将随机距离放到属性 targetBoxDelta
中
this.targetBoxDelta = Tiny.randomInt(150, 300);
然后在生成盒子之前,执行一下上面两个方法即可。
现在可以向右上方 / 左上方生成小盒子了,但是玩了一下发现了问题,有些小盒子莫名的消失了。
原因很简单,前面写的销毁盒子的判断逻辑有问题,之前的策略是移到屏幕左侧之外的小盒子就不会再出现了,现在玩法发生了变化,当向左上方生成盒子后,由于屏幕整体向右进行了移动,原本屏幕左侧之外的盒子又进入了视野中,因此还得需要进行绘制。所以销毁策略需要进行修改,改成销毁移到屏幕下方之外的盒子。
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);
}
}
现在无论怎么操作,蚂蚁每次都能稳稳地跳到下一个小盒子上,一点挑战性都没有,应该按照用户的操作,来控制蚂蚁每次跳的距离。 这个距离可以通过按住鼠标的时间长短来控制。按住时间越长,蚂蚁聚力越多,所以跳的应该越远。
下面的代码中,使用 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, () => {
...
});
效果如下:
游戏可以玩了,但是明显还有问题:就算没有跳到下一个盒子上,游戏还是可以继续运行。还要进行一下碰撞检测哦,留到下篇文章再说吧,本篇文章的源码可见: https://github.com/stonelee/jump/tree/feat-3