German (Deutsch) translation by Valentina (you can also view the original English article)

Demo
Die Demo zeigt das Endergebnis der Steigungsimplementierung. Verwenden Sie WASD, um den Charakter zu verschieben. Mit der rechten Maustaste wird eine Kachel erstellt. Sie können das Scrollrad oder die Pfeiltasten verwenden, um eine Kachel auszuwählen, die Sie platzieren möchten. Die Schieberegler ändern die Größe des Charakters des Spielers.
Die Demo wurde unter Unity 5.5.2f1 veröffentlicht und der Quellcode ist auch mit dieser Unity-Version kompatibel.
Steigungen
Steigungen erhöhen die Vielseitigkeit eines Spiels, sowohl hinsichtlich möglicher Interaktionen mit dem Gelände des Spiels als auch hinsichtlich der visuellen Varianz. Die Implementierung kann jedoch sehr komplex sein, insbesondere wenn wir eine große Anzahl von Hangsorten unterstützen möchten.
Wie bei den vorherigen Teilen der Serie, werden wir unsere Arbeit von dem Moment an fortsetzen, an dem wir den letzten Teil aufgegeben haben, auch wenn wir einen großen Teil des Codes, den wir bereits geschrieben haben, überarbeiten. Was wir von Anfang an brauchen, ist ein beweglicher Charakter und eine Tilemap.
Sie können die Projektdateien aus dem vorherigen Teil herunterladen und den Code zusammen mit diesem Lernprogramm schreiben.
Änderungen in der Bewegungsintegration
Da es schwierig ist, die Steigung zu arbeiten, ist es schön, wenn wir die Dinge in einigen Aspekten einfacher machen könnten. Vor einiger Zeit bin ich auf einen Blogbeitrag gestoßen, wie Matt Thorson in seinen Spielen mit der Physik umgeht. Grundsätzlich erfolgt bei dieser Methode die Bewegung immer in 1px-Intervallen. Wenn eine Bewegung für ein bestimmtes Bild größer als ein Pixel ist, wird der Bewegungsvektor in viele 1px-Bewegungen aufgeteilt, und nach jeder Bewegung werden die Bedingungen für eine Kollision mit dem Gelände geprüft.
Das erspart uns die Kopfschmerzen, wenn wir versuchen, Hindernisse entlang der Bewegungslinie auf einmal zu finden. Stattdessen können wir es iterativ tun. Dies macht die Implementierung einfacher, erhöht aber leider auch die Anzahl der durchgeführten Kollisionsprüfungen, so dass dies für Spiele mit vielen sich bewegenden Objekten ungeeignet sein kann, insbesondere bei Spielen mit hoher Auflösung, bei denen die Geschwindigkeit, mit der sich die Objekte bewegen, natürlich höher ist. Das Plus ist, dass, obwohl es mehr Kollisionsprüfungen gibt, jede Prüfung viel einfacher ist, da sie weiß, dass sich der Charakter jedes Mal um ein Pixel bewegt.
Steigung-Daten
Beginnen wir mit der Definition der Daten, die zur Darstellung der Steigungen erforderlich sind. Zunächst benötigen wir eine Höhenkarte mit einer Neigung, die die Form bestimmt. Beginnen wir mit einem klassischen 45-Grad-Gefälle.

Definieren wir auch eine andere Böschungsform. Dieser wird mehr als ein Boden auf dem Boden als alles andere dienen.

Natürlich werden wir Varianten dieser Steigung verwenden wollen, je nachdem, wo wir sie platzieren möchten. Im Fall unserer definierten 45-Grad-Neigung wird es z. B. gut passen, wenn sich rechts ein fester Block befindet. Wenn sich jedoch der feste Block links befindet, möchten wir eine umgedrehte Version der Kachel verwenden definiert. Wir müssen in der Lage sein, die Neigungen auf der X-Achse und der Y-Achse umzudrehen und sie um 90 Grad zu drehen, um auf alle Varianten einer vordefinierten Neigung zugreifen zu können.
Schauen wir uns an, wie die Transformationen der 45-Grad-Neigung aussehen.

Wie Sie sehen, können wir in diesem Fall alle Varianten mit Flips erhalten. Wir müssen die Steigung nicht wirklich um 90 Grad drehen, aber sehen wir, wie die zweite Steigung aussieht, die wir zuvor definiert haben.

Rotierende SteigungenIn diesem Fall ermöglicht die 90-Grad-Rotationstransformation das Anordnen der Neigung an der Wand.
Offsets berechnen
Verwenden Sie unsere definierten Daten, um die Offsets zu berechnen, die auf das Objekt angewendet werden müssen, das sich mit einer Kachel überschneidet. Der Versatz enthält die Informationen über:
- wie viel ein Objekt nach oben/unten/links/rechts bewegen muss, um nicht mit einer Kachel zu kollidieren
- wie viel ein Objekt bewegt werden muss, um sich direkt neben der oberen/unteren/linken/rechten Oberfläche einer Neigung zu befinden

Die grünen Teile des obigen Bildes sind die Teile, bei denen das Objekt die leeren Teile der Kachel überlappt, und die gelben Quadrate geben den Bereich an, in dem das Objekt die Neigung überlappt.
Beginnen wir nun mit der Berechnung des Offsets für Fall 1.
Das Objekt kollidiert mit keinem Teil der Steigung. Das bedeutet, dass wir ihn nicht wirklich aus der Kollision herausbewegen müssen, sodass der erste Teil unseres Versatzes auf 0 gesetzt wird.
Wenn Sie möchten, dass der untere Teil des Objekts die Steigung berührt, müssen Sie den zweiten Teil des Versatzes um 3 Pixel nach unten verschieben. Wenn wir wollten, dass die rechte Seite des Objekts die Steigung berührt, müssen wir das Objekt um 3 Pixel nach rechts verschieben. Damit die linke Seite des Objekts den rechten Rand der Neigung berührt, müssen Sie das Objekt um 16 Pixel nach rechts verschieben. Wenn der obere Rand des Objekts die Steigung berühren soll, müssen wir das Objekt um 16 Pixel nach unten verschieben.
Warum brauchen wir nun die Information, wie weit der Abstand zwischen Objektkante und Steigung ist? Diese Daten sind für uns sehr nützlich, wenn wir möchten, dass ein Objekt an der Steigung haften bleibt.
Angenommen, ein Objekt bewegt sich auf unserer 45-Grad-Steigung nach links. Wenn es sich schnell genug bewegt, landet es in der Luft und fällt schließlich wieder auf den Abhang und so weiter. Wenn wir möchten, dass er auf der Piste bleibt, wird er jedes Mal, wenn er sich nach links bewegt, nach unten gedrückt, damit er mit der Piste in Kontakt bleibt. Die folgende Animation zeigt den Unterschied zwischen der Aktivierung oder Deaktivierung der Neigung eines Charakters.

Animation einer HangabfahrtHier werden viele Daten zwischengespeichert. Grundsätzlich möchten wir für jede mögliche Überlappung mit einer Kachel einen Versatz berechnen. Das bedeutet, dass wir für jede Position und für jede Überlappungsgröße einen schnellen Überblick darüber haben, wie viel ein Objekt verschoben werden soll. Beachten Sie, dass wir Die endgültigen Offsets können nicht zwischengespeichert werden, da wir keinen Offset für jede mögliche AABB im Cache speichern können. Es ist jedoch einfach, den Offset anzupassen, da die AABB sich mit der Kachel der Steigung überlappt.
Fliesen definieren
Wir definieren alle Steigungsdaten in einer statischen Slopes-Klasse.
public static class Slopes { }
Lassen Sie uns zunächst mit den Höhenkarten umgehen. Lassen Sie uns einige davon definieren, die später verarbeitet werden sollen.
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 };
Fügen wir die Testkacheltypen für die definierten Steigungen hinzu.
public enum TileType { Empty, Block, OneWay, TestSlopeMid1, TestSlopeMid1FX, TestSlopeMid1FY, TestSlopeMid1FXY, TestSlopeMid1F90, TestSlopeMid1F90X, TestSlopeMid1F90Y, TestSlopeMid1F90XY, TestSlope45, TestSlope45FX, TestSlope45FY, TestSlope45FXY, TestSlope45F90, TestSlope45F90X, TestSlope45F90Y, TestSlope45F90XY, Count }
Lassen Sie uns auch eine weitere Aufzählung für Fliesenkollisionstypen erstellen. Dies ist hilfreich, wenn Sie verschiedenen Steinen den gleichen Kollisionstyp zuweisen möchten, beispielsweise eine grasbewachsene Neigung von 45 Grad oder eine Steigung von 45 Grad.
public enum TileCollisionType { Empty, Block, OneWay, SlopeMid1, SlopeMid1FX, SlopeMid1FY, SlopeMid1FXY, SlopeMid1F90, SlopeMid1F90X, SlopeMid1F90Y, SlopeMid1F90XY, Slope45, Slope45FX, Slope45FY, Slope45FXY, Slope45F90, Slope45F90X, Slope45F90Y, Slope45F90XY, Count }
Nun erstellen wir ein Array, das alle Heightmaps der Kacheln enthält. Dieses Array wird von der TileCollisionType
-Enumeration indiziert.
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; public static sbyte[][] slopesHeights;
Bearbeitung der Steigungen
Bevor wir mit der Berechnung der Offsets beginnen, wollen wir unsere Heightmaps in vollständige Kollisionsbitmaps entfalten. Das macht es leicht zu bestimmen, ob eine AABB mit einer Kachel kollidiert, und ermöglicht auch komplexere Kachelformen, wenn dies erforderlich ist. Erstellen wir ein Array für diese Bitmaps.
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended;
Nun erstellen wir eine Funktion, die die Höhenkarte in die Bitmap erweitert.
public static sbyte[][] Extend(sbyte[] slope) { sbyte[][] extended = new sbyte[Map.cTileSize][]; for (int x = 0; x < Map.cTileSize; ++x) { extended[x] = new sbyte[Map.cTileSize]; for (int y = 0; y < Map.cTileSize; ++y) extended[x][y] = System.Convert.ToSByte(y < slope[x]); } return extended; }
Nichts hier kompliziert - wenn eine bestimmte Position auf der Kachel fest ist, setzen wir sie auf 1; Wenn nicht, wird es auf 0 gesetzt.
Jetzt erstellen wir unsere Init
-Funktion, die schließlich alle Caching-Aufgaben erledigt, die wir auf den Steigungen erledigen müssen.
public static void Init() { }
Lassen Sie uns hier die Container-Arrays erstellen.
public static void Init() { slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; }
Lassen Sie uns nun jeden Fliesenkollisionstyp auf die entsprechenden zwischengespeicherten Daten zeigen.
for (int i = 0; i < (int)TileCollisionType.Count; ++i) { switch ((TileCollisionType)i) { case TileCollisionType.Empty: slopesHeights[i] = empty; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Full: slopesHeights[i] = full; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Slope45: slopesHeights[i] = slope45; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Slope45FX: case TileCollisionType.Slope45FY: case TileCollisionType.Slope45FXY: case TileCollisionType.Slope45F90: case TileCollisionType.Slope45F90X: case TileCollisionType.Slope45F90XY: case TileCollisionType.Slope45F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; break; case TileCollisionType.SlopeMid1: slopesHeights[i] = slopeMid1; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.SlopeMid1FX: case TileCollisionType.SlopeMid1FY: case TileCollisionType.SlopeMid1FXY: case TileCollisionType.SlopeMid1F90: case TileCollisionType.SlopeMid1F90X: case TileCollisionType.SlopeMid1F90XY: case TileCollisionType.SlopeMid1F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.SlopeMid1]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.SlopeMid1]; break; } }
Versatzstruktur (Offset Structure)
Jetzt können wir unsere Versatzstruktur definieren.
public struct SlopeOffsetSB { public sbyte freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop; public SlopeOffsetSB(sbyte _freeLeft, sbyte _freeRight, sbyte _freeDown, sbyte _freeUp, sbyte _collidingLeft, sbyte _collidingRight, sbyte _collidingBottom, sbyte _collidingTop) { freeLeft = _freeLeft; freeRight = _freeRight; freeDown = _freeDown; freeUp = _freeUp; collidingLeft = _collidingLeft; collidingRight = _collidingRight; collidingBottom = _collidingBottom; collidingTop = _collidingTop; } }
Wie zuvor erläutert, entsprechen die Variablen freeLeft
, freeRight
, freeDown
und freeUp
dem Offset, der angewendet werden muss, damit das Objekt nicht mehr mit der Steigung kollidiert, während collidingLeft
, collidingRight
, collidingTop
und collidingBottom
der Abstand des Objekts sind muss verschoben werden, um die Steigung zu berühren, ohne sie zu überlappen.
Es ist an der Zeit, unsere Hochleistungs-Caching-Funktion zu erstellen, aber bevor wir dies tun, erstellen wir einen Container, der all diese Daten enthält.
public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended; public static SlopeOffsetSB[][][][][] slopeOffsets;
Und erstellen Sie das Array in der Init
-Funktion.
slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];
Speicherprobleme
Wie Sie sehen, hat dieses Array viele Dimensionen, und für jeden neuen Kacheltyp ist tatsächlich viel Speicher erforderlich. Für jede X-Position in der Kachel, für jede Y-Position in der Kachel, für jede mögliche Breite in der Kachel und für jede mögliche Höhe gibt es eine separate Berechnung des Offsetwerts.
Da die verwendeten Kacheln 16 x 16 sind, bedeutet dies, dass die für jeden Kacheltyp benötigte Datenmenge 16 * 16 * 16 * 16 * 8 Byte beträgt, was 512 kB entspricht. Dies ist eine Menge von Daten, die jedoch immer noch überschaubar sind. Wenn das Zwischenspeichern dieser Informationsmenge nicht möglich ist, müssen wir entweder auf die Berechnung der Offsets in Echtzeit umstellen, wahrscheinlich mit einer effizienteren Methode als der, die wir verwenden zum Zwischenspeichern verwenden oder unsere Daten optimieren.
Wenn die Kachelgröße in unserem Spiel größer wäre, beispielsweise 32x32, würde jeder Kacheltyp 8 MB belegen, und wenn wir 64x64 verwenden, wären es 128 MB. Diese Beträge scheinen viel zu groß zu sein, um nützlich zu sein, vor allem, wenn wir einige Hangtypen im Spiel haben möchten. Eine sinnvolle Lösung dafür scheint die Aufteilung der großen Kollisionsplättchen in kleinere zu sein. Beachten Sie, dass nur für jede neu definierte Steigung mehr Speicherplatz erforderlich ist. Die Umwandlungen verwenden dieselben Daten.
Kollisionen innerhalb einer Kachel prüfen
Bevor wir mit der Berechnung der Offsets beginnen, müssen wir wissen, ob ein Objekt an einer bestimmten Position mit den festen Teilen der Kachel kollidiert. Lassen Sie uns zuerst diese Funktion erstellen.
public static bool Collides(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { for (int x = posX; x <= posX + w && x < Map.cTileSize; ++x) { for (int y = posY; y <= posY + h && y < Map.cTileSize; ++y) { if (slopeExtended[x][y] == 1) return true; } } return false; }
Die Funktion übernimmt die Kollisionsbitmap, die Position der Überlappung und die Überlappungsgröße. Die Position ist das untere linke Pixel des Objekts und die Größe ist die 0-basierte Breite und Höhe. Mit 0 meine ich, dass eine Breite von 0 bedeutet, dass das Objekt tatsächlich 1 Pixel breit ist, und eine Breite von 15 bedeutet, dass das Objekt 16 Pixel breit ist. Die Funktion ist sehr einfach: Wenn sich die Pixel eines Objekts mit einer Steigung überlappen, wird true zurückgegeben, andernfalls false.
Berechnen Sie die Offsets
Beginnen wir nun mit der Berechnung der Offsets.
public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { }
Um den Versatz zu berechnen, benötigen wir das Kollisionsbitmap, die Position und die Größe der Überlappung. Beginnen wir mit der Deklaration der Offset-Werte.
public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { sbyte freeUp = 0, freeDown = 0, collidingTop = 0, collidingBottom = 0; sbyte freeLeft = 0, freeRight = 0, collidingLeft = 0, collidingRight = 0; }
Lassen Sie uns nun berechnen, um wie viel wir das Objekt verschieben müssen, damit es nicht mit der Steigung kollidiert.Um das zu erreichen, müssen wir, während das Objekt mit der Steigung kollidiert, es weiter nach oben bewegen und auf Kollision prüfen, bis es keine Überlappung mit den festen Teilen der Kachel gibt.

Keine ÜberlappungenOben sehen Sie, wie wir den Versatz berechnen. Im ersten Fall müssen Sie, da das Objekt die obere Begrenzung der Kachel berührt, nicht nur es nach oben verschieben, sondern auch seine Höhe verringern. Wenn sich ein Teil der AABB außerhalb der Kachelgrenzen befindet, interessiert uns das nicht mehr. In ähnlicher Weise werden Offsets für alle anderen Richtungen berechnet. Für das obige Beispiel wären die Offsets also:
- 4 für den Aufwärtsversatz
- -4 für den linken Versatz
- -16 für den Abwärtsversatz - das ist die maximale Entfernung, denn wenn wir das Objekt nach unten verschieben, müssen wir es ganz aus den Grenzen der Kachel heraus bewegen, um die Kollision mit der Steigung zu beenden
- 16 für den richtigen Versatz
Beginnen wir damit, die temporäre Variable für die Höhe des Objekts zu deklarieren. Wie oben erwähnt, ändert sich dies abhängig davon, wie hoch das Objekt bewegt wird.
sbyte movH = h;
Jetzt ist es Zeit für die Hauptbedingung. Solange sich das Objekt nicht aus den Kachelgrenzen herausbewegt hat und mit den festen Teilen der Kachel kollidiert, müssen wir die offsetUp
erhöhen.
sbyte movH = h; while (movH >= 0 && posY + freeUp < Map.cTileSize && Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) { ++freeUp; }
Schließlich passen wir die Größe des überlappenden Bereichs der Objektkachel an, wenn sich das Objekt außerhalb der Kachelgrenzen befindet.
sbyte movH = h; while (movH >= 0 && posY + freeUp < Map.cTileSize && Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) { if (posY + freeUp == Map.cTileSize) --movH; ++freeUp; }
Jetzt machen wir dasselbe für den linken Versatz. Beachten Sie, dass wir die Position nicht wirklich ändern müssen, wenn wir das Objekt nach links verschieben und das Objekt aus den Kachelgrenzen herausbewegt wird. Stattdessen ändern wir nur die Breite der Überlappung. Das ist auf der rechten Seite der Animation dargestellt und veranschaulicht die Offsetberechnung.
movW = w; while (movW >= 0 && posX + freeLeft >= 0 && Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) { if (posX + freeLeft == 0) --movW; else --freeLeft; }
Da hier jedoch der freeLeft
-Versatz nicht verschoben wurde, da wir die Breite verringert haben, müssen wir die reduzierte Größe in den Versatz konvertieren.
movW = w; while (movW >= 0 && posX + freeLeft >= 0 && Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) { if (posX + freeLeft == 0) --movW; else --freeLeft; } freeLeft -= (sbyte)(w - movW);
Jetzt machen wir dasselbe für die Abwärts- und Rechtsversätze.
movH = h; while (movH >= 0 && posY + freeDown >= 0 && Collides(slopeExtended, posX, (sbyte)(posY + freeDown), w, movH)) { if (posY + freeDown == 0) --movH; else --freeDown; } freeDown -= (sbyte)(h - movH); sbyte movW = w; while (movW >= 0 && posY + freeRight < Map.cTileSize && Collides(slopeExtended, (sbyte)(posX + freeRight), posY, movW, h)) { if (posX + freeRight == Map.cTileSize) --movW; ++freeRight; }
In Ordnung, wir haben den ersten Teil des Versatzes berechnet - das ist, wie viel wir das Objekt bewegen sollten, damit es nicht mehr mit der Steigung kollidiert. Nun ist es an der Zeit, die Offsets herauszufinden, die das Objekt direkt neben den festen Teilen der Kachel bewegen sollen.
Beachten Sie, dass wir das bereits tun, wenn wir das Objekt aus der Kollision herausbewegen müssen, da wir unmittelbar nach der Kollision aufhören.

Bewegen Sie das Objekt aus der KollisionIm rechten Fall ist der Aufwärtsversatz 4, aber es ist auch der Versatz, den wir benötigen, um das Objekt zu verschieben, damit der untere Rand auf einem festen Pixel liegt. Gleiches gilt für die anderen Seiten.
if (freeUp == 0) { } else collidingBottom = freeUp;
Nun ist der Fall links, wo wir die Offsets selbst finden müssen. Wenn Sie den collidingBottom
-Versatz dort finden möchten, müssen Sie das Objekt um 3 Pixel nach unten verschieben. Die Berechnungen, die hier benötigt werden, ähneln den vorherigen, aber stattdessen werden wir suchen, wann das Objekt mit der Neigung kollidiert und dann verschoben wird, während der Versatz um eins verringert wird, so dass die durchgehenden Pixel kaum berührt werden, anstatt sie zu überlappen.
if (freeUp == 0) { while ( posY + collidingBottom >= 0 && !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) --collidingBottom; collidingBottom += 1; } else { collidingBottom = freeUp; }
Wenn freeUp
gleich 0 ist, muss free down gleich 0 sein, sodass wir die Berechnungen für collidingTop
unter denselben Klammern angeben können. Diese Berechnungen sind wiederum analog zu dem, was wir bisher gemacht haben.
if (freeUp == 0) { while (posY + h + collidingTop < Map.cTileSize && !Collides(slopeExtended, posX, (sbyte)(posY + collidingTop), w, h)) ++collidingTop; collidingTop -= 1; while ( posY + collidingBottom >= 0 && !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) --collidingBottom; collidingBottom += 1; } else { collidingBottom = freeUp; collidingTop = freeDown; }
Machen wir dasselbe für den linken und den rechten Versatz.
if (freeRight == 0) { while (posX + w + collidingRight < Map.cTileSize && !Collides(slopeExtended, (sbyte)(posX + collidingRight), posY, w, h)) ++collidingRight; collidingRight -= 1; while (posX + collidingLeft >= 0 && !Collides(slopeExtended, (sbyte)(posX + collidingLeft), posY , w, h)) --collidingLeft; collidingLeft += 1; } else { collidingLeft = freeRight; collidingRight = freeLeft; }
Daten zwischenspeichern
Nachdem nun alle Offsets berechnet wurden, können wir den Offset für diesen bestimmten Datensatz zurückgeben.
return new SlopeOffsetSB(freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop);
Erstellen Sie einen Container für alle unsere zwischengespeicherten Daten.
public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended; public static SlopeOffsetSB[][][][][] slopeOffsets;
Initialisieren Sie das Array.
slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];
Erstellen Sie schließlich die Caching-Funktion.
public static SlopeOffsetSB[][][][] CacheSlopeOffsets(sbyte[][] slopeExtended) { var offsetCache = new SlopeOffsetSB[Map.cTileSize][][][]; for (int x = 0; x < Map.cTileSize; ++x) { offsetCache[x] = new SlopeOffsetSB[Map.cTileSize][][]; for (int y = 0; y < Map.cTileSize; ++y) { offsetCache[x][y] = new SlopeOffsetSB[Map.cTileSize][]; for (int w = 0; w < Map.cTileSize; ++w) { offsetCache[x][y][w] = new SlopeOffsetSB[Map.cTileSize]; for (int h = 0; h < Map.cTileSize; ++h) { offsetCache[x][y][w][h] = GetOffset(slopeExtended, (sbyte)x, (sbyte)y, (sbyte)w, (sbyte)h); } } } } return offsetCache; }
Die Funktion selbst ist sehr einfach, daher ist es sehr einfach zu sehen, wie viele Daten zwischengespeichert werden, um unsere Anforderungen zu erfüllen!
Stellen Sie jetzt sicher, dass die Offsets für jeden Fliesenkollisionstyp zwischengespeichert werden.
case TileCollisionType.Slope45: slopesHeights[i] = slope45; slopesExtended[i] = Extend(slopesHeights[i]); slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]); break; case TileCollisionType.Slope45FX: case TileCollisionType.Slope45FY: case TileCollisionType.Slope45FXY: case TileCollisionType.Slope45F90: case TileCollisionType.Slope45F90X: case TileCollisionType.Slope45F90XY: case TileCollisionType.Slope45F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45]; break;
Und damit ist unsere Haupt-Caching-Funktion beendet!
Berechnung des Weltraumversatzes
Lassen Sie uns nun die zwischengespeicherten Daten verwenden, um eine Funktion zu erstellen, die einen Versatz für ein Zeichen zurückgibt, das in einem Weltraum existiert.
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { }
Der Offset, den wir zurückgeben, ist nicht derselbe Struktur, die wir für die zwischengespeicherten Daten verwendet haben, da die Weltraumoffsets letztendlich größer sein können als die Grenzen des einzelnen Bytes. Die Struktur ist im Grunde dasselbe, aber es werden Ganzzahlen verwendet.
Die Parameter lauten wie folgt:
- das Weltraumzentrum der Fliese
- die linke, rechte, untere und obere Kante der AABB, für die wir den Versatz erhalten möchtendie Art der Fliese, für die wir
- den Versatz erhalten möchten
Zuerst müssen wir herausfinden, wie sich die AABB mit einer Neigungskachel überlappt. Wir müssen wissen, wo die Überlappung beginnt (die untere linke Ecke) und auch, wie stark sich die Überlappung über die Kachel erstreckt.
Um das zu berechnen, lassen Sie uns zuerst die Variablen deklarieren.
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; SlopeOffsetI offset; }
Lassen Sie uns nun die Kanten der Kachel im Weltraum berechnen.
float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize;
Das sollte jetzt ganz einfach sein. Es gibt zwei Hauptkategorien von Fällen, die wir hier finden können. Erstens ist die Überlappung innerhalb der Kachelgrenzen.

Das dunkelblaue Pixel ist die Position der Überlappung und die Höhe und Breite sind mit den blauen Kacheln markiert. Hier ist alles recht einfach, so dass die Berechnung der Position und der Größe der Überlappung keine zusätzlichen Aktionen erfordert.
Die zweite Kategorie von Fällen sieht wie folgt aus, und im Spiel beschäftigen wir uns hauptsächlich mit diesen Fällen:

Sehen wir uns eine Beispielsituation an, die oben abgebildet ist. Wie Sie sehen, erstreckt sich die AABB weit über die Kachel hinaus. Wir müssen jedoch die Position und Größe der Überlappung innerhalb der Kachel selbst ermitteln, sodass wir unseren zwischengespeicherten Versatzwert abrufen können. Im Moment kümmern wir uns nicht wirklich um alles, was außerhalb der Fliesengrenzen liegt. Das erfordert, dass wir die Überlappungsposition und -größe an die Grenzen der Fliese klemmen.
Position x ist gleich dem Abstand zwischen der linken Kante der AABB und der linken Kante der Kachel. Wenn sich AABB links von der linken Kante der Kachel befindet, muss die Position auf 0 geklemmt werden. Um die Überlappungsbreite zu erhalten, müssen wir die rechte Kante der AABB von der x-Position der Überlappung subtrahieren, die wir bereits berechnet haben.Die
Werte für die Y-Achse werden auf dieselbe Weise berechnet.
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);
Jetzt können wir die zwischengespeicherten Versätze für die Überlappung abrufen.
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
Passen Sie den Offset an
Bevor wir den Versatz zurückgeben, müssen wir ihn möglicherweise anpassen. Betrachten Sie die folgende Situation.

Mal sehen, wie unser zwischengespeicherter Offset für eine solche Überlappung aussehen würde. Beim Zwischenspeichern waren wir nur über die Überlappung innerhalb der Kachelgrenzen besorgt. In diesem Fall wäre der Aufwärtsversatz gleich 9. Sie können sehen, dass der Überlappungsbereich innerhalb der Kachelgrenzen um 9 Pixel nach oben verschoben würde mit der Steigung kollidieren, aber wenn wir die gesamte AABB verschieben, bewegt sich der Bereich, der sich unterhalb der Kachelgrenzen befindet, in die Kollision.
Grundsätzlich müssen wir hier den Aufwärtsversatz um die Anzahl der Pixel anpassen, die die AABB unterhalb der Kachelgrenzen erstreckt.
if (bottomTileEdge > bottomY) { if (offset.freeUp > 0) offset.freeUp += (int)bottomTileEdge - (int)bottomY; offset.collidingBottom = offset.freeUp; }
Für alle anderen Offsets - links, rechts und unten - muss dasselbe getan werden, mit der Ausnahme, dass wir die linke und rechte Offsets auf diese Weise jetzt überspringen, da das nicht notwendig ist.
if (topTileEdge < topY) { if (offset.freeDown < 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge > bottomY) { if (offset.freeUp > 0) offset.freeUp += (int)bottomTileEdge - (int)bottomY; offset.collidingBottom = offset.freeUp; }
Sobald wir fertig sind, können wir den angepassten Offset zurückgeben. Die fertige Funktion sollte so aussehen.
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize; SlopeOffsetI offset; posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (topTileEdge < topY) { if (offset.freeDown < 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge > bottomY) { if (offset.freeUp > 0) offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); offset.collidingBottom = offset.freeUp; } return offset; }
Natürlich ist es noch nicht vollständig fertig. Später werden wir auch die Kachelumwandlungen hier behandeln, sodass der Versatz entsprechend zurückgegeben wird, je nachdem, ob die Kachel auf den XY-Achsen gedreht oder um 90 Grad gedreht wurde. Im Moment spielen wir jedoch nur mit den nicht transformierten Plättchen.
Ein-Pixel-Schrittphysik implementieren
Überblick
Wenn Sie die Objekte um ein Pixel verschieben, ist es sehr einfach, viele Dinge zu handhaben, insbesondere bei Kollisionen mit Neigungen für schnelle Objekte. Obwohl wir jeden Pixel, auf den wir uns bewegen, auf Kollision prüfen, sollten wir uns in einem bestimmten Muster bewegen Genauigkeit gewährleisten. Dieses Muster hängt von der Objektgeschwindigkeit ab.

Auf dem Bild oben können Sie sehen, dass, wenn wir das Objekt zunächst blind bewegen, alle Pixel, die es horizontal bewegen muss, und danach der Pfeil mit einem festen Block kollidieren würde, der nicht wirklich auf Kurs ist. Die Reihenfolge der Bewegung sollte auf dem Verhältnis der vertikalen zur horizontalen Geschwindigkeit basieren. Auf diese Weise wissen wir, wie viele Pixel wir für jedes horizontal bewegte Pixel vertikal verschieben müssen.
Definieren Sie die Daten
Gehen wir zu unserer Klasse für sich bewegende Objekte und definieren Sie einige neue Variablen.
Zunächst enthält unsere mPosition
-Hauptvariable nur die Ganzzahl, und wir behalten eine andere Variable mit dem Namen mRemainder
, um den Wert nach dem Fließkomma zu behalten.
public Vector2 mPosition; public Vector2 mRemainder;
Als Nächstes fügen wir ein paar neue Positionsstatusvariablen hinzu, um anzuzeigen, ob sich der Charakter aktuell auf der Steigung befindet. An diesem Punkt ist es gut, wenn wir den gesamten Positionsstatus in eine einzige Struktur packen.
[Serializable] public struct PositionState { public bool pushesRight; public bool pushesLeft; public bool pushesBottom; public bool pushesTop; public bool pushedTop; public bool pushedBottom; public bool pushedRight; public bool pushedLeft; public bool pushedLeftObject; public bool pushedRightObject; public bool pushedBottomObject; public bool pushedTopObject; public bool pushesLeftObject; public bool pushesRightObject; public bool pushesBottomObject; public bool pushesTopObject; public bool pushedLeftTile; public bool pushedRightTile; public bool pushedBottomTile; public bool pushedTopTile; public bool pushesLeftTile; public bool pushesRightTile; public bool pushesBottomTile; public bool pushesTopTile; public bool onOneWayPlatform; public Vector2i leftTile; public Vector2i rightTile; public Vector2i topTile; public Vector2i bottomTile; public void Reset() { leftTile = rightTile = topTile = bottomTile = new Vector2i(-1, -1); pushesRight = false; pushesLeft = false; pushesBottom = false; pushesTop = false; pushedTop = false; pushedBottom = false; pushedRight = false; pushedLeft = false; pushedLeftObject = false; pushedRightObject = false; pushedBottomObject = false; pushedTopObject = false; pushesLeftObject = false; pushesRightObject = false; pushesBottomObject = false; pushesTopObject = false; pushedLeftTile = false; pushedRightTile = false; pushedBottomTile = false; pushedTopTile = false; pushesLeftTile = false; pushesRightTile = false; pushesBottomTile = false; pushesTopTile = false; onOneWayPlatform = false; } }
Lassen Sie uns nun eine Instanz der Struktur für das Objekt deklarieren.
public PositionState mPS;
Eine andere Variable, die wir brauchen, ist das Hängenbleiben.
public bool mSticksToSlope;
Grundlegende Implementierung
Beginnen wir mit dem Erstellen der grundlegenden Funktionen zur Kollisionsprüfung. Diese werden die Pisten noch nicht bewältigen können.
Kollisionsprüfungen
Beginnen wir mit der rechten Seite.
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { }
Die hier verwendeten Parameter sind die aktuelle Position des Objekts, seine oberen rechten und unteren linken Ecken sowie der Positionsstatus. Zuerst berechnen wir die obere rechte und obere linke Kachel für unser Objekt.
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
Lassen Sie uns nun alle Kacheln entlang der rechten Kante des Objekts durchlaufen.
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); }
Abhängig von der Kollisionsplatte reagieren wir jetzt entsprechend.
switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; }
Wie Sie sehen können, überspringen wir zunächst die Pisten. Wir möchten nur die grundlegenden Einstellungen vornehmen, bevor wir uns näher damit befassen.
Insgesamt sollte die Funktion jetzt so aussehen:
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; } } return false; }
Wir werden dasselbe für alle anderen drei Richtungen tun: links, oben und unten.
public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f)); for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesLeftTile = true; state.leftTile = new Vector2i(bottomLeftTile.x, y); return true; } } return false; } public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesTopTile = true; state.topTile = new Vector2i(x, topRightTile.y); return true; } } return false; } public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.onOneWayPlatform = false; state.pushesBottomTile = true; state.bottomTile = new Vector2i(x, bottomleftTile.y); return true; } } return false; }
Funktionen verschieben
Nun, da wir uns damit befasst haben, können wir zwei für die Bewegung verantwortliche Funktionen erstellen. Einer wird die Bewegung horizontal und ein anderer die vertikale Bewegung ausführen.
public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { }
Die Argumente, die wir in dieser Funktion verwenden, sind die aktuelle Position, ein Boolescher Wert, der angibt, ob wir auf dem Weg ein Hindernis finden oder nicht, ein Versatz, der angibt, wie viel wir bewegen müssen, ein Schritt, bei dem wir das Objekt verschieben Bei jeder Iteration die unteren linken und oberen rechten Eckpunkte der AABB und schließlich der Positionsstatus.
Im Grunde wollen wir das Objekt so oft um einen Schritt verschieben, dass sich die Schritte auf den Versatz summieren.Wenn wir auf ein Hindernis stoßen, müssen wir natürlich auch aufhören, uns zu bewegen.
while (!foundObstacleX && offset != 0.0f) { }
Bei jeder Iteration subtrahieren wir den Schritt vom Versatz, so dass der Versatz schließlich zu Null wird und wir wissen, dass wir so viele Pixel verschoben haben, wie wir benötigen.
while (!foundObstacleX && offset != 0.0f) { offset -= step; }
Mit jedem Schritt möchten wir prüfen, ob wir mit einer Kachel kollidieren. Wenn wir uns nach rechts bewegen, wollen wir prüfen, ob wir rechts mit einer Wand kollidieren. Wenn wir uns nach links bewegen, wollen wir links nach Hindernissen suchen.
while (!foundObstacleX && offset != 0.0f) { offset -= step; if (step > 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); }
Wenn wir kein Hindernis gefunden haben, können wir das Objekt verschieben.
while (!foundObstacleX && offset != 0.0f) { offset -= step; if (step > 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); if (!foundObstacleX) { position.x += step; topRight.x += step; bottomLeft.x += step; } }
Nachdem wir uns bewegt haben, überprüfen wir schließlich, ob Kollisionen auf und ab auftreten, da wir direkt unter oder über einen Block rutschen könnten. Das ist nur zum Aktualisieren des Positionsstatus, um genau zu sein.
public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { while (!foundObstacleX && offset != 0.0f) { offset -= step; if (step > 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); if (!foundObstacleX) { position.x += step; topRight.x += step; bottomLeft.x += step; CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); } } }
Die MoveY
-Funktion funktioniert ähnlich.
public void MoveY(ref Vector2 position, ref bool foundObstacleY, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { while (!foundObstacleY && offset != 0.0f) { offset -= step; if (step > 0.0f) foundObstacleY = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); else foundObstacleY = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); if (!foundObstacleY) { position.y += step; topRight.y += step; bottomLeft.y += step; CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); } } }
Konsolidieren Sie die Bewegung
Da wir nun Funktionen haben, die für die vertikale und horizontale Bewegung zuständig sind, können wir die Hauptfunktion für die Bewegung erstellen.
public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) { }
Die Funktion berechnet den Wert, um den das Objekt bewegt werden soll, die aktuelle Geschwindigkeit des Objekts, seine aktuelle Position zusammen mit dem Gleitkomma-Rest, der AABB des Objekts und dem Positionsstatus.
Das erste, was wir hier tun werden, ist den Versatz zum Rest hinzuzufügen, so dass wir im Rest den vollen Wert haben, wie viel sich unser Charakter bewegen sollte.
remainder += offset;
Da wir die MoveX
- und MoveY
-Funktionen von hier aus aufrufen, müssen wir die oberen rechten und unteren linken Ecken der AABB übergeben, also lassen Sie uns diese jetzt berechnen.
Vector2 topRight = aabb.Max(); Vector2 bottomLeft = aabb.Min();
Wir müssen auch den Schrittvektor erhalten. Es wird als eine Richtung verwendet, in der wir unser Objekt bewegen werden.
var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y));
Nun wollen wir sehen, wie viele Pixel wir wirklich bewegen müssen. Wir müssen den Rest abrunden, da wir uns immer um eine ganze Zahl bewegen und dann diesen Wert vom Rest abziehen müssen.
var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); remainder -= move;
Jetzt teilen wir die Bewegung in vier Fälle auf, abhängig von unseren Bewegungsvektorwerten. Wenn die x- und y-Werte des Bewegungsvektors gleich 0 sind, ist keine Bewegung vorzunehmen, sodass wir einfach zurückkehren können.
if (move.x == 0.0f && move.y == 0.0f) return;
Wenn nur der y-Wert 0 ist, bewegen wir uns nur horizontal.
if (move.x == 0.0f && move.y == 0.0f) return; else if (move.x != 0.0f && move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state);
Wenn nur der x-Wert 0 ist, bewegen wir uns nur vertikal.
if (move.x == 0.0f && move.y == 0.0f) return; else if (move.x != 0.0f && move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); else if (move.y != 0.0f && move.x == 0.0f) MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); else { }
Wenn wir uns sowohl auf der x- als auch auf der y-Achse bewegen müssen, müssen wir uns in einem Muster bewegen, das zuvor beschrieben wurde. Lassen Sie uns zunächst das Drehzahlverhältnis berechnen.
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x);
Lassen Sie uns auch den vertikalen Akkumulator erklären, der enthält, wie viele Pixel wir mit jeder Schleife vertikal bewegen müssen.
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f;
Die Bedingung zum Anhalten der Bewegung ist die, dass wir entweder auf einer der Achsen ein Hindernis getroffen haben oder das Objekt durch den gesamten Bewegungsvektor bewegt wurde.
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f; while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) { }
Lassen Sie uns nun berechnen, um wie viele Pixel wir das Objekt vertikal verschieben sollen.
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; }
Für die Bewegung bewegen wir uns zunächst einen Schritt horizontal.
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; }
Und danach können wir uns vertikal bewegen. Hier wissen wir, dass wir das Objekt um den im vertAccum
enthaltenen Wert verschieben müssen, aber bei Ungenauigkeiten, wenn wir uns auf der X-Achse ganz bewegen, müssen wir uns auch auf der Y-Achse bewegen.
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f)) { move.y -= step.y; vertAccum -= step.y; MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); } }
Alles in allem sollte die Funktion so aussehen:
public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) { remainder += offset; Vector2 topRight = aabb.Max(); Vector2 bottomLeft = aabb.Min(); bool foundObstacleX = false, foundObstacleY = false; var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y)); var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); remainder -= move; if (move.x == 0.0f && move.y == 0.0f) return; else if (move.x != 0.0f && move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); else if (move.y != 0.0f && move.x == 0.0f) MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); else { float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f; while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f)) { move.y -= step.y; vertAccum -= step.y; MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); } } } }
Jetzt können wir die Funktionen verwenden, die wir erstellt haben, um unsere Hauptfunktion UpdatePhysics
zusammenzustellen.
Erstellen Sie die Physik-Aktualisierungsfunktion
Zuallererst möchten wir den Positionsstatus aktualisieren, so dass alle Daten des vorherigen Frames zu den entsprechenden Variablen gehen und die Daten des aktuellen Frames zurückgesetzt werden.
public void UpdatePhysics() { mPS.pushedBottom = mPS.pushesBottom; mPS.pushedRight = mPS.pushesRight; mPS.pushedLeft = mPS.pushesLeft; mPS.pushedTop = mPS.pushesTop; mPS.pushedBottomTile = mPS.pushesBottomTile; mPS.pushedLeftTile = mPS.pushesLeftTile; mPS.pushedRightTile = mPS.pushesRightTile; mPS.pushedTopTile = mPS.pushesTopTile; mPS.pushesBottom = mPS.pushesLeft = mPS.pushesRight = mPS.pushesTop = mPS.pushesBottomTile = mPS.pushesLeftTile = mPS.pushesRightTile = mPS.pushesTopTile = mPS.pushesBottomObject = mPS.pushesLeftObject = mPS.pushesRightObject = mPS.pushesTopObject = mPS.onOneWay = false; }
Lassen Sie uns nun den Kollisionszustand unseres Objekts aktualisieren. Wir tun dies, damit wir, bevor wir unser Objekt bewegen, Daten aktualisiert haben, ob es sich auf dem Boden befindet oder andere Kacheln schiebt. Normalerweise wären die Daten des vorherigen Rahmens immer noch auf dem neuesten Stand, wenn das Gelände nicht veränderbar wäre und andere Objekte dieses Objekt nicht verschieben könnten. Hier wird jedoch davon ausgegangen, dass dies passieren könnte.
Vector2 topRight = mAABB.Max(); Vector2 bottomLeft = mAABB.Min(); CollidesWithTiles(ref mPosition, ref topRight, ref bottomLeft, ref mPS);
CollidesWithTiles
ruft einfach alle Kollisionsfunktionen auf, die wir geschrieben haben.
public void CollidesWithTiles(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); }
Dann die Geschwindigkeit aktualisieren.
mOldSpeed = mSpeed; if (mPS.pushesBottomTile) mSpeed.y = Mathf.Max(0.0f, mSpeed.y); if (mPS.pushesTopTile) mSpeed.y = Mathf.Min(0.0f, mSpeed.y); if (mPS.pushesLeftTile) mSpeed.x = Mathf.Max(0.0f, mSpeed.x); if (mPS.pushesRightTile) mSpeed.x = Mathf.Min(0.0f, mSpeed.x);
Und aktualisieren Sie die Position. Lassen Sie uns zunächst den alten speichern.
mOldPosition = mPosition;
Berechnen Sie die neue.
Vector2 newPosition = mPosition + mSpeed * Time.deltaTime;
Berechnen Sie den Abstand zwischen den beiden.
Vector2 offset = newPosition - mPosition;
Wenn der Versatz nicht Null ist, können wir unsere Verschiebungsfunktion aufrufen.
if (offset != Vector2.zero) Move(offset, mSpeed, ref mPosition, ref mRemainder, mAABB, ref mPS);
Aktualisieren Sie schließlich die AABB des Objekts und den Positionsstatus.
mAABB.Center = mPosition; mPS.pushesBottom = mPS.pushesBottomTile; mPS.pushesRight = mPS.pushesRightTile; mPS.pushesLeft = mPS.pushesLeftTile; mPS.pushesTop = mPS.pushesTopTile;
Das ist es! Dieses System ersetzt jetzt das ältere, die Ergebnisse sollten die gleichen sein, obwohl die Art und Weise, wie wir sie ausführen, ein bisschen anders ist.
Zusammenfassung
Das ist es, um die Grundlagen für die Pisten zu schaffen. Was bleibt, ist, diese Lücken in unseren Kollisionsüberprüfungen zu füllen! Wir haben hier den Großteil unserer Caching-Arbeit geleistet und viele geometrische Komplexitäten durch die Implementierung der Einpixel-Bewegungsintegration beseitigt.
Das macht die Implementierung der Piste zu einem Kinderspiel, verglichen mit dem, was wir sonst tun müssten. Wir werden den Job im nächsten Teil der Serie beenden.
Danke fürs Lesen!
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post