I. Introduction

Romeo Vittorio est étudiant à l'université de Messine. Il a appris la programmation en autodidacte et s'intéresse au développement de logiciels et plus particulièrement, de jeux vidéo.

II. Vidéo


CppCon 2014 - Développement rapide de jeux en C++11/C++14


III. Résumé

III-A. Introduction

III-A-1. Pourquoi le développement de jeux vidéo ?

Le développement de jeux vidéo est amusant : il touche de nombreux domaines en programmation, il implique le programmeur dans une communauté et le programmeur reçoit un retour direct de ce qui est programmé.

III-A-2. Pourquoi le C++ ?

Le C++ est efficace en permettant de créer des abstractions sans le moindre coût et d'avoir un accès bas niveau.
Si vous suivez le standard, votre programme sera portable.
Finalement, il y a de très nombreuses ressources et bibliothèques disponibles pour le C++.

III-A-3. Pourquoi le C++11/C++14 ?

Le C++11/C++14 apporte de très nombreux avantages qui sont applicables dans le monde du jeu vidéo :

  • les templates variadiques et les lambdas pour les fabriques et les callbacks ;
  • les pointeurs intelligents pour la gestion d'entités ;
  • les ajouts dans l'en-tête chrono pour la gestion de la boucle principale.

III-B. Programmation en live : casse-briques

Durant cette session, Romeo Vittorio a programmé un casse-briques, tout en expliquant sa démonstration. Le code final fait environ 200 lignes et pour réaliser son projet, il utilise la SFML.

III-B-1. Quel compilateur ?

Pour son projet de casse-briques, il est obligatoire d'utiliser le C++11 et d'avoir accès à quelques fonctionnalités du C++14. Du coup, les compilateurs candidats sont : g++ 4.9 et clang++ 3.4.

III-B-2. Ouverture de la fenêtre

Les étapes pour cette première partie, s'occupant de la mise en place de la fenêtre de jeu, sont :

  • ajout du fichier d'en-tête de la SFML : #include <SFML/Graphics.hpp> ;
  • ajout des constantes pour la résolution de la fenêtre de jeu : constexpr unsigned int wndWidth{800}, wndHeight{600}; ;
  • création de la fenêtre de jeu : sf::RenderWindow window{{wndWidth, wndHeight}, "Arkanoid - 1"}; ;
  • hack pour limiter le nombre d'images par seconde window.setFramerateLimit(60);(il existe de meilleures méthodes) ;
  • nettoyage de la fenêtre (en noir) : window.clear(sf::Color::Black); ;
  • vérification si la touche Échap est appuyée : if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Escape)) break; dans ce cas, on quitte le jeu en sortant de la boucle principale ;
  • rendu à l'écran window.display();.

III-B-3. La balle

Dans cette deuxième partie, on implémente la balle à l'aide d'une classe.

 
Sélectionnez
class Ball
{
public:
    static const sf::Color defColor;
    static constexpr float defRadius{10.f};
    static constexpr float defVelocity{1.f};
    
    sf::CircleShape shape;
    
    sf::Vector2f velocity{-defVelocity, -defVelocity};
    
    Ball(float mX, float mY)
    {        
        shape.setPosition(mX, mY);
        shape.setRadius(defRadius);
        shape.setFillColor(defColor);
        shape.setOrigin(defRadius, defRadius);
    }
    
    void update()
    {
        shape.move(velocity);
    }
    void draw(sf::RenderWindow& mTarget)
    {
        mTarget.draw(shape);
    }
};

const sf::Color Ball::defColor{sf::Color::Red};

Jusqu'à présent, la balle pouvait sortir de la fenêtre. Pour éviter cela, la méthode update() est modifiée :

 
Sélectionnez
float x() const noexcept { return shape.getPosition().x; }
float y() const noexcept { return shape.getPosition().y; }
float left() const noexcept { return x() - shape.getRadius(); }
float right() const noexcept { return x() + shape.getRadius(); }
float top() const noexcept { return y() - shape.getRadius(); }
float bottom() const noexcept { return y() + shape.getRadius(); }

void update()
{
    shape.move(velocity);

    if(left() < 0) velocity.x = defVelocity;
    else if(right() > wndWidth) velocity.x = -defVelocity;
    if(top() < 0) velocity.y = defVelocity;
    else if(bottom() > wndHeight) velocity.y = -defVelocity;
}

III-B-4. La batte

Une fois arrivé là, on peut rajouter la batte :

 
Sélectionnez
class Paddle
{
public:
    static const sf::Color defColor;
    static constexpr float defWidth{60.f};
    static constexpr float defHeight{20.f};
    static constexpr float defVelocity{8.f};

    sf::RectangleShape shape;
    sf::Vector2f velocity;

    Paddle(float mX, float mY)
    {
        shape.setPosition(mX, mY);
        shape.setSize({defWidth, defHeight});
        shape.setFillColor(defColor);
        shape.setOrigin(defWidth / 2.f, defHeight / 2.f);
    }
    void update()
    {
        processPlayerInput();
        shape.move(velocity);
    }

    void draw(sf::RenderWindow& mTarget) { mTarget.draw(shape); }

    float x() const noexcept { return shape.getPosition().x; }
    float y() const noexcept { return shape.getPosition().y; }
    float width() const noexcept { return shape.getSize().x; }
    float height() const noexcept { return shape.getSize().y; }
    float left() const noexcept { return x() - width() / 2.f; }
    float right() const noexcept { return x() + width() / 2.f; }
    float top() const noexcept { return y() - height() / 2.f; }
    float bottom() const noexcept { return y() + height() / 2.f; }

private:
    void processPlayerInput()
    {
        if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left) && left() > 0)
        {
            velocity.x = -defVelocity;
        }
        else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right) && right() < wndWidth)
        {
            velocity.x = defVelocity;
        }
        else
        {
            velocity.x = 0;
        }
    }
};

const sf::Color Paddle::defColor{sf::Color::Red};

Jusqu'à présent, la balle ne rebondissait pas sur la batte. Ce nouveau morceau de code corrige cela :

 
Sélectionnez
template<typename T1, typename T2>
bool isIntersecting(const T1& mA, const T2& mB) noexcept
{
    return mA.right() >= mB.left()
        && mA.left() <= mB.right()
        && mA.bottom() >= mB.top()
        && mA.top() <= mB.bottom();
}

void solvePaddleBallCollision(const Paddle& mPaddle, Ball& mBall) noexcept
{
    if(!isIntersecting(mPaddle, mBall)) return;
    
    mBall.velocity.y = -Ball::defVelocity;

    mBall.velocity.x = mBall.x() < mPaddle.x() ?
        -Ball::defVelocity : Ball::defVelocity;
}

isIntersecting() est une fonction générique, pouvant vérifier la collision entre n'importe quel objet du jeu.

On peut aussi voir que, suivant la position de la balle par rapport à la batte, la balle pourra changer de direction.

III-B-5. Les briques

Il manque toujours les briques. Corrigeons cela avec une nouvelle classe :

 
Sélectionnez
class Brick
{
public:
    static const sf::Color defColor;
    static constexpr float defWidth{60.f};
    static constexpr float defHeight{20.f};
    static constexpr float defVelocity{8.f};
    
    sf::RectangleShape shape;

    bool destroyed{false};
    
    Brick(float mX, float mY)
    {
        shape.setPosition(mX, mY);
        shape.setSize({defWidth, defHeight});
        shape.setFillColor(defColor);
        shape.setOrigin(defWidth / 2.f, defHeight / 2.f);
    }
    
    void update() { }
    void draw(sf::RenderWindow& mTarget) { mTarget.draw(shape); }
    
    float x() const noexcept { return shape.getPosition().x; }
    float y() const noexcept { return shape.getPosition().y; }
    float width() const noexcept { return shape.getSize().x; }
    float height() const noexcept { return shape.getSize().y; }
    float left() const noexcept { return x() - width() / 2.f; }
    float right() const noexcept { return x() + width() / 2.f; }
    float top() const noexcept { return y() - height() / 2.f; }
    float bottom() const noexcept { return y() + height() / 2.f; }
};

const sf::Color Brick::defColor{sf::Color::Yellow};
III-B-5-a. Création des briques

Une fois la classe créée, il faut placer des briques dans le jeu. Comme elles sont placées sur une grille, on peut utiliser une boucle :

 
Sélectionnez
std::vector<Brick> bricks; 

constexpr int brkCountX{11}; // How many columns? 
constexpr int brkCountY{4}; // How many rows? 
constexpr int brkStartColumn{1}; // What column number to start at? 
constexpr int brkStartRow{2}; // What row number to start at? 
constexpr float brkSpacing{3}; // Spacing between adjacent bricks. 
constexpr float brkOffsetX{22.f}; // X offset for the grid pattern. 

for(int iX{0}; iX < brkCountX; ++iX) 
    for(int iY{0}; iY < brkCountY; ++iY) 
    { 
        float x{(iX + brkStartColumn) 
        * (Brick::defWidth + brkSpacing)}; 
        float y{(iY + brkStartRow) 
        * (Brick::defHeight + brkSpacing)}; 
        bricks.emplace_back(brkOffsetX + x, y); 
    }
III-B-5-b. Destruction des briques

Lorsque la balle touche une brique, il faut détruire la brique.

 
Sélectionnez
void solveBrickBallCollision(Brick& mBrick, Ball& mBall) noexcept
{
    if(!isIntersecting(mBrick, mBall)) return;
    
    mBrick.destroyed = true;
    
    float overlapLeft{mBall.right() - mBrick.left()};
    float overlapRight{mBrick.right() - mBall.left()};
    float overlapTop{mBall.bottom() - mBrick.top()};
    float overlapBottom{mBrick.bottom() - mBall.top()};
    
    bool ballFromLeft(std::abs(overlapLeft) < std::abs(overlapRight));
    
    bool ballFromTop(std::abs(overlapTop) < std::abs(overlapBottom));
    
    float minOverlapX{ballFromLeft ? overlapLeft : overlapRight};
    float minOverlapY{ballFromTop ? overlapTop : overlapBottom};

    if(std::abs(minOverlapX) < std::abs(minOverlapY))
    {
        mBall.velocity.x = ballFromLeft ?
        -Ball::defVelocity : Ball::defVelocity;
    }
    else
    {
        mBall.velocity.y = ballFromTop ?
        -Ball::defVelocity : Ball::defVelocity;
    }
}

Une fois les briques marquées comme détruites, on peut utiliser les fonctions de la STL pour les retirer du vecteur :

 
Sélectionnez
bricks.erase(
        std::remove_if(std::begin(bricks), std::end(bricks),
        [](const auto& mBrick){ return mBrick.destroyed; }),
        std::end(bricks)
    );

III-B-6. Refactoring

Le code est fonctionnel. Toutefois, beaucoup de duplications existent.

III-B-6-a. Les accesseurs

Pour éviter d'avoir de la duplication dans les accesseurs, il est possible de créer deux nouvelles classes pour les deux types de formes géométriques que nous avons :

 
Sélectionnez
struct Rectangle 
{ 
    sf::RectangleShape shape; 

    float x() const noexcept { return shape.getPosition().x; } 
    float y() const noexcept { return shape.getPosition().y; } 
    float width() const noexcept { return shape.getSize().x; } 
    float height() const noexcept { return shape.getSize().y; } 
    float left() const noexcept { return x() - width() / 2.f; } 
    float right() const noexcept { return x() + width() / 2.f; } 
    float top() const noexcept { return y() - height() / 2.f; } 
    float bottom() const noexcept { return y() + height() / 2.f; } 
}; 
struct Circle 
{ 
    sf::CircleShape shape; 

    float x() const noexcept { return shape.getPosition().x; } 
    float y() const noexcept { return shape.getPosition().y; } 
    float radius() const noexcept { return shape.getRadius(); } 
    float left() const noexcept { return x() - radius(); } 
    float right() const noexcept { return x() + radius(); } 
    float top() const noexcept { return y() - radius(); } 
    float bottom() const noexcept { return y() + radius(); } 
};

La balle hérite de la classe Circle et la batte ainsi que les briques héritent de la classe Rectangle.

III-B-6-b. Classe pour le jeu

Afin de permettre plus de choses au jeu tel qu'avoir une pause, ou de redémarrer une partie, il est possible d'encapsuler toute la logique dans une classe Game :

 
Sélectionnez
class Game
{
private:
    enum class State{Paused, InProgress};
    
    static constexpr int brkCountX{11}, brkCountY{4};
    static constexpr int brkStartColumn{1}, brkStartRow{2};
    static constexpr float brkSpacing{3.f}, brkOffsetX{22.f};
    
    sf::RenderWindow window{{wndWidth, wndHeight}, "Arkanoid - 9"};
    
    Ball ball{wndWidth / 2.f, wndHeight / 2.f};
    Paddle paddle{wndWidth / 2, wndHeight - 50};
    std::vector<Brick> bricks;
    
    State state{State::InProgress};
    bool pausePressedLastFrame{false};
    
public:
    Game() { window.setFramerateLimit(60); }

    void restart()
    {
        state = State::Paused;
        for(int iX{0}; iX < brkCountX; ++iX)
            for(int iY{0}; iY < brkCountY; ++iY)
            {
                float x{(iX + brkStartColumn)
                * (Brick::defWidth + brkSpacing)};
                float y{(iY + brkStartRow)
                * (Brick::defHeight + brkSpacing)};
                bricks.emplace_back(brkOffsetX + x, y);
            }
            
        ball = Ball{wndWidth / 2.f, wndHeight / 2.f};
        paddle = Paddle{wndWidth / 2, wndHeight - 50};
    }

    void run()
    {
        while(true)
        {
            window.clear(sf::Color::Black);
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Escape))
                break;
                
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::P))
            {
                if(!pausePressedLastFrame)
                {
                    if(state == State::Paused)
                        state = State::InProgress;
                    else if(state == State::InProgress)
                        state = State::Paused;
                }
                pausePressedLastFrame = true;
            }
            else pausePressedLastFrame = false;
            
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::R))
                restart();

            if(state != State::Paused)
            {
                ball.update();
                paddle.update();
                
                for(auto& brick : bricks)
                {
                    brick.update();
                    solveBrickBallCollision(brick, ball);
                }
                
                bricks.erase(
                    std::remove_if(std::begin(bricks), std::end(bricks),
                    [](const auto& mBrick){ return mBrick.destroyed; }),
                    std::end(bricks)
                );
                
                solvePaddleBallCollision(paddle, ball);
            }
            
            ball.draw(window);
            paddle.draw(window);
            for(auto& brick : bricks) brick.draw(window);
            
            window.display();
        }
    }
};
III-B-6-c. Modularité

Pour rendre le jeu modulaire, deux nouvelles classes sont intégrées :

 
Sélectionnez
class Entity
{
public:
    bool destroyed{false};

    virtual ~Entity() { }
    virtual void update() { }
    virtual void draw(sf::RenderWindow& mTarget) { }
};

class Manager
{
private:
    std::vector<std::unique_ptr<Entity>> entities;
    std::map<std::size_t, std::vector<Entity*>> groupedEntities;

public:
    template<typename T, typename... TArgs> T& create(TArgs&&... mArgs)
    {
        static_assert(std::is_base_of<Entity, T>::value,"`T` must be derived from `Entity`");

        auto uPtr(std::make_unique<T>(std::forward<TArgs>(mArgs)...));
        auto ptr(uPtr.get());

        groupedEntities[typeid(T).hash_code()].emplace_back(ptr);
        
        entities.emplace_back(std::move(uPtr));
        return *ptr;
    }
    
    void refresh()
    {
        for(auto& pair : groupedEntities)
        {
            auto& vector(pair.second);
            vector.erase(
                std::remove_if(std::begin(vector), std::end(vector),
                [](auto mPtr){ return mPtr->destroyed; }),
                std::end(vector)
            );
        }

        entities.erase(
            std::remove_if(std::begin(entities), std::end(entities),
            [](const auto& mUPtr){ return mUPtr->destroyed; }),
            std::end(entities)
        );
    }

    void clear()
    {
        groupedEntities.clear();
        entities.clear();
    }
    
    template<typename T> auto& getAll()
    {
        return groupedEntities[typeid(T).hash_code()];
    }
    
    template<typename T, typename TFunc>
    void forEach(const TFunc& mFunc)
    {
        auto& vector(getAll<T>());

        for(auto ptr : vector) mFunc(*reinterpret_cast<T*>(ptr));
    }

    void update()
    {
        for(auto& e : entities) e->update();
    }
    
    void draw(sf::RenderWindow& mTarget)
    {
        for(auto& e : entities) e->draw(mTarget);
    }
};

III-B-7. Code source

Le code source de la présentation est disponible sur GitHub.

IV. Commenter

Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.