Chinaunix首页 | 论坛 | 博客

  • 博客访问: 294282
  • 博文数量: 57
  • 博客积分: 2014
  • 博客等级: 大尉
  • 技术积分: 605
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-18 14:30
文章存档

2015年(3)

2009年(5)

2008年(32)

2007年(17)

我的朋友

分类: C/C++

2008-05-18 13:40:19

by Marius Andra


Hello and welcome to the 6th Cone3D SDL tutorial. This lesson will be about couple of things: scrolling background, frame rate independent movement, simple collision detection and playing sounds and music with the SDL_mixer library. This time we'll create a really simple 2D horizontal space shooter that you may later extend into a full game, if you wish (and are really bored). We'll use modified code from lesson 3 for the sprites, so if you don't remember anything about lesson 3, check it out first. We'll also use the font routines from lesson 4. Now, since the code for this tutorial will need SDL_mixer, let's learn how to include it your programs before we do anything else.

Using SDL_mixer in Dev-C++

First you must upgrade the SDL that you have in Dev-C++ to version 1.2.4 (see the first lesson). Then you must download the file sdl_mixer-DevCpp-1.2.4.zip and extract into your dev-c++ folder (c:\dev-c++\ on my system). Now open up the project options in your project and add -lSDL_mixer to the end of the "further object files and linker options" list. Also add "c:\Dev-C++\include\SDL" (repcace C:\Dev-C++\ with something else if Dev-C++ isn't installed in C:\Dev-C++ on your system) to the "Extra include directories" field. And now you should be all set.

Using SDL_mixer in Visual C++

To use SDL_mixer in Visual C++ you must first download this zip: SDL_mixer-devel-1.2.4-VC6.zip. Extract the file SDL_mixer.lib (from the SDL_mixer-1.2.4\lib folder in the zip) into your Visual C++ library folder (on my system it's c:\program files\microsoft visual studio\vc98\lib). Also, extract the file SDL_mixer.h (from the SDL_mixer-1.2.4\include folder in the zip) into your Visual C++ include folder (on my system it's c:\program files\microsoft visual studio\vc98\include). Also, know that this version of SDL_mixer only works with SDL version 1.2.4 (or newer). So if you have 1.2.3 or older, you must upgrade (see the first SDL tutorial on how to do that). One more thing that you should know is that starting now your SDL include files should be in the folder "include", not "include\SDL" like they have been up to this point (or they can be at both places). So, copy them into the folder "include" (from "include\SDL") as well to make everything work fine. Now, in Visual C++ go to the project settings (from the menu: project->settings). Click the 'LINK' tab and add 'sdl_mixer.lib' to the end of the long line of the other .lib's (Object/library modules). That should be it.

Using SDL_mixer in Linux

To use SDL_mixer in linux, visit the SDL_mixer homepage at and 1) download the SDL_mixer-x.x.x....rpm and the SDL_mixer-devel-x.x.x-....rpm and install them OR 2) download the source tarball and compile and install it yourself (configure; make; make install;). Both methods work fine.

Back to the lesson...

The only things that have changed in the sprite code from lesson 3 are few lines in CSprite.h. The biggest change is that we now use floats instead of integers to store the sprite's position. And also I added some functions to get the position of the sprite (x and y coordiantes) and it's width and height. That's all.

Now let's start with lesson6.cpp. First we have some lines that include some files. time.h is used for the random number generator and math.h for the cosinus function.
// System includes
#include 
#include 
#include 
#include 
We now also include 2 files: SDL_mixer.h and SDL.h. We won't use the wrong way of including the libraries, but we simply include them like "SDL_mixer.h" and tell the compiler (non-vc++ compilers) where to look for extra libraries. Altough including might seem easier at first, the method we'll use now is better. Plus I got some error when including SDL_mixer.h using the old method...
// The SDL include files
#include "SDL_mixer.h"
#include "SDL.h"
We must also include CSprite.h and CSpriteBase.h if we want to use the sprite routines and font.h if we want to use the font ones.
// Sprite classes and font
#include "CSprite.h"
#include "CSpriteBase.h"
#include "font.h"
As you might have seen from the screenshot above, we'll have some bullets and/or some enemies on the screen. To prevent the screen for being over-crowded we must limit the number of bullets/enemies that can be on the screen at one time. We set this limit at 10. We'll store the number of bullets and enemies as prefenifed constants.
// We set a limit on the number of bullets and enemies that
// can be on the screen at a time.
#define BULLETS 10
#define ENEMIES 10
Now we have some spooky variables that will be used with the frame-rate independent movement. We first have 2 variables: td and td2 (td = time difference). They are used to count how much time one frame lasted. We store the length of one frame in the variable dt (delta time) and if the game isn't paused then we increase sdlgt (short for SDL_GetTicks()) with dt. More on these 4 variables later (when we start using them...).
int td=0,td2=0;      // Used when checking the time difference
                     // (how much time has passed since last frame)
float dt=0;          // The time that has elapsed since
                     // the previous frame
float sdlgt=0;       // Stores the time that has passed
We now have couple of SDL_Surfaces. screen holds the entire screen that's diplayed, back holds the large image that we scroll in the background, logo stores the logo at the top of the screen, smallship is the image that we use when we show how many lives are left and gameoverimg is the crappy image that you see when you die.
SDL_Surface *screen, *back, *logo, *smallship, *gameoverimg;
Next come 2 fonts - a white font and the same font, but in yellow.
SDLFont *font,*yellowfont;// White and yellow fonts
Now come the sprites. shipbase stores all the ship graphics and ship is the ship itself. bulletbase stores the bullet's graphic. The array of CSprites, bullets, stores all the individual bullets that you can shoot out of your ship. The array bdraw tells us whether a specific bullet is currently on the screen (1) or not (0).
CSpriteBase shipbase;     // Stores the images of the ship
CSprite ship;             // Stores the ship

CSpriteBase bulletbase;   // The Bullet sprite's base
CSprite bullets[BULLETS]; // Individual bullets
int bdraw[BULLETS];       // Shows which elements of the bullets
                          // array are in use
As with the bullets, enemybase stores the graphics of the enemy sprites and the array enemies stores all the individual enemies. edraw tells us whether an enemy is on the screen or not, elife tells us how much life the enemy has left (the amount is defined randomly when the enemy is created) and einfo tells us whether the enemy is blue (and moving up and down, value of einfo is >0 then) or purple (not moving up and down, value of einfo is 0 at that case).
CSpriteBase enemybase;    // The enemy sprite's base
CSprite enemies[ENEMIES]; // Individual enemies
int edraw[ENEMIES];       // Marks active enemies
int espeed[ENEMIES];      // Speeds of each enemy
int elife[ENEMIES];       // Life of each enemy
int einfo[ENEMIES];       // The type of enemy - bouncing or not
paused is 1 when the game is paused and 0 if it isn't. The same is with gameover that tells us whether the game is over or not.
int paused=0;             // Is the game paused or not
int gameover=0;           // Is the game over?
Scroll is a simple float that tells us how much of the background image has been scrolled. elast remembers when the last enemy came out. We use it to make a new enemy come out after 750 milliseconds (3/4 seconds). It prevents enemies from popping up too quickly. dtime is similar to elast, but it remembers when you were last brought back to life. We use it to make you immortal for 3 seconds after you die (the blinking ship effect). score and lives should be obvious - score stores your score and lives the number of lives you have left. If lives goes below zero, then you die.
float scroll=0;           // How much has the screen scrolled
Uint32 elast;             // Remembers when the last enemy came out
Uint32 dtime;             // Remembers when you were resurrected
int score=0,lives=5;      // The score and the lives
The next 3 variables should be new to you. Mix_Music is the music datatype from SDL_mixer and Mix_Chunk's are simple sound files that we can play when we wish.
Mix_Music *music;         // This will store our music
Mix_Chunk *shot;          // This will store our shot sound
Mix_Chunk *explode;       // This will store our "explosion" sound
And that's the end of the global variables. The first function on our list is called Sprite_Collide. It takes references to 2 CSprite objects as parameters and returns 1 if they are colliding or 0 if they aren't. We do the collision detection with reduced rectangles that are 80% of the width and height of the sprites. This basically means that if 2 sprites touch at their outer borders then no collision will occur. We do collision detection with reduced rectangles instead of full rectangles because our sprites aren't fully rectangular (there's empty space at the corners of the sprites). More information on all sorts of sprite collision detections can be found here: http://www.gamedev.net/reference/articles/article735.asp. There's an ascii image there that may make it clearer why we use reduced rectangles instead of full rectangles when checking for collision.

Although this method of collision detection works fine, it may not work on older machines where everything is very very slow. Imagine a bullet in front of an enemy at one frame and the same bullet at the other side of the enemy at the next frame. This is so because with frame-rate independent movement we move stuff depending on the time that has passed and not on the number of frames that have passed. So it IS possible for an object to move THROUGH another object after one frame and thus causing no collision although there actually should have been one...
short int Sprite_Collide(CSprite &object1, CSprite &object2)
{
We store the coordinates of our reduced rectangles in the variables left1, right1, top1 and bottom1 for one sprite and left2, right2, top2 and bottom2 for the second sprite. Then we make them equal the corresponding values from our sprites
  // We store the coordinates of our reduced rectangles here
  double left1, left2;
  double right1, right2;
  double top1, top2;
  double bottom1, bottom2;

  left1 =   object1.getx()+object1.getw()*0.1;
  left2 =   object2.getx()+object2.getw()*0.1;
  top1 =    object1.gety()+object1.geth()*0.1;
  top2 =    object2.gety()+object2.geth()*0.1;

  right1 =  object1.getx()+object1.getw()*0.9;
  right2 =  object2.getx()+object2.getw()*0.9;
  bottom1 = object1.gety()+object1.geth()*0.9;
  bottom2 = object2.gety()+object2.geth()*0.9;
And finally we do some "magical" comparing and return 1 if the sprites do in fact collide or 0 if they don't. NOTE: you could perfectly replace "bottom1", "top2", "top1", etc with "object1.gety()+object1.geth()*0.9", etc on the next 4 lines. I didn't do it this time because I wanted the code to look clearer... You should do something like this in your own code though.
  if (bottom1 < top2) return 0;
  if (top1 > bottom2) return 0;
  if (right1 < left2) return 0;
  if (left1 > right2) return 0;

  return 1;
};
Our next 3 functions are common in all the lessons so I won't get into them much this time. ImageLoad loads an image and converts it to the display format for faster blitting and the two DrawIMG's blit an image onto the screen.
// Load in an image and convert it to the display format
// for faster blitting.
SDL_Surface * ImageLoad(char *file)
{
  SDL_Surface *temp1, *temp2;
  temp1 = SDL_LoadBMP(file);
  temp2 = SDL_DisplayFormat(temp1);
  SDL_FreeSurface(temp1);
  return temp2;
}

// Blit an image on the screen surface
void DrawIMG(SDL_Surface *img, int x, int y)
{
  SDL_Rect dest;
  dest.x = x;
  dest.y = y;
  SDL_BlitSurface(img, NULL, screen, &dest);
}

// Blit a portion of an image on the screen surface
void DrawIMG(SDL_Surface *img, int x, int y,
                        int w, int h, int x2, int y2)
{
  SDL_Rect dest;
  dest.x = x;
  dest.y = y;
  SDL_Rect dest2;
  dest2.x = x2;
  dest2.y = y2;
  dest2.w = w;
  dest2.h = h;
  SDL_BlitSurface(img, &dest2, screen, &dest);
}
Now we have the function DrawScroll that draw the background on the screen. It's actually quite simple. The first thing that we check is whether the background has scrolled more than it's own width - whether we see the end of one scrolling background on the screen or not. If this is the case then we must draw 2 images on the screen as the background - the start of one scrolling background and the end of an other. We draw 3909-scroll pixels of the first part of the background at position 0,60 and 640-(3909-scroll) pixels of the second part of the background at the spot where the first background blit ended. If the background hasn't scrolled enough so that we could see the end of one background and the start of an other then draw only one image as the background.
// This function draws the scrolling background
void DrawScroll()
{
  // We first check, if the background has scrolled more than it's
  // own width. And if so,then we must draw 2 images on the screen:
  // the end of one scrolling background and the start of another.
  if(scroll>3909-640)
  {
    DrawIMG(back, 0,60,(int)(3909-scroll),400,(int)scroll,0);
    DrawIMG(back, (int)(3909-scroll-1),60,
                           (int)(640-(3909-scroll)),400,0,0);
  // If not then we draw only one background
  } else {
    DrawIMG(back, 0,60,640,400,(int)scroll,0);
  }
}
The next function, addBullet(), adds a bullet onto the screen at the position where the ship currently is. It is called when we press the space key on the keyboard. In the function we loop through all the possible places where to store the new bullet. If we find an empty spot then we add the bullet there.
// Add a bullet onto the screen
void addBullet()
{
  // We loop through all the spots for bullets
  for(int i=0;iaddEnemy() is similar to addBullet(). It's called every 3/4'th of a second. We first loop through all the spots for enemies. If we find a place where to put a new enemy then we first randomly make the enemy either blue or purple. The blue enemy's image is in frame 1 in the sprite animation so we mark that. All blue enemies move up and down on the screen. We also give the blue enemy some random data to prevent all blue enemies from following the same path on the screen. If we randomly made the enemy purple instead, then mark him purple (frame 0 in the sprite animation) and set einfo to 0 so that the whole world knows that this fella is purple.
// Add an enemy onto the screen
void addEnemy()
{
  // We loop through all the spots for enemies
  for(int i=0;iFor safety we stop the enemy from animating preventing it from accidentaly blinking blue/purple all the time. We also give the enemy a random y coordinate to make all enemies start from different places. There are 2 more random things that we need to set - the enemies speed and it's life points. After we do all that we mark the spot saying to draw this enemy.
      // we stop the animation on the enemy sprite for safety
      // (so that it wouldn's start blinking blue
      // and purple all the time)
      enemies[i].stopAnim();
      // We give the enemy a random y coordinate so that each enemy
      // pops up at a different location...
      enemies[i].set(640,60+rand()%350);
      // We also give the enemy random speed and life values
      espeed[i]=rand()%4+1;
      elife[i]=rand()%3+1;

      // fill in the spot
      edraw[i]=1;
      return;
    }
  }
}
Now we are nearing the DrawScene() function. We first clear the screen with black. Actually we don't need to clear the entire screen cause the scrolling background already Clears most of it for us - we would only need to clear the score and lives since the logo and credits don't change every frame, but those improvements are for you to implement... Anyway, after we clear the screen, we draw the scrolling background and then the logo at position 0,0.
// Draw the scene
void DrawScene()
{
  // We first fill the entire screen with black
  SDL_FillRect(screen,0,0);
  // Draw the scroll
  DrawScroll();
  // Now we draw some logo on the screen
  DrawIMG(logo, 0, 0);
Now, if we are still alive then draw little icons to the top of the screen representing the number of lives that we have left. The code should be quite understandable.
  // If we are alive
  if(lives>0)
  {
    // Then draw a small ship for each life that we got
    for(int i=0;iAfter all that we draw some information: the score and the credits. We draw the word "Score:" in white and the score after it in yellow. We also draw the some info in white and the names after it in yellow. The method I used here, lots of spaces, is actually quite bad. In a real program/game you would calculate where to draw each string, or something. Anyway, don't use the method I'm using...
  // Draw the score
  drawString(screen,font,5,37,"Score:");
  drawString(screen,yellowfont,50,37,"%d",score);
  // and credits
  drawString(screen,font,5,460,
    "Code by              Sprites by\
                           Music by");
  drawString(screen,yellowfont,63,460,
    "Marius                   Dann / Jotaroh\
                  seablue");
  drawString(screen,yellowfont,465,460,
                  "http://cone3d.gamedev.net");
Now we loop through all the bullets and enemies and if they are drawable, then draw them.
  // Loop thorough all the spots for bullets
  for(int i=0;iAfter that we draw our ship and if the game should be over, then we also draw an ugly message saying so. And at the end of this function, we flip the screen.
  // Draw the main ship
  ship.draw();
  // If the game is over, post a little message
  if(gameover)
  {
    DrawIMG(gameoverimg, 192, 200);
  }
  // Filp the screen..
  SDL_Flip(screen);
}
And finally, our main function. We start by initalizing the random number generator and creating a pointer to an array where we would store the state of our keyboard keys...
// The main function...
int main(int argc, char *argv[])
{
  // We initalize the random number generator
  srand(time(NULL));
  // We store the state of keys here
  Uint8* keys;
Now, since we're using audio in this program, we must also pass the parameter SDL_INIT_AUDIO to SDL_Init(..) to make everything work fine. In the real life we would actually check if the audio got initalized fine (see lesson 1) so that if it didn't, we could better fulfill the requirements of the folks without sound cards. And then we call a little function that tells our program to call SDL_Quit before exiting no matter what happends...
  // Initalize the SDL Video, Audio and Joystick subsystems.
  if(SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0)
  {
    printf("Unable to init SDL: %s\n", SDL_GetError());
    exit(1);
  }
  atexit(SDL_Quit);
Here's a new part - initalizing the audio stream. We do that with the command Mix_OpenAudio(int frequency, Uint16 format, int channels, int chunksize); We want our frequency to be CD audio rate - 44100Hz (44.1KHz). Most games use 22050 though because it requires less CPU on older computers. We want our chunksize to be 2048. The less it is the more hooks will be called, but sound may skip. The larget it is, sound effects may lag behind the action more. You just have to find a happy medium. Maybe 512 or 1024 will be better on your system. So, if the sound is really weird on your computer, adjust the frequency and the chunksize. In a real game you should give the user an option choose the values, if he likes, though... If channels is 2, we'll get 16bit (stereo) audio. If it's 1 then we'll get 8bit (mono) audio. AUDIO_S16SYS means that we get "signed 16-bit samples, in system byte order", whatever that means :).

Check out the SDL_mixer documentation at for more detailed info on Mix_OpenAudio(). If the page is down by any chance, paste the url (except the #SEC9 part) into google and check out the cached version...
  if(Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 2048) < 0)
  {
    printf("Warning: Couldn't set 44100 Hz 16-bit audio\n\
- Reason: %s\n", SDL_GetError());
  }
Initalizing video should be familiar to you already since we have done that in every lesson so far.
  // Now we get ourself a 640x480x32 fullscreen surface...
  screen=SDL_SetVideoMode(640,480,32,SDL_SWSURFACE|SDL_FULLSCREEN);
  if ( screen == NULL )
  {
    printf("Unable to set 640x480 video: %s\n", SDL_GetError());
    exit(1);
  }
Next we load in the music and sound effects. This is done with the Mix_LoadMUS() for the music and Mix_LoadWAV() for the sounds. Altough these functions might be new to you, they are really easy to grasp. But remember now: when you load in music and/or sounds then always remember to free them before exiting.
  // Load in the music
  music = Mix_LoadMUS("data/sound/(sblu)moon6.xm");
  // And sound effects
  shot = Mix_LoadWAV("data/sound/shot.wav");
  explode = Mix_LoadWAV("data/sound/explode.wav");
In the next part of the code we load in all the graphics. We also make the image that we display when the game is over transparent at the pixels where there would normally be the color green.
  // We also load in all the graphics...
  back = ImageLoad("data/repeat.bmp");
  logo = ImageLoad("data/logo.bmp");
  smallship = ImageLoad("data/smallship.bmp");
  gameoverimg = SDL_LoadBMP("data/gameover.bmp");
  // And make the game over image transparent
  SDL_SetColorKey(gameoverimg, SDL_SRCCOLORKEY,
    SDL_MapRGB(gameoverimg->format, 0, 255, 0));
Now we initalize the graphics for the sprites and then make all the CSprites in the bullets array look like bullets and the ones in the enemies array look like enemies.
  // Now we initalize the bases for the sprites.
  // eg: load in their graphics
  shipbase.init("data/ship");
  bulletbase.init("data/bullet");
  enemybase.init("data/enemy");

  // We make all the bullets and the enemies use these graphics
  for(int i=0;iWe must also initalize the ship. For that we make it look like a ship (assign the shipbase to it), set it's position and make it blink.
  // We also give the ship the correct spritebase
  ship.init(&shipbase,screen);
  // We set it's positon at 50,220
  ship.set(50,220);
  // And make it blink
  ship.setSpeed(1);
A few things remaining before the game loop - we hide the cursor and load in the fonts. We actually load in the same font twice, but make it white in one case and yellow in the other. After that we start the main loop.
  // We also hide the cursor
  SDL_ShowCursor(0);

  // Now we load in the fonts - a white one and a yellow one
  font = initFont("data/font");
  yellowfont = initFont("data/font",1,1,0);

  // Loop while done equals true
  int done=0;
  while(done == 0)
  {
Now comes a block of code which we use to get how much time has passed between 2 frames. We make td2 equal the current SDL tick count. At this point, td will hold the SDL tick count that we had at the start of the previous frame. So td2-td (the current time - the previous time) will equal the time that has passed since the previous frame. We'll make the variable dt equal that amount multiplied with 0.1. This gives us a nice thingy: if we move something dt units in some direction every frame, then after 1 second it would have moved 100 units. If we move something 2.5*dt pixels up every frame, then after 1 second it would have moved 250 pixels. After that we make td equal td2 - the tick count at the start of this frame. We do that because we want something so divide from in the next frame.
    td2=SDL_GetTicks();
    dt=((float)(td2-td))*0.1;
    td=td2;
If the game isn't paused, we'll increase the variable sdlgt with the number of milliseconds (dt*10) that have passed since the previous frame. From this moment on, we will not use SDL_GetTicks(); in our program anymore, but the variable sdlgt instead. This is becaused SDL_GetTicks(); returns the amount of time the program has been run, but sdlgt will contain the amount of milliseconds the game has been played. If we would pause the game, we would also stop sdlgt from increasing. So, in conclusion we can use sdlgt to do time based stuff in the game without it realizing that we have paused the game at some point, etc...
    if(!paused)
    {
      sdlgt+=dt*10;
    }
Now comes some common code: the event checking loop. We first check if we should quit or if ESCAPE has been pressed. If so then we make done equal 1 making the loop breake before it's next run.
    // Check if we have some interesting events...
    SDL_Event event;
    while(SDL_PollEvent(&event))
    {
      // If we must quit, then do so
      if(event.type == SDL_QUIT) { done = 1; }

      // Has a key been pressed down?
      if(event.type == SDL_KEYDOWN)
      {
        // If it was escape then quit
        if(event.key.keysym.sym == SDLK_ESCAPE)
        {
          done = 1;
        }
If space has been hit and the game isn't paused nor over then we first add a bullet (remember, space makes the player shoot stuff).
        // If it was space and the game isn't paused nor over
        if(event.key.keysym.sym == SDLK_SPACE && 
	                         !paused && !gameover)
        {
          // Then add a bullet
          addBullet();
After that we play a shot sound in the 0th mixer channel (the first parameter to the function) using Mix_PlayChannel. By default there are 8 mixer channels, but you can allocate more using Mix_AllocateChannels(16); (That specific example would allocate 16 channels.) Check the SDL_mixer docs for more info (located at ). In each channel only one sound can play at a time. If you start playing some other sound in a channel where some sound is playing at the moment, the first sound will be stopped and the new one will start playing. Since we don't want all the bullets to echo, we play them all in channel 0. The second parameter to the function tells us which sound chunk we want to play. The last 0 that we give to the function tells it that we don't want to loop the sound. The bigger the number, the more the sound will loop. If we would give -1 as the number of times we would want the sound to loop, it would actually loop (unsigned int)(-1) or 4294967295 times. That could be considered almost unlimited though.
          // And play a corresponding sound
          Mix_PlayChannel(0,shot,0);
        }
If somebody presses the P key then pause or resume the game.
        if(event.key.keysym.sym == SDLK_p)
        {
          paused=!paused;
        }
If the game is over and ENTER is pressed then reset everything (the bullets, the enemies, the ship's position, score, lives, etc - everything).
        // If the user pressed ENTER and the game is over then
        if(event.key.keysym.sym == SDLK_RETURN && gameover)
        {
          // Reset all the bullets
          for(int i=0;iNow we check which keys are being hold down. And if the game isn't paused nor over then
    // Get the state of the keyboard keys
    keys = SDL_GetKeyState(NULL);

    // If the game isn't paused nor over
    if(!paused && !gameover)
    {
move the ship in that direction of the key(s) that is(are) being hold down 2*dt pixels. We also prevent the ship from going outside the "playfield".
      // If the keys UP, DOWN, LEFT and RIGHT have been pressed
      if(keys[SDLK_UP])
      {
        // Move the ship up
        ship.yadd((int)(-2*dt));
        // Prevent the ship from going outside the screen
        if(ship.gety()<60) ship.yset(60);
      }
      if(keys[SDLK_DOWN])
      {
        // Move the ship down
        ship.yadd((int)(2*dt));
        // Prevent the ship from going outside the screen
        if(ship.gety()>410) ship.yset(410);
      }
      if(keys[SDLK_LEFT])
      {
        // Move the ship left
        ship.xadd((int)(-2*dt));
        // Prevent the ship from going outside the screen
        if(ship.getx()<0) ship.xset(0);
      }
      if(keys[SDLK_RIGHT])
      {
        // Move the ship right
        ship.xadd((int)(2*dt));
        // Prevent the ship from going outside the screen
        if(ship.getx()>570) ship.xset(570);
      }
    }
Now we check if we aren't playing music at the moment. And if so then play the music once. If the music has run out then with the next run of this loop we start playing it again. Also note that SDL_mixer has a special channel for music that's independent of all these channels you use for sound effects.
    // If we aren't playing music at the moment
    if(!Mix_PlayingMusic())
    {
      // then start playing it.
      Mix_PlayMusic(music, 0);
    }
And here's our main game code. We only run all of this if the game isn't paused and it isn't over.
    // If the game isn't paused nor over then
    if(!paused && !gameover)
    {
We loop through all the bullets and if they are visible, move them right 3.5*dt units. If they have moved outside the screen, we'll get rid of them.
      // Loop through all the bullets
      for(int i=0;i640)
          {
            // hide them
            bdraw[i]=0;
          }
        }
      }
We have a similar loop with each enemy. If the enemy is visible then
      // Loop through all the enemies
      for(int i=0;iWe first move the enemy left (thier speed+1)*0.5*dt units. And if the enemy is blue (moving up/down) we must also adjust it's y coordinate. We'll do this with the cosinus function. We'll make their y coordinate equal the cosinus of their x coordinate + some random stuff (to prevent everyone from following the same path). The cosinus function doesn't like degrees though so we have to multiply the (x coordinate+random stuff) value with pi/180 (0.0174532925) to make it work. The cosinus function will always return some value between -1 and +1. That's too small for us. So we multiply it with 100 to get a value from -100 to +100. We must also add 230 to it to center it onto the screen. So, in conclusion this function makes the y coordinate of blue enemies equal from -100+230 to +100+230 (130 to 330). And since the x coordinate changes all the time, the y coordinate will now create smooth wawey movement.
          // Move them left (their speed+1)*0.5*dt units
          enemies[i].xadd(-((float)espeed[i]+1)*dt*0.5);
          // If the sprites are colored blue then
          if(einfo[i]>0)
          {
            enemies[i].yset(
             (cos((enemies[i].getx()+einfo[i])*0.0174532925)
	                                          *100)+230);
          }
If the enemy has moved out of the screen (is no longer visible) then we must get rid of it. Since we have nothing more to check with this enemy, we'll move on to the next enemy (the next run of this enemy-checking for loop).
	  // If the enemy moves out of the screen
          if(enemies[i].getx()<-50)
          {
            // Get rid of it
            edraw[i]=0;
            // and then move on to the next enemy
            continue;
          }
We also check for enemy and bullet collisions here. For that we loop through all the bullets. If the bullet is visible and it collides with the enemy
	  // Check for bullet collisions
          for(int j=0;jthen we decrease the enemies life. If that resulted in the death of the enemy, we increase the player's score by 10, get rid of the enemy and the bullet and play some spooky explosion sound in the mixer channel 1 (not 0 like with the bullets). We play it in a different channel from the bullets so that we could hear bullets and explosions at the same time. Also know that if you play something in the channel -1, SDL_mixer automatically finds a free channel where to play it. Anyway, if the enemy died, then we stop doing stuff (lots of checks) with this enemy and move on to the next one.
                // If so then decrease the enemies life
                elife[i]--;
                // If the enemy is dead
                if(elife[i]<1)
                {
                  // Then don't draw it anymore
                  edraw[i]=0;
                  // Increase the players score by 10
                  score+=10;
                  // play some spooky sound
                  Mix_PlayChannel(1,explode,0);
                  // Get rid of the bullet
                  bdraw[j]=0;
                  // move on to the next enemy
                  continue;
If the enemy didn't completely die then we increase the player's score by one and get rid of the bullet.
                } else {
                  // If we only injured the enemy then increase
                  // the score by one
                  score+=1;
                  // Get rid of the bullet
                  bdraw[j]=0;
                }
              }
            }
          }
Now we check for enemy/player collisions. If the ship isn't blinking (it has the protective immortality shield that it gets for 3 second after it dies) AND the enemy collided with it (the ship) then decrease the number of lives that we have remaining. If we die then mark the game as over and stop checking the remaining enemies.
          // Check for player collisions
          // If the ship is vulnearable to attack - is not blinking
          // And the enemy collided with the ship then
          if(ship.getSpeed()==0 && Sprite_Collide(ship,enemies[i]))
          {
            // decrease our lives
            lives--;
            // if we are dead then
            if(lives<0)
            {
              // mark the game as over
              gameover=1;
              // stop moving/checking the enemies,
              // eg: get out of this loop
              break;
            }
If we didn't die completely then get rid of the enemy, reset the ship's position, make in blink and mark the time when we died so that we would know how long to make the ship immortal. We'll also play the explosion sound in the mixer channel 1.
            // Get rid of the enemy
            edraw[i]=0;
            // If we didn't completely die then reset our position
            ship.set(10,245);
            // Make the ship blink (animate)
            ship.setSpeed(1);
            ship.startAnim();
            // Mark the time when we died
            dtime=(Uint32)sdlgt;
            // Play the spooky explosion sound
            Mix_PlayChannel(1,explode,0);
          }
        }
      }
Now, if if we were resurrected more than 3 seconds ago (sdlgt-dtime is larger than 3000) then we get rid of the ships protective shield (stop it from blinking). We must also show the frame zero in the ship's animation so that we would have a ship on the screen no matter what frame of the animation was visible before.
      // If 3 seconds have passed since we died/were resurrected
      if(sdlgt-dtime>3000)
      {
        // Stop the ships blinking animation
        ship.stopAnim();
        ship.setSpeed(0);
        // Show the frame of animation with the ship on it
        ship.setFrame(0);
      }
We also check if the last enemy came out more than 750 milliseconds ago. If so, we'll release a new enemy and mark the time when it came out.
      // If 3/4 seconds have passed since the last enemy came out
      if(sdlgt-elast>750)
      {
        // Add a new enemy
        addEnemy();
        // Mark the time when this enemy came out
        elast=(Uint32)sdlgt;
      }
Now we scroll the background. We increase the varable (float) scroll by dt. If scroll is bigger than the width of the scrolling background (3909) then decrease the variable scroll with the width of the scrolling background.
      // Scroll the screen by dt units
      scroll += dt;
      // If we have scrolled more than the background image then
      // decrease the scrolled amount by 
      // the width of the background image
      if(scroll>=3909) scroll-=3909;
    }
Now all that's left for us to do is to draw everything. After that we pause for 1 millisecond to allow the system to catch some breath. If the scrolling is really choppy on your system, try increasing the value a bit or commenting out the line.
    // Draw the scene
    DrawScene();

    // If the scrolling is really choppy on your system,
    // try commenting this next line. It allows the system to
    // catch some breath after each frame.
    SDL_Delay(1);
  }
When we quit the main loop, we'll fade out all the channels and the music within 1 second using Mix_FadeOutChannel(-1,1000) (-1 means all channels) and Mix_FadeOutMusic(1000). After that we pause for one second so that we would actually hear the music fade.
  // Fade out all the audio channels in 1 second
  Mix_FadeOutChannel(-1, 1000);
  // and the music in 1 second
  Mix_FadeOutMusic(1000);
  // wait one second
  SDL_Delay(1000);
And the final thing that we do: we free the music and sound chunks. And return zero after that.
  // Free the sounds
  Mix_FreeMusic(music);
  Mix_FreeChunk(shot);
  Mix_FreeChunk(explode);

  return 0;
}
That's it. I hope you had more much fun reading this tutorial than I had writing it :).

You can download the code for the lesson here: lesson6.zip (~3MB), here: lesson6.tar.gz (~3MB) or here: lesson6.tar.bz2 (~2MB). Pick your favourite :). Know that compiling the code on Visual C++ will give you some errors, but they aren't hard to fix...

Oh, and the credits for the sprites go to Dann / Jotaroh and for the music to seablue. Check out his site at for more of his tunes.
阅读(2087) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~