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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12424/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-10

Dans ce jour 11, on va ajouter un peu de son sur ce jeux qui est bien muet pour l’instant
On ne pas va pas encore mettre l’ambiance, juste 2-3 son histoire d’ajouter un peu de vie

On va ajouté un son lorsqu’une unité coupe du bois, travaille dans la mine, construit un batiment se bat ou meure.
J’ai trouvé ces divers sons sur ce site: http://www.universal-soundbank.com/

Le son en html5
Tout d’abord comment jouer un son en HTML5, comme il y a une classe Image pour créer un objet image, il y a une classe Audio pour crée un objet son.
Vous pouvez ensuite interagir avec ce son: le lire, le mettre en pause…

Nous créons ici dans une classe Sound

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
function Sound(){
   
    this.counter=0;
    this.total=0;
    this.tSrc=Array();
    this.tOAudio=Array();
    this.tPlaying=Array();
}
Sound.prototype={
    resetPlaying:function(){
    this.tPlaying=Array();
    },
    add:function(src,id){
        this.tSrc[this.total]=Array();
        this.tSrc[this.total]['src']=src;
        this.tSrc[this.total]['id']=id;
       
        this.total++;
    },
    load:function(){

        for(var i=0;i< this.total;i++){
        var id=this.tSrc[i]['id'];

        this.tOAudio[id]=new Audio();
        this.tOAudio[id].src=this.tSrc[i]['src'];
        this.tOAudio[id].onload=function(){
           
        };
        this.tOAudio[id].load();
        oSound.counter++;
            preload2();

        }      
    },
    playSound:function(sType){
    console.log("play sound "+sType);
    this.tPlaying[sType]=1;
       //on joue le son
    this.tOAudio[sType].play();
    },
    stopSound:function(sType){
    console.log("stop sound "+sType);
   

    //on remet le son au début
    this.tOAudio[sType].pause();
    this.tOAudio[sType].currentTime=0;
   
    this.tPlaying[sType]=0;
    },
    stopPlaying:function(){
    //return;
    for(var i=0;i< this.total;i++){
        var id=this.tSrc[i]['id'];
        if( !this.tPlaying[ id ] && id!='building'){
        this.stopSound(id);
        }
       
    }
   
    }
   
};

Ajoutons un son de coupe du bois
Il y a deux choses à faire: démarrer le son au début de la coupe du bois avec oGame.playSound(‘wood’);
Et l’arretez une fois le travail terminé avec oGame.stopSound(‘wood’);

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
}else if(aBuild && aBuild.name=='wood' && oUnit.counter = 8 && oUnit.cycleToX!=''){
    //on indique à l'unité qu'elle transporte 10
    oUnit.wood=10;
   
    //a chaque iteration on decremente la ressource
    aBuild.ressource-=10;
    //si l'arbre est épuisé, on le supprime de la carte
    if(aBuild.ressource<=0){
        aBuild.clear();
        oGame.removeBuild(aBuild);
       
        //on définit un nouvel arbre à prendre en compte
        var tWood=[
            [-1,-1],
            [0,-1],
            [1,-1],
           
            [-1,0],
            [1,0],
           
            [-1,+1],
            [0,+1],
            [1,+1],
        ];
        for(var i=0;i<tWood.length;i++){
            var oBuild2=this.getBuild(aBuild.x+tWood[i][0],aBuild.y+tWood[i][1]);
            if(oBuild2 && oBuild2.name=='wood'){
                oUnit.cycleToX=oBuild2.x;
                oUnit.cycleToY=oBuild2.y;
               
                break;
            }
        }
    }
   
    //on remet le compteur à 0
    oUnit.counter=0;
   
    //on redéfinit la nouvelle cible (c'est un cycle)
    oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY);
                       
    oSound.stopSound('wood');

//si la cible c'est une mine d'or et que le compteur est inferieur à N
}

Faisons de même pour la mine d'or

Ajoutons un son pour le combat
Ici il y a deux sons ici à mettre en place: celui du combat et celui de la mort d’une unité.
Pour le combat à chaque fois qu’on est en position d’attaque (à proximité d’une unité enemie)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(iAttack){
    oSound.playSound('attack');
                   
    //on decremente l'enemie de la puissance d'attaque
    oUnit2.life-=oUnit.attak;
    if(oUnit2.life <=0){
        //on joue le son d'attaque
        oSound.stopSound('attack');
                           
        //si unite dead, on l'efface du jeu
        oUnit2.clear();
        oGame.removeUnit(oUnit2);
       
        //on joue le son de mort de l'unite
        oSound.playSound('dead');
                               
    }

Dans le cas présent, si l'unité ne finit pas son combat, le son ne sera pas arrêté.
Il faudrait au minimum mettre à zéro en début de boucle de rafraichissement d'unité et stopper en fin de boucle tous les sons sauf ceux en cours
Pour cela on créé une méthode: oSound.resetPlaying();

1
2
3
resetPlaying:function(){
    this.tPlaying=Array();
},

Et en fin de boucle:
oSound.stopPlaying();

1
2
3
4
5
6
7
8
9
10
stopPlaying:function(){
    for(var i=0;i< this.total;i++){
        var id=this.tSrc[i]['id'];
        if( !this.tPlaying[ id ] && id!='building'){
        this.stopSound(id);
        }
       
    }

}

On a ainsi un jeu un peu plus vivant.

Le github
Le projet GitHub : https://github.com/imikado/rtshtml5

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12423/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-9

Dans ce jour 10, j’ai corrigé pas mal de bug notamment concernant le problème de suppression d’un arbre ou d’une unité
En effet, ces derniers étaient bien supprimés de la variable tCoordBuild/tCoordUnit mais pas du tableau tBuild/tUnit
J’ai également changé le comportement de base des unités: seule les unités enemies cherchent à se rapprocher pour attaquer: c’est d’ailleurs mieux car les ouviers n’ont pas la même capacité d’attaque que les soldats enemies qui font une ronde.

On va ici permettre à une unité qui coupe du bois, une fois que l’arbre n’existe plus d’aller en chercher un autre à proximité

note: pour rappel, le projet est toujours disponible sur github, dans les billets sur developpez je détaille des parties que je considère interressante, mais je ne détaille pas forcément certains bugfix ou réorganisation du code (pour le rendre plus propre)
N’hésitez pas à suivre le projet github (lien en bas du billet)

Trouver un autre arbre à proximité
On va dès l’instant que l’arbre est supprimé d’en trouver un autre et de mettre à jour la ronde de l’unité

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
}else if(aBuild && aBuild.name=='wood' && oUnit.counter >= 8 && oUnit.cycleToX!=''){
    //on indique à l'unité qu'elle transporte 10
    oUnit.wood=10;
   
    //a chaque iteration on decremente la ressource
    aBuild.ressource-=10;
    //si l'arbre est épuisé, on le supprime de la carte
    if(aBuild.ressource<=0){
        aBuild.clear();
        oGame.removeBuild(aBuild);
       
        //on définit un nouvel arbre à prendre en compte
        var tWood=[
            [-1,-1],
            [0,-1],
            [1,-1],
           
            [-1,0],
            [1,0],
           
            [-1,+1],
            [0,+1],
            [1,+1],
        ];
        for(var i=0;i<tWood.length;i++){
            var oBuild2=this.getBuild(aBuild.x+tWood[i][0],aBuild.y+tWood[i][1]);
            if(oBuild2 && oBuild2.name=='wood'){
                oUnit.cycleToX=oBuild2.x;
                oUnit.cycleToY=oBuild2.y;
               
                break;
            }
        }
    }
   
    //on remet le compteur à 0
    oUnit.counter=0;
   
    //on redéfinit la nouvelle cible (c'est un cycle)
    oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY);

Arretez son cycle bois si plus d’arbre
On a vu précédement que dans le cas où l’on a terminé de couper l’arbre il cherche à proximité pour changer d’arbre.
Nous rajoutons maintenant le cas où il n’en trouve pas pour cesser son cycle bois.

1
2
3
4
5
6
7
8
9
10
11
12
13
}else if(oUnit.cycleFromX!='' && (oUnit.targetX==oUnit.x || oUnit.targetY==oUnit.y) ){
   
    //si arrivee a destination et cycle
    if(oUnit.cycleObject=='wood'){
        //si il arrive a venir sur les coordonnées cible
        //c'est que l'arbre n'y est plus
        oUnit.clearCycle();
    }else if(oUnit.cycleFromX==oUnit.x && oUnit.cycleFromY==oUnit.y ){
        oUnit.setTarget(oUnit.cycleToX,oUnit.cycleToY);
    }else{
        oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY);
    }
}

Donner un prix aux unités comme pour les batiments
Jusqu’à présent on pouvait créer autant d’unité que l’on voulait, désormais chaque unité à un prix, comme pour les batiments
D’abord dans la classe Unit, on ajoute un cout dans le constructeur.

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
if(this.name=='Soldier'){
    this.shortname='Soldat';
    this.src='img3/unit-soldier.png';
    this.idImg='unit-soldier';
    this.life=150;
    this.attak=20;
   
    this.costOr=200;
   
}else if(this.name=='Archer'){
    this.shortname='Archer';
    this.src='img3/WC.png';
    this.idImg='unit-archer';
    this.life=100;
    this.attak=30; 
   
    this.costOr=500;
}else if(this.name='Worker'){
    this.shortname='Ouvrier';
    this.src='img3/unit-worker.png';
    this.idImg='unit-worker';
    this.life=50;
    this.attak=5;
   
    this.costOr=100;
   
    this.tBuildCreation.push(new Build('SoldierHouse',this.team));
    this.tBuildCreation.push(new Build('ArcherHouse',this.team));
}

Ensuite dasn la fonction de création de navigation de la classe Build, on ajoute le cout et on indique que si la ressource n’est pas suffisante, on ne peut plus créer d’unité.

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
buildNav:function(){
    var sHtml='';
   
    sHtml+='<h1>'+this.shortname+'</h1>';
    sHtml+='<p><img src="'+this.src+'"></p>';

   
    if(this.unitCreation){
       
        var sImg='';
   
        sHtml+='<h2>Cr&eacute;ation unit</h2>';
       
        sHtml+='<table><tr>';
       
        sHtml+='<td>';
       
        if(oGame.iOr &gt; this.unitCreation.costOr){

       
            sImg='';
            sColor='#fff';
           
        }else{
           
            sImg='';
           
            sColor='#333';
        }
       
        sHtml+='<td style="background:#444;color:'+sColor+';padding:4px 8px">';
                       
        sHtml+=sImg;
   
        sHtml+='<br />';
        sHtml+='<span style="border:1px solid gray;background:yellow">&nbsp;&nbsp;&nbsp;&nbsp;</span> ';
        sHtml+=this.unitCreation.costOr;
       
           
        sHtml+='</td>';
       
        sHtml+='</tr></table>';
       
    }

    getById('nav').innerHTML=sHtml;
},

rts_12janv

Le github
Le projet GitHub : https://github.com/imikado/rtshtml5

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12422/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-8

Gérer un mouse over
Il serait utile de pouvoir, au survol afficher un curseur particulier en fonction de l’unité/batiment présent sous la souris

Après avoir ajouté une variable onMouseOver à la classe Game, on va indiquer dans la méthode mousmove quand l’initialiser à 1

1
2
3
}else if(this.mouseX &gt; 0 &amp;&amp; this.mouseX  0 &amp;&amp; this.mouseY &lt; oLayer_map.height){
    this.onMouseOver=1;
}

On ajoute également dans la méthode refreshunit de dessiner la selection sous la souris

1
2
//on dessine le mouseover
this.drawMouseOver();

Qui ressemble à ceci:

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
drawMouseOver:function(){
    oLayer_cursor.clear();
    if(this.onMouseOver==0){
        return;
    }
   
    if(!this.checkCoordVisible(this.mouseCoordX,this.mouseCoordY)){
        return false;
    }

    var oBuild=this.getBuild(this.mouseCoordX,this.mouseCoordY);
    var oUnit=this.getUnit(this.mouseCoordX,this.mouseCoordY);
   
    var sColor=&#039;#ffffff&#039;;
   
    if(oBuild){
        if(oBuild.name==&#039;or&#039;){
            sColor=&#039;yellow&#039;;
        }else if(oBuild.name==&#039;wood&#039;){
            sColor=&#039;#01ad4e&#039;;
        }else if(oBuild.team!=this.team){
            sColor=&#039;red&#039;;
        }
        this.strokeMouseOver(oBuild,sColor);
    }else if(oUnit){
        if(oUnit.team!=this.team){
            sColor=&#039;red&#039;;
        }
        this.strokeMouseOver(oUnit,sColor);
    }
},

Ajout de la gestion d’équipe
On va ajouter une propriété team à la classe Build et Unit afin de différencier ses unités et les unités enemies.

On modifie également le process pour que le brouillard ne soit éclairci que par les unités de notre équipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
    //que si c'est une unité de notre équipe
    if(oUnit.team==this.team){
        this.setVisibility(x,y);
    }
},

Ajout de gestion d’attack
Pour l’instant on va gerer l’attaque si une unité est à proximité, on va également indiquer de se rapprocher si une unité enemie est à proxmimité

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
//recherche si unité à porté d'attaque
var tAttak=[
            [-1,-1],
            [0,-1],
            [1,-1],
           
            [-1,0],
            [1,0],
           
            [-1,+1],
            [0,+1],
            [1,+1],
];
for(var j=0;j&lt;tAttak.length;j++){
    var oUnit2=this.getUnit(oUnit.x+tAttak[j][0],oUnit.y+tAttak[j][1]);
    //si unité enemie
    if(oUnit2 &amp;&amp; oUnit2.team!=oUnit.team){
        //on decremente l&#039;enemie de la puissance d&#039;attaque
        oUnit2.life-=oUnit.attak;;
        if(oUnit2.life &lt;=0){
            //si unite dead, on l&#039;efface du jeu
            oUnit2.clear();
        }
        break;
    }
}


//recherche si unité adverse à proximitée
var tMove=[
            [-2,-2],
            [-1,-2],
            [0,-2],
            [1,-2],
            [2,-2],

            [-2,-1],
            [-1,-1],
            [0,-1],
            [1,-1],
            [2,-1],
           
            [-2,0],
            [-1,0],
            [1,0],
            [2,0],
           
           
            [-2,+1],
            [-1,+1],
            [0,+1],
            [1,+1],
            [2,+1],
           
            [-2,+2],
            [-1,+2],
            [0,+2],
            [1,+2],
            [2,+2],
];
for(var j=0;j&lt;tMove.length;j++){
    var oUnit2=this.getUnit(oUnit.x+tMove[j][0],oUnit.y+tMove[j][1]);
    //si unité enemie
    if(oUnit2 &amp;&amp; oUnit2.team!=oUnit.team){
        //l&#039;unité se rapproche pour attaquer
        oUnit.setTarget(oUnit2.x,oUnit2.y)
        break;
    }
}

}

Brouillard de deuxième niveau
Un essai d’ajout de brouillard de deuxième niveau qui permet de voir qu’autour des unités

rts_brouillard2

Le github
Le projet GitHub : https://github.com/imikado/rtshtml5

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12416/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-7

Ajourd’hui nous allons passer aux sprites
L’idée est d’éviter d’avoir une image par éléments, mais 2-3 images qui contiendrait l’ensemble afin d’avoir peu de fichiers à télécharger
On va créer 2 images: une qui contiendra les images 1×1 (map, unités, arbres…) cases et l’autre les images 2×2 (batiment, mine d’or…)

gimp-sprites2x2
rts_sprite

Ensuite on va créer une classe qui permettra dans un premier temps de charger les 2 images, et dans un second temps d’identifier chaque élément du sprite

Création de la classe de gestion de sprites

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
function Images(){
    this.tOImg=new Array();
    this.tDetail=new Array();
    this.counter=0;
}
Images.prototype={
    //methode qui permet de charger une image
    load:function(src,idImg){
        this.tOImg[idImg]=new Image();
        this.tOImg[idImg].src=src;
        this.tOImg[idImg].onload=function(){
            oImages.counter++;
            preload2();
        }
    },
    //methode qui permet d'identifier un élément du sprite
    setDetailOnId:function(id,y,x,width,height,idImg){
        this.tDetail[id]=new Array();
        this.tDetail[id]['x']=x;
        this.tDetail[id]['y']=y;
        this.tDetail[id]['width']=width;
        this.tDetail[id]['height']=height;
        this.tDetail[id]['idImg']=idImg;
    },
    //methode qui permet de dessiner un element sur un des canvas
    drawImageOnLayer:function(id,x,y,width,height,sLayer){
        var oCanvasTmp;
        if(sLayer=='map'){
            oCanvasTmp=oLayer_map;
        }else if(sLayer=='apercu'){
            oCanvasTmp=oLayer_apercu;
        }else if(sLayer=='perso'){
            oCanvasTmp=oLayer_perso;
        }else if(sLayer=='building'){
            oCanvasTmp=oLayer_building;
        }
       
        oCanvasTmp.drawImage2(this.tOImg[ this.tDetail[id]['idImg'] ],this.tDetail[id]['x'],this.tDetail[id]['y'],this.tDetail[id]['width'],this.tDetail[id]['height'],x,y,width,height);
       
    },
};

Chargement des sprites et identification
Il faut ensuite identifier chaque éléments:
On créé un tableau qui représente virtuellement les éléments sur le sprite
En bouclant dessus, on identifie dans la classe Images ceux-ci

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
oImages=new Images();
   
var tDetailTmp=new Array();
tDetailTmp=[
    ['case-beige2','case-water','case-beige','case-wood'],
    ['unit-worker'],
    ['unit-soldier'],
    ['unit-archer'],
];
for(var y=0;y&lt;tDetailTmp.length;y++){
    for(var x=0;x&lt;tDetailTmp[y].length;x++){
        oImages.setDetailOnId(tDetailTmp[y][x],y*40,x*40,40,40,&#039;1x1&#039;);
    }
}
var tDetailTmp=new Array();
tDetailTmp=[
    [&#039;build-SoldierHouse&#039;,&#039;build-SoldierHouse_-2&#039;,&#039;build-SoldierHouse_-1&#039;],
    [&#039;build-QG&#039;,&#039;build-QG_-2&#039;,&#039;build-QG_-1&#039;],
    [&#039;build-ArcherHouse&#039;,&#039;build-ArcherHouse_-2&#039;,&#039;build-ArcherHouse_-1&#039;],
   
    [&#039;build-mineOr&#039;],
];
for(var y=0;y&lt;tDetailTmp.length;y++){
    for(var x=0;x&lt;tDetailTmp[y].length;x++){
        oImages.setDetailOnId(tDetailTmp[y][x],y*80,x*80,80,80,&#039;2x2&#039;);
    }
}

oImages.load(&#039;img3/sprite1x1.png&#039;,&#039;1x1&#039;);
oImages.load(&#039;img3/sprite2x2.png&#039;,&#039;2x2&#039;);

Dessin d’une image issu du sprite
Pour dessiner, il suffit d’appeler l’objet ainsi.
Exemple pour la classe Build: on dessine l’id « this.idImg sur le canvas « building »

1
oImages.drawImageOnLayer(this.idImg+this.sSprite,(this.x-currentX)*widthCase,(this.y-currentY)*heightCase,widthCase*2,widthCase*2,'building');

Animation de construction d’un batiment
Pour feter l’arrivé des sprites j’ai ajouté un cycle de construction pour les batiments:
Dans la classe Game, ajout de la méthode:

1
2
3
4
5
6
7
8
9
refreshBuild:function(){
    for(var i=0;i&lt; this.tBuild.length;i++){
        var oBuild= this.tBuild[i];
        if(oBuild.level  3){
    oGame.refreshBuild();
    iRefreshBuild=0;
}

iRefreshBuild++;

note: je gère un compteur pour que le batiment ne se construise pas trop vite.

Optimisation du code
En passant au sprite, il a y a eu une autre optimisation de faite: comme on ne charge que 2 images en début de script, avec un mécanimes d’attente de chargement, on n’a plus besoin de se demander/ de coder de méthode onload asynchrone
Avant:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
build:function(){
    if(this.oImage==''){
        this.oImage=new Image(this);
        this.oImage.src=this.src;
        this.oImage._x=this.x;
        this.oImage._y=this.y;
        this.oImage.onload=this.drawImage;
    }else{
        oLayer_building.drawImage(this.oImage ,(this.x-currentX)*widthCase,(this.y-currentY)*heightCase,widthCase*2,widthCase*2);
    }
    (...)
},
drawImage:function(){
    oLayer_building.drawImage(this ,(this._x-currentX)*widthCase,(this._y-currentY)*heightCase,widthCase*2,widthCase*2);
},

Après:

1
2
build:function(){
    oImages.drawImageOnLayer(this.idImg+this.sSprite,(this.x-currentX)*widthCase,(this.y-currentY)*heightCase,widthCase*2,widthCase*2,'building');

Plus besoin de gérer deux modes: si l’image n’existe pas , avec gestion asynchrone via onload + si l’image existe
On affiche simplement l’image via l’objet oImages (via les sprites)
Le github
Le projet GitHub : https://github.com/imikado/rtshtml5

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12415/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-6

Modification de la selection d’unités
Si on veut par la suite pouvoir faire une multiple selection, on va remplacer la variable selected par un tableau tSelected
Et ainsi en selectionnant une unité on l’ajoute à ce tableau

1
2
3
4
5
6
7
8
9
10
select:function(oMix){
    //on enregistre l'unité/batiment
    this.tSelected.push(oMix);
   
    //on demande son dessin
    this.drawSelected();
   
    //on affiche la navigation
    oMix.buildNav();
},

Et lors de l’affichage des unités sélectionnés, on va boucler sur celui-ci

1
2
3
4
5
6
7
8
9
drawSelected:function(){
    //on efface le calque
    oLayer_select.clear();
   
    for(var i=0;i&lt;this.tSelected.length;i++){
        //on dessine un cadre sur un des calques au dimension de l&#039;élement
        oLayer_select.drawRectStroke((this.tSelected[i].x-currentX)*widthCase,(this.tSelected[i].y-currentY)*heightCase,this.tSelected[i].width,this.tSelected[i].height,&#039;#880044&#039;,3);
    }
},

Permettre de sélectionner plusieurs unités avec shift
Premièrement ajouter un onkeydown appelant oGame.keydown(event)
Puis nous définissons cette méthode pour qu’elle enregistre une variable shiftClicked

1
2
3
4
5
6
7
8
keydown:function(e){
    var touche = e.keyCode;

    //si shirt, on enregistre que shift est pressé
    if(touche==16){
        this.shiftClicked=1;
    }
},

Et l’on prévoit le fait de relacher la touche shift pour ne plus sélectionner plusieurs unités

1
2
3
keyup:function(e){
    this.shiftClicked=0;
},

Enfin on modifie la methode click de la classe Game pour prendre en compte le fait que la touche est pressée

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//si il y a une unité
if(oUnit){
    //si la touche shift n'est pas cliqué en efface le tableau de sélection
    if(!this.shiftClicked){
        this.clearSelect();
    }
    //on selectionne celle-ci
    this.select(oUnit);
}else if(oBuild){
    this.clearSelect();
    //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();
}

Correction des déplacements des unités
Si on sélectionne plusieurs unités et que l’on clique à un endroit, celles-ci vont fusionner.
Pour éviter cela, on modifie la méthode checkCoord (de la classe Game) pour prendre en compte également les coordonnées des unités et pas seulement des batiments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
checkCoord:function(x,y){
    y=parseInt(y+0);
    x=parseInt(x+0);
    if(this.tCoordBuild[ y ] &amp;&amp; this.tCoordBuild[ y ][ x ] &amp;&amp; this.tCoordBuild[ y ][ x ]!=''){
        console.log('not libre tCoordBuild[ '+y+' ][ '+x+' ]');
        return false;
    }
   
    if(this.tCoordUnit[ y ] &amp;&amp; this.tCoordUnit[ y ][ x ] &amp;&amp; this.tCoordUnit[ y ][ x ]!=''){
        console.log('not libre tCoordUnit[ '+y+' ][ '+x+' ]');
        return false;
    }
   
    if(map.tMap[y] &amp;&amp; map.tMap[y][x] &amp;&amp; map.tMap[y][x]==3){
   
        return true;
       
    }
    return false;

},

Tant que l’on corrige, on va également indiquer que l’on ne peut entrer dans un QG que dans le cas d’une « ronde »
Dans la méthode refreshUnit de la classe Game, on va verifier que les coordonnées de cycle sont renseigné
On passe de

1
2
//si la cible est le QG
if(aBuild &amp;&amp; aBuild.name=='QG' ){

à

1
2
//si la cible est le QG et que l'on est en "ronde"
if(aBuild &amp;&amp; aBuild.name=='QG' &amp;&amp; oUnit.cycleToX!=''){

Idem pour le bois

Gérons les arbres
Un arbre doit disparaitre au bout d’un certains nombre d’itération: ce n’est pas une ressource illimitée
Pour cela, on va ajouté une propriété que l’on va décrémenter sur la classe Wood afin de le faire disparaitre une fois épuisé
Dans la classe wood

1
2
3
4
5
6
7
8
9
10
11
12
13
function Wood(){
    this.name='wood';
    this.shortname='Bois';
    this.src="img3/case-wood.png";
   
    this.x=0;
    this.y=0;
   
    this.width=20;
    this.height=20;
   
    this.ressource=30;
}

Ensuite on va indiquer que supprimer l’arbre apres avoir épuisé l’arbre (dans la méthode refreshUnit de la classe Game)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
}else if(aBuild &amp;&amp; aBuild.name=='wood' &amp;&amp; oUnit.counter &gt;= 8 &amp;&amp; oUnit.cycleToX!=''){
    //on indique à l'unité qu'elle transporte 10
    oUnit.wood=10;
   
    //a chaque iteration on decremente la ressource
    aBuild.ressource-=10;
    //si l'arbre est épuisé, on le supprime de la carte
    if(aBuild.ressource&lt;=0){
        aBuild.clear();
    }
   
    oUnit.x=newX;
    oUnit.y=newY;
   
    //on remet le compteur à 0
    oUnit.counter=0;
   
    //on redéfinit la nouvelle cible (c&#039;est un cycle)
    oUnit.setTarget(oUnit.cycleFromX,oUnit.cycleFromY);

Et l'on va indiquer à l'arbre qu'avec sa methode clear il doit se supprimer de la map

1
2
3
4
5
6
7
clear:function(){
    map.tMap[this.y][this.x]=3;//un arbre c&#039;est 4, 3 c&#039;est une case vide
    oGame.clearBuild(this);
    sDirection=&#039;refresh&#039;;

    console.log(&#039;on supprime l arbre y:&#039;+this.y+&#039; x:&#039;+this.x);
}

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12413/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-5

Nous avons dans les précédents billets expliqué chaque fichier du projet, nous allons à partir d’aujourd’hui continuer ce rts pour arriver à un jeu fini ;)

Gerer les couts des batiments
Plutot que d’avoir écrit en dur 100 d’or pour les batiments, on va stoquer dans l’objet batiment le cout en or et en bois de chaque batiment

Pour cela on va editer la classe Build, on y ajoute 2 propriétés costOr et costWood
J’en ai profité pour remanier un peu le constructeur: cette fois on ne recoit que le type de batiment et le constructeur fait le reste (adresse de l’image, cout…)

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
//build
function Build(name,src){
    this.name=name;
    this.oImage='';
   
    this.x=0;
    this.y=0;
    this.life=100;
   
    this.width=widthCase*2;
    this.height=widthCase*2;
   
    this.color='#064474';
    this.bVisibility=1;
   
    this.costOr=0;
    this.costWood=0;
   
    if(this.name=='or'){
        this.color='#e8bb08';
        this.bVisibility=0;
        this.shortname='Mine d\'or';
        this.src='img3/mine-or.png';
        this.unitCreation ='';
    }else if(this.name=='QG'){
        this.shortname='Quartier g&eacute;n&eacute;ral';
        this.src='img3/build1.png';
       
        this.unitCreation =new Unit('Worker');
    }else if(this.name=='SoldierHouse'){
        this.shortname='Batiment des soldats';
        this.src='img3/build2.png';
       
        this.costOr=100;
        this.costWood=100;
       
        this.unitCreation =new Unit('Soldier');
    }else if(this.name=='ArcherHouse'){
        this.shortname='Batiment des archers';
        this.src='img3/build3.png';
       
        this.costOr=200;
        this.costWood=50;
       
        this.unitCreation =new Unit('Archer');
    }
   
}

J’ai également remanié la classe unit, c’est également le constructeur qui fait tout: image, nom et batiments constructibles.

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
function Unit(name){
    this.name=name;
    this.oImage='';
   
    this.x=0;
    this.y=0;
    this.life=100;
   
    this.targetX='';
    this.targetY='';
   
    this.width=widthCase;
    this.height=widthCase;
   
    this.oBuildOn=null;
   
    this.counter=0;
    this.or=0;
    this.wood=0;
   
    this.cycleFromX='';
    this.cycleFromY='';
   
    this.cycleToX='';
    this.cycleToY='';
   
    this.tBuildCreation=new Array();
   
    if(this.name=='Soldier'){
        this.shortname='Soldat';
        this.src='img3/WPface.png';
       
    }else if(this.name=='Archer'){
        this.shortname='Archer';
        this.src='img3/WC.png';
           
    }else if(this.name='Worker'){
        this.shortname='Ouvrier';
        this.src='img3/WK.png';
       
        this.tBuildCreation.push(new Build('SoldierHouse'));
        this.tBuildCreation.push(new Build('ArcherHouse'));
    }
}

Limitation du scroll à la carte
Pour éviter que l’on puisse scroller à l’infini, on va ajouter un garde fou à la classe Game

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
Game.prototype={
    drawDirection:function(){
        oLayer_cadre.clear();
        //left
        oLayer_cadre.drawRect(0,0,10,oLayer_map.height,'#eeaf17','#eeaf17');
        //right
        oLayer_cadre.drawRect(oLayer_map.width-10,0,10,oLayer_map.height,'#eeaf17','#eeaf17');
        //bottom
        oLayer_cadre.drawRect(0,oLayer_map.height-10,100,10,'#eeaf17','#eeaf17');
        oLayer_cadre.drawRect(500,oLayer_map.height-10,300,10,'#eeaf17','#eeaf17');
        //top
        oLayer_cadre.drawRect(0,0,oLayer_map.width,10,'#eeaf17','#eeaf17');
    },
    goLeft:function(){
        if(currentX-1  map.tMap[0].length){
            return ;
        }
       
        currentX+=1;
        this.rebuild();
        oLayer_cadre.drawRect(oLayer_map.width-10,0,10,oLayer_map.height,'#eeaf17','#eeaf17');
    },
    goDown:function(){
        if(currentY+1+maxY &gt; map.tMap.length){
            return ;
        }
       
        currentY+=1;
        this.rebuild();
        oLayer_cadre.drawRect(0,oLayer_map.height-10,100,10,'#eeaf17','#eeaf17');
        oLayer_cadre.drawRect(500,oLayer_map.height-10,300,10,'#eeaf17','#eeaf17');
    },
    goUp:function(){
        if(currentY-1 &lt; 0){
            return ;
        }
       
        currentY-=1;
        this.rebuild();
        oLayer_cadre.drawRect(0,0,oLayer_map.width,10,&#039;#eeaf17&#039;,&#039;#eeaf17&#039;);
    },

Permettre de scroller via la mini map
Sur une carte très grande, on imagine pas devoir scroller à la souris, ce serait trop long
On préfèrerai cliquer directement sur la map
Pour cela, on va dans un premier temps modifier la partie html afin de prendre en compte l’evenement clic gauche et droite sur la carte d’apercu

Puis dans la classe Game, on rajouter une methode clickApercu

1
2
3
4
5
6
7
8
9
10
11
12
13
clickApercu:function(e){
    //recuperation des coordonnées "tableau" x y
    var x=this.getXApercu(e);
    var y=this.getYApercu(e);
   
    currentX=x-(maxX/2);
    currentY=y-(maxY/2);
   
    if(currentX &lt; 0 ){ currentX=0;}
    if(currentY &lt; 0 ){ currentY=0;}
   
    sDirection=&#039;refresh&#039;;
},

J'ai également ajouté dans la fonction un rafraichissement des batiments dans le cas d'un scroll via l'apercu

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
function run(){  

    //si la souris est sur une zone active de scroll
    if(sDirection==&#039;up&#039;){
        //scroll haut
        oGame.goUp();
    }else if(sDirection==&#039;down&#039;){
        //scroll bas
        oGame.goDown();
    }else if(sDirection==&#039;left&#039;){
        //scroll gauche
        oGame.goLeft();
    }else if(sDirection==&#039;right&#039;){
        //scroll droite
        oGame.goRight();
    }else{
        //si direction refresh, on redessine la map
        if(sDirection==&#039;refresh&#039;){
            oGame.rebuild();
            sDirection=&#039;&#039;;
        }
       
        //sinon on affiche les zones réactives
        oGame.drawDirection();
       
        //on raffraichit les unités
        oGame.refreshUnit();
    }
   
   
   
    //dans N secondes on appelera de nouveau cette fonction
    setTimeout(run,fps);
}

Permettre de déplacer une unité en cliquant directement sur l’aperçu
Afin de rendre la tache plus simple, on va factoriser le contenu de la méthode clickdroit (de la classe Game)
En créant une méthode goto, il est plus simple ainsi de permettre via un clic droit sur l’aperçu de déplacer l’unité

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
goto:function(x,y){
    //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!='' &amp;&amp; aBuild &amp;&amp; ( 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
   
    }else if(this.selected!=''){
        //si la case est accessible, on y indique à l'unité d'y aller
        this.selected.setTarget(x,y);
    }
},
//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);

    this.goto(x,y);
    return false;

},
clickdroitApercu:function(e){
    //recuperation des coordonnées "tableau" x y
    var x=this.getXApercu(e);
    var y=this.getYApercu(e);
   
    this.goto(x,y);
    return false;
},

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12411/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-4

Dans ce billet, nous verrons la classe Unit, qui permet de gerer des unités

Le constructeur d’unité

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
function Unit(name,src){
    //le nom type d'unité
    this.name=name;
    //l'adresse de l'image
    this.src='img3/'+src;
    this.oImage='';
   
    //les coordonnées + les vies
    this.x=0;
    this.y=0;
    this.life=100;
   
    //la destination cible
    this.targetX='';
    this.targetY='';
   
    //la largeur/hauteur de l'unité
    this.width=widthCase;
    this.height=widthCase;
   
    //contiendra le moment venu un batiment à construire
    this.oBuildOn=null;
   
    //compteur utilisé lors de la récupération de ressource
    this.counter=0;
    //ressources or/bois transporté
    this.or=0;
    this.wood=0;
   
    //pour une ronde point de départ
    this.cycleFromX='';
    this.cycleFromY='';
   
    //pour une ronde point d'arrivée
    this.cycleToX='';
    this.cycleToY='';
   
    //tableau contenant les batiments que l'unité peut construire
    this.tBuildCreation=Array();
   
    if(this.name=='soldat'){
        this.tBuildCreation[0]=new Buildcreation('buid2','build2.png');
    }
}

L’affichage de l’unité
Méthode utilisée pour afficher l’unité
Comme vous pouvez le voir il y a également une partie pour la construction effective d’un batiment.

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
build:function(){
    //partie affichage de l'image de l'unité sur le canvas
    if(this.oImage==''){
        this.oImage=new Image(this);
        this.oImage.src=this.src;
        this.oImage._x=this.x;
        this.oImage._y=this.y;
        this.oImage.onload=this.drawImage;
    }else{
        oLayer_perso.drawImage(this.oImage ,((this.x-currentX)*widthCase),((this.y-currentY)*heightCase),widthCase-2,widthCase-2);
       
        oLayer_perso.fillRect((this.x-currentX)*widthCase,((this.y-currentY)*heightCase)+heightCase-2,widthCase,2,'#00ff00');
       
    }
   
    //si l'unité doit construire un batiment, et qu'elle se trouve sur les lieux de la construction
    if(this.oBuildOn &amp;&amp; this.x+1==this.oBuildOn.x &amp;&amp; this.y==this.oBuildOn.y){
       
        //création du batiment à l'emplacement
        var aBuild=new Build(this.oBuildOn.name,this.oBuildOn.src);
        aBuild.x=this.oBuildOn.x;
        aBuild.y=this.oBuildOn.y;
        aBuild.build();
       
        //ajout du batiment à la liste des batiments (pour la reconstruction lors des scroll)
        oGame.tBuild.push(aBuild);
        //on sauvegarde les coordonnées du batiments
        oGame.saveBuild(aBuild);
       
        //on reset les propriétés de construction
        oGame.buildcreation='';
        this.buildOnX='';
        this.buildOnY='';
        this.oBuildOn='';
       
        //on décrément la ressource or
        oGame.iOr-=100;
        //on réactualise les ressources
        oGame.buildRessource();
        //on reset la sélection
        oGame.clearSelect();
       
    }
    //on enregistre les nouvelles coordonnées de l'unité
    oGame.saveUnit(this);
},

Quelques remarques, vous pouvez lire certaines portions de code qui seront amenés à être modifié: par exemple le fait que l’on décrémente uniquement l’or de 100 unités et pas le bois lorsque l’on créé un batiment.
Cette notion de cout de construction sera ensuite stoquée dans la classe build afin de permettre d’avoir des batiments de prix différents (or/bois)

Création de la navigation
Lorsque l’on sélectionne une unité, on ne va pas avoir les mêmes possibilités en fonction du type d’unité.
C’est ici qu’intervient la méthode buildNav qui va construire la navigation correspondant à l’unité.

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
buildNav:function(){
    var sHtml='';
   
    if(this.name=='soldat'){
        sHtml='<p><img src="'+this.src+'"></p>';
       
        sHtml+='<h2>Construction</h2>';
       
        var sEnabled='';
       
        //on boucle sur les batiments que l'unité peu construire
        for(var i=0;i&lt;this.tBuildCreation.length;i++){
            if(oGame.iOr &lt; 100){
                 sHtml+=&#039;';
            }else{
                sHtml+='
';
            }
           
            sHtml+='
';
        }
       
    }else{
        sHtml='
<p><img src="'+this.src+'"></p>';
       
    }
   
    getById('
nav').innerHTML=sHtml;
},

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

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12406/developpement/journal-de-bord-creation-dun-rts-en-html5-jour-3

Dans ce billet je vais vous détailler le fichier rts_Map.js, son utilisation, sa construction.

Faire une application HTML5/canvas: construire la Map
Dans ce jeux de stratégie, comme dans beaucoup d’autres jeux, la carte est trop grande pour être affichée dans son ensemble,.
Il faut donc afficher une partie de la carte, et permettre de visualiser ce que l’on regarde par rapport à la carte complète.

C’est là qu’intervient la classe Map (fichier rts_Map.js)
Comme pour la classe Game, nous allons ici voir bloc par bloc

Création de 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
build:function(){
         
        for(var y=0;y&lt; maxY;y++){
            for(var x=0;x&lt; maxX;x++){
               
                //decalage x/y en fonction du scrolling
                var x2=x+currentX;
                var y2=y+currentY;
                 
                if(this.tMap[y2] &amp;&amp; this.tMap[y2][x2]){
                    //si c&#039;est un arbre
                    if(this.tMap[y2][x2]==4){
                        //on dessine un case normale
                        this.drawImage( 3 ,x,y);
                        //puis on créé un objet arbre par dessus
                        var oWood=new Wood();
                        oWood.x=x;
                        oWood.y=y;
                        oWood.build();
                       
                        //on ajoute cette arbre au tableau des batiments
                        //pour les reconstruire lors du scrolling
                        oGame.tBuild.push(oWood);
                    }
                    //on dessine sur le canvas la valeur du tableau
                    this.drawImage( this.tMap[y2][x2] ,x,y);
                   
                }
            }  
        }
       
    },
    //la methode pour dessiner sur le canvas
    drawImage:function(iImg,x,y){
        if(!this.tOImg[iImg]){
            var oImg=new Image();
            oImg.src=&#039;img3/&#039;+this.tImg[iImg];
            oImg._x=x;
            oImg._y=y;
            oImg.onload=function(){
                oLayer_map.drawImage(this,this._x*widthCase,this._y*heightCase,widthCase,widthCase);
               
            }
            this.tOImg[iImg]=oImg;
           
        }else{
            oLayer_map.drawImage(this.tOImg[iImg],x*widthCase,y*heightCase,widthCase,widthCase);
        }
       
    },

Petite parenthèse sur les images
Pensez bien que lorsque vous souhaitez dessiner une image sur un canvas, vous n’etes pas en local: les images de votre jeu ne sont pas chargées.
Pour gerer ce mode asynchrone: on créé un objet image, on lui assigne des propriétés comme ces coordonnées et enfin on lui indique du code à éxécuter au moment du chargement avec « onload »
Mais il faut également prévoir le fait que l’image ai déjà été chargée précédement.
C’est pour cela que je stoque ici l’objet image dans un tableau de propriété de la classe, ainsi, si l’objet existe on le dessine tout de suite.
En revanche si il n’existe pas on instancie avec les propriétés ainsi que la méthode onload.

Méthode de reconstruction
A chaque scroll, il faut redessiner la map: on se déplace sur la carte, mais il ne faut pas instancier de nouveaux objets arbres
C’est pour cela qu’il y a une méthode rebuild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rebuild:function(){
    for(var y=0;y&lt; maxY;y++){
        for(var x=0;x&lt; maxX;x++){
           
            var x2=x+currentX;
            var y2=y+currentY;
             
            if(this.tMap[y2] &amp;&amp; this.tMap[y2][x2]){
                if(this.tMap[y2][x2]==4){
                    this.drawImage( 3 ,x,y);
                }
                this.drawImage( this.tMap[y2][x2] ,x,y);
               
            }
        }  
    }
},

La construction de l’aperçu
Il faut également afficher l’aperçu de la carte.
La methode buildApercu permet cela, cette méthode ressemble beaucoup à la méthode build
Les différences sont: on dessine les cases beaucoup plus petites, on dessine sur un calque supérieur un cadre indiquant ce que l’on voit en détail.

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
//construction de l'apercu
    buildApercu:function(){
        var maxMiniY=this.tMap.length;
        var maxMiniX=this.tMap[0].length;
       
        for(var y=0;y&lt; maxMiniY;y++){
            for(var x=0;x&lt; maxMiniX;x++){
               
                var x2=x;
                var y2=y;
               
                if(this.tMap[y2] &amp;&amp; this.tMap[y2][x2]){
                    if(this.tMap[y2][x2]==4){
                        this.drawMiniImage( 3 ,x,y);
                    }
                    this.drawMiniImage( this.tMap[y2][x2] ,x,y);
                }
            }
        }
        oLayer_apercuBrouillard.fillRect(0,0,400,400,&#039;#000000&#039;);
    },
    drawMiniImage:function(iImg,x,y){
        oLayer_apercu.drawImage(this.tOImg[iImg],x*this.miniWidth,y*this.miniWidth,this.miniWidth,this.miniWidth); 
    },
    //dessin du cadre indiquant la partie affichée en détail
    buildApercuCadre:function(){
        oLayer_apercuCadre.clear();
        oLayer_apercuCadre.drawRectStroke(currentX*this.miniWidth,currentY*this.miniWidth,this.miniWidth*maxX,this.miniWidth*maxY,&#039;#ff0000&#039;,2);
    },

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 &amp;&amp; e.x!=null &amp;&amp; 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 &amp;&amp; e.x!=null &amp;&amp; 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] &amp;&amp;  this.tCoordUnit[y][x]){
            return this.tCoordUnit[y][x];
        }
        return null;
    },
    getBuild:function(x,y){
        if(this.tCoordBuild[y] &amp;&amp;  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!='' &amp;&amp; aBuild &amp;&amp; ( 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) &amp;&amp; this.getYmouse(e) &lt; oLayer_map.height-1 &amp;&amp; (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&lt; this.tUnit.length;i++){
        var oUnit= this.tUnit[i];
       
        //si l&#039;unité doit se rendre quelques part
        if(oUnit.targetX!=&#039;&#039; &amp;&amp; oUnit.targetY!=&#039;&#039; &amp;&amp; (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!=&#039;&#039; &amp;&amp; oUnit.x  oUnit.targetX ){
                newX-=vitesse;
            }
            if(oUnit.targetY!='' &amp;&amp; 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 &amp;&amp; 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 &gt;0){
                    //on ajoute une ressource or
                    this.addRessourceOr(oUnit.or);
                }else   if(oUnit.wood &gt;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 &amp;&amp; aBuild.name=='wood' &amp;&amp; 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 &amp;&amp; aBuild.name=='or' &amp;&amp; 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