Why Use DirectDraw?
Managed DirectDraw 9 is an excellent choice for 2D games aimed at users
with low-end hardware. Although increasingly more people have machines
that can handle Direct3D, it’s still a good idea to keep the system
requirements for your game low. If you don’t need the special effects
Direct3D gives you, go with DirectDraw. This tutorial will show you how
to display a simple sprite with DirectDraw. This will be a full screen
application, and I will show you how to prevent your application from
crashing when it loses focus.
Preparing Your Window
Create a new Visual Basic project, and change the default window’s
WindowState property to Maximized. Disable the Minimize Box, the
Maximize box, and the border. Change the KeyPreview property to True.
This will allow the window to process all keyboard events. This will
prepare your window for Fullscreen mode in DirectDraw.
Basic Initialization
Now we need to initialize DirectDraw for your window. First, make sure
you have Microsoft.Directx.dll, and Microsoft.Directx.Directdraw.dll
referenced. After you have these referenced, add the following lines of
code to the top of your code:
- Imports Microsoft.DirectX
- Imports Microsoft.DirectX.DirectDraw
This will allow easy access to the DirectX components we will be using.
We now have to define “surfaces”. Surfaces hold graphics. We will need
one primary surface to represent the view screen, one surface to be our
backbuffer, and one surface to hold our sprite. If you have worked with
2D graphics before, you may already be familiar with the process: draw
all sprites to the backbuffer, then draw the backbuffer to the screen.
This method is far faster than drawing all sprites directly to the
screen, because the video display is only updated once, rather than
multiple times.
In order to make the primary surface represent
the view screen, we will need to tie it to a “device”. In your form’s
declarations section, add this code:
- Dim sprite As Surface = Nothing 'The sprite surface will be copied onto the backbuffer surface.
- Dim backbuffer As Surface = Nothing 'The backbuffer surface will be copied onto the primary surface.
- Dim primary As Surface = Nothing 'The primary surface will be attached to the device object.
- Dim GraphicsCard As device = Nothing 'The device object represents the users graphics card.
Before we can use these objects, we need to tell DirectX how we
want them to behave. Create a new subroutine called InitializeGraphics.
- Public Sub InitializeGraphics()
- GraphicsCard = New Device ‘This will initialize the GraphicsCard object.
- '// Now we’ll tell the graphics card to give our window (Me) fullscreen control.
- GraphicsCard.SetCooperativeLevel(Me, CooperativeLevelFlags.FullscreenExclusive)
- '// Set display mode width, height, color depth (in bits), and refresh rate (0 = default, and safest)
- GraphicsCard.SetDisplayMode(800, 600, 16, 0, False) 'the last setting will set the resolution to 320*200 and the color
- depth to 16 colors if set to true. Set bit depth to 0 if you enable this.
Since we set all our objects to equal “Nothing” when they were
declared, we need to describe their behavior in the InitializeGraphics
routine before we can use them. Before we can change the settings on
the GraphicsCard object, we have to set it to equal a new “Device”.
After that, we set the Cooperative Level and Display mode properties.
The Cooperative Level setting takes two arguments. The first tells the
GraphicsCard object which window we are telling it to interact with,
the second tells it what kind of control the device should have. In our
case we gave our device full-screen and exclusive control.
Next, we set the display mode for our device. The first two parameters
are the width and height in pixels. The third parameter is the color
depth in bits, and the fourth is the refresh rate. I would advise you
to always leave this at 0, which tells DirectDraw to use the default
value. If you set the refresh rate too high, you can damage your
customer’s monitor. The last parameter controls whether or not you wish
to use standard VGA mode. Be warned, if you enable this, you will be
limited to a display of 320*200 pixels and 16 colors!
Let’s continue writing our InitializeGraphics subroutine:
- Dim description As SurfaceDescription = New SurfaceDescription
- '// The SurfaceDescription object will be used to describe the primary surface to DirectX
- '// Now we describe the primary surface:
- description.SurfaceCaps.PrimarySurface = True 'This is a primary surface.
- description.SurfaceCaps.Flip = True 'This surface can be flipped.
- description.SurfaceCaps.Complex = True 'This is a complex surface (because it will have a backbuffer).
- description.BackBufferCount = 1 'This surface will have one backbuffer.
- '// Here we set the primary surface up
- primary = New Surface(description, GraphicsCard) 'Give it the description and attach it to the GraphicsCard object.
- 'We will now set the capabilities of the backbuffer.
- Dim caps As New SurfaceCaps 'Makes a new Surface capabilities object.
- caps.BackBuffer = True 'This sets the caps object to give backbuffer capability.
- '// The following will attach the backbuffer to the primary object.
- backbuffer = primary.GetAttachedSurface(caps)
In this code, we use the SurfaceDescription object to describe our
primary surface. We tell the description object that it should describe
a primary, flipable, complex surface, with one BackBuffer. Without
going into the nuts and bolts of DirectDraw in great detail, I couldn’t
explain why it needs to be described this way, so we will skip the
lecture for this basic tutorial.
Next, we set our primary
surface to equal a new surface, describing it with the description
object we created and attaching it to our GraphicsCard device.
The last step is easily understood if you realize that “caps” is short
for “capabilities”. Thus, when we wrote “backbuffer =
primary.GetAttachedSurface(caps)” we were telling DirectX that the
backbuffer surface we made is to represent the primary surface’s
backbuffer!
And now to finish off our InitializeGraphics sub!
- '// Create the sprite bitmap surface.
- sprite = New Surface("C:my gamepic.bmp", New SurfaceDescription,GraphicsCard) 'The default surfacedescription is fine for our spriteobject.
- '// we will now make all black areas of the sprite be transparent
- Dim ck As ColorKey = New ColorKey
- sprite.SetColorKey(ColorKeyFlags.SourceDraw, ck) 'since we didn’t specify a color, it defaults to black.
- 'Release all of our temporary objects.
- description = Nothing
- caps = Nothing
- ck = Nothing
- End Sub '// This is the end of our InitializeGraphics Subroutine
First off, we set our sprite to a new surface object passing three
parameters. The first is a string that holds the path to the image we
want our sprite object to hold. The second is a blank
SurfaceDescription object. We don’t need to put any details in the
SurfaceDescription object, because our sprite object is just a plain
surface; it has no special features. We pass our GraphicsCard object to
the last parameter to tell the sprite which device it is going to be
interacting with.
Finally, we will enable color-keying for our
sprite. A color key is just a color that you wish to be invisible. This
is useful if you want there to be transparent areas on your sprite. We
leave our colorkey object alone in this example, so it will default to
Black. If you want to change the range of colors that are used for
transparency, use the ck.ColorSpaceHighValue, and ck.ColorSpaceLowValue
methods.
Last of all, we set all our objects to nothing, to avoid memory leaks.
The Game Loop
At this point, our program still does nothing, because we never call
our InitializeGraphics subroutine. We also haven’t actually told our
program to draw anything yet, either. We’ll do both in a subroutine
called Main. Add the code below to your program:
- Shared Sub Main()
- Dim frm As New Form1
- frm.Show()
- frm.InitializeGraphics()
- Do While frm.Created = True
- Try
- frm.backbuffer.ColorFill(0) 'Fill the backbuffer with black
- frm.backbuffer.DrawFast(0, 0, frm.sprite,DrawFastFlags.DoNotWait Or DrawFastFlags.SourceColorKey) 'Draw the sprite
- frm.primary.Flip(frm.backbuffer, FlipFlags.DoNotWait) 'Flip the backbuffer to the primary surface
- Catch err As WasStillDrawingException
- '// No need to do anything
- Catch err As SurfaceLostException
- frm.RestoreSurfaces() 'Restore our surfaces
- End Try
- Application.DoEvents() 'allow Windows to continue it's work
- Loop
- End Sub
It’s important that you declare Main as shared, otherwise you can’t use
it as your starting function for your project. To set Main() as your
starting subroutine, open the properties dialog for your Visual Studio
project. Under the general properties, there should be a drop-down box
for the Startup object. Change it from Form1 to Sub Main. The first
line in the Main() sub declares a new instance of Form1 called “frm”.
The next two lines tell our program to show the form, and call Form1's
InitializeGraphics method. Note that we can’t access any of Form1's
non-shared methods from our Shared Sub Main without specifically
referring to a particular instance of the Form1.
Next, we made
a loop that continues as long as the frm object is still created. This
is often called a “Game Loop”. The code inside this loop is enclosed in
a Try...Catch...End Try statements because the code will throw a
SurfaceLostException if the DirectDraw device loses exclusive control
of the graphics card. This will happen if someone switches to another
window while your game is running. When we catch the
SurfaceLostException, we tell our program to call the RestoreSurfaces
method, which we have not yet written. It will also throw a
WasStillDrawingException if device is still busy completing a previous
draw command, but we just ignore it in the code above.
The
first command in the Try section fills the backbuffer with black. The
next line uses the DrawFast command to draw our sprite onto the
backbuffer. The first two parameters are X and Y positions. They tell
the backbuffer where to draw the image. The third parameter tells the
backbuffer which surface we want it to draw, and the last command tells
the backbuffer to draw immediately using the source surface’s colorkey.
The last command in the loop, Application.DoEvents, tells the program
to respond to events raised by windows. If you omit this command our
program will become unresponsive.
Restoring Lost Surfaces
Now we need to write that RestoreSurfaces method!
- Private Sub RestoreSurfaces()
- Try
- '#### Note #####
- ' Will throw wrongmodeexception until exclusive mode is regained
- GraphicsCard.RestoreAllSurfaces()
- Catch
- Debug.WriteLine("Can't restore surfaces right now.")
- Exit Sub
- End Try
- sprite = New Surface("..pic.bmp", New SurfaceDescription, GraphicsCard) 'What's the new surfacedescription for?
- '// Set the colorkey to the bitmap surface.
- '// which is what the colorkey struct is initialized to.
- Dim ck As ColorKey = New ColorKey
- sprite.SetColorKey(ColorKeyFlags.SourceDraw, ck) 'explain colorkeyflags
- ck = Nothing
- End Sub
This should be pretty easy to understand. First, we tell the
device to attempt to restore all surfaces bound to it. If your program
cannot regain exclusive control of the graphics card, the
device.RestoreAllSurfaces() method will throw a WrongModeException. We
deal with that here by using a general Catch statement that notifies
the debugging console and exits the sub. If the
device.RestoreAllSurfaces() method does work, we merely need to
re-setup our sprite surface with its image and colorkey. The code here
is the same as it was in our InitializeGraphics routine, so you should
recognize it.
Handling Keyboard Input
There is only one thing left to do! We need to provide a way to exit
our program. The following code tells our program to close when the
user hits the Escape key.
- Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
- If e.KeyCode = Keys.Escape Then
- Me.Close()
- End If
- End Sub
Finish
That’s all there is to it! If you run the program now, it should
display your sprite in a Fullscreen DirectDraw window. If you have any
questions, I can answer them on the
Forums.