In der vorherigen Folge der Serie haben wir einen Kollisionserkennungsmechanismus zwischen den Spielobjekten implementiert. In diesem Teil verwenden wir den Kollisionserkennungsmechanismus, um eine einfache, aber robuste physische Antwort zwischen den Objekten aufzubauen.
Die Demo zeigt das Endergebnis dieses Tutorials. Verwenden Sie WASD, um den Charakter zu verschieben. Die mittlere Maustaste erzeugt eine unidirektionale Plattform, die rechte Maustaste eine durchgehende Kachel und die Leertaste erzeugt einen Charakterklon. Die Schieberegler ändern die Größe des Charakters des Spielers.
Die Demo wurde unter Unity 5.4.0f3 veröffentlicht, und der Quellcode ist auch mit dieser Unity-Version kompatibel.
Da wir nun alle Kollisionsdaten aus der Arbeit haben, die wir im vorigen Teil ausgeführt haben, können wir eine einfache Antwort auf kollidierende Objekte hinzufügen. Unser Ziel ist es, dass sich die Objekte nicht wie auf einer anderen Ebene durcheinander gehen lassen. Wir möchten, dass sie fest sind und als Hindernis oder Plattform für andere Objekte fungieren. Dazu müssen wir nur eines tun: Bewegen Sie das Objekt aus einer Überlappung, wenn eine auftritt.
Wir benötigen zusätzliche Daten für die MovingObject
Klasse, um das Objekt im Vergleich zur Objektantwort zu behandeln. Zunächst einmal ist es schön, einen Boolean zu haben, um ein Objekt als kinematisch zu markieren. Das heißt, dieses Objekt wird von keinem anderen Objekt herumgeschoben.
Diese Objekte funktionieren gut als Plattformen und können auch Plattformen verschieben. Es wird angenommen, dass es sich um die schwersten Dinge handelt, sodass ihre Position in keiner Weise korrigiert wird - andere Objekte müssen sich entfernen, um Platz für sie zu schaffen.
public bool mIsKinematic = false;
Die anderen Daten, die ich gerne habe, sind Informationen darüber, ob wir auf einem Objekt oder links oder rechts davon stehen usw. Bisher konnten wir nur mit Kacheln interagieren, aber jetzt können wir auch mit anderen Objekten interagieren.
Um etwas Harmonie zu schaffen, brauchen wir einen neuen Satz von Variablen, der beschreibt, ob der Charakter etwas links, rechts, oben oder unten drückt.
public bool mPushesRight = false; public bool mPushesLeft = false; public bool mPushesBottom = false; public bool mPushesTop = false; public bool mPushedTop = false; public bool mPushedBottom = false; public bool mPushedRight = false; public bool mPushedLeft = false; public bool mPushesLeftObject = false; public bool mPushesRightObject = false; public bool mPushesBottomObject = false; public bool mPushesTopObject = false; public bool mPushedLeftObject = false; public bool mPushedRightObject = false; public bool mPushedBottomObject = false; public bool mPushedTopObject = false; public bool mPushesRightTile = false; public bool mPushesLeftTile = false; public bool mPushesBottomTile = false; public bool mPushesTopTile = false; public bool mPushedTopTile = false; public bool mPushedBottomTile = false; public bool mPushedRightTile = false; public bool mPushedLeftTile = false;
Nun, das sind viele Variablen. In einer Produktionsumgebung lohnt es sich, diese in Flags umzuwandeln und nur eine ganze Zahl anstelle all dieser Booleschen zu haben, aber der Einfachheit halber behandeln wir sie, so wie sie sind.
Wie Sie vielleicht bemerken, haben wir hier sehr genaue Daten. Wir wissen, ob der Charakter im Allgemeinen ein Hindernis in eine bestimmte Richtung schiebt oder schiebt, wir können aber auch leicht nachfragen, ob wir uns neben einem Stein oder einem Objekt befinden.
Lass uns die erstellen UpdatePhysicsResponse
Funktion, in der wir das Objekt vs. Objektantwort behandeln.
privat void UpdatePhysicsResponse ()
Wenn das Objekt als kinematisch markiert ist, kehren wir einfach zurück. Wir behandeln die Antwort nicht, da das kinematische Objekt nicht auf ein anderes Objekt reagieren muss - die anderen Objekte müssen darauf reagieren.
if (mIsKinematic) return;
Nun wird davon ausgegangen, dass wir kein kinematisches Objekt benötigen, um die richtigen Daten zu haben, z. B. ob es ein Objekt auf die linke Seite schiebt usw. Wenn dies nicht der Fall ist, muss dies ein wenig geändert werden, was ich Ich werde später in der Zeile weiterfahren.
Beginnen wir nun mit den Variablen, die wir kürzlich deklariert haben.
mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;
Wir speichern die Ergebnisse des vorherigen Frames in den entsprechenden Variablen und gehen davon aus, dass wir kein anderes Objekt berühren.
Beginnen wir jetzt damit, alle unsere Kollisionsdaten zu durchlaufen.
für (int i = 0; i < mAllCollidingObjects.Count; ++i) var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap;
Lassen Sie uns zunächst die Fälle behandeln, in denen sich die Objekte kaum berühren und nicht wirklich überlappen. In diesem Fall wissen wir, dass wir nichts wirklich verschieben müssen, setzen Sie einfach die Variablen.
Wie bereits erwähnt, ist die Anzeige, dass sich die Objekte berühren, dass die Überlappung einer der Achsen gleich 0 ist. Beginnen wir mit der Überprüfung der x-Achse.
if (Überlappung.x == 0.0f)
Wenn die Bedingung erfüllt ist, müssen wir prüfen, ob sich das andere Objekt auf der linken oder rechten Seite unserer AABB befindet.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) else
Schließlich, wenn es auf der rechten Seite ist, setzen Sie die mPushesRightObject
auf true und die Geschwindigkeit so einstellen, dass sie nicht größer als 0 ist, da sich unser Objekt nicht mehr nach rechts bewegen kann, da der Pfad blockiert ist.
if (Überlappung.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else
Lass uns mit der linken Seite genauso umgehen.
if (Überlappung.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);
Schließlich wissen wir, dass wir hier nichts weiter tun müssen, also fahren wir mit der nächsten Schleifeniteration fort.
if (Überlappung.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); fortsetzen;
Lassen Sie uns die y-Achse auf dieselbe Weise behandeln.
if (Überlappung.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); fortsetzen; else if (overlap.y == 0.0f) if (other.mAABB.center.y> mAABB.center.y) mPushesTopObject = true; mSpeed.y = Mathf.Min (mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max (mSpeed.y, 0.0f); fortsetzen;
Dies ist auch ein guter Ort, um die Variablen für einen kinematischen Körper festzulegen, falls dies erforderlich ist. Es wäre uns egal, ob die Überlappung gleich Null ist oder nicht, weil wir sowieso keine kinematischen Objekte bewegen werden. Wir müssen auch die Geschwindigkeitsanpassung überspringen, da wir ein kinematisches Objekt nicht stoppen möchten. Wir werden das alles für die Demo überspringen, da wir die Hilfsvariablen nicht für kinematische Objekte verwenden werden.
Nun, da dies abgedeckt ist, können wir die Objekte behandeln, die sich ordnungsgemäß mit unserer AABB überlappen. Bevor wir das tun, möchte ich jedoch den Ansatz erläutern, den ich in der Demo zur Reaktion auf Kollisionen gewählt habe.
Wenn sich das Objekt nicht bewegt und wir darauf stoßen, sollte das andere Objekt nicht bewegt werden. Wir behandeln es als kinematischen Körper. Ich habe mich für diesen Weg entschieden, weil ich der Meinung bin, dass er generischer ist und das Pushing-Verhalten in der benutzerdefinierten Aktualisierung eines bestimmten Objekts immer weiter unten behandelt werden kann.
Wenn sich beide Objekte während der Kollision bewegten, teilen wir die Überlappung zwischen ihnen basierend auf ihrer Geschwindigkeit auf. Je schneller sie sind, desto größer wird der Überlappungswert.
Der letzte Punkt ist, ähnlich wie bei der Tilemap-Antwortmethode: Wenn ein Objekt herunterfällt und beim Abwärtsgehen ein anderes Objekt horizontal um ein Pixel zerkratzt, rutscht das Objekt nicht ab und fällt weiter, sondern steht auf diesem einen Pixel.
Ich denke, dies ist der formbarste Ansatz, und es sollte nicht sehr schwer sein, ihn zu ändern, wenn Sie mit der Reaktion anders umgehen wollen.
Lassen Sie uns die Implementierung fortsetzen, indem Sie den absoluten Geschwindigkeitsvektor für beide Objekte während der Kollision berechnen. Wir benötigen auch die Summe der Geschwindigkeiten, damit wir wissen, um wie viel Prozent der Überlappung unser Objekt verschoben werden soll.
Vector2 absSpeed1 = new Vector2 (Mathf.Abs (data.pos1.x - data.oldPos1.x), Mathf.Abs (data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = new Vector2 (Mathf.Abs (data.pos2.x - data.oldPos2.x), Mathf.Abs (data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;
Beachten Sie, dass anstelle der in den Kollisionsdaten gespeicherten Geschwindigkeit der Versatz zwischen der Position zum Zeitpunkt der Kollision und dem vorherigen Frame verwendet wird. In diesem Fall ist dies nur genauer, da die Geschwindigkeit den Bewegungsvektor vor der physischen Korrektur darstellt. Die Positionen selbst werden korrigiert, wenn das Objekt beispielsweise eine durchgehende Kachel getroffen hat. Wenn Sie also einen korrigierten Geschwindigkeitsvektor erhalten möchten, sollten Sie ihn so berechnen.
Beginnen wir nun mit der Berechnung des Drehzahlverhältnisses für unser Objekt. Wenn das andere Objekt kinematisch ist, setzen wir das Geschwindigkeitsverhältnis auf Eins, um sicherzustellen, dass der gesamte Überlappungsvektor verschoben wird. Dabei wird die Regel beachtet, dass das kinematische Objekt nicht verschoben werden soll.
Float SpeedRatioX, SpeedRatioY; if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; sonstiges
Beginnen wir nun mit einem ungeraden Fall, bei dem sich beide Objekte überlappen, aber beide keine Geschwindigkeit haben. Das sollte eigentlich nicht passieren, aber wenn ein Objekt erzeugt wird, das ein anderes Objekt überlappt, möchten wir, dass sich die Objekte auf natürliche Weise auseinander bewegen. In diesem Fall möchten wir, dass beide sich um 50% des Überlappungsvektors bewegen.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0,0f && speedSum.y == 0,0f) speedRatioX = speedRatioY = 0,5f;
Ein anderer Fall ist, wenn die speedSum
auf der x-Achse ist gleich Null. In diesem Fall berechnen wir das richtige Verhältnis für die Y-Achse und legen fest, dass wir 50% der Überlappung für die X-Achse verschieben sollten.
if (speedSum.x == 0,0f && speedSum.y == 0,0f) speedRatioX = speedRatioY = 0,5f; else if (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y;
Ebenso behandeln wir den Fall, in dem die speedSum
ist nur auf der y-Achse Null, und für den letzten Fall berechnen wir beide Verhältnisse richtig.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0,0f && speedSum.y == 0,0f) speedRatioX = speedRatioY = 0,5f; else if (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; else if (speedSum.y == 0.0f) speedRatioX = absSpeed1.x / speedSum.x; Geschwindigkeitsverhältnis = 0,5f; else speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y;
Nun, da die Verhältnisse berechnet sind, können wir sehen, wie viel wir zum Versetzen unseres Objekts benötigen.
Float OffsetX = Überlappung.x * speedRatioX; Float OffsetY = Überlappung.y * speedRatioY;
Bevor wir nun entscheiden, ob wir das Objekt auf der X-Achse oder der Y-Achse aus dem Zusammenstoß bewegen sollen, berechnen wir die Richtung, aus der die Überlappung stattgefunden hat. Es gibt drei Möglichkeiten: Entweder stießen wir horizontal, vertikal oder diagonal auf ein anderes Objekt.
Im ersten Fall möchten wir uns aus der Überlappung auf der x-Achse herausbewegen, im zweiten Fall wollen wir uns aus der Überlappung auf der y-Achse herausbewegen, und im letzten Fall wollen wir die Überlappung auf der einen oder anderen Seite verlassen Achse hatte die geringste Überlappung.
Um sich mit einem anderen Objekt zu überlappen, müssen sich die AABBs sowohl auf der x- als auch auf der y-Achse überlappen. Um zu prüfen, ob wir horizontal auf ein Objekt gestoßen sind, werden wir sehen, ob der vorherige Frame, den wir bereits auf der y-Achse überlappten. Wenn dies der Fall ist und wir uns nicht auf der x-Achse überlappen, muss die Überlappung aufgetreten sein, da sich im aktuellen Frame die AABBs auf der x-Achse zu überlappen begannen und wir daher abziehen, dass wir horizontal auf ein anderes Objekt gestoßen sind.
Lassen Sie uns zunächst berechnen, ob wir uns im vorherigen Frame mit der anderen AABB überschnitten haben.
bool overlappedLastFrameX = Mathf.Abs (data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;
Lassen Sie uns nun die Bedingung einrichten, um die Überlappung horizontal zu verschieben. Wie zuvor erklärt, mussten wir uns auf der y-Achse und nicht auf der x-Achse im vorherigen Frame überlappen.
if (! overlappedLastFrameX && overlappedLastFrameY)
Wenn dies nicht der Fall ist, werden wir uns auf der y-Achse aus der Überlappung entfernen.
if (! overlappedLastFrameX && overlappedLastFrameY) else
Wie oben erwähnt, müssen wir auch das Szenario des diagonalen Stoßens mit dem Objekt abdecken. Wir sind diagonal auf das Objekt gestoßen, wenn sich unsere AABBs im vorherigen Frame auf keiner der Achsen überlappen, weil wir wissen, dass sie sich im aktuellen Frame auf beiden Achsen überlappen, sodass die Kollision auf beiden Achsen gleichzeitig aufgetreten sein muss.
if ((! überlappteLastFrameX && überlappteLastFrameY) || (! überlappendeLastFrameX && überlappendeLastFrameY)) else
Wir wollen uns jedoch aus der Überlappung der Achse im Falle eines diagonalen Höckers nur dann entfernen, wenn die Überlappung auf der X-Achse kleiner ist als die Überlappung auf der Y-Achse.
if ((! überlappteLastFrameX && überlappteLastFrameY) || (! überlappteLastFrameX && überlappendeLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) else
Das sind alle Fälle gelöst. Jetzt müssen wir das Objekt tatsächlich aus der Überlappung herausbewegen.
if ((! überlappteLastFrameX && überlappteLastFrameY) || (! überlappteLastFrameX && überlappendeLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else
Wie Sie sehen, behandeln wir es sehr ähnlich wie im Fall, dass wir gerade eine andere AABB gerade berühren, aber zusätzlich bewegen wir unser Objekt um den berechneten Versatz.
Die vertikale Korrektur wird auf dieselbe Weise durchgeführt.
if ((! überlappteLastFrameX && überlappteLastFrameY) || (! überlappteLastFrameX &&! überlappendeLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else mPosition.y += offsetY; if (overlap.y < 0.0f) mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
Das ist fast schon vorbei. Es gibt nur noch eine Einschränkung. Stellen Sie sich das Szenario vor, in dem wir gleichzeitig auf zwei Objekten landen. Wir haben zwei nahezu identische Kollisionsdateninstanzen. Wenn wir alle Kollisionen durchlaufen, korrigieren wir die Position der Kollision mit dem ersten Objekt und bewegen uns ein wenig nach oben.
Dann behandeln wir die Kollision für das zweite Objekt. Die gespeicherte Überlappung zum Zeitpunkt der Kollision ist nicht mehr auf dem neuesten Stand, da wir uns bereits von der ursprünglichen Position entfernt haben. Wenn wir die zweite Kollision genauso behandeln würden wie die erste Kollision, würden wir uns wieder etwas verbessern Dadurch wird unser Objekt um die doppelte Entfernung korrigiert, die es sollte.
Um dieses Problem zu beheben, überwachen wir, wie sehr wir das Objekt bereits korrigiert haben. Lassen Sie uns den Vektor erklären offsetSum
kurz bevor wir alle Kollisionen durchlaufen.
Vector2 offsetSum = Vector2.zero;
Lassen Sie uns nun sicherstellen, dass Sie alle Offsets addieren, die wir in diesem Vektor auf unser Objekt angewendet haben.
if ((! überlappteLastFrameX && überlappteLastFrameY) || (! überlappteLastFrameX &&! überlappendeLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f) mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
Und schließlich lassen wir die Überlappung der aufeinanderfolgenden Kollisionen durch den kumulativen Korrekturvektor, den wir bisher gemacht haben, ausgleichen.
var Überlappung = data.overlap - offsetSum;
Wenn wir jetzt auf zwei Objekten derselben Höhe gleichzeitig landen, wird die erste Kollision ordnungsgemäß verarbeitet, und die Überlappung der zweiten Kollision würde zu Null versetzt werden, wodurch das Objekt nicht mehr verschoben wird.
Nun, da unsere Funktion fertig ist, stellen wir sicher, dass wir sie verwenden. Ein guter Ort, um diese Funktion aufzurufen, wäre nach dem CheckCollisions
Anruf. Dies erfordert, dass wir unsere spalten UpdatePhysics
Funktion in zwei Teile, so erstellen wir jetzt den zweiten Teil in der MovingObject
Klasse.
public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject;
Im zweiten Teil nennen wir unsere frisch fertig UpdatePhysicsResponse
funktionieren und aktualisieren Sie die allgemeinen Variablen left, right, bottom und top. Danach müssen wir nur noch die Position übernehmen.
public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; // Aktualisiere die aabb mAABB.center = mPosition; // die Änderungen auf die Transformation anwenden transform.position = new Vector3 (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), mSpriteDepth); transform.localScale = new Vector3 (ScaleX, ScaleY, 1.0f);
Lassen Sie uns nun in der Hauptspiel-Update-Schleife den zweiten Teil des Physics-Updates nach dem aufrufen CheckCollisions
Anruf.
void FixedUpdate () für (int i = 0; i < mObjects.Count; ++i) switch (mObjects[i].mType) case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break; mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2();
Erledigt! Jetzt können sich unsere Objekte nicht überlappen. Natürlich müssen in einer Spielumgebung einige Dinge wie Kollisionsgruppen usw. hinzugefügt werden. Daher ist es nicht zwingend erforderlich, mit jedem Objekt eine Kollision zu erkennen oder darauf zu reagieren. Dies hängt jedoch davon ab, wie Sie möchten haben Dinge in Ihrem Spiel eingerichtet, damit wir uns nicht näher darauf einlassen.
Das war es für einen anderen Teil der einfachen 2D-Plattform-Physik-Serie. Wir haben den Kollisionserkennungsmechanismus aus dem vorherigen Teil genutzt, um eine einfache physische Reaktion zwischen Objekten zu erzeugen.
Mit diesen Tools ist es möglich, Standardobjekte zu erstellen, z. B. sich bewegende Plattformen, Schieben von Blöcken, benutzerdefinierte Hindernisse und viele andere Arten von Objekten, die nicht wirklich Teil der Tilemap sein können, aber dennoch Teil des ebenen Terrains sein müssen. Es gibt noch ein beliebtes Feature, das unserer Physikimplementierung noch fehlt, und das sind die Steigungen.
Wir hoffen, dass wir im nächsten Teil beginnen werden, unsere Tilemap mit der Unterstützung für diese zu erweitern. Dies würde die grundlegenden Funktionen ergänzen, die eine einfache physische Implementierung für einen 2D-Plattformer haben sollte, und damit wäre die Serie beendet.
Natürlich gibt es immer Raum für Verbesserungen. Wenn Sie also eine Frage oder einen Tipp haben, wie Sie etwas Besseres tun können oder einfach nur eine Meinung zum Tutorial haben, können Sie mich gerne im Kommentarbereich informieren!