Journal de bord: création d’un RTS en HTML5, jour 3

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