Please view README-EN.md
Tetris é um clássico jogo que desenvolvedores adoram recriar em várias linguagens de programação. Há muitas versões em JavaScript, mas fazer uma versão com React se tornou meu objetivo.
Clique aqui para jogar: https://chvin.github.io/react-tetris/
Gravado em velocidade normal, experiência fluida.
Não apenas se adapta à tela, mas também permite usar o teclado no PC e o toque no celular
:
O maior medo de jogar offline é perder progresso. Através do store.subscribe
, o estado é salvo no localStorage, registrando todos os estados precisamente. Mesmo que a página seja fechada ou atualizada, ou o celular fique sem bateria, você pode continuar de onde parou.
Visualização do estado Redux (Redux DevTools extension)
O Redux gerencia todos os estados armazenados, garantindo a persistência mencionada acima.
A estrutura do jogo usa React + Redux, com Immutable para gerenciar o estado do Redux. (Para mais informações sobre React e Redux, veja: Introdução ao React, Documentação do Redux em Chinês)
Immutable é um tipo de dado que, uma vez criado, não pode ser alterado. Qualquer modificação, adição ou remoção de um objeto Immutable retorna um novo objeto Immutable.
Vamos ver o seguinte código:
function keyLog(touchFn) {
let data = { key: 'value' };
f(data);
console.log(data.key); // O que será impresso?
}
Sem ver a função f, não sabemos o que ela faz com data
, então não podemos ter certeza do valor impresso. Mas se data
for Immutable, podemos ter certeza de que imprimirá value
:
function keyLog(touchFn) {
let data = Immutable.Map({ key: 'value' });
f(data);
console.log(data.get('key')); // value
}
Em JavaScript, Object
e Array
usam atribuição por referência. Um novo objeto referencia o objeto original, de modo que alterar o novo objeto também altera o antigo:
foo = {a: 1}; bar = foo; bar.a = 2;
foo.a // 2
Embora isso economize memória, pode causar estados incontroláveis em aplicativos complexos, transformando a economia de memória em um problema.
Com Immutable, é diferente:
foo = Immutable.Map({ a: 1 }); bar = foo.set('a', 2);
foo.get('a') // 1
No Redux
, a melhor prática é que cada reducer
retorne um novo objeto (array). Muitas vezes vemos código assim:
// reducer
...
return [
...oldArr.slice(0, 3),
newValue,
...oldArr.slice(4)
];
Para retornar um novo objeto (array), temos que escrever código estranho como o acima. Com estruturas de dados mais profundas, isso se torna ainda mais complicado. Com Immutable, fica assim:
// reducer
...
return oldArr.set(4, newValue);
Muito mais simples, não?
Sabemos que a comparação ===
para Object
e Array
é baseada na referência de endereço, não no valor:
{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false
Para comparar esses valores, precisaríamos de deepCopy
ou deepCompare
, que são trabalhosos e consomem muita performance.
Com Immutable, a comparação fica assim:
map1 = Immutable.Map({a:1, b:2, c:3});
map2 = Immutable.Map({a:1, b:2, c:3});
Immutable.is(map1, map2); // true
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
List1 = Immutable.fromJS([1, 2, [3, 4]]);
List2 = Immutable.fromJS([1, 2, [3, 4]]);
Immutable.is(List1, List2); // true
Muito mais eficiente!
Quando otimizamos a performance do React, uma técnica importante é shouldComponentUpdate()
, que por padrão retorna true
, sempre executando o método render()
. Para calcular shouldComponentUpdate
corretamente com objetos nativos, precisaríamos de deepCopy
e deepCompare
, o que consome muita performance. Com Immutable, a comparação de estruturas profundas é simples.
No Tetris, imagine que o tabuleiro é uma matriz 2D
, e a peça móvel é uma forma (também uma matriz 2D)
+ coordenadas
. A combinação do tabuleiro e da peça forma o resultado final Matrix
. Essas propriedades são construídas com Immutable, facilitando a escrita de shouldComponentUpdate
. Código-fonte: /src/components/matrix/index.js#L35
Recursos para aprender Immutable:
Objetivo: Tornar o state
Immutable.
Biblioteca-chave: gajus/redux-immutable
Troque combineReducers
do Redux pela biblioteca acima:
// rootReducers.js
// import { combineReducers } from 'redux'; // Método antigo
import { combineReducers } from 'redux-immutable'; // Novo método
import prop1 from './prop1';
import prop2 from './prop2';
import prop3 from './prop3';
const rootReducer = combineReducers({
prop1, prop2, prop3,
});
// store.js
// Criação da store é igual ao método convencional
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
O novo combineReducers
transforma o objeto store em Immutable. No container, o uso também será ligeiramente diferente (mas é exatamente o que queremos):
const mapStateToProps = (state) => ({
prop1: state.get('prop1'),
prop2: state.get('prop2'),
prop3: state.get('prop3'),
next: state.get('next'),
});
export default connect(mapStateToProps)(App);
O jogo possui muitos efeitos sonoros diferentes, mas na verdade só utiliza um arquivo de som: /build/music.mp3. Com a Web Audio API
, é possível reproduzir efeitos sonoros de alta frequência e precisão em milissegundos, algo que a tag <audio>
não consegue fazer. Durante o jogo, ao segurar as teclas de direção, você pode ouvir efeitos sonoros de alta frequência.
WAA
é uma nova e independente interface que oferece maior controle sobre arquivos de áudio e efeitos de som profissionais. Ela é recomendada pelo W3C e permite manipulações profissionais de "velocidade do som, volume, visualização do ambiente, timbre, alta frequência e direção do som". Abaixo está o fluxo de uso do WAA.
No diagrama, Source representa uma fonte de áudio e Destination representa a saída final. Múltiplas fontes se combinam para formar a Destination. Código-fonte: /src/unit/music.js, que implementa o carregamento AJAX de mp
3 e a conversão para WAA, controlando a reprodução.
A compatibilidade do WAA com as versões mais recentes dos navegadores (CanIUse):
Podemos ver que IE e a maioria dos dispositivos Android não são compatíveis, mas os outros navegadores sim.
Recursos para aprender Web Audio API:
- Técnica:
- A frequência de disparo para movimento horizontal e vertical ao pressionar as teclas de direção é diferente. O jogo define a frequência de disparo, substituindo a frequência de eventos nativos. Código-fonte: /src/unit/event.js;
- Movimentos laterais podem atrasar a velocidade de queda, mas quando se move ao bater na parede, o atraso é menor. Na velocidade nível 6, o atraso garante que você possa mover horizontalmente por uma linha completa;
- Eventos
touchstart
emousedown
são registrados para botões, permitindo um jogo responsivo. Quandotouchstart
ocorre,mousedown
não é disparado, e quandomousedown
ocorre,mouseout
é usado para simularmouseup
. Código-fonte: /src/components/keyboard/index.js; - O evento
visibilitychange
é monitorado, pausando o jogo quando a página é oculta/trocada, e retomando quando volta ao foco. Esse estado de foco também é armazenado no Redux. Então, se receber uma ligação no celular, o progresso do jogo é salvo; no PC, o jogo pausará quando sair da página e não tocará o som de game over. Isso é semelhante ao comportamento dos aplicativos iOS; - A qualquer momento, atualizar a página (por exemplo, ao limpar linhas ou quando o jogo termina) restaura o estado atual;
- A única imagem usada no jogo é
, o restante é feito com CSS;
- O jogo é compatível com Chrome, Firefox, IE9+, Edge e outros;
- Jogabilidade:
- Você pode definir o tabuleiro inicial (dez níveis) e a velocidade (seis níveis) antes de começar o jogo;
- Eliminar 1 linha dá 100 pontos, 2 linhas dão 300 pontos, 3 linhas dão 700 pontos, e 4 linhas dão 1500 pontos;
- A velocidade de queda aumenta com o número de linhas eliminadas (aumenta um nível a cada 20 linhas);
- Escrever
shouldComponentUpdate
para todos oscomponents
melhorou significativamente o desempenho no celular. Aplicações de médio a grande porte podem se beneficiar muito ao escrevershouldComponentUpdate
. - Componentes sem estado (Stateless Functional Components) não possuem ciclo de vida. Devido ao ponto acima, todos os componentes precisam do ciclo de vida
shouldComponentUpdate
, então componentes sem estado não foram usados. - No
webpack.config.js
, definir o atributo devServer comohost: '0.0.0.0'
permite acessar o desenvolvimento pelo IP, não apenas localhost; - No Redux, a
store
não precisa ser passada apenas via connect para ocontainer
. Pode-se obter a store em outros arquivos para controle de fluxo (dispatch). Código-fonte: /src/control/states.js; - Com react+redux, a persistência é muito conveniente. Basta armazenar o estado Redux e, ao inicializar cada reducer, ler o estado armazenado.
- Integrar ESLint no projeto através da configuração
.eslintrc.js
ewebpack.config.js
ajuda a manter o código conforme as normas, controlando a qualidade do código. Erros fora das normas podem ser detectados durante o desenvolvimento (ou build) pelo IDE e console. Referência: Guia de estilo do React da Airbnb;
- Como um aplicativo de prática com React, a implementação do Tetris revelou muitos detalhes que podem ser otimizados e aprimorados. Isso testa a atenção e habilidade de um desenvolvedor front-end.
- A otimização envolve tanto o próprio React, como decidir quais estados ficam no Redux e quais no state do componente, quanto características específicas do produto. Para alcançar suas necessidades, essas otimizações impulsionam o desenvolvimento da tecnologia.
- Um projeto começa do zero, com funcionalidades acumulando pouco a pouco, até se tornar algo grandioso. Não tenha medo das dificuldades, se tem uma ideia, comece a codificar. ^_^
npm install
npm start
O navegador abrirá automaticamente http://127.0.0.1:8080/
Configurar o ambiente multilíngue em i18n.json, usando o parâmetro "lan" para definir o idioma, por exemplo: https://chvin.github.io/react-tetris/?lan=en
npm run build
O resultado será gerado na pasta build.