Erstellen eines Spiels für Game Boy, Teil 2

Bild

Vor ein paar Wochen habe ich beschlossen, an einem Spiel für Game Boy zu arbeiten, dessen Erstellung mir große Freude bereitete. Sein Arbeitsname ist Aqua and Ashes. Das Spiel hat Open Source und ist auf GitHub veröffentlicht . Der vorherige Teil des Artikels ist hier .

Fantastische Sprites und wo sie leben


Im letzten Teil habe ich mehrere Sprites auf dem Bildschirm gerendert. Dies geschah auf sehr willkürliche und chaotische Weise. Tatsächlich musste ich im Code angeben, was und wo ich anzeigen möchte. Dies machte die Erstellung von Animationen fast unmöglich, verbrachte viel CPU-Zeit und komplizierte Code-Unterstützung. Ich brauchte einen besseren Weg.

Insbesondere brauchte ich ein System, in dem ich einfach die Animationsnummer, die Bildnummer und den Timer für jede einzelne Animation iterieren konnte. Wenn ich die Animation ändern müsste, würde ich einfach die Animation ändern und den Frame-Zähler zurücksetzen. Das in jedem Frame durchgeführte Animationsverfahren sollte einfach die geeigneten Sprites auswählen, um sie anzuzeigen, und sie ohne mein Zutun auf den Bildschirm werfen.

Und wie sich herausstellte, ist diese Aufgabe praktisch gelöst. Was ich gesucht habe, heißt Sprite-Mappings . Sprite-Maps sind Datenstrukturen, die (grob gesagt) eine Liste von Sprites enthalten. Jede Sprite-Map enthält alle Sprites zum Rendern eines einzelnen Objekts. Mit ihnen sind auch Animationskarten (Animationszuordnungen) verbunden , bei denen es sich um Listen von Sprite-Karten mit Informationen zum Schleifen handelt.

Es ist ziemlich lustig, dass ich im Mai dem vorgefertigten Sprite-Karteneditor für 16-Bit-Sonic-Spiele über Sonic einen Animationskarten-Editor hinzugefügt habe. (Er ist hier , Sie können lernen) Es ist noch nicht abgeschlossen, weil es ziemlich rau, schmerzhaft langsam und unpraktisch zu bedienen ist. Aber aus technischer Sicht funktioniert es. Und es scheint mir ziemlich cool zu sein ... (Einer der Gründe für die Rauheit war, dass ich buchstäblich zuerst mit dem JavaScript-Framework gearbeitet habe.) Sonic ist ein altes Spiel, daher ist es ideal als Grundlage für mein neues und altes Spiel.

Sonic 2-Kartenformat


Ich wollte den Editor in Sonic 2 verwenden, weil ich einen Hack für Genesis erstellen wollte. Sonic 1 und 3K sind im Grunde fast gleich, aber um es nicht zu komplizieren, werde ich mich auf die Geschichte über den zweiten Teil beschränken.

Schauen wir uns zunächst die Sprite-Karten an. Hier ist ein ziemlich typisches Tails-Sprite, das Teil der Blink-Animation ist.


Die Genesis-Konsole erstellt Sprites etwas anders. Die Genesis-Kachel (die meisten Programmierer nennen sie ein "Muster") ist 8x8, genau wie beim Game Boy. Das Sprite besteht aus einem Rechteck mit bis zu 4x4-Kacheln, ähnlich wie der 8x16-Sprite-Modus in Game Boy, jedoch flexibler. Der Trick dabei ist, dass diese Kacheln im Speicher nebeneinander liegen sollten. Die Entwickler von Sonic 2 wollten so viele Kacheln wie möglich für einen blinkenden Tails-Frame aus einem stehenden Tails-Frame wiederverwenden. Daher ist Tails in 2 Hardware-Sprites unterteilt, die aus 3x2 Kacheln bestehen - eine für den Kopf, die andere für den Körper. Sie sind in der folgenden Abbildung dargestellt.


Am oberen Rand dieses Dialogfelds befinden sich die Hardware-Sprite-Attribute. Es enthält ihre Position relativ zum Startpunkt (negative Zahlen werden abgeschnitten; tatsächlich sind dies -16 und -12 für das erste Sprite und -12 für das zweite), die im VRAM verwendete Anfangskachel, die Breite und Höhe des Sprites sowie verschiedene Statusbits für Spiegelbild von Sprite und Palette.

Kacheln werden unten angezeigt, wenn sie vom ROM in den VRAM geladen werden. Es ist nicht genügend Speicherplatz vorhanden, um alle Tails-Sprites im VRAM zu speichern. Daher müssen die erforderlichen Kacheln in jedem Frame in den Speicher kopiert werden. Sie werden als Dynamic Pattern Load Cues bezeichnet . Wir können sie jedoch überspringen, da sie fast unabhängig von Sprite-Maps sind und daher später problemlos hinzugefügt werden können.


Was die Animation betrifft, ist hier alles etwas einfacher. Eine Animationskarte in Sonic ist eine Liste von Sprite-Karten mit zwei Metadaten - dem Geschwindigkeitswert und der Aktion, die nach Abschluss der Animation ausgeführt werden soll. Die drei am häufigsten verwendeten Aktionen sind: eine Schleife über alle Frames, eine Schleife über die letzten N Frames oder ein Übergang zu einer völlig anderen Animation (z. B. beim Wechsel von einer Animation eines stehenden Sonic zu einer Animation seines eifrigen Tretens mit dem Fuß). Es gibt einige Befehle, die interne Flags im Speicher von Objekten angeben, aber nicht viele Objekte verwenden sie. (Jetzt ist mir der Gedanke gekommen, dass Sie das Bit im RAM des Objekts beim Schleifen der Animation auf einen Wert setzen können. Dies ist nützlich für Soundeffekte und andere Dinge.)

Wenn Sie sich den zerlegten Sonic 1- Code ansehen (der Sonic 2-Code ist zu groß, um ihn zu verknüpfen), werden Sie feststellen, dass der Link zu den Animationen von keiner ID hergestellt wird. Jedes Objekt erhält eine Liste mit Animationen, und der Animationsindex wird gespeichert. Um eine bestimmte Animation zu rendern, nimmt das Spiel einen Index, sucht ihn in der Liste der Animationen und rendert ihn dann. Dies erleichtert die Arbeit ein wenig, da Sie keine Animationen scannen müssen, um die gewünschte zu finden.

Wir reinigen die Suppen von den Strukturen


Schauen wir uns die Kartentypen an:

  1. Sprite-Karten: Eine Liste von Sprites, bestehend aus einer anfänglichen Kachel, der Anzahl der Kacheln, der Position, dem Reflexionsstatus (Sprite wird gespiegelt oder nicht) und einer Palette.
  2. DPLC: Eine Liste von ROM-Kacheln, die in VRAM geladen werden müssen. Jedes Element in einer DPLC besteht aus einer anfänglichen Kachel und einer Länge. Jedes Element wird nach dem letzten in VRAM platziert.
  3. Animationskarten: Eine Liste von Animationen, die aus einer Liste von Sprite-Karten, Geschwindigkeitswerten und Zyklusaktionen besteht.
  4. Animationsliste: Eine Liste von Zeigern auf die Aktion jeder Animation.

Da wir mit Game Boy arbeiten, können einige Vereinfachungen vorgenommen werden. Wir wissen, dass es in Sprite-Karten in einem 8x16-Sprite immer zwei Kacheln gibt. Alles andere muss jedoch erhalten bleiben. Im Moment können wir DPLC komplett aufgeben und einfach alles in VRAM speichern. Dies ist eine vorübergehende Lösung, aber wie gesagt, dieses Problem wird leicht zu lösen sein. Schließlich können wir den Geschwindigkeitswert verwerfen, wenn wir davon ausgehen, dass jede Animation mit derselben Geschwindigkeit arbeitet.

Beginnen wir damit, herauszufinden, wie ich ein ähnliches System in meinem Spiel implementieren kann.

Überprüfen Sie mit Commit 2e5e5b7 !

Beginnen wir mit Sprite-Karten. Jedes Element in der Karte sollte OAM (Object Attribute Memory - Sprite VRAM) spiegeln. Daher reichen eine einfache Schleife und ein Memcpy aus, um das Objekt anzuzeigen. Ich möchte Sie daran erinnern, dass ein Element in OAM aus Y, X, einer Anfangskachel und einem Attributbyte besteht . Ich muss nur eine Liste von ihnen erstellen. Unter Verwendung des zusammengesetzten Pseudooperators EQU habe ich das Attributbyte im Voraus vorbereitet, sodass ich für jede mögliche Kombination von Attributen einen lesbaren Namen hatte. (Sie können sehen, dass ich beim vorherigen Festschreiben die Y / X-Kachel in den Karten ersetzt habe. Dies geschah, weil ich versehentlich die OAM-Spezifikationen gelesen habe. Ich habe auch einen Sprite-Zähler hinzugefügt, um zu wissen, wie lange die Schleife dauern sollte.)

Sie werden feststellen, dass Körper und Schwanz des Polarfuchses getrennt aufbewahrt werden. Wenn sie zusammen gespeichert würden, würde es viel Redundanz geben, da jede Animation für jeden Endzustand dupliziert werden müsste. Und das Ausmaß der Redundanz würde schnell zunehmen. In Sonic 2 trat das gleiche Problem bei Tails auf. Sie haben es dort gelöst und Tails Tails zu einem separaten Objekt mit einem eigenen Animationsstatus und Timer gemacht. Ich möchte dies nicht tun, weil ich nicht versuche, das Problem der Beibehaltung der richtigen Schwanzposition relativ zum Fuchs zu lösen.

Ich habe das Problem durch Animationskarten gelöst. Wenn Sie sich meine (einzelne) Animationskarte ansehen, enthält sie drei Metadaten. Es zeigt die Anzahl der Animationskarten, sodass ich weiß, wann sie enden werden. (In Sonic wird überprüft, ob die folgende Animation ungültig ist, ähnlich dem Konzept des Null-Bytes in C-Zeilen. Eine Lösung von Sonic gibt den Fall frei, fügt jedoch einen Vergleich hinzu, der gegen mich funktionieren würde.) Natürlich gibt es immer noch eine Schleifenaktion. (Ich habe die 2-Byte-Sonic-Schaltung in eine 1-Byte-Zahl umgewandelt, in der Bit 7 das Modusbit ist.) Ich habe aber auch die Anzahl der Sprite-Karten , aber nicht in Sonic. Wenn ich mehrere Sprite-Maps pro Animationsrahmen habe, kann ich Animationen in mehreren Animationen wiederverwenden, was meiner Meinung nach viel wertvollen Platz spart. Sie können auch feststellen, dass die Animationen für jede Richtung dupliziert werden. Dies liegt daran, dass die Karten für jede Richtung unterschiedlich sind und Sie sie hinzufügen müssen.

Bild

Tanzen mit Registern


Siehe diese Datei unter 1713848.

Beginnen wir mit dem Zeichnen eines einzelnen Sprites auf dem Bildschirm. Also, ich gestehe, ich habe gelogen. Ich möchte Sie daran erinnern, dass wir außerhalb von VBlank nicht auf dem Bildschirm aufnehmen können. Und dieser ganze Prozess ist zu lang, um in VBlank zu passen. Daher müssen wir den Speicherbereich aufzeichnen, den wir für DMA zuweisen. Am Ende ändert sich nichts, es ist wichtig, an der richtigen Stelle aufzunehmen.

Beginnen wir mit dem Zählen der Register. Der GBZ80-Prozessor verfügt über 6 Register, von A bis E, H und L. H und L sind spezielle Register, sodass sie sich gut für die Durchführung von Iterationen aus dem Speicher eignen. (Da sie zusammen verwendet werden, heißen sie HL.) In einem Opcode kann ich in die in HL enthaltene Speicheradresse schreiben und eine hinzufügen. Das ist schwer zu handhaben. Sie können es entweder als Quelle oder als Ziel verwenden. Ich habe es als Adressen und die Kombination von BC-Registern als Quelle verwendet, weil es am bequemsten war. Wir haben nur A, D und E. Ich brauche Register A für mathematische Operationen und dergleichen. Wofür kann DE verwendet werden? Ich benutze D als Schleifenzähler und E als Arbeitsbereich. Und hier endeten die Register.

Nehmen wir an, wir haben 4 Sprites. Wir setzen das D-Register (Zykluszähler) auf 4, das HL-Register (Ziel) die OAM-Pufferadresse und BC (die Quelle) den Ort im ROM, an dem unsere Karten gespeichert sind. Jetzt möchte ich memcpy anrufen. Es tritt jedoch ein kleines Problem auf. Erinnern Sie sich an die X- und Y-Koordinaten? Sie sind relativ zum Startpunkt angegeben, die Mitte des Objekts wird für Kollisionen und dergleichen verwendet. Wenn wir sie so aufnehmen, wie sie sind, wird jedes Objekt in der oberen linken Ecke des Bildschirms angezeigt. Das passt nicht zu uns. Um dies zu beheben, müssen wir die X- und Y-Koordinaten des Objekts zu X und Y des Sprites hinzufügen.

Kurzer Hinweis: Ich spreche von „Objekten“, habe Ihnen dieses Konzept jedoch nicht erklärt. Ein Objekt ist einfach eine Reihe von Attributen, die einem Objekt in einem Spiel zugeordnet sind. Attribute sind eine Position, Geschwindigkeit, Richtung. Artikelbeschreibung usw. Ich spreche darüber, weil ich X- und Y-Daten aus diesen Objekten extrahieren muss. Dazu benötigen wir einen dritten Satz von Registern, die auf die Stelle im RAM der Objekte zeigen, an denen sich die Koordinaten befinden. Und dann müssen wir X und Y irgendwo speichern. Gleiches gilt für die Richtung, da es uns hilft zu bestimmen, in welche Richtung die Sprites schauen. Außerdem müssen wir alle Objekte rendern, sodass sie auch einen Schleifenzähler benötigen. Und wir sind noch nicht zu den Animationen gekommen! Alles gerät schnell außer Kontrolle ...

Entscheidungsüberprüfung


Also renne ich zu weit voraus. Lassen Sie uns zurückgehen und über jedes Datenelement nachdenken, das ich verfolgen muss, und wo ich es schreiben soll.

Lassen Sie uns dies zunächst in „Schritte“ unterteilen. Jeder Schritt sollte nur Daten für den nächsten empfangen, mit Ausnahme des letzten, der die Kopie ausführt.

  1. Objekt (Schleife) - Findet heraus, ob das Objekt gerendert werden soll, und rendert es.
  2. Animationsliste - Legt fest, welche Animation angezeigt werden soll. Ruft auch die Attribute eines Objekts ab.
  3. Animation (Schleife) - legt fest, welche Liste von Karten verwendet werden soll, und rendert jede Karte daraus.
  4. Karte (Zyklus) - Durchläuft iterativ jedes Sprite in der Liste der Sprites
  5. Sprite - kopiert Sprite-Attribute in den OAM-Puffer

Für jede der Phasen habe ich die Variablen aufgelistet, die sie benötigen, die Rollen, die sie spielen, und die Orte, an denen sie gespeichert werden. Diese Tabelle sieht ungefähr so ​​aus.

BeschreibungGrößeBühneVerwenden SieWoher?PlatzierenWohin
OAM-Puffer2SpriteZeigerHlHl
Kartenquelle2SpriteZeigerBCBC
Aktuelles Byte1SpriteArbeitsbereichKartenquelleE.
X.1SpriteVariableHiramA.
Y.1SpriteVariableHiramA.
Start der Animationskarte2Sprite-KarteZeigerStack3DE
Kartenquelle2Sprite-KarteZeiger[DE]BC
Verbleibende Sprites1Sprite-KarteKratzerKartenquelleD.
OAM-Puffer1Sprite-KarteZeigerHlHlStack1
Start der Animationskarte2AnimationArbeitsbereichBC / Stack3BCStack3
Verbleibende Karten1AnimationArbeitsbereichAnimationsstartHiram
Gesamtzahl der Karten1AnimationenVariableAnimationsstartHiram
Objektrichtung1AnimationVariableHiramHiram
Karten pro Frame1AnimationVariableAnimationsstartWIRD NICHT BENUTZT!!!
Rahmennummer1AnimationVariableHiramA.
Kartenzeiger2AnimationZeigerAnimStart + Dir * TMC + MpF * F #BCDE
OAM-Puffer2AnimationZeigerStack1Hl
Beginn der Animationstabelle2AnimationslisteArbeitsbereichHarter SatzDE
Objektquelle2AnimationslisteZeigerHlHlStack2
Rahmennummer1AnimationslisteVariableObjektquelleHiram
Animationsnummer1AnimationslisteArbeitsbereichObjektquelleA.
X Objekt1Liste der ObjekteVariableObjektquelleHiram
Y Objekt1AnimationslisteVariableObjektquelleHiram
Objektrichtung1AnimationslisteVariableObj srcHiram
Start der Animationskarte2AnimationslisteZeiger[Anim Table + Anim #]BC
OAM-Puffer2AnimationslisteZeigerDEStack1
Objektquelle2ObjektzyklusWegweiserHard Set / Stack2Hl
Verbleibende Objekte1ObjektzyklusVariableBerechnetB.
Aktives Bitfeld eines Objekts1ObjektzyklusVariableBerechnetC.
OAM-Puffer2ObjektzyklusZeigerHarter SatzDE

Ja, sehr verwirrend. Um ganz ehrlich zu sein, habe ich diese Tabelle nur für den Beitrag erstellt, um sie klarer zu erklären, aber sie hat bereits begonnen, nützlich zu sein. Ich werde versuchen, es zu erklären. Beginnen wir am Ende und kommen zum Anfang. Sie sehen alle Daten, mit denen ich beginne: die Quelle des Objekts, den OAM-Puffer und die vorberechneten Schleifenvariablen. In jedem Zyklus beginnen wir mit diesem und nur diesem, außer dass die Quelle des Objekts in jedem Zyklus aktualisiert wird.

Für jedes Objekt, das wir rendern, muss die angezeigte Animation definiert werden. Währenddessen können wir auch die Attribute X, Y, Frame # und Direction speichern, bevor wir den Objektzeiger auf das nächste Objekt erhöhen und sie auf dem Stapel speichern, um sie beim Beenden zurückzunehmen. Wir verwenden die Animationsnummer in Kombination mit der im Code fest codierten Animationstabelle, um zu bestimmen, wo die Animationskarte beginnt. (Hier vereinfache ich, vorausgesetzt, jedes Objekt hat dieselbe Animationstabelle. Dies beschränkt mich auf 256 Animationen pro Spiel, aber es ist unwahrscheinlich, dass ich diesen Wert überschreite.) Wir können auch einen OAM-Puffer schreiben, um mehrere Register zu speichern.

Nach dem Extrahieren der Animationskarte müssen wir herausfinden, wo sich die Liste der Sprite-Karten für den angegebenen Frame und die Richtung befindet und wie viele Karten gerendert werden müssen. Möglicherweise stellen Sie fest, dass die Kartenvariable pro Frame nicht verwendet wird. Es ist passiert, weil ich nicht nachgedacht und den konstanten Wert 2 eingestellt habe. Ich muss ihn reparieren. Wir müssen auch den OAM-Puffer aus dem Stapel extrahieren. Möglicherweise stellen Sie auch einen völligen Mangel an Zykluskontrolle fest. Es wird in einer separaten, viel einfacheren Unterprozedur ausgeführt, mit der Sie das Jonglieren mit Registern loswerden können.

Danach wird alles ganz einfach. Eine Karte besteht aus einer Reihe von Sprites, daher gehen wir in einer Schleife um sie herum und zeichnen basierend auf den gespeicherten X- und Y-Koordinaten. Wir speichern jedoch erneut den OAM-Zeiger am Ende der Sprite-Liste, sodass die nächste Karte dort beginnt, wo wir fertig sind.

Was war das Endergebnis von all dem? Genau das gleiche wie zuvor: Ein Polarfuchs schwenkt seinen Schwanz im Dunkeln. Das Hinzufügen neuer Animationen oder Sprites ist jetzt viel einfacher. Im nächsten Teil werde ich über komplexe Hintergründe und Parallaxen-Scrollen sprechen.

Bild

Teil 4. Parallaxenhintergrund


Ich möchte Sie daran erinnern, dass wir zum gegenwärtigen Zeitpunkt Sprites auf einem festen schwarzen Hintergrund animiert haben. Wenn ich nicht vorhabe, ein Arcade-Spiel der 70er Jahre zu machen, wird dies eindeutig nicht ausreichen. Ich brauche eine Art Hintergrundbild.

Im ersten Teil, als ich Grafiken zeichnete, habe ich auch mehrere Hintergrundkacheln erstellt. Es ist Zeit, sie zu benutzen. Wir werden drei "grundlegende" Arten von Kacheln (Himmel, Gras und Erde) und zwei Übergangskacheln haben. Alle von ihnen sind in VRAM geladen und einsatzbereit. Jetzt müssen wir sie nur noch im Hintergrund schreiben.

Hintergrund


Die Hintergründe des Game Boy werden in einem 32x32-Array von 8x8-Kacheln gespeichert. Alle 32 Bytes entsprechen einer Kachelzeile.


Bisher habe ich vor, dieselbe Kachelspalte im gesamten 32x32-Bereich zu wiederholen. Das ist großartig, aber es schafft ein kleines Problem: Ich muss jede Kachel 32 Mal hintereinander setzen. Das Schreiben wird lange dauern.

Instinktiv entschied ich mich, den Befehl REPT zu verwenden, um 32 Bytes / Zeile hinzuzufügen, und dann memcpy zu verwenden, um den Hintergrund in VRAM zu kopieren.

 REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ... 

Dies bedeutet jedoch, dass Sie 256 Bytes nur für einen Hintergrund zuweisen müssen, was ziemlich viel ist. Dieses Problem wird noch verschärft, wenn Sie sich daran erinnern, dass Sie beim Kopieren einer zuvor erstellten Hintergrundkarte mit memcpy keine anderen Spaltentypen (z. B. Tore, Hindernisse) hinzufügen können, ohne dass eine erhebliche Komplexität und eine Menge verschwendeter ROM-Kassetten erforderlich sind.

Also habe ich stattdessen beschlossen, eine einzelne Spalte wie folgt einzurichten:

 db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS 

Verwenden Sie dann eine einfache Schleife, um jedes Element in dieser Liste 32 Mal zu kopieren. (Siehe LoadGFX Datei LoadGFX von Commit 739986a .)

Der Vorteil dieses Ansatzes besteht darin, dass ich später eine Warteschlange hinzufügen kann, um so etwas zu schreiben:

 BGCOL_Field: db BG_SKY, ... BGCOL_LeftGoal: db BG_SKY, ... BGCOL_RightGoal: db BG_SKY, ... ... BGMAP_overview: db 1 dw BGCOL_LeftGoal db 30 dw BGCOL_Field db 1 dw BGCOL_RightGoal db $FF 

Wenn ich mich entscheide, BGMAP_overview zu rendern, wird 1 Spalte von LeftGoal gezeichnet, danach werden 30 Spalten von Field und 1 Spalte von RightGoal angezeigt. Wenn sich BGMAP_overview im RAM befindet, kann ich es abhängig von der Kameraposition in X im BGMAP_overview ändern.

Kamera und Position


Oh ja, die Kamera. Dies ist ein wichtiges Konzept, über das ich noch nicht gesprochen habe. Hier haben wir es mit einer Vielzahl von Koordinaten zu tun. Bevor wir also über die Kamera sprechen, werden wir dies alles zunächst analysieren.

Wir müssen mit zwei Koordinatensystemen arbeiten. Das erste sind die Bildschirmkoordinaten . Dies ist ein Bereich von 256 x 256, der im VRAM der Game Boy-Konsole enthalten sein kann. Wir können den sichtbaren Teil des Bildschirms innerhalb dieser 256x256 scrollen, aber wenn wir über die Grenzen hinausgehen, kollabieren wir.

In der Breite benötige ich mehr als 256 Pixel, also füge ich Weltkoordinaten hinzu , die in diesem Spiel Abmessungen von 65536 x 256 haben. (Ich brauche keine zusätzliche Höhe in Y, da das Spiel auf einem flachen Feld stattfindet.) Dieses System ist vollständig vom Bildschirmkoordinatensystem getrennt. Alle Physik und Kollisionen müssen in Weltkoordinaten ausgeführt werden, da sonst die Objekte mit Objekten auf anderen Bildschirmen kollidieren.


Vergleich von Bildschirm- und Weltkoordinaten

Da die Positionen aller Objekte in Weltkoordinaten dargestellt werden, müssen sie vor dem Rendern in Bildschirmkoordinaten konvertiert werden. Am äußersten linken Rand der Welt stimmen die Weltkoordinaten mit den Bildschirmkoordinaten überein. Wenn wir die Dinge rechts auf dem Bildschirm anzeigen müssen, müssen wir alles in Weltkoordinaten nehmen und nach links verschieben, so dass sie in Bildschirmkoordinaten sind.

Dazu setzen wir die Variable „Kamera X“, die als linker Rand des Bildschirms in der Welt definiert ist. Wenn beispielsweise camera X 1000 ist, können wir die Weltkoordinaten 1000-1192 sehen, da der sichtbare Bildschirm eine Breite von 192 Pixel hat.

Um Objekte zu verarbeiten, nehmen wir einfach ihre Position in X (z. B. 1002), subtrahieren die Kameraposition gleich 1000 und zeichnen das Objekt an der durch die Differenz gegebenen Position (in unserem Fall 2). Für einen Hintergrund, der nicht in Weltkoordinaten angegeben ist, aber bereits in Bildschirmkoordinaten beschrieben ist, setzen wir die Position gleich dem unteren Byte der camera X Variablen. Dank dessen wird der Hintergrund mit der Kamera nach links und rechts gescrollt.

Parallaxe


Das von uns erstellte System sieht ziemlich flach aus. Jede Hintergrundebene bewegt sich mit der gleichen Geschwindigkeit. Es fühlt sich nicht dreidimensional an und wir müssen es reparieren.

Eine einfache Möglichkeit, eine 3D-Simulation hinzuzufügen, ist das Parallaxen-Scrollen. Stellen Sie sich vor, Sie fahren auf einer Straße und sind sehr müde. Dem Game Boy sind die Batterien ausgegangen und Sie müssen aus dem Autofenster schauen. Wenn Sie auf den Boden neben sich schauen, werden Sie sehen. dass sie sich mit einer Geschwindigkeit von 70 Meilen pro Stunde bewegt. Wenn Sie sich jedoch die Felder in der Ferne ansehen, scheinen sie sich viel langsamer zu bewegen. Und wenn man sich die sehr fernen Berge ansieht, scheinen sie sich kaum zu bewegen.

Wir können diesen Effekt mit drei Blatt Papier simulieren. Wenn Sie eine Bergkette auf ein Blatt zeichnen, das Feld auf das zweite und die Straße auf das dritte Blatt und legen Sie sie so übereinander. Damit jede Schicht sichtbar ist, ist sie eine Nachahmung dessen, was wir vom Autofenster aus sehen. Wenn wir das „Auto“ nach links bewegen wollen, bewegen wir das oberste Blatt (mit der Straße) weit nach rechts, das nächste ist ein wenig nach rechts und das letzte ist ein wenig nach rechts.



Bei der Implementierung eines solchen Systems auf Game Boy tritt jedoch ein kleines Problem auf. Die Konsole hat nur eine Hintergrundebene. Dies ähnelt der Tatsache, dass wir nur ein Blatt Papier haben. Sie können keinen Parallaxeeffekt mit nur einem Blatt Papier erzeugen. Oder ist es möglich?

H-blank


Der Game Boy-Bildschirm wird zeilenweise gerendert. Aufgrund der Nachahmung des Verhaltens alter CRT-Fernseher kommt es zwischen den einzelnen Leitungen zu einer leichten Verzögerung. Was ist, wenn wir es irgendwie benutzen können? Es stellt sich heraus, dass Game Boy speziell für diesen Zweck einen speziellen Hardware-Interrupt hat.

Ähnlich wie beim VBlank-Interrupt, bei dem wir ständig bis zum Ende des Frames auf die Aufnahme im VRAM gewartet haben, gibt es einen HBlank-Interrupt. Indem Sie Bit 6 des Registers auf $FF41 , den LCD STAT Interrupt $FF41 und die Zeilennummer auf $FF45 , können Sie Game Boy $FF45 , den LCD STAT Interrupt zu starten, wenn die angegebene Linie gezeichnet werden soll (und wenn sie sich in der HBlank befindet).

Während dieser Zeit können wir alle VRAM-Variablen ändern. Dies ist nicht viel Zeit, daher können wir nicht mehr als ein paar Register ändern, aber wir haben noch einige Möglichkeiten. Wir wollen das horizontale $FF43 bei $FF43 . In diesem Fall bewegt sich alles auf dem Bildschirm unterhalb der angegebenen Linie um eine bestimmte Verschiebung, wodurch ein Parallaxeeffekt entsteht.

Wenn Sie zum Bergbeispiel zurückkehren, können Sie ein potenzielles Problem feststellen. Berge, Wolken und Blumen sind keine flachen Linien! Wir können die ausgewählte Zeile während des Rendervorgangs nicht nach oben und unten verschieben. Wenn wir es wählen, bleibt es mindestens bis zum nächsten HBlank gleich. Das heißt, wir können nur in geraden Linien schneiden.

Um dieses Problem zu lösen, müssen wir etwas schlauer vorgehen. Wir können eine Linie im Hintergrund als eine Linie deklarieren, die nichts kreuzen kann, was bedeutet, dass die Modi von Objekten darüber und darunter geändert werden und der Spieler nichts bemerken kann. Hier befinden sich beispielsweise diese Linien in der Szene mit dem Berg.


Hier habe ich direkt über und unter dem Berg Scheiben gemacht. Alles von der oberen bis zur ersten Linie bewegt sich langsam, alles bis zur zweiten Linie bewegt sich mit einer Durchschnittsgeschwindigkeit und alles unterhalb dieser Linie bewegt sich schnell. Dies ist ein einfacher, aber kluger Trick. Und wenn Sie davon erfahren, können Sie es in vielen Retro-Spielen bemerken, hauptsächlich für Genesis / Mega Drive, aber auch auf anderen Konsolen. Eines der offensichtlichsten Beispiele ist der Teil der Höhle von Mickey Mania. Sie können feststellen, dass die Stalagmiten und Stalaktiten im Hintergrund genau entlang einer horizontalen Linie mit einem offensichtlichen schwarzen Rand zwischen den Schichten getrennt sind.

In meinem Hintergrund wurde mir dasselbe klar. Es gibt jedoch einen Trick. Angenommen, der Vordergrund bewegt sich mit einer Geschwindigkeit eins zu eins, die mit der Bewegung der Kamera zusammenfällt, und die Hintergrundgeschwindigkeit beträgt ein Drittel der Pixelbewegung der Kamera, dh der Hintergrund bewegt sich wie ein Drittel des Vordergrunds. Aber natürlich existiert ein Drittel des Pixels nicht. Daher muss ich den Hintergrund für jeweils drei Pixel Bewegung um ein Pixel verschieben.

Wenn Sie mit Computern arbeiten, die mathematische Berechnungen ausführen können, nehmen Sie die Kameraposition, teilen Sie sie durch 3 und machen Sie diesen Wert zu einem Hintergrundversatz. Leider ist der Game Boy nicht in der Lage, die Teilung durchzuführen, ganz zu schweigen von der Tatsache, dass die Programmteilung ein sehr langsamer und schmerzhafter Prozess ist. Das Hinzufügen eines Geräts zum Teilen (oder Multiplizieren) einer schwachen CPU für eine tragbare Unterhaltungskonsole in den 80er Jahren schien kein kostengünstiger Schritt zu sein, daher müssen wir einen anderen Weg erfinden.

Im Code habe ich Folgendes getan: Anstatt die Kameraposition aus einer Variablen zu lesen, habe ich verlangt, dass sie zunimmt oder abnimmt. Dank dessen kann ich mit jedem dritten Inkrement ein Inkrement der Hintergrundposition und mit jedem ersten Inkrement ein Inkrement der Vordergrundposition durchführen. Dies erschwert ein wenig das Scrollen zu einer Position vom anderen Rand des Feldes (der einfachste Weg besteht darin, die Positionen der Ebenen nach einem bestimmten Übergang einfach zurückzusetzen), erspart uns jedoch das Teilen.

Ergebnis


Nach all dem habe ich folgendes bekommen:


Für ein Spiel auf Game Boy ist das eigentlich ziemlich cool. Soweit ich weiß, haben nicht alle das Parallaxen-Scrollen so implementiert.

Source: https://habr.com/ru/post/de436918/


All Articles