|
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. |
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.
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.
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.
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.
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.
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.
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).
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
![]() |
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" _ }
![]() |
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.
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.
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.
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.
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)
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 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."
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
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
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.
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
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.
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 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 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.
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 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. |
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.
|