Gearing Up For Games

From EDM2
Revision as of 21:32, 7 December 2017 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Michael T. Duffy

Introduction

I would like to welcome you all to a new series of articles in EDM/2. The purpose of this series is to provide you with much of the technical information you need to know in order to write games under OS/2. Every issue I will cover a different topic on game programming, and each article will build on the previous articles. I don't know how many articles there will be, but I will write them until I run out of things to talk about, I run out of time to write articles, or everyone basically tells me to sit down and shut up. <grin>

I want to point out that I am not an expert on this topic. However, seeing as there are not many people writing games for OS/2 yet, there hasn't been time for a group of experts to develop. I have spent a lot of time researching and working in this area though, and I am currently developing games for OS/2 on my own. The purpose of these articles is to show how to develop commercial grade games for OS/2, so sometimes I will sacrifice ease of illustration for speed. The code for these articles is derived from my own game aimed at the commercial market, so please forgive me if parts of it appear rough or not very descriptive.

I would also like to encourage any feedback I can get. I cannot improve my code and these articles if I do not know what I am doing wrong. Everything I will present will be the best way I can find for accomplishing the task at hand. Occasionally I will step into areas I do not fully understand yet, and if you catch me making a blunder, please email me and let me know. Flames are welcome! In future articles I will pass on all information I gain from your responses.

The Plan of Attack

Each issue I will cover a different topic, all of which will lead up to a final, complete game. This game will be along the lines of the classics "Space Invaders" or "Galaxian", although the approaches used apply to action, adventure, puzzle, RPG, sports, and many other kinds of games. I will initially focus on sprite based games. A sprite is a graphical image that is written to the screen. Imagine a sprite as a bitmap where some of the pixels are specified as the background. The background pixels are clear when the image is written, so that you can see what is behind the sprite instead of the sprite appearing as an image within a rectangle. Most computer games currently use sprites in one form or another, and the concept is important to know if you want to write games.

A proposed list of topics follows. The order and contents of the list are not set in stone, and your feedback will influence whether or not I bump up a topic in importance and thus cover it sooner. I will cover at least one topic per article, but sometimes I will have to present two or three topics at once in order to cover the material at a decent pace. Once the concepts of each topic are understood, you should be able to expand upon the information and sample code given, and apply it towards any genre of game you like. Not all topics are directly related to OS/2 programming, but all are applicable to game programming.

The topics are:

  • DIVE fundamentals: How to write to the screen quickly.
  • The Canvas class: An object which simplifies writing to bitmaps. Basic drawing primitives are presented. We will be able to draw simple patterns to the screen after this article.
  • The Palette: A class is presented that simplifies palette operations and converts palettes from one format to another. Palette handling under OS/2 will be covered.
  • PCX Displaying: A class will be presented for handling loading and copying bitmaps to the Canvas class. Code for working with the common PCX format will be presented. We will be able to display a PCX bitmap with the proper colors at the end of this lesson.
  • The Blit Thread: Here we will cover thread, semaphore, and timer basics. We will look at how threads can be used in games, and will set up a thread to handle drawing to the screen.
  • The Basic Sprite: Here I will explain the concept of a sprite. We will take a small image from a PCX file and display it as a sprite. We will cover simple sprite animation, and learn how to draw it to the screen and move it around. Double buffering techniques will be introduced.
  • The Basic Sprite part II: Dirty rectangle techniques will be used to speed up sprite animation.
  • The Advanced Sprite: Encoding and Displaying of sprites that use RLE (Run Length Encoding) compression will be covered. These sprites will display much faster than the basic sprites, but they take more work to create and display. Assembly language display routines will also be presented.
  • The Advanced Sprite II: Techniques to take RLE sprites and store them in a large sprite "book" will be covered. Management of the many sprites a game requires will be covered.
  • User Input: Techniques for reading the keyboard, mouse, and joystick for games will be covered. Our sample program will be expanded to allow user control of a sprite's movement around the screen.
  • Collision detection: We will look at how to determine when one object hits another. This topic will also include creating and storing the masks needed for exact collision detection.
  • Event Handling: Here we will look at the overall internals of a game and how to manage all the individual parts. At the end of this article we should have the basics of a working game running.
  • The Advanced Sprite III: Clipping an RLE sprite against the edge of the canvas will be covered. C++ and assembly language implementations of the routines will be presented.
  • Advanced DIVE: Here we will look at the full-screen capabilities of DIVE, as well as how to work with different full-screen resolutions. By this time the toolkits needed for full-screen DIVE support should have been released. We will add full-screen capabilities to our game, and possibly we will create a 640x480x256 color full-screen version.
  • File management: We will look at how to store the data that goes into a game and manage its loading and storage. We will look at how to separate the process of requesting data and using it to take advantage of multi-threading to speed up our game startup time. We will organize the data of our sample game.
  • The Advanced Sprite IV: Here we will cover sprites with semi-transparent parts (smoke and shadows), sprite flipping, and possibly sprite rotation and scaling.
  • Basic Sound: This article will cover playing digitized sound under OS/2. We will look at MMPM/2 basics and see what raw MMPM/2 is capable of. We will add sound effects to the game as best we can.
  • Advanced Sound: This article will look into mixing sounds in realtime and playing them back with MMPM/2. We will look at building sound queues and managing them. We will add this ability to our game.
  • Advanced Sound II: Our final article will look into playing music under OS/2, either as MIDI files and as digitized music such as found in MOD trackers. Depending on how complicated this topic turns out to be, we may or may not get a full system for playing music implemented. At the very least we will have covered how to implement such a system under OS/2, though the specifics of say, writing a MOD tracker, may be left as an exercise for the reader. <grin>

Well, that's what I have in mind for this series. I may be able to cover material faster or slower than the above list implies, but it all depends on what you, the reader wants. I have split up the topics so that I don't overwhelm anyone with too much information at one time, and also to give myself time to write the articles while working full-time as well.

Sometimes I will have to cover topics a couple at a time. For example, the first topic on DIVE is useless until the Canvas class is covered to show DIVE in action. Also, I will probably cover palettes and PCX decoding in the same article as well. I may also combine some of the sprite topics into one article. A lot of these decisions will hinge on my free time, and your feedback.

In closing, I'd like to welcome your to our journey into game programming, and I hope that in the near future we will all see a few more games that run natively under OS/2.

Overview of DIVE

So what exactly is DIVE? DIVE stands for Direct Interface Video Extensions, and is meant for blitting (copying) bitmaps to the screen quickly. I believe it was originally designed to simplify direct writing to the screen for the video codecs (compression/decompression algorithms) in OS/2, but it is also extremely useful for games. It allows a standardized way to write to the screen quickly while preserving the desktop around it.

With DIVE, the programmer composes into an off-screen buffer the desired image to put to the screen. Then DIVE can be used to place this buffer to the screen quickly. In doing so DIVE performs several tasks. It handles color remapping from the source buffer to the destination buffer or screen, so the game author only has to support one color format and it will be automatically remapped to accommodate the screen color depth that the end user has chosen. DIVE also clips the image to all other windows on the desktop, thus preserving the look of the desktop and freeing the game author from this task. DIVE can also resize an image, stretching the pixels of the source image to fit in the destination window size. Finally, the next version of DIVE (due out at the end of Summer 1995) will offer a way to take over the screen entirely and offer full-screen games at the resolution the game programmer chooses. The API will stay basically the same, so the game programmer can write a game that will run in either a window or in a full-screen session with a minimum of extra code required. Finally, DIVE applications will be able to take advantage of hardware video acceleration as it becomes available, all without requiring a recompile or any code changes.

DIVE provides two ways to put graphics on the screen. The first method is direct writing. In this method, DIVE provides a list of the capabilities of the screen, and a direct pointer to the screen's surface. The programmer is responsible for remapping colors, clipping against other windows on the desktop, and bank switching on video cards that require it. This is the kind of nightmare that has plagued the DOS platform for years, and it is exactly the kind of thing we would like to avoid under OS/2. I will not address this method of using DIVE. It _may_ be a bit faster than the second method of placing graphics on the screen, but the headaches far outweigh the advantages as far as I'm concerned. Furthermore, applications that write directly to the screen will not be able to take advantage of hardware acceleration as it becomes more available, whereas programs using the second method will.

The preferred method of using DIVE is to let it blit the image to the desktop for you. This way DIVE will handle all the color remapping, pixel stretching, and clipping against other windows for you. Considering that with direct writing the programmer would have to handle all these tasks anyway, the second method makes even more sense. All future references to DIVE refer to this second method of putting images on the screen.

Why should you use DIVE? When should I avoid it?

Whether or not you use DIVE depends a lot on the type of game you want to write. It has many advantages, and several disadvantages as well. I am assuming that the game we wish to write will run in a window on the desktop so that the user may switch from the game to other applications. This may be done so that while playing a game the user can check on a process running in the background, flip between a game and an application when the user needs a break, or just to handle something else in the middle of a game without having to stop, save, and restart the game. The advantages of having programs that integrate seamlessly into a GUI environment like the Presentation Manager are obvious to anyone who uses a GUI regularly, so I will not try to extoll, explain, or justify them here. However, in gaining the advantages a GUI offers, some trade-offs must be made. Some of the advantages and disadvantages are as follows:

  • Speed: If you are writing an action game, DIVE is a must. There is simply no other way to quickly write graphics to the PM desktop. The Gpi API is just too slow to get good results, and it is not very useful for quickly drawing the kind of graphics commonly found in games, namely bitmaps and sprites. DIVE allows the author to compose to an off-screen bitmap and then quickly throw the completed bitmap to the screen. Considering that DIVE must often remap colors and/or stretch the image during the blit, DIVE is very fast. When one writes a game for a GUI, the color remapping and image stretching is just a fact of life that cannot be avoided. DIVE makes it as painless as possible. There is an unavoidable speed hit because of this extra overhead (compared to what one would have to deal with if the game owned the entire screen), but the authors of DIVE made sure the overhead was handled in the fastest possible way. However, because the newest version of DIVE supports full-screen sessions, even this speed hit can disappear if the end user decides to run the game full-screen.
  • Integration: With DIVE, one can write a game that is fast and integrates seamlessly into the desktop. The game runs in a window, and the user can switch amongst the game and any other applications with the simple click of a mouse button. All the advantages of a GUI are available

while the game runs in a window, and this includes using PM menus, windows, dialogs, and other controls.

  • Color support: DIVE allows the game author to support one color format, and DIVE will handle the remapping to all other formats. This simplifies the game authors work, and frees him or her from having to mess with the intricacies of each video mode and color resolution. DIVE supports modes from 8-bit (256 colors) to 24 bit (16.4 million colors), with a few other color

resolutions in between. There are 10 input formats and 5 output formats, and I unfortunately do not have the space or experience to go into them all here.

The ones of interest for us here are:

FOURCC names Bit depth Colors
LUT8 8-bit 256
R565 R555 R664 16-bit 32k
RGB3 BGR3 24-bit 64k
RGB4 BGR4 32-bit 16.4 million
  • Full-screen Graphics: As mentioned earlier, at the end of Summer 1995 IBM plans to release a game development toolkit for OS/2.

In this toolkit will be a dynamic link library called GAMESRVR.DLL, and it will extend DIVE to include full-screen support. This means that a game running in a 320x200x256color window can be expanded to fill the entire screen without having to resort to pixel stretching. The resolution of the screen is 320x200; the rest of the desktop is stored away. In this mode, DIVE applications are really fast. Their speeds approach that of DOS games. No color remapping or pixel stretching is done (if your bitmap size and color depth match the target screen mode), so there is no slowdown from this overhead. Best of all, the code to compose and display images is the exact same as writing to a window. Very little code is required to add the capability to play the game in either a window on the desktop or as a full-screen game. Currently the 320x200x256 mode is the only one that is easily supported because it is the most common mode for DOS games. However with a bit of work, any of the modes that the video card supports may be used in a full-screen mode. This means games can be written in 640x480x256 and DIVE will handle all the intricacies of blitting to the screen in a full-screen mode. I have not experimented with this capability yet, but once I have I will include any information I learn in a future article. I will cover the full-screen capabilities of DIVE in a future article as the release date for the toolkit draws closer.

Availability

I believe that currently the only place to get hold of the DIVE development files is in the Warp Toolkit. This Toolkit is available on the Developer's Connection (DevCon) CD ubscription which runs $199US a year for 4 to 5 issues. If you are a member of IBM's Developer Assistance Program (DAP), you can get a DevCon subscription for $119US. In North America the phone number for DAP is (407) 982-6408 and the number for DevCon subscriptions is 1-800-633-8266. I don't mean this to be a commercial for DevCon, but as far as I know it is the only way to get your hands on the DIVE library.

The dynamic link library that is needed to run DIVE applications may come standard with the MMPM/2 portion of Warp. I believe the current version of DIVE will only run with OS/2 Warp or better, and future versions of DIVE will also have this requirement.

The DIVE API

Well enough of the propaganda as to why you should use DIVE. Let's look at how to use it! I will not be giving a complete reference to the functions, as I will only be covering what is needed to get us up and running in the mode where DIVE will do the blitting for us. A more complete treatment can be found in the Multimedia Programming Reference.

For all routines, the ULONG return value is an error code. A return code of zero means that no error occurred. For a complete listing of error codes, check the Multimedia Programming Reference, or dive.h.

First you must know how to ready DIVE for use, and how to cleanup when you are done.

ULONG DiveOpen
 (
 HDIVE  *phDiveInst,
 BOOL   fNonScreenInstance,
 PVOID  pvFrameBuffer
 )

phDiveInst
pointer to the variable that will receive the DIVE handle.
fNonScreenInstance
FALSE if you will use this handle to write to the screen. TRUE if you will only blit from one DIVE buffer to another for color conversion purposes.
pvFrameBuffer
we will not use this because we will not be writing directly to the screen. Pass it a 0.

This function is used to obtain a DIVE handle which shall be used in all future dealings with DIVE. You will need one DIVE handle for each window that is blitted to the screen. Typically you will only need to use one handle, and thus this routine will be called once at startup.

ULONG DiveClose
 (
 HDIVE  hDiveInst
 )

hDiveInst
This is the handle of the DIVE instance you wish to close.
This function returns a 0 if successful, otherwise an invalid instance handle was passed.
This function must be called when you are done with the DIVE handle, typically at the end of the program.

Once DIVE has been setup and you have a DIVE handle, you can create a buffer to which you can write. The buffer allocation and deallocation routines are:

ULONG DiveAllocImageBuffer
 (
 HDIVE  hDiveInst,
 PULONG  pulBufferNumber,
 FOURCC  fccColorSpace,
 ULONG  ulWidth,
 ULONG  ulHeight,
 ULONG  ulLineSIzeBytes,
 PBYTE  pbyImageBuffer
 )

hDiveInst
Handle of DIVE instance.
pulBufferNumber
Pointer to variable to receive the buffer number. Make
fccColorSpace
FOURCC code for the color type of the buffer. We will only be using LUT8 (256 color palette based mode)
ulLineSizeBytes
Width of the buffer in bytes. When you use an 8-bit mode, this will likely be the same as ulWidth.
pbyImageBuffer
Pointer to already allocated image buffer.

There are a few things to consider when using this routine. If ulLineSizeBytes is 0, then DIVE will calculate the optimum line width itself. ulWidth should be evenly dividable by 4 if you allocate the image buffer yourself (as we will do here.) If pbyImageBuffer is NULL, then DIVE will allocate memory for the buffer itself. DIVE has the option of allocating the buffer in the video card's VRAM if such memory is available, but the programmer cannot control this. Also, writing to buffers in VRAM is slower than writing to buffers in system memory because the data must be transferred over the system bus, and even with VLB or PCI motherboards this is a slowdown. Finally, some video cards will have enough memory for VRAM buffers and others won't, so performance across different computers or different memory situations will lead to uneven performance from one session of the game to another. In order to standardize all of this to provide even performance across systems and to keep compositing of the image in the off-screen buffer fast, we will opt to allocate the memory for the off-screen buffer ourselves.

ULONG DiveFreeImageBuffer
 (
 HDIVE  hDiveInst,
 ULONG  ulBufferNumber
 )

hDiveInst
Handle of DIVE instance.
ulBufferNumber
Number of buffer to free (obtained from DiveAllocImageBuffer())

This routine is used to free any buffer that DiveAllocImageBuffer() allocated, and to release the buffer number. Even if you allocate the buffer yourself, you must call this routine during cleanup to disassociate the buffer number from the buffer.

If you used DIVE to allocate the off-screen buffer, you must request access to it before you can write to it or read from it. This is because the buffer may be in VRAM, and may be moved around by DIVE when not in use. This is yet another reason to allocate the buffer ourselves, as we don't have the overhead of securing access to the buffer with these calls. Furthermore, the location of the buffer and the line width in bytes may change from call to call, and this uncertainty will require more robust routines to write to the buffers. However, the techniques used to make the writing routines more robust will also make them slower, and when writing games one should avoid slowdowns at all costs.

ULONG DiveBeginImageBufferAccess
 (
 HDIVE  hDiveInst,
 ULONG  ulBufferNumber,
 PBYTE * ppbyImageBuffer,
 PULONG  pulBufferScanLineBytes,
 PULONG  pulBufferScanLines
 )

hDiveInst
Handle of the DIVE instance.
ulBufferNumber
Number of buffer you wish to access.
ppbyImageBuffer
Pointer to pointer of type byte to receive location of buffer.
pulBufferScanLineBytes
Pointer to variable to receive the width in bytes of the buffer.
pulBufferScanLines
Pointer to variable to receive number of scan lines in buffer.

This variables of this routine are fairly self-explanatory. We will not be using this routine, but if you do wish to have DIVE allocate your buffers, refer to the Multimedia Programming Reference for more information.

ULONG DiveEndImageBufferAccess
 (
 HDIVE  hDiveInst,
 ULONG  ulBufferNumber
 )

hDiveInst
Handle of the DIVE instance.
ulBufferNumber
Number of buffer you wish to access.

As explained above, the variables to this routine are self-explanatory and we will not go into them.

The next two routines are used to actually get the buffer to the screen, or to another DIVE buffer.

ULONG DiveSetupBlitter
 (
 HDIVE  hDiveInst,
 PSETUP_BLITTER	pSetupBlitter
 )

hDiveInst
Handle of the DIVE instance.
pSetupBlitter
Pointer to the Setup Blitter structure.

This routine readies DIVE for blitting. DIVE manages to write to the screen quickly by clipping against other windows once, and then storing that information so it doesn't have to be recalculated every blit. DIVE accomplishes this by creating code that when run will blit the buffer to the screen. This code handles the color remapping, stretching, clipping, and blitting in one operation. However, because new code is being created every time this routine is called, it should not be called with great regularity. The author of DIVE has told me that he believes that for small bitmaps where not much code will be generated, this routine should be fast enough to be called regularly, but I have not been able to determine this for a fact yet.

You will need to set up the blitter before you use DIVE for the first time, and then every time the visibility of your application changes by another window overlapping or your window moving or resizing. To do this you will have to intercept and process the WM_VRNENABLED and WM_VRNDISABLED messages. These are the visible region messages sent from the OS to your application. You have to request that these messages be sent with the function WinSetVisibleRegionNotify(hwndClient,TRUE). Calling this function with the second parameter set to FALSE turns off the notification.

The SETUP_BLITTER structure determines how DIVE readies itself for the blit. It is as follows:

typedef struct _SETUP_BLITTER {
   ULONG ulStructLen;        /* Length of structure */
   BOOL fInvert;             /* Image is inverted on blit */
   FOURCC fccSrcColorFormat; /* Source color format */
   ULONG ulSrcWidth;         /* Width in pixels of source buffer */
   ULONG ulSrcHeight;        /* Height in pixels of source buffer */
   ULONG ulSrcPosX;          /* X-coordinate of source data
                                  (lower- left relative to src buffer)  */
   ULONG ulSrcPosY;          /* Y-coordinate of source data
                                  (lower- left relative to src buffer)  */
   ULONG ulDitherType;       /* 0 is no dither. 1 is 2x2 dither */
   FOURCC fccDstColorFormat; /* Destination color format */
   ULONG ulDstWidth;         /* Width in pixels of destination buffer  */
   ULONG ulDstHeight;        /* Height in pixels of destination buffer  */
   ULONG ulDstPosX;          /* X-coordinate of dest data
                                  (lower- left relative to dst buffer)  */
   ULONG ulDstPosY;          /* Y-coordinate of dest data
                                  (lower- left relative to dst buffer)  */
   LONG lScreenPosX;         /* Screen X-coordinate for output */
   LONG lScreenPosY;         /* Screen Y-coordinate for output */
   ULONG ulNumDstRects;      /* Number of visible rectangles (for  clipping) */
   PRECTL pVisDstRects;      /* Pointer to array of destination  rectangles */
} SETUP_BLITTER;

This structure can be understood better by looking at the sample code distributed along with this article.

ULONG DiveBlitImage
 (
 HDIVE  hDiveInst,
 ULONG  ulSrcBufNumber,
 ULONG  ulDstBufNumber
 )

hDiveInst
Handle of DIVE instance.
ulSrcBufNumber
Buffer number of source buffer.
ulDstBufNumber
Buffer number of destination buffer.

For the ulDstBufNumber variable, one may also use the constants DIVE_BUFFER_SCREEN, DIVE_BUFFER_GRAPHICS_PLANE, or DIVE_BUFFER_ALTERNATE_PLANE. I have yet to see any good documentation on the use of the last two constants, and the first one is the only one we will concern ourselves with. This routine will take the source buffer and DIVE instance specified, and blit it to the screen or another DIVE buffer by running the code created by DiveSetupBlitter().

As you can probably tell from the input parameters of DiveSetupBlitter() and DiveBlitImage(), each DIVE instance may have more than one buffer associated with it, but each instance is limited to one area of the screen at a time (as specified in the SETUP_BLITTER structure.)

Two more functions are of interest to us, and they are used to specify the color remapping required.

ULONG DiveSetSourcePalette
 (
 HDIVE  hDiveInst,
 ULONG  ulStartIndex,
 ULONG  ulNumEntries,
 PBYTE  pbyRGB2Entries
 )

hDiveInst
Handle of DIVE instance.
ulStartIndex
Index of first palette entry to set.
ulNumEntries
Number of palette entries to set.
pbyRGB2Entries
Palette color values.

This routine is used to specify the palette for the source DIVE buffer. The array specified by pbyRGB2Entries is in the same format as an ordinary OS/2 palette.

ULONG DiveSetDestinationPalette
 (
 HDIVE  hDiveInst,
 ULONG  ulStartIndex,
 ULONG  ulNumEntries,
 PBYTE  pbyRGB2Entries
 )

Variable and routine descriptions are the same as DiveSetSourcePalette(), except this routine is used for the destination buffer, or the screen.

If a NULL pointer is given for pbyRGB2Entries, then DiveSetDestinationPalette() will use the actual palette of the device as the palette for remapping purposes.

It is important to note that neither of these palette functions actually sets the system palette. The programmer must set the system palette via the normal Gpi routines, and then tell DIVE what the palette has been changed to.

I will be covering palettes in more detail in my next article. The <a HREF="game1src.zip">sample code</a> that accompanies this article only has minimal palette code included.

Procedure for using DIVE

Ok, this is what you need to do in order to get a DIVE program working:

  1. Query the DIVE capabilities with DiveQueryCaps() to make sure DIVE exists and everything is ok with it. Check the screen color depth to make sure it is 256 colors or higher so that DIVE can use it.
  2. Use DiveOpen() to get an instance of DIVE.
  3. Use the DIVE instance handle to create a DIVE buffer with DiveAllocImageBuffer().
  4. Request visible region notification with WinSetVisibleRegionNotify().
  5. Send an initial WM_VRNENABLED message to ready the blitter for the first blit.
  6. If DIVE allocated your buffer, use DiveBeginImageBufferAccess() to get a pointer to your bufffer.
  7. Draw you image to the buffer.
  8. If DIVE allocated your buffer, end access to it with DiveEndImageBufferAccess() .
  9. Set the palette as needed (both system and Dive Source and Destination Palettes.)
  10. During the WM_VRNENABLED, setup the blitter with DiveSetupBlitter().
  11. Blit the image.
    Now you can alternate amongst setting the palette, changing the image, blitting, and so on as needed. When you end the program...
  12. Reset the system palette as needed.
  13. Turn off visible region notification with WinSetVisibleRegionNotify().
  14. Free your image buffer and release the buffer number with DiveFreeImageBuffer().
  15. Free the instance of DIVE with DiveClose().

Overall it is a fairly straightforward process. You don't have to use any of the routines creatively, and once you get one program running with DIVE you will probably just clip and paste a lot of the DIVE code from your first program into any later DIVE enabled programs.

I hope by now you are starting to grasp how to use DIVE. Reading the sample source code provided with this article is perhaps the best way to see how DIVE works in action.

Getting Down to Work: The Canvas Class

DIVE gives us a way to get a buffer of graphics to the screen, but how do we get the graphics into the buffer? The Canvas class serves this function. I have implemented Canvases as C structures with a group of associated functions that work with the structures, but it is implemented much more neatly as a C++ class. If you only work with C, it should not be too difficult to translate the canvas functions back to C from C++.

Design Considerations

My C++ is not good object oriented programming because of speed consideration. OOP works really well in situations where the code does not have to run as fast as humanly possible, but for games it often brings in a lot of overhead that is simply not needed. As a result, I have added inline routines for access to many of the canvas's internal variables. This allows other classes that must write to a canvas's surface to do so quickly and irectly. Alternative approaches include adding a routine to the Canvas class for each new type of drawing one needs to do, making each new class a friend class of the canvas, or making public the variables to which you need access. C++ offers a lot of advantages over C as far as organization and code re-use is concerned, but it has overhead that can drag your time critical code to a screeching halt. You will have to judge for yourself whether the overhead of C++ is worth it in a particular situation.

Another speed consideration for game programming is error checking. It is wise to catch errors before they can cause a system crash (or in OS/2's case, an application crash), but the overhead required slows games too much. Therefore for routines called often, error checking should be removed. You may want to use compiler directives or the assert() macro to include error checking in these time critical routines only during the development cycle, then remove the error checking when you release the game. I normally use a variant of the assert() macro in my programming, but I have removed such error checking from the sample code so as to make the code clearer.

For graphics, there is also the consideration of bounds checking. For example, let's say we want to write a rectangle to the canvas. If we draw a line past the right side of the canvas, part of the line will appear on the left side of the canvas due to the wrap-around nature of the buffer. To avoid this, we must test the endpoints of the line and clip the line if part of it extends beyond the canvas. We must write only the part of the line that is visible. However, if you know that the line will always be entirely visible on the screen this boundary checking and clipping is not needed. To avoid the extra overhead and speed up the routine, we don't perform bounds checking or clipping when we know the line is entirely on screen. Often this leads to two different versions of a routine; one with clipping and bounds checking and one without. Alternatively one could pass a flag to the routine instructing whether or not bounds checking needs to be done, but I prefer to avoid even the overhead associated with passing this variable and the extra branching code that decides whether or not to check. When we get to sprites, we will see that entirely different approaches need to be taken for drawing clipped sprites verses unclipped sprites, and having separate routines for each situation will make even more sense.

Finally, although DIVE supports a variety of input formats as far as color is concerned, for now we will only write canvas routines to support 256 color (8-bit) canvases. If you want to do 16, 24, or 32 bit graphics then the ideas presented here will work, but you will have to extend the size of buffer and the number of bytes per line. 256 color canvases are simple because you have one byte per pixel, and the width in bytes of a row is the same as the number of pixels in that row.

Bitmap Orientation

DIVE graphics work around a buffer. The buffer is simply a block of memory that is organized as a two dimensional array for the x and y coordinates. All the pixels in a horizontal stretch are stored one after another to form a row, and the rows are stored one after the other with one row per column. This is all pretty elementary if you have ever worked with bitmapped graphics before.

The only tricky part is that you have to deal with both top down and bottom up bitmaps. Top down bitmaps are the ones most people are familiar with if you have done any graphics programming in DOS. The first row in the buffer is the top row, the next is the next one down, etc. Therefore the first byte of the buffer makes up the top left pixel, and the last byte of the buffer makes up the lower right pixel. Top down bitmaps are used for (S)VGA graphics, PCX, GIF, JPEG, and the majority of bitmap storage formats. With these bitmaps, as your y coordinate increases, you move down the canvas.

Bottom up bitmaps have the first row on the bottom of the bitmap, the next row is right above it and so on. Here you have the first byte of the buffer as the lower left pixel, and the last as the upper right. Incrementing y coordinates move up the canvas. This format is used for OS/2 bitmaps (BMP format), and DIVE bitmaps.


Writing a top down bitmap to a bottom up bitmap requires that you invert the position of all the rows so that the top row is on the bottom, and the bottom row is on the top. Since later we will be working with the PCX format, and for me it is more natural to have the coordinate (0,0) in the top left hand corner of the bitmap, I organized the canvas class so that you can treat all bitmaps as top down. This is accomplished by taking advantage of a speedup we will need anyway, the row lookup table.

Allocating Memory

The canvas class allocates memory for the buffer using the standard C malloc. This can easily be replaced by the memory allocation scheme you desire. Note that by having the class allocate the memory instead of DIVE, you cannot use DIVE allocated bitmaps. This is fine, because we will be avoiding DIVE allocated bitmaps for several reasons.

Buffers allocated by DIVE may be in video memory or system memory, the performance from one setup to another is uneven. Also, since the location of these buffers may change, we cannot calculate a row lookup table. The table would become invalid every time the buffer's position changed. The lack of a lookup table means both the addressing is slower (as explained below) and we would have to worry about the orientation of the bitmap. To avoid these problems, we simply allocate the buffers for DIVE ourselves.

Addressing schemes

Normally you would determine a pixel's location in a buffer with the equation:

pbyLocation = pbyBuffer + (sYPos * sBufferWidth) + sXPos

This requires a multiply and two additions every time you figure out a position. Multiplication instructions are slow, and should be avoided if at all possible. Instead we can use a lookup table that contains the address of the starting pixel of each row. Now to calculate a pixel's position, we just use the y coordinate as an index into the starting position array, and add the x coordinate to it. Hence:

pbyLocation = apbyRowStart [sYPos] + sXPos

You compiler can optimize this down to be faster than a multiply instruction, and hand-tuned assembly can really make this approach fly. The added benefit of using this addressing scheme is that you don't have to know whether a bitmap is top down or bottom up; this information only needs to be known at the time the lookup list is created.

When you want to make graphics routines go fast, the fastest calculations are the ones that you don't do. Calculating the position of each pixel when you want to place it is just not efficient at all. A much better approach is to take the information you have and use it to derive other information. For example, when you are writing out a horizontal line, you need only calculate the position of the left-most pixel. All other pixels will be right after the first one in memory. Also, for vertical lines you need only calculate the top-most pixel. For top down bitmaps, the next pixel will be forward in memory the size of one row; you need only add the size of a row to the current pointer to get the next pixel. For bottom up bitmaps, you must move backwards in memory this distance. To facilitate this process, simply calculate the row width, make it positive or negative depending on the bitmap orientation, and store it for later use. This gives us:

pbyNextHorizPixel = pbyCurrPixel + 1;

and

pbyNextVertPixel = pbyCurrPixel + lRowIncrement;

For many operations you will only need to calculate an address once, then you can increment a pointer to move to the right, and you can subtract or add the row increment value to move up or down respectively.

Consider in the sample code the routines PutPixel (), HorizLine (), and VertLine (). Notice how I did not call the PutPixel routine repeatedly to draw the lines. Not only is the recalculation of each pixel's position unneeded overhead, but the overhead for even calling the routine wastes valuable time. Also notice how drawing a horizontal line is approached somewhat differently than drawing a vertical line. I could have drawn each pixel separately for the horizontal line routine, but using memset () or the inline assembly code will prove even faster.

The Sample Code

The sample code that comes with this article simply initializes DIVE, creates a Canvas object, draws a simple pattern to that canvas, and displays the canvas during the WM_PAINT messages. The screen of the demo when it runs should look like this:

Game Sample

For most games you will not blit your images during the WM_PAINT message, but this is the simplest way to do it and it will serve for this demo. In a later article we will look at blit threads and how to use them.

I did not cover palettes in this article, so only the most basic palette code is included in the sample source. A very good article on palettes was done in issue 0101 of EDM/2, and I would suggest that you look it up if you are unfamiliar with palettes under OS/2. I will cover palettes briefly in my next article.

Conclusion

Well, that about covers it. I hope that I explained the basics of DIVE and canvases somewhat clearly. The canvas class doesn't do much right now, but it is the foundation for the rest of our graphic routines to come later. I have only implemented a few graphic primitives because our games will be sprite based. The reader is free to implement more primitives as needed.

I highly suggest you read through the sample code that accompanies this article. I have tried to make it as clear and self-explanatory as possible while still remaining fast.

I encourage you to write me back and let me know how I did in explaining things. This is my first attempt at writing an article for a magazine, and I know my writing and explaining styles still have a lot of areas that need to be polished. I also hope that I did not lose the beginning programmer or bore the advanced programmer. As you write to me and I learn who my audience is, I will be able to better target future articles to suit you.