Ultimativer Leitfaden zur Verarbeitungssprache Teil II: Erstellen eines einfachen Spiels
Veröffentlicht: 2022-03-11Dies ist der zweite Teil des ultimativen Leitfadens zur Verarbeitungssprache. Im ersten Teil habe ich eine grundlegende Anleitung zur Verarbeitungssprache gegeben. Der nächste Schritt zum Erlernen von Processing ist einfach mehr praktische Programmierung.
In diesem Artikel zeige ich Ihnen Schritt für Schritt, wie Sie mit Processing Ihr eigenes Spiel implementieren. Jeder Schritt wird im Detail erklärt. Dann werden wir das Spiel ins Internet portieren.
Bevor wir mit dem Processing-Tutorial beginnen, ist hier der Code der DVD-Logo-Übung aus dem vorherigen Teil. Wenn Sie Fragen haben, hinterlassen Sie unbedingt einen Kommentar.
Processing Tutorial: Ein einfaches Spiel
Das Spiel, das wir in diesem Processing-Tutorial bauen werden, ist eine Art Kombination aus Flappy Bird, Pong und Brick Breaker. Der Grund, warum ich ein Spiel wie dieses ausgewählt habe, ist, dass es die meisten Konzepte enthält, mit denen Anfänger beim Erlernen der Spieleentwicklung zu kämpfen haben. Dies basiert auf meiner Erfahrung aus meiner Zeit als Lehrassistent, als ich neuen Programmierern half, den Umgang mit Processing zu erlernen. Diese Konzepte umfassen Schwerkraft, Kollisionen, das Führen von Punkten, den Umgang mit verschiedenen Bildschirmen und Tastatur-/Maus-Interaktionen. Flappy Pong hat sie alle in sich.
Spiel jetzt spielen!
Ohne die Konzepte der objektorientierten Programmierung (OOP) zu verwenden, ist es nicht einfach, komplexe Spiele zu erstellen, wie z. B. Plattformspiele mit mehreren Ebenen, Spielern, Entitäten usw. Im weiteren Verlauf werden Sie sehen, wie der Code sehr schnell kompliziert wird. Ich habe mein Bestes getan, um dieses Processing-Tutorial organisiert und einfach zu halten.
Ich rate Ihnen, dem Artikel zu folgen, den vollständigen Code zu besorgen, selbst damit zu spielen, so schnell wie möglich über Ihr eigenes Spiel nachzudenken und mit der Implementierung zu beginnen.
Fangen wir also an.
Flappy Pong bauen
Verarbeitungs-Tutorial Schritt #1: Verschiedene Bildschirme initialisieren und handhaben
Der erste Schritt besteht darin, unser Projekt zu initialisieren. Für den Anfang werden wir unser Setup schreiben und wie gewohnt Blöcke zeichnen, nichts Besonderes oder Neues. Dann behandeln wir verschiedene Bildschirme (Startbildschirm, Spielbildschirm, Game Over Screen usw.). Es stellt sich also die Frage, wie wir die Verarbeitung dazu bringen, die richtige Seite zur richtigen Zeit anzuzeigen?
Die Erfüllung dieser Aufgabe ist ziemlich einfach. Wir werden eine globale Variable haben, die die Informationen des derzeit aktiven Bildschirms speichert. Wir zeichnen dann je nach Variable den Inhalt des richtigen Bildschirms. Im Zeichenblock haben wir eine if-Anweisung, die die Variable überprüft und den Inhalt des Bildschirms entsprechend anzeigt. Immer wenn wir den Bildschirm ändern möchten, ändern wir diese Variable in die Kennung des Bildschirms, den wir anzeigen möchten. Vor diesem Hintergrund sieht unser Skeleton-Code folgendermaßen aus:
/********* 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; }
Das mag auf den ersten Blick beängstigend aussehen, aber wir haben lediglich die Grundstruktur aufgebaut und verschiedene Teile mit Kommentarblöcken voneinander getrennt.
Wie Sie sehen können, definieren wir für jeden anzuzeigenden Bildschirm eine andere Methode. In unserem Draw-Block überprüfen wir einfach den Wert unserer gameScreen
Variablen und rufen die entsprechende Methode auf.
Im void mousePressed(){...}
-Teil hören wir auf Mausklicks und wenn der aktive Bildschirm 0 ist, der Anfangsbildschirm, rufen wir die startGame()
Methode auf, die das Spiel wie erwartet startet. Die erste Zeile dieser Methode ändert die gameScreen
Variable auf 1, den Spielbildschirm.
Wenn dies verstanden wurde, besteht der nächste Schritt darin, unseren Startbildschirm zu implementieren. Dazu bearbeiten wir die Methode initScreen()
. Hier kommt's:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
Jetzt hat unser Einstiegsbildschirm einen schwarzen Hintergrund und einen einfachen Text, „Zum Starten klicken“, der sich in der Mitte befindet und an der Mitte ausgerichtet ist. Aber wenn wir klicken, passiert nichts. Wir haben noch keine Inhalte für unseren Spielbildschirm festgelegt. Die Methode gameScreen()
enthält nichts, also decken wir nicht den vorherigen Inhalt ab, der vom letzten Bildschirm (dem Text) gezeichnet wurde, indem wir background()
als erste Zeichenzeile haben. Deshalb ist der Text immer noch da, obwohl die Zeile text()
nicht mehr aufgerufen wird (genau wie das Beispiel mit dem sich bewegenden Ball aus dem letzten Teil, der eine Spur hinterlassen hat) . Der Hintergrund ist aus dem gleichen Grund immer noch schwarz. Lassen Sie uns also weitermachen und mit der Implementierung des Spielbildschirms beginnen.
void gameScreen() { background(255); }
Nach dieser Änderung werden Sie feststellen, dass der Hintergrund weiß wird und der Text verschwindet.
Verarbeitungs-Tutorial Schritt Nr. 2: Erstellen des Balls und Implementieren der Schwerkraft
Jetzt beginnen wir mit der Arbeit am Spielbildschirm. Wir werden zuerst unseren Ball erstellen. Wir sollten Variablen für seine Koordinaten, Farbe und Größe definieren, da wir diese Werte später vielleicht ändern möchten. Zum Beispiel, wenn wir die Größe des Balls erhöhen wollen, wenn der Spieler mehr Punkte erzielt, damit das Spiel schwieriger wird. Wir müssen seine Größe ändern, also sollte es eine Variable sein. Wir werden auch die Geschwindigkeit des Balls definieren, nachdem wir die Schwerkraft implementiert haben.
Fügen wir zunächst Folgendes hinzu:
... 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); }
Wir haben die Koordinaten als globale Variablen definiert und eine Methode erstellt, die den Ball zeichnet, die von der gameScreen- Methode aufgerufen wird. Das Einzige, worauf Sie hier achten müssen, ist, dass wir Koordinaten initialisiert , aber wir haben sie in setup()
definiert. Der Grund dafür ist, dass wir wollten, dass der Ball ein Viertel von links und ein Fünftel von oben beginnt. Es gibt keinen besonderen Grund, warum wir das wollen, aber das ist ein guter Punkt, um den Ball zu starten. Also mussten wir die width
und height
der Skizze dynamisch erhalten. Die Skizzengröße wird in setup()
nach der ersten Zeile definiert. width
und height
werden nicht gesetzt, bevor setup()
ausgeführt wird, deshalb könnten wir dies nicht erreichen, wenn wir die Variablen oben definieren würden.
Schwere
Jetzt ist die Implementierung der Schwerkraft eigentlich der einfache Teil. Es gibt nur ein paar Tricks. Hier erstmal die Umsetzung:
... 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); } }
Und das Ergebnis ist:
Halt deine Pferde, Physiker. Ich weiß, dass die Schwerkraft im wirklichen Leben nicht so funktioniert. Stattdessen ist dies eher ein Animationsprozess als alles andere. Die Variable, die wir als gravity
definiert haben, ist nur ein numerischer Wert – ein float
, damit wir Dezimalwerte und nicht nur ganze Zahlen verwenden können –, die wir bei jeder Schleife zu ballSpeedVert
hinzufügen. Und ballSpeedVert
ist die vertikale Geschwindigkeit des Balls, die bei jeder Schleife zur Y-Koordinate des Balls ( ballY
) addiert wird. Wir beobachten die Koordinaten des Balls und stellen sicher, dass er auf dem Bildschirm bleibt. Wenn wir es nicht täten, würde der Ball ins Unendliche fallen. Im Moment bewegt sich unser Ball nur vertikal. Wir beobachten also die Boden- und Deckengrenzen des Bildschirms. Mit der Methode keepInScreen()
prüfen wir, ob ballY
( + der Radius) kleiner ist als height
, und ebenso ist ballY
( - der Radius) größer als 0
. Wenn die Bedingungen nicht erfüllt sind, lassen wir den Ball mit den Methoden makeBounceBottom()
und makeBounceTop()
(von unten oder oben) springen. Um den Ball abprallen zu lassen, bewegen wir den Ball einfach genau an die Stelle, an der er abprallen musste, und multiplizieren die vertikale Geschwindigkeit ( ballSpeedVert
) mit -1
(die Multiplikation mit -1 ändert das Vorzeichen). Wenn der Geschwindigkeitswert ein Minuszeichen hat, wird die Geschwindigkeit durch Addieren der Y-Koordinate zu ballY + (-ballSpeedVert)
, was ballY - ballSpeedVert
ist. Der Ball ändert also sofort seine Richtung mit der gleichen Geschwindigkeit. Wenn wir dann die gravity
zu ballSpeedVert
hinzufügen und ballSpeedVert
einen negativen Wert hat, beginnt es, sich 0
zu nähern, wird schließlich 0
und steigt wieder an. Dadurch steigt der Ball, steigt langsamer, stoppt und beginnt zu fallen.
Es gibt jedoch ein Problem mit unserem Animationsprozess – der Ball hüpft weiter. Wenn dies ein reales Szenario wäre, wäre der Ball jedes Mal Luftwiderstand und Reibung ausgesetzt gewesen, wenn er eine Oberfläche berührt hätte. Das ist das Verhalten, das wir für den Animationsprozess unseres Spiels wollen, daher ist die Implementierung einfach. Wir fügen Folgendes hinzu:
... 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); }
Und jetzt produziert unser Animationsprozess Folgendes:
Wie der Name schon sagt, ist friction
die Oberflächenreibung und airfriction
die Reibung der Luft. friction
muss also offensichtlich jedes Mal auftreten, wenn der Ball eine Oberfläche berührt. airfriction
muss jedoch ständig wirken. Das haben wir also getan. Die Methode applyGravity()
wird bei jeder Schleife ausgeführt, sodass wir bei jeder Schleife 0.0001
Prozent ihres aktuellen Werts von ballSpeedVert
. Die Methoden makeBounceBottom()
und makeBounceTop()
werden ausgeführt, wenn der Ball eine Oberfläche berührt. Also haben wir bei diesen Methoden dasselbe gemacht, nur diesmal mit friction
.
Verarbeitungs-Tutorial Schritt Nr. 3: Erstellen des Schlägers
Jetzt brauchen wir einen Schläger, auf dem der Ball abprallen kann. Wir sollten den Schläger kontrollieren. Machen wir es mit der Maus steuerbar. Hier ist der Code:
... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }
Wir haben die Farbe, Breite und Höhe des Schlägers als globale Variable definiert, wir möchten vielleicht, dass sie sich während des Spiels ändern. Wir haben eine Methode drawRacket()
implementiert, die tut, was ihr Name vermuten lässt. Wir setzen rectMode
auf center, sodass unser Schläger auf die Mitte unseres Cursors ausgerichtet ist.
Jetzt, da wir den Schläger erstellt haben, müssen wir den Ball darauf abprallen lassen.
... 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; } } } }
Und hier ist das Ergebnis:
Was watchRacketBounce()
also macht, ist sicherzustellen, dass der Schläger und der Ball kollidieren. Hier gibt es zwei Dinge zu überprüfen, nämlich ob Ball und Schläger sowohl vertikal als auch horizontal ausgerichtet sind. Die erste if-Anweisung prüft, ob die X-Koordinate der rechten Seite des Balls größer ist als die X-Koordinate der linken Seite des Schlägers (und umgekehrt). Wenn dies der Fall ist, prüft die zweite Anweisung, ob der Abstand zwischen dem Ball und dem Schläger kleiner oder gleich dem Radius des Balls ist (was bedeutet, dass sie kollidieren) . Wenn also diese Bedingungen erfüllt sind, wird die Methode makeBounceBottom()
aufgerufen und der Ball springt auf unseren Schläger (bei mouseY
, wo sich der Schläger befindet).
Ist Ihnen der variable overhead
aufgefallen, der von mouseY - pmouseY
berechnet wird? Die Variablen pmouseX
und pmouseY
speichern die Koordinaten der Maus im vorherigen Frame. Da sich die Maus sehr schnell bewegen kann, besteht eine gute Chance, dass wir den Abstand zwischen dem Ball und dem Schläger zwischen den Frames nicht richtig erkennen, wenn sich die Maus schnell genug auf den Ball zubewegt. Wir nehmen also die Differenz der Mauskoordinaten zwischen den Frames und berücksichtigen dies bei der Entfernungserkennung. Je schneller sich die Maus bewegt, desto größer ist der Abstand.
Wir verwenden overhead
auch aus einem anderen Grund. Wir erkennen, in welche Richtung sich die Maus bewegt, indem wir das Zeichen für overhead
überprüfen. Wenn Overhead negativ ist, war die Maus irgendwo unten im vorherigen Frame, also bewegt sich unsere Maus (Schläger) nach oben. In diesem Fall möchten wir dem Ball eine zusätzliche Geschwindigkeit hinzufügen und ihn etwas weiter als beim normalen Aufprall bewegen, um den Effekt zu simulieren, wenn der Ball mit dem Schläger geschlagen wird. Wenn der overhead
kleiner als 0
ist, fügen wir ihn zu ballY
und ballSpeedVert
, damit der Ball höher und schneller fliegt. Je schneller also der Schläger den Ball trifft, desto höher und schneller bewegt er sich nach oben.
Processing Tutorial Step #4: Horizontale Bewegung & Kontrolle des Balls
In diesem Abschnitt werden wir dem Ball eine horizontale Bewegung hinzufügen. Dann werden wir es ermöglichen, den Ball mit unserem Schläger horizontal zu kontrollieren. Auf geht's:
... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }
Und das Ergebnis ist:

Die Idee hier ist die gleiche wie bei der vertikalen Bewegung. Wir haben eine horizontale Geschwindigkeitsvariable ballSpeedHorizon
. Wir haben eine Methode entwickelt, um horizontale Geschwindigkeit auf ballX
und die Luftreibung zu beseitigen. Wir haben der Methode keepInScreen()
zwei weitere if-Anweisungen hinzugefügt, die darauf achten, dass der Ball den linken und rechten Rand des Bildschirms trifft. Schließlich haben wir die makeBounceLeft()
und makeBounceRight()
erstellt, um die Bounces von links und rechts zu verarbeiten.
Jetzt, da wir dem Spiel horizontale Geschwindigkeit hinzugefügt haben, wollen wir den Ball mit dem Schläger kontrollieren. Wie im berühmten Atari-Spiel Breakout und in allen anderen Brick-Breaking-Spielen sollte der Ball je nach Punkt auf dem Schläger, den er trifft, nach links oder rechts fliegen. Die Ränder des Schlägers sollten dem Ball mehr horizontale Geschwindigkeit verleihen, während die Mitte keinen Einfluss haben sollte. Zuerst codieren:
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; ... } } }
Das Ergebnis ist:
Das Hinzufügen dieser einfachen Zeile zu watchRacketBounce()
hat den Job gemacht. Was wir getan haben, war, den Abstand des Punktes, den der Ball trifft, von der Mitte des Schlägers mit ballX - mouseX
zu bestimmen. Dann machen wir es zur horizontalen Geschwindigkeit. Der tatsächliche Unterschied war zu groß, also habe ich es ein paar Mal versucht und festgestellt, dass sich ein Zehntel des Werts am natürlichsten anfühlt.
Processing Tutorial Step #5: Erstellen der Wände
Unsere Skizze sieht mit jedem Schritt mehr wie ein Spiel aus. In diesem Schritt fügen wir Wände hinzu, die sich nach links bewegen, genau wie 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); } }
Und daraus resultierte:
Auch wenn der Code lang und einschüchternd aussieht, verspreche ich, dass nichts schwer zu verstehen ist. Das erste, was auffällt, ist ArrayList
. Für diejenigen unter Ihnen, die nicht wissen, was eine ArrayList
ist, es ist nur eine Implementierung einer Liste, die sich wie ein Array verhält, aber einige Vorteile gegenüber ihr hat. Es ist in der Größe veränderbar und hat nützliche Methoden wie list.add(index)
, list.get(index)
und list.remove(index)
. Wir halten die Wanddaten als Integer-Arrays innerhalb der Arraylist. Die Daten, die wir in den Arrays speichern, beziehen sich auf die Lücke zwischen zwei Wänden. Die Arrays enthalten die folgenden Werte:
[gap wall X, gap wall Y, gap wall width, gap wall height]
Die tatsächlichen Wände werden basierend auf den Spaltwandwerten gezeichnet. Beachten Sie, dass all dies mit Klassen besser und sauberer gehandhabt werden könnte, aber da die Verwendung von objektorientierter Programmierung (OOP) nicht im Rahmen dieses Processing-Tutorials liegt, werden wir es so handhaben. Wir haben zwei Basismethoden, um die Wände zu verwalten, wallAdder()
und wallHandler
.
Die Methode wallAdder()
fügt der Arrayliste einfach in jeder wallInterval
Millisekunde neue Wände hinzu. Wir haben eine globale Variable lastAddTime
, die die Zeit speichert, zu der die letzte Wand hinzugefügt wurde (in Millisekunden) . Wenn die aktuelle Millisekunde millis()
minus der zuletzt hinzugefügten Millisekunde lastAddTime
größer ist als unser Intervallwert wallInterval
, bedeutet dies, dass es jetzt an der Zeit ist, eine neue Wand hinzuzufügen. Basierend auf den ganz oben definierten globalen Variablen werden dann zufällige Lückenvariablen generiert. Dann wird eine neue Wand (Ganzzahl-Array, das die Lückenwanddaten speichert) zur Arrayliste hinzugefügt und lastAddTime
wird auf die aktuelle Millisekunde millis()
gesetzt.
wallHandler()
durchläuft die aktuellen Wände, die sich in der Arrayliste befinden. Und für jedes Element in jeder Schleife ruft es wallRemover(i)
, wallMover(i)
und wallDrawer(i)
durch den Indexwert der Arrayliste auf. Diese Methoden tun, was ihr Name vermuten lässt. wallDrawer()
zeichnet die tatsächlichen Wände basierend auf den Lückenwanddaten. Es greift das Wanddatenarray aus der Arrayliste und ruft die Methode rect()
auf, um die Wände dorthin zu zeichnen, wo sie eigentlich sein sollten. Die Methode wallMover()
greift das Element aus der Arrayliste und ändert seine X-Position basierend auf der globalen Variablen wallSpeed
. Schließlich entfernt wallRemover()
die Wände aus der Arrayliste, die sich außerhalb des Bildschirms befinden. Wenn wir das nicht getan hätten, hätte Processing sie so behandelt, als wären sie noch auf dem Bildschirm. Und das wäre ein enormer Leistungsverlust gewesen. Wenn also eine Wand aus der Arrayliste entfernt wird, wird sie in nachfolgenden Schleifen nicht gezeichnet.
Die letzte Herausforderung besteht darin, Kollisionen zwischen dem Ball und den Wänden zu erkennen.
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 } }
Die Methode watchWallCollision()
wird für jede Wand in jeder Schleife aufgerufen. Wir erfassen die Koordinaten der Spaltwand, berechnen die Koordinaten der tatsächlichen Wände (oben und unten) und prüfen, ob die Koordinaten des Balls mit den Wänden kollidieren.
Processing Tutorial Step #6: Health and Score
Jetzt, da wir die Kollisionen des Balls und der Wände erkennen können, können wir über die Spielmechanik entscheiden. Nach einigem Tuning des Spiels gelang es mir, das Spiel einigermaßen spielbar zu machen. Aber trotzdem war es sehr schwer. Mein erster Gedanke an das Spiel war, es wie Flappy Bird zu machen, wenn der Ball die Wände berührt, endet das Spiel. Aber dann wurde mir klar, dass es unmöglich wäre zu spielen. Also hier ist, was ich dachte:
Auf dem Ball sollte sich ein Gesundheitsbalken befinden. Der Ball sollte Gesundheit verlieren, während er die Wände berührt. Mit dieser Logik macht es keinen Sinn, den Ball von den Wänden zurückprallen zu lassen. Wenn die Gesundheit also 0 ist, sollte das Spiel enden und wir sollten zum Game Over-Bildschirm wechseln. Auf geht's:
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(); } }
Und hier ist ein einfacher Lauf:
Wir haben eine globale variable health
erstellt, um die Gesundheit des Balls zu erhalten. Und dann eine Methode drawHealthBar()
erstellt, die zwei Rechtecke auf den Ball zeichnet. Die erste ist die Basis-Gesundheitsleiste, die andere ist die aktive, die die aktuelle Gesundheit anzeigt. Die Breite der zweiten ist dynamisch und wird mit healthBarWidth*(health/maxHealth)
, dem Verhältnis unserer aktuellen Gesundheit in Bezug auf die Breite der Gesundheitsleiste. Schließlich werden die Füllfarben entsprechend dem Gesundheitswert festgelegt. Zu guter Letzt Punkte :
... 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); }
Wir mussten ein Tor erzielen, wenn der Ball eine Wand passiert. Aber wir müssen maximal 1 Punktzahl pro Wand hinzufügen. Das heißt, wenn der Ball eine Wand passiert, dann zurückgeht und sie erneut passiert, sollte keine weitere Punktzahl hinzugefügt werden. Um dies zu erreichen, haben wir dem Gap-Wall-Array innerhalb der Arraylist eine weitere Variable hinzugefügt. Die neue Variable speichert 0
, wenn der Ball diese Wand noch nicht passiert hat, und 1
, wenn dies der Fall ist. Dann haben wir die Methode watchWallCollision()
modifiziert. Wir haben eine Bedingung hinzugefügt, die die Methode score()
auslöst und die Wand als passiert markiert, wenn der Ball eine Wand passiert, die er zuvor noch nicht passiert hat.
Wir sind jetzt sehr nah am Ende. Das letzte, was zu tun ist, ist das Implementieren click to restart
auf dem Game Over-Bildschirm. Wir müssen alle Variablen, die wir verwendet haben, auf ihren Anfangswert setzen und das Spiel neu starten. Hier ist es:
... 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; }
Lassen Sie uns weitere Farben hinzufügen.
Voila! Wir haben Flappy Pong!
Den vollständigen Processing-Spielcode finden Sie hier.
Portieren des Verarbeitungsspielcodes in das Web mit p5.js
p5.js ist eine JavaScript-Bibliothek mit einer sehr ähnlichen Syntax wie die der Programmiersprache Processing. Es ist keine Bibliothek, die in der Lage ist, einfach vorhandenen Processing-Code auszuführen; Stattdessen erfordert p5.js das Schreiben von echtem JavaScript-Code – ähnlich dem JavaScript-Port von Processing, der als Processing.js bekannt ist. Unsere Aufgabe besteht darin, Processing-Code mithilfe der p5.js-API in JavaScript umzuwandeln. Die Bibliothek hat eine Reihe von Funktionen und eine ähnliche Syntax wie Processing, und wir müssen bestimmte Änderungen an unserem Code vornehmen, damit sie in JavaScript funktionieren – aber da sowohl Processing als auch JavaScript Ähnlichkeiten mit Java aufweisen, ist es weniger ein Sprung, als es klingt . Auch wenn Sie kein JavaScript-Entwickler sind, sind die Änderungen sehr trivial und Sie sollten problemlos folgen können.
Zuerst müssen wir eine einfache index.html
erstellen und p5.min.js
zu unserem Header hinzufügen. Wir müssen auch eine weitere Datei namens flappy_pong.js
erstellen, die unseren konvertierten Code enthält.
<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>
Unsere Strategie beim Konvertieren des Codes sollte darin bestehen, unseren gesamten Code zu kopieren und in flappy_pong.js
und dann alle Änderungen vorzunehmen. Und das habe ich getan. Und hier sind die Schritte, die ich unternommen habe, um den Code zu aktualisieren:
Javascript ist eine untypisierte Sprache (es gibt keine Typdeklarationen wie
int
undfloat
). Also müssen wir alle Typdeklarationen invar
ändern.In Javascript gibt es keine
void
. Wir sollten alles auffunction
.Wir müssen die Typdeklarationen von Argumenten aus Funktionssignaturen entfernen. (dh
void wallMover(var index) {
tofunction wallMover(index) {
)Es gibt keine
ArrayList
in JavaScript. Aber wir können dasselbe mit JavaScript-Arrays erreichen. Wir nehmen folgende Änderungen vor:-
ArrayList<int[]> walls = new ArrayList<int[]>();
zuvar walls = [];
-
walls.clear();
zuwalls = [];
-
walls.add(randWall);
zu denwalls.push(randWall);
-
walls.remove(index);
zu denwalls.splice(index,1);
-
walls.get(index);
zuwalls[index]
-
walls.size()
zuwalls.length
-
Deklaration des Arrays
var randWall = {width, randY, wallWidth, randHeight, 0};
zuvar randWall = [width, randY, wallWidth, randHeight, 0];
Entfernen Sie alle
public
Schlüsselwörter.Verschieben Sie alle
color(0)
-Deklarationen in diefunction setup()
, dacolor()
nicht vor dem Aufruf vonsetup()
definiert wird.size(500, 500);
createCanvas(500, 500);
Benennen Sie die
function gameScreen(){
in etwas anderes wiefunction gamePlayScreen(){
um, da wir bereits eine globale Variable namensgameScreen
haben. Als wir mit Processing gearbeitet haben, war das eine eine Funktion und das andere eineint
-Variable. Aber JavaScript verwirrt diese, da sie untypisiert sind.Dasselbe gilt für
score()
. Ich habe es inaddScore()
umbenannt.
Den vollständigen JavaScript-Code, der alles in diesem Processing-Tutorial abdeckt, finden Sie hier.
Spielcode verarbeiten: Das können Sie auch
In diesem Processing-Tutorial habe ich versucht zu erklären, wie man ein sehr einfaches Spiel erstellt. Was wir in diesem Artikel getan haben, ist jedoch nur die Spitze des Eisbergs. Mit der Programmiersprache Processing kann fast alles erreicht werden. Meiner Meinung nach ist es das beste Werkzeug, um zu programmieren, was man sich vorstellt. Meine eigentliche Absicht mit diesem Processing-Tutorial war eher, als Processing zu lehren und ein Spiel zu bauen, um zu beweisen, dass Programmieren nicht so schwer ist. Das Erstellen Ihres eigenen Spiels ist nicht nur ein Traum. Ich wollte Ihnen zeigen, dass Sie es mit ein wenig Anstrengung und Enthusiasmus schaffen können. Ich hoffe wirklich, dass diese beiden Artikel jeden dazu inspirieren, Programmieren eine Chance zu geben.