仿微信「跳一跳」- 5. 碰撞后的细节优化

微信的「跳一跳」有个细节做得很赞,就是小瓶子如果恰好跳到了小盒子的边缘,会有一个沿速度方向慢慢倒地的动画。这个应该如何实现呢?

1. 判断倒地状态

首先要判断蚂蚁落地的位置跟小盒子边缘的具体情况:

  • 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,
  };
}

2. 实现落地动画

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);
  }
}

看看效果:

3. 总结

本篇文章源码可见: https://github.com/stonelee/jump/tree/feat-5