Développement d'un jeu sur Svelte 3

Svelte 3 est sorti il ​​y a un peu plus d'un mois. Un bon moment pour vous rencontrer, pensai-je, et parcouru un excellent tutoriel , qui a également été traduit en russe.


Pour consolider le passé, j'ai réalisé un petit projet et partagé les résultats avec vous. Ce n'est pas une liste de tâches de plus, mais un jeu dans lequel vous devez tirer à partir de carrés noirs.


image


0. Pour impatient


Dépôt de didacticiels
Référentiel avec extras
Démo


1. Préparation


Nous clonons un modèle de développement


git clone https://github.com/sveltejs/template.git 

Installez les dépendances.


 cd template/ npm i 

Nous démarrons le serveur de développement.


 npm run dev 

Notre modèle est disponible sur
http: // localhost: 5000 . Le serveur prend en charge le rechargement Ă  chaud, nos modifications seront donc visibles dans le navigateur lors de leur enregistrement.


Si vous ne souhaitez pas déployer l'environnement localement, vous pouvez utiliser les sandbox codesandbox et stackblitz en ligne qui prennent en charge Svelte.


2. Le cadre du jeu


Le dossier src se compose de deux fichiers main.js et App.svelte .
main.js est le point d'entrée de notre application. Pendant le développement, nous ne le toucherons pas. Ici, le composant App.svelte est monté dans le corps du document.
App.svelte est un composant de svelte. Le modèle de composant se compose de trois parties:


 <script> // JS   export let name; </script> <style> /* CSS   */ h1 { color: purple; } </style> <!--   --> <h1>Hello {name}!</h1> 

Les styles de composants sont isolés, mais il est possible d'assigner des styles globaux avec la directive: global (). Plus sur les styles .
Ajoutez des styles communs pour notre composant.


src / App.svelte
 <script> export let name; </script> <style> :global(html) { height: 100%; /*     100% */ } :global(body) { height: 100%; /*     100% */ overscroll-behavior: none; /*  pull to refresh*/ user-select: none; /*        */ margin: 0; /*  */ background-color: #efefef; /*    */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; /*   */ } </style> <h1>Hello {name}!</h1> 

Créons le dossier src / components dans lequel nos composants seront stockés
Dans ce dossier, créez deux fichiers qui contiendront le terrain de jeu et les contrôles.


src / components / GameField.svelte
 <div>GameField</div> 

src / components / Controls.svelte
 <div>Controls</div> 

Le composant est importé par directive


 import Controls from "./components/Controls.svelte"; 

Pour afficher un composant, insérez simplement la balise du composant dans le balisage. En savoir plus sur les tags .


 <Controls /> 

Maintenant, nous importons et affichons nos composants dans App.svelte.


src / App.svelte
 <script> //   import Controls from "./components/Controls.svelte"; import GameField from "./components/GameField.svelte"; </script> <style> :global(html) { height: 100%; } :global(body) { height: 100%; overscroll-behavior: none; user-select: none; margin: 0; background-color: #efefef; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } </style> <!--  . ,     , , ,  react --> <Controls /> <GameField /> 

3. ContrĂ´les


Le composant Controls.svelte sera composé de trois boutons: déplacer à gauche, déplacer à droite, tirer. Les icônes des boutons seront affichées par l'élément svg.
Créez le dossier src / asssets , auquel nous ajoutons nos icônes svg.


src / assets / Bullet.svelte
 <svg height="40px" viewBox="0 0 427 427.08344" width="40px"> <path d="m341.652344 38.511719-37.839844 37.839843 46.960938 46.960938 37.839843-37.839844c8.503907-8.527344 15-18.839844 19.019531-30.191406l19.492188-55.28125-55.28125 19.492188c-11.351562 4.019531-21.664062 10.515624-30.191406 19.019531zm0 0" /> <path d="m258.65625 99.078125 69.390625 69.390625 14.425781-33.65625-50.160156-50.160156zm0 0" /> <path d="m.0429688 352.972656 28.2812502-28.285156 74.113281 74.113281-28.28125 28.28125zm0 0" /> <path d="m38.226562 314.789062 208.167969-208.171874 74.113281 74.113281-208.171874 208.171875zm0 0" /> </svg> 

src / assets / LeftArrow.svelte
 <svg width="40px" height="40px" viewBox="0 0 292.359 292.359" transform="translate(-5 0)"> <path d="M222.979,5.424C219.364,1.807,215.08,0,210.132,0c-4.949,0-9.233,1.807-12.848,5.424L69.378,133.331 c-3.615,3.617-5.424,7.898-5.424,12.847c0,4.949,1.809,9.233,5.424,12.847l127.906,127.907c3.614,3.617,7.898,5.428,12.848,5.428 c4.948,0,9.232-1.811,12.847-5.428c3.617-3.614,5.427-7.898,5.427-12.847V18.271C228.405,13.322,226.596,9.042,222.979,5.424z" /> </svg> 

src / assets / RightArrow.svelte
 <svg width="40px" height="40px" viewBox="0 0 292.359 292.359" transform="translate(5 0) rotate(180)"> <path d="M222.979,5.424C219.364,1.807,215.08,0,210.132,0c-4.949,0-9.233,1.807-12.848,5.424L69.378,133.331 c-3.615,3.617-5.424,7.898-5.424,12.847c0,4.949,1.809,9.233,5.424,12.847l127.906,127.907c3.614,3.617,7.898,5.428,12.848,5.428 c4.948,0,9.232-1.811,12.847-5.428c3.617-3.614,5.427-7.898,5.427-12.847V18.271C228.405,13.322,226.596,9.042,222.979,5.424z" /> </svg> 

Ajoutez le composant de bouton src / components / IconButton.svelte .
Nous accepterons les gestionnaires d'événements du composant parent. Pour pouvoir maintenir le bouton enfoncé, nous avons besoin de deux gestionnaires: le début du clic et la fin du clic. Nous déclarons les variables start et release , où nous accepterons les gestionnaires d'événements pour le début et la fin du clic. Nous avons également besoin de la variable active , qui s'affichera si le bouton est enfoncé ou non.


 <script> export let start; export let release; export let active; </script> 

Nous stylisons notre composant


 <style> .iconButton { /*   flex     */ display: flex; align-items: center; justify-content: center; /*    60px */ width: 60px; height: 60px; /*   */ border: 1px solid black; /*    */ border-radius: 50px; /*     */ outline: none; background: transparent; } .active { /*    ,    */ background-color: #bdbdbd; } </style> 

Un bouton est un élément de bouton dans lequel le contenu transféré d'un composant parent est affiché. L'endroit où le contenu transféré sera monté est indiqué par la balise <slot />. En savoir plus sur l'élément <slot /> .


 <button> <slot /> </button> 

Les gestionnaires d'événements sont indiqués à l'aide de la directive on: par exemple, on: click .
Nous gérerons les événements de la souris et les clics tactiles. En savoir plus sur la liaison d'événements .
La classe active sera ajoutée à la classe de base du composant si le bouton est enfoncé. Vous pouvez affecter une classe par la propriété class. En savoir plus sur les cours


 <button on:mousedown={start} on:touchstart={start} on:mouseup={release} on:touchend={release} class={`iconButton ${active ? 'active' : ''}`}> <slot /> </button> 

En conséquence, notre composant ressemblera à ceci:


src / components / IconButton.svelte
 <script> export let start; export let release; export let active; </script> <style> .iconButton { display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; border: 1px solid black; border-radius: 50px; outline: none; background: transparent; } .active { background-color: #bdbdbd; } </style> <button on:mousedown={start} on:touchstart={start} on:mouseup={release} on:touchend={release} class={`iconButton ${active ? 'active' : ''}`}> <slot /> </button> 

Maintenant, nous importons nos icônes et l'élément bouton dans src / components / Controls.svelte et créons la disposition.


src / components / Controls.svelte
 <script> //      import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; </script> <style> /*    ,   */ .controls { position: fixed; bottom: 0; left: 0; width: 100%; } /*          */ .container { display: flex; justify-content: space-between; margin: 1rem; } /*     */ .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <IconButton> <LeftArrow /> </IconButton> <IconButton> <RightArrow /> </IconButton> </div> <IconButton> <Bullet /> </IconButton> </div> </div> 

Notre application devrait ressembler Ă  ceci:
image


4. Le terrain de jeu


Le terrain de jeu est un composant svg, où nous ajouterons nos éléments de jeu (pistolet, obus, adversaires).
Mettre Ă  jour le code src / components / GameField.svelte


src / components / GameField.svelte
 <style> /*  ,         */ .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <!--    viewBox          --> <svg viewBox="0 0 480 800"> </svg> </div> 

Créez l' arme src / components / Cannon.svelte . Fort pour un rectangle, mais néanmoins.


src / components / Cannon.svelte
 <style> /*   ,        */ .cannon { transform-origin: 4px 55px; } </style> <!--      svg .   <g>     --> <g class="cannon" transform={`translate(236, 700)`}> <rect width="8" height="60" fill="#212121" /> </g> 

Maintenant, nous importons notre arme sur le terrain de jeu.


src / GameField.svelte
 <script> //    import Cannon from "./Cannon.svelte"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--    --> <Cannon /> </svg> </div> 

5. Cycle de jeu


Nous avons le cadre de base du jeu. La prochaine étape consiste à créer une boucle de jeu qui traitera notre logique.
Créons des stockages où les variables de notre logique seront contenues. Nous aurons besoin du composant inscriptible du module svelte / store. En savoir plus sur le magasin .
La création d'un référentiel simple ressemble à ceci:


 //     import { writable } from "svelte/store"; //      null export const isPlaying = writable(null); 

Créons le dossier src / stores / , toutes les valeurs modifiables de notre jeu seront stockées ici.
Créez le fichier src / stores / game.js dans lequel les variables responsables de l'état général du jeu seront stockées.


src / stores / game.js
 //     import { writable } from "svelte/store"; //        ,    true/false export const isPlaying = writable(false); 

Créez le fichier src / stores / cannon.js , dans lequel seront stockées les variables responsables de l'état de l'arme


src / stores / cannon.js
 //     import { writable } from "svelte/store"; //    ,     . //    'left', 'right', null,    export const direction = writable(null); //     export const angle = writable(0); 

Svelte vous permet de créer des référentiels personnalisés qui incluent la logique de travail. Vous pouvez en savoir plus à ce sujet dans le manuel . Je ne pouvais pas intégrer cela magnifiquement dans le concept du cycle de jeu, donc nous ne déclarons que des variables dans le référentiel. Nous effectuerons toutes les manipulations avec eux dans la section src / gameLoop .


La boucle de jeu sera planifiée à l'aide de la fonction requestAnimationFrame . Un tableau de fonctions décrivant la logique du jeu sera fourni à l'entrée. À la fin du cycle de jeu, si le jeu n'est pas terminé, la prochaine itération est planifiée. Dans la boucle du jeu, nous accèderons à la valeur de la variable isPlaying pour vérifier si le jeu est terminé.


En utilisant le stockage, vous pouvez vous abonner à une valeur. Nous utiliserons cette fonctionnalité dans les composants. Pour l'instant, nous allons utiliser la fonction get pour lire la valeur de la variable. Pour définir la valeur, nous utiliserons la méthode .set () de la variable.
Vous pouvez mettre à jour la valeur en appelant la méthode .update () , qui prend une fonction en entrée, dont la première valeur est passée la valeur actuelle. Plus de détails dans la documentation . Tout le reste est pur JS.


src / gameLoop / gameLoop.js
 //     import { isPlaying } from '../stores/game'; //    get     ,  . import { get } from 'svelte/store'; //      function startLoop(steps) { window.requestAnimationFrame(() => { //      steps.forEach(step => { //    -  if (typeof step === 'function') step(); }); //    ,    if (get(isPlaying)) startLoop(steps); }); } //       export const startGame = () => { //  ,      true isPlaying.set(true); //   .     startLoop([]); }; //       export function stopGame() { //  ,      false isPlaying.set(false); } 

Nous décrivons maintenant la logique du comportement de notre arme.


src / gameLoop / cannon.js
 //    get     ,  . import { get } from 'svelte/store'; //      cannon import { angle, direction } from '../stores/cannon.js'; //      export function rotateCannon() { //     const currentAngle = get(angle); //    ,   ,    switch (get(direction)) { //    ""     -45°, //      0.4 case 'left': if (currentAngle > -45) angle.update(a => a - 0.4); break; //    ""     45°, //      0.4 case 'right': if (currentAngle < 45) angle.update(a => a + 0.4); break; default: break; } } 

Ajoutez maintenant notre gestionnaire de rotation des armes Ă  feu Ă  la boucle de jeu.


 import { rotateCannon } from "./cannon"; /* ... */ export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon]); }; 

Code du cycle de jeu actuel:


src / gameLoop / gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon } from './cannon'; //     function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon]); //      }; export function stopGame() { isPlaying.set(false); } 

Nous avons une logique qui peut faire tourner le pistolet. Mais nous ne l'avons pas encore associé à la poussée des boutons. Il est temps de le faire. Les gestionnaires d'événements de clic seront ajoutés à src / components / Controls.svelte .


 import { direction } from "../stores/cannon.js"; //       //    const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); 

Ajoutez nos gestionnaires et l'état actuel des clics aux éléments IconButton . Pour ce faire, passez simplement les valeurs aux attributs précédemment créés start , release et active , comme décrit dans la documentation .


 <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> 

Nous avons utilisé l'expression $ pour la variable $ direction . Cette syntaxe rend la valeur réactive, souscrivant automatiquement aux modifications. Plus de détails dans la documentation .


src / components / Controls.svelte
 <script> import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; //     import { direction } from "../stores/cannon.js"; //    const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); </script> <style> .controls { position: fixed; bottom: 0; left: 0; width: 100%; } .container { display: flex; justify-content: space-between; margin: 1rem; } .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <!--        --> <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> </div> <IconButton> <Bullet /> </IconButton> </div> </div> 

Pour le moment, en appuyant sur notre bouton, une sélection se produit, mais le pistolet ne tourne toujours pas. Nous devons importer la valeur d' angle dans le composant Cannon.svelte et mettre à jour les règles de transformation de transformation


src / components / Cannon.svelte
 <script> //      import { angle } from "../stores/cannon.js"; </script> <style> .cannon { transform-origin: 4px 55px; } </style> <!--    rotate(${$angle})--> <g class="cannon" transform={`translate(236, 700) rotate(${$angle})`}> <rect width="8" height="60" fill="#212121" /> </g> 

Il reste à exécuter notre boucle de jeu dans le composant App.svelte .


 import { startGame } from "./gameLoop/gameLoop"; startGame(); 

App.svelte
 <script> import Controls from "./components/Controls.svelte"; import GameField from "./components/GameField.svelte"; //     import { startGame } from "./gameLoop/gameLoop"; //  startGame(); </script> <style> :global(html) { height: 100%; } :global(body) { height: 100%; overscroll-behavior: none; user-select: none; margin: 0; background-color: #efefef; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } </style> <Controls /> <GameField /> 

Hourra! Notre arme a commencé à bouger.
image


6. Coups de feu


Maintenant, apprenez Ă  notre fusil Ă  tirer. Nous devons stocker les valeurs:


  • Le pistolet tire-t-il maintenant (le bouton de tir est enfoncĂ©);
  • L'horodatage du dernier coup, vous devez calculer la cadence de tir;
  • Tableau de coquillages.

Ajoutez ces variables à notre référentiel src / stores / cannon.js .


src / stores / cannon.js
 import { writable } from 'svelte/store'; export const direction = writable(null); export const angle = writable(0); //   export const isFiring = writable(false); export const lastFireAt = writable(0); export const bulletList = writable([]); 

Mettez Ă  jour les importations et la logique du jeu dans src / gameLoop / cannon.js .


src / gameLoop / cannon.js
 import { get } from 'svelte/store'; //   import { angle, direction, isFiring, lastFireAt, bulletList } from '../stores/cannon.js'; export function rotateCannon() { const currentAngle = get(angle); switch (get(direction)) { case 'left': if (currentAngle > -45) angle.update(a => a - 0.4); break; case 'right': if (currentAngle < 45) angle.update(a => a + 0.4); break; default: break; } } //   export function shoot() { //           800 , //          if (get(isFiring) && Date.now() - get(lastFireAt) > 800) { lastFireAt.set(Date.now()); //         . //  id   Math.random    bulletList.update(bullets => [...bullets, { x: 238, y: 760, angle: get(angle), id: () => Math.random() + Date.now() }]); } } //    export function moveBullet() { //    ,      y  -20, //          . //    , ,     . //      , ? bulletList.update(bullets => bullets.map(bullet => ({ ...bullet, y: bullet.y - 20, x: (780 - bullet.y) * Math.tan((bullet.angle * Math.PI) / 180) + 238, })), ); } //    ,     . export function clearBullets() { bulletList.update(bullets => bullets.filter(bullet => bullet.y > 0)); } //     Id. ,        export function removeBullet(id) { bulletList.update(bullets => bullets.filter(bullet => bullet.id !== id)); } 

Maintenant, nous importons nos gestionnaires dans gameLoop.js et les ajoutons Ă  la boucle de jeu.


 import { rotateCannon, shoot, moveBullet, clearBullets } from "./cannon"; /* ... */ export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon, shoot, moveBullet, clearBullets ]); }; 

src / gameLoop / gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; //        import { rotateCannon, shoot, moveBullet, clearBullets } from "./cannon"; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets ]); }; export function stopGame() { isPlaying.set(false); } 

Il ne nous reste plus qu'à créer le traitement en appuyant sur le bouton de tir et à ajouter l'affichage des obus sur le terrain de jeu.
Modifiez src / components / Controls.svelte .


 //  ,       import { direction, isFiring } from "../stores/cannon.js"; //      const startFire = () => isFiring.set(true); const stopFire = () => isFiring.set(false); 

Ajoutez maintenant nos gestionnaires au bouton de contrĂ´le de tir, comme nous l'avons fait avec les boutons de rotation


 <IconButton start={startFire} release={stopFire} active={$isFiring}> <Bullet /> </IconButton> 

src / components / Controls.svelte
 <script> import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; //  ,       import { direction, isFiring } from "../stores/cannon.js"; const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); //      const startFire = () => isFiring.set(true); const stopFire = () => isFiring.set(false); </script> <style> .controls { position: fixed; bottom: 0; left: 0; width: 100%; } .container { display: flex; justify-content: space-between; margin: 1rem; } .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> </div> <!--     --> <IconButton start={startFire} release={stopFire} active={$isFiring}> <Bullet /> </IconButton> </div> </div> 

Reste à afficher les obus sur le terrain de jeu. Tout d'abord, créez le composant projectile


src / components / Bullet.svelte
 <script> //   bullet  ,    export let bullet; </script> <!--  -  svg  --> <g transform={`translate(${bullet.x}, ${bullet.y}) rotate(${bullet.angle})`}> <rect width="3" height="5" fill="#212121" /> </g> 

Puisque les shells que nous avons sont stockés dans un tableau, nous avons besoin d'un itérateur pour les afficher. Dans svelte, il existe une directive Each pour de tels cas. Plus de détails dans la documentation .


 //    bulletList,      bullet. //      id  ,  svelte       ,   . //  key   React {#each $bulletList as bullet (bullet.id)} <Bullet {bullet}/> {/each} 

src/components/GameField.svelte
 <script> import Cannon from "./Cannon.svelte"; //    import Bullet from "./Bullet.svelte"; //      import { bulletList } from "../stores/cannon"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--       --> {#each $bulletList as bullet (bullet.id)} <Bullet {bullet} /> {/each} <Cannon /> </svg> </div> 

.
image


7.


Super. . src/stores/enemy.js .


src/stores/enemy.js
 import { writable } from "svelte/store"; //   export const enemyList = writable([]); //      export const lastEnemyAddedAt = writable(0); 

src/gameLoop/enemy.js


src/gameLoop/enemy.js
 import { get } from 'svelte/store'; //      import { enemyList, lastEnemyAddedAt } from '../stores/enemy.js'; //    export function addEnemy() { //         2500 , //     if (Date.now() - get(lastEnemyAddedAt) > 2500) { //      lastEnemyAddedAt.set(Date.now()); //        1  499 // (   ) enemyList.update(enemies => [ ...enemies, { x: Math.floor(Math.random() * 449) + 1, y: 0, id: () => Math.random() + Date.now(), }, ]); } } //   .       0.5 export function moveEnemy() { enemyList.update(enemyList => enemyList.map(enemy => ({ ...enemy, y: enemy.y + 0.5, })), ); } //      id,     export function removeEnemy(id) { enemyList.update(enemies => enemies.filter(enemy => enemy.id !== id)); } 

.


src/gameLoop/gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon'; //      import { addEnemy, moveEnemy } from './enemy'; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets, addEnemy, moveEnemy]); }; export function stopGame() { isPlaying.set(false); } 

src/components/Enemy.js .


src/components/Enemy.js
 <script> //   enemy   ,   export let enemy; </script> //    ,     . <g transform={`translate(${enemy.x}, ${enemy.y})`} > <rect width="30" height="30" fill="#212121" /> </g> 

, Each


src/components/GameField.svelte
 <script> import Cannon from "./Cannon.svelte"; import Bullet from "./Bullet.svelte"; //    import Enemy from "./Enemy.svelte"; import { bulletList } from "../stores/cannon"; //      import { enemyList } from "../stores/enemy"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--       --> {#each $enemyList as enemy (enemy.id)} <Enemy {enemy} /> {/each} {#each $bulletList as bullet (bullet.id)} <Bullet {bullet} /> {/each} <Cannon /> </svg> </div> 

!
image


8.


, .
. src/gameLoop/game.js . MDN


src/gameLoop/game.js
 import { get } from 'svelte/store'; //    import { bulletList } from '../stores/cannon'; //    import { enemyList } from '../stores/enemy'; //     import { removeBullet } from './cannon'; //     import { removeEnemy } from './enemy'; //       . //     ,   svg,   , //        . const enemyWidth = 30; const bulletWidth = 5; const enemyHeight = 30; const bulletHeight = 8; //    export function checkCollision() { get(bulletList).forEach(bullet => { get(enemyList).forEach(enemy => { if ( bullet.x < enemy.x + enemyWidth && bullet.x + bulletWidth > enemy.x && bullet.y < enemy.y + enemyHeight && bullet.y + bulletHeight > enemy.y ) { //   ,         removeBullet(bullet.id); removeEnemy(enemy.id); } }); }); } 

.


src/gameLoop/gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon'; //    import { checkCollision } from './game'; import { addEnemy, moveEnemy } from './enemy'; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets, addEnemy, moveEnemy, checkCollision]); }; export function stopGame() { isPlaying.set(false); } 

, .
image


9.


, ToDo :


  • , ;
  • ;
  • ;
  • . svelte ;
  • ;
  • . .

github .


Conclusion


, , React. 60 FPS, Svelte .
Svelte , .

Source: https://habr.com/ru/post/fr452684/


All Articles