Un jeu HTML5 : le jeu !

Dans ce chapitre nous allons enfin faire fonctionner le jeu !

Les références

On va avoir les nouveaux fichiers play.js et end.js, on va donc le charger dans index.html :

<!-- Load Crazy Bird -->
<script src="js/game.js"></script>
<script src="js/boot.js"></script>
<script src="js/particles.js"></script>
<script src="js/load.js"></script>
<script src="js/settings.js"></script>
<script src="js/utils.js"></script>
<script src="js/menu.js"></script>
<script src="js/collisions.js"></script>
<script src="js/play.js"></script>
<script src="js/end.js"></script>

D’autre part dans le fichier load.js on avait commenté les événements pour éviter de générer des erreurs, on va avoir besoin maintenant de tous les dé-commenter :

...
C.sprites.sky.mousedown = C.play.onFlight;
C.sprites.sky.tap = C.play.onFlight;
...
C.sprites.land.mousedown = C.play.onFlight;
C.sprites.land.tap = C.play.onFlight;
...
C.sprites.playAgain.mousedown = C.end.onPlayAgain;
C.sprites.playAgain.touchstart = C.end.onPlayAgain; 
...

Le fichier play.js

Toute la logique du jeu se trouve dans ce fichier :

/* global C, SAT */

C.play = {
    on: function () {
        // Manage sky and land
        C.sprites.sky.tilePosition.x -= 1;
        C.sprites.land.tilePosition.x -= 2;

        // Manage planes collisions
        C.sprites.planes.forEach(function (e) {
            if (e.state) {
                if (C.collisions.birdPlane(e)) {
                    // Check lifes
                    if (C.lifes) {
                        C.sounds.crashPlane.play();
                        C.particles.planeParticles.updateOwnerPos(e.x + 180, e.y + 110);
                        C.particles.planeParticles.emit = true;
                        e.state = false;
                        e.position.set(C.renderer.width, C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222));
                        --C.lifes;
                        C.score += 10;
                        C.utils.refreshText();
                    } else {
                        // Bird crash and end game
                        C.sounds.crashBird.play();
                        C.particles.birdParticles.updateOwnerPos(C.sprites.bird.x + 68, C.sprites.bird.y + 60);
                        C.particles.birdParticles.emit = true;
                        C.sprites.bird.visible = false;
                        if (C.score > localStorage.getItem('highScore')) {
                            localStorage.setItem('highScore', C.score);
                        }
                        C.state = C.end.on;
                    }
                }
            }
        });

        // Manage balloons collisions
        C.sprites.balloons.forEach(function (e) {
            if (e.state === 1) {
                if (C.collisions.birdBalloonCircle(e)) {
                    // Check life balloon
                    if (e.life) {
                        C.sounds.life.play();
                        ++C.lifes;
                    } else {
                        C.sounds.balloon.play();
                        ++C.balloonsDone;
                        if (C.balloonsDone === C.maxBalloons) {
                            C.balloonsDone = 0;
                            ++C.level;
                        }
                    }
                    ++C.score;
                    e.state = 2;
                    e.play();
                    C.utils.refreshText();
                } else if (C.collisions.birdBalloonPolygon(e)) {
                    e.rotation = -0.5;
                }
            }
        });

        // Move bird
        var newY = C.sprites.bird.y - C.sprites.bird.vy;
        if (newY < C.pannelHeight + 1) {
            // Top collision
            C.sprites.bird.vy = .1;
            C.sprites.bird.y += C.sprites.bird.vy;
        } else if (newY > C.renderer.height - 120) {
            // Bottom collision
            C.sprites.bird.vy = 0;
        } else {
            // Casual flight
            C.sprites.bird.y = newY;
            C.sprites.bird.vy -= 0.07;
        }

        // Move planes
        C.sprites.planes.forEach(function (e) {
            if (e.state) {
                e.x -= C.speed + C.level * 2;
                if (e.x < -356) {
                    e.state = false;
                    e.x = C.renderer.width;
                    e.y = C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222);
                }
            }
        });

        // Move balloons 
        C.sprites.balloons.forEach(function (e) {
            if (e.state) {
                e.x -= (C.speed + C.level * 2) / 2;
                if (e.rotation < 0) {
                    e.rotation += .004;
                }
                if (e.state === 2) {
                    e.y += 5;
                }
                if (e.x < -50 || e.y > C.stageHeight + 80) {
                    e.state = false;
                    e.rotation = 0;
                    e.gotoAndStop(0);
                    e.position.set(C.renderer.width + 50, C.utils.getRandomInt(C.pannelHeight + 80, C.renderer.height - 75));
                }
            }
        });

        
        // Check less than 2 planes in right section
        var n = 0;
        for (var i = 0; i < C.nbrPlanes; ++i) {
            if (C.sprites.planes[i].state && C.sprites.planes[i].x > C.renderer.width - 400) {
                ++n;
            }
        }
        // Manage planes
        if (n < 2) {
            C.sprites.planes.forEach(function (e) {
                if (!e.state) {
                    if (C.utils.getRandomInt(0, 200 - C.level * 10) === 50) {
                        // Check for free place
                        while (!C.collisions.freeOfPlane(e)) {
                            e.y += 1;
                            // Check stage limit
                            if (e.y > C.renderer.height - 222) {
                                e.y = C.pannelHeight;
                            }
                        }
                        e.state = true;
                    }
                }
            });
        }

        // Manage balloons
        C.sprites.balloons.forEach(function (e) {
            if (!e.state) {
                if (C.utils.getRandomInt(0, 200 - C.level * 10) === 50) {
                    if (C.utils.getRandomInt(0, 10) === 5) {
                        e.tint = 0xff0000;
                        e.life = true;
                    } else {
                        e.tint = 0xffff00;
                        e.life = false;
                    }
                    e.state = 1;
                }
            }
        });

    },
    onFlight: function () {
        C.sprites.bird.vy += 2.4;
        C.sprites.bird.gotoAndPlay(0);
    }
};

On va un peu analyser tout ça…

Déplacement du fond

La partie la plus simple est le déplacement du fond (pour donner l’impression que l’oiseau se déplace) :

C.sprites.sky.tilePosition.x -= 1;
C.sprites.land.tilePosition.x -= 2;

Le ciel se déplace deux fois moins vite que les collines pour créer l’effet de perspective

Déplacement de l’oiseau

L’oiseau peut juste monter et descendre. Il monte par action du joueur et il descend par action de la gravité :

// Move bird
var newY = C.sprites.bird.y - C.sprites.bird.vy;
if (newY < C.pannelHeight + 1) {
    // Top collision
    C.sprites.bird.vy = .1;
    C.sprites.bird.y += C.sprites.bird.vy;
} else if (newY > C.renderer.height - 120) {
    // Bottom collision
    C.sprites.bird.vy = 0;
} else {
    // Casual flight
    C.sprites.bird.y = newY;
    C.sprites.bird.vy -= 0.07;
}

On gère les collisions avec les bord supérieur et inférieur.

Pour le vol de l’oiseau on gère un clic (ou toucher) sur le fond :

onFlight: function () {
    C.sprites.bird.vy += 2.4;
    C.sprites.bird.gotoAndPlay(0);
}

Déplacement des avions

Les avions se déplacent de plus en plus vite avec les niveaux :

C.sprites.planes.forEach(function (e) {
    if (e.state) {
        e.x -= C.speed + C.level * 2;
        if (e.x < -356) {
            e.state = false;
            e.x = C.renderer.width;
            e.y = C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222);
        }
    }
});

D’autre part on détecte la sortie du canvas, on les repositionne alors de façon aléatoire à droite du canvas en prévision de leur prochain départ.

Les avions on deux états (state) :

  • 0 : en attente de départ,

  • 1 : en vol.

Déplacement des ballons

Le principe est le même que pour les avions :

C.sprites.balloons.forEach(function (e) {
    if (e.state) {
        e.x -= (C.speed + C.level * 2) / 2;
        if (e.rotation < 0) {
            e.rotation += .004;
        }
        if (e.state === 2) {
            e.y += 5;
        }
        if (e.x < -50 || e.y > C.stageHeight + 80) {
            e.state = false;
            e.rotation = 0;
            e.gotoAndStop(0);
            e.position.set(C.renderer.width + 50, C.utils.getRandomInt(C.pannelHeight + 80, C.renderer.height - 75));
        }
    }
});

Mais les ballons peuvent avoir 3 états (state) :

  • 0 : en attente de départ,

  • 1 : en vol,

  • 2 : en chute.

Gestion des avions

Il faut gérer le départ des avions :

// Check less than 2 planes in right section
var n = 0;
for (var i = 0; i < C.nbrPlanes; ++i) {
    if (C.sprites.planes[i].state && C.sprites.planes[i].x > C.renderer.width - 400) {
        ++n;
    }
}
// Manage planes
if (n < 2) {
    C.sprites.planes.forEach(function (e) {
        if (!e.state) {
            if (C.utils.getRandomInt(0, 200 - C.level * 10) === 50) {
                // Check for free place
                while (!C.collisions.freeOfPlane(e)) {
                    e.y += 1;
                    // Check stage limit
                    if (e.y > C.renderer.height - 222) {
                        e.y = C.pannelHeight;
                    }
                }
                e.state = true;
            }
        }
    });
}

Avant de faire partir un avion on vérifie qu’il n’y en a pas déjà deux dans la partie droite du canvas pour éviter de coincer l’oiseau. Si ce n’est pas la cas alors on lâche les avions de façon aléatoire en augmentant la fréquence avec les niveaux. D’autre part on évite d’empiler les avions avec une détection de collision avec un avion déjà en vol.

Gestion des ballons

La gestion des ballons est plus simple parce qu’on ne s’inquiète pas de les empiler :

// Manage balloons
C.sprites.balloons.forEach(function (e) {
    if (!e.state) {
        if (C.utils.getRandomInt(0, 200 - C.level * 10) === 50) {
            if (C.utils.getRandomInt(0, 10) === 5) {
                e.tint = 0xff0000;
                e.life = true;
            } else {
                e.tint = 0xffff00;
                e.life = false;
            }
            e.state = 1;
        }
    }
});

Par contre de façon aléatoire on les teinte en rouge.

Collision avec les avions

Lorsque l’oiseau entre en collision avec un avion on a deux cas :

  • il a des vies : l’avion explose (on gagne 10 points),

  • il n’a pas de vie : il explose et le jeu est fini.

// Manage planes collisions
C.sprites.planes.forEach(function (e) {
    if (e.state) {
        if (C.collisions.birdPlane(e)) {
            // Check lifes
            if (C.lifes) {
                C.sounds.crashPlane.play();
                C.particles.planeParticles.updateOwnerPos(e.x + 180, e.y + 110);
                C.particles.planeParticles.emit = true;
                e.state = false;
                e.position.set(C.renderer.width, C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222));
                --C.lifes;
                C.score += 10;
                C.utils.refreshText();
            } else {
                // Bird crash and end game
                C.sounds.crashBird.play();
                C.particles.birdParticles.updateOwnerPos(C.sprites.bird.x + 68, C.sprites.bird.y + 60);
                C.particles.birdParticles.emit = true;
                C.sprites.bird.visible = false;
                if (C.score > localStorage.getItem('highScore')) {
                    localStorage.setItem('highScore', C.score);
                }
                C.state = C.end.on;
            }
        }
    }
});

Si le jeu se termine on vérifie si le score obtenue est meilleur que celui mémorisé et, si c’est le cas, on remplace cette valeur par la nouvelle.

Collision avec les ballons

On a aussi deux cas :

  • ballon jaune : on ajoute un point, le ballon explose et commence sa chute,

  • ballon rouge : on gagne une vie, le ballon explose et commence sa chute.

// Manage balloons collisions
C.sprites.balloons.forEach(function (e) {
    if (e.state === 1) {
        if (C.collisions.birdBalloonCircle(e)) {
            // Check life balloon
            if (e.life) {
                C.sounds.life.play();
                ++C.lifes;
            } else {
                C.sounds.balloon.play();
                ++C.balloonsDone;
                if (C.balloonsDone === C.maxBalloons) {
                    C.balloonsDone = 0;
                    ++C.level;
                }
            }
            ++C.score;
            e.state = 2;
            e.play();
            C.utils.refreshText();
        } else if (C.collisions.birdBalloonPolygon(e)) {
            e.rotation = -0.5;
        }
    }
});

D’autre part on vérifie si on a crevé le nombre de ballons nécessaire (maxBalloons)  pour changer de niveau.

Le fichier end.js

On a dans ce fichier la gestion de la fin du jeu avec la possibilité de rejouer :

/* global C, SAT */

C.end = {
    // Properties
    rotationStep: .01,
    on: function () {
        if (C.sprites.gameOver.y > C.renderer.height / 3) {
            C.sprites.gameOver.y -= 3;
            C.sprites.gameOver.rotation += C.end.rotationStep;
            if (C.sprites.gameOver.rotation > .15) {
                C.end.rotationStep = -.01;
            } else if (C.sprites.gameOver.rotation < -.15) {
                C.end.rotationStep = .01;
            }
        } else {
            C.sprites.gameOver.rotation = 0;
            if (C.sprites.playAgain.y > C.renderer.height * 2 / 3) {
                C.sprites.playAgain.y -= 4;
            }
        }
    },
    onPlayAgain: function() {
        // Reset variables
        C.state = C.play.on;
        C.sprites.gameOver.y = C.renderer.height + 70;
        C.sprites.playAgain.y = C.renderer.height + 70;
        C.level = 1;

        // Set planes
        C.sprites.planes.forEach(function (e) {
            e.state = false;
            e.position.set(C.renderer.width, C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222));
        });

        // Set balloons
        C.sprites.balloons.forEach(function (e) {
            e.state = false;
            e.position.set(C.renderer.width + 50, C.utils.getRandomInt(C.pannelHeight + 75, C.renderer.height - 75));
        });
        C.balloonsDone = 0;

        // Set bird
        C.sprites.bird.visible = true;
        C.sprites.bird.position.set(C.renderer.width / 4, C.renderer.height / 2);

        // Refresh text
        C.utils.refreshText();        
    }
};

La fin du jeu intervient lorsque l’oiseau explose suite à une collision avec un avion alors qu’il n’a plus de vie en réserve. On a alors un message de fin de jeu et une proposition de rejouer :

img01

Le « GAME OVER » arrive du bas de l’écran et oscille, il est suivi par le « PLAY AGAIN » qui monte normalement :

rotationStep: .01,
on: function () {
    if (C.sprites.gameOver.y > C.renderer.height / 3) {
        C.sprites.gameOver.y -= 3;
        C.sprites.gameOver.rotation += C.end.rotationStep;
        if (C.sprites.gameOver.rotation > .15) {
            C.end.rotationStep = -.01;
        } else if (C.sprites.gameOver.rotation < -.15) {
            C.end.rotationStep = .01;
        }
    } else {
        C.sprites.gameOver.rotation = 0;
        if (C.sprites.playAgain.y > C.renderer.height * 2 / 3) {
            C.sprites.playAgain.y -= 4;
        }
    }
},

Si on clique (ou touche) sur « PLAY AGAIN » on fait un reset complet pour relancer le jeu :

onPlayAgain: function() {
    // Reset variables
    C.state = C.play.on;
    C.sprites.gameOver.y = C.renderer.height + 70;
    C.sprites.playAgain.y = C.renderer.height + 70;
    C.level = 1;

    // Set planes
    C.sprites.planes.forEach(function (e) {
        e.state = false;
        e.position.set(C.renderer.width, C.utils.getRandomInt(C.pannelHeight, C.renderer.height - 222));
    });

    // Set balloons
    C.sprites.balloons.forEach(function (e) {
        e.state = false;
        e.position.set(C.renderer.width + 50, C.utils.getRandomInt(C.pannelHeight + 75, C.renderer.height - 75));
    });
    C.balloonsDone = 0;

    // Set bird
    C.sprites.bird.visible = true;
    C.sprites.bird.position.set(C.renderer.width / 4, C.renderer.height / 2);

    // Refresh text
    C.utils.refreshText();        
}

Et c’est reparti pour un tour !

Minification

Mantenant que le jeu est terminé il est judicieux de rassembler et minifier tous les fichiers JavaScript. Il existe des services en ligne pour le faire, par exemple ici. On copie et on colle le code et on clique sur « Compress Javascript » pour compresser le tout :

img02

On obtient alors un seul fichier JavaScript comprenant l’ensemble de code. On a donc beaucoup moins de fichiers à charger :

img03

Et le fichier index.html est très allégé :

<!DOCTYPE HTML>
<html>
    <title>Crazy Bird</title>
    <meta charset="UTF-8">
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: black;                
        }
    </style>
    <body>
        <script src="js/crazybird.js"></script>
    </body>
</html>

C’est la version du jeu qui se trouve dans le dossier www du dépôt.

En résumé

Dans ce chapitre on a :

  • mis en place la logique du jeu,

  • mis en place la gestion de la fin du jeu,

  • créé une version alléger du jeu pour le mettre en ligne.

Le jeu est à présent terminé et j’espère que ce cours vous donnera envie d’en créer un !

Laisser un commentaire