Je ne suis exposé à la toile que depuis plus d'un mois. C'est la première fois que je mets complètement en œuvre un processus de jeu, et la récolte est assez importante.
Captures d'écran du jeu de tir
Accédez d'abord à la démo : https://littleyljy.github.io/demo/shootgame/
règles du jeuLe joueur doit contrôler l'avion pour tirer des balles et détruire les monstres en mouvement. Si tous les monstres sont détruits, le jeu réussit. Si les monstres se déplacent vers le bas, le jeu échoue.
Le jeu est divisé en plusieurs scènes :
Pour réaliser le changement de scène, vous devez d'abord afficher : none pour toutes les scènes, puis contrôler l'état des données via js pour démarrer, jouer, échouer, réussir, tout réussir et arrêter pour implémenter l'affichage de scène correspondant : bloquer.
Le HTML et le CSS sont les suivants :
<div id=game data-status=start> <div class=game-panel> <section class=game-intro game-ui> <h1 class=section-title>Jeu de tir</h1> <p class=game- desc>Il s'agit d'un jeu de tir addictif. Utilisez ← et → pour piloter votre avion, utilisez l'espace pour tirer et utilisez Entrée pour mettre le jeu en pause. Détruisons les monstres spatiaux ensemble ! </p> <p class=game-level>Niveau actuel : 1</p> <button class=js-play button>Démarrer le jeu</button> </section> <section class=game-failed game-ui> <h1 class=section-title>Game over</h1> <p class=game-info-text>Score final : <span class=score></span></p> <button class=js-replay button> Recommencer</button> </section> <section class=game-success game-ui> <h1 class=section-title>Succès du jeu</h1> <p class=game-next-level game-info-text></p> <button class=js-next button>Continuer le jeu</button> </section> <section class=game-all-success game-ui> <h1 class=section-title>Réussi</h1> <p class=game- niveau suivant game-info-text>Vous avez réussi à vous défendre contre toutes les attaques de monstres. </p> <button class=js-replay button>Rejouer</button> </section> <section class=game-stop game-ui> <h1 class=section-title>Pause du jeu</h1> < bouton class=js-stop button>Le jeu continue</button> </section> </div> <div class=game-info game-ui> <span class=title>Score :</span> <span class=score > </span> </div> <canvas id=canvas width=700 height=600> <!-- Planche à dessin d'animation --> </canvas> </div>
#jeu{ largeur : 700 px ; hauteur : 600 px ; position : relative ; gauche : 50 % ; haut : 40 px ; arrière-plan : dégradé linéaire (-180deg, #040024 0%, #07165C 97 % );}.game-ui{ affichage : aucun ; remplissage : 55 px ; border-box; hauteur : 100 % ;}[data-status=start] .game-intro { display : block ; padding-top : 180px ; arrière-plan : url(./img/bg.png) no-repeat 430px 180px ; taille d'arrière-plan : 200 px ;}[data-status=playing] .game-info { display : position du bloc : top : 0 ; gauche :0 ; remplissage :20px;}[data-status=failed] .game-failed,[data-status=success] .game-success,[data-status=all-success] .game-all-success,[ data-status=stop] .game-stop{ display: block; padding-top: 180px background: url(./img/bg-end.png) pas de répétition 380px 190px; taille d'arrière-plan : 250px ;}orienté objet
L'ensemble du jeu peut traiter les monstres (Enemy), les avions (Plane) et les balles (Bullets) comme des objets, ainsi que des objets de configuration (CONFIG) et des objets de jeu (GAME) qui contrôlent la logique du jeu.
Configuration liée au jeu/** * Configuration liée au jeu* @type {Object} */var CONFIG = { status: 'start', // Le jeu démarre par défaut au niveau de départ : 1, // Le niveau par défaut du jeu totalLevel : 6, // Un total de 6 Off numPerLine : 7, // Le nombre de monstres par défaut du jeu par ligne canvasPadding : 30, // L'intervalle de toile par défaut bulletSize : 10, // La longueur de puce par défaut bulletSpeed : 10, // Vitesse de déplacement de la balle par défaut enemySpeed : 2, // Distance de mouvement de l'ennemi par défaut enemySize : 50, // Taille de l'ennemi par défaut enemyGap : 10, // Distance par défaut entre les ennemis enemyIcon: './img/enemy.png', / / L'image de le monstre ennemiBoomIcon : './img/boom.png', // L'image de la mort du monstre ennemiDirection : 'right', // L'ennemi par défaut se déplace vers la droite au début planeSpeed: 5, // La distance par défaut parcourue par l'avion à chaque pas planeSize: { width: 60, height: 100 }, // La taille par défaut de l'avion, planeIcon: '. /img/avion.png' };Définir la classe parent
Étant donné que les monstres (Enemy), les avions (Plane) et les balles (Bullet) ont tous les mêmes attributs x, y, taille, vitesse et méthode move(), vous pouvez définir une classe parent Element et l'implémenter en héritant de la classe parent de la sous-classe.
/*Classe parent : contient xy speed move() draw()*/var Element = function (opts) { this.opts = opts || {}; this.y = opts.y; this.size = opts.size; this.speed = opts.speed;};Element.prototype.move = fonction (x, y) { var addX = x || addY = y || 0; this.x += addX; this.y += addY;};//Fonction qui hérite de la fonction prototype EnsurePrototype(subType, superType) { var proto = Object.create(superType.prototype); proto .constructor = sous-Type; sous-Type.prototype = proto;}
La méthode move(x, y) s'empile en fonction de la valeur (x, y) transmise.
Définir le monstreLes monstres incluent des attributs uniques : le statut du monstre, l'image, boomCount qui contrôle la durée de l'état d'explosion et les méthodes draw(), down(), direction() et booming().
/*Enemy*/var Enemy = function (opts) { this.opts = opts || {}; //Appelle l'attribut de classe parent Element.call(this, opts); 'normal';//normal, en plein essor, noomed this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon = this.boomCount = 0;};//Hériter la méthode Element hériterPrototype(Enemy, Element);//Méthode : dessiner l'ennemi Enemy.prototype.draw = function () { if (this.enemyIcon && this.enemyBoomIcon) { switch (this.status ) { cas 'normal' : var ennemiIcon = new Image(); ennemiIcon.src = this.enemyIcon ctx.drawImage(enemyIcon, this.x, this.y, this.size, this.size); break ; cas 'en plein essor' : var ennemisBoomIcon = new Image(); this.y, this.size, this.size); break ; cas 'en plein essor' : ctx.clearRect(this.x, this.y, this.size, this.size); break; par défaut : break; } } return this;};//Méthode : down Descendre Enemy.prototype.down = function () { this.move(0, this.size); ;//Méthode : Déplacer vers la gauche ou la droite Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0 } else {); this.move(-this.speed, 0); } return this;};//Méthode : l'ennemi explose Enemy.prototype.booming = function () { this.status = 'booming'; (this.boomCount > 4) { this.status = 'boomed' } renvoie ceci ;}
Les puces ont les méthodes fly() et draw().
/*Bullet*/var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts);};inheritPrototype(Bullet, Element);//Méthode : laisser la balle voler Bullet . prototype.fly = function () { this.move(0, -this.speed); return this;};//Méthode : dessiner une puce Bullet.prototype.draw = function () { ctx.beginPath(); ctx.StrokeStyle = '#fff'; ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y - CONFIG.bulletSize()); ; ctx.Stroke(); renvoie ceci ;};
L'objet avion contient des attributs uniques : statut, largeur et hauteur, image, valeurs d'abscisse maximale et minimale et méthodes haveHit(), draw(), direction(), shoot() et drawBullets().
/*Plane*/var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //Statut d'attribut unique et image this.status = 'normal'; = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; this.minX = this.maxX = opts.maxX; //Bulles liées this.bullets = []; this.bulletSpeed = opts.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || (Avion, Élément) ;//Méthode : La balle atteint la cible Plane.prototype.hasHit = function (ennemi) { var bullets = this.bullets; for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; var isHitPosX = (enemy.x < bullet.x) && (bullet.x < (ennemi.x + ennemi.size)); var isHitPosY = (ennemi.y < bullet.y) && (bullet.y < (ennemi.y + ennemi.size)); (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } } return false;};//Méthode : dessiner le plan Plane.prototype.draw = function () { this.drawBullets(); var planeIcon = new Image(); planeIcon.src = this.planeIcon; ctx.drawImage(planeIcon, this.x, this.y, this.width, this.height);};//Méthode : Direction du plan Plane.prototype.direction = function (direction) { var speed = this.speed; var planeSpeed; ) { planeSpeed = this.x < this.minX ? 0 : -speed; } else { planeSpeed = this.x > this.maxX 0 : vitesse ; console.log('planeSpeed:', planeSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX); .maxX:', this.maxX); this.move(planeSpeed, 0); return this;//Appel en chaîne pratique};//Méthode : lancer des balles Plane.prototype.shoot = function () { var bulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x : bulletPosX, y : this.y, taille : this.bulletSize, vitesse : this.bulletSpeed })); this; };//Méthode : dessiner des puces Plane.prototype.drawBullets = function () { var bullets = this.bullets; var i = bullets.length; (i--) { var bullet = bullets[i]; bullet.fly(); if (bullet.y <= 0) { bullets.splice(i, 1 } bullet.draw( }});
Les événements de clavier ont les états suivants :
Parce que l'avion doit continuer à bouger lorsque le bouton gauche (keyCode=37) et le bouton droit (keyCode=39) sont enfoncés (keydown), et le keyup ne bouge pas lorsqu'il est relâché. Lorsque l'espace (keyCode=32) ou la touche fléchée vers le haut (keyCode=38) est enfoncé (keydown), les balles sont tirées et lorsqu'elles sont relâchées, la touche up arrête de tirer. Appuyez également sur la touche Entrée (keyCode=13) pour mettre le jeu en pause. Par conséquent, vous devez définir un objet KeyBoard pour surveiller si onkeydown et onkeyup appuient ou relâchent une touche.
Étant donné que les touches gauche et droite sont contradictoires, par mesure de sécurité, vous devez définir la touche droite sur false lorsque vous appuyez sur la touche gauche. Il en va de même pour le clic droit.
//Événement clavier var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this);} //Objet KeyBoard KeyBoard.prototype = {ippedLeft : faux, presséDroite : faux, presséEn haut : faux, tenuLeft : faux, tenuDroite : faux, presséEspace : faux, presséEnter : faux, touche enfoncée : fonction (e) { var key = e.keyCode; switch (clé) { case 32://Espace - balles de tir this.pressedSpace = true; case 37://Touche fléchée gauche this.pressedLeft = true; false; this.heldRight = false; case 38://Touche fléchée vers le haut - lancer les puces this.pressedUp = true; case 39://Touche fléchée vers la droite this.pressedLeft = false; = false; this.pressedRight = true; this.heldRight = true; break; touche Entrée - mettre le jeu en pause this.pressedEnter = true } }, keyup: function (e) { var key = e .keyCode; switch (clé) { cas 32 : this.pressedSpace = false ; cas 37 : this.heldLeft = false this.pressedLeft = false ; this.pressedUp = false; cas 39 : this.heldRight = false ; this.pressedRight = false ; cas 13 : this.pressedEnter = false ;Logique du jeu
L'objet de jeu (GAME) contient la logique de l'ensemble du jeu, y compris init (initialisation), bindEvent (bouton de liaison), setStatus (mise à jour de l'état du jeu), play (dans le jeu), stop (pause), end (fin), etc. ., dans Cela n'étend pas la description. Il comprend également des fonctions telles que la génération de monstres et le dessin d'éléments de jeu.
//L'objet de jeu entier var GAME = { //Une série de fonctions logiques // Fonctions d'élément de jeu}1. Initialisation
La fonction d'initialisation définit principalement les coordonnées initiales de l'avion, la plage de mouvement de l'avion, la plage de mouvement du monstre, initialise le score, le tableau de monstres, crée l'objet KeyBoard et ne l'exécute qu'une seule fois.
/** * Fonction d'initialisation, cette fonction n'est exécutée qu'une seule fois * @param {object} opts * @return {[type]} [description] */init: function (opts) { //Set opts var opts = Object.assign ( {}, opts, CONFIG);//Fusionner tous les paramètres this.opts = opts; this.status = 'start'; //Calculer les coordonnées initiales de l'objet avion this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; // Coordonnées de la limite du plan this.planeMinX = opts.canvasPadding; this.planeMaxX = canvasWidth - opts.canvasPadding - opts.planeSize. width; //Calculer la zone de mouvement de l'ennemi this.enemyMinX = opts.canvasPadding = this.enemyMaxX = canvasWidth - opts.canvasPadding - opts.enemySize; //Le score est défini sur 0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); ); },2. Lier les événements du bouton
Parce que plusieurs scènes de jeu incluent des boutons pour démarrer le jeu (playBtn), redémarrer (replayBtn), le niveau suivant du jeu (nextBtn) et mettre le jeu en pause pour continuer (stopBtn). Nous devons effectuer différents événements pour différents boutons.
La raison pour laquelle on définit var self = this est en premier lieu l'utilisation de this ; Dans la fonction bindEvent, cela pointe vers l'objet GAME, et dans playBtn.onclick = function () {} ; ce n'est évidemment pas ce que nous voulons, car playBtn n'a pas d'événement play(), seulement l'événement play(). L'objet GAME l'a. Par conséquent, l'objet GAME doit être attribué à une variable self, puis l'événement play() peut être appelé dans playBtn.onclick = function () {};.
Il convient de noter que le bouton replayBtn apparaît à la fois dans les scénarios d'échec et de liquidation, ce qui est donc obtenu est la collection de tous les .js-replay. Ensuite, forEach parcourt chaque bouton replayBtn, réinitialise le niveau et le score et appelle l'événement play().
bindEvent : function () { var self = this; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelector('. js-next'); var stopBtn = document.querySelector('.js-stop'); Démarrer la liaison du bouton de jeu playBtn.onclick = function () { self.play(); // Redémarrer la liaison du bouton de jeu replayBtn.forEach(function (e) { e.onclick = function () { self.opts. level = 1 ; self.play(); self.score = 0; totalScoreText.innerText = self.score }; Liaison du bouton de jeu de niveau suivant nextBtn.onclick = function () { self.opts.level += 1; self.play(); // Mettre le jeu en pause et continuer la liaison du bouton stopBtn.onclick = function () { setStatus. ('jouer'); self.updateElement();3. Générer des avions
createPlane : function () { var opts = this.opts ; this.plane = new Plane ({ x : this.planePosX, y : this.planePosY, largeur : opts.planeSize.width, hauteur : opts.planeSize.height, minX : this.planeMinX, vitesse : opts.planeSpeed, maxX : this.planeMaxX, planeIcon : opts.planeIcon });}4. Générez un groupe de monstres
Parce que les monstres apparaissent en groupes et que le nombre de monstres dans chaque niveau est également différent, la fonction des deux boucles for est de générer une rangée de monstres et d'augmenter la rangée de niveaux des monstres en fonction du nombre de niveaux. Ou augmentez la vitesse du monstre (vitesse : vitesse + i,) pour augmenter la difficulté de chaque niveau, etc.
// Générer des ennemis createEnemy : function (enemyType) { var opts = this.opts ; var level = opts.level ; var ennemis = this.enemies ; var numPerLine = opts.numPerLine ; var padding = opts.canvasPadding ; .enemyGap ; var taille = opts.enemySize ; var vitesse = opts.enemySpeed ; //Ajoutez une ligne pour chaque niveau de niveau ennemi for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //Paramètres des éléments complets var initOpt = { x : rembourrage + j * (taille + espace), y : rembourrage + i * (taille + espace), taille : taille, vitesse : vitesse, statut : typeennemi, icôneennemi : opts.enemyIcon, ennemiBoomIcon : opts.enemyBoomIcon }; ennemis.push(new Enemy(initOpt)); renvoie les ennemis },5. Mettre à jour les monstres
Obtenez la valeur x du tableau de monstres et déterminez s'il atteint la bordure de la toile. S'il atteint la bordure, le monstre se déplace vers le bas. Dans le même temps, le statut des monstres doit également être surveillé, si les monstres en statut normal ont été touchés, les monstres en statut explosif et les monstres qui ont disparu doivent être retirés du tableau et marqués en même temps.
//Mettre à jour le statut de l'ennemi updateEnemeis : function () { var opts = this.opts; var plane = this.plane; var ennemis = this.enemies; var i = var isFall = false;//La localisation des ennemis var ennemisX = getHorizontalBoundary(ennemis); if (ennemisX.minX < this.enemyMinX || ennemisX.maxX >= this.enemyMaxX) { console.log('ennemisX.minX', ennemisX.minX); console.log('ennemisX.maxX', ennemisX.maxX); opts.enemyDirection = opts.enemyDirection === 'gauche' : 'droite '; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true } //Boucle de mise à jour de l'ennemi while (i--) { var ennemi = ennemis[i]; if (isFall) { ennemi.down(); } ennemi.direction(opts.enemyDirection); switch (enemy.status) { case 'normal' : if (plane.hasHit(enemy)) { ennemi .booming(); } break; cas 'en plein essor': ennemi.booming(); cas 'en plein essor': ennemis.splice(i, 1); par défaut : pause } } },
La fonction de la fonction getHorizontalBoundary est de parcourir la valeur x de chaque élément du tableau, de filtrer les valeurs plus grandes ou plus petites et d'obtenir la valeur x maximale et minimale du tableau.
//Obtenir la fonction de limite horizontale du tableau getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x } } }); maximum }}6. Mettre à jour le panneau du clavier
Appuyez sur la touche Entrée pour exécuter la fonction stop(), appuyez sur le bouton gauche pour déplacer l'avion vers la gauche, appuyez sur le bouton droit pour déplacer l'avion vers la droite et appuyez sur le bouton espace pour exécuter l'avion tirant des balles afin d'empêcher les balles. de la connexion en ligne droite, définissez le keyBoard ici et keyBoard.pressedSpace sont faux.
updatePanel : function () { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); if (keyBoard.pressedLeft || keyBoard.heldLeft) { plan. direction('gauche'); } if (keyBoard.pressedRight || keyBoard.heldRight) { plan.direction('droite' }); (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = false; keyBoard.pressedSpace = false;7. Dessinez tous les éléments
draw: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemy) { //console.log('draw:this.enemy',enemy); ennemi. dessiner(); }); },8. Mettez à jour tous les éléments
Tout d'abord, déterminez si la longueur du tableau de monstres est de 0. Si elle est de 0 et que le niveau est égal à totalLevel, cela signifie que le niveau est réussi. Sinon, l'écran de préparation du jeu pour le niveau suivant s'affichera si la coordonnée y est affichée. du tableau de monstres est supérieur à la coordonnée y de l'avion plus la hauteur du monstre, cela indiquera que le jeu a échoué.
Le principe de l’animation du canevas est de dessiner, mettre à jour et effacer en permanence le canevas.
Le principe de la pause de jeu est d'empêcher l'exécution de la fonction requestAnimationFrame(), mais pas de réinitialiser l'élément. Par conséquent, lorsque l'état est jugé arrêté, la fonction sera sautée.
//Mettre à jour l'état de tous les éléments updateElement : function () { var self = this; var opts = this.opts; var ennemis = this.enemies ; if (enemies.length === 0) { if (opts.level = == opts.totalLevel) { this.end('all-success'); } else { this.end('success' } return; - 1].y >= this.planePosY - opts.enemySize) { this.end('failed'); return; } //Effacer le canevas ctx.clearRect(0, 0, canvasWidth, canvasHeight); canvas this .draw(); //Mettre à jour l'état de l'élément this.updatePanel(); this.updateEnemeis(); //Boucle continue updateElement requestAnimationFrame(function ()); if(self.status === 'stop'){ return; }else{ self.updateElement( } });écris à la fin
Grâce aux étapes ci-dessus, les fonctions de base du jeu sont complétées. Les autres commandes du processus de jeu, notamment le début, la fin, le calcul du score, etc., ne seront pas décrites ici.
Ce qui peut être optimisé : lorsque vous maintenez la barre d'espace enfoncée, les balles peuvent être tirées en continu. Cependant, lorsque j’ai appuyé à nouveau sur la touche de direction, j’ai constaté que je ne pouvais plus tirer de balles. Il est préférable de pouvoir continuer à tirer des balles lorsque vous pouvez vous déplacer.
Il est très intéressant de jouer à des jeux avec Canvas. De plus, ce jeu peut être étendu et transformé en version mobile. La taille du Canvas est déterminée en obtenant la largeur et la hauteur de l'écran. La partie clavier est modifiée en événements tactiles (touchstart, touchmove). , touchend). L'apparence des monstres peut également être modifiée. Changez-la pour qu'elle tombe aléatoirement du haut de l'écran, et le monstre augmentera sa santé (par exemple, il disparaîtra après avoir tiré 4 fois), etc.
Adresse de téléchargement : https://github.com/littleyljy/shoot
Ce qui précède représente l’intégralité du contenu de cet article. J’espère qu’il sera utile à l’étude de chacun. J’espère également que tout le monde soutiendra le réseau VeVb Wulin.