Korok 的動畫系統 - 精靈動畫
來自專欄 korok引擎開發
在 Korok 中提供(打算)三種動畫:精靈動畫、補間動畫和骨骼動畫。目前精靈動畫和補間動畫都是可以工作的,骨骼動畫還沒有著手去做。本文主要講述精靈動畫的設計。
所謂精靈動畫大概就是提供一組圖片,快速挨個的播放就會產生動畫的效果。
對於精靈動畫,我們希望的介面是這樣的:
- 可以方便的切換不同的動畫
- 動畫數據需要緊湊的存儲
- 可以方便的控制動畫的播放幀率
- 可以方便的跳幀
這是 Korok 對精靈動畫的基本需求。
介面 API 設計
在開始之前,先調研了一些常見遊戲引擎的實現:
Cocos2DX
在 Cocos2D 中, 使用精靈動畫是這樣的:
auto animation = Animation::create(); for( int i=1;i<15;i++) { char szName[100] = {0}; sprintf(szName, "Images/grossini_dance_%02d.png", i); animation->addSpriteFrameWithFile(szName); } // should last 2.8 seconds. And there are 14 frames. animation->setDelayPerUnit(2.8f / 14.0f); animation->setRestoreOriginalFrame(true); auto action = Animate::create(animation); _grossini->runAction(Sequence::create(action, action->reverse(), NULL));
Animation 維護了精靈動畫的內部狀態,然後通過 Node 來播放。這個實現有個特殊的好處,它的每一幀都是一個真實的紋理,即使沒有紋理圖集也可以把零散的文件組織成一個動畫。所以我們在實現 Korok 的時候也會維持這種設定。
註:在 Cocos2D 中,Action 是屬性動畫系統(既補間動畫),Animation 是精靈動畫。
Xenko
在 Xenko 裡面 SpriteComp 本身是具備播放動畫的能力的,需要這麼設置:
var spriteGroup = new SpriteSheet(); var sprite = new SpriteComponent { SpriteProvider = new SpriteFromSheet { Sheet = spriteGroup } }; // add a few sprites for (int i = 0; i < nbOfFrames; i++) { spriteGroup.Sprites.Add(new Sprite(Guid.NewGuid().ToString())); } return sprite;
注意,此處的 SpriteComponent 傳入的 SpriteProvider 是一個 SpriteSheet,之後每幀更新的時候,從此取出對應的幀來實現動畫的。從介面設計的角度來說,能夠直接讓 SpriteComp 直接播放動畫是很有吸引力的(比如雲風的 ejoy2D 引擎也是內建了動畫支持)。這種設計也成了我們的一個考量因素。
Unity
Unity中有兩種辦法實現動畫:
1. 直接再腳本裡面寫一個動畫的幀數組,然後在 Update 方法裡面自己更新即可。
2. 使用動畫狀態機和Animation系統來創建動畫...
Unity 實現精靈動畫一個太原始,一個太高級,不知道如何評價。
Unreal
在 UE 中,精靈動畫是通過一個組件實現的 - FlipbookComp,因為大部分操作都是在 IDE 裡面完成的,所以看教程吧:Sprite Animation (Flipbooks) 文字不好描述,我也沒有做深入的體驗。UE的設計也蠻好的,SpriteComponent 就是靜態的2D圖片,FlipbookComponent 是動態的精靈動畫,兩者分的很開。
Korok的設計
在參考了上面各種設計風格之後我們開始設計自己的精靈動畫介面。如果按照 UE 的方式來做,非常簡單但是會失去一種常見的場景,比如原來的某個組件只是一個靜態的圖片,如果現在想把它變成精靈動畫那麼就要明確的把 SpriteComponent 換成 FlipbookCompment, 所以我覺得還是需要一種更快的方式來滿足這個需求。如果按照 Paradox 或 ejoy2D 的做法,也是非常具有吸引力的,精靈和精靈動畫本身就是結合非常緊密的,即使產生深度的耦合也沒有什麼不對,但是如果這麼做的話,就需要在精靈系統中添加動畫的支持,而對於存有大量靜態圖片的使用場景(比如瓦片地圖)而言這些狀態的維護是多餘的,所以我們也放棄了這種選擇。最後就是類似 Cocos2D 的實現,動畫是獨立的存放在 Animation 裡面的,需要的時候用 Sprite 來播放即可,這種設計也有不好的地方,依然需要在精靈系統中寫一部分代碼來維護動畫的邏輯。
而我們希望的系統是這樣的,精靈的渲染是獨立的,它的職責非常單一 ——渲染一個紋理,這非常適合渲染大量的靜態圖片。精靈動畫系統執行動畫邏輯,比如計算幀率並給出每幀需要的紋理。我們的解決方案是 - 依賴反轉!讓動畫系統依賴於精靈渲染系統,而不是像 Cocos2D 那樣,讓 Sprite 依賴於 Animation。這裡有一個想法上的轉變,在 Cocos2DX 中你會想:「讓 Sprite 來播放一段動畫」,在 korok 中會是:「讓這段動畫來驅動精靈的渲染 」。
於是我們有了第一步的草稿:
// 動畫定義 anim.NewAnimation("hero.attack", frames:[...], loop) // 播放動畫 anim.With(entity).Play("hero.attack")
這個方案非常簡單,如果我們想給某個 Entity 添加動畫支持,只要定義一組動畫,然後用
anim.With(entity).Rate(.1).Play("hero.attack")
的語法就可以讓它執行動畫了(這個Entity需要存在一個 SpriteComp)。有了介面設計,便可以著手具體的系統設計。這種風格的API滿足了我們之前設定的介面設計的目標,同樣簡單易用。
動畫系統設計
因為依賴反轉的關係,我們也不需要改動原來的精靈渲染系統就可以簡單的給它加上動畫的支持。這樣便可以獨立的設計這個系統,此時需要解決兩個問題:
- 如何高效的存儲動畫的幀序列
- 如何給 SpriteComp 賦值
第一個問題並不好解決,這個問題更抽象的描述是:如何高效的存儲可變長的數據?在我們現在實現中,幀數據是不能刪除的,只能堆加。這個實現可以把所有的幀緊湊的存在一個內存塊中,如果要刪除一段幀序列就會產生大量的內存移動。另外也在看一些新演算法,比如:
http://bitsquid.blogspot.com/2011/11/example-in-data-oriented-design-sound.html它的思路是把變長的數據截取成定長的數據然後通過類似鏈表的結構連接起來,這種方案的缺點在於存在填充率的問題,受應用層的影響比較大。不過相對於我們當前的實現應該是比較好的了。
第二個問題,給 SpriteComp 賦值是我們的潛在要求,我們的動畫系統只處理動畫邏輯,沒有渲染邏輯,那麼就需要依賴於一個 SpriteComp 來提供渲染支持(實際上我們的動畫系統也可以用來給GUI系統播放幀動畫)。Korok 是一個 Component/Table/System 的系統架構,動畫系統只要依賴於 SpriteTable 既可方便的給 SpriteComponent 賦值,實現較為簡單。
這樣精靈動畫系統便有了雛形。
// Sprite Animation Systemtype SpriteEngine struct { // raw frames frames []gfx.Tex2D // raw animation data []Animation // mapping from name to index names map[string]int // sprite and animate table st *gfx.SpriteTable at *FlipbookTable}// 核心演算法func (eng *SpriteEngine) Update(dt float32) { var ( at, st = eng.at, eng.st anims = at.comps[:at.index] ) // update animation state for i := range anims { if seq := &anims[i]; seq.running { seq.dt += dt if seq.dt > seq.rate { seq.ii = seq.ii + 1 seq.dt = 0 } } } // update sprite-component for _, am := range anims { comp := st.Comp(am.Entity) ii := eng.names[am.define] anim := eng.data[ii] jj := am.ii % anim.Len frame := eng.frames[anim.Start+jj] comp.SetSprite(frame) }}
這段代碼其實非常簡單,計算一個 AnimationState 的當前幀,然後賦值給 SpriteComp. AnimationState 是精靈動畫系統內部表示當前動畫狀態的結構,它記錄了當前播放時長,當前播放到了第幾幀,並計算出下一幀的值。
優化
目前我們的動畫系統已經可以很好的工作了,使用 『anim.With(entity).Play("hero.attack")』 可以方便的播放動畫,但是很快發現一個問題。這種風格的API比較適合 fire-and-forget 的用法(這種用法已經大量應用在補間動畫裡面),但是對於精靈動畫來說,一旦我們有了一個動畫序列,我們馬上就會給它在加上另一個動畫序列。這樣這個動畫就不能 forget 而是 stateful 的,為了處理狀態我們又設計新的組件:
// Sprite Animation Componenttype FlipbookComp struct { engi.Entity define string dt, rate float32 ii int running bool loop bool}
它其實就是把原來的 AnimationState 重新命了名而已,並且可以直接通過
korok.Flipbook.Comp(entity)
索引。這樣使用精靈動畫就變成給這個 SpriteComp 添加一個 FlipbookComp,注意這和 UE 中實現的不同,我們的 FlipbookComp 只產出幀並不執行渲染,它依賴於 SpriteComp 來渲染。現在使用精靈動畫的完整代碼如下:
// SpriteCompsprite := korok.Sprite.NewComp(hero)sprite.SetSize(50, 50)// FlipbookCompfb := korok.Flipbook.NewComp(hero)fb.SetRate(.2)// play animationfb.Play("hero.top")
它的效果如下(示例代碼見 KorokEngine/SpriteAnimation ):
最後是廣告時間,真誠的希望您能夠在 Github 上給我們的項目 Github - KorokEngine 來一套 watch/star/fork 三連擊。 關注一下twitter也是歡迎的。
以上。
推薦閱讀:
※Dirty Game Engine
※從零開始手敲次世代遊戲引擎(四十九)
※第1章 引擎的紛爭 (感謝大食堂對本章的校驗)
※Unity3D魔改渲染系列之亂改圖形篇
※[DOD Series][OGRE] Pitfalls and Design proposal for Ogre 2.0