美文网首页
游戏循环

游戏循环

作者: hanpfei | 来源:发表于2017-07-23 09:16 被阅读31次

    实现一个游戏的一种非常流行的方式看起来像这样:

    while (playing) {
        advance state by one frame
        render the new frame
        sleep until it’s time to do the next frame
    }
    

    这种方式有几个问题,最基本的是游戏可以定义什么是 “帧” 的想法。不同的显示器将以不同的频率刷新,且频率可能随时间而变。如果你产生帧的速度比显示器能够展示它们的快,你将不得不偶尔丢弃一个。如果你生成它们的速度太慢,SurfaceFlinger 将周期性地无法获得新缓冲区并重新展示之前的帧。这两种情况都会导致可见的毛刺。

    你需要做的就是匹配显示器的帧率,并根据自上一帧开始经过了多长时间来推进游戏状态。有两种方式做到这一点:(1) 填充BufferQueue,并依赖“交换缓冲区”的背压;(2) 使用 Choreographer (API 16+)。

    队列填充

    这实现起来很简单:仅仅尽快交换缓冲区。在早期的 Android 版本中,这实际可能付出的代价是 SurfaceView#lockCanvas() 将使你休眠 100ms。现在,现在它被 BufferQueue 加速了,BufferQueue 清空的速度可以和 SurfaceFlinger 一样快。

    Android Breakout 中可以看到一个这种方法的例子。它使用了 GLSurfaceView,其运行于一个调用应用程序的 onDrawFrame() 回调并交换缓冲区的循环中。如果 BufferQueue 满了,eglSwapBuffers() 将等待直到有缓冲区可用。缓冲区在 SurfaceFlinger 释放它们时可用,在为显示器获取一个新的之后,缓冲区就可以使用。由于这发生在 VSYNC 时,你的绘制循环时序将与刷新频率匹配。大多是。

    这种方法有两个问题。首先,应用程序被绑定到了 SurfaceFlinger 活动,根据需要做多少工作以及是否与其他进程竞争 CPU 时间,将需要花费不同的时间。由于你的游戏状态根据缓冲区交换的时间推进,你的动画将不会以固定频率更新。当以 60fps 运行时,随着时间的推移,平均值不一致,尽管你可能不会注意到颠簸。

    其次,第一对缓冲区交换将发生的非常快,由于 BufferQueue 还没有满。帧之间计算的时间将接近于零,因此游戏将产生一些什么也没发生的帧。在一个像 Breakout 这样的游戏中,其在每一次刷新时更新屏幕,除了游戏首次启动(或取消暂停)时队列总是满的,所以效果不明显。偶尔暂停动画,然后返回尽可能快的模式的游戏可能会看到奇怪的打嗝。

    Choreographer

    Choreographer 允许你设置一个在下次 VSYNC 时被调用的回调。实际的 VSYNC 时间作为一个参数传入。因此即使你的应用没有立即唤醒,对于显示器何时开始刷新你依然有一个精确的图景。使用这个值,而不是当前时间,将为你的游戏状态更新逻辑产生一个一致的时间源。

    不幸的是,在每个 VSYNC 之后你得到回调的事实并不能保证你的回调将及时执行,或者你将能够迅速地执行回调。你的应用程序将需要检测它落后的情况,并手动丢弃帧。

    Grafika 中的 "Record GL app" activity 提供了一个这种方法的例子。在一些设备上 (比如 Nexus 4 和 Nexus 5),如果你只是坐着观看,activity 将开始下丢帧。GL 渲染是微不足道的,但偶尔地 View 元素会被重绘,如果设备已经掉入了节电模式的话测量/布局过程可能消耗非常长的时间。(根据systrace,在Android 4.4上的时钟缓慢之后,需要28ms而不是6ms。如果在屏幕上拖动你的手指,它认为你正在与 activity 交互,因此时钟速度将保持高速,且你将从不会丢弃帧。)

    简单的修复办法是在 Choreographer 回调中,如果当前时间晚于
    VSYNC 之后 N 毫秒就丢弃帧。理想的 N 值根据之前观察到的 VSYNC 间隔决定。比如,如果刷新周期是 16.7ms (60fps),你可以在你运行多于 15 ms 之后丢弃帧。

    如果你观看 "Record GL app 运行,你将看到丢弃的帧的计数增加,甚至能够在丢弃帧时在边缘看到红色的闪光。除非你的视力非常好,尽管,你将看不到动画波动。在 60fps 的情况下,只要动画以恒定的速度继续前进,应用程序可以丢弃偶尔的帧,而没有任何人能注意到。你能逃脱多少次取决于你在绘制什么,显示器的特性,以及使用该应用程序的人员是否在检测闪避。

    线程管理

    一般来说,如果你正在向 SurfaceView,GLSurfaceView,或 TextureView 渲染,你想要在一个专门的线程中执行该渲染。不要在 UI 线程中做任何 “重活” 或任何需要不确定时间的事情。

    Breakout 和 "Record GL app" 使用专门的渲染线程, 且它们还在该线程中更新动画状态。只要游戏状态能够快速更新这就是合理的方法。

    其它的游戏将游戏逻辑和渲染完全分开。如果你有一个简单的游戏,它什么也不做,只是每 100ms 移动一个块,你可以让专门的线程只做这些:

        run() {
            Thread.sleep(100);
            synchronized (mLock) {
                moveBlock();
            }
        }
    

    (您可能希望使睡眠时间是基于一个固定的时钟的偏移计算的,以防止漂移 - sleep() 不是完美的一致的,moveBlock() 接收非零的时间值 - 但你可以根据你的想法来。)

    当绘制代码唤醒时,它只是获得锁,获得时钟的当前位置,释放锁,并绘制。而不是基于帧间增量时间进行分数移动,你只需要一个线程来移动事物,而另一个线程可以在绘图开始时随时绘制事物。

    对于任何复杂的场景,您都希望创建一个按照唤醒时间排序的即将到来的事件列表,并且在下一个事件到期之前睡休眠,但这是一样的。

    原文

    相关文章

      网友评论

          本文标题:游戏循环

          本文链接:https://www.haomeiwen.com/subject/wkvpkxtx.html