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