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.