• Recent Tutorials

  • Tripping The Class Fantastic: State Machines

    Getting Started
    If you are completely new to state machines, I've written a very basic implementation to help get you started.

    Code: [View]
    unit classBaseStateMachine;
    
    interface
    
    uses
      SysUtils, Generics.collections;
    
    type
      TStateMethod = function:integer of object;
    
      TBaseStatemachine = class(TObject)
      protected
        fCurrentState     : integer;
        fStateList        : TDictionary;
    
        procedure addState(stateIndex:integer;aStateMethod:TStateMethod);
      public
        constructor create;
        destructor Destroy; override;
    
        procedure process;
    
        procedure initialiseStates; virtual; abstract;
    
        property currentState:integer read fCurrentState write fCurrentState;
      end;
    
    implementation
    
    procedure TBaseStateMachine.addState(stateIndex:integer;aStateMethod:TStateMethod);
    begin
      if (fStateList.ContainsKey(stateIndex)) then
      begin
        raise exception.createFmt('%s.addState - Add failed, duplicate state %d',[self.className,stateIndex]);
      end;
    
      fStateList.Add(stateIndex,aStateMethod);
    end;
    
    constructor TBaseStateMachine.create;
    begin
      inherited;
    
      fStateList:=TDictionary.create;
      fCurrentState:=0;
    
      initialiseStates;
    end;
    
    destructor TBaseStateMachine.Destroy;
    begin
      try
        fStateList.free;
      except
      end;
    
      inherited;
    end;
    
    procedure TBaseStateMachine.process;
    var
      nextState       : integer;
    begin
      if (fStateList.ContainsKey(fCurrentState)) then
      begin
        nextState:=fStateList.Items[fCurrentState];
    
        if (nextState<>0) then
        begin
          fCurrentState:=nextState;
        end;
      end
      else
      begin
        raise exception.createFmt('%s.process failed - Unknown state %d',[self.className,fCurrentState]);
      end;
    end;
    
    end.
    To use this class you need to derive a new class from it. The new descendant class should override the initialiseStates method, using the addState method to build the list of known states. Each one should link to a function that matches the TStateMethod prototype (i.e. it should return an integer). The integer returned will be either 0, meaning don't change state, or a new state index.

    You can also use initialiseStates to setup any internal variables.

    To use it, you simply call process each time you want to run the current state. The appropriate function will be executed and the machine will switch to a new state if the return value of the function is not 0.

    As an example, here is a very basic state machine that counts up to 100 and then back down to 0 before counting back to 100 etc. ad infinitum.

    Interface

    Code: [View]
    const
      STATE_COUNTINGUP   = 1;
      STATE_COUNTINGDOWN = 2;
    
    type
      TMyStateMachine = class(TBaseStateMachine)
      protected
        fCounter        : integer;
      public
        procedure initialiseStates; override;
    
        function stateCountingUp:integer;
        function stateCountingDown:integer;
    
        property counter:integer read fCounter;
      end;
    Implementation

    Code: [View]
    procedure TMyStateMachine.initialiseStates;
    begin
      addState(STATE_COUNTINGUP,stateCountingUp);
      addState(STATE_COUNTINGDOWN,stateCountingDown);
    
      fCurrentState:=STATE_COUNTINGUP;
      fCounter:=0;
    end;
    
    function TMyStateMachine.stateCountingUp;
    begin
      result:=0;
      inc(fCounter);
    
      if (fCounter>=100) then
      begin
        result:=STATE_COUNTINGDOWN;
      end;
    end;
    
    function TMyStateMachine.stateCountingDown;
    begin
      result:=0;
      dec(fCounter);
    
      if (fCounter<=0) then
      begin
        result:=STATE_COUNTINGUP;
      end;
    end;
    This is then used in an application like this:-

    Code: [View]
    procedure TfrmBasicStateMachine.FormCreate(Sender: TObject);
    begin
      fMyStateMachine:=TMyStateMachine.create;
    end;
    
    procedure TfrmBasicStateMachine.FormDestroy(Sender: TObject);
    begin
      try
        fMyStateMachine.free;
      except
      end;
    end;
    
    procedure TfrmBasicStateMachine.tmrTickTimer(Sender: TObject);
    begin
      tmrTick.enabled:=false;
    
      fMyStateMachine.process;
    
      lblCount.caption:=intToStr(fMyStateMachine.counter);
      lblCurrentState.caption:=intToStr(fMyStateMachine.currentState);
    
      tmrTick.enabled:=true;
    end;
    In this example, the machine is called every 100ms by a timer.

    All the source code for this example is included in the attached ZIP file.

    Comments 3 Comments
    1. Ñuño Martínez's Avatar
      Ñuño Martínez -
      Just did a very fast reading (titles and some of the code, not much more). Looks very interesting.

      I did my own state-machine library translating the one described in"Programming Game AI by Example". Your approach seems slightly different. I'll tell you once I read your tutorial in-deep and compare both ways.
    1. AthenaOfDelphi's Avatar
      AthenaOfDelphi -
      If anyone has downloaded the attachment, the file classBaseStateMachine.pas was missing. Sorry about that, thanks to d.spinetti for pointing out my mistake.
    1. Ñuño Martínez's Avatar
      Ñuño Martínez -
      Read. Your implementation is way different than the one described in"Programming Game AI by Example" but it's a nice approach.

      Thank-you.