Game Engines
Overview
In this chapter, you
will see how to implement the key pieces of a simple strategy game called Byzantium. In
particular, you will see how to define problem domains that assure that
graphics-based problems are confined to the graphics engine and that
game-based problems are confined to the game engine.
There are no hard and
fast rules defining where the lines between problem domains should be
drawn. The particular scope of a problem domain can be decided only by the
individuals involved in any one project. The key is not necessarily seeking
out the ideal problem domains, but rather creating certain reasonable
domains and then rigorously enforcing their sovereignty.
Unlike the code in the
preceding chapter, all the code in this chapter is written in C++. In
particular, you will see two base objects called TCharacter and TGame, each of which can be used in a
wide variety of projects. Descending from these broad, abstract tools are
two classes called TByzCharacter and TByzGame. These objects will help to
define the character of the particular game implemented in this chapter.
Creating
the Framework for a Simple Game
In the preceding
chapter, you learned how to develop a graphics engine. Now you're ready to
go on and create the framework for a simple game. I have not had time to
complete the entire game for this book, but I can give you the basic tools
you need to start creating it. In other words, I will give you a game
engine, but not a complete game. I hope that you will find this reasonable,
because games are plentiful and game engines are in short supply.
The partially complete
game called Byzantium
described in this chapter uses all the elements in the graphics engine you
learned about in the preceding chapter. This game is a rudimentary version
of the type of strategy game you find in the old hack games that used to
circulate on the Net, and it is also remotely related to WarCraft, Heroes
of Might and Magic, and Civilization. Byzantium
is not nearly as fancy as those games, but it has some of the same
underlying architectural features, albeit only in nascent form.
The game has three main
screens, shown in Figures 29.1 through 29.3. The first scene is the
introduction to the game, the second is the main playing field, and in the
third you can have battles between the hero and various nefarious
opponents. The hero can find food and medical kits as he wanders around,
and the game tracks his hunger and strength.
FIGURE 29.1. The introductory scene to Byzantium.
FIGURE 29.2. The main playing field for Byzantium. An apple is visible near the
top of the screen, and a medical kit near the bottom. In the center are the
hero and an opponent.
FIGURE 29.3. A battle occurs between the hero and an
evil queen.
CAUTION: You are free to play around with these game
elements to whatever degree you want. If you create your own games for
public distribution with these tools, you must devise your own map, using
your own art. In other words, the world found here, with mountains, grass,
and ocean in particular locations, cannot be used in your own games. You
can, however, use the tools provided with this book to create your own
world, with different mountains, grass, and lakes and different bitmaps
used to depict them. You can use both the graphics engine and game engine
in any way you see fit. It's the art and the world I have created that I
want to reserve for my own use.
The source for Byzantium is divided
into three parts:
- The graphics engine, which was
explained in the preceding chapter
- The game engine, which is described in
this chapter
- The game itself, which consists of a
set of objects resting on top of both the graphics engine and the game
engine
In Listings 29.1
through 29.9, you will find the source for the game engine in a set of
files called GameEngine1.cpp and GameEngine1.h. You can find the game itself
primarily in ByzEngine.cpp, ByzEngine1.h, GameForm1.cpp, GameForm1.h, FightClass1.cpp, and FightClass1.h. I have a few other related
files, such as one that contains a set of global constants and types, but
the heart of the game is located in the files described here. Besides the
main form, one global object called ByzGame is available to all the modules
of the program. This object encapsulates the game engine itself.
Listing 29.1. The header file for the game
engine.
/////////////////////////////////////// // File: GameEngine1.h // Project: GameObjects // Copyright (c) 1997 by Charlie Calvert #ifndef GameEngine1H #define GameEngine1H #include <vcl\Forms.hpp> #include "gameform1.h" #include "creatures1.hpp" class TGame; class TCharacter : public TObject { private: AnsiString Bitmaps; TGame *FGame; TCreature *FCreature; int GetRow(void); int GetCol(void); AnsiString GetName(void); TStringList *FCustomFeatures; TStringList *GetCustomFeatures(void); protected: virtual __fastcall ~TCharacter(void); public: virtual __fastcall TCharacter(TGame *AGame); __property TGame *Game={read=FGame, write=FGame, nodefault}; __property int Row={read=GetRow, nodefault}; __property int Col={read=GetCol, nodefault}; __property TCreature *Creature={read=FCreature, write=FCreature, nodefault}; __property AnsiString Name={read=GetName, nodefault}; __property TStringList *CustomFeatures={read=GetCustomFeatures, nodefault}; void Move(int Key); }; class TScoreCard : public TObject { }; // typedef void __fastcall (__closure *TSetSceneProc)(int NextScene); class TGame : public TObject { private: TCreatureList *FCreatureList; TCharacter *FHero; TCharacter *FBadGuy; AnsiString FCreatureFile; AnsiString FScreenFile; TGameForm *FCurrentGameForm; int FCurrentScene; void SetHero(TCharacter *Hero); void SetBadGuy(TCharacter *ABadGuy); protected: virtual void CreateCharacters(void); virtual __fastcall ~TGame(void); public: virtual __fastcall TGame(void); void Initialize(TGameForm *AOwner, TCreatureList *ACreatureList); void SetScene(TGameForm *AOwner, HANDLE MainHandle); void UpdateMap(); // properties __property TCharacter *Hero= {read=FHero, write=SetHero, nodefault}; __property TCharacter *BadGuy= {read=FBadGuy, write=SetBadGuy, nodefault}; __property AnsiString CreatureFile= {read=FCreatureFile, write =FCreatureFile, nodefault}; __property AnsiString ScreenFile= {read=FScreenFile, write=FScreenFile}; __property TGameForm *CurrentGameForm= {read=FCurrentGameForm, write=FCurrentGameForm}; __property int CurrentScene= {read=FCurrentScene, write=FCurrentScene, nodefault}; __property TCreatureList *CreatureList= {read=FCreatureList, write=FCreatureList}; }; #endif
Listing
29.2. The main source for the game engine.
/////////////////////////////////////// // File: GameEngine1.cpp // Project: GameObjects // Copyright (c) 1997 by Charlie Calvert // #include <vcl.h> #pragma hdrstop #include "GameEngine1.h" /////////////////////////////////////// // Constructor /////////////////////////////////////// __fastcall TGame::TGame(void) { FCreatureList = NULL; FHero = NULL; FBadGuy = NULL; } /////////////////////////////////////// // Destructor /////////////////////////////////////// __fastcall TGame::~TGame(void) { } /////////////////////////////////////// // Initialize /////////////////////////////////////// void TGame::Initialize(TGameForm *AOwner, TCreatureList *ACreatureList) { if (FCreatureList == NULL) { CurrentGameForm = AOwner; FCreatureList = ACreatureList; CreateCharacters(); FHero->Creature = FCreatureList->CreatureFromName("Hero"); } } void TGame::CreateCharacters(void) { FHero = new TCharacter(this); FBadGuy = new TCharacter(this); } /////////////////////////////////////// // SetHero /////////////////////////////////////// void TGame::SetHero(TCharacter *AHero) { FHero = AHero; FHero->Game = this; } void TGame::SetBadGuy(TCharacter *ABadGuy) { FBadGuy = ABadGuy; FBadGuy->Game = this; } void TGame::SetScene(TGameForm *AOwner, HANDLE MainHandle) { CurrentGameForm = AOwner; CurrentScene = AOwner->ShowModal(); PostMessage(MainHandle, WM_NEXTSCENE, 0, 0); } void TGame::UpdateMap() { FCreatureList->UpdateMap(); } // ------------------------------------ // -- TCharacter -------------------- // ------------------------------------ /////////////////////////////////////// // Constructor /////////////////////////////////////// __fastcall TCharacter::TCharacter(TGame *AGame) { FGame = AGame; FCustomFeatures = new TStringList(); } __fastcall TCharacter::~TCharacter(void) { FCustomFeatures->Free(); } int TCharacter::GetRow(void) { return Creature->TrueRow; } int TCharacter::GetCol(void) { return Creature->TrueCol; // return Game->CurrentGameForm->Tiler->Hero->TrueCol; } AnsiString TCharacter::GetName(void) { if (FCreature) return FCreature->CreatureName; else return "Creature not initialized"; } TStringList *TCharacter::GetCustomFeatures(void) { int i; FCustomFeatures->Clear(); for (i = 0; i < FCreature->GetCustomCount() - 1; i++) { FCustomFeatures->Add(FCreature->GetCustom(i)->ValueName); } return FCustomFeatures; } void TCharacter::Move(int Key) { if (Name == "Hero") Game->CurrentGameForm->HermesChart1->Move(Key); }
Listing
29.3. The header file for the game objects specific to Byzantium.
/////////////////////////////////////// // File: ByzEngine1.h // Project: GameObjects // Copyright (c) 1997 by Charlie Calvert // #ifndef ByzEngine1H #define ByzEngine1H #include "gameengine1.h" class TByzCharacter : public TCharacter { private: int FArmor; int FWeapon; int FHitPoints; int FHunger; int FStrength; bool FVisible; int GetArmor(void); void SetArmor(int Value); int GetHitPoints(void); void SetHitPoints(int Value); int GetHunger(void); void SetHunger(int Value); int GetWeapon(void); void SetWeapon(int Value); int GetStrength(void); void SetStrength(int Value); protected: virtual __fastcall ~TByzCharacter(void); public: virtual __fastcall TByzCharacter(TGame *AGame); bool DefendYourself(TByzCharacter *Attacker); void SetVisible(bool Value); __property int Armor={read=GetArmor, write=SetArmor, nodefault}; __property int Hunger={read=GetHunger, write=SetHunger, nodefault}; __property int HitPoints={read=GetHitPoints, write=SetHitPoints, nodefault}; __property int Weapon={read=GetWeapon, write=SetWeapon, nodefault}; __property int Strength={read=GetStrength, write=SetStrength, nodefault}; // __property bool Visible={read=FVisible, write=SetVisible, nodefault); }; class THero : public TByzCharacter { public: __fastcall THero(TGame *AGame): TByzCharacter(AGame) {} }; class TBadGuy : public TByzCharacter { public: __fastcall TBadGuy(TGame *AGame): TByzCharacter(AGame) {} }; class TByzGame : public TGame { protected: virtual void CreateCharacters(void); public: __fastcall TByzGame(void): TGame() {} }; extern TByzGame *ByzGame; #endif
Listing
29.4. The main source for the game objects specific to Byzantium.
/////////////////////////////////////// // File: ByzEngine1.cpp // Project: GameObjects // Copyright (c) 1997 by Charlie Calvert // #include <vcl.h> #pragma hdrstop #include "ByzEngine1.h" TByzGame *ByzGame; void TByzGame::CreateCharacters(void) { Hero = new THero(this); BadGuy = new TBadGuy(this); } __fastcall TByzCharacter::TByzCharacter(TGame *AGame) : TCharacter(AGame) { } __fastcall TByzCharacter::~TByzCharacter(void) { } int TByzCharacter::GetArmor(void) { return Creature->GetCustomInt("Armor"); } void TByzCharacter::SetArmor(int Value) { Creature->SetCustomInt("Armor", Value); } int TByzCharacter::GetHitPoints(void) { return Creature->GetCustomInt("Hit Points"); } void TByzCharacter::SetHitPoints(int Value) { Creature->SetCustomInt("Hit Points", Value); } int TByzCharacter::GetHunger(void) { return Creature->GetCustomInt("Hunger"); } void TByzCharacter::SetHunger(int Value) { Creature->SetCustomInt("Hunger", Value); } int TByzCharacter::GetWeapon(void) { return Creature->GetCustomInt("Weapon"); } void TByzCharacter::SetWeapon(int Value) { Creature->SetCustomInt("Weapon", Value); } int TByzCharacter::GetStrength(void) { return Creature->GetCustomInt("Strength"); } void TByzCharacter::SetStrength(int Value) { Creature->SetCustomInt("Strength", Value); } void TByzCharacter::SetVisible(bool Value) { Creature->Visible = Value; FVisible = Value; if (!Value) ByzGame->UpdateMap(); } int GetResistanceChance() { int i = random(49); i -= (24); return i; } int GetWeaponChance() { return 0; } void PlaySound(AnsiString S) { sndPlaySound(S.c_str(), SND_ASYNC); } bool TByzCharacter::DefendYourself(TByzCharacter *Attacker) { int Resistance = (Strength - Attacker->Strength) + (Armor - Attacker->Weapon); if (Resistance + GetResistanceChance() < 0) { HitPoints -= (Attacker->Weapon - GetWeaponChance()); PlaySound("..\\media\\bang.wav"); return False; } else { PlaySound("..\\media\\rev.wav"); return True; } }
Listing
29.5. The header file for the game form.
/////////////////////////////////////// // GameForm1.h // Byzantium Project // Copyright (c) 1997 by Charlie Calvert // #ifndef GameForm1H #define GameForm1H #include <Classes.hpp> #include <Controls.hpp> #include <StdCtrls.hpp> #include <Forms.hpp> #include "globals.h" #include "Creatures1.hpp" #include "FightClass1.h" #include "Mercury2.h" class TGameForm : public TForm { __published: THermes *Hermes1; THermesChart *HermesChart1; TFileCreatureList *FileCreatureList1; TScene *Scene1; TSpriteScene *SpriteScene1; TSprite *Hero1; TSprite *BadQueen1; void __fastcall FormShow(TObject *Sender); void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift); void __fastcall FormDestroy(TObject *Sender); void __fastcall HermesChart1DrawScene(TObject *Sender); void __fastcall SpriteScene1SetupSurfaces(TObject *Sender); void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y); void __fastcall SpriteScene1DrawScene(TObject *Sender); void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y); void __fastcall Scene1DrawScene(TObject *Sender); void __fastcall HermesChart1HeroMove(TObject *Sender, const tagPOINT &NewPos, int NewType, bool &MoveOk); private: // TNotifyEvent FHitCreatureProc; TFightClass *FFightClass; MESSAGE void StartShow(TMessage &Msg); public: virtual __fastcall TGameForm(TComponent* Owner); void Run(void); BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_STARTSHOW, TMessage, StartShow); END_MESSAGE_MAP(TForm); }; extern TGameForm *GameForm; #endif
Listing
29.6. The main source for the game form.
/////////////////////////////////////// // GameForm1.cpp // Byzantium Project // Copyright (c) 1997 by Charlie Calvert // #include <vcl.h> #pragma hdrstop #include "Globals.h" #include "ByzEngine1.h" #include "GameForm1.h" #pragma link "Creatures1" #pragma link "Mercury2" #pragma resource "*.dfm" TGameForm *GameForm; __fastcall TGameForm::TGameForm(TComponent* Owner) : TForm(Owner) { FFightClass = NULL; ByzGame = new TByzGame(); ByzGame->CurrentScene = mrIntroMap; } void __fastcall TGameForm::FormDestroy(TObject *Sender) { delete ByzGame; } /////////////////////////////////////// // Run /////////////////////////////////////// void TGameForm::Run(void) { if (FFightClass) { delete FFightClass; FFightClass = NULL; } switch(ByzGame->CurrentScene) { case mrWorldMap: Hermes1->Scene = HermesChart1; break; case mrIntroMap: Hermes1->Scene = Scene1; break; case mrFightMap: Hermes1->Scene = SpriteScene1; FFightClass = new TFightClass(Handle, SpriteScene1); break; } Hermes1->InitObjects(); ByzGame->Initialize(this, Hermes1->CreatureList); Hermes1->Flip(); } void __fastcall TGameForm::FormShow(TObject *Sender) { PostMessage(Handle, WM_STARTSHOW, 0, 0); } void TGameForm::StartShow(TMessage &Msg) { Run(); } void __fastcall TGameForm::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { if ((Shift.Contains(ssAlt)) && (Key=='X')) { if (Hermes1->Exclusive) Hermes1->EndExclusive(); Close(); } else if ((Shift.Contains(ssAlt)) && (Key=='A')) { ByzGame->CurrentScene = mrIntroMap; Run(); } else if ((Shift.Contains(ssAlt)) && (Key=='B')) { ByzGame->CurrentScene = mrWorldMap; Run(); } else if (ByzGame) dynamic_cast<THero*>(ByzGame->Hero)->Move(Key); } void __fastcall TGameForm::HermesChart1DrawScene(TObject *Sender) { AnsiString S; S = "Col: " + IntToStr(ByzGame->Hero->Col); // S = S + " Scr Col: " + IntToStr(ByzGame->Hero->Creature->ScreenCol); // S = S + " Map Col: " + IntToStr(Hermes1->CreatureList->MapCol); S = S + "Hit Points: " + dynamic_cast<TByzCharacter*>(ByzGame->Hero)->HitPoints; HermesChart1->WriteXY(370, 410, S); S = "Row: " + IntToStr(ByzGame->Hero->Row); // S = S + " Scr Row: " + IntToStr(ByzGame->Hero->Creature->ScreenRow); // S = S + " Map Row: " + IntToStr(Hermes1->CreatureList->MapRow); S = S + " Hunger: " + dynamic_cast<TByzCharacter*>(ByzGame->Hero)->Hunger; HermesChart1->WriteXY(370, 430, S); } void __fastcall TGameForm::SpriteScene1SetupSurfaces(TObject *Sender) { SpriteScene1->AddSprite(Hero1); SpriteScene1->AddSprite(BadQueen1); } void __fastcall TGameForm::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { if (ByzGame->CurrentScene == mrFightMap) { if (BadQueen1->IsHit(X, Y)) Screen->Cursor = crCross; else Screen->Cursor = crDefault; } } void __fastcall TGameForm::SpriteScene1DrawScene(TObject *Sender) { if (FFightClass) FFightClass->ShowData(); } void __fastcall TGameForm::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { if (ByzGame->CurrentScene == mrFightMap) { if (BadQueen1->IsHit(X, Y)) FFightClass->PerformHit(this); } } void __fastcall TGameForm::Scene1DrawScene(TObject *Sender) { Scene1->WriteXY(375, 405, "Press Alt-B to Start"); Scene1->WriteXY(375, 430, "Press Alt-X to Exit"); } void __fastcall TGameForm::HermesChart1HeroMove(TObject *Sender, const tagPOINT &NewPos, int NewType, bool &MoveOk) { switch (TMapType(NewType)) { case mtGrass: MoveOk = True; break; case mtCreature: MoveOk = False; ByzGame->BadGuy->Creature = ByzGame->CreatureList->CreatureFromLocation(NewPos.x, NewPos.y); if (ByzGame->BadGuy->Creature->Kind == "Food") { dynamic_cast<TByzCharacter*>(ByzGame->Hero)->Hunger += 3; dynamic_cast<TByzCharacter*>(ByzGame->BadGuy)->SetVisible(False); } else if (ByzGame->BadGuy->Creature->Kind == "Medicine") { dynamic_cast<TByzCharacter*>(ByzGame->Hero)->HitPoints += 3; dynamic_cast<TByzCharacter*>(ByzGame->BadGuy)->SetVisible(False); } else { ByzGame->CurrentScene = mrFightMap; Run(); } break; default: MoveOk = False; } }
Listing
29.7. A header file containing some global declarations.
/////////////////////////////////////// // Globals.h // Byzantium Project // Copyright (c) 1997 by Charlie Calvert // #ifndef GlobalsH #define GlobalsH #define mrHitCreature 0x5001 #define mrGameOver 0x5002 #define mrWorldMap 0x6001 #define mrFightMap 0x6002 #define mrIntroMap 0x6003 #define WM_NEXTSCENE WM_USER + 1 #define WM_STARTSHOW WM_USER + 2 enum TMapType {mtGrass, mtWater, mtMountain, mtRoad, mtWater2, mtFootHill, mtNorthShore, mtWestShore, mtSouthShore, 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}; #endif
Listing
29.8. The header file for the fight class.
/////////////////////////////////////// // Fightclass.h // Project: Byzantium // Copyright (c) 1997 by Charlie Calvert // #ifndef FightClass1H #define FightClass1H #include "Mercury1.hpp" class TFightClass { private: AnsiString FBadGuyName; AnsiString FDisplayString; HWND FHandle; TScene *FScene; bool FHitInProcess; void Button1Click(void); bool BadGuyAttacks(void); bool CheckCharacters(void); bool HeroAttacks(void); void DisplayData(AnsiString S); public: TFightClass(HWND AHandle, TScene *AScene); void PerformHit(TObject *Sender); void ShowData(); __property AnsiString BadGuyName={read=FBadGuyName}; }; #endif
Listing
29.9. The main source file for the fight class.
/////////////////////////////////////// // Fightclass.cpp // Project: Byzantium // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #include <time.h> #pragma hdrstop #include "Creatures1.hpp" #include "FightClass1.h" #include "ByzEngine1.h" #include "Mercury2.h" TFightClass::TFightClass(HWND AHandle, TScene *AScene) { FHandle = AHandle; FHitInProcess = False; FScene = AScene; FBadGuyName = ByzGame->BadGuy->Name; } void TFightClass::DisplayData(AnsiString S) { TCustomValue *CustomValue; AnsiString DisplayValue; CustomValue = ByzGame->Hero->Creature->FindCustomByName("Hit Points"); DisplayValue = CustomValue->CurrentValue; FScene->WriteXY(270, 405, DisplayValue); CustomValue = ByzGame->BadGuy->Creature->FindCustomByName("Hit Points"); DisplayValue = CustomValue->CurrentValue; FScene->WriteXY(270, 440, DisplayValue); FScene->WriteXY(375, 410, FDisplayString); } void TFightClass::ShowData() { DisplayData("Hit Points"); if (ByzGame->BadGuy->Creature) DisplayData("Hit Points"); } void TFightClass::Button1Click() { ShowMessage(ByzGame->Hero->Name + " retreats. Receives 5 points damage."); dynamic_cast<TByzCharacter *>(ByzGame->Hero)->HitPoints -= 5; if (CheckCharacters()); } void WaitTime(int Delay) { time_t t1, t2; t1 = time(NULL); while (True) { Application->ProcessMessages(); t2 = time(NULL); if (t2 - t1 >= Delay) return; } } bool TFightClass::CheckCharacters(void) { TBadGuy *B = dynamic_cast<TBadGuy*>(ByzGame->BadGuy); if (B->HitPoints <= 0) { ByzGame->CreatureList->HideCreature(B->Name, False); FDisplayString = "Victory is sweet!"; WaitTime(1); ByzGame->CurrentScene = mrWorldMap; PostMessage(FHandle, WM_STARTSHOW, 0, 0); return False; } if (dynamic_cast<THero*>(ByzGame->Hero)->HitPoints <= 0) { FDisplayString = "Defeat is bitter ashes!"; WaitTime(1); ByzGame->CurrentScene = mrIntroMap; PostMessage(FHandle, WM_STARTSHOW, 0, 0); return False; } return True; } bool TFightClass::BadGuyAttacks(void) { THero *H = dynamic_cast<THero*>(ByzGame->Hero); TBadGuy *B = dynamic_cast<TBadGuy*>(ByzGame->BadGuy); FDisplayString = H->Name + " Under attack!"; WaitTime(1); if (H->DefendYourself(B)) { FDisplayString = H->Name + ": No damage!"; } else FDisplayString = H->Name + " is hit!"; WaitTime(1); return CheckCharacters(); } bool TFightClass::HeroAttacks(void) { TBadGuy *B = dynamic_cast<TBadGuy*>(ByzGame->BadGuy); THero *H = dynamic_cast<THero*>(ByzGame->Hero); FDisplayString = B->Name + " Under attack!"; WaitTime(1); if (B->DefendYourself(H)) FDisplayString = B->Name + ": No damage!"; else FDisplayString = B->Name + " is hit!"; WaitTime(1); return CheckCharacters(); } void TFightClass::PerformHit(TObject *Sender) { if (FHitInProcess) return; FHitInProcess = True; if (HeroAttacks()) BadGuyAttacks(); FHitInProcess = False; FDisplayString = "Waiting..."; }
As it is implemented
here, Byzantium is a very simple game. When the program is first launched,
you see a main form with a picture of a bucolic landscape. A window in the
form states that you can start the game by pressing Alt+B, or you can press
Alt+X to exit.
If you press Alt+B,
then you can see the hero standing on a tiled world map. You can use the
arrow keys to move the hero, pushing the Insert key to toggle back and
forth between moving the hero alone, or moving the entire landscape.
The hero can interact
with various objects on the tiled surface. For example, the hero can eat
bits of food, thereby alleviating his hunger. He also can pick up medical
kits to restore the hit points or his health.
The hero can also encounter
various bad guys, most of whom live in castles or stone turrets. If you
bump into a bad guy, you will be switched to a third scene where the hero
can engage in combat with the bad guy.
When in fight mode, the
hero, dressed as a monk, appears on the left. The villain, who is always a
wicked queen, is standing on the right. If you move the mouse cursor over
the queen, the cursor changes shape; that is, it moves into attack mode. When
the cursor is in attack mode, you can left-click the wicked queen to attack
her. The hero gets a chance to do some damage to her, and she in turn will
have a chance to attack the hero.
NOTE: I feel the need to defend myself against
possible charges of sexism. As I develop this game further, I will give the
user a chance to choose whether the main character is a man or woman. It
was perhaps not wise of me to pick the word "Hero" as a field of
the TGame
object, but it seemed to me more concise and easy to understand than a
phrase like "MainCharacter."
The fact that the villain is a queen is mostly a function of my artist's
inclination when she produced her first evil character for me to use. As
the game matures, it will have more evil characters, some male, some
female.
In short, the game is not intended to contain any political messages about
sexuality, and a more egalitarian world view will emerge as the game
matures.
The game is designed so
that the hero can easily withstand several fights with the bad guys. Eventually,
however, he will be worn down and will need to find more food or medical
kits or else perish. The condition for losing the game is to run out of hit
points before killing all the bad guys.
As I stated earlier,
this game is not complete. My goal was just to give you enough pieces so
that you could begin to construct your own game with its own rules. Where
you take the game from the point at which I have left it is up to you. You
might, however, want to check my Web site to see whether I have found time
to actually complete a full game.
Understanding
the Technology Behind Byzantium
Now you can take a
closer look at Byzantium. In the next few sections of the book, I help you
examine the technology behind the game, showing the way it was put together
and giving hints about ways in which the game could be expanded.
This program uses all
the graphics engine components introduced in the preceding chapter. I lay
them out on the main form, as shown in Figure 29.4. The properties of these
objects are filled out almost exactly as they were in Chapter 28,
"Game Programming," only this time I'm using all the components
at once. To see the details of which properties are connected to which
values, you should launch the game and study the main form.
FIGURE 29.4. The graphics components used in the
Byzantium program as they appear on the main form at design time.
On top of the graphics components I lay a game engine that consists of two
main objects called TCharacter and TGame. These objects are meant to be
base classes from which you can make descendants of the characters and
games that you want to create.
The key fact to
understand about TGame and TCharacter is that they know how to work
with the graphics engine and shield the user from the graphics engine's
complexity. In short, the user should feel most of the time as though he or
she is manipulating a game or character that simply knows how to draw him,
her, or itself to the screen. In short, the programmer can stay inside the
problem domain defined by the game itself and can ignore the problems
inherent in implementing a graphics engine.
For example, the user
can simply ask the character to move, hide, state its name, or keep track
of its health, hit points, and so on. The technical implementation of all
these traits should not be a concern to the programmer. It doesn't matter
how a character moves, hides, or is drawn to the screen. When you're
writing a game, you don't want to have to think about those kinds of
issues. You just want to design a game.
Furthermore, you want
to be sure that problems with the graphics engine can occur only if a
mistake is made in Mercury1.pas or in Creatures1.pas. Graphics-based problems should
never be caused by errors in the game engine because the game engine
shouldn't contain any graphics-based code. Conversely, problems with the
logic of the game should not ever occur in the graphics engine because it
should contain no game logic. Game-based problems are put in the game engine,
and graphics-based problems are put in the graphics engine. If you want to
have a maintainable code base, then setting up clearly defined problem
domains is important.
NOTE: Once again, I have to ask myself how
completely I have managed to achieve my goals. Can you really afford to
forget about what goes on in Mercury1.pas when you're working with the game objects?
Well, in all truthfulness, you probably can't completely ignore the
graphics engine or its implementation. However, it is hidden well enough
that you can forget about it at times, and a clearly defined partition
exists between the game objects and the graphics objects.
The only time you might have to bridge the gap between the game engine and
graphics engine would be if something went wrong, that is, when you find a
bug. At such times, you have to decide whether the bug is in the game
engine or in the graphics engine, and then you have to implement the fix in
the right place. Though it might not seem likely from this perspective,
fixing the graphics engine by putting some kind of patch in the game
engine, or vice versa, can be very tempting. You should avoid this
temptation whenever possible.
Understanding
the TGame Object
The game object, implemented
in GameEngine1.cpp, has five key properties:
__property TCharacter *Hero; __property TCharacter *BadGuy; __property TGameForm *CurrentGameForm; __property int CurrentScene; __property TCreatureList *CreatureList;
In Byzantium, CurrentScene can be set to one of the
following values:
#define mrWorldMap 0x6001 #define mrFightMap 0x6002 #define mrIntroMap 0x6003
Each of these values
represents one of the possible scenes that can be displayed by the
Byzantium game. Notice that these values are defined as part of Byzantium
itself and are not declared inside the game engine. You therefore can make
up as many of these constants as you need to implement your game. In short,
the TGame
object knows that you will need to define constants specifying the name and
type of the current scene. It does not know or care, however, about the
specific value or meaning of these constants.
In almost all cases,
the game will have only one main form on which a series of different scenes
will be drawn. But the fact that a programmer would want to have more than
one form is conceivable, so I provide for that possibility.
The CreatureList is implemented in Creatures1.pas. It is needed internally by the
TGame object and is made available to
the user in case it might come in handy. Allowing the user to access the CreatureList directly in this manner is not
very wise from a design point of view, but I found it the most practical
solution to a series of potential problems. The CreatureList is made available in the TGame object not through multiple
inheritance, but through aggregation.
The hero is probably
the most important feature of the TGame object. From both the user's
and game programmer's point of view, the hero is the center of the game. One
of the primary goals of the game engine is to allow the user and programmer
to access the hero freely and to treat him as a stand-alone entity with his
own autonomous existence. The hero is really stored on the CreatureList. One of the goals of the TGame object is to allow the
programmer to access the hero without having to think about the CreatureList or the hero's position in it.
The fact that the CreatureList is a public property of TGame shows that I am not sure the
game object automatically provides all necessary access to the creatures on
the CreatureList. As a result, I hedge my bets by giving the user direct access to
the CreatureList, just in case it is needed.
Understanding
the TCharacter Object
The THermes, TScene, and THermesChart objects give you access to
characters that can be moved on a tiled surface. However, these characters
have no separate existence apart from the technology that implements them,
and in particular, they are hung on the CreatureList object, which is a bit unwieldy
to use.
The TCharacter object is designed to give you
some meaningful way to access the characters that live on a tiled grid. In
particular, notice that you can use the Entities program to define
characters, to give them names, and to give them traits such as Hit Points, Hunger, Speed, Weapons, and so on. You can use the
Entities program to add as many characters and traits as you want to the
tiled world implemented by THermesChart.
TCharacter exists in order to lift the
characters out of their tiled world and give them a specific,
easy-to-recognize identity. In particular, note the following traits of the
TCharacter object:
__property int Row={read=GetRow, nodefault}; __property int Col={read=GetCol, nodefault}; __property TCreature *Creature={read=FCreature, write=FCreature, nodefault}; __property AnsiString Name={read=GetName, nodefault}; __property TStringList *CustomFeatures={read=GetCustomFeatures, nodefault};
Each character can have
a position, as defined by the Row and Col properties. Furthermore, it can
have a name and a set of CustomFeatures. The Creature property is like the CreatureList property associated with the
game. In particular, it is implemented by Creatures1.pas and should, from the point of
view of an ideal design, be entirely hidden from the programmer. However, I
cover it here in case it is needed by the programmer.
The CustomFeatures listed in the properties of the
TCharacter object can be defined by the
Entities program, as shown in Figure 29.5. Notice that the properties at
the top of the form, such as Name and Kind, are core properties that
belong to all characters. The properties in the grid at the bottom of the
form are custom properties that can be created by the user. To edit one of
the custom properties, just double-click the appropriate row in the grid.
FIGURE 29.5. Here is a list of the features associated
with the hero.
All the properties shown in the grid at the bottom of the form are custom
properties defined by the user at runtime.
Working
with the TByzCharacter Object
The TCharacter object is an abstraction that
can be used in any game. The TByzCharacter object is a descendant of the TCharacter object designed for use in
Byzantium. TByzCharacter is implemented in ByzEngine1.cpp.
In addition to the
properties it inherits from TCharacter, TByzCharacter has the following traits:
__property int Armor={read=GetArmor, write=SetArmor, nodefault}; __property int Hunger={read=GetHunger, write=SetHunger, nodefault}; __property int HitPoints={read=GetHitPoints, write=SetHitPoints, nodefault}; __property int Weapon={read=GetWeapon, write=SetWeapon, nodefault}; __property int Strength={read=GetStrength, write=SetStrength, nodefault};
Each of these
properties is a custom property surfaced by TByzCharacter so that it can be easily
accessed by the programmer. The key point you need to grasp here is that
the TCreature
object found in Creatures1.pas has a few simple traits such as a name, a
column, and a row. In addition, it has a series of custom properties that
can be defined by the user via the Entities program. The type and number of
these custom properties can be defined by the user.
In Byzantium, I have
decided that the hero and each of the bad guys will have five key traits
called Armor, Hunger, HitPoints, Weapon, and Strength. These properties are given to
the individual creatures in the tiled map through the good graces of the
Entities program. The game programmer can find out about the traits of any
one creature at runtime by accessing the TByzCharacter object, which is one of the
fields of the game object.
Here is the code that TByzCharacter uses to define the armor of a
character:
int TByzCharacter::GetArmor(void) { return Creature->GetCustomInt("Armor"); } void TByzCharacter::SetArmor(int Value) { Creature->SetCustomInt("Armor", Value); }
As you can see, these
methods are just wrappers around the Creature object defined in Creatures1.pas. You can retrieve an individual
creature by finding where it is stored on the CreatureList.
TByzCharacter hides complexity. For example,
if this object did not exist, then you could find out the hero's current
armor value only be iterating through the CreatureList till you found the creature
called Hero. Then
you would have to ask that creature for a custom value called Armor. The game engine objects allow
you to avoid all this confusion; instead, you can write simple code along
these lines:
int Armor = ByzGame->Hero->Armor; ByzGame->Hero->Armor = 3;
The
Character in Battle Against the Queen
Another key trait of
the TByzCharacter object is that it helps define how a character performs in battle:
int GetResistanceChance() { int i = random(49); i -= (24); return i; } int GetWeaponChance() { return 0; } void PlaySound(AnsiString S) { sndPlaySound(S.c_str(), SND_ASYNC); } bool TByzCharacter::DefendYourself(TByzCharacter *Attacker) { int Resistance = (Strength - Attacker->Strength) + (Armor - Attacker->Weapon); if (Resistance + GetResistanceChance() < 0) { HitPoints -= (Attacker->Weapon - GetWeaponChance()); PlaySound("..\\media\\bang.wav"); return False; } else { PlaySound("..\\media\\rev.wav"); return True; } }
The DefendYourself method is called whenever a
character is forced to defend himself or herself. For example, when the
hero is wandering around the world and encounters a bad guy, the fight
scene is launched. Whenever you click the bad queen, she is forced to
defend herself. If she survives, then she goes on the attack and calls on
the hero to defend himself.
The math shown in DefendYourself, GetWeaponChance, and GetResistanceChance is designed to give a fair
degree of randomness to any particular battle. More sophisticated
simulations take into account a wider number of factors and have more
complex forms of randomness. However, the simple math shown here should
serve as a starting point if you want to design your own games.
The actual course of a
battle between the hero and the bad guy is dictated by the TFightClass object, found in FightClass1.cpp:
class TFightClass { private: AnsiString FBadGuyName; AnsiString FDisplayString; HWND FHandle; TScene *FScene; bool FHitInProcess; void Button1Click(void); bool BadGuyAttacks(void); bool CheckCharacters(void); bool HeroAttacks(void); void DisplayData(AnsiString S); public: TFightClass(HWND AHandle, TScene *AScene); void PerformHit(TObject *Sender); void ShowData(); __property AnsiString BadGuyName={read=FBadGuyName}; };
As you can see, this
object has only a few public methods. The ShowData method is meant to be called
whenever a new buffer is being prepared so it can be flipped to the front. In
particular, the object gets a chance to write text to the screen describing
how the battle is proceeding.
The PerformHit method ends up calling the DefendYourself method described previously:
bool TFightClass::HeroAttacks(void) { TBadGuy *B = dynamic_cast<TBadGuy*>(ByzGame->BadGuy); THero *H = dynamic_cast<THero*>(ByzGame->Hero); FDisplayString = B->Name + " Under attack!"; WaitTime(1); if (B->DefendYourself(H)) FDisplayString = B->Name + ": No damage!"; else FDisplayString = B->Name + " is hit!"; WaitTime(1); return CheckCharacters(); } void TFightClass::PerformHit(TObject *Sender) { if (FHitInProcess) return; FHitInProcess = True; if (HeroAttacks()) BadGuyAttacks(); FHitInProcess = False; FDisplayString = "Waiting..."; }
As you can see, PerformHit uses a flag to ensure that the
user can perform only one hit at a time. This game is turn-based, so the
user must wait for the bad guy to strike back before attempting a second
hit.
The HeroAttacks method is really just a wrapper
around DefendYourself. It tells the user that an attack is beginning and then pauses the
game for a moment so that things don't happen so quickly that the user
can't follow the logic of the events as they unfold. The actual call to DefendYourself is over in a flash, but I again
pause the game long enough for the user to read a message about what has
happened.
After the call to HeroAttacks, a similar method called BadGuyAttacks is called.
The following method
checks to see if either the hero or the bad guy has been defeated:
bool TFightClass::CheckCharacters(void) { TBadGuy *B = dynamic_cast<TBadGuy*>(ByzGame->BadGuy); if (B->HitPoints <= 0) { ByzGame->CreatureList->HideCreature(B->Name, False); FDisplayString = "Victory is sweet!"; WaitTime(1); ByzGame->CurrentScene = mrWorldMap; PostMessage(FHandle, WM_STARTSHOW, 0, 0); return False; } if (dynamic_cast<THero*>(ByzGame->Hero)->HitPoints <= 0) { FDisplayString = "Defeat is bitter ashes!"; WaitTime(1); ByzGame->CurrentScene = mrIntroMap; PostMessage(FHandle, WM_STARTSHOW, 0, 0); return False; } return True; }
As you can see, the
condition for losing or winning is simply that the hit points of some
character descend below zero. If this happens to a bad guy, then he is
erased from the screen, and the game continues on the tiled world map. If
the hero runs out of hit points, then the user is returned to the
introductory screen, and the game is assumed to be over. To restart the
game, the user must run the Entities program and restore the hero's hit
points to some reasonably healthy value.
Managing
Game Flow
The course of the game
is managed by the GameForm object. In particular, it has one method
called Run that
sets up each scene:
void TGameForm::Run(void) { if (FFightClass) { delete FFightClass; FFightClass = NULL; } switch(ByzGame->CurrentScene) { case mrWorldMap: Hermes1->Scene = HermesChart1; break; case mrIntroMap: Hermes1->Scene = Scene1; break; case mrFightMap: Hermes1->Scene = SpriteScene1; FFightClass = new TFightClass(Handle, SpriteScene1); break; } Hermes1->InitObjects(); ByzGame->Initialize(this, Hermes1->CreatureList); Hermes1->Flip(); }
If the CurrentScene is set to mrWorldMap, the Scene property of the THermes object is set to HermesChart1, which creates and controls the
tiled world map. InitObjects is then called. This method will ensure
that the graphics objects associated with the last scene are destroyed and
that the graphics objects for the new scene are properly allocated and
initialized. The ByzGame object is then given a chance to catch up
with what is happening. In particular, it checks to make sure that the hero
and bad guy, if any, are set up properly. Finally, the DirectDraw pump is primed through a call
to Flip.
As you can see, the GameForm object calls the graphics
engine through a series of very abstract calls that do not have anything
specific to do with DirectDraw. It does, in fact, matter that I call InitObjects first, then ByzGame->Initialize, and finally Flip. In an ideal API, the order in
which I do things would not be important, and the act of starting the
graphics engine would take one call instead of two. However, the degree of
complexity shown here is manageable, and the possibility that any serious
bugs could be introduced while completing these simple steps is unlikely.
A really ugly
architecture would force you to get into the specifics of DirectDraw at
such a time. For example, it might ask you to create a surface or adjust
the palette. That kind of detail should never be necessary on the level of
the game objects. Game objects are about writing the game; they should not
ask the user to also manipulate the innards of the graphics engine.
NOTE: I make such extreme statements about good
and bad versions of a game engine and graphics engine only because I
personally made a host of mistakes while creating previous versions of
these objects. As I said earlier, seeing an object emerge perfectly the
first time it is implemented is rare. Most objects mature only over time
and over a series of revisions.
As I gain experience creating objects, I find that I tend to avoid certain
egregious errors even in my first drafts of an object hierarchy. Creating
software is part science and part art. Someone can teach you how to get a
science right the first time and every time you implement it. The artistic
portion of the equation is a bit trickier, and my skill in that part of
programming emerges only slowly, and generally only through experience.
Of course, the artistic side of programming is the most interesting. If
writing code ever really did become a science, then I imagine I would lose
interest in the field altogether. Designing objects is fun, and perhaps the
most interesting part of the process is the joy found in improving an
object through a series of revisions.
That's all I'm going to
say about the Byzantium program. Clearly, there is more to this game than I
have explained in these pages. I hope, however, that I have given you
enough insight so that you can productively play with the code on your own.
Summary
In this chapter, you
saw some simple game objects and the rudimentary framework of a game. The
main theme of this chapter is the importance of separating the game objects
from the underlying graphics engine. As such, the task of creating a game
becomes manageable, primarily because it supports separate problem domains,
each of which has an easily defined scope.
For all but a handful
of programmers, the future of programming is likely to center on content
manipulation, education, or entertainment. In short, most programs will
either manage content of some kind or another, or else be intended to
entertain or educate the user. Database and Web-based applications will
focus on content, whereas games and various educational tools will round
out the picture for most programmers. Of course, other programming jobs
will involve the creation of operating systems, compilers, or hardware
management, but they will probably employ only a relatively small number of
workers.
In this book, you
learned about creating database applications, and about how to publish over
the Web. In these last two chapters, I introduced some rudimentary tools
for creating strategy games. All these fields should be very important
during the next few years of computer development.
Whether your interest
lies primarily in games, in content, or in an esoteric field such as
compilers, I hope you have found this text interesting and informative. As
programmers, we are all very fortunate to be involved in such a fascinating
profession with so many opportunities.
Twenty or thirty years
ago the possibility that a class of workers called programmers would emerge
from nowhere to become one of the major forces shaping our society was
inconceivable. Each of us must remember that our primary goal is not to
make money, not to wield power, but to create the kind of world that we and
our children will want to inhabit.
VMS Desenvolvimentos
Diversas Dicas, Apostilas, Arquivos Fontes,
Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++
Builder.
|