文/陳杰

作者聯(lián)系方式:bitcowboy@gmail.com,同時,作者也是U Sparkle活動參與者哦,UWA歡迎更多開發(fā)朋友加入U Sparkle開發(fā)者計劃,這個舞臺有你更精彩!

譯者注:隨著國內游戲研發(fā)水平的不斷提高,對畫面品質的不斷提升,同時大量手游使用Unity和Unreal 4等成熟的工具開發(fā),動作狀態(tài)機已經(jīng)不是什么陌生的概念了。我們在項目開發(fā)時也大量使用了動作狀態(tài)機。但是隨著游戲規(guī)模變大,我們也踩了很多動作狀態(tài)機的坑。

前段時間在《Game AI Pro 2》這本書上看到這篇(其實和游戲AI沒啥關系的)文章,深受啟發(fā)。在把這篇文章推給同事們看的過程中,還是不可避免的會遇到語言障礙(不是看不懂,而是大多數(shù)國人閱讀英文的速度會比閱讀中文慢,而大家又實在是很忙),所以就突發(fā)奇想決定把它翻譯成中文。由于并不是專業(yè)翻譯,也只能平時抽空做這件事,所以前前后后花了好多天時間,翻譯的過程也經(jīng)常被打斷,導致可能文章中會有一些對術語前后翻譯不一致的情況。在不影響理解的情況下,還請大家海涵,歡迎大家留言討論。

序言

為了在如今的游戲中創(chuàng)建出令人信服的角色,我們有兩個前提:
?

  • 首先角色需要能做出正確的決定——AI;
  • 其次他們在將決定付諸實施的過程中還要有好的表現(xiàn)——動畫(Animation)。

?


在當今游戲非常重視視覺表現(xiàn)的情況下,可以說一個AI系統(tǒng)的成敗也建立在是否有一個高質量的動畫系統(tǒng)基礎上。如果Animation系統(tǒng)不能表現(xiàn)出很好的視覺效果,那么AI系統(tǒng)是否做出了聰明的決定都顯得不那么重要了。

雖然我們已經(jīng)在之前的游戲中提升了Animation系統(tǒng)的還原度,但是卻付出了巨大的工作量。這些工作量不僅包含了大量的動畫數(shù)據(jù),也包含大量用來關聯(lián)動畫的結構數(shù)據(jù),以及相應的控制和驅動代碼。如今我們面臨最大的挑戰(zhàn)簡單來說就一個——復雜度。我們如何能有效的管理、利用和維護這些新產生的內容?

我們覺得傳統(tǒng)的技術在處理上一代游戲數(shù)據(jù)量的時候就已經(jīng)達到極限了。這一代主機和上一代主機相比,不管是內存容量還是玩家的期望值都經(jīng)歷了指數(shù)級的增長。我們沒有理由懷疑游戲容量上也會有類似的增長,因為,我們需要花點時間來重新考量和調整我們的工作流程及軟件架構來更好得消化內容和復雜度上的增長。

基于我們在開發(fā)上一代和這一代游戲中獲取的經(jīng)驗,本文試圖提出一種軟件架構來管理現(xiàn)代動畫系統(tǒng)的復雜性。

一、動畫圖(Animation Graphs)

在我們討論更高層的架構前,我們先來快速回顧一下現(xiàn)代的Animation系統(tǒng)。動畫圖(Animation Graphs,animgraphs)在業(yè)內被廣泛用于表示在游戲內一組Animation是如何被關聯(lián)起來完成一個行為的。

簡單來說,Animgraph就是一張有向無環(huán)圖。圖中的葉子節(jié)點代表了動畫源文件,而分支節(jié)點則是對動畫的操作(例如混合,Blending)。因為主要表述了一組動畫源文件如何混合在一起,所以我們通常把這類動畫圖又叫做混合樹(Blend Tree)。Blend Tree中的操作通常通過控制參數(shù)(Control Parameter)來驅動。例如,將兩個Animation混合需要一個“混合權重”控制參數(shù)來指定每個Animation對最終混合結果的貢獻度。這些控制參數(shù)是我們用來控制(或驅動)Blend Tree的主要手段,此外還有動畫事件(Animation Events)。

Animation Events是打在動畫源文件上的時間附加信息。它提供了關于Animation自己和系統(tǒng)關聯(lián)的上下文信息。舉例來說,在一個走路的動畫中,我們可能想標識出左腳或右腳落地的時刻,好讓游戲觸發(fā)相應的腳步聲。Animation Events從各個源文件中采樣然后沿著圖向上傳遞直到根節(jié)點。在此過程中,這些Animation Events也能被分支節(jié)點用作判斷條件,尤其是在狀態(tài)機的狀態(tài)轉換中(Transitions)。一個用于表述角色向前移動的簡單Blend Tree如圖12.1所示。圖中,我們能通過“Direction”和“Speed”兩個控制參數(shù)來控制角色的方向和速度。

110006g6pntqt4rqdhzzh4.jpg

除了混合(Blending)之外,我們也能通過分支節(jié)點在兩個Animation之間做選擇(Select)。如圖12.1中,我們可以用一個“Select”節(jié)點來替換對速度的混合,從而可以在走和跑之間選擇特定的Blend Tree。

雖然一個Blend Tree足以用來執(zhí)行一個行為所需的所有操作,但只用一個Blend Tree卻難以涵蓋一個角色的所有可能行為。因此,我們希望能夠對每個行為單獨構建一個Blend Tree,然后再通過某種機制在它們之間切換。這些行為之間通常都有預先定義好的序列來約束彼此之間哪些可以被連接在一起,依照傳統(tǒng),這種切換機制采用了狀態(tài)機的形式。

在這些狀態(tài)機中,每個狀態(tài)就是一個Blend Tree,狀態(tài)變遷(Transition)會在兩個Blend Tree之間進行Blend。每個狀態(tài)變遷都有一組條件(Condition),一旦條件滿足,就會發(fā)生Transition。這些條件同時也需要檢查一些與Animation相關的判斷(Criteria),例如控制參數(shù)的值、Animation Events以及一些基于時間的判斷,如是否Animation已經(jīng)播放完了之類的。此外,狀態(tài)本身還包含用來控制和驅動Blend Tree的邏輯(為Blend Tree設置相應的控制參數(shù)等)。

有了狀態(tài)機,我們就能夠將所有單獨的動作組合在一個系統(tǒng)中,通過把相應動作串起來,從而能讓角色做出各種復雜的行為。下面讓我們再來看看圖12.1中的那個例子,由于他只包含了向前移動,我們首先要擴展朝向來包含全部朝向。我們還缺少角色停下來不動的時候的Animation,我們需要添加一個Blend Tree。于是我們需要一個狀態(tài)機用來在這個兩個Blend Tree之間進行切換。緊接著,我們發(fā)現(xiàn)從移動到停止的切換過程看起來很生硬,我們想在移動和停止之間增加兩個過渡狀態(tài)來引入自然的過渡動作。然后我們發(fā)現(xiàn),當我們要停下來的時候,我們哪只腳先落地關系到我們如何播放過渡動畫,于是我們又加了兩個狀態(tài)來區(qū)分左右腳,還要通過在走路動畫中打的Animation Events來正確遷移到相應的狀態(tài)。最終,我們得到了一個如圖12.2所示的狀態(tài)機。

110005iflimsdqm7tieliq.jpg

我們已經(jīng)能夠看到,即使在如此基礎的設置中也已經(jīng)存在大量的復雜度了??紤]到每個Blend Tree事實上還有各自不同的控制參數(shù)以及相應的控制代碼,每個Transition的觸發(fā)邏輯和各個狀態(tài)的設置和驅動??紤]到現(xiàn)在的游戲角色通常都有幾十個行為,每個包含數(shù)個Blend Tree,以及每個行為對應的狀態(tài)邏輯。我們已經(jīng)面臨復雜性爆炸,并且還要在廢墟中繼續(xù)前進。

二、復雜性爆炸和擴展性難題

說到擴展性,不得不說狀態(tài)機。傳統(tǒng)上,很多開發(fā)者可能會復用同一個狀態(tài)機來同時驅動Animation和游戲邏輯(Gameplay)狀態(tài)變化。不僅包括玩家角色的狀態(tài)機,也包括在狀態(tài)上采用類似行為樹(Behavior Tree)或決策結構的AI狀態(tài)機。我們在這里統(tǒng)一使用狀態(tài)機來指代各種AI狀態(tài)變更機制(行為樹、規(guī)劃器等等)。簡而言之,后文中我們用游戲邏輯狀態(tài)機(Gameplay State Machine)來指代任何上層AI或玩家行為系統(tǒng)。

復用上層的Gameplay狀態(tài)機來驅動Animation有很多問題,其中最主要的問題就是代碼和數(shù)據(jù)的耦合。由于Blend Graph并不寫死在代碼里,可以認為它們是在運行時加載的資源數(shù)據(jù)。但是負責設置必要的控制參數(shù)來驅動Blend Graph的又是代碼。因此,代碼必須要明確知道有哪些控制參數(shù)以及分別是干嘛的。很重要的一點是,控制參數(shù)通常是為Animation設計的(例如歸一化了的Blend值[0-1]),所以Gameplay代碼需要去把相應的參數(shù)轉換成Animation系統(tǒng)能用的形式(例如,把圖12.1中的方向值由角度值轉換成0-1之間的Blend權重)。在我們的代碼中,這種明確知道如何轉換特定的控制參數(shù)就已經(jīng)在代碼和Blend Graph之間形成了耦合。只要我們修改了Blend Tree,就要配套修改代碼。這種代碼和數(shù)據(jù)的耦合看起來無法避免,但是我們還有很多方法可以嘗試讓這種耦合盡可能遠離Gameplay代碼,從而減小風險,同時加快迭發(fā)的速度。

復用Gameplay狀態(tài)機的另一個大問題是狀態(tài)間不一致的生命周期,因為并不存在Gameplay狀態(tài)和Animation狀態(tài)一一對應的關系。舉例來說,一個簡單的移動行為,在Gameplay的角度來看,一個狀態(tài)就足夠表示角色在移動了,但是從Animation的角度來看,我們需要一系列狀態(tài)和Transition來實現(xiàn)移動。這常常意味著最后我們在Gameplay狀態(tài)機里內嵌了一個只有Animation的狀態(tài)機。隨著開發(fā)過程的不斷推進,這兩個狀態(tài)機之間的界限慢慢變得模糊了。事實上,這是對代碼和數(shù)據(jù)耦合的主要擔憂。因為一旦我們要對Blend Tree做些什么大改動,代碼就要跟著改,不幸的是,由于兩個系統(tǒng)如此交織在一起,這樣就會影響到甚至完全讓Gameplay停擺。更糟的是,因為Animation和Gameplay兩個系統(tǒng)如此緊密連接,自然會有程序員直接用Blend Tree的信息或者假設Blend Tree有特定的結構來做Gameplay的判斷。那么,一旦Animation修改了,就必然導致需要重寫大量的Gameplay代碼。

圖12.2的例子其實有一些誤導:Idle狀態(tài)是一個單獨的Gameplay狀態(tài)。如果我們要為角色創(chuàng)建一個額外擁有跳躍和攀爬的Gameplay狀態(tài)機,我們可能會得到一個如圖12.3所示的結果。

110005g53vzvj7xmbsp7nc.jpg

雖然圖12.3看起來已經(jīng)非常復雜,但事情還沒那么簡單?,F(xiàn)在,Gameplay狀態(tài)之間的切換還要承擔驅動Animation切換的工作。舉個例子,當我們在Idle狀態(tài)和Jump狀態(tài)之間切換時,我們要設法讓Jump狀態(tài)知道我們是從哪個Animation切換過來的,如此我們才能正確選擇新的Animation狀態(tài)。修改或者新增新的Animation Transiton就意味著我們需要修改Gameplay的狀態(tài)切換及其代碼和動畫邏輯。隨著我們的系統(tǒng)日益復雜,維護和除錯變成了一場噩夢。在開發(fā)的后期,新增一個狀態(tài)的成本和風險變得高到令人無法接受。要想繼續(xù)發(fā)展并避免這種窘境的最好辦法就是設法解開兩個系統(tǒng)之間的耦合,這也是“分而治之”(Separation Of Concerns、SoC)這個架構的由來。

三、分而治之(SoC)

SoC的原則是,多個相互交互的系統(tǒng),應該各司其職,而不應該有相互重疊的職責[Greer 08]。顯然,圖12.3里的兩個職責混亂的狀態(tài)機并不符合這個原則。首先,我們試圖將Animation的邏輯從Gameplay邏輯里拆出來。這個相對容易,并且很多開發(fā)者已經(jīng)用專門的Animation系統(tǒng)來支持Animation狀態(tài)機(例如Morpheme、Mecanim、EmotionFX)??上В珹nimation狀態(tài)機的概念并沒有想象中的那么普及,即使在Unity和Unreal引擎中也是到了他們的第四版才開始支持。正如上一節(jié)介紹的,Animation狀態(tài)機是在Animation系統(tǒng)一層的用來定義和轉換Animation狀態(tài)的一個簡單狀態(tài)機。此后,我們會用“Animgraph”來表示Blend Tree和狀態(tài)機組合而成的一整張圖。

Animation狀態(tài)機也可以是分級的,它的Blend Tree的葉子節(jié)點也可以是一個額外的子狀態(tài)機。這讓我們能容易地在一個Animation的基礎上實現(xiàn)分層(Layering)Animation。不過并不是所有的Animation系統(tǒng)都支持這個特性。比如,Natural Motion的Morpheme中間件就是完全基于這個概念搭建起來的。而Unity的Mecanim對一張Animgraph則只支持一個單一的狀態(tài)機,但在此之上卻允許有多個Graphs同時運行充當不同的層(Layering)。

如果我們把圖12.3中的Animation狀態(tài)機邏輯全部提取出來,我們就會得到一個如圖12.4所示的狀態(tài)機。正如你所看到的,分離出來的Animation狀態(tài)機依然很復雜,但是這種復雜性可以進一步通過使用只包含單一行為(如“Jump”或“Ladder”)的分級狀態(tài)機來簡化。Gamplay狀態(tài)機現(xiàn)在可以只關系游戲邏輯的狀態(tài)變遷而完全不用去管Animation要怎么切換。另外,從某種程度上來說,我們現(xiàn)在有可能獨立修改各個狀態(tài)機了。

110004sezlz1nvtlon60t1.jpg

這里還有個問題。雖然初步的分離對我們解耦系統(tǒng)有很大的幫助,但是在Gameplay狀態(tài)機和Animation狀態(tài)機之間耦合依舊。我們還是需要明確理解控制參數(shù)并驅動狀態(tài)機,即明確理解Animgraph的拓撲結構以便決定何時開始切換到何狀態(tài),何時結束。這也意味著我們依舊需要很多代碼來驅動和查詢Animation系統(tǒng),很不幸,這些代碼看起來也和狀態(tài)機差不多。不知道你是否發(fā)現(xiàn)一個令人驚訝的事,我們才把Animation狀態(tài)機從Gameplay狀態(tài)機里拆出來,事實上卻得到了3個糾纏不清的狀態(tài)機:用來控制Player的Gameplay狀態(tài)機,用來描述動畫和銜接的Animation狀態(tài)機,和用來充當中間層的Animation驅動狀態(tài)機。事實上這個一直存在的隱含狀態(tài)機正凸顯了構建一個大一統(tǒng)系統(tǒng)的危險。

說回Animation驅動狀態(tài)機,它的職責之一是檢視Animation狀態(tài)機并為Gameplay系統(tǒng)提供必要的信息。同時,它還需要負責將Gameplay中的控制參數(shù)轉換成Animation系統(tǒng)能理解的形式,并在必要的時候觸發(fā)Animation狀態(tài)變遷。在很多情況下,這些驅動代碼也負責對Animation做必要的后處理,例如,對旋轉和位移的后期修正。正因如此,我們依然有很多不同的職責參雜在一個系統(tǒng)中,我還沒能真正意義上提高角色的可維護性和擴展性。為了更進一步,我們需要將所有和Animation相關的代碼都從Gameplay系統(tǒng)中抽離出來,以消除在Animation數(shù)據(jù)和Gameplay代碼之間殘存的耦合。

四、分離Gameplay和Animation

我們可以通過兩件事情來抽離Animation驅動代碼,第一件相對簡單并對簡化Gameplay代碼大有益處,第二件則明顯更耗時費力。如果你已經(jīng)在項目的開發(fā)末期,可能前者會更有幫助。

我們提到Animation驅動代碼的一個關鍵職責是將上層Gameplay邏輯的需求,像是“在左轉53°的同時以3.5米/秒的速度移動”,轉換成Animation層的控制參數(shù),也許看起來像是“方向=0.3,速度=0.24”(用于合成最終視覺效果的blend權重)。為了完成轉換,顯然Gameplay代碼需要知道有哪些可用的Animation,有哪些Blend組合存在,哪個值控制哪個Blend之類的知識。幾乎等于驅動代碼要做類似于角度到Blend權重之類的轉換就要完全了解Blend Tree。這就意味著假如有個動畫師修改了Blend Tree,Gameplay代碼就可能因為失效而需要修改。這導致任何對Animgraph的修改都需要程序員和動畫資源配合,并可能花費相當長的周期才能把代碼和數(shù)據(jù)變動集成到版本中交給產品團隊。

有一個簡單的方法來規(guī)避這個問題,將所有的轉換邏輯移到Animgraph里去(直接將上層Gameplay用的值傳遞給Animgraph)?;谀悴捎玫腁nimation系統(tǒng),這個可能做得到,也可能做不到。例如,在Unreal 4中可以通過藍圖(Blueprints)輕易實現(xiàn),而在Unity中貌似沒有辦法在圖里對控制參數(shù)做任何數(shù)算。把轉換邏輯移到圖里有兩個好處:

?

?

  • 首先,Gameplay代碼無需知道任何關于圖和Blend的細節(jié),它只需要知道它需要把它所理解的“方向”和“速度”值發(fā)給Animation系統(tǒng)。通過把和代碼的耦合轉移到Animgraph里,現(xiàn)在動畫師可以對Animgraph大改而不會導致Gameplay代碼需要修改。實際上只要輸入的參數(shù)不變,即便他們整個換了張圖也沒關系。在圖12.1的基礎上,我們把所有的轉換邏輯都放到圖里,就得到圖12.5。


110004bgnv9fvvlvmrdrrw.jpg

?

?

  • 此外,我們還能將除了轉換邏輯之外的更多控制參數(shù)邏輯移到圖里(例如,抑制輸入值來來獲得向新值得平滑過度而不是跳變)。


仍然值得指出的是,Gameplay的代碼可能依舊應該了解Animation的能力極限(例如,轉向的大約限制、合理的移動速度,不過不需要很精確)。實際上,通常是由Gameplay來確定這些約束的。試想我們采用圖12.5的設置,玩法上想要角色做一個沖刺行為,游戲邏輯團隊只需要給出一個更快的移動速度參數(shù)并告訴動畫團隊。動畫團隊現(xiàn)在可以創(chuàng)建一個新的動畫放進去,而無須游戲邏輯團隊的干預。從技術角度來看,把轉換邏輯從代碼轉移到圖中消除了一層耦合,使得我們離最終理想的SoC架構更近一步。

實現(xiàn)SoC要做的第二件事是,將所有Animation驅動狀態(tài)機的代碼從Gameplay狀態(tài)機中移到一個處于Animation系統(tǒng)和Gameplay代碼之間的新的中間層。在經(jīng)典的AI代理人架構中[Russel 03],作者將一個代理人分成三個層次:感知層、決策層和行動層。這本身就是一種SoC的設計,我們可以直接套用過來。如果我們把Gameplay狀態(tài)機看作決策層,Animation系統(tǒng)看作最終的執(zhí)行器,那么我們需要一個行動層來將來自決策層的指令傳輸給執(zhí)行器。這個新的層由Animation和Animation行為組成。對于任何動畫需求,Gameplay系統(tǒng)都會直接和這個新的層來交互。

五、動畫行為(Animation Behaviors)

動畫行為(Animation Behavior)是一個通過執(zhí)行一系列操作從視覺上來實現(xiàn)角色行為的程序。因此,動畫行為單純從視覺上來考慮角色行為,并不負責任何游戲邏輯的狀態(tài)變化。但并不是說它們對游戲邏輯完全沒影響,游戲邏輯和動畫行為之間的信息流通是雙向的,會間接導致游戲邏輯狀態(tài)的改變,但這些改變不是由動畫行為來執(zhí)行的。實際上,我們推薦(在你的引擎架構中)將動畫行為放在游戲邏輯之下,動畫行為應該根本訪問不到游戲邏輯。

在講解動畫行為的過程中,我們發(fā)現(xiàn)從動畫系統(tǒng)開始講起再回到游戲邏輯會更容易理解。那就讓我們先來看看如圖12.6所示的Animgraph的例子。我們有一個包含角色所有能做的全身動畫的動畫狀態(tài)機。在這個狀態(tài)機中,我們有個一狀態(tài)叫“移動(Locomotion)”,它又包含了一個包含所有移動狀態(tài)的狀態(tài)機。每個狀態(tài)又包含了相應的Blend Tree或子狀態(tài)機。

110003joov6ood17v6kqo4.jpg

接下來,讓我們來構建一個“移動(Move)”動畫行為。要讓這個行為正常工作,它需要了解Animgraph(尤其是“移動”狀態(tài)機),內包含的所有狀態(tài),以及每個狀態(tài)的內容。一旦我們給出圖上所有的拓撲信息,動畫行為就要來驅動狀態(tài)機,也就表明它需要了解所有的控制參數(shù)及上下文。有了這些信息,動畫行為就準備好干活了。干活分三個階段:

“開始(Start)”

“執(zhí)行(Execute)”

“停止(Stop)”

“開始”階段的主要職責是確保Animgraph處于一個執(zhí)行階段能夠繼續(xù)處理的狀態(tài)。舉例來說,當從站立開始移動,我們需要觸發(fā)“從站立到移動(Idle to move)”的過渡(Transition)并等它完成。只有當過度完成,我們處于“移動”狀態(tài),我們才能轉到“執(zhí)行”階段。如果是要做路徑跟蹤,那么我們還需要在這個階段進行尋路和路徑后處理。

“執(zhí)行”階段負責主要的工作,驅動Animgraph產生所需的視覺結果。在移動的情況下,我們需要執(zhí)行路徑跟蹤模擬,并為Animgraph設置正確的方向和速度控制參數(shù)。一旦我們完成了任務,就開始轉到“停止”階段。

“停止”階段負責完成所有的清理工作,并將Animgraph切換到可以繼續(xù)做行為的中立狀態(tài)。在我們的移動例子中,我們需要在這階段中釋放路徑并觸發(fā)“移動到停止(Move to idle)”的過度(Transition),然后結束這個行為。

需要注意的是,圖12.6中的例子里,“站立”和“移動”之間的過渡其實是存在于“全身動作”狀態(tài)機里的。這意味著,“站立”和“移動”都需要了解“全身動作”狀態(tài)機。其實,所有包含在“全身動作”狀態(tài)機里的狀態(tài)都要了解這個狀態(tài)機。這就引出了Animgraph試圖的概念。Animgraph視圖是一個能夠識別圖的一個特定局部并包含驅動這個局部所需的工具函數(shù)(Utility Functions)的對象。根據(jù)這個描述,動畫行為本身其實就是Animgraph視圖,只是還包含了執(zhí)行過程(Execution Flow)。我們最好把視圖理解成工具庫(Utility Libraries),而把動畫行為看成一個程序。多個動畫行為可以共享使用同一個視圖,以便提成代碼的復用度,減小因Animgraph變更帶來的代碼修改。在我們的例子中,我們會有一個了解“全身動畫狀態(tài)機”拓撲結構的“全身動畫視圖”,并提供一些方便觸發(fā)狀態(tài)過渡的函數(shù),例如,設置全身狀態(tài)(站立)。

要執(zhí)行給定的任務,動畫行為還需要一些指示和目標。目標來自于動畫指令,動畫指令由游戲邏輯發(fā)出,并包含所有執(zhí)行給定行為所需數(shù)據(jù)。例如,如果游戲邏輯想要移動一個角色到特定的位置,它就會發(fā)出一個帶有目標點的“移動指令”,要求的速度,結束時的朝向等等。每個動畫指令都帶有一個類型,并對應產生一個動畫行為(例如,一個“移動指令”對應產生一個“移動行為”)。動畫指令一旦發(fā)出之后就不管了,游戲邏輯并不能控制動畫行為的生命周期。取消或更新動畫行為的唯一方式是再發(fā)一條新的指令,這點我們在下一節(jié)詳細介紹。

除了動畫指令外,我們還有動畫行為句柄的概念,它在發(fā)出動畫指令時得到。這些句柄將成為動畫行為和游戲邏輯相互溝通的機制。首先,通過句柄,游戲邏輯可以檢查一個已經(jīng)發(fā)出的動畫指令的執(zhí)行狀態(tài)(例如,這個指令是完成了還是失敗了,及其原因)。動畫句柄包含一個指向動畫行為的指針,通過它來執(zhí)行對動畫行為狀態(tài)的必要查詢。在某些情況下,例如對于一個玩家角色,最好每幀都能對一個行為進行更新(例如,動畫行為每幀都能根據(jù)控制搖桿的數(shù)據(jù)來設置動畫控制參數(shù))。

在圖12.7中,我們簡單畫出了一條游戲邏輯和“移動”動畫行為之間相互交互的時間軸。需要注意的是,游戲邏輯和動畫行為之間是如何通過動畫句柄來完成所有溝通的。

110003mu51c9arrwb1a0wa.jpg

在三個主要階段外,動畫行為還提供了一個動畫“后處理”階段。這個階段主要用于執(zhí)行類似于軌跡變換(Trajectory Warping)之類的后處理操作,但也可用于物理和動畫計算完成后對姿態(tài)的再修改(例如,反向動力學、LK)。另外插一句,LK和物理/動畫交互理想情況下應該作為動畫更新的一部分,但是并不是所有的動畫系統(tǒng)都支持這么做。

既然我們需要很多個動畫行為才能包含角色所有的行動(Action),我就需要某種機制來調度和運行他們。而這就是動畫的由來。

六、動畫(Animation Controller)

動畫的主要角色就是充當動畫行為的調度器。它是上層Gameplay系統(tǒng)通過Animation命令來向底層Animation系統(tǒng)發(fā)出請求的主要接口。它負責創(chuàng)建和執(zhí)行動畫行為。同時,動畫還提供動畫行為分軌(多隊列)的功能,用來實現(xiàn)多層動畫。例如,類似“注視”、“裝”或“揮手”等行為可以和的全身動作(如“站立”、“走路”)一起做,因此,我們可以在中將這些動畫實現(xiàn)成一個疊加在全身動畫上的分層動畫。在以前的游戲中,我們發(fā)現(xiàn)一般只需要兩個動畫層就足夠了(特別是人形角色):一個用于全身行為,另一個用于附加行為[Anguelov 13, Vehkala 13]。對不同的分軌,我們還有不同的調度規(guī)則。在任何時間我們只允許一個全身行為處于激活狀態(tài),而附加行為可以同時有多個。

對于全身動畫行為,我們的隊列中有兩個插槽。一旦有全身動畫指令被發(fā)出,我們就會將其放入主插槽。如果我們又收到另一個全身動畫指令,我們會創(chuàng)建一個新的動畫行為,并嘗試將其和之前的行為合并。動畫行為和一個簡單合并機制就是更新動畫行為的指令。例如,如果我們發(fā)出了一個移動到A點的指令,我們會創(chuàng)建一個以A點位目標的移動動畫行為。如果之后我們覺得B點其實是更好的終點,我們就會再發(fā)一個以B點為目標的移動指令。這會產生一個新的動畫行為,并通過更新原有動畫行為的指令進行合并。合并后,第二個動畫行為被廢棄。一旦合并過程完成,動畫行為會檢測到更新后的指令,并作相應的處理。如果新的動畫指令產生出與之前不同類型的全身動畫行為,那么新的行為會被放進第二個插槽并執(zhí)行。當然,我們還要通知第一個行為該終止了。終止一個行為強制將使其進入“停止”狀態(tài)并完成。一旦一個動畫行為完成了,它就會被從隊列里移除并不再被執(zhí)行。這意味著我們本質上有能力在兩個全身動畫之間交叉混合(Cross-Fade),以便在轉換的過程中實現(xiàn)更好的視覺質量。

合并機制要求所有的動畫行為都支持指令被更新。乍一看這種更新動畫行為的方式很奇怪,但對游戲邏輯代碼而言大有益處。最主要的是,游戲邏輯不必再關心動畫什么時候結束,也不必關心不同動畫狀態(tài)之間如何遷移,這些都由動畫和動畫行為處理了。當需要在不同系統(tǒng)間轉移對動畫的控制權時,例如觸發(fā)劇情動畫時需要由協(xié)議動畫系統(tǒng)來控制角色動畫,由游戲邏輯來精細化管理動畫狀態(tài)變遷本身也是極其有問題的。當控制權轉移時,角色有可能處在任意狀態(tài)。劇情動畫系統(tǒng)需要用合適的方法去恢復狀態(tài),這在以前,除非把幾個無關系統(tǒng)耦合起來(例如讓協(xié)議動畫系統(tǒng)了解AI在干什么),否則是非常難以實現(xiàn)的。用我們的方法,就可以在無需將無關系統(tǒng)耦合起來的前提下,隨意轉移動畫的控制權。例如,可以為協(xié)議動畫編寫動畫行為,當協(xié)議動畫開始時,它就發(fā)出終止現(xiàn)有指令的指令,并在指令之間做出合適的銜接。實際上,協(xié)議動畫系統(tǒng)甚至都可以直接復用AI使用的移動指令,因為它們是系統(tǒng)無關的。試想,我們需要一個NPC在劇情動畫中爬梯子。我們并不需要重新制作一個完整的長動畫,也不需要去編寫AI腳本讓NPC爬梯子,我只需要讓協(xié)議動畫系統(tǒng)直接發(fā)出動畫指令而不用擔心AI和NPC狀態(tài)。

這種方法對動畫方面也有額外的好處。萬一由于某些原因,游戲邏輯不停地發(fā)出動畫指令,也就是說行為振蕩,我們的全身動畫隊列機制只需要將新的行為簡單的覆蓋/合并到隊列里已有的行為上。這極大得緩解了傳統(tǒng)上會出現(xiàn)的視覺抖動問題。壞處是,由于看起來沒啥問題,也使得QA更難去發(fā)現(xiàn)那一類Bug。所以,我們建議你實現(xiàn)某種動畫指令濫發(fā)檢測機制。

對于附加動畫行為,我們允許隊列里有任意多個行為并存。這需要由游戲邏輯來保證這些這些組合是合理的。對附加動畫我們也使用和全身動畫一樣的合并規(guī)則。

關于調度動畫行為還有最后一件事需要討論:動畫行為的生命周期和發(fā)布它的游戲邏輯狀態(tài)是不同步的。一旦一個動畫行為完成了,它就會被從隊列里移除,但卻不能立刻刪掉,因為以不同頻率執(zhí)行的游戲邏輯仍可能通過句柄繼續(xù)訪問它。反之亦然,游戲邏輯可能不等動畫行為完成就先結束了。因此,我們決定按共享所有權(Shared Ownership)的概念來管理動畫行為的生命周期。一個動畫行為,只要還有句柄指向它,或它還在動畫的某個隊列中,它就繼續(xù)存在(內存中)。這可以通過STL的Shared_Ptr智能指針來輕松實現(xiàn)。最終包含所有系統(tǒng)層級的整個架構,如圖12.8所示。關于“/行為”(Controller/Behavior)架構的更多細節(jié),大家可以參考[Anguelov 13]。

110002v8x8pga01ygytt8g.jpg

七、SoC架構的好處

直到現(xiàn)在,我們都是圍繞著如何解決已有的問題來討論SoC。然而,必須要指出,轉向SoC架構還有一些額外的好處。雖然不是顯而易見,但是我們還是有必要在這里提一提其中的一部分。

1、功能測試

第一個顯著的好處是,現(xiàn)在我們可以進行獨立的功能測試了。例如,我們想測試AI系統(tǒng),那么我們可以創(chuàng)建一個假的Animation,它除了接收命令并根據(jù)需要返回成功或失敗外,并不真正的執(zhí)行任何Animation的代碼。這樣能極大的簡化調試AI問題工作,而完全不必考慮Animation代碼是否有問題。而在先前那種相互交織的系統(tǒng)中,我們很難真正定位問題的根源。而這也同樣適用于Animation的測試。曾經(jīng),為了測試我們的Animation層,我們還不得不另外搭了一套腳本系統(tǒng)。這套腳本系統(tǒng)完全模仿AI發(fā)出指令,但能讓我們在一個隔離的環(huán)境中測試。這在我們上一個項目的多個Gameplay代購重構過程中,對維護和驗證Animation功能都起到了極大的作用。

2、系統(tǒng)重構

這個方法的另一個巨大好處是,當我們想對Animation做一些大的改動時,可以變得更安全更簡單。這個架構讓我們能夠逐個替換角色的行為,而不用擔心會搞壞Gameplay代碼。此外,這個方法允許我們進行非破壞性原型開發(fā)。當在構建一個行為的新版本的時候,我們只需在現(xiàn)有基礎上創(chuàng)建一個新的Behavior,而由Controller在運行時動態(tài)切換。這個方法優(yōu)雅之處在于,我們可以在運行時動態(tài)替換Behavior而完全不需要Gameplay代碼知曉。如果我們結合前面講的功能測試,就能夠做到構建一個新版行為然后和舊的一起對比,而無須修改舊的行為也無需修改Gameplay代碼。這使得我們能夠在保證最基礎功能的前提下快速創(chuàng)建原型,并今早和Gameplay整合,在不影響版本的前提下實驗各種效果,逐漸完善最終版本。

3、細節(jié)分級

由于能在不影響Gameplay的前提下動態(tài)切換動畫行為,這使我們能夠利用這點來構建一套動態(tài)動畫細節(jié)分級系統(tǒng)(LOD)。在屏幕外角色不能直接銷毀,AI還要繼續(xù)運行的情況下,我們需要某些機制來減小或消除Animation的運行開銷。如果NPC的移動是基于Animation的,那么在Animation和Gameplay沒有分清楚的情況下,要實現(xiàn)這個功能就會非常復雜。

通過我們的方法,我們可以創(chuàng)建一些開銷很小的替換行為,用以在運行時根據(jù)角色的LOD等級動態(tài)替換[Anguelov 13]。當我們的NPC處于最高LOD級別時,我們會運行默認的Animation行為。當角色逐漸遠離,LOD等級下降,我們可以用輕量級的行為替換掉一些消耗很大的多層疊加動畫來減小開銷。當角色完全移出屏幕時,我們可以將全部Animation行為替換成空行為,只維持切換回高級別LOD所需要的Animation狀態(tài)。

拿移動舉個例子。在最高LOD等級時,我們運行完整的包含腳底LK的移動動畫。在中等LOD時,我將腳底LK替換成一個空行為,移動動畫保持不變。在最低級LOD時,我們只定時更新位置并估算速度而不執(zhí)行動畫。一旦角色重新進入屏幕,我們即可將標準的移動動畫行為無縫切換回來。我們建立為不同的LOD等級創(chuàng)建單獨的動畫行為,這樣使得你能夠通過對不同行為的組合為不同角色創(chuàng)建出各自的LOD集合。例如,對于體型巨大的角色,即使在中等LOD級別的時候,你也想保留腳底LK,因為他們的滑步會比小體型的角色看起來明顯得多。

八、總結

在這篇文章中,我們提出了一種解耦Gameplay系統(tǒng)和Animation系統(tǒng)的方法。我們討論了這種方法對生產力和可維護性的潛在改進,并給出了如何向類似方法過渡的建議。


via:UWA


銳亞教育

銳亞教育,游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨立游戲|游戲產業(yè)|游戲研發(fā)|游戲運營| unity|unity3d|unity3d官網(wǎng)|unity3d 教程|金融帝國3|8k8k8k|mcafee8.5i|游戲蠻牛|蠻牛 unity|蠻牛