Tutorial Bomberman2
Inhaltsverzeichnis
- 1 Wir programmieren einen Bombermanklon! Teil 1 : Die Codebasis und der Editor
- 1.1 Vorwort
- 1.2 Voraussetzungen
- 1.3 Ziel
- 1.4 Projektstruktur
- 1.5 Verwendete Techniken
- 1.6 Verwendete Dateiformate
- 1.7 Der Quellcode
- 1.8 Schlusswort
- 1.9 Dateien
Wir programmieren einen Bombermanklon!
Teil 1 : Die Codebasis und der Editor
Vorwort
Ave, und willkommen zum zweiten Tutorial der Bombermanserie! Nachdem wir uns im ersten Tutorial mit der Programmierung eines Editor und der grundlegenden Codebasis beschäftigt haben, gehts nun ans Eingemachte, nämlich den 3D-Bombermanklon.
Da der Quellcode unseres Klons recht umfangreich ist, und in diesem Tutorial viele neue Techniken behandelt werden, wirds vielleicht etwas langatmig. Aber keine Angst, denn zum einen versuche ich alles recht ausführlich und auch für Neulinge verständlich zu erklären, und zum anderen werdet ihr am Ende dieses Tutorials einen kompletten Bombermanklon programmiert haben der euch (und eure Bekannten) mit stundenlangem Spielspaß belohnt!
Deshalb gibts zum Ansporn erstmal zwei nette Screenshots direkt aus dem Spiel :
Voraussetzungen
Wie schon im ersten Tutorial vorausgesetzt, solltet ihr recht gute Kenntnisse in Object Pascal und OpenGL mitbringen. Des Weiteren werde ich in unserem Quellcode starken Gebrauch von dynamischen Arrays machen, deren Nutzung und Verwaltung besonders für Personen die sich damit noch nicht beschäftigt haben recht verwirrend sein kann. Deshalb sind gute Kenntnisse zum Thema dynamische Arrays unentbehrlich. Aber keine Angst, denn alles was man zu diesem Thema braucht, findet sich in der Delphi-Hilfe.
Neben der programmiertechnischen Seite, erfordert das Erschaffen eines Spiels natürlich auch den Umgang mit diversen Anwendungen. Einige davon sind kleine Tools, die sich mit einem Klick bedienen lassen während andere Programme eine nicht unerheblich Einarbeitungszeit voraussetzen. Die von mir genutzten Anwendungen und Tools im Überblick :
Soviel also zu den (von mir) genutzten Anwendungen und Tools. Natürlich steht euch die Wahl frei, und ihr könnt die Tools und Programme eurer Wahl nutzen.
Ziel
Wie schon kurz im Vorwort erwähnt, werden wir in diesem Tutorial einen kompletten Bombermanklon programmieren. Ziel dabei ist es, eine saubere Codebasis zu schreiben und euch grundlegende Techniken der Spieleprogrammierung beizubringen.
Außerdem möchte ich euch einen recht sauberen Programmierstil beibringen, der bei vielen missen lässt und Programmcodes für andere oft unnutzbar macht.
Folgendes wird euch im Tutorial erwarten, und euch auch bei zukünftigen Ausflügen in die Welt der Spieleprogrammierung von Nutzen sein :
- Timebased Movement (Frameunabhängige Bewegungen)
- Texturenfonts
- Statemachine
- Kollisionsabfrage
- Sound (via FMOD)
Projektstruktur
Widmen wir uns nun kurz der Projektstruktur. Gemeint sind damit die genutzten Units, deren Funktion und Inhalt. Was genau wo gemacht wird, darüber werden wir uns später unterhalten.
Bei eurer Projektstruktur solltet ihr vor allem darauf achten, nicht alles in eine Unit zu packen. Zum einen kann das später nämlich eingige Probleme machen (ich sage nur Kreuzbezüge) und zum anderen schadet es der Portabilität. Damit sind z.B. wiederverwendbare Programmteile wie der Texturenmanager oder das Soundsystem gemeint, die wenn sie in eine externe Unit ausgelagert werden, später ohne großen Aufwand in andere Projekte eingebunden werden können. Dadurch spart man sich über die Zeit sehr viel Arbeit und baut sich nach und nach mit jedem Projekt eine größere Codebasis auf.
Wichtig ist natürlich (wie auch bei euren Variablen) die Namensgebung. Es scheint zwar verlockend, Objekt- oder Unitnamen so kurz wie möglich zu halten (besonders dann wenn man schreibfaul ist), aber das wird sich früher oder später rächen. Wenn man dann nämlich dutzende Units mit kryptischen Namen in denen dutzende Objekte mit noch kryptischeren Namen verteilt sind quer auf der Platte verteilt hat, fällt die Wiederverwendung und Zuordnung recht schwer!
Projektunits
Nachfolgend gebe ich einen kurzen Überblick über alle dem Projekt zugehörigen Units und deren Inhalt.
- In dieser Unit befindet sich das Hauptformular unseres Bombermanklons. Hier findet neben der Initialisierung einiger wichtiger Variablen und Objekte nichts wirklich Interessantes statt.
- Dies ist wie der Name schon vermuten lässt der Sitz unserer Codebasis und somit auch die wichtigste Unit unseres Projektes. Im letzten Tutorial hieß sie noch global.pas, wurde jedoch aufgrund der besseren Übersicht umbenannt.
- Naja, ich glaube es ist ein Einfaches anhand der Namensgebung herauszufinden was sich hinter dieser Unit versteckt. Im letzten Tutorial befand sich unser Texturenmanager noch in der globalen Unit. Allerdings will unser 3DS-Lader diesen Texturenmanager auch nutzen, und um Kreuzbezüge zu vermeiden habe ich ihn deshalb in eine extra Unit ausgelagert.
- Vorteil ist, neben der Tatsache das er nun von mehrern Units genutzt werden kann, auch noch die praktische Tatsache das man diese Unit unverändert für seine anderen Projekte nutzen kann. Also quasi ein Texturenmanager mit Zukunft.
- Auch hier sollte die Unitfunktion anhand der Namensgebung recht einfach zu erkennen sein. Hier befindet sich das (momentan noch recht einfache) Soundsystem. Vorteil der Auslagerung in eine estra Unit ist auch hier die Portabilität.
- In dieser Unit befindet sich unser Fontsystem zur Ausgabe von Bitmapfonts.Mehr dazu später.
- Diese Unit bietet einige einfache Methode um 3D-Modelle im 3DS-Format zu laden und in eine unter OpenGL darstellbare DisplayListe zu wandeln.
- Die Unit basiert ursprünglich auf dem 3DS-Loader von Mike Lischke, und wurde von mir vereinfacht. Deshalb wirds hierzu keine genaue Erklärung geben.
Fremdunits
Nachdem ich vorher alle dem Projekt zugehörigen Units aufgezählt habe, gibts hier noch einen ganz kurzen Überblick über die Fremdunits, die vom Projekt benötigt werden.
Utils3DS.pas,Const3DS.pas,File3DS.pas,Types3DS.pas
- Diese Units werden von unserem 3DS-Lader benötigt und stammen auch aus Mike Lischkes 3DS-Packet.
fmod.pas,fmodtypes.pas,fmoddyn.pas,fmoderros.pas,fmodpresets.pas
- Auch hier lässt die Namensgebung recht schnell auf die Units für die FMOD-Library schliessen, die als Grundlage für unser Soundsystem dient.
- Zu dieser Unit muß ich wohl kaum noch viele Worte verlieren, haben wir diese doch schon im Editor zum Laden der Texturen genutzt.
- Quelle : http://delphigl.cfxweb.net
- Auch zu dieser Unit gibts nicht viel zu sagen. Sie beinhaltet alle nötigen Deklarationen und Funktionen zur Nutzung von OpenGL (1.2).
Verwendete Techniken
Bevor wir uns also in den Quellcode stürzen, werde ich hier kurz die verwendeten Techniken, deren Nutzen und deren Umsetzung erklären. Alle hier erwähnten Dinge werdet ihr in weiteren Projekten verwenden können und sind für das Gelingen größerer Projekte fast unerlässlich, also lest aufmerksam!
Timebased Movement
Bisher werden viele von euch (wenn sie denn schonmal ein kleines Spiel programmiert haben) die Ereignisse wie z.B. Spielerbewegung oder das aktualisieren einer Animation am Ende eines jeweiligen Frames abgearbeitet haben. Das ist zwar eine einfache Lösung, hat jedoch den riesigen Nachteil, dass Bewegungen und Animationen je nach Rendergeschwindigkeit (sprich Anzahl der pro Sekunde berechneten Bilder) verschieden sind. Daraus resultiert z.B., das sich eine Spielfigur auf einem schnellen Rechner unkontrollierbar schnell nach vorne bewegt, während sie auf einem langsamen Rechner kaum von der Stelle kommt. Besonders schlecht ist sowas in einem Netzwerkspiel, da der Spieler am langsamen Rechner durch die langsamere Bewegung einen großen Nachteil hat. Abhilfe schafft hier das sog. "Timebased Movement" (zu Deutsch : Zeitbasierte Bewegung), welches für gleichmäßige Bewegungen bzw. Animationen sorgt, in dem es (im Gegensatz zum "Framebased Movement") diese mittels Zeitmessung auf einen festen Faktor angleicht.
Die Implementation dieser Technik ist zum Glück recht einfach, bietet uns Windows doch netterweise (über einen im Chipsatz integrierten Timer) einen Hochpräzisionstimer dessen Auflösung um ein Tausendfaches höher ist als die eines Systemtimers. Damit wird es uns möglich die oft nur sehr geringen Zeitabstände zwischen dem Rendern der Frames zu berechnen. Ein kleines Beispiel : Ein moderner Rechner schafft heute in einer nicht besonders Aufwendigen 3D-Szene ohne Probleme an die 300-400 Bilder pro Sekunde. Dies bedeutet, das er zum Berechnen und Ausgeben eines Bildes 2,5ms (=1000ms/400fps) benötigt. Ein normaler Systemtimer würde uns eine solche Messung mit seiner "Genauigkeit" von ~55ms gänzlich unmöglich machen.
Windows bietet uns nun zwei Funktionen zur Nutzung dieses Hochpräzisionstimer an :
- function QueryPerformanceFrequency(var lpFrequency : TLargeInteger): Bool;
- Diese Funktion liefert uns in lpPerformanceCount die Anzahl der Takte pro Sekunde zurück, also die Auflösung des Timers. Dieser Wert variiert von Chipsatz zu Chipsatz und muß deshalb unbedingt beim Programmstart abgefragt und in einer Variable abgelegt werden.
- Meine Hauptplatine auf Basis des nVidia nForce2-Chipsatz liefert z.B. den Wert 3579545, d.h. dass der Hochpräzisionstimer genau 3.579.545 mal pro Sekunde taktet.
- function QueryPerformanceCounter(var lpPerformanceCount : TLargeInteger): Bool;
- Obige Funktion liefert uns in lpPerformanceCount den aktuellen Stand des Hochpräzisionscounter, gemessen in Systemtakten zurück, und wird von uns zur genauen Zeitmessung genutzt.
Ermitteln der Counterfrequenz
Wie schon in der Funktionsbeschreibung erwähnt müssen wir vor der Nutzung des Hochpräzisionscounters erstmal dessen Frequenz ermitteln. Dies tun wir direkt beim Start unseres Programms im OnCreate-Ereignis des Hauptfensters :
procedure TBomberManForm.FormCreate(Sender: TObject);
begin
QueryPerformanceFrequency(PerfCounterFreq);
[...]
end;
Die Variable PerfCounterFreq wird in der Unit BombermanGlobal.pas deklariert und ist vom Typ Int64. In dieser ist von nun an die Frequenz unseres Hochpräzisionstimers hinterlegt. Sofern dieser Wert größer 0 ist, läuft alles nach Plan. Solltet ihr hier wider Erwarten den Wert Null zurückgeliefert bekommen, unterstützt die Hardware dieses Timingverfahren nicht, was jedoch nur bei sehr alten CPUs so ist.
Die Zeitmessung
Nach der recht langen Präambel widmen wir uns nun dem eigentlich Kern des Timebased Movements, nämlich der Zeitmessung. Diese wird in in der DrawScene-Prozedur unseres TBombermanGame-Objektes (BombermanGlobal.pas) umgesetzt :
procedure TBomberManGame.DrawScene;
var
QPCStartCount, QPCEndCount : Int64;
begin
QueryPerformanceCounter(QPCStartCount);
[...]
// Szene zeichnen
[...]
QueryPerformanceCounter(QPCEndCount);
Der Quellcode spricht eigentlich für sich. Wir ermitteln insgesamt zwei Zeitwerte, zum einen vor dem Zeichnen der Szene und zum anderen nach dem Zeichnen der Szene, und speichern diese ab.
Danach berechnen wir über eine recht einfache Formel unseren Zeitfaktor :
BomberManGame.TimeFactor := (QPCEndCount-QPCStartCount)/PerfCounterFreq*100;
end;
Dieser Zeitfaktor entspricht dann der zum Rendern benötigten Zeit in ms/10 und kann dann beim aktualisieren der Bewegungen bzw. Animationen als Faktor genutzt werden.
Kleine Rechenkunde
Um denn Sinn und die Funktion des Timebased Movement besser verstehen zu können, gibts hier ein paar kleine Rechenbeispiele, die darlegen dass der Code nun frameunabhängig Funktioniert :
Rechner 1 braucht 7 ms um die Szene zu Rendern
- Zeitfaktor = 0.7
- Bewegung des Spieler = 0.05*0.7 = 0,035 Einheiten
- Bewegungen werden 142 mal pro Sekunde aktualisiert (142fps)
- Gesamtbewegung/s = 142*0,035 = 4,97 Einheiten
- Ohne Timebased Movement : Gesamtbewegung = 142*0,05 = 7,1 Einheiten
Rechner 2 braucht 11 ms um die Szene zu Rendern
- Zeitfaktor = 1.1
- Bewegung des Spieler = 0.05*1.1 = 0,055 Einheiten
- Bewegungen werden 90 mal pro Sekunde aktualisiert (90fps)
- Gesamtbewegung/s = 90*0,055 = 4,95 Einheiten
- Ohne Timebased Movement : Gesamtbewegung = 90*0,05 = 4,5 Einheiten
Ich hoffe mal, dass diese beiden Beispiele verdeutlichen, das sich der Spieler unter Zuhilfenahme des Timebased Movements auf verschieden schnellen Rechner (und in verschieden komplexen Szenen) mit gleicher Geschwindigkeit bewegt. Die Diskrepanz vin 0,02 Einheiten bei obigen Werten ist darauf zurückzuführen, dass ich mit abgerundeten Werten gerechnet habe.
Auch wenn dieser Teil des Tuts etwas theoretisch und trocken war, so ist das Timebased Movement, wie ihr sicher schon aus dem Text herauslesen konntet unerlässlich für moderne Spieletitel, und zwar ganz besonders dann wenn diese später auch im Netzwerk bzw. über das Internet spielbar sein sollen. Allerdings wird sich Bomberman drüber freuen, das er sich jetzt auf jedem System gleichschnell fortbewegen kann...
Statemachine
Auch diesen Begriff dürften einige von euch schonmal irgendwo aufgeschnappt haben. Vermutlich war das aber in einem anderen und weitaus komplexerem Zusammenhang als dies bei unserer (recht primitiven) Statemachine der Fall ist.
Spiele bestehen ja bekannterweise aus verschiedenen Szene (=States) wie z.B. dem Hauptmenü und der 3D-Ansicht. Die Statemachine macht in unserem Falle nichts anderes als den in TBombermanGame.CurrentState gesetzten Status abzufragen und dann in die entsprechende Zeichenroutine zu verzweigen. So hat man die für die einzelnen Szenen erforderlichen Zeichneroutinen in verschiedene Prozeduren separiert und kann später bequem und ohne großen Aufwand neue Szenen hinzufügen.
Für jede Szene deklarieren wir in der Konstantensektion der globalen Unit eine eigene Konstanten, anhand derer sie eindeutig identifiziert werden kann :
const
GSMainMenu = $00;
GSNewGame = $01;
GSInGame = $02;
GSMatchEnd = $03;
Die Statemachine prüft dann in der Zeichenprozedur TBombermanGame.DrawScene in welchem Status sie sich befindet und verzweigt dann mittels einer einfachen Case-Anweisung zur entsprechenden Zeichneroutine :
procedure TBomberManGame.DrawScene;
[...]
case CurrentState of
GSMainMenu : DrawScene_GSMainMenu;
GSNewGame : DrawScene_GSNewGame;
GSInGame : DrawScene_GSInGame;
GSMatchEnd : DrawScene_GSMatchEnd;
else
DrawScene_GSMainMenu;
end;
[...]
Den Status unserer Statemachine setzen wir dann mittels der Prozedur TBombermanGame.SetState. Aber warum tun wir das nicht direkt über die Variable TBombermanGame.CurrentState? Dafür gibts einen ganz einfachen Grund : Es ist möglich, dass beim Wechsel zwischen zwei Zuständen verschiedene Variablen geändert oder gesetzt werden müssen. Wechseln wir den Status also über oben genannte Prozedur, so können wir vor dem finalen Wechsel noch prüfen in welchen Status gewechselt wird, und eventuell noch benötigte Variablen setzen oder verändern.
Auch wenn oben beschriebenes recht banal erscheinen mag, so ist eine Statemachine (egal wie primitiv) ein einfaches Mittel zur Verwaltung des Spielzustandes, die sich dazu noch recht leicht erweitern lässt.
Texturenfonts
Was vielen ein Dorn im Auge ist, und sich hoffentlich bald ändert, ist das Fehlen von Routinen zur Fontdarstellung in OpenGL. Aber zum Glück haben sich einige findige Programmierer zu diesem Thema Gedanken gemacht und Methoden zur Schriftdarstellung mittels OpenGL entwickelt die recht einfach zu nutzen und gleichzeitig schnell sind. Eine davon sind Texturenfonts, die wie deren Name schon vermuten lässt, ihre Schriftzeichen aus einer Textur herausholen.
Ein Tool das diese Texturen erstellen kann, wurde von mir bereits am Anfang dieses Tutorials vorgestellt, nämlich der Bitmap Font Builder. Dieser erstellt auf der Basis einer Windowsschriftart eine Textur die alle nötigen Schriftzeichen enthält, und z.B. so aussehen könnte :
Das Erstellen und Ausgeben dieser Schrift erledigt für uns die Klasse TTexFont in der Unit TBombermanFont.pas. Dort finden sich am Anfang in der Konstantesektion die vorgenerierten Texturenkoordinaten für die einzelnen Buchstaben. Da grundlegende Kenntnisse zum Thema Texturemapping eine Voraussetzung für dieses Tut sind, gehe ich davon aus das ihr wisst was diese zu bedeuten haben und wie sie funktionieren.
Unsere TTexFont-Klasse tut jetzt bei der Initialiserung nichts anderes, als für jedes in der Textur hinterlegte Schriftzeichen anhand der in der Konstantesektion festgelegten Texturkoordinate eine Displayliste zu erstellen, die beim Ausgeben des Textes dann nur noch aufgerufen wird.
Animierte Texturen
In unserem Bombermanklon nutzen wir für die Explosionen animierte Texturen. Diese animierten Explosionen lassen sich auf zwei Arten realisieren :
Verschiedene Texturen
Wir laden für jede Explosionsphase eine eigene Textur. Dazu benötigen wir also 16 Texturen, die auf der Festplatte recht viel Speicher beanschlagen. Je nach Explosionsphase binden wir nun vor dem Zeichnen des Explosionsquad die zur aktuellen Explosionsphare gehöhrende Textur.
Diese Methode ist zwar recht einfach zu implementieren, hat jedoch einige Nachteile : Da jede Animationsphase in einer eigenen Textur liegt wird sowohl auf der Festplatte als auch im Grafikspeicher recht viel Overhead erzeugt.
Eine Textur
Dies ist die Lösung die wir verwenden werden, da sie recht viele Vorteile beherbergt. Wir erstellen uns mittels eines Tools (z.b. den am Anfang des Tuts erwähnten ExplosionsGenerator) eine Textur auf der in Reihen und Spalten alle Animationsphasen der Explosion dargestellt sind.
Nun binden wir für jedes Explosionsquad dieselbe Textur und verändern entsprechend der Animationsphase nur unsere Texturkoordinaten, die wir in einem speziellen Array abgelegt haben :
ExpTexCoord : array[0..15, 0..3] of Single =
((0, 0, 0.25, 0.25), (0.25, 0, 0.5, 0.25), (0.5, 0, 0.75, 0.25), (0.75, 0, 1, 0.25),
(0, 0.25, 0.25, 0.5), (0.25, 0.25, 0.5, 0.5), (0.5, 0.25, 0.75, 0.5), (0.75, 0.25, 1, 0.5),
(0, 0.5, 0.25, 0.75), (0.25, 0.5, 0.5, 0.75), (0.5, 0.5, 0.75, 0.75), (0.75, 0.5, 1, 0.75),
(0, 0.75, 0.25, 1), (0.25, 0.75, 0.5, 1), (0.5, 0.75, 0.75, 1), (0.75, 0.75, 1, 1));
Wenn man sich dieses Array genauer betrachtet, dann kann man schon erahnen das unsere Explosionstextur in 4x4 Zellen aufgeteilt ist, und von links nach rechts verläuft. Links seht ihr die vom Explosionsgenerator erstellte Textur, und rechts selbige mit ein paar von mir eingetragenen Texturkoordinaten, die euch das Verständnis dieser Technik ein wenig erleichtern sollten :
Wer also weiß wie man mit Texturkoordinaten umgeht dürfte hier absolut keine Verständnisprobleme haben. Unser Quad zeichnen wir dann wie folgt (siehe TMap.Draw) :
glBegin(GL_QUADS);
glTexCoord2f(ExpTexCoord[Round(ExplosionPhase)][0], -ExpTexCoord[Round(ExplosionPhase)][3]); glVertex3f(0, 0, 0);
glTexCoord2f(ExpTexCoord[Round(ExplosionPhase)][2], -ExpTexCoord[Round(ExplosionPhase)][3]); glVertex3f(2, 0, 0);
glTexCoord2f(ExpTexCoord[Round(ExplosionPhase)][2], -ExpTexCoord[Round(ExplosionPhase)][1]); glVertex3f(2, 2, 0);
glTexCoord2f(ExpTexCoord[Round(ExplosionPhase)][0], -ExpTexCoord[Round(ExplosionPhase)][1]); glVertex3f(0, 2, 0);
glEnd;
Wie leicht zu erkennen nehmen wir unsere Texturkoordinaten anhand der aktuellen Explosionsphase aus unserem vorher in der Konstantensektion deklariertem Koordinatenarray. Bei ExplosionPhase=0 sieht unser Quad also so aus :
glBegin(GL_QUADS);
glTexCoord2f(0, -0.75); glVertex3f(0, 0, 0);
glTexCoord2f(0.25, -0.75); glVertex3f(2, 0, 0);
glTexCoord2f(0.25, 0); glVertex3f(2, 2, 0);
glTexCoord2f(0, 0); glVertex3f(0, 2, 0);
glEnd;
Der offensichtliche Vorteil dieser Methode ist die Tatsache, das die komplette Animation in einer Textur abgelegt ist, und somit nur eine Textur auf der Festplatte sowie im Grafikkartenspeicher Platz verbraucht.
Soundsystem
Zu einer intensiven Spielumgebung gehören natürlich nicht nur aufwendige Grafiken, sondern auch Soundeffekte die dem Spieler das Gefühl vermitteln, er bewege sich inmitten einer dreidimensionalen Umgebung.
Hier springt unser Soundsystem ein (BombermanSoundSystem.pas), welches einfache Routinen zur Initialiserung und Ausgabe von Soundateien mitbringt.
Auf dem Markt gibt es inzwischen einige sehr gute Soundlibraries, die vor allem für Freewareprogramme kostenlos nutzbar sind. Deshalb machen wir von einer dieser Soundbibliotheken gebrauch, nämlich fmod, dessen Vorteile ein riesiger Funktionsumfang und die Nutzung aller gängigen Soundformate ist.
Unser Soundsystem tut deshalb nichts anderes als diese (sowieso schon einfach nutzbare) Bibiliothek zu kapseln. So kann man später einfach auf eine andere Bibliothek umsteigen, oder sein Soundsystem komplett selbst z.B. unter Verwendung von DirectSound programmieren.
Auf die Funktionsweise bzw. Implementation von fmod werde ich hier jedoch nicht genauer eingehen. Die mit dieser Bibliothek gelieferten Hilfedateien und Beispiele sind so ausführlich, dass jede weitere Dokumentation Zeitverschwendung wäre.
Unsere Implementation der FMOD-Library ins Soundsystem werden wir später aber noch genauer betrachten.
So viel also zu den verwendeten Techniken. Fehlen tut in der Liste zwar der Texturenmanager, diesen haben wir allerdings schon im letzten Tutorial besprochen, das ihr ja alle bestimmt aufmerksam gelesen habt ;-)
Verwendete Dateiformate
Natürlich sollte man sich bei der Programmierung eines Spiels Gedanken über die verwendeten Dateiformate machen. Hier braucht man das Rad nicht neu zu erfinden und sollte wenn möglich auf bereits bestehende Formate und Editoren zurückgreifen. Ich werde jetzt alle in unserem Bombermanklon verwendeten Formate auflisten, kurz begründen warum wir diese verwenden und auch Alternativen nennen.
3D-Modelle
Für unsere 3D-Modelle (Spieler, Bomben) nutzen wir das sehr weit verbreitet 3DS-Format, in das fast alle gängigen 3D-Modellierprogramme exportieren können. Ein weiterer Vorteil neben der breiten Unterstützung durch gängige Programme ist die Existenz diverser 3DS-Laderoutinen in allen möglichen Programmiersprachen. Großer Nachteil dieses Formates sind Animationen, die auf Keyframe-Basis gesichert werden und deshalb recht schlecht zu implementieren sind, weshalb der genutzte 3DS-Loader diese auch nicht lädt. Als Alternative (vielleicht gibts dazu etwas im nächsten Tutorial) schlage ich deshalb das Milkshapeformat vor, zu dem es auf DGL übrigens eine praktische Ladebibliothek gibt.
Weitere Infos zum 3DS-Format gibts auf [www.wotsit.org www.wotsit.org].
Auf der Suche nach 3D-Modellen sollten vor allem folgende Seiten gute Anlaufstellen sein :
- http://www.planetquake.com/polycount (Die Netzweit wohl größte Datenbank an Modellen für Q2/Q3/UT/UT2003/HL
- http://www.3dcafe.com (Recht große Sektion mit frei downloadbaren Modellen im 3DS-Format)
Sounds
Soundformate exisitieren inzwischen reichlich, und dank der FMOD-Library sind wir in dieser Hinsicht auch sehr flexibel. Da wir unser Spiel aber im Netz vertreiben wollen, nutzen wir für größere Sounds (also z.B. die Hintergrundmusik) das OGG-Vorbis-Format. Nicht nur das es ggü. dem MP3-Format bessere Qualität bei gleicher Komprimierung bietet, nein es ist sogar ein komplett gebührenfreies Format und kann deshalb selbst in kommerziellen Anwendungen ohne Copyrightprobleme genutzt werden. Für kleinere Sounds wie z.B. die Bombenexplosion nutzen wir weiterhin das altbewährte Wave-Format.
Sounds finden sich im Netz leider weitaus schwerer als 3D-Modelle und Texturen, ein guter Anfang bildet allerdings folgende Page : http://www.findsounds.com
Texturen
Auch hier sind wir bei der Wahl des Formates dank glBMP.pas recht flexibel. Allerdings wollen wir auch Alphamasking unterstützen, eine Technik bei der man vorher im Bildbearbeitungsprogramm markierte Bereiche durchsichtig machen kann. Deshalb benötigen wir ein Format das die Speicherung des Alphakanals unterstützt.Dazu kämen entweder TGA oder PNG in Frage. Da PNG jedoch recht neu ist und es für Delphi kaum Loader gibt, verwenden wir für unsere Texturen das TGA-Format.
Texturen für Spiele in Hülle und Fülle, und dazu noch in der Public Domain gibts übrigens beim WadFather.
Karten
Natürlich müssen wir auch ein Format für unsere selbsterstellten Karten wählen. Dieses Kapitel haben wir ja schon hinreichend im ersten Tutorial durchgekaut, weshalb ich hier nicht weiter drauf eingehen werde.
Der Quellcode
So Leute, jetzt gehts ans Eingemachte. Während der Quellcode zum Editor noch leicht überschaubar, recht kurz und die Anzahl der Objekte/Klassen sehr gering war, gehts jetzt etwas heftiger zur Sache, da unser Projekt bereits auf weit über 1000 Zeilen angewachsen ist.
Ich werde aber mein Bestes geben und versuchen alle wichtigen Codestellen, Variablen und Prozeduren möglichst detailliert und leicht nachvollziehbar zu erklären. Wenns doch noch irgendwo happert und selbst die Kommentare im Quellcode keine Lösung bringen, dann gibts ja immer noch das DGL-Forum in dem ich dann auf eure Fragen gerne eingehe...
Grundgedanken
Bevor ihr wild drauflos programmiert, solltet ihr euch natürlich über die in eurem Spiel benötigten Klassen, Objekte und Variablen Gedanken machen. Bei einem bereits vorgegebenem Spielprinzip wie Bomberman ist das natürlich eine recht einfache Sache. Hier brauchen wir Klassen (bzw. Objekte) die unser Spielfeld, die Spieler und gelegte Bomben repräsentieren. Des weiteren gibts noch die Klasse TBombermanGame, welche das eigentliche Spiel kapselt und neben Routinen zur Initialisierung auch zur Verwaltung der globalen Spiellogik dient.
Wie ihr seht, ist es also essentiell sich über seine Objekte und Klassen Gedanken zu machen, denn besonders in der OOP sind dies mächtige Werkzeuge um den Code leichter nutzbar und übersichtlicher zu machen. In größeren Projekten spielt diese Planung natürlich eine weit wichtiger Rolle, kann man sich doch viel Arbeit mittels Vererbung sparen.
Wenn ihr nun z.B. KI-gesteuerte Monster in unseren Bombermanklon integrieren wollt, so könntet ihr euch eine TActor-Klasse schreiben, die sich um Kollsionsabfrage und das Zeichnen der Figur kümmert und davon sowohl die menschlich gesteuerten Figuren (TPlayer) als auch die KI-Aktoren (TKIActor) ableiten.
Auslagern von Klassen/Objekten
Ich hab es im Verlaufe dieses Tuts bereits kurz angeschnitten (bei der Unitauflistung). Klassen bzw. Objekte die nicht spezifisch auf das Spiel zugeschnitten sind (wie z.B. der Texturenmanager oder das Soundsystem) sollten am besten in eine externe Unit ausgelagert werden.
Dies hat den Vorteil, das man sich so nach und nach eine große Codebasis schafft die man dann später in anderen Projekten nutzen kann. So werdet ihr dann nur noch einen Texturenmanager programmieren müssen, den ihr von nun an in jedes andere Projekt einbinden und sofort nutzen könnt. Ein weiterer Vorteil dieser Methode ist die Tatsache, dass eine Erweiterung des Texturenmanagers z.B. direkt in jedem eurer Projekte verfügbar ist. Deshalb wäre es keine schlechte Idee, wenn ihr euch für solche Units einen eigenen Ordner erstellt und in euren Projekten direkt auf die dort befindlichen Units linkt.
BombermanGlobal.pas
Zu aller erst kümmen wir uns um unsere stark erweiterte Codebasis, zu der sich einige neue Objekte und Klassen gesellen. Im Folgenden werde ich euch zu jedem Objekt bzw. jeder Klasse die Deklaration liefern und wichtige Codestellen und Prozeduren detailliert erläutern :
Das Spielfeld (TMap)
Dieses Objekt haben wir ja bereits im ersten Tutorial besprochen. Hier ist nichts dazugekommen, weshalb wir uns mit diesem nicht weiter beschäftigen werden.
Die Spielfigur (TPlayer)
Dieses Objekt repräsentiert die Spieler und kümmert sich um deren Verwaltung. Die Liste der Spieler wird in dem dynamischen Array Player verwaltet, so dass es in der Anzahl der Spieler rein theoretisch keine Grenzen gibt. Einige der Eigenschaften dieses Objektes wie z.B. Size verändern sich im Verlaufe des Spielgeschehens nicht. Der Grund warum ich diese dennoch implementiert habe liegt darin, das unser Klon später mit aufsammelbaren Extras erweitert wird, die einige Eigenschaften des Spielers ähnlich dem Original beinflussen. Außerdem lässt sich über diese Eigenschaften das Gameplay recht einfach anpassen, ohne das man diverse Stellen im Quellcode nachträglich verändern muß.
Das Objekt TPlayer bringt folgende Eigenschaften und Funktionen mit :
- Position : TVector3f;
- Die aktuelle Spielerposition auf dem Spielfeld .Hierzu verwenden wir einen Fließkommavektor, da sich der Spieler frei und nicht nur Feld für Feld bewegen soll.
- ExplosionRadius : Integer;
- Gibt den Explosionsradius der vom Spieler gelegten Bomben an.
- MaxBombs : Integer;
- Die Anzahl der von diesem Spieler maximal gleichzeitig platzierbaren Bomben.
- Direction : Integer;
- Die Blickrichtung des Spielers.Diese Variable wird nur genutzt um das 3D-Modell in die korrekte Richtung zu drehen.
- Wins : Integer;
- Anzahl der von diesem Spieler gewonnenen Spiele.
- IsDead : Boolean;
- Wie fast alle anderen Variablen ist auch diese selbstredend. Wenn diese True ist, dann hats den Spieler erwischt und er kann nicht mehr am Spiel teilnehmen.
- Speed : Single;
- Bewegungsgeschwindigkeit des Spielers.
- Size : Single;
- Die Ausmaße der Spielfigur.
- Model3DS : String;
- Der Name des zu diesem Spieler gehörenden 3D-Modells.
- KeyUp, KeyDown, KeyLeft, KeyRight, KeyFire : Word
- Die Tastenbelegung des Spielers.
- function IsPositionValid(pNewPosX, pNewPosZ : Single) : Boolean;
- Diese Funktion implementiert die (zum Glück recht einfache) Kollisionsabfrage, und prüft ob die Position an die sich der Spieler bewegen will auch begehbar ist. Dabei wird geprüft, ob das Feld unter der neuen Position begehbar ist, ob Kollision mit anderen Spielern stattfindet und ob Bomben im Weg liegen.
Wenn die neue Position gültig ist, dann liefert diese Funktion den Wert TRUE zurück.
- procedure UpDate;
- In dieser Version unseres Bombermanklons hat diese Prozedur nur den einzigen Zweck zu prüfen ob sich der Spieler in einer Explosion befindet. Ist dies der Fall, so stirbt er und es wird ein entsprechender Sound abgespielt.
- procedure Draw;
- Wie der Name bereits vermuten lässt kümmert sich diese Prozedur um das Zeichnen der Spielfigur und deren Schattens. Die hier verwendeten Schatten sind jedoch von der Sorte "Ganzbillig" und sind nichts anderes als auf den Boden geblendete Texturen. Für unseren Bombermanklon sind sie jedoch ausreichend und geben der Szene etwas mehr an Realismus.
- procedure DropBomb;
- Auch dieser Prozedurnamen lässt bereits auf ihre Funktion schliessen. Sie prüft, ob die Zahl der vom Spieler gelegten Bomben nicht die maximale Anzahl an legbaren Bomben überschreitet und legt platziert eine Bombe auf dem Spielfeld.
- procedure ProcessInput;
- Auch hier gibts nix besonderes. Mittels GetAsyncKeyState prüfen wir auf Hardwareebene ob eine Taste im gedrückten Zustand ist und führen dann die damit verbundene Aktion aus. Ist die übergebene Taste gedrückt, wird das höchstwertige Bit des zurückgegebenen Wertes gesetzt.
Im Falle einer Bewegung prüfen wir natürlich nach, ob die neue Position des Spielers auch gültig ist.
Einzige Besonderheit ist hier die Nutzung unseres vorher berechneten Zeitfaktors BomberManGame.TimeFactor als Faktor für die Bewegungsgeschwindigkeit.
Bomben (TBomb)
Auch die von den Spielern legbaren Bomben habe ich in einer Klasse gekapselt um deren Verwaltung und Aktualisierung zu erleichtern. Hier gibts eigentlich nix Spektakuläres, weshalb ich direkt zu den Eigenschaften und Prozeduren übergehe :
- Position : TVector3i;
- Die Position der Bombe als Integer-Vektor, da Bomben nur genau in der Feldmitte abgelegt werden (wie beim original Bomberman).
- Owner : ^TPlayer;
- Zeiger auf den Besitzer dieser Bombe.Diese Eigenschaft wird benötigt um herauszufinden wie viele Bomben dieser Spieler bereits gelegt hat, und um die Explosionsreichweiter der Bombe (die auch spielerabhängig ist) zu ermitteln.
- CountDown : Single;
- Wie der Eigenschaftsname bereits vermuten lässt ist hier der Countdown bis zur Explosion der Bombe gespeichert. Sinkt dieser Wert auf >= 0, so wird die Explosion eingeleteitet und die Bombe aus der dynamischen Liste entfernt.
- AnimPhase : Single;
- Die Animationsphase wird als Grundlage für die sinusförmigen Skalierung der Bombe genutzt und lässt so den Eindruck entstehen als würde die Bombe sich strecken und wieder zusammenziehen.
- procedure Draw;
- Kümmert sich um das Zeichnen der Bombe sowie deren Schattens.
- procedure Explode;
- Startet die lineare Explosion in vier Richtungen. Wie im Original wird die Explosion in die jeweilige Richtung durch einen Block gestoppt. Ist dieser Block sprengbar, so wird er vom Spielfeld entfernt.
Trifft die Explosion auf ihrem Weg auf eine andere Bombe, so wird der Countdown dieser Bombe auf einen Wert nahe 0 gesetzt, so dass eine Kettenrekation stattfindet.
Das Spiel (TBomberManGame)
Wie bereits erwähnt repräsentiert dieses Objekt unser Spiel und kümmert sich neben der Verwaltung dynamischer Listen und der Spiellogik auch um die Initialisierung wichtiger Listen bzw. OpenGL und implementiert unsere bereits ausgiebig beschriebene Statemachine. Bei Erweiterung solltet ihr hier alles was global geschieht und nicht von den Objekten selbst getan werden kann unterbringen.
Private Deklarationen
- ShakeScreen : Single;
- In dieser Eigenschaft wird die Restdauer gespeichert während der der Bildschirm nach einer Bombenexplosion erschüttert wird.
- procedure DrawScene_GSMainMenu/DrawScene_GSNewGame/DrawScene_GSInGame/DrawScene_GSMatchEnd;
- Wie bereits im Kapitel zur Statemachine angesprochen sind dies die Unterprozeduren die von der Statemachine zum Zeichnen der verschiedenen Spielstadien (sprich Szenen) genutzt werden.
Öffentliche Deklarationen
- gsMainMenu_Selected : Integer;
- Wie am Präfix dieser Variable zu erkenne, gibt sie den im Hauptmenü ausgewählten Menüpunkt wieder.
- gsMainMenu_Rotation : Single;
- Die Rotation der im Hintergrund des Menüs dargestellten Karte.
- gsNewGame_Selected : Integer;
- Im Menü "Neues Spiel" gewählter Menüpunkt.
- gsNewGame_Players : Integer;
- Anzahl der im Menü "Neues Spiel" eingestellten Spieler.
- MapList : TStringList;
- In dieser Stringlist werden mit der Prozedur CreateMapList alle Namen der in einem bestimmten Ordner gefundene Karten abgelegt, so dass diese im Spielmenü bequem ausgewählt werden können.
- ExtremeMode : Boolean;
- Der Name lässts wohl bereits vermuten. Hiermit wird dem Spiel mitgeteilt das es sich im Extremmodus befindet, in dem sowohl Anzahl der legbaren Bomben als auch deren Reichweite und die Geschwindigkeit der Spielfigur stark erhöht werden.
- MapListPos : Integer;
- Position der gewählten Karte in MapList
- TimeFactor : Single;
- Diese Variable wurde bereits im Kapitel zum Thema "Timebased Movement" besprochen und behinhaltet den Zeitfaktor der für eine gleichmäßige Bewegung/Animation sorgt.
- OwnerForm : ^TForm;
- Zeiger auf die Form in der das Spiel dargestellt wird. Dieser Zeiger muß einen Wert besitzen und darf nicht ins Leere zeigen.
- CurrentState : Word;
- Aktueller Status der Statemachine.
- procedure SetState(pNewState : Word);
- Versetzt die Statemachine in einen neuen Status.
- procedure CreateMapList(pMapDir : String);
- Erstellt eine Liste aller in pMapDir' befindlichen Karten, so dass auf diese einfach und schnell zugegriffen werden kann.
- procedure Create3DSList(p3DSDir : String);
- Erstellt eine Liste aller in p3DSDir befindlichen 3DS-Modelle und generiert auch gleichzeitig deren Displaylisten.
- procedure InitOpenGL;
- Erstellt den OpenGL-Rendercontext und initialisiert einige OpenGL-Parameter.
- procedure DrawScene;
- Diese Prozedur verzweigt je nach Zustand der Statemachine in die entsprechende Zeichenprozedur.
- procedure UpDateBombs;
- Aktualisiert die dynamische Bombenliste und entfernt ggf. explodierte Bomben. Diese Aktion muß außerhalb des TBomb-Objektes getätigt werden, da sich ein Objekt nicht selbst aus einer dynamischen Liste entfernen kann.
- procedure StartNewMatch(pMapName : String);
- Wie der Prozedurname vermuten lässt starte diese eine neues Match auf der in pMapName angegeben Karte. Dabei wird die Bombenliste geleert und es werden alle Spieler wieder auf die Grundwerte zurückgesetzt.
Soviel also zur Deklaration unserer Objekte bzw. Klassen. Wie ihr sehen könnt habe ich hier eine sehr saubere Namensgebung verwendet, durch die viele Eigenschaften bzw. Prozeduren selbsterklärend sind.
Wenden wir uns nun einigen interessanten Stellen im Quellcode zu, die eine genauere Erklärung notwendig machen.
Interessante Codestellen
Zeichnen der InGame-Action (TBomberManGame.DrawScene_GSInGame)
Die wichtigste Funktion der TBombermanGame-Klasse bedarf natürlich einer kleinen Erklärung, enthält sie doch recht viele Anweisungen. Deshalb werde ich den Quellcode hier detailliert durchgehen :
[...]
glClear(GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, OwnerForm^.ClientWidth, OwnerForm^.ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(45, OwnerForm^.ClientWidth/OwnerForm^.ClientHeight, 1, 1024);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity;
glTranslatef(0,0,-30*Length(Map.PlayGround)/24);
glRotatef(45, 1, 0 ,0);
[...]
Das Ganze fängt erstmal recht gemütlich mit OpenGL-Standardkost an. Zuert löschen wir den Tiefenpuffer. Ein Löschen des Farbpuffers ist aufgrund der Tatsache das sich unser Spielfeld über den kompletten Bildschirm erstreckt nicht nötigt.
Danach werden Viewport und Perspektive unserer Fenstergröße angepasst, bevor wir unsere Kamera zuguter Letzt in einem 45°-Winkel über dem Spielfeld ausrichten. Die Höhe der Kamera (wie sich aus der Formel in glTranslatef herbeileiten lässt) wird je nach Größe des Spielfelds in einer anderen Höhe positioniert, so dass man immer das komplette Spielfeld im Sichtbereich hat.Die 24 am Ende der Formel besagt, das sich die Kamera bei einer Kartengröße von 24 Feldern genau 30 Einheiten über dem Spielfeld befindet, bei einer Größe von 48 Spielfeldern würde sie also 60 Einheiten über dem Spielfeld schweben.
[...]
if ShakeScreen > 0 then
begin
glTranslatef(-Length(Map.PlayGround)/2-Random+Random, Random-Random, -Length(Map.PlayGround)/2-Random+Random);
ShakeScreen := ShakeScreen-1*TimeFactor;
end
else
glTranslatef(-Length(Map.PlayGround)/2, 0, -Length(Map.PlayGround)/2);
Map.Draw;
[...]
Dieses Quellcodesubstrakt sorgt dafür, dass unsere Karte auf dem Bildschirm zentriert wird. Wenn gerade eine Bombe explodiert ist (ShakeScreen = True) verändern wir per Zufall die Position der Karte, so dass der Eindruck entsteht als würde das Spielfeld aufgrund der Bombenexplosion erzittern. Besonders bei einer längeren Kettenreaktion trägt dies nicht unerheblich zu einem dichteren Spielgefühl bei.
Nach der Zentrierung müssen wir natürlich noch unser Spielfeld zeichnen. Die dazu genutzte Prozedur TMap.Draw kümmert sich übrigens auch um das Zeichnen der Spielfiguren.
[...]
for i := Low(Player) to High(Player) do
Player[i].ProcessInput;
[...]
Mittels dieser Zeilen verarbeiten wir die Eingaben der Spieler (sprich Tastendrücke). Dies könnne wir jetzt getrost in jedem Frame machen, da wir die Bewegung des Spielers ja mittels unseres errechneten Zeitfaktors an die Rendergeschwindigkeit angleichen.
[...]
glPushAttrib(GL_ALL_ATTRIB_BITS);
if Length(Bomb) > 0 then
for i := Low(Bomb) to High(Bomb) do
Bomb[i].Draw;
UpDateBombs;
glPopAttrib;
[...]
Bevor wir nun unsere Bomben zeichnen können, sichern wir alle OpenGL-Attribute, damit wir diese für den Fall, das sie durch das Zeichnen der 3DS-Modelle verändert wurden, später wiederherstellen können.Täten wir das nicht, so würden folgende Zeichneoperationen nicht so ausgeführt wie wir das wollten.
Mittels UpDateBombs lassen wir alle Bomben aktualisieren und die dynamische Bombenliste falls nötig aktualisieren.
[...]
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
glOrtho(0,640,0,480,-2000,2000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity;
[...]
Nachdem wir nun alles in unserer 3D-Ansicht gezeichnet haben schalten wir in die orthogonale Ansicht, also den 2D-Modus von OpenGL um unsere Informationsleiste mit den Spielerpunktständen zu zeichnen. Dies geschieht mittels glOrtho.Mit den ersten vier Parametern legen wir die Ausmaße der 2D-Ansicht fest, während die letzten zwei der Near- und Far-Wert für das Viewclipping sind.Diese Werte sind deshalb so groß, damit die 3D-Modelle der Spielerköpfe nicht geclippt werden.
[...]
for i := 0 to 3 do
begin
glPushMatrix;
glTranslatef(128+i*128, 455, 20);
glScalef(0.5, 0.5, 0.5);
if Player[i].IsDead then
Draw3DSModel('bomberhead'+IntToStr(i+1)+'d')
else
Draw3DSModel('bomberhead'+IntToStr(i+1));
glColor3f(1,1,1);
glPopMatrix;
end;
[...]
Nun zeichnen wir in einer Schleife die 3D-Modelle der Spielerköpfe in dieser 2D-Ansicht. Dabei wird mittels der If-Anweisung je nach Status des Spielers (tot oder lebendig) ein anderes Modell gezeichnet. Da wir uns ja im 2D-Modus befinden können wir die Positionen mittels glTranslatef pixelgenau angeben.
[...]
for i := 0 to 3 do
begin
glPushMatrix;
glTranslatef(128+i*128, 455, 20);
glScalef(1,-1,1);
TextureFont.Print2D(IntToStr(Player[i].Wins), [16,-3,0], [1,1,1], 1, 1);
glScalef(1,-1,1);
glPopMatrix;
end;
[...]
Zu dieser Schleife gibts eigentlich auch nicht viel zu sagen. Hier geben wir neben den Köpfen die Punktzahlen der einzelnen Spieler aus. Einzige Besonderheit ist hier das glScalef(1,-1,1) vor der Textausgabe.Dies ist deshalb nötig, da ich bei der Definition des 2D-Modus mittels glOrtho die Werte für Oben/Unten aus beleuchtungstechnischen Gründen getauscht habe.
[...]
PlayersAlive := 0;
for i := 0 to High(Player) do
begin
Player[i].UpDate;
if not Player[i].IsDead then
inc(PlayersAlive);
end;
case PlayersAlive of
0 : begin
BomberManGame.StartNewMatch(Map.FileName);
end;
1 : begin
for i := 0 to High(Player) do
if not Player[i].IsDead then
inc(Player[i].Wins);
BomberManGame.StartNewMatch(Map.FileName);
end;
end;
[...]
Mit der Schleife in diesem Codekonstrukt prüfen wir (neben dem Aktualisieren der Spieler, siehe dazu das Kapitel zum Objekt TPlayer) wie viele Spieler noch lebendig sind, erhöhen bei einem überlebendem Spieler dessen Punktzahl bzw und starten das Spiel neu. Ein Neustart des Spiels geschieht natürlich auch wenn alle Spieler tot sind, dann gibts aber natürlich keine Punkte.
Soviel also zur wichtigsten Zeichenroutine. Hier dürftet ihr eigentlich keine Probleme gehabt haben, aber dennoch wars mir wichtig einige Dinge anzuschneiden.
Zeichnen des Hauptmenüs (TBomberManGame.DrawScene_GSMainMenu)
Auch in dieser Zeichenprozedur gibts ein paar Besonderheiten die eventuell einer Erklärung bedürfen, weshalb wir uns auch diesen Quellcode vornehmen.
[...]
const
EntryName : array[0..2] of String = ('Neues Spiel', 'Credits', 'Spiel beenden');
EntryPos : array[0..2] of Single = (160,240,320);
[...]
Statt die Menüeinträge und deren Bildschirmposition zu "hardcoden" (also für jeden Menüeintrag eine eigene Textausgabe) ists weitaus praktischer die Menüeinträge sowie deren Positionen (in unserem Falle nur die auf der Y-Achse) am Anfang der Zeichenprozedur in einem Konstantenarray vorzubelegen, so dass wir diese später einfach in einer Schleife zeichnen können.
[...]
TextureManager.BindTexture('gamewindow');
glEnable(GL_ALPHA_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(1,1,1,0.7);
glBegin(GL_QUADS);
glTexCoord2f(0,1); glVertex3f(224, 112, 0);
glTexCoord2f(1,1); glVertex3f(416, 112, 0);
glTexCoord2f(1,0); glVertex3f(416, 368, 0);
glTexCoord2f(0,0); glVertex3f(224, 368, 0);
glEnd;
[...]
Nachdem wir in den 2D-Modus gewechselt haben, binden wir die Textur für unser Hauptmenü, aktivieren Alphamasking um die Ränder der Textur loszuwerden und aktivieren auch noch Blending, damit die im Hintergrund rotierende Karte durch das Menü hindurch sichtbar ist.
Danach müssen wir nur noch ein zentriertes Quad mit den Ausmaßen der Textur auf den Bildschirm bringen und schon ist unser Menühintergrund sichtbar.
[...]
for i := Low(EntryName) to High(EntryName) do
if i = gsMainMenu_Selected then
TextureFont.Print2D(EntryName[i], [230,EntryPos[i],0],[1,0.3,0.3],1,1)
else
TextureFont.Print2D(EntryName[i], [230,EntryPos[i],0],[1,1,1],1,1);
[...]
Diese Schleife gibt dann unsere vorher definierten Menüpunkte an den im Array EntryPos festgelegten Bildschirmpositionen aus. Falls der Eintrag markiert ist, wird er rot hervorgehoben.
Zeichnen der Spielfiguren (TPlayer.Draw)
[...]
glTranslatef(Position.x+0.5, Position.y, Position.z+0.5);
glRotatef(Direction, 0, 1, 0);
[...]
Mit diesen beiden Zeilen werden wir zum einen das 3D-Modell des Spielers zentrieren, und des zum anderen in die aktuelle Blickrichtung ausrichten.
[...]
glPushMatrix;
if IsDead then
begin
glTranslatef(0,0,-0.5);
glRotatef(90,1,0,0);
end;
glScalef(0.0085, 0.0085, 0.0085);
Draw3DSModel(Model3DS);
glPopMatrix;
[...]
Nun kommt der interessante Teil dieser Prozedur. Natürlich sichern wir zuerst unsere Matrix, um sie später wieder schnell herstellen zu können. Dann prüfen wir ob der Spieler tot ist. Ist dies der Fall, so "legen" wir die Spielfigur mit einer Translation und Rotation auf die Stelle an der der Spieler gestorben ist.
Die dann folgende Skalierung ist deshalb so "überdimensioniert", da die Maßeinheiten im 3D-Studio relativ groß sind, und auf unsere Maßeinheit angepasst werden müssen.
Letztendlich rufen wir noch mit Hilfe der Prozedur Draw3DSModel die vom 3D-Modell des Spielers generierte Displayliste auf. Mehr dazu gibts in der Dokumentation zur BombermanLoad3DS.pas.
[...]
glColor3f(1,1,1);
glDisable(GL_LIGHTING);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);
glBlendFunc(GL_ZERO, GL_SRC_COLOR);
[...]
Kommen wir nun zur "Schattendarstellung". Dieses Wort steht mit Absicht in Anführungszeichen, da es keine realistischen Schatten sind, sondern einfache mittels Blending auf den Boden gezeichnete "schwarze Kreise", die den Eindruck eines grade nach unten geworfenen Schattens entstehen lassen.Dies ist jedoch für den Anfang ausreichend und auf jeden Fall besser als gar keine Schatten.Eventuell werden wir uns im nächsten Tutorial mit echten Schattenwürfen beschäftigen.
Die Schattentextur wurde übrigens auf recht einfache Art und Weise mit Hilfe des Farbverlauftools in einem Bildbearbeitungsprogramm erstellt. Links seht ihr die Schattentextur, und rechts das Endergebnis im Spiel :
Obige Zeilen dienen also erstmal der Intialisierung. Hier wird nichts weiter getan als den Tiefentest so einzustellen, das der Schatten auf jeden Fall gezeichnet wird, und das Blending wird aktiviert.Außerdem setzen wir noch einen additiven Blendingmodus.
[...]
TextureManager.BindTexture('shadow_player.tga');
glBegin(GL_QUADS);
glTexCoord2f(0,1); glVertex3f(-0.5, 0.01, -0.5);
glTexCoord2f(1,1); glVertex3f( 0.5, 0.01, -0.5);
glTexCoord2f(1,0); glVertex3f( 0.5, 0.01, 0.5);
glTexCoord2f(0,0); glVertex3f(-0.5, 0.01, 0.5);
glEnd;
[...]
Diese Zeilen dürften selbsterklärend sein. Nach dem Binden unserer Schattentextur zeichnen wir unter die Spielerposition noch ggü. dem Boden leicht erhöht das Schattenquad. Die minimale Erhöhung auf der Z-Achse dient dazu das sog. Z-Fighting zu verhindern, bei dem es je nach Kameraentfernung durch die Ungenauigkeit des Z-Puffers zum Überlappen von Poylgonen kommen kann.
Auch in der Zeichenprozedur unserer Spielfiguren eigentlich nur Standardkost. Die Zeichenprozedur für unsere Bomben sieht genauso aus, weshalb ich diese nicht besprechen werden.
Verarbeiten der Benutzereingaben (TPlayer.ProcessInput)
Auch wenn diese Prozedur schon kurz angesprochen wurde, werde ich hier nochmal auf die Besonderheiten eingehen, da wir die Benutzereingaben nicht wie die meisten von euch mittels der entsprechenden OnKeyDown/OnKeyUp/OnKeyPress-Ereignisse der Form, sondern direkt auf Hardwareebene abfragen. Allerdings reicht es wenn wir hier exemplarisch nur eine Tastenabfrage besprechen.
[...]
if Hi(GetAsyncKeyState(KeyUp)) = 128 then
begin
[...]
Wie schon angesprochen prüft die Funktion GetAsyncKeyState auf Hardwareebene ob die übergebene Taste bzw. der übergebene Mausbutton (was in unserem Falle ohne Interesse ist) gedrückt sind. Ist dies der Fall, so liefert uns diese Funktion im höchstwertigen Bit (also das Bit das ganz links "sitzt") eine 1. Mit der Delphifunktion Hi lassen wir uns also das höherwertige Byte (nicht Bit!) liefern und bekommen als Ergebnis ein Oktett (in dem sich die linken 8 Bits einer 16Bittigen Integervariable befinden). Das Bitmuster von 128 ist ja bekannterweise 1000 0000 und euch sollte sofort aufgefallen sein das bei diesem Bitmuster das von uns gesuchte höchstwertige Bit gesetzt ist .Ein kleiner Vergleich liefert uns nun also TRUE, falls diese Taste gedrückt ist. Das hat sich jetzt wahrscheinlich mächtig kompliziert angehört, war aber mehr als kleine Verständnishilfe Gedacht, und muß nicht unbedingt bekannt sein.
[...]
if IsPositionValid(Position.x, Position.z-0.05*BomberManGame.TimeFactor*Speed-Size/2) then
Position.z := Position.z-0.05*BomberManGame.TimeFactor*Speed;
[...]
Nun kommen wir zum Kern der Sache.Zuerst prüfen wir im If-Statement, ob die Position an die sich der Spieler bewegen will auch gültig ist. Nur wenn dies der Fall ist, wird die Spielerposition auch gesetzt. Ansonsten ändert sich nichts an seiner Position.
BombermanSoundSystem.pas
Wenn euch das vorherige Kapitel nicht erschlagen hat, dann habt ihrs jetzt eigentlich geschafft, denn das Schwerste liegt hinter euch. Als nächstes werden wir uns dann dem weitaus weniger komplexem Soundsystem widmen.
Kommen wir zuerst zur Typendeklaration :
Das SoundSystem (TSoundSystem)
- SampleHandle : array of PFSOUNDSample;
- Wie in der Dokumentation zu FMOD nachzulesen (die ihr euch unbedingt ansehen solltet) greift diese Bibliothek auf seine Sounds (ähnlich wie Windows auf seiner Fenster) über Handles zu, die wir in diesem dynamischen Array ablegen werden.
- SampleList : TStringList;
- Hier legen wir die Namen der in obigem array abgelegten Sounds zwecks schnellen Zugriffs ab.
- constructor Create;
- Naja, der Konstuktor eben.Hier tun wir eigentlich nix anderes als FMOD zu initialiseren.
- destructor Destroy;
- Hier passiert genau das Gegenteil ;-).
- procedure AddSample(pFileName, pSampleName : String);
- Fügt unserem Samplepool das in der Datei pFileName abgelegte Soundfile unter dem Namen pSampleName hinzu.
- procedure PlaySample(pSampleName : String;pLoop : Boolean);
- Sucht das Soundsample mit dem Name pSampleName und spielt dies ab.Per pLoop kann man angeben ob es kontinuierlich wiederholt wird.
Interessante Codestellen
In unserem momentan noch recht simplen Soundsystem gibts eigentlich nur recht wenige Interessante Codestellen. Fangen wir aber troztdem an, und schauen uns die Initialsierung der FMOD-Library in Konstruktor Create an :
[...]
FSOUND_Init(44100, 32, 0);
[...]
Simpel, nicht wahr =). Als Parameter müssen wir der FMOD-Library zur Initialisierung nur die Mixrate (44,1kHz) und die Anzahl der Soundkanäle übergeben (in unserem Falle 32).
Das wars schon mit der Initialisierung. Gehen wir also gleich über zum Abspielen eines Sounds in PlaySample :
[...]
Index := SampleList.IndexOf(pSampleName);
[...]
Channel := FSOUND_PlaySound(FSOUND_FREE, SampleHandle[Index]);
[...]
Das Abspielen von Sounds unter FMOD geschieht immer auf einem Kanal, den das Sample von FMOD zugeteilt bekommt. Die Zuteilung dieses Parameters geschieht entweder fest oder dynamisch durch FMOD, indem der Parameter FSOUND_FREE angegeben wird.
Als zweiter Parameter wird noch das Handle des Samples übergeben, dessen Index wir vorher über die StringList ermittelt haben.
[...]
if pLoop then
FSOUND_SetLoopMode(Channel, FSOUND_LOOP_NORMAL);
[...]
Mit diesen beiden Zeilen setzten wir (falls dies gewünscht wird) den Kanal auf dem das Sample abgespielt wird in den Loopmodus, so dass der Sound kontinuierlich wiederholt wird.
So, dass war eigentlich alles Wissenswerte zu unserem einfachen Soundsystem. Mehr Infos gibts in der Doku zu FMOD.
BombermanLoad3DS.pas
Nachdem wir nun eigentlich alle relevanten Stellen und Deklarationen unseres Quellcodes abgeklappert haben gibts noch ein paar Worte zur 3DS-Ladeunit. Das Groß des dort enthaltenen Quelltextes stammt nicht von mir, sondern aus dem Beispiel-3DS-Lader zu Mike Lischkes 3DS-Library.
Meine Arbeit an dieser Unit "beschränkt" sich auf das einfache Laden und Zeichnen der 3DS-Modelle, weshalb ich hier auch nur kurz auf die dafür von mir gebastelte Klasse und Routine eingehe.
Kapselung des 3DS-Models (TModel3DS)
Diese Klasse kapselt das 3DS-Model und bietet mit der Routine LoadFromFile eine einfache Möglichkeit dies in eine OpenGL-Displayliste zu konvertieren. Die 3D-Modelle werden im dynamischen array List3DS, und ihre Namen (zum schnellen Zugriff) in Names3DS verwaltet.
- ListIndex : TGLUInt;
- Index der OpenGL-Displayliste dieses 3D-Modells.
- Name : String;
- Der Dateiname.
- procedure LoadFromFile(pFileName : String);
- Lädt das 3DS-Model aus der in pFileName angegebenen Datei und erstellt eine DisplayListe, deren Kennung in ListIndex abgelegt wird.
Aufrufen der DisplayListe (Draw3DSModel)
Mit Hilfe dieser Prozedur kann man die DisplayListe eines vorher bereits geladenen 3D-Models bequem über dessen Namen zeichnen.
Schlusswort
BOOOOOM...ich hoffe dieser Knall holt euch wieder zurück in die harte Realität. Ich weiß, dieses Tut war ein harter Brocken, aber wenn ihr alles hier angesprochene begriffen habt, habt ihr ne Menge dazugelernt und solltet von nun an wissen wie man die Programmierung eines Spiels angeht, und welche Techniken man dabei nutzt.
Wenn ihr dieses Tut dann endgültig verdaut habt, wäre ich über Feedback sehr erfreut. Denn das lässt leider (nicht nur bei meinen Tuts) zu wünschen übrig...aber diesmal hängt der Umfang des nächsten Tuts, in dem wir unseren Bombermanklon um einige tolle Effekte und Funktionen erweitern von eurem Feedback ab! Also gebts mir, und am besten im Forum, so das Alle was davon haben!
Euer
- Sascha Willems
Dateien
|
||
Vorhergehendes Tutorial: Tutorial_Bomberman1 |
Nächstes Tutorial: Tutorial_Softwareentwicklung1 |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |