發揮游戲人工智能的最大價值:線程化

>>>  創業先鋒 眾人拾柴火焰高  >>> 簡體     傳統


  文 / Donald Kehoe

  之前的所有本系列文章一直都在為本文奠定基礎。我們希望,您現在能夠清楚地了解到游戲人工智能 (AI) 是什么,并知道如何將其用于您的游戲中。如今,極具挑戰性的任務是,最大程度地提升您系統的性能。

  無論您的系統有多好,但若它拖慢游戲速度,則一切毫無意義。高效的編程和優化竅門所起作用比較有限;若您的目標超出了單內核的限制,那么您只能進行并行處理(見圖 1)。


  圖 1:在 Blizzard Entertainment 的《星際爭霸 2》* 中,大量單元在同一時間運行自己的人工智能。多線程是最佳的處理方法。

  若進行處理時所用的系統具有一顆以上處理器或一顆多核處理器,您可將工作分配給多顆處理器。有兩種分配方式:任務并行化和數據并行化。

  任務并行化

  將您的應用進行多線程化處理的最輕松方法是,將它分解為多個特定的任務(見圖 2)。我們通常只使用幾種方法對組成游戲引擎的不同任務進行封裝,以便其它系統可與它們進行通信。


  圖 2:功能并行化讓每個子系統利用各自的線程和內核。遺憾的是,具有多個內核而非任務的系統并未得到充分利用。

  一個最好的例子是,該游戲引擎的音頻系統。音頻不需要與其它系統進行交互。它只是按照命令進行工作,即按需播音和混音。通信功能是用于播放和停止聲音的調用,可使音頻自動、完美地適用于功能并行化。使用線程分析工具為該工作提供幫助,并通過您想在其自己的線程中運行的代碼段之前和之后進行的調用,音頻系統可被分解成自己的線程。

  讓我們來了解下,您的人工智能系統如何運用該功能并行化。根據您的游戲需要,您可能會有很多不同的任務,您可為它們提供自身線程。我們來了解下其中三種任務:路徑查找、戰略人工智能和實體系統本身。

  路徑查找

  您可以以這樣的方式實施您的路徑查找系統,即每個搜索路徑的實體可隨時按需調用自己的路徑。盡管本方法會起作用,但它意味著,每當有路徑被請求時,引擎就要等待路徑查找器。若您將路徑查找重組為自己的系統,您可將它分解成它自己的線程。路徑查找器將會像資源管理器那樣運行,在資源管理器中,新資源就是路徑。

  想查找路徑的任何實體都可發出路徑查找請求,然后再立刻從路徑查找器取回一個“搜查令 (ticket)”。該搜查令就是一種路徑查找系統用來查找路徑的特殊句柄。這時實體可繼續運行直至游戲循環到下一幀。實體可查驗搜查令是否有效。若有效,實體找回路徑;否則,它可在繼續等待時繼續實施所需的任何操作。

  在路徑查找系統中,搜查令用于記錄跟蹤路徑請求,而系統則對路徑請求進行處理,不必担心系統性能會受到影響。該類系統有一種積極效果,即自動跟蹤發現的路徑。所以,當收到對先前所發現的路徑的請求時,路徑查找器可為現有路徑提供搜查令。在任何具有很多實體路徑的系統中,該方法都非常高效,因為任何路徑一被找到就可能會被再次需要。

  戰略人工智能

  如上篇文章所述,全面管理游戲的人工智能系統與自身的線程匹配完美。它能分析游戲場并給不同實體發送命令,當實體靠近游戲場時,它可分析命令。

  在自身線程中的實體系統將很難為決策圖搜集信息。這些發現可發送到戰略人工智能系統,作為對決策圖的最新請求。當戰略人工智能更新時,它可解析這些請求,更新該決策圖并進行判斷調用。無論這兩個系統(戰略人工智能和實體)是否同步都是可以的:即使不同步,它們也不會影響人工智能的決策。(我們是在談論 1/60 秒,玩家不會注意到人工智能反應中的單幀延遲。)

  數據并行化

  數據并行化非常優秀,并利用了具有多內核的系統。但是有一個很大的弊端:功能并行化可能并未充分利用所有的可用內核。當您擁有的內核數量多于任務時,您的程序將不再應用所有的可用處理能力(除非運行程序的平臺對它進行了處理,但最好不要依賴專用于通用應用的功能)。輸入數據并行化(見圖 3)。


  圖 3:數據并行化,某單一功能可利用所有可用內核。


  在功能并行化中,您利用一套完整的自動單元,并為它提供和它一起運行的自身線程。現在,您要將單一工作進行分解,并將其分配給不同線程來完成。這樣做可達到與系統內核一起按比例擴大的優點。您有帶八內核的系統?很好。有 64 位的嗎?為什么沒有?雖然功能并行化支持您先指定要線程化的代碼段,再讓其自由運行,但數據并行化可能還需一點額外工作以實現流暢運行。例如,您可能會使用內核線程(一種“主”線程),它可跟蹤誰在處理什么任務。子線程將需向主線程請求“工作”,以確保避免將同一任務執行兩次。

  實際上,使用內核線程管理數據并行化是一種混合方法。內核線程正使用功能并行化,然后該線程在用于數據并行化的不同內核之間對數據進行分解。

  實施

  線程工具如 OpenMP* (在大部分操作系統中免費提供),可助您比過去更輕松地將代碼分解成不同線程。只用編譯指令標記可被分解的代碼段,用 OpenMP 處理其余的代碼段。要用工作模塊分解事物,您只需把線程調用放入遍歷所述資源的循環中。

  在路徑查找系統示例中,路徑查找器會保存請求路徑列表。然后它會依次遍歷該列表并根據各個請求運行實際的路徑查找功能,從而將它們保存在路徑列表中。可將該循環線程化,從而將循環的每次迭代分解成不同的線程。這些線程將會在第一可用內核中運行,支持最大程度地利用可用的處理能力。當無任務可執行時,內核才會空閑。

  用于多個請求以實施相同工作的那些系統潛力巨大。當這些請求被間隔開時,路徑查找器會自動查驗請求是否已得到處理。對于數據并行化,很可能會在同一時間發生對同一路徑的多個請求,這會導致冗余,破壞整個線程點。

  為解決該冗余及其它冗余問題,所述系統需跟蹤記錄正在進行的任務都有哪些,而且只有在它們完成之后才能把它們從請求隊列中除去。所以,若收到的請求是針對已請求過的路徑,就需要先進行檢查,然后再返回到搜查令指定的現有路徑。

  不能隨便生成新線程。該過程將涉及對操作系統 (OS) 的系統調用。當該操作系統要完成它時,它會完成所需代碼,并創建線程。這可能會花費很多時間(相對于處理速度來說)。這就是為何我們不想生成更多線程。如果正在請求的工作已得到處理,那么切勿運行該任務。而且,如果該任務非常簡單(例如從兩個非常近的點位之間查找路徑),則不值得對其進行過細分解。

  下面介紹了路徑查找功能線程將要進行的分解,并介紹了它如何將工作分解成數據線程:

  請求路徑(開始,目標)。從路徑查找器外部調用該功能,以得到一條線程。該功能:

  遍歷完整的請求列表,確定該路徑(或其相似路徑)是否已被找到,然后再返回用于該路徑的搜查令。

  (若路徑還未被找到)遍歷主動請求列表以查找該路徑;若該路徑在該列表中,則該功能返回用于該待處理路徑的搜查令。

  生成一條新請求并返回新搜查令(若以上方法均無效)。

  檢查路徑(“搜查令”)。通過使用該搜查令,該功能遍歷完整的請求列表,并查找路徑,其中該搜查令對該路徑有效。它能夠返回路徑是否存在。

  更新路徑查找器()。這是 shepherd 功能,可處理路徑查找線程的費用。該功能可執行以下任務:

  解析新請求。相同路徑的多個請求可通過不同內核同時生成。該段刪除了冗余,并將多個搜查令(來自不同請求)分配至相同請求。

  通過主動請求循環。該功能支持所有主動請求并對它們進行線程處理。每個循環開始和結束時,代碼標記為線程。每個線程將會 (1) 查找請求的路徑,(2) 借助分配的搜查令將它保存在完成的路徑列表中,以及 (3) 將任務從主動列表中刪除。

  解決沖突

  您可能已注意到這一設置可能造成災難性后果。所有需要寫入請求隊列的不同線程,或所有需要添加某些內容至已完成“樁”的數據線程,會導致寫入沖突,即一個線程將某內容寫入插槽 A,而另一個線程將其他內容同時寫入插槽 A。該沖突會導致眾所周知的“競態條件”。

  為避免寫入沖突,代碼段可標記為“關鍵”。當某內容標記為關鍵時,一次只有一個線程能夠訪問該代碼段:所有想要執行相同操作(訪問相同內容)的其他線程都需要等待。當多個線程相互阻止訪問內容時,該行為會導致嚴重問題,如崩潰。該設置可切實避免崩潰。線程工作完成后,方便時可訪問內存段,且無需關聯其他線程可能需要的其他段。

  保持系統同步

  因此,您讓所有的單獨人工智能子系統實現了自主,使它們能夠隨意使用找到的所有可用計算資源。運行速度極快,但它們失去控制了嗎?

  游戲需要提供結構化用戶體驗。引擎必須能夠保持系統同步。您不能讓部分游戲元素先于其他元素運行 1-2 個幀。當敵人開始行動時,您不能讓部隊無所事事地等待路徑。好父母需要公平對待子女。

  主游戲引擎循環負責兩類操作:渲染和更新。串行編程可幫助輕松保持相關系統同步。首先所有更新執行,然后渲染可繪制更新內容。此外,相關信息可能不易理解。

  最終,移動更新(常常基于軌跡)的運行速度可能比渲染器快幾幀。結果,動畫會出現跳動情況,即實體可能呈現跳動和加速移動的情況。路徑查找可能考慮實體位置快照,并可能運行無效數據。

  各種系統的同步解決方案具有出色的簡單性。事實上,它可應用于多數引擎。當主游戲循環得到更新時,它可跟蹤全局時間索引。所有的線程將只需要處理當前(和過去,而非未來)的時間索引更新。

  當有關當前時間索引的指定任務的工作全部完成,線程可處于睡眠狀態,直到新的時間索引生成。這一行為不僅可幫助確保系統相互同步,而且可確保線程不會使用多余的內核。移動工作能夠完美處理不斷出現的碰撞和移動軌跡,并能在提前完成時共享處理能力。再次強調,您能夠充分利用可用內核。

  線程指南

  以下是大家在設計多線程系統時需要了解的一些事情:

  功能并行化:用于系統可以自主運行的情況。一些功能需要配置在系統中以解決沖突和冗余問題。

  數據并行化:

  用于實施批量操作的情況。

  設計原則是確保回寫保持最少程度,并在流程結束時發生。

  對最新信息(可在其他線程中編輯)的依賴度最低,或避免這種依賴。(我的戰略信息過時 1/60 秒會怎么樣?)

  確保您在工作時對于另一個系統的使用需求不會阻妨礙線程運行:“請求路徑”,“檢查路徑”,而非“獲取路徑”。

  線程失效時

  有時,線程可能無法正常運行。借助 OpenMP 等工具,您可以輕松調整系統將即時把工作分解為線程的數量。借助英特爾® VTuneTM 性能分析器和英特爾® 線程調節器, 您能夠清晰了解系統在不同并行化級別中的有效性。在下面這些案例中,您可能希望避免線程操作,然而:

  極度復雜的系統。如果子系統關聯過多其他系統,子系統或其他系統常常需要等待,此時,線程操作可能無濟于事。此外,系統本身可能獲益于重新設計。

  原子工作負載。如果子系統的工作無法分解,您可能無法進行并行化處理。音頻混合任務可能與線程一樣可以發揮重要作用,但該任務工作需要將多種聲音混合至最終傳輸至揚聲器的頻道。如果您的系統在混合之前對單個音頻數據塊進行了計算,那么它可能對其進行線程處理。

  高昂費用。這些系統中部分需要實施額外的工作(如路徑查找)。如果線程帶來的效益不足以彌補相關費用,那么可能需要避免實施或禁用線程處理。這可能適用于具有較少元素(實體、路徑等)的系統。

  重復代碼。在某些情況下,多個線程會使用相同代碼,結果卻造成代碼部分浪費或被忽略。在工作開始之前,冗余檢查一般能避免這種情況。

  多顆處理器和多核處理器(和多顆多核處理器)能夠顯著增強線程處理的重要性。任何程序員的目標是充分利用可用的處理能力。系統人工智能愿景受到硬件發展現狀限制可以理解,但不能因為硬件未得到充分利用而停滯不前。借助能夠簡化線程實施的現代工具,您完全應該設計支持線程處理的代碼。

  總結

  開發有趣的、可高效運行的動態人工智能系統非常簡單。首先應該提升效率和實施優化。通過合理組織系統以充分利用任務和數據并行化,您能夠幫助確保系統實現最大運行速度,并能夠隨著標配計算內核的日益增多不斷擴展以滿足行業需求。

  如本系列文章所述,游戲人工智能的人工特征比智能特征更為顯著。程序員有責任創建系統代理功能,以模擬真實對手的行為。從低級規則和路徑查找到高級戰術與戰略人工智能,基本組件并不會過度復雜。

英特爾開發人員專區 授權


GameRes游資網 2015-08-23 08:54:54

[新一篇] 博弈論與戰斗系統的核心規則設計探討

[舊一篇] 內容營銷如何植入廣告
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表