游戏循环
實(shí)現(xiàn)一個(gè)游戲的一種非常流行的方式看起來(lái)像這樣:
while (playing) {advance state by one framerender the new framesleep until it’s time to do the next frame }這種方式有幾個(gè)問(wèn)題,最基本的是游戲可以定義什么是 “幀” 的想法。不同的顯示器將以不同的頻率刷新,且頻率可能隨時(shí)間而變。如果你產(chǎn)生幀的速度比顯示器能夠展示它們的快,你將不得不偶爾丟棄一個(gè)。如果你生成它們的速度太慢,SurfaceFlinger 將周期性地?zé)o法獲得新緩沖區(qū)并重新展示之前的幀。這兩種情況都會(huì)導(dǎo)致可見的毛刺。
你需要做的就是匹配顯示器的幀率,并根據(jù)自上一幀開始經(jīng)過(guò)了多長(zhǎng)時(shí)間來(lái)推進(jìn)游戲狀態(tài)。有兩種方式做到這一點(diǎn):(1) 填充BufferQueue,并依賴“交換緩沖區(qū)”的背壓;(2) 使用 Choreographer (API 16+)。
隊(duì)列填充
這實(shí)現(xiàn)起來(lái)很簡(jiǎn)單:僅僅盡快交換緩沖區(qū)。在早期的 Android 版本中,這實(shí)際可能付出的代價(jià)是 SurfaceView#lockCanvas() 將使你休眠 100ms。現(xiàn)在,現(xiàn)在它被 BufferQueue 加速了,BufferQueue 清空的速度可以和 SurfaceFlinger 一樣快。
Android Breakout 中可以看到一個(gè)這種方法的例子。它使用了 GLSurfaceView,其運(yùn)行于一個(gè)調(diào)用應(yīng)用程序的 onDrawFrame() 回調(diào)并交換緩沖區(qū)的循環(huán)中。如果 BufferQueue 滿了,eglSwapBuffers() 將等待直到有緩沖區(qū)可用。緩沖區(qū)在 SurfaceFlinger 釋放它們時(shí)可用,在為顯示器獲取一個(gè)新的之后,緩沖區(qū)就可以使用。由于這發(fā)生在 VSYNC 時(shí),你的繪制循環(huán)時(shí)序?qū)⑴c刷新頻率匹配。大多是。
這種方法有兩個(gè)問(wèn)題。首先,應(yīng)用程序被綁定到了 SurfaceFlinger 活動(dòng),根據(jù)需要做多少工作以及是否與其他進(jìn)程競(jìng)爭(zhēng) CPU 時(shí)間,將需要花費(fèi)不同的時(shí)間。由于你的游戲狀態(tài)根據(jù)緩沖區(qū)交換的時(shí)間推進(jìn),你的動(dòng)畫將不會(huì)以固定頻率更新。當(dāng)以 60fps 運(yùn)行時(shí),隨著時(shí)間的推移,平均值不一致,盡管你可能不會(huì)注意到顛簸。
其次,第一對(duì)緩沖區(qū)交換將發(fā)生的非常快,由于 BufferQueue 還沒(méi)有滿。幀之間計(jì)算的時(shí)間將接近于零,因此游戲?qū)a(chǎn)生一些什么也沒(méi)發(fā)生的幀。在一個(gè)像 Breakout 這樣的游戲中,其在每一次刷新時(shí)更新屏幕,除了游戲首次啟動(dòng)(或取消暫停)時(shí)隊(duì)列總是滿的,所以效果不明顯。偶爾暫停動(dòng)畫,然后返回盡可能快的模式的游戲可能會(huì)看到奇怪的打嗝。
Choreographer
Choreographer 允許你設(shè)置一個(gè)在下次 VSYNC 時(shí)被調(diào)用的回調(diào)。實(shí)際的 VSYNC 時(shí)間作為一個(gè)參數(shù)傳入。因此即使你的應(yīng)用沒(méi)有立即喚醒,對(duì)于顯示器何時(shí)開始刷新你依然有一個(gè)精確的圖景。使用這個(gè)值,而不是當(dāng)前時(shí)間,將為你的游戲狀態(tài)更新邏輯產(chǎn)生一個(gè)一致的時(shí)間源。
不幸的是,在每個(gè) VSYNC 之后你得到回調(diào)的事實(shí)并不能保證你的回調(diào)將及時(shí)執(zhí)行,或者你將能夠迅速地執(zhí)行回調(diào)。你的應(yīng)用程序?qū)⑿枰獧z測(cè)它落后的情況,并手動(dòng)丟棄幀。
Grafika 中的 "Record GL app" activity 提供了一個(gè)這種方法的例子。在一些設(shè)備上 (比如 Nexus 4 和 Nexus 5),如果你只是坐著觀看,activity 將開始下丟幀。GL 渲染是微不足道的,但偶爾地 View 元素會(huì)被重繪,如果設(shè)備已經(jīng)掉入了節(jié)電模式的話測(cè)量/布局過(guò)程可能消耗非常長(zhǎng)的時(shí)間。(根據(jù)systrace,在Android 4.4上的時(shí)鐘緩慢之后,需要28ms而不是6ms。如果在屏幕上拖動(dòng)你的手指,它認(rèn)為你正在與 activity 交互,因此時(shí)鐘速度將保持高速,且你將從不會(huì)丟棄幀。)
簡(jiǎn)單的修復(fù)辦法是在 Choreographer 回調(diào)中,如果當(dāng)前時(shí)間晚于
VSYNC 之后 N 毫秒就丟棄幀。理想的 N 值根據(jù)之前觀察到的 VSYNC 間隔決定。比如,如果刷新周期是 16.7ms (60fps),你可以在你運(yùn)行多于 15 ms 之后丟棄幀。
如果你觀看 "Record GL app 運(yùn)行,你將看到丟棄的幀的計(jì)數(shù)增加,甚至能夠在丟棄幀時(shí)在邊緣看到紅色的閃光。除非你的視力非常好,盡管,你將看不到動(dòng)畫波動(dòng)。在 60fps 的情況下,只要?jiǎng)赢嬕院愣ǖ乃俣壤^續(xù)前進(jìn),應(yīng)用程序可以丟棄偶爾的幀,而沒(méi)有任何人能注意到。你能逃脫多少次取決于你在繪制什么,顯示器的特性,以及使用該應(yīng)用程序的人員是否在檢測(cè)閃避。
線程管理
一般來(lái)說(shuō),如果你正在向 SurfaceView,GLSurfaceView,或 TextureView 渲染,你想要在一個(gè)專門的線程中執(zhí)行該渲染。不要在 UI 線程中做任何 “重活” 或任何需要不確定時(shí)間的事情。
Breakout 和 "Record GL app" 使用專門的渲染線程, 且它們還在該線程中更新動(dòng)畫狀態(tài)。只要游戲狀態(tài)能夠快速更新這就是合理的方法。
其它的游戲?qū)⒂螒蜻壿嫼弯秩就耆珠_。如果你有一個(gè)簡(jiǎn)單的游戲,它什么也不做,只是每 100ms 移動(dòng)一個(gè)塊,你可以讓專門的線程只做這些:
run() {Thread.sleep(100);synchronized (mLock) {moveBlock();}}(您可能希望使睡眠時(shí)間是基于一個(gè)固定的時(shí)鐘的偏移計(jì)算的,以防止漂移 - sleep() 不是完美的一致的,moveBlock() 接收非零的時(shí)間值 - 但你可以根據(jù)你的想法來(lái)。)
當(dāng)繪制代碼喚醒時(shí),它只是獲得鎖,獲得時(shí)鐘的當(dāng)前位置,釋放鎖,并繪制。而不是基于幀間增量時(shí)間進(jìn)行分?jǐn)?shù)移動(dòng),你只需要一個(gè)線程來(lái)移動(dòng)事物,而另一個(gè)線程可以在繪圖開始時(shí)隨時(shí)繪制事物。
對(duì)于任何復(fù)雜的場(chǎng)景,您都希望創(chuàng)建一個(gè)按照喚醒時(shí)間排序的即將到來(lái)的事件列表,并且在下一個(gè)事件到期之前睡休眠,但這是一樣的。
原文
總結(jié)
- 上一篇: TextureView
- 下一篇: H.264 视频的 RTP 载荷格式