Guida definitiva al linguaggio di elaborazione Parte II: Costruire un gioco semplice

Pubblicato: 2022-03-11

Questa è la seconda parte della guida definitiva al linguaggio di elaborazione. Nella prima parte, ho fornito una procedura dettagliata di base del linguaggio di elaborazione. Il prossimo passo per imparare l'elaborazione è semplicemente una programmazione più pratica.

In questo articolo, ti mostrerò come utilizzare Processing per implementare il tuo gioco, passo dopo passo. Ogni passaggio verrà spiegato in dettaglio. Quindi, porteremo il gioco sul web.

Costruisci un gioco semplice con il linguaggio di elaborazione.

Prima di iniziare il tutorial sull'elaborazione, ecco il codice dell'esercizio del logo del DVD della parte precedente. Se hai domande, assicurati di lasciare un commento.

Tutorial di elaborazione: un gioco semplice

Il gioco che costruiremo in questo tutorial sull'elaborazione è una sorta di combinazione di Flappy Bird, Pong e Brick Breaker. Il motivo per cui ho scelto un gioco come questo è che contiene la maggior parte dei concetti con cui i principianti lottano quando imparano lo sviluppo del gioco. Questo si basa sulla mia esperienza di quando ero un assistente didattico, aiutando i nuovi programmatori a imparare come usare Processing. Questi concetti includono gravità, collisioni, mantenimento dei punteggi, gestione di schermi diversi e interazioni tastiera/mouse. Flappy Pong li ha tutti dentro.

Gioca ora!

Senza utilizzare i concetti di programmazione orientata agli oggetti (OOP), non è facile creare giochi complessi, come giochi di piattaforma con più livelli, giocatori, entità ecc. Man mano che andiamo avanti, vedrai come il codice si complica molto velocemente. Ho fatto del mio meglio per mantenere questo tutorial sull'elaborazione organizzato e semplice.

Ti consiglio di seguire l'articolo, prendere il codice completo, giocarci da solo, iniziare a pensare al tuo gioco il più rapidamente possibile e iniziare a implementarlo.

Quindi iniziamo.

Costruire Flappy Pong

Esercitazione sull'elaborazione Passaggio 1: inizializza e gestisci schermate diverse

Il primo passo è inizializzare il nostro progetto. Per cominciare, scriveremo la nostra configurazione e disegneremo i blocchi come al solito, niente di speciale o nuovo. Quindi, gestiremo diverse schermate (schermata iniziale, schermata di gioco, schermata di gioco, ecc.). Quindi sorge la domanda, come possiamo fare in modo che Processing mostri la pagina corretta al momento giusto?

Portare a termine questo compito è abbastanza semplice. Avremo una variabile globale che memorizza le informazioni dello schermo attualmente attivo. Quindi disegniamo il contenuto dello schermo corretto a seconda della variabile. Nel blocco di disegno, avremo un'istruzione if che controlla la variabile e visualizza di conseguenza il contenuto dello schermo. Ogni volta che vogliamo cambiare lo schermo, cambieremo quella variabile nell'identificatore dello schermo che vogliamo che visualizzi. Detto questo, ecco come appare il nostro codice scheletro:

 /********* 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; }

All'inizio può sembrare spaventoso, ma tutto ciò che abbiamo fatto è stato costruire la struttura di base e separare le diverse parti con blocchi di commenti.

Come puoi vedere, definiamo un metodo diverso per ogni schermata da visualizzare. Nel nostro blocco di disegno, controlliamo semplicemente il valore della nostra variabile gameScreen e chiamiamo il metodo corrispondente.

Nella parte void mousePressed(){...} , stiamo ascoltando i clic del mouse e se lo schermo attivo è 0, lo schermo iniziale, chiamiamo il metodo startGame() che avvia il gioco come ti aspetteresti. La prima riga di questo metodo cambia la variabile gameScreen in 1, la schermata di gioco.

Se questo è compreso, il passo successivo è implementare la nostra schermata iniziale. Per farlo, modificheremo il metodo initScreen() . Eccolo:

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

Ora la nostra schermata iniziale ha uno sfondo nero e un semplice testo, "Fai clic per iniziare", situato al centro e allineato al centro. Ma quando clicchiamo, non succede nulla. Non abbiamo ancora specificato alcun contenuto per la nostra schermata di gioco. Il metodo gameScreen() non contiene nulla, quindi non stiamo coprendo i contenuti precedenti disegnati dall'ultima schermata (il testo) avendo background() come prima riga di disegno. Ecco perché il testo è ancora lì, anche se la riga text() non viene più chiamata (proprio come l'esempio della palla mobile dell'ultima parte che stava lasciando una traccia dietro) . Lo sfondo è ancora nero per lo stesso motivo. Quindi andiamo avanti e iniziamo a implementare la schermata di gioco.

 void gameScreen() { background(255); }

Dopo questa modifica, noterai che lo sfondo diventa bianco e il testo scompare.

Esercitazione sull'elaborazione Fase n. 2: Creazione della palla e implementazione della gravità

Ora inizieremo a lavorare sulla schermata di gioco. Per prima cosa creeremo la nostra palla. Dovremmo definire variabili per le sue coordinate, colore e dimensione perché potremmo voler cambiare quei valori in seguito. Ad esempio, se vogliamo aumentare le dimensioni della palla man mano che il giocatore ottiene un punteggio più alto, il gioco sarà più difficile. Dovremo cambiarne le dimensioni, quindi dovrebbe essere una variabile. Definiremo anche la velocità della palla, dopo aver implementato la gravità.

Innanzitutto, aggiungiamo quanto segue:

 ... 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); }

Abbiamo definito le coordinate come variabili globali, creato un metodo per disegnare la palla, chiamato dal metodo gameScreen . L'unica cosa a cui prestare attenzione qui è che abbiamo inizializzato le coordinate, ma le abbiamo definite in setup() . Il motivo per cui l'abbiamo fatto è che volevamo che la palla partisse a un quarto da sinistra e un quinto dall'alto. Non c'è un motivo particolare per cui lo vogliamo, ma questo è un buon punto per iniziare la palla. Quindi dovevamo ottenere la width e l' height dello schizzo in modo dinamico. La dimensione dello schizzo è definita in setup() , dopo la prima riga. width e height non sono impostate prima dell'esecuzione di setup() , ecco perché non potremmo raggiungere questo obiettivo se avessimo definito le variabili in alto.

Gravità

Ora implementare la gravità è in realtà la parte facile. Ci sono solo alcuni trucchi. Ecco prima l'implementazione:

 ... 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 il risultato è:

Una palla che rimbalza all'infinito con pseudo-gravità.

Tieni i tuoi cavalli, fisico. So che non è così che funziona la gravità nella vita reale. Invece, questo è più un processo di animazione che altro. La variabile che abbiamo definito come gravity è solo un valore numerico, un float in modo da poter utilizzare valori decimali, non solo interi, che aggiungiamo a ballSpeedVert su ogni ciclo. E ballSpeedVert è la velocità verticale della pallina, che viene aggiunta alla coordinata Y della pallina ( ballY ) su ogni loop. Osserviamo le coordinate della palla e ci assicuriamo che rimanga sullo schermo. Se non lo facessimo, la palla cadrebbe all'infinito. Per ora, la nostra palla si muove solo in verticale. Quindi osserviamo i confini del pavimento e del soffitto dello schermo. Con il metodo keepInScreen() , controlliamo se ballY ( + il raggio) è minore di height e allo stesso modo ballY ( - il raggio) è maggiore di 0 . Se le condizioni non soddisfano, facciamo rimbalzare la pallina (dal basso o dall'alto) con makeBounceBottom() e makeBounceTop() . Per far rimbalzare la palla, spostiamo semplicemente la palla nel punto esatto in cui doveva rimbalzare e moltiplichiamo la velocità verticale ( ballSpeedVert ) per -1 (moltiplicando per -1 cambia il segno). Quando il valore della velocità ha un segno meno, sommando la coordinata Y la velocità diventa ballY + (-ballSpeedVert) , che è ballY - ballSpeedVert . Quindi la palla cambia immediatamente direzione con la stessa velocità. Quindi, quando aggiungiamo la gravity a ballSpeedVert e ballSpeedVert ha un valore negativo, inizia ad avvicinarsi a 0 , alla fine diventa 0 e ricomincia ad aumentare. Ciò fa salire la palla, salire più lentamente, fermarsi e iniziare a cadere.

Una palla che rimbalza all'infinito su una racchetta.

C'è un problema con il nostro processo di animazione, però: la palla continua a rimbalzare. Se questo fosse uno scenario reale, la palla avrebbe dovuto affrontare la resistenza dell'aria e l'attrito ogni volta che toccava una superficie. Questo è il comportamento che vogliamo per il processo di animazione del nostro gioco, quindi implementarlo è facile. Aggiungiamo quanto segue:

 ... 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 ora il nostro processo di animazione produce questo:

Una palla che rimbalza ma si ferma per attrito.

Come suggerisce il nome, l' friction è l'attrito della superficie e l'attrito airfriction è l'attrito dell'aria. Quindi, ovviamente, l' friction deve essere applicato ogni volta che la palla tocca una superficie. airfriction , tuttavia, deve applicarsi costantemente. Quindi è quello che abbiamo fatto. Il metodo applyGravity() viene eseguito su ogni loop, quindi togliamo lo 0.0001 percento del suo valore corrente da ballSpeedVert su ogni loop. makeBounceBottom() e makeBounceTop() vengono eseguiti quando la pallina tocca qualsiasi superficie. Quindi con quei metodi, abbiamo fatto la stessa cosa, solo che questa volta con friction .

Esercitazione sull'elaborazione Fase n. 3: Creazione della racchetta

Ora abbiamo bisogno di una racchetta su cui far rimbalzare la palla. Dovremmo controllare il racket. Rendiamolo controllabile con il mouse. Ecco il codice:

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

Abbiamo definito il colore, la larghezza e l'altezza della racchetta come una variabile globale, potremmo volere che cambino durante il gioco. Abbiamo implementato un metodo drawRacket() che fa ciò che suggerisce il suo nome. rectMode al centro, quindi la nostra racchetta è allineata al centro del nostro cursore.

Ora che abbiamo creato la racchetta, dobbiamo far rimbalzare la pallina su di essa.

 ... 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 questo è il risultato:

Una palla che rimbalza su una racchetta ma si ferma per attrito.

Quindi quello che watchRacketBounce() è assicurarsi che la racchetta e la palla si scontrino. Ci sono due cose da controllare qui, ovvero se la palla e la racchetta sono allineate sia verticalmente che orizzontalmente. La prima istruzione if controlla se la coordinata X del lato destro della palla è maggiore della coordinata X del lato sinistro della racchetta (e viceversa). Se lo è, la seconda istruzione controlla se la distanza tra la palla e la racchetta è minore o uguale al raggio della palla (il che significa che si stanno scontrando) . Quindi, se queste condizioni soddisfano, il metodo makeBounceBottom() viene chiamato e la pallina rimbalza sulla nostra racchetta (su mouseY , dove si trova la racchetta).

Hai notato l' overhead variabile calcolato da mouseY - pmouseY ? Le variabili pmouseX e pmouseY memorizzano le coordinate del mouse nel frame precedente. Poiché il mouse può muoversi molto velocemente, è molto probabile che non rileviamo correttamente la distanza tra la palla e la racchetta tra i fotogrammi se il mouse si sta muovendo verso la palla abbastanza velocemente. Quindi, prendiamo la differenza delle coordinate del mouse tra i fotogrammi e ne teniamo conto durante il rilevamento della distanza. Più veloce è il movimento del mouse, maggiore è la distanza accettabile.

Usiamo le overhead anche per un altro motivo. Rileviamo in che direzione si sta muovendo il mouse controllando il segno di overhead . Se l'overhead è negativo, il mouse si trovava da qualche parte al di sotto del frame precedente, quindi il nostro mouse (racchetta) si sta spostando verso l'alto. In tal caso, vogliamo aggiungere una velocità extra alla palla e spostarla un po' più in là del normale rimbalzo per simulare l'effetto di colpire la palla con la racchetta. Se overhead è inferiore a 0 , lo aggiungiamo a ballY e ballSpeedVert per far andare la palla più in alto e più velocemente. Quindi più velocemente la racchetta colpisce la palla, più in alto e più velocemente salirà.

Tutorial di elaborazione Fase 4: movimento orizzontale e controllo della palla

In questa sezione, aggiungeremo il movimento orizzontale alla palla. Quindi, renderemo possibile il controllo della palla orizzontalmente con la nostra racchetta. Eccoci qui:

 ... // 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 il risultato è:

Una palla che ora rimbalza anche orizzontalmente.

L'idea qui è la stessa di quella che abbiamo fatto per il movimento verticale. Abbiamo creato una variabile di velocità orizzontale, ballSpeedHorizon . Abbiamo creato un metodo per applicare la velocità orizzontale a ballX e togliere l'attrito dell'aria. Abbiamo aggiunto altre due istruzioni if ​​al metodo keepInScreen() che osserverà la palla per colpire i bordi sinistro e destro dello schermo. Infine abbiamo creato makeBounceLeft() e makeBounceRight() per gestire i rimbalzi da sinistra e da destra.

Ora che abbiamo aggiunto la velocità orizzontale al gioco, vogliamo controllare la palla con la racchetta. Come nel famoso gioco Atari Breakout e in tutti gli altri giochi di rottura mattoni, la pallina dovrebbe andare a sinistra oa destra a seconda del punto della racchetta che colpisce. I bordi della racchetta dovrebbero dare alla palla più velocità orizzontale mentre il centro non dovrebbe avere alcun effetto. Prima il codice:

 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; ... } } }

Il risultato è:

Fisica orizzontale in stile breakout.

L'aggiunta di quella semplice riga a watchRacketBounce() ha fatto il lavoro. Quello che abbiamo fatto è stato determinare la distanza del punto che la pallina colpisce dal centro della racchetta con ballX - mouseX . Quindi, la rendiamo la velocità orizzontale. La differenza effettiva era troppa, quindi ho fatto alcuni tentativi e ho pensato che un decimo del valore fosse il più naturale.

Esercitazione sull'elaborazione Fase n. 5: Creazione dei muri

Il nostro schizzo inizia a sembrare più simile a un gioco a ogni passaggio. In questo passaggio, aggiungeremo muri spostandoci verso sinistra, proprio come in 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 questo ha portato a:

Una palla che rimbalza attraverso un livello con i muri.

Anche se il codice sembra lungo e intimidatorio, prometto che non c'è nulla di difficile da capire. La prima cosa da notare è ArrayList . Per quelli di voi che non sanno cos'è un ArrayList , è solo un'implementazione di list che si comporta come un Array, ma presenta alcuni vantaggi su di esso. È ridimensionabile, ha metodi utili come list.add(index) , list.get(index) e list.remove(index) . Manteniamo i dati del muro come array di interi all'interno dell'elenco di array. I dati che conserviamo negli array sono per lo spazio tra due pareti. Gli array contengono i seguenti valori:

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

I muri effettivi vengono disegnati in base ai valori del muro di intercapedine. Nota che tutti questi potrebbero essere gestiti in modo migliore e più pulito usando le classi, ma poiché l'uso della programmazione orientata agli oggetti (OOP) non è nell'ambito di questo tutorial sull'elaborazione, è così che lo gestiremo. Abbiamo due metodi di base per gestire i muri, wallAdder() e wallHandler .

Il metodo wallAdder() aggiunge semplicemente nuovi muri in ogni millisecondo wallInterval all'arraylist. Abbiamo una variabile globale lastAddTime che memorizza l'ora in cui è stato aggiunto l'ultimo muro (in millisecondi) . Se l'attuale millisecondo millis() meno l'ultimo millisecondo aggiunto lastAddTime è maggiore del nostro valore di intervallo wallInterval , significa che ora è il momento di aggiungere un nuovo muro. Le variabili di gap casuali vengono quindi generate in base alle variabili globali definite in alto. Quindi un nuovo muro (array intero che memorizza i dati del gap wall) viene aggiunto all'arraylist e lastAddTime viene impostato sull'attuale millisecondo millis() .

wallHandler() scorre i muri correnti che si trovano nell'arraylist. E per ogni elemento in ogni ciclo, chiama wallRemover(i) , wallMover(i) e wallDrawer(i) in base al valore di indice dell'arraylist. Questi metodi fanno ciò che suggerisce il loro nome. wallDrawer() disegna i muri effettivi in ​​base ai dati del gap wall. Prende l'array di dati del muro dall'arraylist e chiama il metodo rect() per disegnare i muri dove dovrebbero effettivamente essere. Il metodo wallMover() prende l'elemento dall'arraylist, cambia la sua posizione X in base alla variabile globale wallSpeed . Infine, wallRemover() rimuove dall'arraylist i muri che sono fuori dallo schermo. Se non lo avessimo fatto, Processing li avrebbe trattati come se fossero ancora sullo schermo. E sarebbe stata un'enorme perdita di prestazioni. Quindi, quando un muro viene rimosso dall'arraylist, non viene disegnato nei loop successivi.

L'ultima cosa difficile da fare è rilevare le collisioni tra la palla e le pareti.

 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 } }

Il metodo watchWallCollision() viene chiamato per ogni muro in ogni ciclo. Prendiamo le coordinate del muro del gap, calcoliamo le coordinate dei muri effettivi (in alto e in basso) e controlliamo se le coordinate della palla si scontrano con i muri.

Tutorial di elaborazione Passaggio n. 6: salute e punteggio

Ora che possiamo rilevare le collisioni della palla e dei muri, possiamo decidere le meccaniche di gioco. Dopo alcune modifiche al gioco, sono riuscito a renderlo in qualche modo giocabile. Ma comunque, è stato molto difficile. Il mio primo pensiero sul gioco è stato di farlo come Flappy Bird, quando la palla tocca i muri, il gioco finisce. Ma poi ho capito che sarebbe stato impossibile giocare. Allora ecco cosa ho pensato:

Dovrebbe esserci una barra della salute sopra la palla. La palla dovrebbe perdere salute mentre tocca le pareti. Con questa logica non ha senso far rimbalzare la palla dai muri. Quindi, quando la salute è 0, il gioco dovrebbe terminare e dovremmo passare alla schermata di fine gioco. Quindi eccoci qui:

 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(); } }

Ed ecco una semplice corsa:

Una palla con una barra della salute che rimbalza attraverso un livello, perdendo salute ogni volta che si scontra con un muro.

Abbiamo creato una health variabile globale per mantenere la salute della palla. E poi ha creato un metodo drawHealthBar() che disegna due rettangoli sopra la palla. La prima è la barra della salute di base, l'altra è quella attiva che mostra la salute attuale. La larghezza del secondo è dinamica e viene calcolata con healthBarWidth*(health/maxHealth) , il rapporto tra la nostra salute attuale rispetto alla larghezza della barra della salute. Infine, i colori di riempimento vengono impostati in base al valore della salute. Ultimo ma non meno importante, i punteggi :

 ... 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); }

Dovevamo segnare quando la palla supera un muro. Ma dobbiamo aggiungere massimo 1 punteggio per muro. Ciò significa che se la palla supera un muro, poi torna indietro e lo supera di nuovo, non dovrebbe essere aggiunto un altro punteggio. Per ottenere ciò, abbiamo aggiunto un'altra variabile all'array gap wall all'interno dell'arraylist. La nuova variabile memorizza 0 se la palla non ha ancora superato quel muro e 1 se lo ha fatto. Quindi, abbiamo modificato il metodo watchWallCollision() . Abbiamo aggiunto una condizione che attiva il metodo score() e contrassegna il muro come passato quando la palla supera un muro che non era mai passato prima.

Ormai siamo molto vicini alla fine. L'ultima cosa da fare è implementare il click to restart sullo schermo del gioco. Dobbiamo impostare tutte le variabili che abbiamo usato al loro valore iniziale e riavviare il gioco. Ecco qui:

 ... 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; }

Aggiungiamo altri colori.

Il Flappy Pong completato a colori.

Ecco! Abbiamo Flappy Pong!

Il codice del gioco di elaborazione completo può essere trovato qui.

Portare il codice di gioco in elaborazione sul Web utilizzando p5.js

p5.js è una libreria JavaScript con una sintassi molto simile a quella del linguaggio di programmazione Processing. Non è una libreria in grado di eseguire semplicemente il codice di elaborazione esistente; invece, p5.js richiede la scrittura del codice JavaScript effettivo, simile alla porta JavaScript di Processing nota come Processing.js. Il nostro compito è convertire il codice di elaborazione in JavaScript utilizzando l'API p5.js. La libreria ha un insieme di funzioni e una sintassi simili a Processing, e dobbiamo apportare alcune modifiche al nostro codice per farle funzionare in JavaScript, ma poiché sia ​​Processing che JavaScript condividono somiglianze con Java, è meno di un salto di quanto sembri . Anche se non sei uno sviluppatore JavaScript, le modifiche sono molto banali e dovresti essere in grado di seguirle bene.

Prima di tutto, dobbiamo creare un semplice index.html e aggiungere p5.min.js al nostro header. Abbiamo anche bisogno di creare un altro file chiamato flappy_pong.js che conterrà il nostro codice convertito.

 <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>

La nostra strategia durante la conversione del codice dovrebbe essere copiare e incollare tutto il nostro codice in flappy_pong.js e quindi apportare tutte le modifiche. Ed è quello che ho fatto. Ed ecco i passaggi che ho seguito per aggiornare il codice:

  • Javascript è un linguaggio non tipizzato (non ci sono dichiarazioni di tipo come int e float ). Quindi dobbiamo cambiare tutte le dichiarazioni di tipo in var .

  • Non c'è alcun void in Javascript. Dovremmo cambiare tutto per function .

  • Dobbiamo rimuovere le dichiarazioni di tipo degli argomenti dalle firme delle funzioni. (es. void wallMover(var index) { per function wallMover(index) { )

  • Non c'è ArrayList in JavaScript. Ma possiamo ottenere la stessa cosa usando gli array JavaScript. Apportiamo le seguenti modifiche:

    • ArrayList<int[]> walls = new ArrayList<int[]>(); a var walls = [];
    • walls.clear(); alle walls = [];
    • walls.add(randWall); alle walls.push(randWall);
    • walls.remove(index); to walls.splice(index,1);
    • walls.get(index); alle walls[index]
    • walls.size() a walls.length
  • Modificare la dichiarazione dell'array var randWall = {width, randY, wallWidth, randHeight, 0}; to var randWall = [width, randY, wallWidth, randHeight, 0];

  • Rimuovi tutte le parole chiave public .

  • Sposta tutte le dichiarazioni color(0) nella function setup() perché color() non sarà definito prima della chiamata setup() .

  • Cambia size(500, 500); createCanvas(500, 500);

  • Rinomina function gameScreen(){ in qualcos'altro come function gamePlayScreen(){ perché abbiamo già una variabile globale denominata gameScreen . Quando stavamo lavorando con Processing, uno era una funzione e l'altro era una variabile int . Ma JavaScript li confonde poiché non sono tipizzati.

  • La stessa cosa vale per score() . L'ho rinominato in addScore() .

Il codice JavaScript completo che copre tutto in questo tutorial sull'elaborazione può essere trovato qui.

Elaborazione del codice di gioco: puoi farlo anche tu

In questo tutorial sull'elaborazione, ho cercato di spiegare come costruire un gioco molto semplice. Tuttavia, ciò che abbiamo fatto in questo articolo è solo la punta dell'iceberg. Con il linguaggio di programmazione Processing si può ottenere qualsiasi cosa. Secondo me, è lo strumento migliore per programmare ciò che stai immaginando. La mia vera intenzione con questo tutorial sull'elaborazione era piuttosto che insegnare l'elaborazione e la creazione di un gioco, per dimostrare che la programmazione non è così difficile. Costruire il tuo gioco non è solo un sogno. Volevo mostrarti che con un po' di impegno ed entusiasmo, puoi farcela. Spero davvero che questi due articoli ispirino tutti a provare la programmazione.