Ghid definitiv pentru limbajul de procesare Partea a II-a: Construirea unui joc simplu
Publicat: 2022-03-11Aceasta este a doua parte a ghidului final pentru limbajul de procesare. În prima parte, am oferit o prezentare de bază a limbajului de procesare. Următorul pas pentru a învăța Procesarea este pur și simplu o programare mai practică.
În acest articol, vă voi arăta cum să utilizați Processing pentru a vă implementa propriul joc, pas cu pas. Fiecare pas va fi explicat în detaliu. Apoi, vom porta jocul pe web.
Înainte de a începe tutorialul Procesare, iată codul exercițiului de logo DVD din partea anterioară. Dacă aveți întrebări, asigurați-vă că lăsați un comentariu.
Tutorial de procesare: un joc simplu
Jocul pe care îl vom construi în acest tutorial de procesare este un fel de combinație de Flappy Bird, Pong și Brick Breaker. Motivul pentru care am ales un astfel de joc este că are cele mai multe dintre conceptele cu care se luptă începătorii atunci când învață dezvoltarea jocului. Acest lucru se bazează pe experiența mea de când eram asistent didactic, ajutând programatorii noi să învețe cum să folosească Procesare. Aceste concepte includ gravitația, coliziunile, păstrarea scorurilor, manipularea diferitelor ecrane și interacțiunile tastatură/mouse. Flappy Pong le are pe toate în el.
Joacă acum!
Fără a utiliza concepte de programare orientată pe obiecte (OOP), nu este ușor să construiești jocuri complexe, cum ar fi jocuri cu platforme cu mai multe niveluri, jucători, entități etc. Pe măsură ce mergem mai departe, vei vedea cum codul se complică foarte repede. Am făcut tot posibilul să păstrez acest tutorial de procesare organizat și simplu.
Vă sfătuiesc să urmați articolul, să luați codul complet, să vă jucați singur cu el, să începeți să vă gândiți la propriul joc cât mai repede posibil și să începeți să îl implementați.
Deci să începem.
Construirea Flappy Pong
Tutorial de procesare Pasul #1: Inițializați și gestionați diferite ecrane
Primul pas este să ne inițializam proiectul. Pentru început, ne vom scrie configurația și vom desena blocuri ca de obicei, nimic deosebit sau nou. Apoi, ne vom ocupa de diferite ecrane (ecran inițial, ecran de joc, joc peste ecran etc.). Deci apare întrebarea, cum facem ca Procesarea să arate pagina corectă la momentul potrivit?
Îndeplinirea acestei sarcini este destul de simplă. Vom avea o variabilă globală care stochează informațiile ecranului activ în prezent. Desenăm apoi conținutul ecranului corect în funcție de variabilă. În blocul draw, vom avea o instrucțiune if care verifică variabila și afișează conținutul ecranului în consecință. Ori de câte ori dorim să schimbăm ecranul, vom schimba acea variabilă cu identificatorul ecranului pe care vrem să îl afișeze. Acestea fiind spuse, iată cum arată codul nostru schelet:
/********* 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; }
Acest lucru poate părea înfricoșător la început, dar tot ce am făcut a fost să construim structura de bază și să separăm diferite părți cu blocuri de comentarii.
După cum puteți vedea, definim o metodă diferită pentru fiecare ecran de afișat. În blocul nostru de desen, pur și simplu verificăm valoarea variabilei gameScreen
și apelăm metoda corespunzătoare.
În partea void mousePressed(){...}
, ascultăm clicurile mouse-ului și dacă ecranul activ este 0, ecranul inițial, numim metoda startGame()
care pornește jocul așa cum v-ați aștepta. Prima linie a acestei metode schimbă variabila gameScreen
la 1, ecranul jocului.
Dacă acest lucru este înțeles, următorul pas este implementarea ecranului nostru inițial. Pentru a face asta, vom edita metoda initScreen()
. Aici merge:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
Acum ecranul nostru inițial are un fundal negru și un text simplu, „Click to start”, situat în mijloc și aliniat la centru. Dar când facem clic, nu se întâmplă nimic. Nu am specificat încă niciun conținut pentru ecranul jocului nostru. Metoda gameScreen()
nu are nimic în ea, așa că nu acoperim conținutul anterior desenat din ultimul ecran (textul) având background()
ca primă linie de desen. De aceea, textul este încă acolo, chiar dacă linia text()
nu mai este apelată (la fel ca exemplul de minge în mișcare din ultima parte care a lăsat o urmă în urmă) . Fundalul este încă negru din același motiv. Deci, să mergem mai departe și să începem implementarea ecranului de joc.
void gameScreen() { background(255); }
După această modificare, veți observa că fundalul devine alb și textul dispare.
Tutorial de procesare Pasul #2: Crearea mingii și implementarea gravitației
Acum, vom începe să lucrăm la ecranul jocului. Mai întâi ne vom crea mingea. Ar trebui să definim variabile pentru coordonatele, culoarea și dimensiunea acesteia, deoarece s-ar putea să dorim să schimbăm aceste valori mai târziu. De exemplu, dacă vrem să creștem dimensiunea mingii pe măsură ce jucătorul înscrie mai mult, astfel încât jocul să fie mai greu. Va trebui să îi modificăm dimensiunea, deci ar trebui să fie o variabilă. Vom defini și viteza mingii, după ce implementăm gravitația.
Mai întâi, să adăugăm următoarele:
... 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); }
Am definit coordonatele ca variabile globale, am creat o metodă care trage mingea, numită din metoda gameScreen . Singurul lucru la care trebuie să acordați atenție aici este că am inițializat coordonatele, dar le-am definit în setup()
. Motivul pentru care am făcut asta a fost că am vrut ca mingea să înceapă la un sfert de la stânga și la o cincime de sus. Nu există niciun motiv anume pentru care vrem asta, dar acesta este un punct bun pentru ca mingea să înceapă. Deci, trebuia să obținem width
și height
schiței în mod dinamic. Mărimea schiței este definită în setup()
, după prima linie. width
și height
nu sunt setate înainte de rularea setup()
, de aceea nu am putea realiza acest lucru dacă am defini variabilele deasupra.
Gravitatie
Acum, implementarea gravitației este de fapt partea ușoară. Există doar câteva trucuri. Iată mai întâi implementarea:
... 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); } }
Iar rezultatul este:
Ține-ți caii, fizician. Știu că nu așa funcționează gravitația în viața reală. În schimb, acesta este mai mult un proces de animație decât orice. Variabila pe care am definit-o ca fiind gravity
este doar o valoare numerică - un float
, astfel încât să putem folosi valori zecimale, nu doar numere întregi - pe care o adăugăm la ballSpeedVert
la fiecare buclă. Iar ballSpeedVert
este viteza verticală a mingii, care este adăugată la coordonatele Y a mingii ( ballY
) pe fiecare buclă. Urmărim coordonatele mingii și ne asigurăm că rămâne pe ecran. Dacă nu am fi făcut-o, mingea ar cădea la infinit. Deocamdată, mingea noastră se mișcă doar pe verticală. Așa că urmărim limitele podelei și tavanului ecranului. Cu metoda keepInScreen()
, verificăm dacă ballY
( + raza) este mai mică decât height
și, în mod similar, ballY
( - raza) este mai mare de 0
. Dacă nu se îndeplinesc condițiile, facem mingea să sară (de jos sau de sus) cu makeBounceBottom()
și makeBounceTop()
. Pentru a face mingea să sară, pur și simplu mutăm mingea în locația exactă în care trebuia să sară și înmulțim viteza verticală ( ballSpeedVert
) cu -1
(înmulțirea cu -1 schimbă semnul). Când valoarea vitezei are semnul minus, adăugând coordonatele Y, viteza devine ballY + (-ballSpeedVert)
, care este ballY - ballSpeedVert
. Deci mingea își schimbă imediat direcția cu aceeași viteză. Apoi, pe măsură ce adăugăm gravity
la ballSpeedVert
și ballSpeedVert
are o valoare negativă, începe să se apropie de 0
, devine în cele din urmă 0
și începe să crească din nou. Asta face ca mingea să se ridice, să se ridice mai încet, să se oprească și să înceapă să cadă.
Există totuși o problemă cu procesul nostru de animație – mingea continuă să sară. Dacă acesta ar fi fost un scenariu real, mingea s-ar fi confruntat cu rezistența aerului și frecarea de fiecare dată când atingea o suprafață. Acesta este comportamentul pe care îl dorim pentru procesul de animație al jocului nostru, așa că implementarea acestuia este ușoară. Adăugăm următoarele:
... 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); }
Și acum procesul nostru de animație produce asta:
După cum sugerează și numele, friction
este frecarea suprafeței și airfriction
este frecarea aerului. Deci, evident, friction
trebuie să se aplice de fiecare dată când mingea atinge orice suprafață. airfriction
trebuie să se aplice în mod constant. Deci asta am făcut. applyGravity()
rulează pe fiecare buclă, așa că luăm 0.0001
la sută din valoarea sa actuală din ballSpeedVert
pentru fiecare buclă. makeBounceBottom()
și makeBounceTop()
rulează atunci când mingea atinge orice suprafață. Deci, în acele metode, am făcut același lucru, doar că de data aceasta cu friction
.
Tutorial de procesare Pasul #3: Crearea rachetei
Acum avem nevoie de o rachetă pe care mingea să sară. Ar trebui să controlăm racheta. Să-l facem controlabil cu mouse-ul. Iată codul:
... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }
Am definit culoarea, lățimea și înălțimea rachetei ca o variabilă globală, am putea dori ca acestea să se schimbe în timpul jocului. Am implementat o metodă drawRacket()
care face ceea ce sugerează numele său. rectMode
la centru, astfel încât racheta noastră să fie aliniată la centrul cursorului nostru.
Acum că am creat racheta, trebuie să facem mingea să sară pe ea.
... 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; } } } }
Și iată rezultatul:
Deci ceea ce watchRacketBounce()
este că se asigură că racheta și mingea se ciocnesc. Există două lucruri de verificat aici, și anume dacă mingea și racheta s-au aliniat atât pe verticală, cât și pe orizontală. Prima declarație if verifică dacă coordonata X a părții drepte a mingii este mai mare decât coordonata X a părții stângi a rachetei (și invers). Dacă este, a doua afirmație verifică dacă distanța dintre minge și rachetă este mai mică sau egală cu raza mingii (ceea ce înseamnă că acestea se ciocnesc) . Deci, dacă se îndeplinesc aceste condiții, metoda makeBounceBottom()
este apelată și mingea sare pe racheta noastră (la mouseY
, unde este racheta).
Ați observat variabila overhead
care este calculată de mouseY - pmouseY
? pmouseX
și pmouseY
stochează coordonatele mouse-ului în cadrul precedent. Deoarece mouse-ul se poate mișca foarte repede, există șanse mari să nu detectăm corect distanța dintre minge și rachetă între cadre dacă mouse-ul se mișcă destul de repede spre minge. Deci, luăm diferența dintre coordonatele mouse-ului între cadre și luăm în considerare aceasta în timp ce detectăm distanța. Cu cât mouse-ul se mișcă mai repede, cu atât distanța mai mare este acceptabilă.
De asemenea, folosim overhead
din alt motiv. Detectăm direcția în care se mișcă mouse-ul verificând semnul de overhead
. Dacă overhead este negativ, mouse-ul era undeva mai jos în cadrul precedent, așa că mouse-ul nostru (racheta) se mișcă în sus. În acest caz, dorim să adăugăm o viteză suplimentară mingii și să o deplasăm puțin mai departe decât săritura obișnuită pentru a simula efectul lovirii mingii cu racheta. Dacă overhead
este mai mic de 0
, îl adăugăm la ballY
și ballSpeedVert
pentru a face mingea să meargă mai sus și mai rapid. Deci, cu cât racheta lovește mai repede mingea, cu atât se va ridica mai sus și mai repede.
Tutorial de procesare Pasul #4: Mișcarea orizontală și controlul mingii
În această secțiune, vom adăuga mișcare orizontală mingii. Apoi, vom face posibil să controlăm mingea pe orizontală cu racheta noastră. Începem:
... // 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); } }
Iar rezultatul este:
Ideea de aici este aceeași cu cea pe care am făcut-o noi pentru mișcarea verticală. Am creat o variabilă de viteză orizontală, ballSpeedHorizon
. Am creat o metodă de a aplica viteză orizontală ballX
și de a elimina frecarea aerului. Am adăugat încă două instrucțiuni if la metoda keepInScreen()
care vor urmări mingea pentru a lovi marginile din stânga și din dreapta ale ecranului. În cele din urmă, am creat makeBounceLeft()
și makeBounceRight()
pentru a gestiona săriturile din stânga și dreapta.

Acum că am adăugat viteză orizontală jocului, vrem să controlăm mingea cu racheta. Ca și în faimosul joc Atari Breakout și în toate celelalte jocuri de spargere cărămizi, mingea ar trebui să meargă la stânga sau la dreapta în funcție de punctul de pe rachetă pe care îl lovește. Marginile rachetei ar trebui să ofere mingii mai multă viteză orizontală, în timp ce mijlocul nu ar trebui să aibă niciun efect. Cod mai întâi:
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; ... } } }
Rezultatul este:
Adăugarea acelei linii simple la watchRacketBounce()
a făcut treaba. Ceea ce am făcut este că am determinat distanța punctului pe care mingea îl lovește din centrul rachetei cu ballX - mouseX
. Apoi, o facem viteza orizontală. Diferența reală a fost prea mare, așa că am făcut câteva încercări și mi-am dat seama că o zecime din valoare mi se pare cea mai naturală.
Tutorial de procesare Pasul #5: Crearea pereților
Schița noastră începe să arate mai mult ca un joc cu fiecare pas. În acest pas, vom adăuga pereți care se mișcă spre stânga, la fel ca în 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); } }
Și asta a dus la:
Chiar dacă codul pare lung și intimidant, promit că nu este nimic greu de înțeles. Primul lucru de observat este ArrayList
. Pentru cei dintre voi care nu știu ce este un ArrayList
, este doar o implementare a listei care acționează ca un Array, dar are câteva avantaje față de acesta. Este redimensionabil, are metode utile precum list.add(index)
, list.get(index)
și list.remove(index)
. Păstrăm datele de perete ca matrice întregi în cadrul listei de matrice. Datele pe care le păstrăm în matrice sunt pentru decalajul dintre doi pereți. Matricele conțin următoarele valori:
[gap wall X, gap wall Y, gap wall width, gap wall height]
Pereții efectivi sunt desenați pe baza valorilor peretelui gol. Rețineți că toate acestea ar putea fi gestionate mai bine și mai curat folosind clase, dar, deoarece utilizarea programării orientate pe obiecte (OOP) nu este în scopul acestui tutorial de procesare, așa o vom trata. Avem două metode de bază pentru a gestiona pereții, wallAdder()
și wallHandler
.
wallAdder()
adaugă pur și simplu pereți noi în fiecare milisecundă wallInterval
la lista de matrice. Avem o variabilă globală lastAddTime
care stochează ora când a fost adăugat ultimul perete (în milisecunde) . Dacă milisecunda actuală millis()
minus ultima milisecundă adăugată lastAddTime
este mai mare decât valoarea intervalului wallInterval
, înseamnă că acum este timpul să adăugați un nou perete. Variabilele aleatorii gap sunt apoi generate pe baza variabilelor globale definite în partea de sus. Apoi, un nou perete (matrice întreg care stochează datele peretelui gol) este adăugat în lista de matrice și lastAddTime
este setat la milisecunde millis()
curente.
wallHandler()
parcurge pereții curenti care se află în lista de matrice. Și pentru fiecare articol din fiecare buclă, apelează wallRemover(i)
, wallMover(i)
și wallDrawer(i)
după valoarea indexului listei de matrice. Aceste metode fac ceea ce sugerează numele lor. wallDrawer()
desenează pereții actuali pe baza datelor peretelui gol. Acesta preia matricea de date perete din lista de matrice și apelează metoda rect()
pentru a atrage pereții acolo unde ar trebui să fie de fapt. wallMover()
elementul din lista de matrice, își schimbă locația X pe baza variabilei globale wallSpeed
. În cele din urmă, wallRemover()
elimină pereții din lista de matrice care sunt în afara ecranului. Dacă nu am făcut asta, Processing le-ar fi tratat așa cum sunt încă pe ecran. Și asta ar fi fost o pierdere uriașă în performanță. Deci, atunci când un perete este eliminat din lista de matrice, acesta nu este desenat în buclele ulterioare.
Ultimul lucru dificil de făcut este detectarea ciocnirilor între minge și pereți.
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()
este apelată pentru fiecare perete din fiecare buclă. Luăm coordonatele peretelui gol, calculăm coordonatele pereților efectivi (sus și jos) și verificăm dacă coordonatele mingii se ciocnesc de pereți.
Tutorial de procesare Pasul #6: Sănătate și scor
Acum că putem detecta ciocnirile mingii și ale pereților, putem decide asupra mecanicii de joc. După câteva reglaje la joc, am reușit să fac jocul oarecum jucabil. Dar totuși, a fost foarte greu. Primul meu gând despre joc a fost să-l fac ca Flappy Bird, când mingea atinge pereții, jocul se termină. Dar apoi mi-am dat seama că va fi imposibil să joc. Deci iată ce m-am gândit:
Ar trebui să existe o bară de sănătate deasupra mingii. Mingea ar trebui să-și piardă sănătatea în timp ce atinge pereții. Cu această logică, nu are sens să faci mingea să sară înapoi de pe pereți. Deci, când sănătatea este 0, jocul ar trebui să se încheie și ar trebui să trecem la jocul peste ecran. Deci iată-ne:
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(); } }
Și iată o alergare simplă:
Am creat o health
variabilă globală pentru a păstra sănătatea mingii. Și apoi a creat o metodă drawHealthBar()
care desenează două dreptunghiuri deasupra mingii. Prima este bara de sănătate de bază, cealaltă este cea activă care arată starea de sănătate curentă. Lățimea celui de-al doilea este dinamic și este calculată cu healthBarWidth*(health/maxHealth)
, raportul dintre sănătatea noastră actuală în raport cu lățimea barei de sănătate. În cele din urmă, culorile de umplere sunt stabilite în funcție de valoarea sănătății. Nu în ultimul rând, scoruri :
... 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); }
Trebuia să înscriem când mingea trece de un perete. Dar trebuie să adăugăm maxim 1 scor pe perete. Adică, dacă mingea trece de un perete, apoi se întoarce înapoi și o depășește din nou, nu trebuie adăugat un alt scor. Pentru a realiza acest lucru, am adăugat o altă variabilă la matricea de perete gol din lista de matrice. Noua variabilă stochează 0
dacă mingea nu a trecut încă de acel perete și 1
dacă a trecut. Apoi, am modificat metoda watchWallCollision()
. Am adăugat o condiție care declanșează metoda score()
și marchează peretele ca trecut atunci când mingea trece de un perete pe care nu a trecut înainte.
Acum suntem foarte aproape de final. Ultimul lucru de făcut este să implementați click to restart
pe ecranul jocului. Trebuie să setăm toate variabilele pe care le-am folosit la valoarea lor inițială și să repornim jocul. Iată-l:
... 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; }
Să mai adăugăm câteva culori.
Voila! Avem Flappy Pong!
Codul complet al jocului Processing poate fi găsit aici.
Portarea codului de procesare a jocului pe web folosind p5.js
p5.js este o bibliotecă JavaScript cu o sintaxă foarte asemănătoare cu cea a limbajului de programare Processing. Nu este o bibliotecă capabilă să ruleze pur și simplu codul de procesare existent; în schimb, p5.js necesită scrierea unui cod JavaScript real, similar cu portul JavaScript al Procesării cunoscut sub numele de Processing.js. Sarcina noastră este să convertim codul de procesare în JavaScript folosind API-ul p5.js. Biblioteca are un set de funcții și o sintaxă similară procesării și trebuie să facem anumite modificări codului nostru pentru a le face să funcționeze în JavaScript - dar, deoarece atât Procesarea, cât și JavaScript au similarități cu Java, este mai puțin un salt decât pare. . Chiar dacă nu sunteți un dezvoltator JavaScript, modificările sunt foarte banale și ar trebui să puteți urmări foarte bine.
În primul rând, trebuie să creăm un index.html
simplu și să adăugăm p5.min.js
la antetul nostru. De asemenea, trebuie să creăm un alt fișier numit flappy_pong.js
care va găzdui codul nostru convertit.
<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>
Strategia noastră în timpul conversiei codului ar trebui să fie copierea și lipirea întregului cod în flappy_pong.js
și apoi efectuarea tuturor modificărilor. Și asta am făcut. Și iată pașii pe care i-am făcut pentru a actualiza codul:
Javascript este un limbaj netipizat (nu există declarații de tip precum
int
șifloat
). Deci trebuie să schimbăm toate declarațiile de tip învar
.Nu există niciun
void
în Javascript. Ar trebui să schimbăm totul pentru afunction
.Trebuie să eliminăm declarațiile de tip ale argumentelor din semnăturile funcției. (adică
void wallMover(var index) {
pentru afunction wallMover(index) {
)Nu există
ArrayList
în JavaScript. Dar putem realiza același lucru folosind matrice JavaScript. Facem următoarele modificări:-
ArrayList<int[]> walls = new ArrayList<int[]>();
avar walls = [];
-
walls.clear();
lawalls = [];
-
walls.add(randWall);
lawalls.push(randWall);
-
walls.remove(index);
lawalls.splice(index,1);
-
walls.get(index);
lawalls[index]
-
walls.size()
lawalls.length
-
Modificați declarația matricei
var randWall = {width, randY, wallWidth, randHeight, 0};
tovar randWall = [width, randY, wallWidth, randHeight, 0];
Eliminați toate cuvintele cheie
public
.Mutați toate declarațiile
color(0)
înfunction setup()
deoarececolor()
nu va fi definit înainte de apelulsetup()
.Modificați
size(500, 500);
pentru acreateCanvas(500, 500);
Redenumiți
function gameScreen(){
la altceva precumfunction gamePlayScreen(){
deoarece avem deja o variabilă globală numităgameScreen
. Când lucram cu Processing, una era o funcție, iar cealaltă era o variabilăint
. Dar JavaScript le confundă, deoarece nu sunt scrise.Același lucru este valabil și pentru
score()
. L-am redenumit înaddScore()
.
Codul JavaScript complet care acoperă totul din acest tutorial de procesare poate fi găsit aici.
Procesarea codului jocului: și tu o poți face
În acest tutorial de procesare, am încercat să explic cum să construiești un joc foarte simplu. Totuși, ceea ce am făcut în acest articol este doar vârful aisbergului. Cu limbajul de programare Processing, aproape orice se poate realiza. După părerea mea, este cel mai bun instrument pentru a programa ceea ce îți imaginezi. Intenția mea reală cu acest tutorial de procesare a fost mai degrabă decât să predau Procesarea și construirea unui joc, să demonstrez că programarea nu este atât de grea. Construirea propriului joc nu este doar un vis. Am vrut să vă arăt că, cu puțin efort și entuziasm, o puteți face. Sper cu adevărat că aceste două articole îi inspiră pe toată lumea să dea o șansă programării.