• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            芳草春暉

            偶爾記錄自己思緒的地方...

             

            調試游戲程序的學問(轉)

            鏈接:http://blog.csdn.net/fannyfish/

            版權聲明

            • 作者:Steve Rabin, Nintendo of America Inc.
            • 郵箱:steve@aiwisdom.com
            • 譯者:沙鷹
            • 校對:萬太平

            概述

            調試游戲程序,和調試任何其它軟件的代碼一樣,都可能是一項艱巨的任務。一般說來,有經驗的程序員能迅速地識別并糾正哪怕是最難的bug,但是對于新手而言,改bug可能更像是一件難以處理的,并且容易使人灰心喪氣的任務。更糟的是,當你初步著手開始尋找bug的根源時,永遠也不會知道究竟要花費多長時間才能找到。此時不必慌張,要像個訓練有素的程序員,集中精力尋找bug。一旦你消化了本文介紹的技巧和知識,你將能夠擊退最“兇猛”的bug,重獲對游戲的控制。

            運用本文描述的五步調試法,困難的調試過程也可能變得簡單一些。訓練有素地運用該方法,將確保你花費最少的時間在尋找和定位每一個bug上。在你著手對付一些有難度的bug時,牢記一些專家技巧也很重要,因此本文也收集了一些有價值的、經過時間考驗的技巧。然后本文還列出了一些有難度的調試情境,解釋了當遇到一些特定的bug模式時應當做些什么。因為好的工具對于調試任何游戲都很重要,本文還將討論一些特定的工具,你可將這些工具嵌入你的游戲中,從而幫助調試一些游戲編程所獨有的調試情形。最后讓我們回顧一些在前期預防bug的簡單技術。

            五步調試法

            老練的程序員們具有一種超能力,能夠迅速地、駕輕就熟地捕捉到即使是最不可思議的bug。他們總是神奇地、近乎直覺地知道錯誤源自何方,這一點實在令人敬畏。他們之所以顯得天才,除了因為擁有豐富的經驗外,還因為他們對于勘探和減少需排查的可能的原因的方法訓練有素、融會貫通。下面給出的五步調試法旨在重現他們所熟練掌握的技能,助你在跟蹤bug的問題上形成一種有系統的、且注意力集中的風格。

            第一步:始終如一地重現問題

            不論是什么bug,重要的是,你應當了解如何能夠始終如一地重現它。

            試圖糾正一個隨機出現的bug常會使人感到挫敗,而且通常不過是浪費時間。事實是,幾乎所有的bug都會在特定的情境下可靠地重現,因此發現這個情景和規律就成為你的、或貴公司測試部同仁的工作。

            讓我們舉一個假想的游戲bug為例,在測試員報告里寫道:“有時候,游戲會在玩家殺死敵人時死機(Crash)。”不幸地,像這樣的bug報告太過于含糊,而且由于這個問題看上去不是百分之百會出現的,多數時候玩家仍可以正常地摧毀敵人。因此當游戲crash時,必然還有一些其它相關因素。

            對于不容易重現的bug,理想情形是創建一系列“重現步驟(Repro Steps)”,說明每次應怎樣才能重現bug。例如,下面的步驟極大地改善了之前的bug報告。

            重現步驟:

            1. 開始單人游戲。
            2. 選擇在第44號地圖上進行Skirmish也就是多人練習模式的游戲。
            3. 找到敵人營地。
            4. 在一定的距離開外,使用投射類武器(Projectile Weapon)攻擊在營地里的敵人。
            5. 結果:90%的時候游戲死機。

            顯然,重現步驟是一種很好的方法,測試人員藉此幫助其他人重現bug。不過,精簡可能導致bug發生的事件鏈(Chain of Events)的過程也是至關重要的,其原因有三。第一:對當時bug為何發生提供了有價值的線索。第二:提供了一種比較系統地測試bug是否已被徹底改正的方法。第三:可用于回歸測試,確保bug不再卷土重來。

            盡管這里的信息沒有告訴我們bug的直接誘因,它使我們能夠始終重現bug。一旦你確定了bug發生的環境,你就可以進行下一步驟,開始搜集有用的線索。

            第二步:搜集線索

            現在你能夠可靠地使bug重現,下一步請你戴上偵探的鴨舌帽并搜集線索。每條蛛絲馬跡都是排除一個可能的原因并縮短疑點列表的機會。有了足夠的線索,bug的發源地會變得明顯。因此為了明了每條線索并理解其潛臺詞,付出的努力是值得的。

            不過有一點要注意,你應當總是在心里質疑每一條已發現的線索,是不是誤導的,或不正確的。舉例來說,我們被告知某個bug總發生在爆炸之后。盡管這可能是一條非常重要的線索,但它仍然可能是一個虛假的誤導。時刻準備著放棄那些與收集來的信息沖突的線索。

            還是以上面的bug報告為例,我們了解到游戲的crash發生在玩家使用投射類武器攻擊某個特定的敵人營地的時候。究竟關于投射類武器和從遠處攻擊這兩者,有什么特別之處?這是需要深思的重點,但也不要耗費太多時間思考。親臨其境,觀察錯誤究竟是如何發生的,因為我們需要獲取更多的確鑿的證據,而留連于表面的線索是獲得實際證據最不有效的方式。

            在本例中,當我們進入游戲,并實際觀察錯誤的發生時,我們會發現游戲死機發生在一個“箭”對象里,錯誤的癥狀是一個無效指針。進一步的檢查顯示,該指針本來是應當指向那個發射此箭的角色的。在此情況下,這支箭原本要向其發射者報告它擊中了某個敵人,使發射者為該次成功的攻擊獲得一定的經驗值。但盡管看上去找到了原因所在,我們對真實的潛原因仍然一無所知。我們必須首先找出是什么擾亂了這個指針。

            第三步:查明錯誤的源頭

            當你認為收集到的線索已經夠多時,就到了專注于搜索和查明錯誤的源頭的時候了。有兩個主要方法,第一個方法是先提出關于bug發生原因的假設,接著對該假設進行驗證(或證明它不正確);第二個方法是較為系統的分而治之的方法。

            方法1:假設法

            搜集了足夠的線索,你會開始懷疑有些什么事情導致了bug發生。這就是你的假設(Hypothesis)。當你能夠在心里清楚地陳述這假設,你就可以開始設計一些能驗證該假設,或反證證明該假設不正確的測試用例。

            在我們的例子里,通過測試得出了以下線索和關于游戲設計的信息:

            • 當一支箭射出的時候,該箭被賦予一個指向射箭人的指針。
            • 當一支箭射中某個敵人的時候,將獎勵送給射箭人。
            • 游戲死機發生在一支箭試圖通過一個無效指針向射箭人傳回獎勵。

            我們的第一個假設可能是這樣,指針的值在箭的飛行途中被損壞。基于此種假設,我們開始設計測試,并搜集數據來支持或推翻此原因。例如我們可以讓每一支箭都將射箭人的指針注冊到同一個備份區域。當我們又捕捉到crash時,可以檢查備份下來的數據,看無效指針的值是否與這支箭在被射出的時候所賦予的值相同。  

            不幸的是在我們所舉的例子里,最后發現這條假設是不正確的。備份的指針和導致游戲死機的指針具有相同的值。這樣一來,我們就面臨著一個抉擇。是再提一個假設并進行驗證,還是重頭尋找更多的線索?現在讓我們試著再提一條假設。

            如果箭的發射人指針從沒有被破壞(新線索),或許從箭射出到箭射中敵人的這段時間里,這個發射人被刪除了。為了檢查這點,讓我們記錄下敵人營地里死亡的每個角色的指針。當crash發生時,我們可以將出錯指針和死亡并從內存中刪除的敵人的列表進行比較。這樣進行,很快就證實原因正是如此。射箭人死時,箭還在飛行途中。

            方法2:分治法

            兩個假設使我們找出了bug,同時也表現了分而治之的概念。我們知道指針的值無效,但我們不知道它是因為值被修改過而損壞,或者這個指針在更早些的時候就已經無效。通過測試第一個假設,我們排除了兩個可能性中的一個。像歇洛克·福爾摩斯(Sherlock Holmes)曾說過的:“……當你排除了不可能的情況后,其余的情況,盡管多么不可能,卻必定是真實的。”[譯注:綠玉皇冠案(柯南·道爾)]

            有人將分而治之的方法簡單形容為確定故障發生的時刻,并從輸入開始回溯而發現錯誤。比如有一個并不會造成死機的bug,在某個時刻發生的初始錯誤將影響層層傳遞,最終導致故障發生。確定初始錯誤通常通過在所有輸入分支上設置(有條件或無條件的)斷點(Breakpoint)來進行,直到找到那個不能正常輸出——也就是導致bug的輸入。

            當從故障發生的時刻開始回溯,你在局部變量和棧里面的上級函數中尋找任何異常。對于死機bug來說,通常你會試圖尋找一個空值(NULL)或極大的數字值。如果是關于浮點數的bug,在棧上尋找NAN或極大的數字。

            無論是對問題進行有根據的推測,檢驗假設,還是有系統地搜捕肇事代碼,最終你會找到問題所在。在這個過程中你要相信自己,并保持清醒。本文接下來的部分將詳細討論一些可用于在這步驟中的專門技術。

            第四步:糾正問題

            當我們發現bug的真正根源,接下來要做的便是提出和實現一個解決方案。無論如何,修改必須對項目所處的階段是恰當的。例如,在開發的后期,通常不能只為了糾正一個bug,就修改底部的數據結構或程序體系結構。參照開發工作所處的階段,主程序員或系統架構師將決定應當進行何種類型的修改。在關鍵的時刻,個別工程師(初級或中級)常常做出不好的決定,因為他們沒有全盤考慮。

            此外需要特別注意的是,理想情況下,代碼的編寫者應當負責修改自己代碼里的bug。不過如果必須修改別人的代碼,你至少應當在進行修改前和原作者進行討論。討論將使你了解一些方面,例如在以往對于類似的問題是怎么處理的,如果實施你的方案提議可能會造成什么影響等??傊谖磸氐桌斫庥蓜e人編寫的代碼的上下文前,急于進行修改是非常危險的。

            繼續討論我們的例子,死機源于一個指向了一個不復存在的對象的無效指針。對此類問題模式的一個好的解決方案是使用一層間接引用,使crash不再發生。通常,正是因為這個理由,游戲使用對象的句柄而不是直接指針。這將是一個合理的修改。

            但是,如果游戲項目因為某個里程碑、或一個重要的演示版交付日迫在眉睫,而需要快速完成修改,你可能會傾向于對現有的特殊情況實現一個較為直接的修改方案(例如讓射箭者在自身被刪除的時候使其射出的箭中關于自身的指針失效)。如果在程序里打上了這一類的快速補丁(Quick Hack),你要記得將有關的注釋文檔化,以使其在這截止期限后被重新評估。開發中這樣的情況屢見不鮮:快速補丁被人們遺忘,而在幾個月后才造成了難于發現和解決的麻煩。

            雖然看上去我們發現了bug并且確定了一種修改(使用句柄而非指針),探索其他可能造成同樣問題出現的途徑是很關鍵的。這雖然需要額外的時間,但是為了確保bug從根本上被消滅,而非只是消除了bug的一種表現形式,這努力是值得的。在我們的例子中,可能其他類型的投射類武器同樣會造成游戲死機,但其它非武器對象的關系、甚至角色之間的關系也會受到同一個設計缺陷的影響。應找出所有這些相關的場合,使你的修改方案針對的是問題的核心,而非僅僅是問題的某一種征兆。

            第五步:對所作的修改進行測試

            解決方案實施后,還必須進行測試以確認它的確修補了錯誤。第一步要確保先前有效的重現步驟不會導致bug重現。通常應當讓bug修改者以外的其他人,例如測試員,獨立地確認bug被修復與否。

            第二步還要確保沒有新的bug被引入游戲。你應當讓游戲運行一段可觀的時間,確保所作的修改沒有影響其它部分。這是非常重要的,因為很多時候,尤其是在項目開發周期接近尾聲的時候,為修改bug所作的改動,會導致其他系統出錯。在項目的后期,你還應當讓主程序員或其他開發者來檢視每一個修改,這額外的可靠性檢驗要保證新的修改不會對版本有負面影響。

            高級調試技巧

            如果你遵循以上所述的基本調試步驟,你應能找到并修復大多數bug。不過在你嘗試提出假設、驗證/否決一個候選的原因、或者嘗試找出出錯位置的時候,或許你會愿意考慮下列的技巧。

            分析你的假設

            調試程序的時候要保持心胸開闊是很重要的,而且不要作太多假設。如果你假設某些貌似簡單的東西總是正確的,你可能就過早地縮小了搜索范圍,從而完全錯過了找出真相的機會。舉例來說,不要總是想當然地認為你正在使用最新的軟件或程序庫。檢驗你的假設是否正確常常是值得的。

            將交互和干擾最小化

            有時,多個系統之間會以某種方式交互,這會使調試復雜化。試試看關閉那些你認為和問題無關的子系統(例如,關閉聲音子系統),從而將系統之間的交互降到最低限度。有時候這有助于識別問題,因為原因可能就在你關閉的系統中,這樣你就知道接下來該看那里。

            將隨機性最小化

            通常,bug之所以難于重現,要歸咎于從幀速率和實際隨機數等方面引入的可變性。如果你的游戲沒有采取固定的幀速率,試試看將“在每幀內流逝的時間”鎖定為常量。至于隨機數,可以關閉隨機數發生器,或給它固定的常數作為隨機發生種子,這樣每次運行都會得到同樣的序列。不幸地是,玩家會給游戲帶來無法控制的顯著的隨機性。如果連這玩家帶來的隨機性也必須得到控制,請考慮將玩家的輸入記錄下來,從而能以可預料的方式將輸入記錄直接送入游戲[Dawson01]。

            將復雜的計算拆分成幾步進行

            若某行代碼含有大量計算,或許將這行拆分為多個步驟會有助于識別問題。例如,可能其中的某小段計算產生了類型轉換錯誤,或某個函數并未返回你期望它返回的值,或運算進行的順序并不是你所想的那樣。這也使你能夠檢查每一步中間過程的計算。

            檢查邊界條件

            幾乎我們中的每一個人都曾被經典的“差一錯誤”(Off-by-one)問題折磨過。要檢查算法的邊界條件,特別是在循環結構中。

            分解并行計算

            如果你懷疑程序里的競爭條件(Race Condition,不同的執行順序會產生不同的結果),試試看將代碼改寫為串行的,然后檢查bug是否消失。在線程中,增加額外的延遲,觀察是否問題也隨之變化。問題范圍能縮小——若你能夠確定問題是競爭條件,并通過試驗將問題孤立出來。

            充分利用調試器提供的工具

            明白和懂得如何使用條件斷點、內存watch、寄存器watch、棧,以及匯編級/混合調試。工具能幫你尋找線索和確鑿的證據,這是識別bug的關鍵。

            檢查新近改動的代碼

            調試也可以通過源代碼版本控制來進行,這真是一個令人驚訝的方法。如果你清楚地記得在某個日期前程序還是工作的,但是從某天開始就失靈了,你就可以專注于期間改動過的代碼,從而較快地找到引入缺陷的代碼段。至少,也可以將搜索范圍縮小至某個特定子系統,或某幾個文件。

            另一個利用版本控制的方法是生成游戲在bug出現之前的一個版本。當你看不清問題的時候這尤其有用。將新老版本分別在調試器中運行,將值互相比較,你就可能找出問題的關鍵所在。

            向其他人解釋bug

            常常在你向他人解釋bug的時候,你會追憶起一些步驟,并意識到一些遺漏或忘記檢查的地方。與其他的程序員交流的益處還在于他們可能會精辟地提出別樣的值得檢驗的假設。不要低估和他人交談的作用,也永遠不要羞于尋求他人的建議。你團隊中的同事是你的伙伴,也是你與最有難度的bug戰斗時最精良的武器之一。

            和同事一起調試

            這通常是很合算的,因為每個人在對付bug上都有自己的獨門經驗和策略。你也能學到新的技術,學會從從未嘗試過的角度入手處理bug。讓某人看著你進行調試,這可能是追捕bug最有效的方法之一。

            暫時放下問題

            有的時候,你已經如此接近問題,以至于無法再清楚全面地看待它。試試看改變一下環境,出門閑逛一下。當你放松,再回到問題上,你可能會有新的認識。有時候,當你決定讓自己休息一下時,你的心里下意識地還在思考問題,過后答案就自然浮現了。

            尋求外部的幫助

            獲得幫助有多種很好的途徑。如果是在開發視頻游戲,那么每家游戲機制造商都有一整班的人,他們將在你遇到麻煩的時候協助你。了解他們的聯系方式。三大游戲機制造商現在都提供電話支持、電子郵件支持、和開發者互相幫助的新聞討論組。

            困難的調試情景和模式

            消滅bug常有模式可循。在艱苦的調試情景中,模式是關鍵。在此經驗起了很大作用。如果你曾經見過某個模式,你就可能迅速地找出bug所在。希望下列情景和模式能給你一些方向。

            Bug僅在發布版里出現,調試版則正常

            通常,Bug只出現于發布版(Release Build)中意味著這是數據未初始化,或與代碼優化有關的bug。一般來說,即使你沒有特地編寫進行初始化的代碼,調試版(Debug Build)也會自動將變量初始化為零。而這隱式初始化在發布版中是不存在的,因而出現了bug。

            找出原因的另一個策略是:在調試版里,慢慢地逐一打開優化開關。對每一點優化都進行測試,你可以找到罪魁禍首。例如,在調試版里,函數一般都不是內聯的。但在優化后有些函數自動進行了內聯,有時某個bug就這樣發作了。

            還有一點值得注意的是,在發布版中也可以打開調試符號(Debug Symbol)。這使得在一定程度上(雖然一般并不)對優化過的代碼進行調試成為可能,你甚至可以讓一部分調試系統保持開啟。舉例來說,你可以讓你的異常處理函數在崩潰的現場執行一個全面的堆棧回溯(這需要符號)。這是非常有用的,因為當測試員必須運行優化過的游戲版本的時候,你還是可以回溯程序崩潰。

            在作了一些無害的改動后,bug不見了

            如果bug在一些完全無關的改動(例如添加了一行無害的代碼)后不見了,那么這就像是一個時序問題,或內存覆蓋問題。盡管表面上bug已經消失了,但是實際上可能只是轉移到了代碼的另一個部分。不要錯過這個找出bug的機會。Bug就在那兒,將來遲早有一天它肯定會不知不覺地、狡猾地害你。

            確實具有間歇性的問題

            像前面提過的那樣,許多問題會在合適的環境下穩定地重現。但如果你無法控制環境,那就必須要趁問題抬起它丑陋的小腦袋時抓住問題。這里的關鍵是在捕捉到問題的時候要記下盡可能多的信息,以便隨后可在必要時檢查。機會可不是很多的,因此要充分利用每一次出錯的機會。還有一個有效的技巧就是將程序出錯時收集得到的數據和程序正常時收集的數據進行比較,發現其中差異。

            無法解釋的行為

            有時當你在單步執行代碼的時候,卻發現變量自說自話地被修改了。這種真正怪異的現象通常表示系統或調試器失去了同步。解決方案是試試看“加快清除緩存的頻率”,使系統重獲同步。

            感謝Scott Bilas為清除緩存歸納出如下的“四重”方針。

            • 重試(Retry):清除游戲的當前狀態再運行。
            • 重建(Rebuild):刪除已編譯過的中間對象,并進行徹底的版本重建。
            • 重啟(Reboot):通過硬復位,將你機器里的內存擦除。
            • 重裝(Reinstall):通過重裝,恢復你的工具和操作系統中的文件和設置。

            在這“四重”里,“重建”是最重要的。有時候,編譯器不能正確地識別代碼間的依賴關系,導致受牽連的代碼不能通過編譯。癥狀常常是不可思議的怪異。一次徹底的重建有時就能解決問題。

            處理這些無法解釋的行為的時候,一定要預先猜測調試器會給出何種結果。通過printf函數輸出并檢驗變量的實際值,因為調試器有時候會被迷惑,而無法準確地反映真實的值。

            編譯器內部錯誤

            偶爾你會碰到這種情況,編譯器承認它無法理解你的代碼,從而拋出一個編譯器內部錯誤(Internal Compiler Error)。這些錯誤可能顯示在代碼中存在合法性問題,也可能根本是編譯器軟件自身的問題(例如,超出了內存上限,或無法處理你如同天書一般的模板代碼)。遇到編譯器內部錯誤的時候,建議執行如下步驟:

            1. 進行完整的版本重建。
            2. 重啟電腦,再進行一次完整的版本重建。
            3. 檢查是否正在使用最新版本的編譯器。
            4. 檢查任何正在使用的庫是否是最新版本。
            5. 試驗同樣的代碼是否能在其他電腦上通過編譯。

            如果這些步驟不能解決問題,試試確定究竟是那段代碼引起了錯誤。如果可能的話,用分治法減少編譯到的代碼,直至編譯器內部錯誤消失。當故障的位置已經確定后,檢視這段代碼并保證它看上去沒錯(最好能多請幾個人讀它)。如果代碼看上去的確合理,下一步試著重新組織一下代碼,希望編譯器能報告出更有意義的錯誤信息。最后你還可以嘗試用舊版本的編譯器來編譯。很可能在最新版的編譯器里存在bug,而使用舊版本的編譯器就能順利完成編譯。

            如果這些辦法都不奏效,試試看在網上搜索相似的問題。如果還是沒有用,向編譯器的制造商尋求額外的幫助。

            當你懷疑問題不是出在自己的代碼里

            不象話,應該總是懷疑自己的代碼!不過,如果你確信不是你們的代碼的問題,最好的行動方針是到網站上尋找所使用的函數庫或編譯器的更新補丁。詳細閱讀其readme文件,或者在網上搜索關于此函數庫或編譯器的已知問題。很多時候,其他的人也碰到了相似的問題,解決辦法或補丁也已經有了。

            不過,你發現的bug來自他人提供的函數庫,或來自有故障的硬件(碰巧你是第一個發現它的人)的幾率不大。雖然不太可能,但有時還是會發生的。最快解決方法是編寫一小段例程將問題隔離開來。然后你可以把這段程序email給函數庫的作者,或硬件生產商,以便他們進一步就此問題進行調查。如果這真是其他人造成的bug,由于你的幫助,他人可以快速地識別和重現問題,從而bug以最快速度得到改正。

            理解底層系統

            有時為了找到一些難度很高的bug,你必須了解底層系統。僅僅通曉C或C++還遠遠不夠。為了成為一個優秀的程序員,你必須懂得編譯器是如何實現較高層次的概念,必須懂匯編語言,還必須了解硬件的細節(尤其是對游戲機游戲開發而言)。雖然認為高級語言掩蓋了所有的復雜性并沒有錯,但是事實是當系統崩潰時你會感覺手足無措,除非你的理解深刻至抽象以下。若要進一步討論高層抽象會如何造成隱患,請參見“The Law of Leaky Abstractions”[Spolsky02]。

            那么,有哪些底層細節需要了解呢?就游戲而言,你應當了解如下事項:

            • 了解編譯器實現代碼的原理。熟悉繼承、虛函數調用、調用約定、異常是如何實現的。懂得編譯器如何分配內存和處理內存對齊。
            • 了解你所使用的硬件的細節。例如,懂得與某個特定硬件的高速緩存有關的問題(緩存中的數據何時會和主存儲器中不同)、內存對齊的限制、字節順序(Endianness,高位還是低位字節在前)、棧的大小、類型的大?。ㄈ缯蚷nt、長整型long、布爾型bool)。
            • 了解匯編語言的工作原理,能夠閱讀匯編代碼。這在調試器無法跟蹤源代碼時,例如在優化后的版本里查找問題時,很有幫助。

            如不能牢牢掌握這些知識,在對付真正困難的bug的時候,你的致命弱點就會暴露出來。所以必須理解底層的系統,熟悉其規則。

            增加有助于調試的基礎設施

            沒有合適的工具的幫助,在真空中調試程序必定會很費勁。解決辦法是走另一個極端,直接將好的調試工具整合到游戲里。下列工具能極大地幫助修理bug。

            允許在運行中修改游戲變量

            調試和重現bug時,在運行中修改游戲變量的值的功能是非常有用的。實現此功能的經典界面是通過游戲中的一個調試命令行接口(CLI,Command-Line Interface)用鍵盤修改變量。按下某個鍵后,調試信息覆蓋顯示在游戲屏幕上,提示你用鍵盤進行輸入。例如,當你想把游戲里的天氣改成狂風暴雨,你可以在提示下輸入“weather stormy”。此類界面在調節和檢查變量的值或特定游戲狀態的時候也很好用。

            可視化的AI診斷

            在調試中,好的工具是無價之寶,而標準調試器在診斷AI問題的時候總是那么力不從心。各種調試器雖然在某個具體時刻能給出很好的深度,但在解答AI系統怎樣隨著游戲進行而變化這個問題上完全無用。解決辦法是在游戲里直接構造能夠監控任意角色的診斷數據的可視化版本。通過將文字和3D線條組合起來,一些重要的AI系統如尋路(Pathfinding)、警覺邊界(Awareness Boundaries)、當前目標等,會較容易跟蹤和查錯[Tozour02][Laming03]。

            日志的能力

            通常,我們在游戲里有成堆的角色彼此交互和通訊,以得到非常復雜的行為。當交互失敗,bug出現之時,關鍵在于能夠記錄導致bug的每個角色的個別狀態及事件。通過對每個角色創建單獨的日志,將帶有時戳的關鍵事件記錄下來,我們就可能通過檢查日志來發現錯誤。

            記錄和回放的能力

            像前面提到的那樣,找出bug的關鍵在于可重現性。極致的可重現性需要通過記錄和回放玩家的輸入來實現[Dawson01]。對于那些概率很小的死機bug,記錄和回放是找出確切原因的關鍵工具。但是為了支持記錄和回放,你必須讓游戲的行為是可預料的,也就是說對于同樣的初始狀態,同樣的玩家輸入必定會得到同樣的輸出結果。這并不意味著你的游戲對玩家來說是可以預知的,只是意味著你應當小心處理隨機數的產生[Lecky-Thompson00][Freeman-Hargis03]、初始狀態、輸入等方面,并能在程序崩潰時將輸入序列保存下來[Dawson99]。

            跟蹤存儲分配事件

            這樣實現你的存儲分配算子,使其對每次分配操作都進行全面的棧跟蹤。通過不斷地記錄究竟是誰在申請內存,你將不再有內存泄漏問題需要解決。

            崩潰時打印出盡可能多的信息

            “事后調試(Post-mortem Debug)”是很重要的。程序崩潰時,理想的情況下,你會希望能夠捕捉到調用堆棧、寄存器以及所有其它可能相關的狀態信息。這些信息可以顯示在屏幕上,寫入某個文件,或自動發送至開發者的電子信箱。這一類的工具讓你迅速找出崩潰的源頭,只消幾分鐘而不是幾個小時。尤其是當故障發生在美工或策劃同仁的機器上,而他們并不記得是怎樣觸發這次崩潰的時候。

            對整個團隊進行培訓

            雖然這并非一個能夠編程實現的結構,但是你應當確定團隊正確使用你創建的工具。請他們不要忽視錯誤對話框,確信他們知道怎樣搜集信息從而不會丟失已找到的bug等等?;〞r間來培訓測試員、美工、策劃是值得的。

            預防bug

            關于調試的討論,若沒有一段文字指導如何在第一時間避免bug,便不能算完整。遵照這些指導方針,你或可避免編寫出有bug的代碼,或可在偶然之間發現自己不知不覺寫出來的bug。不論是什么結果,都會最后幫你排除bug。

            將編譯器的警告級別(Warning level)調到最高,并指示將警告當作錯誤處理(Enable warnings as errors)。首先盡可能多地排除警告,最后才用#pragma將剩下的警告關閉掉。有時,自動類型轉換及其它一些警告級的問題會帶來潛在的bug。

            使你的游戲能在多個編譯器上編譯通過。如果你確保游戲用多個編譯器、面向多個平臺都能編譯通過,不同的編譯器之間在警告和錯誤方面的差異將保證你的代碼總體上更可靠。例如,編寫任天堂GameCube™游戲機上的程序的人也可以在Win32下生成一個功能稍弱的版本。這也使你能夠判斷某個bug是否是具體平臺所特有的。

            編寫你自己的內存管理器。這對于游戲機游戲是至關重要的。你必須清楚地知道正在使用那幾塊內存,并對內存上溢進行保護。由于內存溢出會帶來一些最難查處的bug,首先確保不發生溢出是很重要的。在調試版本中使用預留的上溢和下溢保護內存塊能使bug更早地暴露身份。對PC開發者來說,編寫自己的內存管理器不是必須的,因為VC++里的內存系統功能已經很強了,而且還有像SmartHeap之類的好工具可以用來確定內存錯誤。

            用assert來檢驗假設。在函數的開頭加上assert來檢驗關于參數的假設(例如指針非空或范圍檢查)。另外,如果switch語句的default情況不應該被執行到,在其中加上assert。還有,標準assert可以被擴展以得到更好的調試性能[Rabin00b]。例如,讓assert將調用堆棧打印出來是很有用的。

            總是在聲明變量的時候初始化它們。如果你無法在聲明某個變量時賦予它一個有意義的值,那么就給它賦一個將來一眼就能認出它有沒有被初始化過的容易辨認的值。有時候我們會用0xDEADBEEF、0xCDCDCDCD,或直接使用零。

            總是將循環體和if語句體用花括號({})括起來。也就是將你所想的代碼老老實實地包起來,使代碼所實現的功能更直觀。

            變量起名要容易區分彼此。例如,m_objectITime和m_objectJTime看上去幾乎一模一樣。此類問題的典型例子是把“i”和“j”用作循環計數變量。“i”和“j”看上去很相似,你很容易把其中一個誤認為另一個??晒┻x擇的方法是,你可以用“i”和“k”,或者干脆使用更能描述其意義的名字。更多有關變量命名的認知差異的信息可以在[McConnell93]中找到。

            避免在多處重復同樣的代碼。一模一樣的代碼同時出現在幾個不同的地方,這是不利的。如果對其中一處代碼作了改動,其余幾個地方不一定也會被改動。如果看上去重復代碼是必要的,重新考慮一下其核心功能,盡量將大多數的代碼集中到一處。

            避免使用那些中固定寫死的“神奇數”(Magic numbers)。當單獨一個數字出現在代碼中,其意義可能是完全不為人知的。如果沒有寫注釋,就無法讓人理解之所以選擇這個數字的理由,及這個數字代表什么。如果必須使用神奇數,將它們聲明為有字面意義的常量或define。

            測試的時候要注意代碼覆蓋率。在編寫完一段代碼之后,應驗證它的每一個分支都能正確地執行。若其中一個分支從未被執行過,那么很可能其中正潛伏著bug。在測試不同分支的過程中你可能會發現這樣一個bug,即其中某個分支是根本不可能被執行到的。這樣的bug越早發現就越好。

            結論

            本文向你介紹了有效率地調試游戲所需的工具。調試有時候被形容為一門藝術,但那只是由于人們越有經驗就做得越好。當你把五步調試法融會貫通,又學會了識別bug模式,并將自己的調試工具集成到游戲中,再形成自己在調試上的個人風格和絕招,很快地,你將熟練地有系統地追捕到并且消滅最困難的bug。最后再加上一點預防,我想你的游戲開發會一帆風順,一個bug都沒有也說不定。

            致謝

            感謝Scott Bilas和Jack Matthews,他們提了極好的建議,并為本文貢獻了一些個人經驗和智慧。人們看待調試有各自的角度,因此他們的意見在推敲本文建議的時候起了非常大的作用。

            參考文獻

            • [Dawson99] Dawson, Bruce, “Structured Exception Handling,” Game Developer Magazine (Jan 1999), pp. 52–54.
            • [Dawson01] Dawson, Bruce, “Game Input Recording and Playback,” Game Programming Gems 2, Charles River Media, 2001.
            • [Freeman-Hargis03] Freeman-Hargis, James, “The Statistics of Random Numbers,” AI Game Programming Wisdom 2, Charles River Media, 2003.
            • [Laming03] Laming, Brett, “The Art of Surviving a Simulation Title,” AI Game Programming Wisdom 2, Charles River Media, 2003.
            • [Lecky-Thompson00] Lecky-Thompson, Guy, “Predictable Random Numbers,” Game Programming Gems, Charles River Media, 2000.
            • [McConnell93] McConnell, Steve, Code Complete: A Practical Handbook of Software Construction, Microsoft Press, 1993.
            • [Rabin00a] Rabin, Steve, “Designing a General Robust AI Engine,” Game Programming Gems, Charles River Media, 2000.
            • [Rabin00b] Rabin, Steve, “Squeezing More Out of Assert,” Game Programming Gems, Charles River Media, 2000.
            • [Rabin02] Rabin, Steve, “Implementing a State Machine Language,” AI Game Programming Wisdom, Charles River Media, 2000.
            • [Spolsky02] Spolsky, Joel, “The Law of Leaky Abstractions,” Joel on Software, 2002, available online at www.joelonsoftware.com/articles/LeakyAbstractions.html.
            • [Tozour02] Tozour, Paul, “Building an AI Diagnostic Toolset,” AI Game Programming Wisdom, Charles River Media, 2002.

            posted on 2010-04-27 20:48 CrazyDev 閱讀(421) 評論(0)  編輯 收藏 引用 所屬分類: 通用技術

            導航

            統計

            常用鏈接

            留言簿(1)

            隨筆檔案

            文章分類

            文章檔案

            C/C++

            CEGUI

            Friend Bog

            Game Industry

            Lua

            OGRE

            Other

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            亚洲色欲久久久久综合网| 婷婷久久五月天| 新狼窝色AV性久久久久久| 久久久免费观成人影院| 国产91久久综合| 成人午夜精品久久久久久久小说| 久久天天躁狠狠躁夜夜avapp| 亚洲国产成人久久精品99| 国产视频久久| 久久香蕉一级毛片| 久久综合综合久久狠狠狠97色88| 国产精品女同久久久久电影院| 人妻无码αv中文字幕久久 | 久久国产乱子伦免费精品| 99精品久久久久久久婷婷| 久久久久久精品免费免费自慰| 成人综合久久精品色婷婷| 亚洲精品无码久久一线| 狠狠色婷婷久久一区二区三区| 国产产无码乱码精品久久鸭 | av国内精品久久久久影院| 精品久久香蕉国产线看观看亚洲| 久久久91精品国产一区二区三区 | 情人伊人久久综合亚洲| 久久996热精品xxxx| 久久久久久免费视频| 奇米综合四色77777久久| 久久免费高清视频| 理论片午午伦夜理片久久 | 久久夜色精品国产亚洲| 久久亚洲2019中文字幕| 久久精品国产AV一区二区三区 | 91精品国产色综合久久| 久久久久久A亚洲欧洲AV冫| 一本久久a久久精品亚洲| 久久九九有精品国产23百花影院| 久久激情五月丁香伊人| 国产精品无码久久久久久| 色8激情欧美成人久久综合电| 人妻无码中文久久久久专区| 亚洲欧洲久久久精品|