我为完全相同的问题争论了好几天。没有任何Android时间参考的循环看起来很流畅,但是一旦它包括任何类型的“时间同步”,Android开发控制之外的外部因素就会给最终结果带来严重的不连续性。
基本上,这些因素是:
- eglSwapInterval不是在Android中实现的,因此很难知道硬件在屏幕中暴露最终绘制的时刻(硬件屏幕同步)
- Thread.sleep 并不精确。线程的睡眠状态可能多于或低于请求的睡眠状态。
- SystemClock.uptimeMillis()System.nanoTime(), System.currentTimeMillis() 和其他与时序相关的测量不准确(其精确)。
该问题与绘图技术(绘图,openGL 1.0 / 1.1和2.0)和游戏循环方法(固定时间步长,插值,可变时间步长)无关。像你一样,我正在尝试Thread.sleep,疯狂的插值,计时器等。无论你做什么,我们都无法控制这些因素。
根据这个网站上的许多问答,制作流畅的连续动画的基本规则是:
- 通过删除所有动态内存请求,至少减少 GC。
- 渲染帧的速度与硬件可以处理它们的速度一样快(在大多数Android设备中,40到60fps是可以的)。
- 使用带有插值或可变时间步长的固定时间步长。
- 优化更新物理场并绘制例程,以便在相对恒定的时间内执行,没有高峰值方差。
可以肯定的是,在发布这个问题之前,您通过优化updateGame()和drawGame()(没有可观的GC和相对恒定的执行时间)来做很多工作,以便在主循环中获得流畅的动画,正如您提到的:“简单而绝对流畅”。
您的特定情况是可变的 stepTime,并且没有与实时事件(如音乐)完美同步的特殊要求,解决方案很简单:“平滑步长时间变量”。
该解决方案可与其他游戏循环方案(具有可变渲染的固定时间步长)配合使用,并且易于移植概念(平滑 updateGame 和实时时钟在几帧中产生的位移量)。
// avoid GC in your threads. declare nonprimitive variables out of onDraw
float smoothedDeltaRealTime_ms=17.5f; // initial value, Optionally you can save the new computed value (will change with each hardware) in Preferences to optimize the first drawing frames
float movAverageDeltaTime_ms=smoothedDeltaRealTime_ms; // mov Average start with default value
long lastRealTimeMeasurement_ms; // temporal storage for last time measurement
// smooth constant elements to play with
static final float movAveragePeriod=40; // #frames involved in average calc (suggested values 5-100)
static final float smoothFactor=0.1f; // adjusting ratio (suggested values 0.01-0.5)
// sample with opengl. Works with canvas drawing: public void OnDraw(Canvas c)
public void onDrawFrame(GL10 gl){
updateGame(gl, smoothedDeltaRealTime_ms); // divide 1000 if your UpdateGame routine is waiting seconds instead mili-seconds.
drawGame(gl);
// Moving average calc
long currTimePick_ms=SystemClock.uptimeMillis();
float realTimeElapsed_ms;
if (lastRealTimeMeasurement_ms>0){
realTimeElapsed_ms=(currTimePick_ms - lastRealTimeMeasurement_ms);
} else {
realTimeElapsed_ms=smoothedDeltaRealTime_ms; // just the first time
}
movAverageDeltaTime_ms=(realTimeElapsed_ms + movAverageDeltaTime_ms*(movAveragePeriod-1))/movAveragePeriod;
// Calc a better aproximation for smooth stepTime
smoothedDeltaRealTime_ms=smoothedDeltaRealTime_ms +(movAverageDeltaTime_ms - smoothedDeltaRealTime_ms)* smoothFactor;
lastRealTimeMeasurement_ms=currTimePick_ms;
}
// Optional: check if the smoothedDeltaRealTIme_ms is too different from original and save it in Permanent preferences for further use.
对于固定的时间步长方案,可以实现一个相互连接的updateGame来改善结果:
float totalVirtualRealTime_ms=0;
float speedAdjustments_ms=0; // to introduce a virtual Time for the animation (reduce or increase animation speed)
float totalAnimationTime_ms=0;
float fixedStepAnimation_ms=20; // 20ms for a 50FPS descriptive animation
int currVirtualAnimationFrame=0; // useful if the updateGameFixedStep routine ask for a frame number
private void updateGame(){
totalVirtualRealTime_ms+=smoothedDeltaRealTime_ms + speedAdjustments_ms;
while (totalVirtualRealTime_ms> totalAnimationTime_ms){
totalAnimationTime_ms+=fixedStepAnimation_ms;
currVirtualAnimationFrame++;
// original updateGame with fixed step
updateGameFixedStep(currVirtualAnimationFrame);
}
float interpolationRatio=(totalAnimationTime_ms-totalVirtualRealTime_ms)/fixedStepAnimation_ms;
Interpolation(interpolationRatio);
}
使用 canvas 和 openGlES10 绘图在以下设备上进行测试:SG SII (57 FPS)、SG Note(57 FPS)、SG Tab(60 FPS)、在 Windows XP(8 FPS) 上运行的无品牌 Android 2.3 (43 FPS) 慢速仿真器。测试平台绘制了大约45个对象+ 1个巨大的背景(来自7000万像素源图像的纹理),沿着真实物理参数(km / h和G)中指定的路径移动,在多个设备之间没有尖峰或轻拂(好吧,模拟器上的8 FPS看起来不太好,但它的恒定速度如预期)
查看图表,了解 Android 如何报告时间。有时,Android报告的增量时间很大,而下一个循环它比平均值小,这意味着实时值的读数存在偏移。
更多细节:
使用Android的GLSurfaceView.RENDERMODE_CONTINUOUSLY时如何限制帧速率?
System.currentTimeMillis vs System.nanoTime
System.currentTimeMillis() 方法真的返回当前时间吗?