Why Use DirectDraw?
Managed DirectDraw 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.
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 width, height, color depth (in bits), and refresh rate (0 = default, and safest) GraphicsCard.SetDisplayMode(800, 600, 16, 0, False) 'the last setting sets the resolution to 800*600 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. Very old monitors could be damaged when refresh rate was set too high but today they just show a warning. 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 (since 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 game\pic.bmp", New SurfaceDescription, GraphicsCard) 'A blank SurfaceDescription suffices. '// we will now make all black areas of the sprite be transparent Dim ck As ColorKey = New ColorKey sprite.SetColorKey(ColorKeyFlags.SourceDraw, ck) 'You can specify a color, but 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.
Note: C:\my game\pic.bmp must be changed to reflect an actual picture's location or the program will encounter an error
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, and draw the sprite frm.backbuffer.DrawFast(0,0, frm.sprite, DrawFastFlags.DoNotWait Or DrawFastFlags.SourceColorKey) 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 its 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) 'The blank surface description defaults to the correct settings for a sprite. '// 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
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.
The original version of this tutorial can be found at VBGamer