微信的「跳一跳」有个细节做得很赞,就是小瓶子如果恰好跳到了小盒子的边缘,会有一个沿速度方向慢慢倒地的动画。这个应该如何实现呢?
首先要判断蚂蚁落地的位置跟小盒子边缘的具体情况:
- drop: 正常下落到盒子上
- right: 右侧倒地
- right-drop: 右侧正常下落
- left: 左侧倒地
- left-drop: 左侧正常下落
- top: 上方倒地
- top-drop: 上方正常下落
- bottom: 下方倒地
- bottom-drop: 下方正常下落
getDropAction(antRegion, boxRegion) {
// 垂直下落
var dropAction = 'drop';
var r = this.ant.width / 2; // 底部半径
var d1, d2;
// 向右上方跳跃
if (this.targetBoxDirection === direction.right) {
d1 = getDistance(antRegion, boxRegion[0], boxRegion[1]);
d2 = getDistance(antRegion, boxRegion[2], boxRegion[3]);
// 如果离右边更近
if (d1 < d2) {
// 如果与右边缘的距离小于底部半径
if (d1 < r) {
// 在右侧边缘
dropAction = 'right';
} else {
// 在右侧垂直下落
dropAction = 'right-drop';
}
} else {
if (d2 < r) {
// 在左侧边缘
dropAction = 'left';
} else {
// 在左侧垂直下落
dropAction = 'left-drop';
}
}
} else {
// 向左上方跳跃
d1 = getDistance(antRegion, boxRegion[0], boxRegion[3]);
d2 = getDistance(antRegion, boxRegion[1], boxRegion[2]);
// 如果离上边更近
if (d1 < d2) {
// 如果与上边缘的距离小于底部半径
if (d1 < r) {
// 在上边缘
dropAction = 'top';
} else {
// 在上方垂直下落
dropAction = 'top-drop';
}
} else {
if (d2 < r) {
// 在下边缘
dropAction = 'bottom';
} else {
// 在下方垂直下落
dropAction = 'bottom-drop';
}
}
}
return dropAction;
}
为了计算蚂蚁落地点到盒子边缘的距离,我们封装了一个方法 getDistance
,算法来自《3D 数学基础:图形与游戏开发》第 50 页的推导公式,然而书上这个公式有点问题,应该改成右侧这个:
为了方便进行矢量计算,我们使用了开源库 gl-matrix,然后将公式翻译为代码。
/**
* 得到从 _c 点到 _a, _b 所构成的直线的距离
* @param {number[]} _c
* @param {number[]} _a
* @param {number[]} _b
*/
export function getDistance(_c, _a, _b) {
var c = vec2.fromValues(_c[0], _c[1]);
var a = vec2.fromValues(_a[0], _a[1]);
var b = vec2.fromValues(_b[0], _b[1]);
var out = vec2.create();
var v = vec2.sub(out, c, a);
out = vec2.create();
var n = vec2.sub(out, b, a);
var _x = vec2.dot(v, n) / vec2.squaredDistance(b, a);
out = vec2.create();
var _v2 = vec2.scale(out, n, _x);
out = vec2.create();
var v2 = vec2.sub(out, v, _v2);
return vec2.length(v2);
}
下一步就是具体地判断蚂蚁的跳跃结果了。我们先判断落地点是否在小盒子的碰撞盒内(通过上篇文章的碰撞检测),如果不是,则判断具体的落地情况。
jumpResult() {
const antRegion = this.getAntRegion();
const boxRegion = this.getBoxRegion();
var isInside = inside(antRegion, boxRegion);
var dropAction = '';
if (!isInside) {
dropAction = this.getDropAction(antRegion, boxRegion);
}
return {
isInside,
dropAction,
};
}
dropAction
确定后,就可以进行不同的落地动画处理了。
如果正好跳到了碰撞盒的边缘,可以根据具体位置进行相应的旋转动画。注意这里又使用了 setPivot
来调整旋转中心,并修正了初始的旋转角度。
还有一个小细节,蚂蚁本来是在盒子前方渲染的(蚂蚁会遮挡盒子),但是如果蚂蚁在盒子右侧边缘正常落地,从视觉效果上来看,盒子应该将蚂蚁的下半部分遮住,这样才能表现出蚂蚁落在地上的效果。这涉及到 2D 动画的渲染顺序的问题,按照目前的层级结构设计来说,暂时不太好修改,后面在代码重构时再看看怎么处理。
// 蚂蚁摔倒
antFall(dropAction, onComplete) {
const ant = this.ant;
setPivot(ant, ant.width / 2, ant.height);
ant.setRotation(0);
var action;
switch (dropAction) {
case 'right':
action = Tiny.RotateTo(1000, {rotation: Tiny.deg2radian(55)});
break;
case 'left':
action = Tiny.RotateTo(2000, {rotation: Tiny.deg2radian(-125)});
break;
case 'top':
action = Tiny.RotateTo(1000, {rotation: Tiny.deg2radian(-55)});
break;
case 'bottom':
action = Tiny.RotateTo(2000, {rotation: Tiny.deg2radian(125)});
break;
case 'right-drop':
case 'top-drop':
ant.parent.setChildIndex(ant, 0); // TODO:需要修改遮挡情况
action = Tiny.MoveBy(500, {x: 0, y: 50});
break;
case 'left-drop':
case 'bottom-drop':
case 'drop':
action = Tiny.MoveBy(500, {x: 0, y: 50});
break;
default:
onComplete && onComplete();
break;
}
if (action) {
if (onComplete) {
action.onComplete = onComplete;
}
ant.runAction(action);
}
}
看看效果:
本篇文章源码可见: https://github.com/stonelee/jump/tree/feat-5