游戏循环
實現一個游戲的一種非常流行的方式看起來像這樣:
while (playing) {advance state by one framerender the new framesleep 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() 接收非零的時間值 - 但你可以根據你的想法來。)
當繪制代碼喚醒時,它只是獲得鎖,獲得時鐘的當前位置,釋放鎖,并繪制。而不是基于幀間增量時間進行分數移動,你只需要一個線程來移動事物,而另一個線程可以在繪圖開始時隨時繪制事物。
對于任何復雜的場景,您都希望創建一個按照喚醒時間排序的即將到來的事件列表,并且在下一個事件到期之前睡休眠,但這是一樣的。
原文
總結
- 上一篇: TextureView
- 下一篇: H.264 视频的 RTP 载荷格式