android游戏循环vs渲染线程中的更新

use*_*101 19 android opengl-es game-loop thread-sleep

我正在制作一款安卓游戏,目前我还没有达到我想要的性能.我在自己的线程中有一个游戏循环,用于更新对象的位置.渲染线程将遍历这些对象并绘制它们.目前的行为似乎是波涛汹涌/不均匀的运动.我无法解释的是,在我将更新逻辑放入自己的线程之前,我在onDrawFrame方法中,就在gl调用之前.在这种情况下,动画非常流畅,当我尝试通过Thread.sleep来限制我的更新循环时,它只会变得不连贯/不均匀.即使我允许更新线程进行狂暴(无睡眠),动画也是平滑的,只有当涉及Thread.sleep时它才会影响动画的质量.

我已经创建了一个骨架项目来查看是否可以重新创建该问题,下面是渲染器中的更新循环和onDrawFrame方法: Update Loop

    @Override
public void run() 
{
    while(gameOn) 
    {
        long currentRun = SystemClock.uptimeMillis();
        if(lastRun == 0)
        {
            lastRun = currentRun - 16;
        }
        long delta = currentRun - lastRun;
        lastRun = currentRun;

        posY += moveY*delta/20.0;

        GlobalObjects.ypos = posY;

        long rightNow = SystemClock.uptimeMillis();
        if(rightNow - currentRun < 16)
        {
            try {
                Thread.sleep(16 - (rightNow - currentRun));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的onDrawFrame方法:

        @Override
public void onDrawFrame(GL10 gl) {
    gl.glClearColor(1f, 1f, 0, 0);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
            GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();

    gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
    gl.glTranslatef(transX, GlobalObjects.ypos, transZ);
    //gl.glRotatef(45, 0, 0, 1);
    //gl.glColor4f(0, 1, 0, 0);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

    gl.glVertexPointer(3,  GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, uvBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length,
              GL10.GL_UNSIGNED_SHORT, indiceBuffer);

    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}
Run Code Online (Sandbox Code Playgroud)

我查看了replica岛的源代码,他在一个单独的线程中执行更新逻辑,并使用Thread.sleep限制它,但他的游戏看起来非常流畅.有没有人有任何想法或有任何人经历过我所描述的?

---编辑:1/25/13 ---
我有一些时间思考并大大平滑了这个游戏引擎.我如何管理这可能是对实际游戏程序员的亵渎或侮辱,所以请随时纠正任何这些想法.

基本的想法是保持更新模式,绘制...更新,绘制...同时保持时间增量相对相同(通常超出您的控制范围).我的第一个行动方案是以这样一种方式同步我的渲染器,它只是在被通知后才被允许这样做.这看起来像这样:

public void onDrawFrame(GL10 gl10) {
        synchronized(drawLock)
    {
        while(!GlobalGameObjects.getInstance().isUpdateHappened())
        {
            try
            {
                Log.d("test1", "draw locking");
                drawLock.wait();
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

当我完成更新逻辑时,我调用drawLock.notify(),释放渲染线程以绘制我刚刚更新的内容.这样做的目的是帮助建立更新模式,绘制...更新,绘制...等.

一旦我实现了它,它就会相当平滑,尽管我仍然偶尔会遇到运动跳跃.经过一些测试,我发现在ondrawFrame的调用之间发生了多次更新.这导致一帧显示两次(或更多次)更新的结果,跳跃比正常更大.

我解决这个问题的方法是在两个onDrawFrame调用之间将时间增量限制为某个值,比如18ms,并将额外时间存储在余数中.如果他们可以处理它,那么剩余部分将分配给随后的几个更新的后续时间增量.这个想法可以防止所有突然的长跳,从根本上平滑多个帧的时间尖峰.这样做给了我很好的结果.

这种方法的缺点是,在一段时间内,物体的位置随着时间的推移将不准确,并且实际上会加速以弥补这种差异.但它更平滑,速度的变化也不是很明显.

最后,我决定用上面的两个想法重写我的引擎,而不是修补我最初制作的引擎.我对可能有人可以评论的线程同步进行了一些优化.

我当前的线程交互如下:
- 更新线程更新当前缓冲区(双缓冲区系统以便同时更新和绘制),然后如果绘制了前一帧,则将此缓冲区提供给渲染器.
- 如果前一帧尚未绘制或正在绘制,则更新线程将等待,直到渲染线程通知它已绘制.
- 渲染线程等待直到更新线程通知已发生更新.
- 当渲染线程绘制时,它设置一个"最后绘制的变量",指示它最后绘制的两个缓冲区中的哪一个,并且还通知更新线程它是否正在等待绘制前一个缓冲区.

这可能有点复杂,但是正在做的是允许多线程的优点,因为它可以在帧n-1绘制时执行帧n的更新,同时如果渲染器正在执行,则还防止每帧多次更新迭代很久.为了进一步说明,如果检测到lastDrawn缓冲区等于刚刚更新的缓冲区,则更新线程锁定将处理此多重更新方案.如果它们相等,则向更新线程指示尚未绘制之前的帧.

到目前为止,我取得了不错的成绩.如果有人有任何意见,请告诉我,很高兴听到您对我正在做的事情的想法,无论是对还是错.

谢谢

fad*_*den 17

(Blackhex的回答提出了一些有趣的观点,但我不能把所有这些都塞进评论中.)

让两个线程以异步方式运行必然会导致这样的问题.以这种方式看待它:驱动动画的事件是硬件"vsync"信号,即Android表面合成器向显示硬件提供充满数据的新屏幕的点.每当vsync到达时,您都希望拥有一个新的数据帧.如果您没有新数据,游戏看起来很不稳定.如果您在此期间生成了3帧数据,则会忽略两个数据,并且您只是在浪费电池寿命.

(运行CPU已经完全耗尽也可能导致设备升温,从而导致热量限制,从而减慢系统中的所有内容......并且可能会使您的动画不稳定.)

与显示屏保持同步的最简单方法是执行所有状态更新onDrawFrame().如果有时需要超过一帧来执行状态更新并渲染帧,那么您将看起来很糟糕,并且需要修改您的方法.简单地将所有游戏状态更新转移到第二个核心并不像你想要的那样多 - 如果核心#1是渲染器线程,而核心#2是游戏状态更新线程,那么核心#1将继续在核心#2更新状态时闲置,之后核心#1将继续执行实际渲染,而核心#2处于空闲状态,这将需要同样长的时间.要实际增加每帧可以执行的计算量,您需要有两个(或更多)内核同时工作,这会引发一些有趣的同步问题,具体取决于您如何定义分工(请参阅http:// developer .android.com/training/articles/smp.html如果你想走那条路的话.

试图用来Thread.sleep()管理帧速率通常会很糟糕.你不知道vsync之间的时间长短,或下一次到达之前的时间.它对于每个设备都是不同的,在某些设备上它可能是可变的.你最终会得到两个时钟 - vsync和sleep - 相互击败,结果是不稳定的动画.最重要的是,Thread.sleep()没有对准确性或最小睡眠持续时间做出任何具体保证.

我还没有真正浏览过Replica Island的来源,但是GameRenderer.onDrawFrame()你可以看到他们的游戏状态线程(创建一个要绘制的对象列表)和GL渲染器线程(它只是绘制列表)之间的交互.在他们的模型中,游戏状态仅根据需要进行更新,如果没有任何更改,则只需重新绘制上一个绘制列表.此模型适用于事件驱动的游戏,即当某些事情发生时屏幕上的内容更新(您点击某个键,计时器触发等).当事件发生时,他们可以进行最小状态更新并根据需要调整绘图列表.

从另一个角度来看,渲染线程和游戏状态并行工作,因为它们没有严格地绑在一起.游戏状态只是根据需要运行更新,渲染线程将每个vsync锁定下来并绘制它找到的任何内容.只要任何一方都没有长时间锁定任何东西,它们就不会明显干扰.唯一有趣的共享状态是绘制列表,用互斥锁保护,因此它们的多核问题被最小化.

对于Android Breakout(http://code.google.com/p/android-breakout/),游戏中有一个球在连续运动中弹跳.我们希望在显示允许的情况下更新状态,因此我们使用前一帧的时间增量来驱动vsync的状态更改,以确定事物的进展程度.每帧计算很小,而现代GL设备的渲染非常简单,因此它很容易适应1/60秒.如果显示更新得更快(240Hz),我们可能偶尔丢帧(再次,不太可能被注意到),我们将在帧更新上烧掉4倍的CPU(这是不幸的).

如果出于某种原因,其中一个游戏错过了vsync,则玩家可能会或可能不会注意到.状态通过经过时间而不是固定持续时间"帧"的预设概念前进,因此例如球将在两个连续帧中的每一个上移动1个单位,或者在一个帧上移动2个单位.根据帧速率和显示器的响应性,这可能不可见.(这是一个关键的设计问题,如果你按照"滴答声"的方式设想你的游戏状态,那么这个问题就会让你头疼.)

这两种都是有效的方法.关键是在onDrawFrame调用时绘制当前状态,并尽可能不频繁地更新状态.

请注意其他任何碰巧阅读此内容的人:请勿使用System.currentTimeMillis().使用的问题中的示例SystemClock.uptimeMillis(),它基于单调时钟而不是挂钟时间.那个,或者System.nanoTime()是更好的选择.(我正在进行一次小小的讨伐currentTimeMillis,在移动设备上突然向前或向后跳跃.)

更新:我写了一个更长的回答类似的问题.

更新2:我写了一篇关于一般问题的更长的答案(见附录A).