Structures de données des coordonnées - HelmDefense/HelmDefense GitHub Wiki

Les structures de données avancées des coordonnées

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 en Vector :
    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 en Location :
    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 Projectiles.

// 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)