优化Android游戏循环

3

我正在为一个简单的安卓游戏使用下面的代码作为游戏循环。它可以正常工作,但是偶尔会出现游戏卡顿的情况。所有东西都会冻结一秒钟,然后向前跳了一点比应该更远的距离。我阅读了大量关于游戏循环的资料,但是我仍然有一些困难来实现平滑循环。是否可以通过插值来解决这个问题?我该如何平滑移动?

private final static int MAX_FPS = 30;
private final static int MAX_FRAME_SKIPS = 5;
private final static int FRAME_PERIOD = 1000 / MAX_FPS;

protected Void doInBackground(Void... params) {
    Log.d("MainTask", "starting game task");
    Canvas canvas;

    long begin;     // the time when the cycle began
    long delta;     // the time it took for the cycle to execute
    int sleep;      // ms to sleep if ahead
    int skipped;    // number of frames being skipped

    sleep = 0;

    while (running) {
        canvas = null;
        try {
            canvas = surfaceHolder.lockCanvas();
            synchronized (surfaceHolder) {
                begin = System.currentTimeMillis();
                skipped = 0;

                gamePanel.update();
                gamePanel.draw(canvas);

                delta = System.currentTimeMillis() - begin;
                sleep = (int)(FRAME_PERIOD - delta);

                if (sleep > 0) {
                    try {
                        Thread.sleep(sleep);
                    } catch(InterruptedException e) {}
                }

                while (sleep < 0 && skipped < MAX_FRAME_SKIPS) {
                    gamePanel.update();
                    sleep += FRAME_PERIOD;
                    skipped++;
                }
            }
        } finally {
            if (canvas != null) surfaceHolder.unlockCanvasAndPost(canvas);
        }
    }
    Log.d("MainTask", "the task is complete");
    return null;
}

请参阅:https://source.android.com/devices/graphics/architecture.html#loops - fadden
2个回答

5

对于这个循环,您实际上已经做得很正确了。但是我可以看到一些潜在的陷阱。

  • You don't pass the frame time to your update method. This means your update method must assume a certain frame time. This is likely the cause of your jumps. You won't get constant frame rates if you are anywhere near the maximum performance of the machine, you just wont. And this broadly doesn't matter. If you miss a frame your brain fills in the missing frame and it looks fine. What absolutely does matter is a variable object speed. In this case a variable frame rate will cause a variable speed of your objects. Your update method should be:

    long previousFrameTime=System.currentTimeMillis();
    while (running){
        .....
        .....
        long currentTime=System.currentTimeMillis();
        long frameTime=currentTime-previousFrameTime
        previousFrameTime=currentTime
        gamePanel.update(frameTime/1000.0);  //I personally like my frametimes in seconds, hence /1000.0, but thats up to you
        .....
        .....
     }
    

    And all movement within the update should take that frametime into account when determining how far to move

为了说明这点的重要性,看下面两张图。一张是假定帧率,另一张是测量出来的帧率。点代表实际渲染的帧数,线代表观众大脑中正在发生的插值。
  • You also hold on to the Canvas lock for a lot longer than nessissary. All the way through your update method and through the sleep as well you have a lock on the canvas. This saps performance. Get the lock just before you start rendering and release it as soon as you have finished

    gamePanel.update();
    canvas = surfaceHolder.lockCanvas();
    gamePanel.draw(canvas);
    surfaceHolder.unlockCanvasAndPost(canvas);
    

2
即使持有画布锁定不会导致问题,修复它也绝对是值得的。 - zgc7009
@user2694189,您的delta似乎是帧处理时间(忽略任何睡眠)-这通常不是delta的含义,通常delta是frameTime。您需要在更新之前记录时间,然后在下一帧更新之前再次获取时间,并获取两者之间的差异。这就是frameTime。在update方法内部,每个物体都会有一个速度;您将frameTime乘以速度,以获取它应该移动多远的距离。 - Richard Tingle
同样适用于任何加速度,加速度乘以帧时间以获得速度变化。这被称为欧拉积分。有更好的整合方法,但欧拉通常足以处理游戏循环,而且是最简单的。在理想情况下,frameTime应等于FRAME_PERIOD,但FRAME_PERIOD是目标,我的frameTime是您获得的实际时间(您可能需要将FRAME_PERIOD重命名为TARGET_FRAME_PERIOD)。 - Richard Tingle
举个例子。假设一个物体的速度为1m/s。然后我们使用目标FRAME_PERIOD为0.1秒进行渲染。在当前代码下(使用一个可变的frameRate),我们可能会得到以下结果(时间,位置):(0,0)(0.1,0.1)、(0.15,0.2)、(0.28,0.3)。你可以看到所有这些数据点不仅处于可变帧速率下,位置也是错误的。但是通过考虑帧时间,你可以得到(0,0)、(0.15,0.15)、(0.28,0.28)的结果。帧速率仍然是错误的,但物体至少在正确的位置上。 - Richard Tingle
谢谢,这帮了我很多!虽然我仍然遇到一些小问题,但情况肯定好多了。我想这是因为现在动画不是恒定的。不过我会找方法来解决的。再次感谢。 - Danny Buonocore
显示剩余2条评论

1
这可能有点晚了,但是这是我如何实现我的游戏循环的方法。通过这个方法,它可以帮助您跟踪游戏的每秒更新次数和帧数。
long lastTime = System.nanoTime();
double nsPerTick = 1_000_000_000D/60D;
int frames = 0;
int ticks = 0;
long lastTimer = System.currentTimeMillis();
double delta = 0;

while (mRunning) {
    Canvas canvas = null;
    long now = System.nanoTime();
    delta += (now-lastTime)/nsPerTick;
    lastTime = now;
    boolean shouldRender = false;

    while (delta >= 1) {
        ticks++;
        gameView.update();
        delta -= 1;
        shouldRender = true;
    }

    if (shouldRender) {
        frames++;
        canvas = surfaceHolder.lockCanvas();
        gameView.render(canvas);
        surfaceHolder.unlockCanvasAndPost(canvas);
    }

    if (System.currentTimeMillis() - lastTimer >= 1000) {
        lastTimer += 1000;
        Log.d("FPS", "Ticks: " + ticks + ", Frames: " + frames);
        ticks = 0;
        frames = 0;
    }
}

这是基于vanZeben的视频教程制作的Java游戏,但也适用于Android游戏(至少对我来说是这样)。
希望能够有所帮助。
参考资料: https://www.youtube.com/watch?v=VE7ezYCTPe4&list=PL8CAB66181A502179

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接