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.
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.