Kompletny przewodnik po języku przetwarzania, część II: Tworzenie prostej gry
Opublikowany: 2022-03-11To jest druga część ostatecznego przewodnika po języku przetwarzania. W pierwszej części przedstawiłem podstawowy przewodnik po języku przetwarzania. Następnym krokiem do nauki Przetwarzania jest po prostu bardziej praktyczne programowanie.
W tym artykule pokażę, jak krok po kroku wykorzystać Przetwarzanie do zaimplementowania własnej gry. Każdy krok zostanie szczegółowo wyjaśniony. Następnie przeniesiemy grę do sieci.
Zanim rozpoczniemy samouczek Przetwarzanie, oto kod ćwiczenia z logo DVD z poprzedniej części. Jeśli masz jakieś pytania, koniecznie zostaw komentarz.
Samouczek przetwarzania: prosta gra
Gra, którą zbudujemy w tym samouczku Przetwarzania, jest rodzajem połączenia Flappy Bird, Pong i Brick Breaker. Powodem, dla którego wybrałem taką grę, jest to, że zawiera większość koncepcji, z którymi borykają się początkujący, ucząc się tworzenia gier. Jest to oparte na moim doświadczeniu z czasów, gdy byłem asystentem nauczania, pomagając nowym programistom uczyć się obsługi przetwarzania. Pojęcia te obejmują grawitację, kolizje, zachowywanie wyników, obsługę różnych ekranów i interakcje klawiatury/myszy. Flappy Pong ma je wszystkie.
Zagraj w grę teraz!
Bez korzystania z koncepcji programowania obiektowego (OOP) nie jest łatwo budować złożone gry, takie jak gry platformowe z wieloma poziomami, graczami, jednostkami itp. W miarę postępów zobaczysz, jak kod bardzo szybko się komplikuje. Zrobiłem co w mojej mocy, aby ten samouczek dotyczący przetwarzania był uporządkowany i prosty.
Radzę śledzić artykuł, pobrać cały kod, pobawić się nim na własną rękę, jak najszybciej zacząć myśleć o własnej grze i zacząć ją wdrażać.
Więc zacznijmy.
Budowanie Flappy Pong
Samouczek przetwarzania Krok 1: Inicjowanie i obsługa różnych ekranów
Pierwszym krokiem jest zainicjowanie naszego projektu. Na początek napiszemy nasze ustawienia i narysujemy bloki jak zwykle, nic wymyślnego ani nowego. Następnie zajmiemy się różnymi ekranami (ekran początkowy, ekran gry, gra po ekranie itp.). Powstaje więc pytanie, jak sprawić, by Przetwarzanie wyświetlało właściwą stronę we właściwym czasie?
Wykonanie tego zadania jest dość proste. Będziemy mieli zmienną globalną, która przechowuje informacje o aktualnie aktywnym ekranie. Następnie rysujemy zawartość odpowiedniego ekranu w zależności od zmiennej. W bloku rysowania będziemy mieli instrukcję if, która sprawdza zmienną i odpowiednio wyświetla zawartość ekranu. Za każdym razem, gdy chcemy zmienić ekran, zmienimy tę zmienną na identyfikator ekranu, który chcemy wyświetlić. Powiedziawszy to, oto jak wygląda nasz szkielet kodu:
/********* 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; }
Na początku może to wyglądać przerażająco, ale wszystko, co zrobiliśmy, to zbudowaliśmy podstawową strukturę i oddzieliliśmy różne części za pomocą bloków komentarzy.
Jak widać, definiujemy inną metodę wyświetlania każdego ekranu. W naszym bloku rysowania po prostu sprawdzamy wartość naszej zmiennej gameScreen
i wywołujemy odpowiednią metodę.
W części void mousePressed(){...}
nasłuchujemy kliknięć myszą i jeśli aktywny ekran ma wartość 0, czyli ekran początkowy, wywołujemy metodę startGame()
, która uruchamia grę zgodnie z oczekiwaniami. Pierwszy wiersz tej metody zmienia zmienną gameScreen
na 1, czyli ekran gry.
Jeśli to zrozumiesz, następnym krokiem jest zaimplementowanie naszego początkowego ekranu. W tym celu edytujemy metodę initScreen()
. Oto jest:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
Teraz nasz początkowy ekran ma czarne tło i prosty tekst „Kliknij, aby rozpocząć”, umieszczony pośrodku i wyrównany do środka. Ale kiedy klikamy, nic się nie dzieje. Nie określiliśmy jeszcze żadnej zawartości naszego ekranu gry. Metoda gameScreen()
nie ma w sobie nic, więc nie zasłaniamy poprzedniej zawartości narysowanej z ostatniego ekranu (tekstu) przez ustawienie background()
jako pierwszej linii rysowania. Dlatego tekst wciąż tam jest, mimo że linia text()
nie jest już wywoływana (tak jak przykład poruszającej się kuli z ostatniej części, która zostawiała ślad) . Z tego samego powodu tło jest nadal czarne. Więc chodźmy dalej i zacznijmy implementować ekran gry.
void gameScreen() { background(255); }
Po tej zmianie zauważysz, że tło staje się białe, a tekst znika.
Samouczek przetwarzania Krok 2: Tworzenie kuli i wprowadzanie grawitacji
Teraz zaczniemy pracę nad ekranem gry. Najpierw stworzymy naszą piłkę. Powinniśmy zdefiniować zmienne dla jego współrzędnych, koloru i rozmiaru, ponieważ możemy później zmienić te wartości. Na przykład, jeśli chcemy zwiększyć rozmiar piłki, gdy gracz zdobywa wyższe punkty, aby gra była trudniejsza. Będziemy musieli zmienić jego rozmiar, więc powinna to być zmienna. Zdefiniujemy również prędkość piłki po wprowadzeniu grawitacji.
Najpierw dodajmy:
... 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); }
Zdefiniowaliśmy współrzędne jako zmienne globalne, stworzyliśmy metodę rysowania piłki, wywoływaną z metody gameScreen . Jedyną rzeczą, na którą należy zwrócić uwagę, jest to, że zainicjalizowaliśmy współrzędne, ale zdefiniowaliśmy je w setup()
. Powodem, dla którego to zrobiliśmy, jest to, że chcieliśmy, aby piłka zaczynała się w jednej czwartej od lewej i jednej piątej od góry. Nie ma żadnego konkretnego powodu, dla którego tego chcemy, ale to dobry moment na rozpoczęcie piłki. Musieliśmy więc dynamicznie uzyskać width
i height
szkicu. Rozmiar szkicu jest zdefiniowany w setup()
, po pierwszej linii. width
i height
nie są ustawiane przed setup()
, dlatego nie moglibyśmy tego osiągnąć, jeśli zdefiniowaliśmy zmienne na górze.
Powaga
Teraz wdrożenie grawitacji jest właściwie najłatwiejsze. Jest tylko kilka sztuczek. Oto implementacja pierwsza:
... 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); } }
A wynik to:
Trzymaj konie, fizyku. Wiem, że grawitacja tak nie działa w prawdziwym życiu. Zamiast tego jest to bardziej proces animacji niż cokolwiek innego. Zmienna, którą zdefiniowaliśmy jako gravity
, to tylko wartość liczbowa — float
, dzięki której możemy używać wartości dziesiętnych, a nie tylko liczb całkowitych — którą dodajemy do ballSpeedVert
w każdej pętli. A ballSpeedVert
to pionowa prędkość piłki, która jest dodawana do współrzędnej Y piłki ( ballY
) w każdej pętli. Obserwujemy współrzędne piłki i upewniamy się, że pozostaje na ekranie. Gdybyśmy tego nie zrobili, piłka spadłaby w nieskończoność. Na razie nasza piłka porusza się tylko w pionie. Obserwujemy więc granice podłogi i sufitu ekranu. Za pomocą metody keepInScreen()
sprawdzamy, czy ballY
( + promień) jest mniejszy niż height
, i podobnie ballY
( - promień) jest większy niż 0
. Jeśli warunki nie są spełnione, piłka odbija się (od dołu lub od góry) za pomocą makeBounceBottom()
i makeBounceTop()
. Aby piłka odbiła się, po prostu przesuwamy ją dokładnie w miejsce, w którym miała się odbić, i mnożymy prędkość pionową ( ballSpeedVert
) przez -1
(mnożenie przez -1 zmienia znak). Gdy wartość prędkości ma znak minus, po dodaniu współrzędnej Y prędkość staje się ballY + (-ballSpeedVert)
, czyli ballY - ballSpeedVert
. Tak więc piłka natychmiast zmienia kierunek z tą samą prędkością. Następnie, gdy dodajemy gravity
do ballSpeedVert
, a ballSpeedVert
ma wartość ujemną, zaczyna zbliżać się do 0
, ostatecznie staje się 0
i ponownie zaczyna rosnąć. To sprawia, że piłka wznosi się, wznosi się wolniej, zatrzymuje i zaczyna opadać.
Jest jednak problem z naszym procesem animacji — piłka wciąż się odbija. Gdyby to był rzeczywisty scenariusz, piłka napotykałaby na opór powietrza i tarcie za każdym razem, gdy dotknie powierzchni. Takiego zachowania chcemy w procesie animacji naszej gry, więc wdrożenie tego jest łatwe. Dodajemy:
... 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); }
A teraz nasz proces animacji daje to:
Jak sama nazwa wskazuje, friction
to tarcie powierzchniowe, a airfriction
powietrza to tarcie powietrza. Tak więc, oczywiście, friction
musi działać za każdym razem, gdy piłka dotyka dowolnej powierzchni. airfriction
musi jednak obowiązywać stale. Więc to właśnie zrobiliśmy. applyGravity()
działa w każdej pętli, więc pobieramy 0.0001
procent jej bieżącej wartości z ballSpeedVert
w każdej pętli. makeBounceBottom()
i makeBounceTop()
działają, gdy piłka dotknie dowolnej powierzchni. Więc w tych metodach zrobiliśmy to samo, tylko tym razem z friction
.
Samouczek przetwarzania Krok 3: Tworzenie rakiety
Teraz potrzebujemy rakiety, aby piłka się odbiła. Powinniśmy kontrolować rakietę. Zróbmy to sterowane za pomocą myszy. Oto kod:
... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }
Zdefiniowaliśmy kolor, szerokość i wysokość rakiety jako zmienną globalną, możemy chcieć, aby zmieniały się podczas gry. Zaimplementowaliśmy metodę drawRacket()
, która robi to, co sugeruje jej nazwa. Ustawiamy rectMode
na center, więc nasza rakieta jest wyrównana do środka naszego kursora.
Teraz, gdy stworzyliśmy rakietę, musimy sprawić, by piłka odbiła się od niej.
... 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; } } } }
A oto wynik:
Więc to, co watchRacketBounce()
, to upewnienie się, że rakieta i piłka się zderzają. Należy tutaj sprawdzić dwie rzeczy, to znaczy, czy piłka i rakieta są ustawione zarówno w pionie, jak iw poziomie. Pierwsza instrukcja if sprawdza, czy współrzędna X prawej strony piłki jest większa niż współrzędna X lewej strony rakiety (i odwrotnie). Jeśli tak, drugie stwierdzenie sprawdza, czy odległość między piłką a rakietą jest mniejsza lub równa promieniowi piłeczki (co oznacza, że się zderzają) . Jeśli więc te warunki są spełnione, wywoływana jest metoda makeBounceBottom()
i piłka odbija się od naszej rakiety (w mouseY
, gdzie jest rakieta).
Czy zauważyłeś overhead
zmiennej, który jest obliczany przez mouseY - pmouseY
? pmouseX
i pmouseY
przechowują współrzędne myszy w poprzedniej klatce. Ponieważ mysz porusza się bardzo szybko, istnieje duża szansa, że nie wykryjemy prawidłowo odległości między piłką a rakietą między klatkami, jeśli mysz porusza się wystarczająco szybko w kierunku piłki. Tak więc bierzemy różnicę współrzędnych myszy pomiędzy klatkami i bierzemy to pod uwagę podczas wykrywania odległości. Im szybciej mysz się porusza, tym większa odległość jest akceptowalna.
overhead
używamy również z innego powodu. Wykrywamy, w którą stronę porusza się mysz, sprawdzając znak nad overhead
. Jeśli overhead jest ujemny, mysz była gdzieś poniżej w poprzedniej klatce, więc nasza mysz (rakieta) porusza się w górę. W takim przypadku chcemy dodać piłce większą prędkość i przesunąć ją nieco dalej niż zwykłe odbicie, aby zasymulować efekt uderzenia piłki rakietą. Jeśli overhead
jest mniejszy niż 0
, dodajemy go do ballY
i ballSpeedVert
, aby piłka leciała wyżej i szybciej. Więc im szybciej rakieta uderzy piłkę, tym wyżej i szybciej będzie się poruszać.
Samouczek przetwarzania Krok 4: Ruch poziomy i kontrolowanie piłki
W tej sekcji dodamy do piłki ruch poziomy. Następnie umożliwimy poziome kontrolowanie piłki naszą rakietą. No to ruszamy:
... // 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); } }
A wynik to:
Pomysł tutaj jest taki sam, jak w przypadku ruchu pionowego. Stworzyliśmy zmienną prędkości poziomej, ballSpeedHorizon
. Stworzyliśmy metodę zastosowania prędkości poziomej do ballX
i wyeliminowania tarcia powietrza. Do metody keepInScreen()
dodaliśmy jeszcze dwie instrukcje if, która będzie obserwować piłkę pod kątem uderzenia w lewą i prawą krawędź ekranu. W końcu stworzyliśmy makeBounceLeft()
i makeBounceRight()
do obsługi odbić z lewej i prawej strony.

Teraz, gdy dodaliśmy do gry prędkość poziomą, chcemy kontrolować piłkę rakietą. Podobnie jak w słynnej grze Atari Breakout i we wszystkich innych grach w łamanie cegieł, piłka powinna lecieć w lewo lub w prawo w zależności od tego, w jaki punkt na rakiecie trafi. Krawędzie rakiety powinny nadawać piłce większą prędkość poziomą, podczas gdy środek nie powinien dawać żadnego efektu. Najpierw kod:
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; ... } } }
Wynik to:
Dodanie tej prostej linii do watchRacketBounce()
. To, co zrobiliśmy, to określenie odległości punktu, w który piłka uderza od środka rakiety za pomocą ballX - mouseX
. Następnie ustawiamy prędkość poziomą. Rzeczywista różnica była zbyt duża, więc wykonałem kilka prób i stwierdziłem, że jedna dziesiąta wartości wydaje się najbardziej naturalna.
Samouczek przetwarzania Krok 5: Tworzenie ścian
Nasz szkic z każdym krokiem zaczyna przypominać grę. W tym kroku dodamy ściany przesuwające się w lewo, tak jak w 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); } }
A to zaowocowało:
Mimo że kod wygląda na długi i onieśmielający, obiecuję, że nie ma nic trudnego do zrozumienia. Pierwszą rzeczą do zauważenia jest ArrayList
. Dla tych z was, którzy nie wiedzą, czym jest ArrayList
, jest to po prostu implementacja listy, która działa jak tablica, ale ma nad nią pewne zalety. Jest zmienny, ma przydatne metody, takie jak list.add(index)
, list.get(index)
i list.remove(index)
. Przechowujemy dane o ścianach jako tablice liczb całkowitych w arraylist. Dane, które przechowujemy w tablicach, dotyczą przerwy między dwiema ścianami. Tablice zawierają następujące wartości:
[gap wall X, gap wall Y, gap wall width, gap wall height]
Rzeczywiste ściany są rysowane na podstawie wartości ścian szczeliny. Zwróć uwagę, że wszystkie te elementy mogą być obsługiwane lepiej i czyściej przy użyciu klas, ale ponieważ użycie programowania zorientowanego obiektowo (OOP) nie jest objęte zakresem tego samouczka dotyczącego przetwarzania, tak właśnie sobie z tym poradzimy. Mamy dwie podstawowe metody zarządzania ścianami, wallAdder()
i wallHandler
.
wallAdder()
po prostu dodaje nowe ściany w każdej milisekundzie wallInterval
do tablicy arraylist. Mamy globalną zmienną lastAddTime
, która przechowuje czas dodania ostatniej ściany (w milisekundach) . Jeśli bieżąca milisekunda millis()
minus ostatnia dodana milisekunda lastAddTime
jest większa niż nasza wartość interwału wallInterval
, oznacza to, że nadszedł czas na dodanie nowej ściany. Zmienne luki losowej są następnie generowane na podstawie zmiennych globalnych zdefiniowanych na samej górze. Następnie nowa ściana (tablica liczb całkowitych, która przechowuje dane ściany odstępów) jest dodawana do arraylist, a lastAddTime
jest ustawiana na bieżącą milisekundę millis()
.
wallHandler()
przegląda w pętli bieżące ściany znajdujące się w arraylist. I dla każdego elementu w każdej pętli wywołuje on wallRemover(i)
, wallMover(i)
i wallDrawer(i)
według wartości indeksu tablicy arraylist. Te metody robią to, co sugeruje ich nazwa. wallDrawer()
rysuje rzeczywiste ściany w oparciu o dane ściany odstępu. Pobiera tablicę danych ściany z arraylist i wywołuje metodę rect()
, aby narysować ściany tam, gdzie powinny być. wallMover()
pobiera element z arraylist, zmienia jego położenie w X w oparciu o zmienną globalną wallSpeed
. Na koniec wallRemover()
usuwa ściany z arraylist, które są poza ekranem. Gdybyśmy tego nie zrobili, przetwarzanie potraktowałoby je tak, jak wciąż są na ekranie. A to byłaby ogromna utrata wydajności. Więc kiedy ściana jest usuwana z arraylist, nie jest rysowana w kolejnych pętlach.
Ostatnią trudną rzeczą do zrobienia jest wykrycie kolizji między piłką a ścianami.
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()
jest wywoływana dla każdej ściany w każdej pętli. Łapiemy współrzędne ściany szczeliny, obliczamy współrzędne rzeczywistych ścian (góra i dół) i sprawdzamy, czy współrzędne kuli kolidują ze ścianami.
Samouczek przetwarzania Krok #6: Zdrowie i wynik
Teraz, gdy jesteśmy w stanie wykryć kolizje piłki ze ścianami, możemy decydować o mechanice gry. Po pewnym dostrojeniu gry, udało mi się sprawić, że gra będzie grywalna. Ale i tak było bardzo ciężko. Moją pierwszą myślą o grze było zrobienie jej jak Flappy Bird, kiedy piłka dotknie ścian, gra się kończy. Ale potem zdałem sobie sprawę, że nie da się grać. Oto, co pomyślałem:
Na górze piłki powinien znajdować się pasek zdrowia. Piłka powinna tracić zdrowie, gdy dotyka ścian. Przy tej logice nie ma sensu odbijać piłki od ścian. Więc gdy zdrowie wynosi 0, gra powinna się zakończyć i powinniśmy przejść do ekranu gry. Więc zaczynamy:
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(); } }
A oto prosty bieg:
Stworzyliśmy globalną zmienną health
, aby utrzymać zdrowie piłki. Następnie utworzono metodę drawHealthBar()
, która rysuje dwa prostokąty na górze kuli. Pierwszy to podstawowy pasek zdrowia, drugi to aktywny, który pokazuje aktualne zdrowie. Szerokość drugiego jest dynamiczna i jest obliczana za pomocą healthBarWidth*(health/maxHealth)
, stosunek naszego obecnego zdrowia do szerokości paska zdrowia. Wreszcie kolory wypełnienia są ustawiane zgodnie z wartością zdrowia. Ostatnie, ale nie najmniej ważne, wyniki :
... 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); }
Musieliśmy zdobyć bramkę, gdy piłka przeleci przez ścianę. Ale musimy dodać maksymalnie 1 wynik na ścianę. Oznacza to, że jeśli piłka minie ścianę, a następnie cofnie się i przejdzie ponownie, nie należy dodawać kolejnego wyniku. Aby to osiągnąć, dodaliśmy kolejną zmienną do tablicy gap wall w arraylist. Nowa zmienna przechowuje 0
, jeśli piłka nie przeleciała jeszcze przez tę ścianę, i 1
, jeśli przeleciała. Następnie zmodyfikowaliśmy watchWallCollision()
. Dodaliśmy warunek, który uruchamia metodę score()
i oznacza ścianę jako przeszłą, gdy piłka minie ścianę, której wcześniej nie minęła.
Jesteśmy teraz bardzo blisko końca. Ostatnią rzeczą do zrobienia jest zaimplementowanie click to restart
grę na ekranie. Musimy ustawić wszystkie używane przez nas zmienne na ich początkową wartość i zrestartować grę. Oto on:
... 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; }
Dodajmy więcej kolorów.
Voila! Mamy Flappy Ponga!
Pełny kod gry Processing można znaleźć tutaj.
Przenoszenie kodu gry przetwarzającej do sieci za pomocą p5.js
p5.js to biblioteka JavaScript o bardzo podobnej składni do języka programowania Processing. Nie jest to biblioteka, która jest w stanie po prostu uruchomić istniejący kod przetwarzania; zamiast tego p5.js wymaga napisania rzeczywistego kodu JavaScript — podobnie jak port JavaScript Processing znany jako Processing.js. Naszym zadaniem jest przekonwertowanie kodu Processing na JavaScript za pomocą API p5.js. Biblioteka ma zestaw funkcji i składnię podobną do Przetwarzania i musimy wprowadzić pewne zmiany w naszym kodzie, aby działały w JavaScript — ale ponieważ zarówno Przetwarzanie, jak i JavaScript mają podobieństwa z Javą, jest to mniejszy skok niż się wydaje . Nawet jeśli nie jesteś programistą JavaScript, zmiany są bardzo trywialne i powinieneś być w stanie nadążać za nimi.
Przede wszystkim musimy stworzyć prosty index.html
i dodać p5.min.js
do naszego nagłówka. Musimy również stworzyć inny plik o nazwie flappy_pong.js
, który będzie zawierał nasz przekonwertowany kod.
<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>
Naszą strategią podczas konwersji kodu powinno być skopiowanie i wklejenie całego naszego kodu do flappy_pong.js
, a następnie wprowadzenie wszystkich zmian. I to właśnie zrobiłem. A oto kroki, które podjąłem, aby zaktualizować kod:
JavaScript to języki bez typu (nie ma deklaracji typu, takich jak
int
ifloat
). Musimy więc zmienić wszystkie deklaracje typu navar
.W JavaScript nie ma
void
. Powinniśmy wszystko zmienić, abyfunction
.Musimy usunąć deklaracje typów argumentów z sygnatur funkcji. (tj.
void wallMover(var index) {
tofunction wallMover(index) {
)W JavaScript nie ma
ArrayList
. Ale to samo możemy osiągnąć za pomocą tablic JavaScript. Wprowadzamy następujące zmiany:-
ArrayList<int[]> walls = new ArrayList<int[]>();
dovar walls = [];
-
walls.clear();
dowalls = [];
-
walls.add(randWall);
dowalls.push(randWall);
-
walls.remove(index);
dowalls.splice(index,1);
-
walls.get(index);
dowalls[index]
-
walls.size()
dowalls.length
-
Zmień deklarację tablicy
var randWall = {width, randY, wallWidth, randHeight, 0};
tovar randWall = [width, randY, wallWidth, randHeight, 0];
Usuń wszystkie
public
słowa kluczowe.Przenieś wszystkie deklaracje
color(0)
dofunction setup()
, ponieważcolor()
nie zostanie zdefiniowany przed wywołaniemsetup()
.Zmień
size(500, 500);
createCanvas(500, 500);
Zmień nazwę
function gameScreen(){
na inną, taką jakfunction gamePlayScreen(){
, ponieważ mamy już zmienną globalną o nazwiegameScreen
. Kiedy pracowaliśmy z Processing, jedna była funkcją, a druga zmiennąint
. Ale JavaScript myli je, ponieważ są one nieopisane.To samo dotyczy
score()
. Zmieniłem jego nazwę naaddScore()
.
Pełny kod JavaScript obejmujący wszystko w tym samouczku dotyczącym przetwarzania można znaleźć tutaj.
Przetwarzanie kodu gry: Ty też możesz to zrobić
W tym samouczku dotyczącym przetwarzania próbowałem wyjaśnić, jak zbudować bardzo prostą grę. Jednak to, co zrobiliśmy w tym artykule, to tylko wierzchołek góry lodowej. Z językiem programowania Processing można osiągnąć prawie wszystko. Moim zdaniem to najlepsze narzędzie do programowania tego, co sobie wyobrażasz. Moim faktycznym zamiarem w tym samouczku dotyczącym przetwarzania było raczej niż uczenie przetwarzania i budowania gry, aby udowodnić, że programowanie nie jest takie trudne. Budowanie własnej gry to nie tylko marzenie. Chciałem Ci pokazać, że przy odrobinie wysiłku i entuzjazmu możesz to zrobić. Naprawdę mam nadzieję, że te dwa artykuły zainspirują wszystkich do spróbowania programowania.