Article Description
GDI+ is a feature rich graphics API that makes sophisticated graphical effects highly accessible to the C# developers. Unfortunately if youve tried to develop smooth detailed animation using GDI+ you have undoubtedly discovered that just how slow it can be. Consequently it is not particularly suited to games development, if you want to really take advantage of your 1 gig + processor and that fancy new graphics card youre going to have to get a little more low level and dirty, enter Microsofts DirectX API.
DirectX is a collection of COM dlls developed by Microsoft with games programming particularly in mind. DirectX communicates more directly with the computer hardware (ie Video Card, Sound Card etc..) then GDI+ yet still maintains a sufficient level of abstraction, freeing the developer from the particulars of the hardware running on the users machine.
Unfortunately .net and C# is not an officially supported platform for DirectX development and will not be until DirectX 9 is released at the end of the year. However we can use DirectX with C# in the meantime by using the Visual Basic type library's that come with version 7 and 8 of the API. This article uses the example of a relatively simple Breakout style game imaginatively called Space Breakout to demonstrate how to use the DirectX VB Type Lib in C#.
The Game
Figure I
Figure I is a UML diagram of the games design. There are eight classes in total, one class that represents the main windows form, one class which encapsulates the main directx objects, five classes which deal with the game sprites and a class that represents the group of block sprites. Each of these classes will be dealt with in detail below. To play the game use the left and right arrow keys to move the paddle and space to launch the game ball, to exit the game press the escape key.
DirectX and Direct Draw
DirectDraw was the main 2D interface to DirectX prior to version 8 (which is the latest version of DirectX). Since 2D graphics are no longer the mainstay of windows game programming Microsoft decided to merge the 2D and 3D elements of the DirectX API into a new interface known as DirectGraphics. There are advantages to this approach, however if you want to develop purely 2D games without any hardware accelerated 3D there are several disadvantages, the biggest being:-
Direct Graphics requires a 3D accelerator card for hardware 2D acceleration
Generating 2D graphics in DirectGraphics is more complex then using DirectDraw as it requires knowledge of 3D concepts.
Therefore when developing purely 2D games DirectDraw is probably the first place to start as it is still possible to create a DirectX7 object(which supports directDraw) under DirectX8.
Setting up a DirectX Application
Before we can begin a .net DirectX application we need to create a reference to the DirectX COM dll. In Visual Studio.net this is done by choosing Add Reference from the project menu. If DirectX 7 or 8 are installed on your machine then you should see a reference to the DirectX for Visual Basic Type Library under the COM tab of the Add reference Dialog. DirectX applications in .net must be based on a WinForm, so the next step is to create a standard borderless WinForm.
Initialising DirectX
In SpaceBreakout the main DirectX objects and their initialisation are encapsulated in the GameDirectDrawClass. There are two main display modes in DirectDraw, full-screen mode and windowed mode. Full-screen mode is the most useful for game development as DirectDraw is able to monopolise the users graphics display while the game is running, this in turn leads to significantly better performance in comparison to windowed mode. The following variables are used in the GameDirectDrawClass to initialise Full-Screen mode (through out the article I will be assuming that you have the statement 'using DxVBLib;' at the top of your code file):-
private DirectX7 m_objDirectX = new DirectX7();
private DirectDraw7 m_objDirectDraw;
private DirectDrawSurface7 m_objPrimarySurface;
private DirectDrawSurface7 m_objBackBufferSurface;
private DDSURFACEDESC2 m_objPrimarySurfaceDescription;
private DDSURFACEDESC2 m_objBackBufferSurfaceDescription;
m_objDirectX is the main DirectX instance that is used by the game and predictably m_objDirectDraw is the main DirectDraw instance for the game. The next two variables (m_objPrimarySurface and m_objBackBufferSurface) are DirectDraw surfaces, a surface represents the actual drawing surfaces that we use and are analogous to 'Graphics' objects in GDI+.
Every DirectDraw application must have at least one surface which represents the actual video buffer being rasterised and displayed by the graphics card, this is usually refered to as the primary surface and will be created using the m_objPrimarySurface variable. In addition to the primary surface an application will have a number of off screen surfaces that exist in memory, the second surface variable which was created is a special kind of offscreen surface known as the Back Buffer (m_objBackBufferSurface).
The Backbuffer surface is used in a process known as double buffering. Double buffering is the process of using a second surface to perform all the drawing which builds up a frame of animation, when all the drawing operations are complete the entire secondary or BackBuffer surface is copied to the Primary surface. The process of copying the Backbuffer to the Primary surface in DirectDraw full screen mode is page flipping. When the pages are flipped the Primary Surface points to the area of memory containing the BackBufferSurface and the Backbuffer points to the area of memory that the Primary surface pointed. The effect is that the Primary Surface object contains the contents of the Backbuffer and the BackBuffer object contains the contents of the primary surface. Once the pages have been flipped the BackBuffer can be cleared and the next frame drawn, figure II demonstrates the process.
Figure II
The remaining two variables m_objPrimarySurfaceDescription and m_objBackBufferSurfaceDescription are used to set the properties of the two surfaces and will be explained later on.
The method in the GameDirectDraw class that initialises the direct draw objects is InitialiseDirectXFullScreen(), it has one argument which is an integer representing the handle of the game's form. The code for this function is below:-
InitialiseDirectXFullScreen(int objDisplayFormHandle)
{
m_objDirectDraw = m_objDirectX.DirectDrawCreate("");
m_objDirectDraw.SetCooperativeLevel (objDisplayFormHandle,
ONST_DDSCLFLAGS.DDSCL_FULLSCREEN |
ONST_DDSCLFLAGS.DDSCL_EXCLUSIVE);
m_objDirectDraw.SetDisplayMode(640, 480, 16, 0,
ONST_DDSDMFLAGS.DDSDM_DEFAULT);
m_objPrimarySurfaceDescription.lFlags =
ONST_DDSURFACEDESCFLAGS.DDSD_CAPS |
ONST_DDSURFACEDESCFLAGS.DDSD_BACKBUFFERCOUNT;
m_objPrimarySurfaceDescription.ddsCaps.lCaps =
ONST_DDSURFACECAPSFLAGS.DDSCAPS_PRIMARYSURFACE
CONST_DDSURFACECAPSFLAGS.DDSCAPS_FLIP |
ONST_DDSURFACECAPSFLAGS.DDSCAPS_COMPLEX;
m_objPrimarySurfaceDescription.lBackBufferCount = 1;
m_objPrimarySurface = m_objDirectDraw.CreateSurface(ref
_objPrimarySurfaceDescription);
DDSCAPS2 ddscaps = new DDSCAPS2();
ddscaps.lCaps = CONST_DDSURFACECAPSFLAGS.DDSCAPS_BACKBUFFER;
m_objBackBufferSurfaceDescription = m_objPrimarySurface.GetAttachedSurface(ref
dscaps);
m_objBackBufferSurfaceDescription(ref m_objBackBufferSurfaceDescription);
}
The first line of code creates the DirectDraw object from the DirectX 7 object. Once this has been done the SetCooperativeLevel() method of the DirectDraw object is called, this sets the level of co-operation that the DirectDraw object will have with the operating system. The first parameter is the handle of the form that will be used for the application and the second contain the flags indicating the required co-operative level, the two flags used indicate that we will be using full screen mode and taking exclusive control of the display.
The next DirectDraw method (SetDisplayMode) sets the display mode for the game, the first two parameters indicate that the resolution will be 640 pixels by 480 pixels, the third specifies 16bit colour, the fourth is the refresh rate - by setting it to 0 DirectDraw will automatically use the best rate, the fifth argument indicates that no advanced resolutions are to be used.
Next properties are assigned to the Primary Surface descriptor variable, this variable will be used to create the Primary Surface. First we indicate that the description will specify the capabilities of the surface and it's Backbuffer count, this is done using the lflags property. The next line specifies the sets the capabilities of the surface, the first constant indicates that this will be a primary surface, the next constant indicates that we will be using page flipping to copy the backbuffer to the primary surface and the final constant is needed if we are using page flipping. The next line sets the back buffer count for the surface, it indicates that 1 backbuffer will be used. With the surface description configured the next line creates the primary surface using the CreateSurface() method of the DirectDraw object, the method takes a reference to the surface descriptor as it's one parameter.
With the Primary surface initialised the Backbuffer can then be created. We first need to set the caps property of the Backbuffer descriptor to indicate that this is a BackBuffer surface, we then use the GetAttachedSurface() method of the PrimarySurface object to create the Backbuffer.
Displaying Bitmaps
Bitmaps in DirectDraw must be loaded into new Surfaces and then copied onto the BackBuffer. Since there are a number of bitmap graphics in the game an abstract class (called BitmapObject) contains the code for bitmaps, this class is then inherited by each of the individual game object classes. The main variables used in the BitmapObject class to create a bitmap object and display it on the Backbuffer are:-
public const int BLACK_TRANSPARANT_SPRITE = 1;
public const int NORMAL_BITMAP = 0;
protected DDSURFACEDESC2 m_objDDSurfaceDescription;
private DirectDrawSurface7 m_objBitmapSurface;
protected RECT m_objSizeRECT;
The first two constants are used to indicate whether the object will be a sprite or a standard bitmap(a sprite is a non-square bitmap). Next m_objDDSurfaceDescription is a surface description object and is used to set the properties of the Surface object, m_objBitmapSurface is the DirectDraw surface object that will contain the bitmap. The remaining variable is a RECT which is an object containing rectangular information, it represents the rectangular dimensions of the surface and is used when the surface is copied to the backbuffer.
The surface description is configured in the function InitialiseSurfaceDescription() which is listed below, it contain two arguments which indicate the width and the height of the bitmap:-
InitialiseSurfaceDescription(int intBitmapWidth, int intBitmapHeight)
{
m_objDDSurfaceDescription.lFlags = CONST_DDSURFACEDESCFLAGS.DDSD_CAPS |
CONST_DDSURFACEDESCFLAGS.DDSD_WIDTH |
ONST_DDSURFACEDESCFLAGS.DDSD_HEIGHT;
m_objDDSurfaceDescription.ddsCaps.lCaps =
ONST_DDSURFACECAPSFLAGS.DDSCAPS_OFFSCREENPLAIN;
m_objDDSurfaceDescription.lWidth = intBitmapWidth;
m_objDDSurfaceDescription.lHeight = intBitmapHeight;
m_objSizeRECT.Bottom = intBitmapHeight;
m_objSizeRECT.Right = intBitmapWidth;
}
The first line of code sets the lFlags property of the surface descriptor, it indicates that the description will set the capabilities, the width and the height of the surface. The next line sets the capabilities of the surface specifying that it is an off screen surface. An off screen surface is a surface that exists in memory and is not itself physically visible to the user. The next two lines set the width and height of the surface and the final two lines set the width and the height of the RECT.
Once the surface description is setup the surface can be created, the code for this is in the InitialiseSurface() method. The method takes three arguments, the DirectDraw object for the application, a string indicating the source of the bitmap file and an integer indicating if a normal bitmap or a sprite is to be created. The code for the method is below:-
InitialiseSurface(DirectDraw7 DirectDraw, string strBackgroundBitmap, int intBitmapType)
{
try
{
m_objBitmapSurface = DirectDraw.CreateSurfaceFromFile( strBackgroundBitmap,
ref m_objDDSurfaceDescription );
}
catch ( Exception e)
{
System.Windows.Forms.MessageBox.Show( "Unexpected exception: " + e.ToString(), "Unexpected Exception" );
System.Windows.Forms.Application.Exit();
}
switch (intBitmapType)
{
case BitmapObject.BLACK_TRANSPARANT_SPRITE:
DDCOLORKEY objBlackKey;
objBlackKey.low = 0; objBlackKey.high = 0;
_objBitmapS
urface.SetColorKey( CONST_DDCKEYFLAGS.DDCKEY_SRCBLT, ref
bjBlackKey);break;
}
The first section of code attempts to create a DirectDraw surface object using the specified bitmap file. This is done using the CreateSurfaceFromFile() method of the applications DirectDraw object. CreateSurfaceFromFile() takes two parameters, a string indicating the source of the bitmap and the DirectDraw surface description object which was configured earlier.
The final section of code is used if a sprite is being created. As I mentioned earlier a sprite is a non-square graphic, unfortunately bitmap files can only be square therefore DirectDraw has to turn a square graphic into a sprite. This is done by defining a color key for the surface, DirectDraw will then make any part of the bitmap that is the specified colour transparent when it overlaid on another surface. All the sprites in the game use black for the colour key, the first line of code creates a DirectDraw colour key object and the next two lines set it's high and low colours to black. Next the SetColourKey() method of the bitmap surface object is called, the first parameter specifies that the colour key is to be applied to the bitmap surface object and not the surface that the object is being overlaid onto, the second parameter is a reference to the colour key object that was just created.
With the bitmap surface created it can be overlaid onto the Backbuffer surface, the process of overlaying one surface onto another is known as 'blitting' which is colloquial for "bit block transfer". To blit a bitmap surface onto the Backbuffer the BltFast() method is used, the following code is used in the BltFast() method of the games BitmapObject class to blit a sprite onto the backbuffer using the colour key for transparency:-
objBackBufferSurface.BltFast( this.XPosition, this.YPosition, m_objBitmapSurface, ref m_objSizeRECT, CONST_DDBLTFASTFLAGS.DDBLTFAST_SRCCOLORKEY | CONST_DDBLTFASTFLAGS.DDBLTFAST_WAIT );
The first parameter of the BltFast() method is the number of pixels from the left of the Backbuffer that the sprite will be placed; the next parameter is the number of pixels from the top to position the sprite; the third is the surface object of the sprite; the fourth is a RECT which indicates the size of the area being blitted to and the final parameter is the blitt flags to use; the two used indicate that we are using a colour key for the object and that DirectDraw is to wait until blitting is completed before moving to the next operation.
Flipping the Backbuffer to the primary surface
Once all the bitmaps for a frame have been blitted to the backbuffer it needs to be flipped to the Primary surface. This is done in the FlipBackBufferAndPrimary() method of the GameDirectDraw class. The method has one line of code which is:- this.PrimarySurface.Flip(this.BackBufferSurface, CONST_DDFLIPFLAGS.DDFLIP_WAIT); As you can see the Flip() method of the Primary Surface object is called, the method has two parameters. The first parameter is the Backbuffer surface object and the second is a flag indicating that DirectDraw should wait until the flipping is complete before moving onto the next operation. Once the flipping is complete the backbuffer can then be cleared and the next frame of animation blitted to it.
Conclusion
This article has detailed the core elements of building a game using DirectX and DirectDraw. DirectX is a huge topic but it is hoped that in conjunction with the source code for the Space Breakout game you'll have all you need to begin building your own 2D DirectX games.