Search Tips Advanced Search |
| Delphi Graphics and Game Programming Exposed with DirectX 7.0 by John Ayres Wordware Publishing, Inc. ISBN: 1556226373 Pub Date: 12/01/99
Search this book:
Previous | Table of Contents | Next
This makes our enemy sprite appear to move in a more deliberate manner. We can adjust the offsets accordingly to make some patterns move the sprite faster or slower, and with some minor modifications we could make some patterns longer than others. By selecting different patterns, the movement appears more deliberate while at the same time reducing its predictability.
| Note: In real–world situations, we should make sure that the sprite is not positioned such that a movement pattern will run it into a movement boundary immediately. In our example, we simply clip to the edges of the screen, but this can make a movement pattern almost useless. In a real–world situation, we should reverse the pattern when it detects a movement boundary, or even exit the pattern immediately and choose another form of movement.
### Finite State Machines We’ve been examining methods by which intelligent movement can be modeled, and so far we’ve come up with some acceptable algorithms that can provide convincing, if somewhat limited, intelligent movement. However, we haven’t discussed methods by which enemy units determine which movement technique to use. We could just randomly pick something, but this would not model any sort of intelligent thought process. We need an algorithm by which an enemy unit can make a series of rational decisions based on various inputs from the game world, such as the current environment as well as the actions of the player. This can be accomplished through a method known as a finite state machine. A finite state machine is a logical construction that consists of several “states,” each of which defines a specific behavior, and “state transitions,” which define specific rules for moving from state to state.
| finite state machine: A logical construction that contains a series of states defining specific behaviors and a series of state transitions defining rules for moving from state to state.
Using finite state machines, enemy units can have very complex behavioral models. These can include not only movements but also different tactics or other actions based on the actions of the player, the state of other game objects, and the overall situation. State machines also allow for the easy extension of behaviors, as all that would be required is the logic for the behavior under the new state and logic for moving into and out of this state. An arbitrary state machine could be visualized as illustrated below. Figure 12–4: An abstracted finite state machine Each circle in the illustration represents a specific state, or behavior, and the lines connecting them represent the logic determining how one state leads to another. One way to model this in software would be to define each state as an integer value or perhaps an enumerated type. A case statement could then be utilized to execute the appropriate code for the current state. The transitions from state to state could be controlled in a number of ways, from randomly choosing a different state at a specific interval to complex logic that considers the player’s attributes versus the computer controlled adversary’s. In the following example, we’ve used a case statement to control the logic for each state, and we simply choose a random state after a specific amount of time has elapsed. While viewing the animation, you can definitely tell a difference in the sprite’s movement behavior among the various states. Listing 12–4: Movement behavior through a finite state machine
    type

       .

       .

       .

      TSpriteStates = (ssChasing, ssEvading, ssRandomMoves, ssPatternMoves, ssWait,

                       ssPlayer);

      TStateNames = array[ssChasing..ssWait] of string;

      TSprite = class

        XPos, YPos: Integer;

        Direction: Integer;

        CurPattern,

        CurPatternIdx: Integer;

        PatternIdxChange,

        CurPatternTimer,

        StateChange,

        StateTimer: Longint;

        State: TSpriteStates;

        procedure ChangeState;

        procedure Move;

        procedure Draw(Canvas: TCanvas);

      end;

      TPatterns = array[0..7, 0..31] of Integer;

    const

      {this array allows us to display the name of the state}

      StateNames: TStateNames = (‘Chasing’, ‘Evading’, ‘RandomMoves’,

                                 ‘PatternMoves’, ‘Wait’);

      {in this example, pattern movement is implemented as a direction, with 0

       indicating north, or upward movement. the directions increment in a

       clockwise manner}

      Patterns: TPatterns = (

      (0, 0, 0, 1, 1, 1, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7,

       1, 1, 1, 0, 0, 0),

      (1, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0,

       2, 2, 2, 1, 1, 1),

      (2, 2, 2, 3, 3, 3, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1,

       3, 3, 3, 2, 2, 2),

      (3, 3, 3, 4, 4, 4, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2,

       4, 4, 4, 3, 3, 3),

      (4, 4, 4, 5, 5, 5, 3, 3, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3,

       5, 5, 5, 4, 4, 4),

      (5, 5, 5, 6, 6, 6, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 4, 4, 4, 4, 4, 4, 4, 4,

       6, 6, 6, 5, 5, 5),

      (6, 6, 6, 7, 7, 7, 5, 5, 5, 5, 5, 5, 7, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5,

       7, 7, 7, 6, 6, 6),

      (7, 7, 7, 0, 0, 0, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6,

       0, 0, 0, 7, 7, 7)

      );

      {these indicate sprite positions in the array of sprites}

      ENEMY1IDX = 0;

      ENEMY2IDX = 1;

      PLAYERIDX = 2;

    var

       .

       .

       .

      {tracks all of the sprites}

      Sprites: array[ENEMY1IDX..PLAYERIDX] of TSprite;

    implementation

       .

       .

       .

    procedure TfrmDXAppMain.FormActivate(Sender: TObject);

    begin

       .

       .

       .

      {seed the random number generator}

      Randomize;

      {initialize the enemy sprites}

      Sprites[ENEMY1IDX] := TSprite.Create;

      Sprites[ENEMY1IDX].XPos := 15;

      Sprites[ENEMY1IDX].YPos := 240;

      Sprites[ENEMY1IDX].PatternIdxChange := 100;

      Sprites[ENEMY1IDX].State := ssRandomMoves;

      Sprites[ENEMY1IDX].StateChange := 500;

      Sprites[ENEMY1IDX].StateTimer := timeGetTime;

      Sprites[ENEMY1IDX].Direction := 2;

      Sprites[ENEMY2IDX] := TSprite.Create;

      Sprites[ENEMY2IDX].XPos := 625;

      Sprites[ENEMY2IDX].YPos := 240;

      Sprites[ENEMY2IDX].PatternIdxChange := 50; // this sprite will change pattern

                                                 // steps faster than the other...

      Sprites[ENEMY2IDX].State := ssRandomMoves;

      Sprites[ENEMY2IDX].StateChange := 250;     // ...as well as states

      Sprites[ENEMY2IDX].StateTimer := timeGetTime;

      Sprites[ENEMY2IDX].Direction := 6;

      {initialize the player sprite}

      Sprites[PLAYERIDX] := TSprite.Create;

      Sprites[PLAYERIDX].XPos := 320;

      Sprites[PLAYERIDX].YPos := 240;

      Sprites[PLAYERIDX].State := ssPlayer;

       .

       .

       .

    end;

       .

       .

       .

    procedure TfrmDXAppMain.DrawSurfaces;

    var

      SrfcDc: HDC;

      TempCanvas: TCanvas;

      iCount: Integer;

    begin

      {erase the last frame of animation}

      ColorFill(FBackBuffer, clBlack, nil);

      {move the player sprite}

      if (GetAsyncKeyState(VK_LEFT) and $8000) = $8000 then

        Sprites[PLAYERIDX].XPos := Sprites[PLAYERIDX].XPos – 2;

      if (GetAsyncKeyState(VK_RIGHT) and $8000) = $8000 then

        Sprites[PLAYERIDX].XPos := Sprites[PLAYERIDX].XPos + 2;

      if (GetAsyncKeyState(VK_UP) and $8000) = $8000 then

        Sprites[PLAYERIDX].YPos := Sprites[PLAYERIDX].YPos – 2;

      if (GetAsyncKeyState(VK_DOWN) and $8000) = $8000 then

        Sprites[PLAYERIDX].YPos := Sprites[PLAYERIDX].YPos + 2;

      {move all sprites. note that this simply performs screen boundary clipping

       for the player sprite, but we could have any number of enemy sprites}

      for iCount := ENEMY1IDX to PLAYERIDX do

        Sprites[iCount].Move;

      {retrieve a device context and create a canvas}

      FBackBuffer.GetDC(SrfcDc);

      TempCanvas := TCanvas.Create;

      try

        {set up the canvas}

        TempCanvas.Handle := SrfcDc;

        TempCanvas.Font.Color := clWhite;

        {draw the sprites}

        for iCount := ENEMY1IDX to PLAYERIDX do

          Sprites[iCount].Draw(TempCanvas);

        {indicate the current state for each enemy sprite}

        TempCanvas.Brush.Style := bsClear;

        TempCanvas.TextOut(0, 0, ‘Enemy1 State:’+

                           StateNames[Sprites[ENEMY1IDX].State]);

        TempCanvas.TextOut(500, 0, ‘Enemy2 State: ’+

                           StateNames[Sprites[ENEMY2IDX].State]);

      finally

        {cleanup}

        TempCanvas.Handle := 0;

        FBackBuffer.ReleaseDC(SrfcDc);

        TempCanvas.Free;

      end;

    end;

       .

       .

       .

    { TSprite }

    procedure TSprite.ChangeState;

    begin

      {set the state randomly}

      State := TSpriteStates(Random(5));

      {if this is the pattern move state...}

      if (State = ssPatternMoves) then

      begin

        {choose a random pattern}

        CurPattern := Random(8);

        {initialize to the start of the pattern}

        CurPatternIdx := 0;

        {retrieve the current time}

        CurPatternTimer := timeGetTime;

        {32 steps in a pattern multiplied by the time per each step should

         give enough time to complete the entire pattern}

        StateChange := PatternIdxChange * 32;

      end

      else

        {...otherwise, change state again in 1 to 3 seconds}

        StateChange := Random(2000)+1000;

      {begin timing the state}

      StateTimer := timeGetTime;

    end;

    procedure TSprite.Draw(Canvas: TCanvas);

    begin

      {draw enemy sprites in blue, player sprites in red}

      if State <> ssPlayer then

        Canvas.Brush.Color := clBlue

      else

        Canvas.Brush.Color := clRed;

      {draw a small dot for the sprite}

      Canvas.Pen.Style := psClear;

      Canvas.Brush.Style := bsSolid;

      Canvas.Ellipse(XPos, YPos, XPos + 3, YPos + 3);

    end;

    procedure TSprite.Move;

    const

      {initialize a horizontal and vertical velocity}

      XVel: Integer = 1;

      YVel: Integer = 1;

    begin

      {if this sprite is an enemy sprite...}

      if State <> ssPlayer then

      begin

        {if it is time to change the state, then change it}

        if timeGetTime – StateTimer > StateChange then

          ChangeState;

        {for each state...}

        case State of

          ssChasing       : begin

                              {we’re chasing the player, so move the sprite

                               closer to the player sprite’s position}

                              if Sprites[PLAYERIDX].XPos > XPos then

                                XPos := XPos + 1;

                              if Sprites[PLAYERIDX].XPos < XPos then

                                XPos := XPos – 1;

                              if Sprites[PLAYERIDX].YPos > YPos then

                                YPos := YPos + 1;

                              if Sprites[PLAYERIDX].YPos < YPos then

                                YPos := YPos – 1;

                            end;

          ssEvading       : begin

                              {we’re evading the player, so move the sprite

                               farther away from the player sprite’s position}

                              if Sprites[PLAYERIDX].XPos > XPos then

                                XPos := XPos – 1;

                              if Sprites[PLAYERIDX].XPos < XPos then

                                XPos := XPos + 1;

                              if Sprites[PLAYERIDX].YPos > YPos then

                                YPos := YPos – 1;

                              if Sprites[PLAYERIDX].YPos < YPos then

                                YPos := YPos + 1;

                            end;

          ssRandomMoves   : begin

                              {we’re moving randomly, but we don’t want to move

                               each frame of animation or the sprite will look

                               like it’s having a seizure}

                              if timeGetTime–CurPatternTimer>PatternIdxChange*3 then

                              begin

                                {modify the horizontal and vertical velocity}

                                XVel := Random(3) – 1;

                                YVel := Random(3) – 1;

                                {reset the timer}

                                CurPatternTimer := timeGetTime;

                              end;

                              {add the current velocity to the horizontal and

                               vertical positions}

                              XPos := XPos + XVel;

                              YPos := YPos + YVel;

                            end;

          ssPatternMoves  : begin

                              {if it is time to change the current pattern step

                               index...}

                              if timeGetTime–CurPatternTimer > PatternIdxChange then

                              begin

                                {increment to the next step}

                                CurPatternIdx := CurPatternIdx + 1;

                                {if we’ve reached the end of this pattern...}

                                if CurPatternIdx > 31 then

                                begin

                                  {choose a new pattern}

                                  CurPattern := Random(8);

                                  {initialize to the first step of the pattern}

                                  CurPatternIdx := 0;

                                end;

                                {reset the pattern timer}

                                CurPatternTimer := timeGetTime;

                              end;

                              {as stated above, pattern movement is implemented

                               as a direction, so modify the sprite’s position

                               according to the direction at this pattern’s

                               movement step}

                              case Patterns[CurPattern, CurPatternIdx] of

                                0 : begin

                                      XPos := XPos + 1;

                                    end;

                                1 : begin

                                      XPos := XPos + 1;

                                      YPos := YPos + 1;

                                    end;

                                2 : begin

                                      YPos := YPos +1;

                                    end;

                                3 : begin

                                      XPos := XPos – 1;

                                      YPos := YPos + 1;

                                    end;

                                4 : begin

                                      XPos := XPos – 1;

                                    end;

                                5 : begin

                                      XPos := XPos – 1;

                                      YPos := YPos – 1;

                                    end;

                                6 : begin

                                      YPos := YPos – 1;

                                    end;

                                7 : begin

                                      XPos := XPos + 1;

                                      YPos := YPos – 1;

                                    end;

                              end;

                            end;

        end;

      end;

      {clip to the boundaries of the screen}

      if XPos < 0 then

        XPos := 0;

      if XPos > DXWIDTH – 3 then

        XPos := DXWIDTH – 3;

      {we’ll keep sprites from moving up where the status is displayed}

      if YPos < 20 then

        YPos := 20;

      if YPos > DXHEIGHT – 3 then

        YPos := DXHEIGHT – 3;

    end;
Implementing artificial intelligence as a finite state machine makes it easy to specify some very complex behavior and add new behavior if necessary. When coupled with some logic to control state changes based on the current game state, such as hit points, power, player hit points, etc., enemy sprites can appear to be very intelligent indeed. However, the current implementation lacks personality. Enemy sprites may exhibit some fairly complex thought patterns, but they all think alike, and that can soon become boring to players. What is needed is a way to imbue enemy sprites with a little more character and individualism.
Previous | Table of Contents | Next

Products | Contact Us | About Us | Privacy | Ad Info | Home Use of this site is subject to certain Terms & Conditions, Copyright © 1996-2000 EarthWeb Inc. All rights reserved. Reproduction whole or in part in any form or medium without express written permission of EarthWeb is prohibited. Read EarthWeb's privacy statement.