How to Integrate the Finite State Machine Compiler With Cocos2D

| Comments

I was looking for an example of how to write a FSM in Objective-C, in my research i found more than one example, in fact what i found was a tool that make my states patterns for me!, believe me, once you start using it you will be totally amazed.

Mr. CaveMan

In this tutorial, you will learn how to make a simple enemy AI with the FMS, Mr. CaveMan it’s beating someone!

To see what i mean, check out this video:

The Finite State Machine Compiler

The SMC it’s a Java application that help us to implement a Finite State Machine in your code in a blink of an eye, we just put our state diagram in a file using an easy-to-understand language and SMC will generate the state pattern classes!!, it works on any platform where Java 1.6.0 or higher is supported, the generated source code it’s no only for Objective-C, it supports:

  • C
  • C++
  • C#
  • Groovy
  • Java
  • JavaScript
  • Lua
  • Objective-C
  • Perl
  • Php
  • Python
  • Ruby
  • Scala
  • Tcl
  • VB.net

Setting up the SMC

first we need to download the latest version of SMC from here, next, unzip the contents of the ZIP file that you have just downloaded to a new directory (i saved in the root directory, because it’s easy to remember), and that’s all!.

SMC by example

Finally we are going to write some code, we are going to use a FSM to define a very basic enemy AI (that you could extend to whatever you want), you can download the starting project from here, it’s a Cocos2D for iPhone application that contains all the assets that we’re going to use, our main character Mr. CaveMan. needs some help!, he can’t move, he just stays there doing nothing, don’t believe me? go ahead compile and run your project.

if you take a look at the CaveMan class, you can notice that has 3 CCAnimations.

1
2
3
4
5
6
7
8
9
@interface CaveMan : CCSprite {
    CCAnimation *standingAnim;
    CCAnimation *walkingAnim;
    CCAnimation *punchingAnim;
}

@property (nonatomic, retain) CCAnimation *standingAnim;
@property (nonatomic, retain) CCAnimation *walkingAnim;
@property (nonatomic, retain) CCAnimation *punchingAnim;

and… if you go to the implementation file at the init method, you can see the walking action that i made.

1
2
3
4
5
6
7
8
9
10
11
12
13
-(id) init {
    self = [super init];
    if (self != nil) {

        [self initAnimations];

        id action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkingAnim restoreOriginalFrame:NO]];
        [self runAction:action];

        [self schedule:@selector(stopWalking) interval:3.0];
    }
    return self;
}

What we need to do is that Mr. CaveMan moves! and changes his walking direction at some point and finally, Mr. CaveMan needs to use his mighty wooden club!

The CaveMan FSM

The CaveMan’s states are:

  • Standing: Mr CaveMan is waiting to do some work.
  • Walking: Mr CaveMan is actually moving, and he’s ready to beat someone.
  • Punching: Someone is taking a beating.

Some notes on this FSM:

  • The CaveMan object starts in the Standing state.
  • The Standing state is reached only when the Punching state completes.

Creating an SMC .sm File

open your terminal and navigate to your project directory and run the following command:

1
2
$ cd CaveMan/Assets
$ mkfile 1k CaveMan.sm

now add the following lines to the CaveMan.sm file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
%{
//
//CaveMan FSM
//
%}


%start CaveManMap::Standing
%class CaveMan
%include "CaveMandefs.h"
%header   CaveMan.h

%map CaveManMap
%%
// State      Transition      End State       Action(s)
// Start State definitions

// End State definitions
%%

The CaveMan.sm is a skeleton with no states or transitions defined. It contains only the following:

  • A verbatim code section which is copied verbatim into the generated source code file.
  • The %start keyword specifies the FSM’s start state. For the CaveMan FSM it is the Standing state.
  • The %class keyword which specifies the application class to which this FSM is associated.
  • The %include keyword specifies which file will be imported in the generated implementation source code.
  • The %header keyword specifies the header file where is declared the main class.
  • The %map keyword is the FSM’s name.

Defining FSM States

The state definitions are placed inside the %map CaveManMap %% … %% delimiter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.
.
.
%map CaveManMap
%%
// State      Transition      End State       Action(s)
Standing
{

}

Walking
{ 

}

Punching
{
  
}

%%

Defining FSM Transitions

A transition definition consists of four parts:

  • The transition name.
  • An optional transition guards (not used in CaveMan FSM).
  • The transition’s end state.
  • The transition’s actions .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
.
.
%map CaveManMap
%%
// State      Transition      End State       Action(s)
Standing
{
              walk            Walking         {}
}

Walking
{ 
              walk            Walking         {}
              punch           Punching        {}
              update          nil             {}
}

Punching
{
              stand           Standing        {}
}

%%

Defining FSM Transition Actions

Transition actions are the first coupling between the FSM and the application class CaveMan, actions are CaveMan methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
.
.
%map CaveManMap
%%
// State      Transition      End State       Action(s)
Standing
{
              walk            Walking         {}
}

Walking
{ 
              walk            Walking         {}
              punch           Punching        {}
              update          nil             {updateWalking();}
}

Punching
{
              stand           Standing        {}
}

%%

we just added one action: updateWalking it’s going to be the method that move’s Mr. CaveMan.

Defining FSM Default State

What happens if a state receives a transition that is not defined in that state? SMC has two separate mechanisms for handling that situation. The first is the “Default” state. Every %map may have a special state named “Default” (the uppercase D is significant). Like all other states, the Default state contains transitions, The second mechanism is the “Default” transition. This is placed inside a state and is used to back up all transitions, but it is not used in the CaveMan FSM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
.
.
%map CaveManMap
%%
// State      Transition      End State       Action(s)
Standing
{
              walk            Walking             {}
}

Walking
{ 
              walk            Walking         {}
              punch           Punching        {}
              update          nil             {updateWalking();}
}

Punching
{
              stand           Standing        {}
}

Default
{ 
              walk            nil             {}
              punch           nil             {}
              update          nil             {}
              stand           nil             {}
}

%%

Using nil as the next state causes the transition to remain in the current state and not leave it. This means that the state’s exit and entry actions are not executed.

Defining State Entry/Exit Actions

When a transition leaves a state, it executes the state’s exit actions before any of the transition actions. When a transition enters a state, it executes the state’s entry actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
.
.
.
%map CaveManMap
%%
// State      Transition      End State       Action(s)
Standing
Entry {
    runAnimation(kStandingAnimation);
}
Exit {
    startPunchTimer();
}
{
              walk            Walking             {}
}

Walking
Entry {
    runAnimation(kWalkingAnimation);
  startWalkingTimer();
  initCaveManDirection();
}
Exit {
    stopWalkingTimer();
}
{ 
              walk            Walking         {}
              punch           Punching        {}
              update          nil             {updateWalking();}
}

Punching
Entry {
    runAnimation(kPunchingAnimation);
  stopPunchTimer();
}
{
              stand           Standing        {}
}

Default
{ 
              walk            nil             {}
              punch           nil             {}
              update          nil             {}
              stand           nil             {}
}


%%

In all Entry actions of each state we are going to call the runAnimation method and pass an AnimationID to know which animation to run, the kStandingAnimation, kWalkingAnimation and kPunchingAnimation are defined in the CaveMandefs.h that we are going to create later. Also keep an eye on the actions that start and stop timers.

Compiling the CaveMan.sm

Let´s do the magic!, from your terminal run the following

1
$ java -jar /smc/bin/Smc.jar -objc -d ./../ CaveMan.sm
  • the /smc/bin/Smc.jar is the path to the compiler
  • the -objc is the output language
  • the -d ./../ is the output folder, here we are choosing the parent from the Assets folder.
  • the CaveMan.sm is the .sm to process.

take a look at the generated source code and be amazed!

Adding a state machine to your Project

The SMC-generated code is designed to be loosely coupled with your application. The only changes that we need to make in our project is to:

  • Include the SMC class definitions into your application: first create a new group in xcode and name it SMC, then add the statemap.h & statemap.m they are located in smc/lib/ObjC folder.
  • Include the state machine’s source files: select the CaveMan_sm.h & CaveMan_sm.m from finder and add them to the GameObjects group in your xcode project.

next create a new header file and name it CaveMandefs, and include the following lines:

1
2
3
4
5
6
7
8
9
10
11
#ifndef CaveMan_CaveMandefs_h
#define CaveMan_CaveMandefs_h

typedef enum
{
    kStandingAnimation = 0,
    kWalkingAnimation,
    kPunchingAnimation
} CaveManAnimation;

#endif

now replace the contents of CaveMan.h with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "cocos2d.h"
#import "CaveMandefs.h"

@class CaveManContext;

@interface CaveMan : CCSprite {
    CCAnimation *standingAnim;
    CCAnimation *walkingAnim;
    CCAnimation *punchingAnim;

    CaveManContext *_fsm;
}

@property (nonatomic, retain) CCAnimation *standingAnim;
@property (nonatomic, retain) CCAnimation *walkingAnim;
@property (nonatomic, retain) CCAnimation *punchingAnim;

-(void)runAnimation:(CaveManAnimation)anim;
-(void)startPunchTimer;
-(void)stopPunchTimer;
-(void)startWalkingTimer;
-(void)stopWalkingTimer;
-(void)initCaveManDirection;
-(void)updateWalking;

@end

Here is what is happening at the CaveMan.h class:

  • we are importing the Mr. CaveMan animation ids: #import "CaveMandefs.h"
  • we are forward declaring the FSM class by adding this line: @class CaveManContext;
  • adding the member data to the class CaveMan: CaveManContext *_fsm;
  • finally we are declaring the CaveMan public methods that we defined in the CaveMan FMS.

now open CaveMan.m and replace the contents of it with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#import "CaveMan.h"
#import "CaveMan_sm.h"

@implementation CaveMan

@synthesize standingAnim;
@synthesize walkingAnim;
@synthesize punchingAnim;

-(void) dealloc {
    [standingAnim release];
    [walkingAnim release];
    [punchingAnim release];
    [super dealloc];
}

-(CCAnimation *)getAnimation:(NSString *)animationName withTotalFrames:(int)totalFrames {

    NSMutableArray *animFrames = [NSMutableArray array];

    for(int i = 0; i <= totalFrames; ++i) {
        NSString *file = [NSString stringWithFormat:@"%@.swf/%04d", animationName, i];
        [animFrames addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:file]];
    }

    return [CCAnimation animationWithFrames:animFrames delay:0.04];
}

-(void)initAnimations {

    [self setStandingAnim: [self getAnimation:@"standing" withTotalFrames:67]];
    [self setWalkingAnim:[self getAnimation:@"walking" withTotalFrames:23]];
    [self setPunchingAnim:[self getAnimation:@"punching" withTotalFrames:77]];

}

-(void)stopWalking {

    [self stopAllActions];

    id action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:standingAnim restoreOriginalFrame:NO]];
    [self runAction:action];

    [self unschedule:@selector(stopWalking)];

}

-(id) init {
    self = [super init];
    if (self != nil) {

        [self initAnimations];

        _fsm = [[CaveManContext alloc] initWithOwner:self];
        [_fsm setDebugFlag:YES];
        [_fsm enterStartState];

        id action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkingAnim restoreOriginalFrame:NO]];
        [self runAction:action];

        [self schedule:@selector(stopWalking) interval:3.0];
    }
    return self;
}

-(void)runAnimation:(CaveManAnimation)anim {
    CCLOG(@"CaveMan.runAnimation: %d", anim);
}

-(void)startPunchTimer {
    CCLOG(@"CaveMan.startPunchTimer");
}

-(void)stopPunchTimer {
    CCLOG(@"CaveMan.stopPunchTimer");
}

-(void)startWalkingTimer {
    CCLOG(@"CaveMan.startWalkingTimer");
}

-(void)stopWalkingTimer {
    CCLOG(@"CaveMan.stopWalkingTimer");
}

-(void)initCaveManDirection {
   CCLOG(@"CaveMan.initCaveManDirection");
}

-(void)updateWalking {
   CCLOG(@"CaveMan.updateWalking");
}

@end

notice that we are importing the FMS header: #import "CaveMan_sm.h
and allocating the FMS in the init method

1
2
3
_fsm = [[CaveManContext alloc] initWithOwner:self];
[_fsm setDebugFlag:YES];
[_fsm enterStartState];

compile and run, you will see in the output the following:

With this we completes the first part of this tutorial, you can download from here.
Take a deep breath, we’re almost getting there!, we are going to implement our game logic, and having some fun with Mr. CaveMan beating and moving.

Implement Game Logic

Open CaveMan.m and include the following lines, above the init method:

1
2
3
4
5
6
7
8
9
10
11
-(void)stand {
    [_fsm stand];
}

-(void)walk {
    [_fsm walk];
}

-(void)punch {
    [_fsm punch];
}

these methods only hide the FMS delegate class. (Maybe you want to make them public)
now go to the runAnimation:(CaveManAnimation)anim method and paste the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-(void)runAnimation:(CaveManAnimation)anim {

    CCLOG(@"CaveMan.runAnimation: %d", anim);

    id action = nil;
    id callback = nil;
    id animation = nil;

    switch (anim) {
        case kStandingAnimation:

            animation = [CCAnimate actionWithAnimation:standingAnim restoreOriginalFrame:NO];

            callback = [CCCallBlock actionWithBlock:^{
                [self walk];
            }];

            action = [CCSequence actions: animation, callback, nil];
            break;

        case kWalkingAnimation:

            action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkingAnim restoreOriginalFrame:NO]];
            break;

        case kPunchingAnimation:

            animation = [CCAnimate actionWithAnimation:punchingAnim restoreOriginalFrame:NO];

            callback = [CCCallBlock actionWithBlock:^{
                [self stand];
            }];

            action = [CCSequence actions: animation, callback, nil];
            break;

        default:
            NSAssert( NO, @"Unknow CaveMan Animation" );
            break;
    }

    if (action != nil) {
        [self stopAllActions];
        [self runAction:action];
    }

}

Depending of the AnimationID, we are running the corresponding action, if you look closely, the CCCallBlocks are the one´s that make the calls to the next state.

Wooden club time!

Let´s create the timers that will activate the Wooden club beating!, locate the startPunchTimer and the stopPunchTimer and add the following code:

1
2
3
4
5
6
7
8
9
10
11
-(void)startPunchTimer {
    CCLOG(@"CaveMan.startPunchTimer");
    ccTime nextPunchTime = ((MAX_NEXT_PUNCH_TIME - MIN_NEXT_PUNCH_TIME) * CCRANDOM_0_1() + MIN_NEXT_PUNCH_TIME);
    CCLOG(@"nextPunchTime: %f", nextPunchTime);
    [self schedule:@selector(punch) interval: nextPunchTime];
}

-(void)stopPunchTimer {
    CCLOG(@"CaveMan.stopPunchTimer");
    [self unschedule:@selector(punch)];
}

the startPunchTimer method creates new schedule at random time: nextPunchTime.

open Constants.h and add the following lines

1
2
#define MIN_NEXT_PUNCH_TIME 4.0f
#define MAX_NEXT_PUNCH_TIME 10.0f

if you compile and run, you will see that Mr. CaveMan first animation is the standing, next he starts walking (but not moving) and then after a while he starts beating up!

you can download the second part of the tutorial from here.

Moon Walking!

Piufff, it´s been a long ride huh?? are you having fun?? let’s implement the final part, Mr CaveMan is going to move!!

but first we need to add a parameter to the update transition in the CaveMan FMS, that´s because we are going to pass the ccTime from the update method to the FMS to the delegate.

open your CaveMan.sm and add the following code in the Walking and Default states:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
.
.
Walking
Entry {
    runAnimation(kWalkingAnimation);
  startWalkingTimer();
  initCaveManDirection();
}
Exit {
    stopWalkingTimer();
}
{ 
              walk                    Walking         {}
              punch                   Punching        {}
              update(delta:ccTime)    nil             {updateWalking(delta);}
}

.
.
.

Default
{ 
              walk                    nil             {}
              punch                   nil             {}
              update(delta:ccTime)    nil             {}
              stand                   nil             {}
}
.
.
.

don´t forget to compile again the CaveMan.sm file.

now change the update method declaration in the CaveMan.h and the CaveMan.m to:

1
-(void)updateWalking:(ccTime)dt;

nothing new if you compile and run your project. Did you notice how easy it is to make a change to the FMS and update your implementation?? we need the (ccTime)dt because Mr. CaveMan movement will be based on time.

let´s move Mr. CaveMan!

open Constants.h and add the following lines

1
2
3
4
5
6
7
8
#define MIN_NEXT_WALKING_TIME 1.0f
#define MAX_NEXT_WALKING_TIME 3.0f

#define MAX_WALKING_RANGE 400.0f
#define WALKING_SPEED 50.0f

#define SCREEN_WIDTH [CCDirector sharedDirector].winSize.width
#define SCREEN_HEIGHT [CCDirector sharedDirector].winSize.height

Add new member variable to the class CaveMan: float vel;

copy the following methods above the init implementation

1
2
3
4
5
6
7
8
9
10
11
12
-(void) update:(ccTime)delta
{
    [_fsm update:delta];
}

-(void)fixFlipX {
   if (vel > 0) {
       [self setFlipX:YES];
   } else {
       [self setFlipX:NO];
   }
}

pay attention to [_fsm update:delta] that’s where we are passing the ccTime
the fixFlipX method just set’s the flipX property of the CCSprite, if the velocity is a negative value our direction it’s going to be left, if not… it’s going to be right.

now update the init method with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(id) init {
    self = [super init];
    if (self != nil) {

        [self initAnimations];

        _fsm = [[CaveManContext alloc] initWithOwner:self];
        [_fsm setDebugFlag:YES];
        [_fsm enterStartState];

        vel = WALKING_SPEED;

        [self scheduleUpdate];

    }
    return self;
}

the only changes that we made are the vel = WALKING_SPEED; to setting up Mr. CaveMan velocity and the [self scheduleUpdate]; to setup the schedule update.

next: update the startWalkingTimer and the stopWalkingTimer and initCaveManDirection with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-(void)startWalkingTimer {
    CCLOG(@"CaveMan.startWalkingTimer");
    ccTime nextWalkTime = ((MAX_NEXT_WALKING_TIME - MIN_NEXT_WALKING_TIME) * CCRANDOM_0_1() + MIN_NEXT_WALKING_TIME);
    CCLOG(@"nextWalkTime: %f", nextWalkTime);
    [self schedule:@selector(walk) interval: nextWalkTime];

}

-(void)stopWalkingTimer {
    CCLOG(@"CaveMan.stopWalkingTimer");
    [self unschedule:@selector(walk)];
}

-(void)initCaveManDirection {
   CCLOG(@"CaveMan.initCaveManDirection");

    float offset = (CCRANDOM_0_1() > 0.5f) ? 1 : -1;
  vel *= offset;
    [self fixFlipX];
}

You’ are already familiar with the startWalkingTimer method because it is the same logic as the startPunchTimer method, the initCaveManDirection start’s Mr. CaveMan walking direction.

and finally update the updateWalking:(ccTime)dt with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-(void)updateWalking:(ccTime)dt {
   CCLOG(@"CaveMan.updateWalking");

    const float xMax = (SCREEN_WIDTH * 0.5f) + MAX_WALKING_RANGE/2;
    const float xMin = (SCREEN_WIDTH * 0.5f) - MAX_WALKING_RANGE/2;

    CGPoint pos = [self position];
    pos.x += round(vel * dt);

    if (pos.x > xMax) {

        vel *= -1;
        pos.x = xMax;

        [self fixFlipX];

    }

    if (pos.x < xMin) {

        vel *= -1;
        pos.x = xMin;

        [self fixFlipX];
    }

    //CCLOG(@"pos: %@", NSStringFromCGPoint(pos));

    [self setPosition:pos];

}

this method what actually do is the Walking, we update Mr. CaveMan position and check for the walls to go back. And that’s it we’re done!! you can download the final part of the tutorial from here.

What do you think???
Did you liked the SMC??

if you wanna go deeper go and read the following resources:
SMC Programmer’s Manual
SMC Javadocs

SMC Slides:

Downloading the State Machine Compiler

Happy Coding.

Comments