L'objectif
Le but était de construire un ou plusieurs format(s) de données utiles à la manipulation de coordonnées (x, y)
. La première étape était donc d'avoir une classe Location
englobant un x
et un y
. Les coordonnées sont ainsi exprimées en cases (avec 1 case = 64 pixels à l'affichage) et sont décimales afin de pouvoir être situé entre 2 cases. Nous avons ensuite eu besoin d'une autre structure de donnée, pour représenter une distance entre deux points (ex : pour les déplacements) : La classe Vector
. Comme ces deux classes représentaient des coordonnées, nous avons extrait une interface Coords
, implémentée par ces deux classes. Enfin, nous avons eu besoin de gérer les collisions entre les entités (ex : pour les projectiles), nous avons donc choisi de créer une classe Hitbox
, implémentant également l'interface Coords
.
Les utilisations
Ces différentes classes assez présentes dans notre code, étant donné que toutes les entités ont une Location
et une Hitbox
. Les projectiles (sous-type des entités) possèdent en plus un Vector
définissant leur déplacement. De plus, certaines autres classes du modèle utilisent ces format, comme la classe Cell
(qui représente une cellule du graphe utilisé pour effectuer des calculs de déplacements (BFS)), de nombreuses Action
, la GameMap
(pour les positions d'apparition et la cible), etc...
Les structures
Coords
Présentation
Cette interface représente simplement une notion de coordonnées x
et y
.
Les méthodes
Les seules méthodes communes aux coordonnées sont :
public double getX();
public double getY();
Implémentations
Les trois classes qui implémentent cette interface sont : Location
, Vector
et Hitbox
.
Location
Présentation
Cette classe permet de représenter un point au sens mathématique du terme et possède donc un attribut x
et y
ainsi que de nombreuses méthodes utilitaires pour effectuer des calculs. Lorsqu'une méthode ne revoie pas de résultat particulier, nous avons décidé de renvoyer l'objet lui-même afin de pouvoir enchaîner les appels, comme par exemple Location loc = new Location(3, 5).add(6, 1).center();
.
Les méthodes
- Addition / soustraction entre coordonnées (y compris avec un vecteur) :
public Location add(Coords coords); public Location add(double x, double y); public Location subtract(Coords coords); public Location subtract(double x, double y);
- Multiplication par un réel :
public Location multiply(double factor);
- Arrondir / centrer / mettre à zéro une
Location
:public Location round(); public Location center(); public Location zero();
- Copier la
Location
(la location renvoyée aura donc les mêmes coordonnées mais ne sera pas liée à l'ancienne) :new Location(Coords coords); new Location(double x, double y); public Location copy();
- Calculer la distance entre deux
Location
(la méthode utilise une racine carrée qui est lourde et il vaut mieux enregistrer son résultat si on doit l'utiliser à plusieurs reprises) :public double distance(Location loc);
- Vérifier que la
Location
est dans la map (possibilité de fournir une taille pour vérifier que la taille entière est comprise dans la map) :public boolean isInMap(); public boolean isInMap(Size size); public boolean isInMap(double dx, double dy);
- Transformer la
Location
enVector
:new Vector(Coords coords); public Vector toVector();
- Vérifier l'égalité de deux
Location
de manière stricte ou en passant par les centres :public boolean equals(Location loc); public boolean equalsCenter(Location loc);
De plus, cette classe possède de nombreux accesseurs pour obtenir les coordonnées sous de multiples formes (brut, pixel, arrondi, etc...) ainsi que deux mutateurs pour les modifier. Les coordonnées sont stockées dans des Property
de JavaFX
et peuvent donc être liées et écoutées.
Exemples d'utilisation
L'exemple d'utilisation le plus classique est la vérification de la distance entre deux Location
, ou encore le déplacement des attaquants.
Location loc = this.entity.getLoc(); for (Entity e : action.getLvl().getEntities()) if (e.getLoc().distance(loc) <= this.range) e.gainHp((int) (e.stat(Attribute.HP) * this.healingPercentage));
Exemple d'utilisation : La capacité de soin des entités à proximité (simplifiée)
Vector
Présentation
Cette classe permet de représenter un vecteur au sens mathématique du terme. Elle possède ainsi un attribut x
et y
et des méthodes de calcul comme les Location
. Sur le même principe, les méthodes renvoyant void
ont été remplacées pour renvoyer l'objet lui-même pour enchaîner les appels.
Les méthodes
- Ajouter / soustraire des
Vector
:public Vector add(Coords coords); public Vector add(double x, double y); public Vector subtract(Coords coords); public Vector subtract(double x, double y);
- Multiplier / diviser le vecteur par un réel :
public Vector multiply(double factor); public Vector divide(double factor);
- Prendre la négation du vecteur :
public Vector negate();
- Copier le vecteur (le vecteur renvoyé aura donc les mêmes coordonnées mais ne sera pas lié à l'ancien) :
new Vector(Coords coords); new Vector(double x, double y); public Vector copy();
- Calculer la norme (longueur) du vecteur (la méthode utilise une racine carrée qui est lourde et il vaut mieux enregistrer son résultat si on doit l'utiliser à plusieurs reprises) :
public double length();
- Calculer l'angle (par rapport à l'axe) en radians (sauf si précisé en degrés) :
public double angle(); public double angle(boolean degrees);
- Transformer le
Vector
enLocation
:new Location(Coords coords); public Location toLocation();
Exemples d'utilisation
Un exemple assez intéressant de l'usage des Vector
est le calcul de la trajectoire des Projectile
s.
// LivingEntity source, Location target, double angle Location loc = source.getLoc(); double d = target.distance(loc), a = Math.acos((target.getX() - loc.getX()) / d); if (Math.asin((target.getY() - loc.getY()) / d) < 0) a += (Math.PI - a) * 2; a += Math.toRadians(angle); this.vector = new Vector(Math.cos(a), Math.sin(a));
Exemple d'utilisation : Initialisation de la trajectoire des projectiles à partir d'une entité vers une position avec un angle de décalage (simplifiée)
Hitbox
Présentation
Cette classe représente un rectangle au sens mathématique du terme. Elle est constituée de deux attributs : Une Location
(centre) et une Size
(largeur et hauteur). Contrairement aux autres, elle ne possède aucune méthode renvoyant l'objet lui-même. Il est possible de "bloquer la Location
" (action irréversible) pour faire en sorte que l'accesseur renvoie une copie de la Location
à la place de l'originale, afin de pouvoir partager la Hitbox
liée à la Location
d'une entité en gardant la Location
protégée.
Les méthodes
- Vérifier le chevauchement de deux
Hitbox
:public boolean overlaps(Hitbox other);
- Vérifier qu'un point est contenu dans la
Hitbox
:public boolean contains(Coords coords); public boolean contains(double x, double y);
De plus, cette classe dispose de nombreux accesseurs pour obtenir les données sous plusieurs formes (centre, min / max, dimensions, etc...). Il n'y a cependant aucun mutateur, il est nécessaire de modifier directement la Location
du centre, et la taille ne peut pas être modifiée.
Exemples d'utilisation
L'exemple le plus évidant est bien entendu les collisions entre les entités, comme par exemple les projectiles.
for (Entity e : this.getLevel().getEntities()) if (this.source.isEnemy((LivingEntity) e) && this.getHitbox().overlaps(e.getHitbox())) this.attack((LivingEntity) e);
Exemple d'utilisation : La collision des projectiles (simplifiée)
Un exemple plus complet
Ces différentes structures relatives aux coordonnées sont conçues pour pouvoir être utilisée ensemble et ainsi simplifier des problèmes complexes en les décomposant en tâches plus simples. Dans cet exemple, le but est de faire avancer une entité vers une Location
selon sa vitesse de déplacement.
Nous appelons loc
la Location
actuelle de l'entité et l
son objectif. La première étape est de calculer le Vector
v
entre ces deux positions, ainsi que la distance d
à parcourir. Pour calculer un vecteur unitaire ayant la même direction, il faudrait diviser ce vecteur par d
, puis mettre à l'échelle ce vecteur unitaire en le multipliant par la vitesse en case par tick pour obtenir le vecteur de translation de notre déplacement.
Cependant, effectuer une division puis une multiplication ne serait pas optimisé, en effet il vaut mieux d'abord calculer notre facteur de mise à l'échelle en case par tick, le diviser par d
et enfin multiplier notre vecteur par le résultat. Ainsi, pour obtenir ce facteur, nous avons la vitesse de déplacement de l'unité (en case par seconde), qu'il nous suffit de diviser par les TPS (en tick par seconde) pour avoir des case par tick.
Enfin, la dernière étape dans l'exemple consiste à vérifier que notre déplacement ne nous a pas fait dépasser notre objectif, de ce revient à rechercher si la norme de notre vecteur final est supérieure à la distance initiale à parcourir.
Location loc = this.entity.getLoc(), l = this.movingTo.getLoc().center(); Vector v = new Vector(loc, l); double d = l.distance(loc); v.multiply(this.entity.stat(Attribute.MVT_SPD) / GameLoop.TPS / d); this.entity.teleport(loc.add(v)); if (v.length() >= d) this.movingTo = this.movingTo.getNext();
Exemple d'utilisation : Le déplacement des attaquants (simplifiée)