Double Buffer Drawing with DirectDraw: Part1

 Because we will be primary focusing on 2D game programming in the first articles, we will use the DirectX7 API which is perfect for its DirectDraw class and simplicity. Dont worry for not having the right DLL because DirectX8 has full backward compatibility to DirectX7 and the COM object should already be available on your computer if you have DirectX 8 installed. So if you have installed the DirectX8 version, the DirectX7 API should be in your COM list as wellJ. A lot of 2D game programmers still make use of DirectX7, just because of the lighter API which does not require you to setup up complete 3D worlds, cameras, and lights, and the simplicity of the DirectDraw class which encapsulate the 2D drawing capabilities. 

When drawing with DirectDraw, you can specify to use more than one surface to draw on and flip between the primary and secondary screen surfaces. This technique is called double-buffering and is used to get a stable FPS and smooth screen updates. In this article we will create a DirectX wrapped class that will be able to initialize a full-screen DirectX screen and draw a couple of rounded boxes with different visual styles on the secondary surface and then flip the surface to primary output surface. Next to that, we will learn the basics of DirectInput, which we will use to handle the keyboard actions to exit the application.

How we are going to structure the code

The code we will write consists of three blocks. I really hate to mix reusable code with application specific code, and therefore we will put all application specific DirectX functions in a separate class which we call from the Form class. The DirectX application class will call and initialize the needed DirectX objects by using the reusable DirectX wrapper class, which is there to hold generic DirectX functions and objects.

Dx-image002.jpg


The application loop

The DirectX application class contains a loop which updates screen surface after a certain frames per second has elapsed, pauses the application when the screen is not focused and exits the application when the Quit button is pressed. Games are generally big loops which contains the same kind of structure, so if you are already thinking about a nice concept; take your time to work this structure out on a piece of paper before writing the code.

Dx-image003.gif


Interoping with DirectX

In order to invoke the DirectX COM component, you will need to add the COM reference to your project. Add the reference DirectX 7 for Visual Basic Type Library. This will create an interop object, which can be reached under the name DxVBLib.

Creating the DirectXWrapperClass

We will encapsulate the basic DirectX objects in a separate class, which we will call the wrapper class. Within this class we will store the DirectX objects and functions that are abstract enough to serve most of our future DirectX projects.

1. Within the head of the class add the following using statement to make the COM component accessible directly:

using System;
using DxVBLib;
namespace DirectDraw_lesson1
{

2. Now we will add the DirectX specific classes and objects that we will use throughout almost any DirectDraw application. 

namespace DirectDraw_lesson1
{
///<summary>
/// Summary description for DirectXWrapper.
///</summary>
public class DirectXWrapper{
private DirectX7 DirectX = new DirectX7();
private DirectDraw7 directdraw;
private DirectDrawSurface7 dd_primarysurface;
private DirectDrawSurface7 dd_backbuffersurface;
private DDSURFACEDESC2 DD_PrimarySurfaceDesc;
private DDSURFACEDESC2 DD_BackbufferSurfaceDesc;

Lets take a look at these classes and explain what they all mean. The first one you can pretty much guess is the core object of DirectX. Most of the DirectX objects will have to be initialized by using the return value from the DirectX core object functions. We will see that later when we will initialize the DirectDraw object. The DirectDraw class holds all drawing related functions, like e.g. surface painting, buffer swapping. With the DirectDraw class, you will always draw on a surface. A surface can either be the primary output or on an in-memory located buffer (secondary buffer or backbuffer). Most games use at least two surfaces in order to enable double-buffering techniques. This means that all images are first drawn onto an in memory canvas, and then flipped to the primary surface. This causes a nice and smooth update because the screen shown towards the user is updated as an entire frame at once.


Dx-image004.gif

Surfaces always need to have a reference pointer to the structure that describes the surface. In order to do so, we will create the DDSURFACEDESC2 structs for both surfaces. 

Okay, now we know all this lets update the accessibility of these objects.
Surfaces always need to have a reference pointer to the structure that describes the surface. In order to do so, we will create the DDSURFACEDESC2 structs for both surfaces.
Okay, now we know all this lets update the accessibility of these objects.using System;

using DxVBLib;
namespace DirectDraw_lesson1
{
public class DirectXWrapper
{
// Create and Initialize the DirectX core object class
private DirectX7 DirectX = new DirectX7();
// Create the DirectDraw class
private DirectDraw7 directdraw;
public DirectDraw7 DirectDraw
{
get { return directdraw; }
}
private DirectDrawSurface7 dd_primarysurface;

public DirectDrawSurface7 DD_PrimarySurface
{
get { return dd_primarysurface; }
}
private DirectDrawSurface7 dd_backbuffersurface;
public DirectDrawSurface7 DD_BackbufferSurface
{
get { return dd_backbuffersurface; }
}
private DDSURFACEDESC2 DD_PrimarySurfaceDesc;
private DDSURFACEDESC2 DD_BackbufferSurfaceDesc;

3. Initializing the DirectDraw classOn our main class entry we will initialize the DirectDraw class by using the DirectDrawCreate function from the DirectX object.

public DirectXWrapper()
{
// TODO: Add constructor logic here
//
directdraw = this.DirectX.DirectDrawCreate("");
}

4. The Initialize full screen functionThe following function initializes a new exclusive DirectX screen based on the input parameters.

public void InitializeDirectXFullScreen(int intDisplayFormHandle, int width, int height, int depth)
{
//set co-opperative level for full screen
directdraw.SetCooperativeLevel (intDisplayFormHandle CONST_DDSCLFLAGS.DDSCL_FULLSCREEN | CONST_DDSCLFLAGS.DDSCL_EXCLUSIVE);
directdraw.SetDisplayMode ( width , height , depth , 0 , CONST_DDSDMFLAGS.DDSDM_DEFAULT );
InitializeSurfaces ();
}

The SetCooperativeLevel function sets the behavior of the DirectX screens. In this case, we tell DirectX to provide us a full-screen surface based on the window handle.

5. Surface initialization.Within the function, we will populate the surface describing structs and create the surface by using surface describer struct as reference.

private void InitializeSurfaces()
{
/*
*** Primary Surface : BufferDescription */
//lFlags : indicates that we want to specify the
//DDS_CAPS and backbuffer options
DD_PrimarySurfaceDesc.lFlags = CONST_DDSURFACEDESCFLAGS.DDSD_CAPS | CONST_DDSURFACEDESCFLAGS.DDSD_BACKBUFFERCOUNT;
DD_PrimarySurfaceDesc.ddsCaps.lCaps = CONST_DDSURFACECAPSFLAGS.DDSCAPS_PRIMARYSURFACE | CONST_DDSURFACECAPSFLAGS.DDSCAPS_FLIP | CONST_DDSURFACECAPSFLAGS.DDSCAPS_COMPLEX;
//set backbuffer count to 1 DD_PrimarySurfaceDesc.lBackBufferCount = 1;
/* *** Primary Surface : Initialize */
dd_primarysurface =
this.directdraw.CreateSurface (ref DD_PrimarySurfaceDesc);
/* ** Backbuffer : BufferDescription */
DD_BackbufferSurfaceDesc.lFlags = CONST_DDSURFACEDESCFLAGS.DDSD_CAPS;
//set the descriptors caps to backbuffer surface
DD_BackbufferSurfaceDesc.ddsCaps.lCaps = CONST_DDSURFACECAPSFLAGS.DDSCAPS_BACKBUFFER;
/* ** Backbuffer : Initialize */
dd_backbuffersurface =
this.dd_primarysurface.GetAttachedSurface (ref DD_BackbufferSurfaceDesc.ddsCaps);

Within DirectX the lFlags property describes which other properties of the object are going to be changed. We will use it to tell that the DD_PrimarySurfaceDesc struct is the primary surface and that it has one extra back-buffer. After the primary surface has been created we add the dd_backbuffersurface as secondary buffer for the primary surface.

6. Flipping buffers.The last step to perform on the game process is to flip the primal surface with the backbuffer surface you will be drawing on. This means that the pointers in memory holding the DirectX surfaces are swapped in order to get a very fast update.

public void FlipBackBufferAndPrimary()
{
//flip backbuffer to primary surface this.dd_primarysurface.Flip(dd_backbuffersurface , CONST_DDFLIPFLAGS.DDFLIP_WAIT);
//.Flip
}

Adding DirectInput supportDirectInput is a DirectX API which handles devices like joysticks, keyboards, and mousses. You can even query its capabilities and then make use of the device specific features like e.g. ForceFeed-back effects and special keyboard buttons. However this article was intended not to dive too deep on this one, we still will create a class to handle certain key actions in order to exit the application. With the DirectInput class you can create a device handler, and populate its buffer data from memory into an object. This is done by using the GetDeviceStateKeyboard command which populates a DIKEYBOARDSTATE object. After this is done, you can search this object for checking if a key was pressed by looping through the .key byte array attribute.

DirectInputClass.cs

using System;using DxVBLib;
namespace DirectDraw_lesson1
{
///<summary>
/// Summary description for DirectInput.
///</summary>
public class DirectInputClass
{
private DirectInput Di;
private DirectInputDevice DiDev;
public DIKEYBOARDSTATE KEYB_STATE = new DIKEYBOARDSTATE();
public DirectInputClass()
{
//
// TODO: Add constructor logic here
//
}
public bool InitializeInputDevice(DirectX7 oDirectX7,int hWND )
{
bool b_return;
try
{
Di = oDirectX7.DirectInputCreate();
DiDev = Di.CreateDevice ("GUID_SysKeyboard");
DiDev.SetCommonDataFormat (CONST_DICOMMONDATAFORMATS.DIFORMAT_KEYBOARD);
DiDev.SetCooperativeLevel (hWND,CONST_DISCLFLAGS.DISCL_NONEXCLUSIVE | CONST_DISCLFLAGS.DISCL_BACKGROUND );
DiDev.Acquire ();
b_return =
true;
}
catch
{
b_return =
false;
}
return b_return;
}

After creating the DirectInput class by using the DirectX object, we will add a device identified by GUID_SysKeyboard which is the standard keyboard device handler for that buttonized thing in front of you. Then we set the cooperative level by binding it to a window handler. This states out how the device handler is listening to the application.

public void UpdateKeyBoardState()
{
if (Di!=null)
{
DiDev.GetDeviceStateKeyboard (
ref KEYB_STATE);
}
}
public bool QuitKeyDown()
{
bool bReturn = false;
if ( KEYB_STATE.key[ ( int )CONST_DIKEYFLAGS.DIK_RETURN ]==0x80 )
{
bReturn =
true;
}
return bReturn;
}
public void Acquire()
{
DiDev.Acquire ();
}
public void Unacquire()
{
DiDev.Unacquire ();
}
}
}

Creating the DirectApplication Class

At last!!! All our stuff we will need for the generic part has been put into place. Now we can just hook up the application specific class that will do the stuff we want to do in this article.

DirectXApplication.cs

using System;
using DxVBLib;
namespace DirectDraw_lesson1
{
///<summary>
/// Summary description for DirectXApplication.
///</summary>
public class DirectXApplication
{
private DirectXWrapper DX = new DirectXWrapper();
///<summary>
/// Windows HWND
///</summary>
private int handle;
public int Handle
{
set { handle = value; }
get { return handle; }
}
private int framespersecond = 100;
public int FramesPerSecond
{
get { return framespersecond; }
set { framespersecond = value; }
}
private int screenwidth = 640;
public int ScreenWidth
{
get { return screenwidth; }
set { screenwidth = value; }
}
private int screenheight = 480;
public int ScreenHeight
{
get { return screenheight; }
set { screenheight = value; }
}
private int screenbits = 16;
public int ScreenBits
{
get { return screenbits; }
set { screenbits = value; }
}
private int GetTickCount()
{
return System.Environment.TickCount;
}

The GetTickCount function, enables us to count the milliseconds which have passed since the application was loaded. It is a perfect .Net framework object to do your FPS counting maths with.

private bool running = false;
public bool IsRunning
{
get { return running; }
}

This will specify the application to loop or not to loop, that is the question.

public DirectXApplication()
{}
public void Initialize()
{
DX.InitializeDirectXFullScreen (
this.handle, this.screenwidth, this.screenheight, this.screenbits);DX.InitializeDirectInput (this.handle);
}
public void StartApplication()
{
this.running = true;
MainLoop ();
}
public void StopApplication()
{
this.running = false;
}
private void MainLoop()
{
//get the current ticks
int intCurrentTickCount = GetTickCount();
while (this.running)
{
// If the number of milliseconds elapsed the setted frames per second then UPDATE screen
if (GetTickCount() - (1000/this.framespersecond) >= intCurrentTickCount)
{
/*
** KeyHandler
*/
this.DX.DirectInput.Acquire ();
this.DX.DirectInput.UpdateKeyBoardState ();
if ( DX.DirectInput.KEYB_STATE.key[ ( int )CONST_DIKEYFLAGS.DIK_RETURN ]==0x80| DX.DirectInput.KEYB_STATE.key[ ( int )CONST_DIKEYFLAGS.DIK_ESCAPE ]==0x80)
{
//Application.Exit ();
System.Windows.Forms.Application.Exit ();
}
if (!SurfacesLost())
{
/*
** BackBuffer paint
*/
DX.RepaintBackBuffer ();
/*
** Blit Background Picture
*/
DrawScreen ();
DX.FlipBackBufferAndPrimary ();
}
else
{
//this.RestoreSurfaces ();
}
//get current tick count
intCurrentTickCount = GetTickCount();
//do windows events
System.Windows.Forms.Application.DoEvents ();
}
//do windows events
System.Windows.Forms.Application.DoEvents ();
}
}
private void RestoreSurfaces()
{
while ( ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_NOEXCLUSIVEMODE )| ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_WRONGMODE )| ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_SURFACELOST ))
{
//Invoke window events for application first System.Windows.Forms.Application.DoEvents ();
// Restore Surfaces
DX.DirectDraw.RestoreAllSurfaces ();
}
//do window events for application
System.Windows.Forms.Application.DoEvents ();
}


The restore surfaces function is a function which is called if a surface gained focus again after the focus was lost i.e. someone has played with ALT+TAB combo. If a surface can not be drawn, it is lost and we will try to restore it until everything is okay again. The Application DoEvents command in this loop makes sure we dont overkill other procedures running, like the actual screen switching update.

public void ResumeApplication()
{
RestoreSurfaces ();
StartApplication ();
}
///<summary>
/// Checks if the DirectDraw surfaces are lost e.g. Need to be restored
/// Needed for Surface restoring after flipping to Windowsmode and Back
///</summary>
///<returns></returns>
public bool SurfacesLost()
{
bool rBool = false;
if ( ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_NOEXCLUSIVEMODE )| ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_WRONGMODE )| ( DX.DirectDraw.TestCooperativeLevel() == ( int )CONST_DDRAWERR.DDERR_SURFACELOST ))
{
rBool =
true;
}
return rBool;
}

At the moment I am not making use of this OnExit handler, but Ive implemented it for you to play with.

public delegate void ExitHandler( object sender, EventArgs e);
public event ExitHandler OnExit;
protected virtual void Exit(EventArgs e)
{
if (OnExit != null)
OnExit (
this, e);
}

Okay, now here comes some drawing stuff we will use to generate the drawing.

private class MyRect
{
private int x_1;
private int x_2;
private int y_1;
private int y_2;
public int X_1
{
get { return x_1; } }
public int X_2
{
get { return x_2; } }
public int Y_1
{
get { return y_1; } }
public int Y_2
{
get { return y_2; } }
public MyRect( int ix_1, int iy_1, int ix_2, int iy_2 )
{
this.x_1 = ix_1;
this.x_2 = ix_2;
this.y_1 = iy_1;
this.y_2 = iy_2;
}
public void ShiftX()
{
x_1 = x_2 + 10;
x_2 = x_1 + 100;
}
}
private void DrawScreen()
{
this.DX.DD_BackbufferSurface.SetForeColor(255);
this.DX.DD_BackbufferSurface.SetFillColor(8913937); 
this.DX.DD_BackbufferSurface.DrawText(10,10,"Double buffered DirectDraw paint example in C#",false);
this.DX.DD_BackbufferSurface.DrawText(10,28,"Press ESC or Return to exit",false); 
MyRect myrect =
new MyRect (40, 100, 140 ,150);
// Solid Box
this.DX.DD_BackbufferSurface.setDrawStyle(0);
this.DX.DD_BackbufferSurface.DrawRoundedBox(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2 ,12,12);
// Dashed Box
myrect.ShiftX ();
this.DX.DD_BackbufferSurface.setDrawStyle(1);
this.DX.DD_BackbufferSurface.DrawRoundedBox(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2 ,12,12);
// Dotted Box
myrect.ShiftX ();
this.DX.DD_BackbufferSurface.setDrawStyle(2);
this.DX.DD_BackbufferSurface.SetFillStyle(1);
this.DX.DD_BackbufferSurface.DrawRoundedBox(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2 ,12,12);
// Dash Dot Box
myrect.ShiftX ();
this.DX.DD_BackbufferSurface.setDrawStyle(3);
this.DX.DD_BackbufferSurface.SetFillStyle(2);
this.DX.DD_BackbufferSurface.DrawRoundedBox(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2 ,12,12);
// Dash Dot Dot Box
myrect.ShiftX ();
this.DX.DD_BackbufferSurface.setDrawStyle(4);
this.DX.DD_BackbufferSurface.SetFillStyle(3);
this.DX.DD_BackbufferSurface.DrawBox(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2);
// Dash Dot Dot Box
myrect.ShiftX ();
this.DX.DD_BackbufferSurface.setDrawStyle(6);
this.DX.DD_BackbufferSurface.SetFillStyle(4);
this.DX.DD_BackbufferSurface.DrawEllipse(myrect.X_1,myrect.Y_1,myrect.X_2 , myrect.Y_2);
}
}
}

Last but not least: The Form1 Class

This is the main class that contains some event handlers to initialize and resume the DirectX application. Here is the last listing of the application.

Form1.cs

using System;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
namespace DirectDraw_lesson1
{
///<summary>
/// Summary description for Form1.
///</summary>
public class Form1 : System.Windows.Forms.Form
{
private DirectXApplication MyDirectXApp;
///<summary>
/// Required designer variable.
///</summary>
private System.ComponentModel.Container components = null;

public Form1()
{
InitializeComponent ();
}
///<summary>
/// Clean up any resources being used.
///</summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose ();
}
}
base.Dispose ( disposing );
}
#region Windows Form Designer generated code
///<summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///</summary>
private void InitializeComponent()
{
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size (5, 13);
this.ClientSize = new System.Drawing.Size (400, 332);
this.Name = "Form1";
this.Text = "Form1";
this.GotFocus += new System.EventHandler (this.Form1_GotFocus);
this.Load += new System.EventHandler (this.Form1_Load);
this.LostFocus += new System.EventHandler (this.Form1_LostFocus);
}
#endregion
///<summary>
/// The main entry point for the application.
///</summary>
[STAThread]
static void Main ()
{
Application.Run (
new Form1());
}
private void Form1_Load(object sender, System.EventArgs e)
{
Cursor.Dispose ();
MyDirectXApp =
new DirectXApplication ();
// Initializes a 800x600 32bit DirectX screen
// based on the Handle of this window
MyDirectXApp.Handle = this.Handle.ToInt32();
MyDirectXApp.ScreenWidth = 800;
MyDirectXApp.ScreenHeight = 600;
MyDirectXApp.ScreenBits = 32;
MyDirectXApp.FramesPerSecond = 100;
MyDirectXApp.OnExit +=
new DirectXApplication.ExitHandler (this.DxExit);
MyDirectXApp.Initialize ();
MyDirectXApp.StartApplication ();
}
private void DxExit(object sender, System.EventArgs e)
{
Application.Exit ();
}
///<summary>
/// LostFocus Handler
/// Stops the DirectX application when a Windows objects got focus.
///</summary>
///<param name="sender"></param>
///<param name="e"></param>
private void Form1_LostFocus(object sender, System.EventArgs e)
{
if (MyDirectXApp!=null)
{
MyDirectXApp.StopApplication ();
}
}
private void Form1_GotFocus(object sender, System.EventArgs e)
{
if (MyDirectXApp!=null)
{
if (MyDirectXApp.IsRunning==false)
{
if (MyDirectXApp.SurfacesLost())
{
Console.WriteLine ("Surface lost");
MyDirectXApp.ResumeApplication ();
}
}
}
}
}
}

Round up

Thats it for now folks. Now you know most of the basics, we can start the really cewl stuff next article. However we did not dive into the drawing to deep, we really did code a damn good fundament for future DirectX applications.
In the next article we will play a lot with images. You will learn how to blit them on the surface and learn how to create nice rectangle effects. We will reuse a lot of code we have written here, so you can go out and explore those DirectX classes.

Have fun coding!!!


Similar Articles