O Svelte 3 foi lançado há pouco mais de um mês. Um bom momento para conhecê-lo, pensei, e fiz um excelente tutorial , que também foi traduzido para o russo.
Para consolidar o passado, fiz um pequeno projeto e compartilhei os resultados com você. Não é mais uma lista de tarefas, mas um jogo em que você precisa atirar em quadrados pretos.

0. Para impaciente
Repositório do tutorial
Repositório com extras
Demo
1. Preparação
Clonamos um modelo para desenvolvimento
git clone https://github.com/sveltejs/template.git
Instale as dependências.
cd template/ npm i
Iniciamos o servidor de desenvolvimento.
npm run dev
Nosso modelo está disponível em
http: // localhost: 5000 . O servidor suporta recarga a quente, para que nossas alterações sejam visíveis no navegador conforme as alterações são salvas.
Se você não deseja implantar o ambiente localmente, pode usar as caixas de proteção codesandbox e stackblitz online que oferecem suporte ao Svelte.
2. O quadro do jogo
A pasta src consiste em dois arquivos main.js e App.svelte .
main.js é o ponto de entrada para nosso aplicativo. Durante o desenvolvimento, não vamos tocá-lo. Aqui, o componente App.svelte é montado no corpo do documento.
App.svelte é um componente do svelte. O modelo do componente consiste em três partes:
<script> </script> <style> h1 { color: purple; } </style> <h1>Hello {name}!</h1>
Os estilos de componente são isolados, mas é possível atribuir estilos globais com a diretiva: global (). Mais sobre estilos .
Adicione estilos comuns para o nosso componente.
src / App.svelte <script> export let name; </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> <h1>Hello {name}!</h1>
Vamos criar a pasta src / components na qual nossos componentes serão armazenados
Nesta pasta, crie dois arquivos que conterão o campo de jogo e os controles.
src / components / GameField.svelte src / components / Controls.svelte O componente é importado pela diretiva
import Controls from "./components/Controls.svelte";
Para exibir um componente, basta inserir a etiqueta do componente na marcação. Saiba mais sobre tags .
<Controls />
Agora importamos e exibimos nossos componentes no App.svelte.
src / App.svelte <script> </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 />
3. Controles
O componente Controls.svelte consistirá em três botões: mover para a esquerda, mover para a direita, disparar. Os ícones de botão serão exibidos pelo elemento svg.
Crie a pasta src / asssets , à qual adicionamos nossos ícones 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>
Adicione o botão componente src / components / IconButton.svelte .
Aceitaremos manipuladores de eventos do componente pai. Para manter o botão pressionado, precisamos de dois manipuladores: o início do clique e o final do clique. Declaramos as variáveis start e release , onde aceitaremos os manipuladores de eventos para o início e o fim do clique. Também precisamos da variável ativa , que será exibida se o botão for pressionado ou não.
<script> export let start; export let release; export let active; </script>
Nós estilizamos nosso componente
<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>
Um botão é um elemento do botão no qual o conteúdo transferido de um componente pai é exibido. O local onde o conteúdo transferido será montado é indicado pela tag <slot />. Saiba mais sobre o elemento <slot /> .
<button> <slot /> </button>
Os manipuladores de eventos são indicados usando a diretiva on :, por exemplo, on: click .
Lidaremos com eventos do mouse e tocaremos em cliques. Saiba mais sobre a associação de eventos .
A classe ativa será adicionada à classe base do componente se o botão for pressionado. Você pode atribuir uma classe pela propriedade da classe. Mais sobre aulas
<button on:mousedown={start} on:touchstart={start} on:mouseup={release} on:touchend={release} class={`iconButton ${active ? 'active' : ''}`}> <slot /> </button>
Como resultado, nosso componente ficará assim:
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>
Agora importamos nossos ícones e o elemento button para src / components / Controls.svelte e criamos o layout.
src / components / Controls.svelte <script> </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>
Nosso aplicativo deve ficar assim:

4. O campo de jogo
O campo de jogo é um componente svg, onde adicionaremos nossos elementos de jogo (arma, projéteis, oponentes).
Atualizar código 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"> <svg viewBox="0 0 480 800"> </svg> </div>
Crie a pistola src / components / Cannon.svelte . Alto para um retângulo, mas mesmo assim.
src / components / Cannon.svelte <style> .cannon { transform-origin: 4px 55px; } </style> <g class="cannon" transform={`translate(236, 700)`}> <rect width="8" height="60" fill="#212121" /> </g>
Agora importamos nossa arma no campo de jogo.
src / GameField.svelte <script> </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. Ciclo de Jogo
Nós temos a estrutura básica do jogo. O próximo passo é criar um ciclo de jogo que processará nossa lógica.
Vamos criar armazenamentos onde as variáveis para nossa lógica estarão contidas. Vamos precisar do componente gravável do módulo svelte / store. Saiba mais sobre a loja .
A criação de um repositório simples se parece com isso:
Crie a pasta src / stores / , aqui todos os valores mutáveis do nosso jogo serão armazenados.
Crie o arquivo src / stores / game.js no qual as variáveis responsáveis pelo estado geral do jogo serão armazenadas.
Crie o arquivo src / stores / cannon.js , no qual as variáveis responsáveis pelo estado da arma serão armazenadas
O Svelte permite criar repositórios personalizados que incluem a lógica do trabalho. Você pode ler mais sobre isso no livro . Não consegui encaixar isso perfeitamente no conceito do ciclo do jogo, portanto, apenas declaramos variáveis no repositório. Executaremos todas as manipulações com eles na seção src / gameLoop .
O loop do jogo será planejado usando a função requestAnimationFrame . Uma matriz de funções que descreve a lógica do jogo será alimentada na entrada. No final do ciclo do jogo, se o jogo não terminar, a próxima iteração será planejada. No loop do jogo, acessaremos o valor da variável isPlaying para verificar se o jogo terminou.
Usando o armazenamento, você pode criar uma assinatura para um valor. Usaremos essa funcionalidade em componentes. Por enquanto, usaremos a função get para ler o valor da variável. Para definir o valor, usaremos o método .set () da variável
Você pode atualizar o valor chamando o método .update () , que assume uma função como entrada, cujo primeiro valor é passado no valor atual. Mais detalhes na documentação . Tudo o resto é JS puro.
src / gameLoop / gameLoop.js Agora descrevemos a lógica do comportamento de nossa arma.
src / gameLoop / cannon.js Agora adicione nosso manipulador de rotação de armas ao loop do jogo.
import { rotateCannon } from "./cannon"; export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon]); };
Código do ciclo de jogo atual:
src / gameLoop / gameLoop.js import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon } from './cannon';
Temos uma lógica que pode girar a arma. Mas ainda não o associamos ao apertar de botões. Está na hora de fazer isso. Os manipuladores de eventos de clique serão adicionados ao src / components / Controls.svelte .
import { direction } from "../stores/cannon.js";
Adicione nossos manipuladores e o estado atual do clique aos elementos IconButton . Para fazer isso, basta passar os valores para os atributos criados anteriormente, start , release e active , conforme descrito na documentação .
<IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton>
Usamos a expressão $ para a variável $ direction . Essa sintaxe torna o valor reativo, assinando automaticamente as alterações. Mais detalhes na documentação .
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 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>
No momento, ao pressionar nosso botão, uma seleção ocorre, mas a arma ainda não gira. Precisamos importar o valor do ângulo para o componente Cannon.svelte e atualizar as regras de transformação de transformação
src / components / Cannon.svelte <script> </script> <style> .cannon { transform-origin: 4px 55px; } </style> <g class="cannon" transform={`translate(236, 700) rotate(${$angle})`}> <rect width="8" height="60" fill="#212121" /> </g>
Resta executar o loop do jogo no componente App.svelte .
import { startGame } from "./gameLoop/gameLoop"; startGame();
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> <Controls /> <GameField />
Viva! Nossa arma começou a se mover.

6. Tiros
Agora ensine nossa arma a atirar. Precisamos armazenar os valores:
- A arma dispara agora (o botão de disparo está pressionado);
- O registro de data e hora do último tiro, você precisa calcular a taxa de tiro;
- Matriz de conchas.
Adicione essas variáveis ao nosso repositório src / stores / cannon.js .
src / stores / cannon.js import { writable } from 'svelte/store'; export const direction = writable(null); export const angle = writable(0);
Atualize as importações e a lógica do jogo em src / gameLoop / cannon.js .
src / gameLoop / cannon.js import { get } from 'svelte/store';
Agora importamos nossos manipuladores para gameLoop.js e os adicionamos ao loop do jogo.
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';
Agora só precisamos criar o processamento de pressionar o botão de disparo e adicionar a exibição de conchas no campo de jogo.
Edite src / components / Controls.svelte .
Agora adicione nossos manipuladores ao botão de controle de disparo, como fizemos com os botões de rotação
<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"; </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>
Resta exibir as conchas no campo de jogo. Primeiro, crie o componente de projétil
src / components / Bullet.svelte <script> </script> <g transform={`translate(${bullet.x}, ${bullet.y}) rotate(${bullet.angle})`}> <rect width="3" height="5" fill="#212121" /> </g>
Como os shells que temos são armazenados em uma matriz, precisamos de um iterador para exibi-los. Em svelte, há uma diretiva Each para esses casos. Mais detalhes na documentação .
// 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"; </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>
.

7.
Ótimo. . src/stores/enemy.js .
src/stores/enemy.js import { writable } from "svelte/store";
src/gameLoop/enemy.js
src/gameLoop/enemy.js import { get } from 'svelte/store';
.
src/gameLoop/gameLoop.js import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon';
src/components/Enemy.js .
src/components/Enemy.js <script> </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"; </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>
!

8.
, .
. src/gameLoop/game.js . MDN
src/gameLoop/game.js import { get } from 'svelte/store';
.
src/gameLoop/gameLoop.js import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon';
, .

9.
, ToDo :
github .
Conclusão
, , React. 60 FPS, Svelte .
Svelte , .