You are on page 1of 11

GFX with SDL

Lesson 5: Clickomania
by Marius Andra

Hi all! In this tutorial we will build a simple game in SDL called Clickomania.
Clickomania is actually a really simple game to build. Our version of the game
will consist of a 10x14 grid of balls. When you click (with the mouse) on a ball,
that's connected to 2 or more balls, all the connected balls of the same color as
the ball you clicked disappear. And the balls that were above the disappeared
balls will simply fall down. If you manage to clear a row of balls, then the other
rows move in to the left from the right. These 4 images illustrate the basic
aspects of the game:

Before After

After clicking one of the purple balls in the yellow region, all of them disappear
and the balls that were above them fall down.

1
After
Before

When we get rid of the balls in the yellow region the rest move from right to left.
Note that the rows of balls move from right to left, not the individual balls
themselves. Now let's get to coding the game.

We first have some #includes and some #defines.

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>

#include <SDL/SDL.h>

Since we'll also use the SDL Font routines (tutorial 4), we'll have to include
font.h

#include "font.h"

Now come some variables. The first two contain the dimensions of the playfield.
They will be set to some values later.

int rows;
int cols;

screen should be obvious. balls contain the images of the 10 possible ball
types. font1 and font2 shouldn't be too much to grasp as well.

SDL_Surface *screen; // The screen surface


SDL_Surface *balls[10];// The ball images
SDLFont *font1; // 2 fonts
SDLFont *font2;

playf contains the grid of all the balls. We will initialize it later.

char *playf; // The playfield itself

2
scrwidth and scrheight contain the default width and height of the screen.
Later we'll "parse" the command line arguments checking, if the user wants to
run this program at some other screen resolution. Depending on the screen
resolution, the grid of balls might and might not fully cover the screen when
drawn from the screen coordinates (0,0). centx and centy tell us from where to
start drawing the array of balls. They will be calculated later.

// The default width and height of the screen


int scrwidth=640, scrheight=480;
// Used to center the grid nicely onto the screen
int centx=0,centy=0;

The next variable, bls, tells us how many differently colored balls are there on
the screen. bla is used with the mouse. More specifically it's used to see
whether a mouse button has been clicked. score should be obvious.

int bls=4; // Number of differently colored balls

int bla=0; // Used with the mouse...

int score=0; // The current score

The function DrawIMG should be clear to all of you by now.

// This function draws a surface onto the screen


void DrawIMG(SDL_Surface *img, int x, int y)
{
SDL_Rect dest;
dest.x = x;
dest.y = y;
SDL_BlitSurface(img, NULL, screen, &dest);
}

Now the function swap takes references to 2 char variables (the playfield is an
array of type char, btw). It then simply swaps them.

// This swaps two type char variables


void swap(char &r1, char &r2)
{
char temp=r1;
r1=r2;
r2=temp;
}

The function grid takes the coordinates of the playfield as parameters and
returns the value (color of the ball) that's on that specific coordinate. It's used
almost everywhere in the rest of the program. The function returns a reference to
the char (instead of an instance of the char like we usually do) at the coordinate
so that we could manipulate it (swap it with some other, assign a value to it, etc)
outside the function.

3
// Returns the color of a ball in the playfield.
inline char &grid(int a, int b)
{
return playf[a*cols+b];
}

The next function, collapse, makes the level collapse. It's called right after we
remove a bunch of balls from the grid.

// This function makes balls fall down and rows move left.
void collapse()
{

First we check if we can make any balls fall down. For that we loop through all
the columns in the grid. With each column we loop through all the rows. If we find
some ball with the value -1, then we remember it's row and move the others onto
it. After we move one ball onto the previously marked spot, we decrease the
location of the marked spot so that the next ball we move goes onto the top of
the spot where we moved our previous ball. After we do this all the balls will be
fallen down.
// Make balls fall down
int to=-1;
for(int j=0;j<cols;j++)
{
to=-1;
for(int i=rows-1;i>=0;i--)
{
if(to==-1 && grid(i,j)==-1) {to=i;}
else if(to!=-1 && grid(i,j)!=-1)
{swap(grid(i,j),grid(to,j)); to--;}
}
}

Now we want to move the balls from right to left. For that we loop through all the
columns. If one of those columns should have the bottom cell in the row empty (-
1), then we know that the entire column is empty. And if it is empty, we mark the
location and move all the other columns onto it. After we move one column, we
increase the marked location counter and move our columns there.

// Move rows to the left


to=-1;
{
for(int j=0;j<cols;j++)
{
if(to==-1 && grid(rows-1,j)==-1)
{
to=j;
} else if(to!=-1 && grid(rows-1,j)!=-1) {
for(int i=0;i<rows;i++)
{
grid(i,to) = grid(i,j);
}

4
to++;
}
}

After we have done all that we blank out all the leftover columns.

{
if(to!=-1)
{
for(int j=to;j<cols;j++)
{
for(int i=0;i<rows;i++)
{
grid(i,j)=-1;
}
}
}
}
}
}

Now the next function recursively clears out a bunch of balls. We use the flood fill
algorithm in it. How it works is really simple. We first make the current location
(passed as the parameters to this function) in the grid equal -1. We then check all
the sides of the current location (top, bottom, left, right). If they equal the color
the current location was before, then we call this same function on them. The
function runs until all of the connected balls get changed to the value -1. This
function also keeps track on the number of balls cleared and returns the cleared
number with the return statement at the end. The number of balls cleared is
important with the scoring system. You should also know that after we run this
function on a specific ball, we call the collapse function (look above for it).

// This is a recursive function that clears a bunch of balls.


// It uses the flood fill algorithm
int pick(int i, int j, int a)
{
int sum=1;
grid(i,j)=-1;
if(i != 0 && grid(i-1,j)==a) sum+=pick(i-1,j,a);
if(j != 0 && grid(i,j-1)==a) sum+=pick(i,j-1,a);
if(i<rows-1 && grid(i+1,j) == a) sum+=pick(i+1,j,a);
if(j<cols-1 && grid(i,j+1) == a) sum+=pick(i,j+1,a);
return sum;
}

The next function, bunch, is similar to the previous function. But it only returns 1
when the ball at the given coordinates is connected to any other ball of the same
color, or it returns 0, when there aren't any connected balls.

// This function checks whether one ball is connected to an other


int bunch(int i,int j)
{
int a = grid(i,j);

5
if(i!=0 && grid(i-1,j) == a) return 1;
if(j!=0 && grid(i,j-1) == a) return 1;
if(i<rows-1 && grid(i+1,j) == a) return 1;
if(j<cols-1 && grid(i,j+1) == a) return 1;
return 0;
}

The next function, DrawScene is really simple. We first hide the mouse cursor
(comment out that line to see why we do that). After that we fill the screen with
black.

// Draw the screen


void DrawScene()
{
SDL_ShowCursor(0); // Hide the mouse cursor
// Clear the entire screen with black
SDL_FillRect(screen,NULL,0x000000);

Now we loop through the grid and draw all the balls on the screen.
// Draw the balls
for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
{
DrawIMG(balls[grid(i,j)],j*45+centx,i*45+centy);
}
}

Then we draw some information, ...

// Draw some info and the score


drawString(screen,font1,1,scrheight-16,
"SPACE restarts, gray + and - change the nr \
of balls (%d). Score: %d",bls,score);

// Draw the webspace url to the bottom-right of the screen


drawString(screen,font2,scrwidth-stringWidth(font2,
"http://cone3d.gamedev.net"),scrheight-16,
"http://cone3d.gamedev.net");

... flip the screen and show the mouse cursor again.
SDL_Flip(screen); // Flip the buffers
SDL_ShowCursor(1); // Show the cursor again
}

The next function, newgame, puts some random balls on the grid of balls over the
old ones and also sets the score to zero. After that it calls DrawScene to update
the changes.

// Resets the entire grid by adding random balls to it


void newgame()
{

6
// Reset the score
score=0;

// Fill the grid with random balls


for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
{
grid(i,j) = rand()%bls;
}
}

// Redraw the screen


DrawScene();
}

And now the final function, main. We first initialize the random number
generator. After that we check if the first command line argument given to this
program equals 640, 800, 1024, 1152, 1280 or 1600. If so, we change the screen
dimensions to the requested ones.

// This is our main function


int main(int argc, char *argv[])
{
srand(time(NULL)); // We initialize the random number generator

// Depending on the command line arguments given to this program,


// we change the screen resolution.
if(argc>1 && strcmp(argv[1],"640")==0)
{scrwidth=640;scrheight=480;}
if(argc>1 && strcmp(argv[1],"800")==0)
{scrwidth=800;scrheight=600;}
if(argc>1 && strcmp(argv[1],"1024")==0)
{scrwidth=1024;scrheight=768;}
if(argc>1 && strcmp(argv[1],"1152")==0)
{scrwidth=1152;scrheight=864;}
if(argc>1 && strcmp(argv[1],"1280")==0)
{scrwidth=1280;scrheight=960;}
if(argc>1 && strcmp(argv[1],"1600")==0)
{scrwidth=1600;scrheight=1200;}

We then do the usual initialization stuff.


// Initialize SDL
if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 )
{
printf("Unable to init SDL: %s\n", SDL_GetError());
exit(1);
}
atexit(SDL_Quit);

Now we set up a random picture of a ball as the icon of the program. So that
(almost) every time you run the game, it's icon will be different.

// We load in a random ball image as the icon for this program

7
char tempstring[100];
sprintf(tempstring,"data/balls/%d.bmp",rand()%9+1);
SDL_WM_SetIcon(SDL_LoadBMP(tempstring),NULL);

Now we initialize the screen surface


// Initialize the video mode
screen=SDL_SetVideoMode(scrwidth,scrheight,32,
SDL_SWSURFACE|SDL_HWPALETTE|SDL_FULLSCREEN);
if ( screen == NULL )
{
printf("Unable to set %dx%d video: %s\n", scrwidth, scrheight,
SDL_GetError());
exit(1);
}

Load in 2 fonts and 10 ball surfaces.

// Load in the fonts


font1 = initFont("data/font1");
font2 = initFont("data/font2",1,1,0);

// Load in all the balls


for(int a=1;a<10;a++)
{
char temp[100];
sprintf(temp,"data/balls/%d.bmp",a);
balls[a-1]=SDL_LoadBMP(temp);
}

Now we see how many balls can the current screen resolution hold. We simply
divide the screen width and height (minus 16 because of the info text) by 45 (the
width/height of one ball).

// Calculate how much balls fit on the screen with the current
// screen resolution
rows=(scrheight-16)/45;
cols=(scrwidth)/45;

And now we calculate how much must we move the balls in order to get the
playfield centered on the screen. First we check how much pixels does the grid
occupy. We then subtract that value from the width/height of the screen. We then
get the amount of free space left over by the playfield. We then divide that value
by 2 and get the amount we must move the playfield on the x and y coordinates.

// Calculate how much must the balls be moved


// so that they would be centered.
centx = (scrwidth-cols*45)/2;
centy = (scrheight-16-rows*45)/2;

We now allocate memory to store the playfield and then make it contain random
balls.

8
// Allocate space for the playfield
playf = new char[rows*cols];

// Reset the score and generate the playfield


newgame();

Now comes the game loop.

// Loop a bit
int done=0;
while(done == 0)
{
SDL_Event event;

We check for all sorts of events.


while ( SDL_PollEvent(&event) )
{
// If someone closes the prorgam, then quit
if ( event.type == SDL_QUIT ) { done = 1; }

If someone presses ESC, we quit. SPACE restarts the game. Gray + and gray -
increase/decrease the number of differently colored balls.

if ( event.type == SDL_KEYDOWN )
{
// If someone presses ESC, then quit
if ( event.key.keysym.sym == SDLK_ESCAPE ) { done = 1; }
// Space restarts the game
if ( event.key.keysym.sym == SDLK_SPACE ) { newgame(); }
// The gray keypad + increases the nr. of different balls
if ( event.key.keysym.sym == SDLK_KP_PLUS )
{ if(bls<9) {bls++;newgame();} }
// The gray keypad - decreases the nr. of different balls
if ( event.key.keysym.sym == SDLK_KP_MINUS )
{ if(bls>3) {bls--;newgame(); } }
}
}

And now we must check if the user clicked a ball. We first get the coordinates of
the mouse. After that we check over which ball the mouse currently is. Because
the playfield is centered on the screen, the ball coordinates may be negative. If
they are, we restart the loop hoping that on the next run they won't be. If they
aren't then we move on.

int x,y; // Used to hold the mouse coordinates


int x2,y2; // Used to hold the ball coordinates
float x3,y3; // Used to temporarily hold the ball coordinates
SDL_GetMouseState(&x, &y); // Get the mouse coords
x3=(((float)(x-centx))/45); // Get the ball coords
y3=(((float)(y-centy))/45);
if(x3<0 || y3<0) continue; // If negative, restart the loop
else {x2=(int)x3;y2=(int)y3;} // else store them as ints

9
If at the moment the mouse button is released, then we make bla equal one.

// If the mouse button is released, make bla equal 1


if(!SDL_GetMouseState(NULL, NULL)&SDL_BUTTON(1))
{
bla=1;
}
If the mouse button was released the next frame and is held down at the moment
then we make bla equal zero. That way we can only reach this if statement the
next time the player clicks the mouse button again.

// If the mouse button was released the previous frame and


// is now held down, then ...
if(SDL_GetMouseState(NULL, NULL)&SDL_BUTTON(1) && bla==1)
{
// Make bla equal zero so that we could get here only
// if we release the mouse button and then click it again.
bla=0;

We now check if the clicked ball coordinates are inside the allowed area and if
so, we check if coordinates don't point to an empty ball. And if they don't we
check if the ball has other balls connected to it.

// Investigate the clicked ball


if(x2>=0 && y2>=0 && x2<cols && y2<rows && grid(y2,x2) != -1
&& bunch(y2,x2))
{

If so we then remove all the connected balls, increase the score by the [number
of balls removed-2] squared, make the playfield collapse and redraw the screen.

// If it really is clickable then get rid of the balls


int a=pick(y2,x2,grid(y2,x2));
// Increase the score by the (removed balls-2)^2
score+=(a-2)*(a-2);
// Make the balls fall down
collapse();
// And update the screen
DrawScene();
}
}
}

And now, when the loop has finished, we clean up a bit.

// Let's clean up...


freeFont(font1);
freeFont(font2);

return 0;
}

10
Here's the code.

© Marius Andra 2001

11

You might also like