上篇文章留了个问题:蚂蚁就算没有跳到下一个盒子上,游戏也会照常进行,这样明显不合理。下面就来解决这个问题。
要想判断蚂蚁是否成功落到了小盒子上,要用到碰撞检测。
Tiny.js 已经提供了简单的碰撞检测方法,可以检测两个无旋转矩形是否发生了碰撞。此外,还提供了一种像素检测碰撞的方法。对于简单的业务开发,直接使用这两种方法就可以了。
但是我们的游戏是斜 45 度地图,碰撞盒需要进行旋转。而且盒子的图片是包含阴影的,如果蚂蚁跳到阴影上显然不能算作成功,因此需要自定义一个不规则的多边形来作为碰撞盒。因此,我们需要寻找一个合适的碰撞检测算法。
通常的碰撞检测算法包括光线投射、分离轴等等,想了解的同学可以看下这篇文章:https://aotu.io/notes/2017/02/16/2d-collision-detection/
算法需要根据业务的实际需要来选择,因此我们重新梳理一下要解决的问题:
蚂蚁的脚算作一个点,如果蚂蚁跳到小盒子可以着力的位置(一个不规则的多边形),就算成功,否则失败。
这个问题可以抽象为一个点是否在一个不规则多边形内部的问题,因此我们直接使用了 @substack 大神的开源库 point-in-polygon。
蚂蚁的碰撞盒实际上是它的脚底的一个点。因此确定这个点的坐标就好。注意还要考虑整个窗口的偏移,然后进行微调。
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);
}
同样,可以绘制小盒子的碰撞盒。我们期望能够完全自定义这个多边形的形状,因此对其边界坐标进行了一些微调。同样要使用 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);
}
使用 point-in-polygon 的 inside 方法判断即可。
jumpResult() {
const antRegion = this.getAntRegion();
const boxRegion = this.getBoxRegion();
return inside(antRegion, boxRegion);
}
const jumpSuccess = this.jumpResult();
if (jumpSuccess) {
this._jumpSuccess();
} else {
this._jumpFail();
}
看看效果:
如果用户的跳跃失误了,我们期望可以自动清空已有的游戏进程,然后用户可以立刻重新开始游戏。
_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();
}
因为游戏状态全部进行了重置,要特别注意是否有内存泄露问题。
看下现在的游戏效果:
基本的游戏已经成型了,本篇文章的源码可见: https://github.com/stonelee/jump/tree/feat-4