Version 3: Creating the DicePanel Class
The DicePanel class will be a descendant of the .NET Framework Panel class (just as the old PaintPanel was), which in turn is a descendant of the Control class. The entire object hierarchy looks like this:
System.Object
System.MarshalByRefObject
System.ComponentModel.Component
System.Windows.Forms.Control
System.Windows.Forms.ScrollableControl
System.Windows.Forms.Panel
DicePanel
All of the classes listed in this hierarchy (except for the DicePanel you’re about to write, of course) are part of the .NET Framework. The Control class serves as the ancestor to any class with a visual representation. Therefore, all of the buttons, listboxes, labels, radio buttons, checkboxes, and so forth are all descendants of the Control class. You might also notice that the Panel class inherits directly from something called ScrollableControl, which provides support for scrolling—functionality that won’t be used in the DicePanel class.Because the DicePanel will be a descendant of the Control class, you’ll be able to place it into the Toolbox on the left side of Visual Studio, which means aprogrammer will have the ability to drag a DicePanel off of the Toolbox and onto any form (how cool is that?). To set this behavior up, however, it’s best to separate the DicePanel into its own project.
Creating the DicePanel Project
The Class Library project is the type of project you should use when creating new controls to add to the Toolbox. If desired, you can put multiple classes inside the class library if it makes sense to do so (if the classes have similar or related functionality, for example). In this case, you’ll implement just one class in this library.Figure 2-1 shows how to create a new VB .NET Class Library solution. This is the project into which you should copy the entire DieStuff.vb contents from version 2 of the die-rolling program (the Die and PaintPanel classes). Once you copy the old code into the new project, the refactoring will begin once again.

Figure 2-1: Creating a Class Library solution in Visual Studio
Setting Up More Refactoring
The DicePanel class will be acting as the “manager” class to some number of individual dice, so it still makes sense to have a Die class encapsulate the functionality of a single die. However, you don’t ever need to expose this Die class to the outside world. Remember, good object-oriented programming dictates that objects should be responsible for themselves and rely on as little outside help as possible. To that end, the DicePanel class will completely encapsulate the functionality of the die initializing, rolling, drawing, and bouncing—meaning that the user of the DicePanel won’t ever be exposed to all of that code.Because the Die class is to be used only by the DicePanel class, you can structure the classes in this way:
Class DiceManager
<...stuff>
Private Class Die
<...stuff>
End Class
End Class
As you can see, the Die class is declared within the DicePanel class, and it’s declared private, meaning that the outside world can’t access it.
Moving Code
You’ve moved several things out of the Die class and into the DicePanel class in this final version. First, you declared and initialized the animated bitmaps in the DicePanel class. This means that only one copy of each bitmap is in memory, regardless of how many dice the panel will display. Also, you’ve moved the background bitmap (named bmBack) that serves as the destination for off-screen rendering into the DicePanel class.Furthermore, you’ve moved the Paint event, which used to be external to the PaintPanel class and on the main form of the game, inside the DicePanel class. This means the user of the class doesn’t need to do anything to get the panel to draw correctly. Listing 2-14 shows the Paint functionality.Listing 2.14: Painting and Resizing Functionality Inside of DicePanel
Protected Overrides Sub OnPaint(ByVal e As _
System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
'happens in design mode
If bmBack Is Nothing Then
Call SetupBackgroundAndDice()
End If
e.Graphics.DrawImageUnscaled(bmBack, 0, 0)
End Sub
Protected Overrides Sub OnResize(ByVal eventargs As System.EventArgs)
MyBase.OnResize(eventargs)
Call SetupBackgroundAndDice()
End Sub
Actually, I fibbed a bit when I said the Paint event moved into the class. In truth, the OnPaint method is what’s coded here. What’s the difference? The programmer of a class can’t code event-handling code for that class. If you’ll recall, events are notification routines called by the class to the outside world and aren’t part of the class itself. For example, the Click event of a button isn’t part of the button class; it’s the code that the user of the button runs whenever a button in his project is clicked.Most class events are raised from inside a method whose name corresponds to the event with the prefix On. Thus, the Paint method of a class is raised from a method named OnPaint. So, if you want code to execute when your control is painted, you need to override the OnPaint method of the class.Note how the first line of the OnPaint method shown in Listing 2-14 calls the OnPaint method in the base class. This is necessary so that events are raised properly. Without this call, users of the class would never receive a Paint event. The remainder of the OnPaint method performs the copy of the background bitmap to the surface of the panel, creating the background bitmap first if it hasn’t already been created.Listing 2-14 shows a second method—the OnResize method, which is called whenever the DicePanel changes size. If the panel changes size, then the size of the background bitmap must change to match; otherwise, it might not be large enough to render the panel correctly. Like the OnPaint method, the OnResize method also calls the base class method of the same name, ensuring that users of the class receive their events.
Examining the Die Class Changes
The Die class itself has several additions, but the original code hasn’t changed drastically. Listing 2-15 shows the interface for versions 2 and 3 of the Die class, side by side. The interface is a listing of all the members of a class without showing all of the declarations. (The private fields have been removed from this listing.) Once you’ve studied the two interfaces, you’ll learn about the major differences.
Listing 2.15: Interfaces of the Previous Version of the Die Class and the New One
Public Class Die (version 2)
Public Sub New(ByVal pn As PaintPanel)
Property Frame() As Integer
Property Result() As Integer
Public Sub InitializeLocation()
Public Sub UpdateDiePosition()
Public Sub InitializeRoll()
Public Sub DrawDie()
ReadOnly Property IsNotRolling() As Boolean
ReadOnly Property BackgroundPic() As Bitmap
End Class
Private Class Die (version 3)
Public Sub New(ByVal pn As DicePanel)
Private Property Frame() As Integer
Property Result() As Integer
Private Property xPos() As Integer
Private Property yPos() As Integer
Private Sub BounceX()
Private Sub BounceY()
Public Sub InitializeLocation()
Public Sub UpdateDiePosition()
Public Sub InitializeRoll()
Public Sub DrawDie(ByVal bDest As Bitmap)
ReadOnly Property IsNotRolling() As Boolean
ReadOnly Property IsRolling() As Boolean
ReadOnly Property Rect() As Rectangle
Public Function Overlapping(ByVal d As Die) As Boolean
Public Sub HandleCollision(ByVal d As Die)
Public Sub HandleBounceX(ByVal d As Die)
Public Sub HandleBounceY(ByVal d As Die)
End Class
The following are the major differences between the two versions:The Frame property is private in version 3. The Die class is 100-percent responsible for controlling what animated frame it’s on, which follows the rule that objects should be responsible for themselves. Declaring aproperty private makes certain that outside users of class have no ability to modify or use this member.The variables that control the location of the die, xPos and yPos, are now properties (still private, though). This allows for range checking whenever the property is modified.There are new (private) methods named BounceX and BounceY. These methods reverse the direction of xPos or yPos and call a method on the DicePanel that serves to raise an event named DieBounced. This allows the DicePanel to communicate to the outside world whenever a die bounces, allowing that program to react by playing a WAV file or performing some other custom action.The method DrawDie now takes a bitmap parameter, which serves as the bitmap into which the die should be drawn.The property BackgoundPic has been removed in version 3 because the background bitmap is now declared inside the DicePanel class.The method IsRolling, the opposite of the existing version 2 method IsNotRolling, is used for clarity (it’s clearer to write Do While d.IsRolling than it is to write Do While Not d.IsNotRolling).The property Rect and the methods Overlapping, HandleCollision, HandleBounceX, and HandleBounceY are new. These are all new members that handle collisions between multiple dice. The “Detecting Collisions” section covers collision handling.
Examining the DicePanel Functionality
Now that you’ve seen how the Dice class represents a single die, it’s time to see how the DicePanel class manages some number of them. How can a variable number of Die classes be stored? One common way in VB 6 to store a variable number of something is to use an array. There’s a class in the .NET Framework called the ArrayList that allows for the management of a variable number of classes. The DicePanel declares an ArrayList named aDice. When it’s time to create the Die instances and put them in the ArrayList, the GenerateDice method is called. Listing 2-16 shows this method, which is called whenever the Die class instances need to be created.
Listing 2.16: The Method GenerateDice
Private Sub GenerateDice()
Dim d As Die
Dim dOld As Die
Dim bDone As Boolean
Dim iTry As Integer
aDice = New ArrayList()
Do While aDice.Count < NumDice
d = New Die(Me)
iTry = 0
Do
iTry += 1
bDone = True
d.InitializeLocation()
For Each dOld In aDice
If d.Overlapping(dOld) Then
bDone = False
End If
Next
Loop Until bDone Or iTry > 1000
aDice.Add(d)
Loop
End Sub
This method sets up a loop that runs for a number of iterations equal to the value of the property NumDice. In each iteration, it creates a new Die instance and stores it (temporarily) in the variable d. The program then places this Die instance into its initial location. Once placed, the die location is compared to all previously created dice to see if their locations overlap. If the program finds an overlap, it initializes the current die location again. This happens repeatedly until a “free” location is found or until the location setting is attempted 1,000 times for this die, at which point the loop terminates. The 1,000 upper limit is required because the possibility exists that the panel is simply too small to hold unique locations for the number of dice in the NumDice property. If this was true and the upper bound wasn’t present, attempting to put the dice in a unique location would result in an infinite loop.
Let’s look at the NumDice property in Listing 2-17.Listing 2.17: Controlling the Number of Dice on the Panel via the NumDice Property
Private FNumDice As Integer = 2
Property NumDice() As Integer
Get
Return FNumDice
End Get
Set(ByVal Value As Integer)
FNumDice = Value
'regen dice, but only if done once before, or else dbl init
If DiceGenerated() Then
Dim d As Die
GenerateDice()
Clear()
For Each d In aDice
d.DrawDie(bmBack)
Next
Me.Invalidate()
End If
End Set
End Property
This looks like many of the other property examples you’ve seen so far. Aprivate variable, FNumDice, stores the value of the property. When the property changes, the GenerateDice method is called and then each die is drawn onto the panel. Putting the generation/redrawing code here allows dice to be generated and drawn at design time, something you’ll see once you place the control into the Toolbox. Listing 2-18 shows the constructor of the DicePanel.Listing 2.18: The DicePanel Constructor
Public Sub New()
MyBase.New()
Me.SetStyle(ControlStyles.UserPaint, True)
Me.SetStyle(ControlStyles.DoubleBuffer, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
Me.BackColor = Color.Black
Dim a As Reflection.Assembly = System.Reflection.Assembly.GetExecutingAssembly()
FbmxRot = New Bitmap(a.GetManifestResourceStream("DicePanel.dicexrot.bmp"))
FbmyRot = New Bitmap(a.GetManifestResourceStream("DicePanel.diceyrot.bmp"))
FbmStop = New Bitmap(a.GetManifestResourceStream("DicePanel.dicedone.bmp"))
'NEW. this used to be a major pain in VB6
FbmxRot.MakeTransparent(Color.Black)
FbmyRot.MakeTransparent(Color.Black)
FbmStop.MakeTransparent(Color.Black)
End Sub
You’ve seen much of this code before. The SetStyle commands are necessary to control flicker. The animation bitmaps are loaded dynamically using the GetManifestResourceStream method of the assembly class. Note that the name-space name has changed from the program name to DicePanel, which is declared at the top of this unit. The last three lines convert the color black in the three bitmaps to a transparent color, which means the black isn’t drawn. This is necessary when two dice are close to each other or else the black border of one die would overlap another, as shown in Figure 2-2. (Making a color transparent in VB 6 requires a series of API calls.)

Figure 2-2: Making the black transparent
There are several “mini-members” that perform some simple tasks in Listing 2-19. The Result property, a read-only property, returns the result of all the dice in the panel by iterating through the ArrayList and adding up the Result values of each die. The AllDiceStopped property is used when rolling the dice to determine when all the dice have stopped moving. Finally, the sub OnDieBounced is a protected method that raises the DieBounced event so that the user of the class can react or perform some custom task whenever a die bounces off of an edge or off of another die. Listing 2-19 shows these three members.
Listing 2.19: Some Miscellaneous Tasks Implemented in the DicePanel
ReadOnly Property Result() As Integer
Get
Dim d As Die
Dim i As Integer = 0
For Each d In aDice
i += d.Result
Next
Return i
End Get
End Property
Private ReadOnly Property AllDiceStopped() As Boolean
Get
Dim d As Die
Dim r As Boolean
r = True
For Each d In aDice
If d.IsRolling Then
r = False
End If
Next
Return r
End Get
End Property
Protected Sub OnDieBounced()
RaiseEvent DieBounced()
End Sub
Note | Chapter 1, “Developing Your First Game,” covered public and private members. You can see a protected member in the current class and any subclass of the current class, but it’s unavailable (as if it was private) to everyone outside of the class. |
Detecting Collisions
The last two major topics to discuss are the RollDice method and the members that deal with collision detection in both the Die class and the DicePanel class. This section discusses all the collision detection code because this functionality is new to this version of the die program.You can handle collision detection in a two-dimensional world in several different ways. In this case, you want the dice to “bounce,” or change direction, when they collide either with one of the walls of the panel or with each other. The wall bouncing code has been in place since the first version of the program, so all that remains is the code to check for the dice colliding with each other.This code can get a little tricky. The challenge is to avoid two dice getting “stuck together.” For example, if two dice overlap, you want to change the direction of one or both so they move away from each other.I tried a few different methods with varying results and finally settled on this basic case-by-case scenario: I decided that if two dice collided, they would either bounce in the x direction or in the y direction, but not both. The direction I chose depended on the position of the dice. If the two dice are closer together in height (along y) than in width (along x), this means they’re arranged in more of a horizontal fashion, as opposed to a vertical arrangement. In that case, I decided it would look more natural to have them bounce in the x direction. Similarly, if the dice are closer together in width, then they’re arranged in a more vertical orientation, so it looks more natural to have them bounce in the y direction.Figure 2-3 illustrates the two orientations. You can see two pairs of dice: one pair in a horizontal arrangement and the other pair in a vertical arrangement.

Figure 2-3: Deciding on which way to bounce dice that have just collided
You can determine which of these two orientations to use by comparing the x and y positions of the two dice. As shown in Figure 2-3, if the dice are next to each other, then the difference between x positions will be greater than the difference between y positions. If the dice are one over the other, then the y difference is greater.
Once you’ve determined if the dice are in the horizontal or vertical orientation, you can decide how they collide. There are three possible outcomes for each orientation. Table 2-2 shows the six possibilities and the action the dice will take.
CASE | DESCRIPTION | RESULT |
---|---|---|
A | The left and right dice run into each other. | Bounce both dice along x. |
B | The leftmost die catches up to rightmost die. | Bounce leftmost die along x. |
C | The rightmost die catches up to leftmost die. | Bounce rightmost die along x. |
D | The top and bottom dice run into each other. | Bounce both dice along y. |
E | The topmost die catches up to the bottommost die. | Bounce topmost die along y. |
F | The bottommost die catches up to the topmost die. | Bounce bottommost die along y. |
Figure 2-4 illustrates the six cases. (The letter in Table 2-2 matches the letters in the illustration.)

Figure 2-4: The six die collision cases
This solution works well because it automatically prevents the two dice from getting stuck together and bouncing back and forth repeatedly while inside each other’s boundaries. The reason this is so is that the six cases aren’t all of the possible cases or orientations of dice. For example, it doesn’t include the case when the dice in the horizontal orientation are moving away from one another. So, if two dice moving toward each other collide and both bounce away from each other, the next frame won’t handle the collision because the two dice are no longer in one of the six cases shown in Table 2-2. In my first attempts at collision detection I had trouble taking into account dice that were close together or overlapping but were moving away from each other.Now that you’ve learned about the algorithm in plain English, you’ll see it in VB code. Listing 2-20 shows the HandleCollisions method on the DicePanel class.Listing 2.20: The HandleCollisions Method
Private Sub HandleCollisions()
Dim di As Die
Dim dj As Die
Dim i As Integer
Dim j As Integer
If NumDice = 1 Then Exit Sub
'can't use foreach loops here,
'want to start j loop index AFTER first loop
For i = 0 To aDice.Count - 2
For j = i + 1 To aDice.Count - 1
di = aDice.Item(i)
dj = aDice.Item(j)
di.HandleCollision(dj)
Next
Next
End Sub
This routine is nothing special—it’s simply a nested loop that handles every pair of dice in the aDice ArrayList. One interesting note is that it doesn’t use the For..Each enumerator of the ArrayList because neither of the two loops actually iterates through every item in the list. The first loop starts at the beginning and stops one short of the end, and the second loop starts one more than the beginning of the first loop and goes to the end. Using a For..Each construct for the two loops would result in each pair of dice being checked for collisions twice— clearly an undesired effect.The “meat” of the collision-handling algorithm happens in the Die class itself in the method HandleCollision (yet another example of making an object responsible for itself). Listing 2-21 shows that method, along with the Overlapping method and the Rect property.
Listing 2.21: Collision-Handling Members on the Die Class
Public Sub HandleCollision(ByVal d As Die)
If Me.Overlapping(d) Then
If Abs(d.yPos - Me.yPos) <= Abs(d.xPos - Me.xPos) Then
HandleBounceX(d)
Else
HandleBounceY(d)
End If
End If
End Sub
ReadOnly Property Rect() As Rectangle
Get
Return New Rectangle(xPos, yPos, w, h)
End Get
End Property
Public Function Overlapping(ByVal d As Die) As Boolean
Return d.Rect.IntersectsWith(Me.Rect)
End Function
The Rect and Overlapping members use features already built into the .NET Framework, so the code within them is pretty short. Rect simply returns an instance of the .NET Framework Rectangle class, using the current position, and the constant size of the Die object in question. The Overlapping method leverages the use of the built-in Rectangle.IntersectsWith method.The HandleCollision method takes a Die instance as a parameter, which might seem odd at first because this method is in the Die class itself. By passing a Die instance into a method on the Die class, you have two Die instances at your disposal—the parameter instance (named d) and the current instance that this code is running for (named Me).The first line of the HandleCollision method checks to see if the two dice are overlapping. If they aren’t overlapping, then of course there’s no need to make them bounce off each other. If they’re overlapping, however, the next comparison determines whether the two dice are in the horizontal arrangement or the vertical arrangement. (Refer to Figure 2-3 to compare the If statement with the illustration of this comparison.) Once the orientation is determined, one of two private methods named HandleBounceX (horizontal arrangement) or HandleBounceY (vertical arrangement) are called.Listing 2-22 shows the HandleBounceX method (HandleBounceY is similar). This method creates new die variables named dLeft and dRight and assigns them to the two die (d and Me) for readability (the leftmost die being assigned to dLeft, and the rightmost to dRight, of course). Then, the three cases are checked and acted upon as discussed earlier. If the leftmost die and rightmost die are moving toward each other, both of them are bounced. If instead the dice are moving in the same direction and one has caught the other, then only the faster-moving die is bounced). Other cases (such as the two dice moving away from each other) result in no change in direction.Listing 2.22: HandleBounceX
Private Sub HandleBounceX(ByVal d As Die)
Dim dLeft As Die
Dim dRight As Die
If Me.xPos < d.xPos Then
dLeft = Me
dRight = d
Else
dLeft = d
dRight = Me
End If
'moving toward each other
If dLeft.dxDir > 0 And dRight.dxDir < 0 Then
Me.BounceX()
d.BounceX()
Exit Sub
End If
'moving right, left one caught up to right one
If dLeft.dxDir > 0 And dRight.dxDir > 0 Then
dLeft.BounceX()
Exit Sub
End If
'moving left, right one caught up to left one
If dLeft.dxDir < 0 And dRight.dxDir < 0 Then
dRight.BounceX()
End If
End Sub
Testing the Collisions
The collision code might all seem logical enough when being explained in a book, but coming up with this collision system, as mentioned earlier, took a bit of trial and error. Before clearly defining the algorithm outlined previously, I kept getting in situations where the dice would get stuck together and keep bouncing back and forth while inside each other.While debugging, it was difficult to see exactly which dice were touching (I was using a big test form with the NumDice property cranked up to 7 so that collisions were happening often, as opposed to having only two dice that didn’t collide as frequently). I decided it would be useful to be able to see the bounding box of each die, along with a line indicating what direction the die was traveling. This could help me truly see when two dice were overlapping and in what direction they were moving.You can accomplish this by adding a new property to the DicePanel named DebugDrawMode. This is a simple Boolean property that does nothing fancy or tricky inside its Get or Set members:
Private FDebugDrawMode As Boolean = False
Property DebugDrawMode() As Boolean
Get
Return FDebugDrawMode
End Get
Set(ByVal Value As Boolean)
FDebugDrawMode = Value
End Set
End Property
Tip | Even though I could have used a field instead of a property on this member, I always choose to spend the extra 10 seconds and set up the property using the Get and Set methods in case I want to attach code for reading/writing to the property later. |
Once set up, all you have to do is add a few short lines to the DrawDie method to add the rectangle and direction line. Listing 2-23 shows the new code in the DrawDie method to show a bounding box and directional line, only if the DebugDrawMode property on the panel is set to True.
Listing 2.23: New Code in the DrawDie Method
gr = Graphics.FromImage(bDest)
Try
gr.DrawImage(b, xPos, yPos, r, GraphicsUnit.Pixel)
If FPanel.DebugDrawMode Then
Dim p As New Pen(Color.Yellow)
Dim xc, yc As Single
xc = xPos + (w\2)
yc = yPos + (h\2)
gr.DrawRectangle(p, Me.Rect)
gr.DrawLine(p, xc, yc, xc + Sign(dxDir) * (w\2), yc + Sign(dyDir) * (h\2))
End If
Finally
gr.Dispose()
End Try
You can see the new If statement right after the DrawImage method draws the animated die bitmap onto the Graphics instance. This code uses the Pen class in the .NET Framework to set up a yellow pen (you could even get fancy and make the color of the debug drawing configurable). Then, two new methods on the Graphics class are called. The DrawRectangle method draws a rectangle in the location specified. You already have a method named Rect on the Die class that returns a Rectangle instance, so you can simply pass the result of that method into the DrawRectangle method, along with the Pen instance.Drawing the line takes a bit of math because you want to start at the center of the die and draw out to one of the corners. Once you determine the endpoints of the line, you can simply call the DrawLine method on the Graphics class, passing in these endpoints and the yellow Pen instance as parameters. The result of these few lines of code, shown in Figure 2-5, is instrumental in helping you debug the collision-handling code.

Figure 2-5: Dice drawn with extra lines to denote bounding boxes and direction