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.