上一篇文章《評鑒Maze源碼(1):GamePlayKit的ECS“實體-組件-系統(tǒng)”》里,我已經(jīng)介紹了在Maze游戲中的ECS方法,這個方法里面,關(guān)于Enemy實體的行為,需要狀態(tài)機(jī)來配合管理,這一篇文章,我就跟大家介紹一下GameplayKit里面狀態(tài)機(jī)的使用。
一,狀態(tài)機(jī)的介紹
狀態(tài)機(jī):能夠準(zhǔn)確的表達(dá)同一實體,不同階段的狀態(tài)和狀態(tài)遷移條件。
1,狀態(tài),可能是實體對象的屬性,也可能是屬性集合。
2,遷移條件,指的是外界的突發(fā)事件及滿足特殊條件的屬性變化。
游戲里面的實體會存在很多狀態(tài),比如蘋果的SceneKit,女探險家運動的幾個狀態(tài),在游戲過程中女探險家在這幾個狀態(tài)里面遷移。狀態(tài)機(jī)示意圖狀態(tài)機(jī)示意圖:
其分為,Running狀態(tài)、Jumping狀態(tài)和Falling狀態(tài)。狀態(tài)遷移條件表明在帶箭頭的直線上面。狀態(tài)機(jī)模式給我們編寫程序帶來明顯的好處,通過條件判斷,方便的管理對象實體的狀態(tài)。
關(guān)于狀態(tài)機(jī)的實現(xiàn),有很多方式,其中比較樸素的是if-else的判斷,如果狀態(tài)多,根據(jù)需求,狀態(tài)遷移也會有不斷的變化,那么if-else的編程會帶來很多代碼維護(hù)的問題。《Head first 設(shè)計模式》(Head first是我覺得很輕松愉快的一個系列讀物,推薦想要進(jìn)入一個新的技術(shù),卻苦于無法迅速入門的同學(xué)。但是入門后,仍然需要毅力和付出來完全掌握這項技術(shù),對任何事情都是如此。)為我們提供了很好的狀態(tài)機(jī)編程模式的教學(xué),教會我們簡單,可擴(kuò)展性的狀態(tài)機(jī)設(shè)計模式實現(xiàn)。
但是,在iOS里面,我們再也不用擔(dān)心狀態(tài)機(jī)的代碼編寫問題了,因為蘋果實現(xiàn)狀態(tài)機(jī)模式,我們掌握如何使用就行。而且不僅僅是游戲開發(fā),在別的APP應(yīng)用中,也能很從容的使用GameplayKit所提供的狀態(tài)機(jī)框架。
二,GameplayKit里面的狀態(tài)機(jī)API
樸素的狀態(tài)機(jī)實現(xiàn)方法,這里提一下,就是為了對比狀態(tài)機(jī)模式的實現(xiàn)方法。
比如小明的狀態(tài),定義為下面這三種,吃飯,睡覺和工作。小明作為一個對象,里面有currentState這一屬性,代表當(dāng)前小明的狀態(tài)。暫且將currentState定為int型,吃飯、睡覺和工作類型值分別是1,2,3。暫且將小明的狀態(tài)機(jī)變化簡化為“吃飯->睡覺->工作->吃飯”這一循環(huán)。實現(xiàn)代碼,小明對象提供一個changeState的方法,其參數(shù)是下一個要變化的狀態(tài)。changeState的實現(xiàn):
- (Bool)changeState:(XMState*)state {
switch(state):
{
case eat:
// 判斷當(dāng)前狀態(tài)work到下一步遷移狀態(tài)eat的有效性
if (self.currentState == work) {
// 進(jìn)行狀態(tài)遷移
self.currentState = state;
return YES;
}
return NO;
case sleep:
if (self.currentState == eat) {
self.currentState = state;
return YES;
}
return NO;
case work:
if (self.currentState == sleep) {
self.currentState = state;
return YES;
}
return NO;
}
return NO;
}
這里做的兩個工作,一個是判斷狀態(tài)遷移的有效性(判斷當(dāng)前狀態(tài)),另一個是進(jìn)行狀態(tài)的遷移(設(shè)置currentStatus狀態(tài))。如果,加入新的狀態(tài),或者狀態(tài)循環(huán)發(fā)生變化,狀態(tài)機(jī)的switch-case和if-else的判斷將會不斷增加,可維護(hù)性變差,代碼冗余將不斷上升。
然而,通過狀態(tài)機(jī)模式,可以使得代碼變得可維護(hù)和,GameplayKit提供了這一模式的實現(xiàn),我們現(xiàn)在來好好掌握它。
1,狀態(tài)對象GKState
在GameplayKit里面,蘋果有狀態(tài)對象GKState,來作為所有狀態(tài)的基類祖先。
對象GKState提供了一些方法,這些方法有兩類作用:
(1)狀態(tài)對象本身屬性和管理狀態(tài)遷移的有效性。如:
// 驗證下一個狀態(tài)是否有效,如果無效的話,是不會發(fā)生狀態(tài)遷移的
- (BOOL)isValidNextState:(Class)stateClass {
return stateClass == [WJSSleepState class];
}
(2)為狀態(tài)的更新和遷移提供了填寫邏輯代碼的位置。在實體的狀態(tài)進(jìn)行更新或者遷移的時候,需要開發(fā)者填入相應(yīng)的邏輯來完成實體狀態(tài)的變化。
(3)按照上面小明同學(xué)的“吃飯,睡覺和工作”三個狀態(tài),定義這三個狀態(tài)。
@interface WJSWorkState : WJSState
@end
@interface WJSEatState : WJSState
@end
@interface WJSSleepState : WJSState
@end
如何驅(qū)動實體進(jìn)行狀態(tài)的更新和遷移呢?即樸素編程里面的changeState方法。GameplayKit提供了狀態(tài)機(jī)對象GKStateMachine來對狀態(tài)GKState進(jìn)行管理。
2,驅(qū)動狀態(tài)變化的狀態(tài)機(jī)對象GKStateMachine
GameplayKit提供管理狀態(tài)遷移的狀態(tài)機(jī)對象GKStateMachine,實現(xiàn)狀態(tài)對象的管理、更新和遷移。首先,在初始化的階段,將在上面步驟中實體的所有狀態(tài),都加入到狀態(tài)機(jī)對象GKStateMachine進(jìn)行管理。
// 1,初始化各個狀態(tài)
WJSWorkState *workState = [WJSWorkState new];
WJSEatState *eatState = [WJSEatState new];
WJSSleepState *sleepState = [WJSSleepState new];
// 2,初始化狀態(tài)機(jī),并將各個狀態(tài),加入其當(dāng)前管理的狀態(tài)機(jī)對象
_stateMachine = [GKStateMachine stateMachineWithStates:@[workState,eatState, sleepState]];
// 3,進(jìn)入work狀態(tài)
[_stateMachine enterState:[workState class]];
其次,狀態(tài)機(jī)對象負(fù)責(zé)狀態(tài)的更新和狀態(tài)的遷移,這里涉及兩層意思:
(1)狀態(tài)的更新:指的是當(dāng)前狀態(tài)的更新。在整個程序系統(tǒng)運行的時候,當(dāng)前狀態(tài)也許需要不斷的更新、計算和執(zhí)行規(guī)定操作。調(diào)用狀態(tài)機(jī)的updateWithDelta:方法,狀態(tài)機(jī)會調(diào)用當(dāng)前狀態(tài)的updateWithDelta:方法,開發(fā)者在GKState里面覆寫該方法,填入相應(yīng)的更新邏輯,就可以對當(dāng)前狀態(tài)進(jìn)行更新。
// 狀態(tài)機(jī)更新當(dāng)前狀態(tài)的更新函數(shù)
[_stateMachine updateWithDeltaTime:1];
(2)狀態(tài)的遷移:從當(dāng)前狀態(tài)遷移到下一個狀態(tài)。GKState里面提供的回調(diào),提供給開發(fā)者作為狀態(tài)遷移邏輯代碼的處理。
// 狀態(tài)機(jī)進(jìn)行狀態(tài)遷移
[_stateMachine enterState:[workState class]];
狀態(tài)對象的活動:進(jìn)入新的狀態(tài)前,需要檢查狀態(tài)的可靠性;如果可靠,需要調(diào)用狀態(tài)遷移提供的方法,進(jìn)行業(yè)務(wù)邏輯處理,相應(yīng)需要覆寫的方法如下:
// 1,狀態(tài)遷移時,填寫邏輯代碼的位置
// 離開當(dāng)前狀態(tài)時,調(diào)用該方法,參數(shù)是下一個狀態(tài)
- (void)willExitWithNextState:(GKState *)nextState {
NSLog(@"[WJSState Eat] willExitWithNextState:%@", nextState);
}
// 進(jìn)入當(dāng)前狀態(tài)時,調(diào)用該方法,參數(shù)是上一個狀態(tài)
- (void)didEnterWithPreviousState:(GKState *)previousState {
[super didEnterWithPreviousState:previousState];
NSLog(@"[WJSState Eat] didEnterWithPreviousState:%@", previousState);
}
// 2,狀態(tài)更新
// 狀態(tài)機(jī)調(diào)用updateWithDeltaTime時,狀態(tài)機(jī)會調(diào)用當(dāng)前狀態(tài)的updateWithDeltaTime方法
- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
NSLog(@"[WJSState Eat] updateWithDeltaTime");
}
為了更方便的了解狀態(tài)機(jī)模式的使用,我將小明例子的demo代碼上傳到了Github,地址點我點我!
點擊update按鈕,狀態(tài)更新,實際調(diào)用的是當(dāng)前狀態(tài)里的updateWithDeltaTime:方法。點擊change按鈕,狀態(tài)按照設(shè)定遷移,當(dāng)前狀態(tài)離開的時候,調(diào)用willExitWithNextState方法。進(jìn)入新的狀態(tài)后,調(diào)用新狀態(tài)的didEnterWithPreviousState方法。
3,使用總結(jié)
因此使用狀態(tài)機(jī)模式的步驟按照以下步驟進(jìn)行:
(1)分析好需求,理清實體不同狀態(tài)的更新和遷移邏輯,畫出狀態(tài)機(jī)的設(shè)計圖。
(2)使用GKState,實現(xiàn)具體狀態(tài)。
(3)使用GKStateMachine,在不同處理邏輯里,實現(xiàn)狀態(tài)的遷移。
三,Maze游戲里面如何使用狀態(tài)機(jī)。
Maze游戲中,由于Player(就是那個菱形◇)是玩家控制的,需要管理的就只有兩個狀態(tài)“生和死”。所以并不需要多么復(fù)雜的邏輯。但是enemies(四個方塊)們就不一樣了,他們的狀態(tài)根據(jù)情況有四種,如下圖所示(圖是蘋果提供的):
Maze狀態(tài)機(jī)Maze狀態(tài)機(jī),Enemy的四種狀態(tài)之間的遷移邏輯:
(1)Flee(逃離)狀態(tài)和Chase(捕獵)狀態(tài)的遷移是依賴“Player gets power up”,即玩家輸入(單擊屏幕),玩家power up,狀態(tài)從Chase遷移到Flee。一旦power up的時間到了,狀態(tài)從Flee遷移回Chase狀態(tài)。
// 進(jìn)入Chase狀態(tài),調(diào)用Sprite組件,恢復(fù)enemies的外在
- (void)didEnterWithPreviousState:(__nullable GKState *)previousState {
// Set the enemy sprite to its normal appearance, undoing any changes that happened in other states.
AAPLSpriteComponent *component = (AAPLSpriteComponent *)
[self.entity componentForClass:[AAPLSpriteComponent class]];
[component useNormalAppearance];
}
// 進(jìn)入Flee狀態(tài),調(diào)用Sprite組件,改變enemies的外在,并設(shè)定逃離目標(biāo)(隨機(jī)函數(shù))。
- (void)didEnterWithPreviousState:(__nullable GKState *)previousState {
AAPLSpriteComponent *component = (AAPLSpriteComponent *)
[self.entity componentForClass:[AAPLSpriteComponent class]];
[component useFleeAppearance];
// Choose a location to flee towards.
self.target = [[self.game.random arrayByShufflingObjectsInArray:self.game.level.enemyStartPositions] firstObject];
}
(2)Flee(逃離)狀態(tài)到Defeated(被擊敗)狀態(tài)的遷移,依賴物理碰撞檢測系統(tǒng)。在初始化階段,定義了enemies和player的物理檢測實體范圍和碰撞回調(diào)。如果檢測到回調(diào),在回調(diào)里面調(diào)用GKStateMachine進(jìn)行狀態(tài)遷移。
- (void)didBeginContact:(SKPhysicsContact *)contact {
// 1,發(fā)生碰撞時(碰撞檢測由引擎負(fù)責(zé)),調(diào)用該函數(shù)。
AAPLSpriteNode *enemyNode;
if (contact.bodyA.categoryBitMask == ContactCategoryEnemy) {
enemyNode = (AAPLSpriteNode *)contact.bodyA.node;
}
else if (contact.bodyB.categoryBitMask == ContactCategoryEnemy) {
enemyNode = (AAPLSpriteNode *)contact.bodyB.node;
}
NSAssert(enemyNode != nil, @"Expected player-enemy/enemy-player collision");
// 2,如果enemy處于chase狀態(tài),player掛掉。反之,enemy切換入defeated狀態(tài)
AAPLEntity *entity = (AAPLEntity *)enemyNode.owner.entity;
AAPLIntelligenceComponent *aiComponent = (AAPLIntelligenceComponent *)[entity componentForClass:[AAPLIntelligenceComponent class]];
if ([aiComponent.stateMachine.currentState isKindOfClass:[AAPLEnemyChaseState class]]) {
[self playerAttacked];
}
else {
// Otherwise, that enemy enters the Defeated state only if in a state that allows that transition.
[aiComponent.stateMachine enterState:[AAPLEnemyDefeatedState class]];
}
}
(3)Defeated(被擊敗)狀態(tài)經(jīng)過不斷的更新,回到了重生點,就遷移到了Respawn(重生)狀態(tài)
// 在defeated狀態(tài)里,enemy對象尋路回到重生點,到了重生點后。調(diào)用狀態(tài)機(jī),進(jìn)入重生Respawn狀態(tài)
NSArray<GKGridGraphNode *> *path = [graph findPathFromNode:enemyNode toNode:self.respawnPosition];
[component followPath:path completion:^{
[self.stateMachine enterState:[AAPLEnemyRespawnState class]];
}];
(4)在重生Respwan狀態(tài),重生時間到了,就回到了Chase(捕獵)狀態(tài)。這里的倒計時,是stateMachine采用updateWithDeltaTime自減時間變量實現(xiàn)。
// 1,從Defeated狀態(tài)進(jìn)入Respawn狀態(tài),調(diào)用該函數(shù)
- (void)didEnterWithPreviousState:(__nullable GKState *)previousState {
// 2,倒計時static變量置為10
static const NSTimeInterval defaultRespawnTime = 10;
self.timeRemaining = defaultRespawnTime;
// 3,調(diào)用Sprite組件,設(shè)置重生動畫
AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entity componentForClass:[AAPLSpriteComponent class]];
component.pulseEffectEnabled = YES;
}
// 4, _stateMachine受系統(tǒng)的updateWithDeltaTime驅(qū)動,進(jìn)行倒計時自減。倒計時到后,進(jìn)入Chase狀態(tài)。
- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
self.timeRemaining -= seconds;
if (self.timeRemaining < 0) {
[self.stateMachine enterState:[AAPLEnemyChaseState class]];
}
}
// 5,從當(dāng)前Respawn狀態(tài)進(jìn)入Chase狀態(tài),調(diào)用Sprite組件,改變外在。
- (void)willExitWithNextState:(GKState * __nonnull)nextState {
// Restore the sprite's original appearance.
AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entity componentForClass:[AAPLSpriteComponent class]];
component.pulseEffectEnabled = NO;
}
在Xcode中搜索stateMachine,看看Maze里enemies狀態(tài)的變遷,使用stateMachine調(diào)用位置,這里總結(jié)下:
(1)響應(yīng)玩家點擊時,進(jìn)行power up。
(2)物理碰撞檢測回調(diào)里調(diào)用。
(3)狀態(tài)更新調(diào)用updateWithDelta時,進(jìn)行調(diào)用。
實際上,驅(qū)動游戲里狀態(tài)機(jī)更新的力量和方式,在我上一篇文章的圖里(上篇文章的圖可能有點錯誤,這里修改下),已經(jīng)比較清晰:
componetSysteme的updateWithDelta:方法,會調(diào)用stateMachine的updateWithDelta:方法,進(jìn)而調(diào)用當(dāng)前狀態(tài)的updateWithDelta:方法,這樣實現(xiàn)狀態(tài)的更新。
四,何去何從
除了前兩篇文章所術(shù)的ECS和狀態(tài)機(jī),我還將撰寫兩篇文章,描述Maze游戲里出現(xiàn)的技術(shù)。
1, 尋路系統(tǒng)。
2,隨機(jī)數(shù),rule system。