NOGDUS
February 24, 2018, 03:25:04 AM *
Welcome, Guest. Please login or register.

Login with username, password and session length
 
   Home   Blogs Help Search Tags Login Register  
Pages: [1]   Go Down
  Print  
Author Topic: Allegro Game Programming Series: Part II -- 2009 version  (Read 2929 times)
0 Members and 1 Guest are viewing this topic.
Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« on: March 20, 2009, 04:44:02 PM »

Allegro Game Programming Series: Part II -- 2009 version

Back in 2006, I wrote some really crappy tutorials.. Roll Eyes I mean crappy as in the code was poorly formatted, inconsistent, ugly, etc..but it worked and lots have learned a lot from them over the years. Cool

I decided its high time I clean them up and that is what this is all about.



I have recreated the project using my modern programming conventions, and using a much nicer framework as the starting point.
The original tutorial can be found here.

The framework that I used to start the recreation of the project can be found here.

Here is a screen-shot of the project running:


Download Code::Blocks project, full-source, and resources

Now, this IS supposed to be a tutorial, so lets go over what I added to the framework code to create the project.
I'm assuming you have downloaded the framework from here and have installed it into Code::Blocks as a user-template.
Create a new project using the user-template, and then open up the game.h file.

I decided to place the entire "game demo" into its own class that will be interfaced with the framework.
So, lets get down to it.

Right above the Game class definition, we need to forward declare our demo class.
Code:
// forward declare the demo class
class TileEngineDemo;

Ok, now we add a pointer to an instance of the demo class to the Game class.
Right after the bool lmbDown_; line, lets put our declaration.

Code:
/// the tile engine demo instance
TileEngineDemo* demo_;

Save game.h now we're done with it. (Unless you want to edit the PROJECT_TITLE_STRING define) Roll Eyes

Open the game.cpp file now.

We need to have a way to tell what direction the player sprite is facing, so we will place an enumeration right after the static globals from the framework.

Code:
// movement directions
enum MovementDirection
{
MotionNorth,
MotionEast,
MotionSouth,
MotionWest
};
Its important that you keep the directions in the same order as I have them, otherwise the code will not work as expected without making changes elsewhere.

Next up we have a few constants.
Its not a great idea to use constants for this, but for our little simple demo it will suffice.
In later tutorials I will explain how to create your game in a dynamic manner.

Ok, here are our constants. They go after the enumeration code.
Code:
// tile engine parameters
const int TILE_SIZE = 32;
const int TILE_COUNT = 3;
const int MAP_WIDTH = 20;
const int MAP_HEIGHT = 15;
Should be easy to understand, but I will explain them anyway.
TILE_SIZE is the number of pixels that each tile has for its width and height.

If your tiles are not square, then you will need to create a TILE_WIDTH and TILE_HEIGHT and substitute them in the code..later on I will go over rectangular tiles when I get into Isometric tile engines!
Anyway, lets continue..

I read an article a long time ago that said not to treat the player differently from any other NPC as far as the code was concerned. So, in light of that article, I am creating a structure called NPC that will be used for the player in my demo.

This goes after the constants we added earlier.
Code:
// a simple structure for our NPCs
typedef struct NPCType
{
int positionX_;
int positionY_;
int facingDirection_;
} NPC, *NPCPtr;

Again easy to understand, but I will explain in detail so there is no confusion.
The positionX_ member is the TILE POSITION along the horizontal axis (the X axis in this case)
The positionY_ member is the TILE POSITION along the vertical axis (the Y axis in this case)
The facingDirection_ member is which direction the NPC will be facing. (Use one of the MovementDirection enumerations to set this member variable's value.)

Now we get to writing the class definition for our demo.
I could have created more files for them, but I chose not to just to keep it clear that ALL of the demo is contained within a single class, and that the framework just creates an instance of it and interfaces with it.

Add this after the NPC structure definition.
Code:
// this class contains the tile engine demo project
// and interfaces with the framework.
class TileEngineDemo
{
public:
TileEngineDemo();
~TileEngineDemo();

bool Initialize();
void Update();
void Render(BITMAP* renderTarget);
void Destroy();

Okay, here we have the entire PUBLIC interface for our class.
You can see that the methods are listed in a logical order.
The class must be initialized by calling the Initialize() method, and then there are the Update() and Render() methods which define the two parts of functionality.
Update() is where logic goes, and Render() is where we draw stuff!
Destroy() is where we cleanup after ourselves and release any memory that we allocate for our demo.

We see that the Render() method takes a pointer to an allegro BITMAP structure as all allegro drawing functions do.
The parameter is named renderTarget because that is exactly what it is. Its the target bitmap that we are to render to.

Okay..lets finish this class definition already!

Code:
private:
TileEngineDemo(const TileEngineDemo& rhs);
const TileEngineDemo& operator=(const TileEngineDemo& rhs);

void ShadowPrint(BITMAP* target, const char* text, int x, int y, int textColor, int shadowColor);

We don't want to have copies of our demo being made, so we make the copy constructor and the assignment operator private.

Our demo has only one single utility function right now, and you can see that its obviously something having to do with printing text with a shadow!
Its really simple. I'll explain it more when we get to the implementation code.

We have just one more section of the class definition to go over.

Code:
private:

// the array of bitmap pointers that will hold our tiles
BITMAP** tileset_;

// the array of bitmap pointers that will hold the frames of the player
BITMAP** heroFrames_;

// the array that holds the map
// each value is an index into the tileset_ array for rendering the map
int** map_;

// the player controls this NPC
NPCPtr heroNPC_;

}; // end class

Now the fun begins. I lose some people at this point, so PAY ATTENTION! here we go..nice and easy...

We have a pointer to a pointer to allegro bitmap structures.
This says that we are going to dynamically allocate an array of pointers to an allegro bitmap structure.
Which is right. We will have a number of tiles which are pointers to the allegro bitmap structure, and since we have more than one,
we have to have another pointer with which we will allocate using the C++ new operator.

There is another of these dynamic arrays for the images that the player will need as well.

The same concept applies for our map array, however what we will have is a two-dimensional array of type int.

Lastly is the pointer to the NPC structure we defined earlier to hold our player.

Whew! Ready to get into the implementation details? I am. Lets jump in!

Lets run through the methods of the TileEngineDemo class one at a time, starting with the constructor.

Code:
TileEngineDemo::TileEngineDemo() :
tileset_(0),
heroFrames_(0),
map_(0),
heroNPC_(0)
{
}

We simply set all our pointers to zero using the initializer list syntax.

Next up, the destructor just makes a call to the Destroy() method:
Code:
TileEngineDemo::~TileEngineDemo()
{
this->Destroy();
}

Now, the Initialize() method, this one is a little long, so I'm going to break it into pieces.

First just the empty method, that we will fill up little by little.

Code:
bool TileEngineDemo::Initialize()
{
// return success
return true;
}


First things first, we declare a few variables that are going to be used more than once in the method,
and then we need to ensure that there are no memory leaks, and next we will allocate our 2D map array.

Code:
int row = 0;
int column = 0;
int index = 0;

// release any memory currently allocated
this->Destroy();

// allocate the map array
map_ = new int* [MAP_HEIGHT];
for (row = 0; row < MAP_HEIGHT; row++)
{
map_[row] = new int [MAP_WIDTH];
}

Nothing to explain really...I've already gone over the pointer to pointers being an array of pointers, so no surprises there.

Next up we fill in the map data. The numbers represent the different tiles that make up the map.
In later tutorials we will not hard-code map data, we will use files. But for now, we just copy this hard-coded data into our map array.
Code:
// fill in the map array with the hard-coded map data
int demoMapData[MAP_HEIGHT][MAP_WIDTH] =
{
{2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2},
{2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2},
{2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2},
{2,0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,0,0,0,2},
{2,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,2},
{2,0,0,0,1,1,1,1,1,0,0,0,0,0,1,0,0,0,0,2},
{2,0,0,0,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,2},
{2,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,2},
{2,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2},
{2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,2},
{2,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,2},
{2,0,0,0,1,0,0,0,0,1,1,1,1,1,1,0,0,0,0,2},
{2,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,2},
{2,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,2},
{2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}
};

for (row = 0; row < MAP_HEIGHT; row++)
{
for (column = 0; column < MAP_WIDTH; column++)
{
map_[row][column] = demoMapData[row][column];
}
}

Next lets load in the graphics for our demo, first the tiles.

Code:
// load the tileset images
tileset_ = new BITMAP* [TILE_COUNT];
for (index = 0; index < TILE_COUNT; index++)
{
char filePath[0x80];
sprintf(filePath, "data/tile%d.bmp", index);
tileset_[index] = load_bitmap(filePath, 0);
if (!tileset_[index])
{
// log the error
fprintf (stderr, "Error. Cannot locate the file \"%s\"!\n", filePath);

// return failure
return false;
}
}

Nothing difficult here.
Lets load the graphics for the player now.

Code:
// load the player images
heroFrames_ = new BITMAP* [4];

const char* filePaths[] =
{
"data/heronorth.bmp",
"data/heroeast.bmp",
"data/herosouth.bmp",
"data/herowest.bmp"
};

for (index = 0; index < 4; index++)
{
heroFrames_[index] = load_bitmap(filePaths[index], 0);
if (!heroFrames_[index])
{
// log the error
fprintf (stderr, "Error. Cannot locate the file \"%s\"!\n", filePaths[index]);

// return failure
return false;
}
}

Its not complicated. We just create an array of strings that say where the graphics are, and load them in.

Lastly, we create the NPC for our player and setup its position and set the direction that it will face.
Code:
// create the player NPC and position him in the center of the map
heroNPC_ = new NPC;

heroNPC_->positionX_ = MAP_WIDTH / 2;
heroNPC_->positionY_ = MAP_HEIGHT / 2;

// make the player face south when we start
heroNPC_->facingDirection_ = MotionSouth;

Hurray! We are finished with the Initialize() method!
Next we have the Update() method.

Our demo logic is very simple. We just check for the UP/DOWN/LEFT/RIGHT and W/S/A/D keys to move our player on our map.

Code:
void TileEngineDemo::Update()
{
// arrow-key and W,S,A,D motion buttons
if (key[KEY_UP] || key[KEY_W])
{
// make hero face north
heroNPC_->facingDirection_ = MotionNorth;

// if the hero can move north
if (heroNPC_->positionY_ > 0)
{
// move north
heroNPC_->positionY_--;
}
}

if (key[KEY_DOWN] || key[KEY_S])
{
// make hero face south
heroNPC_->facingDirection_ = MotionSouth;

// if the hero can move south
if (heroNPC_->positionY_ < MAP_HEIGHT - 1)
{
// move south
heroNPC_->positionY_++;
}
}

if (key[KEY_LEFT] || key[KEY_A])
{
// make hero face west
heroNPC_->facingDirection_ = MotionWest;

// if the hero can move west
if (heroNPC_->positionX_ > 0)
{
// move west
heroNPC_->positionX_--;
}
}

if (key[KEY_RIGHT] || key[KEY_D])
{
// make hero face east
heroNPC_->facingDirection_ = MotionEast;

// if the hero can move east
if (heroNPC_->positionX_ < MAP_WIDTH - 1)
{
// move east
heroNPC_->positionX_++;
}
}
}

Each line is commented, so no explanation needed right? Right.

Lets see the Render() method now.
Code:
void TileEngineDemo::Render(BITMAP* renderTarget)
{
// this is a very simple map rendering routine
// since our engine does not support scrolling or any
// fancy features, we simply loop through our map array
// and draw the requested tile

int row = 0;
int column = 0;

for (row = 0; row < MAP_HEIGHT; row++)
{
for (column = 0; column < MAP_WIDTH; column++)
{
// get requested tile index number
int requestedTile = map_[row][column];

// calculate drawing position by multiplying the
// map position times the tile size
int tileX = column * tileset_[requestedTile]->w;
int tileY = row * tileset_[requestedTile]->h;

// blit the tile
blit(
tileset_[requestedTile],
renderTarget,
0, 0,
tileX, tileY,
tileset_[requestedTile]->w, tileset_[requestedTile]->h);
}
}

// lets draw our hero while we are at it, since this is such
// a simple example, its not really neccessary to create another
// function for this..we will though in the next article

// we use draw_sprite instead of blit for our sprites because
// our sprites have areas on them that we do not want to draw
// use RGB (255, 0, 255) in your drawing program when creating
// your graphics to create transparent sections that will not
// be drawn by draw_sprite
draw_sprite(
renderTarget,
heroFrames_[heroNPC_->facingDirection_],
heroNPC_->positionX_ * heroFrames_[heroNPC_->facingDirection_]->w,
heroNPC_->positionY_ * heroFrames_[heroNPC_->facingDirection_]->h);

// lets print some debugging information on the screen
// just so you can see how to do it
int textColor = makecol(0, 255, 255);
int shadowColor = makecol(0, 64, 64);

const char* descriptions[] =
{
"GRASS",
"PATH",
"WALL"
};

int tileUnderHeroNPC = map_[heroNPC_->positionY_][heroNPC_->positionX_];

char text[128];
sprintf(text, "X %d, Y %d", heroNPC_->positionX_, heroNPC_->positionY_);

// we are going to display the info next to our hero as he walks around
this->ShadowPrint(
renderTarget,
text,
TILE_SIZE + 3 + heroNPC_->positionX_ * TILE_SIZE,
heroNPC_->positionY_ * TILE_SIZE - 8,
textColor,
shadowColor);

this->ShadowPrint(
renderTarget,
descriptions[tileUnderHeroNPC],
TILE_SIZE + 3 + heroNPC_->positionX_ * TILE_SIZE,
heroNPC_->positionY_ * TILE_SIZE + 8,
textColor,
shadowColor);
}

I commented that pretty well too.. if you cannot understand something, just ask me.
Next up we have the Destroy() method.
Remember we want to release any memory that we allocate to avoid memory leaks.

Code:
void TileEngineDemo::Destroy()
{
// release memory used by the tileset
if (tileset_)
{
for (int index = 0; index < TILE_COUNT; index++)
{
if (tileset_[index])
{
destroy_bitmap(tileset_[index]);
tileset_[index] = 0;
}
}
delete [] tileset_;
}

// release memory used by the hero frames
if (heroFrames_)
{
for (int index = 0; index < 4; index++)
{
if (heroFrames_[index])
{
destroy_bitmap(heroFrames_[index]);
heroFrames_[index] = 0;
}
}
delete [] heroFrames_;
}

// release memory used by the map
if (map_)
{
for (int row = 0; row < MAP_HEIGHT; row++)
{
if (map_[row])
{
delete [] map_[row];
map_[row] = 0;
}
}
delete [] map_;
map_ = 0;
}

// release memory used by the player NPC
if (heroNPC_)
{
delete heroNPC_;
}
}

All that should be VERY clear what is going on.

The last thing..well almost last thing is the ShadowPrint() method.
Code:
void TileEngineDemo::ShadowPrint(BITMAP* target, const char* text, int x, int y, int textColor, int shadowColor)
{
textprintf_ex(target, font,     x - 2, y,     shadowColor, -1, text);
textprintf_ex(target, font,     x + 2, y,     shadowColor, -1, text);
textprintf_ex(target, font,     x,     y - 2, shadowColor, -1, text);
textprintf_ex(target, font,     x,     y + 2, shadowColor, -1, text);
textprintf_ex(target, font, x - 1,     y,     shadowColor, -1, text);
textprintf_ex(target, font, x + 1,     y,     shadowColor, -1, text);
textprintf_ex(target, font,     x,     y - 1, shadowColor, -1, text);
textprintf_ex(target, font,     x,     y + 1, shadowColor, -1, text);
textprintf_ex(target, font,     x,     y,     textColor,   -1, text);
}

We create the effect of a shadow by printing the text over and over in each surrounding position around the original.
Its not complicated. Just think about the coordinates and it should make perfect sense.

Okay...we're done...well almost.
We need to interface our demo with the framework so that we can run it!


The Game class constructor needs to set our demo pointer to zero:
Code:
Game::Game() :
mainScreen_(0),
gameIsRunning_(false),
mouseEnabled_(false),
lmbDown_(false),

demo_(0)
{
}

Right before we "start our engines" in the Initialize() method of the Game class, we need to create our demo instance and intiialize it.
Code:
demo_ = new TileEngineDemo();
if (!demo_->Initialize())
{
// log the error
fprintf(stderr, "Could not Initialize the Tile Engine Demo!\n");

// return failure
return false;
}

In the Destroy() method of the Game class, between the #define and #undef lines, we destroy our demo instance.
Code:
_TMP_DELOBJ(demo_)

Where you see the comment section:
Code:
////////////////////////////////////////////////////////////////////
// update game objects
////////////////////////////////////////////////////////////////////

You should call the Update() method of our demo.
Code:
demo_->Update();

Between BeginScene and EndScene, you need to call the Render() method of our demo.
Code:
// draw objects here
demo_->Render(mainScreen_);

Because the original demo was 640x480 and I wanted to use 800x600 and keep the 640x480 demo screen centered, I made a few adjustments.
I changed the EndScene code like this:
Code:
// blit(mainScreen_, screen, 0, 0, 0, 0, mainScreen_->w, mainScreen_->h);
blit(mainScreen_, screen, 0, 0, 80, 60, mainScreen_->w, mainScreen_->h);

And in the Intialize() method of the Game class, I changed the creation of the mainScreen_ bitmap:
Code:
//mainScreen_ = create_bitmap(PROJECT_GAME_WINDOW_WIDTH, PROJECT_GAME_WINDOW_HEIGHT);
mainScreen_ = create_bitmap(640, 480);

And now, if you followed everything exactly, you should be able to build and run your demo!
Thank you for reading. Let me know if you have any trouble with anything.
Logged

Tags:
Pages: [1]   Go Up
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.21 | SMF © 2015, Simple Machines Valid XHTML 1.0! Valid CSS!