Книга: Jogos em HTML5
© Casa do Código
Todos os direitos reservados e protegidos pela Lei nº9.610, de
10/02/1998.
Nenhuma parte deste livro poderá ser reproduzida, nem transmitida, sem
autorização prévia por escrito da editora, sejam quais forem os meios:
fotográficos, eletrônicos, mecânicos, gravação ou quaisquer outros.
Casa do Código
Livros para o programador
Rua Vergueiro, 3185 - 8º andar
04101-300 – Vila Mariana – São Paulo – SP – Brasil
Casa do Código
Introdução
Este livro é continuação de minha obra anterior, Desenvolva jogos com HTML5
Canvas e JavaScript ([5]). Nela, introduzi a tecnologia Canvas do HTML5
e, principalmente, guiei o leitor pelos conceitos mais básicos envolvidos na
criação de jogos em 2D: sprites, loops de animação, input do usuário e outros, sem depender de algum framework específico. Agora, para este novo livro, eu
assumo que você já tenha esse conhecimento básico.
O que nós, você e eu, iremos fazer aqui é aprofundar nossos caminhos em
algumas coisas mais específicas (leia-se frameworks), que são:
• Apache Cordova:
inicialmente chamado PhoneGap, consiste em
uma plataforma para a geração de aplicações HTML5 em diversos sis-
temas operacionais móveis (Android, iOS, Windows Phone etc.). Uma
mesma base de código, usando as tecnologias web, rodando como apps
em todos esses sistemas!
• Hammer.js: biblioteca que detecta gestos comuns em telas sensíveis
ao toque, como pinças, rotações e outros, complementando o suporte
do HTML5.
• Box2dWeb: a fantástica engine de simulação física Box2D, criada em
C++ por Erin Catto, foi portada para inúmeras outras linguagens, in-
clusive, claro, JavaScript. Espero que só este fato já faça valer a pena a
leitura deste livro para você, pois é a mesma API (conjunto de classes e
métodos) para C++, Java, JavaScript e outros.
Não que eu pretenda que este livro esteja totalmente amarrado ao
outro mas, para facilitar, vou pegar muitos códigos criados anteriormente e
i
Casa do Código
reutilizá-los (aprendi no meu jardim de infância da programação que é para
isto que serve a orientação a objetos). Não se preocupe, caso não tenha lido
a primeira obra. Darei uma recapitulada. Se você não a leu, será como mais
um frameworkzinho que você está usando.
O livro está dividido em duas partes: Mobile e Física. Na primeira parte,
estão os capítulos:
• 1. Instalação do Apache Cordova: este framework permite trans-
formar páginas HTML em apps para diversos sistemas operacionais
móveis. Aqui explico o que é o Cordova, como funciona e sua insta-
lação passo a passo. Ele se apoia em muitos outros programas que você
deverá ter instalados em sua máquina de desenvolvimento.
• 2. Criando o primeiro projeto: você aprenderá todo o ciclo de criação de um app Cordova, desde a configuração do projeto até a geração do
pacote APK do Android assinado para distribuição.
• 3. Eventos touch: conheceremos a especificação e a API do HTML5
para tratamento de eventos touchscreen. Aprenderemos a verificar as
coordenadas, a cruzá-las com algum objeto no Canvas e como fazer
para o jogador deslocar sprites arrastando-os com o dedo.
• 4. Incrementando o jogo de nave: o jogo de guerra espacial que cri-
amos no primeiro livro ganhará duas versões mobile: na primeira, a
nave será controlada tocando botões na tela; na segunda, o jogador
terá que inclinar o dispositivo móvel na direção em que ele quer que
a nave se mova. Para isso, teremos que fazer a leitura do acelerômetro
do aparelho. Também conheceremos a API de multimídia do Cordova,
que permitirá o uso de som em aparelhos cuja webview nativa não su-
porta a API de áudio do HTML5.
• 5. Interações avançadas com Hammer.js: você aprenderá a usar este
framework para detectar gestos de pinça, rotação, deslocamentos e out-
ros. Aqui, criaremos controles para movimentar um taco de bilhar, que
será usado em um projeto prático de jogo na segunda parte do livro.
ii
Casa do Código
Na segunda parte, é feita uma introdução dos fundamentos do Box2dWeb
e, em seguida, guio o leitor na construção de um jogo de bilhar, mostrando
como unir a lógica de física com a lógica de negócios e de interação com o
jogador:
• 6. Introdução ao Box2dWeb: conceitos básicos desta engine de física.
Criação do mundo físico, corpos, fixtures e gravidade.
• 7. Configuração e movimento de corpos: crie corpos de diversos
formatos, configure sua elasticidade e atrito, aplique forças e impulsos
para movimentá-los.
• 8. Iniciando o jogo de bilhar: construiremos um jogo utilizando a
Box2dWeb para controlar os movimentos com física realista. Progra-
maremos os sprites, posicionaremos as bolas na mesa e simularemos
uma tacada, vendo-as se espalharem pelo cenário.
• 9. Interação com o jogador e lógicas de negócio:
incorporamos
ao jogo os controles criados com o Hammer.js e executaremos lógicas
de acordo com os eventos que ocorrerem, como fazer uma bola en-
caçapada desaparecer ou retornar ao seu ponto inicial, caso seja a bola
branca.
• 10. Simulações internas:
usaremos o Box2dWeb puramente em
memória para testar diversas jogadas que o computador pode realizar.
• 11. Finalizando o jogo:
determinaremos qual variedade de bilhar
será implementada, e aplicaremos suas regras para selecionar a melhor
tacada automática e para arbitrar o andamento do jogo.
O jogo de bilhar pode ser jogado agora mesmo em seu celular ou tablet,
no endereço:
http://gamecursos.com.br/livro2/
iii
Casa do Código
Figura 1: Jogo de bilhar a ser desenvolvido no livro
Procure jogá-lo com o dispositivo móvel na posição paisagem (deitado).
Para jogar:
1) Coloque 2 dedos na área circular e gire
2) Puxe a seta azul para regular a força
3) Clique Pow!
Você deve acertar a branca na bola da vez indicada na tela; qual você en-
caçapa é indiferente. Ganha o jogo quem encaçapar a bola 9.
Todos os códigos, imagens e sons de jogos deste livro podem ser baixados
em:
https://github.com/EdyKnopfler/games-js-2/archive/master.zip
O jogo de bilhar é disponibilizado sob a licença GNU, não sendo permi-
tido o uso de seu código em aplicações comerciais. Os outros códigos e exer-
cícios estão disponíveis sob a licença Apache, estes, sim, podendo ser usados
comercialmente.
Você também precisará dos materiais do meu livro anterior:
http://github.com/EdyKnopfler/games-js/archive/master.zip
Continuo à disposição no Google Groups, onde já presto suporte aos
leitores:
http://groups.google.com/forum/#!forum/livro-jogos-html5-canvas
iv
Casa do Código
A instalação do Apache Cordova é um bocado trabalhosa. No primeiro
capítulo, tratarei desse assunto exaustivamente. No entanto, reconhecendo a
necessidade de uma demonstração prática para tantos detalhes, gravei uma
rápida videoaula como material complementar, na qual demonstro uma in-
stalação do framework em ambiente Windows. Acesse o vídeo em:
https://www.youtube.com/watch?v=Rf85Y0iNJIE
Prepare-se! Desejo-lhe uma ótima leitura e ainda mais ótimas experiên-
cias futuras com a programação de games!
v
Casa do Código
Introdução
Este livro é continuação de minha obra anterior, Desenvolva jogos com HTML5
Canvas e JavaScript ([5]). Nela, introduzi a tecnologia Canvas do HTML5
e, principalmente, guiei o leitor pelos conceitos mais básicos envolvidos na
criação de jogos em 2D: sprites, loops de animação, input do usuário e outros, sem depender de algum framework específico. Agora, para este novo livro, eu
assumo que você já tenha esse conhecimento básico.
O que nós, você e eu, iremos fazer aqui é aprofundar nossos caminhos em
algumas coisas mais específicas (leia-se frameworks), que são:
• Apache Cordova:
inicialmente chamado PhoneGap, consiste em
uma plataforma para a geração de aplicações HTML5 em diversos sis-
temas operacionais móveis (Android, iOS, Windows Phone etc.). Uma
mesma base de código, usando as tecnologias web, rodando como apps
em todos esses sistemas!
• Hammer.js: biblioteca que detecta gestos comuns em telas sensíveis
ao toque, como pinças, rotações e outros, complementando o suporte
do HTML5.
• Box2dWeb: a fantástica engine de simulação física Box2D, criada em
C++ por Erin Catto, foi portada para inúmeras outras linguagens, in-
clusive, claro, JavaScript. Espero que só este fato já faça valer a pena a
leitura deste livro para você, pois é a mesma API (conjunto de classes e
métodos) para C++, Java, JavaScript e outros.
Não que eu pretenda que este livro esteja totalmente amarrado ao
outro mas, para facilitar, vou pegar muitos códigos criados anteriormente e
vii
Casa do Código
reutilizá-los (aprendi no meu jardim de infância da programação que é para
isto que serve a orientação a objetos). Não se preocupe, caso não tenha lido
a primeira obra. Darei uma recapitulada. Se você não a leu, será como mais
um frameworkzinho que você está usando.
O livro está dividido em duas partes: Mobile e Física. Na primeira parte,
estão os capítulos:
• 1. Instalação do Apache Cordova: este framework permite trans-
formar páginas HTML em apps para diversos sistemas operacionais
móveis. Aqui explico o que é o Cordova, como funciona e sua insta-
lação passo a passo. Ele se apoia em muitos outros programas que você
deverá ter instalados em sua máquina de desenvolvimento.
• 2. Criando o primeiro projeto: você aprenderá todo o ciclo de criação de um app Cordova, desde a configuração do projeto até a geração do
pacote APK do Android assinado para distribuição.
• 3. Eventos touch: conheceremos a especificação e a API do HTML5
para tratamento de eventos touchscreen. Aprenderemos a verificar as
coordenadas, a cruzá-las com algum objeto no Canvas e como fazer
para o jogador deslocar sprites arrastando-os com o dedo.
• 4. Incrementando o jogo de nave: o jogo de guerra espacial que cri-
amos no primeiro livro ganhará duas versões mobile: na primeira, a
nave será controlada tocando botões na tela; na segunda, o jogador
terá que inclinar o dispositivo móvel na direção em que ele quer que
a nave se mova. Para isso, teremos que fazer a leitura do acelerômetro
do aparelho. Também conheceremos a API de multimídia do Cordova,
que permitirá o uso de som em aparelhos cuja webview nativa não su-
porta a API de áudio do HTML5.
• 5. Interações avançadas com Hammer.js: você aprenderá a usar este
framework para detectar gestos de pinça, rotação, deslocamentos e out-
ros. Aqui, criaremos controles para movimentar um taco de bilhar, que
será usado em um projeto prático de jogo na segunda parte do livro.
viii
Casa do Código
Na segunda parte, é feita uma introdução dos fundamentos do Box2dWeb
e, em seguida, guio o leitor na construção de um jogo de bilhar, mostrando
como unir a lógica de física com a lógica de negócios e de interação com o
jogador:
• 6. Introdução ao Box2dWeb: conceitos básicos desta engine de física.
Criação do mundo físico, corpos, fixtures e gravidade.
• 7. Configuração e movimento de corpos: crie corpos de diversos
formatos, configure sua elasticidade e atrito, aplique forças e impulsos
para movimentá-los.
• 8. Iniciando o jogo de bilhar: construiremos um jogo utilizando a
Box2dWeb para controlar os movimentos com física realista. Progra-
maremos os sprites, posicionaremos as bolas na mesa e simularemos
uma tacada, vendo-as se espalharem pelo cenário.
• 9. Interação com o jogador e lógicas de negócio:
incorporamos
ao jogo os controles criados com o Hammer.js e executaremos lógicas
de acordo com os eventos que ocorrerem, como fazer uma bola en-
caçapada desaparecer ou retornar ao seu ponto inicial, caso seja a bola
branca.
• 10. Simulações internas:
usaremos o Box2dWeb puramente em
memória para testar diversas jogadas que o computador pode realizar.
• 11. Finalizando o jogo:
determinaremos qual variedade de bilhar
será implementada, e aplicaremos suas regras para selecionar a melhor
tacada automática e para arbitrar o andamento do jogo.
O jogo de bilhar pode ser jogado agora mesmo em seu celular ou tablet,
no endereço:
http://gamecursos.com.br/livro2/
ix
Casa do Código
Figura 2: Jogo de bilhar a ser desenvolvido no livro
Procure jogá-lo com o dispositivo móvel na posição paisagem (deitado).
Para jogar:
1) Coloque 2 dedos na área circular e gire
2) Puxe a seta azul para regular a força
3) Clique Pow!
Você deve acertar a branca na bola da vez indicada na tela; qual você en-
caçapa é indiferente. Ganha o jogo quem encaçapar a bola 9.
Todos os códigos, imagens e sons de jogos deste livro podem ser baixados
em:
https://github.com/EdyKnopfler/games-js-2/archive/master.zip
O jogo de bilhar é disponibilizado sob a licença GNU, não sendo permi-
tido o uso de seu código em aplicações comerciais. Os outros códigos e exer-
cícios estão disponíveis sob a licença Apache, estes, sim, podendo ser usados
comercialmente.
Você também precisará dos materiais do meu livro anterior:
http://github.com/EdyKnopfler/games-js/archive/master.zip
Continuo à disposição no Google Groups, onde já presto suporte aos
leitores:
http://groups.google.com/forum/#!forum/livro-jogos-html5-canvas
x
Casa do Código
A instalação do Apache Cordova é um bocado trabalhosa. No primeiro
capítulo, tratarei desse assunto exaustivamente. No entanto, reconhecendo a
necessidade de uma demonstração prática para tantos detalhes, gravei uma
rápida videoaula como material complementar, na qual demonstro uma in-
stalação do framework em ambiente Windows. Acesse o vídeo em:
https://www.youtube.com/watch?v=Rf85Y0iNJIE
Prepare-se! Desejo-lhe uma ótima leitura e ainda mais ótimas experiên-
cias futuras com a programação de games!
xi
Casa do Código
Sumário
Sumário
Mobile
1
1
Instalando o Apache Cordova
3
1.1
O que é o Cordova e o que tem a ver com o PhoneGap . . . .
4
1.2
Instalando os prerrequisitos . . . . . . . . . . . . . . . . . . .
4
1.3
Considerações específicas de sistema operacional . . . . . . .
11
1.4
Testando a conexão com o dispositivo . . . . . . . . . . . . .
15
2
Criando o primeiro projeto
17
2.1
O projeto Cordova . . . . . . . . . . . . . . . . . . . . . . . . .
17
2.2
O arquivo config.xml . . . . . . . . . . . . . . . . . . . . . . .
18
2.3
Configurando o Android para depuração USB . . . . . . . .
20
2.4
Rodando o projeto padrão . . . . . . . . . . . . . . . . . . . .
22
2.5
Um pequeno projeto do zero: animação com o Canvas . . . .
24
2.6
Um polyfill para requestAnimationFrame . . . . . . . . . . .
28
2.7
Gerar pacote APK assinado para o Android . . . . . . . . . .
31
2.8
Considerações finais . . . . . . . . . . . . . . . . . . . . . . . .
35
3
Eventos touch
37
3.1
Arrastando um objeto . . . . . . . . . . . . . . . . . . . . . . .
38
3.2
Controlando a direção . . . . . . . . . . . . . . . . . . . . . .
46
3.3
Toque em Canvas responsivo . . . . . . . . . . . . . . . . . . .
50
xiii
Sumário
Casa do Código
4
Incrementando o jogo de nave
55
4.1
Revisão das classes do jogo . . . . . . . . . . . . . . . . . . . .
56
4.2
Esqueleto do projeto . . . . . . . . . . . . . . . . . . . . . . . .
57
4.3
API de som do Cordova . . . . . . . . . . . . . . . . . . . . . .
66
4.4
Criando um controle direcional . . . . . . . . . . . . . . . . .
70
4.5
Precisão com múltiplos dedos . . . . . . . . . . . . . . . . . .
80
4.6
API do acelerômetro do Cordova . . . . . . . . . . . . . . . .
83
4.7
Adaptando o Canvas ao tamanho da tela . . . . . . . . . . . .
88
5
Interações avançadas com Hammer.js
93
5.1
Conhecendo o Hammer.js . . . . . . . . . . . . . . . . . . . .
94
5.2
Barra de ajuste de força . . . . . . . . . . . . . . . . . . . . . .
101
5.3
Rotacionando um taco de bilhar . . . . . . . . . . . . . . . . .
110
5.4
Animando a tacada . . . . . . . . . . . . . . . . . . . . . . . .
117
Física
123
6
Introdução ao Box2dWeb
125
6.1
Um primeiro tutorial . . . . . . . . . . . . . . . . . . . . . . .
127
6.2
Objetos fundamentais . . . . . . . . . . . . . . . . . . . . . . .
128
6.3
Animação do mundo físico . . . . . . . . . . . . . . . . . . . .
137
7
Configurações e movimento de corpos
141
7.1
Formas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
142
7.2
Movimentando corpos . . . . . . . . . . . . . . . . . . . . . .
152
7.3
Propriedades físicas . . . . . . . . . . . . . . . . . . . . . . . .
156
8
Iniciando o jogo de bilhar
161
8.1
Preparação do projeto . . . . . . . . . . . . . . . . . . . . . . .
162
8.2
Primeiros sprites . . . . . . . . . . . . . . . . . . . . . . . . . .
167
8.3
As bolas de bilhar . . . . . . . . . . . . . . . . . . . . . . . . .
175
8.4
Executando a tacada . . . . . . . . . . . . . . . . . . . . . . . .
182
8.5
Ângulo da trajetória . . . . . . . . . . . . . . . . . . . . . . . .
186
xiv
Casa do Código
Sumário
9
Interação com o jogador e lógicas de negócio
191
9.1
Controles da tacada . . . . . . . . . . . . . . . . . . . . . . . .
192
9.2
Executando a tacada no ângulo correto . . . . . . . . . . . . .
196
9.3
Posicionando o taco . . . . . . . . . . . . . . . . . . . . . . . .
199
9.4
Tratamento das colisões . . . . . . . . . . . . . . . . . . . . . .
201
10 Simulações internas
209
10.1 Realizando várias simulações a partir do início . . . . . . . .
210
10.2 Salvando e recuperando o estado do mundo físico . . . . . .
213
10.3 Executando a tacada em tempo curto . . . . . . . . . . . . . .
215
10.4 Tratador de colisões para a simulação . . . . . . . . . . . . . .
218
11 Finalizando o jogo
223
11.1
Regras do Bola 9 . . . . . . . . . . . . . . . . . . . . . . . . . .
224
11.2 Mostradores . . . . . . . . . . . . . . . . . . . . . . . . . . . .
234
Conclusão
239
Índice Remissivo
239
Bibliografia
241
xv
Parte I
Mobile
Capítulo 1
Instalando o Apache Cordova
Está preparado? Neste capítulo, você tomará contato com o que eu considero
uma das ideias mais brilhantes para o desenvolvimento de software. Tudo o
que foi feito no primeiro livro, rodando em navegadores desktop, se tornará
realidade em dispositivos móveis. Ao longo deste livro, iremos criar versões
móveis do jogo de nave, simular partidas de bilhar, além de diversos exercícios com telas touch e física.
Preciso avisar-lhe: para isto se tornar realidade, você terá que instalar
várias ferramentas em seu computador. Este primeiro capítulo é dedicado
a ensiná-lo como fazer isso. Pode ser que, como desenvolvedor, você já tenha
uma ou mais delas. Em todo caso, pegue um café e um prato de biscoitos,
ponha uma música relaxante e vamos começar.
1.1. O que é o Cordova e o que tem a ver com o PhoneGap
Casa do Código
1.1
O que é o Cordova e o que tem a ver com o
PhoneGap
Se eu lhe perguntar: qual plataforma de software roda em qualquer dispos-
itivo moderno, não importando seu tamanho físico, velocidade, memória...
Java? Talvez, mas existe uma outra: a web. Uma página web pode ser criada
para todos os dispositivos imagináveis. Como a tecnologia Canvas faz parte
do padrão atual da web (HTML5), podemos dizer o mesmo de jogos criados
com ele.
Por que, portanto, não utilizar as tecnologias da web para desenvolvi-
mento móvel?
Em 2008, a Nitobi (hoje adquirida pela Adobe) começou a trabalhar
no PhoneGap, um sistema que empacota páginas HTML na forma de apps
para diversas plataformas móveis. Não há mágica: são, na realidade, apli-
cações híbridas, nas quais o componente nativo de renderização web de cada plataforma é usado para processar o conteúdo em formato web. Sendo apps
nativas, podem ser distribuídas nas lojas de aplicativos e rodar localmente.
Em 2011, o projeto foi doado à Fundação Apache, onde passou a chamar-
se Cordova. A Adobe adquiriu a Nitobi logo em seguida e deu foco especial à manutenção do projeto, passando a oferecer um serviço de geração de apps
em nuvem, o Adobe PhoneGap Build (https://build.phonegap.com/) , man-
tendo o nome original. Este serviço, no entanto, é oferecido gratuitamente
apenas para projetos de código livre. Por esta razão é que dou preferência,
neste livro, à instalação local da biblioteca base.
Mas não se engane: não se trata apenas de um simples renderizador. A
WebView padrão de cada plataforma é estendida com APIs JavaScript para acesso a diversos recursos dos dispositivos, como telefone, contatos, câmera,
acelerômetro, GPS, sistema de arquivos e outros. Enfim, uma plataforma
completa para desenvolvimento de apps, tendo como base mas não se limi-
tando ao formato web.
1.2
Instalando os prerrequisitos
A partir da versão 3.0, o Cordova traz uma interface de linha de comando,
a Cordova CLI (Command Line Interface). É muito prática de se usar, porém 4
Casa do Código
Capítulo 1. Instalando o Apache Cordova
utiliza internamente diversas outras ferramentas conhecidas por desenvolve-
dores de software.
A seguir, as ferramentas, seus links para download e o devido porquê.
Node.js
http://nodejs.org/download
Engine de execução de código JavaScript, sobre a qual a CLI roda. Esta
será instalada como um plugin do Node.js. A CLI, em si, é uma aplicação
JavaScript do Node.js.
Para Windows e Mac OS X são fornecidos instaladores. No Linux, você
deve extrair o arquivo compactado para o local de sua preferência e configurar o path de seu sistema (mais detalhes na seção 1.3) para enxergar a subpasta
bin, onde se encontra o executável do Node.js e o utilitário npm.
Figura 1.1: Opções de download do Node.js
O npm serve para baixar extensões do Node.js, das quais uma é o Cordova CLI. Para instalá-lo, digite o seguinte em um terminal ou prompt de comando:
5
1.2. Instalando os prerrequisitos
Casa do Código
Neste ponto, o Cordova CLI está instalado, mas ainda temos trabalho até
poder gerar um app para uma determinada plataforma.
Apache Ant
http://ant.apache.org/bindownload.cgi (sim, é “bindownload” tudo junto)
Ferramenta de automação bem conhecida por desenvolvedores Java, é re-
querida pela CLI na hora de gerar projetos Android nativos. Se esta ferra-
menta não estiver instalada, você verá mensagens de erro na hora de adicionar
a plataforma Android ao seu projeto Cordova.
Vá ao endereço indicado e role a página para encontrar os links da versão
atual ( Current Release of Ant). Você novamente terá que extrair os arquivos e configurar a subpasta bin no path do seu sistema.
Figura 1.2: Opções de download do Apache Ant
Git
http://git-scm.com/downloads
A partir da versão 3.0, as diferentes APIs do Cordova foram quebradas em
plugins, que devem ser baixados e instalados nos devidos projetos. Quem se
encarrega desses downloads é o Git, conhecida ferramenta de gerenciamento
de repositórios (como o GitHub). O Cordova também cuida de chamá-lo no
momento da instalação de um plugin.
6
Casa do Código
Capítulo 1. Instalando o Apache Cordova
SDKs
• Android
(requer
Java
SDK):
primeiro
http://www.oracle.
com/technetwork/pt/java/javase/downloads,
depois
http:
//developer.android.com/sdk/
• iOS: http://developer.apple.com/programs/ios/
• Windows Phone: http://developer.windowsphone.com/
Software Development Kits das diversas plataformas, usados para compilar os componentes nativos. Estes são só alguns exemplos, veja a lista completa
em http://goo.gl/NkIfhK. Vou dar o exemplo da configuração de um ambi-
ente Android, mas quero que você tenha em mente que este link traz detalhes
sobre a configuração em cada plataforma suportada pelo Cordova.
Ou seja, não há nenhuma mágica: se o Cordova é capaz de gerar apps
para diversas plataformas, é porque foi criada a comunicação com cada um
dos SDKs nativos. Você pode instalar todos, ou somente aqueles que deseja
usar. Neste livro, focarei no Android.
O Android SDK requer o SDK da plataforma Java (Oracle JDK) para fun-
cionar. Programadores Java já o têm instalado no computador; se não é o
seu caso, dirija-se à primeira URL indicada e role a tela para encontrar o JDK
7. Infelizmente, enquanto escrevo este livro, a plataforma Android ainda não
suporta o Java 8.
7
1.2. Instalando os prerrequisitos
Casa do Código
Figura 1.3: Download do JDK 7
Após instalado o JDK, você deve configurar a variável de ambiente
JAVA_HOME (1.3), apontando para a pasta onde ele está instalado, por exem-
plo: C:\Program Files\Java\jdk1.7.0.
O SDK do Android em si pode ser baixado de duas formas: ADT
Bundle e stand-alone.
Para desenvolvimento com o Cordova CLI, us-
aremos o stand-alone (se você já desenvolve para Android com o ADT,
não precisa baixar, pode usar a SDK integrada). Para baixá-lo, clique no
link Get the SDK for an existing IDE e em seguida em Download
the stand-alone ....
8
Casa do Código
Capítulo 1. Instalando o Apache Cordova
Figura 1.4: Download do Android SDK
Após instalar o Android SDK, configure no path as subpastas tools e
platform-tools (esta última ainda a ser instalada). Inicie um terminal ou
prompt de comando (no Windows, como administrador) e digite:
Será aberto o SDK Manager, onde você terá que fazer o download dos
itens marcados nas figuras 1.5 e 1.6: Platform-tools, Build-tools e SDK Platform da API 19 do Android:
Figura 1.5: Ferramentas de build do Android SDK: tools e platform-tools
9
1.2. Instalando os prerrequisitos
Casa do Código
Figura 1.6: API Android mais recente (marque a SDK Platform)
APIs Android
No momento em que finalizo este livro, o Cordova não dá suporte à
API 20 do Android. Você pode tentar posteriormente atualizar o Cor-
dova e verificar qual SDK Platform ele está suportando.
Emuladores vs. dispositivos reais
O Android SDK possui um emulador. Para usá-lo, você deve insta-
lar pelo menos uma System Image de uma determinada API no SDK
Manager (há uma instalada na figura 1.6). No entanto, o emulador é
muito lento. Pode ter desempenho aceitável para testar apps comuns,
em máquinas com processadores rápidos e bastante memória.
No caso de jogos, procure utilizar um dispositivo real para testá-los.
10
Casa do Código
Capítulo 1. Instalando o Apache Cordova
1.3
Considerações específicas de sistema op-
eracional
Windows
Para testar jogos em dispositivos conectados no Windows, é preciso instalar
um driver USB. Há uma lista em http://developer.android.com/tools/extras/
oem-usb.html#Drivers. Após instalar o driver, procure reiniciar a máquina.
Muito provavelmente você sabe como configurar o path do sistema op-
eracional ou criar a variável de ambiente JAVA_HOME, no entanto, deixo aqui
registrados os passos:
• Clique com o botão direito em
Computador
e escolha
Propriedades.
• À esquerda da tela, escolha
Configurações Avançadas do
Sistema.
• Clique no botão Variáveis de Ambiente.
11
1.3. Considerações específicas de sistema operacional
Casa do Código
Figura 1.7: Entrando nas variáveis de ambiente do Windows
Para acrescentar uma pasta ao path:
• Procure em uma das listas a variável PATH e clique em Editar.
• Acrescente um ponto e vírgula ao final dos caminhos e insira o novo.
12
Casa do Código
Capítulo 1. Instalando o Apache Cordova
Figura 1.8: Acrescentando uma pasta no path (Apache Ant, no exemplo)
Linux
No Linux, é preciso configurar uma permissão de acesso via USB para
aparelhos de determinado fabricante:
• Conecte seu dispositivo ao computador pelo cabo USB.
• Abra o terminal e digite o comando lsusb. Procure na saída do co-
mando o seu dispositivo e anote o primeiro código hexadecimal. Ele
corresponde ao código do fabricante:
13
1.3. Considerações específicas de sistema operacional
Casa do Código
Figura 1.9: Encontrando o código do fabricante (o da Motorola é 22b8)
• Edite como administrador o arquivo:
• Insira a linha (pode ser ao final do arquivo, caso ele já exista):
Isto define uma regra dando ao sistema a permissão de acesso aos dispos-
itivos daquele fabricante.
• Mude a permissão do arquivo para 644:
No caso, o 6 representa as permissões de leitura e gravação (4 + 2) ao
usuário corrente (que é o administrador, devido ao uso do sudo); o 4, a
permissão de leitura para o grupo a que o usuário pertence; e o outro 4, a
permissão de leitura aos outros usuários.
• Mude o proprietário do arquivo para o administrador (root):
As variáveis de ambiente podem ser configuradas editando-se como ad-
ministrador o seguinte arquivo (e reiniciando a máquina depois):
14
Casa do Código
Capítulo 1. Instalando o Apache Cordova
Ao contrário do Windows, os diretórios são separados por dois-pontos:
Figura 1.10: Configuração de path e JAVA_HOME no Linux. Cada path con-
figurado manualmente é destacado com uma cor, para facilitar a visualização
Após realizar todas as edições, reinicie a máquina.
1.4
Testando a conexão com o dispositivo
Em qualquer caso (Windows ou Linux), podemos testar se o SDK consegue
“conversar” com o celular ou tablet.
Abra um prompt de comando ou terminal e dê o comando
adb
devices. Este é um comando do Android SDK que lista os aparelhos conec-
tados:
Figura 1.11: Dispositivos conectados
15
1.4. Testando a conexão com o dispositivo
Casa do Código
Se aparecerem vários pontos de interrogação ou a indicação “no permis-
sions”, confira os passos citados para seu sistema operacional. Mais detalhes podem ser obtidos em http://developer.android.com/tools/device.html, e em
http://goo.gl/42Xila podemos ver uma descrição detalhada de como config-
urar o acesso ao dispositivo móvel no Linux.
Se seu dispositivo aparece como “device” após o comando
adb
devices, abra uma cerveja! Estamos prontos para finalmente começar a
usar o Apache Cordova. Já temos as ferramentas necessárias para trazer para
o mundo mobile todas as experiências que somos capazes de criar com o
HTML5. Para dar continuação ao processo, já no próximo capítulo, você
aprenderá a criar projetos e rodá-los em seu celular ou tablet, e poderá ver
animações em Canvas funcionando. Em breve nossos esforços se transfor-
marão em jogos reais e divertidos.
Sugestão de leitura
Apache Cordova 3 Programming, de John M. Wargo ([3]).
16
Capítulo 2
Criando o primeiro projeto
É hora de tirar a prova dos nove: vamos usar a interface de linha de comando
(CLI) do Cordova para criar um pequeno projeto HTML5. Se algo der errado,
certifique-se de que todas as ferramentas instaladas estejam configuradas no
path do sistema, conforme descrito exaustivamente no capítulo anterior.
2.1
O projeto Cordova
No Prompt de Comando do Windows, ou no Terminal do Linux, navegue até
a pasta onde você pretende salvar os códigos deste livro. Estando nessa pasta, dê o comando:
cordova create primeiro
O Cordova cria a pasta primeiro dentro da atual. Vamos abri-la:
2.2. O arquivo config.xml
Casa do Código
Os próximos comandos atuarão no projeto configurado nesta pasta. Va-
mos adicionar a plataforma Android:
Vale a pena darmos uma olhada na estrutura de pastas do projeto.
Destaque para o arquivo config.xml, a ser descrito na próxima seção, para
a pasta www, onde fica o projeto web, e platforms, onde o Cordova cria
projetos nativos para os SDKs das plataformas adicionadas:
Figura 2.1: Estrutura de pastas de um projeto Cordova
Nosso trabalho será praticamente todo feito na pasta www, pois é lá que
deverão ser salvos os arquivos HTML, CSS e JavaScript de nossos apps e jogos.
Recomenda-se não editar diretamente o conteúdo da pasta platforms, pois
este é sempre re-gerado automaticamente a cada execução do projeto. Nada
impede, porém, que copiemos o projeto Android nativo para outro local e o
o abramos no ADT, por exemplo.
2.2
O arquivo config.xml
Neste arquivo, configuramos algumas propriedades do projeto usadas na ger-
ação da app nativa. Destaque para o atributo id da tag <widget>, onde
definimos o pacote, nome que identificará nosso app de forma exclusiva.
Editei o arquivo como a seguir; sinta-se livre para colocar os seus próprios
dados:
18
Casa do Código
Capítulo 2. Criando o primeiro projeto
<?xml version='1.0' encoding='utf-8'?>
<widget id="br.com.casadocodigo.livrojogoshtml2.primeiro"
version="0.0.1"
xmlns="http://www.w3.org/ns/widgets"
xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Primeiro Projeto Cordova</name>
<description>
Primeiro teste de empacotamento de páginas HTML
em apps móveis.
</description>
<author email="[email protected]"
href="http://gamecursos.com.br">
Éderson Cássio
</author>
<content src="index.html" />
<access origin="*" />
</widget>
Uma vez publicado o app na Play Store, o nome do pacote não de-
verá mais mudar.
A convenção, já conhecida por programadores Java,
é usar a URL reversa, colocando ao final o nome que identifica o app
ou jogo. Por exemplo:
br.com.casadocodigo.livrojogoshtml2,
onde
br.com.casadocodigo corresponderia ao seu URL na web, e
livrojogoshtml2 identifica este livro. Vamos continuar colocando mais
pontos e novos nomes, para identificar cada joguinho criado aqui.
19
2.3. Configurando o Android para depuração USB
Casa do Código
Ícone do aplicativo
No config.xml você pode definir o ícone de um aplicativo através
da tag <icon>:
<icon src="res/icon.png" />
Mais detalhes podem ser obtidos em http://goo.gl/Q6Nmz3.
2.3
Configurando o Android para depuração
USB
Se você já desenvolve para Android, sinta-se livre para pular esta parte.
Por razões de segurança, não se pode sair copiando aplicativos para os
aparelhos. Para desenvolvimento, temos que habilitar a Depuração USB:
• Procure nas configurações de seu aparelho a opção Desenvolvedor
ou Programador, conforme o modelo.
Habilitando as opções de desenvolvedor no Android
4.2 e posteriores
A partir do Android 4.2, a opção
Desenvolvedor das
Configurações está oculta. Para fazê-la aparecer, entre em Sobre o
telefone, nas configurações, e toque sete vezes a opção Número da
versão (figura 2.2).
• Habilite o modo desenvolvedor e a depuração USB (figura 2.3).
20
Casa do Código
Capítulo 2. Criando o primeiro projeto
Figura 2.2: Habilitando as opções de desenvolvedor a partir do Android 4.2
21
2.4. Rodando o projeto padrão
Casa do Código
Figura 2.3: Habilitando a depuração USB
2.4
Rodando o projeto padrão
O Cordova já inseriu alguns arquivos na pasta www. Vamos rodar o projeto
para nos certificarmos de que está tudo ok.
• Conecte o aparelho. A partir do Android 4.2.2, a cada computador
diferente conectado, aparecerá no display um pedido de autorização
para uma chave RSA específica para aquele computador:
22
Casa do Código
Capítulo 2. Criando o primeiro projeto
Figura 2.4: Confirmação para depuração USB a partir do Android 4.4.2
• Abra o prompt ou terminal e entre na pasta de seu projeto:
cd primeiro
• Mande rodar o projeto:
cordova run android
O primeiro build leva alguns minutos. Se tudo estiver ok e o Android
SDK conseguir comunicar-se com o dispositivo, você verá no terminal a men-
sagem LAUNCH SUCCESS e o projeto padrão do Cordova rodando no display.
Nesta altura, como foram muitas configurações, pode ser que ocorram erros.
Certifique-se de que todas as ferramentas utilizadas pelo Cordova listadas na
seção 1.2 tenham sido instaladas, reveja as considerações sobre os sistemas
operacionais (1.3) e garanta que os paths estejam devidamente configurados,
conforme a figura 1.10. Pode ser que, após alguma instalação ou configuração,
seja preciso reiniciar o computador.
23
2.5. Um pequeno projeto do zero: animação com o Canvas
Casa do Código
Figura 2.5: Projeto padrão do Cordova rodando no dispositivo móvel
2.5
Um pequeno projeto do zero: animação com
o Canvas
A página inicial
Vamos apagar os arquivos presentes na pasta www e começar recriando o
index.html. Este representa a página inicial do projeto. Note as tags <meta name="viewport"...> e <script src="cordova.js">:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Primeiro Projeto</title>
<meta name="viewport"
content="width=device-width, height=device-height,
user-scalable=no, initial-scale=1,
maximum-scale=1, minimum-scale=1">
<script src="cordova.js"></script>
<script>
// Aqui vai nosso tratamento do Canvas
</script>
24
Casa do Código
Capítulo 2. Criando o primeiro projeto
O viewport é conhecido de quem já trabalha com webdesign responsivo.
Trata-se do tamanho do conteúdo. Se não estiver definido, o dispositivo trata
a página como sendo para desktop, com um viewport maior que 900 pixels
(o valor padrão varia entre os dispositivos), como uma forma de lidar com
páginas não preparadas para as telas pequenas. É por isso que vemos muitos
sites totalmente comprimidos na minúscula tela e temos que fazer a pinça
(zoom) a todo momento:
Figura 2.6: Um site para desktop aparece minúsculo no dispositivo móvel
(imagem editada para suprimir anúncios)
Em
nosso
caso,
os
atributos
width=device-width
e
height=device-height estão definindo as dimensões do viewport
no tamanho reportado pelo aparelho. Os atributos usados são:
• width: largura do viewport;
• height: altura do viewport;
25
2.5. Um pequeno projeto do zero: animação com o Canvas
Casa do Código
• user-scalable: define se o usuário pode dar zoom. Para jogos, em
muitos casos é interessante setar como no (sem zoom), mas isso de-
penderá das características do jogo;
• initial-scale: zoom inicial. O valor 1 define o zoom “normal” (100%).
Valores abaixo (ex.: 0.5) reduzem a visão da página, e valores acima
(ex.: 2) ampliam;
• maximum-scale: zoom máximo permitido;
• minimum-scale: zoom mínimo permitido.
Definimos aqui características bastante restritivas para o zoom. Para sites,
isto é totalmente contra os bons princípios de usabilidade, mas aqui estamos
falando de jogos (ou apps) em geral. Nesses casos, permitir ou não o zoom,
em que quantidade e em que situações, são decisões do projeto.
Temos também o script cordova.js, cujo arquivo... não existe no pro-
jeto! Ele estará acessível somente no webview do Cordova. Enquanto não
usarmos nenhuma API específica do Cordova, poderemos até mesmo testar
nosso jogo no browser do desktop e ignorar este arquivo. Porém, a partir
do momento em que chamamos APIs do Cordova, estas só rodarão em seu
ambiente, e só poderemos testá-las com um aparelho móvel ou emulador.
Iniciando a API
Para aguardar a página carregar no navegador, nós fazemos o seguinte
script (pode incluí-lo na tag <script> vazia):
window.onload = function() {
// Nosso código de inicialização
}
No Cordova, ainda temos de aguardar o carregamento de suas APIs. O
ambiente nos fornece o evento deviceready:
window.onload = function() {
document.addEventListener('deviceready', iniciar);
}
26
Casa do Código
Capítulo 2. Criando o primeiro projeto
function iniciar() {
// Nosso código de inicialização
}
Mas não temos esse evento no browser, caso queiramos testar. Bastaria
chamar a função iniciar diretamente no evento onload:
//document.addEventListener('deviceready', iniciar);
iniciar();
Quando não estamos usando APIs móveis específicas, isso pode nos per-
mitir depurar melhor o jogo (através do console do Google Chrome ou do
plugin Firebug do Firefox, por exemplo). Lógicas de Canvas ou de engines
físicas (como a Box2dWeb, que aprenderemos na Parte 2 do livro) não neces-
sitam necessariamente de dispositivos móveis para rodar e podem ser depu-
radas em ambiente desktop. Entradas simples via teclado ou mouse podem
ser usadas para simular eventos chave a serem testados.
E vamos para a inicialização: obtenção do Canvas e seu contexto grá-
fico, disparo do loop de animação... Façamos algo básico e conhecido por
enquanto, o complexo problema matemático da bolinha quicando:
var canvas, context;
var x, y, raio = 10;
var vX = 10, vY = -20; // Velocidades
window.onload = function() {
//document.addEventListener('deviceready', iniciar);
iniciar();
}
function iniciar() {
canvas = document.getElementById('primeiro');
context = canvas.getContext('2d');
// Começar no meio da tela
x = canvas.width / 2;
y = canvas.height / 2;
27
2.6. Um polyfill para requestAnimationFrame
Casa do Código
requestAnimationFrame(animar);
}
Estamos disparando um loop de animação com a função animar, por-
tanto vamos criá-la:
function animar() {
// Não deixar passar das bordas
// Como x e y são o centro, descontamos o raio
// Se passou do limite, invertemos o sentido da velocidade
if (x < raio || x > canvas.width - raio)
vX *= -1;
if (y < raio || y > canvas.height - raio)
vY *= -1;
x += vX;
y += vY;
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x, y, raio, 0, Math.PI*2);
context.fill();
requestAnimationFrame(animar);
}
Se abrirmos a página no browser, já veremos a bolinha quicando. No celu-
lar ou tablet, pode ser que sim, pode ser que não. Por quê? Veja na próxima
seção.
2.6
Um polyfill para requestAnimationFrame
O suporte para o requestAnimationFrame em dispositivos móveis é
bem recente. O browser do Android somente o aceita a partir da versão 4.4
(http://caniuse.com/requestanimationframe) ! Um jogo hospedado na web,
28
Casa do Código
Capítulo 2. Criando o primeiro projeto
acessado pelo Chrome ou Firefox devidamente atualizados, pode rodar nor-
malmente. As aplicações Cordova, no entanto, usam o webview padrão do
sistema operacional. No Safari do iOS, o suporte já existia há algumas ver-
sões atrás, mas também é recente.
Nesses casos, a solução é usar um polyfill: uma implementação JavaScript que tenta emular o comportamento de uma funcionalidade recente e ainda
não completamente suportada. No caso do requestAnimationFrame, o
velho setTimeout pode ser usado para construir um polyfill.
Um exemplo simples poderia ser como o mostrado a seguir. Primeiro
checamos a existência, por ordem de prioridade, da especificação padrão
requestAnimationFrame ou das versões prefixadas com webkit, moz
ou ms (específicas de navegador, de quando não tínhamos uma especificação
definida). Usamos para isso o ou lógico ( ||), que fará retornar a primeira referência que for diferente de null. Caso nenhuma delas exista (todas
null), retornamos uma função construída usando o setTimeout, que tenta
simular 60 frames por segundo:
window.requestAnimationFrame =
// Especificação padrão
window.requestAnimationFrame
||
// Chrome, Safari e outros
window.webkitRequestAnimationFrame ||
// Firefox
window.mozRequestAnimationFrame
||
// IE
window.msRequestAnimationFrame
||
// Função substituta
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
Dividindo 1 segundo (1000ms) por 60, nosso polyfill tentará chegar perto
dos 60fps, tanto quanto a precisão do setTimeout permitir. Isso foi só uma
forma mais compacta de se fazer:
// Exemplo teórico
var funcao;
29
2.6. Um polyfill para requestAnimationFrame
Casa do Código
if (window.requestAnimationFrame)
funcao = window.requestAnimationFrame;
else if ( ... ) // etc.
window.requestAnimationFrame = funcao;
Erik Möller, alemão vice-diretor da Wikimedia Foundation e desenvolve-
dor, criou um polyfill bastante robusto, que pode ser obtido em https://gist.
github.com/paulirish/1579671. Este polyfill mede o tempo entre o frame an-
terior e o atual, e desconta o atraso para a próxima chamada. Salve o código
do polyfill na pasta www e faça o link com a página na seção <head>:
<script src="rAF.js"></script>
E troque a chamada à função iniciar:
document.addEventListener('deviceready', iniciar);
//iniciar();
Agora você pode fazer novamente:
cordova run android
A animação funcionará em qualquer aparelho Android.
Figura 2.7: Animação com Canvas e requestAnimationFrame na forma de
app Android
30
Casa do Código
Capítulo 2. Criando o primeiro projeto
2.7
Gerar pacote APK assinado para o Android
Hora de gerar os arquivos .apk, os pacotes de aplicativos Android. Estes
pacotes devem ser assinados para que possam ser publicados na PlayStore ou
instalados diretamente nos aparelhos.
Gerando uma chave de assinatura
Primeiro devemos gerar uma chave de assinatura, usando o co-
mando keytool, presente na JDK. Esta chave será salva em um arquivo
.keystore, que pode conter uma ou mais chaves. Siga os passos:
• Abra o terminal ou prompt em uma pasta separada do projeto (pode
ser na pasta padrão do usuário, onde o prompt começa).
• Dê o comando para criar a chave. Aqui eu quebrei em várias linhas para
facilitar, mas você deverá digitar tudo em uma única linha e substituir
“ederson-cassio” por seu nome ou empresa, naturalmente. Neste caso, a
chave possuirá o alias (nome) ederson_cassio, será armazenada no
arquivo ederson-cassio.keystore e sua validade será de 10000
dias (aproximadamente 27 anos, mais que o suficiente):
keytool
-genkey -v
-keystore ederson-cassio.keystore
-alias ederson_cassio
-keyalg RSA
-keysize 2048
-validity 10000
• Informe a senha da keystore (será pedida confirmação, em caso de nova
keystore).
• Informe os dados de identificação pedidos (seu nome, empresa, local-
idade). A chave será gerada em seguida.
• Ao final, pede-se a senha da nova chave. Você pode apenas dar Enter
se quiser usar a mesma senha da keystore.
31
2.7. Gerar pacote APK assinado para o Android
Casa do Código
Gerando o APK para distribuição
Agora vá para a pasta do projeto e dê o comando:
cordova build android --release
Será gerado o arquivo PrimeiroProjetoCordova-release-unsigned.apk,
na subpasta platforms/android/ant-build. Também estão ali os
APKs de debug gerados anteriormente, que só podem ser usados nos
aparelhos com depuração USB ativada.
Assinando o APK
Para facilitar, mova o arquivo PrimeiroProjetoCordova-release-
unsigned.apk para a pasta onde está a keystore. Você poderá renomeá-lo
para algo mais significativo, como MeuJogo.apk.
De volta ao prompt, na pasta inicial, podemos assinar o arquivo através
do comando a seguir. Perceba que os três últimos parâmetros referem-se ao
arquivo .keystore, ao arquivo .apk e ao alias armazenado:
jarsigner
-verbose -sigalg SHA1withRSA -digestalg SHA1
-keystore ederson-cassio.keystore
MeuJogo.apk
ederson_cassio
Basta informar a senha da keystore e o arquivo será assinado.
Instalando o APK diretamente no aparelho
Para poder instalar um aplicativo diretamente do APK, é necessário con-
figurar o Android para aceitar apps de fontes desconhecidas:
• Entre nas Configurações e, em seguida, em Segurança.
• Role até encontrar a opção Fontes desconhecidas e marque-a:
32
Casa do Código
Capítulo 2. Criando o primeiro projeto
Figura 2.8: Configurando o Android para aceitar APKs estranhos
O Android dará um aviso de segurança. Recomendo que você não o ig-
nore; após instalar o aplicativo, procure desativar essa opção.
Figura 2.9: Não diga que não foi avisado...
• Copie o arquivo .apk para o aparelho e localize-o. Pode ser que você
tenha que instalar um gerenciador de arquivos. No meu celular, estou
usando o ES File Explorer, que é gratuito:
33
2.7. Gerar pacote APK assinado para o Android
Casa do Código
Figura 2.10: Procurando o arquivo APK
Permissões do Android
Quem é usuário do Android sabe que, para instalar um aplicativo ou
jogo, este requer um certo número de permissões do sistema operacional.
O Cordova, por padrão, coloca no projeto um pedido de permissão para
acesso à internet. Se seu jogo utilizar apenas recursos locais, podemos
remover esta permissão:
• Na
subpasta
platforms/android,
abra
o
arquivo
AndroidManifest.xml
• Remova a seguinte tag:
<uses-permission
android:name="android.permission.INTERNET" />
Muito cuidado, porém: a instalação de plugins do Cordova, que
aprenderemos em 4.3, poderá adicionar pedidos de diversas permissões
requeridas pelas APIs. Não remova os novos requerimentos que apare-
cerem, ou as APIs não irão funcionar. Você pode colocar de volta a per-
missão para internet caso tenha constatado alguma falha.
34
Casa do Código
Capítulo 2. Criando o primeiro projeto
2.8
Considerações finais
É muita coisa técnica! Mas isso tudo é necessário para termos nossos jogos
na forma de apps Android. Dominando todo este ciclo de desenvolvimento,
já podemos nos concentrar em criar coisas mais divertidas. No próximo capí-
tulo, brincaremos com a interação por toque.
O que ocorre é que o uso do HTML5 é uma tendência recente, porém em
crescimento, para o desenvolvimento de aplicativos em diversas plataformas
(como é o caso do Windows 8 e do Firefox OS). Ainda não há IDEs e fer-
ramentas de produtividade. Aplicações híbridas (como as geradas pelo Cor-
dova) tendem a ser mais lentas que as nativas, em especial nos aparelhos mais
simples, mas a potência deles só tende a aumentar. A ideia de usar o HTML5
para alcançar qualquer plataforma está se firmando.
Documentação do Cordova
Para aprofundar seus estudos, deixo-lhe o link para a documentação
do projeto:
http://cordova.apache.org/docs/en/3.5.0/
Lá você poderá aprender sobre as opções da CLI, configurações do
config.xml, APIs, plugins e muito mais.
35
Capítulo 3
Eventos touch
Vamos tratar agora da especificação do HTML5 para eventos touch. Existem
frameworks para auxiliar no tratamento de gestos complexos e usuais (mais
adiante, no capítulo 5, abordarei o Hammer.js), que são bastante trabalhosos de tratar. Ainda assim, devemos saber como tratar pelo menos as interações
básicas com o HTML5 “cru”.
A especificação define os seguintes eventos:
• touchstart: início do toque;
• touchmove: usuário move o dedo na tela a partir do objeto que rece-
beu o evento;
• touchleave: o dedo abandona a área do objeto;
• touchenter: o dedo entra de volta na área do objeto;
3.1. Arrastando um objeto
Casa do Código
• touchend: fim do toque (usuário solta o dedo);
• touchcancel: o cancelamento do toque ocorre se a área da página sair
do foco de interação (por exemplo, se surgir uma caixa de alerta).
3.1
Arrastando um objeto
É a base de interação em múltiplos jogos para telas touch (quem nunca jogou
Angry Birds ou Pou?). Com o auxílio dos eventos touchstart, touchmove e touchend, podemos obter as coordenadas da tela onde o toque ocorreu,
verificar se o objeto se encontra por ali e movê-lo.
Preparei uma pequena spritesheet para controlarmos com o dedo. Em
http://www.iconarchive.com/ e muitos outros sites você pode obter diversos
ícones, a partir dos quais você poderá montar spritesheets usando um editor
de imagens:
Figura 3.1: É um pássaro? É um avião? Use sua imaginação!
O esqueleto
Crie um novo projeto Cordova:
cordova create touch
Adicione a plataforma Android:
cd touch
cordova platform add android
38
Casa do Código
Capítulo 3. Eventos touch
Coloque o seguinte esqueleto no index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Arrastando Objeto Via Touch</title>
<meta name="viewport" content="width=device-width,
height=device-height,user-scalable=no,
initial-scale=1,maximum-scale=1,minimum-scale=1">
<script src="cordova.js"></script>
<script src="rAF.js"></script>
<script>
// Código do app
</script>
</head>
<body>
<canvas id="touch" width="320" height="240"></canvas>
</body>
</head>
Façamos a inicialização básica discutida no capítulo anterior. Quando
tudo estiver pronto, você deverá trocar a chamada direta à função iniciar
pelo evento deviceready do Cordova:
var canvas,context;
window.onload = function() {
// Troque as linhas quando for fazer deploy no aparelho
//document.addEventListener('deviceready',iniciar);
iniciar();
}
function iniciar() {
canvas = document.getElementById('touch');
context = canvas.getContext('2d');
39
3.1. Arrastando um objeto
Casa do Código
// inicializações virão aqui
requestAnimationFrame(animar);
}
A função de animação, primeiramente, cuidará de limpar a tela e chamar
o próximo ciclo:
function animar() {
context.clearRect(0, 0,canvas.width,canvas.height);
// desenhos virão aqui
requestAnimationFrame(animar);
}
Antes de continuarmos, abra a página no browser e verifique se não
aparecem erros no Console só para garantir que tudo foi digitado correta-
mente: nomes de variáveis, parênteses etc. O browser dará falta do arquivo
cordova.js, mas este erro pode ser ignorado até o momento em que for
necessário usar alguma API própria do Cordova.
Posicionando e desenhando
Vamos definir agora a posição do objeto. Inicialmente desenharemos um
círculo simples para testar os eventos de touch e, em seguida, o substituiremos pela bruxa.
Declare as variáveis x, y e raio junto com as outras globais da página:
var canvas,context;
var x,y,raio = 32;
Na função iniciar, comece posicionando o objeto no centro do Canvas
e configurando os eventos touchstart, touchmove e touchend:
function iniciar() {
canvas = document.getElementById('touch');
context = canvas.getContext('2d');
40
Casa do Código
Capítulo 3. Eventos touch
x = canvas.width / 2;
y = canvas.height / 2;
canvas.addEventListener('touchstart', touchStart);
canvas.addEventListener('touchmove', touchMove);
canvas.addEventListener('touchend', touchEnd);
requestAnimationFrame(animar);
}
Desenhe o círculo em animar:
function animar() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.save();
context.fillStyle = 'red';
context.strokeStyle = 'blue';
context.lineWidth = 2;
context.beginPath();
context.arc(x, y, raio, 0, Math.PI*2);
context.fill();
context.stroke();
context.restore();
requestAnimationFrame(animar);
}
Programação dos eventos
Vamos agora capturar a posição onde o toque iniciou, na função
touchStart.
O parâmetro de evento recebido possui o atributo
changedTouches, que é um array representando todos os dedos atualmente
na tela. Os atributos pageX e pageY nos dão a posição do toque na página, não relativamente ao Canvas:
41
3.1. Arrastando um objeto
Casa do Código
function touchStart(e) {
console.log('x: ' + e.changedTouches[0].pageX +
', y: ' + e.changedTouches[0].pageY);
}
function touchMove(e) {
}
function touchEnd(e) {
}
Neste ponto, faça outro teste no navegador: você deverá ver uma bola
vermelha, de borda azul (use as cores que desejar).
Os atributos de posição do changedTouches são:
• pageX e pageY: posição relativa à dimensão completa da página (viewport), considerando a posição de rolagem;
• clientX e clientY: posição relativa à área de renderização do browser (sem considerar a rolagem);
• screenX e screenY: posição relativa à tela do computador ou dispositivo.
Como você deve ter percebido, os eventos touch não nos dão a posição
exata de toque na área do Canvas, que é o que precisamos. Mas todo elemento de uma página HTML possui os atributos offsetLeft e offsetTop, que
nos dão a posição atual de um elemento dentro do viewport. Se nosso Canvas
se encontra na posição (10, 10) da página, por exemplo, o toque em (10, 10) na tela deve corresponder a (0, 0) no Canvas. Ou seja, basta subtrair os offsets: 42
Casa do Código
Capítulo 3. Eventos touch
Figura 3.2: Subtraindo os offsets e obtendo a posição de toque dentro de um
elemento
Dessa forma, podemos definir as coordenadas do objeto:
function touchStart(e) {
x = e.changedTouches[0].pageX - canvas.offsetLeft;
y = e.changedTouches[0].pageY - canvas.offsetTop;
console.log('x: ' + x + ', y : ' + y);
}
Vamos verificar se o toque ocorreu na área do círculo. Para isso, contamos
com a ajuda do nosso velho conhecido Teorema de Pitágoras (figura 3.3). A
distância em linha reta entre o ponto do toque e a posição atual do círculo corresponde à hipotenusa ( a). As distâncias horizontal e vertical correspondem aos catetos ( b e c):
43
3.1. Arrastando um objeto
Casa do Código
Figura 3.3: Triângulo retângulo formado com a distância entre o ponto tocado
e o círculo
Se a hipotenusa for menor ou igual ao raio, tocamos dentro do círculo!
Figura 3.4: Posição de toque dentro da área do círculo: a hipotenusa é menor
que o raio
Vamos ao código. Usamos Math.abs para obter as distâncias em valores
absolutos, pois não importa qual posição é anterior ou posterior a qual ( x -
xToque pode ser negativo se x < xToque). Se o toque foi na área do círculo, sinalizamos que o movimento começou (variável arrastando) e mudamos
as variáveis x e y:
function touchStart(e) {
var xToque = e.changedTouches[0].pageX - canvas.offsetLeft;
44
Casa do Código
Capítulo 3. Eventos touch
var yToque = e.changedTouches[0].pageY - canvas.offsetTop;
var distanciaX = Math.abs(x - xToque);
var distanciaY = Math.abs(y - yToque);
// Pitágoras
if (
distanciaX*distanciaX + distanciaY*distanciaY <= raio*raio) {
arrastando = true;
x = xToque;
y = yToque;
}
}
Temos que declarar a variável arrastando como global:
var canvas, context;
var x, y, raio = 32;
var arrastando;
Para finalizar, na função touchMove, verificamos se estamos arrastando
a bolinha e atualizamos a posição, igual feito anteriormente. Em touchEnd,
mudamos a variável arrastando para false, indicando que a bola não
deve mais se mover:
function touchMove(e) {
if (arrastando) {
x = e.changedTouches[0].pageX - canvas.offsetLeft;
y = e.changedTouches[0].pageY - canvas.offsetTop;
}
}
function touchEnd(e) {
arrastando = false;
}
Vamos testar no navegador. O Google Chrome possui uma forma de em-
ular dispositivos móveis em suas dimensões e eventos touch. Siga os passos:
• Tecle Ctrl + Shift + J para abrir o Console.
45
3.2. Controlando a direção
Casa do Código
• Com o foco no Console, tecle Esc. Será aberto um novo conjunto de
guias.
• Na guia Emulation, selecione um modelo pré-configurado qualquer e
clique em Emulate.
• Atualize a página ( F5).
Figura 3.5: Emulando dispositivos móveis no Google Chrome
Você pode agora provocar eventos de toque com o mouse no Chrome!
Infelizmente, no Android ainda não vai funcionar, mas é por causa de um
bug. Acrescente as seguintes linhas ao evento touchstart:
Pode fazer cordova run android e puxar a bolinha com o dedo!
3.2
Controlando a direção
Para usarmos adequadamente a spritesheet da figura 3.1, precisamos contro-
lar a direção para onde ocorrem os movimentos, se para a direita ou para a
esquerda.
46
Casa do Código
Capítulo 3. Eventos touch
O evento touchmove é gerado continuamente conforme nos movimen-
tamos. Para definir se vamos para a direita ou para a esquerda, temos que
guardar a posição no último evento, e comparar com a posição do evento at-
ual. Declare as variáveis xAnterior e yAnterior:
var canvas, context;
var x, y, raio = 32;
var arrastando;
var xAnterior, yAnterior;
Guarde nelas a posição inicial do movimento:
function touchStart(e) {
// ...
// Pitágoras
if (
distanciaX*distanciaX + distanciaY*distanciaY <= raio*raio) {
arrastando = true;
x = xToque;
y = yToque;
xAnterior = x; // Guardar a posição
yAnterior = y;
}
// ...
}
E, ao mudar a posição, guarde os valores anteriores:
function touchMove(e) {
if (arrastando) {
xAnterior = x; // Guardar a posição
yAnterior = y;
x = e.changedTouches[0].pageX - canvas.offsetLeft;
y = e.changedTouches[0].pageY - canvas.offsetTop;
}
}
47
3.2. Controlando a direção
Casa do Código
Usando a spritesheet
No pacote de download, no projeto da pasta touch, está presente o ar-
quivo bruxa.png, uma spritesheet com duas imagens 96x96 (192x96 no to-
tal).
Sabendo as posições atual e anterior, a cada disparo do touchmove,
podemos desenhar a figura certa da spritesheet. Vamos mudar a função
animar para chamar outra,
desenharBruxa, que cuidará do clipping
(recorte):
function animar() {
context.clearRect(0, 0, canvas.width, canvas.height);
desenharBruxa();
requestAnimationFrame(animar);
}
Nessa nova função, consideraremos x e y, definidos nos eventos de
touch, como o centro da imagem. Desta forma, descontamos metade da
largura e da altura de destino ao posicionar. Cada bruxa na imagem pos-
sui 96x96 pixels, mas vamos renderizá-la menor, em 64x64. Para definir a
posição do clipping, comparamos as variáveis x e xAnterior, e assim sabe-
mos se estamos indo para a direita ou para a esquerda:
function desenharBruxa() {
var largOrigem = 96, largDestino = 64;
var xOrigem = (x >= xAnterior)
? 0
// Direita
: 96; // Esquerda
context.drawImage(imgBruxa,
xOrigem, // x na spritesheet
0,
// y na spritesheet
largOrigem,
largOrigem,
x - largDestino/2, // x no Canvas
y - largDestino/2, // y no Canvas
largDestino,
largDestino);
}
48
Casa do Código
Capítulo 3. Eventos touch
Qualquer trabalho análogo na vertical poderia ser feito comparando y
com yAnterior.
Precisamos carregar a imagem. Altere a inicialização do app para ficar
como segue. Para facilitar agora, podemos manter o raio, deixando a área de
touch circular (mas podemos aumentá-lo um pouco para dar mais conforto):
Rode o app no dispositivo e leve a bruxinha de um lado para o outro:
Figura 3.6: E lá vamos nós!
49
3.3. Toque em Canvas responsivo
Casa do Código
3.3
Toque em Canvas responsivo
Nosso Canvas está com tamanho fixo, podendo faltar espaço para ele nos dis-
positivos menores, ou sobrar espaço na página, nos maiores. É interessante
podermos usar um pouco de CSS para tornar nosso Canvas responsivo, ou
seja, adaptado à tela do dispositivo. Também, como estamos criando apps
que rodam localmente, não é nada mau termos imagens um pouco maiores,
sendo reduzidas conforme necessário (já que não será feito download delas).
Mas temos um pequeno problema. A imagem do Canvas sempre é pro-
cessada com as dimensões informadas na tag. Em um Canvas responsivo,
ela aparece redimensionada. Ao tocar ou clicar, os eventos reportam a posição
relativa ao tamanho renderizado em tela, mas na imagem real essas posições
sofrem deslocamento, exigindo a conversão entre tamanho da renderização e
tamanho da imagem real.
Por mais complicado que pareça, resolver isso é muito simples. Basta fazer
uma regra de três. Por exemplo, se o Canvas está sendo renderizado com 960
pixels de largura e eu toco na posição 450 (em x), em qual posição da imagem real eu toquei, se a tag indica 800?
Tamanho
Posição
960
===>
450 (renderizado)
800
===>
x
(real)
x = 800 * 450 / 960
O mesmo raciocício se aplica à posição y.
É fácil descobrir o tamanho com que um elemento da página HTML é
renderizado em tela: basta ler os atributos offsetWidth e offsetHeight.
Assim, podemos ter uma função que recebe as coordenadas capturadas pelos
eventos e as transformam em coordenadas da imagem real, através do cálculo
descrito:
function converterParaCanvas(xToque, yToque) {
return {
x: canvas.width * xToque / canvas.offsetWidth,
y: canvas.height * yToque / canvas.offsetHeight
}
}
50
Casa do Código
Capítulo 3. Eventos touch
Devemos, também, alterar as funções que capturam eventos para chamar
a função de conversão. Modifique touchStart:
function touchStart(e) {
var posicao = converterParaCanvas(
e.changedTouches[0].pageX - canvas.offsetLeft,
e.changedTouches[0].pageY - canvas.offsetTop
);
var xToque = posicao.x;
var yToque = posicao.y;
// O restante continua igual
}
E touchMove:
function touchMove(e) {
if (arrastando) {
xAnterior = x; // Guardar a posição
yAnterior = y;
var posicao = converterParaCanvas(
e.changedTouches[0].pageX - canvas.offsetLeft,
e.changedTouches[0].pageY - canvas.offsetTop
);
x = posicao.x;
y = posicao.y;
}
}
Dimensionando o Canvas
Agora podemos dimensionar o Canvas de forma que fique responsivo,
sem causar problemas. Para isso, coloque uma tag <style> na seção
<head> (ou crie um link para um arquivo .css).
Na folha de estilo, defina o corpo da página ( body) como ocupando toda
a área disponível do browser (ou webview, em nosso caso), pois ela servirá
como uma referência para o Canvas se dimensionar:
51
3.3. Toque em Canvas responsivo
Casa do Código
body {
margin: 0;
width: 100%;
height: 100%;
}
Vamos dimensionar o Canvas, primeiramente, tendo em mente a posição
retrato (aparelho em pé), ou seja, com a largura menor que a altura. Ocu-
pamos toda a largura ( width: 100%;) e mantemos a altura na proporção
para a imagem não ficar distorcida ( height: auto;):
/* Padrão (largura < altura) */
canvas {
width: 100%;
height: auto;
display: block;
}
Para a posição paisagem (deitado), faremos uma media query (consulta
do dispositivo) que determina uma razão mínima entre a largura e a altura.
Acima dessa razão, valerá a nova formatação. Esta razão deve ser passada na
forma de fração, por exemplo:
• 2/1 indica que a largura é o dobro da altura;
• 1/2 indica o contrário, que a largura é a metade da altura;
• 1/1 indica uma área quadrada (ambos os lados iguais).
Qualquer fração pode ser usada (como 322/423), mas proporções sim-
ples são mais fáceis de mentalizar. Tudo que for abaixo de 1/1 é retrato, e
acima é paisagem. Como nossa formatação padrão trata o layout em retrato,
vamos estabelecer o mínimo de 1/1 para a formatação de paisagem, através
do atributo min-aspect-ratio da media query:
/* A partir de 1/1 (iguais ou largura > altura) */
@media (min-aspect-ratio: 1/1) {
}
52
Casa do Código
Capítulo 3. Eventos touch
Dentro da media query, basta fazer o contrário da formatação padrão:
ocupar 100% da altura e ajustar a largura proporcionalmente. Também pode-
mos dar uma margem automática para centralizar o Canvas:
/* A partir de 1 (iguais ou largura > altura) */
@media (min-aspect-ratio: 1/1) {
canvas {
height: 100%;
width: auto;
margin: 0 auto;
}
}
Também
podemos
adicionar
background-color
ou
background-image para compor o cenário, fica a seu critério. Vamos
brincar um pouco!
Figura 3.7: Canvas se ajustando ao tamanho do dispositivo
53
3.3. Toque em Canvas responsivo
Casa do Código
Webdesign responsivo e mobile
Embora seja importante na criação de jogos, não é o objetivo aqui
ficar falando sobre webdesign responsivo; caso queira saber em detalhes,
você pode ler o livro de Tárcio Zemel ([4]):
http://www.casadocodigo.com.br/products/
livro-web-design-responsivo
Também recomendo muito a leitura do A Web Mobile, de Sérgio
Lopes ([2]):
http://www.casadocodigo.com.br/products/livro-web-mobile
Ambos foram fontes de referência fundamentais para este livro.
54
Capítulo 4
Incrementando o jogo de nave
Este capítulo é dedicado a transformar o jogo construído no meu primeiro
livro em aplicação móvel. Você aprenderá a criar um controle direcional
suave para touchscreen e conhecerá as APIs do Cordova para multimídia e
acelerômetro.
Caso você não o conheça, pode jogá-lo agora, em seu desktop ou note-
book, em: http://gamecursos.com.br/livro/jogo. É um jogo de nave e tiro,
onde sua missão é destruir discos voadores e não se deixar ser atingido.
Os arquivos-fonte desse jogo podem ser obtidos em:
http://github.com/EdyKnopfler/games-js/archive/master.zip
Dentro do zip, as pastas estão numeradas pelos capítulos. Procure pegar
os arquivos do último capítulo (pasta 09), pois estão em suas versões finais.
4.1. Revisão das classes do jogo
Casa do Código
4.1
Revisão das classes do jogo
O jogo de nave possui um pequeno game engine composto pelas seguintes
classes de utilidade geral:
• Animacao: o coração do game engine. Realiza o game loop e interage
com os sprites e processamentos gerais associados a ele.
• Teclado: realiza a captura de comandos pelo teclado. Não será usado
aqui; em seu lugar criaremos a classe Direcional.
• Colisor: detecta a colisão entre os sprites associados a ele.
• Spritesheet: anima folhas de sprites, dividindo a imagem em
quadros de igual tamanho e selecionando o quadro atual de acordo com
o tempo e a velocidade da animação.
Já as seguintes classes eram específicas do jogo de nave, mas com o obje-
tivo de demonstrar como os sprites trabalham em um jogo. Tinham que pos-
suir os métodos atualizar e desenhar, para que a Animacao pudesse
interagir com eles:
• Fundo: faz a rolagem das imagens do cenário.
• Nave: a nave controlada pelo jogador. Realizava constantemente a
leitura do Teclado para se movimentar.
• Ovni: o inimigo a ser destruído. Retirava-se da animação após percor-
rer toda a tela, para não se acumular na memória.
• Tiro: disparo da nave. Também se retirava da animação ao sair da tela.
• Painel: mostrador de vidas e pontuação.
A maioria destas classes manterá seu funcionamento idêntico na versão
mobile do jogo.
56
Casa do Código
Capítulo 4. Incrementando o jogo de nave
4.2
Esqueleto do projeto
Vamos começar criando o projeto e adicionando a plataforma Android:
cordova create nave-mobile-1
cd nave-mobile-1
cordova platform add android
Depois, copie para a pasta www os scripts que você baixou (com exceção
de teclado.js), e também as pastas img e snd (imagens e sons).
Façamos uma configuração para travar o jogo na posição retrato (em pé)
do aparelho. Por padrão, por ser página da web, um app Cordova acompanha
a orientação da tela conforme viramos o dispositivo. Para fixar a tela em uma
determinada orientação, podemos inserir uma tag <preference> no ar-
quivo config.xml. A tag aceita os valores portrait (em pé), landscape
(deitado) e default (o mesmo que não usá-la, permitindo girar livremente
a tela):
<?xml version='1.0' encoding='utf-8'?>
<widget ...>
...
<preference name="Orientation" value="portrait" />
</widget>
O arquivo index.html do projeto irá linkar os scripts cordova.js,
rAF.js (ou como você nomeou o polyfill para requestAnimationFrame)
e os scripts que você baixou. No corpo temos o Canvas e o link para o botão
Jogar:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jogo de Nave - controlado via touch</title>
<meta name="viewport" content="width=device-width,
height=device-height, user-scalable=no,
57
4.2. Esqueleto do projeto
Casa do Código
initial-scale=1, maximum-scale=1, minimum-scale=1">
<script src="cordova.js"></script>
<script src="rAF.js"></script>
<script src="animacao.js"></script>
<script src="spritesheet.js"></script>
<script src="colisor.js"></script>
<script src="fundo.js"></script>
<script src="nave.js"></script>
<script src="ovni.js"></script>
<script src="painel.js"></script>
<script src="tiro.js"></script>
<script src="explosao.js"></script>
<script>
// A aventura vai começar no celular!
</script>
</head>
<body>
<canvas id="jogo_nave" width="500" height="500"></canvas>
<div id="link_jogar">
<a href="javascript: iniciarJogo()">Jogar</a>
</div>
</body>
</html>
As variáveis da página são praticamente as mesmas já presentes no jogo.
A inicialização é que mudará um pouco, para ficar no formato do Cordova:
var canvas, context;
var imagens, animacao, colisor, nave, painel, criadorInimigos;
var totalImagens = 0, carregadas = 0;
window.onload = function() {
//document.addEventListener('deviceready', iniciar);
iniciar();
58
Casa do Código
Capítulo 4. Incrementando o jogo de nave
}
function iniciar() {
canvas = document.getElementById('jogo_nave');
context = canvas.getContext('2d');
carregarImagens();
}
A partir daqui, vamos pegar as funções presentes no jogo original, uma
por uma. A página HTML está no arquivo jogo-definitivo.html, na
pasta do capítulo 09. Em alguns casos, vou indicar modificações. Para fa-
cilitar, você pode copiar e colar os códigos para o projeto Cordova, e depois
adaptá-los. A função de carregamento das imagens permanece a mesma:
function carregarImagens() {
imagens = {
espaco:
'fundo-espaco.png',
estrelas: 'fundo-estrelas.png',
nuvens:
'fundo-nuvens.png',
nave:
'nave-spritesheet.png',
ovni:
'ovni.png',
explosao: 'explosao.png'
};
for (var i in imagens) {
var img = new Image();
img.src = 'img/' + imagens[i];
img.onload = carregando;
totalImagens++;
imagens[i] = img;
}
}
Para podermos ir mais direto ao que nos interessa agora, que é a trans-
formação do jogo em app mobile, simplifiquei algumas funções. Uma delas é
a função que verifica quais imagens já carregaram, pois aqui o carregamento
é mais rápido. Sinta-se livre para recriar a barra de progresso (os códigos es-tão lá!) ou quaisquer efeitos visuais que desejar: splashscreens, logos. Estando todas as imagens carregadas, o botão Jogar deverá surgir:
59
4.2. Esqueleto do projeto
Casa do Código
function carregando() {
carregadas++;
if (carregadas == totalImagens) {
iniciarObjetos();
document.getElementById('link_jogar').style.display =
'block';
}
}
A inicialização dos objetos do jogo também mudou pouco. Não há mais o
objeto teclado, e por isso também a nave não o recebe mais no construtor:
function iniciarObjetos() {
animacao = new Animacao(context);
//teclado = new Teclado(document);
colisor = new Colisor();
espaco = new Fundo(context, imagens.espaco);
estrelas = new Fundo(context, imagens.estrelas);
nuvens = new Fundo(context, imagens.nuvens);
// O construtor da nave não recebe mais o teclado
nave = new Nave(context, imagens.nave, imagens.explosao);
painel = new Painel(context, nave);
animacao.novoSprite(espaco);
animacao.novoSprite(estrelas);
animacao.novoSprite(nuvens);
animacao.novoSprite(painel);
animacao.novoSprite(nave);
colisor.novoSprite(nave);
animacao.novoProcessamento(colisor);
configuracoesIniciais();
}
Mudando o construtor da Nave (em nave.js):
60
Casa do Código
Capítulo 4. Incrementando o jogo de nave
function Nave(context, imagem, imgExplosao) {
this.context = context;
//this.teclado = teclado;
// ...
}
Vamos, por enquanto, comentar os trechos em que a Nave faz uso do
teclado. No método atualizar:
atualizar: function() {
var incremento =
this.velocidade * this.animacao.decorrido / 1000;
/*
if (this.teclado.pressionada(SETA_ESQUERDA) && this.x > 0)
this.x -= incremento;
if (this.teclado.pressionada(SETA_DIREITA) &&
this.x < this.context.canvas.width - 36)
this.x += incremento;
if (this.teclado.pressionada(SETA_ACIMA) && this.y > 0)
this.y -= incremento;
if (this.teclado.pressionada(SETA_ABAIXO) &&
this.y < this.context.canvas.height - 48)
this.y += incremento;
*/
},
E também em desenhar:
desenhar: function() {
/*
if (this.teclado.pressionada(SETA_ESQUERDA))
this.spritesheet.linha = 1;
else if (this.teclado.pressionada(SETA_DIREITA))
this.spritesheet.linha = 2;
else
61
4.2. Esqueleto do projeto
Casa do Código
this.spritesheet.linha = 0;
*/
this.spritesheet.desenhar(this.x, this.y);
this.spritesheet.proximoQuadro();
},
De volta ao index.html, as configurações iniciais também não mudam:
function configuracoesIniciais() {
espaco.velocidade = 60;
estrelas.velocidade = 150;
nuvens.velocidade = 500;
nave.posicionar();
nave.velocidade = 200;
criacaoInimigos();
nave.acabaramVidas = function() {
animacao.desligar();
gameOver();
}
colisor.aoColidir = function(o1, o2) {
if ( (o1 instanceof Tiro && o2 instanceof Ovni) ||
(o1 instanceof Ovni && o2 instanceof Tiro) )
painel.pontuacao += 10;
}
}
E nem a inicialização do objeto que cria inimigos a cada segundo:
function criacaoInimigos() {
criadorInimigos = {
ultimoOvni: Date.now(),
processar: function() {
var agora = Date.now();
var decorrido = agora - this.ultimoOvni;
62
Casa do Código
Capítulo 4. Incrementando o jogo de nave
if (decorrido > 1000) {
novoOvni();
this.ultimoOvni = agora;
}
}
};
animacao.novoProcessamento(criadorInimigos);
}
Este chama a criação do disco voador, que usava Math.random para
sortear uma velocidade e uma posição, lembra-se?
function novoOvni() {
var imgOvni = imagens.ovni;
var ovni = new Ovni(context, imgOvni, imagens.explosao);
ovni.velocidade =
Math.floor( 500 + Math.random() * (1000 - 500 + 1) );
ovni.x =
Math.floor(Math.random() *
(canvas.width - imgOvni.width + 1) );
ovni.y = -imgOvni.height;
animacao.novoSprite(ovni);
colisor.novoSprite(ovni);
}
Até aqui, faça um pequeno teste no navegador: ao carregar a página, o
único erro no Console deverá ser a falta do cordova.js. Mas ainda não
dá para clicar em Jogar, pois não há a função iniciarJogo. Também
resolvi simplificá-la; a música será programada posteriormente pela API do
Cordova:
function iniciarJogo() {
criadorInimigos.ultimoOvni = Date.now();
63
4.2. Esqueleto do projeto
Casa do Código
document.getElementById('link_jogar').style.display =
'none';
painel.pontuacao = 0;
animacao.ligar();
}
Da mesma forma, também simplifiquei a função que finaliza o jogo:
function gameOver() {
document.getElementById('link_jogar').style.display ='block';
nave.vidasExtras = 3;
nave.posicionar();
// A nave é reintroduzida por ter sido retirada na explosão
animacao.novoSprite(nave);
colisor.novoSprite(nave);
removerInimigos();
alert('Game Over');
}
Ela chama uma outra função para remover elementos que sobraram na
tela:
function removerInimigos() {
for (var i in animacao.sprites) {
if (animacao.sprites[i] instanceof Ovni ||
animacao.sprites[i] instanceof Tiro)
animacao.excluirSprite(animacao.sprites[i]);
}
}
Vamos ao CSS. O Canvas irá ocupar toda a largura, deixando o restante da
altura para o direcional. O botão Jogar inicia-se oculto, e o link dentro dele terá o formato da imagem botao-jogar.png, que já deve estar presente na
subpasta www/img:
64
Casa do Código
Capítulo 4. Incrementando o jogo de nave
body {
margin: 0;
width: 100%;
height: 100%;
}
#jogo_nave {
width: 100%;
height: auto;
}
#link_jogar {
display: none;
text-align: center;
padding: 20px;
}
#link_jogar a {
color: yellow;
background: url(img/botao-jogar.png);
font-size: 20px;
font-family: sans-serif;
text-decoration: none;
text-shadow: 2px 2px 5px black;
display: inline-block;
width: 52px;
height: 26px;
padding: 23px 10px;
}
Faça outro teste no browser. Pode ser preciso rolar a página para encon-
trar o botão Jogar. A ação já deverá acontecer do começo ao fim, do botão
Jogar ao Game Over, embora ainda não se consiga controlar a nave. Mas
não é possível rodar no celular, pois a webview padrão do Android ainda não
suporta a API de áudio do HTML5 (http://caniuse.com/audio-api) . Vamos
usar a API do Cordova para garantir maior compatibilidade, embora ela não
esteja dentro da especificação da W3C.
65
4.3. API de som do Cordova
Casa do Código
4.3
API de som do Cordova
Como vimos, o Cordova possui uma API própria para tratar arquivos de mul-
timídia. A partir do Cordova 3.0, as APIs foram quebradas em plugins, de-
vendo cada um ser instalado no projeto para poder ser usado.
APIs do Cordova
Você pode encontrar a lista oficial de plugins em:
http://goo.gl/aP905E
(Encurtei a URL por ser um endereço bastante longo)
Para instalar o plugin de multimídia, abra o terminal/prompt, navegue
até a pasta principal do projeto e dê o comando a seguir:
cordova plugin add org.apache.cordova.media
Esse plugin depende de outro, de acesso a arquivos, e a saída do comando
no console indicará que ambos foram instalados:
Fetching plugin "org.apache.cordova.media" via plugin registry
Installing "org.apache.cordova.media" for android
Fetching plugin "org.apache.cordova.file" via plugin registry
Installing "org.apache.cordova.file" for android
Os sons do jogo, baixados do pacote do livro anterior, estão
na subpasta
snd do projeto original.
Mova-os para a subpasta
platforms/android/assets do projeto Cordova (a partir da pasta
principal). Arquivos que serão acessados programaticamente através de
código nativo Android podem ficar nessa pasta.
No arquivo tiro.js, as linhas iniciais do script carregam o som do dis-
paro. Remova-as:
// Remova estas linhas
var SOM_TIRO = new Audio();
SOM_TIRO.src = 'snd/tiro.mp3';
SOM_TIRO.volume = 0.2;
SOM_TIRO.load();
66
Casa do Código
Capítulo 4. Incrementando o jogo de nave
No construtor, executamos o som cada vez que um tiro é criado. Também
remova as linhas indicadas:
function Tiro(context, nave) {
// ...
// Remova estas também
SOM_TIRO.currentTime = 0.0;
SOM_TIRO.play();
}
O mesmo deverá ser feito para o arquivo explosao.js.
Agora, acrescente as variáveis que referenciarão os sons no index.html:
var canvas, context;
var imagens, animacao, colisor, nave, criadorInimigos;
var totalImagens = 0, carregadas = 0;
var musicaAcao, somTiro, somExplosao;
Na função iniciar, chame carregarSons:
function iniciar() {
canvas = document.getElementById('jogo_nave');
context = canvas.getContext('2d');
carregarImagens();
carregarSons();
}
E crie essa função. Na API de multimídia do Cordova, um objeto Media
recebe o arquivo de som no construtor. Na plataforma Android, arquivos
na pasta assets podem ser indicados escrevendo file:///android_asset/meu_
arquivo :
function carregarSons() {
musicaAcao =
new Media('file:///android_asset/musica-acao.mp3');
somTiro = new Media('file:///android_asset/tiro.mp3');
somExplosao =
new Media('file:///android_asset/explosao.mp3');
}
67
4.3. API de som do Cordova
Casa do Código
Na função iniciarJogo, inicie a reprodução do som. O método
seekTo seleciona a posição no som em milissegundos setamos para zero
para voltar a música no início. Depois é só chamar play:
function iniciarJogo() {
// ...
musicaAcao.seekTo(0);
musicaAcao.play();
}
Na função gameOver, mande pausar:
function gameOver() {
musicaAcao.pause();
// ...
}
Os eventos de colisão estão em configuracoesIniciais. Acrescente
a sequência seekTo- play nos eventos de colisão:
function configuracoesIniciais() {
// ...
// Pontuação e som das explosões
colisor.aoColidir = function(o1, o2) {
// Tiro com Ovni
if ( (o1 instanceof Tiro && o2 instanceof Ovni) ||
(o1 instanceof Ovni && o2 instanceof Tiro) ) {
painel.pontuacao += 10;
somExplosao.seekTo(0);
somExplosao.play();
}
// Nave com Ovni
if ( (o1 instanceof Nave && o2 instanceof Ovni) ||
(o1 instanceof Ovni && o2 instanceof Nave) ) {
somExplosao.seekTo(0);
somExplosao.play();
68
Casa do Código
Capítulo 4. Incrementando o jogo de nave
}
}
}
Já é possível rodar o projeto no dispositivo móvel, com o comando
cordova run android.
Observações
Sempre que for testar algo no celular, não se esqueça de mudar o script
de inicialização para começar no deviceready:
window.onload = function() {
document.addEventListener('deviceready', iniciar);
//iniciar();
}
69
4.4. Criando um controle direcional
Casa do Código
Figura 4.1: Nosso velho jogo de nave virando um app móvel e portável
4.4
Criando um controle direcional
Para controlar a nave, primeiro vamos criar um controle direcional para telas
touch. Ou seja, recriar através de ícones a jogabilidade dos maravilhosos joy-
sticks dos anos 90!
70
Casa do Código
Capítulo 4. Incrementando o jogo de nave
Figura 4.2: Controle do console SNES, da Nintendo
Nesses controles, os quatro botões unidos fisicamente em cruz ou círculo
faziam com que, levando o dedo para a diagonal, dois deles ficassem pres-
sionados juntos, forçando a direção diagonal correspondente:
Figura 4.3: Pressionando duas direções juntas para ir para a diagonal
Em nosso caso, como não há união física entre os botões, todas as di-
reções deverão possuir seu ícone correspondente, inteiramente contido em
sua célula, sem invadir a área de outro. A célula do centro servirá como
posição de descanso do dedo:
71
4.4. Criando um controle direcional
Casa do Código
Figura 4.4: Criando um controle direcional
No pacote de downloads deste livro (https://github.com/EdyKnopfler/
games-js-2/archive/master.zip) , está a pasta relativa a este projeto,
nave-mobile-1.
Na subpasta
www/img, se encontra o arquivo
direcional.png. Copie-o para a www/img do seu projeto.
Ao final da tag <body>, acrescente uma nova div contendo esse dire-
cional e um botão para disparo:
<div id="controles">
<img src="img/direcional.png" id="direcional">
<img src="img/botao-jogar.png" id="disparo">
</div>
Seu posicionamento será feito via CSS. A div controles inicialmente se
encontra oculta, pois só deverá aparecer quando clicarmos no botão Jogar.
O direcional é posicionado no canto inferior esquerdo e o botão, no inferior
direito, um pouco afastado das bordas da tela:
#controles {
display: none;
}
#direcional {
position: absolute;
bottom: 0;
left: 0;
}
72
Casa do Código
Capítulo 4. Incrementando o jogo de nave
#disparo {
position: absolute;
right: 40px;
bottom: 64px;
}
Sinta-se livre para mudar as posições e tamanhos, sem se esquecer que as
medidas reais das células influirão na programação posteriormente.
Quando o jogo se inicia, os controles deverão aparecer. Acrescente o co-
mando na função iniciarJogo:
document.getElementById('controles').style.display =
'block';
E também os faça desaparecer em gameOver:
document.getElementById('controles').style.display =
'none';
Configurar o disparo é fácil, basta capturar o evento touchstart. Em
configuracoesIniciais, faça:
function configuracoesIniciais() {
// ...
// Disparo
document.getElementById('disparo').addEventListener(
'touchstart',
function( ) {
nave.atirar();
somTiro.seekTo(0);
somTiro.play();
}
);
}
Já dá pra atirar e destruir uns OVNIs! Mas vamos iniciar o direcional em
iniciarObjetos. Seu construtor receberá a imagem na página. Ele tam-
bém será passado para o construtor da Nave, como era feito com o Teclado
na versão desktop do jogo:
73
4.4. Criando um controle direcional
Casa do Código
function iniciarObjetos() {
// ...
direcional = new Direcional(
document.getElementById('direcional'));
nave = new Nave(context, direcional, imagens.nave,
imagens.explosao);
// ...
}
Criemos agora a classe Direcional no arquivo direcional.js.
Primeiro, definimos nomes para as direções e recebemos a imagem a ser us-
ada no construtor. Os valores não importam, contanto que sejam únicos:
var DIRECAO_NENHUMA = 0;
var DIRECAO_N = 1;
// norte
var DIRECAO_S = 2;
// sul
var DIRECAO_L = 3;
// leste
var DIRECAO_O = 4;
// oeste
var DIRECAO_NE = 5;
// nordeste
var DIRECAO_NO = 6;
// noroeste
var DIRECAO_SE = 7;
// sudeste
var DIRECAO_SO = 8;
// sudoeste
function Direcional(imagem) {
this.imagem = imagem;
this.direcao = DIRECAO_NENHUMA;
// continua...
}
O que temos que fazer é atribuir eventos de toque à imagem. Vamos
atribuir as funções de evento a variáveis, de forma que cada uma possa ser
usada em mais de um evento. Uma delas é responsável por capturar as coor-
denadas, já descontando a posição da imagem ( offsets), passando-as para que o método direcaoPonto, a ser criado logo mais, determine a direção. A
outra servirá para finalizar o movimento, atribuindo DIRECAO_NENHUMA ao
direcional:
function Direcional(imagem) {
// ...
74
Casa do Código
Capítulo 4. Incrementando o jogo de nave
var direcional = this;
var funcaoToque = function(e) {
direcional.direcaoPonto(
e.changedTouches[0].pageX - imagem.offsetLeft,
e.changedTouches[0].pageY - imagem.offsetTop
);
}
var funcaoCancela = function() {
direcional.direcao = DIRECAO_NENHUMA;
}
// continua...
}
Tendo as funções em mãos, vamos atribuí-las a todos os eventos descritos
no início do capítulo 3. Na seção 3.1 foi dito que, no Android, temos que
chamar o preventDefault no touchstart para contornar um bug que
impede o touchmove de ocorrer:
function Direcional(imagem) {
// ...
imagem.addEventListener('touchstart', function(e) {
funcaoToque(e);
if( navigator.userAgent.match(/Android/i) )
e.preventDefault();
});
imagem.addEventListener('touchmove', funcaoToque);
imagem.addEventListener('touchend', funcaoCancela);
imagem.addEventListener('touchleave', funcaoCancela);
imagem.addEventListener('touchenter', funcaoToque);
imagem.addEventListener('touchcancel', funcaoCancela);
}
Permitir continuar mudando a direção no touchmove tornará nosso di-
recional mais suave, sem exigir que o jogador tire o dedo de um ícone para
tocar outro. Já tentei jogar jogos onde não tomaram esse cuidado e achei a
experiência muito frustrante.
75
4.4. Criando um controle direcional
Casa do Código
A função que recebe o comando de toque passa a responsabilidade de
determinar a direção para o método direcaoPonto. Este divide a área da
imagem em três partes iguais, tanto na largura quanto na altura, e determina
qual célula foi tocada. Veja como foi testada a primeira coluna e, dentro dela, testamos pela altura as direções noroeste, oeste e sudoeste:
Direcional.prototype = {
direcaoPonto: function(x, y) {
var larguraCelula = this.imagem.width / 3;
var alturaCelula = this.imagem.height / 3;
// 1ª coluna
if (x < larguraCelula) {
// Verifica qual linha
if (y < alturaCelula)
this.direcao = DIRECAO_NO;
else if (y < alturaCelula * 2)
this.direcao = DIRECAO_O;
else
this.direcao = DIRECAO_SO;
}
// continua...
}
}
Seguindo este raciocínio, na segunda coluna, testamos o norte, a posição
de descanso e o sul; e na terceira testamos o nordeste, o leste e o sudoeste:
// 2ª coluna
else if (x < larguraCelula * 2) {
if (y < alturaCelula)
this.direcao = DIRECAO_N;
else if (y < alturaCelula * 2)
this.direcao = DIRECAO_NENHUMA; // Centro do direcional
else
this.direcao = DIRECAO_S;
}
76
Casa do Código
Capítulo 4. Incrementando o jogo de nave
// 3ª coluna
else {
if (y < alturaCelula)
this.direcao = DIRECAO_NE;
else if (y < alturaCelula * 2)
this.direcao = DIRECAO_L;
else
this.direcao = DIRECAO_SE;
}
A classe Direcional está pronta, temos agora que incluí-la no jogo.
Adicione o script no <head>:
<script src="direcional.js"></script>
Como estamos passando o objeto direcional no construtor da Nave,
receba-o na function da classe e crie o atributo correspondente:
function Nave(context, direcional, imagem, imgExplosao) {
this.context = context;
this.direcional = direcional;
// ...
}
Da mesma forma que fazia com o Teclado, a nave deverá ler o estado
do Direcional para determinar seus deslocamentos em x e y. Poderíamos testar direção por direção, mas isso iria dar muito trabalho para testarmos também os limites do Canvas em cada caso:
// Exemplo teórico
if (dir.direcao == DIRECAO_NO) {
if (this.x > 0)
this.x -= incremento;
if (this.y > 0)
this.y -= incremento;
}
// outras direções...
77
4.4. Criando um controle direcional
Casa do Código
Em vez disso, vamos testar por esquerda, direita, acima e abaixo isolada-
mente. Por exemplo: noroeste, oeste e sudoeste são movimentos para a es-
querda; ou sudoeste, sul e sudeste são movimentos para baixo etc. Comece
testando essas condições no método atualizar da Nave:
atualizar: function() {
var incremento =
this.velocidade * this.animacao.decorrido / 1000;
var dir = this.direcional;
var esquerda = dir.direcao == DIRECAO_NO ||
dir.direcao == DIRECAO_O ||
dir.direcao == DIRECAO_SO;
var direita = dir.direcao == DIRECAO_NE ||
dir.direcao == DIRECAO_L ||
dir.direcao == DIRECAO_SE;
var acima = dir.direcao == DIRECAO_NO ||
dir.direcao == DIRECAO_N ||
dir.direcao == DIRECAO_NE;
var abaixo = dir.direcao == DIRECAO_SO ||
dir.direcao == DIRECAO_S ||
dir.direcao == DIRECAO_SE;
// continua ...
},
Conseguimos reduzir aos quatro casos originais, para saber se há movi-
mento em x e/ou em y:
atualizar: function() {
// ...
if (esquerda && this.x > 0)
this.x -= incremento;
if (direita && this.x < this.context.canvas.width - 36)
this.x += incremento;
if (acima && this.y > 0)
78
Casa do Código
Capítulo 4. Incrementando o jogo de nave
this.y -= incremento;
if (abaixo && this.y < this.context.canvas.height - 48)
this.y += incremento;
},
O método desenhar só precisa testar pela esquerda ou direita, para
saber com qual linha da spritesheet vai executar a animação:
desenhar: function() {
var dir = this.direcional;
if (dir.direcao == DIRECAO_NO ||
dir.direcao == DIRECAO_O ||
dir.direcao == DIRECAO_SO)
this.spritesheet.linha = 1;
else if (dir.direcao == DIRECAO_NE ||
dir.direcao == DIRECAO_L ||
dir.direcao == DIRECAO_SE)
this.spritesheet.linha = 2;
else
this.spritesheet.linha = 0;
this.spritesheet.desenhar(this.x, this.y);
this.spritesheet.proximoQuadro();
},
Por fim, em gameOver, faça com que o movimento cesse, para a nave
não sair desembestada na próxima partida. Quando temos janelas de alerta,
elas interferem no foco da aplicação, fazendo com que os eventos touch na
imagem não sejam ouvidos. Por exemplo, tirar o dedo para dar “OK” no alerta
não força um touchend na imagem. Por isso, façamos os ajustes antes do
alerta:
function gameOver() {
// ...
79
4.5. Precisão com múltiplos dedos
Casa do Código
direcional.direcao = DIRECAO_NENHUMA;
alert('Game Over');
}
Tente jogar. Tudo funciona quase normalmente... Vejamos por quê.
Figura 4.5: Jogo de nave com direcional touch
4.5
Precisão com múltiplos dedos
Se você conseguiu jogar, pode acontecer que a nave vá sempre para a direita,
não importando em qual posição do direcional você está com o dedo. Não é
toda hora que acontece, mas é o suficiente para incomodar e fazer com que
alguém dê nota negativa para seu jogo na Play Store. Isto ocorre especifi-
80
Casa do Código
Capítulo 4. Incrementando o jogo de nave
camente se você primeiro der um disparo, mantiver o dedo no botão e, em
seguida, tocar o direcional com outro dedo:
Figura 4.6: Com o dedo no botão de disparo, a nave move-se para a direita!
Lembra-se de que o atributo changedTouches do evento é, na verdade,
um array com todos os dedos na tela? Se já havia um dedo no botão, ele é o
elemento zero; o toque na imagem então cria o elemento número 1. Estamos
sempre testando o elemento zero, e como o botão fica mais à direita, nosso
algoritmo interpreta que tocamos a terceira coluna:
O primeiro passo é, justamente, colocar um limite final, em vez de um
else simples. No método direcaoPonto da classe Direcional, modi-
fique o else da terceira coluna para incluir a condição:
81
4.5. Precisão com múltiplos dedos
Casa do Código
Também vale a pena colocarmos limites nos elses finais internos, que
testam a posição y:
// 1ª coluna
if (x < larguraCelula) {
// ...
else if (y < alturaCelula * 3)
return DIRECAO_SO;
}
// Faça para todas as colunas!
Por fim, no construtor, ao invés de testar somente a posição zero do
changedTouches, vamos fazer um loop e testar todo o array. Não importa
qual a ordem dos dedos, se algum estiver dentro dos limites de uma célula,
deverá ser considerado. Atenção: no lugar de zero, usamos a variável de loop i:
function Direcional(imagem) {
// ...
var funcaoToque = function(e) {
for (var i in e.changedTouches) {
direcional.direcaoPonto(
e.changedTouches[i].pageX - imagem.offsetLeft,
e.changedTouches[i].pageY - imagem.offsetTop
);
}
}
// ...
}
Perceba que, se testarmos os limites exatos de uma área de toque, pode-
mos verificar todo o array de dedos, sem nos preocuparmos com qual ele-
mento do array representa aquele toque. Agora sim nosso controle tem um
tratamento mais profissional.
E assim concluímos a primeira versão móvel de nosso jogo de nave! E a
82
Casa do Código
Capítulo 4. Incrementando o jogo de nave
aventura só está começando, porque em um dispositivo móvel inúmeras são
as possibilidades de jogabilidade.
4.6
API do acelerômetro do Cordova
O acelerômetro é o sensor de movimento de seu dispositivo móvel: aquele
que detecta se você está usando o aparelho em retrato (em pé) ou paisagem
(deitado), por exemplo. Mas ele é capaz de muito mais que isso. Joysticks
como o do Nintendo Wii também são baseados em acelerômetros. O que ele
faz é detectar movimentos nos seguintes eixos:
Figura 4.7: Eixos de movimentação monitorados pelo acelerômetro
Podemos copiar toda a pasta nave-mobile-1 para nave-mobile-2,
onde iremos trabalhar agora. Acesse esta pasta do terminal/prompt e adicione
o plugin para leitura do acelerômetro:
cordova plugin add org.apache.cordova.device-motion
Na página
index.html, deixe somente botão de disparo na div
controles. Se quiser, você pode deletar deste projeto todos os arquivos
83
4.6. API do acelerômetro do Cordova
Casa do Código
relacionados ao direcional (imagem e script), e também a tag <script
src="direcional.js">.
<div id="controles">
<img src="img/botao-jogar.png" id="disparo">
</div>
Na função iniciarObjetos, não será mais iniciado o direcional, nem
passado para o construtor da Nave:
function iniciarObjetos() {
// ...
// Remova esta linha
//direcional = new Direcional(document.getElementById(
//
'direcional'));
// Não passe mais o direcional
nave = new Nave(context, imagens.nave, imagens.explosao);
// ...
}
Remova também, da função gameOver, a linha que reseta a direção:
direcional.direcao = DIRECAO_NENHUMA;
O construtor da classe Nave fica um pouco mais enxuto sem ele:
function Nave(context, imagem, imgExplosao) {
this.context = context;
// Remova
//this.direcional = direcional;
// ...
}
Deixe, por enquanto, os métodos atualizar e desenhar com os cor-
pos comentados. Logo mais, esses métodos vão se valer dos dados do acel-
erômetro. No entanto, eles não farão a sua leitura diretamente, pois fazer isso 84
Casa do Código
Capítulo 4. Incrementando o jogo de nave
no loop de animação é muito lento. Em vez disso, vamos deixar o código
nativo do Cordova monitorar o acelerômetro para nós.
atualizar: function() {
/* comente */
},
desenhar: function() {
/* comente */
},
No index.html, função iniciarJogo, configure logo no início uma
chamada a navigator.accelerometer.watchAcceleration, que re-
cebe uma função de callback a ser chamada de tempos em tempos. A frequên-
cia é configurada em milissegundos, em um objeto no último parâmetro. Fiz
vários testes até chegar a um valor que me agradou; você pode experimen-
tar outros intervalos e ajustar conforme cada jogo exigir uma sensibilidade
diferente:
function iniciarJogo() {
acelerometro = navigator.accelerometer.watchAcceleration(
function(aceleracao) {
var msg =
'X: ' + aceleracao.x + '<br>' +
'Y: ' + aceleracao.y + '<br>' +
'Z: ' + aceleracao.z;
document.getElementById('controles').innerHTML = msg;
},
function() {
alert('Erro!');
},
{ frequency: 25 }
);
// ...
}
Rode o projeto e veja os valores. O botão de disparo some porque estamos
sobrescrevendo todo o conteúdo da div (mas isso é temporário):
85
4.6. API do acelerômetro do Cordova
Casa do Código
Figura 4.8: Obtendo dados do acelerômetro
Balance o aparelho e confira os valores conforme a direção de inclinação:
• Esquerda: x > 0
• Direita: x < 0
• Cima: y < 0
• Baixo: y > 0
Para nossos propósitos, o eixo z não é necessário. Podemos então passar os valores obtidos para a nave:
86
Casa do Código
Capítulo 4. Incrementando o jogo de nave
function iniciarJogo() {
acelerometro = navigator.accelerometer.watchAcceleration(
function(aceleracao) {
nave.aceleracao.x = aceleracao.x;
nave.aceleracao.y = aceleracao.y;
},
function() {
alert('Erro!');
},
{ frequency:25 }
);
// ...
}
Repare que criamos o atributo aceleracao na nave. Vamos iniciá-lo no
construtor com um objeto contendo os dois eixos usados e o valor zero para
cada um:
function Nave(context, imagem, imgExplosao) {
// ...
this.aceleracao = { x: 0, y: 0 };
}
O método atualizar fará a leitura dos dados e movimentará a nave
conforme o sentido em que balançamos o aparelho, cujos testes vimos há
pouco:
atualizar: function() {
var incremento =
this.velocidade * animacao.decorrido / 1000;
var acc = this.aceleracao;
if (acc.x > 0 && this.x > 0)
this.x -= incremento;
if (acc.x < 0 && this.x < this.context.canvas.width - 36)
this.x += incremento;
87
4.7. Adaptando o Canvas ao tamanho da tela
Casa do Código
if (acc.y < 0 && this.y > 0)
this.y -= incremento;
if (acc.y > 0 && this.y < this.context.canvas.height - 48)
this.y += incremento;
},
O mesmo vale para o método desenhar:
desenhar: function() {
var acc = this.aceleracao;
if (acc.x > 0)
this.spritesheet.linha = 1;
else if (acc.x < 0)
this.spritesheet.linha = 2;
else
this.spritesheet.linha = 0;
this.spritesheet.desenhar(this.x, this.y);
this.spritesheet.proximoQuadro();
},
Na função
gameOver, vamos parar de monitorar o acelerômetro
chamando navigator.accelerometer.clearWatch:
function gameOver() {
navigator.accelerometer.clearWatch(acelerometro);
// ...
}
Comece a se divertir! Fale a verdade, ficou emocionante, não ficou?
4.7
Adaptando o Canvas ao tamanho da tela
Em 3.3, aprendemos a criar um Canvas que se adapta à tela via CSS. Como as
imagens geradas eram dimensionadas, isto requeria que cada elemento grá-
fico tivesse um tamanho razoável, a fim de se adaptar a telas maiores.
88
Casa do Código
Capítulo 4. Incrementando o jogo de nave
Vamos fazer diferente agora: configurar dinamicamente o Canvas para
o tamanho exato disponível na tela. Estamos configurando os atributos da
tag, width e height. Os valores vêm dos atributos availWidth e
availHeight do objeto screen, que nos dão a largura e a altura da tela,
descontadas barras de tarefas, de botões e de ícones indicadores do sistema
operacional. Façamos isto na função iniciar, onde ocorre a inicialização
do app:
function iniciar() {
canvas = document.getElementById('jogo_nave');
context = canvas.getContext('2d');
canvas.width = screen.availWidth;
canvas.height = screen.availHeight;
carregarImagens();
carregarSons();
}
Como o Canvas vai ocupar a tela toda, temos que fazer uso do atributo
z-index do CSS para que os botões Jogar e de disparo fiquem por cima.
Estes recebem o valor 1, enquanto o Canvas recebe o valor zero:
body { /* não mudou */ }
#jogo_nave {
position: absolute;
z-index: 0;
}
#link_jogar {
display: none;
position: absolute;
z-index: 1;
bottom: 20px;
width: 100%;
text-align: center;
}
#link_jogar a { /* não mudou */ }
89
4.7. Adaptando o Canvas ao tamanho da tela
Casa do Código
#controles { /* não mudou */ }
#direcional { /* desaparece */ }
#disparo {
position: absolute;
right: 10px;
bottom: 10px;
z-index: 1;
}
As adaptações ao jogo de nave acabam aqui. Você agora tem uma base
bastante robusta para criar jogos móveis em estilo arcade, além de mais di-
versão para a fila do banco ou do supermercado! No próximo capítulo, dare-
mos início à construção de uma mesa de bilhar móvel, usando o framework
Hammer.js para controlar as tacadas.
90
Casa do Código
Capítulo 4. Incrementando o jogo de nave
Figura 4.9: Jogo com acelerômetro ocupando a tela
91
Capítulo 5
Interações avançadas com
Hammer.js
Neste capítulo, você aprenderá a capturar gestos comuns, porém mais com-
plexos, de forma prática com a biblioteca Hammer.js. A partir daqui, um pequeno jogo de bilhar começará a tomar forma. Deixaremos o jogador rota-
cionar o taco com os dedos e regular a força da tacada arrastando um ponteiro
na tela.
5.1. Conhecendo o Hammer.js
Casa do Código
Figura 5.1: Taco de bilhar touch
5.1
Conhecendo o Hammer.js
Gestos comuns
Você está navegando na internet e surge uma página com letras miúdas. O
que você precisa fazer? Dar zoom. Como dar zoom no browser do dispositivo
móvel? Com o gesto chamado pinch (pinça), que consiste em colocar dois dedos na tela e afastá-los ou aproximá-los. Afastando os dedos, você dá zoom;
aproximando, você tira o zoom.
94
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
Figura 5.2: Fazendo a pinça para ajustar o zoom
Existe também um gesto usado para girar coisas. O gesto de rotação ( ro-
tate) é usado, por exemplo, no Google Earth. Você coloca dois dedos na tela e gira como se estivesse cochando um parafuso:
95
5.1. Conhecendo o Hammer.js
Casa do Código
Figura 5.3: Girando o globo com o gesto rotate
Outros gestos usuais são:
• Tap (toque): toque rápido;
• Press (pressionar): manter o dedo sobre um objeto por um curto in-
stante. Normalmente abre um menu contextual;
• Swipe (deslizar): mover o dedo rapidamente de um ponto a outro. Us-
ado para passar fotos na tela, abrir menus ocultos em apps e virar pági-
nas em e-books;
• Pan ou drag (arrastar): deslocamento de um objeto com o dedo.
96
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
Tecnicamente, a diferença entre o swipe e o pan é a velocidade: o swipe só ocorre a partir de uma velocidade mínima definida.
Implementar esses gestos usando a API de mais baixo nível do HTML5
é relativamente complexo. Para reconhecer um simples press, por exemplo, você iniciaria a contagem do tempo no touchstart e a interromperia no
touchend. Um timeout poderia ser disparado se o dedo foi mantido no ob-
jeto pelo tempo que você determinar:
// Exemplo teórico
elemento.addEventListener('touchstart', function() {
timeout = setTimeout(press, 500);
});
elemento.addEventListener('touchend', function() {
clearTimeout(timeout);
});
function press() {
// Executo minha ação
}
Isso porque não estamos considerando se o usuário moveu o dedo ou
não. Se quiséssemos evitar, teríamos que cancelar o timeout também no touchmove. No capítulo 3, implementamos um pan para fazer uma bruxa
voar pelo Canvas. Se quiséssemos restringir o movimento somente na verti-
cal ou na horizontal, lá se iam mais umas linhas de código. O pinch e o rotate já são um caso a parte de complexidade.
Felizmente, todos esses eventos já se encontram implementados na bib-
lioteca Hammer.js. E o que é melhor: são reconhecidos não só em telas touch, mas também através do mouse!
Usando o Hammer.js
O download da biblioteca pode ser feito em: http://hammerjs.github.io/.
Tendo linkado o arquivo na página, se quisermos reconhecer o gesto de
arrastar, primeiro devemos instanciar um objeto Hammer, passando o ele-
mento da página no construtor. Depois, o método on manda o Hammer.js
reconhecer um evento. Passo para ele a string 'pan' e o callback:
97
5.1. Conhecendo o Hammer.js
Casa do Código
<script src="hammer.min.js"></script>
<script>
var canvas = ...
var hammer = new Hammer(canvas);
hammer.on('pan', function(e) {
console.log(e.center.x + ', ' + e.center.y);
});
</script>
Os eventos podem ser destrinchados: em vez de simplesmente pan,
poderíamos dizer panstart, panmove ou panend. E, melhor ainda:
panleft, panright, panup, pandown! ( panup e pandown ainda não
funcionam, mas logo você saberá por quê.)
Eventos reconhecidos pelo Hammer.js
Aqui estão os eventos padrão do Hammer.js e seus variantes:
• tap
• press
• pan e panstart, panmove, panend, pancancel, panleft, panright, panup, pandown
• swipe e swipeleft, swiperight, swipeup, swipedown
• pinch e pinchstart, pinchmove, pinchend, pinchcancel, pinchin, pin-chout
• rotate e rotatestart, rotatemove, rotateend, rotatecancel Mas calma: nem tudo isso funcionará de imediato. Os eventos pinch
e rotate são desabilitados por padrão, e o pan e o swipe só detectam
98
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
inicialmente movimentos na horizontal. O motivo disso é que esses gestos
são reservados em páginas web: fazer a pinça para dar zoom e arrastar na
vertical para rolar a página. O browser não interpreta o rotate, mas os dedos podem se afastar ou aproximar e um pinch ocorrerá. No caso de jogos e apps em geral, como estamos sempre restringindo a tela, podemos habilitar esses
gestos:
hammer.get('pinch').set({ enable: true });
hammer.get('rotate').set({ enable: true });
E também habilitar as direções desejadas para o pan e o swipe:
hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL });
hammer.get('swipe')
.set({ direction: Hammer.DIRECTION_VERTICAL });
As constantes em Hammer são:
• DIRECTION_NONE
• DIRECTION_LEFT
• DIRECTION_RIGHT
• DIRECTION_UP
• DIRECTION_DOWN
• DIRECTION_HORIZONTAL
• DIRECTION_VERTICAL
• DIRECTION_ALL
Uma configuração a que devemos prestar atenção é a threshold, que
define o deslocamento mínimo para um evento ocorrer. No caso do pan, o
valor padrão é 10 pixels! Podemos setá-la como zero, para um deslocamento
suave. Para podermos arrastar um objeto livremente, o código fica:
99
5.1. Conhecendo o Hammer.js
Casa do Código
hammer.get('pan').set({
direction: Hammer.DIRECTION_ALL,
threshold: 0
});
hammer.on('pan', function(e) {
// ...
});
No callback, um dos atributos mais importantes do evento é center, que
contém as coordenadas x e y do ponto central entre os dedos, ou do próprio
dedo, se houver um só, como no último exemplo. No caso de um rotate ou
pinch, por exemplo, a posição de cada dedo estaria no atributo pointers,
que é um array com os dedos na tela:
hammer.get('rotate').set({ enable: true });
hammer.on('rotate', function(e) {
console.log('Centro: ' + e.center.x + ', ' + e.center.y);
console.log('Dedo 1: ' + evento.pointers[0].pageX + ', ' +
evento.pointers[0].pageY);
console.log('Dedo 2: ' + evento.pointers[1].pageX + ', ' +
evento.pointers[1].pageY);
});
Tendo feito esta introdução ao Hammer.js, podemos começar a utilizá-lo
em nosso jogo de bilhar.
Documentação do Hammer.js
Você pode obter mais informações em:
http://hammerjs.github.io/getting-started/
Lá se encontra uma descrição detalhada da API e dos eventos ( recog-
nizers).
100
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
5.2
Barra de ajuste de força
Vamos começar pelo mais fácil: uma barra para o jogador ajustar a força da
tacada. Ela possuirá um ponteiro arrastável dentro dos seus limites.
Comece criando o projeto taco-bilhar, adicione a plataforma An-
droid e adicione o plugin de multimídia:
cordova create taco-bilhar
cd taco-bilhar
cordova platform add android
cordova plugin add org.apache.cordova.media
No
pacote
de
download
do
livro,
na
pasta
taco-bilhar/platforms/android/assets,
existe
o
arquivo
colisao.mp3. Este é o som da tacada. Copie-o para a pasta de assets do
seu projeto. Copie também as imagens da pasta www/img.
Também configure no config.xml a orientação landscape (deitado),
e um ícone para o projeto. No download já há um ícone na pasta de imagens.
Se desejar, você pode procurar ou criar a imagem que preferir:
<preference name="Orientation" value="landscape" />
<icon src="www/img/icone.png" />
Coloque na pasta www os scripts do requestAnimationFrame e do
Hammer.js. O esqueleto do index.html ficará assim:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
height=device-height, user-scalable=no,
initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>Taco de Bilhar</title>
<script src="cordova.js"></script>
<script src="rAF.js"></script>
<script src="hammer.min.js"></script>
101
5.2. Barra de ajuste de força
Casa do Código
<script src="barra-forca.js"></script>
<script src="taco.js"></script>
<script>
// inicialização
</script>
<style>
/* folha de estilo */
</style>
</head>
<body>
<canvas id="taco_bilhar"></canvas>
</body>
</html>
Dentro da tag <style>, coloque o CSS para ajustar o Canvas conforme
aprendemos, deixando-o responsivo de acordo com a altura da tela:
body {
margin: 0;
width: 100%;
height: 100%;
background: #333;
}
canvas {
height: 100%;
width: auto;
display: block;
margin: 0 auto;
}
Façamos a inicialização do jogo, configurando o Canvas e carregando as
imagens e sons. As linhas que não rodam no browser desktop foram comen-
tadas, você poderá descomentá-las depois quando formos testar no celular ou
tablet:
102
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
var canvas, context;
var imgTaco, imgPonteiro, somTacada;
var taco, barraForca;
window.onload = function() {
canvas = document.getElementById('taco_bilhar');
context = canvas.getContext('2d');
canvas.width = screen.availWidth;
canvas.height = screen.availHeight;
//document.addEventListener('deviceready', iniciar);
iniciar();
}
function iniciar() {
imgTaco = new Image();
imgTaco.src = 'img/taco.png';
imgPonteiro = new Image();
imgPonteiro.src = 'img/ponteiro.png';
//somTacada = new Media('file:///android_asset/colisao.mp3');
iniciarObjetos();
requestAnimationFrame(animar);
}
A função iniciarObjetos criará os objetos taco e barraForca. O
controle do taco de bilhar possuirá uma área circular, dentro da qual faremos
o gesto de rotação. Já a barra de força será retangular:
function iniciarObjetos() {
taco = new Taco(context, imgTaco);
taco.raio = 120;
taco.x = 125;
taco.y = canvas.height / 2 - 50;
barraForca = new BarraForca(context, imgPonteiro, taco);
barraForca.x = taco.x - 100;
barraForca.y = taco.y + taco.raio + 25;
103
5.2. Barra de ajuste de força
Casa do Código
barraForca.largura = 200;
barraForca.altura = 50;
}
Por enquanto, o loop de animação será feito através de uma função sim-
ples. Basicamente pintamos o Canvas de verde, desenhamos o taco e a barra
de força e chamamos o próximo ciclo:
function animar() {
// Fundo verde
context.save();
context.fillStyle = '#050';
context.fillRect(0, 0, canvas.width, canvas.height);
context.restore();
taco.desenhar();
barraForca.desenhar();
requestAnimationFrame(animar);
}
Para podermos testar no Console sem ver nada faltando, já crie o es-
queleto da classe Taco no arquivo taco.js:
function Taco(context, imagem) {
this.context = context;
this.imagem = imagem;
this.raio = 0;
this.x = 0;
this.y = 0;
this.rotacao = 0;
this.forca = 0;
}
Taco.prototype = {
desenhar: function() {
}
}
E o da classe BarraForca em barra-forca.js:
104
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
function BarraForca(context, ponteiro, taco) {
this.context = context;
this.ponteiro = ponteiro;
this.taco = taco;
this.x = 0;
this.y = 0;
this.largura = 0;
this.altura = 0;
this.forca = 0;
}
BarraForca.prototype = {
desenhar: function() {
}
}
Até aqui, teste no navegador.
O único erro deve ser a falta do
cordova.js.
Vamos desenhar a barra de força. Veja na figura 5.1 que ela possui dois
retângulos: um interno, representando a força atual, e um externo que de-
limita a área. O externo é desenhado na largura total da barra, saindo um
pouco fora de seus limites para não encobrir o outro. Já o interno é desen-
hado de acordo com a força atualmente setada no objeto multipliquei essa
força por 2 para a barra não ficar muito pequena. A posição do ponteiro é
calculada por outro método, posicaoPonteiro, já pensando que vamos
precisar dessa posição ao detectar o pan sobre ele:
desenhar: function() {
var ctx = this.context;
// Contêiner
ctx.strokeRect(this.x - 2, this.y - 2, this.largura + 4,
this.altura + 4);
// Força
ctx.save();
ctx.fillStyle = '#900';
105
5.2. Barra de ajuste de força
Casa do Código
ctx.fillRect(this.x, this.y, this.forca * 2, this.altura);
ctx.restore();
// Ponteiro
var pos = this.posicaoPonteiro();
ctx.drawImage(this.ponteiro, pos.x, pos.y,
this.ponteiro.width, this.ponteiro.height);
}, // Atenção para a vírgula
Em posicaoPonteiro, precisamos colocar a ponta da seta, que hor-
izontalmente está no meio da imagem, na mesma posição x que o final da barra de força. Por isso multiplicamos a força por 2 e descontamos metade
da largura da imagem. Já a posição y é na metade da barra, mas nada impede que seja um pouco acima ou abaixo:
posicaoPonteiro: function() {
var x = this.x + this.forca * 2 - this.ponteiro.width / 2;
var y = this.y + this.altura / 2;
return {x:x, y:y};
}
Atribua diferentes valores ao atributo forca, no construtor, e vá testando
no browser.. A barra interna e o ponteiro deverão se ajustar. Volte depois o
valor padrão para zero.
Vamos agora detectar o toque no ponteiro. Em iniciarObjetos, inicie
o Hammer.js e já o passe tanto para o taco quanto para a barra:
function iniciarObjetos() {
var hammer = new Hammer(canvas);
taco = new Taco(context, imgTaco, hammer);
// ...
barraForca =
new BarraForca(context, imgPonteiro, taco, hammer);
// ...
}
106
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
Faça o construtor da BarraForca recebê-lo e detectar os eventos de
pan. Não fizemos o ajuste da direção porque queremos movimentar somente na horizontal, que é o padrão permitido pelo evento:
function BarraForca(context, ponteiro, taco, hammer) {
// ...
var barra = this;
hammer.get('pan').set({ threshold: 0 });
hammer.on('panstart', function(e) {
barra.iniciarAjusteForca(e);
});
hammer.on('panmove', function(e) {
barra.ajustarForca(e);
});
hammer.on('panend', function(e) {
barra.finalizarAjusteForca(e);
});
}
No panstart, chamamos o método iniciarAjusteForca. Como
nosso Canvas é responsivo, temos primeiro que converter a posição do toque
em coordenadas da imagem processada em memória, como vimos em 3.3.
Quem fará isto é o método converterParaCanvas:
posicaoPonteiro: function() {
// ...
}, // Não esqueça da vírgula quando for acrescentar métodos!
iniciarAjusteForca: function(evento) {
var canvas = this.context.canvas;
// Coordenadas na imagem do Canvas
var toque = this.converterParaCanvas(
evento.center.x - canvas.offsetLeft,
evento.center.y - canvas.offsetTop
);
107
5.2. Barra de ajuste de força
Casa do Código
// continua...
},
Em seguida, verificamos se a posição tocada se encontra nos limites do
ponteiro. Se o ponteiro foi tocado, sinalizamos que estamos mexendo na força
e já chamamos o método que faz o ajuste:
iniciarAjusteForca: function(evento) {
// ...
// Posição do ponteiro
var pos = this.posicaoPonteiro();
var largura = this.ponteiro.width;
var altura = this.ponteiro.height;
// Testar os limites
if (toque.x >= pos.x && toque.x <= pos.x + largura &&
toque.y >= pos.y && toque.y <= pos.y + altura) {
this.ajustandoForca = true;
this.ajustarForca(evento);
}
}, // Ainda tem mais métodos...
Somente relembrando, converterParaCanvas faz uma regra de três
correspondendo às dimensões em tela do Canvas ( offsets) com as suas dimensões de imagem “real":
converterParaCanvas: function(x, y) {
var canvas = this.context.canvas;
return {
x: canvas.width * x / canvas.offsetWidth,
y: canvas.height * y / canvas.offsetHeight
}
},
Note que o método ajustarForca, além de ser o tratador do evento
panmove, foi também chamado no panstart para iniciar o movimento.
108
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
Testamos se um movimento válido (dentro dos limites) foi iniciado, converte-
mos a posição para o Canvas e setamos a força de acordo com a posição dentro
da barra. Observe que dividimos a nova medida por 2, pois a força é mostrada
em tela com o dobro do tamanho. Também limitamos a força entre zero e 100,
e setamos a força no objeto Taco associado:
ajustarForca: function(evento) {
// Está dentro do limite?
if (this.ajustandoForca) {
var canvas = this.context.canvas;
// Converter para Canvas
var toque = this.converterParaCanvas(
evento.center.x - canvas.offsetLeft,
evento.center.y - canvas.offsetTop
);
// Nova força
this.forca = (toque.x - this.x) / 2;
// Limites
if (this.forca < 0) this.forca = 0;
if (this.forca > 100) this.forca = 100;
// Força do taco
this.taco.forca = this.forca;
}
},
No evento panend, o método finalizarAjusteForca apenas realiza
o último movimento no ponteiro e sinaliza o fim da movimentação:
finalizarAjusteForca: function(evento) {
if (this.ajustandoForca) {
this.ajustarForca(evento);
this.ajustandoForca = false;
}
} // Esta classe acabou!
109
5.3. Rotacionando um taco de bilhar
Casa do Código
Faça o teste, tanto no browser como agora no aparelho móvel. Se você
fez tudo direitinho, já é possível mexer o ponteiro dentro do limite da barra, tanto com o mouse quanto na tela touch.
5.3
Rotacionando um taco de bilhar
Nesta seção, iremos nos divertir um pouco com o evento rotate do Ham-
mer.js. Forneceremos uma área circular, onde o jogador poderá definir o ân-
gulo do taco realizando uma rotação com dois dedos.
Primeiro, em taco.js, no método desenhar da classe, desenhe essa
área e o taco. Este possui 15 pixels de largura, portanto descontamos 7 pixels da posição x para ficar aproximadamente no meio. Na posição y, somamos
a força a ser aplicada: quanto maior a força, maior a distância:
desenhar: function() {
var ctx = this.context;
// Área de controle
ctx.beginPath();
ctx.arc(this.x, this.y, this.raio, 0, Math.PI*2);
ctx.stroke();
// Taco
ctx.drawImage(this.imagem, this.x - 7,
this.y + this.forca,
this.imagem.width,
this.imagem.height);
}
Já é possível controlar a força pela barra: o taco se move junto, experi-
mente! Isto ocorre porque, ao movimentar a barra de força, esta seta o atrib-
uto forca do taco.
Para desenhar o taco rotacionado, podemos usar o método rotate do
contexto gráfico, que rotaciona o Canvas. Considerando que queremos tra-
balhar em graus, precisamos converter a rotação para radianos. É muito im-
portante fazermos uso do save e do restore para desfazer a rotação assim
que o taco for desenhado:
110
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
desenhar: function() {
var ctx = this.context;
// Área de controle
ctx.beginPath();
ctx.arc(this.x, this.y, this.raio, 0, Math.PI*2);
ctx.stroke();
// Taco
var radianos = this.rotacao * Math.PI / 180;
ctx.save();
ctx.rotate(radianos);
ctx.drawImage(this.imagem, this.x - 7, this.y + this.forca,
this.imagem.width, this.imagem.height);
ctx.restore();
}
Experimente testar, no construtor, valores diferentes no atributo
rotacao. O taco fica meio deslocado! Se o valor for muito alto, ele chega
a sair da tela. Isso acontece porque o centro de rotação do Canvas é seu ponto de origem (0, 0):
111
5.3. Rotacionando um taco de bilhar
Casa do Código
Figura 5.4: O Canvas rotaciona pendurado no ponto (0, 0)
Felizmente, também temos o método translate, que desloca o ponto
(0, 0) para onde quisermos. Colocando a origem do Canvas na posição do
taco, podemos então rotacioná-lo. Repare nos parâmetros do drawImage:
com o Canvas transladado, é a partir do ponto (0, 0) que informamos o deslo-
camento do taco:
// Taco
var radianos = this.rotacao * Math.PI / 180;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(radianos);
ctx.drawImage(this.imagem, -7, this.forca,
this.imagem.width, this.imagem.height);
ctx.restore();
112
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
Figura 5.5: Deslocando a origem do Canvas, rotacionamos e fazemos o de-
senho a partir de (0, 0)
Capturando o evento rotate
Não se esqueça de resetar o atributo rotacao para zero. A rotação será
definida pela captura do evento rotate do Hammer.js. No construtor, con-
figure os eventos à semelhança do que fizemos com a barra de força:
function Taco(context, imagem, hammer) {
// ...
var taco = this;
hammer.get('rotate').set({ enable: true, threshold: 0 });
hammer.on('rotatestart', function(e) {
taco.iniciarRotacao(e);
});
hammer.on('rotatemove', function(e) {
taco.rotacionar(e);
});
hammer.on('rotateend', function(e) {
taco.finalizarRotacao(e);
});
}
113
5.3. Rotacionando um taco de bilhar
Casa do Código
Também semelhantemente à outra classe, vamos converter as posições
para o Canvas em iniciarRotacao. Só que agora são dois dedos. Nosso
objetivo é saber se os pontos tocados estão na área delimitada. Para isso,
chamamos o método naArea: se este retornar verdadeiro para os dois dedos,
sinalizamos o início da rotação:
// Atenção para as vírgulas!
iniciarRotacao: function(evento) {
var canvas = this.context.canvas;
var dedo1 = this.converterParaCanvas(
evento.pointers[0].pageX - canvas.offsetLeft,
evento.pointers[0].pageY - canvas.offsetTop
);
var dedo2 = this.converterParaCanvas(
evento.pointers[1].pageX - canvas.offsetLeft,
evento.pointers[1].pageY - canvas.offsetTop
);
if (this.naArea(dedo1.x, dedo1.y) &&
this.naArea(dedo2.x, dedo2.y)) {
this.rotacionando = true;
this.rotacaoInicial = this.rotacao;
this.rotacionar(evento);
}
},
O método converterParaCanvas é o mesmo:
converterParaCanvas: function(x, y) {
var canvas = this.context.canvas;
return {
x: canvas.width * x / canvas.offsetWidth,
y: canvas.height * y / canvas.offsetHeight
}
},
114
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
O método naArea verifica, pelo Teorema de Pitágoras, se um ponto per-
tence a uma área circular (figura 3.3):
naArea: function(x, y) {
var distanciaX = Math.abs(this.x - x);
var distanciaY = Math.abs(this.y - y);
// Pitágoras
if (distanciaX * distanciaX + distanciaY * distanciaY <=
this.raio * this.raio)
return true;
else
return false;
},
A rotação continua:
o método
rotacionar obtém o atributo
rotation do evento, que é dado em graus. Não podemos ir apenas somando
os valores pois, a cada rotatemove que chama este método, aumentamos
a rotação naquela quantidade de graus. Em vez disso, sempre partimos da
rotação inicial. Também, para simplificar, mantemos o valor dentro do limite
de 360 graus:
rotacionar: function(evento) {
if (this.rotacionando) {
var rotacao = this.rotacaoInicial + evento.rotation;
this.rotacao = rotacao % 360;
}
},
115
5.3. Rotacionando um taco de bilhar
Casa do Código
Arcos côngruos
Existe arco de 2100°? Sim, isto significa dar 5 voltas na circunferência
e andar mais 300°:
Figura 5.6: 2100° e 300° são côngruos, pois param no mesmo ponto
Se a nova rotação do taco de bilhar passar de 360°, o resto da sua di-
visão por 360 é um arco equivalente. Por isso usamos o operador módulo
( % ) do JavaScript, que devolve o resto de uma divisão de inteiros.
O método finalizarRotacao apenas faz o movimento final e sinaliza
que a rotação terminou:
finalizarRotacao: function(evento) {
if (this.rotacionando) {
this.rotacionar(evento);
this.rotacionando = false;
}
}
Tente fazer o movimento de rotação com 2 dedos dentro da área, e tam-
bém controle a força da tacada. Você pode achar que ainda não está muito
fácil rotacionar o taco. Vejamos por quê.
Capturando dois ou mais gestos juntos
É difícil fazer um rotate puro com os dedos, de forma que o Hammer.js detecte o gesto exatamente como um rotate. Com um pouco de treino, até se “pega o jeito”, mas não é exatamente isso que um jogador frustrado pode
116
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
pensar. A razão é que qualquer deslocamento mínimo dos dedos pode ser
interpretado como um pinch.
Podemos tornar o movimento mais suave solicitando que os dois gestos
sejam reconhecidos juntos. No construtor, antes de atribuir os tratadores de
evento, obtenha referências ao pinch e ao rotate e, em seguida, chame o
método recognizeWith em um deles, passando o outro como parâmetro:
function Taco(context, imagem, hammer) {
// ...
// Configuração dos gestos para serem reconhecidos juntos
var pinch = hammer.get('pinch');
var rotate = hammer.get('rotate');
hammer.get('pinch').set({ enable: true, threshold: 0 });
hammer.get('rotate').set({ enable: true, threshold: 0 });
pinch.recognizeWith(rotate);
// Eventos
var taco = this;
hammer.on('rotatestart', function(e) {
taco.iniciarRotacao(e);
});
// ...
}
Experimente novamente. Ficou mais fácil, não?
5.4
Animando a tacada
Vamos disparar o taco com toda a força! No index.html, acrescente uma
imagem após a tag <canvas>. Já há uma imagem no pacote, mas você pode
escolher uma de seu gosto:
<img src="img/tacada.png" id="tacada">
No CSS, posicione-a onde quiser:
117
5.4. Animando a tacada
Casa do Código
#tacada {
position: absolute;
right: 20px;
top: 20px;
}
Na função iniciarObjetos, vamos programar o touchstart da im-
agem. No callback, preparamos o som e iniciamos a tacada, com duração de
50ms:
function iniciarObjetos() {
// ...
var imgTacada = document.getElementById('tacada');
imgTacada.addEventListener('touchstart', function() {
somTacada.seekTo(0);
taco.darTacada(50);
});
}
Não esqueça de descomentar a linha que instancia o som na função
iniciar:
somTacada = new Media('file:///android_asset/colisao.mp3');
De volta à classe Taco, o método darTacada vai iniciar indiretamente
uma animação, apenas sinalizando que a tacada está ocorrendo. Por isso,
guardamos o instante em que a tacada começou, para calcular a posição do
taco no decorrer da animação:
darTacada: function(duracao) {
this.tacada = true;
this.duracaoTacada = duracao;
this.inicioTacada = Date.now();
},
Podemos disparar animações apenas setando um atributo de um objeto,
se em algum lugar no seu ciclo de animação ele se encarregar de ler esse atributo. Ao desenhar a imagem, em vez de deslocar o taco em y pelo montante 118
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
da força, vamos chamar um método, deslocamento, que verificará se a
animação da tacada está ocorrendo:
desenhar: function() {
// ...
ctx.drawImage(this.imagem, -7, this.deslocamento(),
this.imagem.width, this.imagem.height);
// ...
},
Nesse novo método, primeiro verificamos se a animação está ocorrendo.
Caso contrário, usamos a força para deslocar o taco:
deslocamento: function() {
// Não foi disparada tacada
if (! this.tacada) return this.forca;
// continua ...
}
Continuando, para calcular o deslocamento temos que saber quanto
tempo se passou desde o início da animação. Se a força é 100 e quero deslocar
em 50ms, dá 2 unidades de força por milissegundo (100/50). Multiplico isso
pelo tempo decorrido e sei quanto o taco vai se deslocar naquele quadro:
// Tempo decorrido
var agora = Date.now();
var decorrido = agora - this.inicioTacada;
// Deslocamento acumulado
var distancia = this.forca / this.duracaoTacada * decorrido;
Depois, verificamos se o tempo excedeu. Em caso positivo, podemos
deslocar o taco no montante exato da força e finalizar a animação. A nova
posição do taco será a força (afastamento máximo) menos o deslocamento
atual (quanto o taco volta em direção ao centro do círculo):
119
5.4. Animando a tacada
Casa do Código
// Completou
if (decorrido >= this.duracaoTacada) {
distancia = this.forca;
this.tacada = false;
}
return this.forca - distancia;
Quando um evento se encerra, podemos receber uma notificação para
começar outro. Por exemplo, quando a tacada terminar, o jogo pode receber
um aviso para começar a animação das bolas na mesa:
// Completou
if (decorrido >= this.duracaoTacada) {
distancia = this.forca;
this.tacada = false;
// Notificar o jogo
if (this.aposTacada) this.aposTacada();
}
No index.html passamos um callback para a tacada:
function iniciarObjetos() {
// ...
taco.aposTacada = tacada;
}
E criamos esse callback. Aqui, vamos apenas reproduzir o som:
function tacada() {
somTacada.play();
}
Também convém impedir uma tacada de começar enquanto outra estiver
em andamento:
darTacada: function(duracao) {
if (this.tacada) return; // Já está em uma tacada
120
Casa do Código
Capítulo 5. Interações avançadas com Hammer.js
this.tacada = true;
this.duracaoTacada = duracao;
this.inicioTacada = Date.now();
},
Tudo pronto, pode testar no aparelho móvel! Guarde o código deste capí-
tulo com carinho, pois será reutilizado quando formos dar sequência ao jogo.
E aqui finalizamos a primeira parte do livro, sobre plataformas móveis!
Na próxima parte, trataremos de física e a biblioteca Box2dWeb, essencial para quem pretende levar a sério a área de games. Ela será responsável por toda a
ação em cima da mesa de bilhar.
121
Parte II
Física
Capítulo 6
Introdução ao Box2dWeb
Façamos um rápido exercício mental. Com o que temos, o que é necessário
para fazer um herói pular?
Basicamente, deslocá-lo para cima por um instante, e depois, de volta para
baixo. Mas claro que não é só isso. Se, na subida, ele se chocar com o teto ou uma plataforma, o movimento deve se inverter antecipadamente. Na descida,
a colisão com o chão é responsável por parar o movimento. Fora que podemos
ter animações de sprite diferentes para a subida e para a descida.
Casa do Código
Figura 6.1: Uma colisão alterando o sentido de movimento de um sprite.
Por mais complexo que pareça, isso pode ser feito de maneira relativa-
mente simples se soubermos definir bem os estados do sprite: dividir seus movimentos em etapas (os tais estados), sendo que em cada uma ele se comporta e se desenha de maneira diferente, e gerenciar as transições de estado, ou seja, detectar momentos quando o sprite passa de um estado a outro. Caso
queira saber mais sobre gerenciamento de estados, falei sobre isso em meu
livro anterior, no capítulo 4 - Spritesheets.
Um recurso muito utilizado, no entanto, é o uso de engines físicas, ou
motores de física. São bibliotecas, frameworks, que pretendem simular física
realista em um computador, poupando o programador de conhecer fórmu-
las e inúmeros detalhes. No mundo físico, nosso herói é simplesmente um
corpo, no qual aplicamos uma força de baixo para cima, fazendo-o saltar. A gravidade se encarrega de fazê-lo voltar para o chão, e a colisão com este é tratada automaticamente pelo engine, que o faz parar ou até mesmo quicar,
se desejarmos.
Esta é a proposta do Box2D, biblioteca desenvolvida em C++ por Erin
Catto, programador na Blizzard Entertainment. O primeiro protótipo foi cri-
ado como uma demonstração na Game Developers Conference, em 2006, e
desde então a biblioteca foi portada para várias outras linguagens, tamanha foi sua aceitação. A API é praticamente idêntica em C++, Java, C#, Lua, ActionScript e JavaScript. Para esta última, iremos usar o port chamado Box2dWeb, convertido a partir da Box2DFlash desenvolvida em ActionScript.
126
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
Download
Faça o download da Box2dWeb em:
https://code.google.com/p/box2dweb/downloads/list
Existe outro port da Box2D para JavaScript, o Box2DJS. Ele não é atualizado há bastante tempo e requer outras bibliotecas, mas acho interessante
mencioná-lo porque na sua homepage você pode ver exemplos da engine em
ação:
http://box2d-js.sourceforge.net/
Convenceu-se? Um leque de possibilidades abre-se para nós a partir de
agora.
6.1
Um primeiro tutorial
Embora não seja uma engine difícil de usar, a Box2D ou seus ports requerem
alguma prática e podem assustar o programador iniciante. A API é composta
de inúmeros objetos, e é fundamental saber o papel de cada um na engine.
A intenção deste capítulo é mostrar-lhe uma teoria básica, acompanhada de
sua aplicação prática. Trabalharemos com o mundo físico, gravidade, corpos,
fixtures e formas.
Este exercício pode ser feito como uma simples página web, embora no
pacote de download esteja configurado como um projeto Cordova, de nome
box2dweb-intro. Segue o esqueleto da index.html. O Canvas possui
tamanho fixo, pois não queremos neste momento poluir o código com outras
preocupações que não sejam sobre a Box2dWeb:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
height=device-height, user-scalable=no,
initial-scale=1, maximum-scale=1, minimum-scale=1">
127
6.2. Objetos fundamentais
Casa do Código
<title>Introdução ao Box2dWeb</title>
<script src="cordova.js"></script>
<script src="rAF.js"></script>
<script src="Box2dWeb-2.1.a.3.min.js"></script>
<script>
// Inicialização aqui
</script>
</head>
<body>
<canvas id="box2dweb" width="640" height="480"></canvas>
</body>
</html>
A inicialização não é diferente do que já vimos:
var canvas, context;
window.onload = function() {
canvas = document.getElementById('box2dweb');
context = canvas.getContext('2d');
//document.addEventListener('deviceready', iniciar);
iniciar();
}
function iniciar() {
// Aqui começará a física realista!
}
Esqueleto pronto, vamos entrar nos conceitos da engine, uma etapa por
vez.
6.2
Objetos fundamentais
128
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
Mundo ( b2World)
O primeiro objeto criado é sempre o mundo, que representa o espaço onde os corpos interagem. Normalmente só existe um mundo físico por jogo, pois a
necessidade de processamento é elevada.
Figura 6.2: O mundo físico em um jogo
É preciso ressaltar que esse mundo existe apenas na memória do com-
putador. A cada quadro de animação, nós avançamos o mundo físico no
tempo e a engine recalcula as posições dos objetos dentro dele, detecta col-
isões, entre outras coisas, de acordo com as forças que estiverem atuando em
cada corpo. Cabe a nós obter as posições de todos os corpos e desenhar os
sprites.
A Box2dWeb preserva o sistema de espaços de nomes presente na en-
gine original, na forma de objetos que agrupam outros objetos. Por ex-
emplo, para criar um b2World, teríamos que referenciar o construtor
Box2D.Dynamics.b2World:
Para reduzir a complicação, o comum é obter referências a esses constru-
tores no início do código. Posteriormente, usamos essa referência para in-
stanciar os objetos da engine:
129
6.2. Objetos fundamentais
Casa do Código
// Variáveis do jogo
var canvas, context, mundo;
// ...
// Criando o mundo
function iniciar() {
mundo = new b2World( ... ); // O que passamos aqui?
}
O construtor b2World requer os parâmetros:
• um objeto b2Vec2 representando a gravidade (veja logo a seguir);
• um booleano ( true/ false) indicando se os corpos podem dormir
durante a simulação.
Um corpo “dorme” quando está parado e não há forças atuando sobre ele,
e a engine o “acorda” automaticamente se isso acontecer. Um corpo dormindo
é ignorado pela engine durante esse estado, o que economiza processamento.
Vetor ( b2Vec2)
Para podermos criar um mundo físico, temos que passar para ele um vetor
representando a gravidade. Em Física, um vetor é toda grandeza que possui direção e sentido. Por exemplo, a gravidade possui direção vertical e sentido
para baixo. A direção de um vetor pode ser diagonal, mas o mais comum é
pensarmos separadamente nos componentes horizontal ( x) e vertical ( y).
130
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
Figura 6.3: Uma trajetória é um vetor, portanto pode ser decomposta em x e
y
A gravidade atua somente para baixo, com determinado valor y, enquanto o valor x é zero. A gravidade da Terra é de 9,81 metros por segundo ao quadrado, ou seja, a velocidade de um corpo em queda livre, sem a resistência
do ar, aumenta 9,81 metros por segundo a cada segundo.
A Box2D e seus ports representam grandezas vetoriais através do objeto
b2Vec2. Seu construtor recebe os valores nos eixos x e y. Para criar um mundo com essa gravidade, permitindo que os corpos durmam, temos que
fazer:
131
6.2. Objetos fundamentais
Casa do Código
Definições de corpos e fixtures ( b2BodyDef e b2FixtureDef ) Dentro do mundo, o corpo representa um objeto indivisível sujeito à ação de forças. Uma bola, um homem, uma pedra são corpos no mundo físico.
Um corpo é formado por uma ou mais fixtures, que são suas unidades
materiais. Cada fixture possui uma forma e propriedades como densidade,
atrito e elasticidade.
Figura 6.4: Corpo físico e suas unidades (fixtures)
Fixture: fixação?
fixture fix.ture 1 fixação, fixidez. 2 pessoa ou coisa permanentemente ligada a um lugar. 3 acessório, pertence, instalação. [...]
Fonte: http://michaelis.uol.com.br/moderno/ingles/
Uma “fixture” pode ser entendida como algo que é fixo em uma estru-
tura, como uma peça de um carro ou um relógio pendurado na parede:
ambos ficam imóveis na estrutura do carro ou da construção. Muitas
traduções são dadas para a palavra, como “fixação” ou “acessório”, mas
nenhuma me soa bem em português, razão por que opto em usar o termo
em inglês: fixture.
Esses elementos são representados na engine por b2Body e b2Fixture,
mas nós não os instanciamos diretamente. O que criamos são objetos de
132
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
definição, que contêm as informações usadas pela engine para criar os ver-
dadeiros corpos e fixtures. Os objetos de definição são b2BodyDef e
b2FixtureDef:
// Construtores
// ...
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
Para criar uma definição de corpo, o mínimo necessário é definir sua
posição (x, y) em metros. Para passar valores em pixels, divida-os por uma
escala:
// Variáveis do jogo
// ...
var escala = 3 0; // pixels por metro
function iniciar() {
// ...
// Definição de corpo
var corpoDef = new b2BodyDef();
corpoDef.position.Set(200/escala, 100/escala);
// continua ...
}
133
6.2. Objetos fundamentais
Casa do Código
Medidas no Box2D
Um detalhe muito importante é o uso das unidades metro, quilo-
grama e segundo nas simulações físicas. Nossa atenção se voltará principalmente para os comprimentos, que sempre devem ser convertidos
de pixels para metros, ao configurar os objetos, e de metros para pix-
els novamente, ao ler suas posições. Isso não é difícil de se fazer: basta
definirmos uma escala, um número de pixels por metro a ser usado
na simulação:
var escala = 30;
Ao criar um objeto, dividimos o tamanho em pixels desejado pela
escala. Um exemplo simples: se a escala for de 100 pixels por metro, um
objeto de 50 pixels terá meio metro (50/100).
var larguraMetros = 25 / escala; // 25 pixels
Ao obter a posição ou tamanho de um objeto em metros, fazemos o
processo inverso: multiplicamos o valor pela escala para obter em pixels:
var larguraMetros = ...;
var larguraPixels = larguraMetros * escala;
Para dar formato ao corpo, criamos uma ou mais fixtures. O mínimo
necessário é dar à fixture uma forma, que corresponde a uma figura ge-
ométrica simples. No próximo capítulo veremos as opções de formas exis-
tentes. Combinando várias fixtures, podemos obter um corpo com formato
mais complexo. Aqui, criamos uma forma circular de 25 pixels de raio:
function iniciar() {
// ...
// Definição de fixture
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = new b2CircleShape(25/escala);
134
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
// continua...
}
Claro, acrescente a referência ao construtor b2CircleShape no início
do código:
// Construtores
// ...
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
Por fim, temos que criar o corpo no mundo e criar nele a fixture:
function iniciar() {
// ...
// Criar o corpo
var bola = mundo.CreateBody(corpoDef);
bola.CreateFixture(fixtureDef);
// continua...
}
Renderização gráfica de debug ( b2DebugDraw)
Como dito anteriormente, o mundo físico existe somente em memória. A
engine física não realiza tarefas de renderização. Não podemos simplesmente
passar imagens para ela e deixar que ela cuide disso para nós.
No entanto, a Box2D original oferece a possibilidade de se implemen-
tar um Debug Draw: um renderizador simples, voltado para testes, que nos permitiria acompanhar o que está acontecendo no mundo físico. Certamente, há inúmeros deles implementados por aí, para cada port da engine.
A Box2dWeb já vem com um Debug Draw pronto para uso, herdado do
Box2DFlash.
135
6.2. Objetos fundamentais
Casa do Código
Figura 6.5: Debug Draw renderizando o mundo físico com desenhos simples
As vantagens de se usar o Debug Draw são:
• poder trabalhar no desenvolvimento enquanto a equipe de arte tra-
balha nos gráficos;
• ajuda a localizar melhor a origem de bugs. Se o Debug Draw desenha
os objetos nas posições corretas, o erro é na sua lógica de renderização
de sprites; caso contrário, é na lógica física.
Ainda na função iniciar, a criação de um Debug Draw envolve as con-
figurações a seguir. Destaque para o método SetFlags, onde configuramos
que componentes da engine queremos desenhar. A variável e_shapeBit
solicita o desenho das formas associadas às fixtures:
136
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
mundo.SetDebugDraw(debugDraw);
}
Não se esqueça de adicionar a referência ao construtor b2DebugDraw:
// Construtores
// ...
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
Os objetos de nosso mundo estão configurados. Precisamos agora fazer a
física acontecer nele.
6.3
Animação do mundo físico
Avançando o tempo na animação
Vamos terminar a função iniciar disparando a animação. Temos de con-
trolar quanto tempo há entre um ciclo e outro, para saber quanto tempo
avançar no mundo físico. Por isso, guardamos a hora atual na variável
ultimoCiclo:
function iniciar() {
// ...
// Iniciar a animação
ultimoCiclo = Date.now();
requestAnimationFrame(animar);
}
A função de animação necessita avançar o mundo físico no tempo. O
método Step do mundo pede um tempo em segundos, por isso dividimos o
valor que temos (em milissegundos) por 1000. Os valores 8 e 3 representam
os números de iterações que o mundo vai realizar para movimentar os obje-
tos e para verificar sobreposições, respectivamente. Quanto maior o número
de iterações, maior a acurácia e menor o desempenho. Esses são os valores
recomendados pelo próprio Erin Catto no manual da Box2D, e podem ser
usados como padrão caso não tenhamos excesso de objetos e o desempenho
seja satisfatório.
137
6.3. Animação do mundo físico
Casa do Código
Tendo avançado o tempo, chamamos o método DrawDebugData do
mundo para desenhar as formas no Canvas pelo objeto Debug Draw:
Rode o projeto. Vemos um círculo estático no Canvas, com uma linha
traçada no raio. Essa linha serve para visualizarmos a rotação da forma cir-
cular, quando forem aplicadas forças no corpo.
Figura 6.6: Nosso corpo não se move!
Corpos estáticos e dinâmicos
Por padrão, os corpos são estáticos, isto é, ficam parados no seu lugar.
Servem de barreira para outros corpos se movimentando, mas eles mesmos
não reagem a estímulos, como colisões ou aplicação de forças.
Se quisermos que nossa bola caia em função da gravidade,
temos que transformá-la em um corpo dinâmico.
No trecho em
que criamos o
b2BodyDef, vamos setar o atributo
type como
b2Body.b2_dynamicBody:
138
Casa do Código
Capítulo 6. Introdução ao Box2dWeb
E, claro, referenciar b2Body:
// Construtores
// ...
var b2Body = Box2D.Dynamics.b2Body;
Fiz as referências uma a uma, conforme necessário, para você tomar con-
tato com os principais objetos do Box2dWeb. Nossos próximos projetos já
podem começar com um conjunto de referências pronto.
Agora a bola cai! Repare que ela mudou de cor: objetos dinâmicos são
representados em roxo pelo Debug Draw, enquanto os estáticos eram verdes.
Se você achar que ela está caindo muito depressa, diminua o valor y do vetor da gravidade e note que ela cairá mais devagar. Faça experiências: coloque
um valor negativo e veja-a subir; atribua um valor ao eixo x e ela se deslocará para o lado.
Desempenho da animação: requestAnimationFrame x
setTimeout
O Box2dWeb trabalha bem com o velho
setTimeout, al-
iás, foi feito para ser usado com ele!
Faz pouco tempo que o
requestAnimationFrame passou a ser adotado em larga escala pelos
browsers.
O uso comum é avançarmos 60 quadros por segundo, dividindo 1000
milissegundos por 60:
setTimeout(animar, 1000/60);
Já o avanço do tempo no Box2dWeb é dado em segundos, ou seja, 1
segundo dividido por 60:
mundo.Step(1/60, 8, 3);
No próximo capítulo, você aprenderá que aplicando forças a um corpo
podemos mudar a sua trajetória. Uma bola pode ser arremessada para cima,
139
6.3. Animação do mundo físico
Casa do Código
subindo até uma altura máxima de acordo com a força aplicada, e a gravidade
a fará descer novamente. Se quisermos, podemos parar o movimento em uma
altura determinada, e deixá-la à mercê da gravidade. Se a bola for lançada
em sentido diagonal, a engine automaticamente traça uma trajetória em arco.
Também podemos movimentar um corpo manualmente, como se fosse a nave
ou os OVNIs do capítulo 4.
Figura 6.7: Um objeto dinâmico aparece roxo no Debug Draw
Sugiro que estude bastante estes conceitos básicos, pois eles são a base de
tudo o que faremos com o Box2dWeb. No próximo, vamos conhecer todas as
formas que podem ser usadas, aprender a configurar propriedades físicas dos
objetos e aplicar forças, impulsos e velocidade neles.
Documentação original da Box2D
O funcionamento da engine e a sua API foram descritas pelo próprio
criador, Erin Catto, em:
http://box2d.org/manual.pdf
Sugestão de leitura
Box2D for Flash Games, de Emanuele Feronato ([1]).
140
Capítulo 7
Configurações e movimento de
corpos
No capítulo anterior, aprendemos a criar um mundo físico e as etapas
necessárias para colocar um corpo nele. Só esse “básico” já exige de nós con-
hecer a sequência:
1) criar mundo;
2) criar definição de corpo ( b2BodyDef);
3) criar definição de fixture ( b2FixtureDef);
4) criar uma forma e associá-la ao b2FixtureDef;
5) criar o corpo e sua fixture a partir das definições.
7.1. Formas
Casa do Código
Indo um passo além, vamos conhecer mais detalhes e diversas opções que
temos em cada um desses passos. Talvez você esteja se perguntando por que
temos que criar fixtures, em vez de associar a forma diretamente ao corpo.
Veremos que elas são mais do que um simples intermediário entre o corpo e
uma forma.
7.1
Formas
O corpo que criamos no último exercício tinha forma redonda, criada através
de um b2CircleShape. Essa forma é atribuída à propriedade shape da
definição da fixture:
fixtureDef.shape = new b2CircleShape(25/escala);
Vamos criar uma nova página web. No pacote de download, este projeto
se encontra na pasta formas, já configurada para o Cordova. Os arquivos de
script indicados podem ser obtidos do pacote ou dos projetos anteriores:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
height=device-height, user-scalable=no,
initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>Formas</title>
<script src="cordova.js"></script>
<script src="rAF.js"></script>
<script src="Box2dWeb-2.1.a.3.min.js"></script>
<script>
// Inicialização aqui
</script>
</head>
<body>
<canvas id="formas" width="640" height="480"></canvas> 142
Casa do Código
Capítulo 7. Configurações e movimento de corpos
</body>
</html>
Vamos iniciar o código fazendo referência aos principais construtores e
criando o mundo físico:
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2World = Box2D.Dynamics.b2World;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var b2Body = Box2D.Dynamics.b2Body;
var canvas, context, mundo;
var escala = 30; // pixels por metro
window.onload = function() {
canvas = document.getElementById('formas');
context = canvas.getContext('2d');
mundo = new b2World(new b2Vec2(0, 9.81), true);
debugDraw();
iniciar();
//document.addEventListener('deviceready', iniciar);
}
Abstraímos para a função debugDraw a tarefa de configurar a render-
ização padrão de debug. Este código pode ser copiado a partir do projeto do
capítulo anterior:
function debugDraw() {
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(context);
debugDraw.SetDrawScale(escala);
debugDraw.SetFillAlpha(0.6);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit); // Formas!!
143
7.1. Formas
Casa do Código
mundo.SetDebugDraw(debugDraw);
}
Retângulos básicos (caixas)
Começaremos criando algumas paredes delimitando a área do Canvas,
para que outros corpos não consigam sair. Essas paredes serão corpos estáti-
cos de formato retangular:
Figura 7.1: Formas retangulares
No Box2D e seus ports, temos dois tipos de retângulos, sendo que ambos
são definidos a partir do centro:
• Caixas: retângulos básicos, que têm a posição do corpo como centro;
• Caixas orientadas: podem ter inclinação e seu centro pode ser deslo-
cado relativamente à posição do corpo.
144
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Figura 7.2: Caixa comum (centro) e caixas orientadas inclinadas e deslocadas
a partir do centro do corpo
Vamos usar primeiro as caixas comuns, para criar as paredes. Como
são quatro, convém criar uma função separada, parede, a ser chamada em
iniciar. Passamos para ela as coordenadas e dimensões que queremos em
pixels:
Podemos começar a função parede ajustando os valores conforme a es-
cala definida. Lembre-se que o mundo físico requer os dados em metros, não
em pixels:
145
7.1. Formas
Casa do Código
altura /= escala;
// continua...
}
Se estamos recebendo x e y como o canto superior esquerdo, temos que somar metade da largura e da altura para determinar o centro:
function parede(x, y, largura, altura) {
// ...
// Meia caixa
var meiaLargura = largura / 2;
var meiaAltura = altura / 2;
var corpoDef = new b2BodyDef();
corpoDef.position.Set(x + meiaLargura, y + meiaAltura);
// continua...
}
Caixas são criadas usando b2PolygonShape. O método SetAsBox
cria uma caixa simples, tomando a posição do corpo como centro. Devemos
passar para ele as metades da largura e da altura que queremos. Depois disso, basta criar a fixture com a forma e o corpo no mundo:
function parede(x, y, largura, altura) {
// ...
// Formato retangular
var forma = new b2PolygonShape();
forma.SetAsBox(meiaLargura, meiaAltura);
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
// Criar
var corpo = mundo.CreateBody(corpoDef);
corpo.CreateFixture(fixtureDef);
}
146
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Para podermos ver os corpos, vamos chamar a função de animação ao
fim de iniciar:
function iniciar() {
// ...
ultimoCiclo = Date.now();
requestAnimationFrame(animar);
//setTimeout(animar, 1000/60);
}
Procure
depois
fazer
experiências
tanto
com
requestAnimationFrame quanto com o setTimeout. Com poucos
corpos, você não notará grandes diferenças, mas no jogo de bilhar você se
surpreenderá ao ver que o setTimeout apresentará melhor desempenho.
A função de animação simplesmente avança o tempo decorrido, desenha
o Debug Draw e chama o próximo ciclo:
function animar() {
var agora = Date.now();
var decorrido = agora - ultimoCiclo;
mundo.Step(decorrido/1000, 8, 3);
mundo.DrawDebugData();
ultimoCiclo = agora;
requestAnimationFrame(animar);
//setTimeout(animar, 1000/60);
}
As paredes já podem ser vistas!
Retângulos orientados
Se só pudéssemos criar retângulos a partir do centro do corpo, fi-
caríamos de mãos atadas. Para corpos complexos, com várias fixtures, quer-
emos definir a posição de cada uma. O método SetAsOrientedBox, do
b2PolygonShape, permite-nos posicionar a forma relativamente ao centro
do corpo e também definir um ângulo de inclinação.
147
7.1. Formas
Casa do Código
Vamos criar um corpo em forma de “X”. Para isso, precisamos de dois
retângulos inclinados: um em sentido horário, outro em sentido anti-horário:
Figura 7.3: Formas retangulares orientadas com inclinação
Em iniciar, chame a função xis. Pode ser antes ou depois das pare-
des, não importa. Ele receberá as coordenadas do centro, o comprimento do
braço (meia-largura do retângulo), a espessura (meia-altura) e o ângulo de
inclinação:
Comece a função
xis criando as caixas retangulares.
O método
SetAsOrientedBox recebe a meia-largura, a meia-altura, a posição rela-
tiva ao centro do corpo e o ângulo de inclinação. Para a posição, criamos um b2Vec2 com valores zerados, para colocá-los exatamente no centro do
corpo. Repare que um dos ângulos de inclinação deve ter sinal negativo, para
inverter o sentido:
148
Casa do Código
Capítulo 7. Configurações e movimento de corpos
var ret1 = new b2PolygonShape();
ret1.SetAsOrientedBox(braco/escala, espessura/escala,
posicao, angulo);
var ret2 = new b2PolygonShape();
ret2.SetAsOrientedBox(braco/escala, espessura/escala,
posicao, -angulo);
// continua...
}
Por fim, basta criar o corpo. Em caso de corpos com várias fixtures, você
pode usar o mesmo b2FixtureDef, apenas alterando as propriedades con-
forme cria cada fixture:
function xis(x, y, braco, espessura, inclinacao) {
// ...
// Criar corpo e fixtures
var bodyDef = new b2BodyDef();
bodyDef.position.Set(x/escala, y/escala);
var corpo = mundo.CreateBody(bodyDef);
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = ret1;
corpo.CreateFixture(fixtureDef);
fixtureDef.shape = ret2; // Mudando a forma do b2FixtureDef
corpo.CreateFixture(fixtureDef);
}
O cenário já tem um obstáculo!
Polígonos
Claro que o b2PolygonShape, tendo o nome que tem, não serve ape-
nas para criar retângulos. O Box2D e ports permitem a criação de polígonos
convexos, sem “aberturas”. Polígonos côncavos não podem ser criados direta-
mente, mas podemos juntar dois ou mais convexos para formar um côncavo:
149
7.1. Formas
Casa do Código
Figura 7.4: Polígonos convexos e côncavos
Convexo e côncavo
Se você, como eu, é daqueles que precisam parar para pensar quando
tem que distinguir convexo de côncavo, vai aí uma dica: convexo é o lado
de trás da colher. O lado que tem a abertura é côncavo (“com cavidade”).
Figura 7.5: Lados convexo e côncavo da colher
Pense sempre nas costas da colher ao criar um polígono no
Box2dWeb.
150
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Vamos criar um triângulo. Em iniciar, chame uma nova função,
triângulo:
function iniciar() {
triangulo();
// ...
}
Nessa nova função, temos que criar um array de pontos que formarão
os vértices do polígono. O Box2dWeb processará esses pontos em sentido
horário, portanto, caso haja erros na definição de seus polígonos, confira se esses pontos, sendo percorridos dessa forma, formam a figura desejada. Os
valores continuam sendo relativos ao centro do corpo. Esses pontos são passa-
dos para o método SetAsVector, que também recebe o número de vértices
a serem processados:
function triangulo() {
var pontos = [
new b2Vec2(0, -30/escala),
new b2Vec2(30/escala, 0),
new b2Vec2(-30/escala, 0)
]
var forma = new b2PolygonShape();
forma.SetAsVector(pontos, 3);
// continua...
}
Proceda à criação do corpo. Para vê-lo caindo, vamos torná-lo dinâmico:
function triangulo() {
// ...
// Corpo
var corpoDef = new b2BodyDef();
corpoDef.type = b2Body.b2_dynamicBody;
corpoDef.position.Set(320/escala, 100/escala);
151
7.2. Movimentando corpos
Casa do Código
Já é possível ver o corpo triangular caindo sobre o “X” pela ação da gravi-
dade. E o que é mais interessante, a colisão entre diferentes formas já é tratada pelo Box2dWeb!
Mas... e se quisermos dar-lhe algum trajeto diferente?
Figura 7.6: Corpo poligonal
7.2
Movimentando corpos
Existem três formas de movimentar corpos: aplicando forças, aplicando im-
pulsos ou setando a velocidade diretamente. Vamos criar um novo projeto
para testar essas três formas, de nome movimento. O esqueleto e o script
de inicialização do Box2dWeb e do Canvas são os mesmos do projeto ante-
rior (7.1), por isso não vou repeti-los aqui. Copie-os e também as funções
debugDraw e animar.
Vou começar a explicação pela função iniciar, onde vamos criar uma
bolinha e um chão no qual ela pode se apoiar. Precisamos guardar uma refer-
ência à bola se quisermos movimentá-la depois.
152
Casa do Código
Capítulo 7. Configurações e movimento de corpos
chao(320, 470, 10);
ultimoCiclo = Date.now();
// Escolha qual tivermelhordesempenho para SEU jogo
//requestAnimationFrame(animar);
setTimeout(animar, 1000/60);
}
Adicione a bola às declarações de variáveis da página:
var canvas, context, mundo, bola;
var escala = 30; // pixels pormetro
A função criarBola não tem novidades: ela cria a forma circular, as
definições e o corpo a partir delas. O corpo criado é retornado para que a
referência possa ser guardada:
function criarBola(x, y, raio) {
var forma = new b2CircleShape(raio/escala);
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
var corpoDef = new b2BodyDef();
corpoDef.type = b2Body.b2_dynamicBody;
corpoDef.position.Set(x/escala, y/escala);
var corpo = mundo.CreateBody(corpoDef);
corpo.CreateFixture(fixtureDef);
return corpo;
}
Para a função chao não ficar muito confusa, já recebemos as coordenadas
como o centro do retângulo. A largura será a mesma do Canvas e a altura é
o parâmetro espessura. Estes valores devem ser divididos por 2, pois o
SetAsBox recebe meia-largura e meia-altura:
153
7.2. Movimentando corpos
Casa do Código
Já podemos ver a bola sobre o chão:
Figura 7.7: Bola caindo no chão
Aplicando forças
Vamos, a cada quadro da animação, aplicar uma força na bola,
fazendo-a subir. Para isso, vamos obter seu centro de massa, dado pelo
método GetWorldCenter, e aplicar a força nesse ponto, com o método
ApplyForce:
A bola faz um movimento acelerado para cima, como um foguete! Uma
força deve ser aplicada continuamente, por isso nós a aplicamos dentro do
loop de animação.
154
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Aplicando impulsos
Se aplicarmos a força apenas uma vez, seu efeito será inócuo. No entanto,
podemos aplicar um impulso uma única vez, e deixar que a gravidade atue a
partir daí.
Retire ou comente as linhas que aplicam a força em animar. Agora,
vamos aplicar um impulso único em iniciar:
function iniciar() {
// ...
var centro = bola.GetWorldCenter();
bola.ApplyImpulse(new b2Vec2(3, -10), centro);
}
Com valores x e y no vetor, a bola dá um salto para o lado!
Alterando a velocidade
Podemos também setar diretamente a velocidade da bola com o método
SetLinearVelocity, sem depender do centro de massa:
function iniciar() {
// ...
bola.SetLinearVelocity(new b2Vec2(10, -200));
}
Movimentando arbitrariamente
A posição de um corpo também pode ser alterada como se fosse um sprite
comum, com a vantagem de já termos as colisões com outros corpos proces-
sadas pela engine física. Para isso, usamos o método SetPosition. Se quis-
ermos deslocar relativamente à posição atual, usamos antes GetPosition:
function animar() {
var posicao = bola.GetPosition();
posicao.y -= 1; // Movimento de subida
bola.SetPosition(posicao);
155
7.3. Propriedades físicas
Casa do Código
// ...
}
7.3
Propriedades físicas
A bola cai no chão, porém não quica. Isso ocorre porque nem ela, nem o
chão possuem elasticidade, uma propriedade física que provoca um impulso
quando um corpo colide com outro.
As propriedades físicas são configuradas na fixture, ou seja, cada fixture
de um corpo pode ter propriedades físicas diferentes. São elas: elasticidade (restituição), atrito e densidade.
Você pode testar a elasticidade no último projeto trabalhado, desde que
esteja utilizando o impulso único, disparado em iniciar. Deixe no código
as linhas indicadas, e comente ou retire qualquer outra mudança de veloci-
dade ou posição e aplicação de força:
function iniciar() {
// ...
var centro = bola.GetWorldCenter();
bola.ApplyImpulse(new b2Vec2(3, -10), centro);
}
Na função
criarBola, defina o atributo
restitution da
b2FixtureDef. Este atributo deve receber um valor entre 0 e 1:
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
fixtureDef.restitution = 0.5; // Elasticidade
Isso é tudo! Varie os valore: 0.1, 0.2 etc., até 1.0 e veja a bolinha
quicar.
Projeto para demonstrar as propriedades
Vamos
criar
um
projeto
semelhante
ao
anterior,
de
nome
propriedades.
Neste projeto, faremos uso das três propriedades físi-
cas mencionadas, a começar pelo atrito.
156
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Novamente, no início da seção 7.1 encontram-se o esqueleto do HTML
e o código de inicialização. Copie-os e também as funções debugDraw e
animar. Além disso, copie a função parede do projeto formas.
Tendo tudo arrumado, vamos criar duas “paredes” no cenário, sendo que
uma será o chão e a outra, um muro. Se achar que isto não está muito semân-
tico, sinta-se livre para renomear a função parede para algo mais adequado.
Aqui, vou manter o nome. Também criaremos um bloco retangular e apli-
caremos nele um impulso para a direita:
Figura 7.8: Demonstrando atrito, elasticidade e densidade
157
7.3. Propriedades físicas
Casa do Código
A fixture do bloco é iniciada com valores considerados médios para as
propriedades físicas:
function criarBloco() {
var corpoDef = new b2BodyDef();
corpoDef.type = b2Body.b2_dynamicBody;
corpoDef.position.Set(100/escala, 436/escala);
var forma = new b2PolygonShape();
forma.SetAsBox(25/escala, 25/escala);
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
fixtureDef.friction = 0 .5;
// Atrito
fixtureDef.restitution = 0 .5; // Elasticidade
fixtureDef.density = 1;
// Densidade
var bloco = mundo.CreateBody(corpoDef);
bloco.CreateFixture(fixtureDef);
return bloco;
}
Também configure esses valores para a definição de fixture da parede:
function parede(x, y, largura, altura) {
// ...
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
fixtureDef.friction = 0 .5;
// Atrito
fixtureDef.restitution = 0 .5; // Elasticidade
fixtureDef.density = 1;
// Densidade
// ...
}
Nosso laboratório está montado para experiências!
158
Casa do Código
Capítulo 7. Configurações e movimento de corpos
Atrito
Vamos setar valores no atributo friction, sabendo que ele pode variar
de 0 a 1:
• Coloque o atrito do bloco como 1 e rode a simulação. O bloco nem
chega a tocar a parede.
• Coloque os atritos do bloco e da parede como 1. O bloco tomba! É o
mesmo atrito provocado caso você aperte com força o freio da frente
da bicicleta, em alta velocidade.
• Coloque os atritos como zero. O bloco desliza à vontade, quica e vai
embora.
Conclusão: o atrito final depende dos coeficientes de atrito dos dois ob-
jetos sendo friccionados.
Elasticidade ou restituição
Vamos zerar os atritos e experimentar com o atributo restitution:
• Coloque a elasticidade do bloco como 1 e veja-o quicar na parede. O
bloco chega a saltitar, devido ao impulso que recebe do chão.
• Coloque ambas elasticidades como 1 e veja agora. A parede chuta o
bloco para longe!
Conclusão: assim como no atrito, o impulso provocado pela elasticidade
também depende das configurações dos dois corpos.
Densidade
A densidade define a massa de um corpo. Quanto maior o valor, maior
a força ou impulso necessário para deslocá-lo uma dada distância. Experi-
mente mudar o atributo density do bloco: 0.5, 1, 1.5, 2 etc. Não há
restrição de valor. Apenas observe que, conforme aumentamos a densidade
(e, consequentemente, a massa), o deslocamento do corpo com o impulso
159
7.3. Propriedades físicas
Casa do Código
dado é menor. Você pode obter efeitos semelhantes com densidades difer-
entes, desde que as forças e impulsos sejam proporcionais.
Experimente variar densidades também na bolinha do projeto
movimento. Na sua função criarBola:
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = forma;
fixtureDef.restitution = 0.5;
fixtureDef.density = 1; // Experimente valores diversos
Você notará que, quanto menor a densidade, mais alto a bola pula, como
acontece com objetos do mundo real. É a diferença entre bater em uma peteca
e em uma bigorna.
Quanta teoria! Mas isso é necessário se quisermos dominar a engine. Es-
pero estar tornando as coisas fáceis para você. No próximo capítulo, passare-
mos a construir a física do jogo de bilhar. Muitos conceitos sobre a engine
ainda serão aprendidos.
Dominando o Box2dWeb
Se você quer realmente dominar a engine, recomendo que invente
seus próprios cenários e “personagens”, ainda que meros quadradinhos
ou bolinhas, dê movimento a eles e experimente com as propriedades
físicas. Solte sua imaginação!
160
Capítulo 8
Iniciando o jogo de bilhar
Chegou a hora de aplicar o que aprendemos sobre o Box2dWeb em um jogo
de verdade. Ao final dessa jornada, você deverá conhecer os aspectos envolvi-
dos na criação de um jogo com uma engine física, e como integrá-la com os
conceitos mais básicos, como sprites, loop de animação e interação.
8.1. Preparação do projeto
Casa do Código
Figura 8.1: Jogo de bilhar a ser construído
No pacote de download, está presente o projeto jogo-bilhar, já pronto
e funcional. Recomendo que você o rode em seu celular, a fim de ter uma ideia
exata do que será construído. Para dar uma tacada, basta rotacionar o taco
com dois dedos dentro da área circular, puxar o ponteiro da barra de força e
tocar o botão POW! no canto superior direito.
Está pronto?
8.1
Preparação do projeto
Siga os seguintes passos para preparar o projeto:
• Crie um projeto Cordova em seu computador, adicione a plataforma
Android e também o plugin de multimídia:
cordova create jogo-bilhar
cd jogo-bilhar
cordova platform add android
cordova plugin add org.apache.cordova.media
• Apague todos os arquivos da pasta www, porém mantendo as pastas
img e js, para facilitar;
• Copie a pasta img do pacote de download (dentro de www) para o seu
projeto;
162
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
• Copie os arquivos de som da pasta platforms/android/assets;
• Baixe as bibliotecas Hammer.js e Box2dWeb para a pasta js, ou copie-
as do download;
• Copie também os arquivos animacao.js e spritesheet.js do
jogo de nave;
• Configure no config.xml o ícone do projeto e a orientação paisagem:
<icon src="www/img/icone.png" />
<preference name="Orientation" value="landscape" />
O próprio display do aparelho será a mesa, portanto não há a necessidade
de virar a imagem constantemente.
Segue o esqueleto inicial do arquivo
index.html.
O script
construtores.js será criado logo depois. O mundo físico possui gravi-
dade zero, exigida por jogos cujo cenário é visto por cima. Para facilitar os trabalhos mais adiante, guardamos a escala em pixels por metro como um
atributo do mundo. A classe Animacao, criada em meu livro anterior e us-
ada no jogo de nave, vai passar a receber o mundo físico no construtor, pois
será responsável por avançar o tempo:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jogo de Bilhar</title>
<meta name="viewport" content="width=device-width,
height=device-height, user-scalable=no,
initial-scale=1, maximum-scale=1, minimum-scale=1">
<script src="cordova.js"></script>
<script src="js/Box2dWeb-2.1.a.3.min.js"></script>
<script src="js/hammer.min.js"></script>
<script src="js/construtores.js"></script>
<script src="js/animacao.js"></script>
163
8.1. Preparação do projeto
Casa do Código
<script src="js/spritesheet.js"></script>
<script>
var canvas, context, mundo, animacao;
var ESCALA = 30;
window.onload = function() {
canvas = document.getElementById('bilhar');
context = canvas.getContext('2d');
// Gravidade zero!
mundo = new b2World(new b2Vec2(0, 0), true);
mundo.escala = ESCALA; // Armazenando a escala
// Passe o mundo para a Animacao
animacao = new Animacao(context, mundo);
debugDraw();
iniciar();
//document.addEventListener('deviceready', iniciar);
}
function iniciar() {
// Criaremos mais coisas...
animacao.ligar();
}
function debugDraw() {
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(context);
debugDraw.SetDrawScale(ESCALA);
debugDraw.SetFillAlpha(0.6);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit);
mundo.SetDebugDraw(debugDraw);
}
</script>
</head>
164
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
<body>
<canvas id="bilhar" width="700" height="400"></canvas>
</body>
</html>
O arquivo construtores.js é linkado logo após o Box2dWeb e ini-
ciará as referências aos principais objetos do Box2dWeb. O único que ainda
não foi usado é o b2ContactListener, responsável por tratar eventos de
colisão disparados pela engine:
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2World = Box2D.Dynamics.b2World;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var b2Body = Box2D.Dynamics.b2Body;
var b2ContactListener = Box2D.Dynamics.b2ContactListener;
Adaptando a classe Animacao
Precisamos adaptar a classe Animacao (arquivo animacao.js) a uma
nova necessidade. Essa classe é responsável por controlar o loop de animação,
permitindo colocar e retirar dinamicamente sprites e rotinas de processa-
mento. Ela terá agora que chamar o Step do mundo, o método que avança
o mundo físico no tempo (seção 6.3). Por isso, receba-o em seu construtor:
function Animacao(context, mundo) {
this.context = context;
this.mundo = mundo;
// ...
}
O coração da classe é o método proximoFrame, que é nada mais que
a função de animação em si. Vamos modificá-lo para que limpe a tela com
165
8.1. Preparação do projeto
Casa do Código
o fundo verde da mesa de bilhar, avance o tempo do mundo e permita-nos
optar entre setTimeout e requestAnimationFrame. Como o primeiro
oferece bom desempenho com o Box2dWeb, podemos nos livrar da preocu-
pação de usar polyfills e suportar aparelhos antigos (que ainda não são tão
antigos assim):
proximoFrame: function() {
if ( ! this.ligado ) return;
// Fundo verde
var ctx = this.context;
ctx.save();
ctx.fillStyle = '#050';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.restore();
var agora = Date.now();
if (this.ultimoCiclo == 0) this.ultimoCiclo = agora;
this.decorrido = agora - this.ultimoCiclo;
// Mundo físico
this.mundo.Step(1/60, 10, 5);
//this.mundo.DrawDebugData(); // Usado em caso de bugs
for (var i in this.sprites)
this.sprites[i].atualizar();
for (var i in this.sprites)
this.sprites[i].desenhar();
for (var i in this.processamentos)
this.processamentos[i].processar();
this.processarExclusoes();
this.ultimoCiclo = agora;
// Próximo ciclo com setTimeout
var animacao = this;
setTimeout(function() {
166
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
animacao.proximoFrame();
}, 1000/60);
/*
requestAnimationFrame(function() {
animacao.proximoFrame();
});
*/
},
Importância do Debug Draw
Perceba que comentei uma chamada a DrawDebugData. Se algo
der errado no processo, descomente essa linha. Dessa forma, podemos
capturar discrepâncias entre a posição real dos corpos e onde os sprites
são de fato desenhados. Nem é necessário dizer que precisei disso muitas
vezes enquanto criava o jogo.
Abra a página no navegador e vasculhe o Console. Por enquanto, o único
erro deverá ser a falta do cordova.js. O loop de animação e o mundo físico
já estão rodando, embora não haja sprites nem corpos.
8.2
Primeiros sprites
Vamos começar com os sprites mais simples: as paredes e as caçapas. O obje-
tivo é mostrar-lhe como integrar o Box2dWeb com a lógica dos sprites.
167
8.2. Primeiros sprites
Casa do Código
Paredes da mesa
Figura 8.2: Paredes da mesa de bilhar
Na função iniciar, coloque uma chamada a criarParedes. Como
são várias, passamos um array contendo vários objetos (um par de colchetes
[] contendo chaves {}):
Essa função vai simplesmente percorrer o array e instanciar objetos
Parede, adicionando-os como sprites ao loop de animação. Perceba que pas-
samos o mundo para o construtor, delegando para a classe a criação do corpo:
168
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
for (var i in arr) {
var p = new Parede(context, arr[i].x, arr[i].y,
arr[i].largura, arr[i].altura, mundo);
animacao.novoSprite(p);
}
}
Crie um link para o arquivo parede.js, na pasta js:
<script src="js/parede.js"></script>
E vamos criar esse arquivo. O construtor vai começar criando o corpo
no Box2dWeb. Nem precisaríamos configurar um novo b2BodyDef como
estático, mas fiz isso para dar ênfase. A definição do corpo também pode car-
regar um atributo userData, de uso livre do programador. Estamos usando-
o para associar o corpo ao sprite. Ele será importante durante tratamentos de
colisão na engine, para executar ações sobre o sprite. Também, ao definir a
posição e o tamanho, usamos o atributo escala armazenado no mundo. No
mais, sem novidades. Em caso de dúvida reveja os capítulos 6 e 7:
function Parede(context, x, y, largura, altura, mundo) {
// Corpo físico
var corpoDef = new b2BodyDef();
corpoDef.type = b2Body.b2_staticBody;
corpoDef.userData = this; // Sprite associado
var fixtureDef = new b2FixtureDef();
fixtureDef.density = 1;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.5;
var meiaLargura = largura / 2;
var meiaAltura = altura / 2;
var centroX = x + meiaLargura;
var centroY = y + meiaAltura;
// Usando a escala
corpoDef.position.Set(centroX/mundo.escala,
169
8.2. Primeiros sprites
Casa do Código
centroY/mundo.escala);
var retangulo = new b2PolygonShape();
retangulo.SetAsBox(meiaLargura/mundo.escala,
meiaAltura/mundo.escala);
fixtureDef.shape = retangulo;
var parede = mundo.CreateBody(corpoDef);
parede.CreateFixture(fixtureDef);
// continua...
}
Após criar o corpo, podemos proceder aos afazeres comuns de todo sprite:
armazenar seus atributos, atualizá-lo e desenhá-lo. O método atualizar,
tão importante no jogo de nave, perderá espaço porque quem posiciona os
objetos agora é a Box2dWeb. Recomento que, ao criar seus jogos, mantenha
esse método com corpo vazio, para o caso de algum sprite precisar de al-
guma atualização extra que não tenha a ver com a física. Caso ele se revele
desnecessário, ou seja, não haveria nada para executar em lógica de negó-
cio ou de posicionamento, você poderá removê-lo de seus sprites e (não se
esqueça!) tirar a chamada feita na classe Animacao.
function Parede(context, x, y, largura, altura, mundo) {
// ...
// Dados do sprite
this.context = context;
this.x = x;
this.y = y;
this.largura = largura;
this.altura = altura;
}
Parede.prototype = {
atualizar: function() {
//
170
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Podemos carregar a imagem logo ao carregar o script, pois rodando local-
mente não temos que ter preocupação com quais imagens já estão carregadas:
Faça o teste e verifique se o Canvas aparece como na figura 8.2.
Caçapas
Figura 8.3: Mesa com caçapas
As caçapas da mesa devem ser desenhadas sobre as paredes, portanto, os
171
8.2. Primeiros sprites
Casa do Código
sprites devem ser adicionados à animação depois. A spritesheet das caçapas é
formada por seis imagens e, para facilitar, vamos criá-las na mesma sequência
em que estão desenhadas no arquivo. Além das imagens, cada caçapa terá um
pequeno corpo a ser usado como sensor de colisão:
function iniciar() {
// ...
criarCacapas([
{imgX: 0, imgY: 0, sensorX: 30, sensorY: 30},
{imgX: 328, imgY: 0, sensorX: 350, sensorY: 25},
{imgX: 6 56 , imgY: 0, sensorX: 6 70, sensorY: 30},
{imgX: 0, imgY: 356, sensorX: 30, sensorY: 370},
{imgX: 328, imgY: 356, sensorX: 350, sensorY: 375},
{imgX: 6 56 , imgY: 356, sensorX: 6 70, sensorY: 370}
]);
animacao.ligar();
}
Figura 8.4: Spritesheet das caçapas
Cada caçapa criada usará uma coluna da spritesheet, atribuída pelo
número de ordem (variável de loop i):
function criarCacapas(arrayDados) {
var arr = arrayDados;
for (var i in arr) {
var c = new Cacapa(context, arr[i].imgX, arr[i].imgY,
arr[i].sensorX, arr[i].sensorY, mundo);
c.spritesheet.coluna = i;
animacao.novoSprite(c);
}
}
172
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Não se esqueça de linkar o arquivo da classe:
<script src="js/cacapa.js"></script>
E vamos criar a classe Cacapa. Os corpos físicos serão pequenos quadra-
dos, os sensores, posicionados estrategicamente nos pontos de entrada de
cada caçapa. O atributo isSensor da definição da fixture faz com que ela
não impeça a passagem de outro corpo, enquanto a engine pode reportar que
eles estão em contato. As bolas atravessarão os sensores como se não houvesse
um corpo ali.
var SENSOR = 1; // Meia largura/altura
function Cacapa(context, imgX, imgY, sensorX, sensorY, mundo) {
// Corpo do Box2dWeb (sensor)
var corpoDef = new b2BodyDef();
corpoDef.position.Set(sensorX/mundo.escala,
sensorY/mundo.escala);
corpoDef.type = b2Body.b2_staticBody;
corpoDef.userData = this;
var quadrado = new b2PolygonShape();
quadrado.SetAsBox(SENSOR/mundo.escala, SENSOR/mundo.escala);
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = quadrado;
fixtureDef.isSensor = true; // Sensor!
this.corpo = mundo.CreateBody(corpoDef);
this.corpo.CreateFixture(fixtureDef);
// continua ...
}
Da mesma forma que com as paredes, vamos carregar a imagem e ar-
mazenar os atributos do sprite:
var SENSOR = 1;
173
8.2. Primeiros sprites
Casa do Código
var SPRITE_CACAPA = new Image();
SPRITE_CACAPA.src = 'img/cacapa-spritesheet.png';
function Cacapa(context, imgX, imgY, sensorX, sensorY, mundo) {
// ...
// Atributos do sprite
this.context = context;
this.mundo = mundo;
this.imgX = imgX;
this.imgY = imgY;
this.sensorX = sensorX;
this.sensorY = sensorY;
this.spritesheet = new Spritesheet(context, SPRITE_CACAPA,
1, 6);
}
E também desenhar a caçapa.
No método
desenhar do sprite,
chamamos o método homônimo da spritesheet, que desenha conforme a col-
una atribuída anteriormente:
Cacapa.prototype = {
atualizar: function() {
//
},
desenhar: function() {
this.spritesheet.desenhar(this.imgX, this.imgY);
}
}
Faça o teste e veja se as caçapas aparecem. Para vermos os sensores, abra
a classe Animacao, descomente a chamada ao DrawDebugData e comente
o loop que chama desenhar nos sprites:
// Descomente esta linha
this.mundo.DrawDebugData();
// ...
174
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Figura 8.5: Debug Draw da mesa de bilhar
Sempre faça este procedimento para certificar-se de que o mundo físico
está de acordo com o que você deseja em seus jogos.
8.3
As bolas de bilhar
As bolas do jogo representam um tipo de sprite com mais detalhes para lidar.
Em primeiro lugar, é inútil armazenarmos atributos de posição, pois a engine
física os deslocará o tempo inteiro. A posição de desenho de cada bola deverá
sempre ser lida da engine. Faremos uso de uma folha de sprites animada e,
não bastasse isso, o ângulo de desenho da bola deverá corresponder ao ângulo
da sua trajetória. Cuidaremos disto nesta e nas próximas seções. Preparado?
175
8.3. As bolas de bilhar
Casa do Código
Figura 8.6: Não sou nenhum artista, portanto perdoem-me.
Vamos inicialmente declarar variáveis que farão referências às bolas:
E mandar criá-las após as caçapas. O número de cada bola corresponde
à linha da sua cor na spritesheet (figura 8.6). A bola branca se encontra na
linha zero.
176
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
As funções criarBolaBranca e criarBolasColoridas instanciam
objetos Bola, configuram o atributo id com o número e as adicionam como
sprites na animação. Para a bola branca, é melhor criar uma constante (
BOLA_BRANCA) do que escrever diretamente um valor (zero, negativo ou o
que quer que seja), para definir que se trata da bola branca:
function criarBolaBranca(x, y) {
var b = new Bola(context, x, y, 12, mundo); // raio = 12
b.id = BOLA_BRANCA;
animacao.novoSprite(b);
return b;
}
function criarBolasColoridas(arrayDados) {
var arr = arrayDados;
var bolas = new Array(arr.length);
for (var i in arr) {
// raio = 10
var b = new Bola(context, arr[i].x, arr[i].y, 10, mundo);
b.id = arr[i].numero;
animacao.novoSprite(b);
bolas[i] = b;
}
return bolas;
}
Temos que criar a classe Bola. Faça o link para o script:
<script src="js/bola.js"></script>
E segue a primeira versão. Definimos a constante BOLA_BRANCA, carreg-
amos a spritesheet, criamos o corpo e guardamos os atributos essenciais. A
definição de corpo possui um atributo bullet, que aprimora a detecção de
colisão em alta velocidade (mais detalhes a seguir). Não é necessário guardar
atributos de posição no sprite:
var BOLA_BRANCA = 0;
177
8.3. As bolas de bilhar
Casa do Código
var BOLA_SPRITE = new Image();
BOLA_SPRITE.src = 'img/bola-spritesheet.png';
function Bola(context, x, y, raio, mundo) {
// Corpo do Box2dWeb
var corpoDef = new b2BodyDef();
corpoDef.position.Set(x/mundo.escala, y/mundo.escala);
corpoDef.type = b2Body.b2_dynamicBody;
corpoDef.bullet = true; // Colisão de corpos velozes
corpoDef.userData = this;
var fixtureDef = new b2FixtureDef();
fixtureDef.shape = new b2CircleShape(raio/mundo.escala);
fixtureDef.density = 1;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.7;
this.corpo = mundo.CreateBody(corpoDef);
this.corpo.CreateFixture(fixtureDef);
// Atributos da bola
this.id = 0;
this.context = context;
this.raio = raio;
this.mundo = mundo;
// Spritesheet
var sheet = new Spritesheet(context, BOLA_SPRITE, 10, 19);
this.spritesheet = sheet;
}
178
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Atributo bullet
Por padrão, o Box2D e ports calculam a posição de um corpo no
tempo e o posicionam diretamente, da mesma forma como estamos acos-
tumados a fazer com nossos sprites sem engine física. No entanto, corpos
muito pequenos e velozes podem passar por barreiras “de uma vez” entre
um quadro e outro, impedindo a detecção da colisão.
Figura 8.7: Sem um detector de colisão apurado, esta bola atravessaria o muro
Quando uma definição de corpo possui o valor true no atributo
bullet, o Box2D realiza uma detecção mais apurada, considerando os
pontos intermediários da trajetória. Isto não é feito por padrão porque
requer muito processamento; você deve configurar como bullets ape-
nas corpos muito velozes que apresentarem o problema de atravessar out-
ros inadvertidamente.
Para desenhar, façamos um primeiro teste. Primeiro lemos a posição da
bola com o método GetPosition do corpo. Depois convertemos as coor-
denadas para pixels, multiplicando-as pela escala. Por último, desenhamos
círculos básicos para nos certificarmos de que as bolinhas estão sendo desen-
hadas nas posições corretas:
179
8.3. As bolas de bilhar
Casa do Código
// Posição
var posicao = this.corpo.GetPosition();
var x = posicao.x * this.mundo.escala;
var y = posicao.y * this.mundo.escala;
// Simples por enquanto
var ctx = this.context;
ctx.beginPath();
ctx.arc(x, y, this.raio, 0, Math.PI*2);
ctx.fill();
}
}
Rode a página e veja se bolas pretas aparecem sobre a mesa. Vamos então
aplicar a spritesheet. A linha corresponde ao atributo id da bola. O método
proximoQuadro, para quem não conhece do meu outro livro, avança a ani-
mação no tempo. Em seguida, mandamos desenhar o quadro atual:
desenhar: function() {
var posicao = this.corpo.GetPosition();
var x = posicao.x * this.mundo.escala;
var y = posicao.y * this.mundo.escala;
var sheet = this.spritesheet;
sheet.linha = this.id;
sheet.proximoQuadro();
sheet.desenhar(x - this.raio, y - this.raio,
this.raio * 2, this.raio * 2);
}
As bolinhas ficam girando mesmo paradas, o que será tratado mais
à frente. Primeiro, quero chamar a atenção para algo errado: o método
desenhar da classe Spritesheet não recebe a largura e a altura! Pre-
cisamos disso pois a bola branca possui um tamanho diferente das outras, de-
terminado pelo raio. Vamos modificar esse método para que receba opcional-
mente a largura e a altura do desenho. As chamadas antigas que não pas-
sarem estes parâmetros continuarão desenhando o tamanho real do quadro
da spritesheet:
180
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Perceba que agora a spritesheet respeita as dimensões passadas, e a bola
branca aparece maior. Faça o teste com o Debug Draw também.
Figura 8.8: Bolas posicionadas na mesa
181
8.4. Executando a tacada
Casa do Código
8.4
Executando a tacada
Está pensando no que eu estou pensando? Que tal aplicar um impulso na bola
branca e simular uma tacada? Para tratar o desenho correto das bolas se movi-
mentando, precisamos provocar primeiro um movimento. Em iniciar,
acrescente uma chamada à função tacada:
function iniciar() {
// ...
tacada();
}
Nessa função, começamos definindo o ângulo e a força que, lembre-se, são
atributos presentes na classe Taco criada no capítulo 5. Criamos um objeto
Tacada, que é acrescentado como um processamento do loop de animação,
e chamamos um método disparar:
function tacada() {
// Virão do Taco depois :D
var angulo = 0;
var forca = 100;
var t = new Tacada(bolaBranca, bolasColoridas);
animacao.novoProcessamento(t);
t.disparar(angulo, forca);
}
Temos que criar a classe Tacada. Faça um novo link:
<script src="js/tacada.js"></script>
E segue um esqueleto para a classe. Preste atenção ao atributo rodando:
qualquer processamento pode ser ligado e desligado testando-se um atributo
desse tipo, permitindo-nos controlar melhor sua configuração e até mesmo
setar atributos importantes antes de começar o processamento:
function Tacada(branca, coloridas) {
this.branca = branca;
this.coloridas = coloridas;
182
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
this.rodando = false;
}
Tacada.prototype = {
disparar: function(angulo, forca) {
},
processar: function() {
if (!this.rodando) return;
// Faremos algo aqui
}
}
No método disparar, temos que determinar os componentes x e y da força a ser aplicada, com a ajuda do seno e do cosseno (ver box adiante).
Depois, aplicamos essa força como um impulso na bola branca, sinalizamos
que o processamento está rodando e marcamos o instante do início, para
cronometrar o tempo:
disparar: function(angulo, forca) {
var radianos = angulo * Math.PI / 180;
var forcaX = forca * Math.cos(radianos);
var forcaY = forca * Math.sin(radianos);
var forca = new b2Vec2(forcaX, forcaY);
var centro = this.branca.corpo.GetWorldCenter();
this.branca.corpo.ApplyImpulse(forca, centro);
this.rodando = true;
this.inicio = Date.now();
},
183
8.4. Executando a tacada
Casa do Código
Decomposição de vetor em x e y
Dados o valor de uma força, também chamado módulo dessa força, e
o ângulo de aplicação, podemos determinar seus componentes x e y pelas fórmulas:
A tacada já acontece, ainda com as bolinhas girando loucamente. As bolas
que entram na caçapa seguem adiante para fora, pois atravessam os corpos
sensores. O mundo físico não possui barreiras.
Figura 8.9: Pow!
Só que a ação não termina! Os corpos continuam em movimento porque
as paredes não oferecem resistência suficiente para dissipar sua energia. No
método processar, vamos verificar o tempo decorrido e mandar as bolas
frearem gradativamente. Por isso, guardamos todas elas em um array único,
usando o método concat do array coloridas, para juntá-las com a branca.
Depois, o método IsAwake do corpo verifica se ele está acordado, pois con-
figuramos o mundo do Box2dWeb para adormecer corpos que pararem. A
variável mexendo sinalizará se ainda há ou não bolas em movimento:
184
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
processar: function() {
if (!this.rodando) return;
var decorrido = Date.now() - this.inicio;
var mexendo = false;
var bolas = this.coloridas.concat([this.branca]);
for (var i in bolas) {
var b = bolas[i];
// Monitorar o movimento
if (b.corpo.IsAwake()) {
mexendo = true;
// Frear a bola
if (decorrido >= 7000)
b.frear(5.0);
else if (decorrido >= 5000)
b.frear(1.0);
else if (decorrido >= 3000)
b.frear(0.7);
}
}
// continua...
}
Os valores passados para o método frear da Bola, a ser criado logo
mais, representarão a desaceleração que os corpos sofrerão. Esses valores, e
também os intervalos de tempo, foram obtidos após vários testes, de acordo
com o tempo que eu considerei adequado para a animação durar não há
mágica para determinar isso, você pode fazer seus próprios testes depois que
esta parte estiver pronta.
Após iterar por todas as bolas, se nenhuma mais estiver se mexendo, deve-
mos reverter a desaceleração de todas elas, setando o valor zero. Isso é impre-scindível para que elas não parem e passem a ir em sentido contrário! Sinal-
izamos o fim da execução da tacada e chamamos um callback por onde o jogo
poderá dar continuidade a seu processamento:
185
8.5. Ângulo da trajetória
Casa do Código
processar: function() {
// ...
if ( ! mexendo ) {
// Reverter a desaceleração
for (var i in bolas)
if (!bolas[i].foraDeJogo) bolas[i].frear(0);
if (this.aposTacada) this.aposTacada();
this.rodando = false;
console.log('tacada finalizada');
}
}
É boa prática “documentar” o atributo aposTacada inicializando-o
como null no construtor:
this.aposTacada = null;
Precisamos agora criar o método frear na classe Bola. Os métodos
SetLinearDamping e SetAngularDamping configuram uma desaceler-
ação gradativa para as velocidades linear (de trajetória) e angular (de rotação), respectivamente:
frear: function(desaceleracao) {
this.corpo.SetLinearDamping(desaceleracao);
this.corpo.SetAngularDamping(desaceleracao);
}
Pode experimentar a tacada! Quando a animação completar 7 segundos,
a desaceleração vai ser tal que elas pararão em pouco tempo e a mensagem
“tacada finalizada” poderá ser vista no console. Experimente configurar out-
ros ângulos e forças na função tacada!
8.5
Ângulo da trajetória
Vamos resolver o problema da animação da spritesheet. Se fosse apenas an-
imar quando estiverem em movimento, seria fácil. Mas o desenho da bola
deve ser rotacionado de acordo com a sua trajetória:
186
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
Figura 8.10: Bolas rotacionadas em trajetórias diferentes
Para nossa sorte, dado um vetor com componentes (x, y), como a ve-
locidade, o JavaScript nos fornece o método Math.atan2, que devolve o
ângulo desse vetor com relação à horizontal, exigindo somente os parâmet-
ros na ordem invertida: (y, x).
Obtemos a velocidade do corpo com
GetLinearVelocity e e usamos Math.atan2 para determinar o ângulo.
Vamos reconstruir o método desenhar da classe Bola:
Agora, peço que se detenha e preste bastante atenção.
A classe
Spritesheet possui o atributo intervalo, que define o tempo entre um
quadro e outro. Esse intervalo é inversamente proporcional à velocidade: quanto mais rápida a bola, menor o intervalo da animação, fazendo-a girar
mais rápido. Mas há um pequeno porém: uma velocidade negativa é matem-
aticamente menor que uma positiva de 0,001 ou mesmo zero. Mas o sinal
negativo aqui significa apenas o sentido em que a bola percorre a mesa. Devemos trabalhar em valores absolutos, considerando que -20 é maior que 5,
por exemplo.
Tendo os componentes vX e vY da velocidade, vamos obter seus valores
absolutos e ver qual o maior. Se for zero, não definimos intervalo (atribuí-
mos null). Se for maior que zero, o intervalo será o inverso da velocidade
187
8.5. Ângulo da trajetória
Casa do Código
(1/velocidade). Isso, claro, gera intervalos em valores decimais menores que
1, portanto multiplicamos por algum fator, no caso 50, para obter interva-
los maiores. Quanto tudo estiver pronto, você poderá fazer vários testes al-
terando esse valor e ver que o intervalo entre os quadros da spritesheet au-
menta ou diminui, mas sempre de forma inversa à velocidade do movimento:
Confesso, esta foi a parte que mais me deu trabalho de bolar!
Finalmente, temos que obter a posição da bola e desenhá-la rotacionada.
Como aprendemos em 5.3, para desenhar um objeto rotacionado, temos que
transladar o Canvas para o local desejado para depois rotacionar:
Figura 8.11: Como rotacionar a bola no Canvas
Esse conceito é muito importante para desenharmos qualquer objeto
rotacionado! Leia então a posição do corpo e faça as alterações no contexto
do Canvas:
188
Casa do Código
Capítulo 8. Iniciando o jogo de bilhar
// Posição
var posicao = this.corpo.GetPosition();
var x = posicao.x * this.mundo.escala;
var y = posicao.y * this.mundo.escala;
// DESENHAR ROTACIONADA
var ctx = this.context;
ctx.save();
// Ponto de desenho
ctx.translate(x, y);
// Rotaciona
ctx.rotate(angulo);
// continua ...
},
Podemos agora fazer o desenho, descontando o raio da bola da posição at-
ual do Canvas. Caso tenhamos definido um intervalo anteriormente (bola em
movimento), nós o aplicamos na spritesheet e mandamos avançar a animação.
Não se esqueça de chamar o restore quando fizer alterações drásticas no
contexto gráfico!
desenhar: function() {
// ...
// Desenha
var sheet = this.spritesheet;
sheet.linha = this.id;
if (intervalo) {
sheet.intervalo = intervalo;
sheet.proximoQuadro();
}
sheet.desenhar(-this.raio, -this.raio, this.raio * 2,
189
8.5. Ângulo da trajetória
Casa do Código
this.raio * 2);
ctx.restore();
},
Terminamos a primeira etapa! Temos todos os sprites necessários e já so-
mos capazes de conduzir uma tacada no jogo, disparando-a e sabendo quando
a ação terminou.
No próximo capítulo, incorporaremos ao jogo as classes
Taco e
BarraForca, criadas no capítulo 5, para adicionar interação, e aprendere-
mos como reagir a colisões no Box2dWeb, disparando sons e fazendo as bo-
las sumirem quando colidirem com os sensores da caçapa. Espero que esteja
gostando!
190
Capítulo 9
Interação com o jogador e lógicas
de negócio
Vamos agora permitir aos jogadores de nosso bilhar virtual controlar a sua
tacada e encaçapar as bolas, fazendo-as desaparecer.
No capítulo 5, nós criamos o projeto taco-bilhar, que contém os con-
troles de interação com o jogo deste livro. No caso, um controle de rotação do taco e uma barra de regulagem da força da tacada. Copie para o projeto atual
( jogo-bilhar) os arquivos taco.js e barra-forca.js, e também a
biblioteca Hammer.js. Você pode obter esses arquivos no pacote de download
ou da pasta onde você salvou seus exercícios.
Certifique-se de que o projeto já possui todos os sons e imagens copiados
para as pastas corretas. Em caso de dúvidas, reveja o início do capítulo 8.
Confira o conjunto dos scripts que devem ser linkados até aqui:
9.1. Controles da tacada
Casa do Código
<script src="cordova.js"></script>
<script src="js/Box2dWeb-2.1.a.3.min.js"></script>
<script src="js/hammer.min.js"></script>
<script src="js/construtores.js"></script>
<script src="js/spritesheet.js"></script>
<script src="js/animacao.js"></script>
<script src="js/bola.js"></script>
<script src="js/parede.js"></script>
<script src="js/cacapa.js"></script>
<script src="js/tacada.js"></script>
<script src="js/taco.js"></script>
<script src="js/barra-forca.js"></script>
Vamos começar!
9.1
Controles da tacada
Preparação
Na pasta das imagens, existe o arquivo tacada.png, que representa o botão
que dispara a tacada. Você pode usar outra figura, se desejar. Na seção
<body>, acrescente a tag da imagem logo após a tag <canvas>:
<body>
<canvas id="bilhar" width="700" height="400"></canvas>
<img src="img/tacada.png" id="tacada">
</body>
E vamos posicionar este botão via CSS, também aproveitando para deixar
o Canvas responsivo, como aprendemos em 3.3. Na seção <head>, acrescente uma tag <style> e faça as configurações a seguir:
body {
margin: 0;
width: 100%;
height: 100%;
background: #333;
}
192
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
canvas {
height: 100%;
width: auto;
display: block;
margin: 0 auto;
}
#tacada {
position: absolute;
right: 20px;
top: 20px;
}
Acrescente também referências ao Hammer.js, aos controles e ao arquivo
de som da tacada, nas declarações de variáveis da página:
var canvas,context,mundo,animacao;
var ESCALA = 30;
var bolaBranca,bolasColoridas;
var hammer,taco,barraForca,somTacada;
E modifique o evento onload da página para carregar o som e o Ham-
mer.js. Vamos, por enquanto, deixar comentadas as linhas que só rodam
em dispositivo móvel, para podermos realizar testes no console do Google
Chrome no desktop:
window.onload = function() {
canvas = document.getElementById('bilhar');
context = canvas.getContext('2d');
//somTacada = new Media('file:///android_asset/colisao.mp3');
mundo = new b2World(new b2Vec2(0, 0), true);
mundo.escala = ESCALA;
animacao = new Animacao(context,mundo);
hammer = new Hammer(canvas);
debugDraw();
193
9.1. Controles da tacada
Casa do Código
iniciar();
//document.addEventListener('deviceready', iniciar);
}
Em iniciar, fizemos uma chamada à função tacada somente para
testar a lógica. Retire essa chamada:
function iniciar() {
// ...
// Remova
//tacada();
}
Criação dos controles
As classes Taco e BarraForca estão recebendo as imagens pelo con-
strutor, seguindo a linha de carregar as imagens na inicialização. Esta é uma
boa prática para jogos que devem ser publicados na internet, mas para um
jogo local vamos simplificar os construtores. Abra o arquivo taco.js, in-
sira as linhas que carregam a imagem e modifique o construtor como segue:
var IMG_TACO = new Image();
IMG_TACO.src = 'img/taco.png';
function Taco(context, hammer) {
this.context = context;
this.imagem = IMG_TACO;
// ...
}
O mesmo em barra-forca.js:
var IMG_PONTEIRO = new Image();
IMG_PONTEIRO.src = 'img/ponteiro.png';
function BarraForca(context, taco, hammer) {
this.context = context;
this.ponteiro = IMG_PONTEIRO;
194
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
// ...
}
Temos então que criar os controles. Em iniciar, instanciaremos o
Taco e configuraremos o disparo pelo toque no botão da tacada. Ainda man-
teremos comentado o comando que rebobina o som:
function iniciar() {
// Paredes, caçapa e bolas
// ...
taco = new Taco(context, hammer);
taco.raio = 1 20;
taco.x = 1 25;
taco.y = canvas.height / 2 - 50;
var imgTacada = document.getElementById('tacada');
imgTacada.addEventListener('touchstart', function() {
//somTacada.seekTo(0);
taco.darTacada(50);
});
taco.aposTacada = tacada;
animacao.novoSprite(taco);
// Aqui virá a barra de força
animacao.ligar();
}
O mesmo se sucederá com a barra de força:
function iniciar() {
// ...
barraForca = new BarraForca(context, taco, hammer);
barraForca.x = taco.x - 1 00;
barraForca.y = taco.y + taco.raio + 25;
barraForca.largura = 200;
195
9.2. Executando a tacada no ângulo correto
Casa do Código
barraForca.altura = 50;
animacao.novoSprite(barraForca);
animacao.ligar();
}
Como os controles estão sendo adicionados como sprites na animação,
estes devem ter o método atualizar, ainda que vazio. No capítulo anterior,
eu disse que, se o atualizar se mostrar desnecessário em algum jogo, você
poderá removê-lo dos sprites e também sua chamada pela classe Animacao.
Modifique ambos os controles:
Taco.prototype = {
atualizar: function() {
//
},
// ...
}
BarraForca.prototype = {
atualizar: function() {
//
},
// ...
}
Faça o teste no navegador e veja se não há erros de codificação no console,
exceto o do cordova.js. Você pode também rodar no seu celular e testar
os controles.
9.2
Executando a tacada no ângulo correto
A função tacada, atualmente, está definindo arbitrariamente a força e o
ângulo. Vamos modificá-la para que leia esses dados do controle do taco.
Também vamos impedir uma tacada com força nula e mandar esconder
196
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
os controles enquanto a ação acontece. Perceba que criamos um callback,
bolasPararam, para reagir quando a animação tiver terminado:
function tacada() {
if (taco.forca == 0) return;
// Esconder os controles
taco.podeDesenhar = false;
barraForca.podeDesenhar = false;
// Realizar a tacada
var tacada = new Tacada(bolaBranca, bolasColoridas);
animacao.novoProcessamento(tacada);
tacada.aposTacada = bolasPararam;
tacada.disparar(taco.rotacao, taco.forca);
//somTacada.play();
}
function bolasPararam() {
// Aqui executaremos outras regras de negócio!
taco.podeDesenhar = true;
barraForca.podeDesenhar = true;
}
Nos construtores, tanto do Taco quanto da BarraForca, inicie o novo
atributo podeDesenhar como true:
this.podeDesenhar = true;
Em seus métodos desenhar, faça-os considerarem o atributo logo no
início:
desenhar: function() {
if (!this.podeDesenhar) return;
// ...
}
197
9.2. Executando a tacada no ângulo correto
Casa do Código
Rode no celular. Gire o taco e aperte o botão da tacada. Deve funcionar,
mas a bola não parece ir na direção correta!
Na verdade, ela vai. Se você deixar o ângulo padrão (0°), a bola branca
é impulsionada para a direita, porém o taco é desenhado em pé, porque sua
imagem é em pé. Isso é fácil de ser resolvido, basta desenhar a imagem 90°
( pi/2) a mais que a rotação definida no controle. No método desenhar do Taco, modifique a linha que calcula o ângulo da imagem em radianos:
Figura 9.1: Somando 90° à imagem do taco, ele fica alinhado corretamente ao
ângulo da tacada
Experimente novamente e veja que a bola branca vai na direção para onde
você girou o taco.
198
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
Testando a tacada pelo Console
Pelo celular é muito mais interativo, mas demora para carregar. Testes
rápidos podem ser feitos pelo Google Chrome. A barra de força pode ser
puxada com o mouse, pois é gerida pelo Hammer.js. A rotação exige dois
dedos na tela touch, mas podemos setar o atributo e disparar uma tacada
diretamente pelo console:
Figura 9.2: Dando comandos ao jogo pelo console do Google Chrome
9.3
Posicionando o taco
Embora tudo esteja começando a funcionar bem, ainda está estranho ver o
taco sempre no mesmo local, enquanto a bola branca vaga pela mesa. Vamos
criar uma nova função que cuidará de posicionar a imagem do taco junto
à bola branca, no início e após cada tacada. Na função iniciar, comece
chamando posicionarTaco após a sua instanciação:
199
9.3. Posicionando o taco
Casa do Código
posicionarTaco();
A função posicionarTaco vai ler a posição da bola branca no mundo
físico e configurar os novos atributos brancaX e brancaY, a serem usados
somente para o desenho do taco. Os atributos x e y não mudam, pois con-
tinuarão correspondendo ao centro do controle circular. Também é preciso
especificar um raioDistancia para afastar a ponta do taco do centro da
bola, evitando que os desenhos se sobreponham:
function posicionarTaco() {
var pos = bolaBranca.corpo.GetPosition();
var x = pos.x * ESCALA;
var y = pos.y * ESCALA;
taco.brancaX = x;
taco.brancaY = y;
taco.raioDistancia = bolaBranca.raio;
}
No construtor do Taco, inicie esses atributos para documentá-los devi-
damente:
this.brancaX = 0;
this.brancaY = 0;
this.raioDistancia = 0;
E no método desenhar, o translate passa a ser feito para esse ponto
desejado. O deslocamento do taco não será apenas de acordo com a força,
mas também somado com o raio de distância do centro da bola branca:
desenhar: function() {
// ...
// Taco
var radianos = this.rotacao * Math.PI / 180 + Math.PI / 2;
ctx.save();
ctx.translate(this.brancaX, this.brancaY);
ctx.rotate(radianos);
ctx.drawImage(this.imagem, -7,
200
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
this.deslocamento() + this.raioDistancia,
this.imagem.width, this.imagem.height);
ctx.restore();
},
Sabemos quando a tacada terminou, portanto nesse momento vamos
reposicionar o taco e zerar as forças. Acrescente as seguintes linhas em
bolasPararam:
function bolasPararam() {
// ...
posicionarTaco();
barraForca.forca = 0;
taco.forca = 0;
}
Façamos um teste mais amplo no celular. Ative o evento deviceready,
descomente os comandos envolvendo o somTacada e certifique-se de que a
tacada dispara o som e o taco acompanha a bola branca a cada jogada. Isto
está ficando divertido!
9.4
Tratamento das colisões
Só a tacada vai fazer som? Precisamos fazer os choques entre bolas, entre uma
bola e uma parede, e entre uma bola e um sensor de caçapa produzirem sons
também. Portanto, vamos aprender como reagir a colisões entre corpos no
Box2dWeb. A detecção de colisões entre diversas formas geométricas já é re-alizada automaticamente pela engine, bastando-nos realizar o seu tratamento no jogo.
No evento onload da página, mande carregar todos os sons:
somTacada = new Media('file:///android_asset/colisao.mp3');
somMadeira = new Media('file:///android_asset/knock.mp3');
somCacapa = new Media('file:///android_asset/hit.mp3');
Em iniciar, vamos instanciar um novo objeto, ColisaoListener,
a ser criado logo mais.
Este objeto será o responsável por receber os
201
9.4. Tratamento das colisões
Casa do Código
eventos de colisão do Box2dWeb.
Para inseri-lo no mundo, usamos
SetContactListener:
function iniciar() {
// ...
colisaoListener = new ColisaoListener(somTacada, somMadeira,
somCacapa);
mundo.SetContactListener(colisaoListener);
animacao.ligar();
}
Atualize as variáveis da página como segue:
var canvas, context, mundo, animacao;
var ESCALA = 30;
var bolaBranca, bolasColoridas;
var hammer, taco, barraForca;
var somTacada, somMadeira, somCacapa, colisaoListener;
E faça o link para colisao-listener.js:
<script src="js/colisao-listener.js"></script>
Vamos agora criar a nova classe ColisaoListener. O construtor so-
mente armazenará referências aos sons:
function ColisaoListener(somBola, somMadeira, somCacapa) {
this.somBola = somBola;
this.somMadeira = somMadeira;
this.somCacapa = somCacapa;
}
Um objeto ouvinte de colisões do Box2dWeb deve seguir uma interface,
ou seja, implementar um certo conjunto de métodos. No entanto, como nem
todos eles podem nos interessar, existe o objeto b2ContactListener, que
já implementa todos eles, porém com corpos vazios. Devemos fazer nossa
classe herdar os métodos existentes em b2ContactListener e sobrescr-
ever aqueles que nos interessarem. Em JavaScript, uma herança entre classes
202
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
pode ser simulada definindo um objeto como protótipo de outro. Na sequên-
cia do código:
// ColisaoListener herda os métodos de b2ContactListener
ColisaoListener.prototype = new b2ContactListener();
Continuando, vamos redefinir o método BeginContact. Desta vez não
podemos atribuir um novo protótipo, mas modificar o método do protótipo
recém-criado. Esse método é chamado pelo Box2dWeb logo no início de um
contato entre fixtures de corpos, e recebe um parâmetro com os dados desse
contato.
ColisaoListener.prototype.BeginContact = function(contato) {
// tratamento da colisão aqui
}
Preste atenção ao “caminho” até chegar aos corpos e seus sprites associ-
ados. Os métodos GetFixtureA e GetFixtureB, do objeto contato,
obtêm as fixtures em colisão. Para saber quais seus corpos correspondentes,
chamamos GetBody. E o sprite tinha sido associado ao corpo como um user
data, portanto GetUserData deve nos retornar uma Bola, um Taco ou
uma Parede!
ColisaoListener.prototype.BeginContact = function(contato) {
var corpoA = contato.GetFixtureA().GetBody();
var corpoB = contato.GetFixtureB().GetBody();
var objetoA = corpoA.GetUserData();
var objetoB = corpoB.GetUserData();
// continua...
}
Testemos os tipos dos objetos. Para facilitar, salvamos os testes em var-
iáveis. Temos que testar a possibilidade de serem duas bolas se chocando,
perguntando se objetoA e objetoB são do tipo Bola, e a possibilidade
de uma Bola se chocar com Parede ou Cacapa, não importando qual é A
ou B:
203
9.4. Tratamento das colisões
Casa do Código
ColisaoListener.prototype.BeginContact = function(contato) {
// ...
var bola1 = (objetoA instanceof Bola ? objetoA : null);
var bola2 = (objetoB instanceof Bola ? objetoB : null);
var bola = bola1 || bola2; // Quem quer que seja a Bola!
var parede = (objetoA instanceof Parede ? objetoA :
objetoB instanceof Parede ? objetoB : null);
var cacapa = (objetoA instanceof Cacapa ? objetoA :
objetoB instanceof Cacapa ? objetoB : null);
// continua...
}
Podemos, enfim, reagir às colisões, disparando os respectivos sons. No
caso de ser bola com caçapa, chamamos o callback encacapou na bola, de
modo a executar a lógica de negócio:
ColisaoListener.prototype.BeginContact = function(contato) {
// ...
if (bola1 && bola2) {
this.somBola.play();
this.somBola.seekTo(0); // Rebobinar para a próxima
}
else if (bola && parede) {
this.somMadeira.play();
this.somMadeira.seekTo(0);
}
else if (bola && cacapa) {
this.somCacapa.play();
this.somCacapa.seekTo(0);
if (bola.encacapou) bola.encacapou(bola);
}
}
No construtor da Bola, documente esse callback e também crie um atrib-
uto foraDeJogo, com o valor false, para podermos executar alguns testes
204
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
adiante:
this.encacapou = null;
this.foraDeJogo = false;
No index.html, em criarBolaBranca, atribua um callback antes
do return:
function criarBolaBranca(x, y) {
// ...
b.encacapou = encacapouBranca;
return b;
}
E atribua outro em criarBolasColoridas, dentro do loop que cria as
bolas:
function criarBolasColoridas(arrayDados) {
// ...
for (var i in arr) {
// ...
b.encacapou = encacapouColorida;
}
// ...
}
Temos que criar esses callbacks. O da bola branca vai reposicioná-la, e
o das coloridas irá excluí-las quando encaçapadas. Mas aqui é preciso muita
cautela. Não devemos mudar o estado de um corpo durante um tratamento de
colisão, pois o passo ( Step ) ainda está sendo processado! Em outras palavras, nada de reposicionar ou excluir os corpos: simplesmente não funcionará. E
agora?
Temos que agendar essa tarefa para após o Step. Felizmente, já temos
um mecanismo para isso: podemos adicionar processamentos ao loop de an-
imação, para serem executados ao fim de cada ciclo!
No callback encacapouBranca, crie um processamento. Lá dentro,
sinalize que a bola está fora do jogo, pare seu movimento e posicione-a do
205
9.4. Tratamento das colisões
Casa do Código
lado de fora da mesa, para não atrapalhar o movimento das outras. No final,
o processamento deve ser excluído:
function encacapouBranca(bola) {
// Somente ao fim do ciclo
animacao.novoProcessamento({
processar: function() {
bola.foraDeJogo = true;
bola.corpo.SetLinearVelocity(new b2Vec2(0, 0));
bola.corpo.SetPosition(new b2Vec2(-1000, -1000));
// Excluir para executar apenas uma vez
animacao.excluirProcessamento(this);
}
});
}
Em bolasPararam, que responde ao fim de uma tacada, vamos verificar
se a bola branca foi encaçapada, reposicioná-la e retorná-la para o jogo. Isso deve ser feito antes da chamada a posicionarTaco:
function bolasPararam() {
if (bolaBranca.foraDeJogo) {
bolaBranca.corpo.SetPosition(
new b2Vec2(150/ESCALA, 200/ESCALA));
bolaBranca.foraDeJogo = false;
}
// posicionarTaco() por aqui...
}
E temos o outro callback, encacapouColorida. Quando uma bola col-
orida é encaçapada, ela deve sair do jogo. Essa lógica de negócio será delegada para a classe Bola:
function encacapouColorida(bola) {
animacao.novoProcessamento({
processar: function() {
bola.tirarDoJogo();
206
Casa do Código
Capítulo 9. Interação com o jogador e lógicas de negócio
animacao.excluirProcessamento(this);
}
});
}
O método tirarDoJogo, na classe Bola, vai executar todas as ações
necessárias: excluir o corpo do mundo físico, excluir o sprite da animação e
sinalizar que aquela bola está fora do jogo:
tirarDoJogo: function() {
this.mundo.DestroyBody(this.corpo);
this.animacao.excluirSprite(this);
this.foraDeJogo = true;
}
De volta à classe Tacada, verifique se a bola não (símbolo !) está fora de jogo, além de estar acordada, para considerá-la ativa. Dessa forma, qualquer
bola que for embora pelas caçapas não precisa aguardar um bom tempo sendo
freada:
processar: function() {
// ...
// Monitorar o movimento
if (!b.foraDeJogo && b.corpo.IsAwake()) {
// ...
}
A interação com o jogador está finalizada! Fomos capazes de:
• integrar os controles do taco e da barra de força ao jogo;
• alinhar o desenho do taco com o ângulo de tacada e colocar sua ponta
na borda da bola;
• disparar uma tacada no ângulo e com a força definidos nos controles;
• aguardar as bolas pararem para tomar outras medidas;
207
9.4. Tratamento das colisões
Casa do Código
• “sumir” com a bola branca e retorná-la ao início do jogo, se for encaça-
pada;
• excluir de verdade as bolas coloridas encaçapadas.
No próximo capítulo, veremos como o computador poderá calcular uma
tacada automática, aproveitando-se do fato de que a Box2dWeb pode fazer as
simulações físicas somente em memória. Brinque um pouco, o jogo começa
agora a ficar viciante!
208
Capítulo 10
Simulações internas
Neste capítulo, discuto como podemos fazer a máquina pensar para determi-
nar sua jogada. Usando uma engine física, calcular a jogada automática não
requer conhecimentos em inteligência artificial. Sabendo que o Box2D e suas
variações em outras linguagens ( ports) trabalham em memória, podemos realizar diferentes tacadas por baixo dos panos, analisar os resultados e mostrar na tela aquela que que for mais vantajosa.
Isso parece simples, e realmente é, mas há considerações importantes a se
fazer:
• a cada simulação feita em memória, devemos retornar todas as bolas
para as posições em que se encontravam no início;
• o processamento da tacada só termina após passado um período de
tempo, quando se começa a frear o movimento das bolas;
10.1. Realizando várias simulações a partir do início
Casa do Código
• nosso tratador de colisões está disparando sons e tirando as bolas do
jogo.
Vamos pensar em cada um destes problemas por vez.
10.1
Realizando várias simulações a partir do
início
Simularemos diversas tacadas, em diversos ângulos. Só para testar, forçare-
mos o computador a dar uma tacada logo ao fim da função iniciar:
function iniciar() {
// ...
tacadaComputador();
}
Na
função
tacadaComputador,
criamos
um
objeto
TacadaAutomatica e o mandamos fazer os testes, devolvendo os da-
dos da tacada que deu os melhores resultados. Tudo de forma abstraída,
delegando para essa classe o serviço pesado:
function tacadaComputador() {
var automatica = new TacadaAutomatica(mundo, bolaBranca,
bolasColoridas);
var melhorTacada = automatica.realizarTestes();
taco.rotacao = melhorTacada.rotacao;
taco.forca = melhorTacada.forca;
tacada();
}
Crie o arquivo tacada-automatica.js. Para simular uma tacada,
temos que distinguir a bola branca das coloridas (veja a classe Tacada), e
também precisamos do mundo para simular os Steps:
function TacadaAutomatica(mundo, branca, coloridas) {
this.mundo = mundo;
this.branca = branca;
this.coloridas = coloridas;
210
Casa do Código
Capítulo 10. Simulações internas
}
TacadaAutomatica.prototype = {
realizarTestes: function() {
}
}
Em realizarTestes, vamos delegar para a classe Snapshot a tarefa
de salvar e recuperar a posição das bolas. Chamamos salvar no início e
reverter após cada simulação. A simulação é feita no método executar,
que recebe o ângulo e a força a serem testados, e deverá retornar os dados que obteremos da tacada:
realizarTestes: function() {
var estadoJogo = new Snapshot(this.mundo);
estadoJogo.salvar(); // Guarda no início
var resultados = [];
// De 5 em 5 graus para não onerar o processador
for (var i = 0; i <= 360; i += 5) {
var tacada = this.executar(i, 75); // ângulo, força
resultados.push(tacada);
estadoJogo.reverter(); // Reverte após cada uma
}
// Como decidir qual foi a melhor simulação?
return resultados[0];
}, // vírgula pois terá o executar
O salto de 5 em 5 graus é mais um detalhe a que não cheguei logo de
primeira. Fiz vários testes ao fim da implementação, os quais recomendo você
fazer também. Dar 360 tacadas, voltando as bolas para suas posições após
cada uma, leva bastante tempo. Também evitei testar diferentes valores de
força para cada ângulo. Se quiser, você pode, como um exercício, encontrar
um balanceamento adequado entre quantos ângulos e quantas forças podem
ser testadas para cada um.
211
10.1. Realizando várias simulações a partir do início
Casa do Código
Ainda não temos como decidir qual simulação deu o melhor resultado,
pois não definimos quais regras de jogo vamos adotar. Faremos isso no próx-
imo capítulo. Mas vamos pelo menos nos certificar de que as simulações estão
acontecendo e que o desempenho desse processamento seja aceitável.
No método
executar, criamos um objeto
Tacada e “em-
purramos” o processamento com um loop
while, em vez de usar
requestAnimationFrame ou setTimeout. O loop é controlado pela var-
iável continua, que é setada para false quando o processamento reportar
o fim da ação no callback aposTacada:
executar: function(angulo, forca) {
var tacada = new Tacada(this.branca, this.coloridas);
var continua = true;
tacada.aposTacada = function() {
continua = false;
}
tacada.disparar(angulo, forca);
while (continua) {
this.mundo.Step(1/60, 10, 5);
tacada.processar();
}
// Obteremos depois outros dados
return { rotacao: angulo, forca: forca };
}
Nossa prática até aqui tem sido avançar o mundo físico, em cada ciclo
do loop de animação, no mesmo tempo medido pelo relógio do computador,
criando uma simulação em tempo real. Ainda assim, nada impede que todos
os passos ( Steps) de uma tacada sejam simulados de forma seguida, em
loops for ou while comuns, sem nos atrelarmos ao relógio. A Box2dWeb
calcula a posição dos corpos no tempo, independentemente de esse tempo ter
se passado no relógio ou não.
212
Casa do Código
Capítulo 10. Simulações internas
10.2
Salvando e recuperando o estado do
mundo físico
Para tudo isso funcionar, temos que implementar a classe Snapshot. Nela,
guardaremos as posições das bolas. Crie o arquivo snapshot.js:
function Snapshot(mundo) {
this.mundo = mundo;
this.posicoes = [];
}
Snapshot.prototype = {
salvar: function() {
},
reverter: function() {
}
}
O método salvar vai fazer loop nos corpos e guardar as posições.
Perceba que não guardamos diretamente os objetos devolvidos por
GetPosition, e, sim, copiamos os valores para novos objetos. Fazemos isso
porque variáveis de objetos são referências, ou seja, estaríamos na verdade referenciando os objetos de posição reais dos corpos, que poderão estar alter-ados na hora de reverter.
salvar: function() {
var arrPosicoes = [];
var corpo = this.mundo.GetBodyList();
while (corpo) {
var pos = corpo.GetPosition();
var obj = { corpo: corpo, x: pos.x, y: pos.y };
arrPosicoes.push(obj);
corpo = corpo.GetNext();
}
this.posicoes = arrPosicoes;
},
213
10.2. Salvando e recuperando o estado do mundo físico
Casa do Código
Algoritmo para iterar por todos os corpos
A primeira referência é obtida por GetBodyList; as próximas, por
GetNext. Quando for devolvido null, os corpos se acabaram:
var corpo = this.mundo.GetBodyList();
while (corpo) {
// ...
corpo = corpo.GetNext();
}
O método reverter só precisa fazer um loop no array de posições
guardadas e chamar SetPosition para reatribuí-las aos corpos:
reverter: function() {
for (var i in this.posicoes) {
var pos = this.posicoes[i];
pos.corpo.SetPosition(new b2Vec2(pos.x, pos.y));
}
}
Quero frisar que, para a nossa necessidade aqui, apenas guardar e re-
tornar as posições dos corpos é suficiente, já que cada jogada é feita com
as bolas paradas. Você pode estender a classe Snapshot para guardar ve-
locidades lineares e angulares, com os métodos GetLinearVelocity e
GetAngularVelocity, respectivamente, e recuperá-las com os Sets cor-
respondentes.
Não se esqueça de linkar os scripts das duas classes novas:
<script src="js/tacada-automatica.js"></script>
<script src="js/snapshot.js"></script>
214
Casa do Código
Capítulo 10. Simulações internas
10.3
Executando a tacada em tempo curto
Se executarmos o jogo no ponto em que está, veremos a tela parada enquanto
as simulações acontecem. Dá até para ouvir os sons. Mas lembre-se de que
a Tacada faz uma desaceleração progressiva das bolas, sendo que depois
de 7 segundos ela “pisa mais fundo” no freio. Fazendo isso 72 vezes (360/5),
imagine o tempo que leva. Não adianta nada fazermos os Steps de forma
seguida se a classe Tacada só trabalha com o relógio.
Figura 10.1: Aguardando as simulações acontecerem
Vamos flexibilizá-la para que trabalhe também com contagem de
quadros. Sabendo que cada quadro leva 1/60 de segundo, basta contar os
quadros para saber quanto tempo uma tacada levou, sem precisar esperar
esse tempo passar de verdade. Podemos assim fazer simulações mais ráp-
idas, enquanto a animação pode (e deve) continuar dependendo do relógio,
acontecendo na velocidade em que os eventos físicos aconteceriam no mundo
real.
No método executar da TacadaAutomatica, estamos instanciando
a Tacada. Configure dois novos atributos, porRelogio e tempoAvanco:
executar: function(angulo, forca) {
var tacada = new Tacada(this.branca, this.coloridas);
tacada.porRelogio = false;
tacada.tempoAvanco = 1/60;
215
10.3. Executando a tacada em tempo curto
Casa do Código
// ...
}
No construtor da Tacada, inicie o atributo porRelogio como true,
para não quebrar o comportamento padrão. O tempoAvanco pode ser ini-
ciado como zero, para documentação de atributos, pois só será usado se for
para simulação rápida:
function Tacada(branca, coloridas) {
// ...
this.porRelogio = true;
this.tempoAvanco = 0;
}
Na simulação rápida, devemos fazer uma contagem de quadros para de-
terminar o tempo avançado. Em disparar, inicie um atributo quadro com
zero:
disparar: function(angulo, forca) {
// ...
this.quadro = 0;
},
E em processar, incrementamos o contador e decidimos se é por reló-
gio ou por contagem de quadros. Se for por relógio, determinamos o tempo
decorrido da mesma forma como estávamos fazendo. Se for por contagem,
sabemos quantos quadros foram avançados e quanto tempo foi simulado, pelo
atributo tempoAvanco:
processar: function() {
if (!this.rodando) return;
this.quadro++;
// Quantos segundos já se passaram
var sete, cinco, tres;
if (this.porRelogio) {
// Esta linha vem para cá
var decorrido = Date.now() - this.inicio;
sete = (decorrido >= 7000);
216
Casa do Código
Capítulo 10. Simulações internas
cinco = (decorrido >= 5000);
tres = (decorrido >= 3000);
}
else {
var segundos = this.quadro * this.tempoAvanco;
sete = (segundos >= 7);
cinco = (segundos >= 5);
tres = (segundos >= 3);
}
// ...
}
Basta-nos então usar as variáveis dos testes para frear as bolas. Modifique
o trecho de processar em que isso ocorre:
processar: function() {
// ...
// Frear a bola
if (sete)
b.frear(5.0);
else if (cinco)
b.frear(1.0);
else if (tres)
b.frear(0.7);
// ...
}
A classe
Tacada poderia trabalhar somente com contagem de
quadros, mesmo no loop de animação, pois este já controla o tempo en-
tre os passos. Mas isso nos obrigaria, a cada tacada criada, a informar
o tempoAvanco a ser simulado, razão por que preferi mantê-la flexível.
Esteja à vontade para experimentar essa abordagem, se quiser.
Experimente executar no dispositivo móvel neste momento. O tempo
de simulação já melhorou bastante, embora ainda esteja longe do adequado,
217
10.4. Tratador de colisões para a simulação
Casa do Código
e também escutamos os sons disparados pelo tratador de colisão. As bolas
que foram encaçapadas nesse percurso somem, pois é o que mandamos fazer
nesses casos. Tendo resolvido primeiro os problemas de desempenho, vamos
resolver estes.
10.4
Tratador de colisões para a simulação
Vamos criar outra classe tratadora de colisão voltada para as simulações. Esta classe não disparará sons e detectará inicialmente somente as encaçapadas.
Na função tacadaComputador, altere o tratador de colisão no início, e
retorne ao original após o fim das simulações, mas antes de realizar a tacada
“real” do jogo:
function tacadaComputador() {
mundo.SetContactListener(simulacaoListener);
// ...
mundo.SetContactListener(colisaoListener);
tacada();
}
Declare a variável simulacaoListener na página:
var canvas, context, mundo, animacao;
var ESCALA = 3 0;
var bolaBranca, bolasColoridas;
var hammer, taco, barraForca, somTacada;
var somTacada, somMadeira, somCacapa;
var colisaoListener, simulacaoListener;
Instancie o seu objeto em iniciar (pode ser junto com o primeiro):
function iniciar() {
// ...
simulacaoListener = new SimulacaoListener();
colisaoListener = new ColisaoListener(somTacada, somMadeira,
218
Casa do Código
Capítulo 10. Simulações internas
somCacapa);
mundo.SetContactListener(colisaoListener);
// ...
}
Não se esqueça de linkar o arquivo de script:
<script src="js/simulacao-listener.js"></script>
E vamos criá-lo. Perceba que é uma versão bastante reduzida do primeiro,
já que sua função por enquanto é reportar somente contato entre Bola e
Cacapa:
function SimulacaoListener() {
this.encacapou = null;
}
SimulacaoListener.prototype = new b2ContactListener();
SimulacaoListener.prototype.BeginContact = function(contato) {
var objA = contato.GetFixtureA().GetBody().GetUserData();
var objB = contato.GetFixtureB().GetBody().GetUserData();
var bola = (objA instanceof Bola ? objA :
objB instanceof Bola ? objB : null);
var cacapa = (objA instanceof Cacapa ? objA :
objB instanceof Cacapa ? objB : null);
if (bola && cacapa && this.encacapou)
this.encacapou(bola);
}
Respondendo ao callback
Faça com que a TacadaAutomatica receba esse simulador no constru-
tor. Como essa classe é responsável por calcular a jogada do computador, ela
fornecerá um callback que verificará as bolas encaçapadas:
219
10.4. Tratador de colisões para a simulação
Casa do Código
function tacadaComputador() {
mundo.SetContactListener(simulacaoListener);
var automatica = new TacadaAutomatica(mundo, bolaBranca,
bolasColoridas, simulacaoListener);
// ...
}
No seu construtor, recebemos o colisor e criamos um callback que chama
o novo método aoEncacapar:
function TacadaAutomatica(mundo, branca, coloridas, colisor) {
// ...
var tacadaAuto = this;
colisor.encacapou = function(bola) {
tacadaAuto.aoEncacapar(bola);
}
}
Nesse método, precisaríamos fazer algo para tirar a bola da mesa, para
evitar que continue colidindo com outras (caso não tenha saído para fora).
Porém, lembre-se de que, durante o tratamento de colisão, isso não pode ser
feito (seção 9.4). Portanto aqui vamos apenas registrar que a bola foi encaça-
pada:
// Chamado dentro do Step pelo tratador de colisão
aoEncacapar: function(bola) {
this.encacapadas.push(bola);
}
Em executar, inicie o array encacapadas antes de processar todos
os passos. Após cada Step, podemos chamar um método para cuidar das
bolas que deverão “sumir”, deslocarEncacapadas. Por fim, tendo as bo-
las encaçapadas devidamente registradas pelo callback, podemos retorná-las
como resultado:
executar: function(angulo, forca) {
// ...
220
Casa do Código
Capítulo 10. Simulações internas
// Zerar o array antes de processar os ciclos
this.encacapadas = [];
while (continua) {
this.mundo.Step(0.1, 10, 5);
tacada.processar();
// Após o Step podemos dar um jeito nas bolas que se foram
this.deslocarEncacapadas();
}
// Ao fim podemos retornar os resultados (return no final!!)
return {
rotacao: angulo,
forca: forca,
encacapadas: this.encacapadas
};
},
Em deslocarEncacapadas, podemos fazer com as bolas o mesmo que
fazemos com a bola branca em uma tacada “normal": parar seu movimento e
deslocá-las para além das paredes, para não interferir no movimento das que
ficam:
deslocarEncacapadas: function() {
var arr = this.encacapadas;
for (var i in arr) {
arr[i].corpo.SetLinearVelocity(new b2Vec2(0, 0));
arr[i].corpo.SetPosition(new b2Vec2(-1000, -1000));
}
}
Se você rodar o aplicativo, o aparelho já consegue “pensar” por uns in-
stantes, testando tacadas em todos os ângulos que determinamos, antes de
disparar uma tacada para valer. Ainda falta escolher qual o melhor ângulo,
mas para isso temos que ter claras as regras do jogo. No próximo capítulo
discutiremos mais a fundo que regras vamos adotar, pois existem muitas var-
iedades de bilhar e sinuca. Poderemos então facilmente determinar a melhor
221
10.4. Tratador de colisões para a simulação
Casa do Código
tacada, bem como gerenciar a partida, alternando jogadas do humano e do
computador e determinando quando um venceu. Falta pouco agora!
222
Capítulo 11
Finalizando o jogo
Estamos quase lá! Precisamos agora determinar as regras do jogo para po-
dermos fazer a máquina raciocinar sobre suas tacadas e sobre a partida. Por
que deixei isto somente para o final do livro, se o correto é iniciarmos o de-
senvolvimento com as regras definidas? Porque existem inúmeras variações de jogos de bilhar ou sinuca. Tendo programado toda a mecânica de movimento, podemos reaproveitá-la em diferentes versões do jogo. Se quiser, mantenha
uma cópia reservada do jogo até este ponto. Você pode depois implementar
as regras de outra variação que costuma jogar.
11.1. Regras do Bola 9
Casa do Código
Figura 11.1: Resultado final a ser atingido
11.1
Regras do Bola 9
Aqui implementaremos o Bola 9, uma variedade de bilhar, de acordo com o que está descrito em http://pt.wikipedia.org/wiki/Bilhar:
• o objetivo do jogo é encaçapar a bola de número 9;
• a bola branca deve sempre atingir primeiro a de menor número ainda
na mesa;
• se encaçapar alguma bola colorida, desde que a partir da bola de ordem,
o jogador da vez continua;
• se a bola 9 for encaçapada dentro dessas condições, em qualquer mo-
mento, o jogador da vez ganha a partida;
• se a branca não acertar nenhuma bola ou acertar uma bola inválida, o
jogador passa a vez, mesmo que encaçape;
• se a branca for encaçapada, ela retorna para sua posição inicial. O
mesmo ocorre com a bola 9 encaçapada de maneira ilegítima. Nesses
casos, o jogador também passa a vez.
Primeiro, vamos declarar todas as variáveis de página responsáveis pelo
tratamento das regras. Confira a relação final de declarações:
224
Casa do Código
Capítulo 11. Finalizando o jogo
var canvas, context, mundo, animacao;
var ESCALA = 30;
var bolaBranca, bolasColoridas;
var hammer, taco, barraForca;
var somTacada, somMadeira, somCacapa;
var colisaoListener, simulacaoListener;
var bolaOrdem, jogadorDaVez, primeiraAtingida, encacapadas,
mostrador;
var JOG_HUMANO = 1, JOG_COMPUTADOR = 2;
Agora, configure o início de uma partida. Na função iniciar, retire a
chamada direta a tacadaComputador e mude para definir quem é jogador
da vez e qual a primeira bola a ser acertada:
function iniciar() {
// ...
// O final fica:
jogadorDaVez = JOG_HUMANO;
bolaOrdem = bolasColoridas[0];
animacao.ligar();
// Remova
//tacadaComputador();
}
Sorteando o jogador
Se desejar, use Math.random para determinar quem sai na primeira
partida.
Determinando a primeira bola atingida
Antes de tudo, vamos determinar qual foi a primeira bola atingida pela
bola branca em uma tacada. Para isso, temos que ouvir eventos de colisão
entre bolas. Isso já está sendo feito em ColisaoListener, mas não em
SimulacaoListener. Altere a classe para que detecte essa situação e chame
um callback:
225
11.1. Regras do Bola 9
Casa do Código
SimulacaoListener.prototype.BeginContact = function(contato) {
var objA = contato.GetFixtureA().GetBody().GetUserData();
var objB = contato.GetFixtureB().GetBody().GetUserData();
// Detecte ambas as bolas
var bola1 = (objA instanceof Bola ? objA : null);
var bola2 = (objB instanceof Bola ? objB : null);
var bola = (bola1 || bola2);
var cacapa = (objA instanceof Cacapa ? objA :
objB instanceof Cacapa ? objB : null);
if (bola && cacapa && this.encacapou)
this.encacapou(bola);
// Callback de colisão
if (bola1 && bola2)
this.colisaoBolas(bola1, bola2);
}
Em TacadaAutomatica, no método executar, inicie o atributo
primeiraAtingida como null antes do loop que processa os passos da
tacada de simulação:
this.encacapadas = [];
this.primeiraAtingida = null;
while (continua) {
// ...
}
No construtor, crie um callback que verifica qual a primeira bola atingida
pela branca. Se o atributo já estiver setado, nada deve ser feito. Se a branca não pegar em nenhuma, o evento não ocorrerá e o atributo continuará null:
function TacadaAutomatica(mundo, branca, coloridas, colisor) {
// ...
colisor.colisaoBolas = function (bola1, bola2) {
226
Casa do Código
Capítulo 11. Finalizando o jogo
if (tacadaAuto.primeiraAtingida) return;
if (bola1.id == BOLA_BRANCA)
tacadaAuto.primeiraAtingida = bola2;
else if (bola2.id == BOLA_BRANCA)
tacadaAuto.primeiraAtingida = bola1;
}
}
Analogamente, a classe ColisaoListener também deve disparar um
callback em caso de colisão entre bolas:
if (bola1 && bola2) {
this.somBola.play();
this.somBola.seekTo(0);
if (this.colisaoBolas) this.colisaoBolas(bola1, bola2);
}
Claro, documente o callback no construtor:
function ColisaoListener(somBola, somMadeira, somCacapa) {
// ...
this.colisaoBolas = null;
}
Agora, atribua esse callback na página HTML, na função iniciar (pode
ser logo após a instanciação):
colisaoListener = new ColisaoListener(somTacada, somMadeira,
somCacapa);
colisaoListener.colisaoBolas = colisaoEntreBolas;
Por fim, crie a função colisaoEntreBolas, com a mesma lógica
de determinação da primeira bola atingida, mas referenciando a variável
primeiraAtingida da página:
function colisaoEntreBolas(bola1, bola2) {
if (primeiraAtingida) return;
if (bola1.id == BOLA_BRANCA)
227
11.1. Regras do Bola 9
Casa do Código
primeiraAtingida = bola2;
else if (bola2.id == BOLA_BRANCA)
primeiraAtingida = bola1;
}
Analisando as jogadas
Sabendo qual a primeira bola que foi atingida pela branca em uma tacada,
seja em simulação ou numa tacada real do jogo, podemos aplicar as regras
para analisar a jogada. Vamos delegar essa tarefa para uma nova classe,
AnaliseJogada, para podermos usar as mesmas regras em todas as situ-
ações. Portanto, faça seu link na página:
<script src="js/analise-jogada.js"></script>
Antes de programá-la, é preciso definir o que esperar dela, conforme
nossa prática habitual de primeiro “mandar” na classe, para depois fazê-la
obedecer. Na classe TacadaAutomatica, no método realizarTestes,
vamos fazer a análise ao fim de cada ciclo do loop que testa os ângulos,
guardando os resultados. Tão logo ocorrer algo favorável, podemos parar os
testes e retornar o objeto com os dados, economizando tempo de processa-
mento. Perceba que, para analisarmos corretamente uma tacada, temos que
saber qual a bola de ordem (da vez), por isso a recebemos como parâmetro
no início:
// Receba a bola de ordem
realizarTestes: function(bolaOrdem) {
// ...
for (var i = 0; i <= 360; i += 5) {
// ...
var analise = new AnaliseJogada();
analise.analisar(this.primeiraAtingida, bolaOrdem,
tacada.encacapadas);
// Guarda a análise junto no resultado
tacada.analise = analise;
228
Casa do Código
Capítulo 11. Finalizando o jogo
// Por ordem de prioridade
if (analise.ganhouJogo)
return tacada;
else if (analise.encacapou)
return tacada;
}
// ...
}
Ao fim do método, se não foi encontrada nenhuma tacada favorável, fi-
camos com a primeira que for legal:
realizarTestes: function(bolaOrdem) {
// ...
// Encontrar a primeira tacada válida
for (var i in resultados) {
if (resultados[i].analise.jogadaLegal)
return resultados[i];
}
// Se não foi possível fazer nenhuma válida...
return resultados[0];
},
Na função tacadaComptuador, passe a bola de ordem, já definida an-
teriormente, quando chamar realizarTestes:
function tacadaComputador() {
// ...
var melhorTacada = automatica.realizarTestes(bolaOrdem);
// ...
}
Agora comece a implementar a classe AnaliseJogada. No construtor,
documentaremos todas as situações possíveis:
229
11.1. Regras do Bola 9
Casa do Código
function AnaliseJogada() {
this.jogadaLegal = false;
this.encacapouBranca = false;
this.encacapou = false;
this.ganhouJogo = false;
}
No método analisar, começamos montando um array somente com
os números das bolas, a fim de facilitar a análise. Definimos que uma jogada é legal caso a primeira bola atingida seja a bola de ordem e a branca não tenha
sido encaçapada. Em caso de jogada legal, podemos determinar se houve
encaçapada normal (para o jogador poder continuar) e se houve vitória (bola
9 encaçapada em uma jogada legal):
AnaliseJogada.prototype = {
analisar: function(primeiraAtingida,bolaOrdem,encacapadas){
// Números (IDs)
var ids = [];
for (var i in encacapadas)
ids.push(encacapadas[i].id);
// Jogada foi válida?
this.encacapouBranca = (ids.indexOf(BOLA_BRANCA) >= 0);
this.jogadaLegal = (primeiraAtingida &&
primeiraAtingida.id == bolaOrdem.id &&
!this.encacapouBranca);
// Resultados
this.encacapou = (this.jogadaLegal && ids.length > 0);
this.ganhouJogo =
(this.jogadaLegal && ids.indexOf(9) >= 0);
}
}
O aparelho já levará bem menos tempo para encontrar uma tacada que
dê algum resultado ou, pelo menos, seja válida pelas regras do jogo. Se quiser fazer o teste, coloque novamente uma chamada a tacadaComputador ao
fim da função iniciar.
230
Casa do Código
Capítulo 11. Finalizando o jogo
Controlando a partida
Analogamente, temos que fazer as mesmas verificações em uma tacada
real, seja do computador, seja do jogador, para controlar o andamento da
partida. Na função tacada, vamos iniciar as variáveis encacapadas e
primeiraAtingida, da mesma forma que na simulação:
function tacada() {
// ...
// Realizar a tacada
encacapadas = [];
primeiraAtingida = null;
var tacada = new Tacada(bolaBranca, bolasColoridas);
// ...
}
Nas funções encacapouBranca e encacapouColorida, acrescente
a bola encaçapada no array. Não é preciso fazer nenhuma distinção, pois a
classe AnaliseJogada cuida de verificar se a bola branca foi encaçapada,
requerendo apenas um array com todas elas:
function encacapouBranca(bola) {
encacapadas.push(bola);
// ...
}
function encacapouColorida(bola) {
encapapadas.push(bola);
// ...
}
O callback bolasPararam é chamado pela Tacada quando a animação
finaliza. Vamos chamar ali uma nova função responsável por analisar a jogada
e determinar o rumo da partida:
231
11.1. Regras do Bola 9
Casa do Código
function bolasPararam() {
// ...
analisarJogada();
}
Chegamos ao coração da partida, ao árbitro do jogo.
A função
analisarJogada solicita os serviços da classe AnaliseJogada da mesma
forma que a TacadaAutomatica. Se houve vitória, é exibida uma men-
sagem e a página é recarregada. Se encaçapou, não alteramos nada para per-
mitir ao jogador da vez continuar. Porém, caso não haja encaçapada legítima,
trocamos o jogador da vez:
function analisarJogada() {
var analise = new AnaliseJogada();
analise.analisar(primeiraAtingida, bolaOrdem, encacapadas);
// Se ganhou, notifica e reinicia
if (analise.ganhouJogo) {
alert(jogadorDaVez == JOG_HUMANO ? 'Você venceu!' :
'Você perdeu...');
location.reload();
return;
}
// Se não encaçapou legalmente, passa a vez
else if (!analise.encacapou) {
jogadorDaVez = (jogadorDaVez == JOG_HUMANO ?
JOG_COMPUTADOR : JOG_HUMANO );
}
// continua...
}
Também é preciso determinar a bola da vez. No caso, a de menor
número ainda na mesa. Também, se for vez do computador, chamamos
tacadaComputador para realizar a jogada automática:
function analisarJogada() {
// ...
232
Casa do Código
Capítulo 11. Finalizando o jogo
// Mudar a bola de ordem
for (var i in bolasColoridas) {
var b = bolasColoridas[i];
if (!b.foraDeJogo) {
bolaOrdem = b;
break;
}
}
if (jogadorDaVez == JOG_COMPUTADOR)
tacadaComputador();
}
Finalmente, tomemos uma providência com relação à bola 9. Se ela for
encaçapada de maneira ilegítima, temos que retorná-la à mesa. Na função
encacapouColorida, se o número da bola for 9, damos a ela o mesmo
tratamento que à bola branca:
function encacapouColorida(bola) {
encapapadas.push(bola);
animacao.novoProcessamento({
processar: function() {
if (bola.id == 9) {
bola.foraDeJogo = true;
bola.corpo.SetLinearVelocity(new b2Vec2(0, 0));
bola.corpo.SetPosition(new b2Vec2(-100/ESCALA,
-100/ESCALA));
}
else {
bola.tirarDoJogo();
}
animacao.excluirProcessamento(this);
}
});
}
233
11.2. Mostradores
Casa do Código
Também na função bolasPararam, que a voltará para seu lugar inicial.
Isso pode ser feito em qualquer posição da função (pode ser antes ou após a
verificação da bola branca, só para agrupar lógicas semelhantes):
function bolasPararam() {
var bola9 = bolasColoridas[8]; // começa em zero
if (bola9.foraDeJogo) {
bola9.corpo.SetPosition(
new b2Vec2(560/ESCALA, 200/ESCALA));
bola9.foraDeJogo = false;
}
// ...
}
Uau! Já é possível jogar uma partida completa! E o computador é
impiedoso e trapaceiro! Usa o Box2dWeb para prever o futuro!
Nível de dificuldade
Você pode inserir fatores aleatórios com Math.random na classe
TacadaAutomatica para fazer o computador rejeitar jogadas fa-
voráveis de vez em quando, diminuindo assim o nível de dificuldade.
Outra sugestão é fazer os testes de 10 em 10 graus (ou outro intervalo),
tornando o processamento mais rápido e aumentando as chances de não
encontrar uma tacada conveniente.
11.2
Mostradores
Para melhorar a experiência final do jogador, vamos criar um sprite que
mostra qual a bola de ordem.
Um último script para linkar:
<script src="js/mostrador.js"></script>
Em iniciar, instancie o sprite e insira-o na animação. Isso deve ser
feito após a definição da bola de ordem:
234
Casa do Código
Capítulo 11. Finalizando o jogo
function iniciar() {
// ...
jogadorDaVez = JOG_HUMANO;
bolaOrdem = bolasColoridas[0];
// Quebra aqui para criar o mostrador
mostrador = new Mostrador(context);
mostrador.bolaOrdem = bolaOrdem;
animacao.novoSprite(mostrador);
animacao.ligar();
}
Segue o esqueleto da classe.
No construtor, passamos a constante
BOLA_SPRITE, criada em bola.js, claramente criando uma dependência
entre os scripts:
function Mostrador(context) {
this.context = context;
this.bolaOrdem = null;
this.sheet = new Spritesheet(context, BOLA_SPRITE, 10, 19);
}
Mostrador.prototype = {
atualizar: function() {
//
},
desenhar: function() {
}
}
Imprimimos o texto “Próxima” e desenhamos a bola em tamanho re-
duzido:
desenhar: function() {
var ctx = this.context;
ctx.save();
235
11.2. Mostradores
Casa do Código
ctx.fillStyle = 'white';
ctx.font = '10px';
ctx.fillText('Próxima:', 380, 370);
this.sheet.linha = this.bolaOrdem.id;
this.sheet.desenhar(430, 362, 10, 10);
ctx.restore();
}
De volta à página HTML, na função analisarJogada, atribua ao
mostrador a nova bola de ordem quando houver mudança:
function analisarJogada(){
// ...
// Mudar a bola de ordem
for (var i in bolasColoridas){
var b = bolasColoridas[i];
if (!b.foraDeJogo){
bolaOrdem = b;
mostrador.bolaOrdem = b;
break;
}
}
// ...
}
O jogo de bilhar está pronto! Obviamente, há muito espaço para mel-
horas e incrementos na jogabilidade, na funcionalidade, na arquitetura, que
você pode implementar como forma de se exercitar e fixar seus novos con-
hecimentos. Não quis, nem de longe, criar o jogo perfeito, e preferi me ater
ao essencial. Mas espero, com sinceridade, que tenha gostado.
Nós aprendemos a:
• capturar gestos em telas touch com o Hammer.js;
236
Casa do Código
Capítulo 11. Finalizando o jogo
• associar corpos físicos aos sprites;
• detectar eventos de colisão e agendar a modificação ou exclusão dos
corpos para depois do Step;
• disparar callbacks para o jogo e suas classes implementar as regras de
negócio nesses eventos.
Sugestões para melhorar o jogo
• Uma funcionalidade que faz sumir os controles e botões,
permitindo-nos ver toda a mesa.
• Mudar de lado o controle do taco, a barra de força e o botão da
tacada de acordo com o lado da mesa em que se encontra a bola
branca.
• Ou, quem sabe, sumir com o controle circular do taco,
permitindo-nos fazer o rotate em qualquer ponto da mesa. Isto
certamente traria um grande ganho de jogabilidade, e só preferi
fazer do modo mais restrito para demonstrar como limitar a área.
• Menus de configuração e níveis de dificuldade.
• Jogo multiplayer com Ajax e tecnologias de servidor web.
• E o que mais sua imaginação criar.
237
Parte III
Conclusão
Chega ao fim mais uma jornada pelo fantástico mundo do Canvas do
HTML5 e suas possibilidades. Nem de longe eu quis esgotar os assuntos.
Acredito que a maior lição que tem de ficar de todo este esforço é que de-
senvolvimento de jogos, como qualquer desenvolvimento de software, requer
estudo constante, uma vida dedicada a estudar e a praticar. Por isso deixei tantos links no livro, para que você possa se aprofundar da mesma forma como
eu tive que me aprofundar em cada tópico. Você pode e deve saber que existe
mais além do que eu trouxe aqui.
Estarei esperando links para os jogos feitos por meus leitores, seja online,
na Play Store, na Apple Store... Divulgue suas criações para nós no Google
Groups:
http://groups.google.com/forum/#!forum/livro-jogos-html5-canvas
Bom desenvolvimento!
Éderson Cássio Lacerda Ferreira
Casa do Código
Capítulo 11. Finalizando o jogo
Referências Bibliográficas
[1] Emanuele Feronato. Box2D for Flash Games. 2012.
[2] Sérgio Lopes. A Web Mobile - Programe para um mundo de muitos dis-
positivos. 2013.
[3] John M. Wargo. Apache Cordova 3 Programming. 2013.
[4] Tárcio Zemel. Web Design Responsivo - Páginas adaptáveis para todos os dispositivos. 2013.
[5] Éderson Cássio. Desenvolva Jogos com HTML5 Canvas e JavaScript. 2014.
241