Journal de bord: création d’un RTS en HTML5, jour 3
Introduction
Ce billet fait suite au billet précédent: http://blog.developpez.com/ducodeetdulibre/p12405/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-2
Dans ce billet je vais vous détailler le fichier rts_Game.js, son utilisation, sa construction.
Faire une application HTML5/canvas : les interactions
J’expliquai lors du dernier billet que l’utilisation du canvas était un peu moins confortable que de faire du flash.
Une chose toute bête c’est d’identifier où l’on clique: sur une unité, sur un batiment, un arbre ou dans le vide.
Pour cela plusieurs choses: on recupère l’evenement clic, ses coordonnées, on leur soustrait le scroll du navigateur.
Puis on cherche à savoir si on trouve dans un tableau indexé par coordonnées x y un élément.
Cette méthode se trouve dans la classe Game.
La classe Game, fichier rts_Game.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //Game function Game(){ //tableau de coordonnées des unités this.tCoordUnit=Array(); //tableau de corrdonnées des batiments/arbres/mine d'or this.tCoordBuild=Array(); //le mode en cours this.mode=''; //l'element selectionné (unité/batiment...) this.selected=''; //le batiment selectionné à construire this.buildcreation=''; //le tableau des cases visibles sur la carte this.tVisible=Array(); //tableau contenant tous les batiments (utilisé pour reconstruire la map lors d'un scroll) this.tBuild=Array(); //idem pour les unités this.tUnit=Array(); //ressources this.iOr=250; this.iWood=150; } |
Je ne vais pas ici coller la classe Game en entier (plus de 400 lignes), je vais plutot présenter bloc par bloc pour expliquer certains principe de développement
Pour rappel, l’ensemble du projet est disponible sur github (voir en bas d’article)
La récupération des coordonnées
Lorsque l’on clique sur un endroit du canvas, il faut faire plusieurs choses: récupérer l’evenement, recupérer les coordonnées du clic, puis diviser par la taille des cases pour récupérer les coordonnées du tableau.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //recuperation de X + le decalage de la scrollbar getXmouse:function(e){ if(e && e.x!=null && e.y!=null){ return e.x +document.body.scrollLeft; }else{ return e.clientX +document.body.scrollLeft; } }, //recuperation de X + le decalage de la scrollbar getYmouse:function(e){ if(e && e.x!=null && e.y!=null){ return e.y + document.body.scrollTop; }else{ return e.clientY + document.body.scrollTop; } }, //recuperation de la coordonnée de tableau: divisition de x par la largeur d'une case getX:function(e){ var x=this.getXmouse(e); x=parseInt( x/widthCase); //ajout du decallage (scrolling sur la map) return x+currentX; }, getY:function(e){ var y=this.getYmouse(e); y=parseInt( y/widthCase); return y+currentY; }, |
Le clic gauche
Lorsque l’on veut séléctionner une unité, on doit cliquer sur l’image de celle-ci.
Mais malheureusment, cette image est un dessin sur le canvas, il n’y donc pas possibilité de mettre un evement sur l’image.
On doit récupérer les coordonnées du clic pour retrouver l’image en dessous.
Pour cela, on recupere les coordonnées « tableau » puis l’on demande si il y a une unité ou un batiment en dessous.
Lors du clic gauche, si on a demandé à l’unité de construire un batiment, on va vérifier que l’on peut le construire sur ces coordonnées
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | getUnit:function(x,y){ //console.log('search x:'+x+' '+y); if(this.tCoordUnit[y] && this.tCoordUnit[y][x]){ return this.tCoordUnit[y][x]; } return null; }, getBuild:function(x,y){ if(this.tCoordBuild[y] && this.tCoordBuild[y][x]){ return this.tCoordBuild[y][x]; } return null; }, //appelée lors d'un clic gauche sur le canvas (sélection d'une unité/batiment) click:function(e){ //recuperation des coordonnées "tableau" x y var x=this.getX(e); var y=this.getY(e); //si l'utilisateur a séléctionné un batiment à construire if(this.buildcreation!=''){ //on veririfie ensuite les coordonnées //des 4 cases nécéssaires à la construction du batiment var ok=1; if(!oGame.checkCoordVisible(x,y) || !oGame.checkCoord(x,y) ){ ok=0; }else if(!oGame.checkCoordVisible(x+1,y) || !oGame.checkCoord(x+1,y) ){ ok=0; }else if(!oGame.checkCoordVisible(x,y+1) || !oGame.checkCoord(x,y+1) ){ ok=0; }else if(!oGame.checkCoordVisible(x+1,y+1) || !oGame.checkCoord(x+1,y+1) ){ ok=0; } if(!ok){ //si une des cases indisponible, on annule return; } //si c'est ok, on efface la selection d'emplacement this.buildcreation.clear(); //on indique à l'unité qui doit construire //le batiment à construire this.selected.buildOn(this.buildcreation); //on annule la construction en cours this.buildcreation=''; return; } //on recherche si il y a quelquechose aux coordonnées var oUnit=this.getUnit(x,y); var oBuild=this.getBuild(x,y); //si il y a une unité if(oUnit){ //on selectionne celle-ci this.select(oUnit); }else if(oBuild){ //si il y a un batiment, //on le selectionne this.select(oBuild); }else{ //sinon on supprime la selection sur le canvas console.log('pas trouve'); this.clearSelect(); } }, |
Le clic droit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //appelé lors d'un clic droit sur le canvas (pour un deplacement d'unité) clickdroit:function(e){ //recuperation des coordonnées "tableau" x y var x=this.getX(e); var y=this.getY(e); //recuperation de batiment sur ces coordonnées var aBuild=this.getBuild(x,y); //si une unité est sélectionnée et que le batiment cliqué est une mine ou du bois if(this.selected!='' && aBuild && ( aBuild.name=='or' || aBuild.name=='wood' )){ //on créé une ronde du bois/or vers le QG //pour alimenter les ressources Or/bois //on indique que la destination de cycle c'est la mine d'or ou l'arbre this.selected.cycleToX=x; this.selected.cycleToY=y; //on indique que la provenance du cycle c'est le QG this.selected.cycleFromX=QGx; this.selected.cycleFromY=QGy; //on donne comme cible de deplacement la mine d'or/l'arbre cliqué this.selected.setTarget(x,y); }else if(!this.isWalkable(x,y)){ //si la case n'est pas accessible, il ne se passe rien return false; }else if(this.selected!=''){ //si la case est accessible, on y indique à l'unité d'y aller this.selected.setTarget(x,y); } return false; }, |
Le déplacement de souris
On a besoin de verifier certaines choses au déplacement de la souris:
Dans le cas d’une construction, on deplace au dessus de la map, le futur emplacement du batiment, en utilisant un code couleur pour savoir si l’on peut le construire ou non.
On veut également savoir si la souris est à une extremité de la carte pour savoir sil l’on doit scroller la map.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | mousemove:function(e){ sDirection=''; if(this.buildcreation){ //si une construction est en cours //on recupere les coordonnées var x=this.getX(e); var y=this.getY(e); //on efface la derniere position de la construction théorique this.buildcreation.clear(); //on deplace le plan aux nouvelles coordonnées this.buildcreation.x=x; this.buildcreation.y=y; //on affiche le plan this.buildcreation.build(); }else if(this.getYmouse(e) (oLayer_map.height-10) && this.getYmouse(e) < oLayer_map.height-1 && (this.getXmouse(e) 500)){ //ici on verifie plusieurs choses pour laisser un passage vers le bas //vous pouvez voir ces zones de selection en orange //si c'est le cas, scroll de la map vers le bas sDirection='down'; }else if(this.getXmouse(e) (oLayer_map.width-10)){ //si les coordonnées x sont superieur à la largeur de la map -10: //scroll de la map vers la droite sDirection='right'; } }, |
La sauvegarde des coordonées d’une unité/batiment
Il y a 2 écoles pour les jeux: les coordonées et les tableau
L’idée est de savoir où son les unités pour savoir si on clic dessus, si il y a une autre unité à proximité (cas d’une attaque) ou si la zone est accessible.
Dans ce tableau de bord j’ai opté pour le choix du tableau
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | saveUnit:function(oUnit){ //on recupere les coordonnées de l'unité var y=oUnit.y; var x=oUnit.x; //on enregistre dans un tableau indexé //les nouvelles coordonnées if(!this.tCoordUnit[y]){ this.tCoordUnit[y]=Array(); } this.tCoordUnit[y][x]=oUnit; //on rend la zone visible this.setVisibility(x,y); }, saveBuild:function(oBuild){ //on recupere les coordonnées du batiment var y=parseInt(oBuild.y); var x=parseInt(oBuild.x); //on enregistre dans un tableau indexé //les nouvelles coordonnées if(!this.tCoordBuild[y]){ this.tCoordBuild[y]=Array(); } if(!this.tCoordBuild[y+1]){ this.tCoordBuild[y+1]=Array(); } this.tCoordBuild[y][x]=oBuild; this.tCoordBuild[y+1][x]=oBuild; this.tCoordBuild[y+1][x+1]=oBuild; this.tCoordBuild[y][x+1]=oBuild; }, |
La sélection
Que l’on clic sur une unité ou un batiment, il est toujours plus confortable de voir sa sélection à l’écran
Pour cela, on dessine sur un calque un cadre de couleur
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | clearSelect:function(){ //on efface le calque oLayer_select.clear(); this.selected=''; //on efface le bloc du bas this.resetNav(); }, select:function(oMix){ //on enregistre l'unité/batiment this.selected=oMix; //on demande son dessin this.drawSelected(); //on affiche la navigation oMix.buildNav(); }, drawSelected:function(){ //on efface le calque oLayer_select.clear(); //on dessine un cadre sur un des calques au dimension de l'élement oLayer_select.drawRectStroke((this.selected.x-currentX)*widthCase,(this.selected.y-currentY)*heightCase,this.selected.width,this.selected.height,'#880044',3); }, resetNav:function(){ getById('nav').innerHTML=''; }, |
Boucle sur les unités
Régulièrement, on met à jour les coordonnées des unités pour permettre leur déplacement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | refreshUnit:function(){ //on boucle sur les unités existantes for(var i=0;i< this.tUnit.length;i++){ var oUnit= this.tUnit[i]; //si l'unité doit se rendre quelques part if(oUnit.targetX!='' && oUnit.targetY!='' && (oUnit.targetX!=oUnit.x || oUnit.targetY!=oUnit.y) ){ var vitesse=1; var vitesse2=vitesse*-1; //on efface le dessin sur le calques oUnit.clear(); //on initialise les nouvelles coordonnées var newX=oUnit.x; var newY=oUnit.y; //on fait evoluer les coordonnées vers la destination if(oUnit.targetX!='' && oUnit.x oUnit.targetX ){ newX-=vitesse; } if(oUnit.targetY!='' && oUnit.y oUnit.targetY ){ newY-=vitesse; } //on verifie si aux coordonnées cible, il y a un batiment var aBuild=this.getBuild(newX,newY); //si la cible est le QG if(aBuild && aBuild.name=='building'){ oUnit.x=newX; oUnit.y=newY; //on definit la nouvelle cible oUnit.setTarget(oUnit.cycleToX,oUnit.cycleToY); //si l'unité transportait de l'or if(oUnit.or >0){ //on ajoute une ressource or this.addRessourceOr(oUnit.or); }else if(oUnit.wood >0){ //idem pour le bois this.addRessourceWood(oUnit.wood); } //on reset les ressources de l'unité oUnit.or=0; oUnit.wood=0; //si la cible c'est un arbre et que le compteur est inferieur à N }else if(aBuild && aBuild.name=='wood' && oUnit.counter = 8){ //on indique à l'unité qu'elle transporte 10 oUnit.wood=10; oUnit.x=newX; oUnit.y=newY; //on remet le compteur à 0 oUnit.counter=0; //on redéfinit la nouvelle cible (c'est un cycle) oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY); //si la cible c'est une mine d'or et que le compteur est inferieur à N }else if(aBuild && aBuild.name=='or' && oUnit.counter = 8){ //on indique à l'unité qu'elle transporte 10 oUnit.or=10; oUnit.x=newX; oUnit.y=newY; //on remet le compteur à 0 oUnit.counter=0; //on redéfinit la nouvelle cible (c'est un cycle) oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY); }else if(this.checkCoord(newX,newY)){ //si la coordonnées est libre oUnit.x=newX; oUnit.y=newY; }else if(this.checkCoord(oUnit.x,newY)){ //si la coordonnées est libre (cas d'un evitement d'obstacle) oUnit.y=newY; }else if(this.checkCoord(newX,oUnit.y)){ //si la coordonnées est libre (cas d'un evitement d'obstacle) oUnit.x=newX; }else if(this.checkCoord(newX,oUnit.y)){ //si la coordonnées est libre (cas d'un evitement d'obstacle) oUnit.x=newX; }else if(this.checkCoord(oUnit.x+vitesse2,oUnit.y)){ //si la coordonnées est libre (cas d'un evitement d'obstacle) oUnit.x=oUnit.x+vitesse2; }else if(this.checkCoord(oUnit.x,oUnit.y+vitesse2)){ //si la coordonnées est libre (cas d'un evitement d'obstacle) oUnit.y=oUnit.y+vitesse2; } //on dessine l'unité oUnit.build(); console.log('recalcul'); //on met à jour la partie visible de la carte oGame.displayVisibility(); } } //on redessine le cadre de selection this.drawSelected(); }, |
Le github
Le projet GitHub : https://github.com/imikado/rtshtml5