La vidéo de la semaine passée (accessible ICI) présentait mon projet d’ ISN. Il aurait dû y avoir une deuxième partie, elle aussi en vidéo. Cependant, Adobe Audition (logiciel que j’utilise pour enregistrer l’audio), refuse d’exporter la piste audio donc cette seconde et dernière partie sera faite à l’écrit.
Cette article entrera dans les détails du programme. Je vais donc vous présenter la partie ‘Ma Réalisation’ de mon dossier présenté au bac. Les annexes seront disponibles si vous me le demandez. De plus, j’ai fait des tests unitaires pour la plupart des modules. Ils seront disponibles en fin d’article.
PARTIE 3 : MA RÉALISATION
Dès le début du projet, il a fallu scinder la boucle ‘draw’ car nous ne souhaitions pas que toutes les parties du code tournent toutes en permanence : Pour cela j’ai décidé de mettre en place une variable gameStatement. Cette variable peut prendre comme valeur des entiers. Les différentes valeurs définissent chacune un mode de jeu comme suit :
col 1 | col 2 |
---|---|
0 | Menu de début |
1 | Navigation |
2 | Combat naval |
4 | Ile principale |
6 | Combat terrestre |
Le menu de début est lui-même divisé en trois parties (variable subStatement). Choix entre New Game (nouvelle partie) / Saves (choix d’une sauvegarde, arrivera plus tard dans le développement, une solution est en train d’être étudiée avec stockage dans un fichier.json des valeurs des variables.) / Crédits (nos prénoms et noms).
Une fois cliqué sur ‘New Game’, le joueur est invité à entrer son pseudo. Toutes les lettres sont acceptées. Les accents, caractères spéciaux et chiffres sont interdits pour éviter des problèmes de compatibilité. Si jamais le joueur s’est trompé, il peut cliquer sur le bouton ‘Reset’ pour re-taper son pseudo. Une fois le bon pseudo rentré, il est possible de choisir la difficulté.
Pour savoir ce que change chaque niveau de difficulté, il est possible de survoler l’image pour qu’une description brève s’affiche dans le cadre blanc. Le choix de la difficulté change un coefficient de difficulté (ci-dessous) et réduit / augmente les dégâts que les ennemis vous infligent.
col 1 | col 2 |
---|---|
Difficulté (de gauche à droite) | Coeff. De dégâts |
1 | 0.8 |
2 | 1.0 |
3 | 1.2 |
4 | 1.5 |
Une fois cela choisit, vous avez terminé la Première Etape du jeu, et vous êtes prêt à partir conquérir la mer et faire trembler vos ennemis (le gameStatement passe à 1).
Vous découvrirez à peine la navigation qu’une quête vous sera affichée (en haut à droite). Cette quête vous demande de vous rendre en (0,10), déplacez-vous avec les flèches de directions (eventListener des touches keyCode UP, DOWN, LEFT, RIGHT. Vous ne vous déplacez pas réellement, c’est l’image derrière le bateau qui change de position, voir annexe 1.1).
En vous approchant de l’ile, vous aurez un message qui s’affichera (une fonction a été créée, elle demande 1 paramètre, ce paramètre est défini par l’approche du bateau d’une ile ou d’un bateau ennemi. Si c’est une ile, le paramètre est de 1, sinon il est de 2, voir annexe 1.2), cliquez sur la lettre demandée et vous apparaitrez sur l’ile Principale. Ce sera votre base pour la suite de l’aventure. Sur cette île, vous pourrez acheter des bâtiments pour améliorer votre personnage et votre bateau. (Bientôt des mini-jeux apparaitront (fléchettes, bras de fer, tir, …) pour vous créer une nouvelle source de revenu).
Une fois le premier bâtiment acheté, vous n’aurez assez d’argent que pour acheter une amélioration dans le menu. Prenez la première (en haut à gauche), elle vous donnera un boost de dégâts de 20pts, bien utile lors de vos prochains combats. Quittez le menu avec le rectangle rouge en bas à gauche (vérification position souris, annexe 1.3) et dirigez-vous vers la gauche de la carte (le déplacement fonctionne de façon similaire au déplacement en mer). Lorsque vous arriverez suffisamment à l’ouest, vous serez automatiquement remis en mode navigation (voir annexe 1.4).
Vous pouvez maintenant vous déplacer comme vous le souhaitez, la navigation va de (0,0) jusque (100,100). Vos coordonnées sont affichées en haut à gauche
Pour avoir une expérience unique à chaque lancement du jeu, j’ai créé un moteur de génération d’ile et de bateau. Pour cela, j’utilise des constructeurs de classe (notion que j’ai apprise avec la documentation Processing et qui réduit grandement le nombre de lignes de codes, voir annexe 1.5 pour le constructeur de classe bateau). 3 tableaux objets sont créés au début du jeu. J’ai envisagé un tableau à deux dimensions comme une carte découpée en carrés de 1000*1000 pixels. On obtient donc une carte de 10 carrés de large par 10 carrés de haut. C’est-à-dire avec 100 cases. Avec la structure suivante : tableau[x][y].
Dans le premier tableau : Map, il contient uniquement des valeurs de int(random(0,3)); cela permet d’obtenir les valeurs d’un intervalle ]0 ; 3]. En rajoutant int (c’est-à-dire que l’on obtient uniquement des valeurs entières. On peut en obtenir 3 et chacune définie le contenu de la case du tableau.
col 1 | col 2 |
---|---|
1 | Bateau ennemi |
2 | Ile |
3 | Vide |
Pour voir comment la boucle for est utilisée et comment la génération se fait, voir annexe 1.6.
Aussi, pour éviter que deux cases ayant un contenu se touchent, chaque case est divisée en 4 parties égales. C’est le constructeur qui gère aléatoirement la sous-position (‘corner’) qui sera utilisée.
Avant de mettre en ‘production’ ce moteur, je l’ai d’abord codé pour que l’on puisse voir tous les éléments générés dans la fenêtre et s’assurer de son bon fonctionnement.
Version de développement du moteur de génération (vert = ile; rouge = bateau ennemi ; noir = sous-position choisie):
3 exemples de rendu de générations __:
Pour la version de développement, j’ai directement utilisé des objets avec constructeurs et fonctions d’affichage pour les rectangles afin de faciliter la mise en production de ce module.
Pour la mise en production, aucune variable n’est rajoutée. Seule la valeur est modifiée, par exemple les carrés de 100100 pixels contenant des carrés noirs de 4848 sont désormais bien plus grands et contiennent des images de 600*600 pixels. Sinon, la structure du code n’a pas changé.
J’ai aussi participé au développement des modules de combats. Nous allons désormais étudier leur fonctionnement :
LE COMBAT NAVAL
Le combat naval est une fonction du jeu que j’ai créé dès le début du développement. Je savais que cette partie serait des plus complexes d’un point de vue mathématique en terme de visualisation car pour rajouter du dynamisme au jeu, nous souhaitions que le joueur puisse choisir la puissance à donner au boulet avec un curseur (utilisation de la bibliothèque CP5). Nous ne voulions pas que la trajectoire du boulet soit rectiligne, pour cela il a donc fallu implanter dans le programme la capacité à calculer une trajectoire parabolique.
Dans un premier temps, nous calculons les conditions initiales :
Position initiale :
bouletX = 365 + 50 * cos(-angle*PI/180);
bouletY = 520 + 50 * sin(-angle*PI/180);
Vitesse initiale :
dx = puissance * cos(-angle*PI/180);
dy = puissance * sin(-angle*PI/180);
Ensuite le calcul de la pesanteur :
dy = dy+ pesanteur;
La pesanteur est modifiable pour changer sa force (par défaut, elle est égale à 1)
Le boulet en plus d’être entrainé vers le bas, il est aussi ralenti dans sa trajectoire par les frottements de l’air :
dx = dx*frottement;
dy = dy*frottement;
Les frottements sont par défaut définis à 0.95. Il est possible de les négliger en les définissant à 1 (car dx = dx*1 ⬄ dx = dx, de même pour dy). Ou de les augmenter, en rapprochant leurs valeurs de 0.
Pour terminer, on redéfinit à chaque image la position du boulet avec :
bouletX = bouletX+dx;
bouletY = bouletY+dy;
Lors de la création de cette partie, j’ai rencontré un problème majeur : Le repositionnement de repère. En effet, pour définir la position du rectangle qui représente le canon, nous utilisons la fonction rotate(); ainsi que translate();. J’ai cherché à comprendre ce que changeaient ces fonctions, et c’est un utilisateur de StackOverflow qui m’a permis d’obtenir la réponse. rotate(); n’effectue pas une simple rotation des objets, mais une rotation du repère tout entier. translate(); fonctionne d’une manière similaire : Il ne décale pas des coordonnées, mais le repère entier.
Aussi, pour utiliser ces deux fonctions sans casser le reste du programme, j’ai utilisé les fonctions pushMatrix(); et popMatrix();. Je ne pense pas encore avoir compris totalement comment ces fonctions fonctionnent. Mais d’après la documentation, je suppose que pushMatrix(); enregistre les coordonnées du repère au moment où elle est appelée, et que popMatrix(); restaure ensuite les coordonnées conservées lors du dernier appel de pushMatrix();. Voir annexe 1.8 pour l’utilisation de ces fonctions dans notre cas.
Les ennemis possèdent 3 hitboxes (zone touchables) sous ce modèle : (j’ai pris l’exemple du bateau de niveau 3, mais ce modèle est aussi valable pour les autres bateaux)
Lorsque vous touchez la zone verte, l’ennemi perds 40 points de vie. La zone bleue, 60 points de vie. La zone rouge, 80 points de vie. Cela a été choisi en fonction de la difficulté à toucher chaque zone.
L’ennemi peut lui aussi tirer, vous devez appuyer sur la touche SPACE pour le faire tirer (au début était prévu un tir automatique de l’ennemi à la fin d’un timer, mais pour des problèmes de performances nous avons préféré demander une action humaine pour le faire.
Les mêmes calculs sont effectués pour la trajectoire du boulet ennemi que pour le nôtre, seul l’angle est différent (3*PI/4 au lieu de PI/4) et la puissance est choisie aléatoirement avec 20% que l’ennemi tire à côté.
LE COMBAT TERRESTRE :
Lorsque vous souhaitez accoster une île, des pirates vous attendront et seront prêts à se battre. Le combat se fait en tour par tour, vous avez 3 ennemis à abattre pour obtenir une récompense (nous reviendrons à cela plus tard).
Votre personnage se situe à gauche, les ennemis à droite. Il existe 3 types d’ennemis :
col 1 | col 2 | col 3 |
---|---|---|
Nom | Dégâts | Vie |
Simple | 25*dr | 100 |
Tank | 15*dr | 150 |
Assassin | 40*dr | 80 |
(dr : coeff. De difficulté, choisi au début de la partie)
Le combat se déroule en tour par tour et vous pouvez choisir 1 ennemi à attaquer à chaque tour. Par défaut, vous lui retirez 60 points de vie. Et à cela s’ajoute les points d’amélioration que vous aurez acheté dans le magasin sur l’ile principale. Vous pouvez donc infliger jusque 100 points de dégâts. Une fois votre tour terminé, c’est aux ennemis de jouer, ils vous attaqueront un par un. C’est celui en haut qui commence, ensuite celui du milieu et pour finir celui du bas.
Partie programmation :
Les ennemis sont des objets contenus dans un tableau. Chaque objet possède un constructeur et une fonction permettant de l’afficher.
Les cases du tableau contenant les ennemis sont re-générées à chaque assaut d’île. Cela car suivant le niveau de l’ile les ennemis changent. Si vous attaquez une ile de niveau 1, les 3 ennemis seront simples. Si c’est une île de niveau 2, il y aura 2 ennemis simples (haut / bas) et 1 ennemi avec une ‘classe’ choisie aléatoirement (simple, tank ou assassin). Pour finir, si l’ile est de niveau 3, les 3 ennemis seront choisis aléatoirement. Ce choix se fait par le constructeur et permet de diversifier l’expérience utilisateur pour permettre des combats différents à chaque assaut. Voir annexe 1.9.
Le constructeur de classe des ennemis ainsi que la fonction permettant de les afficher sont particulièrement travaillés, je les mets donc en annexe 1.10.
Pour ne pas infliger les dégâts par un simple clic gauche, j’ai créé une animation du joueur mais aussi des ennemis afin de rajouter plus de fluidité. Pour cela, j’ai fait un calcul de coefficient directeur entre deux points.
5 fois par seconde, le personnage avance de 30 pixels sur l’axe x et de la (valeur du coefficient directeur calculé)*30 sur l’axe y.
L’animation des ennemis fonctionnent sur le même principe, sauf que le coefficient directeur est l’opposé étant donné que l’on veut parcourir la ‘droite’ (trajectoire) dans le sens inverse.
L’ordre d’attaque des ennemis est toujours le même (de haut en bas). Pour que l’ordinateur sache qui peut / doit jouer, il y a d’abord une vérification de la vie de l’ennemi (si sa vie est inférieure ou égale à 0, il ne peut plus jouer, c’est donc au tour de l’ennemi suivant de jouer. Si lui a suffisamment de points de vie pour jouer, il effectue son animation. Arrivé à une certaine coordonnée, il s’arrête, inflige les dégâts, retourne à sa position initiale et c’est au tour de l’ennemi suivant. Une fois arrivé au dernier ennemi (celui du bas), si son tour est terminé ou qu’il a déjà été abattu, dans ce cas la boucle du tour ennemi s’arrête et celle de notre tour se relance. Voir annexe 1.11 pour la transcription en code.