© Liashko/Shutterstock.com
Classic Games Reloaded – Teil 8

Geskriptete Spielverläufe und virtuelle Maschinen programmieren


Skriptsprachen erfreuen sich gegenwärtig einer großen Beliebtheit. Aus diesem Grund werden wir uns im heutigen Artikel damit auseinandersetzen, wie sich derartige Sprachen im Rahmen der Spieleentwicklung einsetzen lassen und auf welche Weise die zugehörigen virtuellen Maschinen im Hintergrund ihre Arbeit verrichten.

Im Verlauf der vorangegangenen Artikel habe ich Ihnen bereits mehrfach demonstriert, wie sich abwechslungsreiche Spielewelten dank einer strikten Trennung der jeweils benötigten Daten vom Programmcode unserer kleinen Demoanwendungen auch ohne Programmierkenntnisse im Handumdrehen erschaffen lassen. Betrachten wir als erstes Beispiel einmal unseren Breakout-Klon. Der Spieler kann zwischen einer kreisförmigen und einer rechteckigen Spielfeldbegrenzung wählen, die Farbe des Begrenzungsrahmens festlegen, die Position und Ausrichtung der Kamera variieren, die Punktevergabe modifizieren oder auch die Höhe der Zeitstrafen bestimmen. Darüber hinaus obliegt dem Spieler die Positionierung, Texturierung und Skalierung der Spieler- und Hindernisbälle sowie der Hindernisblöcke und Schläger. Der Schwierigkeitsgrad des Spiels lässt sich einerseits über die Beschleunigung und Geschwindigkeit der einzelnen Bälle regulieren und andererseits über eine Reihe von KI-Parametern (KI: künstliche Intelligenz), mit deren Hilfe der vom Computer kontrollierte Gegenspieler seine eigenen Schläger steuern kann. In unserem horizontalen bzw. vertikalen Shoot-’em-up kann der Spieler unter anderem die gegnerischen Spawn-Positionen und -Zeiten modifizieren und er hat die Möglichkeit, mittels im Vorfeld aufgezeichneter Bewegungsabläufe (Movement-Patterns) die Angriffs- und Verteidigungsmanöver der feindlichen Raumschiffe festzuschreiben. In unserem Asteroids-Klon kann der Spieler zusätzlich zu den zuletzt genannten Optionen einerseits das Bewegungsmodell des von ihm gesteuerten Raumgleiters justieren (Schubkraft des Hauptantriebs, der Bremsraketen sowie der Manövriertriebwerke für eine Rotation in der Bildschirmebene) und andererseits die Anfangspositionen, Translations- sowie die Rotationsgeschwindigkeiten der einzelnen Asteroiden festlegen. Auch auf die Bewegungssimulation der Asteroiden samt ihrem Verhalten bei einer möglichen Kollision kann Einfluss genommen werden, und zwar durch die Wahl des zum Einsatz kommenden Physikmodells. Als wir uns dann im weiteren Fortgang mit der Entwicklung einfacher rundenbasierter Strategiespiele auseinandergesetzt haben, sind wir unter anderem auf die Arbeitsweise einer regelbasierten KI zu sprechen gekommen.

Salopp ausgedrückt definiert man sich eine mehr oder weniger große Anzahl von Funktionen oder Methoden, mit deren Hilfe die KI später einmal ganz spezifische Aktionen ausführen kann. In welchen Situationen die einzelnen Funktionen aufgerufen werden, ist nun vom zugehörigen Regelwerk abhängig. Hierbei handelt es sich im einfachsten Fall um eine Reihe von KI-Parametern, mittels derer wir auf die Beurteilung der einzelnen Situationen Einfluss nehmen können.

Trennung von Daten und Maschinencode – gut, doch es geht noch besser

Die Trennung der benötigten Daten vom Programmcode ist zweifelsohne ein wichtiger Schritt, um die Flexibilität einer Anwendung zu vergrößern. Am Ziel unserer Reise sind wir damit jedoch noch lange nicht angekommen. Wir haben gerade einmal die erste Etappe bewältigt, wie man so schön sagt. Indem wir nun die Werte von einzelnen Parametern modifizieren, können wir zwar beispielsweise das Spielgeschehen oder das Verhalten der KI beeinflussen, jedoch werden die Grenzen, in denen sich diese Änderungen bewegen können, letzten Endes exakt durch den Programmcode vorgegeben. Darüber hinausgehende Anpassungen seitens der Spieler (Modding) wären nur möglich, sofern diese Zugriff auf den Source Code hätten (was lediglich bei Open-Source-Spielen der Fall wäre) und diesen auch in einer integrierten Entwicklungsumgebung (Integrated Development Environment, kurz IDE) bearbeiten könnten, was selbstverständlich ein gewisses Maß an Programmierkenntnissen voraussetzt.

Fassen wir zusammen: Gibt man sich mit der simplen Trennung von Daten und Maschinencode zufrieden, so ist es für einen Spieler bzw. Modder im Nachhinein beispielsweise nicht mehr möglich, neue KI-Routinen in ein Spiel zu integrieren, ein Abenteuerspiel mit weiteren Zaubersprüchen aufzupeppen oder ein Spiel mittels zusätzlicher geskripteter Ereignisse abwechslungsreicher zu gestalten. Allenfalls könnte man die Auswirkungen der in das Spiel integrierten Zaubersprüche ein wenig abändern oder den Verlauf der geskripteten Ereignisse ein bisschen abwandeln. Und selbstverständlich sind auch die ganzen Nachteile, die sich durch die hier skizzierte Vorgehensweise für die Programmentwicklung ergeben, nicht zu unterschätzen. Zum einen wäre da die benötigte Kompilierzeit (Kompilierung: Umwandlung des Source Codes in Maschinencode), die mit einer stetig wachsenden Codebasis mehr und mehr zunimmt. Da man nach jeder noch so kleinen Änderung am Source Code stets das komplette Projekt aufs Neue kompilieren muss, sollte man den hierfür zu veranschlagenden Zeitaufwand besser nicht unterschätzen. Auch die zunehmende Komplexität des Source Codes könnte früher oder später einmal zu einem Problem werden. Spätestens wenn man den Überblick darüber verliert, welche der einzelnen Funktionen und Methoden sich in welcher Reihenfolge gegenseitig aufrufen, sollte man die Warnzeichen nicht mehr ignorieren, dass hinsichtlich der Softwarearchitektur so einiges im Argen zu liegen scheint.

Die oberste Maxime, die ein Programmierer stets beherzigen sollte, ist das Prinzip der Entkoppelung. Vereinfacht ausgedrückt führt die Entkoppelung der einzelnen Source-Code-Abschnitte bestenfalls dazu, dass man die jeweiligen Abschnitte mühelos nachvollziehen und modifizieren kann, ohne sich gleichzeitig mit den anderen Codeabschnitten herumärgern zu müssen. Eine besonders beliebte Strategie, um einerseits den Programmcode zu entkoppeln und andererseits die nachträgliche Modifizierung des Spielgeschehens zu erleichtern, besteht nun darin, lediglich die Low-Level-Funktionsabläufe mit Hilfe einer Compilersprache wie C++ zu implementieren. Was jedoch die Implementierung der High-Level-Funktionsabläufe betrifft (hierzu zählen unter anderem die KI-Routinen, die Handhabung von Zaubersprüchen, ja selbst die Aktualisierung der Spielewelt), greift man hingegen auf eine sogenannte Skriptsprache (Interpretersprache) zurück.

Im Unterschied zu einer Compilersprache wie C++ müssen die in einer Interpreter- bzw. Skriptsprache erstellten Programme vor ihrer Ausführung nicht erst in Maschinencode umgewandelt werden, da die betreffenden Programme bzw. Skripte von einem Interpreter bzw. von einer virtuellen Maschine ausgeführt werden. Mit anderen Worten: Ausführbare Skripte bieten uns unter anderem eine komfortable Möglichkeit, um ein Computerspiel auch ohne eine Neukompilierung des zugrunde liegenden C++-Codes zu modifizieren oder zu erweitern. Natürlich lassen sich aus einem Skript heraus nicht nur die Werte von einzelnen Variablen festlegen oder die auszugebenden Textstrings definieren. Wir können beispielsweise Vergleichsoperationen und Schleifen implementieren oder falls erforderlich auch ausgewählte Funktionen des mittels C++ implementierten Programmunterbaus (Hostprogramms) aufrufen.

Speichermanagement und Funktionsaufrufe im Rahmen eines C++-Programms

Die Implementierung einer virtuellen Maschine, mit deren Hilfe sich eine einfache Skriptsprache ausführen lässt, ist erstaunlicherweise gar nicht mal sonderlich kompliziert. Genau genommen ist hierfür nicht einmal ein tiefergehendes Verständnis über die Arbeitsweise einer CPU (Central Processing Unit) erforderlich. Es ist vollkommen ausreichend, wenn man sich ein wenig mit den Grundzügen einer Compilersprache wie C oder C++ auskennt. Im einfachsten Fall besteht ein kompiliertes C- bzw. C++-Programm lediglich aus einer einzelnen Hauptprogrammfunktion (z. B. main() oder WinMain()), die bei einem Programmstart durch das Betriebssystem aufgerufen wird. Bei umfangreicheren Anwendungen kommt man in der Regel nicht umhin, Teile der Anweisungen in benutzerdefinierte Funktionen auszulagern, deren Aufruf von jeder Stelle des Pro...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang