Tuesday, 27 September 2011

Basic Game Tutorial #10 - A Basic Game

Basic Tutorials
Basic Game Tutorial #10 - A Basic Game
A simple game

 Introduction

This tutorial combines everything that we have learnt so far to create a basic rock dodging game. In this tutorial we will make use of reading input, collision detection, animation, playing sounds and SDL_TTF to create the game.
Compile and run tutorial10. Use the arrow keys (not the ones on the numeric pad) to move the ship around the screen. Your score will increase for as long as you don't hit a rock. If you hit a rock then the ship will vanish and your score will stop increasing. The higher your score is, the more rocks that will appear on the screen.

 An indepth look

This is the most complex tutorial so far, but many of the methods used in this tutorial have been covered in previous tutorials so you should be quite familiar with them. We will start, as always, with structs.h:
The animation structure has changed slightly. This is because we cannot store the current frame index or the frame counter in the structure without making all of the rocks spin in synchronization.
typedef struct Animation
{
    int frameCount;
    SDL_Surface **frame;
} Animation;
The current frame index and the counter have been moved to the Entity structure:
typedef struct Entity
{
    int active, speed;
    int x, y, w, h;
    int currentFrame, animID, frameTimer;
    SDL_Surface *sprite;
    void (*action)(void);
    void (*draw)(void);
} Entity;
We will use the Entity structure for both the player and the rocks. The currentFrame is used to store the index of the current image of the animation for this Entity. animID is the index of the animation that this Entity is using and frameTimer is the delay between moving onto the next frame in the animation. The rest of the variables should be familiar as they are used in previous tutorials. The other structures should also be familiar so we will move onto animation.c. The loadAnimation function has changed. We now pass in the index of the ID of the animation we wish to load up and the data file of the animation.
void loadAnimation(int id, char *name)
{
    /* Load up the data file that describes the animation */
    
    int i;
    FILE *fp = fopen(name, "rb");
    char frameName[20];
    
    if (fp == NULL)
    {
        printf("Failed to load animation file %s\n", name);
        
        exit(1);
    }
    
    /* Read the frame count */
    
    fscanf(fp, "%d", &animation[id].frameCount);
    
    /* Allocate space for the animation */
    
    animation[id].frame = (SDL_Surface **)malloc(animation[id].frameCount
    * sizeof(SDL_Surface *));
    
    if (animation[id].frame == NULL)
    {
        printf("Ran out of memory when creating the animation for %s\n", name);
        
        exit(1);
    }
    
    /* Now load up each frame */
    
    for (i=0;i<animation[id].frameCount;i++)
    {
        fscanf(fp, "%s", frameName);
        
        animation[id].frame[i] = loadImage(frameName);
        
        if (animation[id].frame[i] == NULL)
        {
            printf("Failed to load animation frame %s\n", frameName);
            
            exit(1);
        }
    }
}
So instead of using a pointer to the animation, we are using an array of Animations. This will make adding extra animations easy in the future. We have added an extra function to animation.c:
void loadAllAnimations()
{
    loadAnimation(ROCK_ANIMATION, "data/anim/rock.dat");
}
loadAllAnimations simply loads up the rock animation. If there were more animations then this function would load those too. There is also another new function, freeAnimations:
void freeAnimations()
{
    int i;
    
    for (i=0;i<MAX_ANIMATIONS;i++)
    {
        freeAnimation(&animation[i]);
    }
}
This function loops though all the animations and frees them. This function will be called by cleanup in init.c. We will skip over audio.c since it has not changed.
In collisions.c, we have changed the implementation of the function doCollisions
void doCollisions()
{
    int i;

    /* Check each entity against the player, skipping over inactive ones */
    
    if (player.active == 0)
    {
        return;
    }

    for (i=0;i<MAX_ENTITIES;i++)
    {
        if (entity[i].active == 0)
        {
            continue;
        }

        if (collision(entity[i].x, entity[i].y, entity[i].w, entity[i].h,
        player.x, player.y, player.w, player.h) == 1)
        {
            /* If a collision occured, kill the player */
            
            player.active = 0;
            
            playSound(EXPLOSION_SOUND);
        }
    }
}
Since we are only checking against the player, we will return from the function immediately if the player is not active. If the player is active however, we loop through the Entities, skipping over any inactive ones and check for a player - Entity collision. If one has occured then we set the player to inactive and call playSound to play an explosion. We will skip over draw.c since it does not contain any functions or calls that have not been seen in previous tutorials.
entity.c contains a new function for dealing with animation:
void drawAnimatedEntity()
{
    drawImage(animation[self->animID].frame[self->currentFrame], self->x, self->y);
}
drawAnimatedEntity makes use of the self Entity pointer. We use the animID and currentFrame variables of the Entity. font.c, graphics.c and input.c do not have any function calls not seen in previous tutorials so we will not cover them.
As noted earlier, the cleanup function in init.c calls the freeAnimations function. Apart from that it is the same.
In main.c, we load up our required resources, reset the starfield and then enter the main loop:
/* Get the input */

getInput();

/* Update the player's position */

doPlayer();

/* Add a rock */

addRock(game.score);

/* Update the entities */

doEntities();

/* Update the stars */

doStars();

/* Do the collisions */

doCollisions();

/* Draw everything */

draw();

/* Increment the player's score while the player's still alive */

if (player.active == 1)
{
    game.score += 10;
}

/* Sleep briefly to stop sucking up all the CPU time */

delay(frameLimit);

frameLimit = SDL_GetTicks() + 16;
The function addRock will attempt to add a new rock to the playfield. We will look at this function later. We also increment the player's score by 10 each frame. We will use the score to determine the odds of adding another rock to the playfield. We have made a small change to the drawPlayer function in player.c:
void drawPlayer()
{
    /* Draw the image in the player structure */
    
    if (player.active == 1)
    {
        drawImage(player.sprite, player.x, player.y);
    }
}
This means that if the player is not active, i.e. it has been destroyed, then it will not be drawn on the screen. Finally, we will look at rock.c rock.c has two functions:
void addRock(int score)
{
    int i, rockChance;
    
    /* The chance of a rock being added depends upon the player's score */
    
    if (score < 10000)
    {
        rockChance = rand() % 60;
    }
    
    else if (score < 20000)
    {
        rockChance = rand() % 45;
    }
    
    else if (score < 30000)
    {
        rockChance = rand() % 30;
    }
    
    else
    {
        rockChance = rand() % 15;
    }
    
    if (rockChance != 0)
    {
        return;
    }
    
    i = getFreeEntity();
    
    if (i == -1)
    {
        return;
    }
    
    entity[i].x = SCREEN_WIDTH + rand() % 100;
    entity[i].y = rand() % SCREEN_HEIGHT;
    entity[i].action = &moveRock;
    entity[i].speed = 1 + (rand() % 5);
    entity[i].draw = &drawAnimatedEntity;
    entity[i].animID = ROCK_ANIMATION;
    entity[i].currentFrame = 0;
    entity[i].frameTimer = 3;
    entity[i].w = animation[entity[i].animID].frame[entity[i].currentFrame]->w;
    entity[i].h = animation[entity[i].animID].frame[entity[i].currentFrame]->h;
}
We pass in the player's score to addRock. The intention to make the game more difficult by increasing the probability of a rock appearing based upon the player's score. So when the player's score is less than 10,000 points, the chance of a rock appearing is 1 in 60, which means that a new rock will appear once a second. At 20,000 points the odds increase to 1 in 45, then 1 in 30 at 30,0000 and finally 1 in 15. The rand function returns a random positive number which we use the modulus operator on the value to give us a number between 0 and n - 1, where n is the modulus value. If this value is not 0 then we will not create a rock and simply exit the function.
Provided we need to create a rock, we call getFreeEntity as we have in previous tutorials. We then set the x and y coordinates to random values and also set the speed to a random value between 1 and 5. We set the animID to ROCK_ANIMATION which is the rock animation index. Since we have moved the animation frame index and frame counter to the Entity structure, we set them here, using currentFrame and frameTimer respectively. We also set the width and height of the entity to the animation's current frame's width and height. The Entity's action and draw functions are set to moveRock and drawAnimatedEntity. The moveRock function moves the rock from right to left and also updates its animation:
static void moveRock(void)
{
    /* Move the rock from right to left, once it goes off the screen, kill it */
    
    self->x -= self->speed;
    
    if (self->x <= -self->w)
    {
        self->active = 0;
    }
    
    /* Update the animation frame */
    
    self->frameTimer--;
    
    if (self->frameTimer <= 0)
    {
        self->frameTimer = 3;
        self->currentFrame++;
        
        if (self->currentFrame >= animation[self->animID].frameCount)
        {
            self->currentFrame = 0;
        }
    }
}
The rock will move horizontally from right to left based upon its speed. When its x value is less than or equal to its negative width (i.e. it has completely moved off the left hand side of the screen), then it is made inactive. Otherwise we decrement its frameTimer and if that value becomes less than or equal to 0 then we move to the next animation frame and reset the frameTimer. If the currentFrame is greater than or equal to the animation's frameCount we reset the currentFrame to 0. The stars.c file is the same as in the previous tutorial so we will pass over it too.

 Conclusion

Hopefully if you have read all the tutorials then you will see that putting together a very simple game does not take too much effort and that a lot of the functions from earlier tutorials are just reused or extended as needed. If you are comfortable with this tutorial then you could try extending it to allow the player to fire bullets and destroy the rocks. In the next tutorial we will move onto more intermediate topics and create a basic platform game.

 Downloads

Source Code - tutorial10.tar.gz

2 comments:

  1. Good to see old tutorials back online :]

    ReplyDelete
  2. wow!! its really needed for me.. save my time...

    ReplyDelete