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.
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:
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.
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.
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.
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.
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;
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.
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.
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.
©Copyright, Macmillan Computer Publishing. All rights reserved.