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 : constexprunsignedintwndWidth{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.
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 :
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 :
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 :
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 :
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 :
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.
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 :
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 :
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 :
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 :
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.
