GFX with SDL Lesson 6: Space Shooter
Translations of this material:
- into Ukrainian: Графіка в SDL. Урок 6: Космічний шутер. 88% translated in draft. Almost done, let's finish it!
-
Submitted for translation by eReS 25.03.2010
Text
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 http://www.libsdl.org/projects/SDL_mixer/ 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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
We now also include 2 files: SDL_mixer.h and SDL.h. We won't use the wrong <SDL/SDL.h> 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 <SDL/SDL.h> 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 <SDL/...> 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;
