You are on page 1of 345

Cleuton Sampaio

Manual do Indie Game Developer


Verso Android e iOS

Manual do Indie Game Developer - Verso Android e iOS


CopyrightEditora Cincia Moderna Ltda., 2013
Todos os direitos para a lngua portuguesa reservados pela EDITORA CINCIA
MODERNA LTDA.
De acordo com a Lei 9.610, de 19/2/1998, nenhuma parte deste livro poder ser
reproduzida, transmitida e gravada, por qualquer meio eletrnico, mecnico, por
fotocpia e outros, sem a prvia autorizao, por escrito, da Editora.
Editor: Paulo Andr P. Marques
Produo Editorial: Aline Vieira Marques
Assistente Editorial: Amanda Lima da Costa
Capa: Carlos Arthur Candal
Diagramao: Equipe Cincia Moderna
Vrias Marcas Registradas aparecem no decorrer deste livro. Mais do que
simplesmente listar esses nomes e informar quem possui seus direitos de
explorao, ou ainda imprimir os logotipos das mesmas, o editor declara estar
utilizando tais nomes apenas para fins editoriais, em benefcio exclusivo do dono da
Marca Registrada, sem inteno de infringir as regras de sua utilizao. Qualquer
semelhana em nomes prprios e acontecimentos ser mera coincidncia.
FICHA CATALOGRFICA
MELO JUNIOR, Cleuton Sampaio de.
Manual do Indie Game Developer - Verso Android e iOS
Rio de Janeiro: Editora Cincia Moderna Ltda., 2013.

1. Informtica 2. Linguagem de Programao 3. Jogo eletrnico


I Ttulo
ISBN: 978-85-399-0460-2

Editora Cincia Moderna Ltda.


R. Alice Figueiredo, 46 Riachuelo
Rio de Janeiro, RJ Brasil CEP: 20.950-150
Tel: (21) 2201-6662/ Fax: (21) 2201-6896
E-MAIL: LCM@LCM.COM.BR
WWW.LCM.COM.BR

CDD 0001.642
005.133
323.43

07/13

Dedico este livro minha esposa, Ftima e aos meus filhos:


Rafael, Tiago, Lucas e Ceclia.

Escolha uma profisso que voc ame, e nunca ter


que trabalhar um s dia em sua vida.
Confcio, filsofo e pensador Chins: (551 a.c 479 a.c)

Sumrio
Captulo 1 - Introduo1
Sobre as plataformas 1
Sobre o cdigo-fonte 2

Captulo 2 - Reviso de conceitos3

Tipos bsicos de Games 3


Jogos de ao 7
Macro funes de um game 8
Model / View / Controller 10
Objetos e elementos tpicos de um Game 11
Jogabilidade 13
Fatores de sucesso de um Game 14

Captulo 3 - Kit de ferramentas para criao de games17

Ferramentas grficas 17
Imagens 17
Transparncia e canal alpha 18
Unidades de medida 20
Arquivos de imagem 21
Ferramenta para Sketch 22
Ferramentas para desenho Raster 23
Ferramentas para desenho vetorial 24
Comparao entre renderizao raster e vetorial 25
Game engines 28
HTML 5 + Javascript 28
Nativos 28
Usar a plataforma nativa 30
Ambientes multiplataforma 30
Prototipadores 31
Bibliotecas auxiliares 31

Captulo 4 - Prototipao do game33

Usando o Codea 34
Um tutorial rpido em Codea 35
Criando o prottipo do AsteroidNuts no Codea 39

Sumrio VII

Usando o Processing 49
Um tutorial rpido no Processing 51
Criando o prottipo do AsteroidNuts com o Processing 54
Crie vrios prottipos 64

Captulo 5 - Fsica de games65

Os primrdios 66
Conceitos bsicos 66
Acelerao e movimento 68
Coliso 70
Deteo de coliso 72
Engines de fsica 76
Bullet Physics Library 76
Chipmunk physics engine 77
Box2D  77
Fsica com Box2D 78
Preparando o laboratrio 79
O laboratrio Java 80
Normalizao e ajuste para renderizao 85
Fundamentos do Box2D 87
Fora e atrito 93
Corpos circulares 97
Rotao dos corpos 98
Desenhando corpos como polgonos 100
Desenhando corpos como imagens 102
Torque e impulso 107
Deteo de coliso 110
Juntas ou junes 113
Usando o Box2D em plataformas mveis 123
Box2D no Android 123
Box2D no iOS 132

Captulo 6 - Renderizao com OpenGL ES 2.0143


OpenGL ES 144
Fundamentos 146
Coordenadas dos vrtices e da textura 146
Buffers 150

VIII

Manual do Indie Game Developer - Verso Android e iOS

Programas de sombreamento 152


Matrizes 154
Exemplo utilizando Android e projeo perspectiva 159
Entendendo a GLSurfaceView 160
A implementao 163
Concluindo 175
Exemplo utilizando iOS e projeo perspectiva 176
Entendendo o GLKit 176
Criando uma aplicao OpenGL ES 178
A implementao 179
Achatando as coisas 190
Implementao em Android 190
Implementao em iOS 193

Captulo 7 - Framework de Fsica e Renderizao195


Um framework bsico 196
O esquema XML do arquivo de modelo de game 197
Proporcionalidade dos GameObjects 200
Coordenadas e VBOs 201
Movimento e rotao de objetos 204
Atualizao do mundo Box2D 205
Renderizao do modelo 206
Mipmaps 207
Uso do Framework 209
Concluso 212

Captulo 8 - Tcnicas Comuns em Games215


Como exibir um HUD 215
Aumentando o framework de game 220
Integrando o game em uma aplicao mvel 226
Plataforma Android 227
Plataforma iOS 231
Tempo e movimento 233
Game Loop 234
Movimento 247
Efeito de paralaxe 256
Tcnicas de paralaxe 257
Exemplo de paralaxe com camadas de GameObjects 258

Sumrio IX

Implementao Android 261


Implementao iOS 267
Games do tipo plataforma 273
Implementao Android 276
Implementao iOS 278
Sistemas de partculas 280
Composio 280
Um exemplo 281
Implementao em Android 283
Implementao iOS 291
Concluso 296

Captulo 9 - Vamos Criar um Game297

Limitaes 299
Licena de uso do cdigo-fonte 299
A concepo 300
Jogos casuais 301
Jogabilidade 301
Implementao Bsica 302
I18N e L10N 302
Alteraes no modelo do game 303
Alteraes na carga do nvel corrente 307
Alteraes no Game Loop 309
Colises 311
Registro de tempos 315
Gesto de memria no iOS 323
Uso do GLKBaseEffect 324
Procure Memory Leaks 327
Verifique se est realmente liberando a memria 328
Concluso 332
Possveis melhorias no game 333

Captulo 1
Introduo
Quando eu comecei a desenvolver games, li muitos livros sobre o assunto e
fiz muitas pesquisas no Google. Muitos dos problemas que eu passei no incio
tive que batalhar muito para resolver sozinho. Dessa forma, reuni um conjunto
de tcnicas ao longo do meu aprendizado, que so teis at hoje. Durante esse
aprendizado, notei que muito difcil encontrar guias prticos de uso, com
exemplos simples, o que nos fora a ler muito e escrever muitas provas de
conceito, perdendo tempo neste processo.
Ento, resolvi colocar no papel tudo o que eu havia aprendido, e que poderia ser til para outras pessoas. Assim, nasceu a ideia deste livro, que um
guia prtico com solues para os problemas comuns no desenvolvimento de
games mveis.
Eu queira algo mais que um recipe book (livro de receitas), porm menos que uma referncia completa. Um livro que voc consegue ler rapidamente e consultar sempre que precisar, com inmeros exemplos em cdigo-fonte,
alm de um game completo.
Neste livro, eu assumo que voc, leitor, leitora, um desenvolvedor experiente e que deseja criar games para plataformas mveis, especialmente:
Android e iOS. Apesar de j haver escrito livros introdutrios sobre o assunto,
este um livro para profissionais que desejem entrar no lucrativo negcio de
desenvolvimento de games, tornando-se um Indie Game Developer (desenvolvedor independente de games).
Ao longo do livro, voc construir um framework simples, porm abrangente, e que pode ser utilizado para criar games mveis biplataforma (Android
e iOS) rapidamente. O game exemplo do livro Bola no Quintal, foi feito em
apenas 1 semana para as duas plataformas!
A melhor maneira de aproveitar todo o contedo ler com a mo na massa, rodando todos os exemplos de cdigo.

Sobre as plataformas
Este livro apresenta exemplos em Android e iOS, logo, voc dever ter
instalados os dois kits de desenvolvimento. Nada impede que voc opte por
uma das duas plataformas, pois o livro no obriga voc a conhecer ambas. A

2 Manual do Indie Game Developer - Verso Android e iOS

plataforma Android utilizada a ltima verso disponvel (4.2, API = 17), porm, os exemplos podem ser executados em qualquer dispositivo com verso
Gingerbread (maior ou igual a 2.3). A plataforma iOS utilizada tambm a
ltima verso disponvel (6.1), com Xcode 4.5.
Se optar por utilizar as duas plataformas, ser necessrio utilizar um computador Mac / OS X, com verso mnima Mountain Lion, podendo desenvolver para Android e iOS.

Sobre o cdigo-fonte
Todo o cdigo-fonte do livro est disponvel para download, em formato
zip. Voc poder encontr-lo nos seguintes locais:
Site da editora: http://www.lcm.com.br, procure pelo ttulo do livro;
Site especfico deste livro: http://www.indiegamerbrasil.com;
Site The Code Bakers: http://www.thecodebakers.com.
Se tiver alguma dvida ou dificuldade para instalar os projetos ou sobre o
livro, por favor me contate:
cleuton.sampaio@gmail.com

Captulo 2
Reviso de conceitos
Games... Que coisa apaixonante! So capazes de despertar emoes, tanto
negativas como positivas e so obras de arte vivas, que deixamos para a
posteridade. Se voc nunca criou um game, no sabe quo boa a sensao de
saber que as pessoas passam horas se divertindo com a sua criao.
Se algum me diz que criou um compilador, ou um sistema operacional,
certamente vai me impressionar. Agora, se algum me diz que criou um Game,
certamente vai chamar minha ateno imediatamente. Para mim, o ultimate
fight de todo programador criar um Game. Isto o que diferencia os bons
dos comuns.
Para mim, desenvolver um Game a maior prova de competncia para
um programador. realmente um desafio muito grande, pois existem muitos
aspectos a serem abordados. Um game um projeto de software complexo,
com vrios recursos, de reas diferentes envolvidas.

Tipos bsicos de Games


Existe mais de uma maneira de classificarmos um game, dependendo do
ponto de vista. Se estivermos falando sobre visualizao, podemos comear
com:
Games 2D: aqueles em que jogamos nos movendo apenas em duas
direes. Podem at utilizar recursos para dar iluso de profundidade (como paralaxe, viso isomtrica etc), mas nos movemos em
um plano. Existem jogos que permitem mudar de plano (e perspectiva), como o Fez (http://en.wikipedia.org/wiki/Fez_(video_game)),
mas ainda so basicamente 2D nos movimentos;
Games 3D: aqueles em que jogamos nos movendo em seis graus de
liberdade (http://en.wikipedia.org/wiki/Six_degrees_of_freedom), com
movimentos possveis de: aproximao / afastamento (eixo z), direita / esquerda (eixo x), cima / baixo (eixo y), e rotaes em
cada eixo. Alguns games 3D no permitem isso tudo, restringindo as
rotaes, por exemplo;
H uma certa confuso quando falamos em Games 3D. Algumas pessoas
confundem este conceito com iluso 3D. Na verdade, o conceito que a literatura e os gamers mais aceitam o dos 6 graus de liberdade.

4 Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 1: Seis graus de liberdade


(autor: Horia Ionescu, Wikipedia http://en.wikipedia.org/wiki/Six_degrees_of_freedom)

A figura anterior nos mostra os movimentos possveis em um verdadeiro


Game 3D. Podemos: correr, pular, saltar, rolar (nos trs eixos) etc. Alguns games limitam o movimento somente aos trs eixos, evitando rotaes, porm,
mesmo assim, no se limitam a um plano, como os Games 2D.
Tambm podemos classificar um Game quanto ao seu tipo de projeo, que
a maneira como o modelo projetado na tela.

Ilustrao 2: Projeo 3D

Se for um Game 3D, isto mais crtico ainda. A traduo do modelo espacial para o modelo 2D da tela pode ser feito de vrias maneiras:
Projeo Ortogrfica (ou ortogonal): a iluso de 3D criada ao
criarmos projees ortogonais ao plano, ou seja, no h diferena de
tamanho entre objetos prximos e objetos distantes. Desta forma,
mostramos os elementos em um ngulo diferente do que veramos
apenas de cima, ou de lado. Evita os clculos complexos de projeo
3D e mantm uma boa iluso para o jogador.

Captulo 2 - Reviso de conceitos 5

Viso Isomtrica: no uma projeo, mas uma forma de mostrarmos os objetos. Neste tipo de viso, rotacionamos um pouco os
eixos para mostrar mais detalhes dos objetos, criando a iluso 3D.
Neste caso, o ngulo entre os eixos (x, y, z) no plano igual, ou seja:
120 graus. Normalmente, a viso isomtrica utilizada com projeo ortogrfica. Era muito comum nos anos 80 e 90, embora alguns
jogos ainda a utilizem. Alguns exemplos famosos que usam viso
isomtrica: Q*Bert (Gottlieb), Civilization II (MicroProse), Diablo
(Blizzard);
Projeo Perspectiva: vem do desenho tradicional, no qual as
partes mais prximas parecem maiores do que as mais distantes.
Tambm conhecida como perspectiva cnica. Do maior iluso de
realidade aos games, porm exigem maior complexidade de programao e poder de processamento. Os jogos mais modernos, como:
Battlefield, Assassins Creed, Halo e outros apresentam viso em
perspectiva;

Ilustrao 3: Projees 2D de um cubo

claro que procurei simplificar ao mximo o conceito de projeo 3D,


mas apenas para facilitar o entendimento. Se quiser saber mais, recomendo
um bom livro, como o Mathematics for Game Developers, de Christopher
Tremblay (Amazon). Porm, existem bons artigos introdutrios na Wikipedia:
Perspectiva (grfica): http://pt.wikipedia.org/wiki/Perspectiva_(gr%C3%A1fica);
Isometria: http://pt.wikipedia.org/wiki/Isometria_(geometria);
Isometric graphics in video games and pixel art: http://en.wikipedia.
org/wiki/Isometric_graphics_in_video_games_and_pixel_art;
Tambm existem classificaes de games quanto s suas caractersticas,
algumas delas so:
Arcade: este termo mal interpretado, mas sua origem vem das
mquinas de jogos, conhecidas aqui como Fliperama. Jogos arcade possuem regras simples, partidas rpidas, com pontuao,

6 Manual do Indie Game Developer - Verso Android e iOS

vidas, viso Isomtrica etc). Exemplos de arcades clssicos: Space invaders e Pac-Man. Os jogos arcade evoluram e hoje esto
disponveis em vrias plataformas, incluindo mobile;
Jogos de Ao: desafiam o jogador fisicamente, seja por coordenao motora, viso de movimento ou tempo de reao. Possuem
vrias subcategorias, como: plataforma, tiro e luta. Exemplos clssicos: River raid (Atari), Sonic (SEGA), Super Mario (Nintendo) e outros mais modernos, como: Angry Birds (Rovio), Jetpack joyride (Halfbrick). Na verdade, arcades so jogos de ao, s
que com regras e visual mais simples;
Aventura (adventure): jogos que desafiam a inteligncia do jogador, colocando-o em determinado mundo, com uma misso a cumprir. Nos primrdios da computao pessoal, os jogos de aventura
eram simples, muitas vezes textuais, ou com poucos grficos. Eu
gosto de citar os exemplos do Renato Degiovani, como: Serra Pelada e Angra-I. Hoje em dia, muitos jogos de aventura tambm
misturam caractersticas de jogos de ao, como os Shooters;
Role Playing Game (RPG): so derivados dos famosos jogos de
RPG textuais, como Dungeons & Dragons. Na verdade, so jogos
de aventura, nos quais o jogador assume um papel e uma vida paralela em um universo virtual. A principal diferena para os jogos de
aventura tradicionais a durao do jogo, que pode ser indeterminada. Hoje em dia, os jogos RPG ganharam verses multiusurio
online, como os MMORPG Massively Multiuser On-line RPG
(RPGs on-line massivamente multiusurios). Exemplos: World of
Warcraft (Blizzard) e Ragnark (Gravity Corp);
Tile based games: podem ser jogos de ao ou RPG, nos quais os
personagens se movem em um tabuleiro, de casa em casa. Existem vrios padres e ferramentas para criar Tile Based Games,
como o padro TMX (https://github.com/bjorn/tiled/wiki/TMX-Map-Format) e TiledEditor (http://www.mapeditor.org/). Alguns
RPGs e jogos de aventura so tambm Tile Based;
Menu games: so jogos de simulao e RPG, com interface baseada
em escolha. Parecem muito com os jogos de cartas, como: Magic.
Voc tem uma srie de atributos e vai desafiando os outros jogadores, acumulando vitrias ou amargando derrotas. Um bom exemplo
o Mafia Wars (Zynga);

Captulo 2 - Reviso de conceitos 7

Simuladores: simulam veculos ou atividades. Existem simuladores


de voo (Microsoft Flight Simulator), de carro (Gran Turismo Sony),
de cidade (SimCity Maxis). So mais difceis e desafiadores;
Estratgia: so uma mistura de simulao e RPG, no qual o jogador tem que conquistar um objetivo, geralmente atravs da guerra.
Exemplos: Age of Empires (Microsoft) e Civilization (Micro
Prose);
Outra classificao importante quanto ao pblico-alvo do Game, por
exemplo:
Infantis: jogos para crianas, geralmente no alfabetizadas ou em
fase de alfabetizao. So simples e visuais;
Jogos para meninas: as meninas esto cada vez mais tomando de
assalto o mundo dos games e existem jogos especficos para elas,
como: jogos de vestir, jogos de cozinhar, jogos de princesas;
Casual games (jogos casuais): so feitos para quem no jogador
habitual (ou gamer). So para pessoas que jogam casualmente,
sem ter isto como atividade constante. Podem ser de vrios tipos,
mas, geralmente, so simples, com poucas regras, fcil jogabilidade
e baixo comprometimento (voc no tem que criar uma carreira).
Podem ser quebra-cabeas simples, como: Where is my water
(Disney) ou jogos de tiro, como Angry Birds (Rovio);
Hardcore games: so feitos para jogadores habituais ou gamers.
Normalmente, exigem um comprometimento maior do jogador, que
tem que criar uma carreira virtual, que pode ser comunicada em
redes sociais de games. Geralmente possuem vrias sequncias,
como: Assassins Creed (Ubisoft), Battlefield (EA Games) ou
Gran Turismo (Sony);

Jogos de ao
O objetivo principal deste trabalho so jogos de ao. Em primeiro lugar,
porque todos os elementos clssicos (Player Object, NPC, Game loop) so encontrados neles e, em segundo lugar, porque so uma porta de entrada para
o desenvolvimento de games.
Os jogos de ao podem ser divididos tambm em subcategorias, como:
Plataforma: so jogos 2D com iluso 3D, nos quais o jogador se
move entre plataformas, de diversas altitudes, pulando, correndo e
saltando. Ele pode ter que enfrentar desafios fsicos, como abismos,

8 Manual do Indie Game Developer - Verso Android e iOS

ou mesmo adversrios. Exemplos so: Sonic (SEGA), Super Mario (Nintendo) e, mais modernamente, Super Meat Boy (Team
Meat);
Tiro: seu objetivo matar inimigos atirando diretamente neles ou
derrubando-os. Podem ser simples como o Angry Birds (Rovio),
ou complexos, como o Doom (Id Software / Nerve Software). Entre os jogos de tiro, existem os First Person Shooters, nos quais a
viso a do jogador (em primeira pessoa), ou Third Person Shooters, nos quais a viso externa ao jogador;
Luta: so jogos nos quais o jogador desafiado a derrotar lutadores, que podem ser totalmente virtuais, ou avatares controlados por
outras pessoas. Alguns exemplos so: Street Fighter (Capcom) e
Mortal Kombat (Midway games);
Jogos de ao so baseados em tempo, ou seja, a cada fatia de tempo a
posio do jogador (e das balas) muda, sendo atualizada constantemente.
Quando o processamento de uma fatia de tempo demora demais, acontece o
efeito de Lagging, que quando o jogador percebe a lentido do jogo em
determinados momentos. O Lag pode ter diversas causas, como a latncia
da rede, em caso de jogos multiusurio, ou mesmo a lentido da renderizao
de frames. O Lag pode causar grande insatisfao no usurio, compromentendo o sucesso do Game.
Devido a esta caracterstica de sensibilidade ao tempo, jogos de ao exigem muito do programador. necessrio racionalizar os grficos, utilizando
ao mximo o poder de processamento de GPU (Graphics Processing Unit), e
tambm empregar recursos de multiprogramao, de modo a aproveitar o paralelismo, afinal, muitos dispositivos mveis so multicore (dotados de mais
de uma CPU ou ncleo).

Macro funes de um game


Se pensarmos nas macrofunes de um game, veremos algo como o diagrama seguinte.

Ilustrao 4: Macro funes de um game

Captulo 2 - Reviso de conceitos 9

Inventrio
Responsvel pelos objetos e propriedades do Game. Os elementos ativos
do jogo (personagens ou Player Objects e NPCs) e suas propriedades (quantidade de vidas etc).
Pontuao
Responsvel por acompanhar a pontuao do usurio, suas conquistas e o
compartilhamento destas informaes.
Estratgia
Quando o jogo tem caracteres prprios, no manipulados pelo usurio
(Non-Player Character ou NPC), preciso dar alguma inteligncia a eles,
caso contrrio, o jogo ser fcil demais. Alguns tipos de jogos dispensam a
estratgia, baseando-se apenas em clculos aleatrios, como: labirintos, por
exemplo. Jogos do tipo Shooting (2D, 3D, TPS, FPS), quando no esto em
modo multiusurio, necessitam de estratgias avanadas, para dar a sensao
de realidade ao usurio.
Configurao
Responsvel pela persistncia e recuperao de todas as informaes de
configurao do jogo, como: nveis oferecidos, nvel do jogador, conquistas,
opes etc.
Game loop
o conjunto de comandos repetidos a cada fatia de tempo, responsveis
por movimentar os objetos, atualizando o modelo de dados. Ele tambm pode,
ao final do processamento, invalidar a apresentao, ordenando o redesenho
da tela. Em jogos de ao, geralmente existe um Game loop baseado em temporizador, que atualiza a tela em determinadas fatias de tempo (time frame).
Alguns tipos de jogos dispensam este recurso.
Animao
Os caracteres do jogo e at mesmo o cenrio podem exibir animaes. O
personagem pode sorrir, chorar, gritar e o cenrio pode mudar. At mesmo a
movimentao dos personagens tratada por esta funo. A animao tambm
envolve a tcnica utilizada para desenhar os objetos na tela a cada fatia de
tempo passada.
Fsica
A macrofuno de fsica muito importante em jogos de ao. Ela responsvel por dar realismo ao movimento dos caracteres. Coisas como: impulso, gravidade, movimento e coliso so tratadas por esta funo.

10

Manual do Indie Game Developer - Verso Android e iOS

Interao
Controla a maneira pela qual o usurio interfere no jogo. responsvel por
capturar o input do usurio e converter em aes no jogo.
Comunicao
responsvel por comunicar ao usurio o status do jogo, e tambm o
resultado de suas aes. Pode manter energy bars, contadores, emitir sons,
acionar exploses etc.
Algumas destas macrofunes podem ser implementadas em parte por bibliotecas de terceiros, como: Box2D e OpenGL ES. Porm, mesmo assim,
necessrio um esforo do desenvolvedor para configurar e interagir com elas.
Uma funo muito importante a de Inventrio, que tambm abrange o
modelo de dados do Game.

Model / View / Controller


Um Game bem construdo sempre baseado no padro MVC Model /
View / Controller, no qual os trs aspectos da aplicao esto separados:
Model: responsvel por manter o estado da aplicao;
View: responsvel por ler o estado atual e apresentar ao usurio;
Controller: responsvel por alterar o estado da aplicao atualizando
o modelo e controlar a atualizao da View;
O modelo de um Game a sua representao em memria. Normalmente,
mantemos referncias dos objetos ativos, como: caracteres e cenrio. Mas podemos ter variaes, como camadas diferentes, para dar efeito de paralaxe
(http://en.wikipedia.org/wiki/Parallax_scrolling), que quando os objetos distantes se movem mais lentamente do que os objetos mais prximos, dando iluso
de profundidade.

Ilustrao 5: Efeito paralaxe - Wikipedia: http://en.wikipedia.org/wiki/Parallax_scrolling

Captulo 2 - Reviso de conceitos 11

Na figura, vemos trs camadas de objetos (cu, celeiros e cho), que se


movem em velocidades diferentes, dando iluso de profundidade ao Game.
A camada de apresentao (ou View) de um Game responsvel por capturar o estado do modelo e desenhar os objetos em sua posio atual. Existem
vrias tcnicas para desenhar objetos, desde utilizar os prprios Widgets
nativos do aparelho (Android ImageView ou iOS UIView), ou desenhar dinamicamente os elementos (Android onDraw / iOS drawRect:(CGRect)rect).
Finalmente, a camada Controller tem a misso de atualizar o modelo e
ordenar que a camada de apresentao atualize a tela. A camada Controller
responsvel por executar o Game loop e por obter o input do usurio. Tambm deve se comunicar com a macrofuno de Estratgia para ver
como o jogo deve reagir.

Objetos e elementos tpicos de um Game


Temos alguns elementos comuns em vrios tipos de games, especialmente
nos jogos de ao. Por exemplo, sempre existe um objeto que representa o jogador ou que ele est controlando diretamente. Este tipo de objeto chamado
de Player Object. Existem casos em que mais de um PO (Player Object)
existe ao mesmo tempo, e o jogador consegue mudar o controle de um para
outro (Sonic & Tails, Jogos de Futebol etc). Porm, geralmente, o jogador est
controlado apenas um nico PO a cada momento.
O Player Object pode ser uma nave, como no famoso jogo Asteroids
(Atari http://en.wikipedia.org/wiki/Asteroids_(video_game)), no qual o jogador
tinha uma nave e podia virar e atirar contra os asteroides. Tambm pode ser
um objeto como uma bola, como no jogo para iPad Labirynth HD (Illusion
Labs AB). No jogo que apresentei em meu ltimo livro, BueiroBall, o jogador controlava a mesa, logo, todas as bolas eram POs.
Outro tipo de objeto o NPC Non Player Character (Caractere ou personagem no controlado pelo jogador). Ele representa o adversrio, que pode
(ou no) ser dotado de alguma inteligncia. Nos jogos multiusurio, o NPC
pode ser um PO controlado por outro Jogador, ou pode ser algo controlado
pela inteligncia do jogo em si. NPCs existem para complicar a vida do jogador, tornando a partida mais divertida e interessante. Quem que no se lembra das tartarugas do Super Mario? O ponto mais importante dos NPCs
que eles possuem movimento e comportamento, ou seja, podem atirar,
perseguir, se esconder, explodir ou tomar atitudes, que so controladas pela
macro funo de Estratgia do jogo. Um NPC sensvel ao movimento e
coliso.

12

Manual do Indie Game Developer - Verso Android e iOS

Levando em conta a caracterstica de sensibilidade ao tempo, tpica dos


jogos de ao, um NPC ocupa fatia considervel do processamento e dos recursos. Quanto mais NPCs um jogo tem em determinado momento, maior
a quantidade de recursos para atualiz-los e moviment-los. Balas tambm
podem ser consideradas como NPCs, pois se movimentam e so sensveis a
colises.
Tambm existem os objetos estticos, que no so sensveis a colises.
Objetos estticos podem at se mover, mas sempre como parte de um cenrio.
No so sensveis a colises e nem podem ser controlados pelo jogador. Alguns exemplos: cenrio, nuvens, rvores distantes, montanhas etc. Em certos
casos, os objetos estticos nem existem, sendo parte da imagem de fundo. Em
jogos mais modernos, objetos podem ser acrescentados ao fundo para dar a
iluso de paralaxe. O importante que objetos estticos no podem ser afetados pelo PO ou pelos NPCs. Jogos mais modernos permitem que se deforme
em algum grau o cenrio, destruindo prdios, veculos de fundo etc. Nestes
casos, os objetos estticos esto se comportanto como NPCs, recebendo e se
deformando em funo de tiros, o que exige um alto poder de processamento
da console ou do dispositivo mvel.
Game Loop o ncleo de atualizao de um jogo de ao. nele que
ocorre a atualizao do Modelo e pode tambm ocorrer a renderizao da
View (MVC). Os game loops mais simples podem fazer apenas isto:
1. Verificar se o usurio entrou com algum estmulo;
2. Executar a estratgia pra os NPCs;
3. Mover o Player Object;
4. Mover os NPCs;
5. Verificar colises;
6. Atualizar inventrio;
7. Invalidar a View;
8. Dar feedback sonoro.
Na verdade, o Game loop constri um momento do jogo, que apresentado em sequncia de modo a dar a iluso de movimento. Apesar de parecer
simples, existem vrias consideraes importantes a respeito do Game loop.
Por exemplo, ele comanda a renderizao? Ele baseado em time-frame? Ele
roda em um Thread separado? Como lidar com a concorrncia?
Em alguns casos, o Game loop independente da fase de renderizao,
e ocorrem em momentos (e at em CPUs) separados. O ideal que o Game
loop seja enxuto o suficiente para rodar a cada Time-frame, evitando Lag.

Captulo 2 - Reviso de conceitos 13

Outro problema tpico a acelerao provocada pelo hardware. Por exemplo,


ao jogar em um dispositivo mais veloz, o Game loop fica mais rpido e os
NPCs tambm. preciso haver um controle de temporizao, garantindo um
frame-rate (taxa de atualizao) constante por segundo. Normalmente os
games atuam entre 30 e 60 FPS (frames por segundo).
Game Level ou nvel, pode ser entendido como duas coisas diferentes: o
nvel em que o jogador est e o cenrio do nvel atual. Na verdade, Game Level o conjunto de: cenrio, misso e elementos que o jogador deve enfrentar
em um jogo. Alguns jogos no possuem este conceito, por exemplo os Never
ending games (jogos que no terminam), como o Temple Run (Imangi),
por exemplo.

Jogabilidade
Jogabilidade, ou Gameplay, uma caracterstica ligada usabilidade do
jogo, ou seja, o quo o jogo nos diverte. Eu diria que o somatrio das experincias do usurio durante o jogo, que envolve: facilidade de controle, boa
visualizao de informaes, nvel crescente e ajustado de desafios etc.
Outros aspectos que influenciam a jogabilidade so:
Satisfao proporcionada ao jogador;
Facilidade de aprendizado do jogador;
Eficincia de envolvimento. O quo eficientemente o jogo envolve o
jogador. Jogos eficientes envolvem o jogador rapidamente;
Motivao para evoluo, ou se o jogo motiva o usurio a cumprir
atividades;
Cativao, se o jogo faz com que o usurio volte a jogar novamente
ou se interesse em adquirir novas fases, armas etc;
Um dos fatores importantes na jogabilidade pode ser a socializao, ou a
facilidade que o game d para divulgar suas conquistas.
Muita gente confunde jogabilidade com facilidade de jogar, que um
dos aspectos do conceito. Embora seja muito importante, a usabilidade (facilidade de jogar) sozinha no garante o sucesso de um game.
Talvez seja melhor citar alguns exemplos de games com boa jogabilidade:
Angry Birds: simples, fcil e rpido de aprender;
World of Goo: fantstico! Embora seja um jogo de desafios, sua
jogabilidade tima;
Fruit Ninja: igualmente simples e fcil, com alta eficincia de
envolvimento.

14

Manual do Indie Game Developer - Verso Android e iOS

Fatores de sucesso de um Game


Um game bem projetado envolve vrias atividades separadas:
Game design: o processo de criar o contedo, as regras, os cenrios, os personagens e a Jogabilidade. um trabalho que envolve
vrias especialidades, no apenas programao. Durante o Game
design muito produtivo criar prottipos do Game, para avaliar se o
conjunto est agradando;
Mecnica do Jogo: estuda-se a interao do usurio com o Game,
como sero os movimentos, controles e como sero implementadas
as regras. A mecnica tambm se preocupa com o desacoplamento
entre os nveis e o cdigo-fonte, procurando parametrizar a seleo
e montagem de nveis. Tambm pode envolver prototipao para
estudo e seleo de alternativas, alm de seleo de engines, modalidades e recursos para dar ao usurio a melhor experincia com
o game;
Game construction: a programao do jogo propriamente dita,
que engloba a criao de artefatos (imagens, sons, msica), nveis
(cenrios, dificuldade, etc) e cdigo-fonte para a implementao da
mecnica do jogo de acordo com o Game design;
So aspectos diferentes entre si e que no devem ser tratados apenas como
problemas de programao. Em equipes independentes (indie gamers) comum termos esses conceitos misturados, porm, os games mais bem sucedidos
so aqueles em que existem especialistas de vrias dessas reas envolvidos.
Outro fator de sucesso, sem dvida o visual do Game, que deve ser atrativo e coerente com o Game design, ou com a histria e ambientao. Um dos
exemplos mais interessantes o do World of Goo (2D Boy), que tem um
visual meio dark e nojento, bem apropriado para as bolas de Goo do jogo
(gosma ou coisa nojenta). Outros exemplos de visual interessante so: Angry
Birds (Rovio), Fruit Ninja e Jetpack joyride (Halfbrick), Wheres my
water (Disney) e vrios outros.
Quando falamos em jogos mveis (este trabalho voltado para eles), a
jogabilidade tem outros critrios de avaliao. Quando estamos em casa, com
um Xbox / Kinect, da Microsoft, ou um PS3, temos vrios recursos para controlar o Game, porm, quando estamos com um Smartphone (um iPhone ou
um Android qualquer) ou mesmo um Tablet, os recursos so mais limitados.
Embora contemos com acelermetro e multitoque, no temos o mesmo controle que um Joystick ou um dispositivo tico proporcionam. A tela menor
e, geralmente, no estamos em um ambiente timo para jogar (penumbra,

Captulo 2 - Reviso de conceitos 15

silncio e uma tela de LED enorme). A jogabilidade para jogos mveis deve
ser estudada levando-se em conta estas caractersticas.
Um erro muito comum dos desenvolvedores de games mveis criar as
mesmas verses para smartphones e tablets. No so a mesma coisa, embora,
com o Samsung Galaxy S III e o iPhone 5, os smartphones estejam crescendo, ainda so bem diferentes de um tablet de 8 ou 10 polegadas. necessrio
adaptar a jogabilidade ao tamanho do dispositivo.
A originalidade tambm tem um papel importante no sucesso de um Game,
especialmente se for do tipo casual. Games muito parecidos com outros, de
maior sucesso, no so bem vistos e a tendncia do pblico considerar como
uma imitao, mesmo que seja apenas uma coincidncia. Quanto mais original seu game for, mais poder chamar a ateno deste pblico.
Neste livro, no vou falar sobre este assunto, mas a forma de monetizao tambm pode ter um impacto positivo ou negativo no Game. Em meu
livro anterior Mobile Game Jam (www.mobilegamejam.com), focamos muito
nestes aspectos, porm, no podemos deixar de mencionar alguns problemas.
Para comear, nem todos os usurios so fs de jogos Ads based e podem
se irritar com aquela faixa de tela utilizada para veicular anncios. Eu tenho
games publicados que so deste tipo, e a rentabilidade muito baixa. Outra
forma que deve ser evitada cobrar licena de uso. Se voc j cobra pelo uso
sem deixar o usurio experimentar, receita para o fracasso, a no ser que seu
game seja realmente sensacional. O ideal dar ao jogador uma maneira de
experimentar o game antes de compr-lo. A entra o processo de in-app purchase (compra por dentro da aplicao), e os bens virtuais. uma maneira
de fazer o usurio se div ertir e recuperar gradativamente o seu investimento.

Captulo 3
Kit de ferramentas para
criao de games
Quando eu era garoto, passava aquele seriado antigo do Batman (DC
Comics), no qual o Morcego tinha um cinto de utilidades, e eu achava aquele cinto sensacional. Com Games, no muito diferente, pois necessitamos
ter a mo (e saber usar) algumas ferramentas importantes, que nos pouparo
muito tempo na criao de um Game.
Dependendo do tipo de Game que voc vai fazer, algumas ferramentas se
tornam mais importantes e outras, desnecessrias, mas sempre existe um conjunto bsico que voc deve ter. Vamos mostrar algumas delas aqui.

Ferramentas grficas
Em Games, as imagens so fundamentais e ns j discutimos isso, agora,
o momento de entrarmos mais um pouco a fundo no assunto, focando nas
principais ferramentas para criao de imagens.

Imagens
Vamos comear a estudar imagens em seu formato virtual e, depois, vamos
ver como ela renderizada (apresentada) na tela.
Toda imagem na memria um conjunto de pontos. Estes pontos so armazenados na memria e, posteriormente, so exibidos para o usurio. Cada
ponto tem as propriedades: coordenadas e cor.
As coordenadas de um ponto indicam onde ele est em nosso mapa
virtual (Bitmap no confundir com o formato BMP). Dependendo do tipo
de grfico que estamos trabalhando, pode ser em um mapa tridimensional ou
bidimensional. Como estamos falando de imagens, vamos supor que seja apenas bidimensional.
J a cor, normalmente expressa em quantidade de vermelho (red), verde
(green) e azul (blue), podendo tambm trazer a quantidade de transparncia
(Alfa). Vamos ver alguns exemplos simples de representaes.

18

Manual do Indie Game Developer - Verso Android e iOS

Transparncia e canal alpha


As imagens podem trazer informaes sobre trasnparncia, conhecidas
como Canal Alpha.
Geralmente, um valor zero no canal alfa de uma imagem significa que ela
totalmente transparente, e um valor 100% significa que totalmente opaca,
mas isto varia de acordo com a ferramenta que utilizamos. Por exemplo, o
LibreOffice Draw considera uma imagem com zero no canal alfa como sendo
totalmente opaca, e um valor 100% como totalmente transparente.

Ilustrao 6: Propriedades dos pontos grficos no LibreOffice Draw

Como podemos ver na figura anterior (LibreOffice Draw), temos dois


pontos ( claro que so imaginrios) com suas caractersticas. Note como
formamos a cor em formato RGBA Red, Green, Blue e Alfa (http://www.
w3schools.com/cssref/css_colors_legal.asp). No formato RGB Red, Green
e Blue (http://www.w3schools.com/cssref/css_colors_legal.asp), temos um
indicador da quantidade ou intensidade de cada cor, representado por um valor
entre 0 e 255 (1 byte), assim, podemos representar as cores:
Preto: RGB (0,0,0);
Branco: RGB (255,255,255);
Cinza: RGB (128,128,128);

Captulo 3 - Kit de ferramentas para criao de games 19

Vermelho: RGB (255,0,0);


Verde: RGB (0,255,0);
Azul: RGB (0,0,255);
Amarelo: RGB (255,255,0);
Azul ciano (azul beb): RGB (0,255,255);
Rosa beb: RGB (250,200,200);
Se quiser ver as combinaes de RGB, pode usar um programa como o
LibreOffice Draw (http://pt-br.libreoffice.org/), ou o Gimp (http://www.
gimp.org/) e usar a ferramenta de Cores.
Note que o dilogo de cores do LibreOffice Draw tambm tem uma aba de
transparncia, que permite ajustar a opacidade da cor.
Como vimos na ilustrao 6 (Propriedades dos pontos grficos), alm do
formato RGB, tambm temos o canal Alfa, que regula a opacidade ou
transparncia da cor. Este formato conhecido como RGBA. Existe alguma confuso quanto ao fato do canal representar o nvel de opacidade ou de
transparncia. Uma boa referncia, o W3Schools (http://www.w3schools.com/
cssref/css_colors_legal.asp), diz que o canal alfa representa o nvel de opacidade da cor, variando entre 0 (totalmente transparente) e 1 (totalmente opaco). J alguns softwares e literaturas tratam essa medida como percentual,
variando entre 0% (totalmente transparente e 100% (totalmente opaco) (http://
en.wikipedia.org/wiki/RGBA_color_space). Vamos mostrar alguns exemplos de
transparncia.
Temos que prestar ateno na forma como as ferramentas se referem ao
canal alfa. O LibreOffice Draw encara como transparncia, logo, 0% significa opaco e 100% significa transparente. J, para o Gimp, uma camada
0% significa totalmente transparente, e 100% significa totalmente opaco.

Ilustrao 7: Vrios nveis de transparncia

20

Manual do Indie Game Developer - Verso Android e iOS

Na imagem anterior, usamos o LibreOffice Draw para desenhar. Os quadrados, que possuem cor de fundo preta, esto com o canal alfa variando de
0 at 1, ou, na linguagem do LibreOffice, variando de 100% at 0% de
transparncia.
Toda imagem armazenada da mesma maneira. Porm, h alguns detalhes
sobre imagens que precisamos conhecer.

Unidades de medida
Quando falamos em imagens, muito comum ouvirmos pontos e pixels, conceitos que muitas vezes nos confundem. Um ponto (point) uma
medida de tipografia e equivale a 1/72 de polegada. Um pixel uma unidade
grfica de um dispositivo. a menor unidade utilizvel de um dispositivo grfico, como uma tela de LCD. Temos que deixar claro que estamos estudando
grficos exibidos em telas, e no impressos, pois, caso contrrio tudo mudaria.
Resoluo e densidade
H muita confuso sobre estas duas mtricas. O senso comum diz que resoluo a quantidade de pixels, tanto na horizontal quanto na vertical, que
um dispositivo pode exibir. Tambm serve para determinar o tamanho de
uma imagem. Por exemplo, uma imagem de 5 Megapixels tem 5 milhes de
pixels, o que no est relacionado ao tamanho fsico. Uma tela WXGA, com
1280 x 800 pixels, tem 1,024 Megapixels.
J o tamanho que uma imagem ou tela tero depende muito da densidade,
que a quantidade de pixels por polegada (PPI) que a tela pode exibir (ou que
a imagem espera ser reproduzida). Isto determina qual tamanho a imagem
ter na tela real. Cada dispositivo tem sua densidade especfica, por exemplo
(fonte: gsmarena.com) :
Samsung Galaxy S III: 306 PPI;
Apple iPhone 5: 326 PPI;
Apple iPad 4: 264 PPI;
Apple iPad 2: 132 PPI;
Motorola Xoom 2 Media Edition: 184 PPI.
Profundidade de cor
A profundidade de cor (Color depth) o nmero de bits necessrios para
representar cada cor de um pixel. Temos dois grandes sistemas para representar a cor: indexado e direto. O sistema indexado utilizado quando temos

Captulo 3 - Kit de ferramentas para criao de games 21

baixa profundidade de cor, ou seja, temos poucos bits para representar cada
cor. Ento, criada uma palheta (tabela) com as cores utilizadas e o cdigo
, na verdade, o ndice nesta tabela. Sistemas com 8 ou 12 bits por cor geralmente utilizam palhetas de cor. O sistema direto quanto no usamos palheta, mas representamos diretamente o valor de cada cor. Podemos ter vrias
profundidades neste sistema, como 8 bits, 16 bits ou true color, que so 24
bits de cor (8 para cada cor RGB). Com este sistema, podemos representar
mais de 16 milhes de cores. claro que o sistema direto requer mais memria (RAM ou arquivo) para representar as imagens, porm, fica mais fiel.
Hoje em dia, o sistema indexado utilizado para armazenamento de imagem, de modo a economizar espao. Entre os tipos de arquivo que usam palheta temos: BMP, GIF e PNG.

Arquivos de imagem
Existem vrios padres para armazenamento de imagem, cada um melhor para determinada aplicao. Temos formatos que usam palheta, formatos
que usam Color deph e formatos que podem utilizar os dois sistemas. Tambm
podemos armazenar uma imagem de maneiras diferentes, como: Raster e
Vetorial.
Imagens em formato Raster so as mais comuns. So mais simples em
termos de processamento, pois basicamente armazenam os pontos e suas cores.
S isso. necessrio apenas decodificar (ou descomprimir, caso necessrio)
o arquivo e acionar os pontos na tela com a cor necessria. Porm, o formato
Raster tem a desvantagem de ser grande. claro que podemos comprimir a
imagem, perdendo ou no informao, mas, ainda assim, o arquivo grande.
Existem formatos de arquivo Raster que comprimem a imagem, com
perda ou sem perda de informao. O JPEG, por exemplo, utiliza compresso
com perda, que pode ser regulada atravs da qualidade do arquivo. Arquivos
como PNG comprimem sem perda de informaes, porm geram arquivos
maiores.
J as imagens em formato vetorial no representam a imagem na forma de
pixels, mas como vetores. Elas contm as instrues para desenhar a imagem,
permitindo mais suavidade na renderizao. Os arquivos em formato vetorial
so menores do que os em formato raster, porm, sua renderizao exige mais
recursos de processamento e pode gerar distores quando utilizamos dispositivos diferentes.

22

Manual do Indie Game Developer - Verso Android e iOS

Os principais formatos de arquivos de imagem so:


Formatos Raster:
GIF: usa compresso sem perda e pode representar imagens com
palheta de 256 cores. Suporta animao e mais indicado para imagens com grandes reas de uma s cor, como: grficos comerciais,
desenhos simples etc;
BMP: o formato nativo do Microsoft Windows. No usa compresso e pode representar true color (24 bits). Os arquivos BMP so
grandes, em comparao com outros formatos;
PNG: suporta true color com canal alfa (transparncia). mais
indicado para imagens com grandes reas de cor nica. Tambm
suporta imagens em sistema direto, sem palheta;
JPEG: comprime imagens de maneira extraordinria, podendo reduzir absurdamente o tamanho do arquivo. Porm, usa compresso
com perda de informao. Uma vez salvo, no possvel restaurar a
imagem original. mais indicado para arquivos de fotografias;
Formatos vetoriais:
SVG: Scalable Vector Graphics, um formato padro, criado pelo
W3C, e utilizado no HTML 5;
CGM (Computer Graphics Metafile), CDR (Corel Draw), WMF
(Windows Metafile Format);

Ferramenta para Sketch


muito importante criar Sketches ou rascunhos das imagens de seus
Games. Nada impede que voc use papel e lpis para isto, mas um Sketch digital tem a vantagem de poder ser utilizado diretamente. Eu me lembro
quando criava Games para Windows e desenhava a imagem em papel para
depois passar por um Scanner.
O ideal ter algum dispositivo para digitalizao de desenho livre, como
uma mesa digitalizadora, que um tablet (no confundir com Tablet Mobile), que tem uma caneta especial e permite capturar seu desenho manual.
Outra opo usar algum aplicativo de desenho em um Tablet mobile, como
o iPad (ou algum dispositivo Android).
Eu costumo usar o Sketchbook Pro for iPad, da AutoDesk. um programa barato e muito fcil de usar e que me permite criar imagens rapidamente,
como a da figura a seguir.

Captulo 3 - Kit de ferramentas para criao de games 23

Ilustrao 8: Rascunho de personagem de Game

Utilizando uma caneta para iPad, consigo desenhar rapidamente e at


colorir a imagem, sem grande sacrifcio, pois posso apagar partes e corrigir
rapidamente.

Ferramentas para desenho Raster


Na minha opinio, a melhor ferramenta para edio de imagens raster o
PhotoShop, da Adobe. No h dvida alguma sobre isso. Porm, ele tem um
custo de licena relativamente alto, com relao a outras alternativas. Na minha opinio, se voc tiver condio, compre logo a licena da Adobe Creative
Suite 6, que j vem com tudo o que voc pode necessitar para criar grficos.
Porm, se voc um Indie Gamer, provavelmente no estar to motivado assim para investir uma alta soma, especialmente se estiver comeando.
Mas lembre-se: o investimento se pagar com a qualidade e facilidade de uso
dos produtos da Adobe.
A opo mais popular de editores raster gratuitos o Gimp (http://www.
gimp.org/) - GNU Image Manipulation Program. um editor poderoso, com
interface simples, porm dotado de muitos efeitos interessantes. H verses
para MS Windows, Mac OS X e Linux. Entre outras coisas, o Gimp permite:
Trabalhar com transparncias;
Trabalhar com vrios formatos diferentes de imagem, inclusive
SVG;

24

Manual do Indie Game Developer - Verso Android e iOS

Criar imagens com mltiplas camadas, que podem ser superpostas;


Criar animaes em GIF;
Selecionar partes da imagem de vrias formas: retangular, elptica,
livre, por cor etc;
Usar vrias ferramentas nativas de transformao, cor e pintura;
Usar vrios filtros de efeitos, como: entalhar, esculpir, realar, pintura a leo, clares etc;

Ilustrao 9: Exemplo de uso de filtro de clares do Gimp

Uma opo profissional de custo relativamente mais baixo o Corel Painter


(http://www.corel.com/corel/product/index.jsp?pid=prod4030123&cid=catalog3590
073&segid=2100019&storeKey=br&languageCode=pt). Ele permite baixar uma
verso de avaliao (30 dias) que d uma boa ideia dos seus recursos, porm
s est disponvel para MS Windows e Apple Mac OS X. melhor do que o
Gimp em alguns aspectos, como os recursos de pintura digital.

Ferramentas para desenho vetorial


Voc pode trabalhar com imagens vetoriais em seu game. Em meu livro anterior Mobile Game Jam (www.mobilegamejam.com) ns utilizamos imagens
vetoriais, sendo desenhadas em um Canvas. Porm, isto pode demandar recursos de processador, caso seu game seja muito complexo. Outra maneira de
trabalhar com imagens vetoriais cri-las em um editor de imagens vetoriais
e, posteriormente, converter os arquivos em algum formato raster. O Gimp faz
isto com perfeio.

Captulo 3 - Kit de ferramentas para criao de games 25

Comparao entre renderizao raster e vetorial


A renderizao raster muito rpida e o trabalho pode ser dividido com a
GPU (Graphics Processing Unit), evitando lags no game. Porm, mais sujeita s diferenas de densidade e resoluo entre os equipamentos, exigindo
verses diferentes ou ento operaes de resizing, que podem resultar em
arquivos serrilhados (pixelados, uma consequncia do processo de reduo /
ampliao).
J a renderizao vetorial uma operao de desenho completa e, geralmente, executada na CPU. Existem alguns artigos interessantes sobre tcnicas para utilizara a GPU na renderizao de desenhos vetoriais, entre eles
este, da NVidia: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch25.
html, porm, no a forma comum de se trabalhar. A vantagem da renderizao vetorial a suavidade do desenho, que pode ter sua escala facilmente
aumentada ou diminuda. Alm disto, os arquivos vetoriais so menores e no
necessria mais de uma verso de cada imagem.
De qualquer forma, eu sempre crio a imagem dos meus games em formato
raster (usando o Sketchbook pro) e depois converto para formato vetorial.
No final, eu converto as imagens vetoriais finalizadas em formato raster, com
um arquivo para cada tamanho (resoluo / densidade) de dispositivo.

Ilustrao 10: Um ensaio de personagem

Na figura anterior, vemos dois ensaios de uma personagem de game que eu


estou criando. O da esquerda (raster) foi criado rapidamente no Sketchbook
pro. Note como ainda mantive as linhas de construo que usei. A imagem
da direita feita em formato vetorial (no aplicativo iDesign, para iPad). Eu
tenho a tendncia de criar personagens em estilo mang, porm, resolvi dar

26

Manual do Indie Game Developer - Verso Android e iOS

uma ocidentalizada no rosto (arredondando e diminuindo a inclinao dos


olhos). Mas ficou do jeito que eu queria: uma personagem meio rebelde e
misteriosa.
Eu no vou trabalhar com a imagem vetorial, mas vou utiliz-la para criar
imagens raster. O desenho vetorial me ajuda a criar imagens mais perfeitas, j
que eu no tenho muito talento para desenho manual.
A ferramenta que eu gosto de usar para criar desenhos vetoriais o
iDesign para iPad, da Touchware (https://itunes.apple.com/us/app/idesign/
id342790226?mt=8). Ele barato (cerca de US$ 5,00), possui os recursos bsicos de edio vetorial, como: curvas de Bzier, auto fechamento, pintura,
rotao, combinao com imagens de fundo e at gradientes.
Outra boa ferramenta, na minha opinio o Fireworks, da Adobe (http://
www.adobe.com/br/products/fireworks.html), uma ferramenta voltada para
web designers, que permite trabalhar imagens vetoriais e raster tambm. Embora seu uso seja mais voltado para criao de Web designs (sua integrao
com CSS fantstica), tambm permite gerar temas para JQuery Mobile e trabalhar imagens vetoriais. Seu preo um pouco alto, assim como toda a suite
da Adobe, mas acredite: o investimento se pagar em produtividade!
claro que no podemos sequer falar em grficos vetoriais sem
mencionar o Corel Draw (http://www.corel.com/corel/product/index.
jsp?pid=prod4260069), cujo preo da licena mais competitivo e os recursos so fantsticos.
Como eu mencionei anteriormente, eu uso o iDesign, da TouchWare (), na
verso iPad. Embora tenha menos recursos que os produtos mencionados, tem
baixo custo e funciona muito bem no iPad. Sua licena custa cerca de US$
5,00, e vem com um tutorial muito fcil de entender.
Agora, se voc quer uma opo gratuita, eu recomendaria o Inkscape
(www.inkscape.org), que Open Source e gratuito. Ele possui verses para MS
Windows, Apple Mac OSX e Linux. muito fcil de usar e permite gerar a
maioria das imagens que voc necessita. Suas caractersticas importantes so:
Trabalha com camadas de desenhos;
Gera arquivos raster a partir dos desenhos vetoriais;
Consegue importar vrios formatos de imagem, tanto vetoriais como
raster;
Possui vrios filtros grficos sensacionais.

Captulo 3 - Kit de ferramentas para criao de games 27

Ilustrao 11: O ensaio de personagem no Inkscape

O Inkscape pode ser gratuito, mas cheio de surpresas. Assim como o


Gimp, ele tambm tem filtros sensacionais, que permitem dar um efeito profissional em seus trabalhos. Para dar um exemplo, eu transformei a imagem da
minha personagem em um android, com efeito Glowing Metal.

Ilustrao 12: Imagem robotizada

28

Manual do Indie Game Developer - Verso Android e iOS

Game engines
Um Game engine uma ferramenta para facilitar o desenvolvimento de games. Eles so compostos por editores, bibliotecas e interface de programao,
que podemos utilizar para criar nossos prprios games. Existem game engines
para vrias plataformas, seja 2D ou 3D, com nveis diferentes de facilidade
de uso.
Adotar um Game engine pode aumentar sua produtividade, permitindo que
voc se concentre mais no Game design (incluindo a jogabilidade), ao invs
de se preocupar com detalhes de animao, game loop etc.

HTML 5 + Javascript
Existem vrios Game engines para HTML 5 + Javascript. J que a Adobe
discontinuou o desenvolvimento do Flash player para dispositivos mveis, a
galera que fazia jogos em Flash est migrando para HTML 5 e Javascript.
Em meu ltimo livro, Mobile Game Jam, eu mostrei como desenvolver
games mveis usando HTML 5 e Javascript, e mostrei um engine multiplataforma: o PhoneGap (Cordova). Mas no mostrei nenhum Game engine desta
plataforma.
O ImpactJS (impactjs.com) uma boa opo. Ele permite criar games
em HTML 5 + Javascript para Desktops e dispositivos mveis. Vem com um
editor de nveis (o Weltmeister), que facilita muito a criao de cenrios para
diferentes nveis do jogo. Ele vem com ferramentas de depurao e empacotadores, que permitem colocar sua aplicao diretamente na AppStore iOS. Seu
custo relativamente barato (US$ 99,00).
Existem vrios outros game engines para HTML 5 + Javascript, porm o
ImpactJS, na minha opinio, o mais profissional.

Nativos
Um Game engine nativo aquele que possui bibliotecas nativas e executa
fora do ambiente do Browser. Geralmente, apresentam melhor performance
do que os Game engines baseados em HTML 5 + Javascript.
O Cocos2D for iPhone (http://www.cocos2d-iphone.org/) um port do
Game engine original Cocos2D, para Python. bastante popular, com vrios games famosos no mercado. Ele vem com um ambiente de desenvolvimento (CocosBuilder), a biblioteca do Game engine em si (cocos2D), e um
engine de fsica (Chipmunk). Existem verses para HTML 5 (http://cocos2d-x.

Captulo 3 - Kit de ferramentas para criao de games 29

googlecode.com/files/Cocos2d-html5-v2.1.zip) e Android (http://code.google.


com/p/cocos2d-android/). gratuito.
O AndEngine (http://www.andengine.org/blog/) um game engine para
Android, similar ao Cocos2D. Tambm gratuito e open source.
Para quem quer um engine mais profissional, tem o Antiryad Gx (http://
www.arkham-development.com/antiryadgx.htm), que permite criar games 2D ou
3D para vrias plataformas, entre elas: iOS e Android. Ele possui vrios tipos
de licenas, desde gratuita at enterprise (900 euros).
Porm, se voc quiser realmente detonar com um Game 3D animal, use
o UDK Unreal Development Kit (www.udk.com) para iOS, Android, PS3,
Xbox, Windows, Mac OSX etc. Com o UDK, voc pode desenvolver seu
game sem custo e, quando estiver pronto para publicar, pagar uma taxa de
US$ 99,00 (nica). Porm, h mais cobranas se voc alcanar US$ 50.000,00
em vendas.
Finalmente, temos o Unity (http://unity3d.com/), que um engine para games 3D muito interessante. A verso bsica gratuita e permite criar games
para MS Windows e Apple Mac OSX, e voc pode criar games comerciais
com ela. Se quiser criar games para dispositivos mveis, voc deve adquirir
os add-ons especficos, como: Unit iOS (US$ 400,00) e Unit Android (US$
400,00). Tambm existem as licenas pro para estes ambientes. O Unity
sensacional e vem com um editor fantstico.

Ilustrao 13: Jogo que estou criando com o Unity

Se voc vai criar um game 3D, melhor considerar uma ferramenta de desenho apropriada. Eu recomendo o AutoDesk Maya (http://usa.autodesk.com/
maya/), apropriado para criao de personagens 3D com animao. Agora, se

30

Manual do Indie Game Developer - Verso Android e iOS

preferir um software gratuito, o Blender (http://www.blender.org/) uma excelente opo. E os modelos 3D criados com o Blender podem ser importados
em game engines 3D, como o Unity.

Usar a plataforma nativa


Embora um Game Engine possa acelerar o desenvolvimento do seu game,
ele tambm tem algumas desvantagens, entre elas:
Ficar preso aos recursos disponveis no Engine;
Complexidade de aprender a API;
Preo da licena;
Dependncia de suporte (ou comunidade) para Bugs;
Alm destas possveis desvantagens, necessrio considerar que as APIs
das plataformas nativas, tanto do Android, como do iOS, permitem criar games com recursos avanados, como o OpenGL ES, por exemplo.
Mesmo que voc queira utilizar um Game Engine, eu considero fundamental que voc saiba o que est acontecendo e, para isto, nada melhor do
que criar um game na mo, sem engine algum. Depois, se considerar relevante para o seu projeto, poder escolher um Game Engine apropriado, porm
saber avaliar e usar melhor as funcionalidades, pois j sabe os detalhes de
programao de games.

Ambientes multiplataforma
Fora os Game engines, existem alguns ambientes multiplataforma para
criao de aplicaes mveis, alguns deles com recursos para Games. Para
comear, vou vender o meu peixe. No nosso portal The Code Bakers (www.
thecodebakers.org), criamos um framework de aplicaes mveis multiplataforma, o AAMO (www.aamoframework.org). Ele baseado em Lua e pode gerar aplicaes em iOS e Android. Futuramente, haver verses para Desktop e
tambm um game engine embutido: o AAMO Xtreme. O AAMO totalmente
gratuito e Open Source, mas ainda no contempla os recursos para criao de
Games.
Outro ambiente multiplataforma interessante o CoronaSDK (http://www.
coronalabs.com/products/corona-sdk/). Ele tambm baseado na linguagem Lua
e possui recursos muito interessantes para criao de games. Ele foi escolhido
pela EA (famosa por vrios games) para criar seu jogo mvel: World Smack
(http://www.coronalabs.com/blog/2012/11/08/corona-spotlight-ea-chooses-corona-for-word-smack/). O corona no gratuito, mas tem licena de baixo custo, s
necessria quando voc for publicar o game.

Captulo 3 - Kit de ferramentas para criao de games 31

Prototipadores
Um grande recurso, geralmente ignorado pelos desenvolvedores de game,
a prototipao. Este recurso nos permite capturar, de maneira simples e clara, os requisitos de uma aplicao. Como games possuem requisitos muito
subjetivos (jogabilidade um deles), a prototipao pode ser um excelente
instrumento para acelerar o desenvolvimento, evitando retrabalho.
Seja utilizando um Game engine ou no, o comprometimento do projeto
de um game cresce rpida e exponencialmente. Em poucas semanas, j temos
tanto cdigo escrito que criamos um forte comprometimento com ele, deixando de ver coisas bvias. Com um prottipo isto evitado, pois podemos jogar
tudo fora e comear novamente.
Felizmente, existem alguns softwares excelentes para prototipao de games e aplicaes em geral. Eu uso dois deles, que considero excelentes.
Para comear, gostaria de falar do Codea (http://twolivesleft.com/Codea/),
uma ferramenta que roda diretamente (e apenas) no iPad. Com ela, podemos
programar diretamente no iPad, sem necessidade de um laptop ou desktop.
Infelizmente, s existe verso para iPad. Podemos at baixar o Codea Runtime (https://github.com/TwoLivesLeft/Codea-Runtime) e distribuir a aplicao na AppStore. A licena do Codea custa US$ 9,99 e acredite: ela se paga
rapidamente!
Vou mostrar o prottipo de um game que foi feito originalmente no Codea
em menos de 4 horas! Eu aprendi a usar o Codea, aprendi a linguagem Lua e
criei o Game enquanto estava hospitalizado, aguardando uma cirurgia renal.
Quando fui para a sala de cirurgia, o Game estava pronto. claro que era um
prottipo, mas estava funcionando completamente.
Eu sou f do Codea, porm, fiquei completamente atnito e pasmo quando
conheci a oitava maravilha do mundo: O Processing (http://processing.org/)!
aquele tipo de coisa bombstica que nos faz gritar: Para tudo! O Processing
um ambiente de simulao e prototipao, que j vem com tudo em cima
para criar Games. Ele usa uma linguagem tipo java, mas tambm permite
exportar seu projeto para HTML 5 + Javascript e at mesmo Android, ou seja:
lava, passa, cozinha e, ainda por cima: gratuito!

Bibliotecas auxiliares
Como j discutimos, um Game um projeto muito complexo, com vrias
macrofunes diferentes. Certamente, podemos utilizar Game engines para
nos auxiliar, porm, ainda existem alguns motivos importantes para estudarmos as bibliotecas em separado:

32

Manual do Indie Game Developer - Verso Android e iOS

1. Custo. Se criarmos diretamente o game, no necessitaremos adquirir


licenas e, como j discuti, alguns Game engines possuem requisitos
de licena meio complexos;
2. Domnio. Se voc souber o que est por baixo do cap, vai saber
tirar maior proveito dos componentes para seu Game;
Uma ferramenta importante um engine grfico que permita utilizar os recursos de acelerao de sua placa de vdeo (GPU Graphics Processing Unit)
e esta ferramenta a biblioteca OpenGL ES (http://www.khronos.org/opengles/),
que uma verso da OpenGL (http://www.opengl.org/) para dispositivos mveis. A principal vantagem renderizar imagens mais rapidamente, utilizando
multiprocessamento compartilhado entre CPU e GPU, tanto para grficos 2D
como para 3D. Tanto no iOS como no Android, existem recursos para facilitar
o uso do OpenGL ES na criao de games.
E, finalmente, temos a biblioteca Box2D (http://box2d.org/), que um physics engine, ou seja, uma biblioteca que fornece funes fsicas, como: movimento, foras e colises para nossos games parecerem mais realistas. Para ter
uma ideia de quanto a Box2D importante, o game Angry birds a usa como
seu physics engine.

Captulo 4
Prototipao do game
Bem, vamos comear realmente o nosso game. Imaginemos uma ideia
simples e vamos tentar criar um prottipo com ela, de modo a avaliar se est
boa para virar um Game.
Tudo comea com uma ideia. Que tal criarmos um shooting game? Afinal de contas, shooting games so fceis de criar e, geralmente agradam a
todos. Podemos comear com um rascunho (Sketch) do game. Eu pensei
em um game de escapar de asteroides. Voc est em uma nave e tem que
manobrar em um campo de asteroides, evitando colidir com eles. Voc ganha
pontos de acordo com a distncia percorrida, em anos-luz.
Uma dica: se voc comear a complicar, pensando coisas do tipo: j tem
muita coisa assim, preciso criar algo diferente... seu projeto no vai andar.
Voc vai criar um prottipo do jogo, logo, no o momento de pensar nisso.
Depois, voc pode fazer as otimizaes que desejar.
Um rascunho tudo o que precisamos
Eu peguei meu iPad e, usando o Sketchbook, criei vrios desenhos. Eu
fui desenhando o que me vinha mente, sem me preocupar muito com esttica
ou funcionalidade. O desenho que mais gostei foi o da prxima figura.
A nave se move para cima e para baixo, e os asteroides vm em sua direo, pois ela est se movendo para a frente. Existem asteroides bons (os
brilhantes), que repem a energia da nave, e os asteroides ruins (opacos), que
podem destruir a nave. Caso ela colida com um asteroide ruim, sua energia
drenada, pois ela usa os escudos para impedir sua coliso.

34

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 14: Rascunho do jogo

Se o seu jogo tiver mais telas ou nveis, voc pode ir criando rascunhos
para cada um deles. O importante colocar a idea no papel (digital), antes que
voc se esquea.

Usando o Codea
O Codea um ambiente de programao sensacional. simples e prtico,
permitindo criar rapidamente jogos e simulaes grficas. Ele uma aplicao
para iPad, que est disponvel na AppStore por cerca de US$ 10,00.

Ilustrao 15: A tela inicial do Codea

Captulo 4 - Prototipao do game 35

Os pontos positivos do Codea so:


1. Tem recursos que facilitam o desenvolvimento rpido de games;
2. J vem integrado com o engine de fsica Box2D;
3. Usa a linguagem Lua (www.lua.org), que simples e pouco verbosa;
4. Vem com Spritepacks (conjuntos de imagens) e outros recursos para
criao de games.
Porm, o Codea carece desesperadamente de documentao decente. No
parece um produto comercial e, certamente, no tem o suporte necessrio, em
caso de problemas. O site da empresa, Twolivesleft.com, muito minimalista e no tem uma documentao formal. Existem vrios tutoriais (alguns
criados por terceiros) que explicam rapidamente como ele funciona.
claro que podemos criar Games com o Codea e distribuir na AppStore,
pois ele tem um Codea Runtime (https://github.com/TwoLivesLeft/Codea-Runtime) que permite fazer isto. Porm, quando se trata de criar um produto comercial, eu sou muito conservador, pois, em caso de problemas, o meu
cliente vai reclamar comigo e eu, como desenvolvedor do Game, no terei a
quem recorrer. por isso que eu no uso o Codea para desenvolvimento, mas
apenas para prototipao.
No Codea, podemos criar cdigo-fonte para responder a eventos e para
criar classes, que sero utilizadas no Game. A linguagem a Lua (www.lua.
org), criada pela PUC-RJ. Lua uma linguagem simples, intuitiva e pouco
verbosa, cujo aprendizado extremamente simples. Ns iremos ensinando
Lua conforme o prottipo for se desenvolvendo. Alis, foi assim que eu aprendi a usar Lua. S para comear, Lua canse sensitive e no precisa de ponto
e vrgula no final da linha. Os comentrios so iniciados por --.

Um tutorial rpido em Codea


Vamos fazer uma porcaria em Codea para voc pegar o jeito. Comece
iniciando um novo projeto, na tela do Codea (Ahn, galera: s funciona no
iPad, ok? Se voc no tem um iPad, leia a prototipao no Processing).
Escreva um nome qualquer para o projeto, por exemplo: bola. Ele vai criar
todo o cdigo do projeto, que apenas escreve Hello world na sada.

36

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 16: O cdigo gerado pelo Codea

Ele gerou um cdigo-fonte muito simples, como podemos ver na figura.


O arquivo Main o arquivo principal de sua aplicao Codea, ele contm
algumas funes de Callback, ou seja, so invocadas pelo engine do Codea em determinadas situaes. A primeira funo a setup, que chamada na inicializao. Neste caso, estamos usando a funo print para gerar
uma mensagem na sada da execuo. A outra, a funo draw, invocada
a cada novo frame gerado. Neste caso, ela apenas limpa a tela, com a funo
background (<red>, <green>, <blue>), e ajusta a grossura da caneta para
desenhos em 5 pixels, com a funo strokeWidth(5).
Se voc quiser ver uma referncia rpida (algo semelhante a um Help),
toque no smbolo do Codea na tela principal ( um quadrado verde com dois
C, um grande e um pequeno).
Como eu rodo o programa?
Vamos ver uma referncia rpida dos comandos do editor:
Seta para a esquerda, no canto superior esquerdo: volta ao incio;
Boto +, no canto superior direito: cria uma nova classe ou um
arquivo vazio;
Tabs na parte superior da tela: alternam entre os arquivos;
X no canto inferior esquerdo: fecha o arquivo;
Boto play, no canto inferior direito: executa o programa;

Captulo 4 - Prototipao do game 37

Clique no boto play, no canto inferior direito da tela do editor e voc ir


para o ambiente de execuo.

Ilustrao 17: O ambiente de execuo

O ambiente de execuo bem simples. Ele tem um painel de propriedades, um de output (para sada de comandos print), e uma barra de controle.
A barra de controle est sempre no canto inferior esquerdo, e tem alguns botes:
Seta para a esquerda: termina o programa;
Pausa / Play: para ou continua a execuo do programa;
Seta curva: reseta o programa, executando do incio;
Mquina fotogrfica: tira uma foto instantnea do programa;
Filmadora: filma a execuo do programa;
Agora, vamos fazer uma coisa mais legal. Vamos criar um foguete que se
move. Altere o arquivo Main para acrescentar as linhas em negrito abaixo:
-- bola
-- Use this function to perform your initial setup
function setup()
print(Hello World!)
x = 0
y = HEIGHT / 2
supportedOrientations(PORTRAIT_ANY)
displayMode(FULLSCREEN)
end

38

Manual do Indie Game Developer - Verso Android e iOS


-- This function gets called once every frame
function draw()
-- This sets a dark background color
background(40, 40, 50)
-- This sets the line thickness
strokeWidth(5)

end

-- Do your drawing here


x = x + 1
if x > WIDTH then
x = 0
end
sprite(SpaceCute:Rocketship,x,y)

O resultado da execuo um foguete que sai do canto esquerdo da tela e


vai at o canto direito, recomeando quando atinge o fim.

Ilustrao 18: O foguete se move pela tela

Vamos ver o que mudamos. Para comear, criamos duas variveis, x para
posio horizontal e y para vertical. Note que o valor de y a metade da
tela (HEIGHT / 2) fixamos a orientao em portrait, pois queremos que

Captulo 4 - Prototipao do game 39

o iPad seja segurado em p. Faz sentido, pois os asteroides ficaro mais


prximos. Depois, retiramos os painis que aparecem do lado esquerdo com a
funo displayMode(FULLSCREEN).
Sempre que for necessrio criar um novo frame, a funo draw ser invocada. E, neste momento, ela vai apagar a tela e desenhar um sprite (uma
imagem) de uma das colees de sprites que j vem com o Codea. um pequeno foguete com um tripulante. Ele vai desenhar sempre na posio indicada pelas variveis x e y. O Codea usa a orientao cartesiana correta, com
o ponto inicial (0,0) no canto inferior esquerdo.
A cada chamada da funo draw, vamos incrementando o valor de x.
Quando ele se torna maior que a largura da tela (WIDTH), ns voltamos ao
valor zero.

Criando o prottipo do AsteroidNuts no Codea


Agora, que j entendemos um pouco do Codea, vamos tentar criar nosso
prottipo. Para comear, temos dois tipos de Game Objects: Nave e Asteroide.
A nave um Player Object, ou seja, controlado pelo usurio, e o asteroide
um NPC.
Todo o cdigo-fonte pode ser baixado, logo, no necessrio digitar os
comandos. Veja na Introduo. Tem um arquivo Asteroid.txt (..\Codigo\
AsteroidNuts_Codea\Asteroid.txt) com a classe toda.
No importa se temos dois tipos de asteroides (bom e ruim), pois so apenas variaes de propriedades de um asteroide.
Nossa classe tem que armazenar os atributos de posio do Asteroide, alm
do sei tipo (bom ou ruim). O Codea usa a API Nspire (http://www.inspired-lua.org/2011/05/5-object-classes/), que facilita a criao de classes. Para criar
uma nova classe, abra o editor do projeto e clique no boto +, que fica no
canto superior direito. Escolha Create new class e d o nome Asteroid.
Voc vai notar que j tem alguns comandos:
Asteroid = class()
function Asteroid:init(x)
-- you can accept and set parameters here
self.x = x
end

40

Manual do Indie Game Developer - Verso Android e iOS


function Asteroid:draw()
-- Codea does not automatically call this method
end
function Asteroid:touched(touch)
-- Codea does not automatically call this method
end

A linguagem Lua no suporta a criao de classes diretamente, porm


como sendo uma linguagem baseada em prottipos, permite modificar o comportamento e caracterstica de objetos dinamicamente, imitando o conceito de
classes. A API Nspire tem a funo class() que facilita a criao de classes.
Neste caso, criamos um objeto Asteroid e acrescentamos a ele uma propriedade (x) e trs mtodos (init, draw e touch).
O mtodo init ser invocado sempre que quisermos pegar um novo objeto da nossa classe, por exemplo:
ast1 = Asteroid(10)

Os mtodos draw e touched so dicas que deveramos implementar


estes mtodos e invoc-los, quando necessrio. O Codea inseriu comentrios
indicando que estes dois mtodos no sero invocados automaticamente.
Para comear, vamos trabalhar nosso construtor (o mtodo init). Primeiramente, temos que pensar em como iniciar o Asteroide. Cada Asteroide dever surgir no canto direito da tela, em uma altura varivel. Haver uma faixa de
valores no eixo das ordenadas (Y), onde os asteroides podero surgir. Eu no
quero usar a tela toda, pelo menos neste prottipo. Ento, faz sentido passarmos em passar os limites para o construtor da classe, de modo que este possa
gerar um asteroide dentro da faixa esperada.
function Asteroid:init(upper,lower)
-- you can accept and set parameters here
local tipo =math.random(1,100)
self.position = vec2(100,50)
self.dimension = vec2(50,50)
self.position.x = WIDTH - 100
self.position.y = math.random(upper,lower)
if tipo > 80 then
self.type = 2
else
self.type = 1
end
end

Captulo 4 - Prototipao do game 41

O prefixo Asteroid: indica que este um mtodo a ser atribudo aos


objetos do tipo Asteroid, e o sufixo init o nome do mtodo. O prefixo self., em alguns comandos de atribuio, se refere ao objeto da classe
Asteroid, logo, self.type = 2 significa que estamos atribuindo 2 propriedade type, do objeto. Se ela no existir, ser criada automaticamente.
Lua uma linguagem de tipagem dinmica, ou seja, cada varivel tem seu
tipo atribudo dinamicamente (e pode mudar). Passamos dois parmetros para
o construtor: upper limite superior no eixo das ordenadas, e lower limite inferior. Ns criamos trs propriedades no construtor:
position: do tipo vec2 (do Codea), que retorna um vetor com
coordenadas bidimensionais (x e y). Armazena a posio atual
do asteroide;
dimension: igualmente vec2, armazena a largura e altura da caixa
que contm a figura do asteroide;
type: do tipo number (Lua), que armazena o tipo do asteroide,
se ele bom (type = 2) ou ruim (type = 1). claro que isto poderia ser resolvido melhor por hierarquia ou composio, mas um
prottipo, logo, descartvel;
Nossa estratgia para decidir se vamos criar um asteroide bom ou ruim
apenas aleatria. Poderamos levar alguns fatores em considerao, como o
nvel de energia do jogador e a quantidade de acertos que ele tem, mas, para
efeito de prottipo, vamos deixar assim. Geramos um nmero aleatrio entre
1 e 100 (math.random(1,100)) se ele for maior que 80, ns o transformamos
em bom.
Inicialmente, calculamos a posio horizontal (abscissas) em WIDTH
100. Depois vou explicar o motivo, mas est relacionado com o tamanho do
asteroide, que 50. E calculamos a posio vertical (ordenadas) com um nmero aleatrio entre o limite inferior e o superior (math.random(upper,lower)).
Agora, demos que desenhar um asteroide. O melhor local para fazer isto
no mtodo draw. Ns invocaremos o mtodo draw sempre que desejarmos redesenhar um objeto Asteroid especfico.
function Asteroid:draw()
-- Codea does not automatically call this method
spriteMode(CORNERS)
if self.type == 2 then
sprite(Tyrian Remastered:Energy Orb 1,
self.position.x,
self.position.y,
self.position.x+self.dimension.x,

42

Manual do Indie Game Developer - Verso Android e iOS


self.position.y+self.dimension.y)

else

end

end

sprite(Tyrian Remastered:Eggstroid,
self.position.x,
self.position.y,
self.position.x+self.dimension.x,
self.position.y+self.dimension.y)

A primeira coisa que fizemos foi mudar o modo de desenhar um sprite,


que uma imagem carregada na tela. Por default, o Codea sempre considera
a posio de um sprite (coordenadas x e y) como o centro da imagem. Eu
prefiro trabalhar com os cantos e especificar o tamanho da imagem, ento eu
mudei o modo de desenho para CORNERS (spriteMode(CORNERS)), que
significa: x e y sero o canto inferior esquerdo da caixa que contm o
sprite, e os parmetros w e h sero, respectivamente, as coordenadas do
canto superior direito da mesma.
Se for do tipo 2, um asteroide bom, logo, eu uso um sprite da biblioteca do Codea, que representa uma esfera brilhante. Eu uso a funo sprite
para desenhar a figura do asteroide na tela, na posio especificada (propriedade position) e do tamanho especificado (propriedade dimension). O primeiro parmetro o nome da figura, o segundo e o terceiro, as coordenadas
do canto inferior esquerdo, e o quarto e o quinto, as coordenadas do canto
superior direito.
Finalmente, temos dois mtodos finalx e finaly, que retornam as coordenadas do canto superior direito do asteroide, para facilitar seu uso.
Agora, precisamos representar a nave (arquivo ship.txt). Vamos criar
uma classe da mesma maneira. Em seu mtodo init, ns criamos as mesmas
propriedades do asteroide, exceto o type. Mas colocamos o lower e o
upper como propriedades da nave. Veja o mtodo init:
function ship:init()
-- you can accept and set parameters here
self.position = vec2(100,80)
self.dimension = vec2(100,50)
self.position.y = (HEIGHT - self.dimension.y)/2
self.upper = self.position.y - 4* self.dimension.y
self.lower = self.position.y + 5 * self.dimension.y
end

Captulo 4 - Prototipao do game 43

O mtodo draw mais simples:

function ship:draw()
-- Codea does not automatically call this
spriteMode(CORNERS)
sprite(SpaceCute:Rocketship,self.position.x,
self.position.y,
self.position.x+self.dimension.x,
self.position.y+self.dimension.y)
end

Desenhamos da mesma maneira que o asteroide. E a nave tambm tem os


mtodos finalx e finaly. Na verdade, ambos (Asteroid e ship) poderiam
ser derivadas de uma classe ancestral comum GameObject, mas eu no quis
complicar as coisas, pois um prottipo. Um mtodo callback importante
o touched, que ser invocado quando o usurio tocar na tela.
function ship:touched(touch)
-- Codea does not automatically call this method
if touch.y >= self.upper and touch.y <= self.lower then
self.position.y = touch.y
end
end

Eu simplesmente pego a altura do toque do jogador e posiciono a nave


nela. Note que no necessariamente o toque na nave, mas na tela toda. Eu
poderia sofisticar este controle, mas, como um prottipo, eu preferi deixar
tudo mais simples. Na verdade, quem invoca este mtodo o meu arquivo
Main, j que ele no ser invocado automaticamente pelo Codea (veja o
comentrio no incio do mtodo touched.
Agora, chegou o momento de criar a mecnica do jogo. Eu poderia ter
usado os recursos de fsica cintica do Codea (fornecidos pela biblioteca Box2D), com o objeto body. Assim, toda a parte de movimento e coliso seria
automtica. Porm, considerei que o prottipo ficaria complexo demais e, na
verdade o jogo muito simples na parte de fsica. Ento, eu mesmo criei a
fsica necessria para movimentar e verificar colises.
No arquivo Main, eu crio algumas variveis globais para controle do
jogo, as quais nem vou mencionar diretamente, exceto duas:
asters = {};
nave = nil;

A varivel asters do tipo table, que um array dinmico e associativo


em Lua. E a varivel nave um objeto, inicializado com valor nulo (nil).
A table asters vai armazenar os asteroides ativos em determinado momento
(aqueles que no chegaram ao fim do caminho: o canto esquerdo da tela), e a
varivel nave vai representar o objeto ship.

44

Manual do Indie Game Developer - Verso Android e iOS

A funo setup ser invocada automaticamente pelo Codea, e inicializa


as variveis do game.
function setup()
displayMode(FULLSCREEN)
upperlimit = (HEIGHT - 50) / 2 - 200
lowerlimit = (HEIGHT - 50) / 2 + 250
nave = ship()
lightyears=0
energy=200
contador=0
fimjogo=0
posdec=1
limrandom=60
inirandom=1
gatilho= 53
interval=12
lastaster=0
maxasters=10
limitdec=60
end

Veja na linha em negrito como eu crio uma instncia de uma classe, neste
caso, a nave. O mtodo init da classe ship ser invocado e vai devolver
uma instncia j inicializada, que pode ser referida atravs da varivel nave.
A funo touched ser invocada sempre que o jogador tocar na tela. O
parmetro que ela recebe (touch) contm, entre outras coisas, as coordenadas de onde o jogador tocou na tela. Eu repasso isso para o mtodo touch
da nave.
A funo draw um pouco extensa (e poderia ser refatorada), mas eu
tenho que comentar algumas coisas sobre ela, afinal o Game loop.
-- This function gets called once every frame
function draw()
-- This sets a dark background color
background(40, 40, 50)
-- This sets the line thickness
strokeWidth(5)
-- Do your drawing here
font(MarkerFelt-Wide)

Captulo 4 - Prototipao do game 45


fill(121, 249, 16, 255)
fontSize(60)
textMode(CORNER)
text(AsteroidNuts, 10, HEIGHT-65)
fill(91, 137, 229, 255)
text(string.format(Light years: %d,
lightyears),10,HEIGHT-120)
text(string.format(Energy: %d,energy),
10, HEIGHT-185)
if fimjogo==1 then
fim()
else
contador = contador + 1
if contador >= limitdec then
lightyears = lightyears + 10
energy = energy - 1
if energy <= 0 then
fim()
end
contador=0
end
newaster()
nave:draw()
if lightyears > 200 then
posdec = 2
elseif lightyears > 500 then
interval=6
posdec=10
maxasters=20
elseif lightyears > 1000 then
limrandom=3
gatilho=1
interval=1
posdec=40
maxasters=30
limitdec=40
end
for i,v in ipairs(asters) do
if v.position.x < 0 then
table.remove(asters,i)

46

Manual do Indie Game Developer - Verso Android e iOS


currentasters = currentasters - 1

else

v.position.x = v.position.x - posdec


v:draw()
if asteroidhit(v) == 1 then

spriteMode(CORNERS)
sprite(Tyrian Remastered:Bullet Fire A,
v.position.x,v.position.y,v:finalx
(),v:finaly())
if v.type == 2 then
sound(SOUND_JUMP, 16037)
energy = energy + 20
table.remove(asters,i)
currentasters = currentasters - 1
else
sound(SOUND_EXPLODE, 35632)
energy = energy - 2
end
if energy <= 0 then
fim()
end
end
end
end
end
end

A funo draw ser chamada automaticamente pelo Codea a cada 60


hertz (1/60 de segundo). Eu tenho que verificar se acrescento mais asteroides
e movimentar os que j existem, alm de verificar se a nave colidiu com algum
deles. Tambm verifico se os asteroides chegaram ao fim do seu caminho.
A primeira coisa incrementar o contador de anos-luz (lightyears) e decrementar o de energia, afinal, a nave se movimentou. Eu uso uma funo
newaster() para verificar se tenho que criar mais um asteroide (newaster()), e depois eu redesenho a nave, invocando seu mtodo draw.
function newaster()
if math.random(1,limrandom) > gatilho then
if currentasters < maxasters then

Captulo 4 - Prototipao do game 47

end

end

end

if lastaster >= interval then


ast = Asteroid(upperlimit,lowerlimit)
table.insert(asters,ast)
currentasters = currentasters + 1
lastaster=0
else
lastaster = lastaster + 1
end

Esta funo newaster cria e insere um novo asteroide (table.


insert(aster,ast)) se as seguintes condies forem atingidas:
Gatilho aleatrio maior que 53, para dar um toque de impreviso ao
jogo;
O nmero de asteroides ativos for menor que 10;
O intervalo entre os asteroides for maior ou igual ao intervalo estabelecido (s pode lanar um novo depois de x asteroides);
Na verdade, esta funo faz parte da macro funo de Estratgia do jogo, e
deve ser bem elaborada para aumentar a jogabilidade. Mas, como um prottipo, eu deixei assim mesmo.
Depois, eu criei um loop para movimentar os asteroides, verificando se
houve coliso de algum deles com a nave:
for i,v in ipairs(asters)
...
end

A tabela asters armazena os asteroides como um vetor dinmico comum.


Para percorrer esta tabela, usamos a forma de comando for com a funo
ipairs(tabela). A cada iterao, o valor de i e v apontaro, respectivamente, para a posio do asteroide e para o objeto asteroide e eu posso referenci-lo diretamente:
v.position.x = v.position.x - posdec

A primeira coisa que eu fao no loop verificar se o asteroide chegou ao


fim do caminho, removendo-o da tabela:
if v.position.x < 0 then
table.remove(asters,i)

48

Manual do Indie Game Developer - Verso Android e iOS

...

currentasters = currentasters 1

No d problema algum remover um elemento durante a iterao.


Se o asteroide ainda no chegou ao fim, eu decremento sua posio no eixo
das abscissas, e verifico se houve coliso com a nave (funo asteroidhit).
Se houve, eu verifico se era um asteroide bom ou ruim, tomando a atitude
correta (incrementar ou decrementar energia). Note que eu toco sons diferentes, dependendo do tipo de asteroide com o qual a nave colidiu. Para tocar um
som, o Codea tem a funo:
sound (nome, raiz)

O parmetro nome indica o nome do som a ser tocado, e a raiz o modifica aleatoriamente. Voc deve experimentar com este segundo parmetro at
chegar ao som que deseja mostrar.
O jogo chega ao fim sempre que a energia acaba.
Durante o jogo, eu exibo algumas mensagens na tela. Eu uso a sequncia
de comandos abaixo:
font(nome da fonte): troca a famlia tipogrfica (a fonte) dos
caracteres;
fill(red, green, blue, alfa): estabelece a cor para a escrita;
fontSize(tamanho da letra): estabelece o tamanho da letra, em pontos (1/72 de polegada);
textMode(origem da posio do texto): especifica o tipo de coordenadas que vamos fornecer. CENTER significa que o centro do
texto e CORNER que o canto inferior esquerdo do texto;
text(texto a ser escrito, x, y): os parmetros x e y so as coordenadas do texto (veja textMode);
Com este prottipo, eu posso brincar com o game, repensando a jogabilidade, as regras etc. A codificao em Lua de baixa verbosidade e a API do
Codea facilita tudo. Eu tenho um jogo quase completo que foi desenvolvido
rapidamente.

Captulo 4 - Prototipao do game 49

Ilustrao 19: O jogo funcionando

Infelizmente, o Codea s existe para iPad... Esta uma limitao muito sria. Para comear, temos que ter um iPad para desenvolver o prottipo.
No podemos sequer rodar em um iPhone, quanto mais em um dispositivo
Android.
Neste caso, temos outra ferramenta excelente para prototipao: Processing.

Usando o Processing
Como definir o Processing? Bem, para comear, podemos dizer que ele
um ambiente de programao, voltado para criao de grficos e simulaes.
Na verdade, ele mais do que isso, pois tambm um game engine e um simulador de grficos 3D.
A primeira reao de quem v os demos do processing pela primeira vez
surpresa. Sim, voc tambm vai ficar de boca aberta, babando, ao ver como
possvel criar simulaes fantticas, com to pouco cdigo. Vamos fazer
isso agora. Para comear, baixe o Processing do seu site processing.org, e
descompacte para a pasta que julgar conveniente.
Abra a pasta onde instalou o Processing e execute o programa (no Windows
Processing.exe). Voc ver a tela do PDE (Processing Development
Editor), cuja simplicidade se assemelha muito ao Codea. Na verdade, o Codea
foi inspirado pelo Processing.

50

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 20: A tela inicial do PDE

Selecione o menu File / Examples... e um dilogo com uma lista (Java


examples) vai abrir ao lado. Expanda o n Demos e depois o n Graphics
e selecione o item planets com um duplo clique. A aplicao Planets vai
abrir no PDE. Voc ver um boto com smbolo play na barra de botes do
PDE, clique nele e observe a janela que vai se abrir.

Ilustrao 21: O programa Planets em execuo

Captulo 4 - Prototipao do game 51

Caro leitor, cara leitora, no impressionante? Se analisarmos o cdigo-fonte da aplicao (que tem dois arquivos), veremos que so poucos comandos para gerar um efeito sensacional.
O Processing foi criado para ser uma ferramenta de prototipao de aplicaes grficas, como games e simulaes. S que, hoje em dia, utilizado em
aplicaes profissionais mesmo. Com ele, podemos criar efeitos e animaes
com grficos 2D e 3D, utilizando OpenGL.
Ele permite desenvolvermos aplicaes rapidamente, utilizando uma linguagem parecida com Java, da Oracle. Na verdade, o Processing converte
o cdigo digitado em Java e o executa utilizando a JVM.
O Processing possui alguns modos de execuo, que permitem migrar
o executvel da sua aplicao para as plataformas que ele suporta: Java,
Javascript e Android. Para mudar de plataforma, basta clicar no boto
JAVA que est na barra superior e selecionar a nova plataforma. Porm,
antes que voc fique muito animado com o Android, quero esclarecer que
esta caracterstica ainda no funciona corretamente.
Eu tentei muito tempo rodar uma aplicao Processing no Android e at
consegui, porm, foi to complicado que resolvi no recomendar no livro.
Apesar do Emulador Android suportar OpenGL ES 2.0 (a partir da API 15),
no consegui fazer funcionar. Somente com o dispositivo real conectado e,
mesmo assim, tive diversos problemas.
Mas estes problemas no invalidam o uso do Processing como ferramenta de prototipao de games, pois s rodar no modo Java e tudo vai funcionar, inclusive o OpenGL.

Um tutorial rpido no Processing


Bem, abra o PDE e ver uma aba com contedo em branco. um Sketch,
pronto para que voc digite os comandos. Digite a linha abaixo:
println(Hello World);

Ou ento qualquer texto que considere interessante. Agora, clique no boto


play na barra superior. Voc ver uma pequena janela cinzenta, vazia, e na
outra (a do PDE), o seu texto aparecer na parte inferior (console).
Agora, vamos fazer algo mais bacana no Processing. Vamos fazer uma
nave andar pela tela. Para comear, crie um novo Sketch no PDE e salve.
O Processing salva seus sketches dentro da pasta Documents/Processing
(no Windows), criando uma pasta com o nome do primeiro sketch que voc
salvar. Os arquivos possuem a extenso PDE.

52

Manual do Indie Game Developer - Verso Android e iOS

Agora, dentro da pasta do seu sketch, crie uma pasta chamada data e
copie o arquivo da nave, que fica em: ...\Codigo\AsteroidNuts_Processing\
Main\data\nave.png. Para saber como baixar os arquivos-fonte do livro, veja
na Introduo. A imagem deve ficar em uma subpasta data, dentro da pasta onde voc salvou o seu sketch, certo?
Agora, vamos digitar alguma coisa no PDE. Uma aplicao Processing
tem estrutura bastante simples. Podemos digitar comandos imediatos (semelhante ao Javascript) ou podemos criar funes. Existem duas funes especiais, que so callbacks, ou seja, o runtime do Processing as invoca
quando necessrio: setup() e draw(). A funo setup() chamada no
incio do programa e apenas uma nica vez. Nela, colocamos todo o cdigo
de inicializao. A funo draw() invocada quando necessrio criar um
novo frame.
O Processing serve para criarmos simulaes framed-based, ou seja,
aquelas semelhantes a desenhos animados. A cada intervalo de tempo, atualizamos o modelo, apagamos e redesenhamos a tela. Este intervalo o frame rate, que medido em FPS Frames por Segundo. Podemos ajustar o
intervalo com a funo frameRate(). O FPS default 60, ou seja, a funo
draw() ser invocada 60 vezes por segundo.
Vamos criar duas variveis: uma para armazenar a imagem da nave e a outra para armazenar a posio dela. Antes de mais nada, o Processing utiliza
o sistema cartesiano com a origem no canto superior esquerdo, ao contrrio do
Codea. Eis o cdigo inicial:
PImage nave;
PVector posicao;

A classe PImage serve para armazenar uma imagem (um sprite) em


memria e a classe PVector armazena um vetor. Estes so dois exemplos de
comandos imediatos. Os comandos que ficam dentro de funes s sero executados quando ela for invocada.
Agora, vamos criar nossa funo setup():
void setup() {
size(480, 320);
nave = loadImage(nave.png);
posicao = new PVector(10,80);
}

O que estamos fazendo? Definimos o tamanho da nossa janela grfica em


480 pixels de largura e 320 pixels de comprimento, depois carregamos na

Captulo 4 - Prototipao do game 53

nossa varivel nave o sprite da nave, que est na subpasta data. Finalmente, iniciamos o vetor posicao indicando x = 10 e y = 80.
Agora, vamos fazer com que a nave ande at o final da tela, um pixel a
cada 1/60 de segundo. Para isto, temos que apagar a tela, desenhar a nave e
incrementar a posio no eixo das abscissas. Se o valor for maior que a largura da tela (width) ento temos que voltar posio inicial. Eis o cdigo da
funo draw():
void draw() {
background(40, 40, 50);
imageMode(CORNERS);
image(nave, posicao.x, posicao.y,
posicao.x + 100,
posicao.y + 50);
posicao.x++;
if (posicao.x > width) {
posicao.x = 10;
}
}

A funo background(R,G,B) limpa a tela e a funo image desenha


a imagem, informando as suas coordenadas (canto superior esquerdo e canto
inferior direito).
Ok, tem uma coisa estranha a: imageMode(CORNERS). Estou forando a funo image a carregar a imagem usando as coordenadas do canto
superior esquerdo e do canto inferior direito da imagem. Se voc viu o tutorial
em Codea, semelhante funo spriteMode().

Ilustrao 22: O resultado do primeiro tutorial

54

Manual do Indie Game Developer - Verso Android e iOS

Criando o prottipo do AsteroidNuts com o Processing


Agora, que j conhecemos um pouco do Processing, vamos criar o prottipo do jogo nele. O cdigo-fonte est junto com os exemplos do livro (no
precisa digitar nada, veja na Introduo). E muito semelhante ao que fizemos para o Codea.
Todo o cdigo-fonte pode ser baixado, logo, no necessrio digitar os
comandos. Veja na Introduo. Tem um arquivo Asteroid.pde (...\Codigo\AsteroidNuts_Processing\Main\Asteroid.pde) com a classe toda.
No importa se temos dois tipos de asteroides (bom e ruim), pois so apenas variaes de propriedades de um asteroide.
Nossa classe tem que armazenar os atributos de posio do Asteroide, alm
do seu tipo (bom ou ruim). No Processing, criamos classes de forma semelhante ao Java. Para comear, crie um sketch chamado Main e salve (file
/ new e file / save). Para criar uma nova classe, escolha o menu File/new,
depois, save com o nome Asteroid, na mesma pasta do sketch Main.
O Processing no tem gabarito algum para criarmos uma classe, ento,
escreva o seguinte texto:
class Asteroid {
Asteroid() {
}
void draw() {
}

como uma classe Java comum, s que no usamos modificadores de


acesso. Note que este esqueleto tem um construtor (Asteroid()) e tem um
mtodo (draw()). O construtor chamado quando criamos uma instncia
(um novo objeto) da classe, e o mtodo draw() s chamado quando ns o
invocamos (no confunda com a funo draw(), do arquivo principal).
O construtor ser invocado sempre que quisermos pegar um novo objeto
da nossa classe, por exemplo:
Asteroid ast1 = new Asteroid();

Para comear, vamos trabalhar nosso construtor. Primeiramente, temos que


pensar em como iniciar o Asteroide. Cada Asteroide dever surgir no canto

Captulo 4 - Prototipao do game 55

direito da tela, em uma altura varivel. Haver uma faixa de valores no eixo
das ordenadas (Y), onde os asteroides podero surgir. Eu no quero usar a tela
toda, pelo menos neste prottipo. Ento, faz sentido passarmos em passar os
limites para o construtor da classe, de modo que este possa gerar um asteroide
dentro da faixa esperada.
class Asteroid {
PVector position;
PVector dimension;
PImage imagem;
int type;
Asteroid(int upper, int lower) {
int tipo = int(random(1,101));
position = new PVector(100,50);
dimension = new PVector(50,50);
position.x = width - 100;
float rPos = random(upper, lower);
position.y = int(rPos);
if (tipo > 80) {
// asteroide bom
type = 2;
imagem = loadImage(bom.png);
}
else {
type = 1;
imagem = loadImage(ruim.png);
}
}

Todo o cdigo colocado entre o { e o } da classe, pertence a ela. Pode


ser uma propriedade ou um mtodo.
Processing uma linguagem de tipagem forte, ou seja, cada varivel tem
seu tipo atribudo estaticamente (e no pode mudar). Passamos dois parmetros para o construtor: upper limite superior no eixo das ordenadas, e
lower limite inferior. Ns criamos trs propriedades:
position: do tipo PVector, que armazena um vetor com coordenadas bidimensionais (x e y). Armazena a posio atual do
asteroide;
dimension: igualmente PVector, armazena a largura e altura da
caixa que contm a figura do asteroide;

56

Manual do Indie Game Developer - Verso Android e iOS

type: do tipo int (Java), que armazena o tipo do asteroide, se ele


bom (type = 2) ou ruim (type = 1). claro que isto poderia ser
resolvido melhor por hierarquia ou composio, mas um prottipo,
logo, descartvel;
imagem: do tipo PImage, que armazena uma imagem (um
Sprite).
Nossa estratgia para decidir se vamos criar um asteroide bom ou ruim
apenas aleatria. Poderamos levar alguns fatores em considerao, como o
nvel de energia do jogador e a quantidade de acertos que ele tem, mas, para
efeito de prottipo, vamos deixar assim. Geramos um nmero aleatrio entre
1 e 101 (int(random(1,101)) se ele for maior que 80, ns o transformamos
em bom.
Inicialmente, calculamos a posio horizontal (abiscissas) em width
100 (a varivel global width retorna a largura da tela, e height, a altura).
E calculamos a posio vertical (ordenadas) com um nmero aleatrio entre o
limite inferior e o superior (random(upper, lower)).
Agora, demos que desenhar um asteroide. O melhor local para fazer isto
no mtodo draw. Ns invocaremos o mtodo draw sempre que desejarmos redesenhar um objeto Asteroid especfico.
void draw() {
imageMode(CORNERS);
image(imagem, position.x, position.y,
position.x + dimension.x,
position.y + dimension.y);
}

A primeira coisa que fizemos foi mudar o modo de desenhar um sprite,


que uma imagem carregada na tela. Eu prefiro trabalhar com as coordenadas
dos cantos (superior esquerdo e inferior direito), ento eu mudei o modo de
desenho para CORNERS (imageMode(CORNERS)), que significa: x e
y sero o canto superior esquerdo da caixa que contm o sprite e os parmetros w e h sero, respectivamente, as coordenadas do canto inferior
direito da mesma.
Lembre-se: O Processing possui um sistema de coordenadas diferente
do Codea!
Se for do tipo 2, um asteroide bom, logo, eu uso uma imagem do
OpenClippart.org, que representa uma esfera brilhante (com uma estrela
dentro). Eu uso a funo image para desenhar a figura do asteroide na tela,
na posio especificada (propriedade position) e do tamanho especificado

Captulo 4 - Prototipao do game 57

(propriedade dimension). O primeiro parmetro o nome da figura, o segundo e o terceiro, as coordenadas do canto superior esquerdo, e o quarto e o
quinto, as coordenadas do canto inferior direito.
Finalmente, temos dois mtodos finalx e finaly, que retornam as coordenadas do canto superior direito do asteroide, para facilitar seu uso.
Agora, precisamos representar a nave (arquivo ship.pde). Vamos criar
uma classe da mesma maneira. Em seu construtor, ns criamos as mesmas
propriedades do asteroide, exceto o type. Mas colocamos o lower e o
upper como propriedades da nave. Veja o mtodo init:
class ship {
PVector position;
PVector dimension;
PImage imagem;
int upper;
int lower;
ship() {
position = new PVector(10,80);
dimension = new PVector(100,50);
position.y = (height - dimension.y)/2;
upper = int(position.y - 4 * dimension.y);
lower = int(position.y + 5 * dimension.y);
imagem = loadImage(nave.png);
}

O mtodo draw igualmente simples:

void draw() {
imageMode(CORNERS);
image(imagem, position.x, position.y,
position.x + dimension.x,
position.y + dimension.y);
}

Desenhamos da mesma maneira que o asteroide. E a nave tambm tem os


mtodos finalx e finaly. Na verdade, ambos (Asteroid e ship) poderiam
ser derivadas de uma classe ancestral comum GameObject, mas eu no quis
complicar as coisas, pois um prottipo. Um mtodo callback importante
o clicked, que ser invocado quando o usurio clicar com o mouse.
void clicked(int x, int y) {
if (y >= upper && y <= lower) {

58

Manual do Indie Game Developer - Verso Android e iOS

position.y = y;

Eu simplesmente pego a altura do clique do jogador e posiciono a nave


nela. Note que no necessariamente o clique na nave, mas na tela toda. Eu
poderia sofisticar este controle, mas, como um prottipo, eu preferi deixar
tudo mais simples. Na verdade, quem invoca este mtodo o meu arquivo
Main, j que ele no ser invocado automaticamente pelo Processing.
Agora, chegou o momento de criar a mecnica do jogo. Eu poderia ter
usado os recursos de fsica cintica do Codea (fornecidos pela biblioteca Box2D), com o objeto body. Assim, toda a parte de movimento e coliso seria
automtica. Porm, considerei que o prottipo ficaria complexo demais e, na
verdade o jogo muito simples na parte de fsica. Ento, eu mesmo criei a
fsica necessria para movimentar e verificar colises.
No arquivo Main, eu crio algumas variveis globais para controle do
jogo, as quais nem vou mencionar diretamente, exceto quatro:
ship nave;;
ArrayList asters;;
PFont letraTitulo;;
PImage explosao;;

A varivel asters do tipo ArrayList, que um array dinmico em


Java. E a varivel nave um objeto, que representar um objeto real da
classe ship. A varivel letraTitulo representa uma fonte ou definio
tipogrfica de caractere. A varivel explosao representa a imagem de uma
exploso, que vai aparecer quando a nave tocar em um asteroide ruim. A
tabela asters vai armazenar os asteroides ativos em determinado momento
(aqueles que no chegaram ao fim do caminho: o canto esquerdo da tela).
A funo setup ser invocada automaticamente pelo Processing, e inicializa as variveis do game.
void setup() {
size(320, 480);
upperlimit = (height) / 2 - 100;
lowerlimit = (height) / 2 + 100;
nave = new ship ();
// Init vars
lightyears=0;

Captulo 4 - Prototipao do game 59


energy=200;
contador=0;
fimjogo=0;
posdec=1;
limrandom=60;
inirandom=1;
gatilho= 53;
interval=12;
lastaster=0;
maxasters=10;
limitdec=60;
asters = new ArrayList();
explosao = loadImage(explosao.png);
letraTitulo = loadFont(Andy-Bold-14.vlw);
}

Aqui valem algumas notas importantes sobre arquivos de recursos (imagens e fontes). Todos os recursos em uma aplicao Processing devem ficar
dentro da subpasta data, dentro da pasta onde esto os sketches que voc
vai rodar. Logo, todas as imagens devem estar dentro dela. Esta estrutura j
est desta forma no zip do cdigo-fonte do livro.
Outra coisa importante a fonte. Se voc for escrever alguma coisa, deve
criar um arquivo de fonte no formato vlw, que o Processing usa. Existe
um arquivo vlw para cada combinao de tipo e tamanho de letra. Para criar
um arquivo vlw:
1. Abra o menu tools / create font;
2. Selecione o tipo de letra e o tamanho;
3. Clique em Ok;
4. Copie o arquivo vlw gerado (fica na pasta do prprio Sketch
para a subpasta data).
A funo mouseClicked ser invocada sempre que o jogador clicar na
tela. Quando isto acontece, eu repasso as coordenadas do clique para o mtodo
clicked da nave.
A funo draw um pouco extensa (e poderia ser refatorada), mas eu
tenho que comentar algumas coisas sobre ela, afinal o Game loop.
void draw() {
background(40, 40, 50);
strokeWeight(5);
textFont(letraTitulo);
fill(121, 249, 16, 255);

60

Manual do Indie Game Developer - Verso Android e iOS


textSize(24);
text(AsteroidNuts, 10, 25);
fill(91, 137, 229, 255);
text(Light years: + lightyears,10,50);
text(Energy: + energy,10, 75);
if (fimjogo == 1) {
fim();
}
else {
contador = contador + 1;
if (contador >= limitdec) {
lightyears = lightyears + 10;
energy = energy - 1;
if (energy <= 0) {
fim();
}
contador=0;
}
newaster();
nave.draw();
if (lightyears > 200) {
posdec = 2;
}
else if (lightyears > 500) {
interval = 6;
posdec = 10;
maxasters = 20;
}
else if (lightyears > 1000) {
limrandom = 3;
gatilho = 1;
interval = 1;
posdec = 40;
maxasters = 30;
limitdec = 40;
}
for (int i = asters.size()-1; i > 0; i--) {
Asteroid v = (Asteroid) asters.get(i);

Captulo 4 - Prototipao do game 61


if (v.position.x < 0) {
asters.remove(i);
currentasters = currentasters - 1;
}
else {
v.position.x = v.position.x - posdec;
v.draw();
if (asteroidhit(v) == 1) {
imageMode(CORNERS);
image(explosao,
v.position.x,v.position.y,v.
finalx(),v.finaly());
if (v.type == 2) {
energy = energy + 20;
asters.remove(i);
currentasters = currentasters
- 1;
}
else {
energy = energy - 2;
}
if (energy <= 0) {
fim();
}
}
}
}
}
}

A funo draw ser chamada automaticamente pelo Processing a cada


60 hertz (1/60 de segundo). Eu tenho que verificar se acrescento mais asteroides e movimentar os que j existem, alm de verificar se a nave colidiu
com algum deles. Tambm verifico se os asteroides chegaram ao fim do seu
caminho.
A primeira coisa incrementar o contador de anos-luz (lightyears) e decrementar o de energia, afinal, a nave se movimentou. Eu uso uma funo
newaster() para verificar se tenho que criar mais um asteroide (newaster()), e depois eu redesenho a nave, invocando seu mtodo draw.
void newaster() {
if (random(1,limrandom) > gatilho) {

62

Manual do Indie Game Developer - Verso Android e iOS

mit);

if (currentasters < maxasters) {


if (lastaster >= interval) {
ast = new Asteroid(upperlimit,lowerli
asters.add(ast);
currentasters = currentasters + 1;
lastaster=0;

}
else {
lastaster = lastaster + 1;
}

Esta funo newaster cria e insere um novo asteroide (table.


insert(aster,ast)) se as seguintes condies forem atingidas:
Gatilho aleatrio maior que 53, para dar um toque de impreviso ao
jogo;
O nmero de asteroides ativos for menor que 10;
O intervalo entre os asteroides for maior ou igual ao intervalo estabelecido (s pode lanar um novo depois de x asteroides);
Na verdade, esta funo faz parte da macro funo de Estratgia do jogo, e
deve ser bem elaborada para aumentar a jogabilidade. Mas, como um prottipo, eu deixei assim mesmo.
Depois, eu criei um loop para movimentar os asteroides, verificando se
houve coliso de algum deles com a nave:
for (int i = asters.size()-1; i > 0; i--) {
...
}

A tabela asters armazena os asteroides como um vetor dinmico comum.


A cada iterao, o valor de i apontar, para a posio do asteroide atual.
A primeira coisa que eu fao no loop verificar se o asteroide chegou ao
fim do caminho, removendo-o da tabela:
if (v.position.x < 0) {
asters.remove(i);
currentasters = currentasters - 1;
}

Captulo 4 - Prototipao do game 63

No d problema algum remover um elemento durante a iterao.


Se o asteroide ainda no chegou ao fim, eu decremento sua posio no eixo
das abscissas e verifico se houve coliso com a nave (funo asteroidhit).
Se houve, eu verifico se era um asteroide bom ou ruim, tomando a atitude
correta (incrementar ou decrementar energia).
O jogo chega ao fim sempre que a energia acaba.
Durante o jogo, eu exibo algumas mensagens na tela. Eu uso a sequncia
de comandos abaixo:
textFont(<fonte>): estabeleo qual o objeto PFont a ser utilizado
para a prxima escrita;
fill(R, G, B, A): determino a cor do texto;
textSize(<tamanho>): limito o tamanho do texto (em pontos);
text(<texto>, x, y): escrevo o texto nas coordenadas indicadas;
Com este prottipo, eu posso brincar com o game, repensando a jogabilidade, as regras etc. A codificao de baixa verbosidade, e a API do Processing facilita tudo. Eu tenho um jogo quase completo que foi desenvolvido
rapidamente.

Ilustrao 23: O prottipo do Asteroid Nuts no Processing

64

Manual do Indie Game Developer - Verso Android e iOS

Crie vrios prottipos


Eu sugiro que voc crie vrios prottipos e evite apagar os anteriores.
Mantenha vrias verses, testando novas maneiras de jogar e novos cenrios e
elementos. Depois, voc pode fazer um corte, eliminando as ideias que no
gostou. A facilidade de criar prottipos torna o projeto do game mais objetivo,
pois, uma vez que voc tenha testado todos eles, poder escolher as ideias
mais interessantes e se concentrar apenas nelas.

Captulo 5
Fsica de games
Fsica uma macro funo muito importante em games de ao, pois serve
para aumentar a jogabilidade, aumentando a percepo de realidade e o envolvimento do jogador. A fsica de um game deve lidar com problemas como:
fora, acelerao, movimento e coliso, tentando fazer com que os Game Objects se comportem aproximadamente como os modelos reais.
Os exemplos so incrementais
Eu vou mostrar vrios exemplos neste captulo e todo o cdigo-fonte est
disponvel para voc, logo, no necessitar digitar coisa alguma.
Porm, eu quero fazer uma observao sobre os exemplos: o objetivo
mostrar como aplicar clculos de fsica aos games. Neste momento, eu no
estou preocupado com a preciso do Game Loop ou com quaisquer outros
detalhes. Logo, antes de usar os exemplos como base para o seu game, tenha
em mente que existem mais aspectos a serem analisados, ento, eu sugiro que
voc leia at o final ANTES de sair criando seu jogo definitivo.
Rode todos os exemplos
Este captulo muito grande, mas extremamente importante para que
voc conhea bem a fsica de jogos. Todos os exemplos esto junto com o
cdigo-fonte que acompanha o livro. Eu recomendo que voc baixe todos e
rode em seu computador.

66

Manual do Indie Game Developer - Verso Android e iOS

Os primrdios

Ilustrao 24: O jogo Gorilla.bas, que vinha com o Microsoft QBasic

Um dos exemplos mais antigos de fsica que eu me lembro o do jogo


Gorilla.bas, que vinha com o Microsoft QBasic. Na verdade, eu j o conhecia de um produto mais antigo: o Microsoft QuickBasic, que eu utilizava para
desenvolver sistemas na dcada de 80. A fsica era bem simples e basicamente
lidava com o movimento de um projtil (uma banana), levando em conta a
fora da gravidade.
Se pararmos para pensar, no muito diferente do que o Angry Birds
(Rovio - www.rovio.com/index.php?page=angry-birds) original fazia: atirar
um pssaro, que viaja em trajetria influenciada pela gravidade. Quanto mais
fora, mais larga a parbola. E voc tinha que acertar ou derrubar os porcos.
A mecnica parecida com a do Gorilla.bas, talvez um pouco mais precisa.

Conceitos bsicos
Nem todo game to exigente na fsica quanto os jogos de ao. Existem
games que sequer possuem qualquer tipo de fsica. o caso do game que estou desenvolvendo para Facebook: o RandoSystem, que um quebra-cabeas
baseado em labirinto.

Captulo 5 - Fsica de games 67

Ilustrao 25: Um jogo que dispensa a fsica

Porm, se voc quer criar um jogo onde os Game Objects se movam e


colidam, deve prestar ateno fsica, de modo a aumentar o envolvimento
do jogador.
O jogo que eu descrevi no meu livro anterior, Mobile Game Jam (www.
mobilegamejam.com) era muito mais dependente de fsica.

Ilustrao 26: O jogo BueiroBall, do livro Mobile Game Jam

No BueiroBall (eu sei, o nome poderia ser melhor) voc tem que encaapar as bolas em determinada ordem, seno perde pontos (neste caso, as pretas s podem entrar depois das coloridas) e voc joga usando o acelermetro,

68

Manual do Indie Game Developer - Verso Android e iOS

ou seja, inclinando o seu dispositivo. Neste jogo, a fsica fundamental, pois


temos que acrescentar realismo ao movimento e s colises. As bolas devem
se movimentar de acordo com a inclinao, acelerando ou diminuindo conforme a posio do dispositivo.
E tambm tive que tratar a coliso, ou seja, o que acontece quando duas ou
mais bolas colidem. Qual o vetor de movimento resultante etc.

Acelerao e movimento
A parte que estuda isso na fsica a cinemtica. Eu no vou desperdiar
seu tempo explicando conceitos de fsica, mas, se quiser saber, recomendo a
srie fsica de video game, do portal The Code Bakers (http://www.thecodebakers.org/search/label/F%C3%ADsica).
O grande problema de jogos de movimento dinmico calcular onde voc
deve desenhar a figura no prximo frame. Se voc utiliza uma fsica simples,
como a que vimos no prottipo do AsteroidNuts (captulo anterior), isto
no problema. Basta decrementar ou incrementar o valor da coordenada
correspondente direo do movimento e pronto. No caso do AsteroidNuts,
estamos no espao, e consideramos a acelerao constante. Mas poderamos
criar efeitos interessantes, como o puxo da gravidade, por exemplo.
Para calcular a posio de um objeto com relao a um plano de coordenadas cartesianas, podemos usar o mtodo de Verlet (http://pt.wikipedia.org/wiki/
M%C3%A9todo_de_Verlet ou http://www.fisica.ufjf.br/~sjfsato/fiscomp1/node40.
html). A frmula bsica :
x(t + t) = (2 f)x(t) (1 f)x(t - t) + a(t)( t)2
Podemos calcular a posio em cada eixo, informando as foras que foram
aplicadas a eles (impulso e gravidade, por exemplo).
isso o que fiz no game Ataque das formigas marcianas, publicado
no The Code Bakers (http://www.thecodebakers.org/2012/04/fisica-de-videogame-3-finalmente-um.html). O cdigo-fonte do Game est no Google Code
(http://code.google.com/p/ataque-formigas-marcianas/), e ele usa a licena Apache
2.0 (Open Source).

Captulo 5 - Fsica de games 69

Ilustrao 27: O jogo Ataque das formigas marcianas

No jogo das formigas, o cdigo que controla o movimento e acelerao


calculado na classe que representa a Bola:
/*
* Esta classe representa uma bola
* Podemos criar mais de uma...
*/
class Bola {
double altura;
double alturaAnterior;
double alturaLimite;
double limiteSuperior;
double aceleracao;
double velocidade;
double dt;
double forcaGrav;
double massa;
boolean parada = true;
boolean descendo = true;
double e = 0.50; // Coeficiente de restitui o (pode
variar)
float x;
float y;

70

Manual do Indie Game Developer - Verso Android e iOS


Bola() {
}

reset();

void reset() {
limiteSuperior
=
altura
=
alturaAnterior
=
alturaLimite
=
aceleracao
=
velocidade
=
dt
=
forcaGrav =
massa
=
descendo
=
}

PISO;
limiteSuperior;
altura;
0;
0.0d;
0.0d;
01.d;
-9.8d;
1.0d;
true;

void atualizar() {
altura = altura + velocidade * dt +
(aceleracao * Math.pow(dt, 2)) / 2.0d;
double vel2 = velocidade + (aceleracao * dt) /2.0d;
double aceleracao = forcaGrav / massa ;
velocidade = vel2 + (aceleracao * dt) / 2.0d;
}

E, quando chegamos ao cho, nossa velocidade vai inverter, com uma


acelerao proporcional deformao causada pela coliso.

Coliso
Em fsica, temos dois tipos bsicos de coliso: elstica e inelstica (http://
www.coladaweb.com/fisica/mecanica/colisao-elastica-e-inelastica). Eu postei um
artigo sobre isso no portal The Code Bakers (http://www.thecodebakers.
org/2011/06/fisica-de-videogame-2-colisoes.html). O tipo de material dos dois ob-

jetos pode determinar o tipo de coliso que teremos:


Coliso elstica: ambos os corpos se deformam e se expandem aps
o choque, resultando na mesma energia cintica;
Coliso parcialmente elstica: os corpos perdem parte da energia
cintica, que transferida para trabalho (barulho, calor, deformao
permantente - plstica);

Captulo 5 - Fsica de games 71

Coliso inelstica: toda a energia cintica transferida para trabalho;


O que significa isso? Bem, quando dois Game Objects colidem, dependendo do tipo de material, parte da energia cintica transferida e um deles (ou
ambos) podem perder velocidade. o que acontece no jogo Ataque das formigas marcianas quando a bola atinge o solo, quicando e subindo at uma
altura menor que a inicial. Ela vai quicar algumas vezes at parar.
Em outros tipos de game (como o BueiroBall) ns no precisamos pensar nisso, pois irrelevante. Mas, em games onde o quique e a deformao
provocados pela coliso so importantes, necessrio calcular o que acontece
depois da coliso.
Cada tipo de material tem um CR - Coeficiente de retribuio (http://
www.thecodebakers.org/2011/06/fisica-de-videogame-2-colisoes.html), que
influencia no clculo do movimento de quique e da deformao causados
pela coliso. No caso do Ataque das Formigas, eu estou desprezando a deformao e utilizando o CR apenas para calcular a fora que ser aplicada
bola para subir novamente.
if (bola.altura <= 0) {
// A bola bateu no cho
bola.altura = 0;
bola.x = MEIO;
bola.y = (float)(PISO - bola.altura);
canvas.drawBitmap(bitmap, bola.x, bola.y, null);
if (!bola.parada) {
verificaColisao(canvas);
}
double novaAltura = Math.pow(bola.e, 2) *
bola.alturaAnterior;
bola.velocidade = Math.sqrt(2 * (-bola.forcaGrav) *
novaAltura);
bola.alturaAnterior = novaAltura;
...

O CR que estou usando (0,50) provoca um alto valor de restituio, permitindo que a bola quique alto. Se usarmos algo como bola murcha caindo
em argila, teremos uma baixssima restituio, j que os dois materiais usaro
a maior parte da energia cintica no trabalho de deformao, absorvendo o
impacto.

72

Manual do Indie Game Developer - Verso Android e iOS

H outro fator a ser considerado: a direo dos objetos aps a coliso.


No caso do BueiroBall, estou simplesmente inverto a velocidade nos dois
eixos, que no exatamente o que aconteceria na vida real, mas d um efeito
aceitvel para o jogador. No caso do Ataque das formigas marcianas, estou
desprezando isso e fazendo a bola simplesmente subir na mesma direo. Porm, em uma coliso mais realista, a trajetria dos objetos seria afetada por:
ngulo da trajetria dos objetos;
Massa dos objetos;
Velocidade dos objetos;
CR dos objetos;
Ou seja, h muitas variveis a serem consideradas, e, dependendo do jogo,
pode no ser interessante considerar isso tudo. Voc tem que analisar o valor
que a coliso realista vai agregar ao seu game.
No prottipo de game AsteroidNuts a coliso tratada de forma muito
simples. Ao tocar em um asteroide, a nave perde ou ganha energia (no jogo),
mas ambos (asteroide e nave) no so deformados e nem tm sua trajetria
alterada. Esta uma deciso que temos que tomar, caso sigamos em frente
com este projeto.

Deteo de coliso
Se temos um jogo onde existem objetos em movimento, ento tambm
teremos que detetar colises entre eles. Note que coliso nem sempre significa
desastre, por exemplo, quando o heri pula para uma plataforma, ocorre
uma coliso.
Eu criei alguns artigos sobre deteo de coliso no portal The Code
Bakers (http://www.thecodebakers.org/search/label/Game), mas um assunto
complexo e difcil de ser resolvido em desenvolvimento de games.
Todo Game Object que est em uma determinada camada pode ser afetado
por outros objetos da mesma camada. Por exemplo, a nave e os asteroides no
prottipo AsteroidNuts esto na mesma camada e podem se colidir.
Como saber se dois objetos colidiram? Se eles se tocarem ou se tiverem
pontos em comum, ento podemos deduzir que houve coliso. Todo Game
Object tem sua imagem exterior, logo, podemos assumir que esta a sua
fronteira? Imagine o GO da figura seguinte. Como detectaramos coliso de
um objeto como este?

Captulo 5 - Fsica de games 73

Ilustrao 28: Um Game Object de forma compexa

Realmente, teramos que delimitar um polgono em volta do Game Object,


de modo a testarmos se houve coliso com outro GO.
Podemos simplificar muito o clculo de coliso se utilizarmos polgonos
simples, como retngulos.

Ilustrao 29: Polgono de coliso retangular

Neste caso, a coliso se resume a determinar se os dois retngulos se intercetam. E isto pode ser feito facilmente, tanto no Android, como no iOS.
// Android
private boolean colisao (Rect r1, Rect r2) {
if (r1.intersect(r2)) {

return true;
}
return false;
}
// Objective C iOS
- (BOOL) colisao: (CGRect) r1 outro: (CGrect) r2
{
if (CGRectIntersectsRect(r1,r2)) {

return YES;
}
return NO;
}

74

Manual do Indie Game Developer - Verso Android e iOS

No Android, temos a classe android.graphics.Rect, que possui mtodos


para lidar com retngulos, e no iOS temos a estrutura CGRect, que representa
um retngulo, alm da funo CGRectIntersectsRect(), que verifica se dois
retngulos passados se intercetam.
claro que estamos falando de games 2D, pois para games 3D tudo muda.
O problema de usar polgonos retangulares pode ser notado na figura
seguinte.

Ilustrao 30: Nem sempre retngulos colisores do o melhor resultado

Podemos notar que, dependendo da forma e do tamanho dos objetos,


um retngulo colisor (o polgono que envolve o GO) pode dar um resultado ruim, detetando colises que poderiam ser evitadas, o que compromete a
jogabilidade.
Para resolver este problema, podemos usar outras formas ou malhas
colisoras.

Ilustrao 31: Outros tipos de colisores

Porm, temos outro problema: como vamos detetar colises? Bem, dependendo da forma dos colisores, existem vrias maneiras.

Captulo 5 - Fsica de games 75

Se ambos os objetos usarem colisores circulares, simples, pois basta


comparar a distncia entre seus centros e os seus tamanhos somados.
Se forem crculos e polgonos, podemos pegar o segmento de reta do polgono que est frente do crculo, e calcular a distncia entre o centro do
crculo e a reta, comparando com o raio, ou qualquer variante deste algoritmo.
Eu mesmo dei soluo para alguns destes problemas em: http://www.thecodebakers.org/2012/10/fisica-de-videogame-4-deteccao-de.html, e inclusive criei
uma biblioteca em Java para isto: http://fisica-videogame.googlecode.com/
files/colisoes3.zip.
Se ambos forem polgonos, podemos calcular a interseo de polgonos
convexos.
Um polgono P considerado convexo se qualquer segmento de reta definido por dois pontos dentro de P se situa dentro de P. Podemos usar o mtodo
de projeo para sabermos se dois polgonos se intercetam:
1. Selecionamos um dos segmentos do polgono colisor do PLAYER
OBJECT PO);
2. Selecionamos os GOs que esto na mesma faixa Y, ou seja, aqueles
que podem estar colidindo com o PO;
3. Selecionamos os GOs que esto mais prximos (X) dentro da faixa;
4. Para cada GO selecionado, verificamos a interseo;
A verificao da interseo em si feita pelo seguinte algoritmo:
1. Pegamos um dos segmentos de reta de um polgono;
2. Calculamos um vetor Ortogonal a ele;
3. Calculamos o produto vetorial do vetor ortogonal por cada vetor de
cada outro polgono, usando os pontos iniciais e finais. Isto nos dar
valores numricos reais;
4. Pegamos o mximo e o mnimo de cada produto;
5. Projetamos no segmento. Se houver interseo entre todos os segmentos, ento os polgonos esto se tocando ou se sobrepondo.
Uma boa referncia para isto : http://gamemath.com/2011/09/detectingwhether -two-convex-polygons-overlap/.
Infelizmente, dependendo da complexidade dos polgonos, o clculo pode
demorar demais. Eu propus uma alternativa no post: http://www.thecodebakers.
org/2012/11/fisica-de-videogame-5-multirectangle-cd.html, que consiste em dividir o problema em simples interseo de retngulos. Para isto, dividimos as
imagens em retngulos colisores, conforme a figura seguinte. Neste caso, o
segredo manter o mapeamento entre a posio atual do GO e a de cada retngulo colisor.

76

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 32: Uma opo mais simples de coliso de objetos complexos

A deteo de coliso um problema crtico da macrofuno de fsica de


qualquer game. Falhas na
deteco de coliso podem comprometer a jogabilidade, logo, eu recomendo duas abordagens:
Se o seu game muito simples, use retngulos ou crculos e ponto
final;
Se o seu game mais complexo, use um engine de fsica.

Engines de fsica
Para facilitar o desenvolvimento de jogos de ao, foram criadas vrias
bibliotecas de funes e clculos de fsica, chamadas de Physics engines
(Engines de fsica, a forma que eu prefiro). Existem vrios engines, sejam
gratuitos, pagos, 2D ou 3D. Todos eles servem para calcular: acelerao, movimento, coliso e seus efeitos.
perfeitamente possvel criar um game de ao sem utilizar um engine
de fsica. Na verdade, eu fiz isso em alguns projetos de jogos. O fator a ser
analisado na deciso de uso de um engine : qual o valor agregado por uma
fsica mais realista? certo que uma fsica mais realista vai demandar maiores recursos, como: CPU e/ou memria, ento, temos que ter certeza de que
seu uso vai agregar valor proporcional ao jogador, caso contrrio, estaremos
introduzindo o risco de lag toa.

Bullet Physics Library


um engine de fsica open source desenvolvido por Erwin Coumans.
Pode ser baixado do site: http://bulletphysics.org/wordpress/, e sua documen-

Captulo 5 - Fsica de games 77

tao pode ser acessada no link: http://bulletphysics.com/ftp/pub/test/physics/

Bullet_User_Manual.pdf.

O Bullet, alm de open source, multiplataforma. Ele feito em C++ e


pode ser usado em: Sony PLAYSTATION 3, Microsoft XBox 360, Nintendo
Wii, PC, Linux, Mac OSX, Android e Apple iOS. Alm disto, pode ser integrados a softwares de edio 3D, como o Maya, da AutoDesk, e o Blender.
Para ter uma ideia de como o Bullet profissional, veja s uma pequena
lista de games que o utilizam:
Toy Story 3 (Disney);
GTA IV (Rockstar Games);
Blood Drive (Activision);
Uma caracterstica importante do Bullet que a partir da verso 2.80 os
clculos de fsica de corpos slidos so executados pela GPU, utilizando o
OpenCL (http://www.khronos.org/opencl/), o que um grande recurso para evitar lags devido a clculos complexos de fsica.

Chipmunk physics engine


O Chipmunk um engine completo, tambm open source, e desenvolvido
por Scott Lembcke. Seu site http://chipmunk-physics.net/. Ele utilizado em
vrios games e ferramentas, como o Cocos2D (http://www.cocos2d-iphone.org/).
A verso pro paga, mas tem alguns recursos interessantes, como o
Autogeometry, que permite criar a malha de coliso diretamente a partir de
imagens.

Box2D
O Box2D uma biblioteca de funes fsicas para games totalmente
Open Source e gratuita, desenvolvida em C++ por Erin Catto. Seu site :
http://box2d.org. uma das mais famosas, talvez por ser uma das mais simples.
No possui recursos sofisticados, mas permite calcular movimento e coliso
de forma bem realista.
Ela possui verses para vrias linguagens, como o JBox2D (http://www.
jbox2d.org/), que bem fiel biblioteca original. Algo que ajudou a criar a
fama do Box2D foi sua utilizao no blockbuster Angry Birds (Rovio).
Como eu no posso abordar todos os engines de fsica em apenas um nico
livro, escolhi o Box2D, pois, alm da preferncia pessoal, muito comentado
hoje em dia.

78

Manual do Indie Game Developer - Verso Android e iOS

Fsica com Box2D


Antes de continuarmos, necessrio fazer alguns comentrios sobre o Box2D. Para comear, ele foi feito em C++ e, para utiliz-lo dentro de aplicaes
Android, ns teramos que usar JNI para nos comunicarmos com ele, o que
dificultaria um pouco o desenvolvimento. Ento, eu optei por ensinar Box2D
usando o JBox2D em Java mesmo. Depois, eu explico como isso funciona
com o Box2D e o iOS. Assim, muitas vezes eu vou me referir apenas ao Box2D, neste caso, tudo vale tambm para o JBox2D.
A segunda coisa que precisamos saber que o Box2D no desenha na tela.
Ele apenas calcula posies, sendo sua a responsabilidade de desenhar o que
o Box2D calculou.
A terceira coisa que o Box2D no faz referncia origem do plano cartesiano que usa. Na verdade, ele vai trabalhar bem de qualquer forma, pois, para
ele no existe canto superior esquerdo. Se voc vai apresentar alguma imagem
baseado nas coordenadas calculadas pelo Box2D, voc tem que ajust-las para
a sua viewport, ou seja, se est usando um framework grfico que coloca a
origem do eixo das ordenadas no canto superior esquerdo, ento voc tem que
fazer a translao da coordenada y.
Finalmente, temos que conversar sobre as unidades e medidas. O Box2D
no assume nenhuma unidade, embora esteja ajustado para trabalhar com metros. Por exemplo, um retngulo de 50 x 30 o que significa? Pode ser um
prdio de 50 metros de largura por 30 de altura, ou pode ser uma caixa de
sapato, voc que decide. Em seu manual (http://box2d.org/manual.pdf), ele
recomenda que voc mantenha seus objetos dinmicos entre 0,1m e 10m de
tamanho (altura e largura), e os objetos estticos at 50m. recomendvel
que voc faa a traduo entre as medidas do Box2D e as medidas reais na
tela, evitando associar diretamente as unidades com pixels. Por exemplo, voc
criou uma caixa com largura 50 e altura 30 no Box2D e assume que sero 50
por 30 pixels. Crie os objetos com um valor de unidade que seja fcil de trabalhar, calculando a escala para exibio depois.
Isto muito importante: o Box2D trabalha melhor com objetos dinmicos
entre 0.1 e 10 metros. Objetos estticos podem ser maiores. Voc tem que
criar um fator de escala para traduzir para a tela. NO CONVERTA DIRETAMENTE METROS EM PIXELS!

Captulo 5 - Fsica de games 79

Preparando o laboratrio
Considerando que nem todo mundo tem um Mac ao seu dispor, achei
melhor estudarmos os conceitos do Box2D com um laboratrio, baseado no
JBox2D e em Java. As diferenas so desprezveis, e podemos trabalhar em
qualquer plataforma Desktop. Depois, quando formos estudar a aplicao em
dispositivos mveis, ns usaremos o Box2D para iOS e o JBox2D para o
Android.
Eu recomendo que voc baixe e leia o manual do Box2D (C++): http://
box2d.org/manual.pdf. A definio de todas as funes est nele. Aqui, eu apenas listo um resumo. Se quiser o modelo de objetos em Java, leia o JavaDoc
do JBox2D, que vem dentro da distribuio. Eu aconselho voc a comprarar
sempre os dois.
Eu recomendo que voc baixe os dois: o Box2D e o JBox2D. Para baixar
o Box2D, acesse o link: http://code.google.com/p/box2d/downloads/list e baixe a
ltima verso (no meu caso 2.2.1). E, para baixar o JBox2D, acesse o link:
http://code.google.com/p/jbox2d/downloads/list e baixe a ltima verso (2.1.2.2).
Descompacte os dois arquivos em pastas separadas.
O cdigo-fonte deste projeto est junto com os fontes do livro. Veja no
captulo de introduo. A pasta ...\Codigo\JBox2DLab\jbox2lab.zip. um
projeto eclipse compactado, logo, voc pode importar para sua workspace.
Nota: Configure sua Workspace eclipse para trabalhar com caracteres
UTF-8. Abra o menu Window / Preferences, expanda o item General e
clique em Editors, clique no link Content-types, selecione Text e digite
UTF-8 no campo Default encoding, pressionando o boto Update. Isto
far com que os caracteres acentuados apaream de forma correta. Usurios
Mac acharo o menu Eclipse / Preferences.
Antes de comearmos, quero deixar claro que, nesta parte do livro, vamos
apenas tratar de Box2D, deixando os outros assuntos para mais adiante.
Eu criei um laboratrio Java, porque roda em qualquer plataforma (Microsoft Windows, Linux ou Mac OSX). Vamos aprender a usar o Box2D com ele
(na verdade, usando o JBox2D). Mais para o final do captulo, eu mostrarei
como usar o Box2D em projetos Android e iOS.
Porm, se voc no conhece Java, isto pode ser um problema. Na verdade,
neste livro eu assumo que voc conhece o bsico de Android e de iOS, logo,
tem que conhecer um pouco de Java e de Objective-C. claro que voc pode
optar por usar somente uma das plataformas, ignorando a outra.

80

Manual do Indie Game Developer - Verso Android e iOS

O laboratrio Java
O laboratrio uma aplicao bem simples, que mostra a simulao de
uma bola caindo no cho e quicando.

Ilustrao 33: O laboratrio Box2D

Ele servir para exercitar vrios conceitos que veremos neste captulo. O
prprio Box2D tem um programa de laboratrio, chamado de TestBed, e o
JBox2D tem um programa semelhante, que pode ser acionado ao darmos um
duplo clique no arquivo jbox2d-testbed-2.1.2.2-jar-with-dependencies.jar,
que um JAR executvel, localizado na pasta target do projeto JBox2D
library.

Ilustrao 34: O programa Testbed

Captulo 5 - Fsica de games 81

Eu aconselho que voc brinque um pouco com o Testbed, ajustando a taxa


de Frames Por Segundo, o nmero de interaes etc. Ele excelente para estudar os recursos do Box2D (e do JBox2D), s que meio barra pesada de
encarar, caso voc esteja comeando.
Ento, resolvi criar um projeto laboratrio bem simples, mas que isole
voc de outros aspectos ainda no estudados, como: Game loop e renderizao, por exemplo. Ele vem com um projeto muito simples, mas que pode ser
utilizado para criar outros.
Para usar o projeto laboratrio, basta criar uma workspace no eclipse e
importar o projeto. Depois, voc pode renome-lo.
O laboratrio bem simples e foi implementado em Java/Swing, como
uma aplicao Desktop. A classe principal (LabMain) derivada de JFrame
e possui uma classe derivada de JPanel PainelGrafico como alvo da renderizao. Os mtodos e classes que eu uso esto agrupados por funo, precedidos de comentrios. Os principais grupos so:
Iniciador da aplicao: cdigo para instanciar a classe LabMain
e iniciar a aplicao, exibindo a janela;
Funes que lidam com o Box2D: todas as classes e mtodos que
lidam diretamente com o JBox2D esto neste grupo;
Rotinas do Game Loop: os mtodos e classes que se ocupam do
Game loop;
Rotinas que traduzem as coordenadas e desenham: responsveis
por desenhar o estado atual do mundo Box2D na tela.
Procure identificar estes mtodos e classes no cdigo-fonte.
A simulao iniciada com um clique do mouse sobre a janela. O evento
MouseClicked ser disparado e eu tenho uma classe derivada de MouseListener, que responde a este evento. Se a simulao estiver parada, ela vai
rod-la, caso contrrio, vai par-la.
A simulao baseada em frames, ou seja, de tempos em tempos o Game
loop executado. Alis, vale uma observao aqui... O Game loop um ponto muito sensvel de qualquer projeto de Game, e pode ser responsvel pelo
seu fracasso. O jogador espera que o jogo rode suave, sem saltos e lags, e
que rode com velocidade semelhante, seja em um Smartphone de 600 Mhz
ou em um Tablet Dual de 1.0 Ghz. Se o jogo apresentar repetidamente saltos
ou lags, pode irritar o jogador e fazer com que seu game seja detonado
rapidamente.
Uma boa implementao de Game loop deve ser consistente, ou seja, permitir que o jogo funcione na mesma velocidade aparente, independentemente da velocidade do processador, e tambm deve permitir uma renderizao

82

Manual do Indie Game Developer - Verso Android e iOS

suave dos frames. Mais adiante, veremos tcnicas para criar Game loops otimizados, voltados para games profissionais. Porm, por enquanto, essa no
minha preocupao, logo, criei um esqueleto de Game loop bem simples, que
usa a classe java.util.Timer para controlar sua execuo. Este game loop no
to preciso, mas funciona relativamente bem e permite estudarmos o Box2D
sem grandes preocupaes.
Eis o cdigo-fonte do nosso Game loop, junto com o seu disparador:
// *** Rotinas do Game Loop ***
/*
* Para maior simplicidade, estamos usando as classes java.
util.Timer e
* java.util.TimerTask. Alm disto, estamos usando
renderizao passiva.
* Se quiser um melhor resultado, veja o captulo Framework
bsico de game.
* Este tipo de implementao de Game loop no muito preciso,
servindo apenas
* para demonstrao do Box2D.
*/
public void runGameLoop() {

simulando = true;

task = new GameLoopTask();

timer = new Timer();

timer.scheduleAtFixedRate(task, 0, gameLoopInterval);
}
public void gameLoop() {

synchronized (this) {

// Um lembrete de que pode haver problemas de
concorrncia

update();

redesenhar();

};
this.pGrafico.repaint();
}
class GameLoopTask extends TimerTask {

@Override

public void run() {

gameLoop();

}
}

Captulo 5 - Fsica de games 83

Eu uso um Timer (java.util.Timer) para disparar uma Task (classe GameLoopTask), que vai rodar a cada x milissegundos (varivel gameLoopInterval). A minha Task simplesmente executa o mtodo gameLoop(), que atualiza o estado do modelo (mtodo update()) e comanda
a criao do buffer do frame (mtodo redesenhar()). Depois, ele fora a
atualizao do painel grfico invocando seu mtodo repaint().
Eu estou usando a tcnica de Double buffering, ou seja, eu desenho em
uma imagem no atrelada tela e depois desenho esta imagem no contexto
grfico do painel. Assim, eu evito invocar mtodos de desenho complexos
dentro do mtodo paintComponent() do painel.
private void redesenhar() {
// Estamos usando a tcnica de double buffering
// Vamos desenhar em uma imagem separada

gx.setColor(Color.black);
gx.fillRect(0, 0, larguraImagem, alturaImagem);
gx.setColor(Color.yellow);
Retangulo rect = criarRetangulo(bola);
gx.drawOval(Math.round(rect.x), Math.round(rect.y),

Math.round(rect.width), Math.round(rect.width));

rect = criarRetangulo(chao);
gx.drawRect(Math.round(rect.x), Math.round(rect.y),

Math.round(rect.width), Math.round(rect.height));
}

neste mtodo redesenhar() que voc deve renderizar os objetos que


criou no Box2D. Neste exemplo, eu criei uma bola e um retngulo, que serve
de cho para a bola quicar. Cada vez que o modelo for atualizado pelo Game
loop, eu redesenho tudo em um buffer de imagem (variveis gImagem e
gx).
Quando o painel precisa ser repintado (classe PainelGrafico, que foi adicionada ao JFrame), o mtodo paintComponent() invocado:
class PainelGrafico extends JPanel {
private static final long serialVersionUID =
5173079166655854668L;
@Override
protected void paintComponent(Graphics g) {

84

Manual do Indie Game Developer - Verso Android e iOS



super.paintComponent(g);

if (gImagem != null) {

synchronized(gImagem) {

g.drawImage(gImagem, 0, 0, null);

}

}
}
}

Eu simplesmente desenho a imagem que criei em meu buffer gImagem,


a partir do canto superior da rea livre do Painel. Note que o desenho feito
pelo Thread principal da aplicao, ou seja, aquele que est processando
os mtodos da classe LabMain, enquanto o Game loop executado por um
Thread separado. Quando temos mltiplos processadores (Dual) isso uma
grande vantagem. Eu sincronizei o acesso imagem, pois pode acontecer de
tentarmos acessar a imagem ao mesmo tempo que ela est sendo desenhada, o
que gera imagens incompletas e flicker (piscar).
Porm, temos um problema: ao invalidar o painel com o mtodo repaint()
no Game loop, estamos enviando uma mensagem para que futuramente o mtodo paintComponent() do painel seja executado pelo Thread principal.
Ou seja, assncrono. Pode ser que o paintComponent() seja executando
quando estivermos no meio da gravao do buffer, o que poderia gerar resultados inesperados (Race condition). Para evitar isto, eu sincronizei a chamada
o update() dentro do Game loop:
public void gameLoop() {
synchronized (gImagem) {

// Um lembrete de que pode haver problemas de
concorrncia

update();

redesenhar();
};
this.pGrafico.repaint();
}

Com isto, tenho certeza que o paintComponent() (invocado assincronamente) jamais pegar sujeira da varivel gImagem. Com o novo Memory
Model do Java, os comandos de leitura e gravao so reordenados para evitar
este problema. Usando o synchronized, ns sinalizamos isto.
Agora, vamos ver rapidamente como ns traduzimos as informaes do
mundo virtual do Box2D para a nossa tela.

Captulo 5 - Fsica de games 85

Para comear, eu criei duas reas diferentes:


Uma janela visual (gImagem), com dimenses pr-definidas (larguraImagem e alturaImagem). As dimenses so em pixels e servem
para renderizao;
Uma janela virtual, na qual eu limito os meus objetos no mundo
do Box2D. Esta janela tambm tem dimenses prprias (larguraMundo, alturaMundo), derivadas do tamanho real atravs de um fator de escala (fatorEscalaVisual);
Lembre-se: para o Box2D, o mundo infinito. Voc tem que delimitar
uma janela de trabalho, que possa ser ajustada futuramente tela.
O fator de escala visual que estou utilizando 4.0, logo, os tamanhos e posies no mundo virtual devem ser multiplicados por 4.0 antes de renderizar. importante notar que este fator de escala arbitrrio e pode ser mudado
de acordo com o tamanho da tela real.
Por que eu fiz isto? No custa lembrar... O Box2D trabalha com metros, a
tela com pixels. O Box2D tem um limite de preciso entre 0.1m e 10m, logo.
Se eu criar um objeto com 10 pixels, ele ser to pequeno que, dependendo da
resoluo e densidade da tela, ns sequer o veremos. Ento, eu uso o fator de
escala para renderizar.

Normalizao e ajuste para renderizao


Como eu mencionei, eu uso dois sistemas de coordenadas diferentes: a
janela da tela e a janela virtual. As diferenas so em escala e na origem do
eixo das ordenadas. Muitos ambientes grficos modificam a origem do eixo
das ordenadas, comeando na parte superior da tela.
E por que isso um problema? Bem, quando estamos trabalhando com
objetos em um plano cartesiano, ns tendemos a considerar que o eixo das
ordenadas orientado da origem para cima, ou seja, o cho fica prximo
origem. Se o sistema grfico do dispositivo considera a origem das ordenadas
em cima, podemos ter problemas. Por exemplo, imagine que ns criamos
um crculo cujo centro fica em (3,1), e ns o consideramos como em repouso
no cho. Na renderizao, ele apareceria estar no teto.

86

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 35: Diferenas nas coordenadas do Box2D e da tela

Ento, ns temos que ajustar a escala e a origem das ordenadas ANTES


de exibir a imagem. Para isto, eu criei dois mtodos que so invocados no
momento da renderizao:
normalizarCoordenadas(): obtm as coordenadas dos objetos
no mundo virtual e gera novas coordenadas, para renderizao, aplicando o fator de escala e invertendo a origem do eixo y;
criarRetangulo(): cria um retngulo a partir de cada objeto Box2D, devidamente normalizado, pronto para renderizao. Este mtodo invoca o normalizarCoordenadas;
E, no momento da renderizao, que ocorre no mtodo redesenhar(),
eu invoco criarRetangulo() para obter a posio e tamanho da figura que
tenho que desenhar na tela. Eu tive que sincronizar o desenho para evitar o
mesmo problema: tentar desenhar em uma imagem que est sendo utilizada.
Eis os cdigos destes mtodos:
private void redesenhar() {
// Estamos usando a tcnica de double buffering
// Vamos desenhar em uma imagem separada
synchronized(gImagem) {

gx.setColor(Color.black);

gx.fillRect(0, 0, larguraImagem, alturaImagem);

gx.setColor(Color.yellow);

Retangulo rect = criarRetangulo(bola);

gx.drawOval(Math.round(rect.x),
Math.
round(rect.y),
Math.round(rect.width),Math.round(rect.
width));

rect = criarRetangulo(chao);

Captulo 5 - Fsica de games 87



gx.drawRect(Math.round(rect.x), Math.round(rect.y),
Math.round(rect.width),Math.round(rect.
height));
}
}

private Vec2 normalizarCoordenadas(Vec2 coordB2D) {
Vec2 resultado = new Vec2(0.0f, 0.0f);
resultado.x = coordB2D.x * fatorEscalaVisual;
resultado.y
=
(alturaMundo
coordB2D.y)
*
fatorEscalaVisual;
return resultado;
}
private Retangulo criarRetangulo(Body body) {
Retangulo rect = new Retangulo();
Vec2 tamanho = (Vec2) body.getUserData();
tamanho = tamanho.mul(fatorEscalaVisual);

Vec2
posicao
=
normalizarCoordenadas(body.
getPosition());
rect.x = (int) (posicao.x - tamanho.x / 2);
rect.y = (int) (posicao.y - tamanho.y / 2);
rect.width = (int) tamanho.x;
rect.height = (int) tamanho.y;
return rect;
}

Fundamentos do Box2D
Vamos mostrar como usar o Box2D comeando pelo exemplo que j criei
no laboratrio. Todo o cdigo-fonte relacionado com o Box2D est no grupo
*** Funes que lidam com o Box2D ***. Temos o mtodo initBox2D(),
que inicializa o mundo virtual e cria os objetos que sero animados, e temos
o mtodo update(), que comanda a sua atualizao. O mtodo update()
invocado pelo Game loop e faz com que todos os objetos dinmicos sejam
atualizados. Porm, como o Box2D sabe se os objetos devem se mover e para
onde devem ir? Isto o resultado da atuao de foras sobre eles e, neste
exemplo, a fora da gravidade.
Classe World
A classe World (ou b2World, na verso C++) o container para
todos os outros objetos e atravs dele que tudo calculado. Ns utilizamos

88

Manual do Indie Game Developer - Verso Android e iOS

a instncia de World para criar todos os outros objetos. Para inicializar um


mundo virtual, temos que seguir a sequncia:
no JBox2D:
World world = new World(new Vec2(0.0f, -10.0f), true);

no Box2D (C++):
b2Vec2 gravidade(0.0f, -10.0f);
b2World* world = new b2World(gravidade, true);
...
delete world; // ao final, temos que deletar o objeto,
pois C++ no tem GC!

Ao criarmos o mundo, especificamos o vetor de gravidade que afetar o


nosso mundo. Utilizamos o tipo Vec2 (C++: b2Vec2) para criar uma direo. Este tipo contm as coordenadas x e y. Estamos informando que a
gravidade atuar com acelerao 10 no eixo das ordenadas, movimentando os
objetos para baixo, que um valor prximo acelerao da gravidade real na
Terra: 9,80665 m/s. E se especificarmos zero? Bem, nossos objetos dinmicos no se movero, exceto se alguma fora for aplicada a eles. Experimente
colocar: (0.3f, -10.0f) e veja a bola cair e rolar para a direita.
Corpos
Em um mundo Box2D, podemos ter alguns tipos de corpos:
Estticos: JBox2d: BodyType.STATIC, Box2D:
b2_staticBody;
Cinemticos: JBox2D: BodyType.KINEMATIC, Box2D:
b2_kinematicBody;
Dinmicos: JBox2D: BodyType.DYNAMIC, Box2D:
b2_dynamicBody;
muito importante entender bem a diferena entre os trs tipos de corpos.
Corpos estticos: Possuem massa infinita (armazenada como zero) e no
podem ser movidos como resultado da simulao. O que significa isso? Significa que eles so parecidos com o cho, ou seja, no se movem por conta
de colises ou outras foras. Mas voc pode reposicion-los, se desejar. Os
corpos estticos no reagem a colises com outros corpos estticos ou cinemticos, mas apenas com corpos dinmicos. Servem para representar o cho,
teto ou paredes.
Corpos cinemticos: so semelhantes aos corpos estticos, no sentido de
que no podem sofrer efeitos de quaisquer outras foras. Porm, movem-se

Captulo 5 - Fsica de games 89

automaticamente a cada atualizao, na direo da sua velocidade linear ou


angular. Eles podem afetar corpos dinmicos, mas no so afetados por outros corpos cinemticos ou estticos. Servem para representar elementos comuns em games, como: portas giratrias, alavancas etc. Os jogos como: Sonic
(SEGA) e Super Mrio (Nintendo) usam estes tipos de corpos para representar
dificultades nos nveis, como plataformas mveis, por exemplo.
Corpos dinmicos: recebem efeitos de foras e sofrem alteraes em seu
movimento. Possuem massa finita e determinada. Podem colidir com quaisquer tipos de corpos.
Para voc entender bem o conceito de corpos, preparamos mais um projeto
de exemplo: CorposBox2D (...\Codigo\CorposBox2D\corposbox2d.zip).

Ilustrao 36: A execuo do programa CorposBox2D

Neste programa, temos os seguintes corpos:


Cho, parede esquerda e parede direita: estticos. So mostrados
em vermelho e cercam o retngulo da cena;
Bolas: dinmicos. So mostrados em amarelo. Uma delas est no
centro e no alto, caindo em funo da gravidade, e a outra est no
cho, mais direita da cena e parada;
Quadrado: esttico. Um quadrado vermelho parado no canto direito, na metade da altura da tela;
Barras: cinemticas. Duas barras verdes, uma se move da direita
para a esqueda, e tem a largura maior que a altura, a outra se move
de cima para baixo e tem a altura maior que a largura;

90

Manual do Indie Game Developer - Verso Android e iOS

No se assuste com o tamanho do cdigo, pois a maior parte apenas configurao. Podemos mover toda essa parte para um arquivo XML e ler dentro
do programa, como vou mostrar mais adiante.
Agora, simule o programa dando um clique na tela. Rode at as duas bolas estarem no cho e observe atentamente o comportamento dos corpos. Eis
minhas observaes:
1 Corpos dinmicos colidem com corpos estticos e cinemticos. Voc
nota isso quando a bola mais esquerda resvala na barra cinemtica, que
est se movendo na horizontal. Isto afeta o movimento da bola. Outra prova
que a bola cai no cho e quica;
2 Corpos cinemticos no colidem com nada. Eles simplesmente se
movem, sem serem afetados por foras ou colises. Note que a barra que se
move na horizontal atravessa o quadrado esttico. E a outra barra, que se
move na vertical, atravessa a segunda bola, embora, no movimento de subida, ela carregue a outra bola para cima. Se esperarmos at que as duas bolas
estejam no cho, os caminhos das duas barras cinemticas vo se cruzar e elas
no sero afetadas por isto;
3 Corpos dinmicos colidem com tudo. Conforme demonstrado pelo
programa, as bolas colidem com o cho, com as barras e inclusive uma com a
outra. Corpos dinmicos so afetados por foras e por colises;
Agora, para efeito de simplicidade, vamos voltar ao exemplo inicial do
laboratrio (LabBox2D), de modo a explicar a criao dos corpos bsicos
(dinmicos e estticos).
Criamos corpos com a seguinte sequncia:
1. Criamos uma definio de corpo (JBox2D: BodyDef, Box2D:
b2BodyDef);
2. Ajustamos a posio do corpo com o mtodo .position.set() (Box2D:
.position.Set());
3. Criamos e instanciamos o corpo com o mtodo world.createBody()
(Box2D: world.CreateBody();
4. Informamos o tipo de corpo com o mtodo .setType() (Box2D:
propriedade type);
5. Criamos uma forma de colisor (Collision Shape) e informamos o
tamanho;
6. Criamos uma fixture para associar o corpo e o colisor;
Vamos ver um exemplo de criao de corpo:

Captulo 5 - Fsica de games 91

JBox2D:

BodyDef bolaDef = new BodyDef();


bolaDef.position.set(larguraMundo / 2, alturaMundo - 5);
bola = world.createBody(bolaDef);
bola.setType(BodyType.DYNAMIC);
Vec2 bolaTamanho = new Vec2(10.0f, 10.0f);
CircleShape bolaShape = new CircleShape();
bolaShape.m_radius = 5.0f;
FixtureDef bolaFixDef = new FixtureDef();
bolaFixDef.shape = bolaShape;
bolaFixDef.density = 1.0f;
bolaFixDef.restitution = 0.6f;
bolaFixDef.friction = 0.3f;
bola.createFixture(bolaFixDef);
bola.resetMassData();
bola.setUserData(bolaTamanho);

Box2D (C++):

b2BodyDef bolaDef;
bolaDef.position.Set(gObject.x, gObject.y);
b2Body * objeto = world->CreateBody(&bolaDef);
bolaDef.type = b2_dynamicBody;
b2CircleShape bola;
bola.m_p.Set(0, 0);
bola.m_radius = gObject.altura/2;
b2FixtureDef fixtureDef;
fixtureDef.shape = &bola;
fixtureDef.density = 4.0f;
fixtureDef.friction = 0.3f;
fixtureDef.restitution = 0.8f;
objeto->CreateFixture(&fixtureDef);
bolaDef.userData = (__bridge void *) gObject;

A forma do colisor determina a sensibilidade do objeto. No Box2D podemos ter vrios tipos de colisores:
Circulares: JBox2D CircleShape, Box2D: b2CircleShape;
Retangulares: JBox2D PolygonShape com setAsBox(),
Box2D PolygonShape com SetAsBox();
Segmentos de reta: Box2D b2EdgeShape. O JBox2D no tem
este tipo, mas pode ser criado;
Cadeia: Servem para ligar vrios segmentos de reta. No Box2D
b2ChainShape. O JBox2D no tem este tipo;

92

Manual do Indie Game Developer - Verso Android e iOS

Como vimos no incio deste captulo, importante que a forma colisora


seja o mais prxima possvel da imagem do objeto. Na verdade, podemos at
combinar mais de uma forma colisora, criando objetos agrupados.
Quando criamos colisores circulares, temos que informar o raio, e quando
criamos colisores retangulares, temos que informar a largura e altura com o
mtodo setAsBox() (Box2D SetAsBox().
Corpos estticos e cinemticos so criados da mesma forma, apenas mudando o BodyType (Box2D propriedade Type). Veja o exemplo CorposBox2D
para ver exemplos.
O objeto Fixture serve para associar propriedades a um corpo, como:
densidade, frico, restituio, forma colisora, entre outras. Vamos ver algumas destas propriedades:
Densidade: a razo entre a massa e o volume do corpo. Serve para
calcular a massa;
Frico: a resistncia que um corpo apresenta, quando em contato
com outro. um valor entre 0 e 1;
Restituio: o coeficiente de restituio do corpo, conforme vimos
no incio deste captulo. um valor entre 0 e 1, significando que um
corpo com zero restituio nunca vai quicar;
Depois de alterarmos a densidade de um corpo, precisamos resetar sua
massa com resetMassData() (Box2D ResetMassData());
Agora, j temos elementos suficientes para entender o exemplo bsico,
criado no programa LabBox2. Criamos dois corpos:
// Criamos o chao
BodyDef chaoDef = new BodyDef();
// A origem do eixo das ordenadas e no canto inferior,
e nao no superior
chaoDef.position.set(larguraMundo / 2, 2.5f);
chao = world.createBody(chaoDef);
chao.setType(BodyType.STATIC);
float larguraChao = larguraMundo - 2.5f;
Vec2 chaoTamanho = new Vec2(larguraChao, 2.5f);
// Atribumos uma forma retangular fixture do cho
PolygonShape chaoShape = new PolygonShape();
chaoShape.setAsBox(chaoTamanho.x / 2, 1.25f);
FixtureDef chaoFixDef = new FixtureDef();
chaoFixDef.shape = chaoShape;
chao.createFixture(chaoFixDef);
chao.setUserData(chaoTamanho);

Captulo 5 - Fsica de games 93


// Criamos uma bola
BodyDef bolaDef = new BodyDef();

bolaDef.position.set(larguraMundo / 2, alturaMundo - 5);
bola = world.createBody(bolaDef);
bola.setType(BodyType.DYNAMIC);
Vec2 bolaTamanho = new Vec2(10.0f, 10.0f); // A bola
tem 10 metros de dimetro
// Atribumos uma forma circular fixture da bola
CircleShape bolaShape = new CircleShape();
bolaShape.m_radius = 5.0f;
// A bola tem 5m de raio
FixtureDef bolaFixDef = new FixtureDef();
bolaFixDef.shape = bolaShape;
bolaFixDef.density = 1.0f;
bolaFixDef.restitution = 0.6f;
bolaFixDef.friction = 0.3f;
bola.createFixture(bolaFixDef);
bola.resetMassData();
bola.setUserData(bolaTamanho);

No JBox2D, podemos armazenar dados externos (criados por ns, e no


pelo Box2D), dentro dos objetos Body e BodyDef. Podemos associar a
representao externa do nosso objeto neste campo. para isto que serve o
mtodo setUserData(). E sempre que alteramos a densidade de um corpo,
o que fazemos no caso da bola, temos que mandar o Box2D recalcular a
massa, com o mtodo resetMassData().
No mtodo update(), ns simplesmente pedimos ao mundo virtual que
avance um passo no tempo:
private void update() {
// Atualiza o mundo Box2D e o modelo de dados
world.step(timeStep, velocityIterations, positionIterations);
}

Fora e atrito
Os corpos dinmicos so sujeitos aplicao de foras, como a gravidade.
Porm, tambm podemos aplicar uma fora a eles, causando seu movimento.
De acordo com segunda lei de Newton (http://pt.wikipedia.org/wiki/Leis_de_
Newton), a fora (F) o resultado da multiplicao da massa (m) de um
corpo, por sua acelerao (a), que um vetor:

F = ma

94

Manual do Indie Game Developer - Verso Android e iOS

A unidade de fora o Newton (N), a unidade de Massa o quilograma


(kg) e a unidade de acelerao Metros por segundo ao quadrado (m/s2).
A acelerao ocorre em uma ou em ambas as direes (estamos em um
sistema 2D). Se quisermos aplicar fora a um corpo, basta multiplicar a sua
massa por cada componente da acelerao.
No Box2D, a classe Body pode receber uma fora atravs do mtodo
applyForce():
JBox2D:
quadrado.applyForce(forca, quadrado.getWorldPoint(quadrado.
getLocalCenter().sub(new Vec2(20,0))));
Box2D:
quadrado->ApplyForce(forca, quadrado->getWorldPoint
(quadrado->getLocalCenter())));
Mas precisamos mesmo multiplicar a massa pela acelerao? No podemos simplesmente colocar um valor l? claro que podemos! S que teramos que chutar um valor ou ento calcular por fora. No mais fcil
multiplicar pela massa logo de uma vez?
O exemplo ForcaSimplesBox2D mostra bem como aplicar fora a um
quadrado (...\Codigo\ForcaSimplesBox2D\forcasimplesbox2d.zip).

Ilustrao 37: O resultado de aplicao de uma fora

Como vemos, uma fora foi aplicada a um quadrado, por um determinado


tempo. A velocidade mxima que o quadrado atingiu foi cerca de 9,89 m/s e
deslizou por aproximadamente 15 m. Normalmente, calculamos em unidades

Captulo 5 - Fsica de games 95

padro MKS (metro, quilograma e segundos). Como o Box2D fez isso? Ser
que est certo?
Precisamos relembrar alguns conceitos envolvidos...
Fora: uma grandeza que tem a capacidade de vencer a inrcia
de um corpo, modificando-lhe a velocidade. medida em Newtons
(N). Lembrando que estamos trabalhando em um mundo 2D, logo,
a fora pode ser aplicada a um ou aos dois eixos;
Massa: o produto entre a densidade e o volume de um corpo. No
deve ser confundida apenas com peso do corpo. A unidade de medida de massa o quilograma (kg);
Velocidade: a variao da posio de um objeto no espao, com
relao ao tempo, medida em metros por segundo (m/s);
Acelerao: a variao da velocidade de um objeto no espao e
vetorial, ou seja, pode ocorrer em mais de um sentido. Sua unidade
de medida metros por segundo ao quadrado (m/s2);
Atrito: a fora de resistncia entre dois objetos. Neste caso, estamos tratando de atrito dinmico, que ocorre quando h movimento
relativo entre eles. Cada tipo de material tem um coeficiente de atrito, que deve ser combinado com o do outro para calcular a fora de
resistncia;
Nosso objeto tem massa calculada de 500 kg (Body.getMass()). O Box2D calcula a massa com base nas dimenses e densidade. Ele tem que extrapolar o volume do objeto e ele usa a rea, afinal, em 2D os objetos no
possuem volume. Ao clicar na barra de espao, eu aplico uma fora ao objeto:
Vec2 forca =
new Vec2(+300.0f * quadrado.getMass(), 0.0f *
quadrado.getMass());

Vec2 posicao = quadrado.getWorldCenter();

quadrado.setAwake(true);
origemX = quadrado.getTransform().position.x;
vMax = 0;
quadrado.applyForce(forca, posicao);

Antes de falarmos de getWorldVector e getWorldPoint, vamos ver


como aplicamos a fora. Para comear, multiplicamos 300 m/s2 pela massa do
objeto e aplicamos ao eixo das abiscissas. Note que aplicamos acelerao zero
ao eixo das ordenadas.

96

Manual do Indie Game Developer - Verso Android e iOS

Uma fora tem tambm um sentido (um vetor), logo, devemos indicar isso
ao aplic-la. Estamos acertando bem no centro de massa do objeto, de modo
a evitar torque, pois queremos uma acelerao linear limpa.
O resultado uma fora de 150.000 N.
Para calcular a distncia que o objeto vai percorrer, eu posso aplicar a seguinte frmula:
d = V 2 / (2 . g . (f + G))
Onde:
d = distncia de parada;
g = acelerao da gravidade (estamos usando 10 m/s2);
G = inclinao do plano em percentuais (zero);
V = velocidade inicial (no nosso caso, a maior velocidade obtida
pela aplicao da fora);
f = coeficiente de atrito entre o cho e o objeto;
No sabemos exatamente por quanto tempo a acelerao foi aplicada... certo
que foi por uma frao de segundo, pois eu uso o mtodo world.clearForces();
aps cada atualizao do mundo. Ento, eu tive que obter a maior velocidade
que o objeto alcanou:
if (quadrado.getLinearVelocity().x > vMax) {
vMax = quadrado.getLinearVelocity().x;
}
gx.drawString(Velocidade X max: + (vMax) + m/s, 30, 60);

E esta velocidade foi cerca de 9,89 m/s. Ento, fcil saber o tempo em
que a fora foi aplicada:
v = a.t, logo: t = v/a, ento: t = 9,89 / 300, ou aproximadamente: 0,0330 s.
Bem, voltando ao clculo da distncia, temos que considerar o Coeficiente
de Atrito Combinado dos dois objetos. Uma das maneiras de calcular :
cac = sqr(coeficiente1 * coeficiente2).
O coeficiente do cho 0.5 (chaoFixDef.friction = 0.5f;) e o do quadrado
0.2 (quadradoFixDef.friction = 0.2f;), logo, o coeficiente de atrito combinado 0,31622. Ento, nossa frmula de distncia :
d = 9,892 / (2 . 10 . (0,31622)) = 15,47 m
Logo, chegamos a um resultado aproximado com o que o Box2D calculou,
e utilizamos os conceitos de fora, acelerao, velocidade, massa e atrito.
claro que este um exemplo bem simples, pois s existe um nico objeto
dinmico envolvido. Se tivermos mais objetos dinmicos, ou se tivermos acelerao nos dois eixos, teremos que considerar outros fatores, como o ngulo
final do objeto. Mas, vamos devagar.

Captulo 5 - Fsica de games 97

Ateno: voc no vai necessitar fazer todos esses clculos para criar jogos!
Mas deve entender os conceitos e as propriedades dos corpos, de modo a projetar movimentos mais realistas em seus Games.
Agora, chegou o momento de falarmos sobre alguns mtodos novos.
Para comear, temos que entender o que so coordenadas locais e globais no
Box2D:
Coordenadas locais: so baseadas no centro do objeto, como se ele
estivesse posicionado no ponto de origem (0,0);
Coordenadas globais: so as coordenadas do mundo Box2D;
Vrias propriedades de objetos e parmetros de funo no Box2D exigem
coordenadas locais, logo, importante entender bem os conceitos.
Alguns mtodos que utilizamos neste exemplo:
quadrado.getWordCenter(): obtm as coordenadas do centro de
massa do objeto, transformadas em coordenadas globais;
quadrado.getTransform(): obtm as transformaes aplicadas ao
centro do corpo, como posio e rotao. Podemos saber a posio
atual com getTransform().position, j em coordenadas globais;
quadrado.getMass(): obtm a massa calculada do objeto;
quadrado.applyForce(): aplica uma fora em um determinado
ponto do mundo;

Corpos circulares
J vimos como aplicar fora a corpos quadrados, agora, vamos ver como
fazer isto com corpos circulares. O exemplo ForceBox2D (...\Codigo\ForceBox2D\forcebox2d.zip) mostra como isto acontece.

Ilustrao 38: Fora aplicada a uma bola

98

Manual do Indie Game Developer - Verso Android e iOS

Rode o exemplo, clique para iniciar a animao, e tecle a barra de espao


(sem manter pressionada por muito tempo). Voc ver que a bola chutada
levemente, chegando a dar um pequeno salto. Observe a animao durante
algum tempo, sem chutar novamente. Notou algo errado? Sim! Aps a bola
cair no cho (ou bater na parede), ela rola para sempre.
Atrito de rolamento
Quando um objeto circular desliza sobre um objeto plano, ocorre uma fora de resistncia chamada resistncia de rolagem ou atrito de rolamento.
Esta fora diminui a velocidade do objeto circular, fazendo com que ele pare
completamente. Repare que no igual ao atrito dinmico, que ocorre quando
dois slidos em contato apresentam movimento relativo entre si. Infelizmente, o Box2D ainda no calcula o atrito de rolamento, logo, os objetos circulares tendem a rolar para sempre.
O que podemos fazer marretar os objetos para que eles aparentem o
atrito de rolamento. No mtodo initBox2D(), do exemplo, h uma linha de
comando comentada:
//bola.setFixedRotation(true);

Retire o comentrio e rode novamente. Voc notar que a bola rola menos
e parece estacionar depois de algum tempo. Mas o que foi exatamente que eu
fiz? O mtodo setFixedRotation(true) impede que o objeto role, ou seja, se
submetido a uma fora lateral, ele vai deslizar e NUNCA rotacionar. A bola
parece estar rolando, porm, na verdade, est deslizando sobre o cho. Note
que eu tive que diminuir bem os coeficientes de frico da bola e do cho
(ambos para 0,1).

Rotao dos corpos


Quando temos corpos dinmicos submetidos a foras, independentemente
de sua forma (circulares, quadrados etc), temos a possibilidade de rotao, ou
seja, deles rolarem sobre seus eixos de massa. O exemplo RotationBox2D
(...\Codigo\RotationBox2D\rotationbox2d.zip) mostra exatamente isto.

Captulo 5 - Fsica de games 99

Ilustrao 39: Um exemplo de corpo rotacionando

Para testar, rode o programa, clique com o mouse e tecle a barra de espao
(uma s vez). Voc ver que o quadrado recebe um peteleco na parte superior esquerda, e sai rodando (no sentido horrio) at parar.
Os corpos dinmicos no Box2D esto sujeitos atuao das foras,
que podem provocar ao de torque, causando a rotao do objeto. Quando isto acontece, o ngulo do objeto muda e os seus vrtices reais tambm.
Para renderizar corretamente um objeto em rotao, necessrio calcular as
coordenadas reais de seus vrtices.

Ilustrao 40: Como obter as coordenadas dos vrtices

100

Manual do Indie Game Developer - Verso Android e iOS

Para obter a coordenada global de um vrtice (j aplicada rotao), podemos pegar a coordenada do ponto local correspondente a ele. E o que eu
fao no novo mtodo criarPoligono():
//Coordenadas do canto superior esquerdo
Vec2 ponto = body.getWorldPoint(
new Vec2(

body.getLocalCenter().x - tamanho.x / 2,

body.getLocalCenter().y + tamanho.y / 2

)
);
ponto = normalizarCoordenadas(ponto);

O mtodo getLocalCenter() me d o centro de massa em coordenadas locais. Como um retngulo, eu posso simplesmente calcular a coordenada do vrtice e transformar em coordenadas globais, atravs do mtodo
getWorldPoint(). Note que o mtodo getLocalCenter(), neste caso, sempre retornar (0,0).
Depois de obter as coordenadas globais no Box2D, preciso normaliz-las
para a minha janela grfica, como fazamos no mtodo criarRetangulo().
Por que eu no posso simplesmente desenhar um retngulo? Afinal, eu posso fazer uma transformao ao desenh-lo em Java (Graphics2D.rotate()). A
resposta simples: sim, voc pode! Na verdade, o que faremos mais adiante.
Note que eu apliquei uma fora nos dois eixos e posicionei bem na metade
superior do quadrado, de modo a provocar o torque desejado e a rotao no
sentido horrio.
Vec2 forca =
new Vec2(350.0f * quadrado.getMass(), 350.0f *
quadrado.getMass());

Vec2 posicao = quadrado.getWorldCenter().add(new Vec2
(0,3));
quadrado.setAwake(true);
origemX = quadrado.getTransform().position.x;
vMax = 0;
quadrado.applyForce(forca, posicao);

Desenhando corpos como polgonos


Eu preciso obter as coordenadas de cada vrtice do corpo, devidamente
transformadas e normalizadas. Por isto, eu criei um mtodo criarPoligono(),

Captulo 5 - Fsica de games 101

que faz exatamente este trabalho. Ele retorna uma instncia de java.awt.
Polygon.
private Polygon criarPoligono(Body body) {
int [] xpoint = new int [4];
int [] ypoint = new int [4];

Vec2 tamanho = (Vec2) body.getUserData();

//Coordenadas do canto superior esquerdo
Vec2 ponto = body.getWorldPoint(

new Vec2(

body.getLocalCenter().x - tamanho.x

body.getLocalCenter().y + tamanho.y

)

);
ponto = normalizarCoordenadas(ponto);
xpoint[0] = (int)ponto.x;
ypoint[0] = (int)ponto.y;

//Coordenadas do canto superior direito
ponto = body.getWorldPoint(

new Vec2(

body.getLocalCenter().x + tamanho.x

body.getLocalCenter().y + tamanho.y

)

);
ponto = normalizarCoordenadas(ponto);
xpoint[1] = (int)ponto.x;
ypoint[1] = (int)ponto.y;

//Coordenadas do canto inferior direito
ponto = body.getWorldPoint(

new Vec2(

body.getLocalCenter().x + tamanho.x

body.getLocalCenter().y - tamanho.y

)

);
ponto = normalizarCoordenadas(ponto);
xpoint[2] = (int)ponto.x;

/ 2,
/ 2

/ 2,
/ 2

/ 2,
/ 2

102

Manual do Indie Game Developer - Verso Android e iOS


ypoint[2] = (int)ponto.y;

//Coordenadas do canto inferior esquerdo
ponto = body.getWorldPoint(

new Vec2(

body.getLocalCenter().x - tamanho.x / 2,

body.getLocalCenter().y - tamanho.y / 2

)

);
ponto = normalizarCoordenadas(ponto);
xpoint[3] = (int)ponto.x;
ypoint[3] = (int)ponto.y;

Polygon pol = new Polygon(xpoint, ypoint,xpoint.length);

return pol;
}

O mtodo obtm o tamanho original, que eu armazenei dentro da propriedade userData, e calcula as coordenadas locais de cada vrtice, transformando-as em globais com o mtodo getWorldPoint(). Depois, eu normalizo
as coordenadas (translao e escala) para a minha janela grfica. Finalmente, uma instncia de java.awt.Polygon retornada para ser renderizada:
gx.drawPolygon(criarPoligono(quadrado));

A mecnica para desenhar no Android e no iOS um pouco diferente, mas


os princpios so os mesmos.

Desenhando corpos como imagens


Apesar de podermos colorir os polgonos, o resultado final no fica muito
bacana... Em games, o visual quase tudo! Eu diria que vale at mais do
que a jogabilidade, afinal, eu vejo jogos por a que so lindos e vendem muito,
porm a jogabilidade no to boa.
Carregue o exemplo: ...\Codigo\ImageRotation\imagerotation.zip.
Bem, ao invs de desenhar um quadrado sem graa, que mais parece com
os elementos daqueles games da dcada de 80, vou desenhar a esttua de um
dolo de uma civilizao antiga. Me perdoem os desenhistas, mas eu fiz o
mximo que pude! Execute a classe ImageRotationBox2d.java.

Captulo 5 - Fsica de games 103

Ilustrao 41: A mesma animao usando uma imagem

E ento? Ficou legal? Ainda no? Ento tente a imagem seguinte, bastando
executar a classe Cenario.java...

Ilustrao 42: Agora, com cenrio de game

Agora est parecendo com um game! Bastou pegar algumas imagens do


OpenClippart (www.openclippart.org) e criar um cenrio. Bem, vamos ver
como foi que eu cheguei a esse ponto.
Para comear, abra a classe ImageRotationBox2d.java. Eu modifiquei
a classe do contexto grfico do meu buffer de imagem para Graphics2D,

104

Manual do Indie Game Developer - Verso Android e iOS

que compatvel com o java.awt.Graphics, mas contm mtodos importante


para lidar com transformaes. Depois, eu modifiquei um pouco o mtodo
criarRetangulo:
private Retangulo criarRetangulo(Body body) {
Retangulo rect = new Retangulo();
Vec2 tamanho = (Vec2) body.getUserData();
tamanho = tamanho.mul(fatorEscalaVisual);
Vec2 posicao = normalizarCoordenadas(body.getPosition());
float angBox2d = body.getAngle() > 6.265732015 ? 0 : body.
getAngle();
rect.angulo = -1 * angBox2d;

rect.x = (int) (posicao.x - tamanho.x / 2);
rect.y = (int) (posicao.y - tamanho.y / 2);
rect.width = (int) tamanho.x;
rect.height = (int) tamanho.y;
return rect;
}

Agora, eu obtenho tambm o ngulo que o corpo est, dentro da simulao


do Box2D, utilizando o mtodo getAngle(). Este ngulo vem em radianos
(1 radiano = 180 / graus), e tem uma considerao importante: os valores
positivos so no sentido anti-horrio! Ento, se um objeto est em um ngulo
de 0.785398163 radianos, ento ele est virado no sentido anti-horrio em
45 graus.
Porm, como o sentido do eixo das ordenadas ser invertido na renderizao, eu devo inverter o sinal do ngulo. Se voc estiver usando um ambiente
grfico normal, no necessrio fazer isto.
Como vou desenhar uma imagem, preciso carregar o arquivo. Eu defini o
cone do dolo como uma BufferedImage, e o carrego na inicializao (para
evitar sobrecarga no game loop):
BufferedImage idolo = null;
...
// No mtodo initComponents():
try {
ClassLoader classLoader = this.getClass().getClassLoader();
InputStream input = classLoader.getResourceAsStream(./
images/idolo.png);
idolo = ImageIO.read(input);
}

Captulo 5 - Fsica de games 105


catch (IOException e) {
JOptionPane.showMessageDialog(this, IOException ao
carregar a imagem: \r\n + e.getMessage());
}

Bem, isto puro Java... Eu preciso do ClassLoader para poder carregar


um arquivo que est junto do projeto. E uso o mtodo read(), da classe
ImageIO para carregar a imagem na minha varivel que vai represent-la.
Agora, para desenhar o dolo, eu usei o seguinte cdigo (dentro do mtodo
redesenhar()):
AffineTransform transform = gx.getTransform();
Vec2
centroImagem
=
normalizarCoordenadas(quadrado.
getWorldCenter());
Retangulo rect = criarRetangulo(quadrado);
gx.rotate(rect.angulo, centroImagem.x, centroImagem.y);
gx.drawImage((Image)
idolo,
(int)
rect.x,(int)
rect.y,(int) rect.width,(int)

rect.height, null);
gx.setTransform(transform);

Primeiro, eu preciso saber o centro da imagem, pois eu vou rotacion-la tendo este ponto como eixo. Isto pode ser feito com o mtodo getWorldCenter(),
do objeto Body. S que as coordenadas precisam ser normalizadas (inverter
y e aplicar a escala). Depois, eu preciso saber o retngulo do objeto, e,
para isto, uso o nosso conhecido mtodo criarRetangulo().
Antes de desenhar a imagem (dentro do retngulo), eu aplico uma transformao configurao do Graphics2D, mandando rotacionar todos os desenhos de acordo com o ngulo fornecido e tendo a coordenada do centro da
imagem como eixo. importante guardar a transformao original ANTES
de mandar rotacionar, caso contrrio, TODOS os desenhos sero rotacionados. Note que eu guardo a configurao original na varivel transform e,
depois de desenhar a imagem, eu volto tudo como estava antes, com o mtodo
setTransform.
O desenho da imagem bem simples: eu uso o mtodo drawImage, do
objeto Graphics2D, informando a imagem (precisei fazer um cast, pois eu
usei BufferedImage), o canto superior esquerdo, a largura e altura. O ltimo
parmetro uma instncia de ImageObserver, que ns no vamos utilizar
agora.
E, quando voc tecla a barra de espao, eu aplico uma fora nos dois eixos
(acelerao de 350 m/s2), na metade superior do dolo, fazendo-o pular girando no sentido horrio.

106

Manual do Indie Game Developer - Verso Android e iOS


Vec2 forca =
new Vec2(350.0f * quadrado.getMass(), 350.0f *
quadrado.getMass());

Vec2 posicao = quadrado.getWorldCenter().add(new Vec2
(0,3));
quadrado.setAwake(true);
origemX = quadrado.getTransform().position.x;
vMax = 0;
quadrado.applyForce(forca, posicao);

Agora, se voc gostou da segunda imagem, abra a classe Cenario.java.


As diferenas so mnimas:
Eu no renderizo o cho e as paredes;
Eu no escrevo as informaes sobre o objeto;
Eu desenho a imagem de fundo;

BufferedImage idolo = null;


BufferedImage cenario = null;
...
// No mtodo initComponents():
try {
ClassLoader classLoader = this.getClass().getClassLoader();
InputStream input = classLoader.getResourceAsStream(./
images/idolo.png);
idolo = ImageIO.read(input);
input = classLoader.getResourceAsStream(./images/cenario.
png);
cenario = ImageIO.read(input);
}
catch (IOException e) {
JOptionPane.showMessageDialog(this,

IOException ao carregar a imagem: \r\n +
e.getMessage());
}

No incio do mtodo redesenhar() eu desenho a imagem de fundo:


gx.drawImage((Image)
alturaImagem, null);

cenario,

0,

0,

larguraImagem,

Agora, para fechar com chave de ouro, tenho certeza que voc vai adorar
este exemplo:

Captulo 5 - Fsica de games 107

Ilustrao 43: Um exemplo com bola

Abra a classe JogoDeBola.java, clique com o mouse e tecle a barra de


espao, quantas vezes quiser. Voc ver a bola quicando e rolando, como um
jogo de verdade. Porm, se observar atentamente durante algum tempo, notar
que a bola no para de rolar. Isto devido falta de atrito de rolagem, que j
falamos anteriormente. Eu, particularmente, creio que isto no um problema, mas, porm, se voc quiser, pode descomentar a seguinte linha (dentro de
initBox2D):
//bola.setFixedRotation(true); // Comente esta linha
para giro livre

Torque e impulso
At agora, s aplicamos foras aos corpos, porm, h outras maneiras de
alterar o seu momento (http://pt.wikipedia.org/wiki/Torque), como o impulso e
o torque.
O impulso mede a variao da quantidade de movimento de um corpo.
o resultado da aplicao de uma fora em um intervalo de tempo. Se a fora aplicada constante durante todo o tempo, o clculo simples: I = F.t,
porm, se a fora varivel, o clculo bem mais complexo. No Box2D,
aplicamos impluso com os mtodos applyLinearImpulse() e applyAngularImpulse(). O primeiro aplica um impulso linear em um ou nos dois eixos, se
aplicado ao centro de massa do corpo, e o segundo, aplica um impulso angular, que provoca a rotao do objeto.

108

Manual do Indie Game Developer - Verso Android e iOS

O torque um movimento de alavanca em um objeto, fazendo-o girar sobre


seu eixo. Na verdade, muito parecido com a aplicao de impulso angular.
Tenho mais um exemplo sobre isso: ImpulseTorque (...\Codigo\ImpulseTorque\impulsetorque.zip).

Ilustrao 44: Comparao entre: impulso, fora e torque

Neste exemplo, vemos quatro quadrados, da esquerda para a direita:


1. Amarelo: submetido a uma fora com acelerao de 350 m/s2
apenas no sentido positivo das ordenadas;
2. Verde: submetido a um impluso tambm no eixo y. Aplicamos
um impulso com acelerao de 350 m/s2, por 1 segundo;
3. Azul claro: levantamos o quadrado, aplicando um pequeno impulso
linear e depois aplicamos o torque, no sentido horrio;
4. Magenta: levantamos o quadrado, aplicando um impulso angular no
sentido anti-horrio;
No Box2D, uma fora aplicada e a velocidade do objeto vai aumentando
gradativamente. Quando aplicamos um impulso, a velocidade modificada
instantaneamente! como se tivssemos aplicado uma fora muito maior ao
objeto. A mesma comparao existe entre o torque e o impulso angular.
Quando aplicamos um impulso linear, a unidade que o Box2D espera N/s
(Newtons por segundo).
O cdigo que modifica o momento dos quadrados est no cdigo que intercepta a tecla de espao:

Captulo 5 - Fsica de games 109


public void keyPressed(KeyEvent key) {
if (key.getKeyCode() == KeyEvent.VK_SPACE) {

// Quadrado 1: Fora

Vec2 forca =

new Vec2(0, 350.0f * quadrado1.getMass());


Vec2 posicao = quadrado1.getWorldCenter();

quadrado1.setAwake(true);

quadrado1.applyForce(forca, posicao);


// Quadrado 2: Impulso

Vec2 forca2 =

new Vec2(0, 350.0f * quadrado2.getMass());


Vec2 posicao2 = quadrado2.getWorldCenter();

quadrado2.setAwake(true);

quadrado2.applyLinearImpulse(forca2, posicao2);

// Quadrado 3: Torque


/* primeiro levantamos o quadrado e depois aplicamos
o torque */


Vec2 forca3 =

new Vec2(0, 30.0f * quadrado3.getMass());


Vec2 posicao3 = quadrado3.getWorldCenter();

quadrado3.setAwake(true);

quadrado3.applyLinearImpulse(forca3, posicao3);


quadrado3.setAwake(true);

quadrado3.applyTorque(-5000.0f);


// Quadrado 4: Impulso angular


/* primeiro levantamos o quadrado e depois aplicamos
o impulso */


Vec2 forca4 =

new Vec2(0, 33.0f * quadrado4.getMass());


Vec2 posicao4 = quadrado4.getWorldCenter();

110

Manual do Indie Game Developer - Verso Android e iOS



quadrado4.setAwake(true);

quadrado4.applyLinearImpulse(forca4, posicao4);


quadrado4.setAwake(true);

quadrado4.applyAngularImpulse(550.0f);

}
}

Deteo de coliso
Ok, j sabemos movimentar bem os objetos, porm, como saber quando
eles colidiram? Afinal, muitos games de ao dependem desta informao
para acumular pontos, destruindo inimigos, ou mesmo para saber quando o
heri foi atingido. No Box2D podemos saber isto criando uma classe que implementa a interface ContactListener (C++: b2ContactListener).
Esta interface possui alguns mtodos de Callback que podemos utilizar
para saber informaes sobre a coliso:
Begin Contact: dois corpos comearam a se sobrepor. Esta rotina s
pode ser invocada de dentro do loop do Box2D (Step);
End Contact: dois corpos deixaram de ter contato. Esta rotina pode
ser chamada de fora do loop do Box2D. Por exemplo, voc removeu
um dos objetos que estava sobre o cho;
Pre-solve: esta chamada acontece depois que houve a coliso, porm
antes de sua resoluo, ou seja, do reposicionamento dos objetos;
Post-solve: acontece depois que houve a coliso e ela foi processada.
O importante que o Box2D no deixa voc fazer certas operaes enquanto estiver dentro do processamento do mtodo step(). Por exemplo,
voc poderia destruir um objeto, causando um problema para o processamento
interno dele. A maneira que o Box2D recomenda voc armazenar todos os
dados de coliso que lhe interessam e process-los aps a execuo do mtodo
step().
O exemplo ContactBox2D (...\Codigo\ContactBox2D\contactbox2d.
zip) mostra um bom exemplo de deteo de coliso.

Captulo 5 - Fsica de games 111

Ilustrao 45: Deteo de coliso

Ao rodar a simulao, voc ver que a bola rola at atingir o quadrado,


ento a simulao interrompida e uma mensagem aparece. Este seria o comportamento tpico quando o Player morreu e acabaram as vidas... claro
que voc pode fazer diferente, por exemplo: incrementar (ou decrementar) os
pontos, vidas etc.
Eu tive que modificar um pouco o que eu coloco dentro da propriedade
userData de cada corpo. Eu criei uma classe que armazena o tamanho (que
eu uso para calcular o retngulo) e um id de cada corpo. Assim, posso saber
quais corpos colidiram.
DadosCorpo dadosBola = new DadosCorpo(bolaTamanho,5);

...
bola.setUserData(dadosBola);
...
class DadosCorpo {
public Vec2 tamanho;
public int id;
public DadosCorpo() {

super();
}
public DadosCorpo(Vec2 tamanho, int id) {

this();

this.tamanho = tamanho;

112

Manual do Indie Game Developer - Verso Android e iOS



}

}

this.id = id;

Depois, eu criei uma classe que implementa a interface ContactListener:


class Contato implements ContactListener {

@Override
public void beginContact(org.jbox2d.dynamics.contacts.
Contact c) {

DadosCorpo corpoA = (DadosCorpo) c.getFixtureA().
getBody().getUserData();

DadosCorpo corpoB = (DadosCorpo) c.getFixtureB().
getBody().getUserData();

if (corpoA.id >=4 && corpoB.id >= 4) {

atingiu = true;

}

}
@Override
public void endContact(org.jbox2d.dynamics.contacts.
Contact arg0) {

}
@Override
public void postSolve(org.jbox2d.dynamics.contacts.
Contact arg0,

ContactImpulse arg1) {

}
@Override
public void preSolve(org.jbox2d.dynamics.contacts.
Contact arg0,

Manifold arg1) {

}

}

A instncia de Contact que eu recebo contm informaes sobre os


dois corpos que entraram em contato. Posso usar os mtodos getFixtureA()

Captulo 5 - Fsica de games 113

e getFixtureB(), para obter os dois corpos, e posso usar as propriedades


de suas instncias de userData para saber o id de cada um. Como estes
Callbacks so invocados para TODOS os objetos e TODOS os tipos de contato, eu preciso saber quais foram os corpos que colidiram. No meu caso, o
id da bola 5 e o do quadrado 4, logo, se os dois ids forem maiores ou
iguais a 4, ento a coliso entre a bola e o quadrado.
Para concluir, tenho que adicionar uma instncia do meu ContactListener
ao mundo do Box2D (ao final do mtodo initBox2D()):
// Adiciona o contact listener
Contato c = new Contato();
world.setContactListener(c);

Quando a bola e o quadrado colidem, eu ligo o flag atingiu, e no mtodo


update() eu testo isto logo aps invocar o mtodo step():
private void update() {
// Atualiza o mundo Box2D e o modelo de dados
world.step(timeStep, velocityIterations, positionIterations);
world.clearForces();
if (atingiu) {

setTitle(tituloParado);

timer.cancel();

task = null;

simulando = false;

JOptionPane.showMessageDialog(this, A bola atingiu
o quadrado);
}
}

E assim eu posso processar as colises.


importante saber que os mtodos de Callback sero invocados sempre
que as colises forem possveis:
Dinmico / dinmico;
Dinmico / esttico;
Dinmico / cintico.

Juntas ou junes
Em fsica mecnica, temos o conceito de graus de liberdade, que o nmero de parmetros independentes para obter a posio (estado) de um corpo.
Em um plano (espao bidimensional), temos trs graus de liberdade: deslocamento horizontal (abscissas), vertical (ordenadas) e rotao (em um eixo
perpendicular ao plano).

114

Manual do Indie Game Developer - Verso Android e iOS

Uma junta (ou juno) uma conexo entre dois corpos, que limita seus
graus de liberdade. Por exemplo, se pregarmos um retngulo em um quadro de
avisos, com um alfinete bem no centro, ele poder girar em torno do alfinete,
mas no poder se mover.
Existem vrios tipos de juntas:
Rotacional;
Prismtica
Cilndrica;
Esfrica;
Engrenagem;
Mas vamos nos concentrar nos tipos de juntas que o Box2D permite criar,
usando a sua prpria nomenclatura:
Distance joint (juno de distncia): interliga dois corpos, fazendo
com que a distncia entre eles seja sempre constante. Exemplo: dois
corpos ligados por um cano;
Revolute joint (juno rotacional): quando dois corpos compartilham um ponto em comum, podendo girar um em relao ao outro,
em torno do ponto comum. Exemplo: sobrepomos dois retngulos
de papel (no totalmente) e os atravessamos com um alfinete;
Prismatic joint (juno prismtica): os corpos esto unidos, mas
no podem girar um com relao ao outro, porm, podem deslizar
sobre a junta. difcil pensar em um exemplo elucidativo, mas vamos tentar... Imagine atravessar dois pedaos de madeira com uma
barra quadrada, mas que deixe folga para que os pedaos de madeira
deslizem sobre a barra, embora no possam girar;
Pulley joint (polia): os corpos esto ligados a uma polia (ou roldana). Exemplo: imagine um varal de roupas de prender no teto. Ao
afrouxarmos uma das cordas, a parte do varal ligada a ela desce, e ao
puxarmos, a referida parte sobe;
Gear joint (engrenagem): liga duas juntas (prismticas ou rotacionais), permitindo que o movimento de uma afete o da outra. Exemplo: um crculo com junta rotacional ligado a um retngulo com junta prismtica (ligada a um plano);
Mouse joint: conecta um ponto em um corpo a um ponto no mundo
Box2D, geralmente, determinado pelo clique do Mouse;
Wheel joint (ex Line Joint): conecta um ponto de um corpo a uma
linha de outro corpo. Exemplo: a suspenso de um carro. A roda gira
e tambm se move para cima e para baixo, na linha de suspenso;

Captulo 5 - Fsica de games 115

Weld joint: tenta manter os corpos ligados em uma nica posio,


porm, pode ser quebrada ou amassada. Exemplo: uma parede
com tijolos;
Rope joint (corda): semelhante distance joint, s que permite
variar (para menos) a distncia entre os dois corpos. Ela estabelece
uma distncia mxima. como se os dois corpos fossem ligados por
uma corda;
Friction joint: possui parmetros para limitar o movimento relativo
dos dois corpos ligados;
Bem, o tipo de juno que voc vai usar depender do tipo dos Game Objects envolvidos. Para comear, vou mostrar uma demonstrao simples. Abra
o exemplo: SimpleJoint (...\Codigo\SimpleJoint\simplejoint.zip).

Ilustrao 46: Exemplo de juno

Rode o exemplo, clique com o mouse para iniciar a simulao. Voc ver
que os corpos caem juntos, como se estivessem ligados por algo rgido, como
um cano, porm, que permite rotao nas pontas. Clique algumas vezes com a
barra de espao e veja os objetos pularem e rodarem em torno dos seus pontos
de acoragem (o centro de massa).
H poucas diferenas entre este cdigo e os anteriores. Para comear, criamos uma junta entre os dois corpos, especificando tambm o ponto de ancoragem (onde a junta se prende) entre eles:

116

Manual do Indie Game Developer - Verso Android e iOS


// *** Criamos a juno entre os dois corpos:
DistanceJointDef jointDef = new DistanceJointDef();
jointDef.initialize(quadrado, bola, quadrado.getWorldCenter(),

bola.getWorldCenter());
jointDef.collideConnected = true;
juncao = (DistanceJoint) world.createJoint(jointDef);

Criamos um objeto DistanceJointDef para especificar os parmetros da


juno, depois, usamos o mtodo initialize para informar quais so os dois
objetos conectados e qual o ponto de ancoragem em cada um deles. Eu usei o
centro de massa, em coordenadas globais, como ponto de ancoragem. Depois,
simplesmente mandamos criar uma juno com base na definio.
Note que eu mudei a propriedade collideConnected para true. Isto no
tem o menor efeito neste exemplo, j que estou usando uma ligao rgida
e so apenas dois corpos. Porm, se houvesse mais corpos (e mais juntas)
ou se eu usasse uma Rope joint, isto permitiria que os corpos interligados
colidissem.
E como vou desenhar a juno? Simples:
// Desenha a juno:
gx.setColor(Color.magenta);
Vec2 cdCorpoA = normalizarCoordenadas(juncao.getBodyA().
getWorldCenter());
Vec2 cdCorpoB = normalizarCoordenadas(juncao.getBodyB().
getWorldCenter());
gx.drawLine((int)cdCorpoA.x, (int)cdCorpoA.y, (int)
cdCorpoB.x,
(int)cdCorpoB.y);

Eu desenho uma linha entre os centros dos dois corpos, devidamente normalizados para a minha janela grfica.
Ao teclar barra de espao, eu aplico uma fora ao quadrado:
Vec2 forca =
new Vec2(1000.0f, 1000.0f * quadrado.getMass());
Vec2 posicao = quadrado.getWorldCenter();
quadrado.setAwake(true);
quadrado.applyForce(forca, posicao);

Captulo 5 - Fsica de games 117

Mltiplos objetos interligados


Agora, vamos ver um sistema de objetos ligados por juntas. Abra o exemplo WormJoint (...\Codigo\WormJoint\wormjoint.zip).

Ilustrao 47: Um exemplo interessante de uso de junes

Rode o exemplo e veja uma minhoca composta por 10 bolas interligadas


cair no cho. Elas caem todas juntas. Porm, tecle a barra de espao algumas
vezes e veja a minhoca danar e pular. bem legal no?
Ns fizemos algumas mudanas no exemplo SimpleJoint. Para comear,
criamos as bolas e as juntas usando loops:
// Vamos criar a cadeia de bolas

bolas = new Body[numBolas];

for (int ix = 0; ix < numBolas; ix++) {
BodyDef bolaDef = new BodyDef();
bolaDef.position.set((larguraMundo / (numBolas + 1)) *
(ix+1) , 80);
Body bola = world.createBody(bolaDef);
bola.setType(BodyType.DYNAMIC);
Vec2 bolaTamanho = new Vec2(2.0f, 2.0f);
CircleShape bolaShape = new CircleShape();
bolaShape.m_radius = 1.0f;
FixtureDef bolaFixDef = new FixtureDef();

118

Manual do Indie Game Developer - Verso Android e iOS


bolaFixDef.shape = bolaShape;
bolaFixDef.density = 1.0f;
bolaFixDef.restitution = 0.6f;
bolaFixDef.friction = 0.1f;
bola.createFixture(bolaFixDef);
bola.resetMassData();
bola.setUserData(bolaTamanho);
bolas[ix] = bola;
}

// *** Criamos a juno entre cada par de bolas:

for (int ix = 0; ix < (numBolas - 1); ix++) {
DistanceJointDef jointDef = new DistanceJointDef();
jointDef.initialize(bolas[ix], bolas[ix+1], bolas[ix].
getWorldCenter(),

bolas[ix+1].getWorldCenter());
jointDef.collideConnected = true;
DistanceJoint juncao = (DistanceJoint) world.
createJoint(jointDef);
}

E para renderizar tambm usamos loops:


// Desenha as bolas:

gx.setColor(Color.cyan);
for (int ix = 0; ix < numBolas; ix++) {
Retangulo rect = criarRetangulo(bolas[ix]);
gx.drawOval(Math.round(rect.x), Math.round(rect.y),

Math.round(rect.width), Math.round(rect.width));
}

// Desenha as junes:

gx.setColor(Color.yellow);
for (int ix = 0; ix < (numBolas - 1); ix++) {
Vec2
cdCorpoA
=
normalizarCoordenadas(bolas[ix].
getWorldCenter());
Vec2 cdCorpoB = normalizarCoordenadas(bolas[ix+1].
getWorldCenter());
gx.drawLine((int)cdCorpoA.x, (int)cdCorpoA.y, (int)
cdCorpoB.x,

Captulo 5 - Fsica de games 119



}

(int)cdCorpoB.y);

E, ao teclar a barra de espao, eu escolho aleatoriamente em qual das bolas


ser aplicada a fora:
int num = random.nextInt(9);
Vec2 forca =

new Vec2(2000.0f, 2000.0f * bolas[num].getMass());

Vec2 posicao = bolas[num].getWorldCenter();
bolas[num].setAwake(true);
bolas[num].applyForce(forca, posicao);

Assim, criamos um efeito muito legal, parecido com o que acontece no


jogo World of Goo, por exemplo (www.worldofgoo.com).
Simulando destruio
Bem, as junes so muito interessantes. Os efeitos que podemos criar so
incrveis mesmo, por exemplo, podemos simular a destruio de um objeto,
como uma parede. Podemos destruir um objeto de vrias maneiras:
1. Separando as fixtures: criamos um objeto com mltiplas fixtures
e, ao ser atingido, separamos as fixtures e colocamos em outros objetos, para simular fragmentos;
2. Separando os objetos: criamos vrios objetos agregados (pode ser
por juntas) e, ao ser atingido, ns os separamos;
Eu optei pela segunda abordagem neste exemplo: WeldJoint (...\Codigo\
WeldJoint\weldjoint.zip).

Ilustrao 48: Simulando a quebra de uma parede

120

Manual do Indie Game Developer - Verso Android e iOS

Eu criei um ambiente com alguns objetos:


Bola: dinmico, com densidade 5;
Rampa: esttico, um tringilo com shape poligonal os vrtices tem
que ser no sentido horrio, pois estamos lidando com y invertido;
Quadrado: dinmico. Seis deles unidos por Weld joints;
Ao ser acionada, a bola avana, sobe pela rampa e colide com a parede,
quebrando-a completamente. Rode o exemplo, clique com o mouse e tecle
barra de espao.
Neste exemplo, eu tive que detetar colises, logo, peguei a mesma classe
do exemplo ContactBox2D para isto:
class Contato implements ContactListener {
@Override
public
void
beginContact(org.jbox2d.dynamics.
contacts.Contact c) {
DadosCorpo corpoA = (DadosCorpo) c.getFixtureA().getBody().
getUserData();
DadosCorpo corpoB = (DadosCorpo) c.getFixtureB().getBody().
getUserData();
if (corpoA.id >=4 && corpoB.id >= 4) {
atingiu = true;
if (corpoA.id >=5) {
tijoloAtingido = tijolos[corpoA.id - 5];
}
else if (corpoB.id >= 5) {
tijoloAtingido = tijolos[corpoB.id - 5];
}
}
}

Eu armazeno no userData de cada corpo uma instncia de classe minha


(DadosCorpo), que tem o tamanho e um identificador. Os tijolos so criados
todos com identificao a partir de 5 (5,6,7...). Ento, para saber qual o ndice do tijolo, s subtrair 5 do identificador. Ento, eu guardo o tijolo que foi
atingido, de modo a quebrar suas junes.
Eu tenho que adicionar o meu Listener ao mundo Box2D:
// Adiciona o contact listener
Contato c = new Contato();
world.setContactListener(c);

Captulo 5 - Fsica de games 121

Eu crio os tijolos e os outros objetos da mesma forma que venho fazendo.


A criao da parede muito semelhante criao do verme do exemplo
anterior:
// Vamos criar a cadeia de tijolos

tijolos = new Body[numTijolos];

for (int ix = 0; ix < numTijolos; ix++) {
BodyDef tijoloDef = new BodyDef();
tijoloDef.position.set((larguraMundo / 5) * 4,
8 * (ix) + 5.0f);
Body tijolo = world.createBody(tijoloDef);
tijolo.setType(BodyType.DYNAMIC);
Vec2 tijoloTamanho = new Vec2(5.0f, 5.0f);
FixtureDef tijoloFixDef = new FixtureDef();
PolygonShape tijoloshape = new PolygonShape();
tijoloshape.setAsBox(2.5f, 2.5f);
tijoloFixDef.shape = tijoloshape;
tijoloFixDef.density = 4.0f;
tijoloFixDef.restitution = 0.6f;
tijoloFixDef.friction = 0.1f;
tijolo.createFixture(tijoloFixDef);
tijolo.resetMassData();
DadosCorpo tijoloCorpo = new DadosCorpo(tijoloTamanho,5
+ ix);
tijolo.setUserData(tijoloCorpo);
tijolos[ix] = tijolo;
}

// *** Criamos a juno entre cada par de tijolos:

for (int ix = 0; ix < (numTijolos - 1); ix++) {
WeldJointDef jointDef = new WeldJointDef();
jointDef.initialize(tijolos[ix], tijolos[ix+1], tijolos[ix].
getWorldCenter());
jointDef.collideConnected = true;
world.createJoint(jointDef);
}

122

Manual do Indie Game Developer - Verso Android e iOS

Porm, na hora de renderizar a parede, eu preciso saber quais juntas ainda


existem:
// Desenha as junes:

gx.setColor(Color.yellow);
for (int ix = 0; ix < (numTijolos - 1); ix++) {
JointEdge je = tijolos[ix].getJointList();
while (je != null) {
Vec2 cdCorpoA = normalizarCoordenadas(tijolos[ix].
getWorldCenter());
Vec2 cdCorpoB = normalizarCoordenadas(je.other.
getWorldCenter());
gx.drawLine((int)cdCorpoA.x, (int)cdCorpoA.y,
(int)cdCorpoB.x, (int)cdCorpoB.y); je = je.next;
}
}

A classe Body tem o mtodo getJointList(), que retorna uma lista encadeada (no uma estrutura Java!) apontando para a lista de junes. A varivel next indica a prxima juno do objeto. O mtodo getJointList()
retorna uma instncia de JointEdge, que um elemento desta lista encadeada. Cada JointEdge possui vrios ponteiros, incluindo o other, que para
o outro corpo da juno.
Resumindo, eu s desenho as linhas de juno enquanto elas existirem.
E, para fechar, quando a bola atinge um quadrado, eu guardo qual foi o
atingido. Depois, no mtodo update(), eu removo uma de suas junes:
if (atingiu) {
if (tijoloAtingido != null) {
Joint junta = tijoloAtingido.getJointList() ==
null ? Null :
tijoloAtingido.getJointList().joint;
if (junta !=null) {

world.destroyJoint(tijoloAtingido.
getJointList().joint);

}
}
atingiu = false;
}

Captulo 5 - Fsica de games 123

Note que pode ocorrer coliso vrias vezes, ento, eu tenho que testar se
ainda existem junes naquele objeto.
Se voc rodar a simulao vrias vezes, ver que a parede vai se quebrando
aos poucos, com alguns segmentos ainda inteiros. Isto devido ao fato de eu
ter usado Weld joint. Seno, ela ficaria molenga, como o exemplo do verme.
Se eu no tivesse removido as juntas na coliso, a parede se dobraria e inclinaria, mas os tijolos continuariam juntos.

Usando o Box2D em plataformas mveis


O objetivo deste livro fornecer um conjunto de ferramentas para desenvolvimento de games em plataformas mveis: Android e iOS. Ento, faz todo
sentido vermos como desenvolver prottipos com o Box2D nestas plataformas.

Box2D no Android
Vamos eleger o Android como nossa primeira plataforma, para comear.
Em primeiro lugar, quase tudo que fizemos em Java serve, incluindo o Game
Loop.
Ok. Vamos escolher o nosso exemplo ImageRotation, cuja classe
JogoDeBola simula uma bola sendo chutada em um cenrio com cho e
duas paredes. Eis como o jogo dever rodar no Android:

Ilustrao 49: O jogo rodando no emulador Android

124

Manual do Indie Game Developer - Verso Android e iOS

Antes de mais nada, deixe-me esclarecer que, mesmo sendo feito em Java,
o jogo no pode rodar diretamente no Android. A API de interface de usurio
(GUI) completamente diferente do Javax / Swing, logo, no d nem para
pensar em rodar o jogo como est. Vamos ter que adapt-lo para funcionar
como uma aplicao Android.
O cdigo-fonte do exemplo est em: ...\Codigo\Android\exemplobox2d.zip.
Neste livro, eu pretendo me concentrar apenas nas ferramentas e tcnicas
para desenvolvimento de games para Android e iOS, logo, no faz parte do
escopo ensinar a desenvolver aplicaes para estes dispositivos. Se voc no
conhece programao para Android, eu recomendo duas fontes que podem te
ajudar:
1. Meu outro livro: Mobile Game Jam (www.mobilegamejam.com)
que possui uma boa introduo programao Android;
2. O curso de programao Android do meu portal: The Code Bakers
(http://www.thecodebakers.org);
Para comear, crie um projeto Android. Voc pode utilizar o target que desejar, mas cuidado para no usar caractersticas mais avanadas, pois a maioria dos dispositivos ainda Android 2.3.3 (API 10).
Ns vamos desenhar diretamente na tela, e queremos que a aplicao rode
em tela cheia, na orientao Landscape (deitada). Ento, para comear,
altere a definio da sua Activity, dentro do AndroidManifest.xml, para
acrescentar o atributo android:screenOrientation:
<activity
android:name=com.obomprogramador.box2d.android.exemplo.
MainActivity
android:label=@string/app_name
android:configChanges=orientation
android:screenOrientation=landscape>

Isto determina a orientao da sua Activity na tela e landscape significa Paisagem, ou orientao deitada. E tambm devemos impedir que o
dispositivo pare a Activity ao ser rotacionado. Primeiramente, informamos
o atributo android:configChanges, que informa ao Android que nossa Activity quer ser informada caso o dispositivo mude sua orientao. E temos que
sobrescrever o mtodo onConfigurationChanged():
@Override
public void onConfigurationChanged(Configuration newConfig) {
newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE;
super.onConfigurationChanged(newConfig);
}

Captulo 5 - Fsica de games 125

Se houver mudana de configurao de orientao, ns foramos nossa tela


a ficar em Landscape.
Agora, vamos pensar um pouco: ns vamos desenhar diretamente na tela,
o que no Android significa: View. Ento, vamos criar uma classe derivada
de View. Esta classe implementa o mtodo onDraw(), que invocado
sempre que a View precisa ser regenerada (redesenhada). Ento, parece ser
um bom lugar para desenharmos nossos objetos:
class GameView extends View {
public GameView(Context context) {

super(context);
}
@Override
protected void onDraw(Canvas canvas) {

redesenhar(canvas);
}

}

Uma View precisa ser redesenhada sempre que for considerada invlida,
o que pode acontecer devido a vrios motivos. E ns vamos invalidar nossa
View aps cada atualizao do mundo Box2D. Agora, precisamos informar
nossa Activity que uma instncia de GameView ser o contedo a ser
apresentado na tela. Fazemos isto no mtodo callback onCreate():
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gView = new GameView (this.getApplicationContext());
gView.setOnTouchListener(this);
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
this.getWindow().setFlags(WindowManager.LayoutParams.
FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(gView);
initComponents();
}

O que interessa o cdigo em negrito. Estamos instanciando a nossa classe


GameView, passando para ela o contexto de aplicao do Android. Depois,

126

Manual do Indie Game Developer - Verso Android e iOS

precisamos detectar quando a GameView foi tocada, logo, ns passamos a instncia da Activity para ela (a Activity precisa implementar OnTouchListener.
Depois, ns desabilitamos o ttulo da janela e a marcamos como Fullscreen, logo, nem aquela barra com sinal, hora e bateria aparecer.
Finalmente, usamos o mtodo setContentView() para informa Activity
qual instncia de View fornecer o contedo para a tela. Normalmente, ns
usamos uma view definida em XML (Layout), mas no o nosso caso.
Para terminar, chamamos o nosso conhecido mtodo initComponents(),
que inicializa os principais componentes da animao:
private void initComponents() {
DisplayMetrics displayMetrics = new DisplayMetrics();
WindowManager wm = (WindowManager)

getApplicationContext().getSystemService(Context.
WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(displayMetrics);
int screenWidth = displayMetrics.widthPixels;
int screenHeight = displayMetrics.heightPixels;
alturaImagem = screenHeight;
larguraImagem = screenWidth;
int raioReal = Math.round((larguraImagem * bolaPercentScreen)
/100);
fatorEscalaVisual = raioReal;
Options opc = new Options();
opc.inDither = true;
opc.inScaled = false;
opc.inPreferredConfig = Bitmap.Config.ARGB_8888;
fundo = BitmapFactory.decodeResource(getResources(), R.drawable.
cenario2, opc);
fundo = Bitmap.createScaledBitmap(fundo, larguraImagem,
alturaImagem, true);
imagemBola = BitmapFactory.decodeResource(getResources(),
R.drawable.bola, opc);
imagemBola = Bitmap.createScaledBitmap(imagemBola, (int)(10 *
fatorEscalaVisual),

(int)(10 * fatorEscalaVisual), true);
paint = new Paint();
alturaMundo = alturaImagem / fatorEscalaVisual;

larguraMundo = larguraImagem / fatorEscalaVisual;
initBox2D();
}

Captulo 5 - Fsica de games 127

Calculando o tamanho proporcional dos Game Objects


Eu inicializei a altura e largura da imagem a partir do tamanho real da tela
do dispositivo. Como estamos desenvolvendo para um dispositivo mvel real,
no podemos assumir que a altura e largura da imagem sero fixas, ento,
usamos a classe DisplayMetrics para obter a altura e largura da janela do
dispositivo em pixels. E tambm calculamos o fator de escala visual de maneira relativa ao tamanho da tela. No nosso caso, eu estipulei um percentual
da tela para ser o tamanho do Raio da bola. Assim, a bola aparecer com um
tamanho proporcional em de acordo com o tamanho de cada dispositivo. Caso
contrrio, ela ficaria sempre do mesmo tamanho.
Ento, eu calculo as dimenses do mundo virtual (usando o Fator de escala
visual). Tambm carregamos e reescalamos as duas imagens (fundo e bola),
que devem estar na pasta res/drawable/<densidade> (no meu caso: res/
drawable/mdpi).
Todos os objetos que sero utilizados no desenho, no mtodo onDraw(),
devem ser inicializados previamente. Assim, inicializamos os bitmaps e a configurao de pintura (Paint) que vamos usar.
Preparando os bitmaps antecipadamente
Eu pretendo carregar dois bitmaps: um para o fundo e outro para a bola. E
pretendo reescalar estes bitmaps de acordo com o tamanho fsico da tela do
dispositivo. Para evitar problemas, eu preciso dar um tratamento especial ao
carreg-los. A classe BitmapFactory.Options permite especificar algumas
opes para a carga das imagens:
opc.inDither = true : acrescenta um rudo intencional imagem, evitando erros na converso analgica/digital, como faixas de
cor, por exemplo. importante em imagens que sero reescaladas
pela aplicao;
opc.inScaled = false : se quisermos alterar a densidade (dpi) da
imagem ao carreg-la. Neste caso, desligamos esta opo para no
alter-la;
opc.inPreferredConfig = Bitmap.Config.ARGB_8888 : ele vai
tentar decodificar a imagem utilizando 4 bytes por pixel: RGB +
alpha (transparncia);
Ns usamos o mtodo createScaledBitmap, da classe android.graphics.
Bitmap, de modo a criar uma verso das imagens no tamanho que desejamos.
Isto evita lags na renderizao, pois a operao de reescalar imagens consome muito tempo.

128

Manual do Indie Game Developer - Verso Android e iOS

Para terminar, chamamos o nosso conhecido mtodo initBox2D(), que


EXATAMENTE o mesmo do exemplo ImageRotation da verso Javax /
Swing.
Porm, esta simulao comandada de modo diferente da verso Javax
/ Swing. Ns temos que teclar MENU para iniciar ou parar a simulao e
temos que tocar na tela (no deslizar o dedo, s tocar) para aplicar a fora.
Para isto, tivemos que intercetar o mtodo onKeyUp() da classe Activity:
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
if (!simulando) {
if (world != null) {
initBox2D();
}
simulando = true;
runGameLoop();
}
else {
simulando = false;
gView.invalidate();
timer.cancel();
task = null;
}


return true;
}
else if (keyCode == KeyEvent.KEYCODE_BACK) {
Intent intent = new Intent(Intent.ACTION_
MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}
}

return false;

Ao pressionar e soltar qualquer tecla do Android, o processamento cair


neste callback. Se a tecla MENU foi pressionada, ns iniciamos ou paramos a simulao e se a tecla BACK foi pressionada, ns voltamos para a
Activity HOME do Android.

Captulo 5 - Fsica de games 129

Para saber se a GameView foi tocada pelo usurio, ns usamos o mtodo


onTouch(), da interface OnTouchListener, que nossa Activity implementa. Para usar este recurso, tivemos que habilitar nossa Activity como OnTouchListener da GameView, o que fizemos no onCreate(). Veja como intercetamos o toque na tela:
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {

// Comanda a aplicao de foras
Vec2 forca =
new Vec2(200.0f * bola.getMass(), 200.0f * bola.
getMass());

Vec2 posicao = bola.getWorldCenter().add(new Vec2
(0,3));
bola.setAwake(true);
bola.applyForce(forca, posicao);
return true;
}

O contedo do mtodo praticamente o mesmo utilizado na verso Javax


/ Swing.
O Game Loop o mesmo, exceto por uma pequena diferena no mtodo
que invocado pelo Thread:

public void gameLoop() {


synchronized (this) {

// Um lembrete de que pode haver problemas de
concorrncia
update();
};
this.gView.postInvalidate();
}

Ao invs de chamar o mtodo redesenhar(), ns mandamos nossa GameView se autoinvalidar. Ns no usamos a tcnica de Double buffering
porque resultaria em flicker (piscadas) na execuo do game. Se voc quiser, pode usar uma instncia de SurfaceView, ao invs de View como
sua GameView. E, para evitar problemas de concorrncia, ns no podemos
simplesmente invalidar a view, pois este cdigo est sendo executado por um
Thread diferente do principal e no pode ter acesso s estruturas de dados da

130

Manual do Indie Game Developer - Verso Android e iOS

interface de usurio. O postInvalidade() enfilera um pedido para invalidar a


view, que ser executado pelo Thread principal.
O desenho da tela feito no mtodo redesenhar(), que invocado pelo
onDraw() da nossa GameView:
private void redesenhar(Canvas gx) {

gx.drawBitmap(fundo, 0, 0, null);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setTextSize(14);
if (simulando) {
paint.setColor(Color.BLUE);
gx.drawText(rodando, 10, paint.getTextSize()
+ 3, paint);
}
else {
paint.setColor(Color.RED);
gx.drawText(parado, 10, paint.getTextSize()
+ 3, paint);
}
Vec2 centroImagem = normalizarCoordenadas(bola.
getWorldCenter());
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
Retangulo rect = criarRetangulo(bola);
float angulo = rect.angulo * 57.2957795f;
gx.save();
gx.rotate(angulo, centroImagem.x, centroImagem.y);
gx.drawBitmap(imagemBola, rect.x, rect.y, null);
gx.restore();
}

Primeiramente, desenhamos a imagem de fundo com o mtodo drawBitmap,


do objeto Canvas. Depois, escrevemos uma mensagem indicando se o game
loop est sendo executado ou no. Para desenhar a imagem, precisamos obter
o ngulo em que o corpo da bola est, o que feito no mtodo criarRetangulo() (igual ao da verso anterior), e precisamos transform-lo em graus (1
radiano = 57,2957795 graus). Para mudar o ngulo, fazemos de forma semelhante verso Javax / Swing, ou seja, salvamos a matriz de desenho atual, rotacionamos a imagem, desenhamos e depois restauramos a matriz de desenho.

Captulo 5 - Fsica de games 131

Como aplicar Anti-aliasing na imagem


Quando trabalhamos com games e temos que reescalar imagens, comum nos depararmos com o problema de aliasing, no qual a imagem aparece serrilhada, como nos videogames antigos. Alm de acrescentarmos
Dither ao carregar e reescalar o bitmap, podemos mudar algumas propriedades do objeto Paint para obter um resultado melhor:
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
Porm, tome cuidado, pois ao aplicar o filtro Anti-alias, voc vai acrescentar mais uma etapa ao seu loop de renderizao (a aplicao do filtro no
bitmap), o que pode fazer seu game rodar mais lento. Se voc tiver uma quantidade muito grande de bitmaps, melhor reescalar como eu fiz e no aplicar
filtro na renderizao. No caso do Android, o resultado no melhora muito,
mas no caso do iOS faz toda a diferena.
Ns rotacionamos em torno do centro da imagem, que foi obtido a partir
do centro de massa da bola, em coordenadas globais normalizadas (temos que
inverter o y e aplicar o fator de escala). O mtodo drawBitmap() usa a
imagem e as coordenadas do canto superior esquerdo para desenhar.
Antes de concluir, tive que fazer uma coisa muito importante: lidar com
as pausas. Em um dispositivo mvel, comum o usurio mudar de aplicao
constantemente. Por exemplo, ele pode estar jogando e receber um telefonema. Se o seu programa ficar executando a animao mesmo em pausa, estar
roubando ciclos preciosos de CPU, alm de memria, toa. E o game poder
fazer coisas ruins, sem que o jogador possa interferir. Toda Activity tem dois
mtodos: onPause() e onResume(), que podem ser utilizados para lidarmos com isso. No onPause(), ns pausamos o jogo, e no onResume() ns
continuamos de onde paramos, sem inicializar o mundo Box2D:
@Override
protected void onPause() {
super.onPause();
if (simulando) {

timer.cancel();

task = null;
}
}
@Override
protected void onResume() {

132

Manual do Indie Game Developer - Verso Android e iOS


super.onResume();
if (simulando) {

runGameLoop();
}
}

O resultado de rodar a aplicao em um dispositivo Android pode ser visto


na imagem seguinte. Eu rodei no meu LG Optimus P500 (um pouco antigo,
mas confivel), com CyanogemMod 7 (Android 2.3.4), sem problemas.

Ilustrao 50: A simulao sendo executada em um dispositivo Android real

Cuidado com o emulador Android! O emulador uma mquina virtual, que executada dentro do seu computador, logo, possui uma srie de
restries. Uma delas a capacidade de processamento. Em outras palavras,
sempre que puder, desenvolva seu game rodando em um dispositivo real (d
at para depurar), e no no emulador. Este mesmo programa em um emulador
roda cerca de 1/3 mais lento do que no aparelho e no adianta mudar FPS nem
intervalo de Game loop.

Box2D no iOS
Usar o Box2D no iOS mamo com acar, j que podemos inserir
cdigo C e C++ diretamente nos arquivos-fonte em Objective C. Ento, no
precisamos do JBox2D e podemos baixar diretamente a ltima verso do
Box2D C++ no site: www.box2d.org.
O cdigo-fonte do exemplo est em: ...\Codigo\iOS\Box2DTestbed.zip.

Captulo 5 - Fsica de games 133

Preparando o projeto
Para comear, crie um projeto de aplicao iOS no Xcode. Mais uma vez,
neste livro eu no pretendo ensinar a fazer isto, mas, se quiser, pode ler o meu
outro livro Mobile Game Jam (www.mobilegamejam.com) para ver como
se faz.
Aps baixar e descompactar o Box2D_vX.X.X, copie a subpasta Box2D
para dentro do seu projeto no Xcode. Temos que alterar algumas coisas no
projeto do Xcode. Para comear, os arquivos do Box2D so feitos em C++ e
ns vamos usar elementos definidos neles dentro do nosso cdigo Objective
C, ento, temos que informar ao compilador que nosso cdigo misto. Podemos fazer isto de duas formas: mudar a extenso das implementaes de .m
para .mm, ou mudar a opo do prprio compilador, o que mais fcil. Para
mudar a opo do compilador:
1. Clique na raiz do projeto;
2. Clique em Targets;
3. Selecione a aba Build Settings, na janela do meio;
4. Procure a opo Compile sources as... e mude para Objective-C++;

Ilustrao 51: Como alterar a configurao do cdigo-fonte

Precisamos informar ao compilador onde procurar por arquivos de cabealho .h. Ns usamos a diretiva #import para incluir arquivos header.
Quando queremos usar nossos prprios arquivos, os informamos entre aspas
duplas. Porm, quando queremos usar alguma biblioteca, usamos entre os caracteres < e >. Veja o exemplo:

134

Manual do Indie Game Developer - Verso Android e iOS


#import <UIKit/UIKit.h>
#import B2DTBGraphicView.h

Porm, o cdigo-fonte do Box2D se refere aos seus prprios includes


usando < e >, o que pode ser um problema para us-lo dentro de nosso
projeto. Como o Box2D no uma Library normal e est dentro do nosso
projeto, o compilador no vai encontrar os headers. Ento, temos que adicionar nosso projeto aos locais onde o compilador deve procurar por headers. Para isto, na mesma janela Build Settings, procure a opo Header
Search Paths e acrescente o seguinte texto:
${PROJECT_DIR}/**

Eu sei. Configurar o Xcode um pouco chato, mas o projeto que est no


cdigo-fonte (Box2DTestbed.zip) j est devidamente configurado e voc
pode usar.
Rodando no iOS

Ilustrao 52: A aplicao rodando no Simulador iOS

A aplicao roda muito bem no iOS e a programao basicamente a mesma da verso Android. Vamos nos concentrar nas diferenas.
Para comear, temos que determinar o tipo de projeto. Eu escolhi Universal, logo, ele rodar no iPhone e iPad. Eu criei uma instncia de UIView
chamada B2DTBGraphicView, que dever ser a classe da view nos dois
arquivos de Storyboard: o do iPhone e o do iPad. Para isto, s selecionar
a aba identity e mudar a classe.

Captulo 5 - Fsica de games 135

Eu tenho uma classe View Controller chamada: B2DTBViewController,


que recebe todos os eventos e tambm processa o Game loop. Eu criei dois
loops separados: um Game loop e um Render loop, controlado pela classe
de View (B2DTBGraphicView).
O View controller
Nosso View Controller comea (mtodo viewDidLoad) mudando algumas propriedades na nossa View:
_gview.clearsContextBeforeDrawing = YES;
_gview.vc = self;
UITapGestureRecognizer *umToque =
[[UITapGestureRecognizer alloc]
initWithTarget:self
selector(umToqueNaTela)];
[umToque setNumberOfTapsRequired:1];
[umToque setNumberOfTouchesRequired:1];
[_gview addGestureRecognizer:umToque];

action:@

A propriedade clearContextBeforeDrawing faz com que o buffer de


desenho seja limpo antes do mtodo drawRect, da View, ser invocado.
Depois, eu crio uma referncia direta para o View Controller, e adiciono um
reconhecedor de toque.
Na verso Android, eu solicitava que o usurio pressionasse a tecla
MENU para iniciar o Game loop. Como os dispositivos iOS no possuem
essa tecla, eu simplesmente j comeo com o Game loop rodando e uso o toque para aplicar a fora bola.
O mtodo inicializarVariaveis faz o que o nome diz e inicia o Game
Loop:
- (void) inicializarVariaveis
{
gameRodando = false;
world = nil;
FPS = 30.0f;
raioBola = 5;
bolaPercentScreen = 1.0f;
timeStep = 1.0f / FPS;
updateInterval = timeStep/3;

136

Manual do Indie Game Developer - Verso Android e iOS


velocityIterations = 6;
positionIterations = 2;
// Tela
CGRect screenBounds = [[UIScreen mainScreen]
bounds];
CGFloat screenScale = [[UIScreen mainScreen] scale];
screenSize = CGSizeMake(screenBounds.size.width *
screenScale, screenBounds.size.height * screenScale);
// Escala e janelas:
alturaImagem = screenSize.width;
larguraImagem = screenSize.height;
int
raioReal
=
round((larguraImagem
bolaPercentScreen) /100);
fatorEscalaVisual = raioReal;

UIImage* image = [UIImage imageNamed:@cenario2.


png];
fundo = image.CGImage;
fundo = [self resizeImage:fundo newSize:CGSizeMak
e(larguraImagem, alturaImagem)];
image = [UIImage imageNamed:@bola.png];
imagemBola = image.CGImage;
imagemBola
=
[self
resizeImage:imagemBola
newSize:CGSizeMake(10.0f * fatorEscalaVisual,
10.0f *
fatorEscalaVisual)];
alturaMundo = alturaImagem / fatorEscalaVisual;
larguraMundo = larguraImagem / fatorEscalaVisual;
_gview.tamanhoImagem = CGSizeMake(larguraImagem,
alturaImagem);
_gview.tamanhoMundo = CGSizeMake(larguraMundo,
alturaMundo);
_gview.fatorEscalaVisual = fatorEscalaVisual;
_gview.fundo = fundo;
_gview.bolaImagem = imagemBola;

gameRodando = NO;
[self startGameLoop];

Captulo 5 - Fsica de games 137

Eu comeo inicializando e calculando o timeStep para atualizar o mundo


Box2D. Depois, eu uso o tamanho da tela para calcular o fator de escala visual. A bola sempre ter o raio proporcional largura da tela. Isto faz com que
ela seja menor em iPhones e maior em iPads.
O meu Game loop bem simples e iniciado pelo mtodo startGameLoop:
-(void) startGameLoop
{
if (!gameRodando) {
if (world) {
[self deleteWorld];
}
[self initBox2D];
[_gview startUpdating];
gameRodando = YES;
aTimer = [NSTimer scheduledTimerWithTimeInterval:
updateInterval
target:self
selector:@selector(gameLoop)
userInfo:nil
repeats:YES];
}
}

Eu usei um tipo de mecanismo bem simples, pois o objetivo aqui mostrar


como usar o Box2D. Eu fiz como no Android: criei um disparador que roda de
acordo com a taxa de FPS (frames por segundo) do jogo. No est profissional
ainda, mas serve para este propsito.
O Game loop em s executado pelo mtodo gameLoop:
- (void) gameLoop
{
@synchronized(self) {
world->Step(timeStep, velocityIterations,
positionIterations);
}
}

importante notar o uso da diretiva @synchronized. Eu sincronizei o


acesso a este cdigo, com base no objeto View Controler, de modo a evitar
leituras fantasmas do Render loop, pois o mtodo Step atualiza a posio dos corpos que eu criei no Box2D.

138

Manual do Indie Game Developer - Verso Android e iOS

A criao dos objetos no Box2D


No iOS, ns usamos a verso C++ do Box2D, logo, a sintaxe de criao de
objetos muda um pouco. Vamos ver como eu criei o cho:
// Criamos o chao
b2BodyDef * chaoDef = new b2BodyDef();
chaoDef->position.Set(larguraMundo / 2, 2.5f);
chaoDef->type = b2_staticBody;
chao = world->CreateBody(chaoDef);
float larguraChao = larguraMundo - 2.5f;
b2Vec2 * chaoTamanho = new b2Vec2(larguraChao, 2.5f);
b2PolygonShape * chaoShape = new b2PolygonShape();
chaoShape->SetAsBox(chaoTamanho->x / 2, 1.25f);
b2FixtureDef * chaoFixDef = new b2FixtureDef();
chaoFixDef->shape = chaoShape;
chaoFixDef->friction = 0.5f;
chaoFixDef->density = 10.0f;
chao->CreateFixture(chaoFixDef);

Para comear, todas as classes e estruturas usam o prefixo b2.


Ns informamos o tipo de objeto como uma propriedade da instncia
de b2BodyDef, neste caso: b2_staticBody. Apesar disto, a semntica
a mesma e voc pode conferir tudo no manual do Box2D: http://box2d.org/
manual.pdf.
A classe View
A nossa classe View implementa um Render loop em separado do
Game loop, que iniciado pelo mtodo startUpdating:
- (void) startUpdating
{
updateInterval = 0.03;
aTimer = [NSTimer scheduledTimerWithTimeInterval:
updateInterval
target:self
selector:@selector(timerFired)
userInfo:nil
repeats:YES];
}

O mtodo timerFired disparado a cada 1/FPS segundos:

Captulo 5 - Fsica de games 139


- (void) timerFired
{
[self setNeedsDisplay];
}

Este mtodo simplesmente invalida a view, forando o mtodo drawRect


a ser invocado, e nesse mtodo que eu desenho a situao atual do mundo
Box2D:
- (void)drawRect:(CGRect)rect
{
if (!world) {
// Ainda no tem nada no mundo...
return;
}
@synchronized(vc) {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage (
context,
CGRectMake(0, 0, self.tamanhoImagem.width,
self.tamanhoImagem.height),
fundo);
B2DTBRetangulo * rectBola = [self
criarRetangulo:bola tamanho: CGSizeMake(10.0f,
10.0f)];
CGContextSaveGState(context);
CGFloat sinA = sin(rectBola.angulo);
CGFloat cosA = cos(rectBola.angulo);
CGFloat x = rectBola.x + rectBola.tamanho.width / 2;
CGFloat y = rectBola.y + rectBola.tamanho.height / 2;
CGAffineTransform transform =
CGAffineTransformMake(cosA,sinA,-sinA,cosA,xx*cosA+y*sinA,
y-x*sinA-y*cosA);
CGContextConcatCTM(context, transform);
CGContextDrawImage (context,
CGRectMake(rectBola.x, rectBola.y,
rectBola.tamanho.width,
rectBola.tamanho.height),

140

Manual do Indie Game Developer - Verso Android e iOS

self.bolaImagem);
CGContextRestoreGState(context);

Bem, o que eu estou fazendo no mtodo drawRect a mesma coisa que


fiz no mtodo onDraw, da minha View na verso Android:
1. Desenho o fundo;
2. Calculo o retngulo da bola, com a posio atual devidamente
normalizada;
3. Rotaciono a bola de acordo com o ngulo atual;
Porm, a rotao no Quartz 2D (o mecanismo grfico do iOS que eu estou
usando) mais complexo.
Alterando a matriz de transformao
O Quartz 2D usa uma matriz de transformao, que aplicada a um determinado ponto, de modo a obter sua nova coordenada. Funciona desta forma:
a b 0
[ x ' y '1] = [ xy1] c d 0
t x t x 1
Cada ponto a ser desenhado calculado com base nesta matriz. As coordenadas transformadas so calculadas de acordo com as equaes:
x ' = ax + cy + t x
y ' = bx + dy + t x
A transformao que eu defini foi:
CGAffineTransformMake(cosA,sinA,-sinA,cosA,x-x*cosA+y*sinA,
y-x*sinA-y*cosA);

De acordo com a definio da funo CGAffineTransformMake:


CGAffineTransform CGAffineTransformMake (
CGFloat a,
CGFloat b,
CGFloat c,
CGFloat d,
CGFloat tx,
CGFloat ty
);

Captulo 5 - Fsica de games 141

Ento:
a = cosseno do ngulo da rotao (cos(a));
b = seno do ngulo da rotao (sen(a));
c = -1 x sen(a);
d = cos(a);
tx = x x * cos(a) + y * sen(a);
ty = y x * sen(a) y * cos(a).
Aplicando nas duas equaes:
x = cos(a) * x + (-1 * sen(a)) * y + (x x * cos(a) + y * sen(a));
y = sen(a) * x + cos(a) * y + (y x * sen(a) y * cos(a));
Calma! Antes de jogar o livro pela janela, eu quero dizer que esta uma
receita de bolo, que voc pode usar para qualquer rotao de imagem! Eu
s quero dar uma breve explicao de como isto feito. Se quiser entender a
matemtica por trs desta operao, veja em:
Rotao: http://en.wikipedia.org/wiki/Rotation_(mathematics)
Translao: http://en.wikipedia.org/wiki/Translation_(geometry)
No Android, ns podemos rotacionar o contexto especificando um ponto
de eixo, que pode ser o centro da imagem. No iOS, no existe isso. Temos
que combinar uma rotao com translao. Eu estou deslocando a origem (o
eixo de rotao) para o ponto especificado nas variveis tx e ty, calculado
com base no ngulo que eu desejo rotacionar a imagem. Depois, eu especifico
a rotao que eu desejo aplicar. O ngulo em Radianos, obtido diretamente
do Box2D.
Como eu disse, uma receita de bolo, ou seja, sempre que voc precisar
rotacionar uma imagem em torno do seu centro, use a mesma transformao,
s variando, obviamente, as coordenadas da imagem e o ngulo. Note que o
ponto inicial do retngulo o canto superior esquerdo.
Para concatenar a minha matriz de transformao com a matriz normal do
contexto, eu uso o mtodo:
CGContextConcatCTM(context, transform);
Eu preciso salvar e restaurar a matriz do contexto, caso contrrio, todos os
desenhos sero afetados pela transformao que eu criei. Eu fao isso com os
mtodos:
CGContextSaveGState(context);
CGContextRestoreGState(context);

142

Manual do Indie Game Developer - Verso Android e iOS

Proteo contra concorrncia


Eu estou usando a diretiva @synchronized cercando todos os comandos
de desenho, pois no quero pegar dados incompletos, o que pode acontecer se
o Game loop estiver rodando ao mesmo tempo que o Render loop. Note que
eu estou sincronizando com o mesmo objeto View Controler. Vamos ver o
que aconteceria:
1. O Thread do Game loop obtm o lock do objeto View Controller e comea a atualizar as coordenadas dos corpos no mundo
Box2D;
2. O Thread do Render loop tenta obter o lock do View Controller, de modo a desenhar os objetos nas posies atuais. Ele ser
bloqueado at que o Game loop libere o lock;
O bloco protegido no precisa ser executado pelos dois Threads. O importante que o objeto utilizado para sincronizar seja o mesmo (a mesma
instncia). Assim, cada Thread s entra no cdigo sensvel se conseguir
obter o lock do objeto sincronizador.
Finalmente
Neste exemplo, minha preocupao apenas mostrar como usar o Box2D
em aplicaes iOS. Eu estou utilizando o Quartz 2D para desenhar e mecanismos muito simples para animar a aplicao. Depois, veremos tcnicas mais
avanadas, que permitiro criarmos games melhores.

Captulo 6
Renderizao com
OpenGL ES 2.0
Certamente, voc j ouviu falar sobre OpenGL, e aposto que tambm j
ouviu que muitos games foram desenvolvidos com ele, como:
Doom 3

Half life

Minecraft

Quake

Second Life

StarCraft

Portal
World of
Warcraft

No mundo dos dispositivos mveis, ns temos a especificao OpenGL ES


(OpenGL for Embedded Systems).
Mas o que OpenGL? Quais as vantagens de us-la?
OpenGL uma especificao de API (Application Programming Interface)
multiplataforma, criada pela empresa Silicon Graphics em 1992, e atualmente
mantida pelo grupo Khronos, uma reunio de empresas interessadas em manter e desenvolver padres para tratamento de mdia rica. Fazem parte do grupo
Khronos, entre outras empresas: AMD, Apple, Intel, NVidia, Qualcomm, Samsung entre diversas outras.
Vrias empresas criam produtos que seguem as especificaes do grupo
Khronos, incluindo a especificao OpenGL. Outras criam produtos que renderizam grficos utilizando produtos baseados em OpenGL.
As vantagens de usar OpenGL para desenvolver aplicaes podem ser resumidas em:
1. Uso de uma API padronizada e multiplataforma, o que simplifica o
porte da aplicao;
2. Possibilidade de maior controle sobre a renderizao dos seus
grficos;
3. Usar a acelerao grfica de hardware para melhorar o desempenho
de aplicaes de mdia interativa;
Acelerao grfica de hardware o servio provido pela GPU (Graphics
Processing Unit) do dispositivo. A GPU um Chip especializado em processamento de imagens, que podem aumentar o desempenho de aplicaes

144

Manual do Indie Game Developer - Verso Android e iOS

interativas. O processamento de grficos dividido entre rotinas processadas


pela CPU e rotinas processadas pela GPU, resultando em renderizao mais
suave e com menos lags, especialmente de grficos em 3D.
Algumas pessoas consideram o uso de acelerao grfica de hardware
como a nica vantagem de utilizar OpenGL, porm, as trs vantagens em
conjunto que tornam interessante o investimento em capacitao e utilizao
desta tecnologia.
Existem alternativas ao uso da OpenGL, por exemplo o DirectX, da Microsoft, utilizado em PCs, Smartphones com Windows Phone e no console de
games Xbox.
No Android, ns temos o recurso do Canvas (pacote android.graphics)
para desenhar na tela. Quando desenhamos usando Canvas, no estamos usando o OpenGL ES, o que pode trazer alguns problemas, caso seu game seja
muito dinmico. No iOS, usamos o Quartz com o Core Animation para desenhar em Views, o que tambm pode trazer alguns problemas no caso de games
em tempo real.
A alternativa para quem quer criar games dinmicos, com animaes em
tempo real, utilizar o OpenGL ES.

OpenGL ES
A especificao OpenGL ES foi criada para sistemas embarcados, como
dispositivos mveis e esta API pode ser utilizada tanto em sistemas baseados
em Android como iOS. Atualmente, existem duas verses implementadas nos
dispositivos: OpenGL ES 1.x e OpenGL ES 2.0, com suporte diferenciado:
Android com nvel de API inferior a 8 (Froyo): OpenGL ES 1.x;
Android com nvel de API igual ou superior a 8: OpenGL ES 1.x e
2.0 (em hardware que suporte OpenGL ES 2.0);
iOS verso inferior a 3: OpenGL ES 1.x;
iOS verso igual ou superior a 3: OpenGL ES 1.x e 2.0 (o hardware
tem que ser: iPhone 3GS ou superior ou iPad 2.0 ou superior);
Ambas as verses esto disponveis na maioria dos dispositivos modernos
(Android >= Froyo e iOS >= iPhone 3GS) e so utilizadas at hoje.
A principal diferena entre elas quanto ao Pipeline de renderizao. As
GPUs mais antigas possuam sistemas dedicados para funes especficas, por
exemplo: transformao e iluminao, que podiam ser configurados atravs
das funes da API OpenGL. Para estas GPUs mais antigas, a opo utilizar
a verso 1.x. As GPUs mais modernas possuem processadores de uso geral,

Captulo 6 - Renderizao

com OpenGL ES 2.0


145

que podem executar vrias funes e podem ser programados. Para estas
GPUs, a recomendao utilizar a verso 2.0 (ou 3.0 em breve!)
Neste livro, vamos nos concentrar na verso da OpenGL ES 2.0, que est
presente na maioria dos dispositivos modernos e vamos ver apenas imagens
em 2D, que esto dentro do escopo, porm, os conceitos se aplicam tambm
s imagens 3D.
A API OpenGL ES permite um controle total sobre como queremos que
nossas imagens sejam renderizadas. Podemos at criar programas para sobreamento (Shaders), especificando como a luz incidir sobre os vrtices do
nosso desenho, e estes programas sero executados na GPU, tornando o processamento do Game loop muito mais rpido.
Mas isto tem um custo: complexidade! A API OpenGL tudo, menos fcil
de aprender. complexa, com conceitos diferentes do que estamos acostumados, e a curva de aprendizado longa. A maioria das funes pode ser
feita de diversas maneiras e, dependendo da escolha, teremos melhor ou pior
performance.
Realmente, o uso de OpenGL representa um risco considervel em projetos de games e sua curva de aprendizado no deve ser menosprezada.
Um programador iniciante em plataforma Android consegue criar um programa simples, que desenhe uma imagem em uma view (usando onDraw())
em questo de minutos. Um programador experiente em Android consegue
criar um programa que desenhe a mesma imagem com OpenGL ES em questo de horas!
Afinal, eu devo ou no usar OpenGL no meu game?
Para mim, existem trs vantagens em utilizar OpenGL: API padronizada,
maior controle e uso de acelerao grfica. Devemos considerar as trs vantagens em conjunto. Por exemplo, ao escrever a renderizao de um Game
usando OpenGL ES, fica mais fcil port-lo de Android para iOS, por exemplo, afinal a API est presente em ambas as plataformas. Se eu aprender a usar
OpenGL ES, no preciso estudar detalhadamente os mecanismos de desenho
de cada plataforma.
Agora, as outras vantagens dependem mais da complexidade e dinamismo
do game. Se voc vai criar um game 3D, com certeza deveria investir em
OpenGL ES, porm, mesmo em games 2D, o uso de OpenGL ES pode trazer
os benefcios de padronizao, controle e desempenho esperados.
A maioria dos desenvolvedores de games mveis, tanto Android como
iOS, recomendam o uso do OpenGL ES, ao invs dos mecanismos normais
(Canvas e Quartz).

146

Manual do Indie Game Developer - Verso Android e iOS

Ao usar OpenGL ES, transferimos algum processamento para a GPU, liberando recursos da CPU para outras tarefas, como o clculo de fsica do game,
por exemplo. Porm, no espere que isto resolva todos os problemas de lag
do seu game. Existem atitudes que podem ser tomadas ANTES de considerar
o uso ou no de OpenGL ES, como a criao de um Game Loop consistente, o
gerenciamento eficiente de memria, pr-carga de imagens etc.

Fundamentos
Quando desenhamos com mecanismos nativos, geralmente ns obtemos
um contexto grfico e utilizamos funes da API para desenhar na tela, por
exemplo:
Android: no mtodo onDraw() obtemos um Canvas e usamos o
mtodo drawBitmap();
iOS: no mtodo drawRect:(CGRect)rect obtemos o contexto grfico atual (UIGraphicsGetCurrentContext) e desenhamos usando a
funo CGContextDrawImage;
Daqui para a frente, eu me referenciarei ao motor (engine) grfico OpenGL, logo, vou passar a escrever o OpenGL, para diferenciar da especificao OpenGL ES. O motor uma implementao da especificao OpenGL ES 2.0.
No OpenGL (OpenGL ES 2.0), o processo diferente. Para comear, ns
criamos programas de sombreamento (Shaders), alm de vetores de vrtices e
textura, depois criamos as matrizes de transformao e projeo e mandamos
a GPU renderizar a imagem.
A primeira coisa que temos que aprender como o OpenGL enxerga as
coordenadas das imagens, que um plano cartesiano tridimensional, onde o
eixo das ordenadas cresce do centro para cima, ao contrrio do plano utilizado nas telas dos dispositivos mveis (o y cresce de cima para baixo).

Coordenadas dos vrtices e da textura


Embora seja possvel, ns no vamos desenhar linhas ou figuras geomtricas. Em games, ns utilizamos sprites, que so imagens. Ns precisamos
estabelecer as coordenadas dos cantos da imagem, ou vrtices. Para comear,
vamos imaginar aquele dolo que usamos nos exemplos anteriores. uma
figura simples, um quadrado com desenhos imitando os olhos e um nariz.

Captulo 6 - Renderizao

com OpenGL ES 2.0


147

Ilustrao 53: a imagem que vamos usar

Esta imagem tem 4 vrtices. Independentemente de seu tamanho, podemos


imagin-la centralizada em um plano tridimensional da seguinte forma:

Ilustrao 54: A imagem centralizada no plano cartesiano

148

Manual do Indie Game Developer - Verso Android e iOS

Talvez, voc esteja se perguntando: Mas qual a unidade? Na verdade,


isso no importa. Podemos pensar na unidade que desejarmos. Eu escolhi o
intervalo [-2,2] porque fica fcil de imaginar.
Textura
No OpenGL ES 1.x, o uso de texturas possua uma limitao: todas deveriam ser power of two (POT), ou seja, suas dimenses deveriam ser potncias de 2. O OpenGL ES 2.x suporta texturas NPOT (non-power of two) com
duas condies:
1. Voc deve usar filtro linear;
2. Voc deve usar WRAP MODE: CLAMP_TO_EDGE;
Mesmo assim, j v problemas estranhos acontecerem com texturas, por
exemplo, no meu LG Optimus p500 (Android) eu vejo a textura normal, quando migro a aplicao para outra plataforma ou outro aparelho (como meu
iPAD), vejo a textura com problemas.
O problema acontece quando usamos filtro diferente de linear, ou usamos
MipMaps (veremos mais adiante) e a textura precisa ser reduzida. Alguns
chips grficos conseguem lidar corretamente com texturas NPOT mesmo com
Mipmaps, outros no.
Eu recomendaria que voc sempre utilizasse texturas POT, ou seja, com
tamanhos em potncias de 2 e no precisam ser quadradas, desde que ambas
as dimenses sejam POT. Para facilitar, eis uma tabelinha simples:
22
23
24
25
26
27
28
29
210
211
212

4
8
16
32
64
128
256
512
1024
2048
4096

Cada dispositivo tem um tamanho mximo de textura. Em plataformas Android, podemos usar o seguinte cdigo para verificar isso:
int [] tamanho = new int[1];
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE,
tamanho,0);

Captulo 6 - Renderizao

com OpenGL ES 2.0


149
Log.d(RENDERER, Tamanho Maximo: + tamanho[0]);

No meu dispositivo Android, um Smartphone LG p500, ele retornou o valor 4.096. Mas o que isso quer dizer? Afinal, uma imagem de 64 x 64 pixels
j atinge esse valor... Parece que esta informao o maior tamanho que uma
dimenso de textura pode ter (altura ou largura).
Em dispositivos iOS, podemos usar os comandos:
int tamanhoMaximo;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &tamanhoMaximo);

Nos dispositivos iOS de iPhone 4 para cima, o tamanho mximo 2048 x


2048 pixels.
Diante disto, eu redimensionei as imagens para potncias de 2, por exemplo, os dolos ficaram com: 128 x 128 pixels.
Como vamos usar uma textura em nosso quadrado, precisamos especificar
como ela ser ajustada nele. As coordenadas de textura (s, t) variam de 0 a 1 e
especificam como a imagem ser aplicada a cada vrtice.

Ilustrao 55: As coordenadas de textura

Na verdade, as coordenadas de textura indicam textels ao invs de pixels. Um textel no tem tamanho fsico definido. Um textel a cor que determinado pixel da imagem ter, de acordo com sua posio no mapa de textura.
Os valores das coordenadas s e t da textura precisam ser aplicados aos
vrtices da imagem (de acordo com sua forma). Ento, saberemos qual a parte da imagem que ser mapeada naquele exato vrtice. O OpenGL vai calcular

150

Manual do Indie Game Developer - Verso Android e iOS

qual a cor de cada pixel da imagem baseado na distncia do ponto at os


vrtices. As coordenadas dos vrtices de textura de um objeto OpenGL so
chamadas de u e v, e correspondem s coordenadas s e t. A diferena
que que s e t variam de 0 a 1, e u e v podem variar fora deste intervalo, neste caso as configuraes de wrap de textura influenciaro o resultado.
Se quisermos mapear a textura exatamente sobre os vrtices, usamos coordenadas de textura entre 0 e 1.

Buffers
O OpenGL sempre utiliza buffers para renderizar as imagens e ns tambm
nos comunicamos com ele atravs deste tipo de estrutura. Como vimos anteriormente, temos que criar dois vetores float (no OpenGL, as coordenadas so
sempre nmeros reais): um com as coordenadas do quadrado que queremos
texturizar e outro com as coordenadas de aplicao da textura. Porm, para
passar essas informaes para a GPU, o OpenGL utiliza buffers, que so reas
de memria contnua.
O problema que a especificao muito aberta e podemos passar vrios
tipos de informao dentro de buffers:
Posies: coordenadas de cada vrtice (x, y, z) do nosso polgono;
Cores: a intensidade (entre 0 e 1) de cada cor (RGBA) em cada
vrtice do polgono;
Normais: coordenadas de vetores normais (perpendiculares) a
cada vrtice do nosso polgono. So utilizados para calcular como a
luz vai refletir em cada plano;
Texturas: a cobertura de textura (s, t) em cada vrtice do nosso
polgono;
E tem mais! Podemos compactar mais de uma informao em um mesmo
buffer (Packed Buffers).
Para no complicar, nos exemplos que eu vou mostrar eu uso buffers separados e no especifico as cores nem os normais. Isto porque estou ensinando
o OpenGL como ferramenta para criao de jogos 2D (inicialmente) e estas
duas informaes no so imprescindveis.
Buffer de posies
Para comear, temos que criar um vetor de posies dos vrtices do nosso
polgono, depois, ns o transformaremos em um Buffer. O importante que
este vetor tem que ser criado em uma ordem especfica:
Canto inferior esquerdo;

Captulo 6 - Renderizao

com OpenGL ES 2.0


151

Canto superior esquerdo;


Canto inferior direito;
Canto superior direito;
Vamos ver um exemplo (Android):
static float squareCoords[] = {
-2.0f, -2.0f, 0.0f, // canto inferior esquerdo
-2.0f, 2.0f, 0.0f, // canto superior esquerdo
2.0f, -2.0f, 0.0f, // canto inferior direito
2.0f, 2.0f, 0.0f // canto superior direito


};

Por que nesta ordem? Isto est relacionado maneira como o OpenGL desenha as imagens. Se forem mais de dois vrtices, ele vai desenhar tringulos
(explicaremos mais adiante).
Buffer de textura
Agora, como vamos pintar o quadrado com uma textura, temos que especificar o vetor que indicar como a textura ser aplicada aos vrtices. Veja
um exemplo em Android:
static float textureCoords[] = {




};

0.0f,
0.0f,
1.0f,
1.0f,

1.0f,
0.0f,
1.0f,
0.0f

//
//
//
//

canto
canto
canto
canto

superior
inferior
superior
inferior

esquerdo
esquerdo
direito
direito

A ordem de especificao das coordenadas de textura deve ser sempre


esta e diferente da ordem da especificao das coordenadas de posio da
imagem.
Podemos criar dois tipos de buffer: Client buffers, que ficam residentes
em rea de memria controlada pela CPU, e Vertex Buffer Objects, que ficam residentes em rea de memria controlada pela GPU. Se usarmos Client
buffers, ns teremos que copi-los para a memria da GPU a cada vez que
precisarmos renderizar imagens, o que resulta em mau desempenho. Se os
dados dos vrtices e da textura no mudarem, podemos gerar VBOs (Vertex
Buffer Objects) diretamente na memria controlada pela GPU, evitando este
trfego de dados a todo momento.
Ns criamos buffers remotos utilizando a funo glGenBuffers() e obtemos ponteiros para os VBOs criados, depois, copiamos nossos dados (nossos

152

Manual do Indie Game Developer - Verso Android e iOS

vetores) para dentro dos VBOs com a funo glBufferData(). Para usar o
buffer, s precisamos invocar a funo glBindBuffer().

Programas de sombreamento
A especificao OpenGL ES 2.0 nos permite criar programas de sombreamento (ou Shaders) para serem executados pela GPU. Estes programas
so criados usando a linguagem GLSL (http://www.khronos.org/registry/gles/
specs/2.0/GLSL_ES_Specification_1.0.17.pdf). Para renderizar e texturizar nossa
imagem, precisamos criar pelo menos dois Shaders: Vertex Shader e Fragment Shader.
Os Shaders so invocados no pipeline do OpenGL ES para determinar como
cada vrtice e pixel ser renderizado. Voc pode ver uma imagem do pipeline
do OpenGL no site do grupo Khronos: http://www.khronos.org/opengles/2_X/.
O OpenGL trabalha com comandos primitivos de desenho e dados de formatao de pontos. Os comandos primitivos de desenho, eis os principais:
Points (pontos): uma srie de pontos individuais;
Line Strips (tiras de linhas): uma srie de uma ou mais linhas
concectadas, sendo os vrtices os pontos de ligao;
Line Loops (polgonos): So semelhantes a Line Strips, exceto
que um segmento adicional criado para ligar o ltimo ao primeiro
vrtice;
Triangle strips (tiras de tringulos): uma srie de tringulos
conectados por um lado compartilhado;
Triangle fans (ventilador triangular): semelhante ao Triangle
Strip, s que todos compartilham o vrtice inicial;
Qualquer figura que voc queira desenhar ter que se encaixar em uma das
primitivas. Eu estou usando sempre Triangle Strips, da o ordenamento dos
meus vrtices no buffer.
Bem, aps processar a primitiva, o OpenGL ES vai invocar o Vertex Shader, que serve para mapear a posio de cada vrtice do nosso polgono nas
coordenadas da tela. Ns temos que fornecer um Vertex Shader para que o
OpenGL utilize. Para isto, ns criamos um texto contendo os comandos em
linguagem GLSL, o compilamos e depois o linkeditamos junto com o Fragment Shader.
O Fragment Shader processado mais no final do pipeline e trabalha
sobre cada pixel a ser renderizado, de modo a determinar sua cor, iluminao
e textura. Ele tambm deve ser criado como um texto em GLSL, compilado e
linkeditado junto com o Vertex Shader.

Captulo 6 - Renderizao

com OpenGL ES 2.0


153

Ambos os Shaders se comunicam atravs de parmetros. O Vertex Shader


pode mudar coordenadas, gerando a entrada para o resto do Pipeline. O Fragment Shader pode alterar valores de luz e cor para cada pixel.
Parece complicado, no? E ! Mas permite um controle absoluto sobre o
processo de renderizao do OpenGL.
Vou mostrar o Vertex Shader que eu criei para o exemplo que vou
demonstrar:
uniform mat4 uMVPMatrix;
attribute vec4 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = aTextureCoord;
}

Antes de mais nada, vamos esclarecer os modificadores das variveis:


uniform: declara uma varivel global read only, para uso durante o processamento da primitiva. Neste caso, estamos declarando
uma matriz (veremos adiante) de projeo;
attribute: declara um atributo (parmetro) passado ao Shader
pelo OpenGL. Neste caso, estamos passando um vetor com a posio do vrtice (vec4 um vetor matemtico com quadro campos do
tipo float). Tambm estamos recebendo a coordenada de textura
do vrtice;
varying: um parmetro que ser passado ao Fragment Shader;
A funo main() invocada quando o Shader for executado. Para comear, ns recebemos a posio de um vrtice baseada no modelo, ento, precisamos calcular qual ser a nova posio, baseados na matriz de transformao
(veremos adiante). Ento, fazemos a multiplicao (de matrizes) das coordenadas do vrtice e a matriz de transformao. Isto nos dar a nova posio do
vrtice na tela. A varivel gl_Position a principal sada do Vertex Shader e
ser a coordenada utilizada para renderizar o vrtice (o Vertex Shader calcula
as novas coordenadas de cada vrtice).
Como estamos utilizando uma textura, ento temos que repassar ao Fragment Shader a coordenada de textura deste vrtice. Simplesmente copiamos
nosso atributo para um varying.
Agora, vamos ver o cdigo do Fragment Shader:

154

Manual do Indie Game Developer - Verso Android e iOS


precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {
gl_FragColor = texture2D(sTexture, vTextureCoord);
}

Para comear, declaramos que a preciso do modificador mediump ser


float. O mediump o modificador de preciso utilizado para especificar
dados de fragmentos. Depois, declaramos nosso atributo varying, que foi
gerado no Vertex Shader (a coordenada de textura), e tambm a nossa textura,
representada por uma varivel global read-only do tipo sampler2D. Depois,
simplesmente aplicamos nossa textura coordenada de textura para sabermos
qual ser a cor do fragmento. A varivel gl_FragColor a principal sada do
Fragment Shader.
importante notar que, para os vrtices, o Fragment Shader receber nossas coordenadas de textura, mas para os outros pixels ser uma interpolao
da coordenada de textura naquele determinado ponto.

Matrizes
Apesar do OpenGL utilizar um plano cartesiano tridimensional, as telas
dos dispositivos so 2D, logo, temos que realizar uma transformao nas
coordenadas para criar uma projeo 2D de uma imagem 3D. Ns j falamos
um pouco sobre isso no captulo sobre fundamentos. S para relembrar, vamos repetir a imagem.

Ilustrao 56: Projeo de grfico 3D em tela 2D

Captulo 6 - Renderizao

com OpenGL ES 2.0


155

O OpenGL sempre trabalha com matrizes (da lgebra linear) para calcular
posio, projeo e viso. Ele multiplica as coordenadas de cada vrtice pelo
produto das trs matrizes, de modo a obter a coordenada final.
Eu no pretendo entrar em muitos detalhes sobre matrizes, porm importante notar que todas as matrizes so 4 x 4 e as coordenadas que usamos so
chamadas de coordenadas homogneas, nas quais um vrtice representado
pelo conjunto: (x, y, z, w). O w o divisor dos valores das outras coordenadas, permitindo ajustar a posio relativa dos objetos em profundidade. Ns
no vamos usar diretamente as coordenadas homogneas neste livro, porm,
as matrizes devem prever isso (4 X 4).
No OpenGL, sempre que necessitarmos mover, redimensionar ou girar um
objeto, ns evitamos alterar o vetor de coordenadas. Simplesmente, criamos
uma matriz de transformao do modelo ou matriz modelo, que contm as
primeiras transformaes a serem aplicadas ao nosso modelo (definido pelas
coordenadas de vrtice). Se quisermos manter nosso objeto no mesmo local,
sem alterar tamanho e ngulo de rotao, podemos inicializar a matriz modelo
com a matriz identidade, que no afeta as coordenadas dos vrtices ( a mesma coisa que multiplicar por 1).
Alm da matriz modelo, existe a matriz de viso ou de cmera (view matrix), que indica como o observador ver a renderizao. Como a matriz modelo sempre multiplicada pela de cmera, normalmente tratamos o produto
como Model-View Matrix (matriz de modelo e viso). Para projetar a imagem 3D em um plano 2D (a tela), usamos uma terceira matriz, que chamada
de matriz de projeo, e tambm multiplicada pelo produto das outras duas.
A coordenada final ser calculada pela multiplicao da coordenada do vrtice
pela matriz produto, resultante da multiplicao das trs (modelo, cmera e
projeo).

T x = A x

()

Onde:
x : vetor coluna contendo as coordenadas do ponto;
T : funo de transformao linear;
A : matriz de transformao linear;
Desta forma, o OpenGL calcula as novas coordenadas dos pontos da imagem em seu pipeline, pois ns informamos a matriz combinada (projeo e
cmera) em nosso Vertex Shader:
uniform mat4 uMVPMatrix;
attribute vec4 aPosition;

156

Manual do Indie Game Developer - Verso Android e iOS


attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = aTextureCoord;
}

Note o parmetro uMVPMatrix (M = Model, V = View cmera, P =


Projection). Ele receber a matriz-produto que geramos, aplicando-a s posies dos vrtices.
A matriz de projeo leva em considerao vrios fatores, como: altura e
largura da tela, ngulo de viso, distncias (perto, longe), e a matriz de cmera
considera: o ponto de viso, a direo para cima e o ponto para onde estamos olhando. Ao multiplica-las, temos a matriz de transformao que deve ser
aplicada aos pontos da imagem.
Projeo perspectiva
Para exibir um objeto 3D em um plano 2D, temos que escolher um tipo de
projeo a ser aplicada, conforme expliquei no captulo sobre fundamentos.
Os dois tipos mais utilizados so: projeo perspectiva e projeo ortogrfica
(ou ortogonal).
Na projeo perspectiva, o tamanho visual dos objetos afetado pela distncia, ou seja, objetos (ou arestas) mais distantes aparecem menores que objetos mais prximos.
Para entender melhor como funciona a projeo perspectiva, vamos mostrar o que um Frustum (ou trapzio) de viso.

Ilustrao 57: Medidas utilizadas em projeo 3D

Captulo 6 - Renderizao

com OpenGL ES 2.0


157

O Frustum um slido que parece uma pirmide com a ponta cortada


(um tronco). Todos os objetos cujas coordenadas (aps a transformao) estiverem dentro dele, sero renderizados pelo OpenGL. Vamos ver as principais
medidas:
Coordenadas da cmera (ou do olho): so os valores de x, y, e
z, da localizao da cmera (ou do olho);
Coordenada de cima: So os valores de x, y e z que indicam
o sentido para cima, sob o ponto de vista da cmera. Normalmente, usamos (0,1,0);
Direo do olhar (ou da cmera): so as coordenadas para onde
o vetor do olhar aponta. para onde estamos olhando, neste caso,
diretamente em frente;
Limites da viso: so linhas imaginrias que delimitam a rea visvel. Neste exemplo, esto em linhas pontilhadas;
Campo de viso: a amplitude da viso ou o ngulo formado por
duas linhas de viso contguas. Se os planos forem retangulares,
existir um campo de viso vertical e um horizontal;
Plano perto: um valor de z que determina o plano de viso (a
prpria tela);
Plano longe: um valor de z que determina o limite mximo (em
distncia) de viso;
Frustum: o slido criado entre os limites da viso e os dois planos. Tem a forma de um trapzio. Tudo o que estiver dentro dele, ou
intercetando seus limites, ser renderizado. Tudo o que estiver fora
dele, no ser renderizado;
Na parte inferior da figura, temos um corte longitudinal do trapzio. Note
que os tringulos esto totalmente fora do trapzio, e no sero renderizados.
O crculo maior ser renderizado parcialmente e o pentgono ser ocultado
pelo retngulo.
Um problema muito comum em programao OpenGL a imagem distorcida ou mesmo desaparecida. Neste caso, h grande chance do problema
estar relacionado com alguma das duas matrizes.
Na verdade, estamos utilizando projeo em perspectiva, na qual os objetos mais distantes aparecem menores na tela 2D. Entender como configurar a
projeo (e o frustum) bem complexo. Por exemplo, os planos perto e
longe, representam dois valores do eixo z. O plano perto , na verdade,
a prpria tela. S que as coordenadas de projeo so traduzidas posteriormente para NDC Normalized Device Coordinates, o que gera um pouco de
confuso.

158

Manual do Indie Game Developer - Verso Android e iOS

O OpenGL trabalha internamente com coordenadas normalizadas, ou


NDC (Normalized Device Coordinates). Os valores de cada eixo (x, y e
z) variam de -1 a 1, e o eixo z invertido. Os valores negativos apontam
no sentido do observador. Este tipo de orientao do eixo z chamado de
left handed. Aps todas as transformaes, suas coordenadas sero mapeadas para o padro NDC.
Como ns trabalhamos com os valores do eixo z crescendo na nossa direo (right handed), temos que levar esta diferena em considerao quando especificarmos os valores de perto e longe.

Ilustrao 58: Diferenas entre coordenadas visuais e NDC

Se olharmos apenas para nosso frustum, poderamos bem imaginar que


o plano perto seria 1.0, e o plano longe poderia ser -10.0, certo? Se voc
fizer isto, vai acabar recebendo uma exception. A regra para os valores dos
planos de corte so:
1. Esquerdo tem que ser diferente do Direito;
2. Cima tem que ser diferente de Baixo;
3. Perto tem que ser diferente de Longe;
4. Perto e Longe no podem ser menores que zero;
Bem, se perto e longe no podem ser menores que zero, ento, como
especificamos estes valores, se nossa cmera est apontada na direo do
-z? Basicamente, ns negamos os valores de perto e longe, antes de os
informarmos ao criar a projeo.

Captulo 6 - Renderizao

com OpenGL ES 2.0


159

Lembre-se que as coordenadas normalmente so projetadas a partir da


origem. Assim, um retngulo com centro na origem ser projetado no plano
perto. Quando aumentamos a distncia do plano perto, ampliamos a projeo, da mesma forma que acontece quando aproximamos ou afastamos um
projetor de uma tela.

Exemplo utilizando Android e projeo


perspectiva
Vamos criar um programa para exibir a imagem de dois dolos fixas na tela.
O cdigo-fonte do exemplo est em: ..\Codigo\OpenGLAndroid\openglbasico1.zip.

Ilustrao 59: O resultado do exemplo rodando no Android

Eu modifiquei a imagem do dolo para parecer mais antiga, com uma rachadura direita, e feita de tijolos. Achei que ficou mais game assim. Note
que na figura existem dois dolos: um mais no alto e acima (cor dourada) e
outro mais embaixo e direita (avermelhado), que parece ser menor que o
primeiro, aparentando estar por detrs dele. Na verdade, ambos os dolos so
quadrados com mesmo tamanho, s que o segundo est com coordenada z
menor. Logo, a projeo perspectiva faz com que ele aparea menor que o
primeiro.
Este exemplo no executa animao alguma, apenas exibindo a imagem do
dolo na tela. Veremos como animar imagens mais adiante.

160

Manual do Indie Game Developer - Verso Android e iOS

Antes de mais nada, abra o arquivo AndroidManifest.xml e acrescente a


seguinte linha (em negrito):
<uses-sdk
android:minSdkVersion=9
android:targetSdkVersion=17 />
<uses-feature android:glEsVersion=0x00020000
android:required=true />

Aqui vale uma recomendao: conecte um dispositivo Android, com verso igual ou superior a 2.3, e desenvolva testando diretamente no aparelho.
No use o emulador.
O emulador Android s roda OpenGL, a partir da verso da plataforma
4.0.3 r2, e voc tem que indicar na configurao da AVD que vai utilizar a
GPU do sistema host (o seu desktop). Mesmo assim, ele vai rodar muito lento.
Eu no recomendo.

Entendendo a GLSurfaceView
No ambiente Android, podemos usar OpenGL ES 2.0 em Java (Dalvik)
ou em cdigo nativo, utilizando o NDK. Qual seria a vantagem de programar
diretamente em C++? Desempenho? Pode ser. Mas voc deve lembrar que,
desde a verso Froyo (2.2), o Android utiliza JIT (Just in time) para compilar
classes Dalvik em cdigo nativo, o que acelera muito a execuo das aplicaes. Uma das vantagens seria a utilizao direta de bibliotecas em C++,
como o OpenGL, e outra seria o provvel aumento de desempenho, que s
benfico em aplicaes muito especficas.
Hoje em dia, a maioria dos jogos em Android feita utilizando Java (Dalvik) mesmo, logo, vamos abordar apenas o uso de OpenGL nas aplicaes Java.
O Android disponibiliza algumas classes e interfaces bem teis para o uso
do OpenGL (pacote: android.opengl), entre elas:
GLSurfaceView: uma classe de view dedicada, com Thread prprio
para renderizao, que pode ser acoplada a uma instncia de GLSurfaceView.Renderer, para desenho na tela;
GLSurfaceView.Renderer: interface que determina o comportamento de um renderizador de imagens OpenGL;
GLES20: uma classe de convenincia, que encapsula as funes
C++ do OpenGL, oferecendo mtodos estticos para utilizarmos;
Matrix: uma classe de convenincia para lidarmos com as matrizes
de transformao (projeo e cmera), com vrios mtodos estticos;

Captulo 6 - Renderizao

com OpenGL ES 2.0


161

GLUtils: uma classe de convenincia para lidar com imagens e


texturas;
Para criar uma aplicao Android que use OpenGL, temos que criar trs
classes nossas:
Uma classe de view, derivada de GLSurfaceView;
Uma classe de renderizao, que implemente a interface
GLSurfaceView.Renderer;
Uma classe activity, que instancia as outras duas;
A classe activy muito simples e tem poucos mtodos:
public class OpenGLBasico1Activity extends Activity {
private OpenGLBasico1View mView;

));

@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
mView = new OpenGLBasico1View(getApplication(
}

setContentView(mView);

@Override
protected void onPause() {
super.onPause();
mView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mView.onResume();
}
}

Note que delegamos os eventos de pause e resume para a instncia da


nossa view. O mtodo setContentView() indica para a Activity que o seu
contedo ser fornecido pela nossa view OpenGL.
A nossa classe de view OpenGLBasico1View, igualmente simples, necessitando apenas do mtodo construtor:

162

Manual do Indie Game Developer - Verso Android e iOS


public class OpenGLBasico1View extends GLSurfaceView {
public OpenGLBasico1View(Context context) {

super(context);
setEGLContextClientVersion(2);
setRenderer(new OpenGLBasico1Renderer(context));
}
}

importante especificarmos qual a verso do contexto OpenGL que vamos utilizar, neste caso 2 significa OpenGL ES 2.0. Tambm instanciamos
a nossa classe de renderizao e passamos para a view.
Podemos tambm escolher como ser a renderizao: contnua ou sob
demanda, utilizando o mtodo setRenderMode(). Este mtodo recebe um
parmetro indicando o tipo de renderizao que desejamos. No caso da renderizao contnua (GLSurfaceView.RENDERMODE_CONTINUOUSLY,
default), a nossa classe de renderer ser invocada para renderizar a imagem de
forma contnua (loop). Se escolhermos renderizao sob demanda (GLSurfaceView.RENDERMODE_WHEN_DIRTY), a imagem s ser renderizada
se invocarmos o mtodo requestRender().
Bom, at agora molezinha... A mgica acontece mesmo na classe
de renderizao, que implementa a interface GLSurfaceView.Renderer, que
possui trs mtodos:
abstract void onDrawFrame(GL10 gl) : invocado quando necessrio redesenhar um novo frame, utilizando os VBOs (Vector
Object Buffers), texturas e matrizes;
abstract void onSurfaceChanged(GL10 gl, int width, int height)
: invocado quando o tamanho da imagem mudou, normalmente
quando o dispositivo rotacionado. Aqui, devemos recalcular a matriz de projeo;
abstract void onSurfaceCreated(GL10 gl, EGLConfig config)
: invocado quando a superfcie criada. Neste momento, ns inicializamos buffers, carregamos texturas e realizamos operaes de
inicializao que exigem um contexto OpenGL, como a compilao
de Shaders;
Bem, e agora? O que faremos? O trabalho praticamente todo feito na
nossa classe de renderizao. Eu sigo esta lista:
1. Criar os vetores de vrtices e textura, tomando cuidado com a orientao dos elementos;

Captulo 6 - Renderizao

com OpenGL ES 2.0


163

2. Criar o cdigo-fonte dos Shaders (Vertex e Fragment), colocando-os


dentro de Strings (ou ento carregando de arquivos);
3. Criar o construtor e nele transformar os vetores em ByteBuffers,
alm de carregar a imagem que servir de textura. Estas operaes
so basicamente feitas usando as APIs do Android e do Java, logo,
no h necessidade de um contexto OpenGL;
4. Compilar ambos os Shaders e criar um programa contendo ambos, que ser executado pela GPU. Isto feito no mtodo
onSurfaceCreated();
5. Fazer o binding (apontar) entre campos da nossa classe e os atributos dos Shaders no mtodo onSurfaceCreated(). Assim, teremos
como enviar informaes para o processamento dos vrtices e dos
pixels;
6. Transformar a imagem que carregamos em uma textura OpenGL, no
mtodo onSurfaceCreated();
7. Criar a matriz de cmera que vamos usar, no mtodo
onSurfaceCreated();
8. Criar a matriz de projeo no mtodo onSurfaceChanged();
9. Desenhar a imagem no mtodo onDrawFrame();
No teremos um loop de animao, apenas a renderizao pura e simples
da imagem.

A implementao
Vrtices e textura
A primeira coisa criar os vetores de vrtices e textura:
static float squareCoords[] = {
-2.0f, -2.0f, 0.0f, // canto inferior esquerdo
-2.0f, 2.0f, 0.0f, // canto superior esquerdo
2.0f, -2.0f, 0.0f, // canto inferior direito
2.0f, 2.0f, 0.0f // canto superior direito
};

static float squareCoords2[] = {
0.0f, -4.0f, -1.0f, // canto inferior esquerdo
0.0f, 0.0f, -1.0f, // canto superior esquerdo
4.0f, -4.0f, -1.0f, // canto inferior direito
4.0f, 0.0f, -1.0f // canto superior direito
};

164

Manual do Indie Game Developer - Verso Android e iOS


static float textureCoords[] = {
0.0f, 1.0f,
// canto
0.0f, 0.0f,
// canto
1.0f, 1.0f,
// canto
1.0f, 0.0f
// canto
};

superior
inferior
superior
inferior

esquerdo
esquerdo
direito
direito

Temos 3 coordenadas para cada vrtice (x, y, z) de nossos dois dolos. Um


dolo ser formado pelas suas coordenadas de vrtice, suas coordenadas de
textura e sua imagem. As coordenadas assumem que o centro do quadrado
est no ponto de origem (0, 0, 0). As coordenadas de textura informam como
a imagem cobrir o quadrado. A ordem dos vrtices e das coordenadas de
textura so pr-determinadas e deve ser desta forma, para que o modo de renderizao que voi usar (Triangle Strips) funcione corretamente.
Mas, se vamos criar dois dolos, com texturas e posies diferentes, no
deveramos ter dois vetores de textura? No. O vetor de texturas diz como a
imagem cobrir os vrtices, se for igual para as duas figuras, ento no precisamos de dois vetores de textura.
Escrever os Shaders
Eu no coloquei o cdigo-fonte dos Shaders em arquivos, mas poderia ter
feito isto. Eu simplesmente criei dois Strings:
private final String vertexShaderSource =
uniform mat4 uMVPMatrix;\n +
attribute vec4 aPosition;\n +
attribute vec2 aTextureCoord;\n +
varying vec2 vTextureCoord;\n +
void main() {\n +
gl_Position = uMVPMatrix * aPosition;\n +
vTextureCoord = aTextureCoord;\n +
}\n;
private final String fragmentShaderSource =
precision mediump float;\n +
varying vec2 vTextureCoord;\n +
uniform sampler2D sTexture;\n +
void main() {\n +
gl_FragColor = texture2D(sTexture,
vTextureCoord);\n +
}\n;

Captulo 6 - Renderizao

com OpenGL ES 2.0


165

Variveis auxiliares
Note que eu declarei algumas variveis que so ponteiros ou matrizes,
como:
private int mTextureID : identificador (handler) da textura no
OpenGL;
private FloatBuffer verticesQuadrado : buffer local para receber o
vetor de vrtices;
private FloatBuffer texturaQuadrado : buffer local para receber o
vetor de coordenadas de textura;
private int[] buffers = new int[2] : identificadores dos dois VBOs
que criaremos na GPU;
private float[] matrizProjecao = new float[16] : nossa matriz de
projeo;
private float[] matrizCamera = new float[16] : nossa matriz de
cmera;
private float[] matrizModelo = new float[16] : matriz normal do
modelo, ou seja, onde aplicamos movimento e rotao. Neste exemplo, ela sempre a matriz identidade, pois no desejamos mudar a
posio do objeto;
private float[] matrizIntermediaria = new float[16] : usada para
calcular a matriz final, que a multiplicao das outras;
private int programaGLES : identificador do programa Shader que
criamos. Ele contm o Vertex e o Fragment Shader compilados e
linkeditados;
private int muMVPMatrixHandle : identificador da matriz de
transformao que criamos e vamos passar para o OpenGL;
private int maPositionHandle : identificador do VBO de vrtices;
private int maTextureHandle : identificador do VBO de textura;
Inicializao dos buffers locais e carga da imagem
Sugiro que voc leia sobre as classes do pacote java.nio, especialmente
as classes ByteOrder, ByteBuffer e FloatBuffer, ou ento veja os exemplos de OpenGL do Android (http://developer.android.com/training/graphics/
opengl/shapes.html).
feita no construtor da classe. Primeiro, inicializamos um ByteBuffer
com o tamanho do vetor de vrtices (nmero de vrtices multiplicado pelo
tamanho de um float). Tambm informamos qual o byte order nativo
que estamos utilizando. Vamos criar dois buffers locais, um para cada dolo:

166

Manual do Indie Game Developer - Verso Android e iOS


// Vamos criar um buffer para passar para o OpenGL (que
em C):
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)

squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
verticesQuadrado = bb.asFloatBuffer();
verticesQuadrado.put(squareCoords);
verticesQuadrado.position(0);

ByteBuffer bb2 = ByteBuffer.allocateDirect(

// (# of coordinate values * 4 bytes per float)

squareCoords2.length * 4);
bb2.order(ByteOrder.nativeOrder());
verticesQuadrado2 = bb2.asFloatBuffer();
verticesQuadrado2.put(squareCoords2);
verticesQuadrado2.position(0);

Fazemos a mesma coisa com o buffer local de coordenadas de textura:


ByteBuffer b2 = ByteBuffer.allocateDirect(

textureCoords.length * 4);
b2.order(ByteOrder.nativeOrder());
texturaQuadrado = b2.asFloatBuffer();
texturaQuadrado.put(textureCoords);
texturaQuadrado.position(0);

Agora, vamos carregar a imagem que servir de textura. Ns usamos as


opes Dither, para criar um rudo aleatrio (j falamos sobre isto), no
queremos que ela seja redimensionada na carga e queremos usar o padro
RGBA, com quatro bytes para cada valor (a opo ARG_4444 gera uma imagem ruim):
Options opc = new Options();
opc.inDither = true;
opc.inScaled = false;
opc.inPreferredConfig = Bitmap.Config.ARGB_8888;
imagemIdolo = BitmapFactory.decodeResource(this.context.
getResources(),
R.drawable.idolo, opc);
imagemIdolo2 = BitmapFactory.decodeResource(this.context.
getResources(),
R.drawable.idolo2, opc);

Captulo 6 - Renderizao

com OpenGL ES 2.0


167

Compilar ambos os Shaders e criar um programa


Esta parte parece complicada, mas praticamente uma receita de bolo.
O local para fazermos isto no mtodo onSurfaceCreated(). Primeiramente,
ns compilamos o Vertex Shader:
int iVertexShader = GLES20.glCreateShader(GLES20.GL_
VERTEX_SHADER);
if (iVertexShader != 0) {
GLES20.glShaderSource(iVertexShader,
vertexShaderSource);
GLES20.glCompileShader(iVertexShader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(iVertexShader, GLES20.GL_COMPILE_
STATUS, compiled, 0);
if (compiled[0] == 0) {
// Deu erro...
Log.e(TAG, Erro ao compilar o Vertex Shader:
);

Log.e(TAG, GLES20.glGetShaderInfoLog(iVertex
Shader));

GLES20.glDeleteShader(iVertexShader);

iVertexShader = 0;

return;
}
}
else {
Log.e(TAG, Erro ao criar o Vertex Shader.);
return;
}

O processo bem mecnico: obter um identificador (handler) de Shader


no OpenGL (glShaderCreate()), adicionar o cdigo-fonte (glShaderSource()),
compilar o cdigo-fonte (glCompileShader()) e verificar o resultado da compilao (glGetShaderiv()). Repetimos o processo para o Fragment Shader.
Com ambos os Shaders compilados, precisamos linkedit-los e criar um
programa na GPU. Ns usaremos o identificador (handler) deste programa
para informar ao OpenGL no momento da renderizao. Eis a criao do programa na GPU:
programaGLES = GLES20.glCreateProgram();
if (programaGLES != 0) {

168

Manual do Indie Game Developer - Verso Android e iOS


GLES20.glAttachShader(programaGLES, iVertexShader);
checkGlError(glAttachShader);
GLES20.glAttachShader(programaGLES, iFragmentShader);
checkGlError(glAttachShader);
GLES20.glLinkProgram(programaGLES);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programaGLES, GLES20.GL_LINK_STATUS,
linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e(TAG, No foi possvel linkeditar o
programa: );
Log.e(TAG, GLES20.glGetProgramInfoLog(programa
GLES));
GLES20.glDeleteProgram(programaGLES);
programaGLES = 0;
return;
}
}
else {
Log.e(TAG, Erro ao criar o Programa.);
return;
}

um procedimento igualmente mecnico:


1. Criamos o identificador (handler) do programa com glCreateProgram();
2. Adicionamos o Vertex Shader compilado, utilizando o seu identificador no mtodo glAttachShader();
3. Adicionamos o Fragment Shader compilado, utilizando o seu identificador no mtodo glAttachShader();
4. Linkeditamos o programa com o mtodo glLinkProgram();
5. Verificamos o resultado com o mtodo glGetProgramiv();
Fazer o binding dos atributos dos Shaders
Para podermos enviar informaes aos Shaders, precisamos atribuir identificadores aos seus atributos e uniforms, que tambm fazemos no mtodo
onSurfaceCreated():
maPositionHandle = GLES20.glGetAttribLocation(programaGLES
, aPosition);
checkGlError(glGetAttribLocation aPosition);
if (maPositionHandle == -1) {
throw new RuntimeException(no localizei o

Captulo 6 - Renderizao

com OpenGL ES 2.0


169
atributo aPosition);
}
maTextureHandle = GLES20.glGetAttribLocation(programaGLES,
aTextureCoord);
checkGlError(glGetAttribLocation aTextureCoord);
if (maTextureHandle == -1) {
throw new RuntimeException(no localizei o atributo
aTextureCoord);
}
muMVPMatrixHandle = GLES20.glGetUniformLocation(programaGLES,
uMVPMatrix);
checkGlError(glGetUniformLocation uMVPMatrix);
if (muMVPMatrixHandle == -1) {
throw new RuntimeException(no localizei o
atributo uMVPMatrix);
}

Se voc voltar e observar o cdigo dos Shaders, ver que ns temos 2


atributos e 1 uniform. Temos o atributo que indica a posio dos vrtices, o
atributo que indica a coordenada de textura e o uniform que indica a matriz
de transformao. Depois destes comandos, ns temos identificadores para
cada um destes atributos e podemos enviar valores para eles.
Transformar a imagem que carregamos em uma textura OpenGL
Ns temos um Bitmap com a imagem do dolo, mas precisamos criar
uma textura na GPU. Isto feito no mtodo onSurfaceCreated():
// Finalmente, vamos criar as texturas
int[] texturas = new int[2];
GLES20.glGenTextures(2, texturas, 0);
mTextureID = texturas[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_
TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);

170

Manual do Indie Game Developer - Verso Android e iOS


GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
TEXTURE_WRAP_S,
GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
TEXTURE_WRAP_T,
GLES20.GL_REPEAT);

GLES20.GL_
GLES20.GL_

GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, imagemIdolo, 0);


imagemIdolo.recycle();
mTextureID2 = texturas[1];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID2);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_
TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_
TEXTURE_WRAP_S,
GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_
TEXTURE_WRAP_T,
GLES20.GL_REPEAT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0,
imagemIdolo2, 0);
imagemIdolo2.recycle();

Primeiro, precisamos criar os identificadores de textura. Depois de criar,


ns indicamos para o OpenGL, que so texturas 2D, e passamos alguns parmetros para elas. O mtodo glTexParameterf() passa um parmetro do tipo
float e o mtodo glTexParameteri() passa um parmetro do tipo int:
GL_TEXTURE_MIN_FILTER = GL_NEAREST : usado para
informar o nvel mnimo de detalhes quando a textura diminuda.
GL_NEAREST indica que deve ser utilizado o elemento de textura mais prximo das coordenadas;
GL_TEXTURE_MAG_FILTER = GL_LINEAR : usado para
informar o nvel de detalhe quando a textura aumentada. GL_LINEAR retora a mdia ponderada dos elementos que esto mais prximos das coordenadas;

Captulo 6 - Renderizao

com OpenGL ES 2.0


171

GL_TEXTURE_WRAP_S = GL_REPEAT : o que deve ser feito


se o tamanho da rea for maior que o da textura. GL_REPEAT
permite que eu simplesmente repita a imagem, criando um efeito de
ladrilho. O S para a coordenada inicial da textura;
GL_TEXTURE_WRAP_T = GL_REPEAT : A mesma coisa, s
que para a coordenada final da textura;
Finalmente, transferimos a imagem para a GPU com texImage2D() e
reciclamos o bitmap, j que no vamos necessitar mais dele.
Se voc ficar criando texturas a cada momento eu seu game, importante
apag-las da memria da GPU, quando no estiverem mais em uso. Para isto,
use o mtodo: glDeleteTextures():
int [] texBuffers = {idTextura};
GLES20.glDeleteTextures(1, texBuffers, 0);

Criar a matriz de cmera


Antes de mais nada, vamos ver como projetamos o nosso Frustum de
viso para este exemplo:

Ilustrao 60: Frustum de viso

Precisamos criar uma matriz com o ponto de viso do observador. Ns


vamos utilizar a classe Matrix, do Android, para isto. Ainda no mtodo onSurfaceCreated(), vamos inicializar nossa varivel matrizCamera:
// Vamos posicionar os olhos do usurio atrs do ponto
de origem
float x = 0.0f;
float y = 0.0f;
float z = 7.0f;
// Estamos olhando para a frente
float ox = 0.0f;

172

Manual do Indie Game Developer - Verso Android e iOS


float oy = 0.0f;
float oz = -5.0f;
// Vetor que aponta a direo para cima
float cx = 0.0f;
float cy = 1.0f;
float cz = 0.0f;
Matrix.setLookAtM(matrizCamera, 0, x, y, z, ox, oy,
oz, cx, cy, cz);

As coordenadas dos cmeras so informadas primeiro (0, 0, -7), depois,


informamos o ponto de direo do olhar (0, 0, 0), e, finalmente, o vetor que
aponta para o sentido de cima (0, 1, 0).
Criar a matriz de projeo
O mtodo onSurfaceChanged() ser invocado na primeira vez que a superfcie for criada e, cada vez que for modificada, ou seja, tiver seu tamanho
modificado. No nosso caso, quando o dispositivo for rotacionado. Neste mtodo, temos que considerar os novos valores de altura e largura de tela para
criar uma matriz de projeo. Os planos Longe e Perto devem representar
retngulos de propores semelhantes. Neste caso, criamos uma nova matriz
de projeo, que ser utilizada na gerao da matriz de transformao final:
public void onSurfaceChanged(GL10 gl, int width, int
height) {

// A View foi redimensionada (pode acontecer no incio
e se mudar a orientao)
GLES20.glViewport(0, 0, width, height);
float proporcao = (float) width / height;
final float esquerda = -proporcao;
final float direita = proporcao;
final float baixo = -1.0f;
final float cima = 1.0f;
final float perto = 1.0f;
final float longe = 10.0f;
Matrix.frustumM(matrizProjecao,
baixo, cima, perto, longe);

}

0,

esquerda,

direita,

Captulo 6 - Renderizao

com OpenGL ES 2.0


173

O mtodo glViewPort() indica qual o tamanho da nossa tela e indica


como as coordenadas da imagem devem ser transformadas para ficarem de
acordo com a tela. Depois, criamos uma matriz de projeo informando seis
planos de corte. Dois planos verticais, esquerdo e direito, baseados na proporo entre altura e largura da tela, dois planos horizontais, baixo e cima, e
dois planos verticais baseados na distncia, perto e longe. Os planos esquerdo e direito recebem, respectivamente: -proporo e +proporo, o que serve
para ajustar o sistema de coordenadas quadrado do OpenGL para uma tela
retangular.
Desenhar a imagem
No mtodo onDrawFrame() ns vamos, finalmente, desenhar alguma
coisa na tela. A primeira coisa que temos que fazer limpar a tela e preparar
para desenhar usando nossos Shaders:
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT
COLOR_BUFFER_BIT);
GLES20.glUseProgram(programaGLES);

GLES20.GL_

O mtodo glClear() limpa os buffers informados. Estamos limpando o


buffer de profundidade e o de cor. A cor utilizada para limpar pode ser definida com o mtodo glClearColor(). Como no o utilizamos, a cor (0,0,0,0)
(preto transparente).
E eu tenho que indicar o identificador do programa da GPU que vou utilizar para processar as operaes de sombreamento (Vertex e Fragment). Eu
passo o identificador do programa que eu criei e compilei.
Como vamos usar objetos em planos z diferentes, queremos que os objetos que esto na frente cubram os que esto atrs, logo, habilitamos o teste
de profundidade automtico:
GLES20.glEnable(GLES20.GL_DEPTH_TEST);

Se no fizermos isto, teremos que tomar cuidado com a ordem em que


desenhamos os objetos, que deve ser inversamente proporcional distncia
do observador.
Depois, hora de preparar a textura que vamos usar:
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);

Cada dispositivo de vdeo (GPU) pode ter um determinado nmero de texturas ativas simultaneamente. O OpenGL determina isso atravs de Texture
Units. Neste caso, estou ativando a unidade de textura zero (GL_TEXTURE0) e estou associando o identificador (handler) da minha textura a ela. Isto

174

Manual do Indie Game Developer - Verso Android e iOS

significa que a minha textura ser utilizada na prxima operao de renderizao. So duas texturas diferentes, ento, eu tenho que fazer isto para cada dolo.
Agora, tenho que indicar quais so os buffers que vou utilizar para renderizar os vrtices e aplicar a textura. Lembre-se que eles j esto na GPU, logo,
no passamos ponteiros para buffers locais. Tambm temos que habilitar os
vetores, o que far com que as coordenadas sejam passadas aos atributos dos
Shaders:
// Vrtices:
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
GLES20.glVertexAttribPointer(maPositionHandle, 3,
GLES20.GL_FLOAT, false,12, 0);
GLES20.glEnableVertexAttribArray(maPositionHandle);
// Textura:
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[1]);
checkGlError(glEnableVertexAttribArraymaPositionHandle);
GLES20.glVertexAttribPointer(maTextureHandle, 2,
GLES20.GL_FLOAT, false,

8, 0);
GLES20.glEnableVertexAttribArray(maTextureHandle);

O mtodo glBindBuffer() ativa um VBO da GPU. Como eu tenho os


indicadores (handlers) dentro do meu vetor buffers, s selecionar qual eu
desejo ativar.
Depois, com o mtodo glVertexAttribPointer(), eu indico para a GPU
qual o buffer de coordenadas que eu quero usar, informando o indicador
maPositionHandle. Eu tenho que dar informaes sobre seu contedo, neste
caso: o nmero de coordenadas por vrtice (3), o formato de dados de cada
coordenada (float), se esto normalizadas (entre -1 e 1) e o tamanho total
das coordenadas de cada vrtice (12 bytes, 4 por coordenada). O ltimo argumento o deslocamento desta informao dentro do buffer (no nosso caso,
temos buffers separados, ento zero mesmo). Isto tambm servir para indicar de onde o OpenGL vai retirar o argumento de posio a ser passado para
os Shaders.
Finalmente, com o mtodo glEnableVertexAttribArray(), eu habilito o
vetor que acabei de passar para a GPU.
Depois, eu repito tudo para desenhar o segundo dolo.

Captulo 6 - Renderizao

com OpenGL ES 2.0


175

Agora, o momento de criar nossa matriz de transformao e inform-la


aos Shaders:
Matrix.setIdentityM(matrizModelo, 0);
Matrix.multiplyMM(matrizIntermediaria, 0, matrizCamera,
0, matrizModelo, 0);
Matrix.multiplyMM(matrizIntermediaria, 0,
matrizProjecao, 0, matrizIntermediaria, 0);
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false,
matrizIntermediaria, 0);

Como eu no estou modificando a posio da imagem, estou criando minha matriz modelo com a matriz identidade (no altera nada nas coordenadas
dos pontos). Ento, eu a multiplico pela matriz de cmera e depois multiplico
o resultado pela matriz de projeo, obtendo a matriz de transformao final.
Eu repeti o clculo das matrizes para os dois dolos, embora isto no seja
necessrio neste caso, pois cada dolo usa matriz modelo identidade e as mesmas matrizes de cmera e projeo.
Ento, eu uso o mtodo glUniformMatrix4fv() para atribuir a matriz de
transformao ao uniform uMVPMatrix, que eu defini no Vertex Shader.
Assim, ele vai recalcular os vrtices de acordo com o meu modelo, cmera e
projeo, gerando as novas coordenadas.
E, para concluir, eu invoco o mtodo para desenhar a textura nos vrtices
transformados:
GLES20.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,
squareCoords.length / 3);
checkGlError(glDrawArrays);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

Eu estou usando o modo Triangle Strip para desenhar 4 coordenadas de


vrtices. Note que somente neste momento que eu informo quantos vrtices
sero desenhados.
E, como usei VBOs, tenho que desativar o bind associando a zero.

Concluindo
Terminamos o exemplo! Se rodarmos, teremos a frustrante sensao de ver
imagens quadradas aparecendo na tela.Sem graa.Poderamos ter feito isso

176

Manual do Indie Game Developer - Verso Android e iOS

com muito menos cdigo, no mesmo? Enfim, OpenGL assim mesmo.


por isso que eu disse que a curva de aprendizado muito grande, e acredite:
somente arranhamos a superfcie do que possvel fazer com OpenGL. Entre
as coisas que ficaram de fora esto:
Movimento (rotao, translao);
Efeitos de luz;
Imagens tridimensionais.

Exemplo utilizando iOS e projeo perspectiva


Vamos criar um programa para exibir os dois dolos fixos na tela, como
fizemos com o sistema Android.
O cdigo-fonte do exemplo est em: ..\Codigo\OpenGLiOS\OpenGLIOSBasico1.zip.

Ilustrao 61: O resultado do exemplo rodando no iOS

Este exemplo no executa animao alguma, apenas exibindo a imagem


dos dolos na tela. Veremos como animar imagens mais adiante.

Entendendo o GLKit
O iOS possui um framework chamado GLKit para desenvolver aplicaes baseadas em OpenGL ES. O GLKit inclui: funes, classes e outros tipos
de dados que facilitam o desenvolvimento de aplicaes mveis utilizando
o OpenGL ES. claro que voc no precisa usar o GLKit, mas altamente
recomendvel que o faa.

Captulo 6 - Renderizao

com OpenGL ES 2.0


177

A documentao completa sobre o GLKit pode ser lida em:

http://developer.apple.com/library/ios/#documentation/GLkit/Reference/GLKit_
Collection/Introduction/Introduction.html

As caractersticas mais importantes do GLKit que vamos usar so as classes: GLKViewController, GLKView, GLKBaseEffect e GLKTextureInfo.

GLKViewController
O GLKViewController possui vrias propriedades e mtodos interessantes
e j implementa um loop de renderizao, inicialmente baseado em 30 FPS, o
que pode ser mudado atravs da propriedade: preferredFramesPerSecond,
limitado a 60 FPS.
Ele possui alguns mtodos (delegados de GLKViewControler ou de
GLKView) que devemos sobrescrever, como:
- (void)viewDidLoad : devemos inicializar o contexto OpenGL,
indicar a nossa GLKView e carregar todas as informaes invariantes, como: VBOs, texturas etc;
- (void)dealloc : liberamos nossas estruturas alocadas na GPU
(VBOs, programa etc), e o prprio contexto OpenGL;
- (void)update : devemos atualizar nosso modelo pois um novo
frame ser renderizado;
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect :
devemos renderizar o frame;
Podemos usar o GLKViewController de duas maneiras: instanciando diretamente a classe e interceptando os mtodos em um delegate, ou criar
uma subclasse. Eu prefiro este ltimo mtodo, que utilizado pelo template
fornecido no Xcode (OpenGL ES Game). Alis, este template no cria uma
subclasse de GLKView, usando o View Controller como delegate dela.
GLKBaseEffect
Esta classe j fornece os Shaders necessrios e permite usarmos at duas
texturas em uma operao de desenho. Ela poupa muito trabalho, porm, se
quisermos, podemos fazer da mesma forma que o Android e gerarmos nosso
prprio programa e associarmos as texturas manualmente.
GLKTextureInfo / GLKTextureLoader
A classe GLKTextureLoader pode carregar texturas a partir de imagens em
arquivos, gerando instncias de GLKTextureInfo. Isto poupa todo o trabalho
de carga e definio de texturas.

178

Manual do Indie Game Developer - Verso Android e iOS

Criando uma aplicao OpenGL ES


A maneira mais simples de usar OpenGL no iOS criar uma aplicao
utilizando o template fornecido no Xcode (4.x ou superior). Para isto, abra o
Xcode e crie uma nova aplicao selecionando OpenGL Game na janela de
seleo de templates.

Ilustrao 62: Como usar o template OpenGL no iOS

Aps criar sua aplicao, voc ter as seguintes classes:


XXXViewController;
XXXAppDelegate;
main;
Se voc mandar executar a aplicao no Simulador, ver que aparecem
dois cubos rodando. Muito bacana, porm um problema: se voc quer apenas estudar, pode ser que a aplicao gerada pelo template seja til, mas, se
voc quer usar a aplicao gerada para criar outra aplicao, tem muito lixo
a ser removido.A Apple deveria criar um template OpenGL mais bsico, talvez renderizando apenas um cubo ou tringulo, com uma textura simples.
Bem, de qualquer forma, voc poder usar a aplicao exemplo (que vou mostrar) como seu prprio template, j que ela tem o mnimo necessrio: vetor de
vrtices, vetor de textura, criao de VBOs, matriz de cmera e de projeo.
Mesmo assim, o template tem implementaes bem teis:
Inicializa tudo no viewDidLoad;
Tem um mtodo setupGL, que inicializa as estruturas na GPU;
Tem um mtodo tearDownGL, que desaloca tudo;

Captulo 6 - Renderizao

com OpenGL ES 2.0


179

J carrega os Shaders a partir de arquivos, com o mtodo


loadShaders;
Compila e linkedita os Shaders, fazendo o binding dos atributos,
nos mtodos loadShaders e compileShaders;
Atualiza a matriz de modelo e calcula a projeo no mtodo update;
Desenha a imagem no mtodo drawInRect;
Comeamos adicionando a imagem do nosso dolo ao grupo Supporting
Files, e depois vamos customizar a subclasse de GLKViewController que o
template gerou para ns.
Eu recomendo apagar todo o contedo da implementao do View Controler (XXXViewController.m) e seguir o que vou definir adiante.

A implementao
Ns vamos usar o mesmo Frustum do exemplo Android, com as mesmas
configuraes para matriz de cmera e de projeo.
Abra a implementao do View Controller e apague tudo, s deixando at
a linha em negrito:
//
// OGLB1ViewController.m
// OpenGLIOSBasico1
//
// Created by Cleuton Sampaio on 16/01/13.
// Copyright (c) 2013 Cleuton Sampaio. All rights reserved.
//
#import OGLB1ViewController.h
#define BUFFER_OFFSET(i) ((char *)NULL + (i))

Criar nossos vetores de vrtices


As coordenadas de vrtices e de texturas so iguais s do Android:

// As coordenadas do nosso quadrado, que vai receber a


imagem do dolo.
GLfloat squareCoords[12] = {
-2.0f, -2.0f, 0.0f,
// canto inferior esquerdo
-2.0f, 2.0f, 0.0f,
// canto superior esquerdo
2.0f, -2.0f, 0.0f,
// canto inferior direito
2.0f, 2.0f, 0.0f
// canto superior direito
};

180

Manual do Indie Game Developer - Verso Android e iOS


GLfloat squareCoords2[12] = {
0.0f, -4.0f, -1.0f,
// canto inferior esquerdo
0.0f, 0.0f, -1.0f,
// canto superior esquerdo
4.0f, -4.0f, -1.0f,
// canto inferior direito
4.0f, 0.0f, -1.0f
// canto superior direito
};
GLfloat textureCoords[8] = {
// Mapping coordinates for the vertices
0.0f, 1.0f,
// canto superior esquerdo
0.0f, 0.0f,
// canto inferior esquerdo
1.0f, 1.0f,
// canto superior direito
1.0f, 0.0f
// canto inferior direito
};
@interface OGLB1ViewController () {
GLuint _program;
GLKMatrix4 matrizProjecao;
GLKMatrix4 matrizModelo;
GLKMatrix4 matrizCamera;
float _rotation;
GLuint _vertexBuffer;
GLuint _textureBuffer;
}

So exatamente os mesmos vetores que usamos na implementao Android, s que definidos em linguagem C. Na verdade, podemos salvar os vetores em arquivos e carreg-los, o que facilita mais ainda o desenvolvimento
em mltiplas plataformas. Se voc quiser realmente desenvolver games, pode
criar um framework seu, que carregue de arquivos as vrias configuraes do
OpenGL, como:
Coordenadas de vrtices de cada Game Object;
Coordenadas de texturas, associadas a cada Game Object;
Dados do Frustum;
Cdigo-fonte dos Shaders;

Captulo 6 - Renderizao

com OpenGL ES 2.0


181

Isto diminuiria o cdigo-fonte da aplicao e reduziria o risco de divergncias entre as plataformas. O que voc ter que criar o cdigo que l e aplica
as configuraes, de acordo com a verso (Android ou iOS).
Depois, criamos uma extenso de classe para colocar nossas propriedades
e mtodos privados, dentro da implementao (*.m):
@interface OGLB1ViewController () {
GLuint _vertexBuffer;
GLuint _vertexBuffer2;
GLuint _textureBuffer;
GLKMatrix4 matrizCamera;
}
@property
@property
@property
@property

(strong,
(strong,
(strong)
(strong)

nonatomic) EAGLContext *context;


nonatomic) GLKBaseEffect *effect;
GLKTextureInfo * textureInfo;
GLKTextureInfo * textureInfo2;

- (void)setupGL;
- (void)tearDownGL;
@end

Uma extenso de classe nos permite criarmos propriedades e mtodos internos, para uso apenas na implementao do View Controller. Comeamos
definindo variveis para armazenar nossos indicadores (handlers) dos buffers
remotos, alm da nossa matriz de cmera.
Depois, definimos como propriedades: o contexto OpenGL ES, o GLKBaseEffect, e duas variveis GLKTextureInfo para armazenarem nossas texturas.
Lembre-se de sintetizar os getters / setters com @synthesize.
Note que criamos dois mtodos internos: setupGL, que inicializa tudo, e
tearDownGL, que termina tudo.
Inicializando o View Controller
Agora, temos que implementar o mtodo - (void)viewDidLoad e inicializar nosso VC:
- (void)viewDidLoad
{
[super viewDidLoad];

182

Manual do Indie Game Developer - Verso Android e iOS


// Alocamos o contexto OpenGL ES
self.context = [[EAGLContext alloc] initWithAPI:kE
AGLRenderingAPIOpenGLES2];
if (!self.context) {
NSLog(@Failed to create ES context);
}
GLKView *view = (GLKView *)self.view;
view.context = self.context;
[EAGLContext setCurrentContext:view.context];
view.drawableDepthFormat =
GLKViewDrawableDepthFormat24;
self.effect = [[GLKBaseEffect alloc] init];
self.effect.useConstantColor = GL_TRUE;
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}

[self setupGL];

Comeamos alocando nosso contexto OpenGL ES, informando que vamos


utilizar a verso 2.0. Depois, fazemos um cast da view do nosso VC (todo
View Controller tem uma propriedade view), para GLKView. No precisamos nos preocupar se o cast vlido, pois o template Open GL Game j
associa uma instncia de GLKView propriedade do VC.
Associamos o contexto OpenGL ES view e o tornamos corrente. Tambm
especificamos a profundidade de cor que vamos usar para desenhar (24 bits).
Ento, inicializamos nosso GLKBaseEffect e o configuramos para usar as
cores de vrtices constantes, caso contrrio, teramos que especificar o vetor
de cores, e especificarmos a cor de limpeza dos buffers (R, G, B, Alpha), no
intervalo entre 0 e 1.
Depois, invocamos o mtodo setupGL para inicializarmos nossos vrtices e texturas.
Inicializando vrtices e texturas
Precisamos criar nossos buffers de vrtices e carregar nossas texturas, da
mesma forma que fizemos no Android:
- (void)setupGL
{
glEnable(GL_DEPTH_TEST);

Captulo 6 - Renderizao

com OpenGL ES 2.0


183
// Vetor de coordenadas idolo 1:
glGenBuffers(1, &_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(squareCoords),
squareCoords,

GL_STATIC_DRAW);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_
FLOAT, GL_FALSE,
sizeof(GLfloat) * 3, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribPosition);
// Vetor de coordenadas idolo 2:
glGenBuffers(1, &_vertexBuffer2);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2);
glBufferData(GL_ARRAY_BUFFER, sizeof(squareCoords2),
squareCoords2,

GL_STATIC_DRAW);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_
FLOAT, GL_FALSE,
sizeof(GLfloat) * 3, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribPosition);
// Vetor de textura
glGenBuffers(1, &_textureBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoords),
textureCoords,

GL_STATIC_DRAW);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_
FLOAT, GL_FALSE,
sizeof(GLfloat) * 2, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

184

Manual do Indie Game Developer - Verso Android e iOS


// Carregando nossa textura:
// NO iOS temos que desligar a opo para inverter,
pois estamos usando
// o OpenGL para renderizar, com o eixo Y correto, e
no o do CGContext
NSDictionary * options = [NSDictionary
dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO],
GLKTextureLoaderOriginBottomLeft,
nil];
NSError * error;
NSString *path = [[NSBundle mainBundle]
pathForResource:@idolo.png

ofType:nil];
self.textureInfo = [GLKTextureLoader
textureWithContentsOfFile:path

options:options error:&error];
if (self.textureInfo == nil) {
NSLog(@Error loading file: %@, [error
localizedDescription]);
return;
}
NSError *error2;
NSString *path2 = [[NSBundle mainBundle]
pathForResource:@idolo2.png

ofType:nil];
self.textureInfo2 = [GLKTextureLoader
textureWithContentsOfFile:path2

options:options error:&error2];
if (self.textureInfo2 == nil) {
NSLog(@Error loading file: %@,
localizedDescription]);
return;
}

[error

// Vamos posicionar a matriz de viso (Cmera)

Captulo 6 - Renderizao

com OpenGL ES 2.0


185
// Vamos posicionar os olhos do usurio atrs do
ponto de origem
float x = 0.0f;
float y = 0.0f;
float z = 7.0f;
// Estamos olhando para a frente
float ox = 0.0f;
float oy = 0.0f;
float oz = -5.0f;
// Vetor que aponta a direo para cima
float cx = 0.0f;
float cy = 1.0f;
float cz = 0.0f;
matrizCamera = GLKMatrix4MakeLookAt(x, y, z, ox,
oy, oz, cx, cy, cz);
}

O cdigo muito semelhante ao da verso Android. Habilitamos o teste de


profundidade, definimos os VBOs das coordenadas dos vrtices e da textura,
e depois carregamos os arquivos de textura, usando a classe GLKTextureLoader, armazenando as informaes das texturas nas nossas duas propriedades (textureInfo e textureInfo2). Note que tivemos que desligar a opo
GLKTextureLoaderOriginBottomLeft, para evitar que as texturas fossem
carregadas de cabea para baixo (o y carregado invertido).
Finalmente, posicionamos nossa matriz de cmera, exatamente como fizemos no Android.
Cuidado com memory leaks
Se voc carregar vrias texturas dinamicamente, melhor desalocar o buffer, quando no precisar mais dela. Voc pode desalocar as texturas carregadas com a funo glDeleteTextures(). Vamos imaginar que voc no necessite mais da textura1, ento, antes de carregar outra textura, inclua este cdigo:
GLuint hTextura = self.textureInfo.name;
glDeleteTextures(1, &hTextura);

Outra medida importante desfazer o Bind Buffer, aps renderizar alguma coisa:

186

Manual do Indie Game Developer - Verso Android e iOS


glBindBuffer(GL_ARRAY_BUFFER, 0);

Mas isto no apaga o VBO. Se voc no necessitar mais dele, ento


melhor mandar a GPU liberar a memria com a funo glDeleteBuffers():
glDeleteBuffers(1, &_vertexBuffer);

Na verdade, devemos implementar o mtodo dealloc:


- (void)dealloc
{
[self tearDownGL];

if ([EAGLContext currentContext] == self.context) {


[EAGLContext setCurrentContext:nil];
}

E no nosso mtodo tearDownGL, ns nos desfazemos de tudo que


alocamos:
- (void)tearDownGL
{
[EAGLContext setCurrentContext:self.context];
GLuint hTextura = self.textureInfo.name;
glDeleteTextures(1, &hTextura);
hTextura = self.textureInfo2.name;
glDeleteTextures(1, &hTextura);
glDeleteBuffers(1, &_vertexBuffer);
glDeleteBuffers(1, &_vertexBuffer2);
glDeleteBuffers(1, &_textureBuffer);
self.effect = nil;
}

Atualizando o modelo
Uma das coisas legais do GLKViewController que ele j tem um loop de
atualizao e desenho embutido. O default 30 FPS, mas voc pode mudar
isso com a propriedade preferredFramesPerSecond. Quando for a hora de
atualizar o modelo, seu mtodo update ser invocado:
- (void)update
{

Captulo 6 - Renderizao

com OpenGL ES 2.0


187
glViewport(0, 0, self.view.bounds.size.width, self.
view.bounds.size.height);
float aspect = fabsf(self.view.bounds.size.width /

self.view.bounds.size.height);
float
float
float
float
float
float

esquerda = -aspect;
direita = aspect;
baixo = -1.0f;
cima = 1.0f;
perto = 1.0f;
longe = 10.0f;

self.effect.transform.projectionMatrix
GLKMatrix4MakeFrustum(esquerda,

direita, baixo, cima, perto, longe);

Ns no vamos animar os objetos, mas precisamos calcular novamente a


matriz de projeo, pois a tela pode ter sido redimensionada (rotao), ento
o que fazemos. muito semelhante ao mtodo onSurfaceChanged() da
implementao Android.
Desenhando os dolos
O View Controller atua como delegate da GLKView, logo, podemos
implementar o mtodo drawInRect, que ser invocado sempre que a view
necessitar ser redesenhada:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)
rect
{
// Limpar e preparar:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Idolo 1:
self.effect.texture2d0.name = self.textureInfo.
name;
self.effect.texture2d0.enabled = YES;
// Matriz de posicionamento combinada:
GLKMatrix4 matrizModelo = GLKMatrix4Identity;

//

188

Manual do Indie Game Developer - Verso Android e iOS


No estamos fazendo nada com ela...
GLKMatrix4 matrizIntermediaria = GLKMatrix4Multip
ly(matrizCamera, matrizModelo);
self.effect.transform.modelviewMatrix
=
matrizIntermediaria;
[self.effect prepareToDraw];
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glVertexAttribPointer(GLKVertexAttribPosition,
3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, BUFFER_
OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribPositi

on);

glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glVertexAttribPointer(GLKVertexAttribTexCoord0,
2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, BUFFER_
OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glDrawArrays(GL_TRIANGLE_STRIP, 0,
sizeof(squareCoords) / 3);
// Idolo 2:
self.effect.texture2d0.name = self.textureInfo2.
name;
self.effect.texture2d0.enabled = YES;
[self.effect prepareToDraw];
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2);
glVertexAttribPointer(GLKVertexAttribPosition,
3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, BUFFER_
OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribPosition);
glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_
FLOAT, GL_FALSE, sizeof(GLfloat) * 2, BUFFER_OFFSET(0));

Captulo 6 - Renderizao

com OpenGL ES 2.0


189
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glDrawArrays(GL_TRIANGLE_STRIP, 0,
sizeof(squareCoords2) / 3);
}

glBindBuffer(GL_ARRAY_BUFFER, 0);

Ns estamos usando o GLKBaseEffect para gerar os Shaders e controlar


tudo. Ento, para renderizar uma imagem, ns temos que indicar qual ser a
textura a ser utilizada. A propriedade name, da classe GLKTextureInfo,
tem o handler da textura alocada. Para usar uma textura, precisamos informar
isso ao GLKBaseEffect, que permite usar duas texturas simultanemente. Tambm precisamos habilitar o desenho de texturas 2D:
self.effect.texture2d0.name = self.textureInfo.
name;
self.effect.texture2d0.enabled = YES;

Depois, temos que aplicar transformaes ao nosso objeto, como: reposicionar, rotacionar etc. Ns fazemos isso na matriz de modelo. Como no
estamos fazendo nada, simplesmente a inicializamos com a matriz identidade,
multiplicando-a pela matriz de cmera para formar a Model-View Matrix.
Note que informamos isso ao GLKBaseEffect:
GLKMatrix4 matrizModelo = GLKMatrix4Identity;
GLKMatrix4 matrizIntermediaria =
GLKMatrix4Multiply(matrizCamera, matrizModelo);
self.effect.transform.modelviewMatrix =
matrizIntermediaria;

Antes de habilitar os buffers e desenhar, precisamos invocar o mtodo


prepareToDraw:
[self.effect prepareToDraw];

Isto necessrio antes de qualquer desenho e temos que repetir sempre que
alterarmos alguma propriedade do Base Effect.
O resto igual verso Android: atribumos os VBOs e mandamos desenhar. Depois, repetimos tudo para o segundo dolo.

190

Manual do Indie Game Developer - Verso Android e iOS

Achatando as coisas
A projeo perspectiva legal, mas muito mais complexa para trabalhar.
Se voc estiver criando Games 2D (ou mesmo 2.5D, com iluso tridimensional), no precisa disso tudo.
A projeo ortogrfica facilita muito o desenvolvimento. A forma da projeo um paraleleppedo, limitado pelos planos: perto, longe, cima, baixo,
direita e esquerda.

Ilustrao 63: A projeo ortogrfica

Com a projeo ortogrfica, o tamanho dos objetos no afetado por sua


distncia do observador.

Implementao em Android
O exemplo est em: ..\Codigo\OpenGLAndroid\openglbasico2.zip.
Basicamente, mudamos a criao da matriz de projeo, que fica no mtodo onSurfaceChanged():
@Override
public void onSurfaceChanged(GL10 gl, int width, int
height) {

GLES20.glViewport(0, 0, width, height);
float perto = 1.0f;

float longe = 10.0f;

float baixo = -1.0f;

float cima = 1.0f;

float proporcao = (float) width / (float) height;

float esquerda = -proporcao;

float direita = proporcao;

Captulo 6 - Renderizao

com OpenGL ES 2.0


191

Matrix.orthoM(matrizProjecao, 0, esquerda * 5,

direita * 5, baixo * 5, cima * 5, perto,
longe);
}

O mtodo orthoM cria uma matriz ortogrfica e associa nossa varivel


matrizProjecao. Os parmetros so:
A varivel de referncia para a matriz;
O deslocamento dentro da matriz, neste caso zero;
Esquerda e direita representam a posio no eixo das abscissas (x)
dos planos de corte esquerdo e direito;
Baixo e cima representam a posio no eixo das ordenadas (y) dos
planos de corte inferior e superior;
Perto e longe representam a posio, no eixo z dos respectivos
planos de corte;
Para entender o motivo de eu ter especificado aqueles parmetros, temos
que entender uma diferena fundamental entre o mundo OpenGL e uma tela
real. No OpenGL, a tela quadrada, porm, nossas telas geralmente so
retangulares. Logo, preciso compensar a distoro utilizando a razo entre
largura e altura da tela. Como fazemos isso? Bem, temos que especificar 6 planos: esquerda, direita, baixo, cima, perto e longe. Podemos comear calculando a razo entre largura e altura (no importa se a tela est de p ou deitada):
float proporcao = (float) width / (float) height;

Como dividimos a largura pela altura, podemos compensar especificando o


plano esquerdo como: -1 * proporo, e o plano direito com o prprio valor da
proporo. Depois, especificamos o plano superior como 1 e o inferior como
-1. Assim, o OpenGL saber renderizar corretamente nossa imagem.

Ilustrao 64: Nosso ajuste esquerdo / direito / cima / baixo

192

Manual do Indie Game Developer - Verso Android e iOS

Assim, nossos dolos aparecero na proporo correta, como quadrados.


Voc deve ter duas perguntas, certo?
1. Por que eu usei a proporo apenas na esquerda e na direita?
2. Por que eu multipliquei por 5?
A resposta da primeira pergunta simples: eu quero projetar um plano
quadrado em um plano retangular, logo, s posso usar a proporo em um par
de planos. Neste caso, escolhi a esquerda e a direita. Se usasse a proporo nos
quatro planos, estaria projetando em um plano quadrado, gerando distoro.
Se voc quiser usar a proporo nos planos de cima e de baixo, basta dividir a
altura pela largura e no usar a proporo nos planos esquerda e direita.
Para responder segunda pergunta, eu devo lhe solicitar que analise o tamanho do nosso quadrado, pois cada lado tem 4 unidades (o OpenGL no
especifica uma unidade de medida). Eu estou testando em um celular com tela
de 320 x 480 pixels, logo, a imagem ficaria muito grande, pois a proporo
aproximadamente 0,6666... Ao multiplicar por 5, eu fico com a imagem mais
afastada, pois a janela de projeo maior.
Neste exemplo, estamos associando cada unidade do OpenGL ao valor de
5 pixels de tela. Isto no bom... Se usarmos um dispositivo com resoluo
maior, a imagem ficar muito pequena. Ento, como nos prepararmos para
isto? Uma resposta seria calcular o valor de cada unidade em pixels utilizando uma proporo. Podemos especificar que cada unidade OpenGL equivale
a um percentual da tela, depois, calculamos quanto vale este percentual em
pixels e o utilizamos para escalar a posio dos planos.
Feito isto, s rodar o aplicativo e teremos a imagem de dois dolos, com
o mesmo tamanho.

Ilustrao 65: A projeo ortogrfica em Android

Captulo 6 - Renderizao

com OpenGL ES 2.0


193

Implementao em iOS
O exemplo est em: ..\Codigo\OpenGLiOS\OpenGLIOSBasico2.zip.
Ns mudamos a criao da matriz de projeo, que feita no mtodo
update:
- (void)update
{
glViewport(0, 0, self.view.bounds.size.width, self.
view.bounds.size.height);
float proporcao = fabsf(self.view.bounds.size.width
/ self.view.bounds.size.height);
float
float
float
float
float
float

perto = 1.0f;
longe = 10.0f;
baixo = -1.0f;
cima = 1.0f;
esquerda = -proporcao;
direita = proporcao;

self.effect.transform.projectionMatrix =
GLKMatrix4MakeOrtho(esquerda * 5, direita * 5,

baixo * 5, cima * 5, perto, longe);
}

E o resultado ficou muito parecido com o da verso Android.

Ilustrao 66: Projeo ortogrfica no iOS

Captulo 7
Framework de Fsica e
Renderizao
Agora, que j vimos o bsico de OpenGL ES nas duas plataformas
(Android e iOS), chegou o momento de juntarmos o Box2D e criarmos um
exemplo animado. Para simplificar, pretendo usar o mesmo exemplo que j
mostrei: a bola batendo nas paredes. Na prxima figura, vemos o resultado da
execuo do novo programa.

Ilustrao 67: Box2D e OpenGL ES 2.0

E vamos comear com a verso Android, depois fazendo a verso iOS.


As principais diferenas deste exemplo para o anterior so:
1. Teremos duas texturas;
2. Teremos que calcular a posio de acordo com o movimento;
3. Temos que atualizar a matriz de modelo e combin-la com a de projeo e de cmera;
Porm, eu quero fazer diferente...
Por favor, abra o projeto anterior, tanto a verso Android, como a verso
iOS e olhe bem o cdigo. Aproveite, abra o do Box2D tambm. O que voc
v? Para que serve a maioria do cdigo-fonte? Eu vou dar um chute: configurar

196

Manual do Indie Game Developer - Verso Android e iOS

o Box2D e o OpenGL ES! A parte do cdigo dedicada ao processamento da


lgica de negcio muito pequena. Isto o que eu chamo de boilerplate
code (http://en.wikipedia.org/wiki/Boilerplate_code). Eu gosto de traduzir como
cdigo de enchimento de linguia.
Boilerplate code cdigo-fonte cuja funo apenas configurar estruturas e bibliotecas e que pode ser repetido em outras partes da aplicao, ou
em outras aplicaes, com alteraes mnimas. Se voc criar vrios games
OpenGL/Box2D, provavelmente vai repetir os mesmos comandos, com pouca
variao. Alm de tedioso e trabalhoso, o boilerplate code tambm aumenta
o risco do projeto, pois voc pode esquecer algum pequeno detalhe, que s
aparecer quando o game estiver no mercado.
A necessidade desse boilerplate code imenso a Complexidade acidental
das solues OpenGL ES e Box2D, que so extremamente verbosas. E note
que tudo cdigo de configurao, ou seja, pode ser parametrizado.
Sempre que transformamos configurao programtica em declarativa, eliminamos risco de erros, alm do mais, facilitamos a migrao entre as duas
plataformas-alvo deste livro: Android e iOS. O cdigo-fonte est em:
Verso Android (2.3 ou superior): ..\Codigo\OpenGLAndroid\
openglavancado.zip;
Verso iOS: ..\Codigo\OpenGLiOS\OpenGLIOSAvancado.zip;

Um framework bsico
O objetivo deste livro fornecer um kit de ferramentas e tcnicas para
desenvolvedores independentes de games (indie game developers), de modo
a criarem games mveis para plataforma Android e iOS. Ento, minha nfase
facilitar o porte de games entre as duas plataformas, criando cdigo-fonte
parametrizvel externamente.
Ento, neste captulo, eu vou mostrar como integrar o Box2D com o OpenGL ES usando um framework bsico que permita ler todas as configuraes de
um arquivo XML. Com isto, ainda teremos Boilerplate code, mas no vamos
ter que copi-lo, pois vamos usar herana para transmitir o comportamento
para outras classes.
Eu tenho o meu prprio framework, que foi evoluindo ao longo do tempo,
mas acho que o melhor seria comear simples, mostrando como fazer as coisas bsicas e deixar que voc evolua seu framework como achar melhor.
Este framework bsico, podendo e devendo ser estendido por voc, de
acordo com suas necessidades. Eu mesmo vou fazer isso em exemplos posteriores. Desta forma, eu no explicarei cada mtodo do exemplo, nem vou

Captulo 7 - Framework de Fsica e Renderizao 197

mostrar em separado. Se voc ler este captulo e estudar os exemplos, saber


como integrar Box2D com OpenGL ES e tambm como usar este framework
em seus prprios games.
Limitaes
Este framework no completo e nem serve para todos os tipos de game.
Para comear, eu assumo que todas as texturas sero retangulares e s estou
prevendo objetos Box2D com colisores circulares ou retangulares. Se voc
precisar, pode alterar o framework para acomodar outros tipos, ou, ento, escrever cdigo suplementar, por fora do framework.
Vamos ver algumas das configuraes que inclu no framework.

O esquema XML do arquivo de modelo de game


O game autoconfigurvel atravs de um arquivo XML. Neste arquivo, eu
descrevo cada cena do game (entenda como nvel), suas caractersticas fsicas
e visuais, alm dos Game Objects que pertencem a ela.
Assim, posso usar o mesmo cdigo-fonte para criar diversos games.
Eu criei um esquema XML para descrever o arquivo, mas no possvel
validar o XML em todas as plataformas. Eu recomendaria que voc criasse
uma aplicao desktop, para gerar o XML j validado, algo como um game
editor.

Ilustrao 68: O esquema XML

198

Manual do Indie Game Developer - Verso Android e iOS

O arquivo do esquema XML est dentro dos dois projetos deste captulo
(no use o modelo dos captulos seguintes!):
Arquivo ZIP: ..\Codigo\OpenGLAndroid\openglavancado.zip;
Arquivo de esquema: modeloGame.xsd.
Eu criei um modelo de classes baseado no esquema XML, tanto no Android (Java) como no iOS (Objective C). Vamos analisar o diagrama de classes do modelo Android, s para entender o relacionamento dos elementos.

Ilustrao 69: Diagrama de classes

Um GameModel composto por Cenas, que indicam como as vrias cenas (ou nveis) do game devem ser configuradas.
Uma Cena contm:
Nmero: identificador da cena;
Textura de fundo: imagem background, que ser exibida como
fundo de tela (use sempre imagens com tamanho em potncia de 2;
FPS: taxa de frames por segundo para configurar a atualizao do
mundo Box2D;
ConfigFrustum: o nome frustum no apropriado, afinal, eu vou
usar projeo ortogrfica, mas deixei assim mesmo. So as configuraes da cmera e dos planos de corte, para que eu monte as
matrizes de transformao;

Captulo 7 - Framework de Fsica e Renderizao 199

ConfigBox2D: configuraes gerais do controle de fsica do Game;


Objetos: lista de GameObjects que sero criados nesta cena;
O ConfigFrustum contm:
Cmera: coordenadas da posio da cmera (tridimensionais);
Cima: coordenadas do vetor que aponta a direo de cima;
Direo: coordenadas do vetor que aponta a direo do olhar;
Perto: plano de corte mais perto da cmera;
Longe: plano de corte mais distante;
O ConfigBox2D contm:
Gravidade: vetor de direo da fora de gravidade (bidimensional).
Se voc no vai usar, basta zerar;
Sleep: para economizar ciclo de processamento, evitando fazer clculos para objetos em repouso;
Velocity Interations: quantas interaes dos clculos de velocidade
devem ser realizadas;
Position Interations: quantas interaes dos clculos de posio
devem ser realizadas;
Proporo Metro / Tela: este o pulo do gato, que vamos explicar mais adiante. quanto vale 1 metro do Box2D em pixels da
diagonal da tela;
Agora, vamos ver o que um GameObject contm:
Id: identificador do GameObject (no pode ser repetido);
Tipo: o tipo Box2D deste objeto: 1 esttico, 2 dinmico e 3
cinemtico;
Forma: a forma do colisor do objeto: 1 crculo, 2 retngulo;
Alinhamento: se este objeto OpenGL est alinhado a alguma posio da tela: 0 Nenhum, 1 Alinhado ao cho (parte inferior da
tela), 2 Alinhado esquerda, 3 Alinhado direita, 4 Alinhado
ao teto (parte superior da tela);
Centro: coordenadas da localizao do centro do objeto, dentro do
mundo Box2D;
Altura: altura do objeto em metros (Box2D). Lembre-se de manter
objetos dinmicos at 10 metros. Use a proporo metro / tela para
criar escalas dos objetos;
Esticar Altura: se para esticar a altura do objeto (do centro para
cima e para baixo);
Largura: largura do objeto em metros (Box2D);
Esticar Largura: se para esticar a largura do objeto (do centro
para a esquerda e para a direita);

200

Manual do Indie Game Developer - Verso Android e iOS

Arquivo Textura: o nome da textura a ser carregada pelo OpenGL.


Se voc no informar, deve mander os tags, neste caso, o OpenGL
no vai renderizar o objeto, servindo apenas para clculos de fsica;
Densidade: a densidade (Box2D) do objeto;
CoefRetribuio: o coeficiente de retribuio (Box2D) do objeto;
Atrito: o atrito do material do objeto (Box2D);
Agora, abra o exemplo: ..\Codigo\OpenGLAndroid\openglavancado.zip
e abra o arquivo: assets/modelo/modeloExemplo.xml, analise o modelo de
game que eu criei. Para resumir, eu crio uma cena com o mesmo fundo (capim) que usei antes, uma bola, com textura, e quatro retngulos: teto, cho,
parede esquerda e parede direita.

Proporcionalidade dos GameObjects


Um dos maiores problemas de games mveis ajustar as imagens s propores da tela. claro que voc pode criar imagens com DPIs e tamanhos
diferentes, tanto no Android como no iOS, porm, eu acho essa soluo muito
tosca para games.
As texturas sero criadas a partir de imagens e sero redimensionadas de
acordo. Ento, podemos criar uma tcnica para que a imagem de um GO seja
sempre proporcional ao tamanho da tela. J fizemos uma tentativa simples nos
exemplos do captulo anterior e agora o momento de refinarmos esta tcnica.
Podemos pensar em uma tela tamanho padro Android (Baseline), que
tambm serve para iOS, com 320 x 480 pixels. Calculamos quanto vale 1
metro, em pixels, nessa proporo. Veja a prxima figura.

Ilustrao 70: Proporo com relao tela padro

O valor de x a diagonal da tela, que vale (arredondando) 577. Na imagem, temos uma bola com dimetro a. Neste caso, para que a imagem da

Captulo 7 - Framework de Fsica e Renderizao 201

bola seja renderizada com o mesmo tamanho aparente em qualquer tamanho


de tela, temos que calcular o quantas vezes x maior que a.
Para facilitar, vamos imaginar que a signifique 1 metro de medidas do
Box2D, ento, teremos uma referncia para a proporo correta da imagem.
Por exemplo: 1 metro = 5 pontos da tela.
Vamos ver um exemplo prtico. Digamos que eu queira que uma bola com,
1 metro de dimetro, ocupe 1/5 da diagonal da tela, ento, os clculos seriam:
x: diagonal da tela;
a: tamanho de 1 metro em pixels;
p: proporo metro tela;
x / a = c, logo: a = x / c;
Supondo que a tela tenha 320 x 480 pixels, ento a diagonal vale aproximadamente 577, como queremos p = 5, ento:
a = 577 / 5 = 115,40 pixels
Nossa bola teria uma diagonal de 115,40 pixels. Se passarmos para uma
tela de iPad 2, com 768 x 1024 pixels, mantendo a mesma proporo, teramos:
a = 1280 / 5 = 256 pixels
Ns informamos a proporo de 1 metro, com relao diagonal da tela,
na propriedade proporcaoMetroTela, do tag <box2d>, dentro da cena.

Coordenadas e VBOs
Sistemas de coordenadas
Temos que entender como funcionar a integrao entre as coordenadas
Box2D, OpenGL ES e pixels da tela.
Para comear, eu optei por usar um plano cartesiano com origem no centro
da tela, e o valor das ordenadas na posio correta (aumentando para valores
acima de zero). O Box2D usa um plano de coordenadas semelhante, logo, posso tambm utilizar a mesma orientao para o OpenGL, lembrando de mudar
a matriz de projeo para que fique adequada.
Orientao da tela
Antes de mais nada, deixe-me falar sobre a orientao da tela. Em games
de ao, geralmente a orientao da tela landscape, ou seja: deitada. S
que a tela pode ficar deitada de duas maneiras: esquerda ou direita (posio
dos botes no dispositivo). Mudar a orientao pode ser problemtico, se o
seu game prev que os objetos se movam em trs graus de liberdade (movimento livre 2D), pois ao mudar a orientao, voc muda a posio dos objetos
que esto alinhados, tendo que refazer toda esta parte.

202

Manual do Indie Game Developer - Verso Android e iOS

Para simplificar as coisas, eu fixo uma orientao e pronto! O game s


pode ser utilizado naquela orientao. E no sou s eu que fao isto. Porm,
se voc quiser dar a liberdade de mudar a orientao (girar o aparelho), s
recalcular sua projeo e seus objetos dependentes.
Matriz de projeo ortogrfica
Eu vou usar uma matriz de projeo como a da figura seguinte.

Ilustrao 71: A matriz de projeo

Eu sei. A figura confusa. Eu tentei vrias maneiras sem sucesso. Mas vou
tentar explicar: eu tenho uma tela com determinada altura e largura, em pixels.
O game vai ficar muito mais fcil se eu trabalhar minha projeo OpenGL de
acordo com o tamanho da tela, logo, as coordenadas da minha projeo devem
ser relativas a ela. Eis o comando de criao da Matriz, tanto em Android,
como em iOS:
Android:
Matrix.orthoM(matrizProjecao,
Matrix.orthoM(matrizProjecao,
Matrix.orthoM(matrizProjecao,
Matrix.orthoM(matrizProjecao,
Matrix.orthoM(matrizProjecao,

0, -width / 2,
0, width / 2,
0, -height / 2,
0, height / 2,
0, perto, longe);

iOS:
iOS: matrizProjecao = GLKMatrix4MakeOrtho(-larguraViewPort / 2,
larguraViewPort / 2,
-alturaViewPort / 2,
alturaViewPort / 2,
perto, longe);

Captulo 7 - Framework de Fsica e Renderizao 203

Os planos perto e longe so as coordenadas de z que determinam


o que deve ser cortado da viso. Mantive os valores 1 e 10 respectivamente.
Os planos esquerda e direita so, respectivamente, a metade esquerda
da tela e a metade direita. Se sua tela tiver 320 x 480 pixels, na posio landscape, ter: -240 como limite da esquerda e 240 como o da direita.
Os planos cima e baixo tambm so, respectivamente, a metade superior e inferior da tela. Supondo o mesmo exemplo (320 x 480), o limite
superior ser 160 e o inferior -160.
Isto tambm determina a origem das coordenadas (0,0) e a orientao do
eixo das ordenadas (y). A origem ser no centro da tela, se expandindo 160
pixels para cima e para baixo, e 240 pixels para a esquerda e para a direita.
Transformao de coordenadas
Eu tenho que transformar a posio dos centros e o tamanho dos objetos de
coordenadas Box2D para OpenGL, e isto feito atravs da Proporo Metro
/ Tela, que j expliquei. Neste exemplo, estou utilizando o valor 7,0, ou seja,
1 metro vale 1/7 da diagonal da tela em pixels. Para saber quantos pixels o
metro vale, s dividir a diagonal por 7 e usar este valor como fator de escala.
Cada coordenada de centro de GameObject, alm das alturas e larguras,
devem ser multiplicadas por este fator de escala ANTES da renderizao. Assim, transformamos coordenadas Box2D em coordenadas OpenGL. Porm,
em certos casos, h necessidade de transformar coordenadas OpenGL em Box2D (toque e alinhamento de objetos). Neste caso, s dividir.
Vertex Buffer Objects
O VBO de textura sempre fixo, ou seja, a textura cobre todo o retngulo,
logo, eu gero um s VBO de textura e o utilizo sempre que for renderizar
algum objeto.
O VBO de vrtices calculado para cada GameObject que tenha textura,
logo no incio do programa. Como eles no variam, eu os carrego de forma
esttica, gerando um buffer remoto (para a GPU). Eles so calculados a partir
do centro, da largura e da altura do objeto, sempre usando as coordenadas do
mundo Box2D, multiplicadas pelo fator de escala.
No Android, o mtodo protected void carregarCena(int i) carrega o gameModel no OpenGL, e no iOS, o mtodo - (void)carregarCena: (int) numero faz o mesmo.
Primeiro, eles carregam as texturas, depois os vrtices.
Talvez, voc esteja se perguntando: Se as coordenadas dos vrtices so
fixas, como eu farei para mover e girar a bola? Boa pergunta! A entrar a

204

Manual do Indie Game Developer - Verso Android e iOS

matriz de modelo! Nos outros exemplos, nossa matriz modelo era sempre a
identidade, pois no movamos os objetos.

As texturas so carregadas das mesma maneira que fizemos nos outros exemplos.

Movimento e rotao de objetos


As coordenadas dos vrtices so sempre fixas, pois no meu exemplo, s
interessa renderizar se estiverem dentro da projeo. Eu simplesmente fao
uma translao e rotao das coordenadas, de acordo com o que o Box2D me
disser. Neste exemplo, a bola nunca sair da tela, pois est cercada por quatro paredes (teto, cho, esquerda e direita), que eu criei como objetos estticos
no meu modelo, alinhando cada uma ao seu limite correspondente na tela.
Android:
Matrix.setIdentityM(matrizModelo, 0);
float posicaoX = go.getB2dBody().getTransform().position.x *
proporcaoMetroTela;
float posicaoY = go.getB2dBody().getTransform().position.y *
proporcaoMetroTela;
Matrix.translateM(matrizModelo, 0, posicaoX, posicaoY,
0);
Matrix.rotateM(matrizModelo, 0, (float) (go.getB2dBody().getAngle()
* 57.2957795),
0, 0, 1);
Matrix.multiplyMM(matrizIntermediaria, 0,
matrizCamera, 0,
matrizModelo, 0);
Matrix.multiplyMM(matrizIntermediaria, 0,
matrizProjecao, 0,
matrizIntermediaria, 0);

Eu obtenho o valor atual da posio do objeto Box2D que corresponde ao


GameObject que estou trabalhando, uso o mtodo translateM, que cria uma
matriz de translao, para reposicionar o centro na posio indicada. Finalmente, altero a matriz de modelo para incluir o ngulo de rotao (no Android
deve ser informado em Graus).
iOS:
GLKMatrix4 matrizModelo = GLKMatrix4Identity;
float posicaoX = go.b2dBody->GetPosition().x *
proporcaoMetroTela;
float posicaoY = go.b2dBody->GetPosition().y *
proporcaoMetroTela;

Captulo 7 - Framework de Fsica e Renderizao 205


matrizModelo = GLKMatrix4Translate(matrizModelo, posicaoX,
posicaoY, 0.0f);
matrizModelo
=
GLKMatrix4Rotate(matrizModelo,
go.b2dBody>GetAngle(), 0, 0, 1);
GLKMatrix4 matrizIntermediaria = GLKMatrix4Multiply(m
atrizCamera, matrizModelo);
self.effect.transform.modelviewMatrix =
matrizIntermediaria;
self.effect.transform.projectionMatrix =
matrizProjecao;

basicamente o mesmo processo, considerando as diferenas: estou usando o Box2D C++, logo, a sintaxe deve ser levada em conta. A maior diferena
que a funo GLKMatrix4Rotate recebe o ngulo em radianos. Depois, eu
associo o produto das matrizes de cmera e modelo propriedade modelViewMatrix, da instncia de GLKBaseEffect. Por ltimo, associo a matriz
de projeo.

Atualizao do mundo Box2D


E como o Box2D altera as coordenadas?
No Android, eu crio um Thread separado para atualizar o modelo, deixando o Loop de renderizao em seu prprio Thread. Este Thread do Game Loop
invoca o mtodo update, no qual eu comando a atualizao do mundo
Box2D:
protected void update() {
/*

* Atualiza o mundo Box2D e calcula a projeo

*/
synchronized(world) {

world.step(1.0f / cenaCorrente.getFps(),

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());

}
}

por esta razo que estou sincronizando o acesso ao mundo Box2D.


Assim, evito race conditions.
No iOS, eu poderia ter criado um Thread separado tambm, mas usei o prprio Thread de renderizao, j que a classe GLKViewController oferece o
mtodo delegate update:

206

Manual do Indie Game Developer - Verso Android e iOS


#pragma mark - GLKView and GLKViewController delegate
methods
- (void)update
{
@synchronized(self) {
world->Step(1.0f / cenaCorrente.fps,
cenaCorrente.box2d.velocityInterations,
cenaCorrente.box2d.positionInterations);
}
}

Na verdade, por enquanto, a diretiva @synchronized est aqui s por


enfeite. Futuramente, quando eu criar um Thread separado para o Game Loop,
ela ser necessria.

Renderizao do modelo
Como j vimos, no Android a classe GLSurfaceView.Renderer tem o
mtodo onDrawFrame(), invocado sempre que for necessrio atualizar a
view. Neste caso, eu renderizo primeiro a textura de fundo, utilizando um
truque para que o fundo cubra toda a tela:
1. Fao as matrizes de modelo, cmera e projeo iguais matriz
identidade;
2. Uso um VBO com coordenadas baseadas na unidade: (-1,-1), (-1,1),
(1, -1) e (1,1);
3. Desenho o fundo;
Assim, a imagem cobrir a tela toda.
Depois, eu renderizo cada GameObject que tenha textura:
1. Indico o identificador da textura que vou usar;
2. Informo qual o vetor de vrtices que vou usar. O identificador do
VBO foi carregado na propriedade VBOVertices, do GameObject;
3. Informo o MESMO vetor de textura (no h variao);
4. Ajusto a posio atual do centro do objeto, obtida do Box2D, na
matriz de modelo;
5. Ajusto o ngulo atual do objeto, obtido do Box2D, na matriz de
modelo;
6. Multiplico a matriz de modelo pela de cmera e a de projeo;
7. Desenho a textura do GameObject.
Eu sugiro que voc veja os dois exemplos, Android e iOS, identificando
estas etapas no cdigo.

Captulo 7 - Framework de Fsica e Renderizao 207

Exemplo Android: ..\Codigo\OpenGLAndroid\openglavancado.zip;


Exemplo iOS: ..\Codigo\OpenGLiOS\openGLIOSlavancado.zip

Mipmaps
Um dos problemas mais irritantes que existe o aliasing, um efeito causado pela captura (amostragem) ou renderizao de um sinal, seja ele de udio
ou de vdeo. Seu efeito prtico o serrilhamento da imagem. Observe bem
as duas prximas figuras.

Ilustrao 72: Imagem com aliasing

Ilustrao 73: Imagem sem aliasing, usando Mipmaps

208

Manual do Indie Game Developer - Verso Android e iOS

Note como na primeira imagem, tanto a bola como o cenrio, apresentam algum serrilhamento. Na segunda imagem, as duas figuras aparecem
perfeitas.
O aliasing acontece quando reduzimos uma imagem, porm existem tcnicas para suavizar o efeito. Basicamente, podemos desfocar as pontas de uma
imagem, dando a impresso de que ela est contnua. A criao de Mipmaps
uma das tcnicas para isto.
Basicamente, so criadas vrias verses da mesma imagem, com tamanho
e nvel de detalhe diferente, armazenadas em conjunto. No momento de renderizar a imagem, selecionada aquela cujo tamanho mais se aproxima do tamanho a ser utilizado. Mipmaps podem ser gerados manualmente ou automaticamente, porm ocupam maior memria. Estima-se que o uso de Mipmaps
aumente em 1/3 o tamanho necessrio para armazenar uma nica textura.
No OpenGL ES ns temos a opo de solicitar a criao dos Mipmaps com
a opo:
GL_TEXTURE_MIN_FILTER = GL_LINEAR_MIPMAP_NEAREST
Android:
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, hTextura);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR_MIPMAP_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S,

GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_REPEAT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, imagem, 0);

iOS:
NSDictionary * options = [NSDictionary
dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO],
GLKTextureLoaderOriginBottomLeft,
[NSNumber numberWithBool:YES],
GLKTextureLoaderGenerateMipmaps,
nil];

Captulo 7 - Framework de Fsica e Renderizao 209


NSError * error;
NSString *path = [[NSBundle mainBundle] pathForResource:nome
ofType:nil];
props.textureInfo = [GLKTextureLoader
textureWithContentsOfFile:path

options:options error:&error];
Se eu fosse voc, sempre utilizaria imagens cujas dimenses fossem potncias de 2
(POT). O OpenGL ES 2.0 exige isso se for mandar gerar Mipmaps.

Uso do Framework
No Android
Mais uma vez, os arquivos de exemplo Android esto em:
\Codigo\OpenGLAndroid\openglavancado.zip.
Eu criei este framework para agilizar o desenvolvimento, promovendo o
reuso dos componentes. No exemplo Android, temos os pacotes:
com.obomprogramador.games.exemplo;
com.obomprogramador.games.openglavancado;
com.obomprogramador.games.xmloader;
Voc s precisa escrever as classes do pacote com.obomprogramador.games.exemplo, que so:
Activity do game;
Renderer do game (derivado de: OpenGLAvancadoRenderer);
Para comear, crie seu XML de modelo do game, e coloque na pasta assets/modelo. Abra o arquivo de XML que eu criei para ver um exemplo.
Depois, crie uma Activity para o seu Game. Nela, voc dever:
1. Instanciar o Renderer que ser utilizado pela GLSurfaceView;
2. Instanciar a classe OpenGLAvancadoView, passando o Contexto
de aplicao e a instncia do Renderer para ela;
Eis o exemplo que eu criei (MainActivity.java):
public class MainActivity
OnTouchListener {

extends

Activity

implements

protected OpenGLAvancadoView mView;


protected OpenGLAvancadoRenderer renderer;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);

210

Manual do Indie Game Developer - Verso Android e iOS


try {
renderer = new Renderer(this.
getApplicationContext());
mView = new OpenGLAvancadoView(getApplicat
ion(), renderer);

} catch (Exception e) {

Log.d(GAMEMODEL, Exception: +
e.getMessage());

}
this.requestWindowFeature(Window.FEATURE_NO_
TITLE);
this.getWindow().setFlags(WindowManager.LayoutParams.
FLAG_FULLSCREEN,

WindowManager.LayoutParams.FLAG_FULLSCREEN);
mView.setOnTouchListener(this);
setContentView(mView);
}
@Override
protected void onPause() {
super.onPause();
mView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mView.onResume();
}

@Override
public void onConfigurationChanged(Configuration newConfig)
{
newConfig.orientation = Configuration.ORIENTATION_
LANDSCAPE;
super.onConfigurationChanged(newConfig);
}

@Override

public boolean onTouch(View arg0, MotionEvent arg1) {

mView.getRenderer().toque();

return true;

}
}

Captulo 7 - Framework de Fsica e Renderizao 211

Finalmente, voc ter que criar uma subclasse de OpenGLAvancadoRenderer. Nela, voc poder tratar eventos, como o toque na tela, por exemplo,
ou ento sobrescrever alguns mtodos do Renderer. Eis o meu exemplo:
public class Renderer extends OpenGLAvancadoRenderer {
public Renderer(Context context) throws Exception {

super(context);
}
@Override
public void toque() {

super.toque();

if (simulando) {
synchronized(world) {



// Comanda a aplicao de foras



GameObject go = new GameObject();
go.setId(1);



int inx = cenaCorrente.getObjetos().
indexOf(go);



go = cenaCorrente.getObjetos().
get(inx);

Body bola = go.getB2dBody();
Vec2 forca =

new Vec2(50.0f * bola.getMass(),
50.0f * bola.getMass());


Vec2 posicao = bola.getWorldCenter().
add(new Vec2 (0,3));
bola.setAwake(true);
bola.applyForce(forca, posicao);
}

}
}
}

No meu caso, eu apenas aplico uma fora bola.


No iOS
No iOS, voc pode fazer a mesma coisa, criando subclasses do seu
ViewController.

212

Manual do Indie Game Developer - Verso Android e iOS

Concluso
Combinando OpenGL ES e Box2D, criamos um game com boa sensao
de realidade, pois os efeitos de movimento e coliso so aceitveis e, tambm,
com bom desempenho grfico, evitando lags e outros efeitos indesejveis.
Mas a principal vantagem do uso do OpenGL ES - e eu volto a insistir
nisto - a padronizao. Criar um framework comum para plataformas to
distintas, como Android e iOS, seria muito mais difcil se utilizssemos os
mecanismos grficos nativos. Note como foi possvel abstrair as propriedades
e torn-las parametrizveis.
Mas funciona mesmo?
Se voc adepto ou adepta de So Tom, veja as prximas figuras, que
mostram, respectivamente, o exemplo sendo executado em um dispositivo Android (Smartphone LG p500, com Cyanogen Mod 7, verso Android:
2.3.4), e em um dispositivo iOS (iPad com iOS 6.0.1).

Ilustrao 74: Execuo em dispositivo Android

Ilustrao 75: Execuo em dispositivo iOS

Captulo 7 - Framework de Fsica e Renderizao 213

Como voc leu no captulo, nenhum objeto fixo dentro do cdigo-fonte,


pois todas as propriedades, tanto de Box2D como de OpenGL, so carregadas
a partir do arquivo XML.
Voc pode notar tambm que o tamanho da bola proporcional tela, mesmo em casos de geometrias diferentes. E, como usei Mipmaps, a imagem no
aparece serrilhada, mesmo em tamanho menor.
Estude e execute os exemplos
Este livro apresenta melhor resultado se voc, leitor ou leitora, analisar e
executar os exemplos. Tente mudar, acrescentar outros GameObjects, implementar uma lgica de jogo, sei l. Quer algumas sugestes:
1. Crie alvos nas paredes e tente acert-los com a bola;
2. Implemente chute direcional;
3. Crie um exemplo com Joints! Que tal criar um ragdoll, ou seja,
aqueles bonecos que caem de escadas e se quebram todos?
Estude bem o exemplo deste captulo, pois vou utiliz-lo com base para
todos os outros.
Otimizaes
Sabe uma coisa que eu odeio? Quando eu mostro um trabalho para algum e a primeira coisa que a pessoa faz criticar. Todos adoram criticar, mas
poucos se dispem a colaborar. Eu acho que a inveja a maior causa deste
problema. claro que eu no sou perfeito, logo, minhas criaes esto longe
disso, mas o que essas pessoas sequer tentam entender, antes de dispararem
suas crticas, o contexto no qual o trabalho foi feito.
Poucas pessoas consideram o seu nvel de conhecimento, o trabalho que
voc teve, ou mesmo a beleza das solues que voc deu. Preferem se apegar
a pequenos detalhes, percebidos por quem no fez nada para ajudar. Ao invs
de criticar o trabalho logo de cara, poderiam dar sugestes de melhorias, por
exemplo: ficou muito bom e voc pode melhorar ainda mais se bl-bl-bl.
No se iluda! Voc receber muitas crticas deste tipo, porm, no se deixe
abater e procure analisar com frieza o que o seu algoz est dizendo. Pode ser
uma crtica com fundamento e pode ser at razovel lev-la em considerao.
Por que disso? Bem, claro que possvel melhorar tudo o que mostrei at
agora, especialmente as tcnicas de uso do OpenGL ES. Na verdade, OpenGL
um enorme universo, cuja superfcie ns sequer arranhamos. Quer fazer
uma experincia? Entre no Stack Overflow e procure o tag OpenGL ES
2.0 (http://stackoverflow.com/questions/tagged/opengl-es-2.0). Voc ver que as
pessoas tm dvidas sobre assuntos que voc sequer imaginava que existiam!

214

Manual do Indie Game Developer - Verso Android e iOS

Porm, d para otimizar o uso do OpenGL, embora eu j tenha otimizado


alguma coisa ao usar VBOs estticos para vrtices e textura. Porm, tenha em
mente o que o professor Knuth disse sobre otimizao:
Premature optimization is the root of all evil
Otimizao prematura a raiz de todo o mal
Professor Dr. Donald Knuth (http://en.wikipedia.org/wiki/Donald_Knuth)
O que isto quer dizer? Simplesmente que voc deve se concentrar em terminar o trabalho ANTES de comear a otimizar. Existem muitas atitudes simples, que podem resultar em ganhos significativos. Muitas vezes, os programadores perdem tempo tentando melhorar a performance em milhonsimos
de segundos, sem se perguntar se isto realmente necessrio.
Eu procurei mostrar a aplicao de OpenGL ES de maneira simples e prtica, com um mnimo de otimizao. Eu penso que voc pode e deve otimizar
seu game. Sugiro algumas referncias importantes para isto:
1. http://gamedev.stackexchange.com;
2. Stack Overflow (http://http://stackoverflow.com);
3. Guia de ajustes do OpenGL ES no iOS (http://developer.apple.
com/library/ios/#documentation/3DDrawing/Conceptual/OpenGLES_
ProgrammingGuide/Performance/Performance.html#//apple_ref/doc/

uid/TP40008793-CH105);

Captulo 8
Tcnicas Comuns em Games
Existem muitos problemas interessantes em games, para os quais existem
tcnicas simples que proporcionam solues razoveis. Alis, eu sugiro que
voc sempre parta de solues simples para os seus problemas de desenvolvimento de games. No tente implementar tcnicas sofisticadas logo de cara.
E claro que existem vrias outras maneiras de fazer o que eu vou mostrar
aqui, porm, vou tentar fazer da maneira mais simples e funcional possvel, de
modo a dar uma base para que voc possa melhorar.

Como exibir um HUD


Os games usam e abusam de indicadores, conhecidos como HUDs (Heads-up displays), ou mostradores que flutuam sobre as imagens do jogo.
H vrios tipos de informaes que podemos dar ao jogador, por exemplo:
Hit points: pode ser numrico ou grfico, indicando quantos tiros
o jogador tomou ou quanto pode aguentar;
Life bar: uma barra que indica a quantidade de vida que o jogador
tem ou, ento, sua fora (stamina). Pode ser uma barra contnua ou
formada por pequenas imagens, como coraes, por exemplo;
Recharge bar: uma barra que indica quanto falta para recarregar
algum poder, por exemplo: nitro, em jogos de carros, ou escudo,
em jogos de tiros;
Cronmetro: informao sobre o tempo decorrido ou tempo
restante;
Capacidades: itens que podem ser usados pelo jogador, como: armas, por exemplo;
Controles: itens que servem para controlar o movimento e as aes
do jogador, como: andar, atirar etc. Em games mveis, isto pode ser
substitudo por: toque, arrasto ou mesmo inclinao do dispositivo;
Menus ou botes de ao: servem para invocar outras funes do
game, como: ajuda, opes, sair do game etc;
Pontos e objetivos: quantos pontos o jogador fez e qual a sua
posio com relao aos objetivos a serem alcanados. Alguns games incluem Goal Bars, que funcionam como as Life Bars, s que
mostram os objetivos alcanados.

216

Manual do Indie Game Developer - Verso Android e iOS

Nos jogos mais modernos, existe a tendncia de diminuir o nmero de


informaes HUD, substituindo-as por efeitos visuais nos GameObjects. Por
exemplo, ao invs de representar a Lifebar, podemos fazer o personagem
apresentar machucados ou cansao.
De qualquer forma, voc vai necessitar posicionar algumas informaes na
tela, as quais no representam GameObjects, logo, no esto sujeitas s leis fsicas do game. Na figura seguinte, mostramos a tela do jogo Bueiro Ball, um projeto do meu livro anterior, Mobile Game Jam (http://www.mobilegamejam.com/).

Ilustrao 76: O HUD do game Bueiro Ball

Minimize as informaes a serem exibidas


A ideia de HUD que o jogador no tenha que se desviar muito do jogo
para saber informaes importantes. Ficar mostrando mensagens textuais,
usar indicadores complexos ou poluir a cena com muitos indicadores pode
prejudicar a jogabilidade.
Como eu mencionei anteriormente, a tendncia atual minimalista e voc
pode dar ao jogador a opo de configurar o que ele quer ver no HUD.
O OpenGL ES no possui funo para renderizar texto
Como assim? Eu preciso fazer algo como glDrawText()! Sinto muito,
mas ele no possui esta caracterstica. Bem, se o OpenGL no renderiza texto,
como fazemos para exibir indicadores? Existem algumas opes:
1. Use indicadores grficos, baseados em texturas simples, como Lifebars e Goalbars. Como so texturas simples, podemos controlar quando sero desenhadas e qual o seu tamanho;
2. Para textos, use outras telas. Como veremos mais adiante, possvel
integrar telas OpenGL ES com outras telas, formando um fluxo (ou

Captulo 8 -
Tcnicas Comuns em Games
217

Storyboard) do jogo. E isto possivel tanto com Android como


com iOS;
3. Use texturas para representar mensagens fixas e rtulos de
informaes;
4. Use Bitmap fonts para escrever pequenos campos de texto ou nmeros. Basicamente consiste em criar texturas de cada caractere (ou
uma Sprite Sheet do alfabeto inteiro) e renderizar conforme o caractere ascii desejado;
Seja qual for a abordagem que voc vai usar, lembre-se disto: evite poluir
a cena do jogo! Mensagens textuais atrapalham (mais do que ajudam) o jogador. Se voc precisa mostrar um texto longo, crie uma textura e mostre ou,
ento, desvie para outra tela. Em alguns casos, possvel utilizar dilogos
(Toasts). Mas evite tentar renderizar um longo texto usando Bitmap fonts.
Se voc quiser mesmo representar frases dinamicamente, ou seja, a partir
de um String, ento ter que criar texturas de cada letra, nmero ou caracter
especial. Voc pode, opcionalmente, criar uma Sprite Sheet com todos os
caracteres de determinada fonte, usando as coordenadas de textura para indicar qual ser apresentada.
Para isto, existem at programas que criam Bitmap fonts a partir de fontes
disponveis no sistema, como True type, por exemplo. Um dos mais interessantes o Bitmap Font Generator, do site Angel Code (http://www.angelcode.com/products/bmfont/). Ele gera sprite sheets com os caracteres da fonte
selecionada, alm de um arquivo (pode ser em XML) com os detalhes de cada
caractere, como: altura, largura, espaamento etc.
Existem tcnicas para ler informaes sobre Bitmap fonts e renderizar caracteres dinamicamente, respeitando o espacejamento proporcional das letras
e nmeros (kerning). Eu recomendo os seguintes sites:
http://www.gamedev.net/topic/330742-quick-tutorial-variable-width-bitmap-fonts/, que usa o BMFont do Angel Code;
O excelente tutorial do site NeHe: http://nehe.gamedev.net/tutorial/
bitmap_fonts/17002/;
Outro excelente tutorial do Wikibooks: http://en.wikibooks.org/wiki/
OpenGL_Programming/Modern_OpenGL_Tutorial_Text_Rendering_01;

Com estes tutoriais, voc ser capaz de exibir textos de maneira correta,
com o espacejamento proporcional adequado, exatamente como em um editor
de textos.
Eu, por outro lado, no acredito que esta seja a forma mais eficiente de
implementar um HUD. O jogador est pouco ligando se voc usou o espacejamento correto (kerning) ou no! O que ele quer ver a informao relavante,
de forma descomplicada.

218

Manual do Indie Game Developer - Verso Android e iOS

Uma soluo simples


Eu j usei algumas solues que indiquei. Na verdade, eu gosto muito do
BMFont do Angel Code e usei o tutorial deles para criar HUDs textuais com
sucesso. Porm, eu gosto de seguir dois princpios em meus projetos de game:
Interface intuitiva;
Performance razovel;
Uma interface intuitiva permite ao jogador se concentrar nos objetivos do
game, pois as informaes relevantes sero apresentadas, quando necessrias,
de forma simples. E mensagens textuais no so muito intuitivas. Na verdade,
o jogador acaba decorando o texto e s v alguns caracteres. Com o tempo,
ele pode se confundir, caso voc mude ligeiramente a mensagem.
O outro princpio que o game deve ter uma performance razovel. E,
como j vimos, devemos otimizar muito nosso uso do OpenGL ES. Entre as
formas de otimizao esto:
Utilizar VBOs (estticos e na memria da GPU);
Minimizar a utilizao de memria, tanto da CPU como da GPU;
Utilizar menos operaes de desenho;
Se voc tem um alfabeto completo, s para renderizar algumas mensagens,
provavelmente est disperdiando recursos. Ningum utiliza todas as letras
para formar um HUD. E, caso seja uma mensagem longa, pode ser renderizada estaticamente, ou seja, voc cria uma textura com o texto todo escrito nela.
Logo, usar Bitmap fonts completas para criar mensagens textuais viola este
princpio de performance razovel.
Minha soluo simples:
1. Sempre que possvel, uso indicadores na forma de cones ou barras
fixas;
2. Uso VBOs de vrtices e de textura estticos. Eu crio posies pr-determinadas da tela e crio VBOs para elas na GPU, alm de usar
sempre o mesmo VBO de textura;
3. S exibo rtulos e mensagens absolutamente necessrias. Caso precise exibir uma mensagem mais longa, eu navego para outra tela;
4. Rtulos e textos estticos so criados a partir de texturas;
5. S uso textos dinmicos quando necessrio e, mesmo assim, limito
apenas a nmeros. Assim, s preciso criar texturas para os algarismos (0-9) e para alguns poucos sinais (:, - e +);
Eu gosto de elaborar um layout para a tela do game, no qual crio posies fixas, nas quais os indicadores sero exibidos. Vamos pegar o exemplo
do captulo anterior e acrescentar dois indicadores: o rtulo Tempo: e a hora
atual, no formato hh:mm:ss.

Captulo 8 -
Tcnicas Comuns em Games
219

Ilustrao 77: O Layout dos indicadores

Para o rtulo Tempo: eu crio uma textura completa. Para isto, eu gosto
de usar o programa de desenho do LibreOffice, mas voc pode usar qualquer
outro programa, como o Inkscape ou o Gimp.

Ilustrao 78: Textura do rtulo

E o tempo em si ser um texto dinmico, formado por nmeros e pelo sinal


:. Ento, eu crio texturas a partir destes elementos. Eu crio texturas separadas para cada algarismo e para o sinal :.

Ilustrao 79: Exemplo de textura de nmero

220

Manual do Indie Game Developer - Verso Android e iOS

Lembre-se: crie as texturas POT, ou seja, com dimenses em potncias


de 2!
Eu terei seis posies na tela:
Tempo:
Hh
:
mm
:
ss
E terei que calcular vetores de vrtices para cada uma delas, criando VBOs
na GPU, alm de enviar as texturas separadamente. Porm, as coordenadas de
textura so iguais (eu cubro os quatro vrtices de maneira igual).
Com os VBOs e as texturas na GPU, para desenhar s informar qual
a textura, qual o VBO de vrtices e qual o VBO de textura. Para o rtulo
mais simples, pois eu j sei qual textura devo usar. s guardar o indicador
(handler) de textura.
Para os outros campos, eu preciso transformar os valores em strings e pegar os caracteres ASCII de cada posio, buscando as texturas correspondentes.

Aumentando o framework de game


Vamos fazer um pequeno exemplo e aproveitar para turbinar nosso framework de game.
Os exemplos deste tpico esto em:
Android: ..\Codigo\OpenGLAndroid\openglcomtexto.zip;
iOS: ..\Codigo\OpenGLiOS\OpenGLComTexto.zip;
Na figura seguinte, podemos ver o resultado da execuo destes exemplos.

Ilustrao 80: Cena com HUD

Captulo 8 -
Tcnicas Comuns em Games
221

Bem, como eu desenharei texto frequentemente, resolvi aumentar o framework do captulo anterior para implementar a renderizao baseada em
layout. Para isto, criei um modelo de tela em XML, que descreve as posies fixas na tela e as texturas. No exemplo Android, o arquivo fica em:
assets/texturas/modelotela.xml, e, no iOS, fica dentro de Supporting files.
Abra o arquivo e veja como eu defini as posies e texturas.
Definindo posies
As posies so definidas dentro do tag posicoes:
<mapaTela>
<posicoes>

<posicao>

<id>1</id>

<!-- Espao para o rtulo de tempo -->

<altura>10</altura>

<largura>30</largura>

<topo>0</topo>

<esquerda>0</esquerda>

</posicao>

<posicao>

<id>2</id>

<!-- Espao para a o primeiro algarismo das
horas -->

<altura>10</altura>

<largura>6</largura>

<topo>0</topo>

<esquerda>30</esquerda>

</posicao>
...
</posicoes>

Cada posio tem as seguintes propriedades:


id: nmero identificador da posio. Deve ser nico;
altura: altura da posio;
largura: largura da posio;
topo: coordenada y da posio;
esquerda: coordenada x da posio;
Antes de mais nada, deixe-me explicar a unidade dos tamanhos e coordenadas. Lembra-se da diagonal da tela? Bem, eu segui o mesmo princpio. S
que, para facilitar, eu passei a usar percentuais da diagonal da tela. Por exemplo, as coordenadas topo e esquerda especificam percentuais da diagonal da

222

Manual do Indie Game Developer - Verso Android e iOS

tela, que devero ser convertidos em pixels pelo renderizador, para calcular o
VBO da posio. Da mesma forma, a altura e largura tambm so especificadas atravs de percentuais da diagonal da tela.
Utilizando a unidade baseada em percentual da diagonal da tela, eu mantenho a proporo correta dos elementos de layout em telas de tamanho
diferente.
Definindo texturas
As posies apenas definem VBOs de vrtices. Eu posso determinar quais
texturas sero mapeadas em quais posies. Para isto, preciso saber quais texturas eu quero ter no meu HUD, logo, aps definir posies, eu defino as
texturas:
...
</posicoes>

<texturas>

<textura>

<id>1</id>

<ascii>0</ascii>

<imagem>tempolabel.png</imagem>

<visivel>true</visivel>

<clicavel>false</clicavel>

<posicaoFixa>1</posicaoFixa>

</textura>
<textura>

<id>2</id>

<ascii>48</ascii>

<imagem>zero.png</imagem>

<visivel>false</visivel>

<clicavel>false</clicavel>

<posicaoFixa>0</posicaoFixa>

</textura>
...
</texturas>
</mapaTela>

Cada textura tem as seguintes propriedades:


id: identificador da textura. Deve ser nico dentro das texturas;
ascii: cdigo ascii que corresponde textura, se for um caractere, ou
ento zero, se for uma textura qualquer;
imagem: arquivo de imagem da textura (POT);
visivel: se a textura estar visvel automaticamente;

Captulo 8 -
Tcnicas Comuns em Games
223

clicavel: se a textura pode ser clicada ( para implementao futura!);


posicaoFixa: se a textura est associada a uma posio fixa, dentro
das posies pr-definidas;
Note que a textura cujo id 1 no possui representao ASCII, logo,
no um caractere e j est definida na posio cujo id 1 (os ids no necessitam ser iguais). O renderizador vai exibir esta textura automaticamente
utilizando o VBO da posio 1.
J a textura 2 um caracter ASCII, neste caso, o algarismo zero, e no
possui posio fixa.
Carga do modelo de tela
Eu uso o mesmo mecanismo da carga do arquivo de modelo de game, tanto
em Android como em iOS. No Android, a carga do XML feita pelo mtodo carregarModeloTela(), invocado pelo mtodo onSurfaceChanged(),
do Renderer (OpenGLAvancadoRenderer.java). No iOS, a carga feita no
mtodo - (void) carregarModeloTela, invocado pelo mtodo recalcularAlinhamentos, da classe OGCTViewController.
No iOS, eu usei o mesmo framework para carregar o Modelo de Tela e o Modelo de Game, criado por Nick Farina (http://nfarina.com/post/2843708636/a-lightweight-xml-parser-for-ios). O NSXMLParser, nativo do iOS, muito
verboso, e o SMXMLDocument, do Nick Farina, muito parecido com o
parser do Android.
A carga em si no tem nada demais. Ela simplesmente carrega o meu pequeno modelo de classes.

Ilustrao 81: O modelo de dados do layout

224

Manual do Indie Game Developer - Verso Android e iOS

A coisa comea a ficar interessante quando eu crio os VBOs das posies e


carrego as texturas. No Android, isto feito no mtodo loadXML(), da classe ModeloTelaLoader(), invocado pelo mtodo construtor da classe OpenGLAvancadoRenderer. No iOS, isto feito no mtodo loadModeloTela,
da classe OGMTModeloTelaLoader, invocado pelo mtodo setupGL da
classe OGCTViewController.
O procedimento para ambas as plataformas o mesmo. Para cada posio
carregada do XML:
1. Converto as coordenadas e dimenses de acordo com o valor percentual da diagonal da tela;
2. Calculo o centro da posio, utilizando os valores j convertidos;
3. Invoco o mtodo que cria o VBO de vrtices e retorna o identificador do buffer (handler). o mesmo mtodo que calcula os VBOs
dos vrtices dos GameObjects;
Depois, para cada textura carregada do XML:
1. Crio o buffer de textura;
2. Transfiro para a memria da GPU;
3. Verifico se a textura um cdigo ASCII, ento eu insiro uma ligao
entre ela e o cdigo ASCII no vetor associativo de caracteres;
4. Verifico se a textura ocupa uma posio fixa, neste caso, eu procuro
a posio e implemento a associao entre as duas.
Se a textura for um caractere ASCII, eu vou utiliz-la para escrever alguma
coisa. Ento, eu uso um vetor associativo (HashMap no Android, e NSMutableDictionary no iOS) para descobrir qual a textura correspondente a um
caracter ASCII.
Se a textura ocupa uma posio fixa, ento eu j associo a instncia da
posio.
Renderizao das texturas fixas
Logo aps renderizar a textura de fundo da cena, eu renderizo cada textura
de posio fixa. No Android, isto feito no mtodo onDrawFrame(), da
classe OpenGLAvancadoRenderer, e, no iOS, feito no mtodo drawInRect (delegado de GLKView), da classe OGCTViewController.
O procedimento o mesmo para as duas plataformas. Para cada textura
carregada do XML:
1. Informamos ao OpenGL o seu identificador de textura;
2. Informamos ao OpenGL o VBO a ser utilizado, que da posio
que ela ocupa;

Captulo 8 -
Tcnicas Comuns em Games
225

3. Informamos ao OpenGL o mesmo VBO de texturas que usamos para


todas as texturas do game;
4. Desenhamos;
Renderizao das texturas dinmicas
Uma textura dinmica pode ser uma Life bar ou um texto formado por
vrias texturas. Neste exemplo, eu uso apenas texto, mas o raciocnio o
mesmo para todos os casos. No Android, isto feito no mtodo onDrawFrame(), da classe OpenGLAvancadoRenderer, e, no iOS, feito no mtodo
drawInRect (delegado de GLKView), da classe OGCTViewController.
Logo aps renderizar as texturas de posio fixa, eu invoco um mtodo
para obter o texto dinmico a ser renderizado. No Android, o mtodo desenharTextos(), da classe OpenGLAvancadoRenderer, e, no iOS, o mtodo
desenharTextos, da classe OGCTViewController. Neste mtodo, eu obtenho o valor da hora atual e o formato em hh:mm:ss, invocando o mtodo
que realmente renderiza os textos dinmicos.
O mtodo que renderiza os textos dinmicos (Android e iOS: desenharString), recebe o texto e a primeira posio na tela, a partir da qual as texturas
devem ser renderizadas. Deve haver posies suficientes para renderizar TODOS os caracteres. Ento, ele pega cada caractere ASCII do texto e procura
no dicionrio (Android: HashMap, iOS: NSMutableDictionary) qual a
textura correspondente. Ento, ele seleciona a textura e usa o VBO da posio
corrente para renderiz-la. Depois, ele avana para a prxima posio.
Ser que funciona?
Claro que sim. E com uma vantagem: se eu quiser mudar o game, s terei
que alterar em poucos locais:
Nos arquivos XML de modelo de game e de tela;
No mtodo desenharTexto, tanto no Android, como no iOS;
Note que, neste exemplo, a bola est na frente do HUD. Foi proposital,
pois eu acho que fica melhor assim. Se voc quiser, pode posicionar o HUD
na frente da bola, mudando a posio de renderizao das texturas fixas e
dinmicas para depois dos GameObjects.
Mais uma vez, para os adeptos de So Tom, vou mostrar a soluo sendo
executada nas duas plataformas: um Smartphone Android LG p500 e um iPad.

226

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 82: Verso com HUD executando no Android

Ilustrao 83: Verso com HUD executando no iOS

Integrando o game em uma aplicao mvel


Voc pode criar seu game todo em uma nica janela OpenGL ES, tanto no
Android, como no iOS. Voc pode variar os mapas de tela, criando verses diferentes e alternando conforme o jogador comandar. S que estar replicando

Captulo 8 -
Tcnicas Comuns em Games
227

o funcionamento da interface grfica dos dispositivos, o que um desperdcio, afinal, Android e iOS possuem excelentes recursos de navegao, com os
quais os usurios j esto acostumados. Alm disto, diante da dificuldade para
renderizar textos, podemos criar telas informativas ou de ajuda, ou mesmo de
configurao, de maneira simples e rpida.
Se voc quiser, poder integrar a tela e o controlador do seu game ao fluxo de uma aplicao, o que incluir telas convencionais, telas OpenGL ES e
controles de navegao.
Vamos fazer um exemplo bem simples. Ele ser baseado no exemplo anterior (com HUD) e funcionar desta forma:
1. Ao executar a aplicao, uma tela convencional do dispositivo ser
exibida (Activity no Android, UIView no iOS), com um link para o
Game;
2. Ao clicar no link, a tela OpenGL ES do game caregada e o jogo
comea;
3. Haver uma textura esttica na tela OpenGL ES, com o rtulo Sair.
Ao clicar nela, o game retorna para a tela anterior;
Para implementar isto, vamos utilizar a propriedade clicvel, que inclumos no modelo de tela para cada textura.
Vou mostrar a imagem das duas telas no Android (no iOS a mesma coisa).

Ilustrao 84: A tela inicial ( esquerda), e a tela do Game ( direita)

Plataforma Android
O cdigo-fonte deste exemplo est em: ..\Codigo\OpenGLAndroid\appintegradadroid.zip.
Na plataforma Android bem simples:
1. Criei uma subclasse de Activity e a registrei no arquivo AndroidManifest.xml, como a atividade principal;
2. Nesta Activity principal, criei uma TextView (com os atributos:
android:clicable = true e android:onclick = chamar. Poderia ser
um boto tambm, ou uma imagem;

228

Manual do Indie Game Developer - Verso Android e iOS

3. Na Activity principal, criei um mtodo para receber o clique e invocar a Activity OpenGL ES;
4. Modifiquei o arquivo modelotela.xml para incluir a textura do boto sair (arquivo sair.png);
5. Na Activity do game, eu intercepto o toque e invoco o mtodo toque, no Renderer;
6. No Renderer, eu identifico se o toque foi dentro de alguma textura
clicvel. Se foi, eu comando a Activity do Game para se auto finalizar, o que ativar a Activity anterior (a que o usurio acionou);
A Activity principal invoca a do Game no mtodo chamar():
public void chamar(View view) {
Intent i = new Intent (this.getApplicationContext(),
GameActivity.class);
this.startActivity(i);
}

Na Activity do Game (GameActivity), eu modifiquei o mtodo que interceta o toque (onTouch()):


@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
((Renderer) mView.getRenderer()).toque(arg1);
return true;
}

Agora, eu invoco um mtodo diferente no Renderer, que recebe tambm o


MotionEvent, com as coordenadas do toque.
Na classe Renderer (derivada de OpenGLAvancadoRenderer),
eu deixei de sobrescrever o mtodo toque() e criei outro, que recebe o
MotionEvent:
public void toque(MotionEvent eventoToque) {
super.toque();
if (simulando) {

synchronized(world) {

if (!clicouEmTextura(eventoToque)) {


// Comanda a aplicao de foras


GameObject go = new GameObject();
go.setId(1);


int inx = cenaCorrente.getObjetos().indexOf(go);


go = cenaCorrente.getObjetos().get(inx);

Captulo 8 -
Tcnicas Comuns em Games
229

Body bola = go.getB2dBody();

Vec2 forca = new Vec2(50.0f * bola.getMass(),

Vec2 forca =50.0f * bola.getMass());


Vec2 posicao = bola.getWorldCenter().add(
new Vec2 (0,3));
bola.setAwake(true);
bola.applyForce(forca, posicao);

}

}
}
}

Se o toque foi fora da textura, ento ele tem que aplicar uma fora bola,
conforme j fazia no exemplo anterior. O mtodo clicouEmTextura verifica
se o clique foi dentro da textura selecionada:
private boolean clicouEmTextura(MotionEvent eventoToque) {
boolean resultado = false;
Coordenada toque = new Coordenada (
eventoToque.getX(),
eventoToque.getY(),
0.0f);
for (Textura t : modeloTela.getTexturas()) {

if (t.isClicavel()) {

if (toqueDentro(t,toque)) {
resultado = true;
((GameActivity)activity).sair();
}

}
}
return resultado;
}

Eu poderia verificar o id da textura, para associar ao a ser tomada,


porm, no meu caso, s h uma textura clicvel: o rtulo sair. Para cada
textura, eu verifico se ela clicvel, ento eu invoco o mtodo que verifica se
o toque foi dentro dos limites da textura (VBO):
private boolean toqueDentro(Textura t, Coordenada toque) {
boolean resultado = false;
PosicaoTela p = t.getPosicaoAtual();
float cTop = (alturaViewPort / 2) - ((p.getTopo() *

230

Manual do Indie Game Developer - Verso Android e iOS


diagonalTela) / 100);
float cLeft = - (larguraViewPort / 2) + ((p.getEsquerda() *
diagonalTela) / 100);
float cAltura = (p.getAltura() * diagonalTela) / 100;
float cLargura = (p.getLargura() * diagonalTela) / 100;
float cBottom = cTop - cAltura;
float cRight = cLeft + cLargura;

float toqueX = toque.getX() - (larguraViewPort / 2);
float toqueY = (alturaViewPort / 2) - toque.getY();

if ((toqueX >= cLeft && toqueX <= cRight) &&

(toqueY >= cBottom && toqueY <= cTop)) {

resultado = true;
}

return resultado;
}

Talvez voc esteja se perguntando: porque ele no usou a classe java.


awt.Rectangle ? A resposta que as classes de interface (awt no Android)
costumam assumir que a origem das coordenadas no canto superior esquerdo, e que o valor de y flipped, ou seja, invertido. Ento, como meu sistema de coordenadas diferente (comea no meio da tela e o y normal), eu
criei uma funo prpria para verificar se o ponto est dentro do retngulo. Se
estiver, ento eu tenho que finalizar a minha Activity:
((GameActivity)activity).sair();

Eu no posso fazer isso dentro do Renderer, ento eu criei navegao bidirecional entre a classe GameActivity e a classe Renderer, incluindo uma
propriedade que aponta para a Activity. Na GameActivity, eu criei o mtodo
sair():
public void sair() {
this.finish();
}

E pronto! Temos um game inserido em um fluxo normal de aplicao.


importante lembrar que somente a Activity pode fazer tarefas como: carregar
outras activities, enviar emails ou encerrar a si mesma.

Captulo 8 -
Tcnicas Comuns em Games
231

Plataforma iOS
O cdigo-fonte deste exemplo est em: ..\Codigo\OpenGLiOS\AppIntegradaIOS.zip.
Apesar de parecer muito complexo, mais devido ao Xcode do que ao Objective C (na minha opinio), o processo to simples como criar qualquer
aplicao com mltiplas views, baseada em Storyboard.
A melhor maneira de comear criar uma aplicao normal, com o template Single View, marcando as opes de usar Storyboards e ARC.
Lembre-se de direcionar como universal, ou seja: pode ser executada em
qualquer dispositivo iOS. Depois:
1. Adicione os frameworks: GLKit..framework e OpenGLES.framework;
2. Altere Header Search Path para: ${PROJECT_DIR}/**;
3. Altere Compile Sources As para: Objective C++;
4. Adicione o Box2D do outro projeto (copie o folder evitando criar
referncias!);
5. Adicione as imagens e Arquivos XML (dentro de supporting files);
6. Crie um grupo e adicione as classes (arquivos .h e ,m) do projeto OpenGLCom Textu. No acrescente os outros arquivos, s as
classes;
7. Nos dois Storyboards, crie um boto para invocar o game e adicione mais um GLKViewController, mudando a classe para
OGCTViewController;
8. Nos dois Storyboards, crie uma segue do boto Entrar no game
para a outra View (GLKView);
Antes de mais nada, deixe-me repetir: estou assumindo que voc sabe
programar aplicaes no iOS, logo, conhece Storyboards e segues. Se
voc no conhece, ento recomendo meu livro anterior: Mobile Game Jam
(http://www.mobilegamejam.com/).
Eu estou usando uma segue modal. Se voc quiser criar segues do tipo
push, ter que criar um NavigationController. Com segues do tipo push,
voc pode abrir uma segunda tela, mantendo o jogo pausado.
Como funciona?
Quando voc iniciar a aplicao, a tela exibida ser semelhante da verso
Android, com um boto para entrar no game. Ao clicar no boto, a segue
que criamos vai instanciar a View do Game, juntamente com o nosso View

232

Manual do Indie Game Developer - Verso Android e iOS

Controller (voc mudou as classes da View e do ViewControler, como recomendado no passo 7?)
Ento, temos apenas que implementar a volta... Para comear, nosso mtodo de captura de toque, handleTapFrom, tem que ser modificado:
- (void)handleTapFrom:(UITapGestureRecognizer *)
recognizer {
if (simulando) {
if (![self clicouEmTextura:recognizer]) {
// Comanda a aplicao de foras
b2Vec2 forca =
b2Vec2(200.0f * bola->GetMass(), 200.0f *
bola->GetMass());
b2Vec2 posicao = bola->GetWorldCenter();
posicao.y += 2;
bola->SetAwake(true);
bola->ApplyForce(forca, posicao);
}
}
}

Fizemos praticamente a mesma coisa que na verso Android: criamos um


mtodo para verificar se o toque foi dentro de alguma textura clicvel. Este
mtodo tambm semelhante ao da verso Android:
- (BOOL) clicouEmTextura: (UITapGestureRecognizer *)
recognizer
{
BOOL resultado = NO;
CGPoint ponto = [recognizer locationInView:recognizer.
view];
OGBPCoordenada * toque = [[OGBPCoordenada alloc]
initWithx:ponto.x
OGBPCoordenada * toque = [[OGBPCoordenada alloc]
initWithy:ponto.y
OGBPCoordenada * toque = [[OGBPCoordenada alloc] initWith
z:0.0f];
for (OGMTTextura * t in modeloTela.texturas) {
if (t.clicavel) {
if ([self toqueDentro:toque naTextura: t]) {
resultado = true;

Captulo 8 -
Tcnicas Comuns em Games
233
[self dismissViewControllerAnimated:Y
ES completion:nil];
}
}
}
return resultado;
}
- (BOOL) toqueDentro: (OGBPCoordenada *) toque
naTextura: (OGMTTextura *) t
{
BOOL resultado = false;
OGMTPosicaoTela * p = t.posicaoAtual;
float cTop = (alturaViewPort / 2) - ((p.topo * diagonalTela)
/ 100);
float cLeft = - (larguraViewPort / 2) + ((p.esquerda *
diagonalTela) / 100);
float cAltura = (p.altura * diagonalTela) / 100;
float cLargura = (p.largura * diagonalTela) / 100;
float cBottom = cTop - cAltura;
float cRight = cLeft + cLargura;
float toqueX = toque.x - (larguraViewPort / 2);
float toqueY = (alturaViewPort / 2) - toque.y;
if ((toqueX >= cLeft && toqueX <= cRight) &&
(toqueY >= cBottom && toqueY <= cTop)) {
resultado = YES;
}
}

return resultado;

Para dispensar a tela do Game, eu uso o mtodo dismissViewControllerAnimated, da classe GLKViewController.


Pronto! Temos uma tela de Game integrada a uma aplicao normal iOS.

Tempo e movimento
Lembra-se que eu havia dito para no se preocupar com o Game loop, por
que mais tarde eu entraria nesse assunto? Bem, o momento chegou! Vamos
dar uma ajeitada no nosso Game loop.

234

Manual do Indie Game Developer - Verso Android e iOS

Um dos grandes problemas dos desenvolvedores de game evitar lags.


Como j discutimos, lag em game significa falha do game em acompanhar o
ritmo do jogador. Este termo tem conotao diferente quando aplicado a jogos
Online, especialmente MMORPGs (jogos multiplayer online), denotando o
atraso de comunicao entre a estao do jogador e o servidor do game.
O lag de comunicao tambm acontece em dispositivos online, quando jogamos MMOs, mas, neste caso, o conceito de lag ao qual estou me
referindo o descompasso entre o Game Loop, o Render Loop e o ritmo do
jogador.
S que existe o contrrio tambm: acelerao involuntria, que acontece
se voc executar o mesmo game em um dispositivo mais rpido. Para evitar
lags, ns podemos otimizar os mecanismos de clculo e renderizao, utilizando OpenGL ES, o que geralmente d bons resultados. Porm, para evitar
acelerao involuntria isto no ajuda muito.
A nica maneira sincronizar todos os movimentos do jogo de acordo com
o tempo real, usando uma taxa de atualizao compatvel com a maioria dos
dispositivos.

Game Loop
Muito se fala sobre como o Game loop e sobre o Render loop. Basicamente, suas funes so:
Game loop: atualiza o Modelo do game (lembre-se do padro Model-View-Controller), seja usando o step do Box2D, ou calculando novas posies de cada objeto mvel na mo. Ele tambm verifica se objetivos foram alcanados, alvos atingidos etc;
Render loop: atualiza a Viso (view) a partir do Modelo (Model).
Ele deve gerar um novo frame visvel para o jogador, com a posio corrente de todos os GameObjects, alm das informaes e
indicadores (HUD);
Existem duas abordagens para implementar os dois, e eu as tenho mostrado
nos ltimos exemplos.
Game loop e Render loop sincronizados
Isto significa que os dois so executados em sequncia, dentro de uma taxa
de atualizao fixa. Antes de concluir uma iterao, o Game loop invoca o
Render loop, seja diretamente ou indiretamente (invalidando a viso). O que
importa que o cdigo dos dois executado pelo mesmo Thread.

Captulo 8 -
Tcnicas Comuns em Games
235

As principais vantagens desta abordagem so:


A arquitetura e a implementao so mais fceis;
No necessita de sinalizadores para serializar o acesso
(synchronized);
Consigo manter mais facilmente uma taxa consistente de atualizao;
Eu tenho feito isso nas verses iOS dos ltimos exemplos. Note que eu
crio a lgica de atualizao dentro do mtodo delegate update, do meu
ViewController:
#pragma mark - GLKView and GLKViewController delegate
methods
- (void)update
{
@synchronized(self) {
world->Step(1.0f / cenaCorrente.fps,
cenaCorrente.box2d.velocityInterations,
cenaCorrente.box2d.positionInterations);
}
}

O GLKViewController j cria um loop de atualizao, que invoca em sequncia o mtodo update, e comanda a atualizao da GLKView, que invoca
o mtodo drawInRect. Note que a diretiva @synchronized est a apenas
para efeitar, pois no tem efeito prtico porque no h threads concorrentes
tentando acessar os mesmos dados. Eu a deixei a apenas para efeito de marcar
uma possvel migrao para abordagem assncrona.
uma implementao simples e prtica, que nos isola de muitos problemas. Podemos at configurar a taxa de atualizao do GLKViewController
atravs da propriedade preferredFramesPerSecond. O valor default 30
FPS. O GLKViewController vai utilizar uma taxa de FPS prxima que voc
deseja e voc pode consult-la atravs da propriedade framesPerSecond.
A maioria dos desenvolvedores de game considera que esta abordagem
(Game loop e Render loop sincronizados) suficiente. Apenas em casos ultraextremos, nos quais a renderizao ou a atualizao podem passar por picos
de demora, seria interessante separar os dois loops.
Game loop e Render loop assncronos
Nesta abordagem, o Game loop executado por um Thread separado do
Render loop, com sua prpria taxa de atualizao. O Render loop ativado

236

Manual do Indie Game Developer - Verso Android e iOS

independentemente do Game loop haver completado sua tarefa e vai acessar


os dados necessrios para renderizar a tela, o que pode causar Race condition, logo, necessrio serializar o acesso ao Modelo do game.
A principal vantagem dessa abordagem tornar os dois loops independentes, logo, se um demorar mais do que o outro, teoricamente isto no afetar
ambos. Porm, cria-se um problema de sincronizao muito grande. Imaginemos que o Render loop demorou mais do que o previsto, e o Game loop atualizou muita coisa, atingindo um alvo, por exemplo. O Render loop vai demorar
algum tempo at chegar nesse ponto, o que pode causar lag.
Eu usei essa abordagem nos exemplos Android de propsito, apenas para
mostrar que possvel e que, em jogos simples, no faz a menor diferena,
afinal, o desempenho dos exemplos no Android e no iOS quase o mesmo,
independentemente da diferena arquitetural (CPU, GPU, memria etc).
No Android, o Render loop processado automaticamente pela classe derivada de GLSurfaceView.Renderer. Se ns deixarmos nos valores default,
o renderMode ser RENDERMODE_CONTINUOUSLY, o que significa
que o mtodo onDrawFrame(), do Renderer, ser invocado continuamente.
Qual a consequncia disso? Bem, a principal o gasto de bateria, se o
aparelho estiver rodando desconectado de uma fonte de alimentao. Alm,
claro, da sobrecarga no processador.
Neste caso, eu preferi criar um Thread separado para invocar o Game loop:
public void runGameLoop() {
simulando = true;
task = new GameLoopTask();
timer = new Timer();
timer.scheduleAtFixedRate(task, 0, (long) ((1 /
cenaCorrente.getFps()) * 1000));
}

public void gameLoop() {
synchronized (world) {

// Um lembrete de que pode haver problemas de
concorrncia

update();
};
}
class GameLoopTask extends TimerTask {
@Override

Captulo 8 -
Tcnicas Comuns em Games
237
public void run() {

gameLoop();
}

}

Eu estou utilizando o recurso de Timer com TimerTask para executar


o meu mtodo update() dentro da taxa de FPS que foi configurada no modelo do game (modeloGame.xml). O mtodo scheduleAtFixedRate espera
alguns parmetros, entre eles o perodo de repetio da atualizao. Eu simplesmente converti o valor em FPS em milissegundos para cada repetio do
mtodo update(). Neste exemplo, eu tenho 30 FPS no modelo, logo, o Game
loop ser invocado a cada 33,333336 milissegundos ( a mesma coisa que
dividir 1000 pela taxa de FPS, mas eu quis deixar claro o clculo).
Bem, como estou invocando o update() em um Thread separado do Render loop, tanto o cdigo de atualizao como o de renderizao precisam serializar o acesso ao Modelo do game:
protected void update() {
/*

* Atualiza o mundo Box2D e calcula a projeo

*/
synchronized(world) {

world.step(1.0f / cenaCorrente.getFps(),

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());
}
}
...
@Override
public void onDrawFrame(GL10 gl) {
...
// Vamos renderizar os Game Objects:
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthMask(true);
synchronized(world) {
for (GameObject go : cenaCorrente.getObjetos())
{

238

Manual do Indie Game Developer - Verso Android e iOS


if (go.getArquivoTextura() != null) {
...


float posicaoX =

go.getB2dBody().getTransform().position.x

* proporcaoMetroTela;


float posicaoY =

go.getB2dBody().getTransform().position.y

* proporcaoMetroTela;

Matrix.translateM(matrizModelo, 0, posicaoX,

posicaoY, 0);


Matrix.rotateM(matrizModelo, 0, (float)

go.getB2dBody().getAngle()


* 57.2957795),

0, 0, 1);
...

Os cdigos suscetveis a apresentar race conditions so os que esto em


negrito. Note que eu protegi a atualizao do mundo Box2D e o acesso
s posies dos objetos. Se eu estiver criando novos GameObjects em meu
Game loop, ento tenho que proteger isso tambm nos dois pontos (Game
loop e Render loop).
Finalmente, eu estou comandando a atualizao do meu mundo Box2D
informando a taxa de FPS que est no modelo (ainda no estou satisfeito com
o timeStep que estou usando):
world.step(1.0f / cenaCorrente.getFps(),

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());

Eu poderia ter deixado o onDrawFrame() controlar os dois, tomando


cuidado para s atualizar e desenhar o frame dentro da taxa de FPS desejada.
Por exemplo, algo assim:
public void onDrawFrame(GL10 gl)
{
// Calcular diferea de tempo passada:

agora = System.currentTimeMillis();

intervaloPassado = agora ultimo;

Captulo 8 -
Tcnicas Comuns em Games
239

// Colocar o Thread em sleep mode, pois falta


// algum tempo para iniciar
Thread.Sleep(33 intervaloPassado);

if (intervaloPassado < cenaCorrente.getFPS())

ultimo = System.currentTimeMillis();

// Invocar a atualizao:

update();
// Cdigo de renderizao:

GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT
COLOR_BUFFER_BIT);
GLES20.glUseProgram(programaGLES);
...
}

GLES20.GL_

Para games casuais, especialmente os que utilizam modelos 2D, um nico


Thread mais do que suficiente. O mais importante garantir que o modelo
esteja sendo atualizado e renderizado a uma taxa consistente de FPS, que no
precisa ser exata.
Melhorando o Game loop
Vamos ver uma melhoria em nosso GameLoop, primeiramente na verso
Android. Vamos pegar o projeto anterior e dar uma melhorada. O fonte est em:
Android: ..\Codigo\OpenGLAndroid\gameloopdroid.zip;
iOS: \Codigo\OpenGLiOS\GameLoopIOS.zip;
Este exemplo roda o game bem prximo taxa de FPS desejada e, ainda
por cima, mostra na tela qual o valor de FPS mdio a cada segundo.

240

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 85: Novo Game loop com informao de FPS

Eu j testei at com 60 FPS no meu dispositivo menos potente: um smartphone Android LG p500, com 600 mhz de clock, e funcionou bem. Se aumentar muito o FPS vai notar que a bola gira em alta velocidade e se move
lentamente. Como estamos aplicando a fora em um vetor acima do centro,
o torque gerado absorve grande parte da energia. Se chutar mais prximo ao
centro, ver que ela se move mais rapidamente.
Ajuste para telas de propores diferentes de 2/3
Antes de mais nada, temos que fazer um ajuste no nosso framework...
Quando criei o Modelo de Tela, eu sabia que existiam telas de propores
diferentes de 2/3. Por exemplo, 320 x 480 e 480 x 720 tm razo = 2/3. E o
que isso importa? Bem, eu calculo TODAS as medidas a partir da proporo
Metro/Tela, logo, se a proporo mais esticada, isto pode fazer com que
texturas posicionadas muito prximas aos limites (superior, inferior, esquerdo
e direito) fiquem parcialmente fora da tela.
Quando fui testar este exemplo em um tablet Motorola Xoom 2 Media Edition, cuja tela tem 800 x 1280 pixels (razo = 0,625), notei que algumas texturas estavam com parte fora da tela. Isto no acontece com os GameObjects,
porque eu posiciono limites (teto, cho, parede direita e parede esquerda).
Mas com as posies do Modelo de Tela, isto pode acontecer.
Ento, criei um filtro para telas com proporo menor que 2/3. Este filtro
desloca ligeiramente o centro, de modo que todas as posies do modelo de
tela fiquem dentro dos limites. J testei em vrios tipos de emulador e de dispositivos, com sucesso.

Captulo 8 -
Tcnicas Comuns em Games
241

No Android
Crie um mtodo de verificao na classe OpenGLAvancadoRenderer:
protected void verificarNovaProporcao(Coordenada centro,
float metadeAltura,
float metadeLargura) {
float esquerda = centro.getX() - metadeLargura;
float direita = centro.getX() + metadeLargura;
float topo = centro.getY() + metadeAltura;
float baixo = centro.getY() - metadeAltura;
float limiteEsquerdo = -1 * (larguraViewPort / 2);
float limiteDireito = larguraViewPort / 2;
float limiteSuperior = alturaViewPort / 2;
float limiteInferior = -1 * (alturaViewPort / 2);

if (esquerda < limiteEsquerdo) {

centro.setX(centro.getX() + (limiteEsquerdo - esquerda));
}
else if (direita > limiteDireito) {

centro.setX(centro.getX() - (direita - limiteDireito));
}

if (topo > limiteSuperior) {

centro.setY(centro.getY() - (topo - limiteSuperior));
}
else if (baixo < limiteInferior) {

centro.setY(centro.getY() + (limiteInferior - baixo));
}
}

Este mtodo verifica se as bordas da posio ficaro fora dos limites da


tela e, neste caso, desloca ligeiramente o centro da posio. Este mtodo deve
ser invocado antes de calcular os vrtices finais das posies, o que ocorre no
mtodo carregarModeloTela():
private void carregarModeloTela() {

mapaCaracteres = new HashMap<Integer, Textura>();

for (PosicaoTela p : modeloTela.getPosicoes()) {

float cTop = (alturaViewPort / 2) - ((p.getTopo() *
diagonalTela) / 100);

242

Manual do Indie Game Developer - Verso Android e iOS


float cLeft = - (larguraViewPort / 2) + ((p.getEsquerda() *
diagonalTela)

/ 100);
float cAltura = (p.getAltura() * diagonalTela) / 100;
float cLargura = (p.getLargura() * diagonalTela) / 100;
float metadeAltura = cAltura / 2.0f;
float metadeLargura = cLargura / 2.0f;


Coordenada centro = new Coordenada(cLeft + metadeLargura,

cTop - metadeAltura,

0);


if (((float) alturaViewPort / (float) larguraViewPort)
< 0.66f ) {

// A proporo menor que 2/3, precisamos verificar
os centros

verificarNovaProporcao(centro, metadeAltura,
metadeLargura);

}


p.setScreenTop(centro.getY() + metadeAltura);

p.setScreenLeft(centro.getX() - metadeLargura);

p.setScreenBottom(centro.getY() - metadeAltura);

p.setScreenRight(centro.getX() + metadeLargura);


p.sethVobVertices(carregarCoordenadasVertice(centro,
cAltura,
cLargura));
}

No iOS:
- (void) carregarModeloTela
{
mapaCaracteres = [[NSMutableDictionary alloc] init];
for (OGMTPosicaoTela * p in modeloTela.posicoes) {
float cTop = (alturaViewPort / 2) - ((p.topo * diagonalTela)
/ 100);
float cLeft = - (larguraViewPort / 2) + ((p.esquerda *
diagonalTela) / 100);

Captulo 8 -
Tcnicas Comuns em Games
243
float
float
float
float

cAltura = (p.altura * diagonalTela) / 100;


cLargura = (p.largura * diagonalTela) / 100;
metadeAltura = cAltura / 2.0f;
metadeLargura = cLargura / 2.0f;

OGBPCoordenada * centro = [[OGBPCoordenada alloc]


initWithx:(cLeft + metadeLargura)
y:(cTop - metadeAltura)
z:0];
if (((float) alturaViewPort / (float) larguraViewPort)
< 0.66f ) {
// A proporo menor que 2/3, precisamos verificar os centros
[self verificarNovaProporcao:centro metadeAltura:metadeAltura

metadeLargura:metadeLargura];
}
p.screenTop = centro.y + metadeAltura;
p.screenLeft = centro.x - metadeLargura;
p.screenBottom = centro.y - metadeAltura;
p.screenRight = centro.x + metadeLargura;
p.hVobVertices = [self carregarCoordenadasVertice:centro
altura:cAltura largura:cLargura];

Eu criei quatro novas propriedades na classe de posio, de modo a representar os limites (possivelmente ajustados) da textura. Eu uso isso no teste de
toque dentro de texturas:
Android:
private boolean toqueDentro(Textura t, Coordenada
toque) {

boolean resultado = false;

PosicaoTela p = t.getPosicaoAtual();


float toqueX = toque.getX() - (larguraViewPort / 2);

float toqueY = (alturaViewPort / 2) - toque.getY();



if ((toqueX >= p.getScreenLeft() && toqueX <=
p.getScreenRight()) &&
(toqueY >= p.getScreenBottom() && toqueY <=
p.getScreenTop())) {
resultado = true;

244

Manual do Indie Game Developer - Verso Android e iOS



return resultado;

iOS:
- (BOOL) toqueDentro: (OGBPCoordenada *) toque naTextura:
(OGMTTextura *) t
{
BOOL resultado = false;
OGMTPosicaoTela * p = t.posicaoAtual;
float toqueX = toque.x - (larguraViewPort / 2);
float toqueY = (alturaViewPort / 2) - toque.y;
if ((toqueX >= p.screenLeft && toqueX <= p.screenRight) &&
toqueY >= p.screenBottom && toqueY <= p.screenTop)) {
resultado = YES;
}
}

return resultado;

Isto garante que as texturas do modelo sempre ficaro dentro da tela, independentemente da proporo entre altura e largura.
Voltando ao Game Loop...
Para comear, eu acabei com o Thread extra. Agora, o Game loop invocado dentro do Render loop, ou seja, no incio do mtodo onDrawFrame().
E no s isso: eu tambm sincronizei o Game loop para que apresente uma
taxa de FPS prxima que informamos no XML. Veja as mudanas no mtodo onDrawFrame():
@Override
public void onDrawFrame(GL10 gl) {

// Controle de FPS:
long agora = System.currentTimeMillis();
long intervaloPassado = tempo;
if (ultimo > 0) {

intervaloPassado = agora - ultimo;

Captulo 8 -
Tcnicas Comuns em Games
245
}
if (intervaloPassado < tempo) {

// Colocar o Thread em sleep mode, pois falta

// algum tempo para iniciar
try {

Thread.sleep((long) (tempo - intervaloPassado));

intervaloPassado += (tempo - intervaloPassado);

} catch (InterruptedException e) {

Log.e(GAMELOOP, Interrompido o sleep: +
e.getMessage());

}
}

// Verifica se atingiu 1 segundo
contagemFrames++;
segundo += intervaloPassado;
if (segundo >= 1000) {
// atingiu 1 segundo
FPSmedio = contagemFrames;
segundo = 0;
contagemFrames = 1;
}

ultimo = System.currentTimeMillis();
update(intervaloPassado / 1000.0f);

Para comear, eu preciso saber quantos milissegundos se passaram, desde


a ltima vez que o onDrawFrame() foi invocado. Se foi um intervalo menor
que o nmero de milissegundos relativo taxa de FPS que eu estou usando,
eu calculo a diferena e boto o Thread para dormir. O campo tempo
calculado assim: ((1 / cenaCorrente.getFps()) * 1000).
Depois, eu verifico se j passou 1 segundo completo, ento eu exibo a
quantidade de frames renderizada naquele segundo.
Para terminar, o mtodo update() agora invocado pelo Render loop e eu
passo o intervalo de tempo, desde a ltima atualizao, como parmetro. Este
intervalo acrescido do tempo em que o Thread ficou dormindo. No mtodo update(), uso o diferencial de tempo (deltaT) para atualizar o mundo
Box2D (note que no tem mais o synchronized):
protected void update(float deltaT) {
/*

246

Manual do Indie Game Developer - Verso Android e iOS





}

* Atualiza o mundo Box2D e calcula a projeo


*/
world.step(deltaT,
cenaCorrente.getBox2d().getVelocityInterations(),
cenaCorrente.getBox2d().getPositionInterations());

O resultado um Game loop simples, porm com uma taxa de FPS consistente, que me permite desenvolver o game sem lags e aceleraes indevidas.
claro que a exibio do FPS acaba roubando algum tempo til do Game,
mas, em seu lugar ns certamente exibiramos alguma outra informao no
HUD, logo, a alterao esperada.
Vamos ver a implementao no iOS
No iOS, eu j tenho o tempo decorrido como uma propriedade da classe
GLKViewController, ento, eu movi o cdigo de verificao de FPS para
dentro do mtodo update:
- (void)update
{
float deltaT = [self timeSinceLastUpdate];
// Controle de FPS:

if (deltaT < tempo) {


// Colocar o Thread em sleep mode, pois falta
// algum tempo para iniciar
[NSThread sleepForTimeInterval:(tempo - deltaT)];
//NSLog(@ deltaT %f sleep: %f, deltaT, (tempo - deltaT));
}

// Verifica se atingiu 1 segundo


contagemFrames++;
segundo += deltaT;
if (segundo >= 1) {
// atingiu 1 segundo
FPSmedio = contagemFrames;
segundo = 0;
contagemFrames = 1;
}

Captulo 8 -
Tcnicas Comuns em Games
247
world->Step(deltaT,

cenaCorrente.box2d.velocityInterations,

cenaCorrente.box2d.positionInterations);
}

Ento o cdigo ficou mais simples.

Movimento
J vimos algumas opes para criar um Game loop, agora necessrio ver
como movimentar os GameObjects de forma consistente, evitando a acelerao indevida.
Se estamos utilizando Box2D, basta passarmos a taxa de FPS no momento
de invocar o mtodo step:
world.step(deltaT,

cenaCorrente.getBox2d().getVelocityInterations(),
cenaCorrente.getBox2d().getPositionInterations());

E se estivermos movimentando um objeto manualmente? Um mssil, por


exemplo? Como calcular a o deslocamento de forma consistente?
Para comear, temos que considerar a velocidade. A melhor medida metros por segundo (m/s). Por exemplo, 60 km por hora representam 16,66667
m/s. Como j temos a converso de 1 metro em pixels (usamos o campo proporcaoMetroTela para calcular isso), podemos estabelecer um padro de velocidade em m/s.
Tomando por base uma tela de 320 x 480 pixels de largura, com proporcaoMetroTela de 7, quantos segundos um objeto se movendo a 60 km/h demoraria para atravess-la?
Diagonal aproximadamente 577 pixels;
1 m = 577 / 7 = 82 pixels, aproximadamente;
480 pixels = 6 metros, aproximadamente;
60 km/h = 16,7 m/s, aproximadamente;
Logo, o tempo seria menos de meio segundo, ou seja: rpido demais.
Temos que ajustar o tempo de acordo com o que desejamos. Se nossa tela
tem 6 metros e queremos que o mssil demore 3 segundos, ento a velocidade deveria ser entre 2 e 3 m/s ou aproximadamente 8 km/h. Se voc quiser,
pode aumentar o valor da proporcaoMetroTela.
Lembre-se que, apesar de calcular o tempo de percurso com preciso, na
realidade a velocidade do objeto pode variar devido a outros fatores, sobre os

248

Manual do Indie Game Developer - Verso Android e iOS

quais no temos muito controle. Logo, uma estimativa de velocidade e de


tempo.
Ok, ento j sabemos como calcular a velocidade que nosso objeto precisa
desenvolver, agora, como fazer com que o Game loop respeite isso de forma
consistente? Uma maneira represar a atualizao at que o tempo seja
atingido. Vamos ver isso em pseudocdigo:
update(ultimaAtualizacao) {
tempo += (agora ultimaAtualizacao);
se (tempo >= 1 segundo) {

atualizarObjeto();

tempo = 0;
}
}

Este comportamento parece bom, no? S que daria um efeito parecido


com ponteiro de segundos, ou seja, o objeto aparenta se mover e parar constantemente. Uma maneira melhor seria:
update(ultimaAtualizacao) {
tempo = (1 segundo ) / (agora ultimaAtualizacao);
atualizarObjetos(tempo);
}

Agora, estamos movendo constantemente o objeto, mesmo que uma pequena frao de cada vez, dando a iluso de movimento contnuo e suave.
Nada como um exemplo
Vamos pensar em um outro exemplo: uma nave que atravessa a tela na horizontal, da esquerda para a direita, em velocidade constante relativa de 8 m/s.
O cdigo-fonte deste exemplo est em:
Android: ..\Codigo\OpenGLAndroid\movimentodroid.zip;
iOS: ..\Codigo\OpenGLiOS\MovimentoIOS.zip;
As imagens que estou usando so de domnio pblico e vieram todas do
site openclippart.org.

Captulo 8 -
Tcnicas Comuns em Games
249

Ilustrao 86: Exemplo de movimento controlado sem Box2D

Ilustrao 87: O mesmo exemplo no iOS

Neste exemplo, voc ver uma nave atravessar a tela, da esquerda para a
direita, repetidamente. A velocidade da nave 3 m/s, ela vai percorrer a distncia em cerca de 3 segundos, dependendo da proporo entre altura e largura
da tela. Se frao altura/largura for proporcional a 2/3, ento ela demorar
cerca de 3 segundos para atravessar a tela.
Eu poderia ter criado um novo base code, tirando o Box2D e simplificando tudo, mas achei melhor reusar o framework de game que temos, afinal, eu
posso misturar objetos animados com Box2D e objetos animados manualmente, o que comum em games. Ento, usei o mesmo base code do exemplo
anterior (GameLoop) e fiz algumas mudanas. Todas esto precedidas pelo
comentrio // @@@@ Alterao assim, voc saber onde eu modifiquei o cdigo-fonte.

250

Manual do Indie Game Developer - Verso Android e iOS

Para comear, eu criei uma nova propriedade nos meus GameObjects, que
est dentro do arquivo modeloExemplo.xml:
<objeto>
<!-- nave -->
<id>1</id>
<tipo>2</tipo>
<forma>1</forma>
<centro>

<x>0</x>

<y>0</y>
</centro>
<!-- Vamos alinhar a nave esquerda -->
<alinhar>2</alinhar>
<altura>0.5</altura>
<esticarAltura>false</esticarAltura>
<largura>1.5</largura>
<esticarLargura>false</esticarLargura>
<arquivoTextura>nave.png</arquivoTextura>
<fisicaBox2d>false</fisicaBox2d>
<densidade>1.0</densidade>
<atrito>0.1</atrito>
<coefRetribuicao>0.6</coefRetribuicao>
</objeto>

Eu tenho que alterar as classes que carregam o modelo, de modo a incluir


esta propriedade.
A propriedade fisicaBox2d afetar os seguintes pontos:
1. Deslocamento do centro, caso haja opo de alinhamento. Neste
caso, estou alinhando a nave esquerda da tela;
2. Clculo do VOB de vrtices;
3. Clculo do movimento;
4. Renderizao do GameObject;
O importante que no terei um objeto Box2D para me informar a posio
e o ngulo do objeto, logo, terei que pegar as coordenadas do Centro do GameObject, que j esto convertidas de acordo com a matriz de projeo, logo, no
necessrio multiplicar pela proporo Metro/Tela, ento, em vrios pontos
do cdigo-fonte eu tive que testar se o GO era animado pelo Box2D ou no.
Android: Classe OpenGLAvancadoRenderer:
Vrias alteraes dentro do mtodo onSurfaceChanged():
...

Captulo 8 -
Tcnicas Comuns em Games
251
case ALINHAMENTO_ESQUERDA:
go.setCentro(

new Coordenada(this.telaTopLeft.getX(),

go.getCentro().getY(),0.0f)

);
// @@@@ Alterao

if (go.isFisicaBox2D()) {
go.getB2dBody().setTransform(new
Vec2(this.
telaTopLeft.getX()

/ proporcaoMetroTela, go.getCentro().getY()),

0.0f);
}
break;
...
// @@@@ Alterao

if (go.isFisicaBox2D()) {
// Recalcula os tamanhos dos objetos


if (go.getFixture() != null) {
body.destroyFixture(go.getFixture());
}
...
if (go.getArquivoTextura() != null) {
Coordenada centro = null;

// @@@@ Alterao

if (go.isFisicaBox2D()) {

centro = new Coordenada(go.getCentro().getX() *
proporcaoMetroTela,

go.getCentro().getY() * proporcaoMetroTela,

go.getCentro().getZ()*proporcaoMetroTela);
}
else {

centro = go.getCentro();
}
...

Dentro do mtodo initBox2d():


...

252

Manual do Indie Game Developer - Verso Android e iOS


for (GameObject go : cenaCorrente.getObjetos()) {

// @@@@ Alterao para permitir objetos no animados
pelo Box2D:

if (go.isFisicaBox2D()) {
...

Dentro do mtodo onDrawFrame():


...
float posicaoX = 0.0f;
float posicaoY = 0.0f;
float angulo = 0.0f;
// @@@@ Alterao para permitir objetos no animados
pelo Box2D:

if (go.isFisicaBox2D()) {
posicaoX = go.getB2dBody().getTransform().position.x *
proporcaoMetroTela;
posicaoY = go.getB2dBody().getTransform().position.y *
proporcaoMetroTela;
angulo = go.getB2dBody().getAngle();
}
else {
posicaoX = go.getCentro().getX();
posicaoY = go.getCentro().getY();
}
Matrix.translateM(matrizModelo, 0, posicaoX, posicaoY, 0);
Matrix.rotateM(matrizModelo, 0, (float) (angulo * 57.2957795),

0, 0, 1);
...

Finalmente, dentro do mtodo update():


protected void update(float deltaT) {
/*
* Atualiza o mundo Box2D e calcula a projeo
*/

Captulo 8 -
Tcnicas Comuns em Games
253
world.step(deltaT,

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());
GameObject nave = cenaCorrente.getObjetos().get(0);
nave.getCentro().setX(nave.getCentro().getX()
+
(velocidadeMS * deltaT));
if (nave.getCentro().getX() > larguraViewPort) {

nave.getCentro().setX(origemX);
}
}

Esta alterao simplesmente calcula a nova posio da nave, que no est


sob controle do Box2D. Eu multiplico a velocidade em m/s pelo diferencial de
tempo de atualizao. A velocidade calculada no mtodo carregarCena():
...
diagonalTela = (float) (Math.pow(alturaViewPort, 2) + Math.
pow(larguraViewPort, 2));
diagonalTela = (float) Math.sqrt(diagonalTela);
proporcaoMetroTela = (float) (diagonalTela / cenaCorrente.
getBox2d().getProporcaoMetroTela());

// @@@@ Alterao:

velocidadeMS = 3.0f * proporcaoMetroTela;
...

No iOS:
Vrias alteraes dentro do mtodo recalcularAlinhamentos:
...
case ALINHAMENTO_ESQUERDA: {
go.centro = [[OGBPCoordenada alloc] initWithx:
telaTopLeft.x
y: go.centro.y
z: 0.0f];
// @@@@ Alterao:
if (go.fisicaBox2D) {
novaCoord.x = telaTopLeft.x / proporcaoMetroTela;

novaCoord.y = go.centro.y;

254

Manual do Indie Game Developer - Verso Android e iOS



go.b2dBody->SetTransform(novaCoord, go.b2dBody->
GetAngle());
}
break;
...
// @@@@ Alterao
if (go.fisicaBox2D) {
if (go.fixture != nil) {
body->DestroyFixture(go.fixture);
}
...
if (go.arquivoTextura != nil) {
// @@@@ Alterao
OGBPCoordenada * coordCentro = nil;
if (go.fisicaBox2D) {
OGBPCoordenada * coordCentro = [[OGBPCoordenada
alloc] initWithx:
(go.centro.x * proporcaoMetroTela)
y:(go.centro.y * proporcaoMetroTela)
z:(go.centro.z * proporcaoMetroTela)];

}
...

}
else {
coordCentro = go.centro;

Dentro do mtodo initBox2d():


...
for (OGBPGameObject * go in cenaCorrente.objetos) {
// @@@@ Alterao
if (go.fisicaBox2D) {
...

Captulo 8 -
Tcnicas Comuns em Games
255

Dentro do mtodo onDrawFrame():


...
// @@@@ Alterao
float posicaoX = 0.0f;
float posicaoY = 0.0f;
float angulo = 0.0f;
if (go.fisicaBox2D) {
posicaoX = go.b2dBody->GetPosition().x * proporcaoMetroTela;
posicaoY = go.b2dBody->GetPosition().y * proporcaoMetroTela;
angulo = go.b2dBody->GetAngle();
}
else {
posicaoX = go.centro.x;
posicaoY = go.centro.y;
}
matrizModelo
=
GLKMatrix4Translate(matrizModelo,
posicaoX, posicaoY, 0.0f);
matrizModelo = GLKMatrix4Rotate(matrizModelo, angulo,
0, 0, 1);
...

Finalmente, dentro do mtodo update():


protected void update(float deltaT) {
/*
* Atualiza o mundo Box2D e calcula a projeo
*/
world->Step(deltaT,

cenaCorrente.box2d.velocityInterations,

cenaCorrente.box2d.positionInterations);
// @@@@ Alterao
OGBPGameObject * nave = [cenaCorrente.objetos
objectAtIndex:0];

256

Manual do Indie Game Developer - Verso Android e iOS


nave.centro.x= (nave.centro.x + (velocidadeMS *
deltaT));
if (nave.centro.x > larguraViewPort) {
nave.centro.x = origemX;
}
}

Esta alterao simplesmente calcula a nova posio da nave, que no est


sob controle do Box2D. Eu multiplico a velocidade em m/s pelo diferencial de
tempo de atualizao. A velocidade calculada no mtodo carregarCena():
...

diagonalTela = pow(alturaViewPort, 2)
pow(larguraViewPort, 2);
diagonalTela = sqrt(diagonalTela);
proporcaoMetroTela = (float) (diagonalTela /
cenaCorrente.box2d.proporcaoMetroTela);

// @@@@ Alterao:
...

velocidadeMS = 3.0f * proporcaoMetroTela;

Estou utilizando 3 m/s, e testei em vrios dispositivos, tanto Android como


iOS. O tempo de percurso da tela fica em torno de 3 segundos, com pequena
variao, nada que atrapalhe o gameplay. Assim, continuo podendo criar
jogos multiplataforma, que apresentem a mesma jogabilidade, independentemente do tamanho (e proporo) da tela, ou da velocidade do processador.

Efeito de paralaxe
Paralaxe um conceito de astronomia, mas, em games, uma tcnica para
aumentar a iluso de profundidade em jogos 2D. Tambm conhecido como
Parallax scrolling.
Nesta tcnica, os objetos que esto em planos mais afastados (com relao
ao observador) se deslocam em velocidade menor que os objetos que esto em
planos mais prximos (do observador).
claro que voc pode conseguir isso facilmente em um jogo 3D, afinal, a
prpria projeo dos elementos vai lhe proporcionar o efeito de paralaxe, caso
voc decida seguir um jogador com a cmera. Mas em jogos 2D, um
pouco mais complicado.

Captulo 8 -
Tcnicas Comuns em Games
257

Tcnicas de paralaxe
Existem algumas tcnicas para conseguir o efeito de paralaxe:
Camadas
Todos os sistemas grficos possuem o conceito de camadas. Uma camada
um contexto grfico, no qual podemos desenhar, e elas podem ser sobrepostas,
formando um sanduche que a tela final. Ns podemos deslocar ligeiramente as camadas com diferentes unidades, criando o efeito de paralaxe.
uma tcnica interessante, s que exige maior conhecimento e investimento no sistema grfico nativo de cada plataforma.

Ilustrao 88: Paralaxe com camadas

Camadas de GameObjects
Outra maneira mais simples criar camadas lgicas de GameObjects, separando-os em grupos: primeiro plano, segundo plano e terceiro plano. Normalmente, os objetos em primeiro plano so aqueles com os quais o jogador
pode interagir e tambm respondem a eventos, colises etc. Os de outros planos servem apenas como cenrio.
Nesta tcnica, s existe uma nica camada fsica de desenho, mas os objetos so animados ANTES da renderizao, com velocidades diferentes. Temos uma velocidade para o primeiro plano, uma menor para o segundo plano
e uma ainda menor para o terceiro plano. Na verdade, podemos ter quantos
planos desejarmos.
Esta tcnica independente de plataforma nativa e pode ser facilmente
implementada.

258

Manual do Indie Game Developer - Verso Android e iOS

Exemplo de paralaxe com camadas de GameObjects


Como a inteno deste livro ser multiplataforma, escolhi a tcnica de
camadas lgicas de GameObjects e criei um exemplo simples.

Ilustrao 89: O exemplo em plataforma Android

Ilustrao 90: O exemplo em plataforma iOS

O carro se move para o sentido direito da tela, mas, na verdade, est parado. S criei uma iluso para que as rodas paream se mover (duas imagens
sendo alternadas a cada 2 frames). As rvores e postes esto em segundo plano
e se alternam, aleatoriamente, e as casas, prdios e o morro esto em terceiro
plano, tambm se alternando aleatoriamente.
O cdigo-fonte dos exemplos est em:
Android: ..\Codigo\OpenGLAndroid\paralaxdroid.zip;
iOS: ..\Codigo\OpenGLiOS\ParalaxIOS.zip;

Captulo 8 -
Tcnicas Comuns em Games
259

Implementao geral
Eu peguei o exemplo Movimento e fiz as mudanas. Para comear, eu
criei GameObjects invisveis e animados por fora do Box2D:
<objeto>
<!-- carro -->
<!-- Est no primeiro plano: z = 0 -->
<id>1</id>
<tipo>2</tipo>
<forma>1</forma>
<centro>

<x>0</x>

<y>0</y>

<z>0</z>
</centro>
<!-- Vamos alinhar ao cho -->
<!-- inclumos alinhamento da base ao cho: tipo 5 -->
<alinhar>5</alinhar>
<altura>1.5</altura>
<esticarAltura>false</esticarAltura>
<largura>3.0</largura>
<esticarLargura>false</esticarLargura>
<arquivoTextura>carro.png</arquivoTextura>
<fisicaBox2d>false</fisicaBox2d>
<visivel>false</visivel>
<densidade>1.0</densidade>
<atrito>0.1</atrito>
<coefRetribuicao>0.6</coefRetribuicao>
</objeto>

Os objetos agora tm a coordenada z em seu centro. Aqueles que esto


com z = 0, esto no primeiro plano, os que esto com z = -1, esto em segundo
plano e, finalmente, aqueles que esto com z = -2, esto em terceiro plano.
Os objetos com a propriedade visivel = false, no sero renderizados pelo Render loop. Eu terei que ativar os objetos a serem renderizados
manualmente.
Eu aproveitei a velocidadeMS, criada no exemplo Movimento e calculei
em 2 m/s. Como eu j calculo quanto vale 1 metro, eu calculei a velocidade
bsica em 2 vezes o tamanho de 1 metro em pixels, por segundo.
No mtodo de atualizao (update), eu animo separadamente os objetos, de acordo com a camada. Os que esto no terceiro plano, desloco para
a esquerda com metade da velocidadeMS. Os que esto em segundo plano,

260

Manual do Indie Game Developer - Verso Android e iOS

eu desloco com a prpria velocidadeMS. E os que esto em primeiro plano,


apenas substituo a imagem do carro a cada 2 frames.
No mtodo de renderizao, eu desenho separadamente os objetos por camada. Na verdade, no precisava disto, pois bastava usar o filtro de profundidade do OpenGL (glEnable(GL_DEPTH_TEST) e glDepthMask(GL_
TRUE)), s que isto causa um problema na plataforma Android, pois as
camadas de transparncia das texturas (alfas) tentem se sobrepor. Por exemplo,
criam pontas como se as texturas no fossem transparentes. Ento, como o
filtro de profundidade est desligado, eu tenho que renderizar os objetos camada por camada, comeando com a mais afastada.
Mas importante calcular os vrtices considerando z = 0. Caso contrrio,
cria um efeito indesejado ao aplicarmos a cmera.
Separao de GameObjects por camada
Agora, eu no tenho mais todos os GOs dentro da coleo de objetos da
cenaCorrente. Eu tenho uma coleo para cada plano. Ento, tenho que animar os objetos de acordo com seu plano. Quando um objeto sai de cena (seu
limite direito menor que o limite esquerdo da tela), eu crio um novo GO,
escolhendo, aleatoriamente, na coleo de GOs do plano correspondente. Eu
adiciono o novo GO a uma coleo especfica (adicionados), da qual eu
posso remover posteriormente.
Para adicionar um novo GO, eu preciso clonar o objeto. Se eu apenas
usar a referncia, mudarei as propriedades do objeto original, e isso eu no
quero fazer.
Ento, tive que empregar tcnicas de clonagem de objetos, tanto em Android (Cloneable) como em iOS (NSMutableCopying).
Quando eu vou desenhar os objetos, eu pego os de segundo e terceiro planos da coleo adicionados, que mutvel.
Criar uma pista e alinhar a base dos objetos a ela
Alinhar e posicionar objetos com o OpenGL meio complicado. Eu criei
um novo tipo de alinhamento para os GameObjects: 5 ALINHAMENTO_
BASE_CHAO. Se um GO tem esse tipo de alinhamento, depois de calcular
seus vrtices, eu movimento seu centro para que a base fique sobre uma linha
que fica 50% abaixo da linha de centro da tela. Alis, eu alinho o objeto pista com o topo colado nessa linha.
Assim, a pista fica alinhada com o topo em 50% da metade inferior da tela,
e os objetos com ALINHAMENTO_BASE_CHAO ficam com a base sobre
a pista.

Captulo 8 -
Tcnicas Comuns em Games
261

Se voc quiser usar a parte inferior da tela (alinhar no limite fsico de baixo), vai ter muito trabalho. Veja o prximo exemplo.

Implementao Android
Vamos comear com as alteraes na classe GameObject:
public
class
GameObject
implements
Comparable
<GameObject>, Cloneable {

...
public static enum ALINHAMENTO_GO {

ALINHAMENTO_NENHUM,

ALINHAMENTO_CHAO,

ALINHAMENTO_ESQUERDA,

ALINHAMENTO_DIREITA,

ALINHAMENTO_TETO,

ALINHAMENTO_BASE_CHAO
};

...
private boolean visivel;
...
@Override
public Object clone() throws CloneNotSupportedException {

GameObject copia = new GameObject();

copia.setAlinhamento(this.getAlinhamento());

copia.setAltura(this.getAltura());

copia.setArquivoTextura(this.getArquivoTextura());

copia.setAtrito(this.getAtrito());

copia.setB2dBody(this.getB2dBody());

copia.setCentro(this.getCentro());

copia.setCoefRetribuicao(this.getCoefRetribuicao());

copia.setDensidade(this.getDensidade());

copia.setEsticarAltura(this.isEsticarAltura());

copia.setEsticarLargura(this.isEsticarLargura());

copia.setFisicaBox2D(this.isFisicaBox2D());

copia.setFixture(this.getFixture());

copia.setForma(this.getForma());

copia.setHandlerTextura(this.getHandlerTextura());

copia.setId(this.getId());

copia.setJaCalculado(this.isJaCalculado());

262

Manual do Indie Game Developer - Verso Android e iOS








}

copia.setLargura(this.getLargura());
copia.setTipo(this.getTipo());
copia.setVisivel(this.isVisivel());
copia.setVobTextura(this.getVobTextura());
copia.setVobVertices(this.getVobVertices());
return copia;

A principal a implementao da interface Cloneable, que exige a criao de um mtodo para clonar um GameObject.
Agora, vamos ver as modificaes no mtodo onSurfaceChanged:
// Agora, vamos separar os objetos de segundo e terceiro
planos
segundoPlano = new ArrayList<GameObject>();
terceiroPlano = new ArrayList<GameObject>();
for (Iterator<GameObject> iterator = cenaCorrente.getObjetos().
iterator();

iterator.hasNext();) {
GameObject go = iterator.next();
float alturaGo = (go.isJaCalculado()) ? go.getAltura() :
go.getAltura() *

proporcaoMetroTela;
float larguraGo = (go.isJaCalculado()) ? go.getLargura() :
go.getLargura() *

proporcaoMetroTela;
float linhaBase = this.telaBottomRight.getY() * 0.50f;
if (go.getAlinhamento() == ALINHAMENTO_GO.ALINHAMENTO_
BASE_CHAO) {

// Alinha todos na base

go.getCentro().setY(linhaBase + alturaGo/2);
}

if (go.getId() == 11) {

go.getCentro().setY(linhaBase - alturaGo/2);
}

Captulo 8 -
Tcnicas Comuns em Games
263
if (go.getCentro().getZ() == SEGUNDO_PLANO) {

segundoPlano.add(go);
}
else if (go.getCentro().getZ() == TERCEIRO_PLANO) {

terceiroPlano.add(go);
}

if (go.getId() == 1) {

try {

carro1 = (GameObject) go.clone();
carro1.setVisivel(true);

carroAdesenhar = cenaCorrente.getObjetos().
indexOf(go);

} catch (CloneNotSupportedException e) {
e.printStackTrace();

}
}
else if (go.getId() == 2) {

try {

carro2 = (GameObject) go.clone();
carro2.setVisivel(true);

} catch (CloneNotSupportedException e) {
e.printStackTrace();

}
}
}
// Por segurana, no d para remover um objeto de uma
coleo durante a iterao
for (GameObject go : segundoPlano) {
cenaCorrente.getObjetos().remove(go);
}
for (GameObject go : terceiroPlano) {
cenaCorrente.getObjetos().remove(go);
}

264

Manual do Indie Game Developer - Verso Android e iOS


// Adiciona alguns objetos ao terceiro e segundo planos:
addNewObject(SEGUNDO_PLANO);
addNewObject(TERCEIRO_PLANO);
}

Infelizmente, nem o Java e nem o Objective C resolveram um problema


simples: como modificar os membros de uma coleo, durante a iterao.
um problema simples: estamos navegando em uma coleo e queremos remover um dos objetos. Isso no deveria ser problema, mas . Se voc quiser adicionar ou remover membros de uma coleo, faa em outro loop. Voc pode
usar colees auxiliares para isto, o que fao em certos momentos.
Eu s posso fazer as modificaes na posio dos objetos aps ter carregado os vetores de vrtices, ou seja, no final do mtodo onSurfaceChanged.
Note que eu clonei as imagens dos carros.
E eu tive que fazer uma coisa que no gosto: usar nmero mgico. Eu
preciso alinhar a pista com o topo na linha base, logo, eu procurei o objeto
com id = 11. Na verdade, isto afeta o reuso deste cdigo. Em um framework
de verdade, eu faria essas modificaes hardcode em uma linguagem de
script, associada a um evento, como Lua, por exemplo.
Finalmente, eu adicionei um objeto em cada plano.
Vamos ver as modificaes no mtodo update():
protected void update(float deltaT) {
/*

* Atualiza o mundo Box2D e calcula a projeo

*/

world.step(deltaT,

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());

// Anima objeto de primeiro plano

numeroFrames++;

if (numeroFrames > 2) {

// Troca imagem do carro

numeroFrames = 0;

this.carroFinal = (this.trocou) ? carro2 : carro1;

this.trocou = !this.trocou;

cenaCorrente.getObjetos().set(this.
carroAdesenhar,carroFinal);

}

Captulo 8 -
Tcnicas Comuns em Games
265

// Loop dos objetos de segundo e terceiro plano:


int adicionarSegundoPlano = 0;

int adicionarTerceiroPlano = 0;

for (GameObject go : adicionados) {

float antesX = go.getCentro().getX();

if (go.getCentro().getZ() == TERCEIRO_PLANO)
{

// Anima objeto de terceiro plano

go.getCentro().setX(go.getCentro().getX() ((velocidadeMS * 0.5f) *
deltaT));

}

else if (go.getCentro().getZ() == SEGUNDO_PLANO) {

// Anima objeto de segundo plano

go.getCentro().setX(go.getCentro().getX() ((velocidadeMS) *
deltaT));

}


float direita = go.getCentro().getX() + ((go.getLargura() *

proporcaoMetroTela) / 2);

if (direita < this.telaTopLeft.getX()) {

if (go.getCentro().getZ() == TERCEIRO_PLANO) {
adicionarTerceiroPlano++;

}

else {
adicionarSegundoPlano++;

}

}

}

List<GameObject> aRemover = new ArrayList<GameObject>();
for (GameObject go : adicionados) {

float direita = go.getCentro().getX() + ((go.getLargura() *

proporcaoMetroTela) / 2);

if (direita < this.telaTopLeft.getX()) {

aRemover.add(go);

}

266

Manual do Indie Game Developer - Verso Android e iOS



for (GameObject go : aRemover) {

adicionados.remove(go);

}


for (int x=0; x < adicionarSegundoPlano; x++) {

addNewObject(SEGUNDO_PLANO);

}


for (int x=0; x < adicionarTerceiroPlano; x++) {

addNewObject(TERCEIRO_PLANO);

}

}

protected void addNewObject(float plano) {
List<GameObject> lista = plano == TERCEIRO_PLANO ?
terceiroPlano : segundoPlano;

int posicao = random.nextInt(lista.size());
GameObject go = null;

try {

go = (GameObject) lista.get(posicao).clone();

} catch (CloneNotSupportedException e) {

Log.e(CLONE, CloneNotSupported GameObject);

}


go.setId(plano == TERCEIRO_PLANO ? 300 : 200);

go.getCentro().setX(this.telaBottomRight.getX()
+ ((go.getLargura() *

proporcaoMetroTela) / 2));

go.setVisivel(true);

adicionados.add(go);
}

Quando um objeto sai da tela visvel, eu adiciono um novo, escolhendo,


aleatoriamente, dentre os objetos daquele nvel disponveis (segundoPlano ou
terceiroPlano). Infelizmente, eu s posso modificar os membros de uma coleo fora de sua iterao, ento eu uso alguns artifcios para conseguir isso.
Esta a razo de eu ter vrios loops e colees separadas.
Agora, vamos ver como ficou a renderizao dos objetos, separados de
acordo com o plano, o que acontece no mtodo onDrawFrame():

Captulo 8 -
Tcnicas Comuns em Games
267
desenharTextos();
// Vamos renderizar os Game Objects de terceiro plano:
for (GameObject go : adicionados) {
if (go.getCentro().getZ() == TERCEIRO_PLANO) {
desenharObjeto(go);
}
}
// Agora, os de segundo plano:
for (GameObject go : adicionados) {
if (go.getCentro().getZ() == SEGUNDO_PLANO) {
desenharObjeto(go);
}
}
// Finalmente, os de primeiro plano:
for (GameObject go : cenaCorrente.getObjetos()) {
if (go.getArquivoTextura() != null && go.isVisivel())
{
desenharObjeto(go);
}
}

Eu separei o cdigo de renderizao de GameObjects e o invoco em cada


loop.

Implementao iOS
A implementao iOS foi baseada no mesmo projeto, MovimentoIOS.
As modificaes na interface da classe OGBPGameObject foram:
...
typedef enum {
ALINHAMENTO_NENHUM,
ALINHAMENTO_CHAO,
ALINHAMENTO_ESQUERDA,
ALINHAMENTO_DIREITA,
ALINHAMENTO_TETO,

268

Manual do Indie Game Developer - Verso Android e iOS


ALINHAMENTO_BASE_CHAO
} ALINHAMENTO_GO;
@interface OGBPGameObject : NSObject <NSMutableCopying>
...
@property BOOL visivel;
...

E as modificaes na implementao foram:

...
@synthesize visivel;
...
- (id) mutableCopyWithZone:(NSZone *)zone
{
OGBPGameObject *copiaGo =
[[OGBPGameObject allocWithZone:zone] init];
copiaGo.alinhamento = self.alinhamento;
copiaGo.altura = self.altura;
copiaGo.arquivoTextura = [self.arquivoTextura
copy];
copiaGo.atrito = self.atrito;
copiaGo.b2dBody = self.b2dBody;
copiaGo.centro = [self.centro mutableCopy];
copiaGo.coefRetribuicao = self.coefRetribuicao;
copiaGo.densidade = self.densidade;
copiaGo.esticarAltura = self.esticarAltura;
copiaGo.esticarLargura = self.esticarLargura;
copiaGo.fixture = self.fixture;
copiaGo.forma = self.forma;
copiaGo.idGO = self.idGO;
copiaGo.largura = self.largura;
copiaGo.tipo = self.tipo;
copiaGo.glProps = [self.glProps mutableCopy];
copiaGo.fisicaBox2D = self.fisicaBox2D;

return copiaGo;
}

O mtodo mutableCopyWithZone o equivalente ao clone() do Java.


As propriedades que so objetos Cocoa Touch, eu tenho que usar copy ou
mutableCopy, dependendo se eu quero alterar o clone ou no. Como eu

Captulo 8 -
Tcnicas Comuns em Games
269

tenho duas propriedades que so classes que eu criei, eu tenho que implementar o protocolo NSMutableCopying nelas tambm (OGBPCoordenada
e OGBPGLProps).
Agora, vamos ver as modificaes no mtodo recalcularAlinhamentos,
que o equivalente ao onSurfaceChanged(), do Android:
segundoPlano = [[NSMutableArray alloc] init];
terceiroPlano = [[NSMutableArray alloc] init];
for (OGBPGameObject * go in cenaCorrente.objetos) {
float alturaGo = go.altura * proporcaoMetroTela;
float larguraGo = go.largura * proporcaoMetroTela;
float linhaBase = telaBottomRight.y * 0.50f;
if (go.alinhamento == ALINHAMENTO_BASE_CHAO) {
// Alinha todos na base
go.centro.y = linhaBase + alturaGo/2;
}
if (go.idGO == 11) {
go.centro.y = linhaBase - alturaGo/2;
}
if (go.centro.z == SEGUNDO_PLANO) {
[segundoPlano addObject:go];
}
else if (go.centro.z == TERCEIRO_PLANO) {
[terceiroPlano addObject:go];
}
if (go.idGO == 1) {
carro1 = [go mutableCopy];
carro1.visivel = YES;
carroAdesenhar = [cenaCorrente.objetos
indexOfObject:go];
}
else if (go.idGO == 2) {
carro2 = [go mutableCopy];
carro2.visivel = YES;
}
}

270

Manual do Indie Game Developer - Verso Android e iOS


// Por segurana, no d para remover um objeto de uma
coleo durante a iterao
for (OGBPGameObject * go in segundoPlano) {
[cenaCorrente.objetos removeObject:go];
}
for (OGBPGameObject * go in terceiroPlano) {
[cenaCorrente.objetos removeObject:go];
}
// Adiciona alguns objetos ao terceiro e segundo planos:
[self addNewObject: SEGUNDO_PLANO];
[self addNewObject: TERCEIRO_PLANO];

O cdigo muito semelhante ao do mtodo onSurfaceChanged(), da


verso Android. A maneira de fazer as coisas um pouco diferente, como a
clonagem dos objetos, por exemplo.
As modificaes no mtodo update foram:
- (void)update
{
float deltaT = [self timeSinceLastUpdate];
// Controle de FPS:
if (deltaT < tempo) {

// Colocar o Thread em sleep mode, pois
falta

// algum tempo para iniciar
[NSThread sleepForTimeInterval:(tempo - deltaT)];
//NSLog(@ deltaT %f sleep: %f, deltaT, (tempo
- deltaT));
}
world->Step(deltaT,

cenaCorrente.box2d.velocityInterations,

cenaCorrente.box2d.positionInterations);
// Anima objeto de primeiro plano
contagemFrames++;

Captulo 8 -
Tcnicas Comuns em Games
271
if (contagemFrames > 2) {
// troca imagem do carro
contagemFrames = 0;
carroFinal = (self->trocou) ? carro2 : carro1;
self->trocou = !self->trocou;
[cenaCorrente.objetos setObject:carroFinal at
IndexedSubscript:carroAdesenhar];
}
// Loop dos objetos de segundo e terceiro plano:
int adicionarSegundoPlano = 0;
int adicionarTerceiroPlano = 0;
for (OGBPGameObject * go in adicionados) {
float antesX = go.centro.x;
if (go.centro.z == TERCEIRO_PLANO) {
// Anima objeto de terceiro plano
go.centro.x = go.centro.x - ((velocidadeMS
* 0.5f) * deltaT);
}
else if (go.centro.z == SEGUNDO_PLANO) {
// Anima objeto de segundo plano
go.centro.x = go.centro.x - ((velocidadeMS)
* deltaT);
}
float direita = go.centro.x + ((go.largura *
proporcaoMetroTela) / 2);
if (direita < telaTopLeft.x) {
if (go.centro.z == TERCEIRO_PLANO) {
adicionarTerceiroPlano++;
}
else {
adicionarSegundoPlano++;
}
}
}
NSMutableArray * aRemover = [[NSMutableArray alloc]
init];
for (OGBPGameObject * go in adicionados) {
float direita = go.centro.x + ((go.largura *

272

Manual do Indie Game Developer - Verso Android e iOS


proporcaoMetroTela) / 2);
if (direita < telaTopLeft.x) {
[aRemover addObject:go];
}
}
for (OGBPGameObject * go in aRemover) {
[adicionados removeObject:go];
}
for (int x=0; x < adicionarSegundoPlano; x++) {
[self addNewObject: SEGUNDO_PLANO];
}
for (int x=0; x < adicionarTerceiroPlano; x++) {
[self addNewObject: TERCEIRO_PLANO];
}
}
- (void) addNewObject: (float) plano
{
NSMutableArray * lista = plano == TERCEIRO_PLANO ?
terceiroPlano : segundoPlano;
int posicao = arc4random() % ([lista count]);
OGBPGameObject * go = nil;
go = [[lista objectAtIndex:posicao] mutableCopy];
go.idGO = plano == TERCEIRO_PLANO ? 300 : 200;
go.centro.x = telaBottomRight.x + ((go.largura *
proporcaoMetroTela) / 2);
go.visivel = YES;
[adicionados addObject:go];
}

A implementao tambm muito semelhante da verso Android. Note


como eu obtenho um nmero aleatrio com a funo arc4random(). E tambm como uso o mtodo mutableCopy para obter clones dos GameObjects.
A renderizao no mtodo drawInRect tambm ficou semelhante:
- (void)glkView:(GLKView
rect

*)view

drawInRect:(CGRect)

Captulo 8 -
Tcnicas Comuns em Games
273
{
...
// Vamos desenhar os indicadores dinmicos:
[self desenharTextos];
// Vamos renderizar os Game Objects de terceiro
plano:
for (OGBPGameObject *go in adicionados) {
if (go.centro.z == TERCEIRO_PLANO) {
[self desenharObjeto:go];
}
}
// Agora, os de segundo plano:
for (OGBPGameObject *go in adicionados) {
if (go.centro.z == SEGUNDO_PLANO) {
[self desenharObjeto:go];
}
}
// Finalmente, os de primeiro plano:
for (OGBPGameObject *go in cenaCorrente.objetos) {
if (go.arquivoTextura != nil && go.visivel) {
[self desenharObjeto:go];
}
}

Games do tipo plataforma


Games plataforma so aqueles em que o jogador deve correr e saltar em
2D, movendo-se de uma plataforma para outra. Exemplos clssicos so:
As sries Sonic The Hedgehog, da SEGA, e Super Mario, da Nintendo.
Embora sejam exemplos antigos, games plataforma ainda so desenvolvidos
e vendidos hoje em dia. Temos alguns bons exemplos, citados no filme: Indie
Game: The Movie (http://www.indiegamethemovie.com/), como: Super Meat
Boy, da Team Meat (http://supermeatboy.com/), Braid, da Number None
(http://braid-game.com/) e o sensacional Fez, da Polytron Corporation (http://
fezgame.com/) .

274

Manual do Indie Game Developer - Verso Android e iOS

Todos os trs jogos do filme (Indie Game: The Movie) so muito bons e
divertidos. Porm, o Fez, na minha opinio, sensacional! O criador, Phil
Fish, conseguiu inovar em um game plataforma, pois acrescentou a possibilidade de girarmos o game no eixo y.
Todos os jogos plataforma tm algumas coisas em comum:
So 2D;
O cenrio composto de obstculos e plataformas, entre as quais o
jogador pode pular;
Normalmente, a cmera centrada no PlayerObject, ou em seu
entorno.
Criar um game plataforma com esse framework que fizemos bem simples. Para comear, o PlayerObject (O GameObject controlado pelo jogador) deve poder saltar sem ficar quicando. Podemos conseguir isso zerando
o coeficiente de retribuio da nossa configurao. Depois, ele deve ter um
tamanho compatvel com a tela. Se o criarmos grande demais, teremos dificuldade em faz-lo saltar entre as plataformas.
O cenrio de um game plataforma se movimenta em funo do PlayerObject (PO), logo, podem existir partes ocultas que s aparecem quando o jogador se aproxima. como deslizssemos o mundo com a mo, enquanto
o observamos atravs de uma lente.
Em nossos exemplos com a bola, sempre usamos cho, teto e paredes, s que sem textura associada. Isso criaria um efeito fantasmagrico,
pois o PO fica batendo em coisas invisveis (vamos mostrar isso no exemplo).
Ento, as plataformas devem ter uma textura associada. E, se possuem textura,
ns temos que posicion-las ao final, depois de calcular seus vrtices, mantendo o centro como referncia.
Outro problema posicionar a cmera... No OpenGL no existe cmera, que apenas uma matriz de transformao que multiplicamos pela matriz
de modelo. Quando usamos cmera (gluLookAt), criamos mais um elemento
para atrapalhar o posicionamento do game. Ento, resolvi retirar essa varivel da equao, tornando a matriz cmera igual a identidade.
Simplesmente eu vou manipular a matriz de projeo a cada atualizao,
focando o centro na posio do nosso PlayerObject.
Eu fiz um exemplo simples e voc pode usar qualquer modelo. Usei a mesma bola que usamos nos exemplos anteriores, alterando seu tamanho e seu
coeficiente de retribuio para zero:

Captulo 8 -
Tcnicas Comuns em Games
275
<objeto>
<!-- Bola -->
<id>6</id>
<tipo>2</tipo>
<forma>1</forma>
<centro>

<x>0.0</x>

<y>0.0</y>

<z>0.0</z>
</centro>
<alinhar>0</alinhar>
<altura>0.5</altura>
<esticarAltura>false</esticarAltura>
<largura>0.5</largura>
<esticarLargura>false</esticarLargura>
<arquivoTextura>bola.png</arquivoTextura>
<densidade>4.0</densidade>
<atrito>0.1</atrito>
<coefRetribuicao>0.0</coefRetribuicao>
</objeto>

O cdigo-fonte dos exemplos est em:


Android: ..\Codigo\OpenGLAndroid\pargldroid.zip;
iOS: ..\Codigo\OpenGLiOS\ParGLIOS.zip;
Eis as imagens dos exemplos executando em Android e iOS:

Ilustrao 91: Exemplo de game plataforma Android

276

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 92: Exemplo de game plataforma iOS

A principal alterao no cdigo foi modificar a posio dos objetos. Como


todos os GameObjects so animados pelo Box2D e possuem textura, eu s
posso reposicion-los aps calcular os vrtices e, mesmo assim, modificando
o centro.
E, claro, eu tive que desligar o filtro de profundidade do OpenGL, como
fiz no exemplo anterior, pois d problema.

Implementao Android
Vamos eliminar a matrizCamera, que gerada no final do mtodo onSurfaceCreated(), substituindo-a pela matriz identidade:
Matrix.setIdentityM(matrizCamera, 0);

E vamos modificar nossa matriz de projeo, no mtodo onSurfaceChanged():


Matrix.orthoM(matrizProjecao, 0,




-width
width
-height
height
-1, 1);

/
/
/
/

2,
2,
2,
2,

E no mtodo update, eu recalculo a matriz de projeo:


float camX = bola.getB2dBody().getTransform().position.x *
proporcaoMetroTela;
float camY = bola.getB2dBody().getTransform().position.y *
proporcaoMetroTela;

Captulo 8 -
Tcnicas Comuns em Games
277
Matrix.setIdentityM(matrizProjecao, 0);
Matrix.orthoM(matrizProjecao, 0, camX - larguraViewPort/2,

camX + larguraViewPort/2,

camY - alturaViewPort/2,

camY + alturaViewPort/2,
-1, 1);

A posio da cmera ser o centro do meu PlayerObject (a bola), ento,


eu centralizo a minha projeo nela, tanto na altura, como na largura. Assim,
a bola sempre estar no centro da projeo.
Um ponto importante o posicionamento das plataformas. Elas so objetos Box2D e OpenGL ES, logo, eu tenho que recalcular os seus centros aps
criar os vrtices. Eu sobrescrevi o mtodo onSurfaceChanged() dentro da
classe Renderer para facilitar as coisas:
public class Renderer extends OpenGLAvancadoRenderer {

public Renderer(Context context) throws Exception {


super(context);
}

@Override
public void onSurfaceChanged(GL10 gl, int width,
int height) {
super.onSurfaceChanged(gl, width, height);
GameObject plataforma = new GameObject();
plataforma.setId(5);
int ix = cenaCorrente.getObjetos().
indexOf(plataforma);
plataforma = cenaCorrente.getObjetos().get(ix);
plataforma.getCentro().setX(2.5f);
plataforma.getCentro().setY(-0.5f);
plataforma.getB2dBody().setTransform(new Vec2
(plataforma.getCentro().getX(),
plataforma.getCentro().getY()),
0.0f);

GameObject chao = new GameObject();
chao.setId(7);
ix = cenaCorrente.getObjetos().indexOf(chao);
chao = cenaCorrente.getObjetos().get(ix);
chao.getCentro().setY(-2.5f);

278

Manual do Indie Game Developer - Verso Android e iOS


chao.getB2dBody().setTransform(new
getCentro().getX(),

chao.getCentro().getY()),
0.0f);
}

Vec2(chao.

No modifique os centros dos objetos dentro do arquivo de modelo de


game (XML), pois isto afetar o posicionamento dos vrtices, gerando distores. Talvez seja melhor alterar o framework para s posicionar os centros
aps calcular os vrtices, mas, neste caso basta posicionar ao final do mtodo
onSurfaceChanged().
Voc ter que construir seu cenrio para cada nvel, posicionando as plataformas de acordo com seu desenho. Pode at posicionar inimigos ou barreiras
em cada uma delas.
Para que a bola no casse no abismo, eu mantive as paredes, o teto
e o cho dos exemplos anteriores, que so invisveis (sem textura). Isto cria
uma barreira fantasmagrica e inexplicvel, que mantm a bola presa. Eu
mantive os objetos invisveis para que voc veja o efeito em um game final.
Tudo tem que ter explicao, logo, a melhor sada criar um campo de fora
ou algo do gnero, com a devida textura.
S para finalizar, eu modifiquei o pulo do GO, no mtodo toque, da
classe Renderer:
Body bola = go.getB2dBody();
Vec2 forca =

new Vec2(10.0f * bola.getMass(), 50.0f * bola.
getMass());
Vec2 posicao = bola.getWorldCenter().add(new Vec2
(0.0f,0.0f));

Eu diminu a fora aplicada ao eixo x e mudei a posio da fora para o


centro de massa da bola.
Em um game real, voc vai querer saber a direo e a distncia do toque e
arrasto, de modo a calcular a fora aplicada no pulo.

Implementao iOS
Substituir matriz de cmera pela matriz identidade:
matrizCamera = GLKMatrix4Identity;

Captulo 8 -
Tcnicas Comuns em Games
279

Modificar nossa matriz de projeo, no mtodo recalcularAlinhamentos:


matrizProjecao = GLKMatrix4MakeOrtho(-larguraViewPort /
matrizProjecao = GLKMatrix -larguraViewPort /
matrizProjecao = GLKMatrMakeOho(--alturaViewPort /
matrizProjecao = GLKMatrkeOrtho(- alturaViewPort /
-1, 1);

2,
2,
2,
2,

Recalcular a matriz de projeo no mtodo update:


floatcamX=bolaGO.b2dBody->GetPosition().x*proporcaoMetroTela;
floatcamY=bolaGO.b2dBody->GetPosition().y*proporcaoMetroTela;
matrizProjecao = GLKMatrix4Identity;
matrizProjecao = GLKMatrix4MakeOrtho(camX - larguraViewPort / 2,
camX + larguraViewPort / 2,
camY - alturaViewPort / 2,
camY + alturaViewPort / 2,
-1, 1);

Posicionar as plataformas no final do mtodo recalcularAlinhamentos:


OGBPGameObject * plataforma = [[OGBPGameObject alloc] init];
plataforma.idGO = 5;
int ix = [cenaCorrente.objetos indexOfObject:plataforma];
plataforma = [cenaCorrente.objetos objectAtIndex:ix];
plataforma.centro.x = 2.5f;
plataforma.centro.y = -0.5f;
plataforma.b2dBody->SetTransform(b2Vec2(plataforma.centro.x,
plataforma.centro.y),
0.0f);
OGBPGameObject * chao = [[OGBPGameObject alloc] init];
chao.idGO = 7;
ix = [cenaCorrente.objetos indexOfObject:chao];
chao = [cenaCorrente.objetos objectAtIndex:ix];
chao.centro.y = -2.5f;
chao.b2dBody->SetTransform(b2Vec2(chao.centro.x,
chao.centro.y),
0.0f);

Modificar o pulo da bola no mtodo handleTapFrom:

- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer {

280

Manual do Indie Game Developer - Verso Android e iOS


if (simulando) {
b2Body * bolaBody = bolaGO.b2dBody;
b2Vec2 forca =
b2Vec2(50.0f * bolaBody->GetMass(), 200.0f *
bolaBody->GetMass());
b2Vec2 posicao = bolaBody->GetWorldCenter();
bolaBody->SetAwake(true);
bolaBody->ApplyForce(forca, posicao);
}
}

Sistemas de partculas
Essa a ltima tcnica que vamos explicar no livro, antes de entrarmos no
projeto exemplo. Porm, nem de longe a ltima tcnica que existe. A programao de games muito mais rica e complexa do que eu apresentei ao longo
deste livro. Porm, creio que consegui meu objetivo: resumir as principais
tcnicas, apresentando-as de maneira simples e biplataforma (Android e iOS).
Um sistema de partculas uma simulao computacional formada por
vrios objetos de propores diminutas, com o objetivo de representar elementos fludicos, como: fumaa, exploso, fogo, gua, nuvens e at, pasmem,
cabelos! Sim, cabelos podem ser representados com um sistema de partculas
com rastro.

Composio
Um sistema de partculas composto pela prpria partcula e por um emissor, que origina diversas partculas, sendo distribudas de acordo com sua
necessidade.
Cada partcula possui uma textura acoplada e, em sistemas mais complexos, as partculas podem variar suas texturas e/ou seu brilho ou transparncia.
O emissor contm dados sobre a quantidade de partculas, sua origem, seus
vetores de rota, seus tempos de vida etc. As partculas partem se afastando
do objeto que representa a origem. A maneira como partem e seu ngulo so
muito importantes. Por exemplo, quando representamos fogo, as partculas
tendem a seguir um formato de cabea de cometa (supondo que temos gravidade no ambiente do game), outro exemplo, quando temos um jato direcional, como um motor de foguete, um fogo de artifcio ou uma arma, neste
caso, a tendncia um formato de leque. Quando temos uma exploso, especialmente em ambiente sem gravidade, a tendncia que as partculas se
espalhem em todas as direes.

Captulo 8 -
Tcnicas Comuns em Games
281

Quando temos um jato ou uma exploso, mais fcil calcular a rota de


cada partcula, pois alteramos apenas o ngulo de lanamento. Porm, quando
temos alterao na rota, como no caso do fogo, temos que ficar corrigindo a
trajetria de cada partcula durante o update, preferencialmente, seguindo
uma equao de parbola. Outra opo para o fogo aplicar fora varivel s
partculas, alterando a velocidade angular.
A maioria dos Game Engines possui algum tipo de mecanismo para representar sistemas de partculas, o que facilita muito a criao destes tipos de
efeitos. Mas no difcil criar um sistema de partculas simples, adaptando-o
de acordo com nossas necessidades.
O Box2D excelente para criar sistemas de partculas, porm, um grande
ladro de FPS! Se voc no tiver necessidade de controlar a interao entre
as partculas, ento no precisa utilizar um engine de fsica para representar
seu movimento. Mesmo que voc precise controlar a interao entre as partculas, h a opo de criar a simulao, filmar e transformar em uma animao,
utilizando-a ao invs de acionar o sistema real. Isto nos permite manter nossa
taxa de FPS e ainda d um belo efeito visual.

Um exemplo
O uso mais comum para sistemas de partculas representar exploses.
Jogos de ao sempre tem algum tipo de exploso e o efeito de um sistema de
partculas acrescenta realismo ao game.
Vou criar um pequeno exemplo, utilizando nosso framework, para representar um mssil destruindo um asteroide. Embora no seja necessrio,
vou utilizar Box2D para animar o mssil, pois quero mostrar um exemplo
de anlise de coliso com Android e iOS, mas as partculas sero animadas
manualmente.

282

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 93: Exploso simulada no Android

Ilustrao 94: Exploso simulada no iOS

O cdigo fonte dos exemplos est em:


Android: ..\Codigo\OpenGLAndroid\psdroid.zip;
iOS: ..\Codigo\OpenGLiOS\PSIOS.zip;
Eu criei duas classes: uma para representar o prprio sistema (o emissor) e
outra para representar a partcula. Vou utilizar uma nica textura e um nico
vetor de vrtices para todas elas, animando apenas a matriz de modelo. Neste
caso, a rotao no importa, logo, no vou trabalhar com o ngulo de cada
partcula.
Em meu exemplo, as partculas devero ser lanadas em vetores com ngulos variando de 0 at 359 graus, seguindo a trajetria estabelecida no vetor.
Cada partcula tem um tempo de vida em segundos e se torna inativa aps este

Captulo 8 -
Tcnicas Comuns em Games
283

tempo. O sistema permite fazer refil de partculas, representando exploses


mltiplas.
Fiz algumas modificaes no mtodo de atualizao do mundo e tambm na renderizao. Para comear, eu tenho que saber se houve ou no coliso entre o mssil e o asteroide, e isto conseguido com um ContactListener (b2ContactListener, em C++) do Box2D. Ns j mostramos um exemplo
de uso de ContactListener (..\Codigo\ContactBox2D\contactbox2d.zip), s
que era feito em Java / Swing. Agora, vamos ver como fazer isso em Android
e iOS, utilizando OpenGL ES em ambos.
Na renderizao, eu criei um efeito de fading, que faz as partculas desaparecerem aos poucos, conforme seu tempo de vida vai terminando. Para
isto, tive que alterar o cdigo-fonte do Fragment Shader (em linguagem
GLSL).
Ao rodar o exemplo, voc ver que as partculas so emitidas em ngulo
varivel e aleatrio (de 0 a 359 graus), e se afastam do emissor (o asteroide)
em velocidade aleatria, desaparecendo aos poucos enquanto se movimentam.
Se quiser, pode elimitar a aleatoriedade do ngulo e da velocidade, mas fica
meio artificial, pois exploses de verdade so fuzzy (difusas) e caticas.
Eu repeti a emiso 3 vezes (em cada vez, ele emite menos partculas), mas
voc pode elimitar isso, se quiser.
Efeito de tnel
Quando temos um projtil sendo animado em um jogo baseado em FPS,
dependendo do seu tamanho e velocidade, pode acontecer dele passar atravs
do alvo, sem atingi-lo. Isso chamado de efeito de tunel.
O Box2D procura evitar isso atravs de um algoritmo chamado CCD
(Continuous Collision Detection), de modo a verificar se um objeto dinmico
atravessou um objeto esttico. Porm, quando ambos (o projtil e o alvo) so
objetos dinmicos, ento temos um problema. Voc pode ativar o CCD entre
objetos dinmicos se mudar a propriedade bullet do projtil: bodyDef.
bullet = true;.
No meu exemplo, o mssil um objeto dinmico e o asteroide esttico.
Alm disto, a nave grande e se move devagar, logo, no teremos qualquer
problema.

Implementao em Android
Vamos comear pelo sistema de partculas, que est no pacote com.obomprogramador.games.particlesystem. A classe Particle representa uma nica

284

Manual do Indie Game Developer - Verso Android e iOS

partcula, e sua implementao no tem mistrios. As principais propriedades


so:
private int duracaoSegundos: qual o tempo mximo de vida dessa
partcula em segundos;
private int hTextura: o handler da textura a ser utilizada. No o
VBO de textura, mas o identificador do buffer de textura que enviamos GPU;
private int hVertices: o handler do VBO dos vrtices do objeto (no
varia);
private boolean ativa: se a partcula est ativa, ou seja, ainda dentro
do seu tempo de vida;
private float velocidadeX: a velocidade linear da partcula no eixo
das abscissas;
private float velocidadeY: a velocidade linear da partcula no eixo
das ordenadas;
private Coordenada posicao: a posio atual do centro da partcula,
em coordenadas de tela;
private float tempoAtiva: qual o tempo decorrido desde que esta
partcula se tornou viva;
O sistema de partculas representado pela classe ParticleSystem. Ela
possui algumas propriedades que apenas repassa s partculas, como: hVertices, hTextura, durao e centro, mas possui algumas propriedades que controlam o sistema todo:
private boolean parado: se o sistema est parado ou ativo;
private int refilCount: quantas vezes o sistema suporta refill;
private int quantidade: qual a quantidade atual de partculas ativas
no sistema;
private int qtdeRefil: qual a quantidade de partculas a ser regerada;
public int qtdOriginal: qual a quantidade original de partculas
que foi criada;
A inicializao de partculas feita pelo mtodo criarParticulas(), que
utiliza os parmetros informados no Construtor do sistema:
public void criarParticulas() {
for (int x=0; x < this.quantidade; x++) {

Particle p = new Particle(random.nextInt(5) + 1, x + 1 +
this.refilCount);

p.setPosicao(new Coordenada(this.centro.getX(), this.centro.
getY(), 0.0f));

Captulo 8 -
Tcnicas Comuns em Games
285











}

int angulo = random.nextInt(360);


float radianos = (float) (angulo * Math.PI / 180.0f);
float velocidadeX = (float) Math.cos(radianos);
float velocidadeY = (float) Math.sin(radianos);
p.setVelocidadeX(velocidadeX * (random.nextInt(50) + 1));
p.setVelocidadeY(velocidadeY * (random.nextInt(50) + 1));
p.setDuracaoSegundos(random.nextInt(this.duracaoMaxima+ 3));
p.sethTextura(this.hTextura);
p.sethVertices(this.hVertices);
p.setAtiva(true);
particles.add(p);
}

Existem alguns comandos interessantes neste mtodo. Para comear, eu


clono a coordenada do centro, pois cada partcula ter seu prprio centro
sendo alterado a cada atualizao (eu tive preguia de implementar Cloneable na classe Coordenada). Depois, o ngulo de lanamento de cada partcula calculado aleatoriamente (entre 0 e 359 graus) e depois transformado
em radianos, de modo a calcular o seno e o cosseno, depois eu calculo a velocidade linear em cada eixo, mantendo, assim, a direo da partcula. S que eu
baguno um pouco as velocidades acrescentando mais um nmero aleatrio
(entre 1 e 50) em cada velocidade linear. Isso aumenta o caos no sistema.
Finalmente, eu tambm vario o tempo de vida da partcula de forma aleatria.
O mtodo refilParticles acrescenta uma frao das partculas originais e
recomea o sistema.
Iniciando a exploso
Eu criei uma classe que implementa a interface ContactListener, chamada Contato, dentro do arquivo Renderer.java. O mtodo que mais interessa o beginContact():
@Override
public void beginContact(org.jbox2d.dynamics.contacts.
Contact c) {
if
(((Integer)c.getFixtureA().getBody().getUserData()).
intValue() == 1 &&

((Integer)c.getFixtureB().getBody().getUserData()).
intValue() == 2) {

atingiu();
}
}

286

Manual do Indie Game Developer - Verso Android e iOS

Quando eu crio a nave e o asteroide, eu coloco dentro do UserData de


cada Body uma referncia para um objeto Integer, que a propriedade id
de cada GameObject. Assim, eu crio uma associao entre o Body e o GameObject. Logo, quando ocorre uma coliso, eu posso testar quais objetos esto
colidindo. Neste caso, s me interessa se for a nave (id = 1) com o asteroide
(id = 2). Eu invoco um mtodo para iniciar o processo de exploso:
protected void atingiu() {
nave.setVisivel(false);
asteroide.setHandlerTextura(asteroidechamas.getHandlerTextura());
emChamas = true;
tempoAcumulado = 0.0f;
}

Eu torno a nave invisvel, mudo a textura do asteroide para uma bola de


fogo, ligo o flag indicando que ele foi atingido e zero o acumulador de tempo,
que uso para calcular o tempo decorrido desde o incio da exploso. Se voc
quiser, pode criar um sistema de partculas para a exploso da nave (o mssil),
mas eu no achei necessrio.
Depois, o meu Game Loop vai testar se o flag emChamas foi ligado, o
que significa que tenho que iniciar o sistema de partculas:
protected void update(float deltaT) {
/*

* Atualiza o mundo Box2D e calcula a projeo

*/

if (simulando) {

this.diferencialTempo = deltaT;

world.step(deltaT,

cenaCorrente.getBox2d().getVelocityInterations(),

cenaCorrente.getBox2d().getPositionInterations());

if (emChamas) {

if (particleSystem == null) {

particleSystem = new ParticleSystem(this.world, 100,

new Coordenada(
asteroide.getB2dBody().getWorldCenter().x
* proporcaoMetroTela,
asteroide.getB2dBody().getWorldCenter().y
* proporcaoMetroTela),

0.2f,

Captulo 8 -
Tcnicas Comuns em Games
287

bolafogo.getHandlerTextura(),

bolafogo.getVobVertices(),

2,3);
particleSystem.setParado(false);


}

else {

tempoAcumulado += deltaT;

particleSystem.update(deltaT);

}
}

}
}

Se a instncia do ParticleSystem no tiver sido criada, eu vou cri-la,


passando:
A instncia do mundo Box2D (s necessrio se voc resolver
animar as partculas com o Box2D);
A quantidade de partculas a serem criadas;
As coordenadas do emissor das partculas;
O dimetro de cada partcula (em valores do mundo e no de tela);
O identificador da textura a ser usada nas partculas;
O identificador do VBO de vrtices a ser utilizado;
A durao mxima das partculas, em segundos;
O nmero de vezes de refill do sistema. Cada vez que zerar a
quantidade de partculas ativas, ele vai fazer refill.
Caso o sistema j tenha sido criado, eu acumulo o tempo decorrido desde
o incio da exploso, e atualizo a posio das partculas:
public void update(float deltaT) {
for (Particle p : this.particles) {
if (p.isAtiva()) {
p.getPosicao().setX(

p.getPosicao().getX() + (p.getVelocidadeX() * deltaT)

);
p.getPosicao().setY(

p.getPosicao().getY() + (p.getVelocidadeY() * deltaT)

);
}
}
}

288

Manual do Indie Game Developer - Verso Android e iOS

Renderizao das partculas


Bem, antes de entrarmos no cdigo que renderiza as partculas, deixe-me
lembrar que eu criei um efeito de Fading, de modo que as partculas se tornem mais transparentes, na medida em que vo morrendo. Isto possvel
diminuindo o valor do canal alfa (opacidade) da imagem. Pixeis renderizados
com alfa igual ou prximos de 1, so mais opacos, j os com alfa igual ou
prximos de zero, so transparentes. Um pixel transparente deixa aparecer a
imagem que est no fundo da tela.
Aumentar a transparncia de uma imagem diminuir o valor de alfa para
seus pixels. E quem mexe com as cores dos pixels? O Fragment Shader! Eu
tive que alterar o cdigo do meu Fragment Shader para incluir um uniform
(uma constante que eu passo para ele):
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
uniform float fadefactor;
void main() {
gl_FragColor = texture2D(sTexture, vTextureCoord);
gl_FragColor.a *= fadefactor;
}

Lembre-se que o Fragment Shader um programa, escrito na linguagem


GLSL, e que eu guardo em um String e uso na hora de montar o programa da
GPU.
O que estou fazendo? Eu declarei um uniform, que um parmetro que
eu passo para o Shader. A diferena entre uniform e attribute que o
primeiro no varia entre as chamadas do Shader para uma nica operao de
desenho. Como o fator de esvanecimento (fadefactor) que vou usar o mesmo
para todos os pixels da imagem, posso usar um uniform.
A varivel gl_FragColor global (built-in) no GLSL e seu tipo
vec4 (x,y,z e a), onde o campo a representa o valor do canal alfa. Ao multiplicar o valor de alfa pelo fator, eu vou modificar a transparncia do pixel. Se
o fator for prximo de 1, o pixel ficar mais opaco, se for menor que 1, o pixel
ficar mais transparente.
Ento, antes de renderizar qualquer coisa em meu programa, eu preciso
passar o valor de fadefactor que o Fragment Shader vai usar, e isto feito
com o mtodo glUniform1f, que passa um falor float para um parmetro
uniform. Veja os exemplos abaixo:

Captulo 8 -
Tcnicas Comuns em Games
289

GLES20.glUniform1f(maFadeFactor, (float) 1.0): estou passando


fadefactor 1, logo, a imagem ser totalmente opaca;
GLES20.glUniform1f(maFadeFactor, (float) 0.0): estou passando
fadefactor zero, logo, a imagem ser totalmente transparente;
GLES20.glUniform1f(maFadeFactor, (float) percentalfa): estou
passando fadefactor varivel;
Se eu estou passando um uniform para o Fragment Shader, eu preciso saber qual o seu identificador, ento, aps linkeditar o programa da
GPU, eu obtenho um idendificador para o uniform, o que feito no mtodo
onSurfaceCreated():
maFadeFactor = GLES20.glGetUniformLocation(programaGLES,
fadefactor);

O nome que eu passo para o mtodo glGetUniformLocation deve ser o


mesmo nome da varivel dentro do Shader.
Este processo o mesmo utilizado para passar a Matriz Model-View-Projection para o Vertex Shader:
muMVPMatrixHandle = GLES20.glGetUniformLocation(programaGLES,
uMVPMatrix);
...
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false,
matrizIntermediaria, 0);

Fora o Fading, eu precisava pegar cada partcula, verificar se estava ativa, renderiz-la e testar se ainda existiriam mais partculas ativas no sistema.
Eu sobrescrevi o mtodo onDrawFrame() na classe Renderer.java:
@Override
public void onDrawFrame(GL10 gl) {

super.onDrawFrame(gl);
if (this.emChamas) {

int contagem = 0;

for (Particle p : particleSystem.particles) {

if (p.isAtiva()) {



if (p.getDuracaoSegundos() < p.getTempoAtiva()) {

p.setAtiva(false);
continue;

}

contagem++;

290

Manual do Indie Game Developer - Verso Android e iOS



p.setTempoAtiva(p.getTempoAtiva() +
this.diferencialTempo);



GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,
p.gethTextura());

// Vrtices:



GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,
p.gethVertices());

GLES20.glVertexAttribPointer(maPositionHandle, 3,

GLES20.GL_FLOAT, false,12, 0);


GLES20.glEnableVertexAttribArray(maPositionHandle);


// Textura:


GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,
this.hTextureVOB);

checkGlError(glEnableVertexAttribArray maPositionHandle);

GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.
GL_FLOAT, false,

8, 0);

GLES20.glEnableVertexAttribArray(maTextureHandle);


// Matriz de posicionamento da textura, sem
rotao nem movimento:


Matrix.setIdentityM(matrizModelo, 0);
float posicaoX = p.getPosicao().getX();
float posicaoY = p.getPosicao().getY();

Matrix.translateM(matrizModelo, 0, posicaoX, posicaoY,
0);

Matrix.multiplyMM(matrizIntermediaria, 0,
matrizCamera, 0, matrizModelo, 0);

Matrix.multiplyMM(matrizIntermediaria, 0,
matrizProjecao, 0,
matrizIntermediaria, 0);


// Calculamos o fading:


float percentalfa = 1.0f - (p.getTempoAtiva()
/ p.getDuracaoSegundos());

Captulo 8 -
Tcnicas Comuns em Games
291

GLES20.glUniform1f(maFadeFactor, (float) percentalfa);

GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false,
matrizIntermediaria, 0);

GLES20.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,
QUANTIDADE_DE_VERTICES);

checkGlError(glDrawArrays);


GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

}

}

if (emChamas) {

if (contagem == 0) {

emChamas = false;

particleSystem = null;

asteroide.setVisivel(false);

}

else {

if (contagem < (particleSystem.qtdOriginal / 3)) {

particleSystem.refilParticles();

}

}

}
}
}

Eu testo se a particula est ativa e, caso esteja, calculo o tempo decorrido.


Se for maior que sua durao, ento eu a desativo e no a desenho mais. Caso
ainda esteja ativa, eu desenho a partcula informando um fator de esvanecimento calculado com base no tempo de vida. Quanto mais tempo a partcula
estiver viva menor ser o fator, tornando-a mais transparente.
Aps percorrer todas as partculas, eu preciso saber a quantidade de partculas ativas que restou no sistema. Se no sobrou partcula alguma, eu desligo
o sistema e torno o asteroide invisvel. Caso contrrio, se a quantidade de
partculas restantes for menor que 1/3 do original, eu disparo mando o sistema
fazer um refill de partculas.

Implementao iOS
Bem, agora est na hora de largar tudo, sentar em posio de ltus e ficar
repetindo OOMMMM... A implementao iOS um pouco mais hardcore

292

Manual do Indie Game Developer - Verso Android e iOS

que a implementao Android. No nada de outro mundo, mas um pouco


mais complicada.
Para comear, no podemos usar o GLKBaseEffect, que nos isolava dos
problemas mundanos do Open GL ES. O motivo que o Shaders utilizados
pelo GLKBaseEffect no nos atendem.
Como o resto muito parecido com o da implementao Android, s vou
focar na parte principal, que alterar o projeto para renderizar diretamente
com o OpenGL ES, dispensando o GLKBaseEffect. Sugiro que voc compare
a verso iOS com a verso Android.
Criando Shaders
Eu sugiro que voc comece criando um novo projeto no Xcode, utilizando
o template OpenGL Game. Depois, mantenha esse cdigo mo para poder
copiar alguns mtodos, alm do prprio cdigo-fonte dos Shaders.
Crie um novo projeto, baseado no ParalaxIOS, que fizemos recentemente, pois teremos objetos que no so animados com o Box2D.
Copie, do projeto template, os dois arquivos que esto dentro da pasta
Shaders (Shader.vsh e Shader.fsh). Pode at copiar a pasta inteira. Depois, temos que informar ao Xcode que estes arquivos devem ser empacotados
junto com os outros recursos do projeto:
1. Clique no Target do projeto;
2. Selecione Build Phases;
3. Selecione Copy bundle resources;
4. Adicione seus arquivos Shaders.
Se no fizer isto, os Shaders no estaro disponveis quando o projeto for
executado.
Bem, eu sugiro que substitua o cdigo-fonte dos dois Shaders pelos que
utilizamos na verso Android:
Vertex Shader (Shader.vsh):

uniform mat4 uMVPMatrix;


attribute vec4 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTextureCoord = aTextureCoord;
}

Captulo 8 -
Tcnicas Comuns em Games
293

Fragment Shader (Shader.fsh):


precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
uniform float fadefactor;
void main() {
gl_FragColor = texture2D(sTexture, vTextureCoord);
gl_FragColor.a *= fadefactor;
}

Voc poderia utilizar os mesmos Shaders que o template Xcode gerou, s


que os nomes dos atributos seriam diferentes.
Bem, temos os Shaders, agora precisamos:
1. Compilar os Shaders;
2. Linkeditar;
3. Obter as referncias para os atributos e uniforms.
Felizmente, o template gerado pelo Xcode (OpenGL Game) j tem tudo
isso. Abra o arquivo do View Controller que ele criou e copie para o seu prprio View Controller os mtodos:
- (BOOL)loadShaders;
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)
type file:(NSString *)file;
- (BOOL)linkProgram:(GLuint)prog;
- (BOOL)validateProgram:(GLuint)prog;

E, no mtodo viewDidLoad, invoque o mtodo loadShaders:


- (void)viewDidLoad
{
[super viewDidLoad];

// Alocamos o contexto OpenGL ES


self.context = [[EAGLContext alloc] initWithAPI:k
EAGLRenderingAPIOpenGLES2];
if (!self.context) {
NSLog(@Failed to create ES context);
}
view = (GLKView *)self.view;
view.context = self.context;

294

Manual do Indie Game Developer - Verso Android e iOS


[EAGLContext setCurrentContext:view.context];
[self loadShaders];

Tem que ser exatamente aps indicarmos qual o contexto OpenGL ES


corrente.
Obtendo indicadores dos parmetros dos Shaders
Bem, temos vrias informaes que devem ser passadas para os Shaders:
uniform mat4 uMVPMatrix: matriz Model-View-Projection;
attribute vec4 aPosition: coordenadas da posio que estamos
renderizando;
attribute vec2 aTextureCoord: coordenadas de textura da posio;
uniform float fadefactor: o nosso fator de esvanecimento;
Para passar parmetros aos Shaders, primeiramente, necessrio obter indicadores (handlers) para cada um deles. Os atributos () devem ser indicados
ANTES de linkeditar o programa, dentro do mtodo loadShaders, aps
adicionarmos os Shaders compilados ao programa GPU que estamos criando:
glBindAttribLocation(_program, GLKVertexAttribPosition, aPosition);
glBindAttribLocation(_program, GLKVertexAttribNormal, aTextureCoord);
A diferena entre attribute e uniform que os primeiros variam a cada
chamada do Shader, mesmo dentro de uma nica operao de renderizao.
Depois, linkeditamos o programa final, cujo indicador est na varivel _
program. Ento, temos que pegar o indicador de localizao dos atributos e
uniforms, o que feito no mesmo mtodo, aps a linkedio do programa:
uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX] =
glGetUniformLocation(_program, uMVPMatrix);
uniforms[UNIFORM_FADE_FACTOR] = glGetUniformLocation(_program,
fadefactor);
maPositionHandle = glGetAttribLocation(_program, aPosition);
maTextureHandle = glGetAttribLocation(_program, aTextureCoord);

Eu no gostei muito de usar um vetor para armazenar os indicadores dos


uniforms, mas assim que o template usa e resolvi no mudar.
Renderizao das partculas
No mtodo drawInRect temos mudanas significativas. Para comear,
no vamos mais utilizar o GLKBaseEffect, logo, tudo relacionado com a varivel self.effect deve ser substitudo. Se voc observar a verso Android,
saber o que est faltando. Para comear, vamos usar o programa GPU que
criamos:

Captulo 8 -
Tcnicas Comuns em Games
295
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
// Limpar e preparar:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(_program);
...

Ele inclui os nossos Shaders e atributos.


Em segundo lugar, ao invs de usar as propriedades self.effect.texture2d0.
xxx, temos que invocar funes do OpenGL ES para indicar qual ser a textura ativa:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cenaCorrente.glProps.textureInfo.name);

Eu ainda estou utilizando as classes: GLKTextureInfo e GLKTextureLoader, logo, eu armazeno as texturas utilizando uma instncia de GLKTextureInfo e a propriedade name contm o indicador da textura a ser utilizada.
Outros detalhes que mudam so os atributos de textura e vrtices. Com o
GLKBaseEffect, ns usvamos os indicadores que ele nos formecia:
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT,
GL_FALSE, sizeof(GLfloat) * 3, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribPosition);
glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT,
GL_FALSE, sizeof(GLfloat) * 2, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

Agora, temos outros atributos para vrtice e textura:

glVertexAttribPointer(maPositionHandle, 3, GL_FLOAT, GL_


FALSE, sizeof(GLfloat) * 3, BUFFER_OFFSET(0));
glEnableVertexAttribArray(maPositionHandle);
glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glVertexAttribPointer(maTextureHandle, 2, GL_FLOAT, GL_
FALSE, sizeof(GLfloat) * 2, BUFFER_OFFSET(0));
glEnableVertexAttribArray(maTextureHandle);

Finalmente, antes de invocar a operao de desenho, temos que passar os


nossos uniforms de matriz e de fator:
glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_
MATRIX], 1, 0, matrizIntermediaria.m);
glUniform1f(uniforms[UNIFORM_FADE_FACTOR], 1.0f);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

296

Manual do Indie Game Developer - Verso Android e iOS

Concluso
Existem milhares de tcnicas e formas de implementao diferentes. As
que demonstrei nesse captulo nem sempre so as melhores, porm, so fceis
de entender, simples de implementar e eficientes.
Eu recomendo que voc rode os exemplos, altere e use bastante, criando
seu prprio framework de games.

Captulo 9
Vamos Criar um Game
Seu objetivo ao ler este livro criar um game, certo? Ento, para facilitar
as coisas e reunir tudo o que vimos at agora, vou mostrar um game bem simples, porm interessante: Bola no Quintal.

Ilustrao 95: A tela inicial do game

Voc ter acesso ao cdigo-fonte completo da verso inicial do Game, com


trs nveis. Eu estou finalizando a verso final, que ser publicada nos principais mercados de aplicaes mveis: Google Play e AppStore. Em outras
palavras, voc ver o projeto e construo de um game de verdade, em duas
plataformas diferentes, utilizando as ferramentas e o pequeno framework que
mostrei no livro.
O jogo simples e fcil de jogar, porm apresenta desafios crescentes ao
jogador. Bola no Quintal como aquelas brincadeiras antigas, que eu fazia
quando era criana. O que voc faz quando tem apenas uma bola e algumas
garrafas velhas? Pode brincar de tentar derrubar as garrafas! Isso resume o
game.

298

Manual do Indie Game Developer - Verso Android e iOS

Ilustrao 96: Um jogo simples e fcil

Voc tem um cenrio, com um muro dividindo duas reas. Na parte esquerda, ficam voc e a bola e, na parte direita, alguns objetos equilibrados sobre
pedaos de madeira. Voc deve jogar a bola e derrubar os objetos com ela.
Sempre que ela toca o cho, volta para voc jogar novamente.
No primeiro nvel, voc deve apenas derrubar os objetos, no menor tempo possvel. Nos outros nveis, voc dever derrub-los em uma determinada
ordem. Alm disto, as bolas vo ficando mais pesadas e vazias nos nveis
superiores, o que faz com que seja mais difcil acertar.
Eu criei arquivos de recursos com idioma Ingls e localizao para Portugus. O motivo simples: se eu no tiver recursos no idioma do jogador,
mostrarei a verso em Ingls.
O jogo armazena os menores tempos de concluso de cada nvel. Em uma
verso comercial futura, ele vai compartilhar isso atravs de redes sociais de
games, como o Game Center, da Apple.
Cada nvel definido em seu prprio arquivo XML de modelo, e contm
um tempo limite para concluso. Se o jogador no conseguir derrubar todos os
objetos neste tempo, aparece uma imagem informando que ele perdeu.
Por que no um jogo de tiro?
Na verdade, um jogo de tiro, como o AsteroidNuts (mostrado no incio),
seria at mais simples. Porm, bem comum. Eu quis fazer um game que
usasse bem os recursos do livro, e o Bola no Quintal atende a esse objetivo.
Na verdade, o framework se adapta muito bem a jogos de bola. Finalmente, outra grande vantagem a atratividade para Casual Gamers, crianas

Captulo 9 - Vamos Criar um


Game
299

etc. Um jogo de bola agrada a todos, porm, existem pessoas que no curtem
Shooters.

Limitaes
O objetivo deste trabalho fornecer um conjunto de ferramentas e tcnicas, com exemplo de aplicao em cdigo-fonte, para que voc construa games mveis para Android e iOS. Logo, o game que vou mostrar apenas um
prottipo funcional, que emprega quase tudo o que vimos no livro.
Ele s tem trs nveis, embora seja muito expandir isso para 30 ou mais. O
objetivo mostrar como criar nveis e as diferenas de dificuldade entre eles.
O game tambm carece de efeitos sonoros e msica. Resolvi deixar o game o
mais simples possvel para apresentao no livro.
Todas as ilustraes foram compostas em parte (ou todo) com base em
imagens do site OpenClippart.org, logo, no so imagens para uso comercial.
Finalmente, outra coisa que deixei de fora foi o Social gaming, ou seja, o
uso de uma rede social de games, como o Game Center, da Apple, ou o OpenFeint. Embora o compartilhamento de conquistas seja importante, eu preferi
deixar de fora, pois foge ao escopo do livro.
Resumindo, o game funcional e perfeitamente jogvel, porm, no um
produto acabado, pronto para o mercado.

Licena de uso do cdigo-fonte


Todo o cdigo-fonte do livro liberado sob a licena Apache, verso 2.0,
que pode ser lida em: http://www.apache.org/licenses/LICENSE-2.0.html. Em resumo, esta licena lhe d permisso para usar, copiar e criar trabalhos derivados do cdigo-fonte, desde que mantenha a referncia para o autor original
(alm de uma cpia da licena original).
Um trabalho derivado a criao de um produto que usa o cdigo-fonte
licenciado.
Para todo o cdigo-fonte do livro, isso no problema algum, desde que
voc deixe uma indicao de que voc utilizou partes do cdigo-fonte do meu
livro.
Porm, no caso do cdigo-fonte do game, eu modifiquei ligeiramente os
termos da licena, pois eu tenho uma verso comercial deste mesmo game.
Voc pode fazer o que quiser com o cdigo-fonte do Game, exceto uma
coisa: criar um game derivado dele. Se voc quiser criar um game Bola de
Quintal melhorado (ou mesmo com outro nome), deve solicitar licena para

300

Manual do Indie Game Developer - Verso Android e iOS

isto. O motivo simples: eu tenho uma verso comercial do game, da qual


retirei o cdigo-fonte que foi compartilhado.
Voc pode e deve criar um game usando o cdigo-fonte do Bola no Quintal, desde que seja um jogo diferente. Por exemplo, um jogo de Voleibol.
Licena para uso do cdigo-fonte do Game Bola no Quintal
1. O alvo desta licena o projeto Bola no Quintal, composto pelo
cdigo-fonte, imagens e outros recursos, que esto compactados
dentro dos seguintes arquivos:
Verso Android: byballdroid.zip;
Verso iOS: ByBall.zip;
2. O cdigo-fonte, os arquivos de configurao e os recursos esto liberados para uso e cpia, de acordo com a licena Apache 2.0 (http://
www.apache.org/licenses/LICENSE-2.0.html), sendo sujeitos a restries adicionais, descritas nesta licena;
3. Voc pode fazer tudo o que a licena Apache 2.0 permite, porm, ao
criar um produto derivado, ou seja, um game semelhante a ele, deve
solicitar licena do autor (eu, Cleuton Sampaio);

A concepo
A ideia de fazer um jogo como o Bola no Quintal surgiu antes de eu
comear a escrever o livro. Eu gosto particularmente de jogos de bolas, prova
disto que, no meu livro anterior, eu criei o BueiroBall (que tambm vai
ser lanado no mercado em breve). Porm, quando eu estava experimentando
com o Box2D, criei alguns efeitos interessantes, como o da prxima figura.

Ilustrao 97: Inspirao para o Bola no Quintal

Captulo 9 - Vamos Criar um


Game
301

Eu fui elaborando o game e, quando chegou no captulo 7, eu j tinha


muita coisa do game pronta.
Tcnicas 2D so muito bem aproveitadas em jogos casuais, como o Bola
no Quintal, logo, tudo se encaixa perfeitamente.

Jogos casuais
Jogos casuais so para pessoas comuns, que jogam apenas em determinados momentos, como: antes de dormir, na fila de espera ou ento no nibus.
Ao contrrio de jogos 3D, que exigem muita ateno do jogador, eles so feitos para divertir e passar o tempo. Um bom exemplo o famoso Angry Birds, que conquistou at quem no gosta de games. Recentemente, eu comprei
o game do filme Detona Ralph (Wreck It Ralph), para o iPad, e os vrios
minigames que ele contm so realmente divertidos e viciantes.
O jogo deve ser fcil de jogar, mas isto no quer dizer que ele deva ser fcil
de finalizar. O jogador, geralmente, gosta de nveis crescentes de dificuldade.
O prprio Angry Birds assim.
Ento, eu pensei que o jogo deve ser simples, com jogabilidade fcil e
possvel de rodar em um smartphone ou em um tablet.

Jogabilidade
O jogo simples: voc tem que chutar a bola, tocando-a e arrastando-a
at onde quiser. O ngulo do arrasto e a sua distncia determinaro a direo
e a fora aplicadas na bola. Confesso que fiquei preocupado com este tipo de
ao em um Smartphone, no qual a bola ficaria bem pequena, porm, depois
dos primeiros testes, constatei que no problema.
Eu havia pensado em acionar a bola de outras formas, como usando um
acelermetro, porm, jogadores de smartphone, geralmente, no gostam muito de ficar balanando o aparelho. Tambm pensei em um sistema mais
completo, onde o jogador indicaria o ponto de toque na bola e o ngulo, indicando a fora em um sensor (como os de jogos de golfe), mas ficaria chato
para a maioria dos jogadores casuais.
O resultado ficou bom, sendo que eu testei com vrias pessoas, de idades
diferentes. claro que o pessoal mais velho tem certa dificuldade para jogar
em smartphones, porm apresentam a mesma dificuldade com outros jogos,
como o Angry Birds, logo, para estas pessoas melhor o uso de um tablet.

302

Manual do Indie Game Developer - Verso Android e iOS

Como usei o prprio framework que desenvolvi at aqui, eu consigo controlar o FPS do game, evitando que ele acelere em dispositivos mais rpidos. Da mesma forma, as imagens so redimensionadas de acordo com o
tamanho da diagonal da tela, sendo geradas usando Mipmaps, o que resulta
em escalas muito boas.

Implementao Bsica
Vamos ver aqui as alteraes bsicas que fiz para ambas as verses.
A verso Android est em: ..\Codigo\GameDroid\byballdroid.zip.
A verso iOS est em: ..\Codigo\GameIOS\ByBall.zip;
Sugiro que voc carregue um ou o outro projeto e estude a implementao.

I18N e L10N
Internacionalizao (I18N) e Localizao (L10N) so aspectos fundamentais de games. Neste game, eu j utilizei o mecanismo de internacionalizao
do Android e do iOS, criando a localizao default em Ingls, acrescentando
Portugus como localizao opcional.
No Android, feito assim:
Strings: colocamos os textos em Ingls dentro de: res/values/strings.xml e a verso em Portugus dentro de: res/values-pt/strings.
xml;
Imagens: colocamos as imagens em Ingls, ou as que no tem idioma, dentro de: res/drawable-mdpi, e as que contm texto em Portugus dentro de: res/drawable-pt-mdpi;
No iOS, bem mais complicado. Primeiramente, como o Game Universal, temos que criar dois Storyboards: um para iPhone e outro para iPad. Depois, temos que repetir o mesmo layout em ambos, incluindo todas as views e
segues. A localizao de strings fcil:
1. Crie um arquivo Localizable.strings;
2. No painel de propriedades (lado direito), selecione Identity e adicione as duas localizaes: Portugus e Ingls;
Para as imagens que devam ser localizadas, faa a mesma coisa. Os recursos localizados ficam em pastas separadas: en.lproj (Ingls) e pt-lproj
(Portugus).

Captulo 9 - Vamos Criar um


Game
303

Alteraes no modelo do game


Para comear, inclu alguns Tags novos no XML do game:
<objeto>
<id>7</id>
<visivel>true</visivel>
<gameobject>2</gameobject>
<ordem>0</ordem>
<tipo>2</tipo>
<forma>2</forma>
<centro>

<x>2</x>

<y>0</y>
</centro>
<alinhar>6</alinhar>
<sobre>8</sobre>
<altura>0.5</altura>
<esticarAltura>false</esticarAltura>
<largura>0.15</largura>
<esticarLargura>false</esticarLargura>
<arquivoTextura>garrafa1.png</arquivoTextura>
<densidade>2.0</densidade>
<atrito>0.1</atrito>
<coefRetribuicao>0.2</coefRetribuicao>
</objeto>

Assim como inclu os novos Tags, fiz as alteraes necessrias na classe


que armazena o GameObject e na classe que l o XML:
Android: GameObject.java e GameModelLoader.java;
iOS: OGBPGameObject (.h e .m) e OGBPGameModelLoader (.h
e .m).
Vamos ver o que estes atributos significam:
Visivel: se o GameObject deve ser renderizado;
GameObject: 1 GAMEOBJECT_PLAYER, 2 GAMEOBJECT_NPC e 3 GAMEOBJECT_CENARIO;
Ordem: a ordem de derrubada. Antes de derrubar um objeto de
determinada ordem, todos os objetos da ordem anterior devem
ter sido derrubados;
Alinhar: introduzi a opo 6 ALINHAMENTO_SOBRE_OUTRO, que indica que este GameObject dever ficar com sua base
sobre outro GameObject;

304

Manual do Indie Game Developer - Verso Android e iOS

Sobre: o id do GameObject sobre o qual este GO dever ficar;


Esta nova opo de alinhamento me permite posicionar uma garrafa sobre
um poste ou sobre o muro.
E tambm alterei o objeto <cena>, para incluir o TAG <limitesegundos>, indicando o limite de tempo, em segundos, para o jogador completar
este nvel.
Logo aps carregar o modelo, eu crio um vetor de quantidade de objetos
por ordem:
Android: Classe Renderer.java, mtodo onSurfaceChanged():
int maxOrdem = 0;
for (GameObject go : cenaCorrente.getObjetos()) {
if (go.getOrdem() > maxOrdem) {

maxOrdem = go.getOrdem();
}
if (go.getGameObject() == GameObject.GAMEOBJECT_
NPC) {

this.quantidadeNPC++;
}
if (go.getAlinhamento() ==

GameObject.ALINHAMENTO_GO.ALINHAMENTO_BASE_
CHAO) {

float yChao = -1 * ((this.alturaViewPort / 2) /
proporcaoMetroTela);

yChao = yChao + go.getAltura() / 2.0f;

go.getCentro().setY(yChao);

go.getB2dBody().setTransform(new
Vec2(go.getCentro().getX(),
go.getCentro().getY()), 0.0f);

}
}

// A ordem comea em zero
this.qtdOrdem = new int[maxOrdem + 1];

iOS: Classe OGCTViewController.m, mtodo

int maxOrdem = 0;
for (OGBPGameObject *go in cenaCorrente.objetos) {
if (go.ordem > maxOrdem) {
maxOrdem = go.ordem;
}
if (go.gameobject == GAMEOBJECT_NPC) {
quantidadeNPC++;

Captulo 9 - Vamos Criar um


Game
305
}
if (go.alinhamento == ALINHAMENTO_BASE_CHAO) {
float yChao = -1 * ((alturaViewPort / 2) /
proporcaoMetroTela);
yChao = yChao + go.altura / 2.0f;
go.centro.y = yChao;
go.b2dBody->SetTransform(b2Vec2(go.centro.x,
go.centro.y, 0.0f);
}
}
qtdOrdem = [[NSMutableArray alloc] init];
if (maxOrdem == 0) {
[qtdOrdem addObject:[NSNumber numberWithInt:0]];
}
else {
for (int x=0; x<=maxOrdem; x++) {
[qtdOrdem addObject:[NSNumber numberWithInt:0]];
}
}

Agora, o alinhamento um-sobre-o-outro resolvido logo depois:


Android:

for (GameObject go : cenaCorrente.getObjetos()) {


if (go.getGameObject() == GameObject.GAMEOBJECT_
NPC) {

this.qtdOrdem[go.getOrdem()]++;
}
if (go.getAlinhamento() == GameObject.ALINHAMENTO_
GO.ALINHAMENTO_SOBRE_OUTRO) {

GameObject goBase = new GameObject();

goBase.setId(go.getSobre());

int idBase = cenaCorrente.getObjetos().
indexOf(goBase);

goBase = cenaCorrente.getObjetos().
get(idBase);


float yChao = goBase.getCentro().getY() + goBase.
getAltura() / 2;

306

Manual do Indie Game Developer - Verso Android e iOS



yChao = yChao + go.getAltura() / 2.0f;

go.getCentro().setY(yChao);

go.setyOriginal(go.getCentro().getY());

go.setxOriginal(go.getCentro().getX());

go.getB2dBody().setTransform(new Vec2(go.
getCentro().getX(), go.getCentro().getY()),

0.0f);
}
}

iOS:
for (OGBPGameObject * go in cenaCorrente.objetos) {
if (go.gameobject == GAMEOBJECT_NPC) {
int qtd = [[qtdOrdem objectAtIndex:go.ordem]
intValue];
qtd++;
[qtdOrdem setObject:[NSNumber numberWithInt:qtd]
atIndexedSubscript:go.ordem];
}
if (go.alinhamento == ALINHAMENTO_SOBRE_OUTRO) {
OGBPGameObject * goBase = [[OGBPGameObject
alloc] init];
goBase.idGO = go.sobre;
int idBase = [cenaCorrente.objetos
indexOfObject:goBase];
goBase = [cenaCorrente.objetos objectAtIndex:idBase];
float yChao = goBase.centro.y + goBase.altura
/ 2;
yChao = yChao + go.altura / 2.0f;
go.centro.y = yChao;
go.yOriginal = go.centro.y;
go.xOriginal = go.centro.x;
go.b2dBody->SetTransform(b2Vec2(go.centro.x,
go.centro.y),
0.0f);
}
}

Captulo 9 - Vamos Criar um


Game
307

Alteraes na carga do nvel corrente


Agora, o jogador pode escolher qual nvel quer jogar. Isto feito na tela
anterior, atravs de trs botes em formato de bolas. Ao clicar em um dos botes, a tela seguinte (do Game) carregada com o parmetro, indicando qual
foi o nvel selecionado.
No Android, fazemos isto acrescentando atributos ao Intent que invoca a
prxima tela (classe MainActivity.java, mtodo: selecionar():
public void selecionar(View view) {
Intent i =
new Intent (this.getApplicationContext(),
GameActivity.class);
int nivel = 0;
switch(view.getId()) {
case R.id.btnnivel1:

nivel = 1;

i.putExtra(modelogame, modelonivel1.xml);

i.putExtra(modelotela, tela.xml);

break;
case R.id.btnnivel2:

nivel = 2;

i.putExtra(modelogame, modelonivel2.xml);

i.putExtra(modelotela, tela.xml);

break;
case R.id.btnnivel3:

nivel = 3;

i.putExtra(modelogame, modelonivel3.xml);

i.putExtra(modelotela, tela.xml);

break;
}
i.putExtra(nivel, nivel);
this.startActivity(i);
}

No iOS, eu utilizei uma segue que vai de cada boto de nvel at o nosso
ViewController de game. Ento, eu uso o mtodo prepareForSegue para
alterar propriedades no ViewController do game:
-(void)prepareForSegue:(UIStoryboardSegue
sender:(id)sender
{

*)segue

308

Manual do Indie Game Developer - Verso Android e iOS


if ([segue.identifier isEqualToString:@nivel1]) {
OGCTViewController *destViewController = segue.
destinationViewController;
destViewController.nomeTela = @tela;
destViewController.nivel = 1;
destViewController.nomeGame = @modelonivel1;
}
else if ([segue.identifier isEqualToString:@nivel2])
{
OGCTViewController *destViewController = segue.
destinationViewController;
destViewController.nomeTela = @tela;
destViewController.nivel = 2;
destViewController.nomeGame = @modelonivel2;
}
else if ([segue.identifier isEqualToString:@nivel3])
{
OGCTViewController *destViewController = segue.
destinationViewController;
destViewController.nomeTela = @tela;
destViewController.nivel = 3;
destViewController.nomeGame = @modelonivel3;
}
else if ([segue.identifier isEqualToString:@ajuda])
{
BYBLTextoViewController *destViewController =
segue.destinationViewController;
destViewController.isAjuda = YES;
}
else {
BYBLTextoViewController *destViewController =
segue.destinationViewController;
destViewController.isAjuda = NO;
}
}

Note que eu tenho duas outras segues: uma para o boto ajuda e outra
para o boto pontos, que mostram, respectivamente, o help da aplicao
e a pontuao do jogador.
Eu separei o modelo de game em arquivos diferentes, embora pudesse colocar vrias cenas em cada arquivo, considerei que seria mais simples. No

Captulo 9 - Vamos Criar um


Game
309

momento de carregar o modelo de game (e o modelo de tela), eu recebo o


nome de cada arquivo (e o nvel), carregando o modelo correspondente.
No Android, eu recebo os atributos que vieram no Intent e passo para o
construtor da classe Renderer:
Intent i = this.getIntent();
int cena = i.getIntExtra(nivel, 0);
String nomeModeloGame = i.getStringExtra(modelogame);
String nomeModeloTela = i.getStringExtra(modelotela);
renderer = new Renderer(this.getApplicationContext(),
nomeModeloGame,

nomeModeloTela);

Depois, carregamos o modelo do game e da tela utilizando os nomes informados. Tambm alteramos a carga da cena, para informar o nmero do nvel
desejado.
No iOS j alteramos as propriedades do View Controller.

Alteraes no Game Loop


O Game Loop tem que verificar o tempo decorrido, assim poder terminar
o nvel, caso o usurio no tenha conseguido derrubar os NPCs.
A cada update, eu verifico se algum NPC est com altura inferior a original (se foi atingido). Se estiver, eu verifico se todos os objetos de ordem inferior foram derrubados, caso contrrio, eu mostro a imagem da cara triste.
Se todos os NPCs forem derrubados, o nvel acabou. Ento, eu verifico se
o tempo foi menor que o j registrado para aquele nvel e armazeno.
Android: Classe Renderer.java, mtodo: update():
protected void update(float deltaT) {
super.update(deltaT);
if (!acabou) {

segundosDeJogo += deltaT;
}
if (segundosDeJogo >= cenaCorrente.
getLimitesegundos()) {

// O jogo acabou...

this.tempoAviso = 3;

this.resetBola();

chutou = true;
trocarAviso(15, true);

310

Manual do Indie Game Developer - Verso Android e iOS



acabou = true;
}

if (this.aviso != null) {

tempoAviso -= deltaT;

if (tempoAviso <= 0) {
trocarAviso(0, true);

}
}
tratarColisaoNPC();
if (resetarBola) {

this.resetBola();
}
}

O mtodo tratarColisaoNPC() verifica se algum NPC caiu e se est na


ordem certa.
IOS: Classe OGCTViewController.m, mtodo: update:
- (void)update
{
float deltaT = [self timeSinceLastUpdate];
if(self.pausa) {
return;
}
if (!simulando) {
return;
}
// Controle de FPS:
if (deltaT < tempo) {
// Colocar o Thread em sleep mode, pois falta
// algum tempo para iniciar
[NSThread sleepForTimeInterval:(tempo - deltaT)];
//NSLog(@ deltaT %f sleep: %f, deltaT, (tempo
- deltaT));
}

[self displayMem:5];
world->Step(deltaT,

Captulo 9 - Vamos Criar um


Game
311

cenaCorrente.box2d.velocityInterations,
cenaCorrente.box2d.positionInterations);

if (!acabou) {
segundosDeJogo += deltaT;
}
if (segundosDeJogo >= cenaCorrente.limitesegundos)

{
// O jogo acabou
tempoAviso = 3;
[self resetBola];
chutou = YES;
[self trocarAviso:15 forcar:YES];
acabou = YES;
}
if (aviso != nil) {
tempoAviso -= deltaT;
if (tempoAviso <= 0) {
[self trocarAviso: 0 forcar: YES];
}
}
[self tratarColisaoNPC];

if (resetarBola) {
[self resetBola];
}

O mtodo no iOS parece maior, mas no . No Android, os mtodos so


divididos em duas classes: OpenGLAvancadoRenderer.java e Renderer.
java, que derivada da primeira.

Colises
Eu poderia identificar as colises entre a bola (Player Object) e as garrafas
(NPCs) diretamente dentro de um ContactListener do Box2D. Porm, eu
no preciso saber quando a bola atingiu uma garrafa, mas apenas se a garrafa
caiu, o que faz com que sua altura seja menor que a altura original. O motivo
que, ao ser atingida, a garrafa pode apenas balanar, sem cair, logo, o que
eu preciso saber se a garrafa caiu e, no, se foi atingida. Em um game do tipo
Shooter seria diferente.

312

Manual do Indie Game Developer - Verso Android e iOS

Mas eu preciso de um teste de coliso, pois quero saber se a bola atingiu


o cho. Sempre que a bola cai no cho, reposicionada no lado esquerdo, no
ponto de incio. como se algum pegasse a bola e jogasse de volta.
Ento, usei um ContactListener para identificar isso.
Android
No Java, estou usando JBox2D, ento s preciso criar uma classe Java que
implemente a interface ContactListener. Eu criei uma inner class dentro
da classe Renderer.java:
class Contato implements ContactListener {
@Override
public void beginContact(org.jbox2d.dynamics.
contacts.Contact c) {

int id1 =

((Integer)c.getFixtureA().getBody().getUserData()).
intValue();

int id2 =

((Integer)c.getFixtureB().getBody().getUserData()).
intValue();
// A bola bateu no cho


if ((id1 == bola.getId() &&

id2 == 2) || (id1 == 2 &&

id2 == bola.getId())) {

bolaChao();

}

}
@Override
public void
Contact arg0) {

}

endContact(org.jbox2d.dynamics.contacts.

@Override
public void postSolve(org.jbox2d.dynamics.contacts.
Contact arg0,

Captulo 9 - Vamos Criar um


Game
313


}

ContactImpulse arg1) {

@Override
public void preSolve(org.jbox2d.dynamics.contacts.
Contact arg0,

Manifold arg1) {

}

}

O mtodo que me interessa o BeginContact, que recebe uma instncia


de org.jbox2d.dynamics.contacts.Contact. Atravs desta instncia, eu posso
acessar as instncias dos dois corpos que entraram em contato (Body no
JBox2D e b2Body no Box2D C++).
Neste momento, a nica maneira de identifcar os corpos que colidiram
atravs do atributo UserData. Neste caso, quando eu carrego um GameObject, passo o seu id dentro do UserData, logo, posso testar se foi a Bola
(id = 1) que colidiu com o cho (id = 2). Neste caso, eu invoco o mtodo
bolaChao(), da classe Renderer.java, que aciona o evento de reposicionar
a bola.
Para que nosso ContactListener funcione, temos que adicion-lo ao nosso
mundo Box2D. E o que fazemos no final mtodo onSurfaceChanged():
this.world.setContactListener(new Contato());

iOS
No iOS um pouco mais complicado... O Box2D feito em C++, logo, o
ContactListener tem que ser uma classe C++ , que estenda a classe b2ContactListener. Ento, eu criei uma classe separada no projeto iOS, formada
pelos arquivos Contato.h e Contato.mm (mm a extenso para cdigo
em C++ dentro do projeto).
#import Contato.h
#import OGBPGameObject.h
void Contato::BeginContact(b2Contact* contact) {
OGBPGameObject * id1 = (__bridge OGBPGameObject *)

(contact->GetFixtureA()->GetBody()>GetUserData());
OGBPGameObject * id2 = (__bridge OGBPGameObject *)

314

Manual do Indie Game Developer - Verso Android e iOS


(contact->GetFixtureB()->GetBody()->GetUserData());

// A bola bateu no cho

if ((id1.idGO == 1 &&
id2.idGO == 2) || (id1.idGO == 2 &&
id2.idGO == 1)) {
[this->vc bolaChao];
}

void Contato::EndContact(b2Contact* contact) {


}
void
Contato::PreSolve(b2Contact*
b2Manifold* oldManifold) {
}

contact,

const

void Contato::PostSolve(b2Contact*
b2ContactImpulse* impulse) {
}

contact,

const

Contato::Contato(OGCTViewController * mvc)
this->vc = mvc;
}

Contato::~Contato() {
}

Primeiramente, eu resolvi colocar a instncia de GameObject dentro do


UserData do Box2D, pois isto facilitaria muito a identificao do GameObject dentro do ContactListener:
body->SetUserData((__bridge void*)go);

Mas o que esse tal de __bridge? um cast que copia um ponteiro de


objeto NSObject para um ponteiro comum C++, sem transferncia de propriedade. Isto est relacionado com o ARC e a poltica de liberao de memria.
Sempre que formos usar uma instncia de qualquer NSObject com ponteiros
comuns (void*, int* etc), temos que fazer este cast.
Bem, no mtodo BeginContact eu recebo dois ponteiros void * e os
copio para ponteiros NSObject, de modo que eu possa usar suas propriedades
sem problemas.

Captulo 9 - Vamos Criar um


Game
315

Tem mais uma novidade: tive que criar uma propriedade apontando para o
View Controller, de modo a invocar o mtodo bolaChao. Esta propriedade
passada no momento em que crio a instncia do ContactListener:
if (!mContato) {
mContato = new Contato(self);
}
world->SetContactListener(mContato);

Registro de tempos
Eu precisava de uma maneira de registrar os melhores tempos em cada
nvel, funcionalidade comum em games mveis. Porm, deixei de fora a parte
de Social Gaming, que compartilhar seus pontos com os amigos.
claro que eu poderia usar um banco de dados SQLite para armazenar
a pontuao, porm, para simplificar as coisas, eu usei os mecanismos mais
comuns para armazenamento.
Android
No Android, eu crio um arquivo texto, usando os mtodos openFileOutput() e openFileInput(), da classe android.content.Context. Para isto,
criei uma classe Records.java que lida com a gravao e leitura da pontuao:
public static long[] getNiveis(Context context) {
Records.context = context;
if (Records.niveis == null) {

Records.niveis = loadLevels();
}
return Records.niveis;
}
private static long[] loadLevels() {
long [] levels = new long [Records.MAXLEVELS];
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
for (int x=0; x<Records.MAXLEVELS; x++) {

levels[x] = cal.getTimeInMillis();
}
String nome = records.txt;
try {

FileInputStream arquivo = Records.context.

316

Manual do Indie Game Developer - Verso Android e iOS


openFileInput(nome);

DataInputStream dis =
new DataInputStream(arquivo);

for (int x=0; x < Records.MAXLEVELS; x++) {

levels[x] = dis.readLong();

}

dis.close();

arquivo.close();
}
catch (FileNotFoundException fnf) {

try {

FileOutputStream saida = Records.
context.openFileOutput (nome, Context.MODE_PRIVATE);

DataOutputStream dos =
new DataOutputStream(saida);

for (int x=0; x < Records.MAXLEVELS;
x++) {

dos.writeLong(levels[x]);
}
dos.close();
saida.close();

}

catch (FileNotFoundException e) {

Log.e(RECORDS, Error creating new recors:
+
e.getMessage());

}

catch (IOException e) {

Log.e(RECORDS,
Exception
writing
records: +
e.getMessage());

}
}
catch (IOException e) {

Log.e(RECORDS, Exception reading records:
+
e.getMessage());
}
return levels;
}
public static void updateLevels(long [] niveis) {
try {

Captulo 9 - Vamos Criar um


Game
317

context.deleteFile(records.txt);

FileOutputStream saida = Records.context.
openFileOutput(records.txt, Context.MODE_PRIVATE);

DataOutputStream dos = new DataOutputStream(saida);

for (int x=0; x < Records.MAXLEVELS; x++) {
dos.writeLong(niveis[x]);

}

dos.close();

saida.close();
}
catch (FileNotFoundException e) {

Log.e(RECORDS, Error creating new recors:
+ e.getMessage());
}
catch (IOException e) {

Log.e(RECORDS, Exception writing records:
+ e.getMessage());
}

}

O mtodo loadLevels() retorna um array contendo a pontuao em cada


nvel. E o mtodo updateLevels() grava o arquivo com o contedo do array.
Eu criei uma tela que mostra a pontuao atual (Pontos.java), que l a
pontuao e mostra para o jogador com o comando:
long
[]
tempos
=
getApplicationContext());

Records.getNiveis(this.

Durante o game, se todos os NPCs forem derrubados, eu testo se o tempo


de jogo foi menor que o registrado para aquele nvel, e, neste caso, atualizo o
arquivo:
if (this.quantidadeNPC == 0) {
// Acabou!
this.tempoAviso = 3;
this.resetBola();




chutou = true;
trocarAviso(16, true);
acabou = true;
long [] niveis = Records.getNiveis(context);
if (diferencaTempo < niveis[cenaCorrente.getNumero()

318

Manual do Indie Game Developer - Verso Android e iOS


- 1]) {

niveis[cenaCorrente.getNumero()
diferencaTempo;
}
Records.updateLevels(niveis);
}

1]

iOS
Eu sei que j parece um bordo, mas: No iOS um pouco mais
complicado...
Eu resolvi usar uma Property List para armazenar a pontuao. Para isto,
criei um arquivo records.plist, dentro do grupo Supporting files:
<?xml version=1.0 encoding=UTF-8?>
<!DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN
http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version=1.0>
<dict>
<key>niveis</key>
<array>
<real>0.0</real>
<real>0.0</real>
<real>0.0</real>
</array>
</dict>
</plist>

Este arquivo apenas um modelo. O iOS no me deixa gravar nada dentro


do Application Bundle, logo, eu copio este arquivo para o diretrio gravvel
do dispositivo.
Eu criei uma classe BYBLRecords que l e armazena a pontuao dentro
de uma Property List, localizada no diretrio gravvel da aplicao. Eis os
mtodos desta classe:
@implementation BYBLRecords
+(NSMutableArray *)getNiveis
{
if (niveis == nil) {
[self loadLevels];
}
return niveis;
}

Captulo 9 - Vamos Criar um


Game
319

+(void)updateLevels:(NSMutableArray *)niveis
{
NSError *error;
NSString *errordesc;
NSFileManager* fileManager = [NSFileManager
defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomai
ns(NSDocumentDirectory, NSUserDomainMask, YES);
NSString
*documentsDirectory
=
[paths
objectAtIndex:0];
NSString *writableDBPath = [documentsDirectory str
ingByAppendingPathComponent:@records.plist];
BOOL success = [fileManager fileExistsAtPath:writab
leDBPath];
if (!success) {
NSString *defaultDBPath = [[[NSBundle mainBundle]
resourcePath] stringByAppendingPathComponent:@recor
ds.plist];
success = [fileManager copyItemAtPath:defaultDBPath
toPath:writableDBPath error:&error];
}
NSDictionary *plistDict = [NSDictionary
dictionaryWithObjects:
[NSArray arrayWithObjects: niveis, nil]
forKeys:[NSArray arrayWithObjects:
@niveis, nil]];
NSData *plistData = [NSPropertyListSerialization
dataFromPropertyList:plistDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errordesc];
if(plistData) {
writableDBPath = [documentsDirectory stringBy
AppendingPathComponent:@records.plist];
[plistData writeToFile:writableDBPath
atomically:YES];
}

320

Manual do Indie Game Developer - Verso Android e iOS


else {
NSLog(errordesc);
}
}
+(void) loadLevels
{
niveis
=
[[NSMutableArray
alloc]
initWithCapacity:MAXLEVELS];
for (int x=0; x<MAXLEVELS; x++) {
[niveis addObject:[NSNumber numberWithInt:0]];
}
NSString *errorDesc = nil;
NSPropertyListFormat format;
NSFileManager* fileManager = [NSFileManager
defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomai
ns(NSDocumentDirectory, NSUserDomainMask, YES);
NSString
objectAtIndex:0];

*documentsDirectory

[paths

NSString *writableDBPath = [documentsDirectory str


ingByAppendingPathComponent:@records.plist];
BOOL success = [fileManager fileExistsAtPath:writab
leDBPath];
if (!success) {
[self updateLevels:niveis];
NSString *writableDBPath = [documentsDirectory
stringByAppendingPathComponent:@records.plist];
}
NSData *plistXML = [[NSFileManager defaultManager]
contentsAtPath:writableDBPath];
NSDictionary
*temp
=
(NSDictionary
*)
[NSPropertyListSerialization
propertyListFromData:plistXML
mutabilityOption:NSPropertyListMutableContain
ersAndLeaves
format:&format
errorDescription:&errorDesc];

Captulo 9 - Vamos Criar um


Game
321
if (!temp) {
NSLog(@Error reading plist: %@, format: %d,
errorDesc, format);
}
niveis = [NSMutableArray arrayWithArray:[temp
objectForKey:@niveis]];
}
@end

Nos dois principais mtodos, eu testo se existe o arquivo records.plist


no diretrio gravvel da aplicao. Eu no pretendo explicar como funciona o
sistema de arquivos do iOS, mas recomendo que voc leia o documento File
System Programming Guide, da documentao do iOS:

https://developer.apple.com/library/ios/#documentation/FileManagement/
Conceptual/FileSystemProgrammingGUide/Introduction/Introduction.html

Para apresentar os pontos ao jogador, eu criei uma view, que ao ser invocada, l a Property List e mostra para o usurio (classe BYBLTextoViewController, mtodo viewDidLoad):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
CGRect rect = [[UIApplication sharedApplication]
statusBarFrame];
[[UIApplication sharedApplication]
setStatusBarHidden:YES withAnimation:UIStatusBarAnima
tionFade];
UIImage *img = [UIImage imageNamed:@splash.png];
CGSize landSize = CGSizeMake(self.view.frame.size.
height, self.view.frame.size.width +
rect.size.width);
UIColor *background = [[UIColor alloc]
initWithPatternImage:
[self resizeImage:img scaledToSize:landSize]
];
self.view.backgroundColor = background;
if (self.isAjuda) {
NSBundle * bundle = [NSBundle bundleForClass:self];

322

Manual do Indie Game Developer - Verso Android e iOS


[texto setText:[bundle localizedStringForKey:
@helptext value:@404 table:nil]];
}
else {
[self textoPontos];
}
}
- (void)textoPontos
{
NSBundle * bundle = [NSBundle bundleForClass:self];
NSString * cab = [bundle localizedStringForKey:@
headerpoints value:@X table:nil];
NSString
*
levelhdr
=
[bundle
localizedStringForKey:@level value:@X table:nil];
NSMutableString * saida = [[NSMutableString alloc]
initWithCapacity:200];
[saida appendString:cab];
NSDateFormatter *df = [[NSDateFormatter alloc]
init];
[df setDateFormat:@mm:ss];
NSMutableArray *pontos = [BYBLRecords getNiveis];
int x = 1;
for (NSNumber * ponto in pontos) {
NSTimeInterval wtempo = [ponto doubleValue];
[saida appendString:
[NSString stringWithFormat:@%@ %d:
%@\r\n,levelhdr,(x++),[df stringFromDate:[NSDate dat
eWithTimeIntervalSince1970:wtempo]]
]];

}
[texto setText: saida];

E, quando todos os NPCs caram, eu testo se o tempo foi menor que o


registrado para o nvel. Isto feito no mtodo tratarColisaoNPC, da classe
OGCTViewController:
if (quantidadeNPC == 0) {
// Acabou!
tempoAviso = 3;

Captulo 9 - Vamos Criar um


Game
323
[self resetBola];
chutou = YES;
[self trocarAviso:16 forcar:YES];
acabou = YES;
NSMutableArray * niveis = [BYBLRecords getNiveis];
NSTimeInterval tinterval = [(NSNumber *)
([niveis objectAtIndex:(cenaCorrente.numero - 1)])
doubleValue];
if (segundosDeJogo < tinterval || tinterval ==
0) {
[niveis setObject:

[NSNumber numberWithDouble:segundosDeJ
ogo]

atIndexedSubscript:(cenaCorrente.numero
- 1)];
}
}

[BYBLRecords updateLevels: niveis];

Gesto de memria no iOS


Fazer o game no Android simples: codificar, testar e pronto! Como estou
usando apenas Java (incluindo o JBox2D), o Garbage Collector se encarrega
de limpar a rea, aps o trmino de cada nvel.
Tanto que eu consigo jogar o Game vontade, mesmo em dispositivos
pequenos, como o meu LG P500, com 170 MB de memria RAM.
Assim que terminei de testar o game no simulador do iOS, instalei no meu
iPad e fui jogar, todo feliz. Mal consegui jogar dois nveis, e a tela apagou
toda, sem explicao. Ento, conectei ao Mac e rodei usando o Xcode e descobri o problema: memria!
Quando o iOS est com quantidade de memria baixa, ele informa sua
aplicao atravs do mtodo didReceiveMemoryWarning, e voc tem que
liberar memria rapidamente.
Por que isso acontece? No iOS ns no terminamos as aplicaes. Elas ficam sempre na memria, at que seja necessrio liberar memria. Neste game,
cada vez que o usurio escolhe um nvel, outra instncia do View Controller
criada e todas as informaes so carregadas novamente.

324

Manual do Indie Game Developer - Verso Android e iOS

Como temos muitas texturas, em certos momentos o sistema fica com baixa memria mesmo.
Como resolver este problema?
Existem alguns passos que voc deve seguir, de modo a se certificar que
est utilizando a memria e liberando quando no mais necessria.

Uso do GLKBaseEffect
Neste game, ns carregamos o GLKViewController a cada novo nvel jogado, entrando no modo OpenGL. Ento, a liberao de memria se torna
crtica.
H vrios posts na Internet, especialmente no Stack Overflow (http://
stackoverflow.com/), argumentando que o GLKBaseEffect tem algum tipo de
memory leak, ou seja, no est liberando memria corretamente.
Eu no detectei um Memory Leak especfico do GLKBaseEffect. Se
voc liberar as texturas e os buffers, provavelmente no ter problemas (veremos isto mais adiante). Eu preferi utilizar o OpenGL ES diretamente, pois
quero ter um controle maior do processo de renderizao.
No exemplo \Codigo\OpenGLiOS\OpenGLBasico1.zip, eu uso apenas
as funes do OpenGL ES, criando e compilando Shaders, logo, voc pode
usar este cdigo como base.
Voc ter que criar e compilar os Shaders, da mesma maneira que fazemos
na verso Android. E, alm disto, ter que alterar a maneira como carrega
texturas (eu suspeitei tambm do GLKTextureLoader).
Carregando texturas sem o GLKTextureLoader
Modifiquei significativamente o mtodo carregarCoordenadasTextura,
da classe OGCTViewController, que eu uso para carregar as imagens e passar para a GPU:
- (void)carregarCoordenadasTextura:
props imagem: (NSString*) nome
{

(OGBPGLProps

*)

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
CGImageRef spriteImage = CGImageRetain([UIImage
imageNamed:nome].CGImage);

Captulo 9 - Vamos Criar um


Game
325
if (!spriteImage) {
NSLog(@Failed to load image %@, nome);
exit(1);
}
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
GLubyte * spriteData = (GLubyte *) calloc(width*height*4,
sizeof(GLubyte));
CGContextRef spriteContext = CGBitmapContextCreate(
spriteData, width, height, 8, width*4,CGImageGetColorS
pace(spriteImage),
kCGImageAlphaPremultipliedLast);
CGContextDrawImage(spriteContext, CGRectMake(0, 0,
width, height),
spriteImage);
GLuint texName;
glGenTextures(1, &texName);
glBindTexture(GL_TEXTURE_2D, texName);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_
FILTER,
GL_LINEAR_MIPMAP_NEAREST);
glTexParameterf(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
GL_REPEAT);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height,
0, GL_RGBA,

GL_UNSIGNED_BYTE, spriteData);
glGenerateMipmap(GL_TEXTURE_2D);

326

Manual do Indie Game Developer - Verso Android e iOS


free(spriteData);
CGImageRelease(spriteImage);
CGContextRelease(spriteContext);
spriteImage = nil;
props.textureName = texName;
}

Eu carrego a imagem para uma referncia (CGImageRef) e crio um contexto Quartz, que desenha a imagem em uma rea que eu criei (spriteData).
Depois, eu transfiro a imagem para a GPU, com a funo glTextImage2D e
libero o contexto, a imagem e o buffer que aloquei.
Assim, eu garanto que no haver outras reas de memria com a imagem,
alm da utilizada pela GPU.
Renderizando sem o GLKBaseEffect
Ns temos que usar o programa que criamos com os nossos Shaders e passar
os argumentos necessrios, incluindo a matriz MVP (Model-View-Projection):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(_program);
if (cenaCorrente.texturaFundo != nil) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cenaCorrente.glProps.
textureName);
glBindBuffer(GL_ARRAY_BUFFER, cenaCorrente.glProps.
vobVertices);
glVertexAttribPointer(maPositionHandle, 3,
GL_FLOAT, GL_FALSE,

sizeof(GLfloat) * 3, BUFFER_OFFSET(0));
glEnableVertexAttribArray(maPositionHandle);
glBindBuffer(GL_ARRAY_BUFFER, _textureBuffer);
glVertexAttribPointer(maTextureHandle, 2, GL_
FLOAT, GL_FALSE,

sizeof(GLfloat) * 2, BUFFER_OFFSET(0));
glEnableVertexAttribArray(maTextureHandle);
GLKMatrix4 matrizModelo = GLKMatrix4Identity;
glUniformMatrix4fv(uniforms[UNIFORM_
MODELVIEWPROJECTION_MATRIX], 1, 0, matrizModelo.m);
glUniform1f(uniforms[UNIFORM_FADE_FACTOR],
1.0f);

Captulo 9 - Vamos Criar um


Game
327

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);

Para comear, temos que usar a funo glBindTexture para indicar qual
textura vamos usar, depois, temos que passar os identificadores dos atributos
de posio e textura que criamos nos Shaders (maPositionHandle e maTextureHandle), ao invs do padro que o GLKBaseEffect utiliza (GLKVertexAttribPosition e GLKVertexAttribTexCoord0).
Depois, temos que passar a matriz multiplicada Modelo-Cmera-Projeo
como um uniform para o nosso Shader. E, como estou usando o mesmo
Shader do exemplo de Sistema de Partculas, mantive o uniform que indica
o percentual de transparncia.

Procure Memory Leaks


O Xcode vem com uma ferramenta muito boa: Instruments, que permite executar o programa e observar o que est acontecendo. Para procurar
por Memory Leaks, execute seu aplicativo com o comando Profile (menu
Product / Profile). Ele vai recompilar seu programa e executar capturando
estatsticas.
Ao iniciar a instrumentao, voc pode escolher o que quer detetar, neste
caso, queremos saber se existem Leaks. Ele vai mostrar dois instrumentos:
Allocations e Leaks, conforme a figura a seguir.

Ilustrao 98: Anlise de memria

328

Manual do Indie Game Developer - Verso Android e iOS

Os picos que vemos no grfico mostram os momentos em que entrei em


algum nvel de jogo, e as baixas mostram quando sa. O grfico tem que ficar
neste formado, de vrios submarinos lado a lado. Temos que garantir que
a parte baixa permanea constante, sem aumento de memria alocada. Uma
boa viso a heapshots, que mostra o crescimento da memria em cada
momento.
Se houver Memory Leak de instncias de NSObjects, que esto sujeitas
ao ARC, isto aparecer no instrumento Leaks. Porm, no o caso.
O problema o crescimento da alocao de memria de um ciclo para outro. Ou seja, quando eu entro e saio de um nvel, no pode haver aumento de
memria alocada. Mas o que est acontecendo.
A viso heapshots, do instrumento Allocations, permite que eu marque
determinados instantes no tempo para posterior comparao. Rode seu programa e v realizando operaes. Quando julgar necessrio, clique no boto
Mark heap para marcar uma posio.
Depois, s parar a execuo (boto Stop) e analisar os heapshots.
Note que a coluna Heap Growth mostra o crescimento de memria alocada
a cada momento. No deveria haver crescimento nos heapshots 2, 3 e 5, quando eu sa de um nvel. Isto demonstra que h memria ainda no liberada, o
que pode forar o iOS a cancelar aplicaes.

Verifique se est realmente liberando a memria


A primeira coisa que voc deve fazer implementar o mtodo dealloc
no View Controller. Neste mtodo, voc tem que se certificar que todas as referncias esto liberadas. Como utilizamos ARC, no usamos mais o mtodo
release. Mas isto no significa que o ARC vai liberar imediatamente todas
as referncias. Pode haver referncias cruzadas que impeam a liberao de
memria pelo ARC.
Uma boa prtica a seguinte: faa com que todas as propriedades que referenciem objetos sejam nil. Eu criei um mtodo terminar, que chamado
quando um nvel termina ou quando o mtodo dealloc, do View Controller
invocado:
- (void)terminar
{
[self tearDownGL];

Captulo 9 - Vamos Criar um


Game
329
if ([EAGLContext currentContext] == self.context) {
/*
ATENO: muito importante tornar todas as
referncias ao contexto OpenGL = nil. Assim, o ARC
poder liberar a memria que foi utilizada por ele.
Caso contrrio, as texturas no sero liberadas e haver
um Memory Leak que no aparece na instrumentao, como
tal.
*/
self.context = nil;
view = (GLKView *)self.view;
view.context = nil;
[EAGLContext setCurrentContext:nil];

}
[self deleteBox2D];
self->mapaCaracteres = nil;
self->gameModel = nil;
self->cenaCorrente = nil;
self->modeloTela = nil;
panRecognizer = nil;
tapRecognizer = nil;
telaTopLeft = nil;
telaBottomRight = nil;
view = nil;
bolaBody = nil;
bola = nil;
nomeGame = nil;
nomeTela = nil;
inicio = nil;
qtdOrdem = nil;
aviso = nil;
world = NULL;

O mais importante liberar adequadamente o contexto OpenGL. Como eu


tenho referncias a ele na classe View Controller e na prpria view, tenho que
liberar esta memria antes de mais nada. Depois, eu nulo todas as propriedades que representem ponteiros para instncias de NSObjects.
No mtodo tearDownGL eu libero tudo o que aloquei:

330

Manual do Indie Game Developer - Verso Android e iOS


- (void)tearDownGL
{
[self deleteGameModel:gameModel];
[EAGLContext setCurrentContext:self.context];
self.effect = nil;
}
- (void) deleteGameModel: (OGBPGameModel * ) model
{
for (OGBPCena * cena in model.cenas) {
if (cena.glProps != nil) {
[self deleteGLprops:cena.glProps];
cena.glProps = nil;
}
for (OGBPGameObject * go in cena.objetos) {
if (go.glProps != nil) {
[self deleteGLprops:go.glProps];
GLuint buffer = go.glProps.vobVertices;
glDeleteBuffers(1, &buffer);
if (go.b2dBody != nil) {
world->DestroyBody(go.b2dBody);
go.b2dBody = nil;
}
}
}
}
glDeleteBuffers(1, &(_textureBuffer));
cenaCorrente = nil;
for (OGMTTextura * tex in modeloTela.texturas) {
GLuint
texture;
texture = tex.idTextura;
glDeleteTextures(1, &texture);
[self checkError:@DEL TXT];
}
for (OGMTPosicaoTela * pos in modeloTela.posicoes)
{
GLuint buffer = pos.hVobVertices;
glDeleteBuffers(1, &buffer);
[self checkError:@DEL VBO POS];
}

Captulo 9 - Vamos Criar um


Game
331

modeloTela = nil;

- (void) deleteGLprops: (OGBPGLProps * ) props


{
if (props.textureName != 0) {
GLuint name = props.textureName;
glDeleteTextures(1, &name);
}
if (props.vobVertices != 0) {
GLuint vVert = props.vobVertices;
glDeleteBuffers(1, &vVert);
}
}

Eu vou liberando a memria dos VBOs de vrtices e textura, alm das prprias texturas, de cada objeto do game, incluindo o cenrio de fundo. Depois,
tambm destruo os objetos Box2D que criei.
Finalmente, eu invoco o mtodo deleteBox2D, que libera a parte final
dos objetos Box2D:
- (void) deleteBox2D
{
if (mContato != nil && mContato->vc != nil) {
mContato->vc = nil;
delete mContato;
mContato = nil;
}

if (world != NULL) {
delete world;
}

Lembra-se que eu havia criado um ponteiro para o ViewController dentro


do ContactList? Eu preciso liber-lo tambm. E depois, preciso deletar o
prprio ContactList, que um objeto C++. A funo delete world faz o
resto, liberando qualquer memria ainda retida pelo Box2D.

332

Manual do Indie Game Developer - Verso Android e iOS

Concluso
O game ficou pronto e rodou bem em ambas as plataformas. Conseguimos
criar um game para Android e iOS utilizando Box2D e OpenGL ES.
Para os que no acreditam, vou provar.

Ilustrao 99: O game rodando em um dispositivo Android

Ilustrao 100: O mesmo game em um dispositivo iOS

Captulo 9 - Vamos Criar um


Game
333

Possveis melhorias no game


Eu criei um prottipo funcional de game. Ele serve para explicar os conceitos e tambm para que voc experimente tudo o que mostrei no livro.
possvel melhorar muito este game e eu comearia criando mais nveis,
com diferenas de dificuldade menos gritantes. Com apenas trs nveis, no
ter sucesso. Eu diria que 20 nveis, para comear, est bom.
Outra boa possibilidade de melhoria trabalhar totalmente em OpenGL
ES, evitando carga de views convencionais. Embora seja mais difcil, pois
no existe renderizao de textos, voc pode evitar diversos problemas, como:
lentido e memory leaks (iOS).
Finalmente, inclua algum tipo de progresso obrigatria, de modo a evitar
que o jogador zere os nveis rapidamente. Eu sugeriria:
1. No libere todos os nveis. S libere um nvel aps o jogador zerar
o anterior;
2. Crie grupos de nveis e estabelea limites de desempenho para liber-los. Por exemplo, para jogar os nveis de 4 a 6, necessrio fazer
uma mdia menor ou igual a seis segundos nos nveis de 1 a 3;
3. Os nveis de um mesmo grupo devem ter dificuldades parecidas. S
acrescente desafios ao mudar o nvel.
Em breve, eu publicarei uma verso profissional deste game contemplando todas estas melhorias.

Impresso e Acabamento
*UiFD(GLWRUD&LrQFLD0RGHUQD/WGD
7HO  

You might also like