你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 評鑒Maze源碼(2):GamePlayKit的狀態機

評鑒Maze源碼(2):GamePlayKit的狀態機

編輯:IOS開發基礎

QQ截圖20160323112422.png

上一篇文章《評鑒Maze源碼(1):GamePlayKit的ECS“實體-組件-系統”》裡,我已經介紹了在Maze游戲中的ECS方法,這個方法裡面,關於Enemy實體的行為,需要狀態機來配合管理,這一篇文章,我就跟大家介紹一下GameplayKit裡面狀態機的使用。

一、狀態機的介紹

狀態機能夠准確的表達同一實體,不同階段的狀態和狀態遷移條件。

1.狀態,可能是實體對象的屬性,也可能是屬性集合。

2.遷移條件,指的是外界的突發事件及滿足特殊條件的屬性變化。

游戲裡面的實體會存在很多狀態,比如蘋果的SceneKit,女探險家運動的幾個狀態,在游戲過程中女探險家在這幾個狀態裡面遷移。

屏幕快照-2016-02-17-下午6.36.56.png

狀態機示意圖

其分為,Running狀態、Jumping狀態和Falling狀態。狀態遷移條件表明在帶箭頭的直線上面。狀態機模式給我們編寫程序帶來明顯的好處,通過條件判斷,方便的管理對象實體的狀態。

關於狀態機的實現,有很多方式,其中比較樸素的是if-else的判斷,如果狀態多,根據需求,狀態遷移也會有不斷的變化,那麼if-else的編程會帶來很多代碼維護的問題。《Head first 設計模式》(Head first是我覺得很輕松愉快的一個系列讀物,推薦想要進入一個新的技術,卻苦於無法迅速入門的同學。但是入門後,仍然需要毅力和付出來完全掌握這項技術,對任何事情都是如此。)為我們提供了很好的狀態機編程模式的教學,教會我們簡單,可擴展性的狀態機設計模式實現。

但是,在iOS裡面,我們再也不用擔心狀態機的代碼編寫問題了,因為蘋果實現狀態機模式,我們掌握如何使用就行。而且不僅僅是游戲開發,在別的APP應用中,也能很從容的使用GameplayKit所提供的狀態機框架。

二、GameplayKit裡面的狀態機API

樸素的狀態機實現方法,這裡提一下,就是為了對比狀態機模式的實現方法。

比如小明的狀態,定義為下面這三種,吃飯,睡覺和工作。小明作為一個對象,裡面有currentState這一屬性,代表當前小明的狀態。暫且將currentState定為int型,吃飯、睡覺和工作類型值分別是1,2,3。暫且將小明的狀態機變化簡化為“吃飯->睡覺->工作->吃飯”這一循環。實現代碼,小明對象提供一個changeState的方法,其參數是下一個要變化的狀態。changeState的實現:

- (Bool)changeState:(XMState*)state {
     
      switch(state):
      {
           case eat:
               // 判斷當前狀態work到下一步遷移狀態eat的有效性
               if (self.currentState == work) {
                   // 進行狀態遷移
                   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;
}

這裡做的兩個工作,一個是判斷狀態遷移的有效性(判斷當前狀態),另一個是進行狀態的遷移(設置currentStatus狀態)。如果,加入新的狀態,或者狀態循環發生變化,狀態機的switch-case和if-else的判斷將會不斷增加,可維護性變差,代碼冗余將不斷上升。

然而,通過狀態機模式,可以使得代碼變得可維護和,GameplayKit提供了這一模式的實現,我們現在來好好掌握它。

1.狀態對象GKState

在GameplayKit裡面,蘋果有狀態對象GKState,來作為所有狀態的基類祖先。

對象GKState提供了一些方法,這些方法有兩類作用:

(1)狀態對象本身屬性和管理狀態遷移的有效性。如:

// 驗證下一個狀態是否有效,如果無效的話,是不會發生狀態遷移的
- (BOOL)isValidNextState:(Class)stateClass {
    return stateClass == [WJSSleepState class];
}

(2)為狀態的更新和遷移提供了填寫邏輯代碼的位置。在實體的狀態進行更新或者遷移的時候,需要開發者填入相應的邏輯來完成實體狀態的變化。

(3)按照上面小明同學的“吃飯,睡覺和工作”三個狀態,定義這三個狀態。

@interface WJSWorkState : WJSState

@end

@interface WJSEatState : WJSState
 
@end
 
@interface WJSSleepState : WJSState
 
@end

如何驅動實體進行狀態的更新和遷移呢?即樸素編程裡面的changeState方法。GameplayKit提供了狀態機對象GKStateMachine來對狀態GKState進行管理。

2.驅動狀態變化的狀態機對象GKStateMachine

GameplayKit提供管理狀態遷移的狀態機對象GKStateMachine,實現狀態對象的管理、更新和遷移。

首先,在初始化的階段,將在上面步驟中實體的所有狀態,都加入到狀態機對象GKStateMachine進行管理。

// 1,初始化各個狀態
WJSWorkState *workState = [WJSWorkState new];
WJSEatState *eatState = [WJSEatState new];
WJSSleepState *sleepState = [WJSSleepState new];
 
// 2,初始化狀態機,並將各個狀態,加入其當前管理的狀態機對象
_stateMachine = [GKStateMachine stateMachineWithStates:@[workState, eatState, sleepState]];
 
// 3,進入work狀態
[_stateMachine enterState:[workState class]];

其次,狀態機對象負責狀態的更新和狀態的遷移,這裡涉及兩層意思:

(1)狀態的更新:指的是當前狀態的更新。在整個程序系統運行的時候,當前狀態也許需要不斷的更新、計算和執行規定操作。調用狀態機的updateWithDelta:方法,狀態機會調用當前狀態的updateWithDelta:方法,開發者在GKState裡面覆寫該方法,填入相應的更新邏輯,就可以對當前狀態進行更新。

// 狀態機更新當前狀態的更新函數
[_stateMachine updateWithDeltaTime:1];

(2)狀態的遷移:從當前狀態遷移到下一個狀態。GKState裡面提供的回調,提供給開發者作為狀態遷移邏輯代碼的處理。

// 狀態機進行狀態遷移
[_stateMachine enterState:[workState class]];

狀態對象的活動:進入新的狀態前,需要檢查狀態的可靠性;如果可靠,需要調用狀態遷移提供的方法,進行業務邏輯處理,相應需要覆寫的方法如下:

// 1,狀態遷移時,填寫邏輯代碼的位置
// 離開當前狀態時,調用該方法,參數是下一個狀態 
- (void)willExitWithNextState:(GKState *)nextState {
    NSLog(@"[WJSState Eat] willExitWithNextState:%@", nextState);
}
 
// 進入當前狀態時,調用該方法,參數是上一個狀態
- (void)didEnterWithPreviousState:(GKState *)previousState {
    [super didEnterWithPreviousState:previousState];
    NSLog(@"[WJSState Eat] didEnterWithPreviousState:%@", previousState);
}
 
// 2,狀態更新
// 狀態機調用updateWithDeltaTime時,狀態機會調用當前狀態的updateWithDeltaTime方法
- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
    NSLog(@"[WJSState Eat] updateWithDeltaTime");
}

為了更方便的了解狀態機模式的使用,我將小明例子的demo代碼上傳到了Github,地址點我點我!

點擊update按鈕,狀態更新,實際調用的是當前狀態裡的updateWithDeltaTime:方法。

點擊change按鈕,狀態按照設定遷移,當前狀態離開的時候,調用willExitWithNextState方法。進入新的狀態後,調用新狀態的didEnterWithPreviousState方法。

3.使用總結

因此使用狀態機模式的步驟按照以下步驟進行:

(1)分析好需求,理清實體不同狀態的更新和遷移邏輯,畫出狀態機的設計圖。

(2)使用GKState,實現具體狀態。

(3)使用GKStateMachine,在不同處理邏輯裡,實現狀態的遷移。

三、Maze游戲裡面如何使用狀態機

Maze游戲中,由於Player(就是那個菱形◇)是玩家控制的,需要管理的就只有兩個狀態“生和死”。所以並不需要多麼復雜的邏輯。但是enemies(四個方塊)們就不一樣了,他們的狀態根據情況有四種,如下圖所示(圖是蘋果提供的):

23.png

23.png

Maze狀態機

Enemy的四種狀態之間的遷移邏輯:

(1)Flee(逃離)狀態和Chase(捕獵)狀態的遷移是依賴“Player gets power up”,即玩家輸入(單擊屏幕),玩家power up,狀態從Chase遷移到Flee。一旦power up的時間到了,狀態從Flee遷移回Chase狀態。

// 進入Chase狀態,調用Sprite組件,恢復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];
}
 
// 進入Flee狀態,調用Sprite組件,改變enemies的外在,並設定逃離目標(隨機函數)。
- (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(逃離)狀態到Defeated(被擊敗)狀態的遷移,依賴物理碰撞檢測系統。在初始化階段,定義了enemies和player的物理檢測實體范圍和碰撞回調。如果檢測到回調,在回調裡面調用GKStateMachine進行狀態遷移。

- (void)didBeginContact:(SKPhysicsContact *)contact {
       // 1,發生碰撞時(碰撞檢測由引擎負責),調用該函數。
   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狀態,player掛掉。反之,enemy切換入defeated狀態
   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(被擊敗)狀態經過不斷的更新,回到了重生點,就遷移到了Respawn(重生)狀態

// 在defeated狀態裡,enemy對象尋路回到重生點,到了重生點後。調用狀態機,進入重生Respawn狀態
NSArray*path = [graph findPathFromNode:enemyNode toNode:self.respawnPosition];
    [component followPath:path completion:^{
        [self.stateMachine enterState:[AAPLEnemyRespawnState class]];
    }];

(4)在重生Respwan狀態,重生時間到了,就回到了Chase(捕獵)狀態。這裡的倒計時,是stateMachine采用updateWithDeltaTime自減時間變量實現。

// 1,從Defeated狀態進入Respawn狀態,調用該函數
- (void)didEnterWithPreviousState:(__nullable GKState *)previousState {
       // 2,倒計時static變量置為10
   static const NSTimeInterval defaultRespawnTime = 10;
    self.timeRemaining = defaultRespawnTime;
    
      // 3,調用Sprite組件,設置重生動畫
   AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entity componentForClass:[AAPLSpriteComponent class]];
    component.pulseEffectEnabled = YES;
}
 
// 4, _stateMachine受系統的updateWithDeltaTime驅動,進行倒計時自減。倒計時到後,進入Chase狀態。
- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
    self.timeRemaining -= seconds;
    if (self.timeRemaining < 0) {
        [self.stateMachine enterState:[AAPLEnemyChaseState class]];
    }
}
 
// 5,從當前Respawn狀態進入Chase狀態,調用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狀態的變遷,使用stateMachine調用位置,這裡總結下:

(1)響應玩家點擊時,進行power up。

(2)物理碰撞檢測回調裡調用。

(3)狀態更新調用updateWithDelta時,進行調用。

實際上,驅動游戲裡狀態機更新的力量和方式,在我上一篇文章的圖裡(上篇文章的圖可能有點錯誤,這裡修改下),已經比較清晰:

屏幕快照-2016-03-01-上午10.51.57.png

enemy狀態更新圖示(修改後)

componetSysteme的updateWithDelta:方法,會調用stateMachine的updateWithDelta:方法,進而調用當前狀態的updateWithDelta:方法,這樣實現狀態的更新。

四、何去何從

除了前兩篇文章所術的ECS和狀態機,我還將撰寫兩篇文章,描述Maze游戲裡出現的技術。

1.尋路系統。

2.隨機數,rule system。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved