发挥游戏人工智能的最大价值:线程化
文 / Donald Kehoe
之前的所有本系列文章一直都在為本文奠定基礎。我們希望,您現(xiàn)在能夠清楚地了解到游戲人工智能 (AI) 是什么,并知道如何將其用于您的游戲中。如今,極具挑戰(zhàn)性的任務是,最大程度地提升您系統(tǒng)的性能。
無論您的系統(tǒng)有多好,但若它拖慢游戲速度,則一切毫無意義。高效的編程和優(yōu)化竅門所起作用比較有限;若您的目標超出了單內(nèi)核的限制,那么您只能進行并行處理(見圖 1)。
?
圖 1:在 Blizzard Entertainment 的《星際爭霸 2》* 中,大量單元在同一時間運行自己的人工智能。多線程是最佳的處理方法。
若進行處理時所用的系統(tǒng)具有一顆以上處理器或一顆多核處理器,您可將工作分配給多顆處理器。有兩種分配方式:任務并行化和數(shù)據(jù)并行化。
任務并行化
將您的應用進行多線程化處理的最輕松方法是,將它分解為多個特定的任務(見圖 2)。我們通常只使用幾種方法對組成游戲引擎的不同任務進行封裝,以便其它系統(tǒng)可與它們進行通信。
?
圖 2:功能并行化讓每個子系統(tǒng)利用各自的線程和內(nèi)核。遺憾的是,具有多個內(nèi)核而非任務的系統(tǒng)并未得到充分利用。
一個最好的例子是,該游戲引擎的音頻系統(tǒng)。音頻不需要與其它系統(tǒng)進行交互。它只是按照命令進行工作,即按需播音和混音。通信功能是用于播放和停止聲音的調用,可使音頻自動、完美地適用于功能并行化。使用線程分析工具為該工作提供幫助,并通過您想在其自己的線程中運行的代碼段之前和之后進行的調用,音頻系統(tǒng)可被分解成自己的線程。
讓我們來了解下,您的人工智能系統(tǒng)如何運用該功能并行化。根據(jù)您的游戲需要,您可能會有很多不同的任務,您可為它們提供自身線程。我們來了解下其中三種任務:路徑查找、戰(zhàn)略人工智能和實體系統(tǒng)本身。
路徑查找
您可以以這樣的方式實施您的路徑查找系統(tǒng),即每個搜索路徑的實體可隨時按需調用自己的路徑。盡管本方法會起作用,但它意味著,每當有路徑被請求時,引擎就要等待路徑查找器。若您將路徑查找重組為自己的系統(tǒng),您可將它分解成它自己的線程。路徑查找器將會像資源管理器那樣運行,在資源管理器中,新資源就是路徑。
想查找路徑的任何實體都可發(fā)出路徑查找請求,然后再立刻從路徑查找器取回一個“搜查令 (ticket)”。該搜查令就是一種路徑查找系統(tǒng)用來查找路徑的特殊句柄。這時實體可繼續(xù)運行直至游戲循環(huán)到下一幀。實體可查驗搜查令是否有效。若有效,實體找回路徑;否則,它可在繼續(xù)等待時繼續(xù)實施所需的任何操作。
在路徑查找系統(tǒng)中,搜查令用于記錄跟蹤路徑請求,而系統(tǒng)則對路徑請求進行處理,不必擔心系統(tǒng)性能會受到影響。該類系統(tǒng)有一種積極效果,即自動跟蹤發(fā)現(xiàn)的路徑。所以,當收到對先前所發(fā)現(xiàn)的路徑的請求時,路徑查找器可為現(xiàn)有路徑提供搜查令。二手手機買賣在任何具有很多實體路徑的系統(tǒng)中,該方法都非常高效,因為任何路徑一被找到就可能會被再次需要。
戰(zhàn)略人工智能
如上篇文章所述,全面管理游戲的人工智能系統(tǒng)與自身的線程匹配完美。它能分析游戲場并給不同實體發(fā)送命令,當實體靠近游戲場時,它可分析命令。
在自身線程中的實體系統(tǒng)將很難為決策圖搜集信息。這些發(fā)現(xiàn)可發(fā)送到戰(zhàn)略人工智能系統(tǒng),作為對決策圖的最新請求。當戰(zhàn)略人工智能更新時,它可解析這些請求,更新該決策圖并進行判斷調用。無論這兩個系統(tǒng)(戰(zhàn)略人工智能和實體)是否同步都是可以的:即使不同步,它們也不會影響人工智能的決策。(我們是在談論 1/60 秒,玩家不會注意到人工智能反應中的單幀延遲。)
數(shù)據(jù)并行化
數(shù)據(jù)并行化非常優(yōu)秀,并利用了具有多內(nèi)核的系統(tǒng)。但是有一個很大的弊端:功能并行化可能并未充分利用所有的可用內(nèi)核。當您擁有的內(nèi)核數(shù)量多于任務時,您的程序將不再應用所有的可用處理能力(除非運行程序的平臺對它進行了處理,但最好不要依賴專用于通用應用的功能)。輸入數(shù)據(jù)并行化(見圖 3)。
在功能并行化中,您利用一套完整的自動單元,并為它提供和它一起運行的自身線程。現(xiàn)在,您要將單一工作進行分解,并將其分配給不同線程來完成。這樣做可達到與系統(tǒng)內(nèi)核一起按比例擴大的優(yōu)點。您有帶八內(nèi)核的系統(tǒng)?很好。有 64 位的嗎?為什么沒有?雖然功能并行化支持您先指定要線程化的代碼段,再讓其自由運行,但數(shù)據(jù)并行化可能還需一點額外工作以實現(xiàn)流暢運行。例如,您可能會使用內(nèi)核線程(一種“主”線程),它可跟蹤誰在處理什么任務。子線程將需向主線程請求“工作”,以確保避免將同一任務執(zhí)行兩次。
實際上,使用內(nèi)核線程管理數(shù)據(jù)并行化是一種混合方法。內(nèi)核線程正使用功能并行化,然后該線程在用于數(shù)據(jù)并行化的不同內(nèi)核之間對數(shù)據(jù)進行分解。
實施
線程工具如 OpenMP* (在大部分操作系統(tǒng)中免費提供),可助您比過去更輕松地將代碼分解成不同線程。只用編譯指令標記可被分解的代碼段,用 OpenMP 處理其余的代碼段。要用工作模塊分解事物,您只需把線程調用放入遍歷所述資源的循環(huán)中。
在路徑查找系統(tǒng)示例中,路徑查找器會保存請求路徑列表。然后它會依次遍歷該列表并根據(jù)各個請求運行實際的路徑查找功能,從而將它們保存在路徑列表中。可將該循環(huán)線程化,從而將循環(huán)的每次迭代分解成不同的線程。這些線程將會在第一可用內(nèi)核中運行,支持最大程度地利用可用的處理能力。當無任務可執(zhí)行時,內(nèi)核才會空閑。
用于多個請求以實施相同工作的那些系統(tǒng)潛力巨大。當這些請求被間隔開時,路徑查找器會自動查驗請求是否已得到處理。對于數(shù)據(jù)并行化,很可能會在同一時間發(fā)生對同一路徑的多個請求,這會導致冗余,破壞整個線程點。
為解決該冗余及其它冗余問題,所述系統(tǒng)需跟蹤記錄正在進行的任務都有哪些,而且只有在它們完成之后才能把它們從請求隊列中除去。所以,若收到的請求是針對已請求過的路徑,就需要先進行檢查,然后再返回到搜查令指定的現(xiàn)有路徑。
不能隨便生成新線程。該過程將涉及對操作系統(tǒng) (OS) 的系統(tǒng)調用。當該操作系統(tǒng)要完成它時,它會完成所需代碼,并創(chuàng)建線程。這可能會花費很多時間(相對于處理速度來說)。這就是為何我們不想生成更多線程。如果正在請求的工作已得到處理,那么切勿運行該任務。而且,如果該任務非常簡單(例如從兩個非常近的點位之間查找路徑),則不值得對其進行過細分解。
下面介紹了路徑查找功能線程將要進行的分解,并介紹了它如何將工作分解成數(shù)據(jù)線程:
請求路徑(開始,目標)。從路徑查找器外部調用該功能,以得到一條線程。該功能:
遍歷完整的請求列表,確定該路徑(或其相似路徑)是否已被找到,然后再返回用于該路徑的搜查令。
(若路徑還未被找到)遍歷主動請求列表以查找該路徑;若該路徑在該列表中,則該功能返回用于該待處理路徑的搜查令。
生成一條新請求并返回新搜查令(若以上方法均無效)。
檢查路徑(“搜查令”)。通過使用該搜查令,該功能遍歷完整的請求列表,并查找路徑,其中該搜查令對該路徑有效。它能夠返回路徑是否存在。
更新路徑查找器()。這是 shepherd 功能,可處理路徑查找線程的費用。該功能可執(zhí)行以下任務:
解析新請求。相同路徑的多個請求可通過不同內(nèi)核同時生成。該段刪除了冗余,并將多個搜查令(來自不同請求)分配至相同請求。
通過主動請求循環(huán)。該功能支持所有主動請求并對它們進行線程處理。每個循環(huán)開始和結束時,代碼標記為線程。每個線程將會 (1) 查找請求的路徑,(2) 借助分配的搜查令將它保存在完成的路徑列表中,以及 (3) 將任務從主動列表中刪除。
解決沖突
您可能已注意到這一設置可能造成災難性后果。所有需要寫入請求隊列的不同線程,或所有需要添加某些內(nèi)容至已完成“樁”的數(shù)據(jù)線程,會導致寫入沖突,即一個線程將某內(nèi)容寫入插槽 A,而另一個線程將其他內(nèi)容同時寫入插槽 A。該沖突會導致眾所周知的“競態(tài)條件”。
為避免寫入沖突,代碼段可標記為“關鍵”。當某內(nèi)容標記為關鍵時,一次只有一個線程能夠訪問該代碼段:所有想要執(zhí)行相同操作(訪問相同內(nèi)容)的其他線程都需要等待。當多個線程相互阻止訪問內(nèi)容時,該行為會導致嚴重問題,如崩潰。該設置可切實避免崩潰。線程工作完成后,方便時可訪問內(nèi)存段,且無需關聯(lián)其他線程可能需要的其他段。
保持系統(tǒng)同步
因此,您讓所有的單獨人工智能子系統(tǒng)實現(xiàn)了自主,使它們能夠隨意使用找到的所有可用計算資源。運行速度極快,但它們失去控制了嗎?
游戲需要提供結構化用戶體驗。引擎必須能夠保持系統(tǒng)同步。您不能讓部分游戲元素先于其他元素運行 1-2 個幀。當敵人開始行動時,您不能讓部隊無所事事地等待路徑。好父母需要公平對待子女。
主游戲引擎循環(huán)負責兩類操作:渲染和更新。串行編程可幫助輕松保持相關系統(tǒng)同步。首先所有更新執(zhí)行,然后渲染可繪制更新內(nèi)容。此外,相關信息可能不易理解。
最終,移動更新(常常基于軌跡)的運行速度可能比渲染器快幾幀。結果,動畫會出現(xiàn)跳動情況,即實體可能呈現(xiàn)跳動和加速移動的情況。路徑查找可能考慮實體位置快照,并可能運行無效數(shù)據(jù)。
各種系統(tǒng)的同步解決方案具有出色的簡單性。事實上,它可應用于多數(shù)引擎。當主游戲循環(huán)得到更新時,它可跟蹤全局時間索引。所有的線程將只需要處理當前(和過去,而非未來)的時間索引更新。
當有關當前時間索引的指定任務的工作全部完成,線程可處于睡眠狀態(tài),直到新的時間索引生成。這一行為不僅可幫助確保系統(tǒng)相互同步,而且可確保線程不會使用多余的內(nèi)核。移動工作能夠完美處理不斷出現(xiàn)的碰撞和移動軌跡,并能在提前完成時共享處理能力。再次強調,您能夠充分利用可用內(nèi)核。
線程指南
以下是大家在設計多線程系統(tǒng)時需要了解的一些事情:
功能并行化:用于系統(tǒng)可以自主運行的情況。一些功能需要配置在系統(tǒng)中以解決沖突和冗余問題。
數(shù)據(jù)并行化:
用于實施批量操作的情況。
設計原則是確保回寫保持最少程度,并在流程結束時發(fā)生。
對最新信息(可在其他線程中編輯)的依賴度最低,或避免這種依賴。(我的戰(zhàn)略信息過時 1/60 秒會怎么樣?)
確保您在工作時對于另一個系統(tǒng)的使用需求不會阻妨礙線程運行:“請求路徑”,“檢查路徑”,而非“獲取路徑”。
線程失效時
有時,線程可能無法正常運行。借助 OpenMP 等工具,您可以輕松調整系統(tǒng)將即時把工作分解為線程的數(shù)量。借助英特爾? VTuneTM 性能分析器和英特爾? 線程調節(jié)器, 您能夠清晰了解系統(tǒng)在不同并行化級別中的有效性。在下面這些案例中,您可能希望避免線程操作,然而:
極度復雜的系統(tǒng)。如果子系統(tǒng)關聯(lián)過多其他系統(tǒng),子系統(tǒng)或其他系統(tǒng)常常需要等待,此時,線程操作可能無濟于事。此外,系統(tǒng)本身可能獲益于重新設計。
原子工作負載。如果子系統(tǒng)的工作無法分解,您可能無法進行并行化處理。音頻混合任務可能與線程一樣可以發(fā)揮重要作用,但該任務工作需要將多種聲音混合至最終傳輸至揚聲器的頻道。如果您的系統(tǒng)在混合之前對單個音頻數(shù)據(jù)塊進行了計算,那么它可能對其進行線程處理。
高昂費用。這些系統(tǒng)中部分需要實施額外的工作(如路徑查找)。如果線程帶來的效益不足以彌補相關費用,那么可能需要避免實施或禁用線程處理。這可能適用于具有較少元素(實體、路徑等)的系統(tǒng)。
重復代碼。在某些情況下,多個線程會使用相同代碼,結果卻造成代碼部分浪費或被忽略。在工作開始之前,冗余檢查一般能避免這種情況。
多顆處理器和多核處理器(和多顆多核處理器)能夠顯著增強線程處理的重要性。任何程序員的目標是充分利用可用的處理能力。系統(tǒng)人工智能愿景受到硬件發(fā)展現(xiàn)狀限制可以理解,但不能因為硬件未得到充分利用而停滯不前。借助能夠簡化線程實施的現(xiàn)代工具,您完全應該設計支持線程處理的代碼。
總結
開發(fā)有趣的、可高效運行的動態(tài)人工智能系統(tǒng)非常簡單。首先應該提升效率和實施優(yōu)化。通過合理組織系統(tǒng)以充分利用任務和數(shù)據(jù)并行化,您能夠幫助確保系統(tǒng)實現(xiàn)最大運行速度,并能夠隨著標配計算內(nèi)核的日益增多不斷擴展以滿足行業(yè)需求。
如本系列文章所述,游戲人工智能的人工特征比智能特征更為顯著。程序員有責任創(chuàng)建系統(tǒng)代理功能,以模擬真實對手的行為。從低級規(guī)則和路徑查找到高級戰(zhàn)術與戰(zhàn)略人工智能,基本組件并不會過度復雜。
總結
以上是生活随笔為你收集整理的发挥游戏人工智能的最大价值:线程化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏开发中 UI 是由谁来完成的?
- 下一篇: 基于组块设计执行开放世界等距游戏引擎