jeudi 16 septembre 2010

09 - Déplacement des Ennemis

Avant de m'occuper du déplacement des ennemis, j'ai dû régler un dernier détail ; j'ai en effet oublié de tenir compte de la possibilité que les ennemis soient générés sur la même tile que le Héros, une situation que je veux bien sûr éviter.

J'ai donc modifié mon code : Au moment de faire la liste des positions potentielles, je vérifie d'abord que chacune des positions est différente de celle du Héros ; si c'est le cas je l'ajoute à la liste, dans le cas contraire je l'ignore.

function comparePosTestHero(ar, test){
/*
cette fonction compare la position "test" avec la position du Héros ; si ces deux positions sont différentes, la position "test" est ajoutée à la liste des potentielles ("ar")
*/
if (_global.positionHeros[0] !== test[0] || _global.positionHeros[1] !== test[1]){
ar.push(test);
}
return (ar);
}

Cette fonction me servira aussi plus tard lorsque je m'occuperai de la génération des trésors...

Une fois ce détail réglé, j'ai donc pu programmer le déplacement des ennemis ; à chaque ennemi est associé une série de variables :
  • "ligne" et "colonne" permettent de déterminer la position de l'ennemi ;
  • "vitesse" représente la vitesse de déplacement de l'ennemi (16/12 pour "lent", 16/8 pour "normal", 16/6 pour "rapide") ;
  • "déplacement" représente le nombre de pixels à parcourir avant d'atteindre la position suivante (nombre entre 0 et 16) ;
  • "direction" représente la direction de déplacement (1 bas, 2 gauche, 3 haut, 4 droite) ; sa valeur initiale est fixée à -20.
À chaque frame, on vérifie la variable "déplacement" : Si elle est négative, elle augmente de 1 (cela permet donc d'immobiliser les ennemis pendant 20 frames, soit 0,8 seconde, afin que le Héros ne se fasse pas sauter dessus dès son arrivée dans l'étage) ; si elle est supérieure ou égale à 0, on vérifie les 4 tiles autour du monstre : chaque direction pointant vers une tile de type "dalle" est ajoutée à la liste des directions potentielles, sauf s'il s'agit de la direction d'où vient l'ennemi.

Exemple :
1 : Le monstre a le choix entre 3 directions : "gauche", "haut" et "droite". La direction "bas" est impossible à cause de la brique.
2 : Parmi les 3 directions possibles, c'est la direction "haut" qui a été tirée au sort ; le monstre parcourt donc 16 pixels vers le haut, puis on détermine le déplacement suivant. Seule la direction "haut" est possible (la direction "bas"est retirée de la liste des directions potentielles pour empêcher le monstre de revenir sur ses pas).
3 : Le monstre a le choix entre "gauche" et "haut" ("droite" est impossible à cause de la brique, "bas" est retiré de la liste pour empêcher le monstre de revenir sur ses pas).

Si le monstre se retrouve dans une impasse (la liste des directions potentielles est vide), alors seulement il a le droit de revenir sur ses pas.

dimanche 12 septembre 2010

08 - Tests de Collision

Maintenant que j'ai placé mes monstres, la prochaine étape devrait en toute logique consister à les faire se déplacer dans le Donjon.
Néanmoins j'ai décidé aujourd'hui de profiter de leur immobilité pour m'occuper de tout autre chose : les tests de collision.

Un test de collision, c'est ce qui permet de savoir que deux sprites de mon choix sont entrés en contact ; c'est une donnée importante, car à chaque fois que le Héros entre en contact avec un ennemi, la phase d'exploration est remplacée par une phase de combat.

Étant donné que le sprite du Héros dépasse un peu du cadre de 16 pixels de côté qui forme chaque tile, je m'attendais à avoir quelques soucis lors des tests de collision :

Sur l'image ci-dessus, on voit que la coiffure du Héros touche le monstre "Goblin" alors que les deux personnages ne sont pas sur la même tile. Comme je n'ai pas envie qu'une telle situation ne débouche sur un combat, je dois donc faire en sorte que le test de collision soit négatif (même si à l'image les deux sprites sont en contact).

Finalement la méthode à utiliser s'est avérée bien plus simple que prévue ; j'ai juste placé le code suivant dans le Clip du monstre :

onSelfEvent (enterFrame) {
if (_root.hero.isNearThis(0)) {
trace ("collision avec " add identiteClip);
}
}

"isNearThis" vérifie la collision non pas avec les sprites en entier mais seulement avec un point précis de chaque sprite (le point "central", qui est en fait situé en haut à gauche de la tile). La collision est détectée si la distance entre ces deux points est inférieure ou égale à une certaine valeur (que j'ai ici fixée à 0 pixel, mais qui aurait pu être n'importe quel autre nombre strictement inférieur à 16).

Bref tout marche comme prévu, je vais donc pouvoir m'occuper la prochaine fois du déplacement des ennemis...

samedi 11 septembre 2010

07 - Génération des Ennemis

Au moment de la génération du Donjon, un ennemi est créé dans chacune des salles qui composent l'étage.
Pour chaque ennemi créé, je dois déterminer deux choses : Sa position initiale et sa nature.

Chaque type de salle possède plusieurs positions initiales potentielles pour les ennemis ; par exemple dans une salle en forme de "9" les ennemis apparaitront soit en haut à gauche, soit au milieu, soit en bas à droite.
Pour déterminer la position initiale réelle de l'ennemi, j'utilise une méthode similaire à celle que j'ai utilisée pour créer les jonctions entre les salles (billet n° 04) : J'étudie chaque salle une à une, et à chaque fois je fais la liste de toutes les possibilités avant d'en tirer une au sort.

Chacun des 7 environnements graphiques du jeu propose son propre bestiaire ; ce dernier est constitué de 2 types de monstres : Un monstre commun (2 chance sur 3) et un monstre rare (1 chance sur 3).
Au moment de créer l'ennemi j'effectue donc un tirage au sort pour déterminer s'il s'agit d'un monstre commun ou rare, et en fonction de l'environnement graphique j'affiche le sprite adéquat ; tous ces sprites ont été regroupés dans un seul et même Clip, et chaque frame de ce Clip correspond à un type d'ennemi.

Contrairement au Héros qui possède 8 positions différentes (4 statiques et 4 déplacements), les ennemis n'en ont qu'une seule : En effet ces derniers sont toujours vus de face, quelle que soit la direction dans laquelle ils se déplacent ; comme ils sont constamment en mouvement, ils n'ont pas besoin de positions de repos.
Ce parti-pris minimaliste me permet de réduire considérablement le travail graphique (3 sprites à dessiner au lieu de 10, ou plus précisément 2 au lieu de 6 - certains sprites sont en effet de simples copiers-collers retournés par symétrie horizontale) tout en accentuant le côté rétro du jeu.


Sur la capture d'écran ci-dessus, on voit bien les 3 différentes positions initiales disponibles pour une salle en forme de "6" (en haut à gauche, au milieu, en bas à droite), ainsi que la rareté du monstre "Minotaur"(marron) par rapport au monstre "Demon Knight" (vert).

jeudi 9 septembre 2010

06 - Déplacement du Héros

Le déplacement du Héros fait intervenir 2 variables :
  • une variable de direction (_global.directionHeros) qui indique la direction du Héros (et permet d'afficher la bonne frame du Clip sprite_hero dont je vous parlais dans mon billet précédent).
  • une variable de déplacement (_global.deplacementHéros) qui indique le nombre de pixels à parcourir avant que le Héros n'ait fini son déplacement.
Quand le joueur appuie sur une touche de direction du clavier, si la variable de déplacement est nulle, alors on regarde si le Héros peut se déplacer dans cette direction ou s'il y a un obstacle qui l'en empêche. Si le Héros peut passer, alors la variable déplacement prend la valeur 16 (je rappelle que mes tiles sont des carrés de 16 pixels de côté, et que le Héros doit donc se déplacer de 16 pixels pour passer d'une tile à l'autre).

Pour les extraits de code ci-dessous, je ne vais considérer que le bouton BAS :

directionVerticale = Key.isDown(Key.DOWN) - Key.isDown(Key.UP);
//si on appuie sur BAS, directionVerticale a pour valeur 1
//si on appuie sur HAUT, directionVerticale a pour valeur -1
//si on appuie sur BAS et HAUT, directionVerticale a pour valeur 0
directionHorizontale = Key.isDown(Key.RIGHT) - Key.isDown(Key.LEFT);
// même principe pour GAUCHE et DROITE
if (directionVerticale == 1 && directionHorizontale ==0){
// Le joueur a seulement appuyé sur BAS
if (_global.deplacementHeros ==0){
// la pression sur cette touche n'a d'effet que si le Héros n'est pas déjà en train de se déplacer
_global.directionHeros = 1;
if (_global.donjon[_global.positionHeros[0]][_global.positionHeros[1]+1] ==1){
// la tile en bas du Héros est de type "dalle" : le déplacement a lieu
_global.deplacementHeros += 16;
_global.positionHeros[1]++;
}
}
}

Si la variable déplacement n'est pas nulle, alors à chaque frame de la timeline le Héros se déplace à une vitesse que j'ai fixée à 2 pixels par frame : il faut donc 8 frames pour que le Héros passe d'une tile à l'autre, ce qui correspond à 0,32 secondes (le jeu tourne à 25 frames par seconde).

if (_global.deplacementHeros > 0){
// le Héros de déplace
if (_global.directionHeros == 1){
// Le Héros se déplace vers le BAS
hero._y += _global.vitesseHeros;
_global.deplacementHeros -= _global.vitesseHeros;
}
hero.gotoAndStop(_global.elementActuel * 10 + _global.directionHeros + 4);
//on affiche l'animation "déplacement vers le bas"
} else {
// déplacement nul : le Héros reprend une position statique
hero.gotoAndStop(_global.elementActuel * 10 + _global.directionHeros);
}

Ça y est, j'ai donc un personnage entièrement contrôlable au clavier qui se déplace dans le donjon sans passer à travers les murs !
Prochaine étape : Ajouter des monstres dans le Donjon...

mardi 7 septembre 2010

05 - Le Sprite du Héros

Le Héros de Math & Magic peut être vu sous 4 angles différents : de face, de dos, et de profil (gauche et droit).

Lorsque le Héros se déplace, j'utilise un système d'animation à quatre temps (rudimentaire, mais totalement dans l'esprit rétro que je cherche à donner à mon jeu) ; si par exemple j'applique cette méthode au cas du Héros se déplaçant vers le bas cela donne :
  1. Le Héros est vu de face, dans sa position de repos ;
  2. Le Héros met un pied en avant ;
  3. Le Héros reprend sa position de repos (sprite identique à celui de l'étape 1) ;
  4. Le Héros met l'autre pied en avant.
Ces quatre étapes d'animation se répètent en boucle aussi longtemps que le personnage se déplace dans cette direction.


Quand le déplacement s'arrête, le Héros reprend sa position de repos.

Pour les déplacements vers la gauche ou vers la droite, cette animation est simplifiée : J'alterne juste entre "le Héros est dans sa position de repos" et "le Héros écarte les jambes" ; en effet, vu le manque volontaire de détail de mes sprites 8-bits, je peux tout à fait me permettre d'utiliser la même image pour l'étape 2 et pour l'étape 4.

Donc pour un seul personnage, il existe en tout huit représentations différentes que je vais associer à un numéro : quatre positions statiques (1: de face, 2: de profil regardant à gauche, 3: de dos, 4: de profil regardant à droite) et quatre mouvements de déplacement (5: vers le bas, 6: vers la gauche, 7: vers le haut , 8: vers la droite).
Chaque Héros est lui même associé à un numéro (1: Chevalier de Feu, 2: Chevalier de Glace, 3: Chevalier de Terre).

Je place toutes ces représentations dans un seul et même Clip (appelé "sprite_hero"), en utilisant la formule suivante :

Numéro de la frame = 10 x Numéro du Héros + Numéro de la représentation

Ainsi la frame 11 de mon Clip sprite_hero représente le Chevalier de Feu en position statique vu de face, tandis que la frame 36 représente le Chevalier de Terre se déplaçant vers la gauche.


Pour finir, une petite image pour illustrer ce que je racontais hier au sujet des positions initiales du Héros en fonction de la nature de la dernière salle :

Comme vous le voyez, si la salle la plus à droite a la forme d'un "2", le Héros commencera en bas à gauche de cette salle ; si elle a la forme d'un "9", il commencera plus haut et tout à droite de la salle.


Ah, et un dernier mot sur le sprite du Héros :
Pour accentuer le côté rétro, je me suis limité à une palette de 4 couleurs pour chaque sprite et j'ai opté pour une résolution plus grossière : Alors que les sprites des dalles et des briques ont une dimension de 16 pixels de côté, les sprites des personnages semblent eux mesurer à peu près 8 pixels de côté (chaque "pixel" du Héros est en réalité un groupe de 4 pixels). Je dis "à peu près", car je me suis permis quelques dépassements (ne serait-ce qu'à cause de la coiffure).