《Programming Responsiveness》:遊戲程式設計中的回應性

armored-core原文出處:Programming Responsiveness

在玩遊戲的經驗裡,你是否覺得有些遊戲玩起來不太順暢,但又說不上來是什麼原因;明明是單機的遊戲,卻覺得操作反應有些 Lag;狙擊槍的準星明明抖個不停,竟然也可以命中敵人。這篇文章裡,作者對於遊戲的「回應性」(Responsiveness) 這個鮮少有人提及的議題,做出了詳細的觀察與探究,非常值得遊戲程式設計者與遊戲企畫設計者閱讀參考。

(圖片來源:http://games.mattsarrel.com/)

如果以軟體領域的角度來檢視,遊戲軟體與其他軟體最大的不同之處,或許就是在於遊戲對「即時反應」(Realtime Response) 的嚴格要求。在一些節奏比較快速的遊戲中,如第一人稱射擊、賽車競速或音樂節奏類型遊戲等等,甚至不能夠承擔超過零點幾秒的延遲誤差。那怕只是一點點的延遲反應而已,都會破壞玩家對於遊戲的投入感;更甚者,也可能就此在玩家心中埋下難以逆轉的負面印象。

Response lag is the delay between the player triggering an event and the player receiving feedback (usually visual) that the event has occurred.

「回應延遲」(Response Lag) 的定義,是指從玩家觸發事件,直到玩家得到回饋反應的延遲期間。觸發事件,是指玩家藉由搖桿、手把、鍵盤或滑鼠等輸入裝置,觸發遊戲系統中的行為動作;而回饋反應,則通常是指遊戲中的視覺面呈現。如果在「觸發事件」與「得到回饋」兩者之間的延遲期間過於冗長,將會造成玩家認為遊戲反應遲鈍、動作慢吞吞或是操縱不順暢等等負面觀感。

當遭遇回應延遲問題的時候,許多玩家,甚至是遊戲設計者,都沒有辦法用正確的詞彙敘述遊戲的問題何在;多數玩家不會說「這個事件在我按下按鈕的 0.1 秒後才發生」,而是會說「這個遊戲感覺緩慢」、「遊戲的節奏不對」、「遊戲太過於困難」,或者只是說「這是個爛遊戲」。他們無法真正瞭解「爛」的成因為何,因為他們並沒有意識到延遲現象背後的成因。在進行遊戲測試時亦然如此;即使測試者沒有明確地回報關於回應延遲方面的問題,但這些問題可能只是以不同形式的敘述句表達。無論是企畫設計者,或者程式設計者都不應該忽略掉這些問題的嚴重性。

回應延遲通常不是由單一因素所造成的局面,而為了解決這個問題,首先我們必須知道現象背後的成因。由前述的定義可知,延遲時間簡單來說就是玩家按下按鈕到螢幕產生結果的期間;而為了瞭解延遲從何而來,首先必須知道遊戲程式在主迴圈 (Main Loop) 中進行了哪些工作程序。在遊戲的主迴圈中,主要有兩項基本任務:邏輯運算繪圖;邏輯運算程序用於更新遊戲物件的狀態與數據,繪圖程序則負責繪製遊戲畫面的畫格 (Frame),並且將結果呈現在螢幕上。除了上述的兩大程序之外,在主迴圈中,還會有個負責偵測輸入裝置系統的程序。以最簡化的遊戲主迴圈流程來說,程式碼如下所示:

// 遊戲主迴圈
while (1)
{
    // 偵測輸入
    Input();
    // 邏輯運算
    Logic();
    // 繪圖
    Rendering();
}

特別要注意的是,在 Rendering() 函式中所執行的工作,其實僅限於 CPU 端進行的繪圖運算程序,例如 Transform、Culling、Sorting 等等工作;而真正的低階繪圖程序,則是在 CPU 端的繪圖程序之後,以非同步化 (Asynchronous) 的方式交由 GPU 端進行處理。

那麼,遊戲主迴圈中的回應延遲從何而來?請參照這張精美的圖片;從玩家按下按鈕直到結果顯示於螢幕上,總共可以分為 4 個階段,分別為 Input、CPU、GPU 與 TV:

  • Frame 1:在第 1 個畫格時,於其中的某個時間點,玩家按下了按鈕。
  • Frame 2:第 2 個畫格中,主迴圈於 Input() 函式中偵測到了玩家的輸入事件,並且於接續的 Logic() 與 Rendering() 函式中進行相關的更新與繪製動作。
  • Frame 3:第 3 個畫格中,由 GPU 非同步化進行真正的繪圖程序。
  • Frame 4:最後,在第 4 個畫格裡,將剛完成繪製的畫面顯示到螢幕上。

由 Input 至 TV 階段,正好對應於 Frame 1 到 Frame 4,共計需要花費 3 個畫格的時間。所謂的 3 個畫格是多久的時間?對於以 30 FPS(每秒 30 個畫格)進行的遊戲來說,3 個畫格就是 3/30,十分之一秒;對於以 60 FPS(每秒 60 個畫格)進行的遊戲來說,3 個畫格就是 3/60,二十分之一秒。所以,當玩家按下輸入裝置上的按鈕時,在最佳的情況下,至少需要二十分之一秒的時間,才能夠見到螢幕上呈現出來的結果。

以上,只是最佳狀態 (Best Case) 的情境;但是現實世界經常不是處於最佳狀態。所以,不如來看看怎麼把事情弄得更糟一點。在上述的程式碼中,假設我們把 Logic() 與 Rendering() 函式的順序對調,遊戲的主迴圈程序就會變成:

// 遊戲主迴圈
while (1)
{
    // 偵測輸入
    Input();
    // 繪圖
    Rendering();
    // 邏輯運算
    Logic();
}

在這個主迴圈的程序裡,由於 Redering() 先於 Logic() 函式執行,所以於 Logic() 函式中更新的遊戲物件狀態,就必須等待下次進入主迴圈,也就是下一個畫格裡,才能夠將邏輯運算更新的物件狀態傳入 Rendering() 函式中繪製。因此,只要單純地改變函式的順序,就能夠順利地多出額外的一個畫格延遲!

只是多了一個的畫格延遲,看起來不夠糟嗎?精彩的還在後頭。

假設在遊戲裡,我們使用了某個物理引擎藉以改變物件的行進方向與位置。與物理引擎程式碼相關的部分,可以在 Logic() 函式中進行處理:

void Logic()
{
    // 處理玩家的輸入,並且丟出相關的事件訊息
    HandleInput();
    // 更新物理引擎
    UpdatePhysics();
    // 處理事件訊息,進行相對應的動作
    HandleEvents();
}

看起來是沒有任何問題的處理程序。然而,上述的遊戲系統,又再度多出了一個額外的畫格延遲。

以遊戲中的開槍行為為例,當玩家按下射擊按鈕,在 HandleInput() 函式中,會接收到輸入的指令,並且發出事件訊息告訴「槍」這個物件執行「開火」的動作;而在 HandleEvents() 函式中,「槍」物件會接收到事件訊息,並且做出實際的開槍動作與邏輯處理。然而,由於 UpdatePhysics() 先於 HandleEvents() 函式執行,所以開槍事件對於遊戲中各物件物理狀態的影響,會直到遊戲主迴圈下一次進入 Logic() 函式時才產生效果,因此也就再次成功地製造了一個額外的畫格延遲。

另外一個類似的情境,發生於「先更新物件位置,隨後才更新物件速度」的狀況裡。理由同上,因為已經更新完物件的位置數值,所以在同一個畫格中,即使物件的速度產生了變化,也無法立即反應在物件的位置上;只能夠在下一次處理物件更新程序時,物件的位置才會正確對應於上個畫格中改變的物件速度。

總結以上所述,共計有三項顯而易見的錯誤

  • 在進行邏輯運算之前,先進行繪圖程序。
  • 在更新物理引擎之後,才處理事件訊息邏輯。
  • 先更新物件的位置,之後才更新物件的速度。

「反正只不過是相差一個畫格而已嘛!」或許一個畫格延遲的效應看似微不足道,不會對遊戲造成任何影響;然而,畫格延遲的問題本身具有累積效應,如果遊戲程式設計者,同時犯了上述三項錯誤,將很有可能使得原來只有 3 個畫格的延遲時間,倍增為 6 個畫格的延遲時間。對於 60 FPS 的遊戲來說,6/60 已經佔去 0.1 秒的時間,而對於 30 FPS 的遊戲,6/30 更是達到 0.2 秒,足足有 200 毫秒的延遲時間!就遊戲軟體的需求條件來說,這是相當令人難以接受的執行表現。

而除了上述三項常見的錯誤以外,還有更多其他的因素可能造成時間延遲的問題。有時候美術動作設計者 (Animator) 會在動作上加入一點點額外的速度改變,使得角色的動作看起來更加符合視覺上的效果;然而在配合遊戲操控時,卻可能變成看起來好看,玩起來感覺很糟的情況。另外,像是觸發角色的動作 (Animation) 事件時,系統可能會在下一個畫格中,才真正前進至指定動作的第一個畫格,因此使得角色動作的反應延遲現象更加嚴重。以上這些比較難以察覺的畫格延遲因素,也很容易悄悄地潛入遊戲系統之中。

最後,作者提到一般人對於「回應性」這個議題最大的誤解,就是與反應時間 (Reaction Time) 之間的關連性。在討論遊戲反應性問題時,經常會被提起的反駁論點是:「玩家的反應時間最快約可到達 0.15 秒至 0.3 秒左右,再怎麼樣也不可能有快於 0.1 秒的反應能力。」然而,這項推論其實相當值得懷疑。

It’s not how fast a player reacts to the game; it’s how fast the game reacts to the player. The issue is not one of reaction times, but of synchronization.

guitar-hero-3真正的關鍵在於同步化的感受。以《吉他英雄》為例,玩家必須在音弦符號來到特定區域時,準確地按下相對應的按鈕;在這樣的情境下,身為玩家的你,已經預測了未來幾秒內即將發生的事件,而與所謂的反應能力全然無關。重點在於玩家預期按下按鍵之後,音弦符號的誤差最多不會超過幾個像素點。所以如果在遊戲中發生了時間延遲的現象,就會造成音弦符號飄移過該區域才產生結果反應,也因此導致玩家心理預期與視覺回饋上的落差。

(圖片來源:http://electronics.howstuffworks.com/)

對於其他類型的遊戲來說,同樣存在著同步化的需求;例如在 FPS 類型的遊戲中,熟練的玩家不會等到敵人落在準星範圍內才按鈕開火,而是會先一步預測敵人的行進路線,提早約 0.5 秒左右開槍射擊才能命中目標。如果發生了時間延遲的情形,就會看到敵人明明位於準星的範圍之外,卻仍然中槍爆頭,造成邏輯與視覺效果的不同步狀況。簡單來說,遊戲的表現與回饋機制,必須要能夠符合玩家預想中的結果。

在遊戲的過程中,有許多操控行為例如走路、跳躍、攻擊等等,是每次進行遊戲都需要重複幾百次,甚至上千次的動作;如果在這些基本的操控行為中,產生了嚴重的時間延遲問題,而沒有被研發團隊與測試團隊發現,那麼即使是一款美術再漂亮、玩法再創新的遊戲,都很有可能無法獲得玩家的青睞而只能黯然下台。對於遊戲企畫設計者與程式設計者來說,這些反應性的問題並不容易在開發過程中引起注意,我們需要積極地去發現時間延遲的問題,才能夠製作出操控性更佳的遊戲。以後聽到有人說「這個遊戲真爛」或者「遊戲玩起來有點卡卡的」之類的話,你也能夠從另外一個角度思考了。

在下一篇文章裡,作者將帶領我們使用簡單的器材與方法測量遊戲的延遲時間,並且檢驗各款上市遊戲的延遲數據。

8 Replies to “《Programming Responsiveness》:遊戲程式設計中的回應性”

  1. 好文推推推(從哪抄來的口吻?)
    可惜我好像沒有啥開發即時動作遊戲的經驗,體會不夠深刻…
    找時間應該來試試看?

  2. @exe:
    其實不只是即時動作遊戲而已,我認為在 MMORPG 中也能夠應用這些概念,改善操作使用者介面以及玩家動作的回應時間~

    謝謝回應。 ^^

  3. 對於那張4個frame的圖我看的不是很懂..
    當Rendering處理完後..一般都會執行Present或是Swapbuffer..把螢幕上的畫面更新…
    可能會有掃描頻率同步的問題…但是如果要同步..整個main loop也是等更新完後才進行下一步
    為何使用者到第四個frame使用者才看到結果?

  4. @gino:
    因為不論是使用 DirectX 或者 OpenGL 的 API,我們只是經由 CPU 下命令給 GPU 執行繪圖工作,而 CPU 與 GPU 之間是屬於非同步的溝通程序;也就是說,CPU 下指令後不等待 GPU 的結果返回,就會繼續執行下一行程式碼,所以當 CPU 已經在處理下一個畫格時,GPU 可能還在繪製前一個畫格的內容。也正是因為如此,所以我們才會需要使用 Double Buffering 的繪圖方法。

    以 Present() 與 glSwapBuffers() 的作用來說,就是把前一個畫格中繪製完成的內容呈現到螢幕上,而不是呈現目前畫格中的繪圖結果。

  5. 這邊的疑問在於是否Present/swapbuffer 時候CPU不等待畫完就離開..
    我認為當執行present/swapbuffer..CPU至少會等待backbuffer畫完
    之後就是把back buffer設為front buffer..這時候使用者就依據螢幕更新率來看到更新結果.
    或許它定義的frame是指螢幕更新的次數..而不是main loop更新畫面次數

  6. @gino:
    我認為作者的意思的確是 main loop 的更新次數沒錯喔。

    Present/SwapBuffer 動作其實應該是 pointer 重新指向而已。重點在於 back buffer 中畫好的內容,就是前一個 frame 中的物件呀。做了 swap buffer 動作後,此時螢幕顯示的是 back buffer,也就是前一個 frame 的物件,而 front buffer 則被轉到幕後去畫目前這個 frame 的物件,然後在下一個 frame 中再 swap buffer 顯示到螢幕上。也就是因為怕顯示卡來不及畫,才會需要用兩個 buffer 來交替畫面顯示的內容。

    不知道這樣的解釋你認不認同? @_@a

  7. @Hua:
    既然網路的反應時間無法縮短,這時就需要用一些技巧和障眼法,讓玩家玩到流暢度足夠的遊戲囉。

Leave a Reply