转载

评鉴Maze源码(2):GamePlayKit的状态机

上一篇 文章《评鉴Maze源码(1):GamePlayKit的ECS“实体-组件-系统”》 里,我已经介绍了在Maze游戏中的ECS方法,这个方法里面,关于Enemy实体的行为,需要状态机来配合管理,这一篇文章,我就跟大家介绍一下GameplayKit里面状态机的使用。

一,状态机的介绍

状态机能够准确的表达同一实体,不同阶段的状态和状态迁移条件。

1,状态,可能是实体对象的属性,也可能是属性集合。

2,迁移条件,指的是外界的突发事件及满足特殊条件的属性变化。

游戏里面的实体会存在很多状态,比如苹果的SceneKit,女探险家运动的几个状态,在游戏过程中女探险家在这几个状态里面迁移。

评鉴Maze源码(2):GamePlayKit的状态机
状态机示意图

其分为,Running状态、Jumping状态和Falling状态。状态迁移条件表明在带箭头的直线上面。状态机模式给我们编写程序带来明显的好处,通过条件判断,方便的管理对象实体的状态。

关于状态机的实现,有很多方式,其中比较朴素的是if-else的判断,如果状态多,根据需求,状态迁移也会有不断的变化,那么if-else的编程会带来很多代码维护的问题。《Head first 设计模式》(Head first是我觉得很轻松愉快的一个系列读物,推荐想要进入一个新的技术,却苦于无法迅速入门的同学。但是入门后,仍然需要毅力和付出来完全掌握这项技术,对任何事情都是如此。)为我们提供了很好的状态机编程模式的教学,教会我们简单,可扩展性的状态机设计模式实现。

但是,在iOS里面,我们再也不用担心状态机的代码编写问题了,因为苹果实现状态机模式,我们掌握如何使用就行。而且不仅仅是游戏开发,在别的APP应用中,也能很从容的使用GameplayKit所提供的状态机框架。

二,GameplayKit里面的状态机API

朴素的状态机实现方法,这里提一下,就是为了对比状态机模式的实现方法。

比如小明的状态,定义为下面这三种,吃饭,睡觉和工作。小明作为一个对象,里面有currentState这一属性,代表当前小明的状态。暂且将currentState定为int型,吃饭、睡觉和工作类型值分别是1,2,3。暂且将小明的状态机变化简化为“吃饭->睡觉->工作->吃饭”这一循环。实现代码,小明对象提供一个changeState的方法,其参数是下一个要变化的状态。changeState的实现:

Objective-C

- (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; }
- (Bool)changeState:(XMState*)state {            switch(state):       {           caseeat:               // 判断当前状态work到下一步迁移状态eat的有效性               if (self.currentState == work) {                   // 进行状态迁移                   self.currentState = state;                   return YES;               }               return NO;           casesleep:                if (self.currentState == eat) {                   self.currentState = state;                   return YES;               }               return NO;           casework:                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)状态对象本身属性和管理状态迁移的有效性。如:

Objective-C

// 验证下一个状态是否有效,如果无效的话,是不会发生状态迁移的 - (BOOL)isValidNextState:(Class)stateClass {     return stateClass == [WJSSleepState class]; }
// 验证下一个状态是否有效,如果无效的话,是不会发生状态迁移的 - (BOOL)isValidNextState:(Class)stateClass {     return stateClass == [WJSSleepState class]; } 

(2)为状态的更新和迁移提供了填写逻辑代码的位置。在实体的状态进行更新或者迁移的时候,需要开发者填入相应的逻辑来完成实体状态的变化。

(3)按照上面小明同学的“吃饭,睡觉和工作”三个状态,定义这三个状态。

Objective-C

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

如何驱动实体进行状态的更新和迁移呢?即朴素编程里面的changeState方法。GameplayKit提供了状态机对象GKStateMachine来对状态GKState进行管理。

2,驱动状态变化的状态机对象GKStateMachine

GameplayKit提供管理状态迁移的状态机对象GKStateMachine,实现状态对象的管理、更新和迁移。

首先,在初始化的阶段,将在上面步骤中实体的所有状态,都加入到状态机对象GKStateMachine进行管理。

Objective-C

// 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,初始化各个状态 WJSWorkState *workState = [WJSWorkState new]; WJSEatState *eatState = [WJSEatState new]; WJSSleepState *sleepState = [WJSSleepState new];   // 2,初始化状态机,并将各个状态,加入其当前管理的状态机对象 _stateMachine = [GKStateMachinestateMachineWithStates:@[workState, eatState, sleepState]];   // 3,进入work状态 [_stateMachineenterState:[workStateclass]]; 

其次,状态机对象负责状态的更新和状态的迁移,这里涉及两层意思:

(1)状态的更新:指的是当前状态的更新。在整个程序系统运行的时候,当前状态也许需要不断的更新、计算和执行规定操作。调用状态机的updateWithDelta:方法,状态机会调用当前状态的updateWithDelta:方法,开发者在GKState里面覆写该方法,填入相应的更新逻辑,就可以对当前状态进行更新。

Objective-C

// 状态机更新当前状态的更新函数 [_stateMachine updateWithDeltaTime:1];
// 状态机更新当前状态的更新函数 [_stateMachineupdateWithDeltaTime:1]; 

(2)状态的迁移:从当前状态迁移到下一个状态。GKState里面提供的回调,提供给开发者作为状态迁移逻辑代码的处理。

Objective-C

// 状态机进行状态迁移 [_stateMachine enterState:[workState class]];
// 状态机进行状态迁移 [_stateMachineenterState:[workStateclass]]; 

状态对象的活动:进入新的状态前,需要检查状态的可靠性;如果可靠,需要调用状态迁移提供的方法,进行业务逻辑处理,相应需要覆写的方法如下:

Objective-C

// 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"); }
// 1,状态迁移时,填写逻辑代码的位置 // 离开当前状态时,调用该方法,参数是下一个状态 - (void)willExitWithNextState:(GKState *)nextState {     NSLog(@"[WJSState Eat] willExitWithNextState:%@", nextState); }   // 进入当前状态时,调用该方法,参数是上一个状态 - (void)didEnterWithPreviousState:(GKState *)previousState {     [superdidEnterWithPreviousState: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(四个方块)们就不一样了,他们的状态根据情况有四种,如下图所示(图是苹果提供的):

评鉴Maze源码(2):GamePlayKit的状态机
Maze状态机

Enemy的四种状态之间的迁移逻辑:

(1)Flee(逃离)状态和Chase(捕猎)状态的迁移是依赖“Player gets power up”,即玩家输入(单击屏幕),玩家power up,状态从Chase迁移到Flee。一旦power up的时间到了,状态从Flee迁移回Chase状态。

Objective-C

// 进入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]; }
// 进入Chase状态,调用Sprite组件,恢复enemies的外在 - (void)didEnterWithPreviousState:(__nullableGKState *)previousState {     // Set the enemy sprite to its normal appearance, undoing any changes that happened in other states.     AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entitycomponentForClass:[AAPLSpriteComponent class]];     [componentuseNormalAppearance]; }   // 进入Flee状态,调用Sprite组件,改变enemies的外在,并设定逃离目标(随机函数)。 - (void)didEnterWithPreviousState:(__nullableGKState *)previousState {     AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entitycomponentForClass:[AAPLSpriteComponent class]];     [componentuseFleeAppearance];    // Choose a location to flee towards.  self.target = [[self.game.randomarrayByShufflingObjectsInArray:self.game.level.enemyStartPositions]firstObject]; } 

(2)Flee(逃离)状态到Defeated(被击败)状态的迁移,依赖物理碰撞检测系统。在初始化阶段,定义了enemies和player的物理检测实体范围和碰撞回调。如果检测到回调,在回调里面调用GKStateMachine进行状态迁移。

Objective-C

- (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]];     } }
- (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 *)[entitycomponentForClass:[AAPLIntelligenceComponent class]];     if ([aiComponent.stateMachine.currentStateisKindOfClass:[AAPLEnemyChaseState class]]) {         [self playerAttacked];     }     else {         // Otherwise, that enemy enters the Defeated state only if in a state that allows that transition.         [aiComponent.stateMachineenterState:[AAPLEnemyDefeatedState class]];     } } 

(3)Defeated(被击败)状态经过不断的更新,回到了重生点,就迁移到了Respawn(重生)状态

Objective-C

// 在defeated状态里,enemy对象寻路回到重生点,到了重生点后。调用状态机,进入重生Respawn状态 NSArray<GKGridGraphNode *> *path = [graph findPathFromNode:enemyNode toNode:self.respawnPosition];     [component followPath:path completion:^{         [self.stateMachine enterState:[AAPLEnemyRespawnState class]];     }];
// 在defeated状态里,enemy对象寻路回到重生点,到了重生点后。调用状态机,进入重生Respawn状态 NSArray<GKGridGraphNode *> *path = [graphfindPathFromNode:enemyNodetoNode:self.respawnPosition];     [componentfollowPath:pathcompletion:^{         [self.stateMachineenterState:[AAPLEnemyRespawnState class]];     }]; 

(4)在重生Respwan状态,重生时间到了,就回到了Chase(捕猎)状态。这里的倒计时,是stateMachine采用updateWithDeltaTime自减时间变量实现。

Objective-C

// 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; }

// 1,从Defeated状态进入Respawn状态,调用该函数 - (void)didEnterWithPreviousState:(__nullableGKState *)previousState {       // 2,倒计时static变量置为10  static const NSTimeInterval defaultRespawnTime = 10;     self.timeRemaining = defaultRespawnTime;            // 3,调用Sprite组件,设置重生动画  AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entitycomponentForClass:[AAPLSpriteComponent class]];     component.pulseEffectEnabled = YES; }   // 4, _stateMachine受系统的updateWithDeltaTime驱动,进行倒计时自减。倒计时到后,进入Chase状态。 - (void)updateWithDeltaTime:(NSTimeInterval)seconds {     self.timeRemaining -= seconds;     if (self.timeRemaining < 0) {         [self.stateMachineenterState:[AAPLEnemyChaseState class]];     } }   // 5,从当前Respawn状态进入Chase状态,调用Sprite组件,改变外在。 - (void)willExitWithNextState:(GKState * __nonnull)nextState {     // Restore the sprite's original appearance.     AAPLSpriteComponent *component = (AAPLSpriteComponent *)[self.entitycomponentForClass:[AAPLSpriteComponent class]];     component.pulseEffectEnabled = NO; } 

在Xcode中搜索stateMachine,看看Maze里enemies状态的变迁,使用stateMachine调用位置,这里总结下:

(1)响应玩家点击时,进行power up。

(2)物理碰撞检测回调里调用。

(3)状态更新调用updateWithDelta时,进行调用。

实际上,驱动游戏里状态机更新的力量和方式,在我上一篇文章的图里(上篇文章的图可能有点错误,这里修改下),已经比较清晰:

评鉴Maze源码(2):GamePlayKit的状态机
enemy状态更新图示(修改后)

componetSysteme的updateWithDelta:方法,会调用stateMachine的updateWithDelta:方法,进而调用当前状态的updateWithDelta:方法,这样实现状态的更新。

四,何去何从

除了前两篇文章所术的ECS和状态机,我还将撰写两篇文章,描述Maze游戏里出现的技术。

1, 寻路系统。

2,随机数,rule system。

原文  http://www.tallmantech.com/archives/303
正文到此结束
Loading...