Overview
In this chapter and the next, you'll examine multimedia and game objects. In
particular, you will see the following:
- A simple two-dimensional graphics engine that uses DirectDraw
- A simple game engine
- Examples of how to use both objects
- Various comments on object design
- The opening portions of a strategy game called Byzantium
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:
- Games
- Simulations
- Presentations
- Advanced graphical features of all kinds
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 Resources
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.
Where We're Headed: Object Overview
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.
Creating Public Interfaces
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.
Managing Complexity: Create Humble Objects
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:
- Any hope of code reuse is obliterated. If one small set of objects forms
the whole game, how can you separate the reusable part of the code for a
second game that has similar characteristics?
- Maintaining it is a nightmare. If something goes wrong anywhere in the
game, you have no way of localizing a problem and testing it separately from
the rest of the application. If all you have are a few big, complex objects,
when something goes wrong, all the lines of code in your program are suspect,
and you cannot separate out the code you need to test and repair. This
monumental problem is likely to sink most projects completely. You have to be
able to break down a program into its various components and test each part
separately.
- Teaching someone how to work with the code for a game is extremely hard if
he or she has to understand the entire game to be able to work on any part of
it. Understanding one huge, complex object is difficult. The paradigm is
innately complex. You have a much simpler task getting someone up to speed on
some code if you can say, "Here, we need to make these changes to this
500-line object. Please get to work right away; time is of the essence." The
nightmare scenario sounds like this: "Welcome aboard. We have 200,000 lines of
code here, and we need to make some changes to it. Please spend the next six
months becoming familiar with our code base, and then I would like you to make
some changes to a few small portions of it. Tell me when you're up to speed."
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.
- Code reuse is supported. A well-designed graphics engine can be reused in
almost any graphics-based game. A subset of those objects can be used in
virtually any multimedia venture that requires DirectDraw. The same is true
with the GameEngine code. It's all in one place and can be reused.
Conversely, the code that is specific to Byzantium is all in one place.
- Maintaining it is easy. If something goes wrong in the code, you can
isolate the problem in a few distinct objects that you can test separately.
Problems in one part of the code don't automatically percolate into other,
related domains. Instead, they are isolated in one section of the code. This
problem is like a leak in a submarine: If one section of the ship is damaged,
you can seal it off from the rest of the ship. It won't affect the whole
program; it is isolated in one area. You can then concentrate on fixing that
area without worrying about the ramifications of those repairs on the rest of
the program.
- Understanding it is easy. You know exactly what is going on in the
graphics engine components because the problem domain is manageable. You can
explain the basic structure of the program to someone in five minutes because
each part plays a distinct, easy-to-understand role.
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.
The DirectX Game Engine
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.
Table 28.1. The key objects in the graphics engine.
| 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 |
After I describe the graphics engine, I will describe the game engine that
can be used in conjunction with these objects.
THermes: Getting Into and Out of Exclusive Mode
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:
- I do not quote the entire source until the end of the chapter. Throughout
the main body of this chapter, I quote only the class declarations, one at a
time as needed so that you can use them to grasp the structure of the portion
of code currently under discussion. At the end of the chapter, I quote the
entire object.
- You can install all the key objects in these units as components and use
them directly in BCB, just as you would use any of the other components
discussed in this book.
- You don't really have to understand this code, but you do need to know how
to use the components created when these objects are installed on the
Component Palette.
Listing 28.1. Mercury2.cpp contains logic for handling
sprites, backgrounds, and DirectX. The key object in this file is THermes.
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.
Listing 28.2. The HermesTest1 project shows how to use THermes. The header for
the main unit is shown here.
///////////////////////////////////////
// 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
Listing 28.3. The source for the main unit in the
HermesTest1 project.
///////////////////////////////////////
// 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.
TScene: Drawing a Background
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.
Listing 28.4. The class declaration for the key parts
of the TScene and TDraw objects.
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) { }
};
Listing 28.5. The header for the unit from the
SceneTest1 project that tests the TScene object.
///////////////////////////////////////
// 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
Listing 28.6. The main form for the unit that tests
the TScene object.
///////////////////////////////////////
// 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.
Art and Palettes
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.
Working with TSpriteScene
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.
Listing 28.7. The declarations for the TSpriteScene
and TScene classes.
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;
};
Listing 28.8. The header for the SpriteTest1 program.
///////////////////////////////////////
// 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
Listing 28.9. The main form for the TestSprite1
program.
///////////////////////////////////////
// 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:
- High performance
- Control over the size of the palette
- Control over the dimensions of the screen
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.
THermesChart: Working with a Tiled World
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.
Listing 28.10. The declaration for the THermesChart
class and its parent.
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) { }
};
Listing 28.11. The header file for the TilerTest
program.
///////////////////////////////////////
// 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
Listing 28.12. The main source for the TestTiler
program.
///////////////////////////////////////
// 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:
- You can ship one file with your game rather than a series of files.
- The DLL helps to conceal your bitmaps from prying eyes. Other developers
will know how to get at them, but most users won't be able to find them.
- You can ship a series of DLLs with your projects and then load and unload
the DLLs as needed to access various different bitmaps. If you switch scenes
during the run of a program, the DLL associated with one scene will be
unloaded from memory, and the DLL associated with the new scene will be
loaded.
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 Mercury2 Source Code
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.
Listing 28.13. The header for the Mercury2 unit.
#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
Listing 28.14. The main source file for the Mercury2
module.
///////////////////////////////////////
// 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));
}
}
Summary
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:
- Get into and out of Exclusive mode
- Restore surfaces when the user switches away from a DirectX application
- Create backgrounds and sprites
- Work with a tiled world that expands far beyond the reaches of the screen
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.
|