Guia definitivo para a linguagem de processamento Parte II: Construindo um jogo simples

Publicados: 2022-03-11

Esta é a segunda parte do guia definitivo para a linguagem Processing. Na primeira parte, dei um passo a passo básico da linguagem Processing. O próximo passo para você aprender Processamento é simplesmente mais programação prática.

Neste artigo, mostrarei como usar o Processing para implementar seu próprio jogo, passo a passo. Cada etapa será explicada em detalhes. Então, vamos portar o jogo para a web.

Construa um jogo simples com a linguagem Processing.

Antes de começarmos o tutorial de Processamento, aqui está o código do exercício do logotipo do DVD da parte anterior. Se você tiver alguma dúvida, não deixe de deixar um comentário.

Tutorial de Processamento: Um Jogo Simples

O jogo que vamos construir neste tutorial de Processamento é uma espécie de combinação de Flappy Bird, Pong e Brick Breaker. A razão pela qual escolhi um jogo como este é que ele tem a maioria dos conceitos com os quais os iniciantes lutam ao aprender o desenvolvimento de jogos. Isso é baseado na minha experiência de quando eu era assistente de ensino, ajudando novos programadores a aprender a usar o Processing. Esses conceitos incluem gravidade, colisões, manutenção de pontuações, manipulação de diferentes telas e interações teclado/mouse. Flappy Pong tem todos eles nele.

Jogue agora!

Sem usar conceitos de programação orientada a objetos (OOP), não é fácil construir jogos complexos, como jogos de plataforma com vários níveis, jogadores, entidades etc. À medida que avançamos, você verá como o código fica complicado muito rápido. Fiz o meu melhor para manter este tutorial de Processamento organizado e simples.

Eu aconselho você a seguir o artigo, pegar o código completo, jogar com ele por conta própria, começar a pensar em seu próprio jogo o mais rápido possível e começar a implementá-lo.

Então vamos começar.

Construindo Flappy Pong

Etapa 1 do tutorial de processamento: inicializar e lidar com telas diferentes

O primeiro passo é inicializar nosso projeto. Para começar, vamos escrever nossa configuração e desenhar blocos como de costume, nada extravagante ou novo. Em seguida, lidaremos com diferentes telas (tela inicial, tela de jogo, tela de game over etc.). Então surge a pergunta, como fazemos para que o Processing mostre a página correta no momento correto?

A realização desta tarefa é bastante simples. Teremos uma variável global que armazena as informações da tela atualmente ativa. Em seguida, desenhamos o conteúdo da tela correta dependendo da variável. No bloco draw, teremos uma instrução if que verifica a variável e exibe o conteúdo da tela de acordo. Sempre que quisermos mudar a tela, mudaremos essa variável para o identificador da tela que queremos que ela exiba. Com isso dito, aqui está como nosso código de esqueleto se parece:

 /********* VARIABLES *********/ // We control which screen is active by settings / updating // gameScreen variable. We display the correct screen according // to the value of this variable. // // 0: Initial Screen // 1: Game Screen // 2: Game-over Screen int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // Display the contents of the current screen if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // codes of initial screen } void gameScreen() { // codes of game screen } void gameOverScreen() { // codes for game over screen } /********* INPUTS *********/ public void mousePressed() { // if we are on the initial screen when clicked, start the game if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // This method sets the necessary variables to start the game void startGame() { gameScreen=1; }

Isso pode parecer assustador no começo, mas tudo o que fizemos foi construir a estrutura básica e separar diferentes partes com blocos de comentários.

Como você pode ver, definimos um método diferente para cada tela a ser exibida. Em nosso bloco de desenho, simplesmente verificamos o valor de nossa variável gameScreen e chamamos o método correspondente.

Na parte void mousePressed(){...} , estamos ouvindo os cliques do mouse e se a tela ativa for 0, a tela inicial, chamamos o método startGame() que inicia o jogo como esperado. A primeira linha deste método altera a variável gameScreen para 1, a tela do jogo.

Se isso for entendido, o próximo passo é implementar nossa tela inicial. Para fazer isso, estaremos editando o método initScreen() . Aqui vai:

 void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }

Agora nossa tela inicial tem um fundo preto e um texto simples, “Clique para iniciar”, localizado no meio e alinhado ao centro. Mas quando clicamos, nada acontece. Ainda não especificamos nenhum conteúdo para nossa tela de jogo. O método gameScreen() não tem nada nele, então não estamos cobrindo o conteúdo anterior extraído da última tela (o texto) tendo background() como a primeira linha do desenho. É por isso que o texto ainda está lá, mesmo que a linha text() não esteja mais sendo chamada (assim como o exemplo da bola em movimento da última parte que estava deixando um rastro para trás) . O fundo ainda é preto pelo mesmo motivo. Então vamos em frente e começar a implementar a tela do jogo.

 void gameScreen() { background(255); }

Após essa alteração, você notará que o fundo fica branco e o texto desaparece.

Tutorial de Processamento Etapa #2: Criando a Bola e Implementando a Gravidade

Agora, vamos começar a trabalhar na tela do jogo. Vamos primeiro criar nossa bola. Devemos definir variáveis ​​para suas coordenadas, cor e tamanho porque podemos querer alterar esses valores mais tarde. Por exemplo, se queremos aumentar o tamanho da bola à medida que o jogador pontua mais alto, para que o jogo seja mais difícil. Precisaremos alterar seu tamanho, portanto, deve ser uma variável. Também definiremos a velocidade da bola, depois de implementarmos a gravidade.

Primeiro, vamos adicionar o seguinte:

 ... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); }

Definimos as coordenadas como variáveis ​​globais, criamos um método que desenha a bola, chamado do método gameScreen . A única coisa a prestar atenção aqui é que inicializamos as coordenadas, mas as definimos em setup() . A razão pela qual fizemos isso é que queríamos que a bola começasse a um quarto da esquerda e um quinto de cima. Não há nenhuma razão específica para querermos isso, mas esse é um bom ponto para a bola começar. Portanto, precisávamos obter a width e a height do esboço dinamicamente. O tamanho do sketch é definido em setup() , após a primeira linha. width e height não são definidas antes da execução de setup() , é por isso que não poderíamos conseguir isso se definimos as variáveis ​​no topo.

Gravidade

Agora, implementar a gravidade é realmente a parte mais fácil. Existem apenas alguns truques. Aqui está a implementação primeiro:

 ... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // keep ball in the screen void keepInScreen() { // ball hits floor if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // ball hits ceiling if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } }

E o resultado é:

Uma bola quicando indefinidamente com pseudo-gravidade.

Segure seus cavalos, físico. Eu sei que não é assim que a gravidade funciona na vida real. Em vez disso, isso é mais um processo de animação do que qualquer outra coisa. A variável que definimos como gravity é apenas um valor numérico - um float para que possamos usar valores decimais, não apenas inteiros - que adicionamos a ballSpeedVert em cada loop. E ballSpeedVert é a velocidade vertical da bola, que é adicionada à coordenada Y da bola ( ballY ) em cada loop. Observamos as coordenadas da bola e garantimos que ela permaneça na tela. Se não o fizéssemos, a bola cairia ao infinito. Por enquanto, nossa bola só se move na vertical. Então, observamos os limites do piso e do teto da tela. Com o método keepInScreen() , verificamos se ballY ( + o raio) é menor que height , e da mesma forma ballY ( - o raio) é maior que 0 . Se as condições não atenderem, fazemos a bola quicar (de baixo ou de cima) com makeBounceBottom() e makeBounceTop() . Para fazer a bola quicar, simplesmente movemos a bola para o local exato onde ela deveria quicar e multiplicamos a velocidade vertical ( ballSpeedVert ) por -1 (multiplicando por -1 muda o sinal). Quando o valor da velocidade tem um sinal de menos, adicionando a coordenada Y a velocidade se torna ballY + (-ballSpeedVert) , que é ballY - ballSpeedVert . Assim, a bola muda imediatamente de direção com a mesma velocidade. Então, à medida que adicionamos gravity a ballSpeedVert e ballSpeedVert tem um valor negativo, ele começa a se aproximar de 0 , eventualmente se torna 0 e começa a aumentar novamente. Isso faz a bola subir, subir mais devagar, parar e começar a cair.

Uma bola quicando indefinidamente em uma raquete.

No entanto, há um problema com nosso processo de animação - a bola continua quicando. Se este fosse um cenário do mundo real, a bola teria enfrentado resistência do ar e atrito toda vez que tocasse uma superfície. Esse é o comportamento que queremos para o processo de animação do nosso jogo, então implementar isso é fácil. Adicionamos o seguinte:

 ... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); }

E agora nosso processo de animação produz isso:

Uma bola quicando, mas parando devido ao atrito.

Como o nome sugere, o friction é o atrito da superfície e o airfriction do ar é o atrito do ar. Então, obviamente, o friction tem que ser aplicado cada vez que a bola toca qualquer superfície. a airfriction do ar, no entanto, deve ser aplicada constantemente. Então foi isso que fizemos. O método applyGravity() é executado em cada loop, então retiramos 0.0001 por cento de seu valor atual de ballSpeedVert em cada loop. makeBounceBottom() e makeBounceTop() são executados quando a bola toca qualquer superfície. Então, nesses métodos, fizemos a mesma coisa, só que desta vez com friction .

Tutorial de Processamento Etapa #3: Criando a Raquete

Agora precisamos de uma raquete para a bola quicar. Deveríamos estar controlando a raquete. Vamos torná-lo controlável com o mouse. Aqui está o código:

 ... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }

Definimos a cor, largura e altura da raquete como uma variável global, podemos querer que elas mudem durante o jogo. Implementamos um método drawRacket() que faz o que seu nome sugere. Colocamos o rectMode no centro, para que nossa raquete fique alinhada ao centro do nosso cursor.

Agora que criamos a raquete, temos que fazer a bola quicar nela.

 ... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // racket moving up if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } }

E aqui está o resultado:

Uma bola quicando em uma raquete, mas parando devido ao atrito.

Então, o que watchRacketBounce() faz é garantir que a raquete e a bola colidam. Há duas coisas para verificar aqui, que é se a bola e a raquete estão alinhadas vertical e horizontalmente. A primeira instrução if verifica se a coordenada X do lado direito da bola é maior que a coordenada X do lado esquerdo da raquete (e vice-versa). Se for, a segunda instrução verifica se a distância entre a bola e a raquete é menor ou igual ao raio da bola (o que significa que estão colidindo) . Então, se essas condições forem atendidas, o método makeBounceBottom() é chamado e a bola quica em nossa raquete (em mouseY , onde a raquete está).

Você notou a overhead variável que é calculada por mouseY - pmouseY ? As variáveis pmouseX e pmouseY armazenam as coordenadas do mouse no quadro anterior. Como o mouse pode se mover muito rápido, há uma boa chance de não detectarmos a distância entre a bola e a raquete corretamente entre os quadros se o mouse estiver se movendo em direção à bola com rapidez suficiente. Então, tomamos a diferença das coordenadas do mouse entre os quadros e levamos isso em consideração ao detectar a distância. Quanto mais rápido o mouse estiver se movendo, maior será a distância aceitável.

Também usamos overhead por outro motivo. Detectamos para que lado o mouse está se movendo verificando o sinal de overhead . Se a sobrecarga for negativa, o mouse estava em algum lugar abaixo do quadro anterior, então nosso mouse (raquete) está se movendo para cima. Nesse caso, queremos adicionar uma velocidade extra à bola e movê-la um pouco além do salto normal para simular o efeito de bater na bola com a raquete. Se o overhead for menor que 0 , nós o adicionamos a ballY e ballSpeedVert para fazer a bola subir mais alto e mais rápido. Portanto, quanto mais rápido a raquete atingir a bola, mais alto e mais rápido ela subirá.

Tutorial de Processamento Etapa #4: Movimento Horizontal e Controle da Bola

Nesta seção, adicionaremos movimento horizontal à bola. Então, possibilitaremos controlar a bola horizontalmente com nossa raquete. Aqui vamos nós:

 ... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }

E o resultado é:

Uma bola quicando agora horizontalmente também.

A ideia aqui é a mesma que fizemos para o movimento vertical. Criamos uma variável de velocidade horizontal, ballSpeedHorizon . Criamos um método para aplicar velocidade horizontal à ballX e eliminar o atrito do ar. Adicionamos mais duas instruções if ao método keepInScreen() que observará a bola para acertar as bordas esquerda e direita da tela. Finalmente, criamos makeBounceLeft() e makeBounceRight() para lidar com os saltos da esquerda e da direita.

Agora que adicionamos velocidade horizontal ao jogo, queremos controlar a bola com a raquete. Como no famoso jogo do Atari Breakout e em todos os outros jogos de quebra de tijolos, a bola deve ir para a esquerda ou para a direita de acordo com o ponto da raquete que atinge. As bordas da raquete devem dar à bola mais velocidade horizontal, enquanto o meio não deve ter nenhum efeito. Codifique primeiro:

 void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } }

O resultado é:

Física horizontal estilo breakout.

Adicionar essa linha simples ao watchRacketBounce() fez o trabalho. O que fizemos foi determinar a distância do ponto que a bola atinge do centro da raquete com ballX - mouseX . Então, fazemos a velocidade horizontal. A diferença real era demais, então tentei algumas vezes e percebi que um décimo do valor parece o mais natural.

Tutorial de Processamento Etapa #5: Criando as Paredes

Nosso esboço está começando a parecer mais um jogo a cada passo. Nesta etapa, adicionaremos paredes se movendo para a esquerda, assim como no Flappy Bird:

 ... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // This arraylist stores data of the gaps between the walls. Actuals walls are drawn accordingly. // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // draw actual walls rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } }

E isso resultou em:

Uma bola quicando através de um nível com paredes.

Mesmo que o código pareça longo e intimidador, prometo que não há nada difícil de entender. A primeira coisa a notar é ArrayList . Para quem não sabe o que é um ArrayList , é apenas uma implementação de list que funciona como um Array, mas tem algumas vantagens sobre ele. É redimensionável, possui métodos úteis como list.add(index) , list.get(index) e list.remove(index) . Mantemos os dados da parede como arrays inteiros dentro da arraylist. Os dados que mantemos nas matrizes são para o espaço entre duas paredes. As matrizes contêm os seguintes valores:

 [gap wall X, gap wall Y, gap wall width, gap wall height]

As paredes reais são desenhadas com base nos valores das paredes de folga. Observe que tudo isso poderia ser tratado melhor e mais limpo usando classes, mas como o uso de Programação Orientada a Objetos (POO) não está no escopo deste tutorial de Processamento, é assim que vamos lidar com isso. Temos dois métodos básicos para gerenciar as paredes, wallAdder() e wallHandler .

O método wallAdder() simplesmente adiciona novas paredes em cada milissegundo wallInterval ao arraylist. Temos uma variável global lastAddTime que armazena a hora em que a última parede foi adicionada (em milissegundos) . Se o milissegundo atual millis() menos o último milissegundo adicionado lastAddTime for maior que nosso valor de intervalo wallInterval , significa que agora é hora de adicionar uma nova parede. As variáveis ​​de intervalo aleatório são então geradas com base nas variáveis ​​globais definidas no topo. Em seguida, uma nova parede (matriz de inteiros que armazena os dados da parede de intervalo) é adicionada à lista de matrizes e o lastAddTime é definido como o milissegundo milissegundos atual millis() .

wallHandler() percorre as paredes atuais que estão na lista de matrizes. E para cada item em cada loop, ele chama wallRemover(i) , wallMover(i) e wallDrawer(i) pelo valor do índice do arraylist. Esses métodos fazem o que seu nome sugere. wallDrawer() desenha as paredes reais com base nos dados da parede do intervalo. Ele pega a matriz de dados da parede da lista de matrizes e chama o método rect() para desenhar as paredes para onde elas deveriam estar. O método wallMover() pega o elemento da lista de matrizes, altera sua localização X com base na variável global wallSpeed . Finalmente, wallRemover() remove as paredes da lista de arrays que estão fora da tela. Se não fizéssemos isso, o Processing os trataria como ainda estão na tela. E isso teria sido uma enorme perda de desempenho. Portanto, quando uma parede é removida da lista de matrizes, ela não é desenhada nos loops subsequentes.

O último desafio que resta a fazer é detectar colisões entre a bola e as paredes.

 void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // collides with upper wall } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // collides with lower wall } }

watchWallCollision() é chamado para cada parede em cada loop. Pegamos as coordenadas da parede do vão, calculamos as coordenadas das paredes reais (superior e inferior) e verificamos se as coordenadas da bola colidem com as paredes.

Etapa 6 do tutorial de processamento: integridade e pontuação

Agora que podemos detectar as colisões da bola e as paredes, podemos decidir sobre a mecânica do jogo. Depois de alguns ajustes no jogo, consegui torná-lo um pouco jogável. Mas mesmo assim foi muito difícil. Meu primeiro pensamento sobre o jogo foi fazê-lo como Flappy Bird, quando a bola toca as paredes, o jogo termina. Mas então percebi que seria impossível jogar. Então aqui está o que eu pensei:

Deve haver uma barra de saúde em cima da bola. A bola deve perder saúde enquanto estiver tocando as paredes. Com essa lógica, não faz sentido fazer a bola quicar nas paredes. Então, quando a saúde for 0, o jogo deve terminar e devemos mudar para a tela de fim de jogo. Aqui vamos nos:

 int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // Make it borderless: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } }

E aqui está uma execução simples:

Uma bola com uma barra de saúde saltando através de um nível, perdendo saúde sempre que colide com uma parede.

Criamos uma variável global health para manter a saúde da bola. E então criou um método drawHealthBar() que desenha dois retângulos em cima da bola. A primeira é a barra de saúde básica, a outra é a ativa que mostra a saúde atual. A largura do segundo é dinâmica e é calculada com healthBarWidth*(health/maxHealth) , a proporção de nossa saúde atual em relação à largura da barra de saúde. Por fim, as cores de preenchimento são definidas de acordo com o valor da integridade. Por último, mas não menos importante, pontuações :

 ... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // added another value at the end of the array int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); }

Precisávamos marcar quando a bola passasse por uma parede. Mas precisamos adicionar no máximo 1 pontuação por parede. Ou seja, se a bola passar por uma parede e voltar e passar novamente, outra pontuação não deve ser adicionada. Para conseguir isso, adicionamos outra variável ao array gap wall dentro do arraylist. A nova variável armazena 0 se a bola ainda não passou por aquela parede e 1 se passou. Em seguida, modificamos o método watchWallCollision() . Adicionamos uma condição que dispara o método score() e marca a parede como passada quando a bola passa por uma parede que não passou antes.

Estamos agora muito perto do fim. A última coisa a fazer é implementar click to restart na tela de game over. Precisamos definir todas as variáveis ​​que usamos para seu valor inicial e reiniciar o jogo. Aqui está:

 ... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; }

Vamos adicionar mais cores.

O Flappy Pong completo em cores.

Voilá! Temos Flappy Pong!

O código completo do jogo Processing pode ser encontrado aqui.

Portando o código do jogo de processamento para a Web usando p5.js

p5.js é uma biblioteca JavaScript com uma sintaxe muito semelhante à da linguagem de programação Processing. Não é uma biblioteca capaz de simplesmente executar o código Processing existente; em vez disso, o p5.js requer a escrita de código JavaScript real—semelhante à porta JavaScript do Processing conhecida como Processing.js. Nossa tarefa é converter o código do Processing em JavaScript usando a API p5.js. A biblioteca tem um conjunto de funções e uma sintaxe semelhante ao Processing, e temos que fazer algumas alterações em nosso código para fazê-los funcionar em JavaScript - mas como Processing e JavaScript compartilham semelhanças com Java, é menos um salto do que parece . Mesmo que você não seja um desenvolvedor JavaScript, as mudanças são muito triviais e você deve conseguir acompanhar bem.

Antes de tudo, precisamos criar um index.html simples e adicionar p5.min.js ao nosso cabeçalho. Também precisamos criar outro arquivo chamado flappy_pong.js , que abrigará nosso código convertido.

 <html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html>

Nossa estratégia ao converter o código deve ser copiar e colar todo o nosso código em flappy_pong.js e depois fazer todas as alterações. E é isso que eu fiz. E aqui estão os passos que tomei para atualizar o código:

  • Javascript é uma linguagem não tipada (não há declarações de tipo como int e float ). Portanto, precisamos alterar todas as declarações de tipo para var .

  • Não há void em Javascript. Devemos mudar tudo para function .

  • Precisamos remover as declarações de tipo de argumentos de assinaturas de função. (ou seja void wallMover(var index) { to function wallMover(index) { )

  • Não há ArrayList em JavaScript. Mas podemos conseguir a mesma coisa usando arrays JavaScript. Fazemos as seguintes alterações:

    • ArrayList<int[]> walls = new ArrayList<int[]>(); para var walls = [];
    • walls.clear(); para walls = [];
    • walls.add(randWall); para walls.push(randWall);
    • walls.remove(index); para walls.splice(index,1);
    • walls.get(index); para walls[index]
    • walls.size() para walls.length
  • Altere a declaração do array var randWall = {width, randY, wallWidth, randHeight, 0}; to var randWall = [width, randY, wallWidth, randHeight, 0];

  • Remova todas as palavras-chave public .

  • Mova todas as declarações de color(0) para a function setup() porque color() não será definida antes da chamada de setup() .

  • Alterar size(500, 500); para createCanvas(500, 500);

  • Renomeie function gameScreen(){ para algo como function gamePlayScreen(){ porque já temos uma variável global chamada gameScreen . Quando estávamos trabalhando com Processing, uma era uma função e a outra uma variável int . Mas o JavaScript os confunde, pois não são tipados.

  • A mesma coisa vale para score() . Eu o renomeei para addScore() .

O código JavaScript completo que cobre tudo neste tutorial de Processamento pode ser encontrado aqui.

Processando o código do jogo: você também pode fazer isso

Neste tutorial de Processing, tentei explicar como construir um jogo muito simples. No entanto, o que fizemos neste artigo é apenas a ponta do iceberg. Com a linguagem de programação Processing, praticamente qualquer coisa pode ser alcançada. Na minha opinião, é a melhor ferramenta para programar o que você está imaginando. Minha intenção real com este tutorial de Processing foi ao invés de ensinar Processing e construir um jogo, provar que programar não é tão difícil. Construir seu próprio jogo não é apenas um sonho. Eu queria mostrar a você que com um pouco de esforço e entusiasmo, você pode fazer isso. Eu realmente espero que esses dois artigos inspirem todos a dar uma chance à programação.