|
JustAnotherDnDGame 0.1.0
Jeu de rôle tactique au d20, vue de dessus, en C++/Qt
|
Cette page redéfinit, sans présupposer de bagage en algèbre appliquée aux jeux vidéo, les quelques outils mathématiques dont dépend tout le reste du moteur : vecteurs, boîtes englobantes, conventions d'unités, comparaison de nombres flottants.
Le moteur n'utilise aucune bibliothèque mathématique tierce dans Core (pas de DirectXMath, pas de GLM) : un type minimal, écrit à la main, suffit aux besoins du jeu et garde Core totalement indépendant de DirectX (EX-ARCH-040) — la conversion vers les types du GPU se fait uniquement côté HMI, au moment du rendu.
Un vecteur 2D est simplement une paire de nombres (x, y). Il sert à deux usages différents selon le contexte, qu'il faut garder à l'esprit en lisant le code :
core::Vector2 porte deux float, x et y, et fournit l'algèbre nécessaire :
lengthSquared() renvoie x*x + y*y, sans appeler sqrt. La racine carrée est une opération relativement coûteuse comparée à une multiplication ; or, pour de nombreuses questions, on n'a pas besoin de la longueur exacte, seulement de comparer deux longueurs (« ce vecteur est-il plus long que celui-là ? », « cette distance est-elle inférieure à un seuil ? »). Comme la fonction racine carrée est croissante, comparer a.lengthSquared() < b.lengthSquared() donne exactement le même résultat que comparer a.length() < b.length(), sans jamais calculer de racine — une optimisation classique et systématique en géométrie appliquée aux jeux.
operator== sur deux Vector2 (comme core::approximatelyEqual sur deux float, voir plus bas) compare avec une tolérance, pas une égalité binaire exacte. C'est nécessaire parce que l'arithmétique flottante accumule de minuscules erreurs d'arrondi : deux calculs mathématiquement équivalents (par exemple (a + b) + c et a + (b + c)) peuvent produire des float légèrement différents au dernier bit. Comparer de tels résultats avec == strict échouerait de façon imprévisible et intermittente — un piège classique documenté plus bas.
Une AABB ⧉ (Axis-Aligned Bounding Box, « boîte englobante alignée aux axes ») est la forme géométrique la plus simple pour représenter l'encombrement d'un objet : un rectangle dont les côtés sont toujours parallèles aux axes X et Y — jamais tourné. C'est un compromis délibéré : une boîte tournée ou une forme complexe (cercle, polygone) collerait mieux à la silhouette d'un sprite, mais coûterait bien plus cher à tester en collision (voir Physique du personnage) pour un gain de précision inutile dans un plateformer où les niveaux sont eux-mêmes des grilles de tuiles alignées aux axes.
core::Aabb décrit un rectangle par ses deux coins opposés :
En pratique, le code manipule souvent une entité par son coin haut-gauche (core::Collider combine une taille avec la position du Transform) plutôt que directement par min/max : Aabb::fromTopLeftSize(topLeft, size) construit la boîte correspondante (max = topLeft + size), évitant de refaire ce calcul — et l'erreur de signe qui va avec — à chaque site d'appel.
Trois conventions, fixées une fois pour toutes et valables dans tout Core, expliquent la plupart des signes rencontrés dans le code de physique et de niveau :
Garder ces trois conventions en tête suffit à expliquer, sans avoir à les redériver, la quasi- totalité des signes et des sens de déplacement rencontrés dans le moteur.
Les nombres à virgule flottante (float) ne représentent pas exactement toutes les valeurs réelles : ils utilisent une précision finie, et des opérations en apparence anodines (addition répétée, division) introduisent de minuscules erreurs d'arrondi. Un exemple classique : en C++, 0.1f + 0.2f == 0.3f est faux, car ni 0.1, ni 0.2, ni 0.3 ne sont représentables exactement en binaire — le résultat de l'addition diffère de 0.3f de quelques millionièmes. Comparer deux flottants issus de calculs différents (même mathématiquement équivalents) avec == strict est donc fragile : le test peut échouer de façon imprévisible selon l'ordre des opérations, l'optimiseur du compilateur, ou l'architecture du processeur.
core::approximatelyEqual (dans Core/Math) résout ce problème en comparant deux float à une tolérance près, à la fois relative (proportionnelle à la magnitude des nombres comparés — utile pour de grandes valeurs) et absolue (un plancher minimal — utile près de zéro, où une tolérance purement relative deviendrait nulle). C'est la fonction que Vector2::operator== appelle composante par composante, et celle que les tests du moteur utilisent systématiquement pour comparer des résultats de calcul flottant plutôt qu'un == direct.