Implementing the Project


Trying to understand how the Doggie application works will be much easier if you have played Pac-Man before. If you have not, I encourage you to try Doggie first before you read this section. The following section is a description of how you play the game. If you are already a Pac-Man addict, it is still a good idea to read the following to familiarize yourself with the terms used throughout this section.

Note

In describing how the application works, I have tried to adopt the easiest way possible. So, I do not explain each class in turn like I do in some other chapters. Instead, I start with the easiest part first. I explain the most difficult part of the application, the artificial intelligence that moves the cats, last.

Note

Do not panic if you do not understand when you first read this. This is not an easy topic, especially if this is your first game project. Depending on how good you are at programming, chances are you will have to read it more than once to understand the whole system.

Playing the Game

To play the game, you must first extract the Ch03Codes.zip file that you downloaded. You will find an EXE file after the extraction, and there will also be a subdirectory called images that contains all image files needed by the application.

To run the application, run this EXE file. You will see something like Figure 3-2.

click to expand
Figure 3-2: The Doggie game

It is basically a form with two menus: File and About. The File menu has two menu items: New Game and Exit. The About menu will display the About dialog box when clicked.

The client area consists of three parts:

  • The top part, which is 25-pixels high, where the score and other instructions display.

  • The maze, or the playing ground, which covers the whole client area excluding the top part and the bottom part.

  • The bottom part, 31-pixels high, where the current game level and the remaining Doggie's "lives" display.

When the application is first started, you will see Doggie (the dog in pink color). There is also the cat house at the center of the maze. There are four cats inside. The cats are the bad guys in this game. The colors of the cats are blue, green, red, and black. These cats are simply referred to as blue cat, green cat, red cat, and black cat.

The black bullets scattered around the maze are the foods. There are also four bones at the corners of the maze. You get 10 points for eating each food and bone. The blue structures that look like the letter T and squares represent walls.

To start, press the Enter key. You then use the four arrow keys (up, down, left, right) to move Doggie. Doggie can move along the paths surrounded by the walls. The aim of this game is to get the highest score by eating the foods and catching the cats.

At first the cats will chase Doggie around, and Doggie has to avoid collision with them. However, after Doggie eats one of the bones, the situation is reversed for a few seconds. Doggie becomes stronger and faster than the cats, and Doggie should catch the cats to get higher points. When Doggie catches a cat, it will change color and move to the cat house. The caught cat will then turn back into a normal cat inside the cat house. The cat in the cat house will stay there until the reversed situation is over.

When all the foods have been eaten, the current game level is over and you move to a higher level.

Classes in the Projects

The following are the classes and other types in the Doggie project:

  • The Constants class

  • The GameState enumeration

  • The Direction enumeration

  • The Form1 class

  • The GameManager class

  • The Maze class

  • The GameActor abstract class

  • The Doggie class

  • The Cat abstract class

  • The BlueCat class

  • The GreenCat class

  • The BlackCat class

  • The RedCat class

Figure 3-3 shows the class diagram.

click to expand
Figure 3-3: The class diagram

Doggie and Cat are the direct child classes of GameActor. Cat has four child classes: BlueCat, GreenCat, RedCat, and BlackCat.

During the life of the application, there is one instance of the Maze class, one instance of the GameManager class, one instance of the Form1 class, one instance of the Doggie class, one instance of the BlueCat class, one instance of the GreenCat class, one instance of the BlackCat class, and one instance of the RedCat class.

Each instance of the game actor (Doggie and the four cats) has its own thread that takes care of the movement of the game actor. There is also another thread called game that draws the frames. In addition, there is the application thread itself.

Note

You can find the code listings in the project's directory.

Some of these classes depend on each other. Therefore, explaining how the program works by discussing each class in turn will not be effective. Instead, I explain functionality starting from the functions that have least dependence. I will start from the maze.

Creating the Maze

The maze, represented by the Maze class, is the playing ground in this game. It consists of 26 columns and 31 rows, so there are 26 31 = 806 cells. Each cell is drawn as a 16 16 pixels square. Figure 3-4 shows the maze, with white lines added to indicate the partition between two maze cells.

click to expand
Figure 3-4: The maze

You can refer to each cell by its row number and its column number. For example, the cell at the first column in the first row is cell(0, 0), and the cell in the last column and the last row is cell(30, 25). As you can see, the four bones are respectively in cell(2, 1), cell(2, 24), cell(22, 1), and cell(22, 24). In the beginning of every game level, Doggie is positioned in cell(25, 13), and the four cats are in cell(13, 12), cell(13, 14), cell(15, 11), and cell(15, 14).

The blue lines represent the walls. Both Doggie and the cats can move around the maze by moving from one cell to another. However, they cannot go through the walls.

Let's start with an explanation of how to draw the maze.

Drawing the Maze

You draw the maze by drawing each cell using the DrawMaze method of the Maze class. The DrawMaze method uses the following For loops to draw each cell:

 For i = 0 To mazeRowCount - 1         'mazeRowCount = 31   For j = 0 To mazeColumnCount - 1  'mazeColumnCount = 26     DrawCell(j, i, g)   Next Next 

For each cell, it calls the DrawCell method, passing the column number (j), the row number (i), and the graphics object to draw the cells on (g).

Drawing the Cells

If you look at the maze cells in Figure 3-4, you will see that not all cells are identical. Some contain horizontal or vertical parts of a wall, some contain nothing but the food, and some are a different kind of a block. How does the DrawCell method know how to draw each cell of the maze? The answer to this lies in the maze array of strings that is declared and initialized in the Maze class (see Listing 3-4).

Listing 3-4: The String Representing the Maze

start example
 Private maze() As String = { _     "a-----------ba-----------b", _     "|***********||***********|", _     "|$a-b*a---b*||*a---b*a-b$|", _     "|*| |*| |*||*| |*| |*|", _     "|*d-c*d---c*dc*d---c*d-c*|", _     "|************************|", _     "|*a-b*ab*a------b*ab*a-b*|", _     "|*d-c*||*d--ba--c*||*d-c*|", _     "|*****||****||****||*****|", _     "d---b*|d--b*||*a--c|*a---c", _     "    |*|a--c*dc*d--b|*|    ", _     "    |*||          ||*|    ", _     "    |*|| a--##--b ||*|    ", _     "----c*dc |%%%%%%| dc*d----", _     "< *   |%%%%%%|   *    >", _     "----b*ab |%%%%%%| ab*a----", _     "    |*|| d------c ||*|    ", _     "    |*||          ||*|    ", _     "    |*|| a------b ||*|    ", _     "a---c*dc d--ba--c dc*d---b", _     "|***********||***********|", _     "|*a-b*a---b*||*a---b*a-b*|", _     "|$db|*d---c*dc*d---c*|ac$|", _     "|**||****************||**|", _     "db*||*ab*a------b*ab*||*ac", _     "ac*dc*||*d--ba--c*||*dc*db", _     "|*****||****||****||*****|", _     "|*a---cd--b*||*a--cd---b*|", _     "|*d-------c*dc*d-------c*|", _     "|************************|", _     "d------------------------c" _ } 
end example

The maze array contains 31 strings, and each string consists of 26 characters. It is not a coincidence that the number of strings is the same as the number of rows in the maze. Each string represents a row in the maze. The first string therefore represents the cells in the first row.

In turn, each character in each string represents a cell in the maze row. It is not surprising that the number of characters in each string is the same as the number of columns in the maze row. There are several different characters in all the strings: spaces, a's, b's, c's, d's, dollar ($) signs, pipe (|) characters, asterisks (*), percent (%) signs, brackets (> and <), and hyphens (). The same character in maze represents the same cell. To understand this more fully, you will look at the code that draws the cell (the DrawCell method) shortly.

However, note that you do not use maze to draw each cell. Instead, you use the mazeData variable, which is also an array of strings that initially contains the same exact value as maze. I will explain later why you employ two arrays of strings. For now, remember that the value of mazeData is the same as the value of maze.

Here is the DrawCell method. Recall that this method accepts the cell column number, the cell row number, and the reference to the Graphics object on which the maze will be drawn.

   Sub DrawCell(ByVal x As Integer, ByVal y As Integer, ByRef g As Graphics)     Dim value As Char = mazeData(y).Chars(x)     Select Case value       Case " "c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)       Case "*"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.FillEllipse(foodBrush, x * square + 6, y * square + 6, 4, 4)       Case "J"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.FillEllipse(foodBrush, x * square + 6, y * square + 6, 4, 4)       Case "$"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.FillEllipse(superBrush, x * square + 2, y * square + 2, 12, 12)       Case "-"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(0), x * square, y * square, square, square)       Case "|"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(1), x * square, y * square, square, square)       Case "a"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(2), x * square, y * square, square, square)       Case "b"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(3), x * square, y * square, square, square)       Case "c"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(4), x * square, y * square, square, square)       Case "d"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(5), x * square, y * square, square, square)       Case "#"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)         g.DrawImage(images(6), x * square, y * square, square, square)       Case "<"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)       Case ">"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)       Case "%"c         g.FillRectangle(emptyBrush, x * square, y * square, square, square)     End Select   End Sub 

The first line of the DrawCell method obtains the character at the specified position passed to this method. For instance, for x = 6 and y = 3, the cell to be drawn is the cell in the fourth row of the maze at the seventh column. In other words, it is cell (3, 6). The character to be obtained is from the fourth string of mazeData and character number 7:

 Dim value As Char = mazeData(y).Chars(x) 

Having the right character, it will then go through a Select Case block to determine what to draw. As an example, an asterisk character represents a food. Therefore, if the character is an asterisk, the following two lines of code execute:

 g.FillRectangle(emptyBrush, x * square, y * square, square, square) g.FillEllipse(foodBrush, x * square + 6, y * square + 6, 4, 4) 

This draws a rectangle with a Brush object called emptyBrush (which is a Brush with a black color) starting at position (x * square, y * square), where square is the width and the height of the cell. The value of square is 16 (pixels).

The second line of the previous code draws a circle that represents a food using foodBrush.

Figure 3-5 shows the characters and the cell types they represent.

click to expand
Figure 3-5: The characters and the cell types they represent

Now, let's discuss why you need both maze and mazeData.

As you know, as Doggie moves along, it can eat the foods in the maze. When a food in a cell has been eaten, the cell becomes empty. The program must remember which cells Doggie has visited and removes the foods in those cells because Doggie has eaten them. The program remembers this because it makes a copy of the content of maze in mazeData. Therefore, maze contains the initial structure of the maze cells, but mazeData contains the "value" of each cell as the game proceeds. This means, every time Doggie eats a food in a cell, you modify the value of the character that represents the cell in mazeData.

When the DrawMaze method is called, it draws the maze at its initial stage. It also counts the number of foods in the maze and copies each individual string of maze to mazeData:

   Private Sub DrawMaze()     food = 0     Dim g As Graphics = Graphics.FromImage(image)     Dim i As Integer     For i = 0 To mazeRowCount - 1       ' copy the ith string from maze to mazeData       mazeData(i) = maze(i)       Dim j As Integer       For j = 0 To mazeColumnCount - 1         If mazeData(i).Chars(j) = "*" Or _           mazeData(i).Chars(j) = "$" Then           food += 1         End If         DrawCell(j, i, g)       Next     Next     g.Dispose()   End Sub 

Look at the part of the code that is printed in bold. Because * represents a food and $ represents a bone, both are counted as "food," and the integer food is incremented when one of them is encountered.

The Graphics object g in the DrawMaze method is created from a Bitmap called image using the shared FromImage method of the System.Drawing.Graphics class. You construct image at the declaration section of the Maze class:

 image = New Bitmap(Width, Height) 

where Width is the width of the maze and Height is the height of the maze.

At this point, you can conclude that you can draw a different maze by simply changing the value of maze at initialization. I review other methods of the Maze class at the later sections.

Understanding Game States

A video game is basically a movie that consists of frames. These frames are drawn continually many times every second to fool human eyes into thinking that the video is continuous. Normally, the rate of 30 frames per second is sufficient. Unlike a movie, however, the sequence of frames in a video game is created on the fly, based on the previous user input. Once a game starts, the application executes a While loop that keeps drawing these frames. What is being drawn at a particular instance of time depends on what happened prior to drawing. Specifically, the Doggie game uses members of the GameState enumeration:

 Public Enum GameState As Integer   Init   [New]   Begin   Run   Win   Lose   GameOver   [Stop] End Enum 
Note

The members New and Stop are in brackets because New and Stop are keywords in Visual Basic.

In fact, in most classes there is a variable called currentState that retains the current state of the game. The currentState variable values in all objects will be updated when the state of the game changes.

These states are as follows:

Init: This is the state when the application is first started. In this state a GameManager object is constructed. Instantiating a GameManager object will construct the Maze object and all the game actors (Doggie and the four cats) because the GameManager class's constructor contains code that does so. After the object instantiation, the state will change to New.

New: This is a new game. In this state, the application draws or redraws the maze, displays the string Please press ‘Enter' to play on the top part of the client area and waits for the user to press Enter. When the user presses the Enter key, the state will change to Begin.

Begin: The application draws a string Ready on the top part of the client area, initializes the game actors (Doggie and the four cats), waits for two seconds, and changes the state to Run.

Run: In this state the application checks for collision between Doggie and one of the cats. If collision occurs in a normal circumstance, Doggie dies. However, if the collision occurs at the "reverse" situation (in other words, when Doggie is being stronger after eating a bone), the cat dies. If Doggie dies, the state switches to Lose. If Doggie eats all the foods in the maze, the state goes to Win. If a user presses an arrow key during this state, Doggie changes direction.

Win: The application draws the string YOU WIN!!! on the top part of the client area, redraws the maze, increments the game level by one, and changes the state to Begin.

Lose: The application draws the string YOU LOSE!!! on the top part of the client area, decrements the lives by one if it is not yet zero, or changes the state to GameOver if there are no more lives.

GameOver: The application draws the string Game Over on the top part of the client area, resets the score and lives, waits two seconds, and changes the state to New.

Stop: This state occurs when the form closes. In this state, the game thread aborts.

Working with the Threads

There are seven threads used in this application: one in each of the game actors (Doggie and the four cats), one in the application's thread, and one used for drawing frames.

Starting the Application

When the application is first started, the Form1 class's constructor is called. The constructor consists of only one line: InitializeComponent(). This method is as follows:

 Public Sub InitializeComponent()   Dim mainMenu As New MainMenu()   Dim gameMenuItem As New MenuItem("&Game")   Dim gameNewGameMenuItem As New MenuItem("&New Game", _     New EventHandler(AddressOf gameNewGameMenuItem_Click))   Dim gameExitMenuItem As New MenuItem("E&xit", _     New EventHandler(AddressOf gameExitMenuItem_Click))   gameMenuItem.MenuItems.Add(gameNewGameMenuItem)   gameMenuItem.MenuItems.Add(gameExitMenuItem)   Dim aboutMenuItem As New MenuItem("&About", _     New EventHandler(AddressOf aboutMenuItem_Click))   mainMenu.MenuItems.Add(gameMenuItem)   mainMenu.MenuItems.Add(aboutMenuItem)   Me.Menu = mainMenu   Me.MaximizeBox = False   Me.FormBorderStyle = FormBorderStyle.Fixed3D   Me.Text = "Doggie"   GameManager.SetState(GameState.Init)   Me.ClientSize = New Size(GameManager.Width, GameManager.Height)   game = New Thread(New ThreadStart(AddressOf Run))   AddHandler Me.KeyDown, AddressOf DetectKeyDown   game.Start() End Sub 

The InitializeComponent method sets up the two menus and wires some event handlers to the menu items. It also sets the Icon and the FormBorderStyle properties. The important part of this method is in the last five lines:

 GameManager.SetState(GameState.Init) Me.ClientSize = New Size(GameManager.Width, GameManager.Height) game = New Thread(New ThreadStart(AddressOf Run)) AddHandler Me.KeyDown, AddressOf DetectKeyDown game.Start() 

It calls the static SetState of the GameManager class passing GameState.Init, sets the ClientSize property, instantiates the game Thread objects passing a ThreadStart delegate, wires the KeyDown event of the form with the DetectKeyDown method and start the game Thread.

Note that the ThreadStart delegate that is passed to the Thread class's constructor passes the address of Run. It causes the Run method to execute when the game thread's Start method is called. The Run method contains a While loop, which is the main loop of the game. "The Game's Main Loop" covers this method. However, before doing that, let's discuss the SetState method of the GameManager class, which is called in the InitializeComponent method passing the GameState.Init. This will construct a GameManager object:

 GameManager.SetState(GameState.Init) 

Calling the SetState Method and Constructing the GameManager Object

The GameManager object is constructed when the InitializeComponent method in the Form1 class calls the static SetState method of the GameManager class. The following code is the SetState method of the GameManager class. Remember that in the Form1 class's InitializeComponent method, the SetState method is passed GameState.Init:

   Public Shared Sub SetState(ByVal state As GameState)     If state = GameState.Init Then       gameMgr = New GameManager()     End If     gameMgr.currentState = state     gameMgr.theMaze.SetState(state)     gameMgr.doc.SetState(state)     Dim i As Integer     For i = 0 To Constants.catCount - 1       gameMgr.cats(i).SetState(state)     Next     Select Case state       Case GameState.Init         SetState(GameState.[New])       Case GameState.[New]         gameMgr.level = 1         gameMgr.lives -= 1       Case GameState.Begin         gameMgr.timer.Interval = 2000         gameMgr.timer.Start()       Case GameState.Win         gameMgr.timer.Interval = 2000         gameMgr.timer.Start()       Case GameState.Lose         gameMgr.timer.Interval = 2000         gameMgr.timer.Start()       Case GameState.GameOver         gameMgr.timer.Interval = 2000         gameMgr.timer.Start()     End Select   End Sub 

The SetState method instantiates the GameManager object if the argument passed is GameState.Init. After that it sets the states in the Maze object, Doggie object, and the four cat objects. Then, it enters a Select block, which does the thing explained in the earlier "Understanding Game States" section.

Let's now concentrate on the GameManager class's constructor that gets called when GameState.Init is passed to the SetState method.

The GameManager class's constructor is as follows:

   Public Sub New()     dog = New Doggie()     cats(0) = New RedCat(dog)     cats(1) = New BlueCat(dog)     cats(2) = New BlackCat(dog)     cats(3) = New GreenCat(dog)     theMaze = New Maze(dog, cats)     Const topImageHeight As Integer = 25     Const bottomImageHeight As Integer = 30     topImage = New Bitmap(theMaze.Width, topImageHeight)     boardImage = New Bitmap(theMaze.Width, theMaze.Height)     bottomImage = New Bitmap(theMaze.Width, bottomImageHeight)     Height = theMaze.Height + topImageHeight + bottomImageHeight     Width = theMaze.Width     timer = New System.Timers.Timer()     AddHandler timer.Elapsed, AddressOf OnTimedEvent     timer.AutoReset = False     f14 = New Font("Arial", 14)     f16 = New Font("Arial", 16)     f18 = New Font("Arial", 18)     green = New SolidBrush(Color.Green)     white = New SolidBrush(Color.White)     red = New SolidBrush(Color.Red)     dogImage = New Bitmap("images/doggie/right2.gif")   End Sub 

The GameManager class's constructor instantiates the following objects:

  • The Dog object.

  • The four Cat objects. The constructor of the Cat class accepts a Dog object.

  • The Maze object by passing the Dog object and an array of Cat objects.

  • Three images (for the top part, the maze, and the bottom part).

  • The Timer object. It also wires its Elapsed event with the OnTimedEvent event handler.

  • Three Font objects (f14, f16, and f18).

  • Four SolidBrush objects (blue, green, white, and red).

The Game's Main Loop

The main loop is in the Run method of the Form1 class. The Run method is as follows:

   Public Sub Run()     Thread.Sleep(100)     While True       ' the use of t1 and t2 below is to make the frame rate       ' steady       Dim t1 As Integer = Environment.TickCount       Dim g As Graphics = Me.CreateGraphics()       GameManager.Draw(g)       Dim t2 As Integer = Environment.TickCount       g.Dispose()       Thread.Sleep(Math.Max(0, 30 - (t2 - t1)))     End While   End Sub 
Note

The game's main loop in the Run method runs in a separate thread. Meanwhile, the main thread of the application still works to detect user input.

The loop starts after a delay of 100 milliseconds. It starts by obtaining the reference to the form's Graphics object:

 Dim g As Graphics = Me.CreateGraphics() 

It then calls the GraphicManager class's Draw method. This Draw method draws a frame. Note that the Draw method accepts the Graphics object on which the frame is to be drawn.

The TickCount property of the System.Environment class returns the number of milliseconds that elapsed since the system started. You are interested in the value of the TickCount property before and after the Draw method is called. The difference between them is the number of milliseconds taken to execute the Draw method. You want every frame to be changed every 30 milliseconds; therefore, you use the following line of code:

 Thread.Sleep(Math.Max(0, 30 - (t2 - t1))) 

This will make the thread sleep for (30 – time taken to draw) milliseconds. If the time taken to draw is more than 30 milliseconds, the value of (30 – time taken to draw) will be negative. In this case, the Max method of the Math class will return 0 and the thread will not sleep at all.

The Draw method of the GameManager class draws the current frame. Let's look at how it does it in the following section, "Drawing the Current Frame."

Drawing the Current Frame

The application's client area consists of three parts: the top part, the maze, and the bottom part. The GameManager class's Draw method draws each frame by drawing these three parts in turn. For each part the method employs a temporary Graphics object gTemp. The Draw method is given as follows:

 Public Shared Sub Draw(ByRef g As Graphics)   ' --- Drawing the top part ---   Dim gTemp As Graphics = Graphics.FromImage(gameMgr.topImage)   gTemp.Clear(Color.Black)   gTemp.DrawString("Score: " & gameMgr.theMaze.score * 10, _     gameMgr.f14, gameMgr.white, 10, 0)   Select Case gameMgr.currentState     Case GameState.[New]       gTemp.DrawString("Please press 'Enter' to play.", _         gameMgr.f14, gameMgr.white, 150, 0)     Case GameState.Begin       gTemp.DrawString("Ready!", gameMgr.f16, gameMgr.white, 150, 0)     Case GameState.Run       gameMgr.theMaze.CheckCollission()       If gameMgr.dog.dead Then         SetState(GameState.Lose)       ElseIf gameMgr.theMaze.food = 0 Then         SetState(GameState.Win)       End If     Case GameState.Win       gTemp.DrawString("YOU WIN!!!", gameMgr.f16, gameMgr.green, 150, 0)     Case GameState.Lose       gTemp.DrawString("YOU LOSE!!!", gameMgr.f16, gameMgr.red, 150, 0)     Case GameState.GameOver       gTemp.DrawString("GAME OVER", gameMgr.f18, gameMgr.red, 150, 0)   End Select   gTemp.Dispose()   ' --- Drawing the maze ---   gTemp = Graphics.FromImage(gameMgr.boardImage)   gameMgr.theMaze.Draw(gTemp)   gTemp.Dispose()   ' --- Drawing the bottom part ---   gTemp = Graphics.FromImage(gameMgr.bottomImage)   gTemp.Clear(Color.Black)   gTemp.DrawString("Level - " & gameMgr.level, gameMgr.f14, _     gameMgr.white, 10, 0)   Dim i As Integer   For i = 0 To gameMgr.lives - 1     gTemp.DrawImage(gameMgr.pacImage, (Width - 10) - (i + 1) * 24, 0, 24, 24)   Next   gTemp.Dispose()   ' putting all three parts together   g.DrawImage(gameMgr.topImage, 0, 0)   g.DrawImage(gameMgr.boardImage, 0, 25)   g.DrawImage(gameMgr.bottomImage, 0, gameMgr.theMaze.Height + 25) End Sub 

Note that the maze is drawn by calling the Draw method of the Maze class. At the end of the method, the frame is completed by putting all three parts together.

Of the three, the maze is the most complicated because it includes drawing the game actors.

The Draw method of the Maze class draws the maze. It is passed a Graphics object from the GameManager object. This Graphics object is a reference to the Graphics object of the Form1 object. The Draw method is as follows:

 Public Sub Draw(ByRef g As Graphics)   If rebuild Then     reverse = False     DrawMaze()     rebuild = False     g.Clear(Color.Black)     g.DrawImage(image, 0, 0)   End If   RemoveActor(CType(pac, GameActor), g)   Dim i As Integer   For i = 0 To Constants.catCount - 1     RemoveActor(CType(cats(i), GameActor), g)   Next   DrawActor(CType(pac, GameActor), g)   For i = 0 To Constants.catCount - 1     DrawActor(CType(cats(i), GameActor), g)   Next End Sub 

The Draw method first checks if rebuilding is necessary by testing the value of the rebuild Boolean. The rebuild Boolean is set to True when the game state changes to GameState.New or GameState.Begin. When rebuild is True, the method sets the reverse Boolean to False, calls the DrawMaze method to draw the maze, resets rebuild, clears the Graphics object, and draws the image image onto the Graphics object.

Note

The "Drawing the Maze" section explains the DrawMaze method.

The Boolean reverse indicates whether the situation is reversed. When True, it indicates that Doggie has just eaten a bone and is being stronger than the cats. The DrawMaze method draws the maze on the image image, which is initialized as follows at the class's constructor:

 image = New Bitmap(Width, Height) 

Note that most of the time the DrawMaze is not called. If the maze has to be redrawn for each frame, the game thread will work hard and the whole application will become very slow if the user's computer is not powerful enough. The cells that are occupied by the game actors are updated because the game actors (at least the cats) move all the time.

You do this in the second part of the Draw method:

 RemoveActor(CType(pac, GameActor), g) Dim i As Integer For i = 0 To Constants.catCount - 1   RemoveActor(CType(cats(i), GameActor), g) Next DrawActor(CType(pac, GameActor), g) For i = 0 To Constants.catCount - 1   DrawActor(CType(cats(i), GameActor), g) Next 

The RemoveActor method removes the image of the actor from the previous position in the maze, and the DrawActor method draws the actor in the current position. The RemoveActor and the DrawActor are the topics of the next discussion.

The following is the RemoveActor method:

   Private Sub RemoveActor(ByRef a As GameActor, ByRef g As Graphics)     Dim x As Integer = a.oldXScreen \ square     Dim y As Integer = a.oldYScreen \ square     If x = 0 Or y = 0 Then       Return     End If     Dim actualX As Integer = a.oldXScreen - 4     Dim actualY As Integer = a.oldYScreen - 4     Dim i As Integer = actualX \ square     Dim j As Integer = actualY \ square     Dim upperBound As Integer = (actualX + 24) \ square     While i <= upperBound       For j = actualY \ square To (actualY + 24) \ square         DrawCell(i, j, g)       Next       i += 1     End While   End Sub 

Note

In VB, the \ operator is a division operator that rounds down the result. Therefore, 19 \ 5 = 3.

Each maze cell is a square of 16 16 pixels, and an image actor has a dimension of 24 24 pixels. If an actor is exactly on top of a single cell (see Figure 3-6), there are nine cells that need to be redrawn when the actor is removed. On the other hand, if the actor occupies two adjacent cells (like in Figure 3-7), six cells need to be redrawn.


Figure 3-6: The actor is exactly on top of a single cell.


Figure 3-7: The actor occupies two adjacent cells.

And this is the DrawActor method:

 Private Sub DrawActor(ByRef a As GameActor, ByRef g As Graphics)   SyncLock (a)     If Not a.frameReady Then       Try         Monitor.Wait(a)       Catch e As SynchronizationLockException       Catch e As ThreadInterruptedException       End Try     End If     a.Draw(g)     a.frameReady = False     Monitor.Pulse(a)   End SyncLock End Sub 

Driving Doggie

First of all, Doggie and all cats always move along a straight line. Even when the player is not doing anything, Doggie will move on until it hits the wall. In this case, the player can then "turn" Doggie to another direction. So, the player does not really move Doggie, but only tells Doggie the next "turn direction." The turn direction can be left, right, up, or down. The player notifies Doggie of the next turn direction using one of the arrow keys. If the player presses one of the arrow keys when Doggie is still moving, the turn direction will be remembered and takes effect when Doggie can do the turn—for example, if Doggie is right on top of a junction. The information about the turn direction is kept in the turnDirection Boolean variable in the Doggie class.

To capture a key press, in the Form1 class you write the following code in the InitializeComponent method to wire the form's KeyDown event with the DetectKeyDown event handler:

 AddHandler Me.KeyDown, AddressOf DetectKeyDown 

And the following is the DetectKeyDown method in the Form1 class. Note that game is a Thread object that takes care of the main loop that draws frames:

   Protected Sub DetectKeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)     If e.KeyCode = Keys.Space Then       If game.ThreadState = ThreadState.Suspended Then         game.Resume()       ElseIf game.ThreadState = ThreadState.Running Then         game.Suspend()       End If     Else       GameManager.KeyDown(sender, e)     End If   End Sub 

The method first checks if the key pressed is the spacebar. The spacebar acts as a toggle bar that pauses and resumes the game:

 If e.KeyCode = Keys.Space Then 

If the key is the spacebar, it checks the state of the game Thread. If game is suspended, game is resumed by calling the Resume method of the Thread class. If game is running, game is suspended by calling the Suspend method:

 If game.ThreadState = ThreadState.Suspended Then   game.Resume() ElseIf game.ThreadState = ThreadState.Running   game.Suspend() End If 

If the key pressed is not the spacebar, the key press simply passes to the KeyDown method of the GameManager class, passing the sender and the KeyEventArgs argument:

 Else   GameManager.KeyDown(sender, e) 

The following is the KeyDown method of the GameManager class:

   Public Shared Sub KeyDown(ByVal o As Object, ByVal e As KeyEventArgs)     If Not gameMgr Is Nothing Then       If gameMgr.currentState = GameState.Run Then         gameMgr.pac.KeyDown(o, e)       ElseIf gameMgr.currentState = GameState.[New] And _         e.KeyCode = Keys.Enter Then         SetState(GameState.Begin)       End If     End If   End Sub 

The code in the KeyDown method only executes if the static gameMgr has been instantiated. In this case, the code checks the value of the currentState Boolean. If its value is GameState.Run (the game is running), it calls the KeyDown method of the Doggie class:

 If gameMgr.currentState = GameState.Run Then   gameMgr.dog.KeyDown(o, e) 

In the previous code, dog is an object variable of type Dog. You will have a look at the Dog class's KeyDown method in a moment.

If the currentState of the gameMgr is GameState.New and the key pressed is the Enter key, then the state of the game changes to GameState.Begin:

 ElseIf gameMgr.currentState = GameState.[New] And _   e.KeyCode = Keys.Enter Then   SetState(GameState.Begin) 

Now shift your attention to the KeyDown method of the Dog class, which actually moves Doggie. This is the method:

 Public Sub KeyDown(ByVal o As Object, ByVal e As KeyEventArgs)   Select Case e.KeyCode     Case Keys.Up       turnDirection = 0     Case Keys.Down       turnDirection = 2     Case Keys.Left       turnDirection = 1     Case Keys.Right       turnDirection = 3   End Select End Sub 

As you can see, the KeyDown method in the Dog class is only used to change the value of turnDirection, a Boolean variable in the Dog class. The valid value of turnDirection can be 0, 1, 2, or 3. A value of 1 means that Doggie is not turning anywhere.

Doggie itself will move when the next frame is drawn.

Now, let's see how the game thread in the Form1 class actually moves Doggie (and the cats). Recall that the Run method in the Form1 class calls the Draw method of the GameManager class:

 GameManager.Draw(g) 

Somewhere in the Draw method in the GameManager class, it calls the Draw method of the Maze class:

 ' --- Drawing the maze --- gTemp = Graphics.FromImage(gameMgr.boardImage) gameMgr.theMaze.Draw(gTemp) gTemp.Dispose() 

The following are the last lines of the Draw method of the Maze class:

 RemoveActor(CType(pac, GameActor), g) Dim i As Integer For i = 0 To Constants.catCount - 1   RemoveActor(CType(cats(i), GameActor), g) Next DrawActor(CType(dog, GameActor), g) For i = 0 To Constants.catCount - 1   DrawActor(CType(cats(i), GameActor), g) Next 

The last part in bold in the code is the call to the DrawActor method that passes the Dog instance. Let's look at the DrawActor method:

 Private Sub DrawActor(ByRef a As GameActor, ByRef g As Graphics)   SyncLock (a)     If Not a.frameReady Then       Try         Monitor.Wait(a)       Catch e As SynchronizationLockException       Catch e As ThreadInterruptedException       End Try     End If     a.Draw(g)     a.frameReady = False     Monitor.Pulse(a)   End SyncLock End Sub 

The DrawActor method locks the actor and then calls the Draw method of the GameActor class. The method is an abstract method:

 Public MustOverride Sub Draw(ByVal g As Graphics) 

So, thanks to polymorphism, it will call the overriding Draw method in the child class—in this case, the Dog class. This is the Draw method of the Dog class:

 Public Overrides Sub Draw(ByVal g As Graphics)   g.DrawImage(image, xScreen - 4, yScreen - 4) End Sub 

Updating Positions

The Dog class and all the cats derive from the GameActor class. The GameActor class has the following four integer variables:

 Public xScreen, yScreen As Integer Public oldXScreen, oldYScreen As Integer 

xScreen and yScreen indicate the coordinate of the Doggie instance in the maze. Remember that the maze consists of 31 rows and 26 columns of cells and each cell is a square of 16 16 pixels. Therefore, the maze is 26 16 = 416 pixels wide and 31 16 = 496 pixels high. The top-left corner of the cell in the first column and the first row is on coordinate (0, 0) of the maze. Its bottom-right corner is on coordinate (15, 15).

To move completely from one cell to the next cell on the right, a game actor's xScreen must be incremented by 16. To move from one cell to the next cell on the left, a game actor's xScreen must be decremented by 16. By the same token, to move one cell to the next cell below the current cell, the game actor's yScreen must be incremented by 16.

Between two frames a game actor does not move 16 pixels at a time because such a move would create a very rough video. Instead, it moves 2 pixels. Therefore, a game actor will need 8 frames to move to the next adjacent cell. There is an exception to this rule, however. In the reversed situation, Doggie moves 4 pixels at a time so that it will be able to catch the cats.

The step integer variable in the GameActor class indicates the number of pixels the game actor moves.

Note that the possible values of step are 2 and 4. In theory, it could be anything that is divisible by 16 (the maze cell width)—in other words, it could be 1, 2, 4, 8, or 16. The values are not arbitrary. Remember that a game actor needs to turn when it hits the wall. Before it turns, its screen positions will be evaluated. It is only allowed to turn if its position matches the edge of the cell. For example, suppose that the Doggie is moving to the right. After four frames, its screen position will be in perfect line with the next cell's side and if there is no wall on top of it, it will be able to turn upward, should the user instruct Doggie to do so.

Note

In the discussion of a game actor's turning, you differentiate those turns into two turns: an L turn and a 180-degree turn. An L turn is turning 90 degrees—in other words, from right to up or right to down. A 180-degree turn is from right to left, left to right, up to down, and down to up. A game actor can do a 180-degree turn anytime, regardless of its screen position.

On the other hand, if the step has a value of 3, for example, a game actor will never be able to turn until 3 16 = 48 frames!

Each game actor is moved by a separate thread in the GameActor class:

 ' the thread that moves the actor Private thread As thread 

You instantiate this Thread object in the class's constructor by passing a ThreadStart delegate that receives the address of the Run method:

 Public Sub New()   thread = New Thread(New ThreadStart(AddressOf Run)) End Sub 

The Run method of the GameActor class is as follows:

   Private Sub Run()     While True       SyncLock Me         If frameReady Then           Try             Monitor.Wait(Me)           Catch e As SynchronizationLockException           Catch e As ThreadInterruptedException           End Try         End If         Update()         frameReady = True         Monitor.Pulse(Me)       End SyncLock     End While   End Sub 

Pay special attention to the Update abstract method. Because it is abstract, when the method is called, the overriding Update method in the child class is executed. In the Dog class, the Update method is as follows:

 Public Overrides Sub Update()   If currentState = GameState.Lose Then     image = images(2, anim)     anim += 1     anim = anim Mod 4     Return   End If   If currentState <> GameState.Run Then     Return   End If   If turnDirection = Direction.Invalid Then     Return   End If   Dim ok As Boolean = GameManager.MoveRequest(Me, _     walkDirection, turnDirection)   If ok Then     walkDirection = turnDirection   Else     GameManager.MoveRequest(Me, walkDirection, walkDirection)   End If   'choose the image.   image = images(movement, walkDirection)   movement += 1   movement = movement Mod 3 End Sub 

The method basically checks the game's state and assigns to image an appropriate image. It first checks if the game's state is GameState.Lose:

 If currentState = GameState.Lose Then       image = images(2, anim)       anim += 1       anim = anim Mod 4       Return     End If 

If it is, it will display one of the following images in Figure 3-8.

click to expand
Figure 3-8: Animation effect caused by displaying four images in turn

If the game's state is not GameState.Run, or turnDirection is Direction.Invalid, then it returns.

Next, the game actor has the walk direction. However, you need to check if the next move is permitted. You do this by calling the MoveRequest method. This method returns True if the next move is permitted. Otherwise, it returns False.

The following is the GameManager class's MoveRequest, which simply calls the MoveRequest method in the Maze class:

 Public Shared Function MoveRequest(ByRef actor As GameActor, _     ByVal old_dir As Integer, ByVal dir As Integer) As Boolean     Return gameMgr.theMaze.MoveRequest(actor, old_dir, dir) End Function 

The following is the MoveRequest method in the Maze class. This method returns True if the game actor is permitted to move and False otherwise. The method accepts the game actor, the old direction, and the intended direction.

The following is the MoveRequest method of the Maze class:

 Public Function MoveRequest(ByVal actor As GameActor, _   ByVal oldDir As Integer, ByVal dir As Integer) As Boolean   Dim xMove As Integer = 0   Dim yMove As Integer = 0   Dim x As Integer = actor.xScreen   Dim y As Integer = actor.yScreen   Dim xFood As Integer = x   Dim yFood As Integer = y   If (x Mod square <> 0 Or y Mod square <> 0) And _     Math.Abs(oldDir - dir) Mod 2 <> 0 Then     Return False   End If   Select Case dir     Case Direction.Up       y -= actor.step       yMove = -actor.step       yFood += yMove     Case Direction.Left       x -= actor.step       xMove = -actor.step       xFood += xMove     Case Direction.Down       y += square + actor.step - 1       yMove = actor.step       yFood += yMove     Case Direction.Right       x += square + actor.step - 1       xMove = actor.step       xFood += xMove   End Select   Dim xOff As Integer = x \ square   Dim yOff As Integer = y \ square   Dim val As Char = mazeData(yOff).Chars(xOff)   If val = "a"c Or val = "b"c Or val = "c"c Or val = "d"c Or _     val = "-"c Or val = "|"c Then     Return False   End If   If val = "#"c And dir = 2 And (Not actor.dead) Then     Return False   End If   If val = "#"c And dir = 0 And doorClosed Then     Return False   End If   If val = "<"c Then     actor.SetPos(24, 14)   Else     If val = ">"c Then       actor.SetPos(1, 14)     Else       actor.Move(xMove, yMove)     End If   End If   If xFood Mod square <> 0 Or yFood Mod square <> 0 Then     Return True   End If   xOff = xFood \ square   yOff = yFood \ square   If actor.GetType().ToString().EndsWith("Doggie") And _     (mazeData(yOff).Chars(xOff) = "*"c Or _     mazeData(yOff).Chars(xOff) = "$") Then     food -= 1     score += 1     If val = "$"c Then       reverse = True       Cat.scared = True       actor.step = 4       doorClosed = True       timer.Interval = 10000       timer.Start()     End If     mazeData(yOff) = mazeData(yOff).Remove(xOff, 1)     mazeData(yOff) = mazeData(yOff).Insert(xOff, " ")   End If   Return True End Function 

The MoveRequest method starts by the following declaration:

 Dim xMove As Integer = 0 Dim yMove As Integer = 0 Dim x As Integer = actor.xScreen Dim y As Integer = actor.yScreen Dim xFood As Integer = x Dim yFood As Integer = y 

For now, you are interested in x and y, which represent the screen coordinate of the actor. Recall that an actor can only make an L turn if its screen position is on a cell's corner. Therefore, you check the screen position with the following:

 If (x Mod square <> 0 Or y Mod square <> 0) And _   Math.Abs(oldDir - dir) Mod 2 <> 0 Then   Return False End If 

If both x or y are not evenly divisible by square (16 pixels), then the screen position does not permit the actor to make an L turn. You then check if the actor is trying to make an L turn or a 180-degree turn or is just trying to move straight.

If you look at the Direction enumeration, you will notice that the difference in value of no turn and a 180-degree turn is either 0 or 2. Therefore, if the remainder of (oldDir dir) / 2 is not 0, then the actor is trying to make an L turn.

If the game actor is making an L turn but the position is not good for the game actor to do so, the MoveRequest method returns False straight away.

Otherwise, it flows to the next lines:

 Select Case dir   Case Direction.Up     y -= actor.step     yMove = -actor.step     yFood += yMove   Case Direction.Left     x -= actor.step     xMove = -actor.step     xFood += xMove   Case Direction.Down     y += square + actor.step - 1     yMove = actor.step     yFood += yMove   Case Direction.Right     x += square + actor.step - 1     xMove = actor.step     xFood += xMove End Select 

The code in the Select blocks updates the x or y according to the value of step. However, you now need to check if the destination of the next move is a wall:

     Dim xOff As Integer = x \ square     Dim yOff As Integer = y \ square     Dim val As Char = mazeData(yOff).Chars(xOff)     If val = "a"c Or val = "b"c Or val = "c"c Or val = "d"c Or _       val = "-"c Or val = "|"c Then       Return False     End If     If val = "#"c And dir = 2 And (Not actor.dead) Then       Return False     End If     If val = "#"c And dir = 0 And doorClosed Then       Return False     End If     If val = "<"c Then       actor.SetPos(24, 14)     Else       If val = ">"c Then         actor.SetPos(1, 14)       Else         actor.Move(xMove, yMove)       End If     End If     If xFood Mod square <> 0 Or yFood Mod square <> 0 Then       Return True     End If     xOff = xFood \ square     yOff = yFood \ square       If actor.GetType().ToString().EndsWith("Doggie") And _         (mazeData(yOff).Chars(xOff) = "*"c Or _         mazeData(yOff).Chars(xOff) = "$") Then         food -= 1         score += 1         If val = "$"c Then           reverse = True           actor.step = 4           doorClosed = True           timer.Interval = 10000           timer.Start()         End If         mazeData(yOff) = mazeData(yOff).Remove(xOff, 1)         mazeData(yOff) = mazeData(yOff).Insert(xOff, " ")       End If       Return True     End Function 

Moving the Cats

The movement of the cats is the most difficult part of the program logic. Here, some artificial intelligence determines the next position of each cat.

First of all, recall that the cats are there to catch Doggie and kill it. Therefore, the cat needs to know Doggie's position in the maze. This is not hard because the Cat class's constructor receives the instance of the Dog class. In other words, each of the four Cat objects has a reference to the instance of the Dog class. The Dog class is a child class of the GameActor class, and the GameActor class has the xScreen and yScreen variables that denote the position of the game actor in the maze. Because both xScreen and yScreen are public, once you get a reference of a game actor, you can access its xScreen and yScreen values. Therefore, because the cats have a reference to the Dog object, they can easily know where Doggie is in the maze.

However, if all the four cats start to chase Doggie because they leave the cat house, it would be hard for the player to win, and the game would be frustratingly difficult. To make the game more exciting, each cat has its own time to attack and time where it just wanders around randomly. In the Cat class, there is a Boolean variable named attack that indicates whether the cat is in the attack state. If the value of attack in a cat is True, the cat will try to approach Doggie. If it is False, the cat moves randomly. You toggle the value of attack by a server-based timer named timer. The timer variable in the Cat class is protected so it can be accessed by the child classes.

In the Cat class's constructor, the server-based timer's Elapsed event is wired to the method SwapAttack and its AutoReset property is set to True, causing the Elapsed event to be raised repeatedly:

 timer = New System.Timers.Timer() AddHandler timer.Elapsed, AddressOf SwapAttack timer.AutoReset = True 

The frequency with which the Elapsed event triggers is determined by the value of the Interval property of the System.Timers.Timer class and is set in the child classes (BlueCat, GreenCat, RedCat, and BlackCat). The value of the Interval property of this timer is different in each instance of the Cat subclass to make each cat behave differently.

The Interval property is assigned value in the Init method of each cat child class. For example, this is the Init method of the BlueCat class:

 Public Overrides Sub Init()   SetPos(11, 15)   walkDirection = Direction.Left   image = normalImages(walkdirection, state)   Timer.Interval = 25000   Timer.Start()   attack = False End Sub 

And this is the Init method of the GreenCat class:

 Public Overrides Sub Init()   SetPos(14, 15)   walkdirection = Direction.Right   image = normalimages(walkdirection, state)   Timer.Interval = 8000   Timer.Start()   attack = False End Sub 

The Init method of the RedCat class is as follows:

 Public Overrides Sub Init()   SetPos(12, 13)   walkdirection = Direction.Up   image = normalimages(walkdirection, state)   Timer.Interval = 20000   Timer.Start()   attack = True End Sub 

And the following is the Init method of the BlackCat class:

 Public Overrides Sub Init()   SetPos(14, 13)   walkdirection = Direction.Down   image = normalimages(walkdirection, state)   Timer.Interval = 2000   Timer.Start()   attack = True End Sub 

You call the Init method from the InitActor method of the Maze class. From the Init method of each subclass of the Cat class, you will notice the following:

  • The SetPos method is called and given different arguments in each Init method of the Cat subclass. The SetPos method sets the initial position of the cat in the cat house. It inherits from the GameActor class and is defined as follows:

     Public Sub SetPos(ByVal x As Integer, ByVal y As Integer)    xScreen = x * Maze.square    yScreen = y * Maze.square End Sub 
  • The walkDirection variable in each cat is assigned a different value so that each cat will have a different walk direction from each other.

  • The Interval property of the Timer class in each cat is assigned a different value.

  • The attack Boolean in the BlueCat and the GreenCat classes is set to False. This Boolean is set to True in the RedCat and BlackCat classes. Therefore, the red and black cats are programmed to attack when the game starts, before the SwapAttack method toggles it. The blue cat and the green cat are not in the attack state initially, until the SwapAttack method toggles it.

The event handler SwapAttack is simple, its function being to toggle attack. It only consists of the following code:

 Public Sub SwapAttack(ByVal o As Object, ByVal e As ElapsedEventArgs)   attack = Not attack End Sub 

There is also another Boolean in the Cat class that determines the next position of each individual cat instance: scared. This Boolean is a shared variable; therefore, every cat instance will have the same scared value. This Boolean is public so it can be set from outside the class. It is set to True when Doggie has just eaten a bone and the situation is reversed (so, the cats are scared). It is set back to False when the "reverse" situation has expired.

You set the scared Boolean to True in the MoveRequest method of the Maze class, in the following If block:

 If actor.GetType().ToString().EndsWith("Doggie") And _   (mazeData(yOff).Chars(xOff) = "*"c Or _   mazeData(yOff).Chars(xOff) = "$") Then       food -= 1   score += 1   If val = "$"c Then     reverse = True     Cat.scared = True     actor.step = 4     doorClosed = True     timer.Interval = 10000     timer.Start()   End If 

You set the scared Boolean to False in the ReverseExpired method of the Maze class:

 Public Sub ReverseExpired(ByVal o As Object, ByVal e As ElapsedEventArgs)   doorClosed = False   reverse = False   pac.step = 2   Cat.scared = False End Sub 

Note that in both methods (MoveRequest and ReverseExpired), the scared Boolean needs only to be set once because it is a shared field.

The last Boolean that affects how a cat moves is dead. A cat dies if it collides with Doggie when the situation is reversed. When it is dead, the cat changes color and moves back to the cat house.

Now let's see how the positions of the four cats are updated.

Updating the Cat's Position

The Update method in the Cat class determines the next position of a cat and the next image that will be used to represent the cat in the next frame. If you understand how the Update method in the Dog class works, you should not find it hard to understand how the similar method in the Cat class works.

First of all, this is the Update method in the Cat class:

 Public Overrides Sub Update()   If currentState <> GameState.Run Then     Return   End If   If delay = 1 Then     state += 1     state = state Mod 2   End If   delay += 1   delay = delay Mod 3   If dead Then     image = deadCat(walkDirection)     MoveDeadCat()   Else     If scared Then       image = scaredImages(state)     Else       image = normalImages(normalImageSequence)       normalImageSequence = (normalImageSequence + 1) Mod 4     End If     MoveCat()   End If End Sub 

First, it only makes sense to worry about a cat position if the game's state is GameState.Running. If this is not the case, return from the method:

 If currentState <> GameState.Run Then   Return End If 

The next few lines deal with the animation of the cat image. Each state and delay determines the selection of image files:

 If delay = 1 Then    state += 1    state = state Mod 2 End If delay += 1 delay = delay Mod 3 

Next, this is where the cat's position is actually determined:

 If dead Then   image = deadCat(walkDirection)   MoveDeadCat() Else   If scared Then     image = scaredImages(state)   Else     image = normalImages(normalImageSequence)     normalImageSequence = (normalImageSequence + 1) Mod 4   End If   MoveCat() 

If the cat is dead, the image is assigned an image of a dead cat. In this application, I only use one image to represent a dead cat. However, the program itself animates a dead cat by using four different images of dead cats.

Figure 3-9 shows a dead cat.


Figure 3-9: A dead cat

It then calls the MoveEye method.

If the cat is not dead, the image of the cat will depend on whether the shared scared Boolean is True or False. If the value of scared is True, the image will be one of the two images in Figure 3-10.


Figure 3-10: Two different images of scared looks

If the scared Boolean value is False, the image will be one of the four images in Figure 3-11.


Figure 3-11: Four different images of normal looks

Either way, the Update method calls the MoveCat method.

Now, let's look at the MoveCat method and the MoveDeadCat method. However, before doing that, you will look at the two methods called from both the MoveCat and the MoveDeadCat methods: the RandomWalk and WalkToTarget methods.

The RandomWalk Method

The RandomWalk method creates an array of integers dirs. dirs contains four elements, and each of the elements is populated with a random number between 0 and 3 inclusive. Each of the elements will have a different value. The RandomWalk method then calls the Walk method passing dirs.

The RandomWalk method is as follows:

 Public Sub RandomWalk()   Dim rand As Integer = random.Next(4)   Dim dirs(4) As Integer   dirs(0) = rand   rand = random.Next(1)   If rand = 0 Then     dirs(1) = (dirs(0) + 1) Mod 4     dirs(2) = (dirs(1) + 1) Mod 4     dirs(3) = (dirs(2) + 1) Mod 4   Else     dirs(1) = ((dirs(0) - 1) + 4) Mod 4     dirs(2) = ((dirs(1) - 1) + 4) Mod 4     dirs(3) = ((dirs(2) - 1) + 4) Mod 4   End If   Walk(dirs) End Sub 

And here is the Walk method:

 Public Sub Walk(ByRef dirs() As Integer)   'this sub sets the walkDirection value   Dim ok As Boolean   If dirs(0) <> ((walkDirection - 2) + 4) Mod 4 Then     ok = GameManager.MoveRequest(Me, walkDirection, dirs(0))     If ok Then       walkDirection = dirs(0)       Return     End If   End If   If dirs(1) <> ((walkDirection - 2) + 4) Mod 4 Then     ok = GameManager.MoveRequest(Me, walkDirection, dirs(1))     If ok Then       walkDirection = dirs(1)       Return     End If   End If   If dirs(2) <> ((walkDirection - 2) + 4) Mod 4 Then     ok = GameManager.MoveRequest(Me, walkDirection, dirs(2))     If ok Then       walkDirection = dirs(2)       Return     End If   End If   If dirs(3) <> ((walkDirection - 2) + 4) Mod 4 Then     ok = GameManager.MoveRequest(Me, walkDirection, dirs(3))     If ok Then       walkDirection = dirs(3)       Return     End If   End If End Sub 

The aim of calling the Walk method is to change the value of walkDirection. The walkDirection variable can have one of the following members of the Direction enumeration: Up, Down, Left, and Right.

In changing the value of walkDirection, the Walk method avoids making the game actor make a 180-degree turn. Remember that the value of Direction.Up, Direction.Down, Direction.Left, and Direction.Right are 0, 2, 1, and 3 respectively? A 180-degree turn happens if the value of walkDirection changes from Direction.Down to Direction.Up, from Direction.Up to Direction.Down, from Direction.Left to Direction.Right, or from Direction.Right to Direction.Left.

Converting the members of the Direction enumeration to integers, you can say that a 180-degree turn happens if walkDirection 2 is either 2 or 2. For example, if the current walkDirection value is Direction.Up (integer 0) and the next value is Direction.Down (integer 2), then Direction.Up Direction.Down = 0 2 = 2. However, if the current walkDirection value is Direction.Up and the next value is Direction.Left, it is not a 180-degree turn because Direction.Up Direction.Left = 0 1 = 1.

Therefore, the following line of the Walk method

 If dirs(0) <> ((walkDirection - 2) + 4) Mod 4 Then 

translates into the following: "If changing the current value of walkDirection with the content of the first element in dirs does not make the game actor have a 180-degree turn, then".

If the If statement is satisfied, it calls the MoveRequest method of the GameManager class to find out if such a turn is permitted. If it is, then change the value of walkDirection with dirs(0) and return:

 ok = GameManager.MoveRequest(Me, walkDirection, dirs(0)) If ok Then   walkDirection = dirs(0)   Return End If 

If the If statement is not satisfied, the Walk method will try changing the current value of walkDirection with the next element of the dirs array.

The WalkToTarget Method

The WalkToTarget method causes the cat to move to the specified target. The target could be Doggie or the cat house:

 Public Sub WalkToTarget(ByVal tx As Integer, ByVal ty As Integer)   Dim dirs(4) As Integer   RequestDirection(dirs, tx, ty)   Walk(dirs) End Sub 

The WalkToTarget method accepts the screen coordinate of the target. If the target is Doggie, tx will be Dog.xScreen and ty will be Dog.yScreen.

The WalkToTarget method calls the RequestDirection that will populate an array of integers (dirs) with possible values of turn direction. The populated dirs will then be passed to the Walk method for a possible change of the value of walkDirection.

The MoveCat Method

If you understand the Walk and the WalkToTarget methods, understanding the MoveCat method is not difficult. This method changes the screen position of the cat in the next frame. When moving, the cats use a map as defined by the map array of string in the Maze class.

The map array of string is initialized with the following strings:

 Private map() As String = { _   "+-----------++-----------+", _   "|    J      ||      J    |", _   "| a-b a---b || a---b a-b |", _   "| | | |   | || |   | | | |", _   "| d-c d---c dc d---c d-c |", _   "|J   J  J  J  J  J  J   J|", _   "| a-b ab a------b ab a-b |", _   "| d-c || d--ba--c || d-c |", _   "|    J||    ||    ||J    |", _   "d---b |d--b || a--c| a---c", _   "    | |a--c dc d--b| |    ", _   "    | || JJJJ     || |    ", _   "    | || a--##--b || |    ", _   "----c dc |%%%%%%| dc d----", _   "     J  J|%%%%%%|J   J     ", _   "----b ab |%%%%%%| ab a----", _   "    | || d------c || |    ", _   "    | ||J        J|| |    ", _   "    | || a------b || |    ", _   "a---c dc d--ba--c dc d---b", _   "|    J J    ||   J J |    ", _   "| a-b a---b || a---b a-b |", _   "| db| d---c dc d---c |ac |", _   "|  ||J  J  J  J  J  J||  |", _   "db || ab a------b ab || ac", _   "ac dc || d--ba--c || dc db", _   "| J   ||    ||    ||   J |", _   "| a---cd--b || a--cd---b |", _   "| d-------c dc d-------c |", _   "|          J  J          |", _   "d------------------------c" _ } 

This is similar to the maze array of Strings. Note that the percent (%) sign represents a block inside the cat house and J denotes a junction.

Note

A junction is a cell from which a game actor can move in three or four directions. The cells in the four corners allow a game actor to move in only two directions and therefore are not junctions.

You pass the map array of strings to the Cat class by calling the Cat class's SetMap method:

 Public Sub SetMap(ByVal m() As String)   map = m End Sub 

First, the MoveCat method tries to detect if the cat is in a cell inside the cat house:

 Public Sub MoveCat()   Dim y As Integer = yScreen \ Maze.square   If map(y).Chars(xScreen \ Maze.square) = "%"c And _     yScreen Mod Maze.square = 0 And xScreen Mod Maze.square = 0 Then     ' cat in cat house     If (scared) Then       RandomWalk()     Else       'walk to the cat house door (to get out)       WalkToTarget(12 * Maze.square, 11 * Maze.square)     End If   ElseIf map(y).Chars(xScreen \ Maze.square) = "J"c Then     If (scared) Then       RandomWalk()     ElseIf (attack) Then       WalkToTarget(pac.xScreen, pac.yScreen)     Else       RandomWalk()     End If   Else     If (GameManager.MoveRequest(Me, walkDirection, walkDirection)) Then       Return     End If     If (GameManager.MoveRequest(Me, walkDirection, _       (walkDirection + 1) Mod 4)) Then       ' make an L-turn is okay       walkDirection = (walkDirection + 1) Mod 4       Return     End If     If (GameManager.MoveRequest(Me, walkDirection, _       (walkDirection + 3) Mod 4)) Then       ' make an L-turn is okay     walkDirection = (walkDirection + 3) Mod 4       Return     End If   End If End Sub 

The MoveDeadCat Method

The MoveDeadCat method is called from the Update method of the Cat class when the cat is dead—in other words, when the dead field of the cat object is True. This method is defined as follows:

 Sub MoveDeadCat()   If map(yScreen \ Maze.square).Chars(xScreen \ Maze.square) = "%"c _     And (yScreen Mod Maze.square = 0) _     And (xScreen Mod Maze.square = 0) Then     'the cat is in the cat house     dead = False     RandomWalk()   ElseIf map(yScreen \ Maze.square). _     Chars(CInt(xScreen \ Maze.square)) = "J" Then     WalkToTarget(12 * Maze.square, 13 * Maze.square)   Else     If GameManager.MoveRequest(Me, walkDirection, walkDirection) Then       Return     End If     If GameManager.MoveRequest(Me, walkDirection, _       (walkDirection + 1) Mod 4) Then       walkDirection = (walkDirection + 1) Mod 4       Return     End If     If GameManager.MoveRequest(Me, walkDirection, _       (walkDirection + 3) Mod 4) Then       walkDirection = (walkDirection + 3) Mod 4       Return     End If   End If End Sub 

First the method checks whether the cat is in the cat house. If it is, change its dead field value to False. By changing this, the next frame will display the cat in a normal appearance, not as a pair of eyes. Then, just do a random walk until the reverse situation is back to normal:

 If map(yScreen \ Maze.square).Chars(xScreen \ Maze.square) = "%"c _   And (yScreen Mod Maze.square = 0) _   And (xScreen Mod Maze.square = 0) Then   'the cat is in the cat house   dead = False   RandomWalk() 

If the cat is not in the cat house, check if it is on a junction. If it is, call the WalkToTarget method, passing the location of the cat house for the arguments. A junction is a cell that allows the cat to make an L turn. In other words, if a cat is in a junction, it can turn in three or four directions. The cells at the corner only allow the cat to turn 90 degrees, and they are not junctions:

 ElseIf map(yScreen \ Maze.square). _   Chars(CInt(xScreen \ Maze.square)) = "J" Then   WalkToTarget(12 * Maze.square, 13 * Maze.square) 

If the cat is not in the cat house and not in a junction, it tries to walk straight:

 If GameManager.MoveRequest(Me, walkDirection, walkDirection) Then   Return End If 

If it cannot walk straight, that is when it hits the wall (at a cell in the corner); it will attempt to make an L turn:

 If GameManager.MoveRequest(Me, walkDirection, _   (walkDirection + 1) Mod 4) Then   walkDirection = (walkDirection + 1) Mod 4   Return End If 

If it still cannot walk (because it tries to make the wrong L turn), it will attempt another L turn in another direction:

 If GameManager.MoveRequest(Me, walkDirection, _   (walkDirection + 3) Mod 4) Then   walkDirection = (walkDirection + 3) Mod 4   Return End If 
Note

You can find the code listings in the project's directory.

Compiling and Running the Application

All source files can be found in the Project directory. To compile the application, run the build.bat file.

The result will be a DoggieGame.exe file.

To run the program, type DoggieGame from the command prompt or double-click the icon in Windows Explorer. Note that the images folder containing all the image files must reside in the same directory as the DoggieGame.exe file.




Real World. NET Applications
Real-World .NET Applications
ISBN: 1590590821
EAN: 2147483647
Year: 2005
Pages: 82

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net