









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++).
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.
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.
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;
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.
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.
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;
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;
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;
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;
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 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.

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..
Do you like this article? Help this website improve by donating. Any amounts is appreciated.
Or you can help by bookmarking this page.
Bookmark this on Delicious