Main Page   Compound List   Compound Members   Related Pages  

libKSD Breakout Demo/Tutorial

0.1

Introduction

This tutorial will introduce you to libKSD, a class library for game programming based on SDL. This tutorial is known to work with gcc 2.95.3 and libksd-0.1.0-pre1. Small changes (indicated in the source) are required for later libksd versions.

It is meant more as a annotated demonstration of an example game than as a full-fledged walk-through tutorial. The latter is provided in the Cannon tutorial.

The current version still is not perfect. It works, it works well, but there still are some errors. It's up to you to find them.

I am fully aware that I did not implement the fastest solution in some cases. This is not a course in advanced C++, nor am I qualified to give such a course. If there are any particularly stupid algorithms in my code, please drop me a mail at leonw /at/ gmx.net. Do not suggest to inline all functions - for this demonstration I prefer a relatively clean class declaration.

The full game source is main.cpp. The rest of this tutorial uses code snippets of this source to illustrate some of the interesting points.

Imported Artwork

Firstly, it should be noted that I use the GMSpaceBreakout artwork, provided by GameBlade. This forces a decision on how to use this artwork in our game. GameBlade likes to put multiple logical images in one physical image (Sprite, in their terminology). I would have created separate images for the tiles and paddle. You could either use image processing software to separate the spites, or separate them in the code. I chose the latter to show how this is done:

tmp.Load( BRICKSFILE );
GrayTile.Create( 64, 32 );
GrayTile.GetCanvas()->Blit( 0, -32, tmp );

Here I know in advance that a gray tile is 64x32 (I checked it with the GIMP). As such, I just Create an "empty" image this size. I also know that the gray image is second in a vertically stacked series of images. The first image (the paddle image) also had a height of 32 pixels. That means I to copy the full image tmp into the smaller surface of GrayTile, with a vertical offset of the height of the first image. The image is automatically clipped, the GrayTile surface will not get larger.

For some reason, the GMSpaceBreakout creator has opted for a "BMP" formatted image. This image type does not natively support transparency. Though I would have chosen PNG with a transparent background, this does give me a reason to show how to simulate transparency. This is done by using the ColorKey feature of SDL. A ColorKey is an optional feature of a SDL surface (it need not be set). If you set a ColorKey to a color Color SDL (and thus libKSD) will not blit any pixels with color Color, thus simulating a fully transparent pixel.

Unfortunately the TImage does not yet allow you to set a COLORKEY directly, so we need to get dirty and access the SDL functions directly.

Uint32 uBlack = BallTile.GetFormat().MapToPixel( TColor::black );
SDL_SetColorKey( BallTile.GetSurface(), SDL_SRCCOLORKEY, uBlack );      

In this case we want the black background of the ball to be interpreted as a fully transparent pixel. This is done by SDL_SetColorKey, defined in SDL/video.h and included via <ksd.h>. It takes a SDL_Surface*, provided by TImage::GetSurface(), a flag (in this case indicating that we're going to use a color key) and the Uint32 value of the color to be used as key. The color encoding is done by TPixelFormat::MapToPixel() function. Since I'm not sure that TColor::black is encoded in the same Uint32 value for all images (some may be paletted and have black at palette index 15 or so) I get the current palette for all images I set a color key for. I really don't now whether that's necessary but better safe than sorry.

Note:
It might well be that TImage will provide a function to set the color key. When that time comes, this tutorial should be updated. Please sent me a mail (see Introduction) if this did not happen.

Init

This Application member function is obligatory. It must be implemented. If you wish to draw anything (be it widgets or on a canvas) you must also call CreateScreen( width, height, bpp ), where bpp (bits per pixel) is optional.

SetCaption() sets the caption, shown by the window manager.

TFontList is a handy class to manage fonts. It reads a font configuration file (see the libKSD docs for the format) and reads those fonts. Bitmapped fonts as well as TrueType fonts are supported. The fonts are deleted when FontList is deleted, so make it a Application member.

We then set the Application font. This is actually a member of TWidget, of which TApplication is derived. All children of the application now have access to this font, which is critical for gui widgets that display text (though we don't use them here). The font is NOT automatically used by the TCanvas in the Draw() member, we'll come to that later.

SetColor sets the foreground color. We'll use this later to draw the text.

Key Repeat

By default, if you keep a key pressed for a long time, this is interpreted as a single key press. In a game in which you use two keys to move your "vehicle" this is not very practical. Your keyboard would be in the same state as your fingers if you tried to catch the ball. That is why we enable KeyRepeat. KeyRepeat is a member function of the global variable Keyboard, not part of TApplication.

Unfortunately, it seems that TKeyboard uses a default value for the delay, and does not provide a member to set the delay. The delay is the time between the first moment the key is pressed and the next moment the key seems to be repressed. (E.g. I put my finger on the ArrowLeft key. Immediately a SDL_KeyboardEvent is generated. Then, after delay seconds, a second SDL_KeyboardEvent is generated. After that, each interval seconds a SDL_KeyboardEvent is generated.)

We fix that by bypassing the Keyboard and directly calling SDL_EnableKeyRepeat( delay, interval ). I have chosen to have the second event generated the same as all others, after a SDL defined time of SDL_DEFAULT_REPEAT_INTERVAL (30ms).

The reason we still do Keyboard.EnableKeyRepeat(true) is to ensure that the rest of libKSD knows that this is on. (Not really required in this version of libKSD, but it is kind of polite now that we so unceremoniously bypass libKSD, I think).

The corresponding code is:

Keyboard.EnableKeyRepeat(true);
SDL_EnableKeyRepeat( SDL_DEFAULT_REPEAT_INTERVAL, SDL_DEFAULT_REPEAT_INTERVAL );        

Note:
It might well be that coming versions, after 0.1.0-pre1 provide a member function for this. When that time comes, this tutorial should be updated. Please sent me a mail (see Introduction) if this did not happen.

Background Image

Drawing a background image goes in three stages, first load the image, the set the background image, and then tell the TWidget derived class that it should draw the background image.

Please note that you pass a pointer to SetBackgroundImage, it is up to you to ensure that the image is not deleted before the widget is deleted. It is also up to you to delete the image after you deleted the widget. In our case the images are automatic variables and the compiler generates the code to delete them when it generates the BreakOut destructor.

Background.Load( BACKFILE );
SetDrawBackgroundImage( true );
SetBackgroundImage( &Background );

Warning:
Never do something like this:
void BreakOut::Init()
{
        TImage BackgroundImage( "image.png" ); // automatic scope!
        SetBackgroundImage( &BackgroundImage );
        SetDrawBackgroundImage(true);
} // and BackgroundImage gets deleted, but BreakOut instance not!

NeedsRepaint

If you change anything at all which changes the appearance of your game on screen, call NeedsRepaint(). This is a very light function, probably inlines, which only sets a TWidget flag. When set, the Draw() function of your widget/application (whichever set NeedsRepaint()) will be called. Otherwise it will be skipped to save time.

I did, in first instance, forget to call it when I changed one of the strings, as I thought a redraw will come anyway. Well, it doesn't. Just call NeedsRepaint() on any change, or at least be aware that it can be called.

Cycle

In LibKSD it is possible to decouple the AI (game logic) from the drawing loop. If the two loops are decoupled (not the default) a separate thread is used to compute Cycle. The intention is that the drawing loop runs at maximum speed, but that the Cycle loop is adjusted to the computer speed, giving a constant reaction speed or so of the AI.

In this game, the loops are coupled and each Draw is preceded by a Cycle loop. This might be changed if I find out that for example a 2 GHz Athlon has a fast ball compared to my PII/400.

Just a few notes on the design of this particular game. I chose to represent the logically different objects like Brick, PaddleType and BallType each with their own class, even though the added functionality is limited. This because much of the functionality is logically part of an object. I advise you to do the same, even though it is trivial to call the Canvas->Blit() on the bricks directly in the BreakOut::Draw() function. It will keep your code readable.

I started the Brick class because I wanted a single object which knows both the position, as well as the image. I did not want to separate lists (or vectors). I chose TImage pointers because for the Bricks it seemed a waste of memory to have all those images hanging around. Only afterwards, now that I'm writing this text, I remmeber that TImage is, by default, Copy-On-Write, and that only a reference gets copied normally. Using a TImage in stead of a TImage* would be almost as efficient.

The PaddleType and Brick class use TRect's which hold position and size of the object. It might have been simpler to add an X, Y, Width and Height member to e.g. Brick, but then I would have had to implement collision detection myself.

BallType uses a Circle for the same reasons. I particularly liked the Move( displacement ) function, and of course collision detection. The documentation states that TCircle is not very precise, but I don't really know what that means. It's precise enough for me.

Collision Detection

I use collision detection for different tasks. First, of course, I need to know whether the Ball hits a Brick. I just test for this collision using TCircle::Collision( TShape* shape ). The test is said to be fast, but not extremely accurate. In contrast to most of the libKSD functions, a pointer is passed. This is required for C++ virtual function binding to work.

When the two collide, I want to know where they collided so I can adjust the Ball's Velocity vector accordingly. Presently this code is not yet perfect. It doesn't take into account that the Ball displaces more than a single pixel each step, so it can occur that BreakOut thinks the ball crossed a TopLine (which is tested first) whereas in a pixel-accurate situation it would show it first passed the RightLine of the Brick. The same goes for the Paddle. This should be changed some time, but I don't think this game is in itself interesting enough to spent a lot of time on a good algorithm, yet.

When writing this game, I got a compiler error on the code:

        TLine Wall1( TPoint2D( WALL_WIDTH, 0 ), 
                     TPoint2D( WALL_WIDTH, GetHeight() ) );
        TLine Wall2( TPoint2D( GetWidth() - WALL_WIDTH, 0 );, 
                     TPoint2D p4( GetWidth() - WALL_WIDTH, GetHeight() ) );
and I replaced it by

        TPoint2D p1( WALL_WIDTH, 0 );
        TPoint2D p2( WALL_WIDTH, GetHeight() );
        
        TPoint2D p3( GetWidth() - WALL_WIDTH, 0 );
        TPoint2D p4( GetWidth() - WALL_WIDTH, GetHeight() );
        
        TLine Wall1( p1, p2 );
        TLine Wall2( p3, p4 );

I don't now why, but this seems to work.


Generated on Tue May 13 17:15:57 2003 for Breakout by doxygen1.2.17