Chapter 5. Bitmaps: Our First Building Block

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:

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 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.

Loading a Bitmap File

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.

Definition

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.

Hint

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 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.

Drawing an image loaded from a bitmap file.

Figure 5.1. Drawing an image loaded from a bitmap file.

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.

Drawing a scaled bitmap.

Figure 5.2. Drawing a scaled bitmap.

device.DrawImage(planet, 400, 10, 64, 64)

Rotating and Flipping a Bitmap

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:

Rotating and flipping a bitmap.

Figure 5.3. Rotating and flipping a bitmap.

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 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.

Modifying the color value of pixels in a bitmap.

Figure 5.4. Modifying the color value of pixels in a bitmap.

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

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 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.

Hint

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.

Game Class Constructor

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.

Definition

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

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 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.

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 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.

Level Up!

That wraps up our basic bitmap loading, manipulating, and drawing needs. Most importantly, we have now learned enough about 2D graphics programming to begin working with sprites in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.147.131