Slick2d, leçon 10 :: Séparation en objet

07 September 2014 09:35 Slick2d - Leçons 10-19 Java, Jeux, Slick2d, Tutorial

Enfin, voici la suite des tutos sur Slick2D. Aujourd'hui un sujet dont j'ai peur d'aborder, ce tuto ne va pas introduire de nouveau concepts ou  de technique de jeux vidéo mais une réorganisation du code en plusieurs objets. Pourquoi me demanderez vous ? J'ai besoin de cette réorganisation pour simplifier l’implémentation de nouvelle fonctionnalité, chose qui risque de devenir ardu en gardant tous le code dans la même classe. Et aussi qu’actuellement le code n'est pas très propre et n'est pas un bon exemple.

Je suis conscient que beaucoup d'entre vous n'ont pas de concept d'objet et que la plupart des autres en ont des mauvais, aussi si vous êtes dans un de ces deux cas je vous invite à trouver un article parlant de programmation orienté objet.

Ce tuto n'est pas un cours sur l'objet, loin de la, mais permet de donner un exemple de séparation en plusieurs objet de ce code. Ceux qui ont de bonne connaissance en organisation du code en plusieurs objet peuvent sauter ces deux leçons, et aller directement à la leçon 12 (pas encore disponible).

Préparation

Comme je l'ai dis, le but est ici de séparer le code par spécialisation et pour cela il nous faut faire le point sur l’ensemble des objets actuellement contenus dans notre jeux. Pour les plus évidents, il y a ceux qu'on vois :

  • La carte
  • Le joueur

D'autre objets sont plus dur à cerner, comme ceux prenant en charge l'affichage et ceux prenant en charge les commandes du clavier ou les événements de jeux.

  • La camera : qui assure la position de l'affichage
  • Les évènements : ce qui assure les triggers (leçon 6)
  • Les contrôleurs : ce qui se charge des contrôles clavier, souris, son, déplacement, ia, etc...

Enfin tous ce monde est regroupé dans un objet qui est le jeux. Il se charge de leurs organisation, leurs initialisations et l'ordonnancement entre ces objets.

  • Le jeu

Comme cela fait beaucoup de chose d'un seul coup nous allons séparer ce tuto en deux. Aujourd'hui nous allons nous concentrer sur le joueur et la carte. Globalement ce que nous allons faire c'est de l'extraction de code de notre seul fichier vers plusieurs fichiers.

Code :: Classe Map

Commençons par la carte, ce que nous allons faire ici c'est encapsuler l'objet TileMap dans une classe Map. Puis dans un second temps nous définirons les méthodes nécessaires à l'initialisation, l'affichage et enfin définirons les méthodes de logique utile aux triggers comme la téléportation ou à la collision.

public class Map {
  private TiledMap tiledMap;
  
}
Dans la méthode init() il nous suffit d’extraire le code de la classe main, comme nous l'avons vu dans la <a title="Slick2d, leçon 2 :: Affichage d’une carte" href="http://www.shionn.org/slick2d-lesson-2-map-display">leçon 2.
public class Map {
  // déclaration des attributs [...]
  public void init() throws SlickException {
    this.tiledMap = new TiledMap("map/exemple-change-map.tmx");
  }

}

Pour l'affichage il ne faut pas faire une seul méthode d'affichage mais deux. En effet comme nous l'avons vu dans la leçon 5 il faut afficher l’arrière plan puis le personnage et enfin l'avant-plan. Donc logiquement nous allons afficher la carte en deux étapes, donc deux méthodes.

public class Map {
  // déclaration des attributs [...]
  // méthode d'initialisation [...]

  public void renderBackground() {
    this.tiledMap.render(0, 0, 0);
    this.tiledMap.render(0, 0, 1);
    this.tiledMap.render(0, 0, 2);
  }

  public void renderForeground() {
    this.tiledMap.render(0, 0, 3);
    this.tiledMap.render(0, 0, 4);
  }
}

C'était les premières méthodes faciles. Passons à la collision. L'ancienne méthode isCollision que nous avons vu dans la leçon 5 est déplaçable directement de la classe Game vers notre nouvelle classe Map. Il faut juste penser à changer sa visibilité en public pour qu'elle puisse être appelé par la classe Game.

public class Map {
  // déclaration des attributs [...]
  // méthodes d’initialisation et d'affichage  [...]
  
  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;
  }
}

Passons enfin à la partie la plus délicate, les méthodes nécessaires aux triggers. Comme nous l'avons vu dans la leçon 6, nous utilisons les méthodes getObjectCount, getObjectType, getObjectX, getObjectY, getObjectWith, getObjectHeight et getObjectProperty de la classe TiledMap. Pour aujourd’hui nous allons simplement encapsuler chacune de ces méthodes dans notre nouvelle classe en omettant volontairement le premier argument puisqu'il s'agit du numéro de calque qui est toujours le même 0.

public class Map {
  // déclaration des attributs [...]
  // méthodes d’initialisation et d'affichage  [...]
  // méthode isCollision [...]

  public int getObjectCount() {
    return this.tiledMap.getObjectCount(0);
  }
  public String getObjectType(int objectID) {
    return this.tiledMap.getObjectType(0, objectID);
  }
  public float getObjectX(int objectID) {
    return this.tiledMap.getObjectX(0, objectID);
  }
  public float getObjectY(int objectID) {
    return this.tiledMap.getObjectY(0, objectID);
  }
  public float getObjectWidth(int objectID) {
    return this.tiledMap.getObjectWidth(0, objectID);
  }
  public float getObjectHeight(int objectID) {
    return this.tiledMap.getObjectHeight(0, objectID);
  }
  public String getObjectProperty(int objectID, String propertyName, String def) {
    return this.tiledMap.getObjectProperty(0, objectID, propertyName, def);
  }
}

Enfin il nous faut créer une méthode changeMap() qui sera utilisée pour la téléportation vers une autre carte, comme nous avons vu dans la leçon 9.

public class Map {
  // [...]    
  public void changeMap(String file) throws SlickException {
    this.tiledMap = new TiledMap(file);
  }
}

Code :: Classe Joueur

Continuons par la plus simple à appréhender mais pas forcément la plus simple à écrire, le joueur. Dans les leçons 3 et 6 nous avons vu qu'un personnage était composé de quelques variables, nous pouvons donc écrire notre classe en se contentant de la structure :

public class Player {
  private float x = 300, y = 300;
  private int direction = 0;
  private boolean moving = false;
  private Animation[] animations = new Animation[8];
  private boolean onStair = false;
}

Ensuite on peu ajouter le code d'initialisation tel que nous l'avons vu dans la leçon 3, mais cette fois-ci au lieux de le mettre dans la classe Game on peu le mettre dans la classe Player.

public class Player {

  // déclaration des attributs [...]

  public void init() throws SlickException {
    SpriteSheet spriteSheet = new SpriteSheet("sprite/character.png", 64, 64);
    this.animations[0] = loadAnimation(spriteSheet, 0, 1, 0);
    this.animations[1] = loadAnimation(spriteSheet, 0, 1, 1);
    this.animations[2] = loadAnimation(spriteSheet, 0, 1, 2);
    this.animations[3] = loadAnimation(spriteSheet, 0, 1, 3);
    this.animations[4] = loadAnimation(spriteSheet, 1, 9, 0);
    this.animations[5] = loadAnimation(spriteSheet, 1, 9, 1);
    this.animations[6] = loadAnimation(spriteSheet, 1, 9, 2);
    this.animations[7] = loadAnimation(spriteSheet, 1, 9, 3);
  }

  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;
  }

}

Ajoutons également une méthode render() pour afficher notre personnage toujours en extrayant le code de notre classe Game :

@Override
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);
}

Il nous faut également définir la méthode de mise à jour, celle qui calculera la nouvelle position du joueur lors du déplacement, comme nous l'avons vu dans la leçon 3 :

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;
}

Comme nous le voyons nous avons besoin de la carte pour tester la collision, passons la en argument du constructeur :

public class Player {
  // déclaration des attributs [...]
  private Map map;

  public Player(Map map) {
    this.map = map;
  }
  // [...]
}

Enfin ajoutons des getters pour les variables qui sont encore utilisé dans la classe Game.

  public float getX() { return x; }
  public void setX(float x) { this.x = x; }
  public float getY() { return y; }
  public void setY(float y) { this.y = y; }
  public int getDirection() { return direction; }
  public void setDirection(int direction) { this.direction = direction; }
  public boolean isMoving() { return moving; }
  public void setMoving(boolean moving) { this.moving = moving; }
  public boolean isOnStair() { return onStair; }
  public void setOnStair(boolean onStair) { this.onStair = onStair; }

Code :: Classe Game

Maintenant que cela est fait il nous faut mettre à jour la classe Game pour ne plus utiliser les variables du joueur et la TiledMap mais les remplacer par des instances des classes Player et Map que nous venons de créer.

Supprimons la déclaration des dites variables et les remplacer par une instance des classes Player et Map :

public class ObjectsGame extends BasicGame {
  private GameContainer container;
  private Map map;
  private Player player = new Player(map);
  private float xCamera = player.getX(), yCamera = player.getY(); 
  // [...] suite du code
}

Ensuite mettons à jour le reste du code. Dans l'initialisation, il faut appeler les méthodes init() du joueur et de la carte pour charger les éléments graphiques :

@Override
public void init(GameContainer container) throws SlickException {
  this.container = container;
  this.map.init(); 
  this.player.init();
  [...] Chargement de la musique comme vu dans la <a title="Slick2d, leçon 7 :: La Musique" href="http://www.shionn.org/slick2d-lesson-7-music">leçon 7.
}

Dans la méthode render nous supprimons l'ancien code d'affichage pour le remplacer par des appels aux nouvelles méthodes :

@Override
public void render(GameContainer container, Graphics g) throws SlickException {
  // placement de camera (<a title="Slick2d, leçon 4 :: Une Camera" href="http://www.shionn.org/slick2d-lesson-4-une-camera">leçon 4)
  this.map.renderBackground();
  this.player.render(g);
  this.map.renderForeground();
}

Dans l'update() il faut faire appel à l'update du joueur pour calculer les nouvelles position lors du déplacement :

@Override
public void update(GameContainer container, int delta) throws SlickException {
  // [...] test de trigger (<a title="Slick2d, leçon 6 :: Les Triggers" href="http://www.shionn.org/slick2d-lesson-6-les-triggers">leçon 6) 
  this.player.update(delta);
  // [...] mise à jour de la camera (<a title="Slick2d, leçon 4 :: Une Camera" href="http://www.shionn.org/slick2d-lesson-4-une-camera">leçon 4) 
}

Ensuite il faut remplacer toute les utilisations des variables x, y, moving et direction, par des appel au setters et getters de la classe Player, je vous passe le code fastidieux de ces appels.

De même il faut mettre à jour les anciens appels aux méthodes de l'objet TiledMap vers les nouvelles méthodes de la classe Map. Voici un exemple pour la méthode changeMap(), il faudra suivre le même modèle pour les méthodes :

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

Normalement à ce stade, le code compile et tous re-fonctionne comme avant. La prochaine fois nous continuerons sur notre lancée en refactorant les triggers, les contrôles et la caméra.

tuto-slick2d-064-class-player

Ressource

par Shionn, dernière modification le 09 April 2017 16:07
3 réflexions au sujet de « Slick2d, leçon 10 :: Séparation en objet »
  • jonathan 26 October 2017 22:59

    Mon commentaire n'a pas été publié ?

  • Jonatahan 26 October 2017 23:04

    Bonjour, je re poste donc mon commentaire, je pense avoir sauter une partie de votre tuto cela est dit avec le ChangeMap ;)

  • Shionn 31 October 2017 07:54

    Salut,

    Tu as réussi à t'en sortir ? C'est bizarre que ton commentaire soit pas passé, j'ai effectivement un filtre anti-bot, peut être es-tu tombé dans ce cas la. Ou simplement as-tu fais la prévisualisation sans la validation

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.