.Netterpillars: Artificial Intelligence and Sprites, Part 2

In this article on artificial intelligence, you will take a computer game from first draft to final version. It is excerpted from chapter two of the book Beginning .NET Game Programming in C#, by Ellen Hatton et. al. (Apress, 2004, ISBN: 1590593197) and is the second of two parts.

 

The Coding Phase

As you did in the previous chapter, you’ll start coding the basic objects for the game (simplest first), and then tackle the more difficult code of the game engine and the netterpillar AI classes.

To allow you to test every new method created, you’ll do your code in five steps:

  1. First draft: Code the static objects.

  2. Second draft: Code the player character.

  3. Third draft: Code the game engine and collision detection.

  4. Fourth draft: Code the configuration screen and game over.

  5. Final version: Code the netterpillars AI.

The details of each of these versions are shown in the next sections.

First Draft: Coding the Static Objects

In the next sections, we show the code and discuss the details of the classes for the static objects, mushrooms, and branches, and create an early version of the main program and the GameEngine class, so you can test these classes.

The Sprite Class

You’ll only add a new property in this class, the IMAGE_PATH constant, which will be used by all the child classes to compose the full path from where the images should be loaded.

The Mushroom Class

There’s not much to say about the Mushroom class. It just has an overloaded constructor that creates a sprite with the mushroom drawing, to be used instead of the original sprite constructor with a parameter. This will allow cleaner code when creating mushrooms.

public class Mushroom : Sprite {
  public Mushroom() {…}
}


The code for the constructor will be as follows:

public class Mushroom : Sprite {
  public Mushroom() :
    base(Application.StartupPath+”\”+IMAGE_PATH+
         ”\Mushroom.gif”) {}
}

Note that all you do is call the base class’s new method, passing the appropriate parameters.


NOTE When a child class defines a method that already exists in the base class, any object created from the child class will call the code in the method of this class, unless you explicitly call the base class method, as in the preceding code sample (using base() after the colon, plus any necessary parameters).

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=The Branch Class}

The Branch class will also be derived from the Sprite base class, but because a branch can have different sizes, you’ll have to add extra variables to hold the branch bitmaps, and create a new Draw method and constructors that will do the branch creation and drawing. The next code sample presents the Branch class interface:

public class Branch : Sprite {
  private Bitmap BranchStart;
  private Bitmap[] BranchMiddle;
  private Bitmap BranchEnd;
  public int branchSize;
  public Branch(CompassDirections branchDirection,
                int intSize) {…}
  public void Draw(System.IntPtr winHandle, int x,
                   int y) {…}
}

As noted before, your Branch class will be used to improve the visual interface by placing branches around the game field. This class will have only two methods: the constructor, which will load the bitmaps from disk, and the Draw method, which will draw the branch on screen. Since the branches don’t move or disappear during the game, you won’t need to code an Erase method.

Each branch will be composed by a set of at least three images: a “branch start,” a “branch end,” and one or more “branch middles.” Since you’ll need horizontal and vertical branches, you’ll need six different images, created with a specific naming convention to help you, as shown in Figure 2-13.

 

Figure 2-13.  The branch images

The constructor will use the concepts explained in the Load method of the Sprite class, extending the code to store the images in the specific properties of the Branch class—BranchStart, BranchMiddle array, and BranchEnd.

public Branch(CompassDirections branchDirection,
              int intSize) {
  BranchMiddle = new Bitmap[intSize-2];
  string strImagePrefix;
  branchSize = intSize;
  Direction = branchDirection;
  // Picks the prefix for the branch – Horizontal or
     vertical?
  strImagePrefix = “Hor”; // Default direction is east-west
                             (Horizontal)
  if (Direction==Sprite.CompassDirections.North ||
        Direction==Sprite.CompassDirections.South) {
    strImagePrefix = “Vert”;
  }
  // Load the top, the middle parts, and the end of the
     branch.
  // Magenta is the colorkey (which will be transparent)
     for the Load method.
  BranchStart =
    Load(Application.StartupPath+”\”+IMAGE_PATH+”\”+
    strImagePrefix+”BranchStart.gif”, Color.FromArgb(255,
       255, 0, 204));
  for(int i=0; i<=branchSize-3; i++) {
    BranchMiddle[i] =
      Load(Application.StartupPath+”\”+IMAGE_PATH+”\”+
      strImagePrefix+”BranchMiddle.gif”, Color.FromArgb
       (255, 255, 0, 204));
  }
  BranchEnd =
    Load(Application.StartupPath+”\”+IMAGE_PATH+”\”+
    strImagePrefix+”BranchEnd.gif”, Color.FromArgb(255,
      255, 0, 204));
}

Here are some points to note about the preceding code:

  • You use the naming conventions stated before to load the appropriate images, including the prefix “Hor” for the horizontal images and “Vert” for the vertical ones. You use the branchDirection parameter of the CompassDirections enumeration (defined in the base class Sprite) to choose whether the branch will be vertical (north and south directions) or horizontal (west and east directions).

  • The image files were drawn using the magenta color where you need to create transparency, that’s why you use Color.fromARGB(255, 255, 0, 204) as the parameter for the keycolor of the Load function (defined in the Sprite base class).

  • The dimension of the BranchMiddle array is defined as intSize-3 because the size of the branch will take into account the start and the end of the branch, so you need an array with the defined size minus two. Since all arrays in C# are zero based, you have intSize-2 elements when defining an array that goes from 0 to intSize-3. A little tricky, isn’t it?

The Draw method will be very similar to the method with the same name on the base class. In fact, you’ll be calling the base class method in order to draw each of the parts of the branch, so you won’t have any real drawing code in this method.

public void Draw(System.IntPtr winHandle, int x, int y) {
  // Sets the location and draws the start of the branch.
  Location = new Point(x, y);
  base.Draw(BranchStart, winHandle);
  // Sets the location and draws each of the branch middle
     parts.
  if (Direction==Sprite.CompassDirections.North||
      Direction==Sprite.CompassDirections.South) {
    // It’s a horizontal branch.
    for(int i=0; i<=branchSize-3; i++) {
      y++;
      Location = new Point(x, y);
      base.Draw(BranchMiddle[i], winHandle);
    }
    y++;
  }
  else {
    // It’s a vertical branch.
    for(int i=0; i<=branchSize-3; i++) {
      x++;
      Location = new Point(x, y);
      base.Draw(BranchMiddle[i], winHandle);
    }
    x++;
  }
  // Sets the location and draws the start of the branch.
  Location = new Point(x, y);
  base.Draw(BranchEnd, winHandle);
}

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Main Program and GameEngine Class}

Since you already have two of the base classes, it’s time to do some tests to check whether everything is okay so far. Instead of doing a simple test program, let’s go one step ahead and start implementing the game Main procedure and the GameEngine class, so you can start to understand the game logic, and add to them when new features become available.

Looking at the class diagram, you can pick some properties and methods that will help you to create a subset of the final GameEngine class, which will allow you to test the classes you created. You’ll need to code the properties associated with mushrooms and branches, the constructor (to initialize the objects), the Redraw method (to draw the objects), and a Render object, the method which will do all the game physics (for now, only calling the Redraw method). Your stub class will be as follows:

public class GameEngine {
  public int Width = 25;
  public int Height = 25;
  public static Image BackgroundImage;
  private System.IntPtr ScreenWinHandle;

  // Game objects
  private Branch[] branches;

  private Mushroom objMushrooms;
  private int MushroomNumber = 75;

  //Controls the game end.
  public bool GameOver;

  public void Render() {…}
  public void Redraw() {…}
}

In the constructor, all you do is create the object arrays and each of the objects. You’ll also store the window handle received in the function to be used by the Redraw procedure.

public void GameEngine(System.IntPtr WinHandle) {
  int x; int y;
  objBranchs = new Branch[5];
 
  // Reset the mushroomNumber, forcing a call to the
     property procedure.
  Mushrooms = Mushrooms;

  ScreenWinHandle = WinHandle;

  // Create the branches.
  objBranchs[0] = new Branch
    (Sprite.CompassDirections.North, this.Height);
  objBranchs[1] = new Branch
    (Sprite.CompassDirections.North, this.Height);
  objBranchs[2] = new Branch(Sprite.CompassDirections.East,
    this.Width-2);
  objBranchs[3] = new Branch(Sprite.CompassDirections.East,
    this.Width-2);

  // Create the mushrooms.
  objMushrooms = new Mushroom();
  for(int i=0; i<MushroomNumber; i++) {
    x = rand.Next(0, this.Width-2)+1;
    y = rand.Next(0, this.Height-2)+1;
  }
}

For now, the Render method just calls the Redraw method; in future versions it will call the functions to implement the game physics.

public void Render() {
  Redraw();
}

As for your Redraw method, all you need to do is call the Draw method of each game object.

public void Redraw() {
  for(int x=0; x<Width; x++) {
    for(int y=0; y<Height; y++) {
      if (GameField[x, y]==GameObjects.Mushroom) {
        objMushrooms.Location = new Point(x, y);
        objMushrooms.Draw(ScreenWinHandle);
      }
    }
  }

  objBranchs[0].Draw(ScreenWinHandle, 0, 0);
  objBranchs[1].Draw(ScreenWinHandle, (this.Width-1), 0);
  objBranchs[2].Draw(ScreenWinHandle, 1, 0);
  objBranchs[3].Draw(ScreenWinHandle, 1, (this.Height-1));
}

Now, with the GameEngine class stub done, you need to create a Main procedure that will generate the game engine object and call the Render method until the game is over (in this case when the Esc key is pressed). To do this, add a module to the solution and include the following code:

class MainGame {
  public static GameEngine netterpillarGameEngine;
  public static void Main(string [] args) {
    // Create the game engine object.
    netterpillarGameEngine = new GameEngine();
    WinGameField = new frmGameField();
    WinGameField.Show();
    // Create a copy of the background image to allow
       erasing the sprites.
    GameEngine.BackgroundImage =(Image)
        WinGameField.PicGameField.Image.Clone();
    while ( !objGameEngine.GameOver) {
      netterpillarGameEngine .Render();
      Application.DoEvents();
    }
    WinGameField.Dispose();
    netterpillarGameEngine = null;
  }
}

To finish this first draft, capture the Esc key to end the game. This can be done using the KeyDown event of frmGameField.

private void frmGameField_KeyDown
   (object sender, System.Windows.Forms.KeyEventArgs e) {
  // Just showing Esc key behavior right now.
  switch(e.KeyCode) {
   
case Keys.Escape:
      MainGame.objGameEngine.GameOver = true;
      break;
 
}
}

Running your program now, you can see a basic game field filled with mushrooms, as shown in Figure 2-14.

Figure 2-14.  Testing the first basic classes

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Second Draft: Coding the Player Character}

The next step in your code phase is to code the Netterpillar class and make all adjustments needed to your first draft for the main program and the game engine to allow the player character to be drawn on screen and be controlled by the player, using the keyboard navigation (arrow) keys. The next sections show and discuss the code to do this.

The Netterpillar Class

You’ll now look at the Netterpillar class and begin to code it. The main body of this class is shown here. You’ll look at the methods belonging to it in the subsequent code samples.

public class Netterpillar : Sprite {
  private Bitmap NetterHeadN;
  private Bitmap NetterHeadS;
  private Bitmap NetterHeadE;
  private Bitmap NetterHeadW;
  public NetterBody[] NetterBody;
  public int NetterBodyLength = 4;
  public bool IsComputer = true; // Defaults to
    AI netterpillar.
  public bool IsDead = false; // Defaults to alive
    netterpillar.
  public Netterpillar(int x, int y,
    Sprite.CompassDirections InitialDirection, bool
    bIsComputer) {…}
  public void EatAndMove(int x, int y, System.IntPtr
    WinHandle) {…}
  public void Move(int x, int y, System.IntPtr WinHandle)
    {…}
  public new void Draw(System.IntPtr WinHandle) {…}
}

When deriving the code interface from the class diagram, if you don’t have a detailed project, you usually start with few or even no parameters in the methods. The rich interface just shown, with many parameters in some methods, was created step by step when coding each of the methods. For example, the parameter IsComputerOpponent in the constructor is included later on in the coding process, when you discover that after each call to the constructor, you are setting the IsComputer property of the class, a clear indication that you should include this property as a parameter in the constructor.

Another surprise here is the NetterBody() array. When doing the class diagram, we mentioned something about having an array of “body parts” of the netterpillar. But what exactly is a body part in this case? It might be an array of Point objects, which would store the position to which the body bitmap must be drawn, for example. But then you would need to create a complex logic in the Netterpillar class to deal with the drawing of body parts. Instead, you should create a new class, NetterBody, that will be as simple as the Mushroom class (except that a different bitmap is used), so you can use the Location property and Draw method of the Sprite base class.

Is this the best choice for the implementation? There’s no right answer. The best option is the one that will be simpler for you to create and, most importantly, to debug and update.

As for the images of the netterpillar, besides four different bitmaps for the head (each one heading in a different direction) and one for the body, you’ll need two different sets of images to allow a visual contrast between the player-controlled netterpillar and the computer-controlled ones. Using the prefix “player” for the player bitmaps, follow the naming conventions shown in Figure 2-15.

Figure 2-15.  The names for .Netterpillar Images

With these names in mind, you can create the NetterBody class and the constructor of the Netterpillar class.

public class NetterBody : Sprite {
  public NetterBody(bool IsComputer) : base
    (Application.StartupPath+”\”+ IMAGE_PATH+”\”+
    (IsComputer ? “” : “Player”) +”NetterBody.gif”) {}
}

As you defined in the class properties, the default length of the body of the netterpillar (NetterBodyLength property) is four, so the netterpillar starts with a minimum size. Since the constructor will receive the initial direction for the netterpillar, you’ll use this direction to position the body parts behind the head (for example, if the netterpillar is heading east, the body parts will appear to the west of the head (lower values on the x axis). This code sample works out the position of the body relative to the head:

public Netterpillar(int x, int y, Sprite.CompassDirections
       InitialDirection,bool IsComputerOpponent) {
  // Start with a bigger length so you won’t need to resize
     it so soon.
  NetterBody = new NetterBody[25+1];
  int incX=0, incY=0;

  IsComputer = IsComputerOpponent;
  NetterHeadN = Load
            (Application.StartupPath+”\”+IMAGE_PATH+”\”+
    (IsComputer ? “” : “Player”)+”NetterHeadN.gif”);
  NetterHeadS = Load
            (Application.StartupPath+”\”+IMAGE_PATH+”\”+
    (IsComputer ? “” : “Player”)+”NetterHeadS.gif”);
  NetterHeadE = Load
            (Application.StartupPath+”\”+IMAGE_PATH+”\”+
    (IsComputer ? “” : “Player”)+”NetterHeadE.gif”);
  NetterHeadW = Load
            (Application.StartupPath+”\”+IMAGE_PATH+”\”+
    (IsComputer ? “” : “Player”)+”NetterHeadW.gif”);
  for(i=0; i<NetterBodyLength; i++) {
    NetterBody[i] = new NetterBody(IsComputer);
  }

  // Position the Netterpillar on the given point.
  Direction = InitialDirection;
  Location.X = x;
  Location.Y = y;
  // Position each of the body parts.
  switch(Direction) {
    case Sprite.CompassDirections.East:
      incX = -1;
      break;
    case Sprite.CompassDirections.South:
      incY = -1;
      break;
    case Sprite.CompassDirections.West:
      incX = 1;
      break;
    case Sprite.CompassDirections.North:
      incY = 1;
      break;
  }
  for(int i=0; i<NetterBodyLength; i++) {
    x += incX;
    y += incY;
    NetterBody[i].Location.X = x;
    NetterBody[i].Location.Y = y;
  }
}

Observe that you simply set the location of the netterpillar (the head) and the location of each of the body parts, but there’s no drawing yet. The drawing is done in the Draw procedure (shown in the next code listing), which considers the direction in which the netterpillar is heading in order to choose which bitmap will be used for the head, and then runs through the NetterBody array to draw the body parts.

public new void Draw(System.IntPtr WinHandle) {
  switch(Direction) {
    case Sprite.CompassDirections.East:
      base.Draw(NetterHeadE, WinHandle);
      break;
    case Sprite.CompassDirections.South:
      base.Draw(NetterHeadS, WinHandle);
      break;
    case Sprite.CompassDirections.West:
      base.Draw(NetterHeadW, WinHandle);
      break;
    case Sprite.CompassDirections.North:
      base.Draw(NetterHeadN, WinHandle);
      break;
  }

  for(int i=0; i<NetterBodyLength; i++) {
    NetterBody[i].Draw(WinHandle);
  }
}

The last two methods of the Netterpillar class are very similar: Move and EatAndMove. The Move method will update the head location according to the new x and y values passed as parameters from the game engine, and then update all the body parts to move one step ahead. You could erase and draw everything, but since all the body parts look the same, you can just erase the last body part, copy the first body part over the head, and draw the head in the new position, which will be much quicker than redrawing the whole body.

public void Move(int x, int y, System.IntPtr WinHandle) {
  // Erase the last part of the body.
  NetterBody[NetterBodyLength-1].Erase(WinHandle);

  // Update the whole body’s position and then the head
     position.
  for(int i=NetterBodyLength-1; i>=1; i-=1) {
    NetterBody[i].Location = NetterBody[i-1].Location;
  }
  NetterBody[0].Location = Location;
  Location = new Point(x, y);

  // Redraw only the first part of the body and the head.
  NetterBody[0].Draw(WinHandle);

  //You don’t need to erase the netterpillar head, since
    the body will cover it.
  Draw(WinHandle);
  // Reset the direction controller variable.
  bDirectionSet = false;
}

The main difference between the EatAndMove method and the Move method is that in the first method the netterpillar is eating a mushroom and is getting bigger; so you’ll need to create a new body part (resizing the NetterBody array), set its position to the position of the last body part, and then reposition all other body parts, redrawing only the first one and the head. In the second method the netterpillar will only move, following a similar approach.

public void EatAndMove(int x, int y, System.IntPtr
                       WinHandle) {
  // If the NetterBody array is full, allocate more space.
  if (NetterBodyLength == NetterBody.Length) {
    NetterBody [] tempNetterBody = new NetterBody
      [NetterBody.Length+25+1];
    NetterBody.CopyTo(tempNetterBody, 0);
    NetterBody = tempNetterBody;
  }
  NetterBody[NetterBodyLength] = new NetterBod(IsComputer);
  NetterBody[NetterBodyLength].Location =
    NetterBody[NetterBodyLength-1].Location;

  // Update the whole body’s position and then the head
     position.
  for(int i=NetterBodyLength-1; i>=1; i–) {
    NetterBody[i].Location = NetterBody[i-1].Location;
  }

  NetterBody[0].Location = Location;
  NetterBody[0].Draw(WinHandle);

  NetterBodyLength++;
  // Update the netterpillar head position.
  Location = new Point(x, y);

  //Clear the mushroom.
  Erase(WinHandle);

  // Draw the netterpillar head.
  Draw(WinHandle);
  // Reset the direction controller variable.
  directionSet = false;
}

One extra detail here is that you need to erase the mushroom as you are eating it. You can do that by simply calling the Erase method before you call the Draw method of the Netterpillar class.

 

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Main Program and GameEngine Class}

To test your Netterpillar class, you can add the MoveNetterpillars procedure to the GameEngine and improve the keypress code of the frmGameField to update the direction of your netterpillar.

In order to make the code more readable, you’ll add a Player1 property, which will point to the netterpillar that the player controls. Your netterpillar won’t be eating anything for now; you’ll test the EatAndMove method after you code the collision detection in the GameEngine class, in the final version of the game.

public Netterpillar[] netterPillars = new Netterpillar[4];
public Netterpillar Player1;

You can update the constructor of the GameEngine class to add four new netterpillars, and point the Player1 property to the first one, adding the following lines of code:

netterPillars[0] =
  new Netterpillar((int)(this.Width/3), (int)(this.Height)
    /3,Sprite.CompassDirections.South, false);
  netterPillars[1] =
    new Netterpillar((int)(this.Width/3), (int)
      (this.Height)/3*2,
       Sprite.CompassDirections.East, true);
  netterPillars[2] =
    new Netterpillar((int)(this.Width/3)*2, (int)
      (this.Height)/3*2,
  s   s                                   Sprite.CompassDirections.North, true);
  netterPillars[3] =
    new Netterpillar((int)(this.Width/3)*2, (int)
     (this.Height)/3,
      Sprite.CompassDirections.West, true);
  Player1 = netterPillars[0];


NOTE Notice that you put the netterpillars a distance apart from each other, and set their initial direction to be different, so they won’t hit each other just after the game starts.

Your Move method is ready to move the netterpillar using the direction dictated by the game engine, so you’ll create a simple MoveNetterpillars method in the GameEngine class that will update the x or y position of each of the netter-pillars, based on their current direction.

public void MoveNetterpillars() {
  int incX = 0; int incY = 0;
  for(int i=0; i<NetterpillarNumber; i++) {
    // Moves all the Netterpillars.
    switch(netterPillars[i].Direction) {
      case Sprite.CompassDirections.East:
        incX = 1;
        incY = 0;
        break;
      case Sprite.CompassDirections.West:
        incX = -1;
        incY = 0;
        break;
      case Sprite.CompassDirections.North: 
        incX = 0;
        incY = -1;
        break;
      case Sprite.CompassDirections.South:
        incX = 0;
        incY = 1;
        break;
    }
    netterPillars[i].Move(netterPillars[i].Location.X+incX,
      netterPillars[i].Location.Y+incY, ScreenWinHandle);
  }
}

To finish the second draft of the GameField class, you need to call the MoveNetterpillars method from the Render procedure, as follows:

public void Render() {
  MoveNetterpillars();
  Redraw();
}

and update the Redraw method to include the lines that will draw the netterpillars.

for(int i=0; i<NETTERPILLARNUMBER; i++) { 
  netterPillars[i].Draw(ScreenWinHandle);
}

Your Main program won’t need any updates, but if you want to test the Move method, you’ll have to add some new lines in the keyboard handler to update the direction of the player’s character depending on which key is pressed.

private void frmGameField_KeyDown(object sender,
        KeyEventArgs e) {
  // Just set the next direction for the player.
  // We will not let the player go backwards from the
     current direction,
  // because he would die if he does so.
  switch(e.KeyCode) {
    case Keys.Right:
      if (MainGame.objGameEngine.Player1.Direction !=
          Sprite.CompassDirections.West) {
        MainGame.objGameEngine.Player1.Direction =
          Sprite.CompassDirections.East;
      }
      break;
    case Keys.Left:
      if (MainGame.objGameEngine.Player1.Direction!=
          Sprite.CompassDirections.East) {
        MainGame.objGameEngine.Player1.Direction =
          Sprite.CompassDirections.West;
      }
      break;
    case Keys.Up:
      if (MainGame.objGameEngine.Player1.Direction!=
          Sprite.CompassDirections.South) {
        MainGame.objGameEngine.Player1.Direction =
          Sprite.CompassDirections.North;
      }
      break;
    case Keys.Down:
      if (MainGame.objGameEngine.Player1.Direction!=
          Sprite.CompassDirections.North) {
        MainGame.objGameEngine.Player1.Direction =
          Sprite.CompassDirections.South;
     }
     break;
    case Keys.Escape:
      objGameEngine.GameOver = true;
      break;
  }
}

In the keyboard handler in the preceding code, note the conditional statements: These test the current player’s direction and stop it from running backwards, which will lead to the immediate death of the netterpillar when you include collision detection.

Figure 2-16. Testing the netterpillars

Figure 2-16.  Testing the .Netterpillars 

This test will be a really quick one: Because you aren’t implementing collision detection yet, nor the AI, the computer-controlled netterpillars will go straight through the field and disappear off the edge of the screen. The game will crash a few seconds after that.

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Third Draft: Coding the Game Engine and Collision Detection}

In this last part of your coding, you’ll finish the GameEngine class and code the AI for the computer-controlled netterpillars. You’ll also add the code to allow the configuration screen to function properly.

The GameEngine Class

To code the interface of the GameEngine class, you must refer to the class diagram created in the game project phase and include the implementation details. The GameEngine class interface is presented in the following code listing:

public class GameEngine {

  public int Width = 25;
  public int Height = 25;
  public static Image BackgroundImage;

  // This array and enum controls the object collision.
  protected static GameObjects[,] GameField;
  protected enum GameObjects {
    Mushroom = 0,
    Empty = 1,
    //Spider = 2
    Branch = 3,
    Netterpillar = 4
  };
  private System.IntPtr ScreenWinHandle;

  // Game objects
  private Branch[] objBranches;

  private Mushroom objMushrooms;
  private int MushroomNumber = 75;

  public Netterpillar[] netterPillars = new Netterpillar
     [4];
  public int NetterpillarNumber = 1;
  public Netterpillar Player1;

  //Controls the game end.
  public bool GameOver;
  public bool Paused;

  // These properties are defined as property procedures,
  // and they use the enumerations above as property types.
  public GameFieldSizes Size {…}
  public MushroomSizes Mushrooms {…}

  public void MoveNetterpillars() {…}
  public void KillNetterPillar(Netterpillar Netterpillar)
    {…}
  public void Render() {…}
  public void CreateGameField(System.IntPtr WinHandle) {…}
  public void Redraw() {…}
}

Next, you start coding the collision detection, which is accomplished by making the GameField array hold all the game objects and, before moving the netterpillars (the only moving objects), checking to see whether there’s any collision.

You fill the array in the constructor, including some lines to set the array just after creating the objects. At this point, you can make a simple improvement in your new and Draw procedures: Instead of creating dozens of mushroom objects, you could create a single object and move it as needed to draw all the mushrooms on the game field. This will have no effect on your collision detection algorithms, since you’ll use GameField instead of the Mushroom object to test the collision.

You’ll just include the following lines in the new procedure you coded previously, starting with an initialization loop that will set all the objects in the array to Empty:

// Initialize the game array (for collision detection).
for(int x=0; x<WIDTH; { < P x++) 
  for(int y=”0;” y<Height; y++) }
    GameField[x, y]=”GameObjects.Empty;
   }
}

After creating the netterpillars, you insert the code for setting all the positions in the array (head and bodies) for each netterpillar to the Netterpillar GameObjects enumeration member.

// Populates the array with the netterpillars.
for(int i=0; i<NetterpillarNumber; i++) {
  GameField[netterPillars[i].Location.X,
      netterPillars[i].Location.Y] =
    GameObjects.Netterpillar;
  for(int j=0; j<netterPillars[i].NetterBodyLength; j++) {
      GameField[netterPillars[i].NetterBody[j].Location.X,
    netterPillars[i].NetterBody[j].Location.Y] = 
      GameObjects.Netterpillar;
  }
}

Since the branches are just limiting your game field, you can simply do some loops that will set all the borders (the array elements with x = 0, y = 0, x = Width – 1, or y = Height – 1) to the Branch GameObjects enumeration member.

for(int x=0; x<Width; x++) {
  GameField[x, 0] = GameObjects.Branch;
  GameField[x, Height-1] = GameObjects.Branch;
}
for(int y=0; y<=Height; y++) {
  GameField[0, y] = GameObjects.Branch;
  GameField[Width-1, y] = GameObjects.Branch;
}

And as for the mushrooms, you just need to set the array position to the enumeration element Mushroom for each new mushroom added. You also need to make two more improvements to the code you used previously as a stub: First, let’s check whether the random array position chosen has no objects in it, and if it does, choose another position, until you find an Empty array slot. Second, as planned before, let’s save some memory by creating just one mushroom, and simply moving it from place to place when you need to draw the game field. The final code for the mushroom creation will be as follows:

objMushrooms = new Mushroom();
for(int i=0; i<MushroomNumber; i++) {
  // Check to seek if you are not creating the mushrooms 
     over other objects.
  do {
    x = rand.Next(0, this.Width-2)+1;
    y = rand.Next(0, this.Height-2)+1;
  } while(GameField[x, y]!=GameObjects.Empty);
  GameField[x, y] = GameObjects.Mushroom;
}

Note that you have to change the objMushrooms property definition to a variable, instead of an array. The code for drawing the mushrooms in the Redraw method will be as follows:

for(int x=0; x<Width; x++) {
  for(int y=0; y<Height; y++) {
    if (GameField[x, y]==GameObjects.Mushroom) {
      objMushrooms.Location = new Point(x, y);
      objMushrooms.Draw(ScreenWinHandle);
    }
  }
}

With these modifications, your constructor and Draw methods are filling the array that will help you with collision detection. You now need to change the MoveNetterpillars method to check for any collisions when the netterpillars move, and take the appropriate actions as follows:

  • Kill the netterpillar if it hits an obstacle.

  • Make the netterpillar bigger when it collides with a mushroom, calling the EatAndMove method of the Netterpillar class; at this point you should decrement the mushroom number counter in order to know when all the mushrooms have been eaten.

  • Move the netterpillar when there’s no collision.

In each case, you’ll have to remember to empty the array in every position the netterpillar has visited previously to avoid ghost collisions. You’ll have to change the call of the Move method in MoveNetterpillars to a selection that takes into account the actions just mentioned. Remember, this code goes immediately after the code that will set the incX and incY variables; to point to the next position the netterpillar will occupy, you have to test the current position added to these increment variables.

switch(GameField[netterPillars[i].Location.X+incX,
    netterPillars[i].Location.Y+incY]) {
  case GameObjects.Empty:
    // Update the Game Field – Empty the field after the
      
Netterpillar.
    GameField[netterPillars[i].NetterBody[netterPillars[i].
      NetterBodyLength-1].Location.X,
        netterPillars[i].NetterBody[netterPillars[i].
          NetterBodyLength-1].Location.Y] =
            GameObjects.Empty;
    // Move the Netterpillar.
    netterPillars[i].Move(netterPillars[i].Location.X+incX,
      netterPillars[i].Location.Y+incY, ScreenWinHandle);
    // Update the Game Field – Sets the Netterpillar Head.
    GameField[netterPillars[i].Location.X,
      netterPillars[i].Location.Y] =
        GameObjects.Netterpillar;
    break;
  case GameObjects.Mushroom:
    // Decrement the number of Mushrooms.
    MushroomNumber–;
    netterPillars[i].EatAndMove(netterPillars
        [i].Location.X+incX,
      netterPillars[i].Location.Y+incY, ScreenWinHandle);
    // Update the Game Field – Sets the Netterpillar Head.
    GameField[netterPillars[i].Location.X,
      netterPillars[i].Location.Y] =
        GameObjects.Netterpillar;
    break;
  default:
    KillNetterPillar(netterPillars[i]);
    break;
}

All you need to do now to test your program is to code the KillNetterpillar method, which will erase the netterpillar from the game field and do the updates on the Netterpillar object and the array field.

public void KillNetterPillar(Netterpillar Netterpillar) {
  Netterpillar.IsDead = true;
  // Clears the game field.
  GameField[Netterpillar.Location.X,
      Netterpillar.Location.Y] =
    GameObjects.Empty;
  Netterpillar.Erase(ScreenWinHandle);

  for(int i=0; i<Netterpillar.NetterBodyLength; i++) {
    GameField[Netterpillar.NetterBody[i].Location.X,
      Netterpillar.NetterBody[i].Location.Y] =
        GameObjects.Empty;
      Netterpillar.NetterBody[i].Erase(ScreenWinHandle);
  }
}

In the previous code, you reset the array elements for the head and the bodies, called the Erase method of the Netterpillar class to remove it from sight, and finally set the IsDead property of the netterpillar to true.

At this point, after coding the KillNetterpillar method, you may have noticed that you forgot to do something on the methods you had already coded: You forgot to test whether the netterpillar is alive when moving it at the MoveNetterpillars method and when drawing it in the Redraw method! Okay, don’t panic, you can just add an if statement to solve this. The MoveNetterpillar method will become as follows:

public void MoveNetterpillars() {
  …
  for(i=0; i<NETTERPILLARNUMBER; i++) { 
   
if (!netterPillars[i].IsDead) {
      … // Put the “MoveNeterpillar” code here.
    }
  }
}

And here’s how the Redraw procedure will look:


for(int i=0; i<NETTERPILLARNUMBER; i++) { 
  if (!netterPillars[i].IsDead) {
    netterPillars[i].Draw(ScreenWinHandle);
 
}
}

This will prevent the dead netterpillar from moving and being drawn again.

You can test your program now. The interface will be the same as in the second draft, but now you can effectively eat the mushrooms and get bigger, and die when you hit an obstacle.

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Fourth Draft: Coding the Config Screen and Game Over}

Before starting the AI code, let’s include some details in your game, adding the code for the configuration screen and the test for ending the game.

Coding for the Configuration Screen

Looking at the visual prototype of the configuration screen, you can see that you have two properties that don’t map directly to numbers: the game field size and the quantity of mushrooms on the screen. In order to create a more direct mapping from the screen to the game field properties, let’s add two property procedures: Size (which will set the width and height properties) and Mushrooms (which will set the MushroomNumber property, according to the current size of the game field), as shown in the following code.

Here’s the Size property:

public enum GameFieldSizes {
  Small = 2,
  Medium = 1,
  Big = 0
};
private GameFieldSizes size = GameFieldSizes.Medium;
public GameFieldSizes Size {
  get {
    return size;
  }
  set {
    size = value;
    switch(value) {
      case GameFieldSizes.Small:
        Width = 15;
        Height = 15;
        break;
      case GameFieldSizes.Medium:
        Width = 25;
        Height = 25;
        break;
      case GameFieldSizes.Big:
        Width = 40;
        Height = 30;
        break;
    }
  }
}

And now the Mushroom property:

public enum MushroomSizes {
  Few = 2,
  JustRight = 1,
  Many = 0
};
public MushroomSizes Mushrooms {

  get {
    return mushroomQty;
  }
  set {
    mushroomQty = value;
    switch(value) {
      case MushroomSizes.Few:
        MushroomNumber = 25;
        break;
      case MushroomSizes.JustRight:
        MushroomNumber = 75;
        break;
      case MushroomSizes.Many:
        MushroomNumber = 125;
        break;
    }

    if (Size==GameFieldSizes.Medium) {
      MushroomNumber *= 2;
    }
    else if(Size==GameFieldSizes.Big) {
      MushroomNumber *= 3;
    }
  }
}

You must adjust the constructor too, because you are always creating four netterpillars. Instead of using a fixed number, you should use the NetterpillarNumber property, which will be set in the configuration window.

Because you’ll be creating one to four netterpillars, let’s define where each of them will be created:

  • If you have one netterpillar, create it in the center of the screen.

  • If you have two netterpillars, create them in the center of the y axis (vertical), and at 1/3 and 2/3 along the x axis (horizontal), so you’ll have a constant distance from the borders to the netterpillars and between the two netterpillars. It’s better to initialize them running in different directions; so one will head north and another south.

  • If you have three netterpillars, you’ll put them at 1/4, 2/4, and 3/4 along the x axis, and in the middle of the y axis, heading south, north, and south again.

  • If you have four netterpillars, you’ll put them in a square, each heading in the direction of the next vertex. The vertices will be at 1/3 vertical, 1/3 horizontal; 1/3 vertical, 2/3 horizontal; 2/3 vertical, 2/3 horizontal; and 2/3 vertical, 1/3 horizontal.

The code for this logic is show here:

// Create the Netterpillars.
switch(NetterpillarNumber) {
  case 1:
    netterPillars[0] = new Netterpillar((int)
      (this.Width/2),(int)(this.Height)/2, 
       Sprite.CompassDirections.South, false);
    break;
  case 2:
    netterPillars[0] = new Netterpillar((int)
      (this.Width/3),(int)(this.Height)/2,
       Sprite.CompassDirections.South, false);
    netterPillars[1] = new Netterpillar((int)(this.Width/3)
      *2,(int)(this.Height)/2,
      Sprite.CompassDirections.North, true);
    break;
  case 3:
    netterPillars[0] = new Netterpillar((int)
      (this.Width/4),(int)(this.Height)/2,
       Sprite.CompassDirections.South, false);
    netterPillars[1] = new Netterpillar((int)(this.Width/4)
      *2,(int)(this.Height)/2,
      Sprite.CompassDirections.North, true);
    netterPillars[2] = new Netterpillar((int)(this.Width/4)
      *3,(int)(this.Height)/2,
      Sprite.CompassDirections.South, true);
    break;
  case 4:
    netterPillars[0] = new Netterpillar((int)
      (this.Width/3),(int)(this.Height)/3,
       Sprite.CompassDirections.South, false);
    netterPillars[1] = new Netterpillar((int)
      (this.Width/3),(int)(this.Height)/3*2,
       Sprite.CompassDirections.East, true);
    netterPillars[2] = new Netterpillar((int)(this.Width/3)
      *2,(int)(this.Height)/3*2,
      Sprite.CompassDirections.North, true);
    netterPillars[3] = new Netterpillar((int)(this.Width/3)
      *2,(int)(this.Height)/3, 
      Sprite.CompassDirections.West, true);
    break;
}

To allow you to test the configuration code, you need to add some lines to the Load event and the OK button of the configuration screen.

When loading the form, you must set the controls to the current value of each of the objGameEngine configuration properties.

private void frmConfig_Load(object sender,
    System.EventArgs e) {
  updGameField.SelectedIndex = (int)
    MainGame.objGameEngine.Size;
  updNetterpillars.Value =
    MainGame.objGameEngine.NetterpillarNumber;
 
updMushrooms.SelectedIndex = (int)
    MainGame.objGameEngine.Mushrooms;
}

In the OK click procedure, you’ll do the opposite, setting the objGameEngine properties to the values set on the form.

private void cmdOK_Click(System.Object sender,
     System.EventArgs e) {
  MainGame.objGameEngine.Size =
    (GameEngine.GameFieldSizes)updGameField.SelectedIndex; 
  MainGame.objGameEngine.NetterpillarNumber = (int)
    System.Math.Round(updNetterpillars.Value);
  MainGame.objGameEngine.Mushrooms =
    (GameEngine.MushroomSizes)updMushrooms.SelectedIndex;
}

Everything is now correctly positioned, but you need to show the configuration dialog box at some point in the program, or else you won’t be able to change the configuration settings. It’s time to go back to your Main procedure and include in it the main window, through which you can change the configuration, start a new game, or exit the game. The window will be the one that was shown as a visual prototype in the project phase, including some lines of code in the Config button to show the configuration screen, as demonstrated in the code that follows. Some code to close the window must also be included on the click event of the Exit button.

private void cmdConfig_Click(System.Object sender,
        System.EventArgs e) {
  frmConfig WinConfig;
  WinConfig = new frmConfig();
  WinConfig.ShowDialog();
  WinConfig.Dispose();
}

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Coding for the Introduction Screen}

Now is a good time to create an intro screen for your game. Our suggestion is shown in Figure 2-17, but feel free to use your artistic talent to improve it.

Figure 2-17.  The .Netterpillars splash screen

The Main procedure must be changed to reflect the workflow diagram created in the project phase.

public static void Main(string [] args) {
  frmSplash WinSplash;
  frmGameField WinGameField;
  frmGameOver WinGameOver = new frmGameOver();
  int LastTick=0; int DesiredFrameRate = 10;

  // Create the game engine object.
  objGameEngine = new GameEngine();
  WinSplash = new frmSplash();
  while ( WinSplash.ShowDialog()==DialogResult.OK) {
    WinGameField = new frmGameField();
    WinGameField.Show();
    Application.DoEvents();
    // Create a copy of the background image to allow
       erasing the sprites.
    GameEngine.BackgroundImage =(Image) 
      WinGameField.PicGameField.Image.Clone();
    objGameEngine.CreateGameField
      (WinGameField.PicGameField.Handle);
    while ( !objGameEngine.GameOver) {
      if (!objGameEngine.Paused) {
        // EXTRA: Force a Frame rate of 10 frames per
           second on maximum.
        if (System.Environment.TickCount-
            LastTick>=1000/DesiredFrameRate) {
          MoveComputerCharacters();
          objGameEngine.Render();
          LastTick = System.Environment.TickCount;
        }
      }
      Application.DoEvents();
    }
    WinGameOver.ShowDialog();
    WinGameField.Dispose();
  }
  objGameEngine = null;
  WinSplash.Dispose();
  WinGameOver.Dispose();
}

That’s it. You can now play with different field sizes, number of mushrooms, and netterpillars. But after playing a couple of times, you’ll soon discover that when you run your game a second time without making any configuration changes, your properties don’t get reset; so, among other things, you’ll start with the last quantity of mushrooms (that is, without the ones that were eaten). And worst of all: If the game field screen is being created for each game, your handle (passed to the objGameEngine constructor) becomes invalid.

Since you can’t simply move the objGameEngine creation to inside the loop (you’ll need it in the configuration screen, and if you re-create the object, the previous configuration will be lost), a solution is to create a new method to reset the game variables, which can be called just after the program Game Over loop. You can call this method CreateGameField, and move all the code from the constructor to it, including the parameter that receives the window handle.

We have shown these details to clarify a point: A game project, as any other project, will have problems en route. The better the project, the less unexpected the behavior in the coding phase. Nevertheless, there’s no way to guarantee immediate success. Don’t be ashamed to go back and correct everything if you think that it’ll make your game faster, more stable, or easier to update with new features.

Another detail that requires extra care is the code for setting the game field size: When you resize the game field, the game field window must be resized accordingly. You must do that in the Load event of the frmGameField window.

private void frmGameField_Load(System.Object sender,
          System.EventArgs e) {
 
PicGameField.Location = new Point(0, 0);
 
PicGameField.Size = new Size
    (MainGame.objGameEngine.Width*Sprite.IMAGE_SIZE,
    
MainGame.objGameEngine.Height*Sprite.IMAGE_SIZE);
  this.ClientSize = PicGameField.Size;
}

With this last little adjustment, your code will work. But you don’t have code for the game over yet. We’ll show that next.

Coding for Game Over

Looking back at the game proposal, you can see that we stated “The game is over when all the players die (computer or human ones), or when the last mushroom is eaten.”

Since you have a property stating whether a player is dead or not and a property that stores the number of mushrooms (that is already reduced every time a mushroom is eaten), all you need to do is include the code in the Render procedure to test the preceding conditions and set the GameOver property to True if one of the requirements is met.

public void Render() {
  // Move the Netterpillars.
  MoveNetterpillars();

  // If all Netterpillars die – GameOver.
  GameOver = true;
  for(int i=0; i<NetterpillarNumber; i++) {
    if (!netterPillars[i].IsDead) {
    GameOver = false;
    }
  }

  // If all mushrooms got eaten – Game Over.
  if (MushroomNumber==0) {
    GameOver = true;
  }
}

You mustn’t forget to remove the code for forcing the game to finish when the Esc key is pressed on the keyboard event handler for the frmGameField, unless you need this behavior in your finished game.

Although the code for the game over works fine, it can be improved if you include a screen with game statistics—such as the netterpillar’s size—so players can have clearer information about how well they played. Such a screen is added in the “Adding the Final Touches” section; for now, let’s alter your code to include a real computer-controlled competitor.

 

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Final Version: Coding the Netterpillars AI}

To finish your game, you need to code the NetterpillarAI class and make the final adjustments in the Main procedure, as shown in the next sections.

The Netterpillar AI Class

As you decided in the game proposal and in the game project, you only need to use a simple form of artificial intelligence. Just avoid walls and eat mushrooms if they are near, that’s all.

public class AINetterpillar : GameEngine {
  private int RandomPercent = 5;
  public Sprite.CompassDirections
    ChooseNetterpillarDirection
     
(Point CurrentLocation, Sprite.CompassDirections
       CurrentDirection) {…}
  public Sprite.CompassDirections
    RandomDirection (Point CurrentLocation,
      Sprite.CompassDirections ChoosCompassDirections){…}
}

Let’s review what the game objects are.

protected enum GameObjects {
  Mushroom = 0,
  Empty = 1,
  Branch = 2,
  Netterpillar = 3
};

Not by accident, when you define this enumeration, you put the game objects in ascending order of collision preference. When you check the objects around you, the lowest value is the preferred one: A mushroom is better than empty space, and both are preferable to a collision resulting in death. You can use this to your advantage, to ease the choice of the best object by checking the lowest value (with the min function) from the positions around the current position of the netterpillar’s head.

BestObject = (GameObjects)Math.Min(Math.Min(Math.Min(
(int)GameField[CurrentLocation.X+1, CurrentLocation.Y],
(int)GameField[CurrentLocation.X-1, CurrentLocation.Y]), (int)GameField[CurrentLocation.X, CurrentLocation.Y+1]), (int)GameField[CurrentLocation.X, CurrentLocation.Y-1]);

Once the best object has been chosen, you can check it against the next object in the current direction; and if they are the same (there can be two or more optimal objects), you choose to stay in the current direction to make the netterpillar’s movement less erratic.

One last step is to add some random behavior to make the movement less predictable and less prone to getting stuck in an infinite loop; for example, the netterpillar could move in circles around the game field forever if there’s no aleatory component. In your tests, anything greater than 10 percent randomness can lead to erratic behavior (remember, you choose a new direction many times a second); a value between 0 and 5 generates good results.

public Sprite.CompassDirections ChooseNetterpillarDirection
  (Point CurrentLocation, Sprite.CompassDirections
      CurrentDirection) {
  Sprite.CompassDirections
      ChooseNetterpillarDirection_result =
    (Sprite.CompassDirections)0;
  GameObjects BestObject;
  GameObjects NextObject = (GameObjects)0;

  switch(CurrentDirection) {
    case Sprite.CompassDirections.East:
      NextObject = GameField[CurrentLocation.X+1, 
        CurrentLocation.Y];
      break;
    case Sprite.CompassDirections.West:
      NextObject = GameField[CurrentLocation.X-1,
        CurrentLocation.Y];
      break;
    case Sprite.CompassDirections.South:
      NextObject = GameField[CurrentLocation.X,
        CurrentLocation.Y+1];
      break;
    case Sprite.CompassDirections.North:
      NextObject = GameField[CurrentLocation.X,
        CurrentLocation.Y-1];
      break;
  }

  //Pick the lowest value – Mushroom or empty.
  BestObject = (GameObjects)Math.Min(Math.Min(Math.Min(
  (int)GameField[CurrentLocation.X+1, CurrentLocation.Y],
  (int)GameField[CurrentLocation.X-1, CurrentLocation.Y]),
  (int)GameField[CurrentLocation.X, CurrentLocation.Y+1]),
  (int)GameField[CurrentLocation.X, CurrentLocation.Y-1]);

  // If the current direction is the best direction, stay
     in current direction.
  if (NextObject==BestObject) {
    ChooseNetterpillarDirection_result = CurrentDirection;
  }
  else {
    // Select the direction of the best object.
    if (BestObject == GameField[CurrentLocation.X+1, 
        CurrentLocation.Y]) {
      ChooseNetterpillarDirection_result =
        Sprite.CompassDirections.East;
    }
    else if (BestObject == GameField[CurrentLocation.X-1,
             CurrentLocation.Y]){
      ChooseNetterpillarDirection_result =
        Sprite.CompassDirections.West;
    }
    else if (BestObject == GameField[CurrentLocation.X,
             CurrentLocation.Y+1]){
      ChooseNetterpillarDirection_result =
        Sprite.CompassDirections.South;
    }
    else if (BestObject == GameField[CurrentLocation.X,
             CurrentLocation.Y-1]){
      ChooseNetterpillarDirection_result =
        Sprite.CompassDirections.North;
   }
  }

  ChooseNetterpillarDirection_result = RandomDirection
    (CurrentLocation, ChooseNetterpillarDirection_result);
  return ChooseNetterpillarDirection_result;
}



To code the RandomDirection method, called in the last line of the preceding code, you’ll simply pick a random number from 0 to 100, and if it’s less than the RandomPercent property, choose a new movement direction for the netterpillar. The next code sample presents the full code for this method.



private static Random rand = new Random();
public Sprite.CompassDirections RandomDirection
  (Point CurrentLocation, Sprite.CompassDirections
   ChoosCompassDirections) {
  Sprite.CompassDirections RandomDirection_result;
  int x = rand.Next(0, 100);

  RandomDirection_result = ChoosCompassDirections;
  if (x<RandomPercent) {
    switch(ChoosCompassDirections) {
      case Sprite.CompassDirections.East:
        // Try the other directions.
        if (GameField[CurrentLocation.X,
            CurrentLocation.Y+1]<=GameObjects.Empty) {
          RandomDirection_result =
            Sprite.CompassDirections.South;
        }
        else if(GameField[CurrentLocation.X,
                CurrentLocation.Y-1]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.North;
        }
        else if(GameField[CurrentLocation.X-1,
                CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.West;
        }
        break;
      case Sprite.CompassDirections.West:
        // Try the other directions.
        if (GameField[CurrentLocation.X,
            CurrentLocation.Y+1]<=GameObjects.Empty) {
          RandomDirection_result =
            Sprite.CompassDirections.South;
        }
        else if(GameField[CurrentLocation.X,
                CurrentLocation.Y-1]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.North;
        }
        else if(GameField[CurrentLocation.X+1,
               
CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.East;
        }
        break;
      case Sprite.CompassDirections.North:
        // Try the other directions.
        if (GameField[CurrentLocation.X,
            CurrentLocation.Y+1]<=GameObjects.Empty) {
          RandomDirection_result =
            Sprite.CompassDirections.South;
        }
        else if(GameField[CurrentLocation.X+1,
                CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.East;
        }
        else if(GameField[CurrentLocation.X-1,
                CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.West;
        }
        break;
      case Sprite.CompassDirections.South:
        // Try the other directions.
        if (GameField[CurrentLocation.X,
            CurrentLocation.Y-1]<=GameObjects.Empty) {
          RandomDirection_result =
            Sprite.CompassDirections.North;
        }
        else if(GameField[CurrentLocation.X+1,
                CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result =
                 Sprite.CompassDirections.East;
        }
        else if(GameField[CurrentLocation.X-1,
                CurrentLocation.Y]<=GameObjects.Empty) {
               RandomDirection_result = 
                 Sprite.CompassDirections.West;
        }
        break;
    }
  }
  return RandomDirection_result;
}

Since the code in the GameEngine is intended to take care of the game’s physics (for example, it moves the netterpillars, regardless of whether one is changing direction), you’ll have to put the code for moving the netterpillars based on the AI outside the game engine object; your Main procedure is the best option.

Another valid approach would be to include the AI code inside the Netterpillar object—it’s just a matter of choice: a small number of bigger classes or many smaller ones.

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=The Main Program: Final Version}

In order to call the AI code, you create a new procedure, which will be called from the game main loop. The procedure, shown in the following code, just loops through the Netterpillars objects and, if they aren’t dead and are computer controlled, sets the current direction to the result of the ChooseNetterpillarDirection method:

public static void MoveComputerCharacters() {
  //Move the Netterpillars.
  for(int i=0; i<objGameEngine.NetterpillarNumber; i++) {
    if (!objGameEngine.netterPillars[i].IsDead) {
      // A.I. for the computer-controled Netterpillars.
      if (objGameEngine.netterPillars[i].IsComputer) {
        objGameEngine.netterPillars[i].Direction =
          objAINetterpillar.ChooseNetterpillarDirection
           (objGameEngine.netterPillars[i].Location,
           objGameEngine.netterPillars[i].Direction);
      }
    }
  }
}

The main program loop should include one more section to call the MoveComputerCharacters procedure.

while ( !objGameEngine.GameOver) {
  MoveComputerCharacters();
  objGameEngine.Render();
  Application.DoEvents();
}

This finishes the coding phase; some code to add polish to the final product is suggested in the next section.

Adding the Final Touches

In this section, you add some extra features to your game. These final touches, although simple, are important and need to be considered.

Coding the Pause Game Feature

As in the .Nettrix game, you could insert code to pause (and restart) the game when the Esc key is pressed. This basic improvement is shown here:

private void frmGameField_KeyDown(object sender,
    KeyEventArgs e) {
  …
    case Keys.Escape:
      MainGame.objGameEngine.Paused = !
        MainGame.objGameEngine.Paused;
      if (MainGame.objGameEngine.Paused) {
        this.Text = “.Netterpillars – Press ESC to 
                    continue”;
      }
      else {
        this.Text = “.Netterpillars”;
      }
      break;
  }
}

Improving the Game Over Screen

Your game over routine also needs an improvement. A good game programmer shouldn’t forget that a good game ending is far more important than a nice intro screen. Players must be rewarded for all their efforts in completing the game; it’s very frustrating for players to spend days and days finishing a game and not getting anything in return to give them a feeling of accomplishment. In this game, the Game Over message box is one of these frustrations. Although a high scores table would be better, let’s at least give players some feedback about the results of the game and how well they played.

You can do this by creating a new Game Over window, where you can display some game statistics, as shown in Figure 2-18.

Figure 2-18.  A Game Over screen

This screen can access the objGameEngine, which is a public variable, and gather information about players and how long their netterpillars were when the game finished.

To load the label with the statistics, you must access each of the netterPillars objects, checking the IsComputer property and the NetterBodyLength property. You’ll need to avoid unset objects (remember, the player could be playing with any number of opponents, from 0 to 3).

The ternary operators in the next code sample (which must be placed in the Load event of the window) aren’t new to .NET, although they aren’t commonly used because sometimes they can lead to more complex code. The ternary operator tests the first parameter (an expression) and, if true, returns the second parameter; otherwise it returns the last parameter.

LblPlayer1Length.Text =
  MainGame.objGameEngine.netterPillars[0]
    .NetterBodyLength.ToString();
LblPlayer1Is.Text = MainGame.objGameEngine.netterPillars[0]
    .IsComputer ? “Computer” : “Human”;

if (MainGame.objGameEngine.netterPillars[1]!=null) {
  LblPlayer2Length.Text =
    MainGame.objGameEngine.netterPillars[1]
      .NetterBodyLength.ToString();
  LblPlayer2Is.Text =
    MainGame.objGameEngine.netterPillars[1].IsComputer
      ? “Computer” : “Human”;
}
else {
  LblPlayer2Length.Text = “-”;
  LblPlayer2Is.Text = “-”;
}

if (MainGame.objGameEngine.netterPillars[2]!=null) {
  LblPlayer3Length.Text =
    MainGame.objGameEngine.netterPillars[2]
      .NetterBodyLength.ToString();
  LblPlayer3Is.Text = 
    MainGame.objGameEngine.netterPillars[2].IsComputer
      ? “Computer” : “Human”;
}
else {
  LblPlayer3Length.Text = “-”;
  LblPlayer3Is.Text = “-”;
}
if (MainGame.objGameEngine.netterPillars[3]!=null) {
  LblPlayer4Length.Text =
    MainGame.objGameEngine.netterPillars[3]
      .NetterBodyLength.ToString();
  LblPlayer4Is.Text =
    MainGame.objGameEngine.netterPillars[3].IsComputer
      ? “Computer” : “Human”;
}
else {
  LblPlayer4Length.Text = “-”;
  LblPlayer4Is.Text = “-”;
}

In final version of the main program, you must replace the Game Over message box by a call to the ShowDialog method of the game over form.

Coding for the Garbage Collection

A technical enhancement is to improve the speed of the garbage collection by calling the Collect method of the System.GC object, in the end of the Render method, as shown:

public void Render() {
  …
  System.GC.Collect();

}


NOTE The .NET Framework provides an advanced garbage collector that frees the memory from all objects left behind by the program. The garbage collection takes place in idle system time, but you can force it to run by calling the Collect method, which is good practice if you are dealing with lots of memory allocations and reallocations—which you do, for example, with the Graphics object in each Draw method in the game objects.

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

{mospagebreak title=Further Improvements}

We saved the best for last: What about creating new intelligent characters for your game, maybe some opposition—like a spider who eats the netterpillars?

In the code for this chapter on the samples CD-ROM, you will find an almost fully working spider character. You already did all the dirty work: The Spider and AISpider class interfaces, the call to the moving functions at the MoveComputerCharacters routine and at the Render and Redraw method of the objGameEngine—almost everything is there. The code for ChooseDirection method of the AISpider class is empty, so your spiders aren’t going anywhere. This gives you the opportunity to create the AI from scratch, without worrying about the details. Will the spider attack the netterpillars’ heads and kill them? Or will they just eat part of their tails? Or, maybe make new mushrooms grow? Start making your proposal for the second version of the game, and enjoy!

Summary

In this chapter, via the .Netterpillars game sample, we explored some additional concepts related to game programming, including:

  • Basic concepts about object-oriented programming and analysis

  • Basic concepts about artificial intelligence, and ideas about how to implement it to solve different challenges when programming games

  • The difference between game AI and game physics

  • How to create a basic objects library and use its derived classes in games

  • How to produce high-performance drawings with GDI+, when you need to draw images with transparent colors

  • How to create computer-controlled characters that interact with the game engine like player-controlled characters, with the same physics restrictions

In the next chapter, we’ll introduce you to the use of DirectX graphics with a sample program that will test many of the basic features of Direct3D, so you can use these concepts in the example games in later chapters and in your own games.

Acknowledgments

The authors would like to thank Steven Toub, who assisted in the conversion of the .Netterpillars game.

This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.

[gp-comments width="770" linklove="off" ]