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.
|