In this chapter and the next, you'll examine multimedia and game objects. In particular, you will see the following:
The graphics engine used in this chapter is implemented on disk both in C++ and in Object Pascal, and it appears to BCB programmers as a set of simple components. The game engine, shown in the next chapter, is implemented in C++, and it appears to the user as a set of objects. In short, the graphics engine consists of components, and the game engine consists of simple objects.
You can use all the code found in this book in your own programs, but you cannot use the art or designs found in this chapter and the next. My objection is to distributing the art in your own programs. You are free to use the art in your own home or at your company, but you cannot distribute it to others in any form. You can use the game engine to build your own games, but it must be distinct from the game you find in the next chapter. The world must be different, and so should the art.
I hope that many readers are specifically interested in developing and using game engines. If you're not interested in that subject, I still think that you will find these chapters among the most interesting in the book. In particular, in these pages you will see how to create complex sets of objects and use them in real-world programs that have to perform complex tasks.
The DirectX technology shown in these chapters can be used in a wide variety of applications. For example, you can use the graphics engine shown here to create the following:
Besides describing the raw graphics tools in this chapter, I also show you how to create complex object hierarchies, how to plan and design the functionality of an object properly, how to divide groups of objects into specific engines, and how to get the engines to work together.
The overriding theme of these chapters is the importance of object design. You have to learn how to create objects that are reusable and how to design object hierarchies that are simple enough to meet the needs of real programmers.
In the end, everything comes down to design. If you design the right kinds of objects, you can accomplish almost anything. Furthermore, as you will see, the judicious use of objects can help you dig yourself out of almost any hole. Objects are the Holy Grail of contemporary programming, and if you understand how to use them correctly, they will help you complete project after project on time.
Just understanding the basics of object technology is not enough. Just knowing about polymorphism, inheritance, and encapsulation is not enough. You also need to know how to design objects that you can use and that other people can use. This section of the book is about designing these kinds of objects. Of course, on the way, you will also have the chance to engage in a good deal of fun with graphics, games, and multimedia.
Game programming is more complicated than most standard Windows programming tasks for many reasons. The root of this complexity is the need to use special graphics tools such as DirectX. Another complication that many first-time game programmers underestimate involves creating art.
Many books are available on game programming and on
producing art for games. Two
that I have found useful are
Tricks of the Game Programming GurusI?, LaMothe, Ratcliff,et al., Sams Publishing
The Ultimate Game Developers Sourcebook, Ben Sawyer, Coriolis Group Books.
You will also need a paint program. I find that the inexpensive shareware program
called Paint Shop Pro meets most of my needs, though many other powerful programs
such as Fractal Paint (www.fractal.com)
are available. Here's the contact information for Paint Shop Pro:
JASC, Inc.
P.O. Box 44997
Eden Prairie, MN 55344
930-9171
www.jasc.com
Other key paint programs that I use often include TrueSpace (www.caligari.com)
and VistaPro from Virtual Reality
Laboratories in San Luis Obispo. TrueSpace allows
you to create three-dimensional objects, and VistaPro allows you to create landscapes.
The background scenes that show rolling hills, mountains, and trees in the figures
for this chapter and the next
were created in VistaPro (Virtual Reality Laboratories
in San Luis Obispo, CA, 805-545-8515, e-mail: VRLI@aol.com,
WWW: http://www.romt.com). (See
the files
called Backgrd1.bmp and Backgrd2.bmp on the CD that comes
with this book for examples of VistaPro files that have gone through a color reduction
so that they could fit on a 256-color palette.)
You might find additional game components, links, or code available at these sites:
http://www.silvercrk.com/directx/ http://www.spinlogic.com/GameDev/ http://users.aol.com/charliecal
Many other Web sites are of interest to game developers, but you can find links to most of them from the sites listed here. On CompuServe, type GO GAMEDEV to find the game developers' forum.
As I stated previously, game programming is an extremely complex undertaking. The biggest mistake you can make is to try to create your own tools from scratch. Use the tools included in this book to get some sense of what you can gain from a graphics engine and a set of gaming tools. Then go out and search the Web and your local bookstores for ready-made graphics and game engines. Build your games using these tools; don't try to create your own tools from scratch unless you're sure, double-sure, and then triple-sure you know what you're doing and why.
If you have never built a game before, then don't even consider building one from scratch. Build your first game with someone else's engine. Then, after you understand something about the tools that are available, you might finally be in a position to consider creating some of your own tools. Even then, however, I would still recommend turning around and using someone else's tools rather than trying to create your own. The Ultimate Game Developers Sourcebook, mentioned previously, offers a great deal of information on finding third-party game engines.
In this part of the book, you'll create two sets of objects. Using the first set of objects, you can encapsulate DirectX multimedia functionality inside easy-to-use components. The second set of objects allows you to create games, simulations, or multimedia tools using the game engine.
The classic mistake to make in this process is to find or create the objects that give you access to the multimedia and DirectX interfaces and to then think you're ready to create a program. This error is fatal--and very common.
Suppose that you have a set of objects that give you full access to DirectDraw. The mistake many programmers make is to assume that they can directly use these objects when creating a game or simulation. Indeed, in some cases, that may be possible. However, most of the time you need to create a second set of objects tailored specifically for games or simulations. In particular, you need a TGameEngine object. Furthermore, this object is not necessarily a direct descendant of any of your graphics-based objects. It may use them via aggregation but probably does not descend directly from them. The same applies for simulations: You need a TSimulation object that uses your graphics objects but presents a different interface to the user. A game object, for example, needs items such as TPlayer, TBoard, THero, TScene, and so on. Your graphic objects don't know anything about those kinds of objects.
To get a handle on this issue, you need to think in terms of specific problem domains. One problem domain involves using DirectDraw to create fast graphics. A second problem domain involves creating a game engine, and a third problem domain involves creating a specific game. You need to draw firm lines between these domains so that the complexity of one problem domain does not creep into another domain. As a rule, you should not allow any direct calls to your graphics objects by the users of your TGameEngine object. TGameEngine calls the graphics objects, but it does so via private interfaces that TGameEngine users can never see.
NOTE: An example might be helpful here. In DirectX, you can write text to the screen using device contexts. You get the DC for a device, copy objects into it, and then use TextOut to write to the screen. Having the game engine use these same objects is probably not wise. Instead, you should hide the underlying technology and provide a routine with a name such as WriteXY or WriteLn or printf or Say that will let you write to the screen. Internally, your implementation of WriteXY will use device contexts, but that fact should be hidden from the user. This will allow you to switch your game from Windows to DOS to the Mac without having to change its code. Only the interface to the graphics engine changes, but your game code remains untouched.
One way to help design these objects correctly is to ensure that a user of TGameEngine would have to do nothing more than recompile if you were to switch your game engine technology from one set of graphics objects to another. In other words, the users of TGameEngine should not have to know anything about the tools you use to create fast graphics. You can tell them, "Hey, I use DirectDraw," or "I use WinG," but the actual technology involved should not matter to them in the sense that their code will still compile if you switch the background engine. Users of TGameEngine should not have to know whether TGameEngine uses DirectDraw or WinG. Users of TGameEngine never call the graphics objects directly, and they don't even have to conceive of the fact that such a thing as an IDirectDraw object exists.
Obviously, one of the important benefits of this system is that you can change from WinG to DirectDraw without having to rewrite your game or game engine. But this capability is not the primary benefit that interests me. What I like about this system is that it creates domains for problems. If I'm working with the TGameEngine object and find that it's not drawing things correctly because of some error in the way I access DirectDraw, then I can put the TGameEngine object away, open the graphics engine, and find out what is wrong. My game engine and my game cannot be the cause of the problem because they don't talk to the IDirectDraw interface. They don't know anything about the IDirectDraw interface!
Another benefit of this system is that it allows you to improve one part of your code without affecting existing parts of your code. For example, I could drastically rewrite the graphics engine without having to change the way TGameEngine operates. In these situations, rewriting the graphics engine without changing its interface is best, but even if you do change it, you can still buffer your game from these changes because it talks to TGameEngine, not to the graphics engine. However, the best thing to do is to never change a public interface after you create it.
NOTE: I will concede that some of the theory I am describing here is a bit idealized. In practice, I rarely achieve precisely the degree of object independence described in the preceding paragraphs. However, the closer I can get to it, the better off I am. Aiming as high as possible is always best, thereby limiting as much as possible the amount of trouble you will have if your architecture needs to be upgraded.
Furthermore, the process of learning to design good objects is incremental at best. The first time I tried to create a complex set of objects, I failed miserably. The second time, I took to heart some of the ideas I am describing here and came closer to achieving my goal. The third time out I made yet fewer gross design errors, and so on, till finally I was starting to produce complex object hierarchies that could actually be used and maintained.
The last paragraph of the preceding section introduces a second key rule of object-oriented design. Creating problem domains is the most important step, but a second crucial condition is creating public interfaces that have little to do with your underlying technology.
When you're creating the public interface for the graphics engine, you should take off your IDirectDraw expert hat and put on one that helps you pretend you are the user of this object. What will a user of this object want to see? A user doesn't want to hear about the nitty-gritty of using IDirectDraw; instead, he or she wants to be able to work with broad concepts such as TBackground, TSprite, and TWindow.
Before going further, I want to talk just a little more about limiting the problem domain.
One of the most important jobs that objects can do for you is to help manage complexity. In the first chapter of this book, I stressed the importance of being humble when you program. Here, that theme can really start to pay off.
The worst mistake you can make as a programmer is to decide "Hey, I'm so smart, so cool, that I can make this object stunningly complex and efficient and still be able to use it." In the process, you might end up continually collapsing one object into another and continually finding fast, clever ways to get tasks done in the fewest possible moves. All this cleverness is good and well, if you can simultaneously produce objects that you and others can use and maintain.
Putting all the logic into one set of objects has the following problems:
Putting all the logic in clearly delineated sets of objects has the following advantages, which are the converse of the disadvantages laid out in the preceding three points.
The theme underlying all these points is humility. To be a good programmer, you have to admit that a certain level of complexity simply overwhelms you. Your only bulwark against that confusion is to break down the project into manageable chunks that you can understand.
Humble programmers produce real programs. Hotshots are going to produce fantastic programs Real Soon Now. I know that's true because those hotshots have been telling me about their program ideas for years.
In the next few sections of the chapter, I describe a set of components that make up a simple graphics engine. The key components in this graphics engine are described in Table 28.1.
Component | Purpose |
THermes | Get into and out of DirectX Exclusive mode |
TScene | Draw the background of a graphics scene |
TSpriteScene | Draw the background of a graphics scene that supports sprites |
THermesChart | Create a tiled game scene |
DirectX is a Microsoft technology that allows you to write high-performance gaming, simulation, and multimedia programs. In this chapter, I focus on the IDirectDraw interface, which is a portion of DirectX that gives you the ability to write directly to the video buffer.
The first thing you find when working with DirectX is that, to take full advantage of it, you need to switch into a special video mode called Exclusive mode. For the purposes of this book, this mode has a 640x480 screen dimension and 256 colors. Actually, other screen sizes and color resolutions are available to you as a DirectX programmer, but THermes gives you access only to this one screen mode.
The first object in the graphics engine is designed to switch you into and out of this mode. The class declaration for this object is shown in Listing 28.1. You can find the full source for the code shown here in a file called Mercury2.cpp in the Utils directory on the CD that accompanies this book.
The Mercury unit relies on Creatures.pas, which ships with this book, and DDraw.h, which ships in the included directory with BCB. You will also find a Pascal version of the Mercury unit in the Units subdirectory on the CD that accompanies this book.
NOTE: To find some Web sites of interest to DirectX programmers and full Pascal translations of DirectX 3.X headers, go to the following:
http://www.dkw.com/bstone/
When you're looking at this code, keep these points in mind:
class THermes : public Classes::TComponent { typedef Classes::TComponent inherited; friend THermesChart; friend TDraw; private: bool FActive; Creatures1::TFileCreatureList* FCreatureList; HWND FHandle; bool FTimerOdd; int FTimerInterval; bool FExclusive; Classes::TNotifyEvent FPaintProc; TScene* FScene; bool FUseTimer; bool FFirstTime; IDirectDraw* FDirectDraw; IDirectDrawSurface* FBackSurface; IDirectDrawClipper* FClipper; IDirectDrawSurface* FPrimarySurface; bool __fastcall CreatePrimary(void); void __fastcall DDTest(long hr, System::AnsiString S); void __fastcall InitBaseObjects(void); bool __fastcall MakeItSo(long DDResult); void __fastcall SetScene(TScene* Scene); bool __fastcall SetUpBack(void); protected: void __fastcall DrawBitmaps(void); virtual long __fastcall RestoreSurfaces(void); public: __fastcall virtual THermes(Classes::TComponent* AOwner); __fastcall virtual ~THermes(void) {} void __fastcall EndExclusive(void); void __fastcall ErrorEvent( System::AnsiString S); void __fastcall Flip(void); virtual void __stdcall InitObjects(void); virtual void __fastcall Run(void); __property bool Active = {read=FActive, write=FActive, nodefault}; __property IDirectDrawSurface* BackSurface = {read=FBackSurface, write=FBackSurface, nodefault}; __property Classes::TNotifyEvent OnDrawBitmap = {read=FPaintProc, write=FPaintProc}; __published: __property Creatures1::TFileCreatureList* CreatureList = {read=FCreatureList, write=FCreatureList, nodefault}; __property bool Exclusive = {read=FExclusive, write=FExclusive, nodefault}; __property TScene* Scene = {read=FScene, write=SetScene, nodefault}; __property int TimerInterval = {read=FTimerInterval, write=FTimerInterval, nodefault}; __property bool UseTimer = {read=FUseTimer, write=FUseTimer, nodefault};
};
To install THermes and the other graphics engine objects, choose Component
| Install. Click the Add button, and then browse the Units directory from
the CD that accompanies this book.
Install both Creatures1.pas and Mercury2.cpp.
To use THermes, start a new project, drop a THermes object on it, and create an OnKeyDown handler that closes the form if any key is pressed. The code for such a project is shown in Listings 28.2 and 28.3.
/////////////////////////////////////// // Main.h // Testing the THermes object // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include "..\..\utils\Mercury2.h" #include <vcl\Menus.hpp> class TForm1 : public TForm { __published: THermes *Hermes1; TMainMenu *MainMenu1; TMenuItem *Run1; void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift); void __fastcall Run1Click(TObject *Sender); private: void __fastcall MyExceptions(TObject *Sender, Exception *E); public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1;
#endif
/////////////////////////////////////// // Main.cpp // The TestHermes project tests the THermes component // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma link "Mercury2" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { Application->OnException = MyExceptions; } void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if ((Shift.Contains(ssAlt)) && (Key == `E')) throw Exception("Some Exception"); else if (!Shift.Contains(ssAlt)) Close(); } void __fastcall TForm1::Run1Click(TObject *Sender) { Hermes1->Run(); } void __fastcall TForm1::MyExceptions(TObject *Sender, Exception *E) { Hermes1->EndExclusive(); ShowMessage(E->Message);
}
You should note that this project probably will not run correctly unless you add
Creatures.pas to it, as shown in the USEUNIT statement from the
project source:
//-------------------------------------------------------------------------- #include <vcl\vcl.h> #pragma hdrstop //-------------------------------------------------------------------------- USEFORM("Main.cpp", Form1); USERES("HermesTest1.res"); USEUNIT("..\..\Units\creatures1.pas"); //-------------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TForm1), &Form1); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; } //--------------------------------------------------------------------------
Run the project once with Exclusive set to False. Click the Run button once to switch on DirectX. In this mode, you should see the color of the main form change, but nothing else very special will happen.
Go back into design mode, and check once to make sure that you have not placed any components on the main form other than the menu and the THermes. The rest of the form must be free of visual controls so that you can detect keystrokes directly on the surface of the form.
Set Exclusive to True and run the program again. Click the Run menu item, and the whole screen should go blank as you switch into Exclusive mode. To get out of the mode, simply press any key.
If you want, you can change the code for the OnKeyDown event:
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if ((Shift.Contains(ssAlt)) && (Key == `X')) Close(); }
With this code in place, you will be able to press Alt+Tab to move away from the main window of the program and switch out of Exclusive mode. You can then press Alt+Tab to move back to the program and press Alt+X to exit. When you're pressing these keys, the actual picture drawn on your screen when in Exclusive mode is undefined because THermes does not control the output in Exclusive mode; it controls only the act of entering and exiting Exclusive mode.
DirectX allows you to run applications in a windowed mode. That is, you don't have to slip into Exclusive mode to run DirectX applications. However, windowed mode is not very good in terms of performance. In fact, you'll find little advantage to a windowed mode DirectX application over a GDI-based standard Windows program.
I give you access to windowed mode primarily because it is valuable when you're debugging an application. In short, you should run your DirectX applications in windowed mode while you're debugging them so that you can step through the debugger while viewing your application's output. After you set up your application correctly, switch into Exclusive mode and test it.
If an exception happens when you're in Exclusive mode, that is a bad thing. In particular, your program will appear to lock up, and you may be forced to reboot. Following the steps outlined in the preceding paragraphs helps you eliminate the possibility that this will happen. However, you cannot be sure that an exception will not be raised in your application. As a result, you might want to override the exception handler for your application in this case:
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { Application->OnException = MyExceptions; } void __fastcall TForm1::MyExceptions(TObject *Sender, Exception *E) { Hermes1->EndExclusive(); ShowMessage(E->Message); }
The constructor for the main form designates a custom routine called MyExceptions to handle all the exceptions that occur in this application. Whenever an unhandled exception occurs, it will be passed to this routine. When I get the exception, I call Hermes1->EndExclusive, which pops the application out of Exclusive mode, therefore making it possible to open the dialog box that shows the error message.
NOTE: This system will not do you any good, of course, unless you turn off the Break On Exception option, which you can find by choosing Options | Environment | Preferences. If you are in Exclusive mode, and the IDE tries to take you to the line of code in your application where an exception occurred, your system will appear to lock up.
As a rule, I will not show you the underlying Object Pascal code that makes this component work. However, in this one case, I will show you what goes on:
procedure THermes.EndExclusive; begin if (FDirectDraw <> nil) then begin FDirectDraw.FlipToGDISurface; if FExclusive then FDirectDraw.SetCooperativeLevel(FHandle, DDSCL_Normal); end; end;
This code first flips away from DirectX to GDI mode and then cuts out of Exclusive mode by setting the cooperative level to normal. When I take the application into Exclusive mode, I make the same call but with different parameters:
DDTest(DirectDrawCreate(nil, FDirectDraw, nil), `InitObjects1'); if not FExclusive then Flags := DDSCL_NORMAL else Flags := DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN; DDTest(FDirectDraw.SetCooperativeLevel(FHandle, Flags),'SetCooperativeLevel'); if FExclusive then begin hr := FDirectDraw.SetDisplayMode(640, 480, 8); if(hr <> DD_OK) then raise EDDError.CreateFmt(`THermes.InitObjects: %d %s', [hr, GetOleError(hr)]);
end;
Notice that my EndExclusive method makes no attempt to set the display mode back to the dimensions and bit depth selected by the user in his or her system configuration. Instead, I just leave it to the user to close the application, which will return the screen to its normal resolution. My goal in using EndExclusive is just to keep the system from hanging.
After you know how to get into and out of Exclusive mode, the next step is to learn how to draw a picture to the screen. Most games consist of many pictures, but I start out showing you how to use a simple object that just displays a single bitmap. For most people, this object is not really very useful, in that there is no point in using DirectX if you want to show only one picture. However, TScene serves as a base class for other objects that handle more complex tasks. I will therefore spend a few moments showing you how to use it so that you will understand the class on which the other objects are built.
You can find the declaration for the class and some code that uses it in Listings 28.4 through 28.6. Again, I show only the class declarations for the Pascal code and give the complete listings for the C++ code.
class TDraw; class TDraw : public Classes::TComponent { typedef Classes::TComponent inherited; friend THermesTiler; friend THermesChart; friend TScene; friend TSpriteScene; private: AnsiString FDLLName; THermes* FHermes; HANDLE FLib; IDirectDrawPalette* FPalette; int FTransparentColor; bool __fastcall CreateDDSurface(IDirectDrawSurface* &DDS, System::AnsiString BitmapName, bool UsePalette); IDirectDrawSurface* __fastcall CreateSurface(HANDLE Bitmap); HANDLE __fastcall GetDib(HANDLE Instance, System::AnsiString S); IDirectDrawPalette* __fastcall LoadPalette(HANDLE Instance, const System::AnsiString BitmapName); public: __fastcall virtual TDraw(Classes::TComponent* AOwner); __fastcall virtual ~TDraw(void); void __fastcall WriteXY(int X, int Y, System::AnsiString S); __property int TransparentColor = {read=FTransparentColor, write=FTransparentColor, nodefault}; __published: __property AnsiString DLLName = {read=FDLLName, write=FDLLName}; }; class TScene : public TDraw { typedef TDraw inherited; friend THermes; private: System::AnsiString FBackgroundBitmap; Graphics::TColor FBackColor; RECT FBackRect; tagPOINT FBackOrigin; bool FBlankScene; bool FShowBitmap; IDirectDrawSurface* FWorkSurface; Classes::TNotifyEvent FOnSetupSurfaces; Classes::TNotifyEvent FOnDrawScene; public: __fastcall virtual TScene(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); virtual void __fastcall DrawScene(void); virtual long __fastcall RestoreSurfaces(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); __published: __property System::AnsiString BackgroundBitmap = {read=FBackgroundBitmap, write=FBackgroundBitmap, nodefault}; __property bool BlankScene = {read=FBlankScene, write=FBlankScene, nodefault}; __property long OriginX ={read=FBackOrigin.x, write=FBackOrigin.x, nodefault}; __property long OriginY ={read=FBackOrigin.y, write=FBackOrigin.y, nodefault}; __property bool ShowBitmap = {read=FShowBitmap, write=FShowBitmap, default=1}; __property Classes::TNotifyEvent OnDrawScene = {read=FOnDrawScene, write=FOnDrawScene}; __property Classes::TNotifyEvent OnSetupSurfaces = {read=FOnSetupSurfaces, write=FOnSetupSurfaces}; __property Graphics::TColor BackColor = {read=FBackColor, write=FBackColor, nodefault}; __property TransparentColor ; public: __fastcall virtual ~TScene(void) { }
};
/////////////////////////////////////// // Main.h // Test the TScene object // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\Menus.hpp> #include "Mercury2.h" class TForm1 : public TForm { __published: TScene *Scene1; THermes *Hermes1; TMainMenu *MainMenu1; TMenuItem *Run1; void __fastcall Run1Click(TObject *Sender); void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift); private: public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1;
#endif
/////////////////////////////////////// // Main.cpp // Test the TScene object // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma link "Mercury2" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::Run1Click(TObject *Sender) { Hermes1->Run(); } void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if ((Shift.Contains(ssAlt)) && (Key == `X')) Close();
}
The program shown here can switch you into Exclusive mode and show a picture
like the one shown in Figure
28.1.
Figure 28.1. The main screen of the TestScenel program in windowed mode.
To get started with the TScene object, drop both it and the THermes object on a form. Use the Scene property of THermes to connect it to the TScene object. Click the TScene object, and select a background bitmap by browsing the Chap28/Media directory included on the CD that accompanies this book. Select any bitmap that is smaller than or equal to 640x480 and that has 256 colors in it. For example, you might choose BackGrd2.bmp. Set the Transparent color to black, which will appear at offset zero on the bitmaps. Set the BackColor property of the bitmap to the same shade, and set BlankScene to True:
TransparentColor = 0; BackColor = clBlack; BlankScene = True;
NOTE: TransparentColor refers to the color in a bitmap that will be transparent. That is, the areas in the bitmap where this color is present become transparent. You need a transparent color when you want to blit an irregularly shaped bitmap on top of a background bitmap. For example, if you have a picture of a bird that you want to blit onto a picture of the sky, just set the background area in the picture of the bird to the transparent color, and it will not show up when the bird is blitted to the screen. In other words, the picture of the sky will show through the transparent area, so the bird will appear directly against the sky, with no intervening border.
You choose the selected transparent color from an offset in the palette of the bitmap you're using. For example, if you set TransparentColor to 0, the first color in the bitmap's palette will be the transparent color. Most good paint programs will show you the palette for a picture. For example, Paint Shop Pro provides this service.
If you select a prominently used color from a picture as the transparent color, the portions of the picture with that color in them will look strange at runtime. To avoid this situation, set the BackColor property of TScene and the TransparentColor for the picture to the same shade. Also set BlankScene to True. This way, the transparent color will look through onto a background that is the same shade as the color you're making transparent.
The BlankScene property blanks the background of the TScene object to the color found in the BackColor property. This capability can be useful in the current situation and also when you have a small bitmap that you want to show against a blank window set to some particular color.
When you're creating multimedia applications in Windows, the palette of your bitmaps is very important. In particular, you should usually create the same 256-color palette for all your bitmaps. Use a program such as Paint Shop Pro to set up these bitmaps. Most of the bitmaps included in the Chap28/Media directory on the CD that comes with this book have a palette designed for pictures that have lots of green fields, brown earth, and a range of skin tones.
When you're working with only one bitmap in a scene, all this business about palettes seems unnecessarily complicated. However, after you start adding sprites to the scene, you will be glad that you have some way to set up background colors and to set up TransparentColor. I explain more about palettes in the next section of this chapter.
The TScene object also has a built-in timer that will continually redraw the picture at preset intervals. This capability is not particularly useful in the case of a program that blits only one picture to the screen. However, when you start working with multiple bitmaps, some of which are animated, this capability becomes a key feature. In particular, DirectX allows you to draw to an offscreen buffer, which is then blitted to the screen almost instantly. Using this method is an excellent way to achieve smooth animation.
Instead of using a timer, you can also handle the TApplication::OnIdle event, and then call Hermes1->Flip inside that event handler. The OnIdle event will be called whenever the CPU is not otherwise occupied.
To get the TScene object set up correctly, you should turn on the timer and set the TimerInterval property to some reasonable value, such as 250 milliseconds. Setting this property will cause the picture to be refreshed four times a second. This number is too slow for animation, but it works fine for the simple scene created here. In particular, if you press Alt+Tab to move away from the DirectX program, the timer will automatically restore the picture when you press Alt+Tab to move back to the main scene.
On the CD that accompanies this book, you will find a program called TestScene1. The source for this program is shown in Listings 28.5 and 28.6. You can use this code as a guide for using the TScene object. Remember that TScene is not really meant to be used in a real program. It is just a base object from which other, more useful objects can descend.
In Windows and DOS games, you almost always work with a 256-color palette. Someday this will change, but for now you should get used to working with these palettes and come to understand them well. An in-depth description of palettes is available in some of the books and Web sites mentioned in the section called "Games Resources" at the beginning of this chapter.
Using Paint Shop Pro, you can save a palette to disk as a text file:
JASC-PAL 0100 256 0 0 0 128 0 0 0 128 0 128 128 0 0 0 128 128 0 128 0 128 128 192 192 192 176 136 72 216 192 160 ... // Many numbers omitted here... 240 224 208 160 160 164 128 128 128 255 0 0 0 255 0 255 255 0 0 0 255 255 0 255 0 255 255 255 255 255
Here is how to decode the first few lines of this file:
JASC-PAL 0100 256 0 0 0
JASC-PAL identifies the palette as belonging to Paint Shop Pro. The second line defines the number of entries in the palette in hexadecimal format. The third line has the number of entries in base ten notation. The fourth line is the first member of the palette in RGB format, with all values set to 0, which is black.
Notice that my palette runs through various shades:
118 102 237 117 116 240 129 113 249 155 110 239 107 144 140 115 144 140 123 136 140 123 148 140 111 148 148
Clearly, I'm interested in this area of the palette.
Obviously, you don't want to have to work with raw numbers like this. Paint Shop Pro will let you view a palette as an array of colors, as shown in Figure 28.2. Having some kind of tool that will help you design your palette is essential. You should also find books, such as this one and the ones mentioned previously, that include sample palettes.
Figure 28.2. Using Paint Shop Pro to view an array of shades that make up a 256-color palette.
Now that you know how to show a simple picture in DirectX mode, and now that you know a bit about palettes, you're ready to start working with sprites. The sprite support I have at this time is very rudimentary, as you can see in Listings 28.7 through 28.9. You could, however, use these base sprite classes to create sprites that are more full-featured. You should also check my Web site for updates to this code.
class TSprite : public Classes::TComponent { typedef Classes::TComponent inherited; friend TSpriteScene; private: System::AnsiString FBitmap; tagPOINT FPosition; IDirectDrawSurface* FSurface; RECT FRect; public: bool __fastcall IsHit(int X, int Y); __property IDirectDrawSurface* Surface= {read=FSurface, write=FSurface, nodefault}; __property RECT Rect = {read=FRect, write=FRect}; __published: __property System::AnsiString Bitmap={read=FBitmap, write=FBitmap, nodefault}; __property long XPos = {read=FPosition.x, write=FPosition.x, nodefault}; __property long YPos = {read=FPosition.y, write=FPosition.y, nodefault}; public: __fastcall virtual TSprite(Classes::TComponent* AOwner) : Classes::TComponent(AOwner) { } __fastcall virtual ~TSprite(void) { } }; class TSpriteScene : public TScene { typedef TScene inherited; private: Classes::TList* FSpriteList; protected: __fastcall virtual ~TSpriteScene(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); public: __fastcall virtual TSpriteScene(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); void __fastcall AddSprite(TSprite* Sprite); virtual void __fastcall DrawScene(void); virtual long __fastcall RestoreSurfaces(void); __property Classes::TList* SpriteList = {read=FSpriteList, write=FSpriteList, nodefault}; __published: __property TransparentColor;
};
/////////////////////////////////////// // Main.h // Project: SpriteTest1 // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\Menus.hpp> #include "Mercury2.h" class TForm1 : public TForm { __published: THermes *Hermes1; TSpriteScene *SpriteScene1; TSprite *Sprite1; TSprite *Sprite2; TMainMenu *MainMenu1; TMenuItem *Run1; void __fastcall SpriteScene1SetupSurfaces(TObject *Sender); void __fastcall Run1Click(TObject *Sender); void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift); private: public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1;
#endif
/////////////////////////////////////// // Main.cpp // Project: SpriteTest1 // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma link "Mercury2" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::SpriteScene1SetupSurfaces(TObject *Sender) { SpriteScene1->SpriteList->Add(Sprite1); SpriteScene1->SpriteList->Add(Sprite2); } void __fastcall TForm1::Run1Click(TObject *Sender) { Hermes1->Run(); } void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if ((Shift.Contains(ssAlt)) && (Key == `X')) Close(); }
Don't forget that when using this program, you will probably have to explicitly add Creatures1.pas to your project:
//-------------------------------------------------------------------------- #include <vcl\vcl.h> #pragma hdrstop //-------------------------------------------------------------------------- USEFORM("Main.cpp", Form1); USERES("SpriteTest1.res"); USEUNIT("..\..\Units\creatures1.pas"); //-------------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TForm1), &Form1); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; }
Figure 28.3 shows the TestSpriteScene program in action. In this program, I make no attempt to animate the sprites. To do so, you can merely change the x- and y-coordinates at which they are shown and then wait for the screen to be updated automatically by the timer.
Figure 28.3. The TestSpriteScene program shows a background bitmap with two sprites placed on it.
To work with the TSpriteScene object, you should drop a THermes, a TSpriteScene, and one or more TSprite objects on a form. Connect the THermes object to the sprite scene. Pick a background bitmap and a transparent color for the sprite scene. In the sample program, I use BackGrd2.bmp for the background and 254 for the transparent color.
For the bitmaps that ship in the Chap28/Media directory, I assume that the background color is the 254 element in the palette of the background bitmap. This color is a robin's egg blue, which means that this particular shade of blue cannot be used in any of the bitmaps except for areas that you want to be transparent. Don't forget that if a particular RGB pattern such as 255, 0, 255 is designated as the transparent color, you can change any one value to get a nearby color that is not going to appear as transparent. For example, you can choose 255, 1, 255, which is virtually identical in shade to 255, 0, 255, but it will not be transparent to the user and can be shown in your bitmaps.
Connect the TSprite objects to a bitmap. I connect the first sprite to Queen2.bmp and the second sprite to Monk2.bmp.
NOTE: These graphical figures were created by Kari Marcussen and, like all the artwork in this book, are copyrighted and cannot be used in your own programs. You can, of course, use the art in the "privacy of your own home," but you cannot distribute programs of any kind, even free programs, that contain this art.
The XPos and YPos positions for the first sprite are 100x200, whereas the second sprite is 400x200. You don't have to be concerned with the size of the sprite itself, as the components will calculate the size automatically at runtime.
You need to have some way of telling the TSpriteScene object about the sprites that it owns. I use a simple event handler of TSpriteScene called OnSetupSurface for this purpose:
void __fastcall TForm1::SpriteScene1SetupSurfaces(TObject *Sender) { SpriteScene1->SpriteList->Add(Sprite1); SpriteScene1->SpriteList->Add(Sprite2); }
SpriteList is a simple TList descendant. Keeping all the child sprites in a list allows for easy deallocation of memory:
void __fastcall TSpriteScene::DestroyObjects(void) { if(FSpriteList != NULL) { for(int i = 0; i < FSpriteList->Count; i++) ((TSprite*)(FSpriteList->Items[i]))->Surface->Release(); FSpriteList->Clear(); } }
This kind of routine is important because you might want to switch between numerous TScene descendants during the course of a program. In other words, your game may have more than one scene in it. I will explain this process in more depth later in the chapter. However, the quick overview is that you can simply write code that looks like this:
Hermes1->Scene = Scene1; ... // Code omitted here. Hermes1->Scene = Scene2;
In this case, the program starts by showing Scene1 and then at some later point switches to Scene2. When you switch scenes, the DestroyObjects method for the previous scene is called, thereby deallocating all the memory associated with background and sprite bitmaps. At the same time, new memory is allocated for the bitmaps used in Scene2.
The SpriteList also is important when you switch away from a program in Exclusive mode and then press Alt+Tab to move back to it. At those times, the following routine is called:
long __fastcall TSpriteScene::RestoreSurfaces(void) { TSprite *Sprite; HRESULT Result; Result = TScene::RestoreSurfaces(); if(Result == DD_OK) { for(int i = 0; i < FSpriteList->Count; i++) { Sprite = (TSprite *)FSpriteList->Items[i]; Result = Sprite->Surface->Restore(); if(Result == DD_OK) DDReloadBitmapLib(FLib, Sprite->Surface, Sprite->Bitmap); else break; // Exit on error }
}
This routine iterates through all the bitmaps in the SpriteList and restores each surface. This process occurs so quickly that the user simply sees the scene being restored all at once and is not aware that a list of objects is being re-created.
You can now run the program, trying it first in windowed mode, where the output will appear a bit muddy because the transparent color probably won't work correctly. After you're sure everything is running correctly, you can try the program in Exclusive mode. When in Exclusive mode, you can press Alt+Tab to move away from the main game window and view some other program such as the Windows Explorer. You can then press Alt+Tab to go back to the game window. This capability is one of the key features of DirectX.
If you're concerned about the TransparentColor not working right in windowed mode, you can try using a common color such as black for your TransparentColor. It might work correctly, or you can play with the Windows palette system to make sure the right palette is selected when your program appears in a window. For now, however, my graphics engine assumes that you want to run your program in Exclusive mode, and support for windowed mode is available only so that you can easily debug your programs. After all, DirectX doesn't really provide many advantages over GDI in windowed mode. The whole point of this process is to switch into Exclusive mode where you can get the following:
When I say that DirectX offers high performance, I mean that in Exclusive mode you can write directly to the video buffer, thereby obtaining the same kind of performance you would expect from a DOS program. The actual degree of performance improvement you get is dependent on the amount of RAM on your video card and on the bus between RAM and your video card. The ideal situation is to have a video card with enough RAM in it to hold all the bitmaps used by any one scene. Therefore, 2MB of memory on your video card is pretty much a minimum, and a strong argument can be made in favor of having 4MB. The code that ships with this book will work fine, however, with a small amount of RAM such as 524KB.
The actual system for drawing to the video buffer involves a process called flipping. When using this technology, the program draws the image you want to show the user to an offscreen buffer and then flips that offscreen buffer into the part of video memory the user sees. The user therefore never sees the relatively lengthy process of drawing to the buffer but sees only the finished picture when it is blitted, or "flipped," onto the screen. The core job of the graphics engine presented in this chapter is to automate this process so you don't have to think about it.
Some of the most interesting games consist of large worlds that fill up screen after screen with information. Classic examples of this kind of game are Heroes of Might and Magic, WarCraft, and Civilization. The THermesChart component shows how to work with one of these worlds.
Tiled worlds are made up of bitmaps that consist of lots of tiny tiles that can be combined in various ways to create maps. A picture of one of the tiled bitmaps is shown in Figure 28.4, and a small portion of the world created from these tiles is shown in Figure 28.5.
Figure 28.4. The tiles from which a tiled world is made.
Figure 28.5. A tiled world created by the THermesChart component from the bitmaps shown in Figure 28.4.
The declaration for the THermesChart component is shown in Listing 28.10, although the test program for it is shown in Listings 28.11 and 28.12.
struct TSpecialRect { bool IsCreature; RECT R1; RECT R2; }; class THermesTiler : public TScene { typedef TScene inherited; friend THermesChart; private: int FMaxMapRows; int FMaxMapCols; int FBitmapWidth; int FBitmapHeight; System::AnsiString FTileMap; IDirectDrawSurface* FTileSurface; protected: __fastcall virtual ~THermesTiler(void); virtual void __fastcall DrawScene(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); virtual TSpecialRect __fastcall GetRect(int Col, int Row) = 0; virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip) = 0; public: __fastcall virtual THermesTiler(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); Windows::TRect __fastcall MapTypeToTileRect(int MapType); virtual long __fastcall RestoreSurfaces(void); __published: __property System::AnsiString TileMap = {read=FTileMap, write=FTileMap, nodefault}; __property int BitmapWidth = {read=FBitmapWidth, write=FBitmapHeight, nodefault}; __property int BitmapHeight = {read=FBitmapHeight, write=FBitmapHeight, nodefault}; }; typedef void __fastcall (__closure *TMoveHeroEvent)(System::TObject* Sender, const tagPOINT &NewPos, int NewType, bool &MoveOk); class THermesChart : public THermesTiler { typedef THermesTiler inherited; private: Creatures1::TCreature* FHero; bool FHeroActive; TMoveHeroEvent FOnHeroMove; bool __fastcall CheckHeroPos(Creatures1::TCreatureList* HeroList, int Col, int Row); Windows::TRect __fastcall MapTypeToCreature(int Col, int Row); virtual TSpecialRect __fastcall GetRect(int Col, int Row); virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip); protected: void __fastcall MoveHero(int NewCol, int NewRow); virtual void __fastcall SetupSurfaces(System::TObject* Sender); public: __fastcall virtual ~THermesChart(void); void __fastcall Move(int Value); __published: __property bool HeroActive = {read=FHeroActive, write=FHeroActive, nodefault}; __property TMoveHeroEvent OnHeroMove = {read=FOnHeroMove, write=FOnHeroMove}; public: __fastcall virtual THermesChart(Classes::TComponent* AOwner) : Mercury2::THermesTiler(AOwner) { }
};
/////////////////////////////////////// // Main.h // Project: TilerTest1 // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include "Creatures1.hpp" #include <vcl\Menus.hpp> #include "Mercury2.h" class TForm1 : public TForm { __published: THermes *Hermes1; THermesChart *HermesChart1; TFileCreatureList *FileCreatureList1; TMainMenu *MainMenu1; TMenuItem *Run1; void __fastcall Run1Click(TObject *Sender); void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift); private: public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1;
#endif
/////////////////////////////////////// // Main.cpp // Project: TilerTest1 // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma link "Creatures1" #pragma link "Mercury2" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::Run1Click(TObject *Sender) { Hermes1->Run(); } void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if (Shift.Contains(ssAlt) && (Key == `X')) Close(); else HermesChart1->Move(Key);
}
By now, you should be getting used to the fact that these components are very easy
to use. The only custom code you have to write is to define how to exit the program:
if (Shift.Contains(ssAlt) && (Key == `X')) Close();
Other than this one simple statement, all the other "coding" involves nothing more than changing a few properties.
This particular example uses bitmaps that are stored in a DLL called BitDll.dll. To create a DLL of this type, simply build a resource file containing the bitmaps you want to use and then add the RC file to a DLL project. You can create a DLL by choosing File | New | DLL from the BCB menu.
Here is the RC code for a sample resource that contains multiple bitmaps:
Back BITMAP "PANEL4.BMP" TileMap BITMAP "TILEMAP.BMP" City BITMAP "FLOOR1.BMP" Dirs BITMAP "COMPDIRS.BMP" Treasure BITMAP "TREASURE.BMP" Sage1 BITMAP "SAGE1.BMP"
You can access the various bitmaps in the DLL by name. For example, you should set the BackgroundBitmap property of the THermesChart to Back and the TileMap property to TileMap. As long as the DLLName property is pointing to a valid DLL, you need do nothing else. All TScene descendants know how to read bitmaps from either a file on disk or from a DLL.
You'll discover several obvious advantages to using a DLL rather than a raw BMP file:
The tiled world that you create depends on two interrelated binary files. One binary file contains the screen map that you create, and the second contains a list of creatures that inhabit the map.
The screen map is simply an array 255x252 characters wide. At this time, the map you make must be this size, though I will provide alternative sizes at a later date. Check my Web site for updates.
Each element in the bitmap containing the tiles has a number associated with it. These numbers are described in the following enumerated type:
enum TMapType {mtGrass, mtWater, mtMountain, mtRoad, mtWater2, mtFootHill, mtNorthShore, mtWestShore, SouthShore, mtEastShore, mtSWShore, mtSEShore, mtNWShore, mtNEShore, mtWNWShore, mtWSEShore, mtESEShore, mtENEShore, mtBlank1, mtBlank2, mtBlank3, mtBlank4, mtAllSnow, mtSnowyMountain, mtSouthMtn, mtWestMtn, mtNorthMtn, mtEastMtn, mtSEMtn, mtSWMtn, mtNWMtn, mtNEMtn, mtNWFootHill, mtNEFootHill, mtSEFootHill, mtSWFootHill, mtNorthFootHill, mtEastFootHill, mtSouthFootHill, mtWestFootHill, mtNEDiagShore, mtSEDiagShore, mtSWDiagShore, mtNWDiagShore, mtSWBendShore, mtSEBendShore, mtNWBendShore, mtNEBendShore, mtENBendShore, mtWNBendShore, mtWSBendShore, mtESBendShore, mtCity, mtCreature};
mtGrass is element 0 in this enumerated type. mtWater is element 1; mtMountain, element 2; and so on. This type is not used in the TestTiler1 program, but it will come in handy later in the chapter.
Here is a simple two-dimensional array that encodes a small world:
1 1 1 1 1 1 1 0 1 1 1 0 2 0 1 1 1 0 1 1 1 1 1 1 1
This world consists of a small island with a mountain in the center of it. The entire island is surrounded by water. Of course, this world is tiny and cannot be used by the THermesChart component. To find a world that can be used by THermesChart, look in the Media directory on the CD that accompanies this book. There you will find a textual representation of a world in a file called Screen.txt and binary translation of that file called Screen.dta.
A simple program called TextToBinary found on the CD will translate the textual representation of a world to a binary file. The code that does so takes only a few lines:
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { FMapPointList = new TMapPointList(); } void __fastcall TForm1::FormDestroy(TObject *Sender) { FMapPointList->Free(); } void __fastcall TForm1::Button1Click(TObject *Sender) { int X, Y; if (OpenDialog1->Execute()) { FMapPointList->ReadBinary(OpenDialog1->FileName, X, Y); AnsiString S = ChangeFileExt(OpenDialog1->FileName, ".txt"); FMapPointList->WriteText(S, X, Y); } } void __fastcall TForm1::Button2Click(TObject *Sender) { if (OpenDialog1->Execute()) { FMapPointList->ReadText(OpenDialog1->FileName); AnsiString S = ChangeFileExt(OpenDialog1->FileName, ".dta"); FMapPointList->WriteBinary(S, FMapPointList->StartX, FMapPointList->StartY); } }
The constructor and destructor for this program create an object of type TMapPointList. You can find the TMapPointList object in a file called Creatures1.pas on the CD. To translate a binary file to text, simply call the methods of TMapPointList called ReadBinary and WriteText. To reverse the process, call ReadText and WriteBinary.
NOTE: I don't really expect you to create a world by editing a text file. The file is not ready at the time of this writing, but the CD that ships with this book may contain a program in the Chap28 directory that will allow you to edit a map from inside Windows by using the mouse. Check my Web site for updates to this program.
In particular, note that there are methods in Creatures1.pas that make it easy to create this type of program. For instance, note the following methods of TCreatureList:function NameFromLocation(ACol, ARow: Integer): string; function TypeFromLocation(ACol, ARow: Integer): string; function CreatureFromLocation(ACol: Integer; ARow: Integer): TCreature;
- Methods like this allow you to quickly identify the tile at a particular location. You can then use the Map array property from the same object to change these tiles.
Besides the screen file, you also need to maintain a file that tracks the creatures shown on your tiled world. In the sample programs that ship with this book, this file is called Creatures.dta.
You can edit Creatures.dta with a simple program called Entity. Most of this program is written in Pascal, so I won't discuss its code here. However, it compiles fine in C++Builder.
NOTE: I apologize for including so much Pascal code in this chapter. For various reasons, I need to have these objects working in both C++Builder and Delphi, and the simplest way to do that is to write the core objects and utilities in Object Pascal. In this particular case, I have taken a Delphi program and swapped out the Pascal version of the project source and substituted a version written in BCB. That way, I could get the program to compile in C++Builder by merely changing a few lines of code, all of which are generated automatically by the IDE. In short, I started a new project, removed the main form, and added the main form from the Pascal version of the Entities program. That was all I had to do to translate the program from Delphi to C++Builder.
The Entity program tracks the creature and screen file that it edits through the Registry. To do so, it uses the same technique I discussed in Chapter 13, "Flat-File, Real-World Databases." In particular, the following code should jog your memory as to how I retrieve the current creature and screen file from the Registry:
RegFile := TRegIniFile.Create(`Software\Charlie''s Stuff\Entities'); FCreatureFile := RegFile.ReadString(`FileNames', `CreatureFile', `'); FScreenFile := RegFile.ReadString(`FileNames', `ScreenFile', `');
Running the program once places the proper entries in the Registry. You can then edit the Registry to enter in the names of the actual files you want to use. In particular, you should use the Windows utility called RegEdit.exe to edit the area under HKEY_CURRENT_USER\Charlie's Stuff\Entities.
The creatures file contains a number of creature profiles, as shown in Figure 28.6 and Figure 28.7.
Figure 28.6. The hero's profile as shown in the Entity program.
Figure 28.7. A hostile creature's profile as shown in the Entity program.
When you first open the screen, be sure that the hero is located at column 11 and row 11. To set this up properly, you need to edit the Col and Row fields, as well as the Scrn Col, Scrn Row, Map Col, and Map Row. You need all these values because you can never see the whole map at one time. Instead, you see a window into the entire grid. You therefore need to track where the window is on the grid, where the character is on the grid, and where the character is on the window currently opened onto the grid. You have three pairs of values, all of which are tracked for you automatically by the THermesChart object.
Now that you've examined the screen and creature files, you're ready to run the TestTiler program. Go ahead and run it once in windowed mode to make sure that you're reaching all the files in question. Then run the program again in Exclusive mode.
NOTE: You can possibly mangle the screen file by exiting the TilerTest1 program incorrectly. In other words, if TilerTest1 crashes in mid-run and you reboot, you can lose the contents of the screen file. To check whether you've lost the contents, check that your screen file is equal to 64,276 bytes. If it is some other value, the file is probably corrupt and should be refreshed from the file on the CD that accompanies the book. If you have this problem, you will probably get a Range error when trying to run the program.
The only point left for you to learn is how to move the character on the screen and how to scroll around through the world. To do so, simply modify the OnKeyDown event for TilerTest1:
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if (Shift.Contains(ssAlt) && (Key == `X')) Close(); else HermesChart1->Move(Key); }
This code will exit the program if the user presses Alt+X; otherwise, it will pass the current keystroke on the HermesChart object. This object responds to presses on the arrow keys by moving either the main character or by scrolling the entire map. Press the Insert key to switch between these two modes. Figure 28.8 shows the main character walking on the map.
Figure 28.8.The main character exploring the shore of a lake, with mountains nearby.
The source for Mercury2.cpp is shown in Listings 28.13 and 28.14. Please note that several stand-alone functions right at the top of Mercury2.cpp are only slightly modified versions of code that appears in the DDUtils.cpp file that ships with the DirectDraw SDK. I could not let you use the original functions, available on the Microsoft Web site, because they do not take into account the possibility that the user might want to store bitmaps in a DLL.
#ifndef Mercury2H #define Mercury2H #include <Creatures1.hpp> #include <ddraw.h> #include <DsgnIntf.hpp> #include <OLE2.hpp> #include <Graphics.hpp> #include <Forms.hpp> #include <Controls.hpp> #include <Classes.hpp> #include <Windows.hpp> #include <System.hpp> namespace Mercury2 { //-- type declarations ------------------------------------------------------ typedef tagPALETTEENTRY T256PalEntry[256]; typedef tagRGBQUAD TRGB[256]; typedef TRGB *PRGB; class THermes; class TScene; class THermesTiler; class THermesChart; class TSpriteScene; class TDraw; class TDraw : public Classes::TComponent { typedef Classes::TComponent inherited; friend THermesTiler; friend THermesChart; friend TScene; friend TSpriteScene; private: AnsiString FDLLName; THermes* FHermes; HANDLE FLib; IDirectDrawPalette* FPalette; int FTransparentColor; bool __fastcall CreateDDSurface(IDirectDrawSurface* &DDS, System::AnsiString BitmapName, bool UsePalette); IDirectDrawSurface* __fastcall CreateSurface(HANDLE Bitmap); HANDLE __fastcall GetDib(HANDLE Instance, System::AnsiString S); IDirectDrawPalette* __fastcall LoadPalette(HANDLE Instance, const System::AnsiString BitmapName); public: __fastcall virtual TDraw(Classes::TComponent* AOwner); __fastcall virtual ~TDraw(void); void __fastcall WriteXY(int X, int Y, System::AnsiString S); __property int TransparentColor = {read=FTransparentColor, write=FTransparentColor, nodefault}; __published: __property AnsiString DLLName = {read=FDLLName, write=FDLLName}; }; class TScene : public TDraw { typedef TDraw inherited; friend THermes; private: System::AnsiString FBackgroundBitmap; Graphics::TColor FBackColor; RECT FBackRect; tagPOINT FBackOrigin; bool FBlankScene; bool FShowBitmap; IDirectDrawSurface* FWorkSurface; Classes::TNotifyEvent FOnSetupSurfaces; Classes::TNotifyEvent FOnDrawScene; public: __fastcall virtual TScene(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); virtual void __fastcall DrawScene(void); virtual long __fastcall RestoreSurfaces(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); __published: __property System::AnsiString BackgroundBitmap = {read=FBackgroundBitmap, write=FBackgroundBitmap, nodefault}; __property bool BlankScene = {read=FBlankScene, write=FBlankScene, nodefault}; __property long OriginX ={read=FBackOrigin.x, write=FBackOrigin.x, nodefault}; __property long OriginY ={read=FBackOrigin.y, write=FBackOrigin.y, nodefault}; __property bool ShowBitmap = {read=FShowBitmap, write=FShowBitmap, default=1}; __property Classes::TNotifyEvent OnDrawScene = {read=FOnDrawScene, write=FOnDrawScene}; __property Classes::TNotifyEvent OnSetupSurfaces = {read=FOnSetupSurfaces, write=FOnSetupSurfaces}; __property Graphics::TColor BackColor = {read=FBackColor, write=FBackColor, nodefault}; __property TransparentColor ; public: __fastcall virtual ~TScene(void) { } }; class THermes : public Classes::TComponent { typedef Classes::TComponent inherited; friend THermesChart; friend TDraw; private: bool FActive; Creatures1::TFileCreatureList* FCreatureList; HWND FHandle; bool FTimerOdd; int FTimerInterval; bool FExclusive; Classes::TNotifyEvent FPaintProc; TScene* FScene; bool FUseTimer; bool FFirstTime; IDirectDraw* FDirectDraw; IDirectDrawSurface* FBackSurface; IDirectDrawClipper* FClipper; IDirectDrawSurface* FPrimarySurface; bool __fastcall CreatePrimary(void); void __fastcall DDTest(long hr, System::AnsiString S); void __fastcall InitBaseObjects(void); bool __fastcall MakeItSo(long DDResult); void __fastcall SetScene(TScene* Scene); bool __fastcall SetUpBack(void); protected: void __fastcall DrawBitmaps(void); virtual long __fastcall RestoreSurfaces(void); public: __fastcall virtual THermes(Classes::TComponent* AOwner); __fastcall virtual ~THermes(void) {} void __fastcall EndExclusive(void); void __fastcall ErrorEvent( System::AnsiString S); void __fastcall Flip(void); virtual void __stdcall InitObjects(void); virtual void __fastcall Run(void); __property bool Active = {read=FActive, write=FActive, nodefault}; __property IDirectDrawSurface* BackSurface = {read=FBackSurface, write=FBackSurface, nodefault}; __property Classes::TNotifyEvent OnDrawBitmap = {read=FPaintProc, write=FPaintProc}; __published: __property Creatures1::TFileCreatureList* CreatureList = {read=FCreatureList, write=FCreatureList, nodefault}; __property bool Exclusive = {read=FExclusive, write=FExclusive, nodefault}; __property TScene* Scene = {read=FScene, write=SetScene, nodefault}; __property int TimerInterval = {read=FTimerInterval, write=FTimerInterval, nodefault}; __property bool UseTimer = {read=FUseTimer, write=FUseTimer, nodefault}; }; struct TSpecialRect { bool IsCreature; RECT R1; RECT R2; }; class THermesTiler : public TScene { typedef TScene inherited; friend THermesChart; private: int FMaxMapRows; int FMaxMapCols; int FBitmapWidth; int FBitmapHeight; System::AnsiString FTileMap; IDirectDrawSurface* FTileSurface; protected: __fastcall virtual ~THermesTiler(void); virtual void __fastcall DrawScene(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); virtual TSpecialRect __fastcall GetRect(int Col, int Row) = 0; virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip) = 0; public: __fastcall virtual THermesTiler(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); Windows::TRect __fastcall MapTypeToTileRect(int MapType); virtual long __fastcall RestoreSurfaces(void); __published: __property System::AnsiString TileMap = {read=FTileMap, write=FTileMap, nodefault}; __property int BitmapWidth = {read=FBitmapWidth, write=FBitmapHeight, nodefault}; __property int BitmapHeight = {read=FBitmapHeight, write=FBitmapHeight, nodefault}; }; typedef void __fastcall (__closure *TMoveHeroEvent)(System::TObject* Sender, const tagPOINT &NewPos, int NewType, bool &MoveOk); class THermesChart : public THermesTiler { typedef THermesTiler inherited; private: Creatures1::TCreature* FHero; bool FHeroActive; TMoveHeroEvent FOnHeroMove; bool __fastcall CheckHeroPos(Creatures1::TCreatureList* HeroList, int Col, int Row); Windows::TRect __fastcall MapTypeToCreature(int Col, int Row); virtual TSpecialRect __fastcall GetRect(int Col, int Row); virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip); protected: void __fastcall MoveHero(int NewCol, int NewRow); virtual void __fastcall SetupSurfaces(System::TObject* Sender); public: __fastcall virtual ~THermesChart(void); void __fastcall Move(int Value); __published: __property bool HeroActive = {read=FHeroActive, write=FHeroActive, nodefault}; __property TMoveHeroEvent OnHeroMove = {read=FOnHeroMove, write=FOnHeroMove}; public: __fastcall virtual THermesChart(Classes::TComponent* AOwner) : Mercury2::THermesTiler(AOwner) { } }; class TSprite; class TSprite : public Classes::TComponent { typedef Classes::TComponent inherited; friend TSpriteScene; private: System::AnsiString FBitmap; tagPOINT FPosition; IDirectDrawSurface* FSurface; RECT FRect; public: bool __fastcall IsHit(int X, int Y); __property IDirectDrawSurface* Surface= {read=FSurface, write=FSurface, nodefault}; __property RECT Rect = {read=FRect, write=FRect}; __published: __property System::AnsiString Bitmap={read=FBitmap, write=FBitmap, nodefault}; __property long XPos = {read=FPosition.x, write=FPosition.x, nodefault}; __property long YPos = {read=FPosition.y, write=FPosition.y, nodefault}; public: __fastcall virtual TSprite(Classes::TComponent* AOwner) : Classes::TComponent(AOwner) { } __fastcall virtual ~TSprite(void) { } }; class TSpriteScene : public TScene { typedef TScene inherited; private: Classes::TList* FSpriteList; protected: __fastcall virtual ~TSpriteScene(void); virtual void __fastcall SetupSurfaces(System::TObject* Sender); public: __fastcall virtual TSpriteScene(Classes::TComponent* AOwner); virtual void __fastcall DestroyObjects(void); void __fastcall AddSprite(TSprite* Sprite); virtual void __fastcall DrawScene(void); virtual long __fastcall RestoreSurfaces(void); __property Classes::TList* SpriteList = {read=FSpriteList, write=FSpriteList,nodefault}; __published: __property TransparentColor; }; //////////////////////////////////////// // TSceneEditor //////////////////////// //////////////////////////////////////// class TSceneEditor: public TComponentEditor { protected: virtual __fastcall void Edit(void); public: virtual __fastcall TSceneEditor(TComponent *AOwner, TFormDesigner *Designer) : TComponentEditor(AOwner, Designer) {} }; //-- var, const, procedure -------------------------------------------------- #define Timer1 (Byte)(1) extern void __fastcall Register(void); }/* namespace Mercury1 */ #if !defined(NO_IMPLICIT_NAMESPACE_USE) using namespace Mercury2; #endif //-- end unit ----------------------------------------------------------------
#endif // Mercury2
/////////////////////////////////////// // Mercury2.cpp // DirectX Graphics // Copyright (c) 1997 by Charlie Calvert // Thanks to John Thomas, Stuart Fullmar and Jeff Cottingham /////////////////////////////////////// #include <vcl\vcl.h> #pragma hdrstop #include "Mercury2.h" #include "errors1.h" #include "SceneEditor1.h" #pragma link "Errors1.obj" #pragma link "SceneEditor1.obj" /*{ ------------------------ } { --- THermes ------------ } { ------------------------ } */ THermes *AHermes; void Timer2Timer(HWND H, UINT Msg, UINT Event, DWORD Time) { if (AHermes->Active) AHermes->Flip(); } namespace Mercury2 { // Slightly modified version of code from DDUtils.cpp HRESULT DDCopyBitmap(IDirectDrawSurface *pdds, HBITMAP hbm, int x, int y, int dx, int dy) { HDC hdcImage; HDC hdc; BITMAP bm; DDSURFACEDESC ddsd; HRESULT hr; if (hbm == NULL || pdds == NULL) return E_FAIL; // // make sure this surface is restored. // pdds->Restore(); // // select bitmap into a memoryDC so we can use it. // hdcImage = CreateCompatibleDC(NULL); if (!hdcImage) OutputDebugString("createcompatible dc failed\n"); SelectObject(hdcImage, hbm); // // get size of the bitmap // GetObject(hbm, sizeof(bm), &bm); // get size of bitmap dx = dx == 0 ? bm.bmWidth : dx; // use the passed size, unless zero dy = dy == 0 ? bm.bmHeight : dy; // // get size of surface. // ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH; pdds->GetSurfaceDesc(&ddsd); if ((hr = pdds->GetDC(&hdc)) == DD_OK) { StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y, dx, dy, SRCCOPY); pdds->ReleaseDC(hdc); } DeleteDC(hdcImage); return hr; } // Slightly modified version of code from DDUtils.cpp void DDReloadBitmapLib(HANDLE Lib, IDirectDrawSurface *Surface, const AnsiString BitmapName) { HBITMAP Bitmap; if (Lib) Bitmap = LoadImage(Lib, BitmapName.c_str(), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION); else Bitmap = LoadImage(GetModuleHandle(NULL), BitmapName.c_str(), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION); if (!Bitmap) Bitmap = LoadImage(0, BitmapName.c_str(), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (!Bitmap) throw Exception("Unable to load bitmap %s", OPENARRAY(TVarRec, (BitmapName))); DDCopyBitmap(Surface, Bitmap, 0, 0, 0, 0); DeleteObject(Bitmap); } // Slightly modified version of code from DDUtils.cpp IDirectDrawSurface *DDCreateSurface(IDirectDraw *DD, DWORD Width, DWORD Height, bool SysMem, bool Trans, DWORD dwColorKey) { DDSURFACEDESC SurfaceDesc; HRESULT hr; DDCOLORKEY ColorKey; IDirectDrawSurface *Surface; // fill in surface desc memset(&SurfaceDesc, 0, sizeof(DDSURFACEDESC)); SurfaceDesc.dwSize = sizeof(SurfaceDesc); SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; SurfaceDesc.dwHeight = Height; SurfaceDesc.dwWidth = Width; hr = DD->CreateSurface(&SurfaceDesc, &Surface, NULL); // set the color key for this bitmap if (hr == DD_OK) { if (Trans) { ColorKey.dwColorSpaceLowValue = dwColorKey; ColorKey.dwColorSpaceHighValue = dwColorKey; Surface->SetColorKey(DDCKEY_SRCBLT, &ColorKey); } } else throw EDDError("CreateSurface Failed in DDCreateSurface"); return Surface; } // DDCreateSurface IDirectDrawClipper *CreateClipper(IDirectDraw *DD, HWND Handle) { HRESULT hr; IDirectDrawClipper *lpClipper; hr = DD->CreateClipper(0, &lpClipper, NULL); if (hr != DD_OK) { throw EDDError("No Clipper"); } hr = lpClipper->SetHWnd(0, Handle); if (hr != DD_OK) { throw EDDError("Can''t set clipper window handle"); } return lpClipper; } } __fastcall THermes::THermes(TComponent* AOwner) :TComponent(AOwner) { FHandle = ((TWinControl *)(AOwner))->Handle; FFirstTime = true; }// THermes void __fastcall THermes::EndExclusive() { FDirectDraw->FlipToGDISurface(); if (FExclusive) FDirectDraw->SetCooperativeLevel(FHandle, DDSCL_NORMAL); }// EndExclusive void __fastcall THermes::ErrorEvent(String S) { FActive = false; EndExclusive(); throw EDDError(S); }// ErrorEvent void __fastcall THermes::Run() { InitObjects(); Flip(); }// Run void __fastcall THermes::DDTest(long hr, System::AnsiString S) { if (!Windows::Succeeded(hr)) throw EDDError("DDTest Error: %s $%x %s", OPENARRAY(TVarRec, (S, int(hr), AnsiString(GetOleError(hr))))); }// DDTest ///////////////////////////////////////////////// // Create the primary surface ///////////////////////////////////////////////// bool __fastcall THermes::CreatePrimary(void) { DDSURFACEDESC SurfaceDesc; HResult hr; bool Result = true; memset(&SurfaceDesc, 0, sizeof(SurfaceDesc)); SurfaceDesc.dwSize = sizeof(SurfaceDesc); if (!FExclusive){ SurfaceDesc.dwFlags = DDSD_CAPS; SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; } else{ SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; SurfaceDesc.dwBackBufferCount = 1; }; hr = FDirectDraw->CreateSurface(&SurfaceDesc, &FPrimarySurface, NULL); if (hr != DD_OK) throw EDDError("THermes.CreatePrimary: %d %s", OPENARRAY(TVarRec, (int(hr), GetOleError(hr)))); else return Result; }// CreatePrimary void __fastcall THermes::SetScene(TScene *Scene) { if (Scene) Scene->DestroyObjects(); FScene = Scene; }// SetScene bool __fastcall THermes::SetUpBack(void) { bool Result = false; HResult hr; DDSCAPS DDSCaps; if (!FExclusive) { FBackSurface = Mercury2::DDCreateSurface(FDirectDraw, 640, 480, false, false, 0); if (FBackSurface == NULL) throw EDDError("Can''t set up back surface"); FClipper = Mercury2::CreateClipper(FDirectDraw, FHandle); hr = FPrimarySurface->SetClipper(FClipper); if( hr != DD_OK ) throw EDDError("Can''t attach clipper to front buffer"); } else { memset(&DDSCaps, 0, sizeof(DDSCaps)); DDSCaps.dwCaps = DDSCAPS_BACKBUFFER; hr = FPrimarySurface->GetAttachedSurface(&DDSCaps, &FBackSurface); if (hr != DD_OK) throw EDDError("TSpeedDraw.SetUpBack: %d %s", OPENARRAY(TVarRec, (int(hr), GetOleError(hr)))); else Result = true; }; return Result; }// SetUpBack void __fastcall THermes::InitBaseObjects() { DWORD Flags; HResult hr; DDTest(DirectDrawCreate(NULL, &FDirectDraw, NULL), "InitObjects1"); if (!FExclusive) Flags = DDSCL_NORMAL; else Flags = DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN; DDTest(FDirectDraw->SetCooperativeLevel(FHandle, Flags),"SetCooperativeLevel"); if (FExclusive) { hr = FDirectDraw->SetDisplayMode(640, 480, 8); if(hr != DD_OK) throw EDDError("TSpeedDraw.InitObjects: %d %s", OPENARRAY(TVarRec, (int(hr), GetOleError(hr)))); }; CreatePrimary(); SetUpBack(); if (FCreatureList != NULL) FCreatureList->ReadFiles(); }// InitBaseObjects /* Here are the steps in the initialization: Create DirectDraw Object SetCooperativeLevel if Exclusive then SetDisplayMode CreatePrimary SetupBack Create the work surface Set Active to true */ void __stdcall THermes::InitObjects(void) { AHermes = this; if (FFirstTime) { InitBaseObjects(); FFirstTime = false; }; if ((Scene) && (Scene->FBackgroundBitmap != "")) Scene->SetupSurfaces(this); if (FUseTimer) SetTimer(FHandle, Timer1, FTimerInterval, (FARPROC)Timer2Timer); FActive = true; }// InitObjects void __fastcall THermes::DrawBitmaps() { if (Scene) Scene->DrawScene(); } bool __fastcall THermes::MakeItSo(HResult DDResult) { bool Result; switch(DDResult) { case DD_OK: Result = true; break; case DDERR_SURFACELOST: Result = (RestoreSurfaces() == DD_OK); break; default: Result = DDResult != DDERR_WASSTILLDRAWING; break; }; return Result; } // MakeItSo long __fastcall THermes::RestoreSurfaces() { HRESULT Result; Result = FPrimarySurface->Restore(); if ((Result == DD_OK) && (Scene)) Result = Scene->RestoreSurfaces(); return Result; }// RestoreSurfaces void __fastcall THermes::Flip(void) { RECT R1, R; FTimerOdd = !FTimerOdd; if (!FActive) return; if (!FExclusive) { try { DrawBitmaps(); GetWindowRect(FHandle, &R); R1 = Rect(0, 0, 640, 480); DDTest(FPrimarySurface->Blt(&R, FBackSurface, &R1, 0, NULL), "Flip"); } catch(Exception &E) { ErrorEvent("Flipping"); }; } else { try { if (FActive) { do { Application->ProcessMessages(); } while(!MakeItSo(FPrimarySurface->Flip(NULL, DDFLIP_WAIT))); DrawBitmaps(); } } catch(...) { ErrorEvent("Flipping"); } } }// Flip /////////////////////////////////////// // TDraw ////////////////////////////// /////////////////////////////////////// __fastcall TDraw::TDraw(Classes::TComponent* AOwner) : TComponent(AOwner) { } __fastcall TDraw::~TDraw(void) { if (FLib != NULL) FreeLibrary(FLib); } bool __fastcall TDraw::CreateDDSurface(IDirectDrawSurface* &DDS, System::AnsiString BitmapName, bool UsePalette) { bool Result; DDCOLORKEY ColorKey; HRESULT hr; HANDLE Dib; if (UsePalette) { FPalette = LoadPalette(FLib, BitmapName.c_str()); if (!FPalette) throw EDDError("LoadPalette Failed"); FHermes->FPrimarySurface->SetPalette(FPalette); } Dib = GetDib(FLib, BitmapName); DDS = CreateSurface(Dib); if (!DDS) throw EDDError("CreateSurface Failed"); ColorKey.dwColorSpaceLowValue = FTransparentColor; ColorKey.dwColorSpaceHighValue = FTransparentColor; hr = DDS->SetColorKey(DDCKEY_SRCBLT, &ColorKey); if (hr != DD_OK) throw EDDError("TSpeedDraw.CreateDDSurface: %d %s", OPENARRAY(TVarRec, (int(hr), GetOleError(hr)))); else Result = True; return Result; } IDirectDrawSurface* __fastcall TDraw::CreateSurface(HANDLE Bitmap) { DDSURFACEDESC SurfaceDesc; Windows::TBitmap BM; IDirectDrawSurface *Result; if (Bitmap == 0) throw Exception("No Bitmap in CreateSurface"); try { GetObject(Bitmap, sizeof(BM), &BM); memset(&SurfaceDesc, 0, sizeof(SurfaceDesc)); SurfaceDesc.dwSize = sizeof(SurfaceDesc); SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; SurfaceDesc.dwWidth = BM.bmWidth; SurfaceDesc.dwHeight = BM.bmHeight; if (FHermes->FDirectDraw->CreateSurface(&SurfaceDesc, &Result, NULL) != DD_OK) throw Exception("CreateSurface failed"); DDCopyBitmap(Result, Bitmap, 0, 0, 0, 0); } catch(Exception &E) { FHermes->ErrorEvent("TSpeedDraw.CreateSurface: " + E.Message); } return Result; } HANDLE __fastcall TDraw::GetDib(HANDLE Instance, System::AnsiString S) { HANDLE Result; UINT Flags; if (Instance != 0) Flags = LR_CREATEDIBSECTION; else Flags = LR_LOADFROMFILE | LR_CREATEDIBSECTION; Result = LoadImage((HANDLE)Instance, S.c_str(), IMAGE_BITMAP, 0, 0, Flags); if (Result == 0) FHermes->ErrorEvent("TSpeedDraw.GetDib: Could not load bitmap"); return Result; } IDirectDrawPalette* __fastcall TDraw::LoadPalette(HANDLE Instance, const System::AnsiString BitmapName) { IDirectDrawPalette* ddpal; int i; int n; int fh; HRSRC h; LPBITMAPINFO BitmapInfo; PALETTEENTRY ape[256]; RGBQUAD * RGBQuad; // // build a 332 palette as the default. // for (i=0; i<256; i++) { ape[i].peRed = (BYTE)(((i >> 5) & 0x07) * 255 / 7); ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7); ape[i].peBlue = (BYTE)(((i >> 0) & 0x03) * 255 / 3); ape[i].peFlags = (BYTE)0; } if (BitmapName == "") FHermes->ErrorEvent("No bitmapname in LoadPalette"); // // get a pointer to the bitmap resource. // if (Instance) { h = FindResource(Instance, BitmapName.c_str(), RT_BITMAP); if (h) { BitmapInfo = (LPBITMAPINFO)LockResource(LoadResource(Instance, h)); if (!BitmapInfo) throw EDDError("No LockResouce: " + this->ClassName()); RGBQuad = (RGBQUAD*)BitmapInfo->bmiColors; if (BitmapInfo == NULL || BitmapInfo->bmiHeader.biSize <sizeof(BITMAPINFOHEADER)) n = 0; else if (BitmapInfo->bmiHeader.biBitCount > 8) n = 0; else if (BitmapInfo->bmiHeader.biClrUsed == 0) n = 1 << BitmapInfo->bmiHeader.biBitCount; else n = BitmapInfo->bmiHeader.biClrUsed; // // a DIB color table has its colors stored BGR not RGB // so flip them around. // for(i=0; i<n; i++ ) { ape[i].peRed = RGBQuad[i].rgbRed; ape[i].peGreen = RGBQuad[i].rgbGreen; ape[i].peBlue = RGBQuad[i].rgbBlue; ape[i].peFlags = 0; } } } else { fh = _lopen(BitmapName.c_str(), OF_READ); if (fh != -1) { BITMAPFILEHEADER bf; BITMAPINFOHEADER bi; _lread(fh, &bf, sizeof(bf)); _lread(fh, &bi, sizeof(bi)); _lread(fh, ape, sizeof(ape)); _lclose(fh); if (bi.biSize != sizeof(BITMAPINFOHEADER)) n = 0; else if (bi.biBitCount > 8) n = 0; else if (bi.biClrUsed == 0) n = 1 << bi.biBitCount; else n = bi.biClrUsed; // // a DIB color table has its colors stored BGR not RGB // so flip them around. // for(i=0; i<n; i++ ) { BYTE r = ape[i].peRed; ape[i].peRed = ape[i].peBlue; ape[i].peBlue = r; } } } FHermes->FDirectDraw->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL); return ddpal; } void __fastcall TDraw::WriteXY(int X, int Y, System::AnsiString S) { HDC DC; FHermes->BackSurface->GetDC(&DC); SetBkMode(DC, TRANSPARENT); TextOut(DC, X, Y, S.c_str(), S.Length()); FHermes->BackSurface->ReleaseDC(DC); } // ------------------------ // -- TScene -------------- // ------------------------ __fastcall TScene::TScene(TComponent *AOwner) : TDraw(AOwner) { ShowBitmap = true; } void __fastcall TScene::DestroyObjects(void) { if(FWorkSurface != NULL) FWorkSurface->Release(); FWorkSurface = NULL; } long _fastcall TScene::RestoreSurfaces(void) { long Result; Result = FWorkSurface->Restore(); if(Result == DD_OK) DDReloadBitmapLib(FLib, FWorkSurface, FBackgroundBitmap); return(Result); } void __fastcall TScene::SetupSurfaces(System::TObject *Sender) { AnsiString ErrStr = "TSpeedDraw.SetupWorkSurface: No Surface Desc %d %s"; DDSURFACEDESC SurfaceDesc; HRESULT hr; FHermes = dynamic_cast <THermes *>(Sender); if(FDLLName != "") { if(!FLib) FLib = LoadLibrary(FDLLName.c_str()); if(int(FLib) < 32) throw EDDError("No Library"); } if(!CreateDDSurface(FWorkSurface, FBackgroundBitmap, True)) throw EDDError("TSpeedDraw.SetupWorkSurface: No WorkSurface: " + FBackgroundBitmap); else { SurfaceDesc.dwSize = sizeof(SurfaceDesc); hr = FWorkSurface->GetSurfaceDesc(&SurfaceDesc); if(hr != DD_OK) throw EDDError(ErrStr, OPENARRAY(TVarRec, (int(hr), GetOleError(hr)))); FBackRect = Rect(0, 0, SurfaceDesc.dwWidth, SurfaceDesc.dwHeight); } if(FOnSetupSurfaces) FOnSetupSurfaces(this); } void _fastcall TScene::DrawScene() { AnsiString ErrStr = "TSpeedDraw.BackGroundBlits: $%x \r %s \r %s"; HRESULT hr; HDC DC; HBRUSH OldBrush, Brush; if (FBlankScene) { FHermes->BackSurface->GetDC(&DC); // Don't step through!! Brush = CreateSolidBrush(FBackColor); OldBrush = SelectObject(DC, Brush); Rectangle(DC, 0, 0, 640, 480); SelectObject(DC, OldBrush); DeleteObject(Brush); FHermes->BackSurface->ReleaseDC(DC); } if (FShowBitmap) { hr = AHermes->BackSurface->BltFast(FBackOrigin.x, FBackOrigin.y, FWorkSurface, &FBackRect, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY); if (!Windows::Succeeded(hr)) throw EDDError(ErrStr, OPENARRAY(TVarRec, (int(hr), GetOleError(hr), "Check BackRect, BackOrigin???"))); } if (FOnDrawScene) FOnDrawScene(this); } /////////////////////////////////////// // THermesTiler /////////////////////// /////////////////////////////////////// __fastcall THermesTiler::THermesTiler(Classes::TComponent* AOwner) :TScene(AOwner) { FBitmapWidth = 32; FBitmapHeight = 32; FMaxMapRows = 12; FMaxMapCols = 20; } __fastcall THermesTiler::~THermesTiler(void) { DestroyObjects(); } void __fastcall THermesTiler::DrawScene(void) { TScene::DrawScene(); int i, j, k; TSpecialRect SpR; if (FTileSurface == NULL) return; k = 1; for (j = 1; j <= FMaxMapRows; j++) for (i = 1; i <= FMaxMapCols; i++) { SpR = GetRect(i + FHermes->CreatureList->MapCol, j + FHermes->CreatureList->MapRow); FHermes->BackSurface->BltFast(FBitmapWidth * (k - 1), FBitmapHeight * (j - 1), FTileSurface, &SpR.R1, DDBLTFAST_SRCCOLORKEY); if (SpR.IsCreature) // There's a character or building here FHermes->BackSurface->BltFast(FBitmapWidth * (k - 1), FBitmapHeight * (j - 1), FTileSurface, &SpR.R2, DDBLTFAST_SRCCOLORKEY); if ((i % 20) == 0) k = 1; else k++; } } void __fastcall THermesTiler::SetupSurfaces(System::TObject* Sender) { TScene::SetupSurfaces(Sender); if (FTileMap != "") if (!CreateDDSurface(FTileSurface, FTileMap, True)) throw Exception("THermesTiler.InitObjects"); } void __fastcall THermesTiler::DestroyObjects(void) { if (FTileMap != "") if (FTileSurface != NULL) FTileSurface->Release(); FTileSurface = NULL; } Windows::TRect __fastcall THermesTiler::MapTypeToTileRect(int MapType) { int X, Y = 0; if ((MapType > 19) && (MapType < 41)) { Y = FBitmapHeight; X = (MapType - 21) * FBitmapWidth; } else if (MapType > 39) { Y = 64; X = (MapType - 41) * FBitmapWidth; } else { X = MapType * FBitmapWidth; } return Rect(X, Y, X + FBitmapWidth, FBitmapHeight + Y); } long __fastcall THermesTiler::RestoreSurfaces(void) { long Result = TScene::RestoreSurfaces(); if (Result == DD_OK) { Result = FTileSurface->Restore(); if (Result == DD_OK) if (FTileMap != "") DDReloadBitmapLib(FLib, FTileSurface, FTileMap); } return Result; } /////////////////////////////////////// // THermesChart /////////////////////// /////////////////////////////////////// bool __fastcall THermesChart::CheckHeroPos(Creatures1::TCreatureList* HeroList, int Col, int Row) { int NewPosType; TPoint NewPos; bool MoveOk = True; if (OnHeroMove != NULL) { NewPos.x = FHero->TrueCol + Col; NewPos.y = FHero->TrueRow + Row; NewPosType = HeroList->GetMapType(NewPos.x, NewPos.y); OnHeroMove(this, NewPos, NewPosType, MoveOk); } return MoveOk; } Windows::TRect __fastcall THermesChart::MapTypeToCreature(int Col, int Row) { TCreature *Creature = FHermes->CreatureList->CreatureFromLocation(Col, Row); if (Creature->TypeStr == "Hero") return MapTypeToTileRect(3); else return MapTypeToTileRect(StrToInt(Creature->ID)); } TSpecialRect __fastcall THermesChart::GetRect(int Col, int Row) { Byte MapType; TSpecialRect Result; MapType = FHermes->CreatureList->Map[Col][Row]; if ((FHermes->FTimerOdd == True) && (MapType == 1 /*mtWater*/)) MapType = 4; // mtWater2; if (BitOn(7, MapType)) { Result.IsCreature = True; SetBit(7, 0, MapType); Result.R1 = MapTypeToTileRect(MapType); Result.R2 = MapTypeToCreature(Col, Row); } else { Result.IsCreature = False; Result.R1 = MapTypeToTileRect(MapType); } return Result; } bool __fastcall THermesChart::MoveGrid(int Col, int Row, bool CallFlip) { TPoint P; if (CheckHeroPos(FHermes->CreatureList, Col, Row)) { P.x = FHermes->CreatureList->MapCol + Col; P.y = FHermes->CreatureList->MapRow + Row; if ((P.x < 0) || (P.x > (FHermes->CreatureList->MaxCols - FMaxMapCols)) || (P.y < 0) || (P.y > (FHermes->CreatureList->MaxRows - FMaxMapRows))) return False; FHermes->CreatureList->MapCol = P.x; FHermes->CreatureList->MapRow = P.y; FHermes->CreatureList->MoveCreature(FHero, Col, Row); FHermes->Flip(); return True; } return False; } __fastcall THermesChart::~THermesChart(void) { } void __fastcall THermesChart::MoveHero(int NewCol, int NewRow) { TPoint P; if (!CheckHeroPos(FHermes->CreatureList, NewCol, NewRow)) return; P.x = FHero->ScreenCol + NewCol; P.y = FHero->ScreenRow + NewRow; if ((P.x <= 0) || (P.x > (FMaxMapCols)) || (P.y <= 1) || (P.y > FMaxMapRows)) return; FHero->ScreenCol = P.x; FHero->ScreenRow = P.y; FHermes->CreatureList->MoveCreature(FHero, NewCol, NewRow); FHermes->Flip(); } void __fastcall THermesChart::SetupSurfaces(System::TObject* Sender) { THermesTiler::SetupSurfaces(Sender); if (FHermes->CreatureList == NULL) throw EDDError("CreatureList cannot be blank."); FHeroActive = True; FHero = FHermes->CreatureList->CreatureFromName("Hero"); } void __fastcall THermesChart::Move(int Value) { if (Value == VK_INSERT) HeroActive = !HeroActive; else if (HeroActive) switch (Value) { case VK_RIGHT: MoveHero(1, 0); break; case VK_LEFT: MoveHero(-1, 0); break; case VK_DOWN: MoveHero(0, 1); break; case VK_UP: MoveHero(0, -1); break; } else switch(Value) { case VK_RIGHT: MoveGrid(1, 0, False); break; case VK_LEFT: MoveGrid(-1, 0, False); break; case VK_DOWN: MoveGrid(0, 1, False); break; case VK_UP: MoveGrid(0, -1, False); break; } } /* ------------------------ */ /* -- TSprite ------------- */ /* ------------------------ */ bool __fastcall TSprite::IsHit(int X, int Y) { X = X - XPos; Y = Y - YPos; if ((X >= 0) && (Y >= 0) && (X <= Rect.right) && (Y <= Rect.bottom)) return true; else return false; } /* ------------------------ */ /* -- TSpriteScene -------- */ /* ------------------------ */ __fastcall TSpriteScene::TSpriteScene(TComponent *AOwner) : TScene(AOwner) { FSpriteList = new TList(); } __fastcall TSpriteScene::~TSpriteScene() { DestroyObjects(); delete FSpriteList; } void __fastcall TSpriteScene::DestroyObjects(void) { if(FSpriteList != NULL) { for(int i = 0; i < FSpriteList->Count; i++) ((TSprite*)(FSpriteList->Items[i]))->Surface->Release(); FSpriteList->Clear(); } } void __fastcall TSpriteScene::AddSprite(TSprite *Sprite) { SpriteList->Add(Sprite); } void __fastcall TSpriteScene::DrawScene(void) { const char ErrStr[] = "TSpriteScene.DrawScene"; HResult hr; TSprite *Sprite; TScene::DrawScene(); for(int i = 0; i < FSpriteList->Count; i++) { Sprite = (TSprite *)FSpriteList->Items[i]; hr = FHermes->BackSurface->BltFast(Sprite->XPos, Sprite->YPos, Sprite->Surface, &Sprite->FRect, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY); if(!Windows::Succeeded(hr)) throw(EDDError(ErrStr, OPENARRAY(TVarRec, (int(hr), GetOleError(hr), "Check BackRect, BackOrigin???")))); } } long __fastcall TSpriteScene::RestoreSurfaces(void) { TSprite *Sprite; HRESULT Result; Result = TScene::RestoreSurfaces(); if(Result == DD_OK) { for(int i = 0; i < FSpriteList->Count; i++) { Sprite = (TSprite *)FSpriteList->Items[i]; Result = Sprite->Surface->Restore(); if(Result == DD_OK) DDReloadBitmapLib(FLib, Sprite->Surface, Sprite->Bitmap); else break; // Exit on error } } return((long)Result); } void __fastcall TSpriteScene::SetupSurfaces(TObject *Sender) { IDirectDrawSurface *Surface; TSprite *Sprite; DDSURFACEDESC SurfaceDesc; HRESULT hr; TScene::SetupSurfaces(Sender); if(SpriteList == NULL) return; for(int i = 0; i < SpriteList->Count; i++) { Sprite = (TSprite *)SpriteList->Items[i]; if(!CreateDDSurface(Surface, Sprite->Bitmap, False)) throw EDDError("Could not create surface"); SurfaceDesc.dwSize = sizeof(SurfaceDesc); hr = Surface->GetSurfaceDesc(&SurfaceDesc); if(hr == DD_OK) Sprite->Rect = Rect(0, 0, SurfaceDesc.dwWidth, SurfaceDesc.dwHeight); else throw EDDError("No SurfaceDesc"); Sprite->Surface = Surface; } } void __fastcall TSceneEditor::Edit(void) { RunSceneEditorDlg((TScene *)Component); } namespace Mercury2 { void __fastcall Register() { TComponentClass classes[5] = {__classid(THermes), __classid(TScene), __classid(THermesChart), __classid(TSpriteScene), __classid(TSprite)}; RegisterComponents("Unleash", classes, 4); RegisterComponentEditor(__classid(TScene), __classid(TSceneEditor)); } }
In this chapter, you learned how to create a simple graphics engine that gives you access to DirectX. In particular, you learned how to do the following:
In the next chapter, I add a simple game engine to this graphics engine and then give a simple example of how to use these tools to create the elements of a strategy game.
©Copyright, Macmillan Computer Publishing. All rights reserved.