Guía definitiva para el lenguaje de procesamiento Parte II: Creación de un juego simple
Publicado: 2022-03-11Esta es la segunda parte de la guía definitiva del lenguaje Processing. En la primera parte, di un tutorial básico del lenguaje de procesamiento. El siguiente paso para aprender Processing es simplemente una programación más práctica.
En este artículo, te mostraré cómo usar Processing para implementar tu propio juego, paso a paso. Cada paso será explicado en detalle. Luego, portaremos el juego a la web.
Antes de comenzar el tutorial de procesamiento, aquí está el código del ejercicio del logotipo de DVD de la parte anterior. Si tiene alguna pregunta, asegúrese de dejar un comentario.
Tutorial de procesamiento: un juego simple
El juego que construiremos en este tutorial de Processing es una especie de combinación de Flappy Bird, Pong y Brick Breaker. La razón por la que elegí un juego como este es que tiene la mayoría de los conceptos con los que los principiantes luchan cuando aprenden a desarrollar juegos. Esto se basa en mi experiencia de cuando era asistente de enseñanza, ayudando a los nuevos programadores a aprender a usar Processing. Estos conceptos incluyen la gravedad, las colisiones, el mantenimiento de puntajes, el manejo de diferentes pantallas y las interacciones del teclado y el mouse. Flappy Pong los tiene todos.
¡Juega ahora!
Sin utilizar conceptos de programación orientada a objetos (POO), no es fácil crear juegos complejos, como juegos de plataformas con múltiples niveles, jugadores, entidades, etc. A medida que avanzamos, verá cómo el código se complica muy rápido. Hice lo mejor que pude para mantener este tutorial de Processing organizado y simple.
Le aconsejo que siga el artículo, obtenga el código completo, juegue con él por su cuenta, comience a pensar en su propio juego lo más rápido posible y comience a implementarlo.
Vamos a empezar.
Construyendo Flappy Pong
Tutorial de procesamiento, paso n.° 1: inicializar y manejar diferentes pantallas
El primer paso es inicializar nuestro proyecto. Para empezar, escribiremos nuestra configuración y dibujaremos bloques como de costumbre, nada lujoso o nuevo. Luego, manejaremos diferentes pantallas (pantalla inicial, pantalla de juego, pantalla de fin de juego, etc.). Entonces surge la pregunta, ¿cómo hacemos que Processing muestre la página correcta en el momento correcto?
Llevar a cabo esta tarea es bastante simple. Tendremos una variable global que almacena la información de la pantalla actualmente activa. Luego dibujamos el contenido de la pantalla correcta dependiendo de la variable. En el bloque dibujar, tendremos una instrucción if que verifica la variable y muestra el contenido de la pantalla en consecuencia. Siempre que queramos cambiar de pantalla, cambiaremos esa variable por el identificador de pantalla que queremos que muestre. Dicho esto, así es como se ve nuestro código esqueleto:
/********* 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; }
Esto puede parecer aterrador al principio, pero todo lo que hicimos fue construir la estructura básica y separar las diferentes partes con bloques de comentarios.
Como puede ver, definimos un método diferente para que cada pantalla se muestre. En nuestro bloque de dibujo, simplemente verificamos el valor de nuestra variable gameScreen
y llamamos al método correspondiente.
En la void mousePressed(){...}
, estamos escuchando los clics del mouse y si la pantalla activa es 0, la pantalla inicial, llamamos al método startGame()
que inicia el juego como era de esperar. La primera línea de este método cambia la variable gameScreen
a 1, la pantalla del juego.
Si esto se entiende, el siguiente paso es implementar nuestra pantalla inicial. Para hacerlo, editaremos el método initScreen()
. Aquí va:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
Ahora nuestra pantalla inicial tiene un fondo negro y un texto simple, “Click to start”, ubicado en el medio y alineado al centro. Pero cuando hacemos clic, no pasa nada. Todavía no hemos especificado ningún contenido para nuestra pantalla de juego. El método gameScreen()
no contiene nada, por lo que no estamos cubriendo los contenidos anteriores extraídos de la última pantalla (el texto) al tener background()
como la primera línea de dibujo. Es por eso que el texto todavía está allí, a pesar de que la línea de text()
ya no se llama (al igual que el ejemplo de la bola en movimiento de la última parte que dejaba un rastro) . El fondo sigue siendo negro por la misma razón. Así que sigamos adelante y comencemos a implementar la pantalla del juego.
void gameScreen() { background(255); }
Después de este cambio, notará que el fondo se vuelve blanco y el texto desaparece.
Tutorial de procesamiento, paso n.° 2: creación de la pelota e implementación de la gravedad
Ahora, comenzaremos a trabajar en la pantalla del juego. Primero crearemos nuestra bola. Deberíamos definir variables para sus coordenadas, color y tamaño porque es posible que queramos cambiar esos valores más adelante. Por ejemplo, si queremos aumentar el tamaño de la pelota a medida que el jugador puntúa más alto para que el juego sea más difícil. Tendremos que cambiar su tamaño, por lo que debería ser una variable. También definiremos la velocidad de la pelota, después de implementar la gravedad.
Primero, agreguemos lo siguiente:
... 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 las coordenadas como variables globales, creamos un método que dibuja la pelota, llamado desde el método gameScreen . Lo único a lo que debe prestar atención aquí es que inicializamos las coordenadas, pero las definimos en setup()
. La razón por la que lo hicimos es porque queríamos que la pelota comenzara en un cuarto desde la izquierda y un quinto desde arriba. No hay una razón particular por la que queramos eso, pero ese es un buen punto para que comience la pelota. Así que necesitábamos obtener el width
y la height
del boceto de forma dinámica. El tamaño del boceto se define en setup()
, después de la primera línea. el width
y la height
no se establecen antes de que se ejecute setup()
, es por eso que no podríamos lograr esto si definimos las variables en la parte superior.
Gravedad
Ahora implementar la gravedad es en realidad la parte fácil. Solo hay algunos trucos. Aquí está la implementación primero:
... 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); } }
Y el resultado es:
Mantenga sus caballos, físico. Sé que no es así como funciona la gravedad en la vida real. En cambio, esto es más un proceso de animación que cualquier otra cosa. La variable que definimos como gravity
es solo un valor numérico, un float
para que podamos usar valores decimales, no solo números enteros, que agregamos a ballSpeedVert
en cada bucle. Y ballSpeedVert
es la velocidad vertical de la pelota, que se suma a la coordenada Y de la pelota ( ballY
) en cada bucle. Observamos las coordenadas de la pelota y nos aseguramos de que permanezca en la pantalla. Si no lo hiciéramos, la bola caería hasta el infinito. Por ahora, nuestra pelota solo se mueve verticalmente. Entonces observamos los límites del piso y el techo de la pantalla. Con el método keepInScreen()
, verificamos si ballY
( + el radio) es menor que height
y, de manera similar, ballY
( - the radius) es mayor que 0
. Si las condiciones no se cumplen, hacemos que la pelota rebote (desde abajo o desde arriba) con makeBounceBottom()
y makeBounceTop()
. Para hacer que la pelota rebote, simplemente movemos la pelota al lugar exacto donde tenía que botar y multiplicamos la velocidad vertical ( ballSpeedVert
) por -1
(multiplicar por -1 cambia el signo). Cuando el valor de la velocidad tiene un signo menos, al agregar la coordenada Y, la velocidad se convierte en ballY + (-ballSpeedVert)
, que es ballY - ballSpeedVert
. Entonces la pelota cambia inmediatamente su dirección con la misma velocidad. Luego, a medida que agregamos gravity
a ballSpeedVert
y ballSpeedVert
tiene un valor negativo, comienza a acercarse a 0
, finalmente se convierte en 0
y comienza a aumentar nuevamente. Eso hace que la pelota suba, suba más despacio, se detenga y empiece a caer.
Sin embargo, hay un problema con nuestro proceso de animación: la pelota sigue rebotando. Si este fuera un escenario del mundo real, la pelota habría enfrentado resistencia del aire y fricción cada vez que tocaba una superficie. Ese es el comportamiento que queremos para el proceso de animación de nuestro juego, por lo que implementarlo es fácil. Agregamos lo siguiente:
... 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); }
Y ahora nuestro proceso de animación produce esto:
Como sugiere el nombre, la friction
es la fricción de la superficie y la airfriction
del aire es la fricción del aire. Entonces, obviamente, la friction
debe aplicarse cada vez que la pelota toca cualquier superficie. airfriction
embargo, la fricción del aire tiene que aplicarse constantemente. Así que eso es lo que hicimos. El método applyGravity()
se ejecuta en cada bucle, por lo que quitamos el 0.0001
por ciento de su valor actual de ballSpeedVert
en cada bucle. makeBounceBottom()
y makeBounceTop()
se ejecutan cuando la pelota toca cualquier superficie. Entonces, en esos métodos, hicimos lo mismo, solo que esta vez con friction
.
Tutorial de procesamiento, paso n.º 3: Creación de la raqueta
Ahora necesitamos una raqueta para que rebote la pelota. Deberíamos estar controlando la raqueta. Hagámoslo controlable con el ratón. Aquí está el 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 el color, el ancho y la altura de la raqueta como una variable global, es posible que queramos que cambien durante el juego. Implementamos un método drawRacket()
que hace lo que sugiere su nombre. Establecemos rectMode
en el centro, de modo que nuestra raqueta esté alineada con el centro de nuestro cursor.
Ahora que creamos la raqueta, tenemos que hacer que la pelota rebote en ella.
... 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; } } } }
Y aqui esta el resultado:
Entonces, lo que hace watchRacketBounce()
es asegurarse de que la raqueta y la pelota choquen. Hay dos cosas que verificar aquí, que es si la pelota y la raqueta están alineadas tanto vertical como horizontalmente. La primera instrucción if verifica si la coordenada X del lado derecho de la pelota es mayor que la coordenada X del lado izquierdo de la raqueta (y al revés). Si es así, la segunda declaración verifica si la distancia entre la pelota y la raqueta es menor o igual que el radio de la pelota (lo que significa que están chocando) . Entonces, si estas condiciones se cumplen, se llama al método makeBounceBottom()
y la pelota rebota en nuestra raqueta (en mouseY
, donde está la raqueta).
¿Ha notado la overhead
variable que se calcula con mouseY - pmouseY
? Las variables pmouseX
y pmouseY
almacenan las coordenadas del mouse en el cuadro anterior. Como el mouse puede moverse muy rápido, existe una buena posibilidad de que no detectemos correctamente la distancia entre la pelota y la raqueta entre cuadros si el mouse se mueve hacia la pelota lo suficientemente rápido. Entonces, tomamos la diferencia de las coordenadas del mouse entre fotogramas y lo tenemos en cuenta al detectar la distancia. Cuanto más rápido se mueve el mouse, mayor distancia es aceptable.
También usamos overhead
por otra razón. Detectamos en qué dirección se mueve el ratón comprobando el signo de overhead
. Si la sobrecarga es negativa, el mouse estaba en algún lugar debajo en el cuadro anterior, por lo que nuestro mouse (raqueta) se está moviendo hacia arriba. En ese caso, queremos agregar una velocidad adicional a la pelota y moverla un poco más lejos que el rebote normal para simular el efecto de golpear la pelota con la raqueta. Si la overhead
es menor que 0
, la agregamos a ballY
y ballSpeedVert
para hacer que la pelota vaya más alta y más rápida. Así que cuanto más rápido golpee la raqueta la pelota, más alto y más rápido se moverá hacia arriba.
Tutorial de procesamiento, paso n.º 4: movimiento horizontal y control del balón
En esta sección, agregaremos movimiento horizontal a la pelota. Luego, posibilitaremos el control de la pelota en horizontal con nuestra raqueta. Aquí vamos:
... // 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); } }
Y el resultado es:
La idea aquí es la misma que hicimos para el movimiento vertical. Creamos una variable de velocidad horizontal, ballSpeedHorizon
. Creamos un método para aplicar velocidad horizontal a ballX
y eliminar la fricción del aire. Agregamos dos declaraciones if más al método keepInScreen()
que observará la pelota para golpear los bordes izquierdo y derecho de la pantalla. Finalmente creamos makeBounceLeft()
y makeBounceRight()
para manejar los rebotes de izquierda a derecha.

Ahora que agregamos la velocidad horizontal al juego, queremos controlar la pelota con la raqueta. Como en el famoso juego de Atari Breakout y en todos los demás juegos de romper ladrillos, la pelota debe ir hacia la izquierda o hacia la derecha según el punto de la raqueta en el que golpea. Los bordes de la raqueta deberían darle a la pelota más velocidad horizontal, mientras que el medio no debería tener ningún efecto. Código primero:
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; ... } } }
El resultado es:
Agregar esa línea simple a watchRacketBounce()
hizo el trabajo. Lo que hicimos fue determinar la distancia del punto en el que golpea la pelota desde el centro de la raqueta con ballX - mouseX
. Entonces, lo convertimos en la velocidad horizontal. La diferencia real era demasiada, así que lo intenté un par de veces y pensé que una décima parte del valor se siente más natural.
Tutorial de Procesamiento Paso #5: Creando las Paredes
Nuestro boceto comienza a parecerse más a un juego con cada paso. En este paso, añadiremos paredes moviéndose hacia la izquierda, como en 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); } }
Y esto resultó en:
Aunque el código parece largo e intimidante, prometo que no hay nada difícil de entender. Lo primero que hay que notar es ArrayList
. Para aquellos de ustedes que no saben qué es un ArrayList
, es solo una implementación de list que actúa como un Array, pero tiene algunas ventajas sobre él. Es redimensionable, tiene métodos útiles como list.add(index)
, list.get(index)
y list.remove(index)
. Mantenemos los datos de la pared como matrices enteras dentro de la lista de matrices. Los datos que mantenemos en las matrices son para el espacio entre dos paredes. Las matrices contienen los siguientes valores:
[gap wall X, gap wall Y, gap wall width, gap wall height]
Las paredes reales se dibujan en función de los valores de la pared del espacio. Tenga en cuenta que todo esto podría manejarse mejor y de forma más limpia usando clases, pero dado que el uso de la programación orientada a objetos (POO) no está dentro del alcance de este tutorial de procesamiento, así es como lo manejaremos. Tenemos dos métodos básicos para administrar las paredes, wallAdder()
y wallHandler
.
El método wallAdder()
simplemente agrega nuevas paredes en cada milisegundo de wallInterval
a la lista de arreglos. Tenemos una variable global lastAddTime
que almacena la hora en que se agregó el último muro (en milisegundos) . Si el milisegundo actual millis()
menos el último milisegundo agregado lastAddTime
es mayor que nuestro valor de intervalo wallInterval
, significa que ahora es el momento de agregar un nuevo muro. Las variables de brecha aleatorias se generan luego en función de las variables globales definidas en la parte superior. Luego, se agrega una nueva pared (matriz de enteros que almacena los datos de la pared de la brecha) a la lista de matrices y lastAddTime
se establece en el milisegundo actual millis()
.
wallHandler()
recorre las paredes actuales que están en la lista de arreglos. Y para cada elemento en cada ciclo, llama a wallRemover(i)
, wallMover(i)
y wallDrawer(i)
por el valor de índice de la lista de arreglos. Estos métodos hacen lo que sugiere su nombre. wallDrawer()
dibuja las paredes reales en función de los datos de la pared del espacio. Toma la matriz de datos de la pared de la lista de matrices y llama al método rect()
para dibujar las paredes donde realmente deberían estar. El método wallMover()
toma el elemento de la lista de arreglos, cambia su ubicación X según la variable global wallSpeed
. Finalmente, wallRemover()
elimina las paredes de la lista de arreglos que están fuera de la pantalla. Si no lo hiciéramos, Processing los habría tratado como si todavía estuvieran en la pantalla. Y eso habría sido una gran pérdida en el rendimiento. Entonces, cuando se elimina una pared de la lista de arreglos, no se dibuja en los bucles posteriores.
El desafío final que queda por hacer es detectar colisiones entre la pelota y las 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 } }
Se llama al método watchWallCollision()
para cada pared en cada ciclo. Tomamos las coordenadas de la pared del espacio, calculamos las coordenadas de las paredes reales (superior e inferior) y verificamos si las coordenadas de la pelota chocan con las paredes.
Procesamiento Tutorial Paso #6: Salud y Puntaje
Ahora que podemos detectar las colisiones de la pelota y las paredes, podemos decidir sobre la mecánica del juego. Después de algunos ajustes al juego, logré hacer que el juego fuera algo jugable. Pero aun así, fue muy duro. Mi primer pensamiento sobre el juego fue hacerlo como Flappy Bird, cuando la pelota toca las paredes, el juego termina. Pero luego me di cuenta de que sería imposible jugar. Así que esto es lo que pensé:
Debe haber una barra de salud en la parte superior de la pelota. La pelota debería perder salud mientras toca las paredes. Con esta lógica, no tiene sentido hacer que la pelota rebote en las paredes. Entonces, cuando la salud es 0, el juego debería terminar y deberíamos cambiar a la pantalla de fin del juego. Así que, aquí vamos:
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(); } }
Y aquí hay una ejecución simple:
Creamos una health
variable global para mantener la salud de la pelota. Y luego creó un método drawHealthBar()
que dibuja dos rectángulos encima de la pelota. La primera es la barra de salud básica, la otra es la activa que muestra la salud actual. El ancho de la segunda es dinámico, y se calcula con healthBarWidth*(health/maxHealth)
, el ratio de nuestra salud actual con respecto al ancho de la barra de salud. Finalmente, los colores de relleno se establecen de acuerdo con el valor de la salud. Por último, pero no menos importante, las puntuaciones :
... 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); }
Necesitábamos marcar cuando la pelota pasa una pared. Pero necesitamos agregar un máximo de 1 puntuación por pared. Es decir, si la pelota pasa una pared y luego vuelve y la pasa de nuevo, no se debe agregar otra puntuación. Para lograr esto, agregamos otra variable a la matriz de pared de espacios dentro de la lista de matrices. La nueva variable almacena 0
si la pelota aún no pasó esa pared y 1
si lo hizo. Luego, modificamos el método watchWallCollision()
. Agregamos una condición que activa el método score()
y marca la pared como pasada cuando la pelota pasa una pared que no ha pasado antes.
Ahora estamos muy cerca del final. Lo último que debe hacer es implementar click to restart
en la pantalla de finalización del juego. Necesitamos establecer todas las variables que usamos a su valor inicial y reiniciar el juego. Aquí lo tienes:
... 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; }
Agreguemos algunos colores más.
¡Voila! ¡Tenemos Flappy Pong!
El código completo del juego Processing se puede encontrar aquí.
Portar el código del juego de procesamiento a la web usando p5.js
p5.js es una biblioteca JavaScript con una sintaxis muy similar a la del lenguaje de programación Processing. No es una biblioteca que sea capaz de simplemente ejecutar el código de procesamiento existente; en cambio, p5.js requiere escribir código JavaScript real, similar al puerto JavaScript de Processing conocido como Processing.js. Nuestra tarea es convertir el código de procesamiento en JavaScript utilizando la API p5.js. La biblioteca tiene un conjunto de funciones y una sintaxis similar a Processing, y tenemos que hacer ciertos cambios en nuestro código para que funcionen en JavaScript, pero dado que tanto Processing como JavaScript comparten similitudes con Java, es un salto menor de lo que parece. . Incluso si no es un desarrollador de JavaScript, los cambios son muy triviales y debería poder seguirlos sin problemas.
En primer lugar, debemos crear un index.html
simple y agregar p5.min.js
a nuestro encabezado. También necesitamos crear otro archivo llamado flappy_pong.js
que albergará nuestro 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>
Nuestra estrategia al convertir el código debería ser copiar y pegar todo nuestro código en flappy_pong.js
y luego hacer todos los cambios. Y eso fue lo que hice. Y aquí están los pasos que tomé para actualizar el código:
Javascript es un lenguaje sin tipo (no hay declaraciones de tipo como
int
yfloat
). Entonces necesitamos cambiar todas las declaraciones de tipo avar
.No hay
void
en Javascript. Deberíamos cambiar todo para quefunction
.Necesitamos eliminar las declaraciones de tipo de los argumentos de las firmas de funciones. (es decir,
void wallMover(var index) {
parafunction wallMover(index) {
)No hay
ArrayList
en JavaScript. Pero podemos lograr lo mismo usando matrices de JavaScript. Realizamos los siguientes cambios:-
ArrayList<int[]> walls = new ArrayList<int[]>();
paravar walls = [];
-
walls.clear();
awalls = [];
-
walls.add(randWall);
awalls.push(randWall);
-
walls.remove(index);
awalls.splice(index,1);
-
walls.get(index);
awalls[index]
-
walls.size()
awalls.length
-
Cambie la declaración de la matriz
var randWall = {width, randY, wallWidth, randHeight, 0};
tovar randWall = [width, randY, wallWidth, randHeight, 0];
Eliminar todas las palabras clave
public
.Mueva todas las declaraciones de
color(0)
a lafunction setup()
porquecolor()
no se definirá antes de llamar asetup()
.Cambiar
size(500, 500);
paracreateCanvas(500, 500);
Cambie el nombre
function gameScreen(){
a algo más comofunction gamePlayScreen(){
porque ya tenemos una variable global llamadagameScreen
. Cuando trabajábamos con Processing, una era una función y la otra era una variableint
. Pero JavaScript los confunde ya que no están tipificados.Lo mismo ocurre con
score()
. Le cambié el nombre aaddScore()
.
El código JavaScript completo que cubre todo en este tutorial de procesamiento se puede encontrar aquí.
Procesamiento del código del juego: tú también puedes hacerlo
En este tutorial de procesamiento, traté de explicar cómo construir un juego muy simple. Sin embargo, lo que hicimos en este artículo es solo la punta del iceberg. Con el lenguaje de programación Processing, se puede lograr casi cualquier cosa. En mi opinión, es la mejor herramienta para programar lo que estás imaginando. Mi intención real con este tutorial de Processing era más que enseñar Processing y construir un juego, demostrar que la programación no es tan difícil. Construir tu propio juego no es solo un sueño. Quería mostrarte que con un poco de esfuerzo y entusiasmo, puedes lograrlo. Realmente espero que estos dos artículos inspiren a todos a darle una oportunidad a la programación.