程序響應性
程序響應性
譯者:緋紅KING
來源:http://www.gamasutra.com/view/feature/1942/programming_responsiveness.php
【如果你不能在游戲中控制你的行動,是否應該責怪這個游戲呢?在這篇技術文章中,Neversoft的合伙人Mick West會闡述游戲中造成響應延遲(Lag)的問題,并提出一些必要的解決方案。】
【如果你不能在游戲中控制你的行動,是否應該責怪這個游戲呢?在這篇技術文章中,Neversoft的合伙人Mick West會闡述游戲中造成響應延遲(Lag)的問題,并提出一些必要的解決方案。】
響應性是一種可以在第一時間成就或毀壞一個游戲的東西。在一些游戲的雜志評論中,可以明顯體會到響應性差的游戲會被形容為“遲鈍的”,“沒反應的”,“浮躁的”或“懶散的”。而好的游戲會被評價為“緊湊的”或“有響應的”。
響應性可以從幾個方面來理解,本文主要是從一個程序員的角度來闡述,提供一些可以提高游戲響應性的方法。
響應延遲
響應延遲是指從玩家觸發一個事件到玩家收到事件發生的反饋(通常是視覺上的)之間的時間遲滯。如果時間遲滯過長,游戲會顯得沒有響應。有幾個因素決定了響應延遲時間長度。
如果你的游戲是沒有響應的,這很可能是4或5個不同因素造成的累積效應。只是單獨調整其中一個因素,是不能起到明顯作用的。但是找出所有的因素,并調整它們,會帶來顯而易見的改進。
玩家,有時甚至是游戲設計師,都不能把他們在游戲中操作覺得不適的地方用語言表達出來。通常他們會試著做一些需要同步操作的事情,但是失敗了,他們不會告訴你“這個事件在我輸入后的0.10秒才發生作用”,而是會說這個游戲有些“遲緩”,“不緊湊”或者“難度高”。
或者他們根本不告訴你一些重點,只是單純說這個游戲很爛,但不知道為什么很爛。
設計師和程序員需要時刻注意響應延遲以及它對游戲的負面作用,甚至在測試玩家沒有直接匯報這一點時。
為什么發生延遲
要了解延遲發生的原因,你需要理解事件的發生序列:從用戶按下一個按鈕,到結果顯示在屏幕上。為了理解這一點,我們需要看一下游戲的主循環結構。主循環進行了兩個基本任務:邏輯和渲染。
主循環的邏輯部分更新了游戲的狀態(游戲對象和環境的內部實現),渲染部分則創建了需要顯示在電視上的一幀畫面。
在主循環的一些階段,通常是開始時,我們會接受到用戶的輸入,作為主循環的第三項任務,但也通常把它作為邏輯任務的一部分。在這里我把輸入單獨提出來,是為了看清楚事件發生的順序。
有幾種方式來描述主循環結構。最簡單的一種如表1所示,交替調用邏輯和渲染的代碼。我們假設游戲運行在固定幀率,通常是60fps或30fps的NTSC制式家用機游戲,而一些幀的同步發生在調用Rendering()中。
表1:簡單的主循環
while (1) {
Input();
Logic();
Rendering();
}
Input();
Logic();
Rendering();
}
在這里主循環只展現了故事的一半。對Rendering()的調用是在CPU端處理渲染任務,這些任務包括場景、物體、剔除、動畫、排序、設置變換、構造一個供GPU處理的顯示列表。這是一個迭代的過程。
實際上GPU渲染是在CPU渲染之后進行的,通常是異步的。所以當主循環開始進行到下一幀時,GPU仍然在渲染前一幀。
那么延遲在什么時候發生?要理解造成延遲的原因,你需要理解從用戶按鍵輸入到接受反饋之間的事件序列。
在最頂層,用戶按下一個按鍵;游戲邏輯讀取這個按鍵輸入,更新游戲狀態;CPU渲染函數準備好渲染這個新狀態的一幀,然后GPU將其渲染;最后這個新的一幀被顯示在屏幕上。


圖1:當玩家按下一個鍵,游戲要占用3幀(最理想情況)來創建一個視覺反饋,程序問題會引入多余的幀造成延遲。實際延遲的時間是每一幀時間長度的乘積。
圖1顯示了圖形化的序列內容。在第一幀的某一時刻,玩家按下一個鍵來開槍。輸入過程完成后,這個輸入會被讀取到第二幀。第二幀更新了基于按鍵輸入的邏輯狀態(開火)。
仍然在第二幀,渲染的CPU端開始執行這個新的邏輯狀態。然后在第三幀,GPU執行了這個新邏輯狀態的實際渲染。最后在第四幀的開始,新渲染出來的一幀畫面從幀緩沖中呈現給玩家。
那么這個延遲有多長時間?這要看一幀有多長時間(這里“一幀”是指主循環的一個完整迭代過程)。從用戶輸入到轉換成視覺反饋,需要占用3幀。
如果我們的游戲在30fps下運行,延遲就是3/30,即十分之一秒。如果游戲在60fps下運行,那這個延遲會是3/60,即二十分之一秒。
這個計算說明了一個關于60fps和30fps之間的差異的判斷錯誤。因為這兩個幀率之間的差異是1/60秒,人們推斷出響應性之間的差異也是1/60。
但實際上,從60到30并沒有給延遲增加一個垂直同步,它的效果是乘積形式的,加倍了延遲響應的過程管線。在我們圖1中的理想示例中,增加了3/60秒,并不是1/60。如果事件管線再長一些,這個結果會變得更多,這是極有可能發生的情況。
實際上圖1展示的是事件序列的最佳狀態。按鍵輸入通過最短路徑轉換為視覺反饋。我們可以在事件序列中清楚的看到這一點。
作為一個程序員,熟悉這些事件發生的順序,是理解游戲中事物運作原理的重要環節。如果對事件發生的順序不加注意的話,很容易造成多余幀的延遲(意味著一個1/60或1/30秒的延遲)。
舉一個簡單的例子,如果我們把主循環中的Logic()和Rendering()交換一下,想想會發生什么。來看一下圖1的第二幀:這里GPU邏輯(渲染)發生在CPU邏輯之后,所以在第二幀開始時輸入會影響CPU邏輯,之后是同一幀里的GPU邏輯。
然而如果GPU邏輯在前面進行,那么輸入就得到下一幀才能影響到GPU邏輯,因此造成了一個多余幀的延遲。雖然這只是一個初學者犯得錯誤,但程序員仍要牢記不要讓它發生。
造成延遲的多余幀可能在一些微小的行為中產生,但結果會影響游戲邏輯的整個序列。在我們的例子中,是開槍的過程。
假如現在我們的引擎用一個物理引擎來設置場景中物體的位置信息,并且需要處理的事件在更新中增加了(例如碰撞事件)。這種情況下,輸入的序列或邏輯如表2所示。
表2:更新物理之后是事件處理
void Logic() {
HandleInput();
UpdatePhysics();
HandleEvents();
}
HandleInput();
UpdatePhysics();
HandleEvents();
}
基于消息的事件處理(Event Handling)是用于低耦合系統的一個不錯的手段,程序員也通常會使用它來控制事件。要使槍開火,HandleInput()函數會發出一個事件,告訴槍去開火。
HandleEvents()函數會處理這個事件,并使槍實際開火。但是在這一幀里,物理更新已經發生了,這個效果直到下一幀才能同步,這樣就產生了一個多余幀的延遲。
更多延遲的原因
低層的行為順序會造成更多的延遲。例如,考慮一個跳躍。反饋內容是角色實際的移動。要使游戲中的物體移動,你可以直接設置速度,或者給物體加一個作用力,例如加速度,或者瞬間的推力。
這個情景會產生一個問題,就是如果你的物理引擎在速度改變之前更新位置信息——這是很多游戲編程入門教程中的常見情況。
盡管在處理輸入事件后,物體跳躍的速度變化已經在同一幀里更新了,但是物體直到下一幀才能開始改變位置,這樣就產生了一個多余幀的延遲。
要記住,問題都是累積起來的,并且很難單獨去辨識,這些組合的效果會使你的游戲操作變得很艱難。
假設你同時犯了以上三個錯誤:你在邏輯之前進行渲染,你在物理狀態進行之后處理邏輯事件,并且你在速度變化之前更新位置信息。這是從玩家輸入到接受反饋的三個主循環的完整迭代,在這三幀之上,你至少創建了6幀的延遲。
在60fps的游戲中,這會是1/10秒,已經足夠糟糕了。但如果你的游戲是在30fps下,這個延遲會加倍,變為無法忍受的1/5秒,即200毫秒。
綜合以上的問題,一些其他因素也可以導致延遲。動作可以由動畫來驅動,由動畫特定時間點的速度變化來實現。例如,如果一個動畫師為了讓動畫與視覺更匹配,在動畫中設置了一個不到一秒的跳躍速度變化,這可能看起來挺好,但實際上感覺卻很糟。
動畫師可以修正這個問題,只要確保速度變化是在動畫的第一幀發生,玩家可以迅速得到反饋。但又有一個問題產生了,就是如何觸發一個動畫,并轉換為實際的動作?看起來動畫的更新是由Render()函數來處理的。但任何以動畫形式觸發的事件,都需要在增加一幀后的循環中才能處理。
此外,觸發一個動畫可能在下一幀才會發生作用,又造成一幀的延遲。我們的延遲會從6幀增加到8幀。即使在每秒60幀的情況下,這也就幾乎不能玩了。
這些也并不是全部。還有很多方式使多余的延遲幀潛入一個游戲。你也許會把你的物理部分單獨辟出一個線程(或者一個物理處理單元)。
如果你使用三倍緩沖來使你的幀率更平滑呢? 你也許用抽象事件通過系統的并行過程分解成真實事件。你也許在用一種腳本語言讓等待事件時增加額外的一幀。
你可以通過組合各種關于時間和事件的概念,使你的游戲邏輯更靈活,這是非常必要的。但是在實現它們的時候,程序員可能會忽視發生在表面之下的事情,使造成延遲的多余幀悄悄潛入。
響應性,不是反應時間
有一個關于響應性的極大的誤解,和人類的反應時間相關。人類不能通過視覺刺激做出身體上的反應,并且在0.1秒內移動他們的手指。
游戲玩家的頂峰反應時間分布在0.15秒至0.30秒,這取決于他有多“高橋名人”(形容按鍵反應迅速)。像這些可以計量的信息,通常在討論游戲響應性時會提到。但它們之間是沒有聯系的。
不是玩家對游戲的反應有多塊,而是游戲對玩家的反應有多快。問題并不在于反應時間,而是在于同步性。
用吉他英雄來舉例,這個游戲中會有一些符號出現,玩家需要在恰當的時間按下相應的按鍵(當目標物體在特定區域范圍時)。你是在預判斷事件,這里不涉及任何反應時間。
缺乏響應性的問題一般是游戲沒有及時反饋玩家的操作,并且事件發生時目標物體已經超出目標范圍了。
如果你在正確時間按下鍵,你絕不希望物體會在爆炸前再多移動幾個像素。但是物體通常在每幀會移動幾個像素,有幾幀的延遲使物體滑過目標。
有許多基于預測和按鍵輸入的游戲。在一個滑板游戲中,你想要在到達軌道終點之前跳躍。在一個第一人稱射擊游戲中,你朝一個前方快速跑動的敵人開槍。
再提一次,響應性并不是反應時間。你通常會在射擊的半秒前看見你的目標,或許更長時間,你會移動你的槍,或者等待目標跑到你的準星上。
對響應性問題是不能靠直覺來解決的,程序員全面了解問題本身才是非常重要的。最重要的一點,是要清楚按鍵觸發行為任務到創建視覺反饋這個過程中邏輯和渲染的逐幀過程。一旦你理解了這個過程,你就能夠優化它,使它工作在最佳狀態。