平滑 Android 游戏循环

Posted

技术标签:

【中文标题】平滑 Android 游戏循环【英文标题】:Smoothing out Android Game Loop 【发布时间】:2014-07-03 19:11:25 【问题描述】:

我将下面的代码用作简单 android 游戏的游戏循环。它工作得很好,但游戏中时不时会出现一些小问题。一切都冻结了一秒钟,然后向前跳得比应有的更远。我对游戏循环进行了大量阅读,但在实现平滑循环时仍然遇到了一些麻烦。插值会解决这个问题吗?我怎样才能平滑运动?

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;

【问题讨论】:

见:source.android.com/devices/graphics/architecture.html#loops 【参考方案1】:

您实际上已经为这个循环正确地完成了大多数事情。但是,我可以看到一些潜在的陷阱

您没有将帧时间传递给您的更新方法。这意味着您的更新方法必须假定某个帧时间。这可能是你跳跃的原因。如果您接近机器的最大性能,您将不会获得恒定的帧速率,您就是不会。这大体上无关紧要。如果你错过了一个框架,你的大脑会填补丢失的框架,它看起来很好。绝对重要的是可变对象speed。在这种情况下,可变帧速率将导致对象的速度可变。您的更新方法应该是:

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
    .....
    .....
 

在确定移动多远时,更新中的所有移动都应考虑frametime

要说明这很重要,请参阅以下两个图表。一个具有假定的帧速率,另一个具有测量的帧速率。点代表实际渲染的帧,线条代表观众大脑中发生的插值。

您持有 Canvas 锁的时间也比 nessissary 长得多。一直到你的更新方法和睡眠,你在画布上都有一个锁。 This saps performance.在开始渲染前获取锁,完成后立即释放

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

【讨论】:

即使按住画布锁不会导致打嗝,它绝对值得修复 好吧,Canvas 锁定部分是有意义的。帧时间是指我的增量吗?我将如何使用它?我还需要在内部while循环中重新计算吗?还是继续使用上面找到的增量? @user2694189 您的增量似乎是帧的处理时间(忽略任何睡眠) - 这通常不是增量,通常增量是帧时间。您想在进行更新之前记录时间,然后在更新之前记录下一帧,您将再次获得时间,并获得两者之间的差异。这是帧时间。在更新方法内部,一切都会有速度;你将 frameTime 乘以速度,得到它应该移动那个帧的距离 这同样适用于任何加速度,加速度乘以帧时间以获得速度的变化。这称为欧拉积分。那里有更好的集成,购买 euler 通常足以满足游戏循环,并且是迄今为止最简单的。在理想情况下,frameTime 将等于 FRAME_PERIOD,但 FRAME_PERIOD 是目标,而我的 frameTime 是您获得的真实时间(您可能希望将 FRAME_PERIOD 重命名为 TARGET_FRAME_PERIOD 举个例子。想象一个速度为 1m/s 的物体。然后我们以 0.1 秒的目标 FRAME_PERIOD 进行渲染。在当前代码下(使用变量 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)。帧速率仍然错误,但对象至少在正确的位置【参考方案2】:

这可能有点太晚了,但这是我实现游戏循环的方式。有了这个,它可以帮助您跟踪游戏的滴答声(或更新)和每秒帧数。

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 的视频教程,旨在在 PC 上制作 Java 游戏,但它也适用于 Android 游戏(至少对我而言)。

希望对你有帮助。

参考:https://www.youtube.com/watch?v=VE7ezYCTPe4&list=PL8CAB66181A502179

【讨论】:

以上是关于平滑 Android 游戏循环的主要内容,如果未能解决你的问题,请参考以下文章

在android游戏中放置主游戏循环的位置

Android - 游戏循环中的主动渲染?

使用 OpengL Surface 为 Android 制作游戏循环的最佳方法

使用 SurfaceView 的 Android 游戏循环;快速更改相机“镜头”会导致黑色闪光等

关闭和重新打开应用程序后,Android 画布游戏循环中断 onResume()

android文字横向滚动的自定义view