Slick2d, leçon 11 :: Séparation en objets (suite)

21 September 2014 09:37 Slick2d - Leçons 10-19 Java, Jeux, Slick2d, Tutorial

Dans la leçon précédente, nous avons commencé à convertir notre code à l'utilisation d'objets, aujourd'hui, nous allons voir la suite.

Code :: Camera

Nous allons créer une classe camera, dont son but sera de traquer le joueur. Comme nous l'avons vu dans la leçon 4, la camera est composé de deux coordonnée, x et y auxquelles nous ajoutons le joueur que nous allons suivre.

public class Camera {
  private Player player;
  private float xCamera, yCamera;
}

Nous avons besoin du joueur, nous pouvons le passer en paramètre du constructeur :

public Camera(Player player) {
  this.player = player;
  this.xCamera = player.getX();
  this.yCamera = player.getY();
}

Nous allons ajouter la méthode place() qui sera appelée dans l'affichage pour cadrer l'affichage comme nous l'avons vu dans la leçon 5.

public void place(GameContainer container, Graphics g) {
  g.translate(container.getWidth() / 2 - (int) this.xCamera, container.getHeight() / 2 - (int) this.yCamera);
}

Ensuite il nous reste simplement la méthode update() qui se chargera de la traque du joueur comme nous l'avons déjà vu.

public void update(GameContainer container) {
  int w = container.getWidth() / 4;
  if (this.player.getX() > this.xCamera + w) {
    this.xCamera = this.player.getX() - w;
  } else if (this.player.getX() < this.xCamera - w) {
    this.xCamera = this.player.getX() + w;
  }
  int h = container.getHeight() / 4;
  if (this.player.getY() > this.yCamera + h) {
    this.yCamera = this.player.getY() - h;
  } else if (this.player.getY() < this.yCamera - h) {
    this.yCamera = this.player.getY() + h;
  }
}

Code :: Classe Game

Mettons à jour le code correspondant à l'utilisation de la camera dans la classe Game :

public class ObjectsGame {
  // [...] déclaration des attributs (cf leçon 10)
  private Camera camera = new Camera(player);

  // [...]

  @Override
  public void render(GameContainer container, Graphics g) throws SlickException {
    this.camera.place(container, g);
    // affichage carte et joueur (leçon 10)
  }

  @Override
  public void update(GameContainer container, int delta) throws SlickException {
    // trigger et déplacement (cf leçon 6 & 3)
    this.camera.update(container);
  }
}

A ce stade le code compile et fonctionne.

Code :: Classe PlayerController

Nous allons revoir la gestion du déplacement du joueur avec l'utilisation d'un KeyListener. C'est une interface qui permet de recevoir et de traiter les événements clavier.  Commençons par en faire une implémentation vide.

public class PlayerController implements KeyListener {
  @Override
  public void setInput(Input input) { }

  @Override
  public boolean isAcceptingInput() { return true; }
 
  @Override
  public void inputEnded() { }

  @Override
  public void inputStarted() { }

  @Override
  public void keyPressed(int key, char c) { }

  @Override
  public void keyReleased(int key, char c) { }
}

Nous retrouvons deux méthodes que nous connaissons bien : keyPressed(), et keyReleased(). Complétons les comme nous avons vu dans les leçons 3 et 5.

@Override
public void keyPressed(int key, char c) {
  switch (key) {
  case Input.KEY_UP:
    this.player.setDirection(0);
    this.player.setMoving(true);
    break;
  case Input.KEY_LEFT:
    this.player.setDirection(1);
    this.player.setMoving(true);
    break;
  case Input.KEY_DOWN:
    this.player.setDirection(2);
    this.player.setMoving(true);
    break;
  case Input.KEY_RIGHT:
    this.player.setDirection(3);
    this.player.setMoving(true);
    break;
  }
}

@Override
public void keyReleased(int key, char c) {
  this.player.setMoving(false);
}

Comme on peu le voir il nous faut le joueur. Définissons un constructeur et passons le en argument.

public class PlayerController implements KeyListener {
  private Player player;

  public PlayerController(Player player) {
    this.player = player;
  }
  // [...]
}

Code :: Classe Game

Maintenant que cela est fait il nous faut mettre à jour le code de la classe principale. Il est nécessaire d'instancier le nouveau contrôleur et l'ajouter à l'objet Input pour permettre sa gestion. Nous pouvons également supprimer tous le code devenu inutile.

@Override
public void init(GameContainer container) throws SlickException {
  // initialisation des autres objets
  PlayerController controller = new PlayerController(this.player);
  container.getInput().addKeyListener(controller);
}

Code :: Les Triggers

C'est certainement la partie la plus ardue de ces deux Tutos. J'ai longuement réfléchit à une solution générique et évolutive, j'ai fini par choisir la solution la plus simple, je reviendrais peut-être dessus si j'en ai le temps. J'ai fait ce choix pour éviter que mes lecteurs décrochent, je sais que ces deux tutos ne son pas les plus marrant. Je propose ici la création d'une classe TriggerController.

public class TriggerController {
  private Map map;
  private Player player;

  public TriggerController(Map map, Player player) {
     this.map = map;
     this.player = player;
  }
}

Déplaçons y l’ensemble du code utilisé pour les triggers présent dans la classe Game.

public void update() throws SlickException {
  player.setOnStair(false);
  for (int objectID = 0; objectID < map.getObjectCount(); objectID++) {
    if (isInTrigger(objectID)) {
      if ("teleport".equals(map.getObjectType(objectID))) {
        teleport(objectID);
      } else if ("stair".equals(map.getObjectType(objectID))) {
        player.setOnStair(true);
      } else if ("change-map".equals(map.getObjectType(objectID))) {
        changeMap(objectID);
      }
    }
  }
}

private boolean isInTrigger(int id) {
  return player.getX() > map.getObjectX(id)
    && player.getX() < map.getObjectX(id) + map.getObjectWidth(id)
    && player.getY() > map.getObjectY(id)
    && player.getY() < map.getObjectY(id) + map.getObjectHeight(id);
}

private void teleport(int objectID) {
  player.setX(Float.parseFloat(map.getObjectProperty(objectID, "dest-x", Float.toString(player.getX()))));
  player.setY(Float.parseFloat(map.getObjectProperty(objectID, "dest-y", Float.toString(player.getY()))));
}

private void changeMap(int objectID) throws SlickException {
  teleport(objectID);
  String newMap = map.getObjectProperty(objectID, "dest-map", "undefined");
  if (!"undefined".equals(newMap)) {
    map.changeMap("map/" + newMap);
  }
}

Code :: Classe Game

Nous approchons de la fin. Mettons une dernière fois à jour la classe Game qui commence à devenir ridiculement simple. Instancions notre contrôleur de trigger juste après la carte et le joueur :

public class ObjectsGame extends BasicGame {
  private GameContainer container;
  private Map map = new Map();
  private Player player = new Player(map);
  private Camera camera = new Camera(player);
  private TriggerController triggers = new TriggerController(map, player);

  // [...]
}

Il ne reste qu’à faire appel à la méthode update en lieux et place ou nous faisions l'ancienne mise à jour.

@Override
public void update(GameContainer container, int delta) throws SlickException {
  this.triggers.update();
  // mise à jour du joueur et de la carte
}

Enfin à ce stade nous avons finit le re-factoring nous nous retrouvons avec 6 classes, chacune ne se chargeant que d'une chose. Il ne reste qu'à tester que rien n'as changer et que tous fonctionne parfaitement :]

tuto-slick2d-065-class-map

Pour aller plus loin

Nous n'avons pas fait la musique, cela sera nécessaire quand nous voudront la changer sur un trigger, pour l'instant cela restera comme cela. Ce n'est pas parfait, les triggers ne sont pas super générique et nous somme loin de respecter de bon principe d'objet comme le SRP, mais c'est déjà pas mal et cela me permettra d'introduire plus facilement de nouvelle notion.

J'ai bien conscience que cela fait un gros tuto, de fait dans les ressources j'ajoute, en plus du traditionnel lien vers le repository git, un lien vers chaque classe dans ce repository pour pouvoir explorer le code au complet pour ceux qui ne sont pas à lèse avec ce gestionnaire de version.

Ressources

par Shionn, dernière modification le 09 April 2017 16:25
13 réflexions au sujet de « Slick2d, leçon 11 :: Séparation en objets (suite) »
  • Akio 24 October 2014 16:31

    Bon tutoriel dans l'ensemble ! Dommage que la partie refactoring soit survolé, certes il faut apprendre les notions d'objets mais il faudra bien que les débutant y passe un jour, et là ça va juste les forcer à copier/coller sans forcement comprendre !

    En tout cas merci j'ai pu apprendre les base de Slick en quelques heures pour pouvoir le comparer avec LibGdx. Slick est un peu moins complet mais beaucoup plus intuitif, LibGdx fait un peu usine à gaz mais a comme avantage sa portabilité sur Android.

    D'où ma question, Slick est il portable sur Android ?

  • Shionn 26 October 2014 09:24

    Bonjour, avant tous merci.

    Comme je l'ai dis mon but n'est pas de faire un cours sur l'objet. Peut-être y reviendrais-je... Si juste au lieu de faire un copier/coller, on fais un copier/réfléchir/coller, alors c'est déjà beaucoup !

    Tu as très bien résumé la différence entre slick et LibGDX. C'est justement à cause de ces différences que j'ai choisi Slick2d pour ces tutos. Mon but initial était de simplement abordé la base d'un jeux vidéo, au fur et a mesure je m'en éloigne.

    Et malheureusement Slick2D, n'est pas portable sur Android, je sais que tu as déjà trouvé ta réponse. mais je précise quand même. Cependant, sur le forum Slick2D, je crois avoir vu un portage pour android, je ne connais pas l'état d'avancement du projet.

  • Akio 26 October 2014 11:34

    Dommage travaillant sur Android je devrais me contenter de LibGdx ! Je vais neamoins continuer à suivre tes tuto qui reste simple et efficace, en esperant que le projet pour le portage android aboutisse !

  • yukorin 02 August 2015 17:23

    Une question qui peut paraitre bête : comment se faire pivoter, "sauter" et tourner sur lui même un élément affiché a l’écran ( un personnage par exemple)? Merci !

  • Shionn 16 August 2015 07:35

    Tu dois gérer ton objet que tu veux animer comme le personnage du jeux à l'aide d'animation.

    PS : désoler d'avoir pris du temps à te répondre.

  • Impa 14 November 2016 22:23

    Salut Shionn ! Tres bonne leçon comme d'habitude La séparation en objets était primordiale, au lieu d'un one-page condensé tout dégueulasse x)

    Une question me taraude cependant. Apres refactor du code, on s'aperçoit que la classe Game est très légère :

    • Déclaration des attributs de Map, Player, Camera..
    • Un Controlleur avec super("nom du jeu")
    • 3 méthodes init/render/update qui ne font qu'instancier des objets issues des autres classes

    Mais lors de la compilation, la lecture du code est elle "procédurale" ? De Haut en bas ? Je m'explique :

    J'ai constaté, après refactor, que j'avais des bordures noires autour de ma carte.tmx Le problème venait forcément de la classe Map ou d'une instance de Map dans Game

    En effet, dans la classe Game, dans la méthode render, j'ai littéralement

    this.camera.place(container, g);
    this.map.renderBackground();
    this.player.render(g);
    this.map.renderForeground();
    

    En déplacement ces paramètres, les bugs sont plus ou moins différents ! Si on instancie la caméra en dernier, le contour noir disparait mais la caméra centrée sur le personnage aussi !

    Je suis donc étonné que la classe Main "Game" soit "lu de haut en bas" comme du "procédural" Parce que pour l'instant, le jeu est assez sommaire. Mais souhaitant approfondir ce jeu, les classes et les relations vont se multiplier Donc le potentiel de bug aussi

    Aurais tu une idée pour pallier à cela ? Desole de raconter ma vie XD En te remerciant d'une future réponse, continue tes supers tutos

  • Impa 14 November 2016 22:26

    Edit : Je fais trop de framework

    C'est un constructeur, pas un controlleur x)

  • Ultraime 17 November 2016 11:48

    Salut Impa, la lecture du code est bien procédurale, essaie de modifier les paramètres de ta caméra pour ne plus afficher le bord noir de la carte

  • Shionn 17 November 2016 11:56

    Désoler j'ai oublié de répondre.

    Oui l’exécution du code est bien procédural (sa compilation heureusement ne l'ai pas). Dans les commentaire du tuto sur les caméras, je donne des piste pour que la camera ne sorte pas de la carte :]

  • Atanamis 06 July 2017 17:29

    Salut, j'ai un soucis avec le refactoring du code, je n'ai pas tout a fait suivit ton modele (j'ai des fonctions en moins car je ne pouvait pas ajouter de trigger dans ma map sans que ca la rende incompatibles et je ne me suis pas attardé sur la partie son), j'arrive dans l'ensemble a tout afficher cependant il ne reagit pas lors des evennements

    voici mon code

    public class Joueur {
      private float x = 300, y = 300;
      private int direction = 0;
      private boolean moving = false;
      private Animation[] animations = new Animation[8];
      private boolean onStair = false;
      private Map map;
    
      public Joueur(Map map) {
        this.map = map;
      }
    
      public void init() throws SlickException {
        SpriteSheet spriteSheet = new SpriteSheet("map/tuiles/LPC Base Assets/sprites/people/soldier.png", 64, 64);
        this.animations[0] = loadAnimation(spriteSheet, 0, 1, 0);
        // chargement des autres animations
      }
    
      private Animation loadAnimation(SpriteSheet spriteSheet, int startX, int endX, int y) {
        Animation animation = new Animation();
        for (int x = startX; x < endX; x++) {
          animation.addFrame(spriteSheet.getSprite(x, y), 100);
        }
        return animation;
      }
      
      public void render(Graphics g) throws SlickException {
        g.setColor(new Color(0, 0, 0, .5f));
        g.fillOval(x - 16, y - 8, 32, 16);
        g.drawAnimation(animations[direction + (moving ? 4 : 0)], x-32, y-60);
      }
      
      public void update(int delta) {
        if (this.moving) {
          float futurX = getFuturX(delta);
          float futurY = getFuturY(delta);
          boolean collision = this.map.isCollision(futurX, futurY);
          if (collision) {
            this.moving = false;
          } else {
            this.x = futurX;
            this.y = futurY;
          }
        }
      }
    
      private float getFuturX(int delta) {
        float futurX = this.x;
        switch (this.direction) {
          case 1: futurX = this.x - .1f * delta; break;
          case 3: futurX = this.x + .1f * delta; break;
        }
        return futurX;
      }
    
      private float getFuturY(int delta) {
        float futurY = this.y;
        switch (this.direction) {
          case 0: futurY = this.y - .1f * delta; break;
          case 2: futurY = this.y + .1f * delta; break;
          case 1: if (this.onStair) {
            futurY = this.y + .1f * delta;
          } break;
          case 3: if (this.onStair) {
            futurY = this.y - .1f * delta;
          } break;
        }
        return futurY;
      }
    
    public class Game extends BasicGame {
      public Game() {
        super("RPG");
      }
    
      private GameContainer container;
      private Map map = new Map();
      private Joueur player = new Joueur(map);
      private float xCamera = player.getX(), yCamera = player.getY();
      private Camera camera = new Camera(player);
      
      @Override
      public void init(GameContainer container) throws SlickException {
        this.container = container;
        this.map.init(); 
        this.player.init();
        ControlerJoueur controller = new ControlerJoueur(this.player);
        this.container.getInput().addKeyListener(controller);
      }
      
      public void render(GameContainer container, Graphics g) throws SlickException {
        this.camera.place(container, g);
        this.map.render();
        this.player.render(g);
      }
      
      @Override
      public void update(GameContainer container, int delta) throws SlickException {
        this.player.update(delta);
        this.camera.update(container);
      } 
    }
    
    public class Camera {
      private Joueur player;
      private float xCamera, yCamera;
      
      public Camera(Joueur player) {
        this.player = player;
        this.xCamera = player.getX();
        this.yCamera = player.getY();
      }
      
      public void place(GameContainer container, Graphics g) {
        g.translate(container.getWidth() / 2 - (int) this.xCamera, container.getHeight() / 2 - (int) this.yCamera);
      }
      
      public void update(GameContainer container) {
        int w = container.getWidth() / 4;
        if (this.player.getX() > this.xCamera + w) {
          this.xCamera = this.player.getX() - w;
        } else if (this.player.getX() < this.xCamera - w) {
          this.xCamera = this.player.getX() + w;
        }
        int h = container.getHeight() / 4;
        if (this.player.getY() > this.yCamera + h) {
          this.yCamera = this.player.getY() - h;
        } else if (this.player.getY() < this.yCamera - h) {
          this.yCamera = this.player.getY() + h;
        }
      }
    }
    
    public class ClientControlerGraphic {
      public static void main(String[] args) throws SlickException {
        new AppGameContainer(new Game(), 800, 600, false).start();
      }
    }
    
    public class ControlerJoueur implements KeyListener {
    
      private Joueur player;
    
      public ControlerJoueur(Joueur player) {
        this.player = player;
      }
      
      public void inputEnded() {}
      public void inputStarted() {}
      public boolean isAcceptingInput() {return false;}
      public void setInput(Input arg0) {}
    
      public void keyPressed(int key, char c) {
        switch (key) {
          case Input.KEY_UP:
            this.player.setDirection(0);
            this.player.setMoving(true);
            break;
          case Input.KEY_LEFT:
            this.player.setDirection(1);
            this.player.setMoving(true);
            break;
          case Input.KEY_DOWN:
            this.player.setDirection(2);
            this.player.setMoving(true);
            break;
          case Input.KEY_RIGHT:
            this.player.setDirection(3);
            this.player.setMoving(true);
            break;
        }
      }
    
      public void keyReleased(int key, char c) {
        this.player.setMoving(false);
      }
    }
    
    public class Map {
      private TiledMap tiledMap;
    
      public void init() throws SlickException {
          this.tiledMap = new TiledMap("map/map4.tmx");
      }
      
       public boolean isCollision(float x, float y) {
          int tileW = this.tiledMap.getTileWidth();
          int tileH = this.tiledMap.getTileHeight();
          int logicLayer = this.tiledMap.getLayerIndex("logic");
          Image tile = this.tiledMap.getTileImage((int) x / tileW, (int) y / tileH, logicLayer);
          boolean collision = tile != null;
          if (collision) {
            Color color = tile.getColor((int) x % tileW, (int) y % tileH);
            collision = color.getAlpha() > 0;
          }
          return collision;
       }
       
       public void changeMap(String file) throws SlickException {
         this.tiledMap = new TiledMap(file);
       }
       
       public void render() {
         this.tiledMap.render(0, 0);
       }
    }
    

    je n'ai pas ecrit ici les getters/setters de joueur pour gagner un peu de place

    Merci d'avance

  • Shionn 07 July 2017 13:07

    Salut,

    Ton contrôleur est désactivé. il faut renvoyer true dans isAcceptingInput

  • Atanamis 07 July 2017 14:51

    Merci du coup de main, ca marche maintenant

  • Shionn 07 July 2017 16:05

    Pas de soucis.

    Je me suis permis de ré-indenter un poil ton commentaire pour qu'il soit plus cours/lisible

Laissez un commentaire

Vous pouvez utilisez du markdown pour la mise en forme

Votre adresse de messagerie ne sera pas publiée.

Temporairement, pour lutter contre les bots, il n'est pas permis de mettre http:// dans le commentaire.