PDA

View Full Version : Writing Games with DelphiX's DXDraw



TheLion
04-01-2003, 09:08 AM
Writing Games with DelphiX's DXDraw
by Armand Postma


CONTENTS
1. What is the DelphiX?

2. Using DelphiX

3. High Frequency Redrawing

4. Retrieving the screensize

5. Running fullscreen

6. Don't forget to Flip!

7. Drawing Images
7.1 Using the DXImageList
7.2 Drawing directly on the Surface

8. Transparency
8.1 Transparency with the DXImageList
8.2 Transparency when drawing directly on the Surface

9. Using the DXDraw Canvas

10. When an error is given on the Draw method?¢_¬¶

11. DelphiX Game Template!

12. Contact



1. What is the DelphiX?

DelphiX is a Delphi component collection that has been written by a man called Hiroyuki Hori. It enables programmers to conveniently use MS DirectX, because it works much easier and saves the time of writing it all from scratch. The latest DelphiX version has 13 components:

- TDXDraw: DirectDraw and Direct3D component.
- TDXDIB: Component by which DIB is maintained.
- TDXImageList: List of Tpicture. Surface is controlled.
- TDX3D: The Direct3D component (works with TDXDraw).
- TDXSound: DirectSound component.
- TDXWave: Component by which Wave is maintained.
- TDXWaveList: The list of Wave. The buffer is controlled.
- TDXInput: Input component. (Keyboard and Joystick).
- TDXPlay: Communication component.
- TDXSpriteEngine: Sprite engine.
- TDXTimer: High-speed timers (better than TTimer).
- TDXPaintBox: DIB version of TPaintBox.
- TDXForm: Optimizes forms for DelphiX.

Unfortunately, there hasn't been a DirectX 8 update for DelphiX. However, DelphiX is most commonly used by 2D game-programmers and DirectX 8 hasn't changed anything for 2D programmers and still provides the DirectX 7 Surface there is no problem for us!


2. Using DelphiX

DelphiX is a cool package and after installing it you see 12 cool new components (one is not visual) in your Delphi component-palette. When you examine the components you'll notice that there's a component for everything you ever want to do! However, in this tutorial I will only describe the use of 2 DelphiX components: TDXDraw and TDXImageList. The reason for this is quite simle. A few years ago I wrote my first game using DelphiX, a space invaders clone, and it looked very nice. I used the DXDraw component, the DXImageList and the DXSpriteEngine like all the demo files that are provided with DelphiX do. The problem was however that when the game ran for about 5 to 10 minutes the 2nd row of my aliens escaped of my screen and where never to be seen again, even thought I wrote a piece of code that moved all the aliens (every row) the same number of pixels to the left or to the right. However I made the SpriteEngine do the position calculations and things like that, so it screwed up. Since then I have decided that Delphi in combination with my knowledge of math (which is not THAT much) and my programming skills do all the thinking and I would let DelphiX worry about the drawing and ONLY about the drawing!
However if you need high-level sound handling (like two sounds at the same time), you could consider using the DXSound component, I have had no problems with that, but barely use it now a days. However I will only explain the drawing part of DelphiX in this tutorials.

I use DelphiX for the graphical parts of my game, because its one of the fastest drawing-surfaces out there (well there is also JEDI-SDL, which belongs to the fastest too). I use DelphiX when I write games that require too much graphical handling for the Windows Canvas, or when I write games that have to work fast on slower machines (P200, P400, etc.), because it really is a fast way of drawing stuff, even when you draw multiple layers!

Of course it would be nice if you would have the latest DelphiX version installed on your computer. It can be downloaded from Hori's page that can be found at: http://www.yks.ne.jp/~hori/ The installation procedure is very simple and is explained in the readme.txt file provided with the DelphiX zip-package. Another thing you need to have installed is Microsoft's DirectX 7 or higher, this doesn't have to be the DirectX SDK, because the runtime version works just as well.

Note. If you have an computer with more than 1.2 GHz running Win98 you could experience some compilation problems in Delphi (in my case it showed a message that an error occured in DDHELP.DLL). I haven't heard of a solution for this problem and found that the only solution is to switch either to Windows NT/2000 or to Windows XP.

If you have installed both DirectX 7 or higher and DelphiX you are ready to begin programming games using DelphiX!


3. High Frequency Redrawing

As you might, or might not know windows redraws its windows very frequently in an event called paint, which is a very good thing, it makes sure the window always looks as good as we want it to look. However when we draw an image upon the form and windows redraws the form our image is gone. For example when we draw an image on the form and then minimise the form and restore it to its original size, the image will be gone, because the paint event is called when the window state of a form is changed. The solution to this problem is easy, we redraw the screen as many times as possible, which will also help us to update the data on the screen often, so when a character moves this will be shown very fluently! For programmers who have had some experience in game-programming with DelphiX or DirectX this isn't very new, because you'll have to redraw a DirectX surface as many times as possible to keep seeing the image and to get fluent movements. So what we need is a loop that keeps refreshing our screen with a high frequency. There are three ways of accomplishing such a game loop without the use of DelphiX:

- By using a TTimer object with a low interval number
- By overriding the Paint event
- By using the Application.OnIdle event with done set to false

I do not like the use of the TTimer object when writing games, because its another component and components use memory and because I think its just not fast enough for the task at hand! Overriding the Paint event of the Form has the right speed and doesn't use extra memory. However I would never use it, because what it actually does is capture every WM_PAINT message that the application receives and this means that when you would place a nice button on your form it will not be visible. The WM_PAINT message that was going to tell the button to redraw has never arrived, because it was captured by the Paint event-override! The best way of assuring a high frequency update of your scene is by using the Application.OnIdle event, with done set to false. By using this method we will not use memory, because the event will be triggered anyway and it will not be in the way of normal operations, because this event is not used in any normal application! Using this method is very easy. All you have to do is declare a new event-handler (a procedure) in the forms private declaration like this:

type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
procedure GameLoop(Sender: TObject; var Done: Boolean);
public
{ Public declarations }
end;


Then you have to add a new procedure called TForm1.GameLoop to your unit like this:

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

// Game Stuff Goes here...

End;


Finally we have to assign our event-handler to the Application.Idle event and this we immediately when the form is created. To do this, double-click on your form and Delphi will add the TForm1.OnCreate procedure to your unit. Add the following line to the procedure (between begin and end):

Application.OnIdle := Form1.GameLoop;

Now we have assigned our event-handler to the OnIdle event, so every time an Idle-event occurs our GameLoop procedure will be called and due to the fact that we put Done := False in the procedure, the gameloop event will be infinite. The effect is the same as if we would create an endless loop with a while statement, but by using a while statement all the other events would be neglected and the application would get an overflow. By using the OnIdle event, we will get the same end-effect as with an endless while-loop, but the other events will still occur and we will not get an overflow!


4. Retrieving the screensize

Say you would like to have a tiled background, for example a wooden texture that looks like a table, and the tile you are going to use is 128x128 pixels, how many tiles do I need to draw until my entire screen is filled? This question can't be answered before we know the width and height of the screen. Well you could easily look into the display properties and find that your screen has a resolution of 1024x768 pixels, but when you give your game to a friend who has his resolution set to 800x600 your game would look pretty bad! So it's not that easy to know the width and height of your screen because it can vary from machine to machine, but luckily for us, its almost that easy to find out the resolution of our screen!
The Windows API has a build in function that will provide us with exactly the information we need! The function I'm talking about is GetSystemMetrics. To find out our resolution we will call it twice, one to find out the width of your screen and once to find out the height. Because this is essential information that could be needed immediately we place these calls in the OnCreate event of our form, so we will have access to the screensize-values immediately after the form has been created!

To do this we first have to declare 2 variables called VideoModeX and VideoModeY of the SmallInt data-type. I always declare them in the var section that can be found right above the word implementation, however if you wish to retrieve that screensize-values from other forms, you could declare the variables in the public declarations section of your form. After the variables have been declared the var section will look something like this:


var
Form1: TForm1;
VideoModeX, VideoModeY : SmallInt;


When we then click on our form we will be brought back to our forms OnCreate event. After we have added the GetSystemMetrics function-calls to the procedure it will look as follows:

procedureTForm1.FormCreate(Sender: TObject);
begin
// Retrieve the screensizes...
VideoModeX := GetSystemMetrics(SM_CXSCREEN);
VideoModeY := GetSystemMetrics(SM_CYSCREEN);

// Assign the Gameloop event-procedure to the Application.OnIdle event
Application.OnIdle := Form1.GameLoop;
End;


Now we can find the answer easily if our VideoModeX is 1024 and our VideoModeY is 768 that we need to draw:
Horizontally: 1024 div 128 = 8 tiles to fill the screen.
Vertically: 768 div 128 = 6 tiles to fill the screen.


5. Running fullscreen

The DXDraw component has an option to run fullscreen, so we only have to set it to run our program fullscreen. This would be true if this option would work on every computer, unfortunately it doesn't. On some computers using the DXDraw fullscreen mode will cause the application move a lot to the bottom-right corner and so the application will only be visible partly and it still doesn't run fullscreen. However we can use a little trick to run fullscreen on every computer using DelphiX.

The screen size we have retrieved is the size of the entire screen, however our form doesn't cover the entire screen, but only a part of it. Of course we could maximize our form, but then our form still wouldn't have the same size as our screensize, because of the title-bar!

There is a small trick that is commonly used to make our form have the same dimensions as the screen-dimensions. We turn off the border of our form and then maximize it. This way we will get a form that covers the entire screen. We do this in our forms OnCreate event. So after adding the fullscreen mode to our OnCreate event-handler the event-handler will look like this:

procedure TForm1.FormCreate(Sender: TObject);
begin
// Retrieve the screensizes...
VideoModeX := GetSystemMetrics(SM_CXSCREEN);
VideoModeY := GetSystemMetrics(SM_CYSCREEN);

Form1.BorderStyle := bsNone;
Form1.WindowState := wsMaximized;

// Assign the Gameloop event-procedure to the Application.OnIdle event
Application.OnIdle := Form1.GameLoop;
End;


If you want to make sure that no other application runs on top of your form, you could add the following line to the event-handler, right after the Form1.WindowState := wsmaximized line:

Form1.FormStyle := fsStayOnTop;

This line will tell windows that our form will always be on top, no matter what other applications are being started, we will be on top!

A problem that will occur because we removed the titlebar is that we won't be able to close our application. This can easily be solved by creating an event-procedure that handles key pressing and by closing the application when the ESCAPE key is pressed. This is done by selecting the Events tab in the Object Inspector and double clicking on the OnKeyDown event, this will create an OnKeyDown event-handler. All we have to do is add a few lines which handle the program-close-on-escape procedure. The OnKeyDown event-handler will look like this after the lines have been added:

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin { If the ESCAPE key has been pressed then close the program! }
If Key = VK_ESCAPE then
Begin
SendMessage(Handle, WM_CLOSE, 0, 0);
End;
end;


Now all you have to do is press the ESCAPE key and the application will close itself!


6. Don't forget to Flip!
If we would start drawing on the DXDraw surface we would still see nothing, because everything we would draw would end up in the back-buffer of DirectX. DirectX uses a technique called double-buffering to speed up drawing even more. Double buffering is the technical term for a drawing method that is often also referred to as "offscreen drawing". The name indicates that we do not draw on the screen, but somewhere else and that's exactly what double-buffering means! When we double-buffer we do not draw to the screen, but to our computers memory. Since a computer's memory is one of the fastest media available today, drawing to the memory will be a whole lot faster than drawing directly to the screen. So when we draw we actually draw into the computers memory. To make it visible on the screen we only have to call the DelphiX command Flip, which will "flip" the back-buffer to the screen and make it visible. So every time we have finished drawing our entire scene we flip. So to make it easy on ourself we just add the command DXDraw1.Flip to the Gameloop procedure. This makes our GameLoop look something like this:

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

// Game Stuff Goes here...

DXDraw1.Flip;
End;



7. Drawing Images
If you have not placed a DXDraw component, this would be the moment to do so, because when we draw we have to have something to draw on! At this point we have to make a choice, because there are two ways to draw images to the surface of DXDraw:
Let DXImageList worry about the actual drawing.
Draw directly to the surface of DXDraw.
Normally the choice would be easy, because we would just choose the fastest method, but I do not believe that there is a noticeable difference between both methods. The downside of the first is that you have to use another component and have to fill it with images. The plus of using a DXImageList is that it stores its images inside the application and when compiled the images are compressed, so you get a bigger exe, but your total application will be smaller. The plus of the second is that you are totally in control and that it resembles drawing to the Windows Canvas. I prefer the second way, because I like total control when I am programming, but I will explain both methods to let you make your own choice!

7.1 Using the DXImageList

As said before it has a downside, but also a very important plus the application size! However when you write a dynamic application where the images can change without having to recompile the exe, the plus becomes a downside. However when you would need dynamic images it would still be possible to use the DXImageList, because you can also fill it during runtime!

Before we can begin using the DXImageList we have to add a DXImageList component to our form, so select it from the component palette and place it on the form. Then we will have to tell the DXImageList what surface it should use to draw upon. This surface will be our DXDraw component and to link the DXImageList to the DXDraw component we have to set the DXDraw property of the DXImageList in the object inspector. The easiest way to do this is to simply click two times on the property in the object inspector (make sure the DXImageList component is selected) and the name of our DXDraw component will appear, in my case it is called "DXDraw1". If this property is not set, you cannot draw to the surface, so everything you draw with the DXImageList component will just not be visible. Then we need images to draw of course. We can add them to our DXImageList by using the object inspector. Just double-click on the property Items and an edit window will appear.

http://terraqueous.f2o.org/dgdev/tutorials/editwindw.jpg

In this window you can either add or remove images from the DXImageList. To add an image you have to click on the only available button in the entire window:

http://terraqueous.f2o.org/dgdev/tutorials/addimgbutton.jpg

When you have clicked on it a new DXImageList item will appear in the white part of the edit window.. It is probably called "0 - TpictureCollectionItem" and when you click on it its properties will be visible in the object inspector. It has the look and feel of a VCL Timage object and works about the same way, so to select an image we just have to double-click on the picture property. Then you can put in a name for the image in the name property, but this is not required, because we will use numbers to select the images, like in an array. If you want to place another image you click on the same button as you did before and repeat the whole process until you are done adding pictures!

Okay, now your DXImageList is filled with images and we can start drawing. Say we want to draw our 1st image on position (15, 10) on the DXDraw surface we would simply add the following line to the GameLoop, before we flip the back-buffer to the screen (before the DXDraw1.Flip command):

procedure DXImageList1.Items[0].Draw(DXDraw1.Surface, 15, 10, 0);

Allright, that looks impressive, but how does it work? Well its not that hard actually, the first part of the command specifies which image in the DXImageList we want to draw in our case the first (so number 0) so by specifying DXImageList1.Items[0] we select our first image then we tell the DXImageList we want to draw the image on something by using the command Draw. For the draw command to work, we must answer four questions:
1. Question: Where do I have to draw on? Answer: DXDraw1.Surface
2. Question: How many to the right should I draw (X coordinate)? Answer: 15 pixels
3. Question: How many to the bottom should I draw (Y coordinate)? Answer: 10 pixels
4. Question: What pattern do I have to draw? Answer: No Pattern (0)

The fourth question is not very important for us, so we just always answer it with 0. So when we put this all together you get the command line that looks like the one we specified above. And when we add it to our GameLoop, the gameloop will look as follows:

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

// Game Stuff Goes here...
DXImageList1.Items[0].Draw(DXDraw1.Surface, 15, 10, 0);

DXDraw1.Flip;
End;


That's all there is to drawing to the DXDraw component when you use the DXImageList component. It may look a bit difficult at first, but once you get the hang of it, its quite easy!

7.2 Drawing directly on the Surface

A lot of people think this is much harder than using the DXImageList, but for those who read my Canvas Game Programming tutorial this method may actually look very easy, because it has a lot of similarities with the Canvas.Draw method and its not as long-winded as using the DXImageList.

However before we can start drawing we have to have something to draw. Normally we would load our into a Tbitmap object, but since DirectX doesn't support Bitmaps as a native-format we will have to use something else. DirectX's native image-format is called DDS (Direct Draw Surface) so we could of course make a TDirectDrawSurface object for each image we are going to need and give the object names like CarImage etc. This would work fine when we would be using two or three images, but in game programming we mostly use more images. In a case like this we create arrays of TDirectDrawSurface objects. This means that we will create a variable that can hold a whole pile of TDirectDrawSurface objects. I will call this variable GameGraphics. Say we need 5 images. In that case we would declare an Array that consists of 5 TDirectDrawSurface objects in the var section of our unit. If we would declare the array of 5 TDirectDrawSurface objects in our var section it will look something like this:

var
Form1: TForm1;
VideoModeX, VideoModeY : SmallInt;
GameGraphics: Array[0..4] of TDirectDrawSurface;


Now that we have declared our GameGraphics array, we will have to fill it with images, but before we can fill the TDirectDrawSurface objects with images, we first have to create the images (remember we only have reserved memory for the object). So the best thing to do would be to create a procedure to load the images from the harddrive into the memory. We declare this procedure in the public section of our form again and call it LoadGameGraphics. After we declared it our public section will look as follows:

type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
procedure GameLoop(Sender: TObject; var Done: Boolean);
public
{ Public declarations }
procedure LoadGameGraphics;
end;


After we have declared the LoadGameGraphics procedure we will have to write the actual procedure somewhere in our form. When we do so, the procedure will look like this:

Procedure TForm1.LoadGameGraphics;
var D : Integer;
Begin
For D := 0 to 4 do
GameGraphics[D] := TDirectDrawSurface.Create(DXDraw1.DDraw);

GameGraphics[0].LoadFromFile(ExtractFilePath(ParamStr(0)) + 'image1.bmp');
GameGraphics[1].LoadFromFile(ExtractFilePath(ParamStr(0)) + 'image2.bmp');
GameGraphics[2].LoadFromFile(ExtractFilePath(ParamStr(0)) + 'image3.bmp');
GameGraphics[3].LoadFromFile(ExtractFilePath(ParamStr(0)) + 'image4.bmp');
GameGraphics[4].LoadFromFile(ExtractFilePath(ParamStr(0)) + 'image5.bmp');
End;


This procedure first creates the TDirectDrawSurface images in a for loop and then loads the images from bitmaps that are in the same directory as the application is located. With the function-call ExtractFilePath(ParamStr(0)) we retrieve the directory where the application is located and then we add the filename of the image. Of course if you would use this procedure you have to place 5 images in the directory where the application is located and you would have to rename the files or change the name of the files in the LoadGameGraphics procedure to the names of your images. This is another great feature of using DelphiX, because we can load Bitmap images into DDS images exactly the same way as we would load an image into a Tbitmap object. To do this from scratch you would need a pretty big procedure! The only thing left is make a call to this procedure when the application starts So in our OnCreate event-handler. However this call can only be made when the DXDraw object is allready created and fully initialized, so before the call to the LoadGraphics procedure can be done we first have to make a call to the DXDraw.Initialize procedure to make sure the DXDraw component is fully initialized and created! After doing this our OnCreate event-handler will look like this:

procedure TForm1.FormCreate(Sender: TObject);
begin
// Retrieve the screensizes...
VideoModeX := GetSystemMetrics(SM_CXSCREEN);
VideoModeY := GetSystemMetrics(SM_CYSCREEN);

// Resize the form so our game runs in fullscreen mode!
Form1.BorderStyle := bsNone;
Form1.WindowState := wsMaximized;

DXDraw1.Initialize;
LoadGameGraphics;

// Assign the Gameloop event-procedure to the Application.OnIdle event
Application.OnIdle := Form1.GameLoop;
End;


Now that we have filled our GameGraphics array with DDS images we can start drawing them to the screen. Let's say I wanted to draw our first image to the surface at position (15, 10). To do this we simply add a line in our GameLoop before the Flip command. This line would look as follows:

DXDraw1.Surface.Draw(15, 10, GameGraphics[0], False);

The first two values are to specify the x and y values of the top-left corner of our image so in our case that is X:15 and Y:10 then we specify which image to draw so we tell the command we want the first image drawn so we provide GameGraphics[0]. Finally the last part of this command is if we want the image drawn transparent, however I will come back on this subject later, but in this case we told the command not to draw transparent.

After adding this line to the GameLoop it will something like this:

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

// Game Stuff Goes here...
DXDraw1.Surface.Draw(15, 10, GameGraphics[0], False);

DXDraw1.Flip;
End;


It's that easy! As you can see, not half so long-winded as the DXImageList and it must seem very familiar for people who have ever worked with the windows Canvas!


8. Transparency

A big problem in computer graphics is that a bitmap (or any other image format) is always a rectangle, while sometimes we want a disk, a triangle or another shape for our sprites. In game programming this problem is solved by using transparency.

http://terraqueous.f2o.org/dgdev/tutorials/transparency.jpg

In the image shown above, you see the same image twice. The image on the left does not use transparency and the image on the right uses transparency, which provides the effect that the spacecar stands on a grassy landscape much more realistic.

Transparency is done different in both methods, although the basic theory is the same. They both use a transparency colour to do transparency. In general we choose the colour Fuchsia for transparency, because it is almost never used in images.

8.1 Transparency with the DXImageList

Using transparency with the DXImageList is very easy, because you can do it entirely with the object inspector. Say you have added the car above to the DXImageList and its the first item in the list. Before we can do transparency we first have to select the item from the itemlist so the object inspector will show the properties of our item.

http://terraqueous.f2o.org/dgdev/tutorials/transpwin.jpg

To do transparency with the colour fuchsia we have to set the TransparentColor property to clFuchsia and the Transparent property to True. Mostly DXImageList recognizes the transparent-colour automatically, however sometimes you have to do it manually and you always have to check it! When both these properties are set correctly you are ready to draw the image and when done so, the image will be drawn transparent.

8.2 Transparency when drawing directly on the Surface

This is a bit harder than using the DXImageList, because it is all done with code, however once you get the hang of this, it is much faster to do than using the DXImageList. Sometimes you can even use a for- or a while-loop and that will save you a lot of coding and therefore time!

To do transparency when drawing directly on the surface with the colour fuchsia we only need two lines of code. In the first line of code we specify the transparency-colour with the TransparentColour command, so when we would use the transparency-colour fuchsia on the first image of our GameGraphics array it would look something like this:

GameGraphics[0].TransparentColor := clFuchsia;

This line can be both called before drawing or in the LoadGraphics procedure. Calling in the LoadGraphics procedure would be the best option because it is only called once and so it saves time and therefore speed! Remember however if you would call the command TransparentColor from the LoadGraphics procedure that you have to call it after the loading of the images otherwise it won't work!

Then we have to draw the image transparent to the screen. When using this method the draw-line will change slightly, because we have to specify that we want to draw transparent. To draw the image transparent to the screen, our draw-line will look like this:

DXDraw1.Surface.Draw(15, 10, GameGraphics[0], True);

The only difference between the non-transparent drawing-line and the transparent one is the True value at the end. By specifying true we tell the Draw command to draw the image transparent using the transparent-colour we have specified before! Pretty easy isn't it?


9. Using the DXDraw Canvas

DXDraw, like many other visual Delphi components, features a Windows Canvas implementation. This enables us to draw lines, rectangles and pixels to the screen without the need of images. I will not go into the details on how to use this canvas, because it works exactly the same as the Windows Canvas and if you need any help using it, you can read my Canvas tutorial on this subject. However there is a tiny little difference between the normal Canvas and the DXDraw canvas, so I will discuss that difference in this tutorial.

The first thing I will have to explain is how to call the DXDraw canvas. The canvas is not directly a part of the DXDraw component, but more a part of the Surface, so to when we call the canvas it will look like DXDraw.Surface.Canvas followed by a canvas command like rectangle. To clearify I will show an example of how to draw a rectangle using the DXDraw canvas:

DXDraw1.Surface.Canvas.Rectangle(50, 50, 100, 100);

This line will draw a rectangle from coordinate (50, 50) to (100, 100).

When we would just put this line in our GameLoop our entire rectangle wouldn't be visible and all the graphics drawn after the rectangle won't be visible either?¢_¬¶ This is the real difference I was talking about. The drawing itself is pretty much the same, however the use of the canvas is a special part of DXDraw and to show whatever we have drawn on the canvas and everything after that we have to release the canvas. It can be compared with flipping the back-buffer to the screen, only with the canvas it is called releasing. To do this we call the command Canvas.Release every time that we are finished drawing to the canvas. So when we only needed to draw the rectangle to the canvas our code would look something like this:

DXDraw1.Surface.Canvas.Rectangle(50, 50, 100, 100);
DXDraw1.Surface.Canvas.Release;


I have found that it is a good idea to release the canvas before we flip the back-buffer, every time in the GameLoop, so we can never forget it. However this solves some problems but not all of them, it can still happen that you draw something that you will not see on the screen when you have forgotten to release the canvas!


10. When an error is given on the Draw method?¢_¬¶

I noticed that older versions of Delphi (4 or lower) or older versions of DelphiX have some problems with the Draw method. This could be because there are two different Draw procedures or that older versions of DelphiX do not feature the draw method used in this tutorial. If the draw method doesn't work in your version, you can use the following procedure for drawing. You have to declare the procedure in the public declarations of your form so the public declarations will look like this:

type
TForm1 = class(TForm)
DXDraw1: TDXDraw;
procedure FormCreate(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
{ Private declarations }
procedure GameLoop(Sender: TObject; var Done: Boolean);
public
{ Public declarations }
procedure LoadGameGraphics;
Procedure DrawImage(X, Y : Integer; DDS : TDirectDrawSurface; Transparent : Boolean);
end;


Then you can have to place the following procedure somewhere in your unit:

Procedure TForm1.DrawImage(X, Y : Integer; DDS : TDirectDrawSurface; Transparent : Boolean);
var SourceRect : TRect;
Begin
SourceRect := Rect(0, 0, DDS.Width, DDS.Height);
DXDraw1.Surface.Draw(X, Y, SourceRect, DDS, Transparent);
End;


Now you can use this procedure exactly the same way you would otherwise use DXDraw.Surface.Draw. So when we would draw the first image of our GameGraphics array non-transparent to the screen at (15, 10) then you would use the line:

DrawImage(15, 10, GameGraphics[0], False);

And our gameloop would look like this:

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

// Game Stuff Goes here...
DrawImage(15, 10, GameGraphics[0], False); // not transparent
DrawImage(15, 10, GameGraphics[0], True); // transparent

DXDraw1.Flip;
End;



11. DelphiX Game Template!
I thought a working example of all the preparations that this tutorial features would come in handy while writing your games using DelphiX, so I have included a game template in this zip file. All the preparations that you need for high frequency drawing and the linking of the DXImageList with DXDraw are allready done for you in this Game Template. So it will save you time when you start a new game and it will help you to understand this tutorial! :) The game template consists of two versions, one is intended for use with the DXImageList and the other is intended for drawing directly to the surface!

>>> Click here to download the DelphiX Game Template <<< (http://terraqueous.f2o.org/dgdev/tutorials/delphixtemplate.zip)


12. Contact

If you find any mistakes in my tutorial or if you have any questions, ideas, comments or if you just want to say hello, please post a message in this thread! :)

Of course you can also send me an e-mail at lion.tiger@home.nl or visit
the Lion Productions website at http://lionprod.f2o.org/

I can also be found on the DelphiGames mailinglist (delphigames@yahoogroups.com)
and on the Delphi@Elists mailinglist (delphi@elists.org).


THE END

Copyright Ac 2002 Lion Productions

Karaman
19-02-2003, 09:02 PM
I cant make the GameLoop work for DXTimer Component
The DXDraw Component simply displays Black
it is initialized though

and I think flipped by the GameLoop but ALAS :)

I here see you know about WMessages
I noticed that when I have my DXDraw in full screen the Tray icons which refresh frequently show themselves (I have an icon that flilckers)

Is there a way to make the Form go full screen and on top and to remove taskbar when DXDraw in Full Screen Mode

and I dont SEE (yes, what my eyes do) what StopFlicker does on the screen

TheLion
24-02-2003, 08:56 AM
Sorry that it took me so long to reply to your message... :(

About the DXDraw component showing simply black that could have a few reasons, like not binding the DXImageList to the DXDraw component (if you use the ImageList) or not flipping the surface or flipping the surface too early... To know what is wrong with it I'll have to look at your code... If you want, send me an e-mail at lion.tiger@home.nl and I'll look into it.

NOTE. If you use the DXTimer component you shouldn't use the OnIdle event anymore, since then you'll be flipping the surface twice, which would cause in a black DXDraw object... (I didn't know if you meant this with the GameLoop)

About the StopFlicker method, it doesn't do anything in DelphiX, it only is usefull with the Windows Canvas, I left it in the code since I have a feeling it might save some memory or might increase the drawing speed (since it's useless to draw the form when the DXDraw component is over it!). When used with the Windows Canvas it reduces flickering and even elliminates it sometimes! :)

About your taskbar problem, there are two things you might try the first is to run in "real" fullscreen mode, which normally is what DXDraw would do if you use the doFullScreen mode, however the doFullScreen mode is buggy and doesn't always work, but the windows 98 fullscreen functions work:



How to run an application fullscreen? (Taken from UDDF)

From: "Mike Lischke" <Lischke@imib.med.tu-dresden.de>

Running an application full screen means that the application window covers the entire desktop area. This is often necessary because of some video cards which can only accelerate full screen apps., but you may also often want that nothing else than your program is visible to the user. BTW: Running fullscreen isn't only related to OpenGL, DirectX or 3D in general. Strictly taken would fullscreen only require that you set your window state to wsMaximize, that's all.

But there's another question implied by asking for fullscreen applications. It's the point that you either may want to let the user choose a specific color and pixel resolution or you want to run your program in a fixed resolution. The latter is in particular very important since not all video cards support all resolutions and often a game or other 3D application has to run in a different (mostly lower) resolution than the user uses for everday work.

So the complete question should read as: How to run an application fullscreen in a specific color and pixel resolution (whithout reboot)? The key point is the function ChangeDisplaySettings. Depending on the video driver you can set many video modes dynamically, without rebooting the computer:


--------------------------------------------------------------------------------

function SetFullscreenMode(ModeIndex: Integer) : Boolean;
// changes to the video mode given by 'ModeIndex'
var DeviceMode : TDevMode;
begin
with DeviceMode do
begin
dmSize:=SizeOf(DeviceMode);
dmBitsPerPel:=VideoModes[ModeIndex].ColorDepth;
dmPelsWidth:=VideoModes[ModeIndex].Width;
dmPelsHeight:=VideoModes[ModeIndex].Height;
dmFields:=DM_BITSPERPEL or DM_PELSWIDTH or DM_PELSHEIGHT;
// if mode set failed, we'll just run in windowed mode
Result:=ChangeDisplaySettings(DeviceMode,CDS_FULLS CREEN) = DISP_CHANGE_SUCCESSFUL;
if Result then ScreenModeChanged:=True;
if ModeIndex = 0 then ScreenModeChanged:=False;
end;
end;


--------------------------------------------------------------------------------

As you may have noticed there's a global variable VideoModes in this example. The reason is that you should enumerate all available video modes which can be set dynamically and store them into a structur like VideoModes, to ensure only these are tried to be set:


--------------------------------------------------------------------------------

const MaxVideoModes = 200; // this isn't very much actually
type TVideoMode = record
Width,
Height,
ColorDepth : Word;
Description : String[20];
end;

var VideoModes : array[0..MaxVideoModes] of TVideoMode;
NumberVideomodes : Integer = 1; // 1 because we have a default mode


--------------------------------------------------------------------------------

This makes our example much larger as you will see, but you'll get a very useful functionality by implementing it. If you still want you can replace VideoModes in the above function by fixed values (say 640, 480, 16). Enumerating all video modes is done by EnumDisplaySettings:


--------------------------------------------------------------------------------

procedure ReadVideoModes;
var I, ModeNumber : Integer;
done : Boolean;
DeviceMode : TDevMode;
DeskDC : HDC;

begin
// prepare 'default' entry
with VideoModes[0] do
try
DeskDC:=GetDC(0);
ColorDepth:=GetDeviceCaps(DeskDC,BITSPIXEL);
Width:=Screen.Width;
Height:=Screen.Height;
Description:='default';
finally
ReleaseDC(0,DeskDC);
end;

// enumerate all available video modes
ModeNumber:=0;
done:=False;
repeat
done:=not EnumDisplaySettings(nil,ModeNumber,DeviceMode);
TryToAddToList(DeviceMode);
Inc(ModeNumber);
until (done or (NumberVideomodes >= MaxVideoModes));

// low-res modes don't always enumerate, ask about them explicitly
with DeviceMode do
begin
dmBitsPerPel:=8;
dmPelsWidth:=42;
dmPelsHeight:=37;
dmFields:=DM_BITSPERPEL or DM_PELSWIDTH or DM_PELSHEIGHT;
// make sure the driver doesn't just answer yes to all tests
if ChangeDisplaySettings(DeviceMode,CDS_TEST or CDS_FULLSCREEN) <> DISP_CHANGE_SUCCESSFUL then
begin
I:=0;
while (I < NumberLowResModes-1) and (NumberVideoModes < MaxVideoModes) do
begin
dmSize:=Sizeof(DeviceMode);
dmBitsPerPel:=LowResModes[I].ColorDepth;
dmPelsWidth:=LowResModes[I].Width;
dmPelsHeight:=LowResModes[I].Height;
dmFields:=DM_BITSPERPEL or DM_PELSWIDTH or DM_PELSHEIGHT;
TryToAddToList(DeviceMode);
Inc(I);
end;
end;
end;
end;


--------------------------------------------------------------------------------

I think this function isn't hard to understand. There are two parts to consider. The first is the standard way to enumerate the video modes. The second ensures that also all low-res modes are tested . This requires, though, a list of low-res modes:


--------------------------------------------------------------------------------

type TLowResMode = record
Width,
Height,
ColorDepth : Word;
end;

const NumberLowResModes = 60;
LowResModes : array[0..NumberLowResModes-1] of TLowResMode =
((Width:320;Height:200;ColorDepth:8),(Width:320;He ight:200;ColorDepth:15),
(Width:320;Height:200;ColorDepth:16),(Width:320;He ight:200;ColorDepth:24),
(Width:320;Height:200;ColorDepth:32),(Width:320;He ight:240;ColorDepth: 8),
(Width:320;Height:240;ColorDepth:15),(Width:320;He ight:240;ColorDepth:16),
(Width:320;Height:240;ColorDepth:24),(Width:320;He ight:240;ColorDepth:32),
(Width:320;Height:350;ColorDepth: 8),(Width:320;Height:350;ColorDepth:15),
(Width:320;Height:350;ColorDepth:16),(Width:320;He ight:350;ColorDepth:24),
(Width:320;Height:350;ColorDepth:32),(Width:320;He ight:400;ColorDepth: 8),
(Width:320;Height:400;ColorDepth:15),(Width:320;He ight:400;ColorDepth:16),
(Width:320;Height:400;ColorDepth:24),(Width:320;He ight:400;ColorDepth:32),
(Width:320;Height:480;ColorDepth: 8),(Width:320;Height:480;ColorDepth:15),
(Width:320;Height:480;ColorDepth:16),(Width:320;He ight:480;ColorDepth:24),
(Width:320;Height:480;ColorDepth:32),(Width:360;He ight:200;ColorDepth: 8),
(Width:360;Height:200;ColorDepth:15),(Width:360;He ight:200;ColorDepth:16),
(Width:360;Height:200;ColorDepth:24),(Width:360;He ight:200;ColorDepth:32),
(Width:360;Height:240;ColorDepth: 8),(Width:360;Height:240;ColorDepth:15),
(Width:360;Height:240;ColorDepth:16),(Width:360;He ight:240;ColorDepth:24),
(Width:360;Height:240;ColorDepth:32),(Width:360;He ight:350;ColorDepth: 8),
(Width:360;Height:350;ColorDepth:15),(Width:360;He ight:350;ColorDepth:16),
(Width:360;Height:350;ColorDepth:24),(Width:360;He ight:350;ColorDepth:32),
(Width:360;Height:400;ColorDepth: 8),(Width:360;Height:400;ColorDepth:15),
(Width:360;Height:400;ColorDepth:16),(Width:360;He ight:400;ColorDepth:24),
(Width:360;Height:400;ColorDepth:32),(Width:360;He ight:480;ColorDepth: 8),
(Width:360;Height:480;ColorDepth:15),(Width:360;He ight:480;ColorDepth:16),
(Width:360;Height:480;ColorDepth:24),(Width:360;He ight:480;ColorDepth:32),
(Width:400;Height:300;ColorDepth: 8),(Width:400;Height:300;ColorDepth:15),
(Width:400;Height:300;ColorDepth:16),(Width:400;He ight:300;ColorDepth:24),
(Width:400;Height:300;ColorDepth:32),(Width:512;He ight:384;ColorDepth: 8),
(Width:512;Height:384;ColorDepth:15),(Width:512;He ight:384;ColorDepth:16),
(Width:512;Height:384;ColorDepth:24),(Width:512;He ight:384;ColorDepth:32));


--------------------------------------------------------------------------------

What remains is the function TryToAddToList:


--------------------------------------------------------------------------------

procedure TryToAddToList(DeviceMode: TDevMode);
// Adds a video mode to the list if it's not a duplicate and can actually be set.
var I : Integer;
begin
// See if this is a duplicate mode (can happen because of refresh
// rates, or because we explicitly try all the low-res modes)
for I:=1 to NumberVideomodes-1 do
with DeviceMode do
if ((dmBitsPerPel = VideoModes[I].ColorDepth) and
(dmPelsWidth = VideoModes[I].Width) and
(dmPelsHeight = VideoModes[I].Height)) then Exit; // it's a duplicate mode

// do a mode set test (doesn't actually do the mode set, but reports whether it would have succeeded).
if ChangeDisplaySettings(DeviceMode,CDS_TEST or CDS_FULLSCREEN) <> DISP_CHANGE_SUCCESSFUL then Exit;

// it's a new, valid mode, so add this to the list
with DeviceMode do
begin
VideoModes[NumberVideomodes].ColorDepth:=dmBitsPerPel;
VideoModes[NumberVideomodes].Width:=dmPelsWidth;
VideoModes[NumberVideomodes].Height:=dmPelsHeight;
VideoModes[NumberVideomodes].Description:=Format('%d x %d, %d bpp',[dmPelsWidth,dmPelsHeight,dmBitsPerPel]);
end;
Inc(NumberVideomodes);
end;


--------------------------------------------------------------------------------

To make your implementation complete, you'd need a function to restore the default video mode, after your program exits:


--------------------------------------------------------------------------------

procedure RestoreDefaultMode;
// restores default desktop video mode
var T : TDevMode absolute 0; // a little trick to create a nil pointer
begin
// Since the first parameter must be a var, we cannot use nil directly. Instead
// we use a variable with an absolute address of 0.
ChangeDisplaySettings(T,CDS_FULLSCREEN);
end;

The second method is to hide the windows taskbar (the code is for 95, but should work in 98/NT too I think, otherwise there are a lot of examples to do so on the internet) :


Hiding Windows 95 Taskbar (Taken from UDDF)

From: "James D. Rofkar" <jim_rofkar%lotusnotes1@instinet.com>

Robert Copier wrote:
> Is there a way to hide the Windows 95 statusbar when i start my application made in delphi 2.01. When the user close the application the statusbar must become visible again.

I'm guessing you're referring to the Windows 95 taskbar and system tray window, and not a statusbar. The answer: Sure you can! And what a cool idea! Here's how:

First declare a variable of type HWND to store the Window handle of the Windows 95 taskbar.
--------------------------------------------------------------------------------

TForm1 = class(TForm)
...
private
hTaskBar: HWND;
...
end;


--------------------------------------------------------------------------------

In your main form's OnCreate() event handler, place some code that resembles:
--------------------------------------------------------------------------------

hTaskBar := FindWindow('Shell_TrayWnd', nil);
ShowWindow(hTaskBar, SW_HIDE);


--------------------------------------------------------------------------------

Finally, in your main form's OnDestroy() event handler, code something like:
--------------------------------------------------------------------------------

ShowWindow(hTaskBar, SW_SHOW);

--------------------------------------------------------------------------------

"Earl F. Glynn" <EarlGlynn@WorldNet.att.net>


--------------------------------------------------------------------------------

PROCEDURE HideWin95TaskBar;
VAR
WindowHandle: hWnd;
BEGIN
{Hide the Windows 95 Taskbar}
WindowHandle := FindWindow('Shell_TrayWnd', '');
IF WindowHandle <> 0
THEN ShowWindow(WindowHandle, SW_HIDE)
END {HideWin95TaskBar};


PROCEDURE ShowWin95TaskBar;
VAR
WindowHandle: hWnd;
BEGIN
{Allow the Windows 95 Taskbar to appear}
WindowHandle := FindWindow('Shell_TrayWnd', '');
IF WindowHandle <> 0
THEN ShowWindow(WindowHandle, SW_RESTORE)
END {ShowWin95TaskBar};


I hope this helps. :)

Karaman
24-02-2003, 12:41 PM
I will try it

10x a lot :)

RMX
31-03-2004, 05:26 PM
Thank you very much :)
But I won't found on this tutorial how to rotate images on DXDraw?

TheLion
31-03-2004, 07:06 PM
I haven't tested this, but you should be able to use one of the :

DXDraw1.Surface.DrawRotate()

They should work about the same as the DXDraw1.Surface.Draw() function described in the tutorial above, only with some extra functions that should explain themselves... If you can't get them to work let me know and I'll see if I can play around with it during my spare time! :)

RMX
01-04-2004, 09:33 AM
I'll be happy if you'll help me :)

TheLion
06-04-2004, 01:35 PM
Sorry for the late reply, to be honest I kinda forgot about it until last night! :)

I did look it up for you tho, but it's a WEIRD system and in my opinion it doesn't work as it should, it rotates, but it also translates while rotating, which sucks! :)

To rotate an image in the ImageList 90 degrees, use this:

DXImageList1.Items[0].DrawRotate(DXDraw1.Surface, 0, 0, DXImageList1.Items[0].Width, DXImageList1.Items[0].Height, 0, 0.5, 0.5, 64);

And yes I know you I put 64 there instead of 90, but believe me it rotates 90 degrees.

It's quite easy to use, this is the procedure:

DrawRotate( Surface, X , Y, Width, Height, PatternIndex, CenterX, CenterY, Angle);

Well the Surface, X, Y, Width, Height and PatternIndex parameters present nothing new, you can read all about them in the tutorial above. However the CenterX, CenterY and Angle parameters are new.

The CenterX and CenterY parameters are of the type double, and don't represent pixel coordinates, but more or less abstract values for the "pivot" the image is going to rotate on. The right/bottom side of the image is represented with the value 1.0 and the left/top side is represented by the value 0, the center of the image is also ( 0.5, 0.5 ), when we rotate the image normally we will most likely use the center of the image. Then we get to the angle, which is a rather unique value and I have no clue what system is used, the maximum rotation angle is 256 (= 360 degrees) and the minimum angle is 0 (= 0 degrees). To convert an angle in degrees you can use the following formula:

DelphiXAngle = ( 256 / 360 ) * DegreeAngle.
( 256 / 360 ) * 90 = 64, hence the 64 I used in the above example.

That should do what you want it to do, however the downside of this supplied method is that it also translates the image, which it shouldn't do, ESPECIALLY not when I provide it with a pivot in the centre of the image.

Here's a procedure that draws graphics to the screen using DelphiX drawrotate method without using the ImageList. It also copes with the translation that occurs when rotating images, if you want to use the ImageList object it should be fairly easy to adapt:

Procedure DrawImageRotated(X, Y : Integer; DDS : TDirectDrawSurface; Transparent : Boolean; Angle : Integer);
var SourceRect : TRect;
DXAngle : Integer;
Begin
SourceRect := Rect(0, 0, DDS.Width, DDS.Height);
DXAngle := ( 256 div 360 ) * Angle;
Form1.DXDraw1.Surface.DrawRotate(X + (DDS.Width div 2), Y + (DDS.Height div 2), DDS.Width, DDS.Height, SourceRect, DDS, 0.5, 0.5, Transparent, DXAngle);
End;

To read more about the rotation methods of DelphiX, I would refer you to this website:

http://www.cerebral-bicycle.co.uk/viewdoc.asp?doc=37(at the bottom there is a lot of info on rotation) ;)

I hope this helps! :)

RMX
12-04-2004, 02:16 PM
Thanks, it really helps :)
Now another, very lame question:
Using your tutorial in this thread above I created moving mouse when I press vk_left button. It uses 4 pictures. But when I move mouse to left, older picture wont delete. Sorry for my bad english, I'll try to explain everything with picture:
http://www.polarhome.com:733/~rmx/delphi.gif

TheLion
12-04-2004, 06:36 PM
Hehe, call this function in your gameloop before you call any other drawing functions:

DXDraw1.Surface.Fill(0); // This will clear the screen each frame.

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Begin
Done := False;

DXDraw1.Surface.Fill(0); // Clear screen

// Game Stuff Goes here...

DXDraw1.Flip;
End;



NOTE: If you are going to fill the entire screen, for example with a tiled map or something, then don't call this function, it wouldn't matter graphic wise but it's not the lightest function to call it eats up a few frames! :)



P.S. There are NO lame questions on this forum! ;)

RMX
12-04-2004, 07:29 PM
Thanks you SO MUCH :)
You'r the BEST :twisted:

TheLion
12-04-2004, 10:32 PM
Thanks you SO MUCH :)
No problem! :)


You'r the BEST :twisted:

LOL, I wish that where true mate! ;)