Chapter 6

6. Basic Game Design

6.1 The basic sprite system

From the previous exercise, we have a program where we could move two helicopter sprites around the screen, and outside it. There are two improvements we want to do at this point; first, we want the helicopter to stay on screen; second, we want the helicopter's ceiling fan to look like it's moving. To make it easier to accomplish similar things for the other objects we are soon going to create, we will implement some more features into the base class, and make the chopper class use these. We will also make a background class. The code now looks like this:
#include <allegro.h>
#include <stl.h>
#include "tutorial.h"

#define MIN_Y 8
#define MAX_Y 180

DATAFILE*data;
BITMAP*backdrop,*framebuf;

class sprite {
protected:
 fix X,Y;
 RLE_SPRITE*image;
public:
 sprite(fix _X,fix _Y) { X=_X; Y=_Y; image=NULL; }
 sprite(fix _X,fix _Y,RLE_SPRITE*img) { X=_X; Y=_Y; image=img; }
 virtual ~sprite() {}
 virtual void draw(BITMAP*dest) {
  draw_rle_sprite(dest,image,X,Y);
 }
 virtual void clippos() { if (Y+image->h>MAX_Y) Y=MAX_Y-image->h; }
 virtual void move(fix DX,fix DY) { X+=DX; Y+=DY; clippos(); }
 virtual void place(fix NX,fix NY) { X=NX; Y=NY; clippos(); }
 virtual int outside(fix _X,fix _Y) { return (_X<-image->w)||(_X>=SCREEN_W); }
 virtual bool animate() { return FALSE; }
};

typedef list<sprite*> sprite_list;

sprite_list sprites;

class background : public sprite {
public:
 background() : sprite(0,MIN_Y) {}
 virtual void draw(BITMAP*dest) {
  blit(backdrop,dest,0,0,X,Y,backdrop->w,backdrop->h);
 }
};

class chopper : public sprite {
protected:
 int frame;
public:
 chopper(fix _X,fix _Y)
  : sprite(_X,_Y,(RLE_SPRITE*)data[TUT_CHOPPER].dat) { frame=0; }
 virtual void clippos() {
  if (X<0) X=0;
  if (X+image->w>SCREEN_W) X=SCREEN_W-image->w;
  if (Y<MIN_Y) Y=MIN_Y;
  sprite::clippos();
 }
 virtual bool animate();
};

bool chopper::animate()
{
 fix DX=0,DY=0;

 if (key[KEY_LEFT]||joy_left) --DX;
 if (key[KEY_RIGHT]||joy_right) ++DX;
 if (key[KEY_UP]||joy_up) --DY;
 if (key[KEY_DOWN]||joy_down) ++DY;

 move(DX,DY);

 if (frame) { image=(RLE_SPRITE*)data[TUT_CHOPPER1].dat; frame=0; }
       else { image=(RLE_SPRITE*)data[TUT_CHOPPER2].dat; frame=1; }

 return FALSE;
}

class chopper2 : public chopper {
public:
 chopper2(fix _X,fix _Y) : chopper(_X,_Y) {}
 virtual void move(fix DX,fix DY) { X-=DX; Y-=DY; clippos(); }
};

int main()
{
 allegro_init();
 install_keyboard();
 initialise_joystick();

 data=load_datafile("tutorial.dat");

 set_gfx_mode(GFX_VGA,320,200,0,0);

 set_palette((RGB*)data[TUT_GAMEPAL].dat);

 // create 320x192 backdrop
 backdrop=create_bitmap(320,192);
 for (int Y=0; Y<128; Y++) hline(backdrop,0,Y,319, (Y/2)+128);
 for (int Y=128; Y<192; Y++) hline(backdrop,0,Y,319, ((Y-128)/2)+192);

 // create 320x200 double buffer
 framebuf=create_bitmap(320,200);
 clear(framebuf);

 background Back;
 chopper Hero(50,100);
 chopper2 AnotherHero(250,50);
 sprites.push_back(&Back);
 sprites.push_back(&Hero);
 sprites.push_back(&AnotherHero);

 while (!key[KEY_ESC]) {
  // draw sprites
  {
   sprite_list::const_iterator spr=sprites.begin();
   while (spr!=sprites.end()) {
    (*spr)->draw(framebuf);
    spr++;
   }
  }
  // display frame
  vsync();
  blit(framebuf,screen,0,0,0,0,320,200);
  // animate sprites
  poll_joystick();
  {
   sprite_list::const_iterator spr=sprites.begin();
   while (spr!=sprites.end()) {
    (*spr)->animate();
    spr++;
   }
  }
 }
 return 0;
}
In the course of writing any application, and games are no exception, there are often times when parts of the code has to be rethought and rewritten. That is essentially what we have done here. We have moved the RLE sprite drawing code and some coordinate manipulation code into the base class. The base class' default clippos() makes sure no object falls below ground level. Its default outside() checks if an object is totally outside the screen in the horizontal direction, since this is the direction we want the game to scroll. chopper() overrides clippos() to keep the helicopter totally on-screen. (Note that it takes care of only three of the edges, and calls upon the inherited clippos() to take care of the fourth.) chopper2 overrides the move() method to reverse the direction moved, thus avoiding having to replace the entire animate() method. We have also created the background class that overrides the default draw() method to draw the backdrop, and put an instance of this class into the sprite list, thus simplifying the game loop even further.

6.2 Dynamic Sprites

You might also have noticed that we have made the animate() method return a bool. This was done as a preparation for the dynamic sprites we are now going to create. If a sprite is created dynamically, it should also be deleted when it is no longer needed, to avoid running out of memory. To signal this, we will let the animate() method return TRUE when the sprite is no longer needed. So, let's try it by writing a class that will handle the lethal stuff we are going to throw at our poor enemies, and that signals that it should be deleted when it hits the ground.

Since it's a good idea to have defined the velocity at which our bombs will fall from the helicopter, we will add this somewhere at the top:

#define BOMB_LAUNCH 1
#define GRAVITY 0.1
Then we add the weaponry class itself. Add this before chopper::animate():
class projectile : public sprite {
 fix DX,DY;
 int force;
public:
 projectile(fix _X,fix _Y,fix _DX,fix _DY,RLE_SPRITE*img,int power)
  : sprite(_X,_Y,img) { DX=_DX; DY=_DY; force=power; }
 virtual bool animate() {
  move(DX,DY); DY+=GRAVITY;
  return Y+image->h>=MAX_Y;
 }
};
To launch bombs whenever Enter is pressed, change chopper::animate() to:
bool chopper::animate()
{
 fix DX=0,DY=0;

 if (key[KEY_LEFT]||joy_left) --DX;
 if (key[KEY_RIGHT]||joy_right) ++DX;
 if (key[KEY_UP]||joy_up) --DY;
 if (key[KEY_DOWN]||joy_down) ++DY;

 move(DX,DY);

 if (key[KEY_ENTER]||joy_b1) {
  sprites.push_back(new projectile(X+32,Y+14,
   BOMB_LAUNCH,0,(RLE_SPRITE*)data[TUT_BOMB].dat,5));
 }

 if (frame) { image=(RLE_SPRITE*)data[TUT_CHOPPER1].dat; frame=0; }
       else { image=(RLE_SPRITE*)data[TUT_CHOPPER2].dat; frame=1; }

 return FALSE;
}
Finally, to delete the sprites that are no longer needed, we need to change the animation code in the main game loop to:
  // animate sprites
  poll_joystick();
  {
   sprite_list::iterator spr=sprites.begin();
   while (spr!=sprites.end()) {
    sprite*itm=*spr;
    if (itm->animate()) {
     sprites.erase(spr++);
     delete itm;
    } else spr++;
   }
  }
This will save the current sprite, then call animate(). If it returns true, it will remove the element from the list, move the iterator to the next element, and then delete the sprite itself; otherwise, it will move the iterator as before.

With this, the player is now able to pour out bombs at high volume, which is definitely going to be of much value against his enemies, once he gets any.

6.3 Moving Bomb Launch

A somewhat unrealistic thing about the way we have launched these bombs, is their launch velocity when the helicopter is moving, which is currently absolute, and thus exactly the same as if the helicopter stood still. In our game, we want some more realism than that (even if the extreme payload of explosives the player has at his disposal is totally unrealistic), so we will make the initial bomb velocity relative to the current helicopter velocity. This also makes it much more fun to try to launch bombs as if they were projectiles by jerking the helicopter forward and upward just as the bomb is launching.

First, to accurately calculate how much the helicopter has moved (its current velocity), we will let the base class keep track of what the last position was, by its animate() method.

class sprite {
protected:
 fix X,Y,LX,LY;
 RLE_SPRITE*image;
public:
 sprite(fix _X,fix _Y) { LX=X=_X; LY=Y=_Y; image=NULL; }
 sprite(fix _X,fix _Y,RLE_SPRITE*img) { LX=X=_X; LY=Y=_Y; image=img; }
 virtual ~sprite() {}
 virtual void draw(BITMAP*dest) {
  draw_rle_sprite(dest,image,X,Y);
 }
 virtual void clippos() { if (Y+image->h>MAX_Y) Y=MAX_Y-image->h; }
 virtual void place(fix NX,fix NY) { X=NX; Y=NY; clippos(); }
 virtual void move(fix DX,fix DY) { X+=DX; Y+=DY; clippos(); }
 virtual int outside(fix _X,fix _Y) { return (_X<-image->w)||(_X>=SCREEN_W); }
 virtual bool animate() { LX=X; LY=Y; return FALSE; }
};
Of course, for this to work, the animate() method for the derived classes has to call this. The chopper class' animate() method, after adding the difference between the current and previous coordinates as the velocity to add to the bomb launch velocity, and doubling the speed of the helicopter itself, is now:
bool chopper::animate()
{
 sprite::animate();

 fix DX=0,DY=0;

 if (key[KEY_LEFT]||joy_left) DX-=2;
 if (key[KEY_RIGHT]||joy_right) DX+=2;
 if (key[KEY_UP]||joy_up) DY-=2;
 if (key[KEY_DOWN]||joy_down) DY+=2;

 move(DX,DY);

 if (key[KEY_ENTER]||joy_b1||(mouse_b&1)) {
  sprites.push_back(new projectile(X+32,Y+14,
   X-LX+BOMB_LAUNCH,Y-LY,(RLE_SPRITE*)data[TUT_BOMB].dat,5));
 }

 if (frame) { image=(RLE_SPRITE*)data[TUT_CHOPPER1].dat; frame=0; }
       else { image=(RLE_SPRITE*)data[TUT_CHOPPER2].dat; frame=1; }

 return FALSE;
}

6.4 Mouse Control

Another thing that I personally find makes the program much more fun to mess with, is to control the helicopter with the mouse. This enables it to move around the screen and launch bombs with ludicrous velocities, but still, why not have all the fun we can get? Unfortunately, this means we will have to dispense with the second helicopter. The full code is now:
#include <allegro.h>
#include <stl.h>
#include "tutorial.h"

#define MIN_Y 8
#define MAX_Y 180
#define BOMB_LAUNCH 1
#define GRAVITY 0.1

DATAFILE*data;
BITMAP*backdrop,*framebuf;
bool usemouse=FALSE;

class sprite {
protected:
 fix X,Y,LX,LY;
 RLE_SPRITE*image;
public:
 sprite(fix _X,fix _Y) { LX=X=_X; LY=Y=_Y; image=NULL; }
 sprite(fix _X,fix _Y,RLE_SPRITE*img) { LX=X=_X; LY=Y=_Y; image=img; }
 virtual ~sprite() {}
 virtual void draw(BITMAP*dest) {
  draw_rle_sprite(dest,image,X,Y);
 }
 virtual void clippos() { if (Y+image->h>MAX_Y) Y=MAX_Y-image->h; }
 virtual void place(fix NX,fix NY) { X=NX; Y=NY; clippos(); }
 virtual void move(fix DX,fix DY) { X+=DX; Y+=DY; clippos(); }
 virtual int outside(fix _X,fix _Y) { return (_X<-image->w)||(_X>=SCREEN_W); }
 virtual bool animate() { LX=X; LY=Y; return FALSE; }
};

typedef list<sprite*> sprite_list;

sprite_list sprites;

class background : public sprite {
public:
 background() : sprite(0,MIN_Y) {}
 virtual void draw(BITMAP*dest) {
  blit(backdrop,dest,0,0,X,Y,backdrop->w,backdrop->h);
 }
};

class chopper : public sprite {
protected:
 int frame;
public:
 chopper(fix _X,fix _Y)
  : sprite(_X,_Y,(RLE_SPRITE*)data[TUT_CHOPPER].dat) {
  frame=0;
  position_mouse(X,Y);
 }
 virtual void clippos() {
  if (X<0) X=0;
  if (X+image->w>SCREEN_W) X=SCREEN_W-image->w;
  if (Y<MIN_Y) Y=MIN_Y;
  sprite::clippos();
 }
 virtual bool animate();
};

class projectile : public sprite {
 fix DX,DY;
 int force;
public:
 projectile(fix _X,fix _Y,fix _DX,fix _DY,RLE_SPRITE*img,int power)
  : sprite(_X,_Y,img) { DX=_DX; DY=_DY; force=power; }
 virtual bool animate() {
  move(DX,DY); DY+=GRAVITY;
  return Y+image->h>=MAX_Y;
 }
};

bool chopper::animate()
{
 sprite::animate();
 if (usemouse) place(mouse_x,mouse_y);

 fix DX=0,DY=0;

 if (key[KEY_LEFT]||joy_left) DX-=2;
 if (key[KEY_RIGHT]||joy_right) DX+=2;
 if (key[KEY_UP]||joy_up) DY-=2;
 if (key[KEY_DOWN]||joy_down) DY+=2;

 move(DX,DY);

 if (usemouse) position_mouse(X,Y);

 if (key[KEY_ENTER]||joy_b1||(mouse_b&1)) {
  sprites.push_back(new projectile(X+32,Y+14,
   X-LX+BOMB_LAUNCH,Y-LY,(RLE_SPRITE*)data[TUT_BOMB].dat,5));
 }

 if (frame) { image=(RLE_SPRITE*)data[TUT_CHOPPER1].dat; frame=0; }
       else { image=(RLE_SPRITE*)data[TUT_CHOPPER2].dat; frame=1; }

 return FALSE;
}

int main()
{
 allegro_init();
 install_keyboard();
 // comment out the next line if you don't want mouse
 usemouse=(install_mouse()!=-1);
 initialise_joystick();

 data=load_datafile("tutorial.dat");

 set_gfx_mode(GFX_VGA,320,200,0,0);

 set_palette((RGB*)data[TUT_GAMEPAL].dat);

 // create 320x192 backdrop
 backdrop=create_bitmap(320,192);
 for (int Y=0; Y<128; Y++) hline(backdrop,0,Y,319, (Y/2)+128);
 for (int Y=128; Y<192; Y++) hline(backdrop,0,Y,319, ((Y-128)/2)+192);

 // create 320x200 double buffer
 framebuf=create_bitmap(320,200);
 clear(framebuf);

 background Back;
 chopper Hero(50,100);
 sprites.push_back(&Back);
 sprites.push_back(&Hero);

 while (!key[KEY_ESC]) {
  // draw sprites
  {
   sprite_list::const_iterator spr=sprites.begin();
   while (spr!=sprites.end()) {
    (*spr)->draw(framebuf);
    spr++;
   }
  }
  // display frame
  vsync();
  blit(framebuf,screen,0,0,0,0,320,200);
  // animate sprites
  poll_joystick();
  {
   sprite_list::iterator spr=sprites.begin();
   while (spr!=sprites.end()) {
    sprite*itm=*spr;
    if (itm->animate()) {
     sprites.erase(spr++);
     delete itm;
    } else spr++;
   }
  }
 }
 return 0;
}
It is reasonably clear what we've done; install_mouse() returns -1 if a mouse is not installed; mouse_x and mouse_y contains the current mouse coordinates; position_mouse sets new coordinates (in case the keyboard or joystick was used to move the helicopter). You can now hold down the mouse button and move the mouse around to discover how the law of physics relate to gameplay (and jerky mouse motion).

For more exciting gameplay, proceed to the next chapter