In Chapter 4, we learned how to tap into the Graphics class in the .NET Framework, which gives access to GDIỵ graphics drawing capabilities above and beyond the forms and controls in Visual Basic. By using the Bitmap class and a PictureBox, we are able to create a rendering surface in code and draw onto it. Now that we have learned the basics of drawing with the Graphicsclass, we can begin to abstract away the“Visual”part of Visual Basic and focus on just a source code approach to game programming, and consider the Form—once the main focus of the program—as just another asset, like a bitmap or audio file.
Here’s what we’ll cover in this chapter:
n Loading a bitmap file
n Drawing a bitmap
n Rotating and flipping a bitmap
n Accessing bitmap pixels
n Creating a Game class
Dissecting Bitmaps
Learning to draw a bitmap is the first step toward creating a 2D game like Celtic Crusader. When we have the ability to draw just one bitmap, then we can extend that to animation by drawing one frame after another in a timed sequence—and
Chapter 5
99
presto, sprite animation becomes a reality! We will focus on sprite animation in Chapter 5, and work on the basics of bitmap drawing now as a prerequisite.
Drawing on the code we learned about in the preceding chapter, aBitmapobject, a PictureBox, and a Graphics object work in tandem to represent a rendering device capable of drawing vector shapes and—as we will see next—bitmaps.
Once again for reference, we have to declare the two variables:
Public surface As Bitmap Public device As Graphics
and then, assuming we have aPictureBoxcontrol called PictureBox1, create the objects. The PictureBox control can be created at runtime (as we saw last chapter), but I’ve added it to the form manually this time.
surface = New Bitmap(Me.Size.Width, Me.Size.Height) PictureBox1.Image = surface
device = Graphics.FromImage(surface)
So, we already knew this startup code, but—just to lay the groundwork—this is what is needed up front as a rendering device to draw a bitmap.
Loading a Bitmap File
We can load a bitmap in Basic by using theBitmapclass. But there is noBitmap.
Load() function (unfortunately!) so we have to use the constructor instead by passing the bitmap filename when the object is created.
Public bmp As Bitmap
bmp = New Bitmap("image.bmp")
Interestingly enough, in Basic we can create the object with this shorthand code:
Public bmp as New Bitmap("image.bmp")
The reason why I advise against doing this is because it breaks our ability to trap errors, and it is bad practice to create an object at the same time it is defined—
better to create it inside a function likeForm1_Load where we have more control over the result.
D e f i n i t i o n
Aconstructoris a class function (also called a method) that runs when an object is first created.
This is where class variables (also called properties) are initialized. Adestructoris a class function that runs when the object is being destroyed: viaobject.Dispose()orobject = Nothing.
Although both approaches work, and we can even pass a string rather than hard coding the filename, there is the very serious problem of error handling: if the file does not exist, an exception error will crash the program. Missing files are fairly common (usually due to their being in the wrong folder), and we want to display a friendly error message rather than allow the program to crash. The solution is to wrap the Bitmap loading code in a try. . .catch block. Here is an example:
Try
bmp = New Bitmap(filename) Catch ex As Exception
MsgBox("Error loading file") End Try
This code will not crashif the file is missing or if some other error occurs while reading the file. So, let’s put it into a reusable function that returns aBitmap if the file exists orNothing(null) if it fails. One caveat: be sure to free memory used by the Bitmap when the program ends.
Public Function LoadBitmap(ByVal filename As String) Dim bmp As Bitmap
Try
bmp = New Bitmap(filename) Catch ex As Exception
bmp = Nothing End Try
Return bmp End Function
If the file does not exist, then LoadBitmap() will return Nothing as the object pointer rather than crashing with an exception error. This is a very handy little function! And it demonstrates the power of code reuse and customization— whatever features we need that are not already in an SDK or library we can just write ourselves. One might even go so far as to write their own new Bitmap wrapper class (called something like CBitmap?) with a Load() function. You could easily do this yourself with just the small amount of code we have used so far.
Dissecting Bitmaps 101
H i n t
To ensure that created objects are properly disposed of when the program ends, I recommend putting the Form1_FormClosed() function at the top of the source code, just below the variable declarations, where it will be quick and easy to write the code needed to free an object.
Always write creation/deletion code together in pairs to avoid memory leaks!
Drawing a Bitmap
There are several versions of the Graphics.DrawImage() function; the alternate versions are calledoverloaded functionsin“OOP speak.”The simplest version of the function calls for just a Bitmap or Image parameter and then the X and Y position. For example, this line
device.DrawImage( bmp, 10, 10 )
will draw the bitmap bmp at pixel coordinates 10,10. Figure 5.1 shows an example.
Figure 5.1
Drawing an image loaded from a bitmap file.
We can optionally use aPointwith the X and Y coordinates combined into one object, or use floating-pointSinglevariables. There are alsoscaling features that make it possible to resize the image. By passing additional width and height parameters, we can define a new target size for the image. Figure 5.2 shows another example with the addition of this line, which draws another copy of the planet bitmap scaled down to a smaller size.
device.DrawImage(planet, 400, 10, 64, 64)
Rotating and Flipping a Bitmap
TheBitmapclass has some helper functions for manipulating the image and even its individual pixels. TheBitmap.RotateFlip()function will rotate a bitmap in 90- degree increments (90, 180, and 270 degrees), as well as flip the bitmap vertically, horizontally, or both. Here is an example that rotates the bitmap 90 degrees:
planet.RotateFlip(RotateFlipType.Rotate90FlipNone) Figure 5.2
Drawing a scaled bitmap.
Dissecting Bitmaps 103
The RotateFlipType options are:
n Rotate180FlipNone
n Rotate180FlipX
n Rotate180FlipXY
n Rotate180FlipY
n Rotate270FlipNone
n Rotate270FlipX
n Rotate270FlipXY
n Rotate270FlipY
n Rotate90FlipNone
n Rotate90FlipX
n Rotate90FlipXY
n Rotate90FlipY
n RotateNoneFlipX
n RotateNoneFlipXY
n RotateNoneFlipY
The Bitmap Drawing Demo has several buttons on the form to let you explore rotating and flipping a bitmap in various ways, as you can see in Figure 5.3. In addition to calling RotateFlip(), we still need to draw the image again and refresh the PictureBox like usual:
planet.RotateFlip(RotateFlipType.Rotate180FlipNone) device.DrawImage(planet, 10, 10)
PictureBox1.Image = surface
Accessing Bitmap Pixels
We can also examine and modify the pixel buffer of a bitmap directly using functions in theBitmapclass. TheBitmap.GetPixel()function retrieves the pixel of a bitmap at given X,Y coordinates, returning it as aColor variable. Likewise,
the Bitmap.SetPixel()will change the color of a pixel at the given coordinates.
The following example reads every pixel in the planet bitmap and changes it to green by setting the red and blue components of the color to zero, which leaves just the green color remaining. Figure 5.4 shows the Bitmap Drawing Demo with the pixels modified—not very interesting but it does a good job of showing what you can do with this capability.
For x = 0 To planet.Width - 1 For y = 0 To planet.Height - 1
Dim pixelColor As Color = planet.GetPixel(x, y)
Dim newColor As Color = Color.FromArgb(0, pixelColor.G, 0) planet.SetPixel(x, y, newColor)
Next Next Figure 5.3
Rotating and flipping a bitmap.
Dissecting Bitmaps 105
Creating a Game Class
We have enough code now at this point to begin constructing a game framework for our future Basic projects. The purpose of a framework is to take care of repeating code. Any variables and functions that are needed regularly can be moved into a Game class as properties and methods where they will be both convenient and easily accessible. First, we’ll create a new source code file called
Game.vb, which will contain the source code for theGame class. Then, we’ll copy thisGame.vb file into the folder of any new project we create and add it to that project. Let’s get started:
Public Class Game
Private p_device As Graphics Private p_surface As Bitmap Private p_pb As PictureBox Private p_frm As Form Figure 5.4
Modifying the color value of pixels in a bitmap.
You might recognize the first three of these variables (oops—I mean, class properties) from previous examples. They have a p_ in front of their names so it’s easy to tell at a glance that they areprivatevariables in the class (as opposed to, say, parameters in a function). The fourth property, p_frm, is a reference to the main Form of a project, which will be set when the object is created.
H i n t
Aclassis a blueprint written in source code for how anobjectshould behave at runtime. Just as an object does not exist at compile time (i.e., when we’re editing source code and building the project), a class does not exist during runtime. An object is created out of the class blueprint.
Game Class Constructor
The constructor is the first function that runs when a class isinstantiatedinto an object. We can add parameters to the constructor in order to send information to the object at runtime—important things like theForm, or maybe a filename, or whatever you want.
D e f i n i t i o n
Instantiationis the process of creating an object out of the blueprint specified in a class. When this happens, an object is created and the constructor function runs. Likewise, when the object is destroyed, thedestructor functionruns. These functions are defined in the class.
Here is the constructor for the Gameclass. This is just a starting point, as more code will be added in time. As you can see, this is not new code, it’s just the code we’ve seen before to create theGraphicsandBitmapobjects needed for rendering onto aPictureBox. Which, by the way, is created at runtime by this function and set to fill the entire form (Dock = DockStyle.Fill). To clarify what these objects are used for, the Graphics variable is called p_device—while not technically correct, it conveys the purpose adequately. To help illustrate when the con- structor runs, a temporary message box pops up which you are welcome to remove after you get what it’s doing.
Public Sub New(ByRef form As Form, ByVal width As Integer, ByVal height As Integer) MsgBox("Game class constructor")
REM set form properties p_frm = form
Creating a Game Class 107
p_frm.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedSingle p_frm.MaximizeBox = False
p_frm.Size = New Point(width, height) REM create a picturebox
p_pb = New PictureBox() p_pb.Parent = p_frm
p_pb.Dock = DockStyle.Fill p_pb.BackColor = Color.Black REM create graphics device
p_surface = New Bitmap(p_frm.Size.Width, p_frm.Size.Height) p_pb.Image = p_surface
p_device = Graphics.FromImage(p_surface) End Sub
Game Class Destructor
The destructor function is called automatically when the object is about to be deleted from memory (i.e., destroyed). In Basic, or, more specifically, in .NET, the name of the destructor is Sub Finalize(). The Protected Overrides part is very important: this allows any subclass (via inheritance—a key OOP feature) to also free memory used by its parent. There is also a message box that pops up from this function to illustrate when the object is being destroyed, and you may remove the MsgBox() function call if you wish.
Protected Overrides Sub Finalize() MsgBox("Game class destructor") REM free memory
p_device.Dispose() p_surface.Dispose() p_pb.Dispose() End Sub
Game Updates
We probably will not need anUpdate() function at this early stage but it’s here as an option should you wish to use it to update the PictureBox any time drawing occurs on the“device.” In due time, this function will be expanded to do quite a bit more than its meager one line of code currently shows. Also shown here is a Property called Device. A Property allows us to write code that looks
like just a simple class property is being used (like p_device), when in fact a function call occurs.
Public Sub Update()
REM refresh the drawing surface p_pb.Image = p_surface
End Sub
Public ReadOnly Property Device() As Graphics Get
Return p_device End Get
End Property End Class
So, for example, if we want to get the value returned by theDeviceproperty, we can do that like so:
Dim G as Graphics = game.Device
Note that I did not include parentheses at the end of Device. That’s because it is not treated as a function, even though we are able to do something with the data before returning it. The key to a property is itsGetandSetmembers. Since I did not want anyone to modify the p_device variable from outside the class, I have made the property read-only via theReadOnlykeyword—and as a result, there is no Set member, just aGet member. If I did want to make p_device writable, I would use a Set member that looks something like this:
Public ReadOnly Property Device() As Graphics Get
Return p_device End Get
Set(ByVal value As Graphics) p_device = value
End Set End Property
Properties are really helpful because they allow us to protect data in the class!
Besides using ReadOnly, you can prevent changes to a variable by making sure
valueis in a valid range before allowing the change—so it’s like a variable with benefits.
Creating a Game Class 109
Framework Demo
The code in this Framework Demo program produces pretty much the same output as what we’ve seen earlier in the chapter (drawing a purple planet). The difference is, thanks to the new Game class, the source code is much, much shorter! Take a look.
Public Class Form1 Public game As Game Public planet As Bitmap
Private Sub Form1_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load REM set up the form
Me.Text = "Bitmap Drawing Demo"
REM create game object game = New Game(Me, 600, 500) REM load bitmap
planet = LoadBitmap("planet.bmp") If planet Is Nothing Then
MsgBox("Error loading planet.bmp") End
End If
REM draw the bitmap
game.Device.DrawImage(planet, 10, 10)
game.Device.DrawImage(planet, 400, 10, 100, 100) End Sub
Public Function LoadBitmap(ByVal filename As String) Dim bmp As Bitmap
Try
bmp = New Bitmap(filename) Catch ex As Exception
bmp = Nothing End Try
Return bmp End Function
Private Sub Form1_FormClosed(ByVal sender As Object, _ ByVal e As System.Windows.Forms.FormClosedEventArgs) _ Handles Me.FormClosed
REM delete game object game = Nothing
planet = Nothing End Sub
End Class
If we had moved theLoadBitmap()function into theGameclass or into some new class for handling bitmaps, then this code would have been even shorter. That’s a good thing—eliminating any reusable source code by moving it into a support file is like reducing a mathematical formula, rendering the new formula more powerful than it was before. Any code that doesnothave to be written increases your productivity as a programmer. So, look for every opportunity to cleanly and effectively recycle code, but don’t reduce just for the sake of code reuse—
make sure you keep variables and functions together that belong together and don’t mish-mash them all together.
Level Up!
That wraps up our basic bitmap loading, manipulating, and drawing needs.
Most importantly, we have now learned enough about 2D graphics program- ming to begin working with sprites in the next chapter.
Level Up! 111
Sprites and Real-Time