GamePlayKit的ECS“實體-元件-系統”
如有侵犯,請來信[email protected]
蘋果去年釋出了GameplayKit的程式碼框架,為遊戲開發者提供了很多超級實用的API及工具,來提升遊戲開發者的效率,使得製作遊戲能更加聚焦在遊戲本身-也就是遊戲性的策劃和創意上了。
業餘時間會用Spritekit做些小遊戲demo,GameplayKit出來後感覺給了極大的方便,我就藉由蘋果提供的Maze的Sample程式碼,來跟大家介紹下GameplayKit提供的新功能,希望大家能夠喜歡!
一、樸素的遊戲程式設計的陷阱
之前小武並不是做遊戲開發出身的,所以並沒有學習很多遊戲開發裡面已經成熟的演算法或者程式設計模式來提升開發效率。僅僅憑藉愛好和隨性的編寫,完成遊戲程式碼(還真是隨便啦)。
1.需求描述
比如小武以前曾經制作過一款類似“合金裝備”的逃脫類遊戲:
(1)戰士需要躲避敵人的監視,逃到制定的出口。
(2)敵人巡邏有規定路線,敵人的能力也有些變化。
(2)戰士能夠利用各種道具,來完成逃離。
2.樸素的程式設計
樸素的程式設計設計(其實就是不過腦子,嘿嘿!):
(1)對“戰士”,和“敵人”做了一個公共的類“人”作為祖先,然後“戰士”和“敵人”類分別繼承自“人”,根據各自需要擴充套件私有部分。
(2)其它操作、移動、AI等都在各自私有類中完成。
(3)後續需求增加了,我想做更多型別的“敵人”,不同“敵人”的能力不盡相同。一開始,將相同部分的能力,放到一個“敵人”的公共類中,所有“敵人”都繼承自這個公共類。後來,隨著“敵人”型別的不同,可以有很多種能力搭配。“敵人”公共類的程式碼就越來越多,類也越來越大了。
(4)有時候,同樣的能力也想為“戰士”提供,要在“戰士”和“敵人”類裡重複Copy很多程式碼。不管怎麼樣,隨著遊戲越來越複雜,裡面重複的程式碼也有可能越來越多。
3.具體分析
(1)一開始“戰士”和“敵人”繼承自“人”,“人”提供基本的移動能力和精靈(負責實體影象);“戰士”和“敵人”分別有隱藏,跑動和偵查,射擊等功能。
如下圖所示:
最初的類繼承圖
(2)現在,“敵人”的種類加了3種,分別具有狙擊,透視,噴火這些能力的組合。而且,戰士升級了,需要有射擊能力了。此時,只能將公共的狙擊,透視和噴火能力,提升到“敵人”的公共類;而射擊能力,要提到“人”這個公共類。
如下圖所示:
需求變化後的類繼承圖
因為不是專職做遊戲,僅僅是業餘時間玩一下,所以寫個小Demo後,也就沒有繼續開發,繼續深究下去(其實是實在寫不下去了,擴充套件性和可維護性太差了!)。
小武有一顆積極向好的心,但是懶癌重度,放著吧~~!
二、GameplayKit的“實體-元件-系統”
直到蘋果推出了GameplayKit框架,我又持續對遊戲開發有點興致。開始學習後,發現遊戲開發裡面,早就有很多事實上的開發模式和方法了。其中“系統-實體-元件”是比較好的解決以上問題的方法。(因為小武水平有限,且比較懶,惡意吐槽傷害作者玻璃心的。。。。。也懶得理你們!)
上面小武所做的那個遊戲Demo,實際上是一種面向物件的繼承(Inheritance)體系,按照這個體系組織遊戲程式碼會造成公共祖先類巨大無比,子孫類也默默繼承了很多無用的程式碼,程式碼明顯有“壞味道”。
實際上,有很多能力是可以抽象出來的,同時給“戰士”和不同型別的“敵人”使用,比如渲染,移動,AI,特殊技能等。這些功能通過不同元件的組合,就構成了戰士和不同型別敵人。這是一種組合(Composite)體系。也就是下面需要介紹的ECS模式了。
1.“實體-元件-系統”:ECS(Entity,Component,System)
ECS(Entity,Component,System)即“實體-元件-系統”。實體,是可以容納很多元件的容器。元件代表一種功能和行為,不同的元件代表不同的功能。實體可以根據自己的需求來加入相應的元件。
比如上面的那個遊戲,我們將遊戲角色的移動,精靈,隱藏,跑動,偵查,設計,狙擊,透視,跑步等都作為元件類,每一個元件都描述了一個實體某些屬性特徵。我們的實體根據自己的實際需要,來組合這些元件,讓對應的實體獲得元件所代表的功能。
現在實體“戰士” “敵人1”“敵人2”“敵人3”“敵人4”實體與功能元件之間的對應關係如下圖所示:
ECS模式下游戲物件管理方式
從繼承(Inheritance)體系到組合(Composite)體系好處顯而易見:
(1)方便通過聚合多種元件,構建複雜的新實體。
(2)不同的實體,通過聚合不同種類的元件來實現。
(3)不必重複寫元件,元件可以複用,也易於擴充套件。
(4)實體可以實現動態新增元件,以動態獲得或者刪除某些功能。
2.遊戲執行的發動機
我們需要將“戰士”“敵人”這些實體放入到遊戲中執行,遊戲需要驅動這些實體的元件發揮功能。我們如何驅動遊戲執行呢?我來跟大家來八一八。
遊戲引擎(如SpriteKit和SceneKit,也包括客戶定製的引擎及直接使用Metal或者OpenGL)在一個稱為“更新/渲染”迴圈裡,執行遊戲有關的程式碼邏輯。這個迴圈貫穿遊戲的生命週期,按字面意思分為更新(Update)階段和渲染(Render)階段。在更新階段,與遊戲邏輯相關所有的狀態更新,數值計算,操作指令,執行序列計算,AI處理,動畫排程等都在該階段完成。在渲染階段,遊戲引擎部分進行自動處理,主要處理動畫渲染和物理引擎的執行,基於當前狀態渲染畫出所有遊戲的場景。通常,遊戲引擎的設計,是讓遊戲開發者完成更新階段的所有邏輯,然後引擎負責遊戲渲染階段的所有工作。蘋果的SpriteKit和SceneKit就是這樣設計的,從蘋果提供的SpriteKit遊戲引擎的執行Loop圖中可以略窺一二。
具體用SpriteKit引擎來驗證下上面的描述。遊戲時看到的每一幀畫面,展示在我們面前時,遊戲引擎都會經歷下面這個Loop圖所示的處理:
SpriteKit的生命週期
(1)每一幀(Frame)在SKScene裡會呼叫update:來進行更新操作,遊戲開發者在這裡放入所有的遊戲邏輯層面的程式碼。
(2)後面的Action,Physics,Render階段分別負責Action執行,物理引擎檢測處理,和遊戲影象的繪製渲染處理,這些工作都是交由SpriteKit來進行。
驅動遊戲執行的動力:更新階段,開發者在update裡面計算出所有遊戲邏輯和遊戲狀態;渲染階段,物理檢測,影象渲染由遊戲引擎完成。
3.GameplayKit裡的ECS執行方式
因此,驅動實體和元件在遊戲裡面執行可以放在更新階段,在SpriteKit引擎裡的update方法裡。GameplayKit提供了GKEntity類、GKComponent類及GKComponetSystem類來實現ECS模式。GKEntity提供介面新增GKComponent元件,並管理這些元件(增,刪,查操作)。GKComponentSystem也能對GKComponent提供同樣的管理操作。
遊戲執行時,每一次呼叫update的更新操作時(spriteKit的update:操作和senceKit的renderer:updateAtTime:方法),呼叫遊戲實體的更新方法,實體會將更新訊息分發給元件(GKComponent),元件執行updateWithDeltaTime來驅動元件執行。
GameplayKit提供了兩種分發策略:
(1)實體更新:如果一個遊戲僅有很少的遊戲實體,遍歷所有活動的實體,執行實體(GKEntity)的updateWithDeltaTime方法。實體(GKEntity)物件會將updateWithDeltaTime訊息轉發給該實體的所有元件(GKComponent),驅動元件執行updateWithDeltaTime方法。
(2)元件更新:在一個更加複雜的遊戲裡面,不需要跟蹤所有實體和元件對應關係,同類型元件能按順序獨立更新更加有用。為了滿足此種情況,可以建立GKComponetSystem對像,GKComponetSystem管理一具體型別的元件,當每次遊戲更新的時候,呼叫GKComponetSystem的updateWithDeltaTime的方法,系統(GKComponetSystem)物件會將updateWithDeltaTime訊息轉發給該系統的所有元件(GKComponent),驅動元件執行updateWithDeltaTime。
在方式(2)裡面,GKComponetSystem系統(System)出現了,它可以被認為是管理同一類元件的容器,並驅動它更新所有該型別的更新操作(Update)。
綜合以上說明,就是ECS模式。
三、Maze遊戲程式碼評鑑
Ok,我們回到正題,蘋果為我們更好的進入蘋果開發這個大坑,提供各種便利。程式碼的Sample是個非常好的手段。Maze遊戲是一個類似吃豆人的簡化版,只是這裡沒有吃豆豆,就是一個菱形與四個方形敵人的捕獵和反捕獵的迴圈。Maze遊戲程式碼下載地址。
1.Maze遊戲的需求分析
Maze遊戲的實體很少,就是player(菱形)和enemies(四個方塊)。分析下它們的需求:
(1)player和enemies都需要在遊戲中顯示,很顯然,它們需要顯示渲染的元件。
(2)player需要受到控制,在Maze迷宮裡面運動。它需要一個控制組件。
(3)enemies的控制需要計算機來給,因此要一個元件來給出enemies的狀態。
(4)enemies需要一個系統物件來管理狀態的更新。
所以,我們現在至少需要:
(1)2個實體,player(菱形)和enemies(四個方塊)。
(2)3個元件,負責渲染的元件SpriteComponent,負責player控制的。PlayerControlComponent,負責enemies的AI的IntelligenceComponent。
(3)1個系統,負責AI即IntelligenceComponent的控制。
如下圖所示意:
Maze遊戲ECS設計架構
2,程式碼實現
(1)實體entity實現:程式碼裡面,實體部分因為僅僅是個容器,所以比較簡單,直接繼承了GKEntity。在Maze遊戲裡面,每個實體其實在迷宮(Maze)裡面移動,都會有位置資訊,因此,在公共的AAPLEntity類中,加入gridPosition資訊(此資訊為迷宮座標值,並非SKScence裡面的sprite位置)。
1 2 3 4 5 6 7 |
@import GameplayKit;
@interface AAPLEntity : GKEntity
@property vector_int2 gridPosition;
@end
|
(2)元件component實現:
a.元件都是繼承的GKComponent物件,該物件僅提供了誰持有該元件的變數entity和系統System物件。
b.實現更新階段,呼叫的updateWithDeltaTime方法以及其他Helper方法。
Sprite元件:繼承GKComponent,相關變數和方法負責持有該元件實體的影象表現和計算控制運動表現兩部分。Sprite部分的渲染和影象展示方法可查閱我之前寫的相關的SpriteKit引擎教程,學習使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@interface AAPLSpriteComponent : GKComponent
@property AAPLSpriteNode *sprite;
...
#pragma mark - Appearance
// 重生時候心跳動畫
@property (nonatomic) BOOL pulseEffectEnabled;
// 捕獵狀態外在
- (void)useNormalAppearance;
...
#pragma mark - Movement
// 移動相關方法
@property (nonatomic) vector_int2 nextGridPosition;
- (void)followPath:(NSArray *)path completion:(void(^)(void))completionHandler;
@end
|
PlayerControl元件:實現play的控制邏輯,在更新階段,通過實體Entity執行updateWithDeltaTime方法來實現對Player實體的控制的計算,這裡可以用手勢(Mobile系統),也可以用鍵盤(iMac系統)來確定移動的方向,PlayerControl元件來完成實際控制操作。
在Player實體的控制移動過程中,實際上也要呼叫Sprite元件。Sprite元件控制實體在遊戲場景中的move渲染,即運動表現部分。
所以這是元件間配合,PlayerControl元件,僅負責處理告訴Player實體該怎麼移動,具體移動渲染表現,還是交給Sprite元件來負責。其實執行示意圖如下所示:
PlayerControl元件處理邏輯
Enemies的AI元件:通過狀態機來實現enemies實體狀態的遷移。狀態機在後面的系列文章裡面會具體描述,這裡僅理解為控制敵人方塊行為的演算法即可。狀態機初始化程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 |
// 初始化enemies的四種不同狀態
AAPLEnemyChaseState *chase = [[AAPLEnemyChaseState alloc] initWithGame:game entity:enemy];
AAPLEnemyFleeState *flee = [[AAPLEnemyFleeState alloc] initWithGame:game entity:enemy];
AAPLEnemyDefeatedState *defeated = [[AAPLEnemyDefeatedState alloc] initWithGame:game entity:enemy];
defeated.respawnPosition = origin;
AAPLEnemyRespawnState *respawn = [[AAPLEnemyRespawnState alloc] initWithGame:game entity:enemy];
// 初始化狀態機,並進入chase狀態
_stateMachine = [GKStateMachine stateMachineWithStates:@[chase, flee, defeated, respawn]];
[_stateMachine enterState:[AAPLEnemyChaseState class]];
|
enemies的AI元件需要加入到intelligenceSystem的系統進行管理,因為所有的AI都需要在更新階段進行執行,使用System管理,更加方便。系統的初始化程式碼如下所示(在遊戲初始化過程程式碼裡):