PDA

View Full Version : Programming Games on the Windows Canvas



TheLion
03-01-2003, 03:05 PM
Programming Games on the Windows Canvas
by Armand Postma


CONTENTS

1. What is the Canvas?

2. Using the Canvas for Game Programming

3. High Frequency Redrawing

4. Solving the Flickering-Image problem

5. Retrieving the screensize

6. Running fullscreen

7. Double Buffering

8. Drawing
8.1 The Canvas.Draw Method
8.2 Copy Rectangle

9. Transparency
9.1 Transparency with Canvas.Draw
9.2 Transparency with Copy Rectangle

10. About Lines and Rectangles
10.1 Lines
10.2 Rectangles
10.3 About the Pen

11. Notes for the Kylix game-programmer

12. Canvas Game Template!

13. Contact


1. What is the Canvas?

The Windows Canvas is the default drawing surface of Windows. The entire graphical shell
of Windows operates through the Canvas. Every button, every window and yes even the
picture of the beautiful and famous young actress on your background is drawn onto the canvas. This should give you some idea of what the canvas is, it's nothing more than a drawing board, a surface windows, and programmers, can draw on so we can make all those fancy looking programs.

As you could tell from the paragraph above the canvas is one cool piece of software. But windows wouldn't be windows if there wasn?_Tt a downside to all this and so here it is: it's great, but very slow! Which isn't a problem if you use the canvas the way it was intended, drawing buttons and a picture now and then, but if we want to use the canvas for something really dynamic like game-programming or animations the problems are starting to show. Examples of such problems are flickering animations and very slow game graphics.

In this tutorial I?_Tm going to show you how the slow canvas can be used for game programming and for showing animations. This tutorial doesn?_Tt provide miracles, you won?_Tt be able to use the windows canvas to build a great game-engine, it just isn?_Tt suitable for this sort of stuff, but it?_Ts very suitable (especially on fast machines) to build board games and small arcade games.


2. Using the Canvas for Game Programming

As said before the canvas is a very slow drawing surface and all I can do with this tutorial is show some tricks to save memory and how we can make the canvas draw faster, so it is suitable for game programming.

I for one use the windows canvas a lot to program board-games and games that have to be able to run on Windows NT4 machines (where DirectX isn?_Tt fully to almost not supported and on company machines mostly not installed!). The canvas can also be very useful to implement a small animation into your application, for example a logo or an assistant (as can be seen in Microsoft office).

In short, the windows canvas is a great drawing surface for games that use ?_osimple graphics?__ (like board games, small arcade games and such) and to beautify applications.

I like using the canvas, because I like programming without the help of third-party tools and I prefer to write everything needed myself! Unfortunately, I?_Tm not that good, but I try to compensate it by using only everything that is supplied with Borland Delphi, whenever that?_Ts possible.

OK, enough ?_osmall?__-talk, let?_Ts get programming! :)


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?_Tt very new, because you?_Tll 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
- 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?_Tt 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;


If you are using Kylix to program games on the Canvas you have to add the next line below the "// Game Stuff Goes here..." comment:

Application.HandleMessage;

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. Solving the Flickering-Image problem

If we would start drawing on our canvas and started to redraw the drawn image at a high frequency we will encounter another huge problem: the flickering-image problem.
The canvas has some trouble redrawing an image at a high frequency, because the canvas is rather slow itself. The poor thing hasn?_Tt finished doing the actual drawing as it is redrawing again, so it overlaps eachother, which results in a line through the image and sometimes in half images?_ Before we can solve a problem, we have to find out what causes the problem first. In this case the problem isn?_Tt the canvas, but windows. Windows wants to look flashy and cool all the time, so to make its forms look great when they start up and when we move them around on our screen they have to be redrawn all the time. We seen before that windows uses the Paint-event for this, however windows doesn?_Tt think the Paint-event is enough for a form and so it has another event, called the EraseBackGround-event. This event makes sure that the surface of the form (the background of our application) is always nice and neat, but in our case this event is a pain in the butt. Because we want to redraw our scene every millisecond the EraseBackGround event is also called often and takes it off and then we draw our scene on the form again and the EraseBackGround event takes it off again ?_ and so on, which results in a flickering image! So now we know the problem, the solution presents itself: Stop windows from Erasing the background of our form! As we have seen before if we would override the Paint-event in a form, we would catch every Paint-message that is being send to our form, so the only thing we have to do is build an event-procedure that catches every EraseBackGround-event. Well to do this we define a new event-handler (procedure) in the private declaration section of the form:

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


Then finally we have to write our event-handler that will take care of the message and the event-handler looks like this:

Procedure TForm1.StopFlicker(var Msg: TWMEraseBkgnd);
begin
Msg.Result := 1;
end;


There you have it, a solution to the annoying flickering image problem!


5. 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?_Tt 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?_Ts 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?_Tm 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:

procedure TForm1.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.


6. Running fullscreen

The screen size we have retrieved is the size of the entire screen, however our form doesn?_Tt cover the entire screen, but only a part of it. Of course we could maximize our form, but then our form still wouldn?_Tt 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?_Tt 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!


7. Double Buffering

Double buffering is the technical term for a drawing method that is often also referred to as ?_ooffscreen drawing?__. The name indicates that we do not draw on the screen, but somewhere else and that?_Ts exactly what double-buffering means! When we double-buffer we do not draw to the screen, but to our computers memory. Since a computer?_Ts 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.

When we draw a scene in a computer-game we have a lot of small images that make up an entire scene. Say you would have a tiled image of desert-land, a small image of a car and an image of building. When we would fill the entire screen with the tiled image of the desert land, this would give the appearance of a desert and when we would put a building in there it would look like a desert with a building. The player could be the car and drive it around the desert and so we have a created a scene out of three small images?_ When we would draw this scene to the screen, we would draw to the the screen at least three times and fill its memory every time with one of the three images. I think its imaginable that drawing the scene will take some time?_

Now imagine that we would draw only one big image, instead of the three smaller once from our example?_ it would be much, much faster! This is basically what we do with double buffering. We take our three small images and build the entire scene in our memory, instead on the screen and when we are finished building the scene we draw the entire scene to the screen at once. This method of drawing makes our frequent screen update more fluent and is also a good solution for the flickering image problem.

Now that we know what double buffering is, we still have to bring it into practice with Delphi. Before we can bring it into practice however we first have to make some preparations.
The preparations for double buffering are easy, we just have to add a way to our program so we can draw our scene to the memory. In Delphi this is mostly done with a Tbitmap object. So we declare a Tbitmap object in the var section of our unit, so after we have done this our var section will look something like this:

var
Form1: TForm1;
VideoModeX, VideoModeY : SmallInt;
OffScreen : TBitmap


We use a Tbitmap object, because it has it?_Ts own canvas, so we can draw on it just like we would draw onto our forms canvas. However before we can draw on the OffScreen object, we first have to create the object. All we have done by now is declaring the object, which only means that our application will reserve memory for the object to exist in. Because we will need the OffScreen object immediately after the form has been created we will add the creation of our OffScreen object to our OnCreate event-handler too. By now our OnCreate event-handler will look something 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;

OffScreen := TBitmap.Create;
OffScreen.Width := VideoModeX;
OffScreen.Height := VideoModeY;

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


As you can see in the code-snapped above we also set the width and the height of our OffScreen image and we give them the same values as the values from our screensize. We do this so when we draw the offscreen image to the screen the image will actually cover the entire screen. If we would only give half the size of the screen, we would only fill half the screen when we drawing the offscreen image on the screen. Wen we would not provide a height and width value for the offscreen image, we would not see anything on our screen, because then the dimensions of the offscreen image would be 0x0!
By now we have almost finished setting up everything we need for double buffering the only thing left is to draw the offscreen image on the screen. This can be done by adding a single line in our Gameloop. The command we use for that is an API call, called BitBlt. It is the fastest way to draw a big image to the canvas and we are going to use it to copy the canvas from our offscreen object to the forms canvas. After implementing the BitBlt call in our gameloop it will look as follows:

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

// Game Stuff Goes here...

BitBlt(Canvas.Handle, 0, 0, VideoModeX, VideoModeY, OffScreen.Canvas.Handle, 0, 0, SRCCOPY);
End;


The only thing that rests now is drawing to the offscreen object?_Ts canvas, all the rest is done for you by BitBlt!


8. Drawing

Finally, the chapter we have all been waiting for, the drawing chapter! However before we can start drawing we have to have something to draw. With the canvas we have several drawing methods and the canvas supports drawing from pixels to entire images. For starters I will start with the drawing of images, because this is used mostly in game programming. To be able to draw images we have to load our images into a Tbitmap object. We could of course make an Tbitmap 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 Tbitmap objects. This means that we will create a variable that can hold a whole pile of Tbitmap objects. I will call this variable GameGraphics. Say we need 5 images. In that case we would declare an Array that consists of 5 Tbitmap objects in the var section of our unit. If we would declare the array of 5 Tbitmap objects in our var section it will look like this:

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


Now that we have declared our GameGraphics array, we will have to fill it with images, but before we can fill the Tbitmap 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);
procedure StopFlicker(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND;
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] := TBitmap.Create;

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 Tbitmap 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. The only thing left is make a call to this procedure when the application starts, so in our OnCreate event-handler. 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;

// Create the OffScreen image for double-buffering!
OffScreen := TBitmap.Create;
OffScreen.Width := VideoModeX;
OffScreen.Height := VideoModeY;

LoadGameGraphics;

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


There are two ways of drawing an image from the GameGraphics array (or any other image) to the screen. The first (and easiest) is simply called Draw and the second is called CopyRect. Now, I have read quite some canvas drawing tutorials and most use CopyRect. Copy Rectangle (CopyRect) is a very fast way of drawing and that why its mostly used on the canvas, however I used it a few times and was very impressed by its speed and very dissapointed by its speed at the same time. Strange? Not at all, copy rectangle is the best method for drawing non-transparent images, because its just the fastest method, however when we want to draw transparent images (I will come back on transparency later) I would advice using the Draw method. I have tried using Copy Rectangle for drawing transparent images not long ago and it even slowed down drawing on my AMD XP 1800+ while I never noticed any slowing down on the Draw method until I ran my programs on a Pentium 400 MHz. So I will explain both methods, but my advice is: When you know that your are going to use transparency or can?_Tt predict that won?_Tt then use the Draw method if you are sure you will not use transparency use Copy Rectangle!
In my chess game I first used the Draw method to do everything and later I used copy rectangle for the background and the black and white tiles of the board and this did speed up the game noticeable on the 400 MHz machine!


8.1 The Draw Method

This is the easiest method to master and can be explained in just a few lines. The Draw function looks as follows:

procedure Draw(X, Y: Integer; Graphic: TGraphic);


The explanation is easy. The higher you make the X-value the more your image will be moved to the right. The higher you make the Y value the lower the image will appear on the screen. At the Graphic spot we have to pass the image we want to draw. Two Examples:

Draw(0, 0, GameGraphics[2]);
Draw(20, 5, GameGraphics[2]);


In the first example we draw the 3rd image of the GameGraphics array in the top-left corner of the screen and in the second example we move the image 20 pixels to the right and 5 pixels down.

Easy, isn?_Tt it? However don?_Tt forget that you always have to call the Draw procedure from the GameLoop and always do all your drawing work before calling BitBlt, otherwise the image won?_Tt be visible on the screen!

8.2 Copy Rectangle

This method is a lot harder to understand and master. With this method we first declare a rectangular-spot that we wish to copy to the OffScreen canvas and then we have to declare a rectangular-spot where we wish to copy the other rectangular-spot to. As you can see very complicated?_ Don?_Tt worry it sounds a lot harder than it is!

Say we want to draw our 2nd image of our GameGraphics array on the OffScreen bitmap using Copy Rectangle. The First thing we have to do is declare two rectangles one called SourceRect (data-type TRect) and DestRect (date-type TRect). We will use the SourceRect to specify the rectangle that ?_oselects?__ our entire 2nd image of our GameGraphics array. The DestRect we will use to specify the position of the image on the OffScreen image.
If we would use Copy Rectangle to draw our 2nd image on position 0, 0 on the OffScreen Image our GameLoop will look like this (don?_Tt worry I?_Tll explain copyrect further):

Procedure TForm1.GameLoop(Sender: TObject; var Done: Boolean);
Var SourceRect, DestRect : TRect;
Begin
Done := False;

// Game Stuff Goes here...
SourceRect := Rect(0, 0, GameGraphics[1].Width, GameGraphics[1].Height);
DestRect := Rect(0, 0, GameGraphics[1].Width, GameGraphics[1].Height);
OffScreen.Canvas.CopyMode := cmSrcCopy;
OffScreen.Canvas.CopyRect(DestRect, GameGraphics[1].Canvas, SourceRect);

// Draw entire OffScreen image to the screen?_
BitBlt(Canvas.Handle, 0, 0, VideoModeX, VideoModeY, OffScreen.Canvas.Handle, 0, 0, SRCCOPY);
End;


Okay, that?_Ts quite something isn?_Tt it! What happens here, well for starters we create two rectangles the first (SourceRect) just selects the entire 2nd image of our GameGraphics array and the second makes a destination selection (DestRect) with which we specify where the image should be placed on the OffScreen image. In this case, both rectangles are the same which will result in drawing the image in the top-left corner of the OffScreen image. If we would want to draw the image 20 pixels to the left and 5 pixels down our destination selection (DestRect) would have looked like this:

DestRect := Rect(20, 5, 20 + GameGraphics[1].Width, 5 + GameGraphics[1].Height);


By doing this the image will move 20 pixels to the left and 5 pixels down, so it?_Ts not that different from the Draw method is it?

Then there is a very odd line below DestRect. With this line we specify the CopyMode, we seems very odd, since all we wish to do is copy images, but copy rectangle can do much more it can invert images, create masks etc. However we just want to copy an image from a source to a destination, so we use cmSrcCopy. And finally we draw the image by using the CopyRect command, I?_Tll explain this line:

Procedure CopyRect(Dest: TRect; Canvas: TCanvas; Source: TRect);


The command is very logical, once you understand how to create the rectangles and how the destination selection works. Once you understand the rectangles the CopyRect procedure speaks for itself. At the Dest position you provide the CopyRect call with the Destination Rectangle (DestRect), then you provide it with the Source Canvas (where it should copy from) and finally you provide the source rectangle (SourceRect) and you are finished!

Of course doing this for every image you want to draw is quite time absorbing, so I wrote a procedure that makes working with copy rectangle just as easy as using the draw method. If you wish to implement the procedure in your game, you?_Tll have to declare the procedure in the public declarations of your form, so that would make our public declarations look like this:

type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
procedure GameLoop(Sender: TObject; var Done: Boolean);
procedure StopFlicker(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND;
public
{ Public declarations }
procedure LoadGameGraphics;
procedure DrawImage(X, Y : Integer; Bmp : TBitmap);
end;


The we have to add the actual procedure to our unit and that procedure looks like this:

Procedure TForm1.DrawImage(X, Y : Integer; Bmp : TBitmap);
var SourceRect, DestRect : TRect;
Begin
SourceRect := Rect(0, 0, Bmp.Width, Bmp.Height);
DestRect := Rect(X, Y, X + Bmp.Width, Y + Bmp.Height);
OffScreen.Canvas.CopyMode := cmSrcCopy;
OffScreen.Canvas.CopyRect(DestRect, Bmp.Canvas, SourceRect);
End;


This procedure works exactly the same as the Draw method, so if you want to draw the 4rd image 20 pixels to the right and 5 pixels down you would call the procedure like this:

DrawImage(20, 5, GameGraphics[3]);


Remember that you have to call every drawing procedure from the GameLoop and that you always have to do all your drawing before calling BitBlt!


9. 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 makes the effect that the spacecar stands on a grassy landscape much more realistic.

The way transparency is done on the canvas is different for both drawing methods. As I said in the previous chapter, doing transparency with the Copy Rectangle method will make your application rather slow, but even though I will still explain this method to you, but I?_Tll start with the easiest method, the Draw method.

9.1 Transparency with Canvas.Draw

When using the Draw method to draw the image on the screen, using transparency is actually very easy. The way it works is very easy also: a color is specified and the parts of the image that have that color are not drawn. The color most commonly used for marking transparency spots is fuchsia (the visible color in the left spacecar image), because its almost never used in images, which makes it very suitable for the task!

Okay, let?_Ts ay the spacecar shown above is the 4th image in our GameGraphics array. If we want to draw it transparent on the OffScreen image we would do it by calling the following lines:

GameGraphics[3]. TransparentColor := clFuchsia;
GameGraphics[3]. Transparent := True;
OffScreen.Draw(20, 5, GameGraphics[3]);


That?_Ts it, of course its useless to call this everytime we want to draw the image, the lines:

GameGraphics[3]. TransparentColor := clFuchsia;
GameGraphics[3]. Transparent := True;


Could also be called in the LoadGameGraphics procedure. When they are called once the properties will be remembered, so then the image will stay transparent until the property Transparent will be changed to False.

If all our images in the GameGraphics array need to be drawn transparent (don?_Tt worry if you use grass without the color Fuchsia, setting transparent to True will have no effect) than you could write a for loop in the LoadGameGraphics procedure. Then our LoadGameGraphics procedure would look something like this:

Procedure TForm1.LoadGameGraphics;
var D : Integer;
Begin
For D := 0 to 4 do
GameGraphics[D] := TBitmap.Create;

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');

For D := 0 to 4 do
Begin
GameGraphics[D].TransparentColor := clFuchsia;
GameGraphics[D].Transparent := True;
End;
End;


9.2 Transparency with Copy Rectangle

Doing transparency with Copy Rectangle is much harder. It doesn?_Tt really use colours for transparency marking, but it uses masks. This means you need two identical images, but one should be in black and white.

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

This black and white counter-image is called a mask and the white parts of this mask mark the parts that are not drawn. Then the original image is drawn over the mask, but only the parts of the original image that overlap the black parts of the mask are drawn. This method is called masking! An example of masking is shown above. The right image is the original image and the left image is the mask.

Using masking with copy rectangle means we have to draw two times with the same rectangle, but with different images and another copymode. Say our spacecar image is the 4th image in our GameGraphics array and we wish to draw it transparent onto the OffScreen image. I?_Tll pretend we created a second Tbitmap array called MaskGraphics and that we filled it with our masks. Using masking with copy rectangle would look like this:


SourceRect := Rect(0, 0, GameGraphics[3].Width, GameGraphics[3].Height);
DestRect := Rect(0, 0, GameGraphics[3].Width, GameGraphics[3].Height);

// Draw the mask?_
OffScreen.Canvas.CopyMode := cmSrcAnd;
OffScreen.Canvas.CopyRect(DestRect, MaksGraphics[3].Canvas, SourceRect);

// Draw the original image over the mask?_
OffScreen.Canvas.CopyMode := cmSrcCopy;
OffScreen.Canvas.CopyRect(DestRect, GameGraphics[3].Canvas, SourceRect);


The only real change is using the cmSrcAnd copymode. This tells CopyRect that we are going to draw a mask. It?_Ts not really hard, but as I said before in comparison to the Draw method is using transparency with Copy Rectangle very slow!


10. About Lines and Rectangles

Sometimes you just need a plain and simple rectangle or line in a game, for example when you want to create a selector. Of course you could use a plain and simple bitmap, which represents a line, a circle or a rectangle, and make it transparent. That would work, but the downside is that it takes memory to store the Tbitmap objects in and your application will grow in size tremendously?_ Another problem you could encouter is resizing, if you need a bigger rectangle, you could resize the image, however this won?_Tt do the quality of the image much good.

Luckily for us, this can all be avoided by using functions included in the canvas like line, rectangle, ellipse, pixel and so on. I will not explain all of those functions, because they all look alike. I will only explain line and rectangle, because they are used most commonly.

10.1 Lines

Using lines on the canvas is easy. You only have to specify a starting point and an end point. If you do not specify a starting point, the line will origin in point 0,0 or at the end-point of the last line drawn. So when a line is drawn, the starting point will be set to the end-point of the drawn line. This is a very nice feature, because when you build a rectangle out of lines, you only have to specify the next point in the rectangle! We specify the starting point of a line with the command MoveTo(X, Y) and we draw a line with the command LineTo(X, Y).
So let?_Ts say we want to draw a line from point (50, 50) to point (100, 50). We would use the following commands:


OffScreen.Canvas.MoveTo(50, 50);
OffScreen.Canvas.LineTo(100, 50);


This would draw a nice straight line from point (50, 50) to point (100, 50). Now say we want to draw another line from point (100, 50) to point (100, 100) we would simply add the line:


OffScreen.Canvas.LineTo(100, 100);


Because the starting point is set to the end-point of the lastline drawn, the starting point is allready set to (100, 50) so we do not have to specify this point! It?_Ts that easy to draw lines!

10.2 Rectangles

Rectangles are also very easy, the are even easier than lines, because we do not have to call another command to specify the starting point. We just define the starting point and the end-point with one command the Rectangle command! The rectangle command looks like this:


procedure Rectangle(X1, Y1, X2, Y2: Integer);


So if we want to draw a rectangle we just specify the origin (X1, Y1) and the end-point (X2, Y2). So if we want a rectangle that originates in point (50, 50) and is 50 pixels high and 50 pixels wide, we would use Rectangle like this:


OffScreen.Canvas.Rectangle(50, 50, 100, 100);


That?_Ts it! Drawing on the canvas is really fun, isn?_Tt it? I must remember you again, however to do all your drawing in the GameLoop before the BitBlt command to be able to see the images and drawings on the screen.


10.3 About the Pen

Okay, when we draw a line or a rectangle on the canvas we use an object called the pen. However when you would start drawing lines and rectangles you would draw them in the colour black and the lines a very thin. If you write a game and wish to use a rectangle or a line you want it to be visible to the user and a thin black line sometimes blends in with the environment! So we want to draw thicker lines with another colour than black! This also is very easy.
To draw a line or a rectangle in another colour we call the command OffScreen.Canvas.Pen.Color to change the colour of the pen. To draw a line or a rectangle with a thicker line we call the command OffScreen.Canvas.Pen.Width. For example say we wish to draw a rectangle in the color red with a thicker line (say width = 4) than we would specify:


OffScreen.Canvas.Pen.Color := clRed;
OffScreen.Canvas.Pen.Width := 4;
OffScreen.Canvas.Rectangle(50, 50, 100, 100);


Remember however that Color and width are properties of the object pen, which means that they will be remembered until they are changed again. So when we would draw a line after we have drawn the rectangle, this line will have a thickeniss of 4 and the colour red. So when we want to draw the line normally again, we would have to specify the normal colour (black) and the thickness (width = 1) again.


11. Notes for the Kylix game-programmer

This entire tutorial talks about the Windows canvas. Conveniently the canvas has been emulated in Kylix and can be used just like the windows canvas. However not every trick mentioned here is can be used in kylix. However the only problems I know of are that the procedure StopFlicker function will most likely not function in Kylix because of the windows message used and I do not know if Kylix supports the GetSystemMetrics function for running fullscreen and I do not know if the path can be retrieved with the ExtractFilePath(ParamStr(0)).

The full screen problem can be easily be solved by requesting the size of the form, but this will be a problem when done in the OnCreate event. You would have to do this somewhere else. Running in full screen, will also cause some trouble, because of the X environment. I am not an experienced Kylix programmer, so I can not give real answers to that problem, but it should be solvable.

Luckily most of the tricks like double buffering and the gameloop will work just fine!


12. Canvas Game Template!

I thought a working example of all the preparations that this tutorial features would come in handy while writing your games on the Delphi Canvas, so I wrote a little template. It does all the preparations that you need for high frequency drawing and without flickering images 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! :)

download it here:
canvastemplate.zip (http://terraqueous.f2o.org/dgdev/tutorials/canvastemplate.zip)


13. 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 2003 Lion Productions

TheLion
22-05-2003, 11:45 AM
I'm very honored to announce that Kim Sungchul has translated my "Programming Games on the Windows Canvas" tutorial to the Korean language. He has also converted the sample code (in the article and the template) to Borland C++ Builder, however the Delphi samples are still in the article for comparison! :)

The translation of the tutorial can be found here (http://user.chollian.net/~oriqpt/materials/old/article/g4.html)

Alimonster
23-05-2003, 09:22 AM
An off-topic aside: what happened to your website? It seems to have vanished. :?

EDIT: Ah wait, I see the new address (the f2o one). Could you please update the link in your profile + the tutorial. Ta

TheLion
23-05-2003, 11:55 AM
Thanks for reminding me! :) I changed my signature, but seem to have forgotten to change the address in the tutorials and in my profile! :)

It's done! ;)