Slick2d, leçon 19 :: Les combats aux tour par tour, (4/6)

19 April 2015 08:57 Slick2d - Leçons 10-19 Java, Jeux, Slick2d, Tutorial

Dans la leçon précédente nous avons vu comment faire une animation qui suit un courbe de bézier, pour rendre notre combat plus dynamique. Mais les animation se déclenchais en même temps pour le joueur et le monstre. Aujourd'hui on va voir comment résoudre ce problème à l'aide de listener.

Préparation ::  Listener ?

Qu'est-ce qu'un Listener ? Un listener c'est quelque chose qui écoute un événement, lorsque l'événement ce produit, alors on appel l'écouteur, c'est à dire le listener. C'est déjà ce qu'on utilise pour la gestion du clavier et de la manette.

J'ai fais évoluer SlickUtils, dans sa version 1.2, j'ai ajouté une interface AnimationListener et par la même la classe PathAnimation à évolué pour permettre d'y ajouter des points d'écoutes. PathAnimation a maintenant une méthode addListener(AnimationListener, long) permettant d'ajouter un point d'écoute à un certain point de l'animation. AnimationListener lui ne demande qu'une seul méthode on() qui sera appeler au point que l'on a demandé. Vous l'aurez compris, si vous n'avez pas la version 1.2 de SlickUtils, mettez-vous à jour.

Quel point d'écoute définir ? C'est un peu en fonction de ce que vous voulez. Moi j'ai choisis 2 points d'écoute pour le joueur.

  1. Quand le joueur arrive à la moitié de son attaque, je calcul les dégâts qu'il fait et je les assigne au monstre.
  2. Quand le joueur arrive à la fin de son mouvement d'attaque, je calcul si le monstre est mort et j'en déduis la suite du combat en fonction de la commande du joueur.

Callback Animation

Et je fais pareil pour l'animation de l'ennemi.

tuto-slick2d-098-callback-ennmi

Code :: Mise en place des listeners.

Dans les classes BattleEnnemy et BattlePlayer, ajoutons une nouvelle méthode, pour pouvoir définir nos listeners. L'animation durant 2000 milliseconde, 1000 correspond donc à la moitié de l'animation et 2000 la fin de l'animation.

// Classe :: BattlePlayer & BattleEnnemy
// [...]
public void addAnimationListener(AnimationListener assignDamage, AnimationListener endAttack) {
  this.animation.addListener(1000, assignDamage);
  this.animation.addListener(2000, endAttack);
}
// [...]

Puis définissons le squelette de nos listeners dans notre BattleController, j'ai choisi de faire 4 inner-classe et j'appelle 4 nouvelles méthodes pour chacun de ces listeners afin de simplifier avant tous l’écriture du code :

// Classe :: BattleController// [...]
public BattleController(BattlePlayer player, BattleEnnemy ennemy, StateBasedGame game) {
  this.player = player;
  this.ennemy = ennemy;
  this.game = game;
  initAnimationListeners();
}

private void initAnimationListeners() {
  AnimationListener playerAssignDamage = new AnimationListener() {
    @Override
    public void on() {
      playerAsignDamage();
    }
  };
  AnimationListener endPlayerAttack = new AnimationListener() {
    @Override
    public void on() {
      endPlayerAttack();
    }
  };
  AnimationListener ennemiAssignDamage = new AnimationListener() {
    @Override
    public void on() {
      ennemyAsignDamage();
    }
  };
  AnimationListener endEnnemiAttack = new AnimationListener() {
    @Override
    public void on() {
      endEnnemyAttack();
    }
  };
  this.player.addAnimationListener(playerAssignDamage, endPlayerAttack);
  this.ennemy.addAnimationListener(ennemiAssignDamage, endEnnemiAttack);
}

  private void playerAsignDamage() { }
  private void endPlayerAttack() { }
  private void ennemyAsignDamage() { }
  private void endEnnemyAttack() { }
// [...]

Code remplir nos listeners.

Il nous reste a donnée un corps à nos 4 listeners que nous venons de creer. Mais on tombe sur un os ! En effet si on souhait remplir la méthode playerAsignDamage, quand on rentrera dedans on ne sais pas si c'est le joueur qui attaque et si on doit ajouter les critiques, ou si le joueur défendais ? Il nous suffit d'enregistrer la commande (cf leçon 17) choisi par le joueur au moment ou il appuie sur la touche.

// Classe :: BattleController
// [...]
private BattleCommand mode;
// [...]
@Override
public void controlPressed(Command command) {
  this.mode = (BattleCommand) command;
  startBattle();
}

/**
 * Si le joueur attack il a l'initiative, dans tous les autres cas l'ennemi frappe avant.
 */
private void startBattle() {
  switch (this.mode) {
  case ATTACK:
    this.player.startAttack();
    break;
  case DEFEND:
  case FLEE:
  default:
    this.ennemy.startAttack();
    break;
  }
}
// [...]

Ainsi on aura dans la variable this.mode, la dernière commande entré par le joueur. Il nous reste à remplir chaque méthode en nous inspirant de ce qu'on à vu dans la leçon 17 :

// Classe :: BattleController
// [...]	
/**
 * Calcul des dégats infligés par le joueur à l'ennemi
 * Dans le cas d'une attaque le joueur peut faire un critique et infliger +50% de dégat
 */
private void playerAsignDamage() {
  int playerAttack = 7 + random.nextInt(4);
  if (mode == BattleCommand.ATTACK && random.nextDouble() < .1) {
    playerAttack += playerAttack / 2;
  }
  ennemy.setPv(ennemy.getPv() - playerAttack);
}
// [...]
Classe :: BattleController// [...]

/**
 * A la fin d'une attaque du joueur,
 *
 * Si l'ennemi n'as plus de pv le joueur à gagné, retour à la carte
 * Sinon, on appel l'action suivant en fonction du mode.
 */ 
private void endPlayerAttack() { 
  if (ennemy.getPv() <= 0) { 
    game.enterState(MapGameState.ID); 
  } else { 
    switch (mode) { 
    // si le joueur attaquait, contre attaque de l'ennemy 
    case ATTACK: 
      ennemy.startAttack(); 
      break; 
    // pas d'autre cas possible 
    default: break; 
    } 
  } 
}
// [...]
// Classe :: BattleController
// [...]

/**
 * Calcul des dégats infligés par le monstre au joueur
 * Si le joueur défend, les dégats sont divisé par 2
 */
private void ennemyAsignDamage() {
  int ennemyAttack = 5 + random.nextInt(5);
  if (mode == BattleCommand.DEFEND) {
    ennemyAttack = ennemyAttack / 2;
  }
  player.setPv(player.getPv() - ennemyAttack);
}
// [...]
// Classe :: BattleController
// [...]

/**
 * A la fin d'une attaque du monstre
 *
 * Si le joueur n'as plus de pv le joueur à perdu, retour à l'écran titre
 * Sinon, on appel l'action suivant en fonction du mode.
 */ 
private void endEnnemyAttack() { 
  if (player.getPv() <= 0) {
    game.enterState(MainScreenGameState.ID); 
  } else { 
    switch (mode) { 
    // si le joueur défend, contre attaque du joueur
    case DEFEND: 
      player.startAttack(); 
      break;
    // si le joueur fuis, retour à la carte
    case FLEE: 
      game.enterState(MapGameState.ID);
    default: break; 
    } 
  } 
} // [...]

A ce stade voila ce que ca donne :

Code :: Une attaque à la fois

Mais il reste un problème, interdire entrée une nouvelle commande si il y en a déjà une d'entrée et qui n'est pas fini. Pour cela on va crée une nouvelle commande NONE, qui sera celle par défaut.

Classe :: BattleCommand// [...]
public enum BattleCommand implements Command {
  ATTACK, DEFEND, FLEE, NONE
}
Classe :: BattleController// [...]
private BattleCommand mode = BattleCommand.NONE;
// [...]
Maintenant il suffit de ne pas prendre en compte une nouvelle commande si la commande en cours est différente de NONE.
Classe :: BattleCommand// [...]
@Override
public void controlPressed(Command command) {
  if (this.mode == BattleCommand.NONE) {
    this.mode = (BattleCommand) command;
    startBattle();
  }
}
// [...]

Voila ce qui est fait. Il reste à ré-initialiser à NONE la variable mode à la fin des attaques, c'est à dire dans les listeners endPlayerAttack et endEnnemyAttack.

// Classe :: BattleController
// [...]
private void endPlayerAttack() { 
  if (ennemy.getPv() <= 0) { 
    game.enterState(MapGameState.ID); 
    // réinitialisation pour le prochain combat
    mode = BattleCommand.NONE;
  } else { 
    switch (mode) { 
    case ATTACK: 
      ennemy.startAttack(); 
      break; 
    default: 
      // Fin de cette étape de combat, réinitialisation
      mode = BattleCommand.NONE;
      break; 
    } 
  } 
}
// [...]
// Classe :: BattleController
// [...]
private void endEnnemyAttack() { 
  if (player.getPv() <= 0) {
    game.enterState(MainScreenGameState.ID); 
    // réinitialisation pour le prochain combat
    mode = BattleCommand.NONE;
  } else { 
    switch (mode) { 
    case DEFEND: 
      player.startAttack(); 
      break;
    case FLEE: 
      game.enterState(MapGameState.ID);
    default: 
      // Fin de cette étape de combat, réinitialisation
      mode = BattleCommand.NONE;
      break; 
    } 
  } 
} // [...]

Et voila pour ce combat !

Pour aller plus loin.

On peu toujours imaginer mieux ! Plus de mode de combat, des calculs plus complexe de dégât en fonction d'attribut comme la force, la défense etc. On peu aussi imaginer avoir un écran GameOver. Mais c'est de l'algorithmie, vous avez déjà la base technique. Dans la prochaine et peut-être dernière leçon sur les combats, nous verrons comment ajouter des boutons pour contrôler le combat avec la souris.

Ressources

par Shionn, dernière modification le 09 April 2017 18:33
6 réflexions au sujet de « Slick2d, leçon 19 :: Les combats aux tour par tour, (4/6) »
  • Oukem 29 April 2015 22:54

    Salut Shionn je t'écris juste pour te dire merci. Depuis que j'ai découvert ton blog, je me suis trouvé une passion folle pour le développement de jeu vidéo.

    Je me suis finalement orienté vers la librairie libgdx que je trouve plus complète et qui surtout est toujours d'actualité. Et je développe actuellement un jeu du type RPG en ligne. Après quelques mois j'ai déjà un projet qui part sur de bonnes bases (je pense). Le jeu est fonctionnel (en version alpha donc il manque encore beaucoup beaucoup de choses) mais on peut déjà s'amuser avec ses amis en local à tuer des monstres.

    J'ai encore du travail mais le principe y est.

    Si j'ai pu arriver à ce résultat (que je n’imaginais même pas il y a quelques mois) c'est grâce à ton blog, à beaucoup de patience et de l'acharnement.

    Enfin bref je m'amuse vraiment à développer ce jeu et je voulais juste te remercier d'avoir été l'élément déclencheur et aussi te dire que je continuerais à visiter ton blog de temps en temps.

  • Shionn 30 April 2015 11:11

    He bien que dire d'autre que simplement merci ! C'est pour cela que je fait ces articles, et j’espère avoir un jour la chance de tester ton jeu :]

  • moonlight 14 May 2015 11:43

    Tes leçon sont juste génial , bonne continuation ! :)

    à quand la suite ?

  • Shionn 14 May 2015 14:20

    Un article sur deux est sur slick, un article un dimanche sur deux :]

  • Ultraime 24 December 2015 00:25

    Merci pour tes tutos !!

  • Ultraime 25 December 2015 21:04

    Salut je voulais savoir si tu aller continuer les tutos ? ^^

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.