Journal de bord: création d’un bomberman-like en HTML5, jour 4

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

Aujourd’hui on va passer l’intelligence du jeu coté serveur, on va arrêter le concept du joueur bleu qui pilote la partie.
Pour cela, on va alléger la partie client qui ne devrait à terme pouvoir uniquement effacer, afficher, animer un personnage, une bombe (demandé par le serveur).
Le client va également indiquer au serveur les touches qu’il appuie ou qu’il relache (pour que le serveur puisse déduire la direction, et les coordonnées des bombes à placer)

Sur le github habituel vous pouvez voir pas mal de mise à jour en conséquence.

Duplication des classes Game,Perso et Bomb
Pour cela, il nous faut simplifier les classes coté client, et modifier un peu celles coté serveur.
Avant, lorsqu’un joueur été déplacé, le joueur bleu indiquait au serveur la modification à faire pour que celui-ci la broadcast.
Maitenant, vu que c’est le serveur qui gère l’ensemble: il sait que le joueur se déplace et le broadcast directement.

Parenthèse Node.js
Quand j’ai commencé cette migration, je me suis dit que ce serait simple simplement ajouté des inclusions dans le fichier server, un chose que je ne savais pas c’est qu’apparement la porté des variables n’est pas la même :(
Une chose à faire par exemple quand vous importez une classe dans node.js, il faut penser dans celle-ci à l’exporter
Par exemple: (dans serverBomberM.js)

1
var Perso=require('./bombermanM_PersoB.js');

Et dans le fichier de la classe bombermanM_PersoB.js (en fin de fichier)

1
module.exports=Perso;

Je vous invite à attendre la fin de cette migration pour lire les fichiers sur github, je ferais un billet quand ce sera fini.

Le dépot Github:
https://github.com/imikado/bomberhtml5

Journal de bord: création d’un bomberman-like en HTML5, jour 3

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12445/developpement/journal-de-bord-creation-dun-bomberman-like-en-html5-jour-2

Aujourd’hui nous allons corriger quelques bugs du jeu rencontré lors des premiers essai avec des collègues.

  • Menu affichage des team (en fonction des team disponibles)
  • Permettre de déposer une bombe pendant que le joueur court
  • Bugfix concernant le haut de la map et la partie gauche

Mise a jour affichage des teams disponibles
Lorsque les joueurs rejoignent la partie, il leur faut choisir une équipe bleu,rouge,jaune ou vert.
On va mettre à jour cette liste à chaque fois qu’un joueur choisit une équipe ou quitte la partie

Du coté client, on va, au moment où l’on choisit une équipe, indiquer au serveur la team choisie pour qu’il l’enregistre
Coté client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTeam:function(team){
    this.team=team;
               
    if(team=='blue'){
        setTimeout(run,fps);
    }
       
    getById('team').style.display='none';
               
    map.build();
    this.refresh();
       
    socket.emit('setTeamBroadcast',team);
},

Coté serveur

1
2
3
4
5
6
7
8
socket.on('setTeamBroadcast',function(team){
    tTeamConnected[team]=1;
    tSocket[socket.id]=team;
       
    var tTeamConnectedB=getTeamConnected();
       
    socket.broadcast.emit('Game.listTeam',tTeamConnectedB);
});

Ensuite du coté du serveur, on va indiquer que lors de la connection on va broadcaster les team actives

1
2
3
4
5
6
7
8
socket.on('setTeamBroadcast',function(team){
    tTeamConnected[team]=1;
    tSocket[socket.id]=team;
       
    var tTeamConnectedB=getTeamConnected();
       
    socket.broadcast.emit('Game.listTeam',tTeamConnectedB);
});

Et la fonction getTeamConnected()

1
2
3
4
5
6
7
8
9
function getTeamConnected(){
    var tTeamConnectedB=Array();
    for(var i=0;i< tColor.length;i++){
        if(tTeamConnected[tColor[i]]==1){
            tTeamConnectedB.push(tColor[i]);
        }
    }
    return tTeamConnectedB;
}

Idem lors de la déconnection:

1
2
3
4
5
6
7
8
socket.on('disconnect', function () {
    tTeamConnected[ tSocket[socket.id] ]=0;
       
    var tTeamConnectedB=getTeamConnected();
       
    socket.emit('Game.listTeam',tTeamConnectedB);
    socket.broadcast.emit('Game.listTeam',tTeamConnectedB);
});

On appelle la fonction d’affichage du menu des teams disponibles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
socket.on('Game.listTeam',function(tTeamConnected){
       
    for(var i=0;i<tTeam.length;i++){
        var a=getById('button-'+tTeam[i]);
        if(a){
            a.style.display='block';
        }
    }
       
    for(var i=0;i< tTeamConnected.length;i++){
        console.log('desactivation '+tTeamConnected[i]);
        var a=getById('button-'+tTeamConnected[i]);
        if(a){
            a.style.display='none';
        }
    }
       
});

Ainsi: lorsqu’on arrive sur la partie, le jeu affiche la liste des 4 team, en parallèle si un autre joueur cliquer sur une team, il va broadcaster aux autres qu’une team n’est plus disponible.
Et au contraire, si un des joueurs quitte la partie: volontairement ou fait un F5 pour un soucis de navigateur, il peut de nouveau se reconnecter avec sa team.

Permettre de déposer une bombe en courant
Je cherchais pourquoi lorsque l’on courrait et appuyait sur la barre d’espace, le joueur déposait sa bombe et s’arretait…
C’était tout bête: un bug lors de la gestion du keyUp (relachement d’une touche)
Dans ce cas là j’annulais la direction du personnage, hors dans le cas du relachement de la barre d’espace, ce ne doit pas être le cas
Ainsi, la modification

1
2
3
4
5
6
7
eventKeyUp:function(e){
    var touche = e.keyCode;
    //si touche relachée différente de la barre d'espace
    if(touche!=32){
        this.setTeamDirectionBroadcast('');
    }
},

Conclusion
Voilà pour ce 3ème jour, comme vous le voyez en peu de temps, on a déjà un jeu multi-joueur jouable.
Les prochains jours permettront d’améliorer la jouabilité ;)

Journal de bord: création d’un bomberman-like en HTML5, jour 2

Introduction
Ce billet fait suite au billet: http://blog.developpez.com/ducodeetdulibre/p12443/developpement/journal-de-bord-creation-dun-bomberman-like-en-html5-jour-1

Ici nous allons voir les classes des personnages et des bombes

bombermanM_Perso.js
Ici la classe de personnage qui va permettre d’afficher chacun des personnages sur la map du jeu.
Elle permet également de gérer son animation: pour simuler la marche de celui-ci en direction de la touche de curseur pressée.
Regardons de plus près la méthode build:

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
build:function(){
               
    var sDirection=oGame.tDirection[this.team];
       
    //si pas de direction, on affiche l'image statique 
    if(sDirection==''){
        tmpImg=this.idImg;
    //si l'image précédente était marche 2, on affiche marche 1
    }else if(this.tmpIdImg==this.idImg+'_walking2'+sDirection){
        tmpImg=this.idImg+'_walking'+sDirection;
    //sinon on affiche la marche 2
    }else{
        tmpImg=this.idImg+'_walking2'+sDirection;
    }
    this.tmpIdImg=tmpImg;
       
    //partie affichage de l'image du personnage sur le canvas
    oImages.drawImageOnLayer(tmpImg,(this.x*widthCase)-20,(this.y*heightCase)-20,widthCase,widthCase,'perso');
       
    //on affiche ici un carré représentant la couleur de la team
    oLayer_perso.fillRect((this.x*widthCase),(this.y*heightCase)-25,8,8,this.team);
       
    //on enregistre les nouvelles coordonnées du joueur
    oGame.savePerso(this);
       
},

Comme vous pouvez le voir, on récupère la direction du personnage qui est déduite dans la classe Game (lors du pressage de la touche)
Si aucune direction: le joueur attend, on affiche l’image statique (de face), sinon il y a une direction d’engagée.
Dans ce cas on gère une animation de marche: pour cela on a par direction 2 images, et on va les alterner à chaque affichage du personnage.
Vous voyez également qu’on affiche un petit carré en haut à gauche du personnage pour signaler son équipe.

bombermanM_Bomb.js
Une nouvelle classe fait ici son apparition afin de gérer des objets « Ã©phémères »: les bombes: en effet, à partir de leur création, elles ont une durée limitée sur la map.
De plus, elles doivent à la fin exploser: créer une image de flamme sur plusieurs cases adjacentes et supprimer les eventuels joueurs sur le passage.

Premièrement, au niveau de l’affichage des bombes, on va gérer une animation:

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
refreshBomb:function(){
           
    for(var i=0;i< this.tBomb.length;i++){
        var oBomb= this.tBomb[i];
        if(oBomb.life < 14){
            //pendant 14 iterations, on va alterner entre deux sprites
            if(oBomb.life % 2 ){
                oBomb.idImg='bomb-0';
            }else{
            oBomb.idImg='bomb-1';
            }  
        }else if(oBomb.life < 17){
            //puis animation d'explosion
            if(oBomb.life % 2 ){
                oBomb.idImg='explosion';
            }else{
                oBomb.idImg='explosion-1';
            }
        }else if(oBomb.life < 19){
            oBomb.idImg='explosion-2';
        }else{
            oBomb.idImg='explosion-finish';          
        }
           
        oBomb.life++;
           
        //on broadcast l'animation de la bombe
        oBomb.animateBroadcast(oBomb.idImg);
    }
               
},

Ensuite au niveau de la classe bombe, on gère l’affichage

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
build:function(){
       
    if(this.idImg=='explosion' || this.idImg=='explosion-1' || this.idImg=='explosion-2'){
        for(var i=-2;i< 3;i++){
            oLayer_bomb.clearRect(((this.x+i)*widthCase),(this.y*heightCase),widthCase,widthCase);
            oLayer_bomb.clearRect(((this.x)*widthCase),((this.y+i)*heightCase),widthCase,widthCase);
           
            if(map.tMap[this.y][this.x+i]==1){
                oImages.drawImageOnLayer(this.idImg,((this.x+i)*widthCase),(this.y*heightCase),widthCase,widthCase,'bomb');
            }
            if(map.tMap[this.y+i][this.x]==1){
                oImages.drawImageOnLayer(this.idImg,((this.x)*widthCase),((this.y+i)*heightCase),widthCase,widthCase,'bomb');
            }
        }
   
    }else if(this.idImg=='explosion-finish'){
        for(var i=-2;i< 3;i++){
            oLayer_bomb.clearRect(((this.x+i)*widthCase),(this.y*heightCase),widthCase,widthCase);
            oLayer_bomb.clearRect(((this.x)*widthCase),((this.y+i)*heightCase),widthCase,widthCase);
           
            var oPersoVictim=oGame.getPerso(this.x+i,this.y);
            if(oPersoVictim){
                oGame.removeBroadcastPersoById(oPersoVictim.id);
                console.log('remove '+oPersoVictim.id);
            }
            oPersoVictim=oGame.getPerso(this.x,this.y+i);
            if(oPersoVictim){
                oGame.removeBroadcastPersoById(oPersoVictim.id);
                console.log('remove '+oPersoVictim.id);
            }
           
        }
       
        oGame.removeBroadcastBombById(this.id);
        return;
    }else{
        oLayer_bomb.clearRect((this.x*widthCase),(this.y*heightCase),widthCase,widthCase);
    }
           
    oImages.drawImageOnLayer(this.idImg,(this.x*widthCase),(this.y*heightCase),widthCase,widthCase,'bomb');
       
    oGame.saveBomb(this);
   
},

Gestion intersection
Afin d’eviter d’avoir un bug graphique de chevauchement: lorsqu’un joueur est à cheval entre deux cases, il ne peut pas descendre/monter au risque de « marcher sur un mur »
Pour cela, dans la gestion du déplacement, on verifie que l’on est pas à cheval:

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
refreshPerso:function(){
   
    //on boucle sur les persos existants
    for(var i=0;i< this.tPerso.length;i++){
        var oPerso= this.tPerso[i];
        if(oPerso.life <=0){ continue;}
       
            var vitesse=0.5;
           
            if(!this.tDirection[oPerso.team]){
                continue;
            }
           
            var sDirection=this.tDirection[oPerso.team];
           
            //on efface le dessin sur le calques
            oPerso.clear();
           
            //on initialise les nouvelles coordonnées
            var newX=oPerso.x;
            var newY=oPerso.y;
            var newXcheck=oPerso.getX();
            var newYcheck=oPerso.getY();
           
           
            if(newY != parseInt(newY)){
                //si entre deux cases, on ne peut pas descendre/monter
            }else if(sDirection=='right'){
                newX+=vitesse;
                newXcheck+=1;
            }else if(sDirection=='left'){
                newX-=vitesse;
                newXcheck-=1;
            }
            if(newX != parseInt(newX)){
                //si entre deux cases, on ne peut pas descendre/monter
            }else if(sDirection=='up'){
                newY-=vitesse;
                newYcheck-=1;
            }else if(sDirection=='down'){
                newY+=vitesse;
                newYcheck+=1;
            }
           
            if(this.checkCoord(newXcheck,newYcheck)){
                //si les coordonnées est libre
                oPerso.x=newX;
                oPerso.y=newY;
               
            }
           
            //on dessine le personnage
            oPerso.buildBroadcast('walking');
           
       
    }
   
   
},

En image
bomber4

Le dépôt github
Le dépôt https://github.com/imikado/bomberhtml5

Journal de bord: création d’un bomberman-like en HTML5, jour 1

bomber1

Introduction
Lors du précédent journal de bord, j’ai développé un jeu de stratégie temps réel ou RTS multiplayer en HTML5.
J’ai beaucoup appris de ces 17 jours, et j’espère que vous aussi.
Aujourd’hui commence un nouveau journal de bord pour developper un jeu bomberman-like multiplayer également en HTML5.
L’avantage, c’est qu’on ne part pas de zéro, on a déjà une base de structure Map/unité/cycle/serveur multiplayer…
note: comme pour le RTS, ce jeu est disponible sur github, je mets des extraites de code pour exemple mais les classes entières sont disponibles sur le dépôt.

Les bases du jeu
Voici la liste des fichiers du jeu:

  • bombermanM.html
  • bombermanM.js
  • bombermanM_Bomb.js
  • bombermanM_Game.js
  • bombermanM_Map.js
  • bombermanM_Perso.js
  • bombermanM_Sound.js

+ Fichier serveur coté node.js

  • serverBomberM.js

bombermanM.html
Comme pour le RTS: une page html qui charge les différents fichiers javascripts du jeu.
Il y a toujours deux div utilisés pour indiquer le chargement et l’autre pour permettre de choisir sa team.
A une différence: il y a moins de canvas (calques) à gerer: ici layer_map, layer_bomb et layer_perso.

bombermanM.js
Script principal du jeu qui contient également la classe Images permettant de charger et d’identifier les sprites.
Une fonction de preload qui charge le sprite, puis instancie les canvas, le jeu et construit la map.
Ce script contient également la méthode appelé en continue: la fonction run()

bombermanM_Map.js
Cette classe est quasiment la même que celle du RTS à une différence près: il n’y a pas ici de gestion d’aperçu.
Cette classe est constitué d’un constructeur et de 2 méthodes build() et drawImage()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Map.prototype={
    build:function(){
        for(var y=0;y< maxY;y++){
            for(var x=0;x< maxX;x++){
                if(this.tMap[y] && this.tMap[y][x]){
                    //on dessine sur le canvas la valeur du tableau
                    this.drawImage( this.tMap[y][x] ,x,y);
                }
            }  
        }
    },
    //la methode pour dessiner sur le canvas
    drawImage:function(iImg,x,y){
        console.log(this.tImg[iImg]);
        oImages.drawImageOnLayer(this.tImg[iImg],x*widthCase,y*heightCase,widthCase,widthCase,'map');
    },
   
};

bombermanM_Game.js
Cette classe ressemble également beaucoup à celle du RTS, il y a dans le constructeur la partie écoute du socket,

1
2
3
4
5
6
7
8
9
10
11
12
13
socket=io.connect('http://localhost:1338');
   
socket.on('Game.createPerso',function(id, team,name,x,y){
    var oPerso=new Perso(name,team);
    oPerso.x=x;
    oPerso.y=y;
    oPerso.id=id;
    oPerso.build();
   
    console.log('creation perso team:'+team+' x:'+x+' y:'+y);
   
    oGame.tPerso.push(oPerso);
});

puis plusieurs méthodes du jeu comme la récupération d’un personnage,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getPersoById:function(id){
    for(var i=0;i< this.tPerso.length;i++){
        if(this.tPerso[i].id==id){
            return this.tPerso[i];
        }
    }
},
getPersoByTeam:function(team){
    for(var i=0;i< this.tPerso.length;i++){
        if(this.tPerso[i].team==team){
            return this.tPerso[i];
        }
    }
},

d'une bombe par son id ou ses coordonnées,

1
2
3
4
5
6
7
getBombById:function(id){
    for(var i=0;i< this.tBomb.length;i++){
        if(this.tBomb[i].id==id){
            return this.tBomb[i];
        }
    }
},

La création d'un bombe sur la map

1
2
3
4
createBombBroadcast:function(team,name,x,y){
    console.log('socket create bomb'+team+' '+name+' x:'+x+' y:'+y);
    socket.emit('Game.createBombBroadcast',team,name,x,y);
},

La boucle d'affichage des joueurs:

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
refreshPerso:function(){
       
    //on boucle sur les persos existants
    for(var i=0;i< this.tPerso.length;i++){
        var oPerso= this.tPerso[i];
        if(oPerso.life <=0){ continue;}
       
            var vitesse=0.5;
           
            if(!this.tDirection[oPerso.team]){
                continue;
            }
           
            var sDirection=this.tDirection[oPerso.team];
           
            //on efface le dessin sur le calques
            oPerso.clear();
           
            //on initialise les nouvelles coordonnées
            var newX=oPerso.x;
            var newY=oPerso.y;
           
            //on fait evoluer les coordonnées en fonction de la direction
            if(sDirection=='right'){
                newX+=vitesse;
            }else if(sDirection=='left'){
                newX-=vitesse;
            }
            if(sDirection=='up'){
                newY-=vitesse;
            }else if(sDirection=='down'){
                newY+=vitesse;
            }
           
            if(this.checkCoord(newX,newY)){
                //si les coordonnées est libre
                oPerso.x=newX;
                oPerso.y=newY;
               
            }
           
            //on dessine le personnage
            oPerso.buildBroadcast('walking');
           
       
    }
   
   
},

le choix d'une team

1
2
3
4
5
6
7
8
9
10
11
12
setTeam:function(team){
    this.team=team;
           
    if(team=='blue'){
        setTimeout(run,fps);
    }
   
    getById('team').style.display='none';
           
    map.build();
    this.refresh();
},

Et une chose nouvelle: la prise en compte des touches du clavier:

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
eventKeyDown:function(e){
    var touche = e.keyCode;

    this.resetKeys();
    if(touche==37){
        this.setTeamDirectionBroadcast('left');
    }
    if(touche==38){
        this.setTeamDirectionBroadcast('up');
    }
    if(touche==39){
        this.setTeamDirectionBroadcast('right');
    }
    if(touche==40){
        this.setTeamDirectionBroadcast('down');
    }
   
    if(touche==32){ //espace
        console.log('depot bombe');
        //boucle perso pour savoir ou creer la bombe
        var oPerso=this.getPersoByTeam(this.team);
           
        this.createBombBroadcast(oPerso.team,'normal',oPerso.getX(),oPerso.getY());
           
    }
},

Qui est appelé avec un onkeydown dans la balise body

1
onkeydown="oGame.eventKeyDown(event)"

Parenthèse sur la gestion d’evenement
Dans le RTS, chaque joueur utilisait sa souris pour sélectionner une unité, un batiment puis cliquait sur le jeu pour intéragir.
Par exemple quand il cliquait gauche sur la map, on donnait l’ordre à l’unité d’aller à un endroit, et l’on broadcastait cette cible.
Mais ici on ne joue pas à la souris mais au clavier, ici on ne va pas communiquer des coordonnées cible ou autre, on va broadcaster la direction voulue pour le personnage.
Ainsi tant que l’on maintient une touche de direction, le personnage continue sa course sans accout.

Le dépôt github
Le dépôt https://github.com/imikado/bomberhtml5

Quelques images
bomber1
bomber2

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

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

Ce jour 17 a été utilisé surtout pour corriger les moultes bugs de la version multi player.
Au fur et à mesure de développement multi joueur, je me suis heurté à beaucoup de soucis.

L’effet Larsen
Premièrement: node.js/socket.io, j’avais identifié les actions coté client et coté serveur avec les mêmes noms, exemple: Unit.build.
Je me suis rendu compte d’un effet Larsen: j’emettais avec le client sur id pensant l’envoyer uniquement au serveur, puis coté serveur j’emmetais vers les clients avec le même identifiant pour que chacun mette à jour son écran.
J’ai donc différencier les identifiants client et serveur avec un suffixe « Broadcast »
Ainsi coté client on fait un

1
socket.emit('Unit.buildBroadcast',this.id,x,y);

Coté serveur, on écoute l’evenement et l’on broadcast un évenement différent (sans le suffixe Broadcast)

1
2
3
4
socket.on('oBuild.buildBroadcast',function(id,x,y){
    socket.broadcast.emit('oBuild.build',id,x,y);
    socket.emit('oBuild.build',id,x,y);
});

Et ainsi coté client, on écoute l’evenement pour intéragir avec l’écran du joueur

1
2
3
4
5
6
7
socket.on('oBuild.build',function(id,x,y){
    var oBuild=oGame.getBuildById(id);
    oBuild.clear();
    oBuild.x=x;
    oBuild.y=y;
    oBuild.build();
});

Persistance de la partie
Sur la première version multijoueur, il fallait lancer les deux navigateurs à peu près en meme temps, pour qu’ils se broadcaste l’un l’autre leurs unités respectives.
Mais ceci n’était pas une bonne idée, j’ai donc ajouté plusieurs choses:
Рon stocke sur le serveur deux tableaux: tBuild et tUnit qui stoque les entit̩s (unit̩s et batiments)
– lors de la connection d’un joueur, on boucle sur ces tableaux pour lui afficher l’état actuel de la partie
– pour le debug: 4 boutons pour choisir sa team
Pour info, c’est la team bleu qui mène la danse: c’est ce joueur qui possède la boucle de jeu (cycle de raffraichissement des unités et batiments)

Note: pour sauvegarder les informations de partie, on met à jour les données des tableaux tUnit et tBuild

1
2
3
4
5
6
7
8
socket.on('unit.buildBroadcast',function(id,x,y){
    var i=getUnitById(id);
    tUnit[i].x=x;
    tUnit[i].y=y;
    socket.broadcast.emit('unit.build',id,x,y);
    socket.emit('unit.build',id,x,y);
   
});

La fonction pour récupérer les données de l’unité:

1
2
3
4
5
6
7
function getUnitById(id){
    for(var i=0;i<tUnit.length;i++){
        if(tUnit[i].id==id){
            return i;
        }
    }
}

Le prototype qui sert juste à stoquer les informations d'unité et batiments:

1
2
3
4
5
6
7
8
9
10
function Data(){
    this.team;
    this.name;
    this.id;
    this.x;
    this.y;
};
Data.prototype={
   
};

Copie de Unit.js et Build.js
Pour permettre de gérer plus facilement certains éléments du jeu multiplateforme, j’ai du dupliquer les classes Unit et Build

Modification de la méthode build de la classe Unit pour broadcaster la création d’un batiment: en effet, à la création d’une unité et d’un batiment, il faut avertir les autres joueurs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//si l'unité doit construire un batiment, et qu'elle se trouve sur les lieux de la construction
if(this.oBuildOn && this.getX()+1==this.oBuildOn.x && this.getY()==this.oBuildOn.y){
   
    oGame.createBuildBroadcast(oGame.team,this.oBuildOn.name,this.oBuildOn.x,this.oBuildOn.y);
   
    var aBuild=new Build(this.oBuildOn.name,this.team);
   
    //on reset les propriétés de construction
    oGame.buildcreation='';
    this.buildOnX='';
    this.buildOnY='';
    this.oBuildOn='';
   
    //on décrément la ressource or et bois
    oGame.useRessource(this.team,'or',aBuild.costOr);
    oGame.useRessource(this.team,'wood',aBuild.costWood);
   
    //on réactualise les ressources
    oGame.buildRessource();
    //on reset la sélection
    oGame.clearSelect();
   
}

note: un bug que j’ai mis longtemps à identifier: le mauvais nom d’event, en effet, si vous vous trompez de nom d’évenement à emettre, vous ne le savez pas : pas d’erreur :(
si vous avez un doute, ajoutez des console.log() pour vérifier que vous passer bien dans vos évenements ;)

Voilà pour le résumé des derniers déboires ;)
J’espère que ce journal de bord vous évitera de perdre autant de temps que moi.

Je pense améliorer un peu leu jeu, en ajoutant un menu, permettre de créer une nouvelle partie mais ce sera tout pour ce jeu.

Le prochain jeu sera un bomberman-like multijoueur ;)

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

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

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

Aujourd’hui nous allons décliner ce jeu en une version multiplayer

Pour cela, nous allons utiliser les websockets ainsi qu’un server node.js

Présentation de node.js et socket.io
Node.js est un serveur javascript utilisant le moteur de chrome, plus d’infos sur le site de nodejs: http://nodejs.org/
Socket.io est un serveur temps réel qui permet de transmettre des messages.
Les deux réunis couplés aux nouvelles capacités d’html5 permettent de mettre en place un jeu multi-joueur en temps réel.

Installons le serveur
Sur un serveur ubuntu, ouvrez un terminal et lancez la commande suivante:

1
sudo apt-get install nodejs

Pour les utilisateurs windows et mac, rendez-vous sur le site de nodejs: http://nodejs.org/

Installer ensuite socket.io
Pour cela il faut le binaire npm
Installez le avec la commande

1
sudo apt-get install npm

Puis installer le packet socket.io

1
npm install socket.io

Principe de base du jeu multi-player
Le principe de ce serveur tient dans un concept evenementiel: un client peut emettre un message et également être à l’écoute.
Le serveur est dans le même cas, il peut emettre soit vers l’émetteur (comme une réponse), soit vers tous les autres clients connectés.
On peut ainsi imaginer à chaque déplacement, avertir les autres joueurs, et vice versa.

Mise en place dans notre jeu
Pour ce RTS, il y a plusieurs solution différente et efficace.

Ici on va choisir d’avoir le premier joueur maitre, qui fera tourner le cycle du jeu et diffusera les ordres d’affichage des unités et batiments.
Pour cela, on va ajouter dans les classes Unit et Build, des copie de certains méthodes

1
2
3
buildBroadcast:function(){
    socket.emit('unit.build',this.id,this.x,this.y);
},

Et coté serveur: (nodejs)

1
2
3
4
socket.on('unit.build',function(id,x,y){
    socket.broadcast.emit('unit.build',id,x,y);
    socket.emit('unit.build',id,x,y);
});

On indique également dans le fichier rtsMultiplayer dans la fonction load, de ne créer la sequence que si le joueur est la team « bleu »

1
2
3
 if(oGame.team=='blue'){
        setTimeout(run,fps);
    }

Ainsi, le joueur bleu, va avoir une séquence normale: toutes les N milli secondes, il boucle pour raffraichir, mais au lieu de raffraichir localement, il broadcast à tout le monde les nouvelles positions: c’est un peu l’intelligence du serveur multi-joueur.

Vous avez compris l’idée: chaque methode d’affichage d’unité et batiment est remplacé par une méthode broadcastant ceci
Ansi chaque joueur recoit l’ordre d’afficher l’unité/batiment par le server socket.

J’ai mis à jour le github, mais pour l’instant il y a quelques bugs: on ne peut pas initié de ronde d’or/bois…
On peut déjà connecter plusieurs navigateurs (au moins 2 joueurs) et voir sur chacun d’eux les personnages bouger simultanément.

Je reviendrais ces prochains jours pour corriger ces bugs

Pour lancer le serveur node.js
Dans le repertoire où sont situé les fichiers du github

1
nodejs serverRtsHtml5.js

Ceci lancera le serveur nodejs du jeu

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

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

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

Dans ce jour 15, j’ai ajouté la gestion de la bière: les unités consomment de la bière pour attaquer. Il faut constuire un champ de houblon ainsi qu’une brasserie.
Enfin assigner un ouvrier au houblon pour qu’il fasse des aller retour entre le champ de houblon et la brasserie.
J’en ai également profité pour ajouter un bouton pour activer/désactiver le son.

Ajout des nouveaux batiments: champ de houblon et brasserie
Premièrement on ajoute dans le sprite le champ de houblon ainsi que la brasserie

Enquite on ajoute ce sprite au tableau d’identification
Dans la fonction preload du fichier rts.js

1
2
3
4
5
6
7
8
9
10
var tDetailTmp=new Array();
tDetailTmp=[
    ['build-SoldierHouse','build-SoldierHouse_-2','build-SoldierHouse_-1'],
    ['build-QG','build-QG_-2','build-QG_-1'],
    ['build-ArcherHouse','build-ArcherHouse_-2','build-ArcherHouse_-1'],
   
    ['build-mineOr'],
    ['case-houblon','case-houblon_-2','case-houblon_-1'],
    ['build-brasserie','build-brasserie_-2','build-brasserie_-1']
];

Ensuite on ajoute ces deux batiments à la liste constructible par une unité worker:
Dans le constructeur de la classe Unit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
}else if(this.name='Worker'){
    this.shortname='Ouvrier';
    this.src='img3/unit-worker.png';
    this.idImg='unit-worker';
    this.life=100;
    this.attak=5;
   
    this.costOr=100;
    this.costBeer=5;
   
    this.tBuildCreation.push(new Build('SoldierHouse',this.team));
    this.tBuildCreation.push(new Build('ArcherHouse',this.team));
    this.tBuildCreation.push(new Build('Houblon',this.team));
    this.tBuildCreation.push(new Build('Brasserie',this.team));
}

Prise en compte d’un clic droit sur un champ de houblon
Dans la méthode goto de 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
}else if(this.tSelected.length && aBuild && ( aBuild.name=='Houblon')){
    //on créé une ronde du bois/or vers le QG
    //pour alimenter les ressources Or/bois

    for(var i=0;i<this.tSelected.length;i++){

        //on indique que la destination de cycle c'est la mine d'or ou l'arbre
        var cycleToX=x;
        var cycleToY=y;

        //on indique que la provenance du cycle c'est la brasserie
        var cycleFromX=-1;
        var cycleFromY=-1;
       
        for(var j=0;j< this.tBuild.length;j++){
            if(this.tBuild[j].name=='Brasserie'){
                cycleFromX=this.tBuild[j].x;
                cycleFromY=this.tBuild[j].y;
            }
        }
       
        if(cycleFromX < 0){
            alert('Il vous manque une brasserie');
        }
       
        this.tSelected[i].setCycle(cycleToX,cycleToY,cycleFromX,cycleFromY,aBuild.name);

        //on donne comme cible de deplacement la mine d'or/l'arbre cliqué
        this.tSelected[i].setTarget(cycleToX,cycleToY);

Ajout d’un nouveau cycle, celui du houblon
Contrairement aux arbres et à l’or, ici, on ne veut pas que l’unité rapporte la ressource au QG mais à la brasserie.
Pour cela il faut ajouter une condition dans le cas du houblon:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//si la cible c'est du houblon et que le compteur est inferieur à N
}else if(aBuild && aBuild.name=='Houblon' && oUnit.counter = 8  && oUnit.cycleToX!=''){
    //on indique à l'unité qu'elle transporte 1
    oUnit.houblon=1;
   
    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);
                       
    oUnit.animate('walking');

Idem pour la brasserie, il faut la prendre en compte au retour du houblon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(aBuild && aBuild.name=='Brasserie' && oUnit.cycleToX!=''){
    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.houblon >0){
        //on ajoute une ressource or
        this.addRessource(oUnit.team,'beer',oUnit.houblon);
    }

    //on reset les ressources de l'unité
    oUnit.houblon=0;

Prendre en compte cette ressource pour l’attaque
L’idée est simple, il faut de la bière aux unités pour pouvoir attaquer, cela donne un nouvel élément de gestion.
Sans bière, peu importe sa force d’attaque l’unité restera statique: elle ne se défendra même pas.
Pour cela, dans la classe Game, méthode refreshUnit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(iAttack && this.getRessource(oUnit.team,'beer') > oUnit.costBeer ){    

    this.useRessource(oUnit.team,'beer',oUnit.costBeer);
    this.buildRessource();

    oUnit.animate('attack');

    //on decremente l'enemie de la puissance d'attaque
    oUnit2.life-=oUnit.attak;
    if(oUnit2.life <=0){
        oUnit.animate('walking');
        oUnit2.animate('dead');      
        //si unite dead, on l'efface du jeu
        oUnit2.clear();
        oGame.removeUnit(oUnit2);

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

rts20140117

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

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

Dans ce billet peu de choses technique:
Une mise à jour graphique ainsi qu’une amélioration de la fluidité de déplacement

Modification du déplacement de 1 à 0.5
Plutot que de déplacer d’une case à la fois, j’ai modifié la vitesse de déplacement de 1 à 0.5 (multiplié par la largeur d’une case)

Les problèmes à gerer
Cela change plusieurs choses: les coordonnées x et y étant jusqu’à présent utilisées comme les coordonnées du tableau, il a fallu modifier un peu pour que le jeux fonctionne toujours.

Ajout de deux méthodes getX() et getY() dans la classe Unit

1
2
3
4
5
6
getX:function(){
    return parseInt(this.x);
},
getY:function(){
    return parseInt(this.y);
},

Vérification des coordonnées utilisant ces nouvelles méthodes:

1
2
//si l'unité doit se rendre quelques part
}else if(oUnit.targetX!='' && oUnit.targetY!='' && (oUnit.targetX!=oUnit.getX() || oUnit.targetY!=oUnit.getY() ) ){

Modification de certaines méthodes pour prendre en compte la coordonnées entières et non les coordonnées décimales

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 ] && this.tCoordBuild[ y ][ x ] && this.tCoordBuild[ y ][ x ]!=''){
            console.log('not libre tCoordBuild[ '+y+' ][ '+x+' ]');
            return false;
        }
       
        if(this.tCoordUnit[ y ] && this.tCoordUnit[ y ][ x ] && this.tCoordUnit[ y ][ x ]!=''){
            console.log('not libre tCoordUnit[ '+y+' ][ '+x+' ]');
            return false;
        }
       
        if(map.tMap[y] && map.tMap[y][x] && map.tMap[y][x]==3){
       
            return true;
           
        }
        return false;

    },

Voilà, c’est tout pour aujourd’hui

rtsspritesrtsgameplay

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

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

Dans ce jour 13, j’ai modifié la gestion des ressources pour permettre de stoquer l’information par équipe. Ainsi il sera plus simple d’une part de développer une intelligence artificielle qui pourra interagir avec ses ressources comme nos unités et également de faciliter la gestion multi utilisateur.
On ajoute également une gestion de sprite multi-direction.

Le stoquage multi team

1
2
3
4
5
6
7
this.team='blue';
   
//ressources
this.tRessource=Array();
this.tRessource[this.team]=Array();
this.tRessource[this.team]['or']=250;
this.tRessource[this.team]['wood']=150;

L’intéraction avec celles-ci

1
2
3
4
5
6
7
8
9
10
11
addRessource:function(team,ressource,nb){
    this.tRessource[team][ressource]+=nb;
    this.buildRessource();
},
useRessource:function(team,ressource,nb){
    this.tRessource[team][ressource]-=nb;
    this.buildRessource();
},
getRessource:function(team,ressource){
    return this.tRessource[team][ressource];
},

Modifier l’appel par des unités/batiments
On modifie l’appel à la méthode lorsque l’on construit un batiment
En modifiant dans la méthode build de la classe Unit

1
2
3
//on décrément la ressource or et bois
oGame.useRessource(this.team,'or',aBuild.costOr);
oGame.useRessource(this.team,'wood',aBuild.costWood);

Idem pour la classe Build : batiment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
createUnit:function(){
       
    oGame.useRessource(this.unitCreation.team,'or',this.unitCreation.costOr);
    oGame.buildRessource();
   
    var oUnit;
    oUnit =new Unit(this.unitCreation.name,this.team);
   
    oUnit.x=this.x+2;
    oUnit.y=this.y;
    oUnit.build();
   
    oGame.tUnit.push(oUnit);
   
    oGame.displayVisibility();
}

D’ailleurs, on modifie également la construction de la navigation
Pour vérifier les ressources disponible avec cette nouvelle méthode.

1
if(oGame.getRessource(this.team,'or') > this.unitCreation.costOr){

Je profite de cette mise à jour pour assombrir totalement le brouillard de niveau 1.

Permettre de rapporter des ressources hors cycle
Imaginons que vous ayez une unité qui a coupé du bois ou récupéré de l’or, vous cliquez dessus pendant son retour pour lui faire faire autre chose, il serait sympa de pouvoir rapporter cette ressource au QG une fois l’action terminée.
Pour cela, il faut pouvoir cliqué droit sur le QG et permettre aux unités transportant une ressource de s’y rendre pour déposer la ressource.

Pour cela on modifie la méthode clic droit (goto de la classe Game) pour ajouter ce cas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
}else if(this.tSelected.length && aBuild && ( aBuild.name=='QG' )){
           
    for(var i=0;i0){
        //on ajoute une ressource or
        this.addRessource(oUnit.team,'or',oUnit.or);
    }else   if(oUnit.wood >0){
        //idem pour le bois
        this.addRessource(oUnit.team,'wood',oUnit.wood);
    }

    //on reset les ressources de l'unité
    oUnit.or=0;
    oUnit.wood=0;
   
    oUnit.buildNav();

Ajouter une gestion de sprite de direction
Il serait plus agréable d’avoir l’unité animé en fonction de sa direction, pour cela plusieurs choses:
1. créer des sprites de direction différente
2. choisir le bon sprite en fonction de la direction de déplacement
Dans la partie identification des sprites, on rajoute les différents sprites de direction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
    'unit-worker',
    'unit-worker_attack',
   
    'unit-worker_walkingRight',
    'unit-worker_walking2Right',
   
    'unit-worker_walkingLeft',
    'unit-worker_walking2Left',
   
    'unit-worker_walkingDown',
    'unit-worker_walking2Down',
   
    'unit-worker_walkingUp',
    'unit-worker_walking2Up'
   
],

Dans la classe Unit on analyse la direction et chargeons ainsi le bon sprite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
}else if(action=='walking'){
           
    var sDirection='';
    if(this.targetY  this.y){
        sDirection='Down';
    }else if(this.targetX > this.x){
        sDirection='Right';
    }else{
        sDirection='Left';
    }
   
    if(this.tmpIdImg==this.idImg+'_walking2'){
        tmpImg=this.idImg+'_walking'+sDirection;
    }else{
        tmpImg=this.idImg+'_walking2'+sDirection;
    }

rts_sprites

Le jeux actuellement
rts_spritesjeux

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

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

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

Dans ce jour 12, on va gérer des sprites d’animation et en profiter pour réorganiser la gestion du son de manière plus propre.

Ajout d’une méthode animate
On va ajouter une méthode animate qui permettra à la fois de modifier l’image affichée mais également de jouer le bon son

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
animate:function(action){
    //on commence par effacer la zone
    oLayer_perso.clearRect(((this.x-currentX)*widthCase),((this.y-currentY)*heightCase),widthCase-2,widthCase-2);

    var tmpImg;
    if(action=='attack'){
        tmpImg=this.idImg+'_attack';
        this.playSound('attack',this.action);
    }else if(action=='walking'){
        //si le précédent sprite était A
        //on prend le B (pour créer une animation)
        if(this.tmpIdImg==this.idImg+'_walking2'){
            tmpImg=this.idImg+'_walking';
        }else{
            tmpImg=this.idImg+'_walking2';
        }
        //on arrête tout son (on marche)
        this.stopSound();
        //on stoque le sprite affiché pour la prochaine animation
        this.tmpIdImg=tmpImg;
    }else if(action=='dead'){
        this.playSound('dead',this.action);
        return;
    }else if(action=='wood'){
        tmpImg=this.idImg;
        this.playSound('wood',this.action);
    }else if(action=='mining'){
        tmpImg=this.idImg;
        this.playSound('mining',this.action);
    }else if(action=='stand'){
        tmpImg=this.idImg;
    }
   
    oImages.drawImageOnLayer(tmpImg,((this.x-currentX)*widthCase),((this.y-currentY)*heightCase),widthCase-2,widthCase-2,'perso');
   
    this.action=action;
},

Pour recentrer le son sur l’unité, on a du recréé des méthodes de gestion de son que voici:
Note: on stoque la précédente animation afin lorsqu’une animation/action se déroule en plusieurs temps, on lise le son en entier plutot que de saccader celui-ci.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
playSound:function(action,lastAction){
    //si la précédente action était la même, on fait rien
    if(action==lastAction){
        return;
    }
    this.stopSound();
   
    this.oAudio=new Audio();
    this.oAudio.src=oSound.getSrc(action);
    this.oAudio.play();
   
},
stopSound:function(){
    if(!this.oAudio){
        return;
    }
    this.oAudio.pause();
},

Appelez au bon moment cette nouvelle méthode
Après avoir supprimer les précédentes méthodes appellées oSound.resetPlaying() et oSound.stopPlaying()
On doit désormais appelé les bonnes méthodes d’animation pour avoir un état cohérent de chaque unité et éviter de jouer un son inutile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(iAttack){    

    oUnit.animate('attack');

    //on decremente l'enemie de la puissance d'attaque
    oUnit2.life-=oUnit.attak;
    if(oUnit2.life <=0){
        oUnit.animate('walking');
        oUnit2.animate('dead');      
        //si unite dead, on l'efface du jeu
        oUnit2.clear();
        oGame.removeUnit(oUnit2);

    }

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