• <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>

            芳草春暉

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

             

            調(diào)試游戲程序的學(xué)問(轉(zhuǎn))

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

            版權(quán)聲明

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

            概述

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

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

            五步調(diào)試法

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

            第一步:始終如一地重現(xiàn)問題

            不論是什么bug,重要的是,你應(yīng)當(dāng)了解如何能夠始終如一地重現(xiàn)它。

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

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

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

            重現(xiàn)步驟:

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

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

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

            第二步:搜集線索

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

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

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

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

            第三步:查明錯誤的源頭

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

            方法1:假設(shè)法

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

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

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

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

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

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

            方法2:分治法

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

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

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

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

            第四步:糾正問題

            當(dāng)我們發(fā)現(xiàn)bug的真正根源,接下來要做的便是提出和實現(xiàn)一個解決方案。無論如何,修改必須對項目所處的階段是恰當(dāng)?shù)摹@纾陂_發(fā)的后期,通常不能只為了糾正一個bug,就修改底部的數(shù)據(jù)結(jié)構(gòu)或程序體系結(jié)構(gòu)。參照開發(fā)工作所處的階段,主程序員或系統(tǒng)架構(gòu)師將決定應(yīng)當(dāng)進行何種類型的修改。在關(guān)鍵的時刻,個別工程師(初級或中級)常常做出不好的決定,因為他們沒有全盤考慮。

            此外需要特別注意的是,理想情況下,代碼的編寫者應(yīng)當(dāng)負責(zé)修改自己代碼里的bug。不過如果必須修改別人的代碼,你至少應(yīng)當(dāng)在進行修改前和原作者進行討論。討論將使你了解一些方面,例如在以往對于類似的問題是怎么處理的,如果實施你的方案提議可能會造成什么影響等。總之,在未徹底理解由別人編寫的代碼的上下文前,急于進行修改是非常危險的。

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

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

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

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

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

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

            高級調(diào)試技巧

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

            分析你的假設(shè)

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

            將交互和干擾最小化

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

            將隨機性最小化

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

            將復(fù)雜的計算拆分成幾步進行

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

            檢查邊界條件

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

            分解并行計算

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

            充分利用調(diào)試器提供的工具

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

            檢查新近改動的代碼

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

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

            向其他人解釋bug

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

            和同事一起調(diào)試

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

            暫時放下問題

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

            尋求外部的幫助

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

            困難的調(diào)試情景和模式

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

            Bug僅在發(fā)布版里出現(xiàn),調(diào)試版則正常

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

            找出原因的另一個策略是:在調(diào)試版里,慢慢地逐一打開優(yōu)化開關(guān)。對每一點優(yōu)化都進行測試,你可以找到罪魁禍?zhǔn)住@纾谡{(diào)試版里,函數(shù)一般都不是內(nèi)聯(lián)的。但在優(yōu)化后有些函數(shù)自動進行了內(nèi)聯(lián),有時某個bug就這樣發(fā)作了。

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

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

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

            確實具有間歇性的問題

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

            無法解釋的行為

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

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

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

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

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

            編譯器內(nèi)部錯誤

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

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

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

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

            當(dāng)你懷疑問題不是出在自己的代碼里

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

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

            理解底層系統(tǒng)

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

            那么,有哪些底層細節(jié)需要了解呢?就游戲而言,你應(yīng)當(dāng)了解如下事項:

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

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

            增加有助于調(diào)試的基礎(chǔ)設(shè)施

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

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

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

            可視化的AI診斷

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

            日志的能力

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

            記錄和回放的能力

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

            跟蹤存儲分配事件

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

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

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

            對整個團隊進行培訓(xùn)

            雖然這并非一個能夠編程實現(xiàn)的結(jié)構(gòu),但是你應(yīng)當(dāng)確定團隊正確使用你創(chuàng)建的工具。請他們不要忽視錯誤對話框,確信他們知道怎樣搜集信息從而不會丟失已找到的bug等等。花時間來培訓(xùn)測試員、美工、策劃是值得的。

            預(yù)防bug

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

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

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

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

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

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

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

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

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

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

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

            結(jié)論

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

            致謝

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

            參考文獻

            • [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 閱讀(428) 評論(0)  編輯 收藏 引用 所屬分類: 通用技術(shù)

            導(dǎo)航

            統(tǒng)計

            常用鏈接

            留言簿(1)

            隨筆檔案

            文章分類

            文章檔案

            C/C++

            CEGUI

            Friend Bog

            Game Industry

            Lua

            OGRE

            Other

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            久久无码AV中文出轨人妻| 狠狠色婷婷久久综合频道日韩| 人妻精品久久久久中文字幕一冢本| 久久久久久伊人高潮影院| 伊人久久大香线蕉AV色婷婷色| 精品久久久久久中文字幕人妻最新| 久久婷婷综合中文字幕| 久久人人爽人人爽人人片AV麻豆| 亚洲国产香蕉人人爽成AV片久久 | 美女久久久久久| 亚洲国产另类久久久精品小说| 久久这里只精品国产99热| 噜噜噜色噜噜噜久久| 亚洲一区中文字幕久久| 久久99精品国产麻豆宅宅| 亚洲国产精品久久66| 人妻精品久久久久中文字幕一冢本| 91性高湖久久久久| 嫩草伊人久久精品少妇AV| 久久有码中文字幕| 2020最新久久久视精品爱| 麻豆AV一区二区三区久久 | 日韩乱码人妻无码中文字幕久久| 亚洲国产精品人久久| 潮喷大喷水系列无码久久精品 | 精品久久久久久无码中文野结衣| 亚洲午夜久久久影院伊人| 亚洲精品美女久久久久99小说| a级毛片无码兔费真人久久| 久久国产乱子伦免费精品| 亚洲AV无一区二区三区久久| 理论片午午伦夜理片久久| 精品久久人人做人人爽综合| 国产99久久九九精品无码| 99久久婷婷国产综合精品草原| 久久99精品国产| 久久久综合九色合综国产| 一本色道久久88加勒比—综合| 久久伊人精品青青草原高清| 99久久www免费人成精品| 精品久久久久久无码中文字幕|