Chapter 3: Understanding Object-Oriented Programming from the Start
You’ve Spent a Lot of Time perfecting the DicePanel class, and now you’re going to put that time to good use. In this chapter, you’ll write a new game that uses the DicePanel class. This game requires two dice and a set of numbered “tiles” that disappear based on the dice value. This game has dozens of variations; this one is called NineTiles (see Figure 3-1).
Figure 3-1: NineTiles
The object of NineTiles is to remove tiles that add up to the result shown on the two dice. In Figure 3-1, the dice result is 8, so you could get rid of the 8 tile, the 5 and 3 tiles, the 6 and 2 tiles, or the 7 and 1 tiles. The dice then roll again, and you repeat the process. You win the game if you get rid of all nine tiles, and you lose the game if the remaining tiles don’t add up to the dice result. In a slightly devious twist, the game automatically restarts after you win or lose, which gives it a somewhat addictive quality (you want your games to be played over and over, right?).
Starting the NineTiles Project
Believe it or not, about half of the NineTiles project is already written. You’re half done, and you haven’t even started! Of course, this is because you slavishly spent time refining the DicePanel class in Chapter 1, “Developing Your First Game,” and Chapter 2, “Writing Your First Game, Again.” Now that the DicePanel class is aToolbox control, you can simply drop it on a form and use it.That’s exactly what you should do to start the NineTiles project. Create a new Windows Forms project in Visual Studio .NET and then drop a DicePanel instance onto the default form. You can see how this might look in Figure 3-2. If you’d rather look at the completed code that comes with the book, you can download the NineTiles project.

Figure 3-2: A new project with a DicePanel control
Note | You can download the code from the Downloads section of the Apress Web site ([http://www.apress.com]). |
As mentioned previously, because DicePanel is an ancestor of the Panel control, you get all the functionality of the parent control in the descendant “for free.” So, use some of the Panel functionality in the NineTiles game by setting the Dock property of the DicePanel instance to Bottom and the Name property to oDice. You can also make the default height a bit larger by using the Properties window or by resizing the actual control in design mode.As for the form in the project, rename it to fMain (also remember to change the startup Object of the project as shown in Chapter 1, “Developing Your First Game”). In addition, you can also change the Size property, making the form much higher than wide, change the Text property (which becomes the window caption on a form instance) to NineTiles, change the font to 8-point Tahoma (my favorite font), and change the FormBorderStyle property to Fixed3D. These decisions are all personal preference, of course. As the designer of this game, you can choose the font, title, border style, and where to put the dice. You can also decide how the dice will look; for instance, instead of the 3D, slightly transparent red color, you could just as easily go with ivory or green and a 2D-only look.Once you’ve set up these initial properties, you can test run the project to see how it looks. Once satisfied that the form is set up and the DicePanel looks okay, it’s time to tackle the numbered tiles.
Note | I’m the type of programmer who test runs the program continuously throughout its development, both checking how it looks and checking if the various features I’m coding are working. |
Creating More Classes
It seems reasonable to encapsulate the functionality of the nine numbered tiles inside of its own class, just like the DicePanel class encapsulates the dice functionality. The structure of this new class will be served well if it’s similar to DicePanel from the standpoint that a single “manager” class will handle several instances of some type of “tile” data class. There are a few differences, however. One is that the “tile manager” class has a fixed number of tiles to manage (nine), so you don’t need a property to control the number of items to manage (such as the NumDice property on the DicePanel class). Another major difference is that the tiles don’t move around within the panel, meaning you don’t have to worry about position, direction, or collision-handling code. It’s for these reasons that you’ll create new classes for the numbered tiles functionality as opposed to somehow combining the functionality of the DicePanel class and this new class.
Note | See the “Looking Back on the Design” section at the end of this chapter for alternate ways of organizing this project. |
Creating a NumberPanel Class
The manager class in the NineTiles game is NumberPanel. Some of the functionality of the NumberPanel class is similar to the DicePanel class, including the following:
Inheriting the control from the .NET Framework Panel control and using the SetStyle method to eliminate flicker
Loading the graphics from a bitmap embedded in the executable
Using a background bitmap where drawing takes place and overriding the Panel control’s OnPaint method to copy the background bitmap to the foreground
Using an embedded class that manages the individual instances of the “managed” objects, which in this case are the nine tiles
Because these parts of the NumberPanel class are so similar to the DicePanel class, this chapter won’t cover their functionality. It focuses instead on the new topics.
Manipulating the Tiles: Left-Clicking
Users manipulate the tiles by clicking them. When a tile is left-clicked, it toggles between facing forward and backward, with an animated sequence moving from one state to the other. If a numbered panel is right-clicked, then all the backward tiles disappear from the panel—but only if these backward tiles add up to the numbers on the dice (this is perhaps better demonstrated than explained; you’ll get it in about two seconds if you play the game). Figure 3-3 shows the game in progress, demonstrating the three states of the tiles. In this game, the 5, 7, and 9 tiles have already been removed, and the 4 and 3 tiles have been flipped over to match the amount rolled on the dice. A right-click at this point of the game removes the 4 and 3 tiles.

Figure 3-3: NineTiles in action
Listing 3-1 shows the code within the NumberPanel class that handles the left-click portion of the tile movement. The OnMouseDown method determines exactly which tile was clicked with the help of the private TileFromPoint method. Once the program determines the clicked-on tile, it calls the ToggleFacing method on the TileData object (this is the class that encapsulates a single tile object, discussed in the “Creating the TileData Class” section).Listing 3.1: Manipulating a Tile
Private Function TileFromPoint(ByVal x As Long, _
ByVal y As Long) As Integer
TileFromPoint = ((y \ IRAD) * 3) + (x \ IRAD)
End Function
Protected Overrides Sub OnMouseDown(ByVal e As _
System.Windows.Forms.MouseEventArgs)
MyBase.OnMouseDown(e)
If e.Button = MouseButtons.Left Then
If Not FInAnimation Then
FInAnimation = True
Try
Dim aTile As TileData
Dim iTile As Integer = TileFromPoint(e.X, e.Y)
aTile = oTiles(iTile)
aTile.ToggleFacing
Catch oEX As Exception
Throw oEX
Finally
FInAnimation = False
End Try
End If
End If
End Sub
ReadOnly Property bInAnimation()
Get
Return FInAnimation
End Get
End Property
A private variable named FInAnimation is checked and set at the beginning and end of the OnMouseDown method. This Boolean variable indicates that the NumberPanel is currently in an animation loop and helps prevent further user action while this action is occurring. This helps prevent, for example, a double-click from animating a tile twice. As you can see, the variable is set back to False at the end of the method within a Finally block, which guarantees that this line will run regardless of errors that might occur in Listing 3-1.This FInAnimation variable is exposed to the outside world via the read-only property bInAnimation, also shown in Listing 3-1.
Note | The code uses the OnMouseDown method instead of OnClick because OnMouseDown makes available the button or buttons that were used, and OnClick doesn’t. |
Manipulating the Tiles: Right-Clicking
When the NumberPanel is right-clicked, the reversed tiles need to disappear. There’s one important trick to this functionality that greatly affects the design of the program, however. The backward tiles disappear from the panel only if they add up to the value on the two dice. For example, if the two dice add up to 7 (perhaps a4 and a 3) and the game player flips over the 8 tile, then the panel needs to reject the right-click.It’s this important fact that prevents you from handling the right-click code in the same OnMouseDown method shown in Chapter 2, “Writing Your First Game, Again”). With this decision, an instance of the PayrollInfo class is “carried around” with every instance of the Employee class. With this decision, you’ve possibly introduced many problems into your application, including the following:
Security: Is the information contained in the PayrollInfo class useful or required every time an Employee class is instantiated? Probably not because the Employee class is probably used in some 90-plus percent of the program. Is embedding the “supersecret” class inside the most well-used and important class of the program a good idea? It isn’t if a team of programmers works on this program. What if your application becomes so important to the company that it promotes you to manager and hires three entry-level developers to work under you? Do these first-year programmers need access to the PayrollInfo code? Probably not. Of course, you can hide the code for this class by putting it in a separate DLL and giving them access to the object code only. But how hard would it be for aprogrammer to write a test program that instantiates an instance of the Employee class, calls the code that loads the PayrollInfo data, and then displays the employee’s salary in the Visual Studio console window? The programmer doesn’t need access to the source code; she only needs to know the implementation details of this class (provided all too happily by Visual Studio’s Intellisense). To counteract this, you need to build user-based security inside the PayrollInfo class.Overhead: Even if the data involved in the new functionality isn’t supersecret, do you need to carry it around in memory every time you create an Employee class? Probably not; after all, this program has existed for ayear without this data. The functionality has been working fine without the new data, so why add to the memory requirement of these old functions?
The degree of connectivity between functionality (classes) in the object-oriented world is called coupling. Good object-oriented design dictates keeping coupling between objects loose. In the human resources application, embedding the PayrollInfo class inside the Employee class is an example of tight coupling— you’ve linked their functionality together forever by putting one inside the other, and the two problems described previously are examples of why this is bad. Instead, you need to come up with a solution that’s more loosely coupled.In this example, you can achieve a more loosely coupled solution by reversing the original concept. Instead of a PayrollInfo property attached to the Employee class, attach an Employee property to the PayrollInfo class.The benefits of this solution are many. From a security standpoint, you can continue to keep your new PayrollInfo source code (and perhaps even compiled code) away from your first-year developers by putting it in a place on the network where they don’t have access. You (as the sole developer of this high-security code and data) will have access to this source code area. All developers can access the area where the Employee class resides. This solves the overhead problem because the often-used Employee class doesn’t “bloat up” with the code and data providing this new functionality. Instead, the PayrollInfo class, when it’s used, creates the equivalent Employee object so it has access to data such as a hire date or married status.You can see how such a seemingly innocuous design decision (which class to make a property of the other) can have possible wide-ranging effects on both the security and memory requirements of the final application.Enough discussion about human resources applications (yuck!); let’s get back to discussing the NineTiles project. There are two classes, the DicePanel and NumberPanel classes, and some part of the program will need access to both the value of the dice and the value of the reversed tiles so that they can be compared. One way to do this is to create a property on the NumberPanel class that refers to the DicePanel class. This allows you to check the public Result property in the protected OnMouseDown method. This solution exhibits tight coupling between the two classes, however. What if, down the road, someone introduced a new game (or even nongame) program where the NumberPanel class might be useful but didn’t require a DicePanel class? This would be too bad for you—the file DicePanel.dll would be required because of the reference to the DicePanel class as a property, meaning you would be installing a DLL on the end user’s computer that might never be used.Instead, you can choose a solution that decouples the DicePanel and NumberPanel classes. This solution is to handle the right-click handling code in the external MouseDown event of the NumberPanel class (see Listing 3-2).Listing 3.2: Tile Right-Click, Handled Outside the NumberPanel Class
Private Sub NumPanelMouseDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
If oNumPanel.bInAnimation Then Exit Sub
If e.Button = MouseButtons.Right Then
If oDice.Result = oNumPanel.Result Then
oNumPanel.HideBackward()
oWav.Play("thud", 400)
If oNumPanel.TilesVisible = 0 Then
oWav.Play("applause", bSync:=True)
StartGame()
Else
oDice.RollDice()
If Not oNumPanel.ResultAvailable(oDice.Result) Then
oWav.Play("laughs", bSync:=True)
StartGame()
End If
End If
Else
oWav.Play("dischord")
End If
End If
End Sub
This event is defined in the game’s form, just as you might handle the Click event of a button. It runs whenever the user clicks the mouse on the panel with either mouse button. Because this code runs from the form, it has access to the NumberPanel instance (named oNumPanel) and the DicePanel instance (named oDice), so the comparison can be done between the two values.In truth, this single event handler comprises almost all of the game functionality. Let’s trace through it to see what it’s doing. First, it checks to see if the NumberPanel is already engaged in an animation via the bInAnimation property shown in Listing 3-1. If this property is True, then the event handler exits immediately (it doesn’t want to do further processing until the animation loop is complete). If this property is False, the code continues.If the user right-clicks, you want to run the “make-tiles-disappear” code. The previously alluded to comparison happens between the DicePanel class’s Result property and the NumberPanel class’s Result property. If they’re unequal, then the knucklehead user has right-clicked with the backward tiles not equaling the result on the dice, and the line of code oWav.Play("dischord") executes. (This will be discussed later; suffice to say that a nasty-sounding WAV file plays to indicate that the user made an error.) If the two values are in fact equal, then the program removes the tiles (the HideBackward method on the NumberPanel), and a little sound effect plays (named thud).The rest of the event deals with what happens after this turn is over. There are two possibilities—the user has won the game because there are no tiles left or the user continues to play because some tiles remain, in which case the dice roll again. After this new roll, the program checks to see if this new roll can be satisfied by the remaining tiles (the ResultAvailable method). If it can, the code ends for now (it will wait for the user to flip over tiles and keep playing). If the result can’t be satisfied, then the user has lost.If the player has either won the game or lost it in this event handler, the same two actions are performed. First, a sound effect plays (some applause for the winner or mocking laughter for the loser), and then the game restarts. This auto-restart of the game is key to its addictive nature (at least I find it hard to stop playing).
Creating One Click but Multiple Methods
You might have noticed something interesting about Listings 3-1 and 3-2. There’s an OnMouseDown method inside the NumberPanel class and a MouseDown event outside of the class, and both pieces of code run when the player clicks on the panel. The functionality between these two procedures is mutually exclusive (one performs actions upon a left-click, the other on a right-click), but this is by choice. It’s possible to have multiple pieces of code run in response to the same event. How is this linked together? Listing 3-3 shows a greatly simplified version of the NumberPanel and its ancestor class, the Panel, to try to explain.Listing 3.3: Relationship Between an Event (MouseDown) and the Method That Calls It (OnMouseDown) in an Ancestor Class
Class Panel
Public Event MouseDown
Protected Sub OnMouseDown
RaiseEvent MouseDown
End Sub
End Class
Class NumberPanel
inherits Panel
Protected Overrides Sub OnMouseDown
MyBase.OnMouseDown()
... (left click code)
End Sub
End Class
The Panel class contains an event named MouseDown that’s raised from within the method named OnMouseDown. This method is protected, meaning that it’s available in descendant classes. In your descendant class, NumberPanel, the intent was to add functionality inside the class to handle the animation of a tile when it was left-clicked. To accomplish this, you can override the OnMouseDown method and place the tile-moving code in that method. The one thing you must remember to include is the MyBase.OnMouseDown call. This important line runs the OnMouseDown code in the base class (Panel), which is the code that raises the event to the outside world or to the form using the NumberPanel class. The right-click handling code is placed in this event handler.
Note | Because Microsoft doesn’t currently make the .NET Framework source code available, Listing 3-3 doesn’t necessarily accurately reflect the exact structure of the Panel class. Instead, it represents a “best guess” as to its structure. |
If you’d like a demonstration of how the event and protected methods are linked, try commenting out the MyBase.OnMouseDown line in the NumberPanel class’s OnMouseDown event and run the program. What you’ll find is that the right-click handling event code on the form is no longer executed. You’ve prevented the RaiseEvent line in the Panel class from running because the Panel class’s OnMouseDown is never called.
Understanding Decoupling vs. Encapsulation
This chapter and the previous one have thrown two important object-oriented principles at you. Chapter 2, “Writing Your First Game, Again,” described the concept of encapsulation and making an object responsible for itself. This chapter has discussed the desire for loose coupling between classes so that they can work on their own.These two principles are actually working against each other in this project. It would be better for the sake of encapsulation if you could bring the right-click handling code inside of the NumberPanel class. To accomplish this, though, you’d need to somehow bring the value of the dice into the NumberPanel class so you could check to see if it’s okay to hide the tiles on which the user has clicked.There are ways to solve this particular problem to achieve better encapsulation but still keep the two classes decoupled. (A hint: The solution requires a new event on the NumberPanel class). This chapter won’t cover this solution for now, though, because although it might solve the problem of comparing the dice value to the NumberPanel value, there are other reasons to couple the two classes later, such as getting the dice to reroll after hiding the backward tiles or after the user has won or lost the game.The point is that there isn’t always a “perfect” design. The current design has some problems (the lack of encapsulation of the NumberPanel right-click functionality), but solving that problem by coupling the two classes might bring on new problems in the future—for instance, if you decided to create a second game with the NumberPanel that didn’t require a die.It often turns out that the solution you come up with now may prove to hinder some aspect of your coding down the road. If you chose to add a DicePanel property on the NumberPanel class, thinking this was the only NumberPanel game you were to ever write, you’d have a problem in the future because of the unnecessary requirement you created by tightly coupling the two classes.
Note | This, of course, leads back to matt tag’s first law of coding: Don’t be afraid to rewrite code. |
Nobody can predict the future (if I could, I’d know the winners of the next 10 World Series and Super Bowls, which would pretty much ruin any interest I have in sports). Therefore, it’s quite possible you may not come up with the best design for aproject today because you don’t know how you might reuse the elements in that project (or future projects) tomorrow. When you do find you’ve made a shortsighted decision that you can correct with some refactoring, and you know this refactoring will improve the code for a future project, then you should do the refactoring. The result is better code with higher reusability.
Getting Back to the NumberPanel Class
The right-click event shown in Listing 3-2 references a number of members found in the NumberPanel class (see Table 3-1).
MEMBER NAME | TYPE | PURPOSE |
---|---|---|
Result | Property (read-only) | Returns summed value of flippedover tiles |
HideBackward | Method | Marks all flipped-over tiles as invisible and redraws the panel so they disappear |
TilesVisible | Method (returns Integer) | Returns number of tiles left showing and determines if the game has been won |
ResultAvailable | Method (returns Boolean) | Returns if the visible tiles can be combined to add up to the value passed in as a parameter and determines if the game has been lost |
The first three members in Table 3-1 have a similar structure in that they loop through all of the tile objects and check some data within them or perform some action on them. Listing 3-4 shows the code for all three members.
Listing 3.4: NumberPanel Members That Loop Through the ArrayList of TileData Objects to Do Something
Public Function TilesVisible() As Boolean
Dim aTile As TileData
Dim i As Integer = 0
For Each aTile In oTiles
If aTile.pVisible Then
i += 1
End If
Next
Return i
End Function
ReadOnly Property Result() As Integer
Get
Dim aTile As TileData
Dim i As Integer = 0
For Each aTile In oTiles
If aTile.pVisible And aTile.pBackwards Then
i += aTile.pTileNum
End If
Next
Return i
End Get
End Property
Public Sub HideBackward()
Dim aTile As TileData
For Each aTile In oTiles
If aTile.pVisible And aTile.pBackwards Then
aTile.pVisible = False
End If
Next
Me.Invalidate()
Application.DoEvents()
End Sub
In all three of these members, the ArrayList named oTiles is iterated against to obtain each TileData object contained within it. Each instance (held in the variable aTile in all cases) is then checked for visibility (in all three members, hidden tiles are ignored). The work done on each tile is slightly different in each case and corresponds to the function each member performs as listed in Table 3-1.The last member, ResultAvailable, is more of a “brute-force” loop that determines if a given value can be obtained by adding up the visible tiles. This routine makes use of the fact that no more than four tiles could ever be summed up to obtain a value rolled on two dice (2 to 12). The five lowest tiles add up to 15, and 1+2+3+4 could be used on a roll of 10. Listing 3-5 shows the beginning of this routine (the remainder of the routine contains a great deal of duplication; you can check it out in its entirely in the project if you’re curious). The method returns True if the passed-in value can be obtained with some combination of visible tiles.Listing 3.5: The ResultAvailable Method
'checks all combinations of 1, 2, 3, and 4 visible tiles
Public Function ResultAvailable(ByVal iDesired As Integer) As Boolean
Dim i, j, k, l As Integer
Dim aTilei, aTilej, aTilek, aTilel As TileData
'one-bangers
For i = 0 To oTiles.Count - 1
aTilei = oTiles(i)
If aTilei.pVisible Then
If aTilei.pTileNum = iDesired Then
Return True
End If
End If
Next
'2-bangers
For i = 0 To oTiles.Count - 2
For j = i + 1 To oTiles.Count - 1
aTilei = oTiles(i)
aTilej = oTiles(j)
If aTilei.pVisible And _
aTilej.pVisible Then
If aTilei.pTileNum + aTilej.pTileNum = iDesired Then
Return True
End If
End If
Next
Next
... <3 and 4 tile checks removed>
Creating the TileData Class
As mentioned earlier, the NumberPanel class acts as a manager to nine instances of a TileData class. This class contains the information necessary to track one of the nine colored, numbered tiles. Listing 3-6 shows the public interface of the TileData class.Listing 3.6: The Class TileData
Private Class TileData
Public Sub New(ByVal oPanel As NumberPanel, _
ByVal iTileNum As Integer)
ReadOnly Property pTileNum() As Integer
Property pVisible() As Boolean
Property pBackwards() As Boolean
Public Sub Reset
Public Sub ToggleFacing
End Class
Looking at the public interface for a class is a good way to study its functionality and behavior without getting bogged down or confused by how this behavior is implemented. You’ll do this quite often when studying the classes in the .NET Framework. These classes are described in the help documentation in terms of their public interface and what function each member of that public interface performs. To use a class in the .NET Framework, you need only understand its public interface.
Tip | When it comes time to extend the functionality of a .NET Framework class through inheritance, you then need to understand the base class’s protected interface. |
Table 3-2 describes the public members on the TileData class.
MEMBER NAME | TYPE | PURPOSE |
---|---|---|
pTileNum | Property | Returns the number on the tile, 1–9. This is read-only because it’s set via the constructor. |
pVisible | Property | Sets/returns whether the tile is visible. The user wins the game when pVisible = False for all nine tiles. |
pBackwards | Property | Sets/returns whether tile is facing forward or backward. |
Reset | Method | Sets tile to face forward, visible. |
ToggleFacing | Method | Plays animation loop of tile moving from forward to backward or vice versa. |
If you’re comparing the TileData class to the Die class discussed in Chapter 2, “Writing Your First Game, Again,” you might find a member or two conspicuously absent from its interface. In particular, where is the Draw method? Doesn’t the NumberPanel class need a way to draw a tile onto itself? Also missing is the pFrame property. How does the TileData know which frame it’s going to draw during the animation or simply when it’s drawing the tile at rest, either forward or backward?The answer is that these two members aren’t missing; they’re simply not public. Remember that Listing 3-6 describes only the public interface for the class. Any functionality declared as private isn’t described, so you as the potential user of this class can be spared from the implementation details.This public/private shielding is rather a moot point in the discussion of the TileData class because the entire class is declared as private inside the NumberPanel class, meaning that you as the user of the NumberPanel would never have access to it anyway. That doesn’t prevent you as the coder from attempting to make the object as responsible for itself as you can, though, if for no other reason than keeping good design methods in practice.
Exploring the Animation Details
Each animated tile is represented by 10 frames rotating from a front-facing to arear-facing position. Figure 3-4 shows the master bitmap used as the source of the animated frames.

Figure 3-4: The 90 animated frames used to render the nine tiles
As you can see, if you’re responsible for rendering one of the nine tiles, you’ll render along a horizontal row on the source bitmap. In other words, the source y coordinate won’t change from frame to frame; all that changes is the x coordinate. You can determine the changing x coordinate by which of the 10 frames (numbered 0–9) is to be drawn. You can calculate the coordinate by multiplying the current frame value by the width of one animated frame (the constant 112). Another constant in rendering is where the tile is to be drawn on the destination panel because the numbered tiles don’t move around like dice do.These three fixed values are stored in private variables on the TileData class. Listing 3-7 shows the declaration of these three values, as well as the constructor for the class, which shows where these values are initially calculated and stored.
Listing 3.7: Private Variables and Constructor for the TileData Class
Private Class TileData
Private FPanel As NumberPanel
'don't need separate height/width, sprite is square
Private Const IRAD As Integer = 112
'tiles are always drawn in the same place on the panel
Private FxPos, FyPos As Integer
Private FySrc As Integer
Public Sub New(ByVal oPanel As NumberPanel, _
ByVal iTileNum As Integer)
MyBase.New()
Dim iTilePos As Integer
FPanel = oPanel
FTileNum = iTileNum
iTilePos = iTileNum - 1 '0-8, one less than tile number
'coordinates to draw tile on panel are fixed
FxPos = (iTilePos Mod 3) * IRAD
FyPos = (iTilePos \ 3) * IRAD
'y coord in source bitmap fixed (see bitmap)
FySrc = iTilePos * IRAD
End Sub
The constructor takes a NumberPanel and a tile variable (integer 1–9) as a parameter. The NumberPanel is referenced by the private variable FPanel, which is similar to the setup that was used in the Die class to hold an instance of the DicePanel class. The FPanel variable will be used later to gain access to required members of that class for drawing. You use the tile number integer as the basis to calculate the destination point on the panel (FxPos and FyPos) and the source y coordinate in the animation frame’s bitmap (FySrc).The Frame property (which is private) tells the class which of the 10 frames to draw. If you think about it, it’s easy to determine the frame. If the tile is facing forward, the frame is 0. If the tile is facing backward, the frame is 9. If the tile is being animated in either direction, the frame is iterating from 0 to 9 or 9 to 0 in a loop.
This gives you enough information to draw a tile onto a bitmap. Listing 3-8 shows the (private) Draw method of the TileData class. Note this method is private, meaning the NumberPanel class can’t call it. Who calls it, then?Listing 3.8: The TileData Class’s Draw Method
Private Sub Draw()
Dim gr As Graphics
Dim r As System.Drawing.Rectangle
Dim xSrc As Integer
Dim bDest As Bitmap = FPanel.bBack
gr = Graphics.FromImage(bDest)
Try
If FVisible Then
xSrc = FFrame * IRAD
r = New System.Drawing.Rectangle(xSrc, FySrc, IRAD + 1, IRAD + 1)
gr.DrawImage(FPanel.fbNum, FxPos, FyPos, r, GraphicsUnit.Pixel)
Else
'draw a black square
r = New System.Drawing.Rectangle(FxPos, FyPos, IRAD, IRAD)
gr.FillRectangle(New SolidBrush(Color.Black), r)
End If
Finally
gr.Dispose()
End Try
End Sub
The Draw method for the tile is somewhat similar to the like-named method of the Die class shown in Chapter 2, “Writing Your First Game, Again.” One difference is that this class has to be able to “draw” an invisible tile, which it does by drawing a filled black rectangle instead of one of the source bitmap frames. Another difference is that this method doesn’t pass in the destination bitmap as a parameter. Instead, the background bitmap associated with the NumberPanel is obtained directly by using the FPanel variable. This is a design choice and doesn’t really provide any design benefit.You’ve now seen the Draw method, but you still haven’t seen what calls it yet. You can make a reasonable guess, though. Because the method is private, it has to be called from within the TileData class. In fact, two places in the class call the code. Listing 3-9 shows both calling members.
Listing 3.9: The TileData Class’s pVisible Property and ToggleFacing Method
Private FVisible As Boolean = True
Property pVisible() As Boolean
Get
Return FVisible
End Get
Set(ByVal Value As Boolean)
FVisible = Value
Me.Draw()
End Set
End Property
Public Sub ToggleFacing()
Dim iStart, iEnd As Integer
Dim iDir, iLoop As Integer
If Not FVisible Then Exit Sub
If pBackwards Then
iStart = 9
iEnd = 0
iDir = -1
Else
iStart = 0
iEnd = 9
iDir = 1
End If
FPanel.OnTileMoving(pBackwards)
For iLoop = iStart To iEnd Step iDir
pFrame = iLoop
Me.Draw()
FPanel.Invalidate()
Application.DoEvents()
Next
pBackwards = Not pBackwards
End Sub
The pVisible property is a great example of connecting code execution to the changing state of an object. When an outside call changes the pVisible property, the Set method calls the Draw method, which redraws the tile on the panel.
The ToggleFacing method handles the animated loop of a tile moving forward or backward when clicked. The animated frames are iterated from 0 to 9 if the tile is currently facing forward and from 9 down to 0 if the tile is currently facing backward. Furthermore, note how this method makes a call to the FPanel.OnTileMoving method, which in turn raises a public TileMoving event on the NumberPanel. This event allows the NumberPanel to notify the calling form that a tile is moving so that it can react by playing a sound, for example.
Connecting It All Together
The NumberPanel class is complete; all that remains is to slap one on the main form, wire it up, and start playing. You could put the NumberPanel class into its own project, compile it into a DLL, and add it to the Visual Studio Toolbox, just as you did the DicePanel. However, because this new class doesn’t seem as “general purpose” as the DicePanel is, adding it to the Visual Studio Toolbox won’t necessarily benefit you in later projects. Therefore, you can leave the class inside this project and then create one and add it to the form on the fly. Listing 3-10 shows how you do this. This isn’t quite as clean (or fun) as adding the control to the Toolbox, but it serves the purpose of this game well enough.Listing 3.10: Adding a NumberPanel to a Form on the Fly
Public Class fMain
Inherits System.Windows.Forms.Form
Private oNumPanel As NumberPanel
Private oWav As WavLibrary
Private oRand As Random
Private Sub fMain_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
oNumPanel = New NumberPanel
oNumPanel.Dock = DockStyle.Top
AddHandler oNumPanel.MouseDown, AddressOf NumPanelMouseDown
AddHandler oNumPanel.TileMoving, AddressOf NumPanelTileMoving
Me.Controls.Add(oNumPanel)
SetupWavLibrary()
oRand = New Random
End Sub
Listing 3-10 shows the Form.Load event of the main form and a few private variables on the form. This event shows a NumberPanel object being instantiated and added to the form via the form’s Controls.Add method. You can also see how the right-click code event handler (shown way back in Listing 3-2) is wired up to this object via the AddHandler statement and how a second method named TileMoving is wired up to another method named NumPanelTileMoving. This method simply plays a WAV file while a tile is animating backward or forward.Speaking of playing WAV files, the next section discusses a new, improved WAV-playing library and how to use that code in multiple programs.
Using WavPlayer Release 2.0: The WavLibrary
You saw a simple class that used a Win32 application programming interface (API) call to play a WAV file in Chapter 2, “Writing Your First Game, Again” (refer to Listing 2-11 if you’d like to review it). Although the basic engine of playing the WAV file appears to function well enough, version 1.0 of the class has two problems:
Unwieldy names for the WAV files: Because the WAV files are embedded in the executable as resources, they end up having names that include the namespace name, such as GuessTheDieRoll2.DIE1.WAV. These long names tend to be distracting on a line of code; it’d be better to refer to a sound by anickname, such as die1.
Unnecessary memory move: If you study Listing 2-11, you’ll see there’s amemory copy from the embedded resource into a byte array named bstr every time the WAV plays. Even though there weren’t any noticeable performance problems, it seemed more efficient to perform this memory move only once per WAV file and then store the byte array in memory until the end of the program.
These two new requirements gave rise to a new version of the WavPlayer class. The concept behind this new design should be familiar to you by now; you can encapsulate a single WAV file as a class and then wrap that class with a manager class that handles the setup and storage of the individual components. This new class is called WavLibrary.
I seem to be stuck on this “manager/embedded class” design methodology in the early stages of this book, but I’m certainly not advocating using this style of design over any other. I find this design useful when I have some number of “things” (such as dice, tiles, or WAV files) that I need to keep track of but have no use for the “things” to be accessed as individuals in the outside world. In other words, I can embed the class that tracks the things inside a manager class and allow that manager class to be the interface to the outside world.This was especially useful in the case of the DicePanel and TilePanel classes because the manager class was also providing the visual representation of the object (because both were descendants of the .NET Framework Panel class). If you’re familiar with the concept of design patterns, this common pattern is known (appropriately enough) as the Manager pattern.
The WavLibrary class contains a private class named WavFile, which contains the byte array into which each WAV resource is loaded, as well as a Name property that will allow you to refer to each sound using a short “handle.” Listing 3-11 shows the public interface for the WavFile class, which encapsulates a single sound used by the game.Listing 3.11: Public Interface for the WavFile Class
Private Class WavFile
Public Sub New(ByVal cName As String)
Public Sub Dispose()
Property Name() As String
Public Function LoadFromResource(ByVal cResName As String) As Boolean
Public Overloads Sub Play(ByVal bSync As Boolean)
Public Overloads Sub Play(ByVal mSec As Integer, _
ByVal bSync As Boolean)
End Class
Chapter 2, “Writing Your First Game, Again,” already discussed how a WAV resource is loaded from the executable into a byte array and how that byte array is passed the Win32 API function sndPlaySound, so this chapter won’t cover that material again. What has changed is when the byte array is loaded: Instead of loading it every time a sound is played, a separate method named LoadFromResource has been added, with the intent that this method be called before the WAV file is played the first time but is then kept in memory for subsequent plays.Another difference in this class is that there are three methods for playing the sound. The first two are both named Play, which is a new feature to Visual Basic .NET. Previous versions of Visual Basic didn’t allow two methods with the same name. True object-oriented languages allow this as long as the method signature (parameter list) of each like-named method is different. In object-oriented programming, this is known as method overloading. You’ll notice that Visual Basic requires the use of the Overloads keyword on any method that you plan to overload with multiple implementations. Method overloading is much easier on the user of your class because she doesn’t have to remember multiple method names such as Play and PlayWithPause.The two different Play methods allow a single method name to provide different functionality. In this case, the second method allows the entry of an integer value that represents a number of milliseconds that the player should pause after playing the WAV file. This allows the user of the WAV library to time her sound effects with the actions they’re augmenting. A parameter named bSync on both Play implementations allows the sound to be played synchronously instead of asynchronously, meaning that all action stops while the sound is being played. This feature is used in NineTiles when the derisive laughter or light applause plays at the end of the game.The WavLibrary class is the manager class for the individual WAV files. It’s small enough that Listing 3-12 shows most of it.Listing 3.12: The WavLibrary Class, the Manager for the WavFile Classes
Public Class WavLibrary
Private FSounds As Hashtable
Public Sub New()
MyBase.New()
FSounds = New Hashtable
End Sub
Public Sub Dispose()
Dim w As WavFile
For Each w In FSounds.Values
w.Dispose()
Next
End Sub
Public Function LoadFromResource(ByVal cResName As String,_
ByVal cName As String) As Boolean
Dim w As WavFile
w = New WavFile(cName)
If w.LoadFromResource(cResName) Then
FSounds.Add(w.Name, w)
End If
End Function
Public Overloads Sub Play(ByVal cName As String, _
Optional ByVal bSync As Boolean = False)
Dim w As WavFile
w = FSounds.Item(cName)
If w Is Nothing Then
Throw New Exception("Sound name " & cName & " not found")
Else
w.Play(bSync)
End If
End Sub
<overloaded Play method removed (very similar to first Play method)
<Wav File declaration removed>
End Class
The first interesting thing about this manager class vs. the others you’ve seen is that this class uses a Hashtable to store its component objects rather than an ArrayList. A Hashtable is a different type of collection class that allows a unique key to be stored along with each object. This satisfies the requirement of being able to name each sound with a “handle” to which it can be referred, rather than the ungainly full name of the WAV file that includes the program’s namespace. You can see the line of code that adds a new WavFile instance to the Hashtable in the LoadFromResource method. The objects contained within a Hashtable can then be retrieved by name, which is shown in the Play method. Note that if a handle is passed into the Play method that doesn’t exist in the Hashtable, meaning that the caller of the library tried to play a WAV file that hasn’t been set up, an exception is thrown. Otherwise, the named sound has been found, and the Play method on the corresponding WavFile instance is called.Another interesting thing about the Play method is the use of an optional parameter named bSync. If the caller omits this parameter, its value is defined to be false. The combination of the optional parameter and the overloaded Play method allows the caller to call this one method four different ways to achieve different combinations of functions:
oWav.Play("snd")
oWav.Play("snd", bSync:=true)
oWav.Play("snd", 100)
oWav.Play("snd", 100, bSync:=true)
Here, the WAV file plays four different ways: with no pause, synchronously, with a 100-millisecond pause, and both synchronously and with a 100-millisecond pause (kind of overkill, but what the heck).
Setting Up the WavLibrary
You can use the WavLibrary class in a program with a few short setup routines. Listing 3-13 shows how the NineTiles program uses this class. All that’s required is loading each WAV resource and calling the Play method at the appropriate time.Listing 3.13: Using the WavLibrary Class
Private oWav As WavLibrary
Private oRand As Random
Private Sub fMain_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
<unrelated code removed>
SetupWavLibrary()
oRand = New Random
End Sub
Private Sub SetupWavLibrary()
oWav = New WavLibrary
oWav.LoadFromResource("NineTiles.die1.wav", "die1")
oWav.LoadFromResource("NineTiles.die2.wav", "die2")
oWav.LoadFromResource("NineTiles.squeak1.wav", "squeak1")
oWav.LoadFromResource("NineTiles.squeak2.wav", "squeak2")
oWav.LoadFromResource("NineTiles.thud.wav", "thud")
oWav.LoadFromResource("NineTiles.applause.wav", "applause")
oWav.LoadFromResource("NineTiles.laughs.wav", "laughs")
oWav.LoadFromResource("NineTiles.dischord.wav", "dischord")
End Sub
Private Sub oDice_DieBounced() Handles oDice.DieBounced
Dim cWav As String
If oRand.Next(0, 1000) Mod 2 = 0 Then
cWav = "die1"
Else
cWav = "die2"
End If
oWav.Play(cWav)
End Sub
Listing 3-13 shows how the WAV library is declared, instantiated, and then loaded with the WAV resource embedded in the program. You can see how each resource is connected to a “handle,” which in this case is simply the filename of each file without the namespace or the extension. You can give any handle to any WAV file.The oDice_DieBounced method shows how one of two available die “clicking” noises plays whenever one of the dice bounces against a wall or against another die.It’s also interesting that the WavLibrary class is used only in the main form and not in any of the classes in this project (DicePanel, NumberPanel). When sounds are to be played in combination to actions happening in these classes, an event is raised back to the main program to play the sound, just as the DieBounced event shown previously. This is another example of reducing coupling between classes.
Creating One Library, Multiple Projects
You might imagine that a class such as WavLibrary has wide-ranging utility, and therefore it might be something you can reuse in multiple projects. When you’ve written a potentially often-used piece of code like this, you can move the source file to a common folder so that you’re not copying the same source file into multiple application folders.However, Visual Studio has a somewhat nasty habit when dealing with source files in other folders. Whenever you add a file to a Visual Studio project that isn’t in the project’s source folder, the file is automatically copied into the project’s source folder. Therefore, if you decide to put the WavLibrary source file into a common folder and then you go and add it to your NineTiles project or any other project, Visual Studio makes a copy of that file and puts the copy into the NineTiles folder.Why is this a bad habit? Well, suppose you find a bug in the WavLibrary class or decide to add new functionality to it. You open your project and make the required changes. Unfortunately, if this “common” source file is used in many projects, then you’ve only changed the single copy of the library, and all of the other copies remain unaltered.
Note | I used to work with a guy who had a “common” string library, but it was copied into every project’s source folder, which I didn’t realize when I first started working there. Over time, both of us were adding useful functions to what I thought was a common function library. I eventually realized that there were in fact a dozen copies of the “common” library, and I had a full-scale mess on my hands trying to combine all the changes into a single useful library module. |
Fortunately, there’s a way to override this functionality and to instead create a link to a file in a folder other than the project’s folder, but it’s a bit hidden. The key is that when you select Project


Figure 3-5: Linking source files to your project instead of making copies of them
You can see that a file is linked to a project by the (very) little shortcut symbol in the lower left of the icon for that file in the Solution Explorer. Feel free to use this linking for items other than source files, as well.
Tip | I often use this linking technique for embedded resources such as bitmaps and WAV files so that I can keep these in a common folder. |