Neuerstellung eines alten DOS-Spiels in C ++ 17

2016 begann ich mit der Arbeit an einem Hobbyprojekt für das Reverse Engineering des Spiels Duke Nukem II und den Umbau seiner Engine von Grund auf neu. Das Projekt heißt Rigel Engine und ist in Open Source verfügbar ( seine Seite auf GitHub ). Heute, mehr als zweieinhalb Jahre später, können Sie auf meiner Engine bereits die gesamte Shareware-Episode des Originalspiels mit nahezu identischem Gameplay wie das Original durchgehen. Hier ist ein Video mit dem Durchgang der ersten Ebene:


Was kann er tun? Rigel Engine ersetzt die ursprüngliche DOS-Binärdatei ( NUKEM2.EXE ) NUKEM2.EXE . Sie können es in das Spielverzeichnis kopieren und alle Daten daraus berücksichtigen oder den Pfad zu den Spieldaten als Argument für die Befehlszeile angeben. Die Engine wird unter Windows, Mac OS X und Linux erstellt und ausgeführt. Es basiert auf SDL und OpenGL 3 / OpenGL ES 2 und ist in C ++ 17 geschrieben.

Es implementiert die Spiellogik aller Feinde und Spielmechaniken aus der Shareware-Episode sowie den größten Teil des Menüsystems. Außerdem können Sie gespeicherte Spiele und eine Highscore-Tabelle aus dem Originalspiel importieren.

Darüber hinaus hat der Motor Vorteile gegenüber dem Original:

  • Kein Emulator oder alte Hardware erforderlich, keine Einstellungen erforderlich
  • Keine Ladebildschirme - wählen Sie "Neues Spiel" im Menü, drücken Sie die Eingabetaste und starten Sie das Spiel sofort
  • Es können mehrere Soundeffekte gleichzeitig abgespielt werden, was im Original nicht möglich war
  • Es gibt keine Einschränkungen hinsichtlich der Anzahl der gleichzeitigen Auswirkungen von Partikeln, Explosionen usw.
  • Speichern Sie Dateien und Highscore-Listen für jeden Player
  • Viel reaktionsschnellere Menüs

Bisher halte ich die Rigel Engine nicht für "out of the box". Dies ist jedoch eine großartige Entwicklungsphase und eine gute Gelegenheit, erneut über den Motor zu schreiben (alte Beiträge hier und hier veröffentlicht ). Schauen wir uns zunächst den aktuellen Status des Codes an und finden heraus, wie ich dazu gekommen bin.

Wie viel Code ist in der Engine?


Zum Zeitpunkt des Schreibens besteht RigelEngine aus 270 Quelldateien mit mehr als 25.000 Codezeilen (keine Kommentare / Leerzeilen). Davon sind 10 Dateien und 2,5 Tausend Zeilen Komponententests. Eine detaillierte Aufschlüsselung der Leerzeilen und Kommentare finden Sie hier .

Was ist in all diesem Code? Ein bisschen gemeinsame Infrastruktur und unterstützende Funktionen, grundlegende Dinge wie das Rendern und ein paar kleine logische Elemente. Darüber hinaus sind die größten Teile:

  • Parser / Downloader für 14 verschiedene Dateiformate, die im ursprünglichen Spiel verwendet wurden - 2.000 Codezeilen (LOC)
  • Verhaltenslogik / Spiellogik für 24 Feinde / feindliche Objekte - 3,8k LOC
  • Spielelogik für 14 interaktive Elemente und Spielmechanik - 2k LOC
  • Spielersteuerungslogik - 1.2k LOC
  • 154 Konfigurationseinträge (der Gesundheitswert jedes Feindes, die Anzahl der Punkte, die für gesammelte Gegenstände erhalten wurden usw.) - 1k LOC
  • 31 Spezifikationen für Zerstörungseffekte (Effekte, die durch die Zerstörung eines Feindes oder eines anderen zerstörbaren Objekts ausgelöst werden) - 254 LOC
  • Kamerasteuerungscode - 159 LOC
  • Spielmenü Beschreibung Sprachinterpreter / Zwischensequenz - 643 LOC
  • Das HUD und anderer UI-Code ist 818 LOC
  • 5 Bildschirme / Modi außerhalb des Menüs, zum Beispiel die erste Animation, der Bonusbildschirm usw. - 789 LOC

Natürlich musste all dieser Code geschrieben werden, und dies führt uns zur nächsten Frage.

Wie viel Arbeit hat es gekostet?


Obwohl seit Beginn des Projekts zweieinhalb Jahre vergangen sind, habe ich die ganze Zeit nicht daran gearbeitet. Einige Monate lang habe ich überhaupt kein Projekt durchgeführt, in einigen anderen habe ich nur einige Stunden damit verbracht. Aber es gab Zeiten, in denen ich ziemlich aktiv an der Rigel-Engine gearbeitet habe. Wenn Sie sich den Commit-Zeitplan für Github ansehen, können Sie eine ungefähre Vorstellung davon bekommen, wie meine Arbeit über die Zeit verteilt war:


Gemäß dem Zeitplan sehen wir, dass 1081 Commits an die Hauptniederlassung vorgenommen wurden. Noch bevor das Repository erstellt wurde, arbeitete ich an einem geschlossenen Repository, in dem es 247 weitere Commits gab, was insgesamt 1328 Commits ergibt. Darüber hinaus gab es mehrere Prototypzweige, die ich für Forschung und Experimente verwendete, aber nie mit dem Hauptzweig kombinierte. Außerdem habe ich vor dem Zusammenführen manchmal große Commit-Storys in kürzere komprimiert.

Ich muss auch sagen, dass das Schreiben von Code nicht der einzige Teil des Projekts war - Reverse Engineering war ein weiterer wichtiger Teil. Ich habe einige Stunden damit verbracht, den zerlegten Code der ursprünglichen ausführbaren Datei in Ida Pro (in der kostenlosen Version) zu studieren, Notizen zu machen, Pseudocode zu schreiben und die Implementierung der Elemente meiner Version zu planen. Außerdem habe ich das Originalspiel aktiv getestet und es in DOSBox und auf der Originalausrüstung (verschiedene bei eBay gekaufte Maschinen 386 und 486) gestartet. Ich sammelte Teststufen für die getrennte Beobachtung bestimmter Feinde und das Studium der Spielmechanik, zeichnete Videoclips mit DOSBox auf und sah mir die Frames Frame für Frame an, um meine Schlussfolgerungen beim Studium des Assembler-Codes zu bestätigen. Nachdem der Feind oder die Spielmechanik erkannt worden war, nahm ich normalerweise einen Videoclip aus meiner Version auf und verglich ihn Frame für Frame mit dem Original, um die Genauigkeit meiner Implementierung zu bestätigen.

Hier sind einige Fotos meiner Notizen:


Reverse Engineering Kamerasteuerungscode. Ein großes Rechteck zeigt den Bildschirm an. Die gestrichelten Linien zeigen die Zonen, in denen sich ein Spieler bewegen kann, ohne die Kamera zu bewegen. Wenn Sie interessiert sind, finden Sie den Kamerasteuerungscode selbst hier .


Allgemeine Hinweise zum besseren Verständnis des Assembler-Codes. Auf der linken Seite sehen Sie das Verfahren zum Aktualisieren des Originalspiels auf hohem Niveau. Auf der rechten Seite befinden sich Hinweise zu Bitfeldern, die den Status einiger Spielobjekte angeben.


Transkription von Assembler-Code in Pseudocode. Normalerweise mache ich es mechanisch genug, transkribiere, ohne darüber nachzudenken, was der Code tut, und verwende dann die Version im Pseudocode, um die zugrunde liegende Logik zu verstehen. Und auf dieser Grundlage habe ich bereits meine Implementierung entwickelt. Den fertigen Code finden Sie hier .


Pseudocode einer bereinigten Version der feindlichen Logik. Die Header geben den Status der Zustandsmaschine an. Der folgende Code erklärt, was in den jeweiligen Zuständen geschehen soll. Es wurde auf der Basis eines rohen Pseudocodes erstellt, der durch Transkribieren von Assembler-Code erhalten wurde. Ready-Code finden Sie hier .

Am Ende erwies sich die Arbeit an dem Projekt als sehr aufregend, und ich habe viel daraus gelernt: über Reverse Engineering, 16-Bit-x86-Assembler, VGA-Programmierung auf niedriger Ebene, strenge Einschränkungen, denen sich PC-Spieleentwickler in den frühen 90er Jahren gegenübersehen mussten; Außerdem habe ich viele Entdeckungen über die internen Funktionen des Originalspiels gemacht und wie seltsam und bizarr einige davon implementiert wurden - dieses Thema an sich verdient eine separate Reihe von Beiträgen.

Was weiter


Neben dem Hinzufügen der letzten verbleibenden Funktionen und dem Abschluss der Unterstützung für die registrierte Version habe ich verschiedene Ideen zur Verbesserung und Erweiterung der Funktionen der Rigel Engine, ganz zu schweigen von der Bereinigung und Umgestaltung des Codes. Wie üblich wird der beste Weg zum Erstellen einer Softwarearchitektur erst nach Abschluss der Erstellung dieser Software ersichtlich.

In Bezug auf zukünftige Verbesserungen sind hier einige Punkte aufgeführt, über deren Implementierung ich nachgedacht habe:

  • Reibungslose Bewegung mit Interpolation. Das Spiel aktualisiert seine Logik ungefähr 15 Mal pro Sekunde und im ursprünglichen Spiel ist es auch die Framerate für das Rendern. Andererseits kann die Rigel Engine problemlos mit einer Frequenz von 60 FPS und höher arbeiten. Im Moment bieten diese zusätzlichen Frames keine Vorteile, aber ich denke, dass sie für Zwischenframes verwendet werden können, um ein reibungsloseres Scrollen und Bewegen von Objekten zu realisieren. Die Logik des Spiels funktioniert immer noch mit der gleichen Geschwindigkeit, aber die Objekte bewegen sich reibungslos und „springen“ nicht wie jetzt mit einem Inkrement von 8 Pixeln. Zuvor habe ich einen Prototyp eines solchen Systems erstellt, der großartig aussieht, obwohl er verbessert werden muss.
  • Gamepad-Unterstützung. Im ursprünglichen Spiel werden Joysticks unterstützt, und DosBox kann sie auf modernen Gamepads emulieren, ihre Einrichtung kann jedoch schwierig sein. Die Vorbereitung der Konfiguration und die Kalibrierung im Spiel sind erforderlich. Ganz zu schweigen davon, dass nicht alle Controller-Tasten unterstützt werden. Um das Menü zu verwenden, müssen Sie jedoch noch eine Tastatur verwenden. Daher glaube ich, dass die Unterstützung nativer Controller das Gameplay erheblich verbessern wird.
  • Klangverbesserung. Derzeit haben alle Soundeffekte die gleiche Lautstärke. Klangerzeugende Objekte, beispielsweise Kraftfelder, werden beim Auftreffen auf den Bildschirm scharf hörbar und brechen ebenso scharf ab. Ich war gespannt, wie sie klingen würden, wenn die Lautstärke der Effekte in der Ferne nachlassen würde. Zum Beispiel konnten wir das Kraftfeld kaum hören, wenn es noch nicht auf dem Bildschirm angezeigt wurde und wenn es sich näherte, wurde es lauter.
  • Remote-Kamera / Blick auf den größten Teil des Levels. Das Spiel wurde nicht dafür entwickelt, daher kann diese Möglichkeit das Gameplay beschädigen - der Spieler beginnt, Feinde zu sehen, die außerhalb des Bildschirms nicht aktiv sind, und dergleichen. Aber ich frage mich immer noch, wie es aussehen und spielen wird. Am Ende beschwerten sich die Spieler sehr oft über dieses Spiel, weil sie nicht in der Lage waren, einen ausreichenden Teil des Levels zu sehen. Es wäre interessant, die Option hinzuzufügen, das HUD auszuschalten oder es mithilfe von Transparenz durch ein minimaleres zu ersetzen.
  • Erhöhen Sie die Grafikauflösung. Diese Funktion wird häufig in vielen Ports / Neuerstellungen von Spielen verwendet, und es wäre großartig, sie hier hinzuzufügen. Mit der Engine können Sie Sprite-Grafiken bereits durch Ihre eigenen Bilder ersetzen. Bisher können sie jedoch keine höhere Auflösung aufweisen, da alles in einem kleinen Puffer gerendert und anschließend vergrößert wird. Zunächst müssen Sie diesen Ansatz ersetzen, damit die Skalierung für einzelne Objekte durchgeführt werden kann.

Ich habe keine Roadmap für die Zukunft, daher kann ich diese Punkte in beliebiger Reihenfolge umsetzen. Zuvor wird der nächste Schritt die Integration von Dear ImGui sein, um das Optionsmenü, das noch nicht im Spiel ist, weiter zusammenzustellen. Darüber hinaus werden die oben genannten Verbesserungen aktiviert oder deaktiviert. Am Ende werde ich sagen, dass ich für jede Unterstützung bei der Arbeit an GitHub dankbar sein werde!

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


All Articles