问题
最近发现游戏在 webview 中操作交换事件掉帧特别厉害,有时候直接跳过了交换的动画。猜想是因为逻辑需要计算后续所有的步骤,在计算完成之前这部分逻辑就相当于阻塞动画。因此阅读动画和Ticker帧刷新的源码,证明猜想。
代码逻辑
- 监听用户 touchmove 事件,将最近两个元素位置获取
- 调用 exchange 方法,同时发送事件 EE.emit(EventType.MAP_EXCHANGE)
- 因为 EE 发送事件是同步执行,所以会执行
sTween.to({ x, y}, 200);
- 执行 CrushCells 方法,这个方法逻辑比较复杂有可能需要递归计算多次,耗时基本在16ms以上。
Tween动画源码实现
Tween 动画最核心的方法是 Tween.to 方法
public to(props: any, duration?: number, ease: Function = undefined) {
if (isNaN(duration) || duration < 0) {
duration = 0;
}
this._addStep({ d: duration || 0, p0: this._cloneProps(this._curQueueProps), e: ease, p1: this._cloneProps(this._appendQueueProps(props)) });
//加入一步set,防止游戏极其卡顿时候,to后面的call取到的属性值不对
return this.set(props);
}
每一个tween实例都有一个steps队列执行,将每次调用需要变化的属性值保存在队列中,等待时机取出执行,这个时机先按下不表,需要记住 _addStep 这个方法。
帧动画更新
由于主流的屏幕刷新率都在 60Hz,那么渲染一帧的时间就必须控制在 16ms 才能保证不掉帧。 也就是说每一次渲染都要在 16ms 内页面才够流畅不会有卡顿感
在 egret 中设置 frameRate = 60,控制canvas重绘的ticker就是每16-17ms秒执行一次回调。
ticker.$startTick(Tween.tick, null);
private static tick(timeStamp: number, paused = false): boolean {
let delta = timeStamp - Tween._lastTime;
Tween._lastTime = timeStamp;
let tweens: Tween[] = Tween._tweens.concat();
for (let i = tweens.length - 1; i >= 0; i--) {
let tween: Tween = tweens[i];
if ((paused && !tween.ignoreGlobalPause) || tween.paused) {
continue;
}
tween.$tick(tween._useTicks ? 1 : delta);
}
return false;
}
在ticker的回调中获取参数 timeStamp (表示从启动Egret框架开始经过的时间ms),在没有阻塞的情况下,delta等于16|17ms,这时候tween.$tick方法再去取steps队列的属性,根据 (prevPosition + delta)/ duration
的比例更新updateTargetProps
。
如果计算量很大,耗时超过了16-17ms,比如说24ms就只能走下一个tick,如果甚至大到超过duration,那么动画就相当于消失了,变成直接设置属性。而CrushCells方法因为考虑到了后续所有的步骤所以都耗时超过了16毫秒,这就是为什么交换事件掉帧。
解决问题
- 之前代码重构过一次,以前逻辑与动画耦合,逻辑每次都等待动画播放完才进行下一次的计算应该不会有掉帧的问题,但是这样会难以实现一些跳过动画的需求。
- 如果后续的计算利用 setTimeout 分割成一片片计算,每次都在异步队列中等待,动画运行完毕之后才继续计算理论上能够解决这个阻塞的问题。
- 这样有点类似于重构前逻辑必须等待动画执行完后才计算,但是不同的地方在于,如果选择跳过动画却不影响逻辑的计算。
表现:在devtool performace中选择cpu slow down 4的情况下,ticker更新动画在交换事件中回调时间会从80多ms降到20多ms。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。