仿微信「跳一跳」- 2. 优化

上一篇文章中实现了一个简单的「跳一跳」,但是效果不算太好,现在我们来对其进行一下优化。

1. 更好一点的按下动画

如果仔细观察微信的「跳一跳」,会发现手指按下时,不仅小瓶子会被压扁,小瓶子所在的小盒子也有一个压缩效果,这个应该如何实现呢?

1.1 更换组

方案其实很多,最简单的实现就是,将小盒子作为一个组,然后将蚂蚁放进去,然后整个组以底部为中心进行压缩,就能实现上面的效果了。

// 将蚂蚁放到 box 组中
this.currentBox.addChild(this.ant);

然而元素的位置计算跟其所在的组是相关的,如果组发生了变化,原本设置的 position 可能就会发生偏差,因此还需要调整下位置。

// 位置也得调整下
this.ant.setPosition(100, 50);

然后在每次跳到新盒子的一瞬间,将蚂蚁放到新的组中(会自动从原来的组中去掉)

// 因为要更换 ant 的 group,所以要计算 ant 相对 targetBox 的位置
var { x, y } = caleRelateTargetPos(this.ant, this.targetBox);
this.ant.setPosition(x, y);
this.targetBox.addChild(this.ant);

1.2 计算更换组后新的相对位置

这里我们封装了个方法 caleRelateTargetPos,可以计算一个元素相对目标组的 position。其原理是用全局坐标来计算相对位置,还要考虑缩放比率和目标组的中心点坐标。

/**
 * 得到 obj 在 targetContainer 中的相对坐标
 * @param {Tiny.DisplayObject} obj
 * @param {Tiny.DisplayObject} targetContainer
 */
export function caleRelateTargetPos(obj, targetContainer) {
  // 使用全局坐标来计算
  const objGlobalPos = obj.getGlobalPosition();
  const targetGlobalPos = targetContainer.getGlobalPosition();

  const newX = objGlobalPos.x - targetGlobalPos.x;
  const newY = objGlobalPos.y - targetGlobalPos.y;

  // 还要考虑缩放比率,不同配置中拿到的宽度是不同的
  const winW = Tiny.config.fixSize ? Tiny.config.width : window.innerWidth;
  const rate = winW / Tiny.WIN_SIZE.width;

  // 还要考虑 targetContainer 的中心点
  const targetPivot = targetContainer.getPivot();

  return {
    x: newX / rate + targetPivot.x,
    y: newY / rate + targetPivot.y,
  };
}

1.3 新的动画实现

解决了更换组引起的位置偏移问题后,盒子跟蚂蚁的整体动画就很容易实现了。只要将动画的执行者由「蚂蚁」更换为「盒子」(内部包含了蚂蚁)就好了。

compress() {
  // 动画 持续 1000ms,y 轴缩放到 0.7
  this.currentBox.runAction(Tiny.ScaleTo(1000, {
    scaleY: 0.7,
  }));
}

compressRestore() {
  this.currentBox.removeActions(); // 移除动画

  const action = Tiny.ScaleTo(500, Tiny.scale(1));
  action.setEasing(Tiny.TWEEN.Easing.Bounce.Out); // 回弹效果
  this.currentBox.runAction(action);
}

1.4 看看对比效果

优化前优化后

2. 蚂蚁跳起动画

2.1 改变旋转中心点

仔细观察蚂蚁的跳起动画,会发现它的旋转是以脚底为中心的,这跟我们普通的认知不太一样,空中翻滚的中心点应该更接近腰部,因此旋转中心换成腰部位置应该会更自然一些。

很多同学就会这样来写了

this.ant.setPivot(this.ant.width / 2, this.ant.height / 2);

然而这样是有问题的,中心点被修改后,又会导致位置计算的偏移问题。因此我们又封装了一个方法,来设置中心点,并自动重设其 position,使得视觉上的渲染位置保持不变。

/**
 * 设置 obj 的中心点为 (x,y),会自动重设其 position,使得视觉上的渲染位置保持不变
 * @param {Tiny.Sprite} obj
 * @param {number} x
 * @param {number} [y]
 */
export function setPivot(obj, x, y) {
  const { x: originPivotX, y: originPivotY } = obj.getPivot();

  obj.setPivot(x, y);

  obj.setPositionX(obj.position.x + x - originPivotX);
  obj.setPositionY(obj.position.y + y - originPivotY);
}

2.2 优化动画细节

蚂蚁跳起的动画不应该是线性的,添加更多的关键帧,动作先快再慢,这样更自然一点。

可以在 Tween 动画中进行细节调整。

const tween = new Tiny.TWEEN.Tween({ // 起始值
  rotation: 0,
  x: originX,
  y: originY,
}).to({ // 结束值
  // rotation: 360, // 旋转 1 周
  // x: originX + deltaX, // 向右移动 deltaX
  // y: [originY - maxHeight, originY], // 设置 2 个关键帧,一个是最高点,一个是最终点。效果就是蚂蚁跳起来然后落下

  rotation: [180, 320, 360], // 旋转 1 周
  x: [originX + deltaX * 0.5, originX + deltaX * 0.8, originX + deltaX], // 向右移动 deltaX
  y: [originY - maxHeight * 0.5, originY - maxHeight * 0.2, originY]
}, 1000)

看看对比效果

优化前优化后

3. 有内存泄漏!

随着游戏的进行,我们发现页面的内存占用越来越多,怎么回事呢?原因很简单,盒子一直在增加,但是并没有销毁,自己手工处理一下就好了。

我们使用一个数组 boxes 来管理所有的盒子,然后在屏幕滚动动画结束之后,将屏幕外的盒子删掉。

for (var i = this.boxes.length - 1; i >= 0; i--) {
  const box = this.boxes[i];
  // 屏幕外的 box 都删掉,防止内存泄露
  if ((box.x - box.pivot.x + box.width + scenePostion.x) < 0) {
    box.parent.removeChild(box);
    this.boxes.splice(i, 1);
  }
}

4. 总结

经过上面的优化,游戏看起来舒服了许多,源码可见: https://github.com/stonelee/jump/tree/feat-2