juhara.com
Language : English Indonesia

Object-Oriented Finite State Machine

Zamrony P Juhara
02 October 2006 11:22:00
 (3899 views)
Tutorial finite state machine implementation with object-oriented programming

Recently, I read book, Programming Game AI by Example, written by Mat Buckland. I was interested on state driven game agent concept discussed in that book. This article will discuss about this concept and of course Finite State Machine implementation with Delphi (the book uses C++).

What is Finite State Machine?

Finite state machine is device or model of device that has number of states, and at a time, can be in one of the state. It can take input and produce transition from one state to another state or action as output.

Example of usage of finite state machine is many. Easy example is 2D Animation with Direct3D Part 3 demo. In that demo Spiderman character can walk, punch, kick, etc. Walking, punching and kicking are some states where Spiderman can be in. Walking state, punching and kicking produce action output, i.e walking animation, punching and kicking animation.

In this article, we are going to improve state management system of 2D Animation with Direct3D demo in object-oriented finite state machine approach. In object-oriented finite state machine, state is modeled as object that can execute action. Action can be pure action or action that cause state transition to occur.

On above demo, state of Spiderman is kept in variable. Take a look at code snippet below.

procedure T2DEngine.Draw;
begin
  FBackground.Draw;

  FSprites.BeginDraw;

  //beri delay agar animasi
  //tidak terlalu cepat
  if delay>2 then
  begin
    case anim of
      animIdle:begin
                 //khusus animasi idle buat lebih slow
                 //soallnya framenya sedikit
                 if delay_idle>2 then
                 begin
                   Spiderman.Texture:=SpidermanIdleTexture[animIndex];
                   Spiderman.Y:=200;

                   inc(animIndex);
                   if animIndex>9 then
                      animIndex:=0;

                   delay_idle:=0;
                 end;
                 inc(delay_idle);
               end;
      animWalk:begin
                   Spiderman.Texture:=SpidermanWalk[animIndex];
                   Spiderman.Y:=205;
                   case dirType of
                     dirLeft:Spiderman.X:=Spiderman.X-6;
                     dirRight:Spiderman.X:=Spiderman.X+6;
                    end;

                   inc(animIndex);
                   if animIndex>11 then
                   begin
                    //animasi punch selesai
                    //kembalikan Spiderman ke animasi idle
                      anim:=animIdle;
                      animIndex:=0;
                   end;
               end;
      animPunch:begin
                  Spiderman.Texture:=SpidermanPunchTexture[animIndex];
                  Spiderman.Y:=202;

                  inc(animIndex);
                  if animIndex>5 then
                  begin
                    //animasi punch selesai
                    //kembalikan Spiderman ke animasi idle
                    anim:=animIdle;
                    animIndex:=0;
                  end;

                 end;
       animHeavyPunch:begin
                       Spiderman.Texture:=SpidermanHeavyPunchTexture[animIndex];
                       Spiderman.Y:=175;

                       inc(animIndex);
                       if animIndex>9 then
                       begin
                         //animasi heavy punch selesai
                         //kembalikan Spiderman ke animasi idle
                         anim:=animIdle;
                         animIndex:=0;
                       end;

                     end;
           animKick:begin
                      Spiderman.Texture:=SpidermanKickTexture[animIndex];
                      Spiderman.Y:=171;
                      inc(animIndex);
                      if animIndex>8 then
                      begin
                        anim:=animIdle;
                        animIndex:=0;
                      end;

                    end;
     end;
     delay:=0;
  end;
  Spiderman.Draw;
  FSprites.EndDraw;
  inc(delay);
end;

We use case..of to produce output of each state. If number of state is not many, this model is not bad, but what if number of state increase, for example, 20 or 30 states? case..of code will get longer and there is good chance that code will be harder to maintain.

Design and Implementation of Object-oriented Finite State Machine

Imagine this situation: we have smart dog-like robot, called Dog. Dog can do some actions such as playing with ball, rolling, eat and sleep. Each action is excuted with cartridges filled with programs plugged in a slot on Dog's body.

This cartridge represents state. Without cartdridge, Dog is only a statue that does nothing. Dog's main ability, except take cartridge and execute action programmed in cartridge (takes input and produce output in form of action), is ability to change cartdridge with another cartridge (produce state transition). Dog also has sensor to monitor battery level. If battery level drops to preset value, Dog will cancel execute current cartridge and replace it with another cartridge for example, battery charging cartridge.

Object-oriented finite state machine that we are going to develop is similar to cartridge system above. State object (TAIAction) can contain command for excuting action or state transition. To execute command, state object is provided with method ExecuteAction. Because state object is base object, what action takes place in ExecuteAction is full responsibility of state object derived class, ExecuteAction must be virtual method so it can be overriden by derived class.

State also has method that will get executed when state transition occurs, i.e when entering new state, EnterAction and when state is about to exit, ExitAction. Purpose of EnterAction and ExitAction is as follow: consider yourself building WarCraft-like game. You want warrior character in battle yell "Hurray" when there is state transition between combat state into stand-by state when enemies they face are lose. "Hurray" yelling action code can be put in ExitAction combat state or EnterAction of stand-by state.

Our framework will look like following:

  TAIAction=class(TObject)
  private
  public
    procedure EnterAction(entity:TAIEntity);virtual;
    procedure ExecuteAction(entity:TAIEntity);virtual;
    procedure ExitAction(entity:TAIEntity);virtual;
  end;

States is executed by character in game, I call it entity (TAIEntity). Entity stores current state. If this state changed, ExitAction of old state is excuted, then new state is stored and EnterAction of new state is excuted.

Entity also has property to store previous state. We need it for example in this situation: in Warcraft, worker is mining gold. When gold is enough for him to carry, he puts gold in granary and then return to mining field to continue gold mining work. State transition occurs here, from mining state to storing gold state. After storing gold is executed, state is changed to previous state (mining state) and excute previous state.

With ability to store previous state, change to previous state is easy.

type
  TAIAction=class;

  TAIEntity=class(TObject)
  private
    FPreviousAction: TAIAction;
    FCurrentAction: TAIAction;
    procedure SetCurrentAction(const Value: TAIAction);
    procedure SetPreviousAction(const Value: TAIAction);
  public
    procedure Update;
  published
    property CurrentAction:TAIAction read FCurrentAction write SetCurrentAction;
    property PreviousAction:TAIAction read FPreviousAction write SetPreviousAction;
  end;

Entity has Update method. In this method, ExecuteAction of current state is executed. This method is meant for safe execution of ExcuteAction to avoid access violation because CurrentAction may contain nil value. Ok, below is complete implementation

{-------------------------------
Implementasi Finite State Machine
menggunakan object-oriented
programming
--------------------------------
(c) 2006 Zamrony P Juhara
website: http://www.juhara.com
mail: zamronyp@juhara.com
-------------------------------}
unit u_ai;

interface
uses classes,sysutils;

type
  TAIAction=class;

  TAIEntity=class(TObject)
  private
    FPreviousAction: TAIAction;
    FCurrentAction: TAIAction;
    procedure SetCurrentAction(const Value: TAIAction);
    procedure SetPreviousAction(const Value: TAIAction);
  public
    procedure Update;
  published
    property CurrentAction:TAIAction read FCurrentAction write SetCurrentAction;
    property PreviousAction:TAIAction read FPreviousAction write SetPreviousAction;
  end;

  TAIAction=class(TObject)
  private
  public
    procedure EnterAction(entity:TAIEntity);virtual;
    procedure ExecuteAction(entity:TAIEntity);virtual;
    procedure ExitAction(entity:TAIEntity);virtual;
  end;

implementation

{ TAIEntity }

procedure TAIEntity.SetCurrentAction(const Value: TAIAction);
begin
  //run ExitAction aksi lama sebelum berpindah ke aksi baru
  if FCurrentAction<>nil then
    FCurrentAction.ExitAction(self);

  //set aksi baru
  FCurrentAction := Value;

  //run EnterAction aksi baru
  if FCurrentAction<>nil then
    FCurrentAction.EnterAction(self);
end;

procedure TAIEntity.SetPreviousAction(const Value: TAIAction);
begin
  FPreviousAction := Value;
end;

procedure TAIEntity.Update;
begin
  if FCurrentAction<>nil then
     FCurrentAction.ExecuteAction(self);
end;

{ TAIAction }

procedure TAIAction.EnterAction(entity: TAIEntity);
begin
  //do nothing
end;

procedure TAIAction.ExecuteAction(entity: TAIEntity);
begin
  //do nothing
end;

procedure TAIAction.ExitAction(entity: TAIEntity);
begin
  //do nothing
end;

end.

TAIAction, by default, does nothing. To use this framework, application must derive TAIEntity and TAIAction and define action.

Back to our main goal, ie, improving 2D Animation with Direct3D demo. We have not added new action for this demo, so Spiderman's actions remains the same, walking, punching, hard punching, kicking and idle/stand-by. Figure below depicts UML diagram.

Finite state machine UML diagram

TBaseAction

All actions are derived from TBaseAction, which is derived from TAIAction. Action take place in ExecuteAction of TBaseAction is updating animation index to next animation frame. EnterAction is filled with code to reset animation to first animation frame. All actions do same thing, so we derive it from TBaseAction. TBaseAction is provided with a virtual method, Load, useful for loading animation frames. Because number of animation frame of each action is different, Actual loading will be delegated to child class of TBaseAction. TBaseAction holds texture collection instance which will be used to create TTexture instance.

  TBaseAction=class(TAIAction)
  private
    FMaxAnim: integer;
    FTextures: TTextureCollection;
    procedure SetMaxAnim(const Value: integer);
    procedure SetTextures(const Value: TTextureCollection);
  public
    procedure EnterAction(aientity:TAIEntity);override;
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);virtual;
  published
    property Textures:TTextureCollection read FTextures write SetTextures;
    property MaxAnim:integer read FMaxAnim write SetMaxAnim;
  end;

Below is actual implementation. Load method does nothing.

{TBaseAction}

procedure TBaseAction.Load;
begin
end;

We override EnterAction to ensure all animation starts from index 0 for all actions. Please note that we typecast TAIEntity to T2DEntity. Typecast is safe with assumption that Spiderman entity will be derived from T2DEntity. We will discuss T2DEntity class in a minute.

procedure TBaseAction.EnterAction(aientity: TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  ent.AnimIndex:=0;
end;

ExecuteAction is fleshed out with code to add animation index. If animation index exceed animation frame count (MaxAnim), it means animation finish and Spiderman animation is restore to previous state (PreviousAction). Please note that idle is default state of Spiderman, so PreviousAction is set with idle state and during runtime, PreviousAction is never changed. This way everytime animation (for example punching animation) finish, Spiderman will automatically put in idle state.

ExecuteAction of TBaseAction must be executed last, so child classes of TBaseAction that override ExecuteAction must call ExecuteAction of TBaseAction last time after updating animation.

procedure TBaseAction.ExecuteAction(aientity:TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);

  ent.AnimIndex:=ent.AnimIndex+1;
  if ent.AnimIndex>MaxAnim then
  begin
    ent.CurrentAction:=ent.PreviousAction;
  end;
end;

TIdleAction

Idle action is encapsulated in TIdleAction class. In TIdleAction we store a variable, type of array of TTexture, (SpidermanIdleTexture) contains Spiderman's idle animation. Load, EnterAction and ExecuteAction method is overriden.

  TIdleAction=class(TBaseAction)
  private
    SpidermanIdleTexture:TSpidermanIdleTexture;
    FDelay: integer;
    procedure SetDelay(const Value: integer);
  public
    procedure EnterAction(aientity:TAIEntity);override;
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);override;
  published
    property Delay:integer read FDelay write SetDelay;
  end;
Take a look at its implementation below,
{TIdleAction}

procedure TIdleAction.EnterAction(aientity: TAIEntity);
begin
  inherited;
  Fdelay:=0;
end;

procedure TIdleAction.ExecuteAction(aientity:TAIEntity);
var ent:T2DEntity;
begin
  if FDelay>3 then
  begin
    ent:=T2DEntity(aiEntity);

    ent.Sprite.Texture:=SpidermanIdleTexture[ent.AnimIndex];
    ent.Sprite.Y:=200;
    FDelay:=0;
    inherited;
  end;
  inc(FDelay);
end;

procedure TIdleAction.Load(const images_dir:string);
var i:integer;
begin
  for i:=0 to 9 do
  begin
    SpidermanIdleTexture[i]:=FTextures.Add as TTexture;
    SpidermanIdleTexture[i].ColorKey:=0;
    SpidermanIdleTexture[i].LoadFromFile(images_dir+
	               'standbystandby'+inttostr(i)+'.png');
  end;
  MaxAnim:=9;
end;

procedure TIdleAction.SetDelay(const Value: integer);
begin
  FDelay := Value;
end;
In TIdleAction class, we override EnterAction. Please note that idle animation uses additional delay as with main animation delay. Without additional delay, idle animation is still quite fast and produce bad result. We need to override it to ensure this delay is reset to 0, everytime Spiderman enters idle state.

ExecuteAction is overriden. Here we add delay check. If delay is more than 2, we update animation with next frame, reset delay dan last we call ancestor's ExecuteAction. Ancestor's ExecuteAction (TBaseAction) call is important to let AnimIndex get updated, without it we won't see Spiderman's idle animation.

Load method is overriden with code to load idle animation images. Idle animation only consist of 10 frames (from 0 to 9) we must set MaxAnim with 9. If you forget, animation may not be visible.

Textures in SpidermanIdleTexture is not freed, because FTextures automatically free its memory when FTextures is freed, but if you want you can free them in TIdleAction.

TPunchAction

Punching action is executed by TPunchAction

  TPunchAction=class(TBaseAction)
  private
    SpidermanPunchTexture:TSpidermanPunchTexture;
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);override;
  end;

We override ExecuteAction and Load only, beacuse we don't need any action takes places during EnterAction or ExitAction

{TPunchAction}

procedure TPunchAction.ExecuteAction(aientity:TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  ent.Sprite.Texture:=SpidermanPunchTexture[ent.AnimIndex];
  ent.Sprite.Y:=202;
  inherited;
end;

procedure TPunchAction.Load(const images_dir:string);
var i:integer;
begin
  for i:=0 to 5 do
  begin
    SpidermanPunchTexture[i]:=FTextures.Add as TTexture;
    SpidermanPunchTexture[i].ColorKey:=0;
    SpidermanPunchTexture[i].LoadFromFile(images_dir+
	                    'punchpunch'+inttostr(i)+'.png');
  end;
  MaxAnim:=5;
end;
You can look that in general, it is not too different with TIdleAction, except animation don't need delay.

THeavyPunchAction

Heavy punch is Spiderman's hard punching action, it is encapsulated in THeavyPunchAction class.

  THeavyPunchAction=class(TBaseAction)
  private
    SpidermanHeavyPunchTexture:TSpidermanHeavyPunchTexture;
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);override;
  end;

Implementation of THeavyPunchAction is as follow:

{THeavyPunchAction}

procedure THeavyPunchAction.ExecuteAction(aientity:TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  ent.Sprite.Texture:=SpidermanHeavyPunchTexture[ent.AnimIndex];
  ent.Sprite.Y:=175;
  inherited;
end;

procedure THeavyPunchAction.Load(const images_dir: string);
var i:integer;
begin
  for i:=0 to 9 do
  begin
    SpidermanHeavyPunchTexture[i]:=FTextures.Add as TTexture;
    SpidermanHeavyPunchTexture[i].ColorKey:=0;
    SpidermanHeavyPunchTexture[i].LoadFromFile(images_dir+
  	    'heavy_punchhpunch'+inttostr(i)+'.png');
  end;
  MaxAnim:=9;
end;

TKickAction

Kicking action is executed in TKickAction, it is similar to TPunchAction.

  TKickAction=class(TBaseAction)
  private
    SpidermanKickTexture:TSpidermanKickTexture;
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);override;
  end;

.....

{ TKickAction }

procedure TKickAction.ExecuteAction(aientity: TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  ent.Sprite.Texture:=SpidermanKickTexture[ent.animIndex];
  ent.Sprite.Y:=171;
  inherited;
end;

procedure TKickAction.Load(const images_dir:string);
var i:integer;
begin
  for i:=0 to 8 do
  begin
    SpidermanKickTexture[i]:=FTextures.Add as TTexture;
    SpidermanKickTexture[i].ColorKey:=0;
    SpidermanKickTexture[i].LoadFromFile(images_dir+
	                      'kickkick'+inttostr(i)+'.png');
  end;
  MaxAnim:=8;
end;
            

TWalkAction

Walking action is encapsulated in TWalkAction

  TWalkAction=class(TBaseAction)
  private
    SpidermanWalkTexture:TSpidermanWalkTexture;
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
    procedure Load(const images_dir:string);override;
  end;

Implementation of TWalkAction is as follow. In ExecuteAction, except displaying walking animation, we also must move Spiderman's position based on where Spiderman face. If it face left, x position is decreased and if face right, x is increased.

{TWalkAction}

procedure TWalkAction.ExecuteAction(aientity:TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);

  ent.Sprite.Texture:=SpidermanWalkTexture[ent.AnimIndex];
  ent.Sprite.Y:=205;
  case ent.Direction of
    dirLeft:ent.Sprite.X:=ent.Sprite.X-6;
    dirRight:ent.Sprite.X:=ent.Sprite.X+6;
  end;

  inherited;
end;

procedure TWalkAction.Load(const images_dir: string);
var i:integer;
begin
  for i:=0 to 11 do
  begin
    SpidermanWalkTexture[i]:=FTextures.Add as TTexture;
    SpidermanWalkTexture[i].ColorKey:=0;
    SpidermanWalkTexture[i].LoadFromFile(images_dir+'walkwalk'+inttostr(i)+'.png');
  end;
  MaxAnim:=11;
end;

TChangeDirectionAction

Changing direction left/right is executed by TChangeDirectionAction. Because TChangeDirectionAction don't need animation, Load is not overriden.

  TChangeDirectionAction=class(TBaseAction)
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
  end;

TChangeDirectionAction implementation is in following code snippet. Explanation how to do sprite mirroring is discussed in article 2D Animation with Direct3D Part 2. We don't need to call ancestor's ExecuteAction because changing direction don't need animation.

{ TChangeDirectionAction }

procedure TChangeDirectionAction.ExecuteAction(aientity: TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  case ent.Direction of
    dirLeft:begin
              ent.Sprite.scaling:=SetD3DXVector2(-1,1);
              ent.Sprite.X:=ent.Sprite.X+ent.Sprite.Texture.Width;
            end;
    dirRight:begin
              ent.Sprite.scaling:=SetD3DXVector2(1,1);
              ent.Sprite.X:=ent.Sprite.X-ent.Sprite.Texture.Width;
             end;
  end;
  inherited;
end;

TWalkOrChangeDirAction

Walking action and changing direction action is executed with same keypress, ie, left/right arrow. When left arrow is pressed and Spiderman is facing right, then executed action is changing direction of spiderman to left. If Spiderman is already facing left, executed action is walking to the left. Same thing happens on right arrow. This way, Spiderman always walks forward.

Action to decide walking action aor changing direction is encapuslated dienkapsulasi in TWalkOrChangeDirAction. This class is example of implementation of state that produce state transition.

  TWalkOrChangeDirAction=class(TBaseAction)
  private
    FChangeDir: TChangeDirectionAction;
    FWalk: TWalkAction;
    FNewDirection: TDirectionType;
    procedure SetChangeDir(const Value: TChangeDirectionAction);
    procedure SetWalk(const Value: TWalkAction);
    procedure SetNewDirection(const Value: TDirectionType);
  public
    procedure ExecuteAction(aientity:TAIEntity);override;
  published
    property NewDirection:TDirectionType read FNewDirection write SetNewDirection;
    property Walk:TWalkAction read FWalk write SetWalk;
    property ChangeDir:TChangeDirectionAction read FChangeDir write SetChangeDir;
  end;

Implementation is as follow. Please note that Load dont need to be overriden because it's not relevant. This action does not produce animation. We need to add Walk and ChangeDir property to store TWalkAction and TChangeDirectionAction instance, also NewDirection which hold direction of Spiderman.

{ TWalkOrChangeDirAction }

procedure TWalkOrChangeDirAction.ExecuteAction(aientity: TAIEntity);
var ent:T2DEntity;
begin
  ent:=T2DEntity(aiEntity);
  if ent.Direction<>FNewDirection then
  begin
    ent.Direction:=FNewDirection;
    ent.CurrentAction:=FChangeDir;
  end else
  begin
    ent.CurrentAction:=FWalk;
  end;
end;

procedure TWalkOrChangeDirAction.SetChangeDir(
  const Value: TChangeDirectionAction);
begin
  FChangeDir := Value;
end;

procedure TWalkOrChangeDirAction.SetNewDirection(
  const Value: TDirectionType);
begin
  FNewDirection := Value;
end;

procedure TWalkOrChangeDirAction.SetWalk(const Value: TWalkAction);
begin
  FWalk := Value;
end;

When ExecuteAction is executed, we ensure that walking state or changing direction is not currently executed. We check wheteher direction of Spiderman is different from the new one. If it is same we change state of Spiderman to walking state (FWalk), if it is different we change state to changing direction (FChangeDir).

T2DEntity

T2DEntity is class representing character in a game, in this case Spiderman, but because it is general, any kind of entity in game can use T2DEntity. T2DEnitity stores sprite data, animation index and direction of entity.

  T2DEntity=class(TAIEntity)
  private
    FAnimIndex: integer;
    FSprite: TSprite;
    FDirection: TDirectionType;
    procedure SetAnimIndex(const Value: integer);
    procedure SetSprite(const Value: TSprite);
    procedure SetDirection(const Value: TDirectionType);
  public
  published
   property Sprite:TSprite read FSprite write SetSprite;
   property AnimIndex:integer read FAnimIndex write SetAnimIndex;
   property Direction:TDirectionType read FDirection write SetDirection;
  end;

  TSpidermanEntity=class(T2DEntity)
  public
  end;

Following code is complete implementation of T2DEntity. Quite easy. Please not that FSprite instance is created and maintained by TSpriteCollection.

{ T2DEntity }

procedure T2DEntity.SetAnimIndex(const Value: integer);
begin
  FAnimIndex := Value;
end;

procedure T2DEntity.SetDirection(const Value: TDirectionType);
begin
  FDirection := Value;
end;

procedure T2DEntity.SetSprite(const Value: TSprite);
begin
  FSprite := Value;
end;

Complete implementaion is available in unit u_spidey.pas.

OK let us now modify main application to utilize actions that we have made. Parts we modifiy are loading sprite which we replace with actIdle, actWalk, actPunch action initialization. We call Load of each action instance to load sprite images. Variable Spiderman previously of type TSprite we change to TSpidermanEntity. After Spoderman instance is created, we create its sprite and set CurrentAction and PreviousAction to actIdle.

{ T2DEngine }

constructor T2DEngine.Create(param: TEngineParam);
var exe_dir,bg_dir,images_dir:string;
begin
  inherited;
  FBackgroundCollection:=TBackgroundCollection.Create(TBackground);
  FBackgroundCollection.Engine:=self;

  FSprites:=TSpriteCollection.Create(TSprite);
  FSprites.Engine:=Self;

  FTextures:=TTextureCollection.Create(TTexture);
  FTextures.Engine:=self;

  exe_dir:=ExtractFilePath(Application.ExeName);
  images_dir:=exe_dir+'imagesspiderman';
  bg_dir:=exe_dir+'imagesbg';

  //load background
  FBackground:=FBackgroundCollection.Add as TBackground;
  FBackground.LoadFromFile(bg_dir+'stage.png');
  FBackground.Y:=80;

  actIdle:=TIdleAction.Create;
  actIdle.Textures:=FTextures;
  actIdle.Load(images_dir);

  actPunch:=TPunchAction.Create;
  actPunch.Textures:=FTextures;
  actPunch.Load(images_dir);

  actHeavyPunch:=THeavyPunchAction.Create;
  actHeavyPunch.Textures:=FTextures;
  actHeavyPunch.Load(images_dir);

  actKick:=TKickAction.Create;
  actKick.Textures:=FTextures;
  actKick.Load(images_dir);

  actWalk:=TWalkAction.Create;
  actWalk.Textures:=FTextures;
  actWalk.Load(images_dir);

  actChangeDir:=TChangeDirectionAction.Create;
  actChangeDir.Textures:=FTextures;
  actChangeDir.Load(images_dir);

  actWalkOrChangeDir:=TWalkOrChangeDirAction.Create;
  actWalkOrChangeDir.Textures:=FTextures;
  actWalkOrChangeDir.Walk:=actWalk;
  actWalkOrChangeDir.ChangeDir:=actChangeDir;
  actWalkOrChangeDir.Load(images_dir);

  Spiderman:=TSpidermanEntity.Create;
  Spiderman.Sprite:=FSprites.Add as TSprite;
  Spiderman.Sprite.X:=200;
  Spiderman.CurrentAction:=actIdle;
  Spiderman.PreviousAction:=actIdle;

  delay:=0;
end;

To make sure there's no memory leak, we destroy action instances and Spiderman.

destructor T2DEngine.Destroy;
begin
  //clean up
  actIdle.Free;
  actPunch.Free;
  actHeavyPunch.Free;
  actKick.Free;
  actWalk.Free;
  actChangeDir.Free;
  actWalkOrChangeDir.free;

  spiderMan.Free;

  FSprites.Free;
  FTextures.Free;
  FBackgroundCollection.Free;
  inherited;
end;

Draw method of T2DEngine needs to be updated to use new framework as follow:

procedure T2DEngine.Draw;
begin
  FBackground.Draw;

  FSprites.BeginDraw;

  //beri delay agar animasi
  //tidak terlalu cepat
  if delay>1 then
  begin
    Spiderman.Update;
    delay:=0;
  end;

  Spiderman.Sprite.Draw;
  FSprites.EndDraw;

  inc(delay);
end;

Next we modify KeyDown to make it look like as follow:

procedure T2DEngine.KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    ord('J'),
    ord('j'):begin
               spiderman.CurrentAction:=actPunch;
            end;
    ord('K'),
    ord('k'):begin
               spiderman.CurrentAction:=actHeavyPunch;
            end;
    ord('L'),
    ord('l'):begin
               spiderman.CurrentAction:=actKick;
             end;
    VK_LEFT: begin
               actWalkOrChangeDir.NewDirection:=dirLeft;
			   if (spiderman.CurrentAction<>actWalk) and
			       (spiderman.CurrentAction<>actWalkOrChangeDir) and
			       (spiderman.CurrentAction<>actChangeDir) then
                 spiderman.CurrentAction:=actWalkOrChangeDir;
             end;
    VK_RIGHT: begin
               actWalkOrChangeDir.NewDirection:=dirRight;
			   if (spiderman.CurrentAction<>actWalk) and
			       (spiderman.CurrentAction<>actWalkOrChangeDir) and
			       (spiderman.CurrentAction<>actChangeDir) then
                  spiderman.CurrentAction:=actWalkOrChangeDir;
             end;
    VK_UP: begin
             FBackground.Y:=FBackground.Y-1;
             end;
    VK_DOWN: begin
             FBackground.Y:=FBackground.Y+1;
             end;
    ord(#27):begin
              TForm(Sender).Close;
             end;
  end;
end;

Below is screenshot of our application. Source code is available for download here.

Application screenshot

Summary

In this article we have modifed 2D Animation with Direct3D part 3 demo to utilize menggunakan object-oriented Finite State Machine. Visually, our current demo is not different with 2D Animation with Direct3D part 3 demo, but engine is changed and to my opinion is much better. In next article we will still struggle with AI where in next article we will improve current demo to include opponent character which have enough AI to avoid Spiderman attack. Ok see you later and have a nice coding session..

Reference

Related Article

Do you like this article? Help this website improve by donating. Any amounts is appreciated.

Or you can help by bookmarking this page. Delicious Bookmark this on Delicious