Teaching - 2i002 - (TME: sujets)


2i002 : Introduction à la programmation Objet

Le but de ce projet est de travailler sur un gros TME, formaté pour 6h, en insistant sur les points qui ne sont pas abordés habituellement. Nous avons choisi dans ce projet plusieurs directions pédagogiques:

  1. l'obligation d'utiliser des outils existants et de s'interfacer avec ceux-ci
  2. l'introduction des énumerations (un outil non étudié dans l'UE)
  3. la compréhension des bases du MVC pour les interfaces graphiques
  4. l'utilisation des outils avancés autour des ArrayList

Téléchargement de l'ensemble des ressources: lien

Partie 1: Moteur physique et moteur graphique externe

Tutoriel pour l'utilisation du moteur graphique (15-20 minutes)

La programmation de logiciel exploitant une interface graphique requiert des bases autour de la programmation évènementielle (le fait que le programme réagisse autour des commandes de l'utilisateur, clic, menu, ...) et des bases autour du MVC (modèle vue contrôleur).

La partie graphique n'est pas un sujet prioritaire, quasiment l'ensemble du code est fourni.

Programmation évènementielle

NON TRAITEE dans le projet: notre interface se contente d'afficher le résultat d'une simulation: pas d'interaction!

Modèle-Vue-Controleur

Idée simple: l'affichage doit être indépendant du modèle. En particulier, il est essentiel que le modèle ne dépende en aucun cas de l'affichage. Il faut vérifier à tout moment que:

  • le modèle de la simulation peut tourner sans aucun affichage;
  • il est possible de définir un nouveau type d'affichage (3D ou autre) sans modifier aucune classe existante.

Cas d'usage et code fourni

Nous voulons afficher un rond se déplaçant en diagonale dans une fenêtre.
Le code fourni est le suivant:

  • Classe Fenetre (package ihm): Gestion d'une fenêtre. Accepte des objets Drawable via la méthode add. Pour actualiser la vue, il suffit d'invoquer la méthode repaint().
    Note: l'instanciation d'un objet Fenetre bloque la fermeture du programme. Le programme tourne tant que la fenêtre n'est pas fermée.
  • Interface Drawable (package ihm): Impose l'implémentation de deux méthodes:
    • public void draw(Graphics g); méthode de dessin (on donne un crayon g, la méthode doit dessiner l'objet).
    • public int getPriority(); donne la priorité de l'objet: plus le chiffre est grand, plus l'objet est dessiné tardivement dans la pile, plus il est dessiné au-dessus des autres
  • Classe PriseEnMainMoteurGraphique dans le package tutoMoteurGraphique: contient le main (imposé)

Pour rappel, le code du main est le suivant:

  1.     // Ajout d'Exception liées à la temporisation que l'on ne souhaite pas gérer
  2.     public static void main(String[] args) throws InterruptedException {
  3.         Fenetre mgraph = new Fenetre();
  4.         VueRond r = new VueRond(100, 100, 20);
  5.         // ajout d'un élément dans le moteur graphique
  6.         mgraph.add(r);
  7.  
  8.         // boucle de mouvement + affichage
  9.         for(int iter = 0; iter<100; iter++) {
  10.             // mouvement du cercle
  11.             r.move();
  12.             // rafraichissement de l'affichage
  13.             mgraph.repaint();      
  14.             // temporisation (sinon, on ne voit rien)
  15.             Thread.sleep(50);
  16.         }
  17.         // Attention, le programme ne s'arrête pas, il faut fermer la fenêtre
  18.     }

Travail demandé

Donner le code de la classe VueRond dont les spécifications correspondent aux lignes du programme ci-dessus:

  • ligne 4: construction = corrdonnées x, y + rayon du cercle
  • ligne 6: VueRond doit être Drawable
    • Pour dessiner un rond (rouge):
g.setColor(Color.RED);
g.drawOval((int) x, (int) y, (int) (2*rayon), (int) (2*rayon));
  • pour la priorité, retrourner arbitrairement 0 (il n'y a qu'un objet à dessiner)
  • ligne 11: méthode move. Pour bouger en diagonale, incrémenter x et y.

Tutoriel pour l'utilisation du moteur physique (20-30 minutes)

Le moteur physique est très simple: il ne gère en fait que des murs (lignes ou rectangles) et des particules (circulaires) selon le schéma UML ci-dessous:

Le fonctionnement du moteur physique est assez basique: on ajoute des ObjectPhysique dans le moteur et on actualise la position des objets avec updateMovablePosition().

Le main ci-dessous illustre ce fonctionnement:

  1.     // Ajout d'Exception liées à la temporisation que l'on ne souhaite pas gérer
  2.     public static void main(String[] args) throws InterruptedException {      
  3.         // Moteur Physique 2D (vue dessus)
  4.         MoteurPhysique mphys = new MoteurPhysique();
  5.         // Création d'une boite = murs. position x, y + largeur/hauteur
  6.         RectanglePhysique mur = new RectanglePhysique(50, 50, 200, 200);
  7.         // Création d'une particule. position x, y, rayon, masse
  8.         CerclePhysique particule = new CerclePhysique(100, 100, 10, 1);
  9.         particule.setRandomDirectionAndVitesse();
  10.         mphys.add(mur);
  11.         mphys.add(particule);
  12.  
  13.         // graphiques: PARTIE A FAIRE
  14.         /*Fenetre mgraph = new Fenetre();
  15.         // Création des vues associées aux objets physiques
  16.         mgraph.add(new VueParticule(particule));
  17.         mgraph.add(new VueMur(mur));
  18.         */
  19.         // boucle de mouvement + affichage
  20.         for(int iter = 0; iter<200; iter++) {
  21.             // mouvements
  22.             mphys.updateMovablePosition();
  23.             // affichage  // A DECOMMENTER UNE FOIS LES CLASSES D'AFFICHAGE REALISEES
  24.             /* mgraph.repaint();      
  25.             // temporisation (sinon, on ne voit rien)
  26.             Thread.sleep(50);
  27.             */
  28.             if(!mphys.isMove()) {
  29.                 System.out.println("plus de mouvement => sortie");
  30.                 break;
  31.             }
  32.         }      
  33.         // Attention, le programme ne s'arrête pas, il faut fermer la fenêtre
  34.     }

Ce code est disponible dans le code fourni. Il faut:

  1. comprendre l'architecture du moteur
  2. valider le fait que l'exécution du code ci-dessus ne donne aucun affichage et n'est pas exploitable
  3. ajouter une partie graphique

Travail à fournir

Donner le code des classes VueMur et VueParticule pour pouvoir afficher le résultat de la simulation physique. Le diagramme UML de ces classes est donné ci-dessous:

Les vues intègrent les objets physiques. Les classes sont simples à développer et il est possible très facilement de proposer une nouvelle vue sans rien changer au modèle.

========================================================================================== ==========================================================================================

Partie 2: simulation de l'évacuation d'une foule

Nous voulons maintenant ré-utiliser le code ci-dessus pour simuler le comportement d'une foule lors de l'évaluation d'une salle de spectacle. Les principales étapes du projet sont:

  1. manipulation d'une énumération [15 minutes]
  2. récupération des fichiers de description de la salle de spectacle [15 minutes]
  3. construction de la salle, des personnes [1h]
  4. ajout d'une intelligence sur nos personnes et mise en place de la simulation [2h]
  5. ajout de cas particuliers [1h]

Manuipulation d'une énumération

une énumération vise à créer un nouveau type de données pouvant prendre plusieurs valeurs constantes. L'énumération est très proche d'une classe mais la philosophie d'usage est orientée static:

  • dans un fichier .java portant le nom de l'énumération (comme une classe)
  • pas de new pour la création / pas de constructeur (en général)
  • méthodes static pour la manipulation

Dans le cadre du projet, il s'agit de gérer la salle de spectacle en distinguant les murs, le vide, la scene, les bornes exit... Le code de l'énumération qui nous intéresse (également disponible dans les sources fournies):

  1. // déclaration enum
  2. public enum Terrain {
  3.     // liste des constantes possible comme valeur de Terrain
  4. Mur, Scene, Vide, Safe,
  5. BorneExit_9, BorneExit_8, BorneExit_7, BorneExit_6, BorneExit_5,
  6. BorneExit_4, BorneExit_3, BorneExit_2, BorneExit_1;
  7.  
  8.     // tableau de conversion des caractères vers les Terrain
  9.     private final static String conversion = "#. +987654321";
  10.     // niveau de sécurité de chaque terrain: les personnes seront attirées par les niveaux les plus faibles
  11.     private final static int[] securitylevel = {12,10,10,0,9,8,7,6,5,4,3,2,1};
  12.  
  13.     // méthodes static de conversion et de calcul du niveau de sécurité
  14.     public static Terrain conv(char c){
  15.         int ind = Terrain.conversion.indexOf((int) c);
  16.         if(ind ==-1)
  17.             throw new RuntimeException("Terrain inconnu");
  18.         return Terrain.values()[ind];
  19.     }
  20.     public static char conv(Terrain t){
  21.         return conversion.charAt(t.ordinal());
  22.     }
  23.     public static int level(Terrain t){
  24.         return securitylevel[t.ordinal()];
  25.     }
  26.     // identification des terrains qui attirent les personnes
  27.     public static boolean isTarget(Terrain t) {
  28.         return !(t==Mur || t == Scene || t== Vide);
  29.     }
  30.     // fonction de controle
  31.     public static void display(Terrain[][] trk){
  32.         for(int i=0; i<trk.length; i++){
  33.             for(int j=0; j<trk[0].length; j++)
  34.                 System.out.print(conv(trk[i][j]));
  35.             System.out.println();
  36.         }
  37.     }
  38. }

Attention: tous les accès dans Terrain se font de manière static:

  • création d'un terrain: Terrain t = Terrain.Mur

Travail à fournir

Tester la création de terrain, la conversion vers et à partir des caractères, l'égalité...

Lecture de fichier, importation de la salle de spectacle

La classe MapFactoryFromFile_Matrix ne contient qu'une méthode:

  • public static Terrain[][] build(String filename)

A partir d'un fichier .trk comme celui ci: lien, la méthode permet de lire le fichier et de créer la matrice de Terrain contenant toutes les informations du fichier.

Travail à fournir

Tester la lecture de fichier et vérifier que vous obtenez bien une salle de spectacle (méthode d'affichage console présente dans l'énumération Terrain). Vous devriez obtenir quelque chose comme:

##################################################
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#                                                #
#    3 ########## 3        3 ################ 3  #
#                                                #
#     2         2               2        2       #
#                                                #
########11##1#####################11########11####
++++++++++++++++++++++++++++++++++++++++++++++++++

Création de la salle de spectacle et des personnes

Classe Salle

La salle de spectacle est centrée autour de la gestion de la matrice de Terrain. Il est nécessaire d'introduire un système de gestion des coordonnées cohérent:

  • toutes les personnes auront des coordonnées réelles 2D gérées par des Vecteur2D (la classe est donnée dans le package tools),
  • toutes les cases de terrain correspondent à un carré de un cote de 10 dans cet univers. Cette valeur de 10 peut
    • soit être donnée lors de la construction de la Salle,
    • soit être stockée comme une constante dans la classe Salle.

Nous proposons l'architecture suivante (et ce n'est qu'une proposition) :

où:

  • le coté est donné en argument lors de la construction,
  • la liste des cibles (toutes les bornes exit et les cases Safe) sont stockées dans une ArrayList,
  • deux méthodes permettent de récupérer une case de la matrice
    • la classique (int, int)
    • la pratique (Vecteur2D) pour récupérer directement ce qui se trouve sous les coordonnées d'une personne. Attention, il faut diviser les coordonnées par le cote et convertir les double en int pour accéder aux cases.
  • la méthode boolean isVisible(Vecteur2D A, Vecteur2D B) est plus ambitieuse: elle retourne vrai si les points A et B ne sont pas séparés par un mur et faux sinon.

Le code de cette méthode est le suivant:

    public boolean isVisible(Vecteur2D source, Vecteur2D target){
        Vecteur2D dir = target.minus(source);
        dir.normalize();
        while(get(source) != Terrain.Mur){
            source = source.add(dir);
            if(source.distance(target) < 2)
                return true;
        }
        return false;
    }

La géométrie de base est facile à comprendre... A condition de prendre un brouillon et un stylo.

Travail à fournir

Donner le code de la classe Salle. Trouver une manière élégante de construire cet objet à partir d'un fichier. Tester le bon fonctionnement de cette classe.

Classe Mur

Un mur est simplement une extension d'un rectangle. De base le code de la classe se limitera à :

public class Mur extends RectanglePhysique{
    public Mur(double x, double y, double larg, double haut) {
        super(x, y, larg, haut);
    }  
}

Classe Personne

Nous sommes face à plusieurs questions fondamentales pour développer cette classe:

1 : Entre la personne et le moteur physique, qui fait quoi?

Afin de pouvoir faire bouger les personnes dans l'espace physique, nous proposons de les faire hériter des particules. La répartition des roles sera la suivante:

  • la personne choisit sa direction et sa vitesse... Mais ne bouge pas sa position
  • le moteur physique effectue les mouvements des particules sur la base des souhaits des personnes et des contraintes physiques

Note: on voit bien sur la figure UML ci-dessous que le réglage de la direction d'un CerclePhysique est par défaut en private. Nous proposons dans le cadre de la classe personne d'ouvrir la redéfinition pour faciliter l'interfaçage avec la stratégie.

2 : Une personne, plusieurs comportements possibles... Quelle architecture?

Comment faire pour avoir un code unique de Personne avec plusieurs comportements associés (celui qui fonce vers la sortie, celui qui veut rester à coté d'une autre personne, celui qui avance lentement...). La solution usuelle pour réaliser cela est d'utiliser un Design Pattern Strategy:

  • La personne a pour attribut un objet Strategy
  • Quand elle veut bouger, une personne demande à sa stratégie ce qu'elle doit faire !

3: Mais du coup, c'est quoi une strategy?

C'est une Interface (uniquement une spécification) qui impose la création d'une méthode deplacement prenant en argument une personne. Comme la méthode setDir est public dans Personne, la stratégie peut effectivement régler la direction qu'elle va prendre.

Au final dans Personne, on se retrouve avec une méthode move un peu torturée (mais standard):

    public void move(){
        target = str.deplacement(this); // usage de la strategie sur la personne qui invoque move
        super.setVit(1.); // arbitraire, ça pourra dépendre des personnes...
                          // En fait, ça pourrait être fait dans la stratégie si on ouvre la redéfinition
    }

Travail à fournir

Dans un premier temps, créer une personne sans stratégie: on lui affecte simplement une direction arbitraire et on vérifie qu'elle bouge lorsque le moteur physique fait une itération.

Dans une second temps, reproduire le premier mouvement avec une stratégie très simple.

La vérification de mouvement sera effectuée dans la console à l'aide des méthodes toString().

Stratégie avancée & manipulation complexe des ArrayList

La stratégie complexe:

  • connait la salle
    • la seule manière de résoudre cela consiste à avoir une Salle en attribut, initialisé lors de la création de la stratégie
  • teste toutes les cibles et retourne la plus proche ET la plus intéressante (niveau de sécurité)
  • calcule la direction vers cette cible
  • l'affecte à la personne à diriger

L'étape en vert est critique, nous la décomposons en deux sous-étapes:

  1. Construire un critère de classification d'une cible par rapport à une autre
  2. Trier toutes les cibles (normalement présentes dans un attribut de la Salle)

1. critère d'ordonnancement

Soient, une cible c1, une cible c2, une position p (pour la personne) et une salle s. Pour rappel, vous avez un algorithme de test de la visibilité dans la salle. L'algorithme est le suivant:

  • si c1 et c2 ne sont pas visibles depuis p: égalité
  • si c1 n'est pas visible c2 est meilleure
  • si c2 n'est pas visible c1 est meilleure
  • scoreC1 = distance(p,c1) + niveauDeSécurité(c1) * 1000 // le plus petit est le meilleur
  • scoreC2 = ...
  • si scoreC1 < scoreC2 alors c1 est meilleure
  • si scoreC1 > scoreC2 alors c2 est meilleure
  • sinon égalité

2. tri d'une liste

En fait, nous n'avons pas besoin de trier la liste, mais simplement de trouver la cible de score minimal. Nous utiliserons la méthode min présente dans la classe Collections. Cela n'est pas trivial car nous souhaitons trouver le minimum dans une liste, PAR RAPPORT A NOTRE CRITERE D'ORDONNANCEMENT! Heureusement, le cas est prévu: il est possible d'utiliser la fonction min en donnant en argument un objet contenant une fonction ad'hoc d'ordonnancement:

Vecteur2D cibleOpt = Collections.min(salle.getListeCibles(), new ComparateurPosition(salle, p.getPos()));

Se pose immédiatement la question de la définition d'un Comparateur... Un comparateur est une classe qui:

  • implémente l'interface Comparator<Vecteur2D> (interface standard existant déjà dans java)
  • par contrat, cette classe doit fournir le code de :
public int compare(Vecteur2D c1, Vecteur2D c2)

retournant 1 si c1 est plus grand que c2, -1 si c2 est plus grand et 0 en cas d'égalité.

Nous ne sommes pas encore sortis d'affaire: la comparaison de c1 et c2 demande beaucoup de connaissances externes: (1) la salle pour avoir accès au niveau de sécurité des points, (2) la personne, pour calculer la distance entre elle et les cibles! Comme précédemment, la seule manière de procéder est de donner ces informations lors de la construction du comparateur et de les stocker en attribut.

Travail à fournir

  1. Construire un comparateur
  2. Vérifier son fonctionnement avec une personne et une liste de cible, en affichant le résultat de la recherche du minimum
  3. Construire une stratégie complexe dirigeant la personne vers la sortie la plus proche

Le but est d'obtenir un comportement comme celui-ci:

Au final, une possibilité d'architecture est la suivante:

Interface graphique

Ajouter des vues pour les différents éléments de la simulation:

  • les vues de personnes intègrent des personnes,
  • les vues de murs intègrent des murs,
  • pour les autres éléments de la simulation, il faut ajouter des vues... Si on souhaite voir ces éléments. Dans les illustrations fournies, j'ai ajouté une visualisation des bornes exit.

Population

Chercher une architecture pour générer une population de manière élégante. Lancer la simulation!

Partie 3 : Extensions possible

une liste non exhaustive de ce qui est possible:

Récupération du nombre de collisions des particules pour détecter qui risque de souffrir dans la sortie

Amélioration de la stratégie de base des individus

Ajout de stratégies avancées comme les personnes qui ne veulent pas être séparées, celles qui visent un coin de la salle, celles qui vont à contre-courant...

Amélioration des vues

...