Chapter 2. Drawing Shapes and Bitmaps with GDI+

We are now on the verge of getting started on the Dungeon Crawler game, which is the focus of most of this book! The first chapter should have brought you up to speed on the goals for the game, while this chapter will explain how we can merge graphics with forms and controls. We will begin studying the graphics capabilities of the .NET Framework that will make it possible to build a complex game. You will learn how to detach controls from the “Form Designer” and just create them at runtime. Although future chapters will continue to need forms and controls, the graphics code will not be dependent on controls such as PictureBox. The .NET Framework has abstracted classes around the Windows Graphics Device Interface (GDI) so that we can create drawing surfaces and render shapes onto them using classes such as Graphics and Bitmap in conjunction with a PictureBox control. We will just create what is needed at runtime.

Here’s what is covered in this chapter:

  • Drawing lines

  • Drawing rectangles

  • Drawing text

  • Loading a bitmap

  • Drawing a bitmap

  • Rotating and flipping a bitmap

  • Accessing bitmap pixels

  • Creating a reusable framework

Drawing Lines

Lines and other vector shapes may not be very exciting but we are going to use line drawing as a starting point for learning about graphics programming with the .NET Framework and GDI+. The graphics code we’ll cover produces the result shown in Figure 2.1.

Drawing lines with managed GDI+ objects.

Figure 2.1. Drawing lines with managed GDI+ objects.

PictureBox Is Our Friend

For our purposes in this chapter, we will just look at the features specific to 2D graphics programming using the Image property of a PictureBox control. The PictureBox can be added to a form manually, but it’s easier to use a global PictureBox control and just create it at runtime in the Form1_Load function. In fact, we will just configure the form in code as well so that no manual property editing is needed. Any property you see in the Properties window of the Form Designer can be modified in code—and it’s easier to do that in code. So, in the globals section of public partial class Form1, let’s add some variables, including a PictureBox control:

PictureBox pb;
Timer timer;
Random rand;

In Form1_Load, we will create this new PictureBox and add it to the form. The Parent property is used to attach the control to Form1 (referred to with the this keyword—which refers to the current Form). DockStyle.Fill causes the PictureBox to fill the entire form, so that we can set the size of the form and the PictureBox will resize with it.

pb = new PictureBox();
pb.Parent = this;
pb.Dock = DockStyle.Fill;
pb.BackColor = Color.Black;

While we’re working in Form1_Load, let’s just go ahead and set the form’s settings. Again, this is being done in code while it could also be done using the Properties window in the Form Designer.

//set up the form
this.Text = "Line Drawing Demo";
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Size = new Size(600, 500);
//create random generator
rand = new Random();

Surfaces and Devices

Back in the globals sections at the top of the code, we need two new objects: a Bitmap and a Graphics object.

Bitmap surface;
Graphics device;

The Bitmap represents a drawing surface and is really just a pointer to the data in memory. After drawing something using the Graphics object (onto a PictureBox.Image), we then set the Bitmap variable (which is a pointer) equal to the PictureBox.Image, and that Bitmap can then be treated as an independent surface—which can be copied elsewhere, saved to a file, and other things. The Bitmap should be created with the same dimensions as the PictureBox control. This code goes in Form1_Load:

//create graphics device
surface = new Bitmap(this.Size.Width, this.Size.Height);
pb.Image = surface;
device = Graphics.FromImage(surface);

There are quite a few versions of the Graphics.DrawLine() function with various parameter variations that use Points, float- and int-based X,Y coordinates, and drawing modes. The version I use will use a Pen defined with the desired color and line width. The drawLine() function creates a pen with a random color and random line size, and two random points for the line ends that fit inside the dimensions of the form. After calling DrawLine(), then the PictureBox.Image is refreshed.

public void drawLine()
{
    //make a random color
    int A = rand.Next(0, 255);
    int R = rand.Next(0, 255);
    int G = rand.Next(0, 255);
    int B = rand.Next(0, 255);
    Color color = Color.FromArgb(A, R, G, B);
    //make pen out of color
    int width = rand.Next(2, 8);
    Pen pen = new Pen(color, width);
    //random line ends
    int x1 = rand.Next(1, this.Size.Width);
    int y1 = rand.Next(1, this.Size.Height);
    int x2 = rand.Next(1, this.Size.Width);
    int y2 = rand.Next(1, this.Size.Height);
    //draw the line
    device.DrawLine(pen, x1, y1, x2, y2);
    //refresh the drawing surface
    pb.Image = surface;
}

4D Programming with a Timer

We can even create a Timer in code without using the Form Designer. There is just one extra step to take and then the new Timer will work like usual—when setting its properties, we have to create an event handler.

Timer timer;

The Timer object is created in Form1_Load:

//set up the timer
timer = new Timer();
timer.Interval = 20;
timer.Enabled = true;
timer.Tick += new System.EventHandler(TimerTick);

When a new event handler is created, then it becomes “visible” to the event handler system in Visual C#, and can be used as an event trigger even when we write the function ourselves (rather than having Visual C# generate it for us). In this example, I want the drawLine() function to run every 20 milliseconds, which is 50 frames per second (50 Hz).

public void TimerTick (object source, EventArgs e)
{
    drawLine();
}

One final point: we should free memory after we finish with objects created in our programs. Visual C# (or, more specifically, the runtime) will free objects automatically in most cases, but it’s a good programming habit to free memory that you use. This is best done in the Form1_FormClosed event. At a certain point, it becomes too difficult to manage everything in code; some events like this one are best left up to the event handler in the Form Properties. To bring up the Events, open the Form1.cs in design view, then in the Properties window click the Events button (which looks like a lightning bolt). You will see all of the events, as shown in Figure 2.2. I wouldn’t say that freeing memory is crucial, but it’s a good idea.

The list of Form events.

Figure 2.2. The list of Form events.

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    device.Dispose();
    surface.Dispose();
    timer.Dispose();
}

Drawing Rectangles

Once we have the framework in place to draw lines, there are many other vector shapes that can be drawn with only a few minor changes in the code. One such shape is a rectangle, which we will look at next. Figure 2.3 shows the result.

Drawing rectangles with GDI+.

Figure 2.3. Drawing rectangles with GDI+.

For reference, we’ll go over the entire code listing (which is still quite short). First up are the global variables, Form1_Load, which initializes the program, and Form1_FormClosed, which frees memory.

using System;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
public partial class Form1 : Form
{
    PictureBox pb;
    Timer timer;
    Bitmap surface;
Graphics device;
Random rand;

public Form1()
{
    InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
    //set up the form
    this.Text = "Rectangle Drawing Demo";
    this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
        FixedSingle;
    this.MaximizeBox = false;
    this.Size = new Size(600, 500);
    //create a new picturebox
    pb = new PictureBox();
    pb.Parent = this;
    pb.Dock = DockStyle.Fill;
    pb.BackColor = Color.Black;
    //create graphics device
    surface = new Bitmap(this.Size.Width, this.Size.Height);
    pb.Image = surface;
    device = Graphics.FromImage(surface);
    //create random generator
    rand = new Random();
    //set up the timer
    timer = new Timer();
    timer.Interval = 20;
    timer.Enabled = true;
    timer.Tick += new EventHandler(timer_Tick);
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    device.Dispose();
    surface.Dispose();
    timer.Dispose();
}

Lastly, we have the timer_Tick event and the drawRect() function, which does the actual rasterizing of rectangle shapes. Again, there are several versions of the Graphics.DrawRectangle() function, and I have just chosen the easiest one, but there are others that let you use a Point for the coordinates instead of individual X and Y values.

    private void drawRect()
    {
        //make a random color
        int A = rand.Next(0, 255);
        int R = rand.Next(0, 255);
        int G = rand.Next(0, 255);
        int B = rand.Next(0, 255);
        Color color = Color.FromArgb(A, R, G, B);

        //make pen out of color
        int width = rand.Next(2, 8);
        Pen pen = new Pen(color, width);
        //random line ends
        int x = rand.Next(1, this.Size.Width - 50);
        int y = rand.Next(1, this.Size.Height - 50);
        Rectangle rect = new Rectangle(x, y, 50, 50);

        //draw the rectangle
        device.DrawRectangle(pen, rect);

        //refresh the drawing surface
        pb.Image = surface;
    }

    private void timer_Tick(object source, EventArgs e)
    {
        drawRect();
    }
}

Drawing Text

We will need to draw text onto the game screen using any desired font, and the Graphics class gives us this ability too, via the DrawString() function. There are several versions of the function with various sets of parameters, but we will be using the simplest version that just needs a String (for the words we want to print out), a custom Font object, the color, and the coordinates. Figure 2.4 shows the result of this example program.

Printing text using a custom font and color.

Figure 2.4. Printing text using a custom font and color.

using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
    string[] text = {
        "AVATAR!",
        "Know that Brittania has entered into a new age of",
        "enlightenment! Know that the time has finally come",
        "for the one true Lord of Brittania to take his place",
        "at the head of his people. Under my guidance, Brit-",
        "tania will flourish. And all of the people shall",
        "rejoice and pay homage to their new...  guardian!",
        "Know that you, too, shall kneel before me, Avatar.",
        "You, too, will soon acknowledge my authority. For I",
        "shall be your companion...  your provider...  and your",
        "master!", "",
        "Ultima VII: The Black Gate",
        "Copyright 1992 by Electronic Arts"
    };

    PictureBox pb;
    Bitmap surface;
    Graphics device;
    Random rand;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //initialize
        this.Text = "Text Drawing Demo";
        this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
            FixedSingle;
        this.MaximizeBox = false;
        this.Size = new Size(600, 500);
        rand = new Random();

        //create a new picturebox
        pb = new PictureBox();
        pb.Parent = this;
        pb.Dock = DockStyle.Fill;
        pb.BackColor = Color.Black;

        //create graphics device
        surface = new Bitmap(this.Size.Width, this.Size.Height);
        pb.Image = surface;
        device = Graphics.FromImage(surface);

        //make a new font
        Font font = new Font("Times New Roman", 26, FontStyle.Regular,
            GraphicsUnit.Pixel);

        //draw the text
        for (int n = 0; n < text.Length; n++)
        {
            device.DrawString(text[n], font, Brushes.Red, 10, 10 + n*28);
        }

        //refresh the drawing surface
        pb.Image = surface;
    }

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        device.Dispose();
        surface.Dispose();
    }
}

There are other shapes in addition to lines, rectangles, and text that the Graphics class can draw. Now that you have a foundation, see if you can modify the program to use any of the following functions:

  • DrawArc

  • DrawBezier

  • DrawCurve

  • DrawEllipse

  • DrawPie

  • DrawPolygon

Trick

To simplify the code in this C# project, I have removed the default namespace that Visual C# automatically added to the new project. In a larger project with many source code files and libraries, we would want to use a namespace, but for simple examples like this it is okay to skip the namespace.

Dissecting Bitmaps

Learning to draw a bitmap is the first step toward creating a 2D game like our impending Dungeon Crawler game. 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 3, and work on the basics of bitmap drawing now as a prerequisite.

Drawing on the code we learned about earlier in the chapter, a Bitmap object, a PictureBox, and a Graphics object work in tandem to represent a rendering device capable of drawing vector shapes as well as bitmaps. Once again for reference, we have to declare the two variables:

Bitmap surface;
Graphics device;

and then, assuming we have a PictureBox control called pictureBox1, create the objects. The PictureBox control can be created at runtime or we can just add it to the form manually.

surface = new Bitmap(this.Size.Width, this.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 C# 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.

Bitmap bmp;
bmp = new Bitmap("image.bmp");

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

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 watch the program 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 (Exception ex) { }

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 Bitmap LoadBitmap(string filename)
{
    Bitmap bmp = null;
    try
    {
        bmp = new Bitmap(filename);
    }
    catch (Exception) { }
    return bmp;
}

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. I’m just going to skip this step, though, and add bitmap loading in the Sprite class when we get to it in Chapter 3.

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, 0, 0);

will draw the bitmap bmp at pixel coordinates 0,0. Figure 2.5 shows an example.

Drawing an image loaded from a bitmap file.

Figure 2.5. 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 2.6 shows another example with the addition of this line, which draws another copy of the bitmap scaled down to a smaller size.

Drawing a scaled bitmap.

Figure 2.6. 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 as follows:

  • 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 2.7. 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 2.7. Rotating and flipping a bitmap.

image.RotateFlip(RotateFlipType.Rotate180FlipNone);
device.DrawImage(planet, 0, 0);
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 2.8 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 2.8. Modifying the color value of pixels in a bitmap.

for (int x = 0; x < image.Width - 1; x++)
{
    for (int y = 0; y < image.Height - 1; y++)
    {
        Color pixelColor = image.GetPixel(x, y);
        Color newColor = Color.FromArgb(0, pixelColor.G, 0);
        image.SetPixel(x, y, newColor);
    }
}

Here is the source code for the Bitmap Drawing demo. There are controls on the form, so you must open the project in order to run it; the source code here does not build the user interface like some of our simpler examples have done.

using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
    Bitmap surface;
    Graphics device;
    Bitmap image;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //set up the form
        this.Text = "Bitmap Drawing Demo";
        this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
            FixedSingle;
        this.MaximizeBox = false;
        //create graphics device
        surface = new Bitmap(this.Size.Width, this.Size.Height);
        pictureBox1.Image = surface;

        device = Graphics.FromImage(surface);
        //load the bitmap
        image = LoadBitmap("skellyarcher.png");
        //draw the bitmap
        device.DrawImage(image, 0, 0);
    }

    public Bitmap LoadBitmap(string filename)
    {
        Bitmap bmp = null;
        try
        {
            bmp = new Bitmap(filename);
    }
    catch (Exception ex) { }
    return bmp;
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    device.Dispose();
    surface.Dispose();
    image.Dispose();
}

private void button9_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.Rotate90FlipNone);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button10_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.Rotate180FlipNone);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button11_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.Rotate270FlipNone);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button12_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.RotateNoneFlipX);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}
private void button13_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.RotateNoneFlipY);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button14_Click(object sender, EventArgs e)
{
    image.RotateFlip(RotateFlipType.RotateNoneFlipXY);
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button15_Click(object sender, EventArgs e)
{
    Color white = Color.FromArgb(255, 255, 255);
    Color black = Color.FromArgb(0, 0, 0);
    for (int x = 0; x < image.Width - 1; x++)
    {
        for (int y = 0; y < image.Height - 1; y++)
        {
            if (image.GetPixel(x,y) == white)
                image.SetPixel(x, y, black);
        }
    }
    device.DrawImage(image, 0, 0);
    pictureBox1.Image = surface;
}

private void button16_Click(object sender, EventArgs e)
{
    for (int x = 0; x < image.Width - 1; x++)
    {
        for (int y = 0; y < image.Height - 1; y++)
        {
            Color pixelColor = image.GetPixel(x, y);
            Color newColor = Color.FromArgb(0, pixelColor.G, 0);
            image.SetPixel(x, y, newColor);
        }
        }
        device.DrawImage(image, 0, 0);
        pictureBox1.Image = surface;
    }
}

Creating a Reusable Framework

We have enough code now at this point to begin constructing a game framework for our future C# 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.cs, which will contain the source code for the Game class. Then, we’ll copy this Game.cs file into the folder of any new project we create and add it to that project. The goal is to simplify the whole process of creating a new game project and make most of our C# game code reusable. Let’s get started:

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;

public class Game
{
    private Graphics p_device;
    private Bitmap p_surface;
    private PictureBox p_pb;
    private Form p_frm;

You might recognize the first three of these 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. Yes, our Game class will even customize its form so we don’t have to do anything more than supply the form to the class.

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 method 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 method runs. Likewise, when the object is destroyed, the destructor method runs. These methods are defined in the class.

Here is the constructor for the Game class. This is just an early version, as more code will be added over 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 Game(Form1 form, int width, int height)
{
    Trace.WriteLine("Game class constructor");
    //set form properties
    p_frm = form;
    p_frm.FormBorderStyle = FormBorderStyle.FixedSingle;
    p_frm.MaximizeBox = false;
    p_frm.Size = new Size(width, height);

    //create a picturebox
    p_pb = new PictureBox();
    p_pb.Parent = p_frm;
    p_pb.Dock = DockStyle.Fill;
    p_pb.BackColor = Color.Black;

    //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);
}

Game Class Destructor

The destructor method is called automatically when the object is about to be deleted from memory (i.e., destroyed). In C#, or, more specifically, in .NET, the name of the destructor is Finalize(), but we create a sub-class destructor using the tilde character (~) followed by the class name. So, if our class name is Game, then the destructor method will be ~Game(). In this method, we again send a message sent to the output console using System.Diagnostics.Trace.WriteLine(). Feel free to use Trace any time you need to see debugging information, as it will be sent to the output window. Note that p_frm is not disposed—leave that alone as it is just a reference to the actual form.

~Game()
{
    Trace.WriteLine("Game class destructor");
    p_device.Dispose();
    p_surface.Dispose();
    p_pb.Dispose();
}

Bitmap Loading

Our first reusable method for the Game class is LoadBitmap:

public Bitmap LoadBitmap(string filename)
{
    Bitmap bmp = null;
    try
    {
        bmp = new Bitmap(filename);
    }
    catch (Exception ex) { }
    return bmp;
}

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 Graphics Device
    {
        get { return p_device; }
    }

    public void Update()
    {
        //refresh the drawing surface
        p_pb.Image = p_surface;
    }
}

So, for example, if we want to get the value returned by the Device property, we can do that like so:

Graphics G = game.Device;

Note that I did not include parentheses at the end of Device. That’s because it is not treated as a method, 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 by using a get without a corresponding set member. If I did want to make p_device writable, I would use a set member.

Properties are really helpful because they allow us to protect data in the class! You can prevent changes to a variable by making sure the change value is in a valid range before allowing the change—so in that sense, a property is 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.

using System;
using System.Drawing;
using System.Windows.Forms;

public partial class Form1 : Form
{
    public Game game;
    public Bitmap planet;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //set up the form
        this.Text = "Framework Demo";

        //create game object
        game = new Game(this, 600, 500);

        //load bitmap
        planet = game.LoadBitmap("planet.bmp");
        if (planet == null)
        {
            MessageBox.Show("Error loading planet.bmp");
            Environment.Exit(0);
        }

        //draw the bitmap
        game.Device.DrawImage(planet, 10, 10);
        game.Device.DrawImage(planet, 400, 10, 100, 100);
        game.Update();
    }

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        //delete game object
        game = null;
    }
}

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!

This chapter gave us the ability to create a rendering system in code and bypass the Form Designer by creating controls at runtime instead of design time. Using this technique, we created a PictureBox for use in rendering. We also learned how to work with bitmaps and manipulate them in interesting ways that will be very useful in a game. 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
18.221.79.26