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

时间:2012-12-29 00:13:35

标签: android opengl-es thread-sleep game-loop

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

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

    @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();
            }
        }
    }
}

这是我的 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);
}

我查看了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();
            }
        }
    }

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

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

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

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

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

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

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

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

由于

2 个答案:

答案 0 :(得分:17)

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

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

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

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

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

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

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

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

如果出于某种原因,其中一个游戏错过了vsync,则播放器可能会或可能不会注意到。状态是经过的时间而不是固定持续时间框架的预先设定的概念,所以例如,球将在两个连续帧中的每一帧上移动1个单位,或在一个帧上移动2个单位。根据帧速率和显示器的响应性,这可能不可见。 (这是一个关键的设计问题,如果您按照&#34; ticks&#34;设想您的游戏状态,那么这个问题可能会让您感到头疼。)

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

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

更新:我在类似问题上写了http://code.google.com/p/android-breakout/

更新2:我写了一篇关于一般问题的even longer answer(见附录A)。

答案 1 :(得分:1)

问题的一部分可能是由Thread.sleep()不准确引起的。试着调查一下睡眠的实际时间。

使动画平滑的最重要的事情是你应该计算一些插值因子,称之为alpha,在两个连续动画更新线程调用之间的连续渲染线程调用中线性插值你的动画。换句话说,如果您的更新间隔与帧速率相比较高,则不插入动画更新步骤就像您将以更新间隔帧速率渲染。

编辑:举个例子,PlayN就是这样做的:

@Override
public void run() {
  // The thread can be stopped between runs.
  if (!running.get())
    return;

  int now = time();
  float delta = now - lastTime;
  if (delta > MAX_DELTA)
    delta = MAX_DELTA;
  lastTime = now;

  if (updateRate == 0) {
    platform.update(delta);
    accum = 0;
  } else {
    accum += delta;
    while (accum >= updateRate) {
      platform.update(updateRate);
      accum -= updateRate;
    }
  }

  platform.graphics().paint(platform.game, (updateRate == 0) ? 0 : accum / updateRate);

  if (LOG_FPS) {
    totalTime += delta / 1000;
    framesPainted++;
    if (totalTime > 1) {
      log().info("FPS: " + framesPainted / totalTime);
      totalTime = framesPainted = 0;
    }
  }
}