ÍNDICE DE CONTEÚDO
Olá pessoal, me chamo Vaneska Sousa e estou no 2º semestre da faculdade de Sistemas e Mídias Digitais na Universidade Federal do Ceará. Ou seja, ainda estou engatinhando no aprendizado de desenvolvimento de software.
E como projeto de final de semestre eu precisei criar um remake, em Java, do jogo Breakout de 1978 para Atari 2600 e – como foi uma experiência bastante desafiadora, mas igualmente vantajosa, já que pude aprender muita coisa, em especial como melhor aplicar os conceitos de orientação a objeto – decidi compartilhar com vocês esse desafio.
Então no final desse artigo você terá um projeto estruturado com a raquete se movendo na tela.
Por que Java e por que TotalCross?
Todos os exercícios, tanto da cadeira de Matemática Aplicada à Multimídia quanto de Programação II, no meu curso, são ensinadas usando Processing Engine, que usa Java. Isso porque Java é uma ótima linguagem para aprender melhor os conceitos por ser fortemente tipada.
Então, apesar de ser livre para escolher qualquer linguagem ou framework, optei por continuar no Java para tentar aplicar melhor o que eu aprendi nesse semestre conturbado (EAD é sempre na base do desespero), porém utilizando algum framework para não precisar fazer tudo do zero. Considerei seriamente usar Godot, entretanto eu pouco precisaria programar, além de não conhecer tão bem esta ferramenta.
Já TotalCross é um framework que tem uma game engine simples e me permite gerar para Linux Arm e smartphones, o que poderia encantar os meus professores – além do fato de eu trabalhar nesta empresa, tendo acesso a desenvolvedores bem mais experientes que eu e que conhecem muito bem a plataforma para me ajudar -. Logo me pareceu o caminho mais seguro e, apesar de alguns perrengues, não me arrependo nem um pouco. É muito legal conseguir desenrolar todo o projeto e no final vê-lo rodando no celular e no Raspberry Pi.
Minha versão do Atari Breakout feito com Java e TotalCross rodando no Raspberry Pi 3 Model B
Definindo mecânicas e estrutura do projeto Atari Breakout
Ao iniciar o desenvolvimento de qualquer que seja a aplicação, em especial quando se trata de jogos, precisamos ter em mente as principais features ou as mecânicas que serão implementadas. Para isso precisei assistir algumas muitas vezes a gameplay original e jogar algumas versões pelas internet.
No final eu obtive as seguintes mecânicas:
- Plataforma se move para esquerda ou para direita, conforme o comando do usuário. Quando chega numa extremidade, ela bata na “parede” (borda).
- A bolinha bate na plataforma e volta na direção contrária a que veio;
- Cada vez que a bolinha bate em um “tijolo” (azul, verde, amarelo, laranja, vermelho) ele some.
- Quando se destrói todos os tijolos do nível 01, aparecem novos (na mesma posição do anterior) e a velocidade da bolinha aumenta.
- Quando todos os tijolos do nível 02 são destruídos, o joguinho continua mesmo sem nenhum obstáculo na tela.
- O jogo só acaba quando a bolinha cai.
E estes são os pontos que precisamos construir em nosso programa, com base neles já podemos notar que teremos 3 objetos principais: plataforma, bola e tijolos. Com isso já podemos ter uma noção da estrutura do nosso projeto.
Estrutura do Projeto
Pensei da seguinte maneira:
- RunBreakoutApplication.java: Classe responsavél por chamar a classe que herda a Game Engine e roda o simulador;
- Breakout.java: Nossa classe principal, que herda da GameEngine e “monta” o jogo, onde vamos chamar os objetos, definir posições e etc.;
- sprites: pacote onde irá todas as classes que são responsáveis pelos sprites (imagem e comportamento dos blocos, plataforma e bola);
- util: pacotes com as classes usadas para facilitar a manutenção do projeto como constantes, inicialização de imagens e cores.
Dito isso, vamos para o código!
Mão no código
Para começar você precisa instalar o plugin TotalCross no VS Code e criar um projeto. Caso você esteja usando outra IDE pode seguir o passo a passo da documentação.
Como eu estou usando o plugin, basta pressionar ctrl + P, digitar totalcross e clicar na opção create new project. Depois disso preenchi o que pede da seguinte maneira:
- Nome da pasta selecionada: gameTC
- Artifactid: com.totalcross
- Nome do projeto: Breakout
- Versão do TotalCross: 6.1.1 (a mais recente)
- Plataformas para build: -android e -linux_arm (mas você pode selecionar as plataformas que desejar)
Ao gerar, se você for na classe RunBreakoutApplication.java clicar com o botão direito em cima dela e em seguida clicar em run irá abrir o simulador e aparecer o hello world na sua tela. Se isso acontecer, você está com o projeto Java com TotalCross devidamente criado.
Caso aconteça algum problema, você pode conferir a documentação ou perguntar na comunidade Totalcross no Discord ou Telegram.
Depois do projeto devidamente configurado, o próximo passo é adicionar as imagens que serão usadas no projeto dentro de resources > sprites. Todos os arquivos estão aqui. E logo em seguida você pode criar dois pacotes chamados util e sprites. Vamos trabalhar neles nos próximos tópicos.
No final a estrutura do seu projeto estará da seguinte maneira:
Preparando o terreno
Para facilitar a manutenção do código e mudanças de imagens nas cores que você quer utilizar, vamos criar algumas classes que centralizam isso. Você pode ler mais sobre os benefícios que essa prática clicando aqui. Todas as classes que tem essa função vai dentro do pacote util.
Constants.java
A primeira classe a ser criada é a de constantes, onde vai padrões de posicionamentos (como a borda entre a tela e onde a plataforma começa), velocidade, número de blocos e etc. Essa parte é boa para brincar, mudar os números e entender onde muda e porquê. É um ótimo exercício para quem está começando.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.totacross.util; import totalcross.sys.Settings; import totalcross.ui.Control; import totalcross.util.UnitsConverter; public class Constants { //Position public static final int BOTTOM_EDGE = UnitsConverter.toPixels(430 + Control.DP); public static final int DP_23 = UnitsConverter.toPixels(23 + Control.DP); public static final int DP_50 = UnitsConverter.toPixels(50 + Control.DP); public static final int DP_100 = UnitsConverter.toPixels(100 + Control.DP); //Sprites public static final int EDGE_RACKET = UnitsConverter.toPixels(20 + Control.DP); public static final int WIDTH_BALL = UnitsConverter.toPixels(15 + Control.DP); public static final int HEIGHT_BALL = UnitsConverter.toPixels(15 + Control.DP); //Bricks public static final int NUM_BRICKS = 10; public static final int WIDTH_BRICKS = Settings.screenWidth / NUM_BRICKS; public static final int HEIGHT_BRICKS = Settings.screenHeight / 32; //Brick Points public static final int BLUE_POINT = 1; public static final int GREEN_POINT = 2; public static final int YELLOW_POINT = 3; public static final int DARK_ORANGE_POINT = 4; public static final int ORANGE_POINT = 5; public static final int RED_POINT = 6; } |
Se você quiser saber mais sobre o que é a unidade DP, recomendo essa leitura aqui.
Colors.java
Como o nome já diz, nessa classe vamos colocar as cores que iremos utilizar durante o jogo. Recomendo que coloque os nomes de acordo com o objetivo daquela cor, como background, color font e etc. Dessa forma fica mais fácil no futuro você atualizar a paleta de cores do seu projeto em uma só classe e não piorar a leiturabilidade do seu código.
A seguir a classe utilizada neste projeto:
1 2 3 4 5 6 7 8 |
package com.totacross.util; public class Colors { public static int PRIMARY = 0x161616; public static int P_FONT = 0xFFFFFF; public static int SECONDARY = 0xE63936; public static int SECONDARY_DARK = 0xCE3737; } |
Images.java
A classe das imagens é sem dúvida a mais utilizada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.totacross.util; import static com.totacross.util.Constants.*; import totalcross.ui.dialog.MessageBox; import totalcross.ui.image.Image; public class Images { public static Image paddle, ball; public static Image red, orange, dark_orange, yellow, green, blue; public static void loadImages() { try { // general paddle = new Image("sprites/paddle.png"); ball = new Image("sprites/ball.png").getScaledInstance(WIDTH_BALL, HEIGHT_BALL); // Bricks red = new Image("sprites/red_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); orange = new Image("sprites/orange_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); dark_orange = new Image("sprites/orange2_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); yellow = new Image("sprites/yellow_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); green = new Image("sprites/green_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); blue = new Image("sprites/blue_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS); } catch (Exception e) { MessageBox.showException(e, true); } } } |
O método getScaledInstance() vai manipular a imagem para ficar de acordo com os valores que passamos através da constante. Vale a pena tentar alterar estes valores e observar o impacto no jogo.
Recapitulando
Só para conferir, nosso projeto até agora está da seguinte maneira:
Criando a primeira sprite (plataforma)
Agora que estamos com o projeto devidamente estruturado, vamos criar nossa primeira classe dentro do pacote sprite, que será o paddle.java: a plataforma/bastão que é o objeto de interação com o usuário.
Paddle.java
Essa classe deve herdar de Sprite, que é a classe responsável por esses objetos em jogos e são conceitos básicos que games engines já são estruturados para facilitar ao desenvolvedor. Com TotalCross não é diferente, ao herdar de sprites o framework já vai se preocupar em delimitar a movimentação dentro da tela, detectar colisão entre sprites e outros pontos importantes. Você pode conferir tudo em detalhes no javadoc.
Se lembrar bem, o paddle se move em uma determinada velocidade no eixo X de acordo com o comando do usuário. Sendo assim, a classe Paddle.java será responsável por definir essa movimentação e qual a imagem (o “rosto”) que a sprite deve assumir.
Veja o código abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.totacross.sprites; import com.totacross.util.Images; import totalcross.game.Sprite; import totalcross.ui.image.ImageException; public class Paddle extends Sprite { private static final int SPEED = 4; public Paddle() throws IllegalArgumentException, IllegalStateException, ImageException { super(Images.paddle, -1, true, null); } //Move the platform according the speed and the direction public final void move(boolean left, int speed) { if (left) { centerX -= SPEED; } else { centerX += SPEED; } setPos(centerX, centerY, true); } } |
Dentro do construtor indicamos qual a imagem (Images.paddle) e dentro do método move (uma facilidade do TotalCross) vamos receber a velocidade que foi definida no início da classe. Você pode depois experimentar outros valores e observar a movimentação.Então vamos testar se o paddle se movendo, caso seja para a esquerda é definimos que o centro da paddle naquele momento é ele mesmo menos a velocidade, e caso o contrário (para a direita), acrescentamos a velocidade.
No final definimos a posição da sprite na tela.
Com isso nossa sprite está pronta, precisamos agora adicioná-la na tela e pegar qual é o movimento do usuário com o mouse para, assim, poder chamar o método move e criar a movimentação. Para isso vamos para a nossa classe principal, Breakout.java.
Adicionando na tela e interação com o usuário
Como estamos construindo nossa game engine agora, teremos alguns pontos padrões para nos atentar. Para não ficar muito extenso, vou adicionar comentários no código.
Entretanto, basicamente, você irá apagar o método initUI que foi gerado automaticamente e, ao invés de herdar de MainWindow, vai herdar de GameEngine. Com isso, aparecerá um “vermelhinho” no nome da sua classe, basta clicar na lâmpada ou no símbolo de sugestão da IDE que você usa e clicar na opção “add unimplemented methods”. Dessa forma, será gerado automaticamente o método onGameInit() que, como o nome já diz, será responsável pelo o momento que o jogo é inicializado, ou seja, a classe breakout é chamada.
Outro ponto é que dentro do construtor você deve adicionar o tipo de estilo (MaterialUI), o tempo refresh na tela (70) e sinalizar que o jogo tem uma interface (gameHasUI = true;). Caso você queira saber mais sobre esses padrões, basta comentar aqui ou no Fórum da TotalCross!
Por último, mas não menos importante, para fazer o jogo realmente iniciar você tem que indicar isso através do this.start(); no onGameInit() e, então, se atentar para alguns outros métodos:
- onGameInit(): como comentei anteriormente, esse é o primeiro método chamado. Nele, você deve inicializar as sprites e as imagens (Images.loadImages) e dizer ao jogo que pode começar;
- onGameStart(): aqui é quando o jogo começa. É nesse método que iremos setar a posição inicial da plataforma (no centro da tela no eixo x e embaixo com uma borda no eixo Y);
- onPaint(): é onde dizemos o que vai ser desenhado a cada frame, primeiro pintando o fundo de preto para não deixar rastros das sprites e, em seguida, dizendo que a sprite pode ser exibida (através do .show());
- onPenDrag e onPenDown: são métodos que identificarão se o usuário está arrastando o dedo na tela (caso seja touch) ou movendo o mouse, enquanto o botão esquerdo é pressionado. É nesses métodos que mudaremos o movimento da paddle através do método setPos(), que irá acionar o método move lá em nossa classe Paddle.java. Note que o último parâmetro do método racket.setPos é true, justamente para delimitar a movimentação da paddle dentro da tela, de forma que ela nunca suma do campo de vista do usuário.
E agora, o código!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
package com.totacross; import com.totacross.sprites.Paddle; import com.totacross.util.Colors; import com.totacross.util.Constants; import com.totacross.util.Images; import totalcross.game.GameEngine; import totalcross.sys.Settings; import totalcross.ui.MainWindow; import totalcross.ui.dialog.MessageBox; import totalcross.ui.event.PenEvent; import totalcross.ui.gfx.Graphics; public class Breakout extends GameEngine { private Paddle racket; public Breakout() { setUIStyle(Settings.MATERIAL_UI); gameName = "Breakout"; gameVersion = 100; gameHasUI = true; gameRefreshPeriod = 70; } @Override public void onGameInit() { setBackColor(Colors.PRIMARY); Images.loadImages(); try { racket = new Paddle(); } catch (Exception e) { MessageBox.showException(e, true); MainWindow.exit(0); } this.start(); } public void onGameStart() { racket.setPos(Settings.screenWidth / 2, (Settings.screenHeight - racket.height) - Constants.EDGE_RACKET, true); } //to draw the interface @Override public void onPaint(Graphics g) { super.onPaint(g); if (gameIsRunning) { g.backColor = Colors.PRIMARY; g.fillRect(0, 0, this.width, this.height); if (racket != null) { racket.show(); } } } //To make the paddle moving with the mouse/press moviment @Override public final void onPenDown(PenEvent evt) { if (gameIsRunning) { racket.setPos(evt.x, racket.centerY, true); } } @Override public final void onPenDrag(PenEvent evt) { if (gameIsRunning) { racket.setPos(evt.x, racket.centerY, true); } } } |
Executando
Para executar, basta ir na classe RunBreakoutApplication.java com o botão direito e clicar em run para ver como ficou (imagem abaixo);
E se você mudar os parâmetros na classe RunBreakoutApplication.java para a linha abaixo:
1 |
TotalCrossApplication.run(Breakout.class, "/scr", "848x480"); |
De forma que fique com o tamanho da tela do Raspberry Pi, você obterá o seguinte resultado:
E está pronto a primeira sprite e mecânica!
Próximos passos
E agora no próximo artigo vamos adicionar o sprite da bola e fazer a colisão, mas para isso preciso saber se vocês realmente gostaram e em que posso melhorar para no próximo conteúdo. Por isso conta nos comentários!
Se precisar de ajuda com qualquer coisa, pode nos chamar no grupo do discord ou publicar em nosso fórum que estou à disposição para ajudar!
E se você chegou até aqui e pôs em prática qualquer etapa desse artigo, compartilhe comigo a experiência. Todo feedback é importante! E, se puder, favorite o TotalCross no GitHub pois ajuda o projeto a ter relevância na plataforma.
Muito obrigada, pessoal!
Saiba mais
Como utilizar o framework TotalCross na Raspberry Pi 3
Expansão de portas de um Raspberry Pi usando serial e Arduino – Parte 1
Vaneska, gostei bastante da intro do seu projeto, e principalmente por estar bastante organizado por sinal. Totalcross parece bem legal de se trabalhar, lembro que cheguei a usar o Superwaba, em 2004, rodando no meu PalmTop, sei que faz um bom tempo mas já naquela época achava bem legal trabalhar com a plataforma. Por favor, continue a mostrar o desenvolvimento do seu projeto, estou gostando bastante e acompanhando sua evolução aqui. Parabéns pelo projeto! Abraços