Home > Game Development, Programming, Tutorial > SFML 2 – Tutorial – Break Out!

SFML 2 – Tutorial – Break Out!

July 19, 2012


Tutorial 03 – Basic Game Structure – Break Out!


In this tutorial, we will cover refresh rate, a basic game structure, a custom game loop and the game of Break Out!.


How to get the refresh rate in frame per second ?


With the Clock class, we can query the time spent between two redraws with getElapsedTime().

One million divided by the number of micro seconds since the last redraw is the number of frames that can be drawn in a second at the current rate. For more precision, we can wait several frames and get an average of the elasped time per frame.

Time time=clock.getElapsedTime();
float fFps=1000000/time.asMicroseconds();
std::stringstream s;
s<<fFps;
fps.setString(s.str());
clock.restart();

With that in place, we can see that the previous pong example ran at more than a thousand frames per second. This is wasteful in resources, I wouldn’t want to make the GPU overheat just for pong. Especially since my monitor doesn’t refresh the screen at more than 60 times a second.

So we’ll limit our framerate with window.setFramerateLimit(unsigned int). It makes window.display sleep a bit.¬†window.setVerticalSyncEnabled(bool) can be used too but it can be overridden by global graphic parameters.

window.setFramerateLimit(60);
window.setVerticalSyncEnabled(true);


How to use Clock to get a smoother animation ?


If there is a long time spent between two redisplays, it can cause jerkiness. Objects that should have a constant speed appear to slow down with the frame rate. We compensate by scaling all object speeds by the time spent since last redisplay.

ball.move(ballSpeed.x*time.asMilliseconds(),  ballSpeed.y*time.asMilliseconds());


Basic Game Structure:


We’ll use a Game class with only an init and an exec public functions, greatly simplifying our main to :

int main()
{
	Game game;
	if (!game.init())
		return EXIT_FAILURE;
	return game.exec();
}

The game class inits SFML, sets up the game objects, it can update various parts of the game and enters a custom game loop when exec is called. It processes the events and displays the game scene.


Custom Game loop:


We’ve seen that calling setFramerate will make window.display() to sleep if it is called more often than the frame rate. How can we take that time back for our game engine ? There are cases where we wish to use as much computing power as possible. Even in pong or in our next example, break out, collision detection is easier if the speed vector of the ball is small (if it is too big we can even miss a collision). Using a faster game loop will allow a finer grained simulation.

	int exec()
	{
		Clock renderClock, updateClock;
		while (window.isOpen())
		{
			time=renderClock.getElapsedTime();
			float fFps=1000000/time.asMicroseconds();
			std::stringstream s;
			s<<fFps;
			fps.setString(s.str());
			renderClock.restart();

			const Int64 frameTime = 1000000/FRAMES_PER_SECOND;
			Clock c;
			Time t=c.getElapsedTime();
			Int64 nextFrameTime=t.asMicroseconds()+frameTime;

			int loops = 0;
			while( t.asMicroseconds() < nextFrameTime && loops < MAX_FRAMESKIP)
			{
				processEvents();
				updateTime=updateClock.restart().asMilliseconds();
				update();
				t=c.getElapsedTime();
				loops++;
			}

			display();
		}

Now the game loop will call update up to ten times faster than display. We use updateTime to scale the game object speed accordingly.


How to add sounds ?


Sound files of various formats are loaded into a SoundBuffer with:

bool 	SoundBuffer::loadFromFile (const std::string &filename)

Then we’ll declare a Sound from any available SoundBuffer and play it. We can adjust its pitch and volume with setPitch and setVolume.

In the next example, we simulate the use of the keyboard as a piano:

#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>

using namespace sf;

void playSound(Sound & s, float pitch)
{
	float p=s.getPitch();
	s.setPitch(pitch);
	s.play();
}

int main()
{
    VideoMode videoMode(320,240);
    RenderWindow window(videoMode,"Piano");

    SoundBuffer soundBuffer1;
    if (!soundBuffer1.loadFromFile("blip.wav"))
        return EXIT_FAILURE;
    Sound blip(soundBuffer1);

	SoundBuffer soundBuffer2;
	if (!soundBuffer2.loadFromFile("blam.wav"))
        return EXIT_FAILURE;
    Sound blam(soundBuffer2);

	SoundBuffer soundBuffer3;
	if (!soundBuffer3.loadFromFile("blap.wav"))
        return EXIT_FAILURE;
    Sound blap(soundBuffer3);

	SoundBuffer soundBuffer4;
	if (!soundBuffer4.loadFromFile("blop.wav"))
        return EXIT_FAILURE;
    Sound blop(soundBuffer4);

	Font font;
	if (!font.loadFromFile("tomb.ttf"))
		return EXIT_FAILURE;

	Text info("Use the keys \n A Z E R \n Q S D F\n W X C V\n to play sounds.",font,20);
 	info.setPosition(20,30);
	info.setColor(Color::Blue);

    while (window.isOpen())
    {
		window.clear();
		window.draw(info);
		window.display();

        Event event;
        while (window.pollEvent(event))
        {
            if (event.type == Event::Closed)
                window.close();
			else
			if (event.type == Event::KeyPressed)
			{
				switch(event.key.code)
				{
				  case Keyboard::Q:
					  playSound(blip, 1.0);
					break;
				  case Keyboard::S:
					  playSound(blam, 1.0);
					break;
				  case Keyboard::D:
					  playSound(blap, 1.0);
					break;
				  case Keyboard::F:
					  playSound(blop, 1.0);
					break;

				  case Keyboard::W:
					  playSound(blip, 0.8);
					break;
				  case Keyboard::X:
					  playSound(blam, 0.8);
					break;
				  case Keyboard::C:
					  playSound(blap, 0.8);
					break;
				  case Keyboard::V:
					  playSound(blop, 0.8);
					break;

				  case Keyboard::A:
					  playSound(blip, 1.2);
					break;
				  case Keyboard::Z:
					  playSound(blam, 1.2);
					break;
				  case Keyboard::E:
					  playSound(blap, 1.2);
					break;
				  case Keyboard::R:
					  playSound(blop, 1.2);
					break;

				  case Keyboard::Escape:window.close();
					break;

				}
			}
        }

    }
    return EXIT_SUCCESS;
}


How to improve our rectangle collisions ?


Until now, we have only simulated collisions coming from one side.

We now want a rectangle that reflects the ball according to the side it is coming from.

Supposing the ball can be represented by a point, we use the angle of its speed vector seen from the center of the rectangle to determine which side it is coming from.

For instance, we can now correct the previous game of pong so that players only hit the ball if it is coming from the front side. Collisions coming from the rear can be ignored.

		// ball collides with player1
		if (intersects(ball,player1))
		{
			FloatRect p= player1.getGlobalBounds();
			FloatRect b= ball.getGlobalBounds();

			//let o be the center of p
			Vector2f o=Vector2f(p.left+p.width/2,p.top+p.height/2);
			//om: vector from o to the center of the ball
			Vector2f om=Vector2f(b.left+b.width/2-o.x,b.top+b.height/2-o.y);
			// let's scale om to square dimensions and act as if p is a square
			om.x/=p.width;
			om.y/=p.height;
			// reflect the ball according to the angle of om
			float angle=atan2(om.y,om.x);
			if ( abs(angle) <PI/2) //right - PI/4 is restrictive
			{
				ballSpeed.x= abs(ballSpeed.x);//in case of double contact
				ballSpeed.y= (b.top+b.height/2 - p.top - p.height/2) / 100;
				int u = p.left +  p.width - b.left;
				b.left = p.left +  p.width + u;
				ball.setPosition(b.left,b.top);
				//increase ball speed by 2%
				ballSpeed.x*=1.02f;
				ballSpeed.y*=1.02f;
				blip.play();
			}
		}

In addition with these changes, we will use the new game structure, the new game loop and sounds.

Let’s now review our Pong source, freshly refactored:

#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <math.h>
#include <sstream>

#define PI   3.14159265358979323846

using namespace sf;

bool intersects (const RectangleShape & rect1,const RectangleShape & rect2)
{
    FloatRect r1=rect1.getGlobalBounds();
    FloatRect r2=rect2.getGlobalBounds();
    return r1.intersects (r2);
}

float clamp (const float x, const float a, const float b)
{
    return std::min(std::max(a,x),b);
}

class Game
{
private:
    static const int FRAMES_PER_SECOND = 60;    
    static const int MAX_FRAMESKIP = 10;
    static const int width = 640;
    static const int height= 480;
    static const int borderSize= 12;
    static const int margin = 50;
    static const int moveDistance = 20;
    

    RenderWindow window;
    Font font;

    RectangleShape top;
    RectangleShape left;
    RectangleShape right;
    RectangleShape bottom;    

    RectangleShape ball;

    Vector2f ballSpeed;

    RectangleShape player1;    
    RectangleShape player2;    

    RectangleShape middleLine;

    Text title;
    Text start;
    Text won;
    Text lost;
    Text score;
    Text fps;

    SoundBuffer soundBuffer1;
    Sound blip;
    SoundBuffer soundBuffer2;
    Sound blam;    
    SoundBuffer soundBuffer3;
    Sound blap;
    SoundBuffer soundBuffer4;
    Sound blop;

    Time time;
    Int32 updateTime;

    unsigned int p1Score, p2Score;

    enum states {INTRO, PLAYING, P1WON, P1LOST};
    
    int gameState;
    
public:
    Game()
    {
    }

    bool init()
    {
        VideoMode videoMode(width, height);
        window.create(videoMode,"Pong Structured");
        window.setVerticalSyncEnabled(true);
        window.setFramerateLimit(FRAMES_PER_SECOND);

        if (!font.loadFromFile("tomb.ttf"))
            return false;  

        if (!soundBuffer1.loadFromFile("blip.wav"))
            return false;
        
        if (!soundBuffer2.loadFromFile("blam.wav"))
            return false;
        
        if (!soundBuffer3.loadFromFile("blap.wav"))
            return false;

        if (!soundBuffer4.loadFromFile("blop.wav"))
            return false;

        setup();
        return true;
    }

    int exec()
    {
        Clock renderClock, updateClock;
        while (window.isOpen())
        {
            time=renderClock.getElapsedTime();
            float fFps=1000000/time.asMicroseconds();
            std::stringstream s;
            s<<fFps<<" fps";
            fps.setString(s.str());
            renderClock.restart();
        
            const Int64 frameTime = 1000000/FRAMES_PER_SECOND;
            Clock c;
            Time t=c.getElapsedTime();
            Int64 nextFrameTime=t.asMicroseconds()+frameTime;

            int loops = 0;
            while( t.asMicroseconds() < nextFrameTime && loops < MAX_FRAMESKIP) 
            {
                processEvents();
                updateTime=updateClock.restart().asMilliseconds();
                update();
                t=c.getElapsedTime();
                loops++;
            }        
            
            display();
        }
        
        return EXIT_SUCCESS;
    }

private:

    void processEvents()
    {
        Event event;
        while (window.pollEvent(event))
        {
            if ( (event.type == Event::Closed) ||
            ((event.type == Event::KeyPressed) && (event.key.code==Keyboard::Escape)) )
                window.close();    
            else
            if ((event.type == Event::KeyPressed) && (gameState == INTRO))
                gameState=PLAYING;
        }
    }

    void update()
    {
        if (gameState!=PLAYING)
            return;

        updatePlayer1();
        updatePlayer2();
        checkCollisions();
        updateBall();

        // detect if game is over
        if (p1Score >=11 && p1Score >= p2Score +2)
            gameState=P1WON;
        if (p2Score >=11 && p2Score >= p1Score +2)
            gameState=P1LOST;
    }

    void setup()
    {
        top.setPosition(-2*borderSize, 0);
        top.setSize(Vector2f(width+4*borderSize, borderSize));

        left.setPosition(-borderSize*3, 0);
        left.setSize(Vector2f(borderSize, height));

        right.setPosition(width+2*borderSize, 0);
        right.setSize(Vector2f(borderSize, height));

        bottom.setPosition(-2*borderSize, height-borderSize);
        bottom.setSize(Vector2f(width+4*borderSize, borderSize));
            
        top.setFillColor(Color(100,100,100));
        top.setOutlineColor(Color::Blue);
        top.setOutlineThickness(3);

        left.setFillColor(Color(100,100,100));
        left.setOutlineColor(Color::Blue);
        left.setOutlineThickness(3);

        right.setFillColor(Color(100,100,100));
        right.setOutlineColor(Color::Blue);
        right.setOutlineThickness(3);

        bottom.setFillColor(Color(100,100,100));
        bottom.setOutlineColor(Color::Blue);
        bottom.setOutlineThickness(3);
        
        ball.setPosition(width/2, height/2);
        ball.setSize(Vector2f(20, 20));
        ball.setFillColor(Color::Yellow);
        ball.setOutlineColor(Color::Red);
        ball.setOutlineThickness(2);

        player1.setSize(Vector2f(borderSize, 90));
        player1.setPosition(margin-borderSize, height/2-25);
        player1.setFillColor(Color(0,122,245));
        player1.setOutlineColor(Color::Red);
        player1.setOutlineThickness(3);
        
        player2.setSize(Vector2f(borderSize, 90));
        player2.setPosition(width-margin, height/2-25);
        player2.setFillColor(Color(0,122,245));
        player2.setOutlineColor(Color::Red);
        player2.setOutlineThickness(3);
            
        middleLine.setFillColor(Color(100,100,100,30));
        middleLine.setOutlineColor(Color(0,0,100,30));
        middleLine.setOutlineThickness(2);
        middleLine.setPosition(width/2, 0);
        middleLine.setSize(Vector2f(0, height));

        title.setString("Pong SFML 2");
        title.setFont(font);
        title.setCharacterSize(50);
        title.setPosition(width/2-title.getGlobalBounds().width/2,100);
        title.setColor(Color::Blue); 
    
        start.setString("Press any key to start");
        start.setFont(font);
        start.setCharacterSize(30);
        start.setPosition(width/2-start.getGlobalBounds().width/2,400);
        start.setColor(Color::Red);

        won.setString("You have won this game.\n\n Congratulations !");
        won.setFont(font);
        won.setCharacterSize(20);
        won.setPosition(width/2-won.getGlobalBounds().width/2,height/2-won.getGlobalBounds().height/2);
        won.setColor(Color::Green);

        lost.setString("You have lost this game, \n better luck next time!");
        lost.setFont(font);
        lost.setCharacterSize(20);
        lost.setPosition(width/2-lost.getGlobalBounds().width/2,height/2-lost.getGlobalBounds().height/2);
        lost.setColor(Color::Red);

        score.setString("0   0");
        score.setFont(font);
        score.setCharacterSize(50);
        score.setPosition(width/2-score.getGlobalBounds().width/2,40);
        score.setColor(Color(0,0,100,50));

        fps.setString("0");
        fps.setFont(font);
        fps.setCharacterSize(30);
        fps.setPosition(fps.getGlobalBounds().width/2,40);
        fps.setColor(Color(52,0,100,50));

        
        blip=Sound(soundBuffer1);        
        blam=Sound(soundBuffer2);    
        blap=Sound(soundBuffer3);    
        blop=Sound(soundBuffer4);
        
        resetGame1();
        p1Score=0;
        p2Score=0;
        gameState=INTRO;

    }

    void display()
    {        
        window.clear(Color::White);
        switch(gameState)
        {
            case INTRO:
                window.draw(title);
                window.draw(start);
                break;
            case PLAYING:
                window.draw(middleLine);
                window.draw(left);
                window.draw(right);            
                window.draw(player1);    
                window.draw(player2);    
                window.draw(ball);    
                window.draw(score);    
                window.draw(top);
                window.draw(bottom);        
                window.draw(fps);    
                break;
            case P1WON:
                window.draw(won);
                break;
            case P1LOST:
                window.draw(lost);
                break;
        }
        window.display();
    }

    void updatePlayer1()
    {
        // move player 1 pad
        if (Keyboard::isKeyPressed(Keyboard::Up))
        {
            player1.move(0,-moveDistance*updateTime/50.0);
        }else
        if (Keyboard::isKeyPressed(Keyboard::Down))
        {
            player1.move(0,moveDistance*updateTime/50.0);
        }
    }

    void updatePlayer2()
    {
        // auto move player2 pad
        if (ball.getPosition().y < player2.getPosition().y)
            player2.move(0, -moveDistance*updateTime/40.0);
        else if(ball.getPosition().y+ball.getSize().y > player2.getPosition().y+player2.getSize().y)
            player2.move(0, moveDistance*updateTime/40.0);
    }

    void updateBall()
    {
        ball.move(ballSpeed.x*updateTime,ballSpeed.y*updateTime);
    }

    void checkCollisions()
    {
                        
        // block players pad inside the play area
        if ( intersects(player1,bottom) || intersects(player1,top) )
        {
            FloatRect t=top.getGlobalBounds();
            FloatRect b=bottom.getGlobalBounds();
            Vector2f p=player1.getPosition();
            p.y=clamp(p.y,t.top+t.height+5,b.top-player1.getSize().y-5);
            player1.setPosition(p);
            blap.play();
        }
        if ( intersects(player2,bottom) || intersects(player2,top) )
        {
            FloatRect t=top.getGlobalBounds();
            FloatRect b=bottom.getGlobalBounds();
            Vector2f p=player2.getPosition();
            p.y=clamp(p.y,t.top+t.height+5,b.top-player2.getSize().y-5);
            player2.setPosition(p);
            blap.play();
        }
        // ball collides with top and bottom
        if (intersects(ball,top))
        {
            FloatRect t=top.getGlobalBounds();
            FloatRect b=ball.getGlobalBounds();
            ballSpeed.y*= -1;
            int u = t.top + t.height - b.top;
            ball.move(0,2*u);   
            blop.play();
        }
        if ( intersects(ball,bottom) )
        {
            FloatRect bot= bottom.getGlobalBounds();
            FloatRect b= ball.getGlobalBounds();
            ballSpeed.y*= -1;
            int u = bot.top - b.height - b.top;
            ball.move(0,2*u); 
            blop.play();
        }
        // ball collides with player1 and player2
        if (intersects(ball,player1))
        {
            FloatRect p= player1.getGlobalBounds();
            FloatRect b= ball.getGlobalBounds();

            //let o be the center of p
            Vector2f o=Vector2f(p.left+p.width/2,p.top+p.height/2);
            //om: vector from o to the center of the ball
            Vector2f om=Vector2f(b.left+b.width/2-o.x,b.top+b.height/2-o.y);
            // let's scale om to square dimensions and act as if p is a square
            om.x/=p.width;
            om.y/=p.height;
            // reflect the ball according to the angle of om
            float angle=atan2(om.y,om.x);
            if ( abs(angle) <PI/2) //right - PI/4 is restrictive
            {
                ballSpeed.x= abs(ballSpeed.x);//in case of double contact
                ballSpeed.y= (b.top+b.height/2 - p.top - p.height/2) / 100;
                int u = p.left +  p.width - b.left;
                b.left = p.left +  p.width + u;
                ball.setPosition(b.left,b.top);
                //increase ball speed by 2%
                ballSpeed.x*=1.02f;
                ballSpeed.y*=1.02f;                
                blip.play();
            }
        }
        if ( intersects(ball,player2) )
        {
            FloatRect p=player2.getGlobalBounds();
            FloatRect b=ball.getGlobalBounds();

            //let o be the center of p
            Vector2f o=Vector2f(p.left+p.width/2,p.top+p.height/2);
            //om: vector from o to the center of the ball
            Vector2f om=Vector2f(b.left+b.width/2-o.x,b.top+b.height/2-o.y);
            // let's scale om to square dimensions and act as if p is a square
            om.x/=p.width;
            om.y/=p.height;
            // reflect the ball according to the angle of om
            float angle=atan2(om.y,om.x);
            if ( abs(angle) > PI/2) //left - 3*PI/4 is restrictive
            {
                ballSpeed.x= -abs(ballSpeed.x);//in case of double contact
                ballSpeed.y= (b.top+b.height/2 - p.top - p.height/2) / 100;
                int u = b.left + b.width - p.left;
                b.left = p.left -  b.width - u;
                ball.setPosition(b.left,b.top);
                //increase ball speed by 2%
                ballSpeed.x*=1.02f;
                ballSpeed.y*=1.02f;
                blip.play();
            }            
        }                        
        
        // check for scoring
        if (intersects(ball,left))
        {
            blam.play();
            p2Score++;
            std::stringstream str;
            str << p1Score << "   " << p2Score;
            score.setString(str.str());
            score.setPosition(width/2-score.getGlobalBounds().width/2,40);
            resetGame2();
        }
        if (intersects(ball,right))
        {
            blam.play();
            p1Score++;
            std::stringstream str;
            str << p1Score << "   " << p2Score;
            score.setString(str.str());
            score.setPosition(width/2-score.getGlobalBounds().width/2,40);
            resetGame1();
        }

    }

    void resetGame1()
    {
        FloatRect p=player1.getGlobalBounds();
        FloatRect b=ball.getGlobalBounds();
        ball.setPosition(p.left+p.width+5, height/2);
        ballSpeed.x=0.3f;
        ballSpeed.y=0.3f;
    }

    void resetGame2()
    {
        FloatRect p=player2.getGlobalBounds();
        FloatRect b=ball.getGlobalBounds();
        ball.setPosition(p.left-b.width-5, height/2);
        ballSpeed.x=-0.3f;
        ballSpeed.y=0.3f;
    }
};


int main()
{
    Game game;
    if (!game.init())
        return EXIT_FAILURE;
    return game.exec();
}

Result:

Our next game is break out. It follows logically from pong. It will stress out our new collision technique because bricks must reflect precisely the ball on each side.


Let’s make Break Out


In Break Out, the player controls a pad and uses it to reflect the ball on a grid of bricks. Bricks disappears if the ball hits them twice. The player wins the game if there are no more bricks. The player loses a live if the ball passes through the bottom of the screen. If he loses his 5 lives, he loses the game and must restart at level1.

We will use a a Brick class for single bricks and a Grid class to store all the bricks. Brick inherits from RectangleShape and Grid from FloatRect. The game is quite similar to pong except there is only one player and that the level is somehow dynamic. The configuration of the parameters is also sligthtly different, the pad moves faster but gives less angle to the ball.

T
he conditions of winning and losing the game depend on playerLives and are easy to handle.

The score is increased by 700 pts one the destructions of bricks. It requires a passing of arguments between Grid and Game.

#include <SFML/System.hpp>
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <math.h>
#include <algorithm>
#include <iostream>
#include <sstream>

#define PI   3.14159265358979323846

using namespace sf;

bool intersects (const RectangleShape & rect1,const RectangleShape & rect2)
{
    FloatRect r1=rect1.getGlobalBounds();
    FloatRect r2=rect2.getGlobalBounds();
    return r1.intersects (r2);
}

float clamp (const float x, const float a, const float b)
{
    return std::min(std::max(a,x),b);
}


class Brick : public RectangleShape
{
public:
    unsigned int armor;

    Brick():RectangleShape()
    {
        armor=2;
    }

    Brick(const Vector2f & size):RectangleShape(size)
    {
        armor=2;
    }


    bool collide(RectangleShape & ball, Vector2f & ballSpeed)
    {
        // ball collides brick
        if (intersects(ball,*this))
        {
            FloatRect r= getGlobalBounds();
            FloatRect b= ball.getGlobalBounds();

            //let o be the center of p
            Vector2f o=Vector2f(r.left+r.width/2,r.top+r.height/2);
            //om: vector from o to the center of the ball M
            Vector2f om=Vector2f(b.left+b.width/2-o.x,b.top+b.height/2-o.y);
            // let's scale om to square dimensions and act as if p is a square
            om.x/=r.width;
            om.y/=r.height;
            // reflect the ball according to the angle of om
            float angle=atan2(om.y,om.x);
            if ( angle >= PI/4 && angle <= 3*PI/4) //bottom
            {
                ballSpeed.y = abs(ballSpeed.y);                
                float u = r.top + r.height - b.top ;
                ball.move(0,2*u);
                return true;
            }
            if ( angle <= -PI/4 && angle >= -3*PI/4) //top
            {
                ballSpeed.y = -abs(ballSpeed.y);        
                float u = b.top + b.height - r.top;
                ball.move(0,-2*u);
                return true;
            }
            if ( angle <= -3*PI/4) //left
            {
                ballSpeed.y = -abs(ballSpeed.x);        
                float u = b.left + b.width - r.left;
                ball.move(-2*u,0);
                return true;
            }
            if ( abs(angle) <= PI/4) //right
            {
                ballSpeed.y = abs(ballSpeed.x);            
                float u = r.left + r.width - b.left;
                ball.move(2*u,0);
                return true;
            }            
        }
        return false;
    }


};


class Grid: public FloatRect
{
private:
    unsigned int rows;
    unsigned int columns;
    
    std::vector<Brick> * bricks;

public:
    Grid()
    {
        rows=0;
        columns=0;
        bricks = NULL;
    }
    
    ~Grid()
    {
        if (bricks != NULL)
            delete bricks;
    }

    void setDimensions(unsigned int col, unsigned int row)
    {
        rows=row;
        columns=col;
        if (bricks != NULL)
            delete bricks;

        bricks = new std::vector<Brick> (rows*columns);        
        init();
    }

    Vector2f cellSize()
    {
        return Vector2f(width/columns,height/rows);
    }

    void init()
    {
        for (int i=0;i<rows;i++)
            for (int j=0;j<columns;j++)
            {
                bricks->at(i+j*rows).setSize(cellSize());
                bricks->at(i+j*rows).setFillColor(Color::Blue);
                bricks->at(i+j*rows).setOutlineColor(Color::Black);
                bricks->at(i+j*rows).setOutlineThickness(3);
                bricks->at(i+j*rows).setPosition(left+j*cellSize().x, top+i*cellSize().y);
            }
    }

    void display (RenderWindow & window)
    {
        for (int i=0;i<rows;i++)
            for (int j=0;j<columns;j++)
            {
                if (bricks->at(i+j*rows).armor <=0)
                    continue;
                window.draw(bricks->at(i+j*rows));
            }
    }

    bool collide(RectangleShape & ball, Vector2f & ballSpeed, unsigned int & score)
    {
        for (int i=0;i<rows;i++)
            for (int j=0;j<columns;j++)
            {
                if (bricks->at(i+j*rows).armor <=0)
                    continue;
                if (bricks->at(i+j*rows).collide(ball, ballSpeed) )
                {
                    bricks->at(i+j*rows).armor--;
                    bricks->at(i+j*rows).setFillColor(Color::Green);
                    if (bricks->at(i+j*rows).armor==0)
                        score+=700;

                    return true;
                }
            }
        return false;
    }

    bool isGameWon()
    {
        for (int i=0;i<rows;i++)
            for (int j=0;j<columns;j++)
                if (bricks->at(i+j*rows).armor >0)
                    return false;
        return true;
    }

};

    

/**
 * Basic Game Structure
 * use init and exec in main
 */
class Game 
{
private:
    static const int FRAMES_PER_SECOND = 60;    
    static const int MAX_FRAMESKIP = 10; 
    static const int width = 640;
    static const int height= 480;
    static const int borderSize= 12;
    static const int margin = 50;
    static const int moveDistance = 40;
    

    RenderWindow window;
    Font font;

    RectangleShape top;
    RectangleShape left;
    RectangleShape right;
    RectangleShape bottom;    

    RectangleShape ball;

    Vector2f ballSpeed;

    RectangleShape player;    

    Text title;
    Text start;
    Text won;
    Text lost;
    Text score;
    Text lives;
    Text fps;

    SoundBuffer soundBuffer1;
    Sound blip;
    SoundBuffer soundBuffer2;
    Sound blam;    
    SoundBuffer soundBuffer3;
    Sound blap;
    SoundBuffer soundBuffer4;
    Sound blop;

    Grid grid;

    Time time;
    Int32 updateTime;

    unsigned int playerScore;
    unsigned int playerLives;

    enum states {INTRO, PLAYING, GAME_WON, GAME_LOST};
    
    int gameState;
    
public:
    Game()
    {
    }

    bool init()
    {
        VideoMode videoMode(width, height);
        window.create(videoMode,"Break Out");
        window.setVerticalSyncEnabled(true);
        window.setFramerateLimit(FRAMES_PER_SECOND);

        if (!font.loadFromFile("tomb.ttf"))
            return false;  

        if (!soundBuffer1.loadFromFile("blip.wav"))
            return false;
        
        if (!soundBuffer2.loadFromFile("blam.wav"))
            return false;
        
        if (!soundBuffer3.loadFromFile("blap.wav"))
            return false;

        if (!soundBuffer4.loadFromFile("blop.wav"))
            return false;

        setup();
        return true;
    }

    int exec()
    {
        Clock renderClock, updateClock;
        while (window.isOpen())
        {
            time=renderClock.getElapsedTime();
            float fFps=1000000/time.asMicroseconds();
            std::stringstream s;
            s<<"fps: "<<fFps;
            fps.setString(s.str());
            renderClock.restart();
        
            const Int64 frameTime = 1000000/FRAMES_PER_SECOND;
            Clock c;
            Time t=c.getElapsedTime();
            Int64 nextFrameTime=t.asMicroseconds()+frameTime;

            int loops = 0;
            while( t.asMicroseconds() < nextFrameTime && loops < MAX_FRAMESKIP) 
            {
                processEvents();
                updateTime=updateClock.restart().asMilliseconds();
                update();
                t=c.getElapsedTime();
                loops++;
            }        
            
            display();
        }
        
        return EXIT_SUCCESS;
    }

private:

    void processEvents()
    {
        Event event;
        while (window.pollEvent(event))
        {
            if ( (event.type == Event::Closed) ||
            ((event.type == Event::KeyPressed) && (event.key.code==Keyboard::Escape)) )
                window.close();    
            else
            if ((event.type == Event::KeyPressed) && (gameState == INTRO))
                gameState=PLAYING;
        }
    }

    void update()
    {
        if (gameState!=PLAYING)
            return;

        updatePlayer();
        checkCollisions();
        updateBall();

        // detect if game is over
        if (playerLives < 0)
            gameState=GAME_LOST;
    }

    void setup()
    {
        top.setPosition(0, 0);
        top.setSize(Vector2f(width, borderSize));

        left.setPosition(0, borderSize);
        left.setSize(Vector2f(borderSize, height+borderSize));

        right.setPosition(width-borderSize, borderSize);
        right.setSize(Vector2f(borderSize, height+borderSize));

        bottom.setPosition(0, height+2*borderSize);
        bottom.setSize(Vector2f(width, borderSize));
            
        top.setFillColor(Color(100,100,100));
        top.setOutlineColor(Color::Black);
        top.setOutlineThickness(2);

        left.setFillColor(Color(100,100,100));
        left.setOutlineColor(Color::Black);
        left.setOutlineThickness(2);

        right.setFillColor(Color(100,100,100));
        right.setOutlineColor(Color::Black);
        right.setOutlineThickness(2);

        bottom.setFillColor(Color(100,100,100));
        bottom.setOutlineColor(Color::Black);
        bottom.setOutlineThickness(2);
        
        ball.setPosition(width/2, height-margin);
        ball.setSize(Vector2f(20, 20));
        ball.setFillColor(Color::Green);
        ball.setOutlineColor(Color::Red);
        ball.setOutlineThickness(2);

        player.setSize(Vector2f(90,borderSize));
        player.setPosition(width/2-45, height-margin-borderSize);
        player.setFillColor(Color(0,122,245));
        player.setOutlineColor(Color::Green);
        player.setOutlineThickness(3);
        
        title.setString("Break Out!");
        title.setFont(font);
        title.setCharacterSize(50);
        title.setPosition(width/2-title.getGlobalBounds().width/2,100);
        title.setColor(Color::Blue); 
    
        start.setString("Press any key to start");
        start.setFont(font);
        start.setCharacterSize(30);
        start.setPosition(width/2-start.getGlobalBounds().width/2,400);
        start.setColor(Color::Red);

        won.setString("You have won this game.\n\n Congratulations !");
        won.setFont(font);
        won.setCharacterSize(20);
        won.setPosition(width/2-won.getGlobalBounds().width/2,height/2-won.getGlobalBounds().height/2);
        won.setColor(Color::Green);

        lost.setString("You have lost this game, \n better luck next time!");
        lost.setFont(font);
        lost.setCharacterSize(20);
        lost.setPosition(width/2-lost.getGlobalBounds().width/2,height/2-lost.getGlobalBounds().height/2);
        lost.setColor(Color::Red);

        score.setString("0");
        score.setFont(font);
        score.setCharacterSize(30);
        score.setPosition(width/2+score.getGlobalBounds().width/2,height-60);
        score.setColor(Color(0,0,100,50));

        lives.setString("5");
        lives.setFont(font);
        lives.setCharacterSize(50);
        lives.setPosition(width/2-lives.getGlobalBounds().width/2,height-60);
        lives.setColor(Color(0,0,100,50));

        fps.setString("0");
        fps.setFont(font);
        fps.setCharacterSize(30);
        fps.setPosition(fps.getGlobalBounds().width/2,height-40);
        fps.setColor(Color(52,0,100,50));
        
        blip=Sound(soundBuffer1);        
        blam=Sound(soundBuffer2);    
        blap=Sound(soundBuffer3);    
        blop=Sound(soundBuffer4);
        
        resetGame();
        gameState=INTRO;

        grid.setDimensions(7,5);
        grid.left=borderSize+5;
        grid.top=borderSize+5;
        grid.width=width-2*borderSize-10;
        grid.height=250;
        grid.init();
    }

    void display()
    {        
        window.clear(Color::White);
        switch(gameState)
        {
            case INTRO:
                window.draw(title);
                window.draw(start);
                break;
            case PLAYING:
                window.draw(left);
                window.draw(right);            
                grid.display(window);
                window.draw(player);    
                window.draw(ball);    
                window.draw(score);    
                window.draw(lives);    
                window.draw(top);
                window.draw(bottom);        
                window.draw(fps);    
                break;
            case GAME_WON:
                window.draw(won);
                break;
            case GAME_LOST:
                window.draw(lost);
                break;
        }
        window.display();
    }

    void updatePlayer()
    {
        // move player 1 pad
        if (Keyboard::isKeyPressed(Keyboard::Left))
        {
            player.move(-moveDistance*updateTime/50.0,0);
        }else
        if (Keyboard::isKeyPressed(Keyboard::Right))
        {
            player.move(moveDistance*updateTime/50.0,0);
        }
    }

    void updateBall()
    {
        ball.move(ballSpeed.x*updateTime,ballSpeed.y*updateTime);
    }

    void checkCollisions()
    {
                        
        // block players pad inside the play area
        if ( intersects(player,left) || intersects(player,right) )
        {
            FloatRect l=left.getGlobalBounds();
            FloatRect r=right.getGlobalBounds();
            Vector2f p=player.getPosition();
            p.x=clamp(p.x,l.left+l.width+5,r.left-player.getGlobalBounds().width-5);
            player.setPosition(p.x,p.y);
            blap.play();
        }
        // ball collides with top, left and right
        if (intersects(ball,top))
        {
            FloatRect t=top.getGlobalBounds();
            FloatRect b=ball.getGlobalBounds();
            ballSpeed.y= abs(ballSpeed.y);
            int u = t.top + t.height - b.top;
            ball.move(0,2*u);   
            blop.play();
        }
        if (intersects(ball,left))
        {
            FloatRect l=left.getGlobalBounds();
            FloatRect b=ball.getGlobalBounds();
            ballSpeed.x= abs(ballSpeed.x);
            int u = l.left + l.width - b.left;
            b.left = l.left +  l.width + u;
            ball.setPosition(b.left,b.top);
            blop.play();
        }

        if (intersects(ball,right))
        {
            FloatRect r=right.getGlobalBounds();
            FloatRect b=ball.getGlobalBounds();
            ballSpeed.x= -abs(ballSpeed.x);
            int u = b.left + b.width - r.left ;
            b.left = r.left -  b.width - u;
            ball.setPosition(b.left,b.top);
            blop.play();
        }

        if ( intersects(ball,bottom) )
        {            
            blam.play();            
            playerLives--;
            std::stringstream str;
            str << playerLives;
            lives.setString(str.str());
            lives.setPosition(width/2-lives.getGlobalBounds().width/2,height-60);
            resetBall();
        }
        // ball collides with player
        if (intersects(ball,player))
        {
            FloatRect p= player.getGlobalBounds();
            FloatRect b= ball.getGlobalBounds();

            //let o be the center of p
            Vector2f o=Vector2f(p.left+p.width/2,p.top+p.height/2);
            //om: vector from o to the center of the ball M
            Vector2f om=Vector2f(b.left+b.width/2-o.x,b.top+b.height/2-o.y);
            // let's scale om to square dimensions and act as if p is a square
            om.x/=p.width;
            om.y/=p.height;
            // reflect the ball according to the angle of om
            float angle=atan2(om.y,om.x);
            if ( angle < -PI/4 && angle > -3*PI/4) //top
            {
                ballSpeed.y = -abs(ballSpeed.y);//in case of double contact
                ballSpeed.x= (b.left+b.width/2 - p.left- p.width/2) / 200;
                int u = b.top + b.height - p.top;
                b.top = p.top - b.height - u;
                ball.setPosition(b.left,b.top);
                //increase ball speed by 2%
                ballSpeed.x*=1.02f;
                ballSpeed.y*=1.02f;                
                blip.play();
            }
        }    
        //ball collides with grid
        if (grid.collide(ball,ballSpeed,playerScore))
        {
            blap.play();
            playerScore+=200;
            std::stringstream str;
            str << playerScore << " pts  ";
            score.setString(str.str());
            score.setPosition(width/2+score.getGlobalBounds().width/2-margin,height-60);
        }
        // check for end of game
        if (grid.isGameWon())
            gameState=GAME_WON;
        if (playerLives<=0)
            gameState=GAME_LOST;
    }

    void resetGame()
    {
        playerScore=0;
        playerLives=5;
        resetBall();
    }
    void resetBall()
    {
        FloatRect p=player.getGlobalBounds();
        FloatRect b=ball.getGlobalBounds();
        ball.setPosition(p.left+p.width/2+5, p.top);
        ballSpeed.x=0.1f;
        ballSpeed.y=-0.3f;        
    }
};


int main()
{
    Game game;
    if (!game.init())
        return EXIT_FAILURE;
    return game.exec();
}

Result:

This is a simple implementation of Break Out! in which the bricks turn green after one hit and disappear after the second hit. The behaviour of the bricks is not completely clean yet.

About these ads
  1. qner
    July 28, 2012 at 9:03 am

    Nice tutorial. Thank you!
    Found this on my way:
    * One million divided [by] the number of
    * window.setFramerateLimit([unsigned int])
    * a #define for 1000000 would be great

  2. July 29, 2012 at 9:21 pm

    Thank you very much.

  3. September 21, 2012 at 3:03 pm

    In both games ball got problem with bouncing. In Pong when CPU hit the ball it start flying vertical, so no one can touch it, and in Break Out its flying with parabolic move and it stop when it hit wall. I don’t think, that it is caused by using another compiler (C::B) :(

    • September 21, 2012 at 3:05 pm

      But its still great tutorial for beginners :)

  1. No trackbacks yet.
Comments are closed.
Follow

Get every new post delivered to your Inbox.