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 Graphics
class, 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:
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 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, a Bitmap
object, 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 a PictureBox
control 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.
We can load a bitmap in Basic by using the Bitmap
class. But there is no Bitmap. 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 like Form1_Load
where we have more control over the result.
A constructor is 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. A destructor is a class function that runs when the object is being destroyed: via object.Dispose()
or object = 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 crash if 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 a Bitmap
if the file exists or Nothing (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.
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!
There are several versions of the Graphics.DrawImage()
function; the alternate versions are called overloaded functions in “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.
We can optionally use a Point
with the X and Y coordinates combined into one object, or use floating-point Single
variables. There are also scaling 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)
The Bitmap
class has some helper functions for manipulating the image and even its individual pixels. The Bitmap.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)
The RotateFlipType
options are:
Rotate180FlipNone
Rotate180FlipX
Rotate180FlipXY
Rotate180FlipY
Rotate270FlipNone
Rotate270FlipX
Rotate270FlipXY
Rotate270FlipY
Rotate90FlipNone
Rotate90FlipX
Rotate90FlipXY
Rotate90FlipY
RotateNoneFlipX
RotateNoneFlipXY
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
We can also examine and modify the pixel buffer of a bitmap directly using functions in the Bitmap
class. The Bitmap.GetPixel()
function retrieves the pixel of a bitmap at given X,Y coordinates, returning it as a Color
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
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 the Game
class. Then, we’ll copy this Game.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
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 are private variables 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.
A class is a blueprint written in source code for how an object should 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.
The constructor is the first function that runs when a class is instantiated into an object. We can add parameters to the constructor in order to send information to the object at runtime—important things like the Form, or maybe a filename, or whatever you want.
Instantiation is 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, the destructor function runs. These functions are defined in the class.
Here is the constructor for the Game
class. 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 the Graphics
and Bitmap
objects needed for rendering onto a PictureBox
. 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 constructor 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 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
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
We probably will not need an Update()
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 the Device
property, 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 its Get and Set members. Since I did not want anyone to modify the p_device
variable from outside the class, I have made the property read-only via the ReadOnly
keyword—and as a result, there is no Set member, just a Get 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 value
is in a valid range before allowing the change—so it’s like a variable with benefits.
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 the LoadBitmap()
function into the Game
class 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 does not have 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.
3.15.147.131