Faire son propre driver.

Les problèmes posé par opengl.

Les technologies dans le jeux vidéo évoluent, et donc, les drivers et les cartes graphique aussi!

Le problème principal d’opengl est que celui-ci souffre beaucoup de son ancienneté, et les appele à draw (pour dessiner) sont bloquant!

Le CPU est donc obligé d’attendre après le GPU. (A moins que vous effectuer le rendu dans un autre thread, chose impossible sous linux avec la librairie X11 qui déteste cela.)

De plus, opengl n’est pas order independant transparency, c’est à dire, que, vous ne pouvez pas dessiner des images semi-transparente dans n’importe quel ordre!

Sinon, le blending ne se fera pas bien.

L’avenir du jeux vidéo.

On voit de plus en plus des technologies modernes voir le jour, comme par exemple openCL et Vulkan.

Ceux-ci permettront de faire du rendu graphique de manière beaucoup plus performante :

-Fini l’attente du CPU après le GPU, les deux pourront effectuer des traitements en parallèle.

-On est plus dépendant du driver pour les calculs au niveau du pipeline, dorénavant, tout est programmable. Possibilité donc de faire de l’order independant transparency et je vais vous le prouver!

Plus obligé donc de devoir fouiller dans pleins de fichiers en c pour optimiser un driver comme c’est le cas avec mesa et x11 qui de plus ne permettent pas de faire du traitement en parallèle à l’aide de plusieurs threads. (fils d’exécutions)

Ce qui facilitera grandement la tâche du programmeur en ayant une api suffisemment de haut niveau pour masquer tout ce qui est allocation de mémoire, exécution de shaders, …, au niveau du GPU, mais suffisemment bas niveau pour pouvoir gérer tout les calculs soit même en les partageant entre les différents threads du GPU.

Effectuer les calculs soit même au niveau du GPU.

C’est très simple, pour cela il va falloir coder plusieurs fonctions appelé “kernels” avec openCL.

Il y a 2 grandes étapes qui permettent de dessiner des objets en 3D du monde réel en pixels sur l’écran.

ETAPE1 : Passer des coordonnées 3D en coordonnées 2D. (Les matrices de projections de ODFAEG)

La première consiste à transformer les coordonnées 3D des sommets en coordonnées 2D à l’écran, pour cela, j’utilise une matrice de projection, il y a deux type de projection, la projection perspective qui donne l’impression que plus un objet est loin, plus il est petit, et la projection othographique plutôt utilisé dans le domaine mathématique ou là, tout les objets on la même grandeur peut importe leur éloignement.

Ce site (en Anglais) explique vraiment très bien comment projeter des sommets en 3D sur un écran en 2D à l’aide d’un tableau de nombres. (une matrice)

http://www.songho.ca/opengl/gl_projectionmatrix.html

Voyager dans le monde. (Les matrices de vue de ODFAEG)

Mais cette matrice à elle seules ne suffit pas, vous aimeriez bien bouger dans votre monde, comme si vous filmiez la scène à l’aide d’une caméra, contrairement à la réalité, ce n’est pas la caméra qui se déplace dans un jeux vidéo mais tout les objets de la scène!

Ceci à l’aide d’une matrice appelée la “lookat matrix”, celle-ci est construit à l’aide de trois axes (forward, up et left) qui pointent vers l’avant, vers la gauche et vers le haut de la caméra.

Voici un lien (en Anglais) qui explique comment construire cette matrice.

http://stackoverflow.com/questions/349050/calculating-a-lookat-matrix

Pour ma part, j’y ai ajouté une matrice de transformation afin de pouvoir faire une rotation de la caméra autout d’elle même, la matrice générale de rotation d’un point autour d’un axe est affichée dans ce tutoriel-ci :

http://openclassrooms.com/courses/creez-des-programmes-en-3d-avec-opengl/les-matrices-1

ODFAEG combine ces deux matrices afin de faire des rotations dans un espace en 2D (faire tourner la caméra sur elle-même, sur l’axe forward) et dans un espace en 3D. (faire tourner la caméra à l’aide de la “lookat matrix” et les coordonnées polaire)

Ce tutoriel explique aussi comment faire des rotations de la caméra à l’aide de coordonnées polaires.

Transformer des objets. (Les matrices de transformation de ODFAEG)

La matrice de transformation, contrairement aux matrices de vue et de projection, s’appliquent non pas à tout les objets du monde, mais à un seul objet, plusieurs objets enfant peuvent hériter de la transofmration de leur objet parent, pour cela, il suffit de combiner les matrices de transformation.

ODFAEG permet de faire des transformations quelconque d’objets autour d’un axe et d’une origine!

Ces trois matrices (projection, vue et transformation) sont combinée entre elle afin de donner la position finale des sommets à l’écran.

Le viewport.

Le viewport est la zone à l’écran dans laquelle on souhaite afficher la scène.

Donc tout ce qui est en dehors du viewport ne sera pas afficher.

Mais la matrice de projection donne des coordonnées entre -1 et 1, hors, il faut convertir ses coordonnées entre la position du viewport à l’écran et sa taille, pour cela ODFAEG utilise une matrice de viewport, la formule est simple : pixel = taille * 0.5 * coordonne + taille * 0.5 + position.

Pour la coordonnées z, elle est toujours comprise entre 0 et 1 par défaut avec opengl, se sera aussi le cas avec ODFAEG.

Passer d’un système de coordonnées en un autre.

Alors, les différentes coordonnées portent un nom :

-Les coordonnées des sommets d”un l’objet porte le nom de coordonnées de l’objet.

-Les coordonnées des sommets d’un objet transformées par la matrice de transformation de l’objet portent le nom de coordonnées monde.

-Les coordonnées monde transformées par la matrice de vue portent le nom de coordonnées vue.

-Les coordonnées vues transformées par la matrice de projection portent le nom de coordonnées clipées.

-Les coordonnées “clippées” sont ensuite divisée par leurs valeurs w, car, en projection perspective, les objets sont plus petit lorsqu’il sont plus élloigné, on parle de la division perspective, après cette division les coordonnées portent le nom de coordonnées normalisées. (Normalized device coordinates)

-Les coordonnées du viewport ne sont rien d’autre que les coordonnées normalisée transformée par la matrice de viewport.

Pour passer des coordonnées objets en coordonnées écran, il suffit alors de faire ceci :

coordonneesMonde = coordonneesObjet * matriceTransformationObjet.

coordonneesVue = coordonneesMonde * matriceDeVue.

coordonneesClippees = coordonneesVue * matriceDeProjection.

coordonneesNormalisees = coordonneesClippees / coordonnees.w

coordonneesViewport = coordonneesNormalisees * matriceDeViewport.

Pour passer des coordonnées écran en coordonnées monde il suffit de faire l’inverse :

coordonneesNormalisees = coordonneesViewport * inverseMatrixViewport.

coordonneesClippees = coordonneesNormalisees * inverseMatriceProjection

coordonneesVue = coordonneesClippees / coordonneesClippees.w

coordonneesMonde = coordonneesVue * inverseMatriceVue.

coordonneesObjet = coordonneesMonde * inverseMatriceTransformationObjet.

Tout est plus simple donc avec des matrices! 😛

ETAPE II : Le rasteriseur!

Ici plus haut je vous ai expliqué comment ODFAEG (et par conséquent opengl) procèdent pour passer des coordonnées des objets en 3D en coordonnées viewport.

Mais cela ne suffit pas, supposons que l’on aie trois sommets d’un triangle en coordonnées viewport (car tout est toujours décomposé en triangles par le driver), maintenant il faut remplir le triangle!

Pour cela, on utilise ce que l’on appelle un rasteriseur, celui-ci va dessiner tout les pixels qui se trouves, à l’intérieur de notre triangles.

La première chose à faire est de rechercher les minimums et les maximums des sommets du triangles car c’est dans cette zone que seront affiché les pixels du triangle.

Ensuite il suffit de faire une boucle, et de tester si les pixels est dans le triangle, pour des raisons de performances et d’anti-crênelage, on va traiter plusieurs pixels d’un coup, ce groupe de pixel s’appelle un fragment.

Ce lien (en Anglais) explique très bien comment vérifier si un ensemble de fragments est dans le triangle ou pas : (cette étape porte le nom de triangle setup)

https://fgiesen.wordpress.com/2013/02/10/optimizing-the-basic-rasterizer/

l suffit alors de faire deux boucles supplémentaire pour dessiner chaque pixel de nos fragments.

Maintenant il reste un dernier problème, comment connaître la profondeur de chaque fragment du triangle ?

Pour cela on utilise un technique qui porte de le nom d’interpolation hardware, on va convertir les coordonnées x et y en coordonnées barycentriques, ce lien :

http://en.wikipedia.org/wiki/Barycentric_coordinate_system

Pour trouver la valeur en z du fragment, il suffit alors de faire. (u, v et w étant les coordonnées barycentrique du fragment de notre triangle)

z = p1.z * u + p2.z * v + p3.z * w.

Il suffit alors d’appliquer cette formule pour tout les attributs des sommets des triangles. (Coordonnées de textures, couleurs, etc…)

Le blending.

Le blending est la dernière étape et elle consiste à déterminer la couleur du pixel final à l’écran, en fonction de la couleur du pixel qui est affiché à l’écran.

Le depth test.

Ce test consiste à déterminé si le pixel à dessiner est devant ou derrière le pixel dessiné.

ODFAEG stocke la valeur de z et de l’alpha du pixel le plus proche de l’observateur dans un buffer (appelé le depthbuffer), ses informations permettront de savoir comment effectuer le blending.

J’aurai pu aussi stocker ça dans deux buffers différent. (Un depthbuffer et un alphabuffer)

L’alpha test.

Ce test évite d’écrire des pixel qui sont invisible dans les buffers, en effet, ça ne sert à rien d’effectuer des traitements sur des pixels invisibles. (C’est à dire qui ont une valeur de alpha égale à  0)

Donc si le pixel à dessiné à une valeur de alpha égale à 0, alors on ne remet pas à jour les buffers.

L’accumulateur.

Le dernier point est de pouvoir calculer la couleur final du pixel en fonction du pixel à dessiner et du pixel déja dessiner et de leurs position par rapport à l’observateur.

La formule est très simple, ici, par exemple, le pixel dessiné est rouge, et le pixel à dessiné est vert, voici quelle sera sa couleur suivant leur positions en z :

rouge (1, 0, 0, 0.5)
z = 0.
vert  (0, 1, 0, 0.5)
z = 1.
nouvellecouleur = (0, 1, 0, 1) * 0.5 + (1, 0, 0, 0.5) * (1 – 0.5) =
SRCCOLOR * SRCALPHA + DSTCOLOR * (1 – SRCALPHA)
(0, 0.5, 0, 0.25) + (0, 0.5, 0, 0.5) = (0.5, 0.5, 0, 0.75)
rouge (1, 0, 0, 0.5)
z = 1.
vert  (0, 1, 0, 0.5)
z = 0.
nouvellecouleur = (0, 1, 0, 1) * 0.5 + (1, 0, 0, 0.5) * (1 – 0.5)
DSTCOLOR * DSTALPHA + SRCCOLOR * (1 – DSTALPHA)
(0, 0.5, 0, 0.5) + (0.5, 0, 0, 0.25) = (0.5, 0.5, 0, 0.75)

SRCCOLOR est la couleur du pixel à dessiner, SRCALPHA est la valeur de alpha du pixel à dessiner, DSTCOLOR est la couleur du pixel dessiné, et DSTALPHA est la valeur alpha présente dans le tampon de profondeur. (Le depthbuffer)

Voici ce à quoi ressemble l’algorithme une fois terminé :

math::Vec3f p1 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[0].position.x,(*m_instances[i]->getVertexArrays()[j])[0].position.y,(*m_instances[i]->getVertexArrays()[j])[0].position.z);
math::Vec3f p2 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+1].position.x,(*m_instances[i]->getVertexArrays()[j])[k+1].position.y,(*m_instances[i]->getVertexArrays()[j])[k+1].position.z);
math::Vec3f p3 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+2].position.x,(*m_instances[i]->getVertexArrays()[j])[k+2].position.y,(*m_instances[i]->getVertexArrays()[j])[k+2].position.z);
p1 = tm.transform(p1);
p2 = tm.transform(p2);
p3 = tm.transform(p3);
p1 = window.mapCoordsToPixel(p1, view);
p2 = window.mapCoordsToPixel(p2, view);
p3 = window.mapCoordsToPixel(p3, view);
sf::Color c1 = (*m_instances[i]->getVertexArrays()[j])[0].color;
sf::Color c2 = (*m_instances[i]->getVertexArrays()[j])[k+1].color;
sf::Color c3 = (*m_instances[i]->getVertexArrays()[j])[k+2].color;
math::Vec3f ct1 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[0].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[0].texCoords.y, 1.f);
math::Vec3f ct2 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+1].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[k+1].texCoords.y, 1.f);
math::Vec3f ct3 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+2].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[k+2].texCoords.y, 1.f);
std::array<math::Vec3f, 3> vertices = {math::Vec3f(p1.x, p1.y, 0),math::Vec3f(p2.x, p2.y, 0), math::Vec3f(p3.x, p3.y, 0)};
std::array<std::array<float, 2>, 3> extends = math::Computer::getExtends(vertices);
int minX = (extends[0][0] < 0) ? 0 : extends[0][0];
int minY = (extends[1][0] < 0) ? 0 : extends[1][0]; int maxX = (extends[0][1] >= size.x) ? size.x-1 : extends[0][1];
int maxY = (extends[1][0] >= size.y) ? size.y-1 : extends[1][1];
math::Vec2f p(minX, minY);
Edge e01, e12, e20;
math::Vec3f w0_row = e12.init(p2, p3, p);
math::Vec3f w1_row = e20.init(p3, p1, p);
math::Vec3f w2_row = e01.init(p1, p2, p);
for (p.y = minY; p.y < maxY; p.y += Edge::stepYSize) {
math::Vec3f w0 = w0_row;
math::Vec3f w1 = w1_row;
math::Vec3f w2 = w2_row;
for (p.x = minX; p.x <= maxX; p.x += Edge::stepXSize) {
math::Vec3f mask ((int) w0.x | (int) w1.x | (int) w2.x,
(int) w0.y | (int) w1.y | (int) w2.y,
(int) w0.z | (int) w1.z | (int) w2.z,
(int) w0.w | (int) w1.w | (int) w2.w);
if (mask.x < 0 || mask.y < 0 || mask.z < 0 || mask.w < 0) {
for (unsigned int y = p.y; y < p.y + Edge::stepYSize; y++) {
for (unsigned int x = p.x; x < p.x + Edge::stepXSize; x++) { math::Matrix2f m1(p1.x – p3.x, p2.x – p3.x, p1.y – p3.y, p2.y – p3.y); float u = ((p2.y – p3.y) * (x – p3.x) + (p3.x – p2.x) * (y – p3.y)) / m1.getDet(); float v = ((p3.y – p1.y) * (x – p3.x) + (p1.x – p3.x) * (y – p3.y)) / m1.getDet(); float w = 1 – u – v; math::Vec3f z (p1.z, p2.z, p3.z); float bz = z.x * u + z.y * v + z.z * w; float actualZ = depthBuffer[y * size.x + x].z; if (bz >= view.getViewport().getPosition().z && bz <= view.getViewport().getSize().z) { math::Vec3f tcx = math::Vec3f(ct1.x, ct2.x, ct3.x); math::Vec3f tcy = math::Vec3f(ct1.y, ct2.y, ct3.y); math::Vec3f tc = invTexM * math::Vec3f(tcx.x * u + tcx.y * v + tcx.z * w, tcy.x * u + tcy.y * v + tcy.z * w, 1.f); math::Vec3f texColor(1.f, 1.f, 1.f, 1.f); if (m_instances[i]->getMaterial().getTexture() != nullptr) {
const sf::Image& texImg = m_instances[i]->getMaterial().getTexture()->getImage();
tc.x = math::Math::clamp(tc.x, 0, texImg.getSize().x);
tc.y = math::Math::clamp(tc.y, 0, texImg.getSize().y);
sf::Color color = texImg.getPixel(tc.x, tc.y);
texColor = math::Vec3f (color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f);
}
math::Vec3f r = math::Vec3f (c1.r / 255.f, c2.r / 255.f, c3.r / 255.f);
math::Vec3f g = math::Vec3f (c1.g / 255.f, c2.g / 255.f, c3.g / 255.f);
math::Vec3f b = math::Vec3f (c1.b / 255.f, c2.b / 255.f, c3.b / 255.f);
math::Vec3f a = math::Vec3f (c1.a / 255.f, c2.a / 255.f, c3.a / 255.f);
std::array<math::Vec3f, 2> colors;
colors[0] = math::Vec3f(frameBuffer[(y * size.x + x)*4] / 255.f,
frameBuffer[(y * size.x + x)*4+1] / 255.f,
frameBuffer[(y * size.x + x)*4+2] / 255.f,
depthBuffer[y * size.x + x].w);
colors[1] = math::Vec3f(r.x * u + r.y * v + r.z * w,
g.x * u + g.y * v + g.z * w,
b.x * u + b.y * v + b.z * w,
a.x * u + a.y * v + a.z * w) * texColor;
bool src=(bz >= actualZ);
float z[2];
z[0] = actualZ;
z[1] = bz;
if (colors[1].w != 0) {
math::Vec3f finalColor = colors[src] * colors[src].w + colors[!src] * (1 – colors[src].w);
depthBuffer[y * size.x + x] = math::Vec3f(0, 0, z[src], colors[src].w);
frameBuffer[(y * size.x + x) * 4] = finalColor.x * 255;
frameBuffer[(y * size.x + x) * 4 + 1] = finalColor.y * 255;
frameBuffer[(y * size.x + x) * 4 + 2] = finalColor.z * 255;
frameBuffer[(y * size.x + x) * 4 + 3] = finalColor.w * 255;
}
}
}
}
}
w0 += e12.oneStepX;
w1 += e20.oneStepX;
w2 += e01.oneStepX;
}
w0_row += e12.oneStepY;
w1_row += e20.oneStepY;
w2_row += e01.oneStepY;
}
}
}

Cet algorithme ne gère pas les inversions des axes en x ou en y, mais il suffit pour cela d’inverser l’image du framebuffer et le problème est résolu!

Voilà sur se billet je vous ai présenté comment coder un rasterizer faire maison, libre à vous maintenant de l’utiliser au mieux à travers openCL et à l’avenir Vulkan!

Le but est de faire un rendu de manière la plus performante et la plus “order indepedant” qui soit!

Ne pas se laisser piégé par les faux arguments.

Le but des vendeurs, à toujours été de “mentir” pour espérer piéger les amateurs dans le but de vendre leur produit.

On peut cotoyer ce genre de personne partout, par exemple, sur les forums.

Voici une liste de faux argument qui pourrait convaincre quelque développeurs non pro :

-Le jeux est capable d’afficher n millions d’entités de manière performante sans utiliser de pointeurs.

C’est archi faux! Pour pouvoir allouer de la mémoire et la libérer au cours de l’exécution d’un programme, il faut passer par des pointeurs, surtout à partir du moment ou il y a de l’héritage.

Ne pas utiliser de pointeurs provoquerait des erreurs de slicing, le seul moyen d’éviter cela est de faire une référence sur une variable globale, mais comme les variables globales sont détruite à la fin de l’application, toutes la RAM sera consommée.

Donc il n’y a pas d’autres solutions que d’allouer et de désallouer la mémoire sur le tas, à l’exécution.

De plus dire qu’une variable qui ne garantis pas l’unicité est un singleton est une erreur.De plus il y a pas mal d’autres techniques qui permette de faire de meilleurs rendu en affichant beaucoup de choses de manière performante, un exemple sont les démos-scene.Je doute fort que un jeux arrive à afficher n millions d’entités sans utiliser de telle techniques.Ne pas utiliser les exceptions augmente les performances et facilite le déboguage et le profiling.Faux! C’est tout le contraire, les exceptions sont faîtes pour déboguer (ça évite les crash lorsque par exemple une resource n’est pas correctement chargée ou allouée) et ne dégradent pas les performance.Plutôt que de devoir chercher partout ou le programme plante, on sait que c’est à l’endroit à l’exception a été levée.Les sockets non bloquant ne fonctionnent pas sur console car, les consoles déconseilles d’utiliser les sockets locaux.Faux!Les socket bloquant et les sockets locaux ne sont pas la même chose!Si vous avez affaire à ce genre de trolls, ignorer les, le mieux qu’ils savent faire c’est de spamer le bouton désaccord de vos réponses sur des sites comme développez.com par exemple, et ils se tairont tout seul.

Le développement de jeux vidéos, bonnes pratiques.

Dans ce post je voudrais revenir sur certaines erreurs à ne pas faire lorsqu’on développe un jeux vidéo.

Et même si le c++ est mon langage préféré et selon moi c’est le seul langage vraiment viable pour développer des jeux vidéos, je vais essayer de ne pas faire de polémique.

Ceci n’est que un retour de mon expérience dans le domaine, pas une obligation (je ne vous oblige donc pas à utiliser le c++), si vous n’êtes pas d’accord avec ses principes libre à vous de faire autrement.

Erreur n°1 : utiliser plein de thread c’est plus performant.

Il faut savoir que l’utilisation de thread requiert souvent au besoin d’utiliser des primitives de synchronisation, et les locks ne sont pas gratuit de plus leur oublier peut amener à des crashs difficile à détecter, et peut engendrer pas mal de problèmes (inter-blocages, ralentissement de l’application, etc…)

En effet, faire des locks trop souvent cela représente un peu près 30% du temps d’exécution de votre application. (J’ai testé avec un profiler)

Il faut donc endormir les threads afin de ne pas faire de locks trop souvent, tout en gardant un rendu fluide, chose qui n’est pas aisée!

Certains langages moderne utilise des threads pour gérer les événements, update les composants, etc…, ce qui est sale et pas performant.

Par exemple, afficher 12345678 en console avec un seul thread ne prendra pas plus de temps que d’afficher 1234 et 5678 avec deux thread différents.

De plus chaque thread possède déjà des traitements qui se font en parallèle au niveau de l’os (j’entends par là les opérations monothread qui sont effectuée par le noyaux de linux par exemple (hyper-threading) et qui offre des optimisations de l’ordre de * 200), donc, si vous les séparer, dans plusieurs threads il faut faire pas mal de benchmark et ça rend les choses beaucoup plus compliquée sans pour autant être sûr de vraiment gagner en performance, le but est de rester simple donc et d’ajouter de la complexité seulement si cela est vraiment nécessaire. (KISS : Keep it simple and stupid)

Pour les traitements graphique il y a le GPU qui complète très bien le CPU.

De plus, les api graphiques évoluent de manière à pouvoir faire de plus en plus de traitements parallèle entre le CPU et le GPU de manière performante à l’aide de shaders, des FBO, etc…

Les threads ne sont donc à utiliser que là ou l’on en a vraiment besoin afin de pouvoir effectuer des traitements lourd sans “freeze” l’application comme par exemple une sauvegarde automatique des états de l’application, mais, généralement il n’y a pas eu d’autre cas jusqu’à présent dans lequel j’ai eu obligatoirement recours à des threads, je sais que j’en utilise pas mal dans ODFAEG des threads mais ceci est une erreur de ma part je pense donc que je vais proposer deux implémentations différente, une implémentation “threadée” et une autre “non-threadée”.

En résumé, avec des threads, le code devient beaucoup plus difficile à déboguer et à maintenir, au pire, si il y a des goulots d’étranglement, essayer d’optimiser les fonctions qui sont les plus souvent appelée.

Erreur n°2 : Ajouter trop de fonctionnalité à une classe dont ce n’est pas son rôle.

Il faudrait limiter les classes à un voir deux rôles maximum, prenons par exemple une classe fenêtre, celle-ci a 2 rôles, s’afficher et générer des événements, il ne faut donc pas qu’elle s’affiche en plus de traiter les événements utilisateurs.

L’idéal aurait été de séparer le code qui génère les événements du code de la fenêtre, car, il se pourrait très bien aussi que l’on veille gérer des événements lorsqu’il n’y a pas de fenêtre comme par exemple pour les applications console, malheureusement, SFML ne permet pas de faire cela sans devoir créer une fenêtre. (Même si ce n’est pas très gênant et qu’il suffit de créer une fenêtre invisible)

Par contre SDL, si je ne m’abuse, permet de le faire. 🙂

Il faut donc essayer de respecter au maximum le principe de classes à responsabilité unique.

Les transferts de données peuvent se faire via d’autres classes ou interfaces communes.

Erreur n°3 : Vouloir coder toutes les fonctionnalités d’une application d’un seul coup.

Il faut faire pas mal d’itération et de tests intermédiaires, ceci afin d’éviter d’avoir trop de bugs à corriger à la fin de l’application, vous aller me dire, comment faire cela si on a besoin de tel modules et tel modules pour tester cela ? Le but est de rendre les modules les plus indépendants possible, et de coder de manière générique, il ne faut pas que votre code ressemble à une usine à gaz avec 36 000 classes qui communiquent à travers vos différent modules, en principe, juste une classe suffit (en général c’est la classe application), certains bon langages de programmations permettent de coder de manière générique de plus en plus simplement tout en respectant les principes de l’orienté objet et permettant en plus de faire de la méta-programmation de manière aisée sans devoir modifier les fichiers sources avec des scripts en compilation. (CF les templates variadiques du c++11 et du c++14)

Pour les différentes étapes des itérations je vous conseille de vous renseigner du côté de l’extrême programming ainsi que d’appliquer le principe de YAGNI. (You ain’t gonna need it)

Erreur n°4 : Utiliser un langage qui utilise un garbage collector c’est mieux.

Hum non. x)

Pour libérer la mémoire automatiquement il faut tester si elle n’est plus référencée et ceci n’est pas toujours gratuit même si certains langage moderne tentent de le faire de manière intelligente. (Avec parcours d’une partie de la mémoire pendant des temps de pause)

Ceci reste coûteux si il y a beaucoup de ressources à libérer et de plus on ne sait pas quand exactement celles-ci vont être libérée. (A éviter donc dans un jeux vidéo qui utilise beaucoup de ressources)

De plus, pour des raisons de logique plutôt, le programmeur devrait toujours savoir quelle classe est propriétaire de la ressource mémoire. (RAII)

Le programmeur devrait aussi savoir si la ressource peut être copiée ou pas, si elle est référencée ou bien copiée, les nouveaux langages cache cette sémantique ce qui complique les choses lors de la relecture du code source.

Certains bons langages de programmation possède des classes qui servent de capsule RAII, ce qui est le cas par exemple de la classe std::unique_ptr en c++14 (mais ce n’est pas la seule), et c’est la classe qui devrait être utilisée dans la majorité des cas de plus, avec la move semantic ont peut changer une ressource de propriétaire de manière performante, et pour utiliser une ressource de manière temporaire il suffit de récupérer un pointeur nu sur la ressource.

Par contre std::shared_ptr et std::weak_ptr ne sont à utiliser que là ou c’est nécessaire, jusqu’à maintenant je n’en ai pas eu besoin.

On peut s’en passer facilement, pour s’en passer il suffit pour cela de créer un thread en début d’application, de faire un pool dessus, et à la fin de l’application, attendre que le thread ai fini sont exécution avant de détruire les ressources, cela ne coûte rien et vous permet d’endormir de temps à autre le thread courant ce qui réduit la consommation car quand un thread est mit en attente il ne fait rien!

Donc vous pouvez réduire le tour de boucle d’un thread sans que cela affecte le tour de boucle d’un autre thread qui affiche le jeux par exemple et c’est vraiment là que les threads sont utile. (Et là encore on pourrait encore se passer de threads avec un timer et un test.)

Il faut savoir que le c++14 est un nouveau langage qui se veut performant, sécurisé et économique, alors, autant utiliser toutes ses fonctionnalités à bien.

Erreur n° 5 : Vouloir à tout pris recompiler car cela est plus rapide à l’exécution.

Hum, oui un peu.

Mais la différence est négligeable, par rapport au temps que ça demande de devoir recompiler une librairie à chaque fois que l’ont change de module!

La compilation d’une grosse application peut nécessité plusieurs minutes voir même plusieurs heures pour un temps d’exécution qui sera réduit de quelque micro-secondes à peine.

Utiliser trop de dll n’est pas bon certes, donc, il faudra bien sûr vous limiter lors du chargement de modules pour pas que l’application mette trop longtemps à charger, mais, si par exemple vous souhaiter utiliser le module  window de sfml et puis que vous voulez changer et utiliser le module window de SDL parce que SFML n’est plus maintenue, cela sera un grand gains de temps surtout si l’on veut tester divers modules et que l’on doit changer de module souvent.

Voilà 5 grosses erreurs qui selon moi sont à éviter lorsque l’on développe des jeux vidéos qui sont très coûteux en matière de performances.