Das Phalanx
Transcrição
Das Phalanx
Besondere Lernleistung Das Phalanx-Projekt Malte Weiß Inhaltsverzeichnis Vorwort 6 1 Skizzierung des Projekts 9 1.1 Leistungsumfang 9 1.2 Team- und Einzelarbeit 10 1.3 Zielplattform 11 1.4 Hilfsprogramme 11 2 Grundwissen 2.1 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 Grundlagen zur Windows-Programmierung Windows und Spiele Die grafische Oberfläche Von linearer Programmierung zum Messaging Multitasking und Multithreading Virtueller Speicher und DLL-Dateien WinAPI gegenüber MFC Dokumente und die „Document/View“-Architektur Die Systemregistrierung 13 13 14 15 16 17 18 19 20 2.2 Einführung in die 3D-Programmierung 2.2.1 Was ist 3D-Rendering? 2.2.2 3D-Spiele – früher und heute 2.2.3 Mathematische Grundlagen 2.2.3.1 Kartesisches Koordinatensystem 2.2.3.2 Die Welt als Dreiecke 2.2.3.3 Grundstrukturen 2.2.3.4 Transformation über Matrizen 2.2.3.5 Vom 3D-Raum auf den Bildschirm 2.2.4 DirectX 2.2.4.1 Installation 2.2.4.2 Komponenten 2.2.4.3 Zugang zu DirectX Graphics 2.2.4.4 Hardware-Beschleunigung und Software-Emulation 2.2.4.5 Mathematische Unterstützung 2.2.5 Bildbuffer 2.2.5.1 Front- und Backbuffer 2.2.5.2 Z-Buffer 21 22 23 25 26 26 27 30 34 36 37 37 38 39 39 40 41 41 13 2.2.6 2.2.7 Texturen Engine 42 43 2.3 2.3.1 Terminologie Programmversionen 43 43 3 Programmiersprache 45 3.1 C++ als Erweiterung von C 45 3.2 3.2.1 3.2.2 3.2.3 3.2.4 Datentypen Einfache Datentypen Komplexe Datentypen Arrays und Strings Zeiger 47 47 49 52 53 3.3 Funktionen 57 3.4 Bedingungen 59 3.5 Schleifen 61 3.6 Dynamische Speicherreservierung 63 4 Komponenten des Projekts 65 4.1 Das Konzept 65 4.2 Worldbuild 4.2.1 Funktion 4.2.2 Der Ablauf einer Kartenerstellung 4.2.2.1 Die Geometrie 4.2.2.2 Texturierung 4.2.2.3 Beleuchtung 4.2.2.4 Besondere Objekte 4.2.2.5 Das Interface 4.2.2.6 Kompilieren 4.2.3 Bedienung 4.2.4 Technische Umsetzung 4.2.4.1 Grundrahmen 4.2.4.2 Das System 4.2.5 Zeitliche Entwicklung 66 66 66 67 68 69 70 71 72 73 75 76 76 77 4.3 Racer 4.3.1 Funktion 4.3.2 Bedienung 78 78 78 4.3.3 4.3.3.1 4.3.3.2 4.3.4 Technische Umsetzung Grundrahmen Das System Zeitliche Entwicklung 80 80 80 83 4.4 Externe Kontrollklassen 4.4.1 Funktion 4.4.2 Kommunikation zwischen Programm und Bibliothek 83 83 84 4.5 Texture Container 4.5.1 Funktion 4.5.2 Bedienung 4.5.3 Technische Umsetzung 4.5.3.1 Grundrahmen 4.5.3.2 Bildformate 4.5.3.3 Datenspeicherung 4.5.4 Zeitliche Entwicklung 84 84 85 87 87 87 88 88 4.6 File Container 4.6.1 Funktion 4.6.2 Bedienung 4.6.3 Technische Umsetzung 4.6.3.1 Grundrahmen 4.6.3.2 Datenspeicherung 4.6.4 Zeitliche Entwicklung 89 89 89 89 90 90 90 4.7 Phalanx Updater 4.7.1 Funktion 4.7.2 Bedienung 4.7.3 Technische Umsetzung 4.7.3.1 Grundrahmen 4.7.3.2 Linearer Prozessablauf 4.7.4 Zeitliche Entwicklung 91 91 92 92 92 92 97 4.8 Ship Editor 4.8.1 Funktion 4.8.2 Bedienung 4.8.3 Technische Umsetzung 4.8.3.1 Grundrahmen 4.8.3.2 Datenspeicherung 4.8.4 Zeitliche Entwicklung 97 97 97 98 98 98 98 4.9 Weitere Werkzeuge 4.9.1 WbCreateUpdate 4.9.2 StatCreator 99 99 100 4.10 102 Zeitlicher Gesamtkontext 5 Ausgewählte Problemsituationen 103 5.1 5.1.1 5.1.2 5.1.3 Blockspeicherung vs. Verkettete Listen Was ist Blockspeicherung? Was sind verkettete Listen? Auswahl des Speicherprinzips 103 103 106 109 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 Das Culling OcTree Culling BeamTree Culling Der Performancetest - OcTree und BeamTree Backface Clipping Fog Culling 110 110 111 113 114 114 5.3 5.3.1 5.3.2 5.3.3 Interpolation Was ist Interpolation? Lineare Interpolation Kubische Interpolation 115 115 117 117 6 Visualisierung 6.1 Logbücher 120 6.2 6.2.1 6.2.2 Worldbuild-Hilfetext Inhalt Zeitliche Entwicklung 121 121 122 120 6.3 Website 6.3.1 Layout 6.3.2 Inhalt 6.3.3 Server 6.3.4 Domain 123 123 124 126 128 6.4 Veröffentlichungen bei Flipcode 128 A CD-Inhalt 130 B Flipcode-Veröffentlichung am 18. Juli 2001 131 C Flipcode-Veröffentlichung am 10. Januar 2002 133 Quellenverzeichnis 136 Vorwort Schon als mir mein Onkel 1993 mein erstes Buch über die Programmiersprache QBasic schenkte, war ich von der Programmierung fasziniert. Die Fäden in der Hand zu haben, durch eine Anreihung von Befehlen etwas verändern zu können, auch wenn es nur die ersten Ausgaben auf dem Bildschirm waren, begeisterte mich. Mit diesem Startschuss fand ich eine starke Motivation, mich in die Programmierung hineinzusteigern. In den neun Jahren bis zum heutigen Tag lernte ich eigenständig acht Programmiersprachen und begann, hier und da meine Kenntnisse kommerziell zu nutzen. Begonnen mit dem QBasic-Interpreter, lernte ich dBase, Clipper, XBase, Assembler, Pascal, Delphi, C und C++. Mein Lernen wurde dadurch erschwert, dass ich ausschließlich auf Eigeninitiative angewiesen war, da es zu dem damaligen Zeitpunkt niemanden in meiner Umgebung gab, der mir die Programmierung hätte näher bringen können. Daher war ich einzig auf Bücher angewiesen, deren sprachliches Niveau auf eine andere Altersgruppe gemünzt war. Beim intensiven Durcharbeiten dieser Bücher entwickelte ich einen Ehrgeiz, der mich noch heute leitet. In mir festigte sich die Vorstellung, dass jedes Problem lösbar sei, wenn man sich nur gut genug damit befasst. Mein hauptsächliches Interesse galt den Computerspielen, insbesondere den 3D-Spielen. Kein anderer Softwaresektor erlaubt meiner Meinung nach so viel kreatives Arbeiten wie die Entwicklung von Spielen. Spiele nutzen ständig die neuste Technik und arbeiten immer am Rande des Machbaren, um das bestmöglichste Ergebnis zu erzielen. Neben diesem technischen Aspekt bietet sich dem Programmierer gerade in der Spielebranche die Möglichkeit, unentdeckte Bereiche zu erforschen, eine eigene Welt zu erschaffen. Seit nun mehr als zwei Jahrzehnten versucht der Mensch, die Realität virtuell abzubilden und strebt dabei eine immer realistischere Darstellung an. Für mich war die Spieleprogrammierung immer vergleichbar mit dem Greifen nach den Sternen, da mir dieser Bereich für lange Zeit aufgrund fehlender Literatur verschlossen blieb. Dennoch ließ ich nie davon ab, Computerspiele zu analysieren. Schon bald merkte ich, dass der Spaß am Spielen daraus resultierte, dass mich die grafische Darstellung und die programmiertechnischen Zusammenhänge interessierten. Es dauerte nicht lange, da nahm ich die Programme förmlich auseinander, indem ich eigene Levels1 entwickelte und erforschte, wie Spiele auf den Benutzer reagierten. 1 Ein Level ist eine Ebene in einem Spiel. Bei einem Kartenspiel kann dies der Schwierigkeitsgrad sein, bei einem 3D-Action-Spiel handelt sich meist um eine bestimmte Umgebung, die durchschritten werden muss. Meine ersten Versuche, Programme zu schreiben, die dreidimensionale Szenen darstellten, erwiesen sich als müßig. Mein erstes Buch zu dem Thema ‚3D-Programmierung mit C++’ führte mich tief in die mathematischen Zusammenhänge der 3D-Spiele ein. So erlernte ich beispielsweise die lineare Algebra zwei Jahre, bevor sie in der Schule behandelt wurde. Leider war der praktische Teil des Buches nicht umfangreich genug, und ich sah keine Möglichkeit, das Wissen in einem eigenen Spiel umzusetzen. Als ich Ende 1999 die Schnittstelle DirectX2 entdeckte, bot sich mir endlich ein Zugang zu dieser Welt. Nach einigen Experimenten, ein zweidimensionales Spiel zu kreieren, fühlte ich mich in der Lage, ein eigenes 3D-Spiel zu entwickeln. So initiierte ich am 3. März 2000 das Phalanx-Projekt. Hätte ich damals gewusst, was auf mich zukäme, hätte ich mein Vorhaben möglicherweise noch einmal überdacht. Die Entwicklung des Phalanx-Projekts brachte mich an den Rand meiner physischen und mentalen Fähigkeiten. Oft lag ich Nächte lang wach im Bett und überdachte neue Problemsituationen, für die es lange Zeit keine Lösung zu geben schien. Nicht selten erwiesen sich geniale Lösungen dann doch als Flops und brachten mich nach tagelanger Reflexionen zurück an den Anfang. Dennoch haben sich meines Erachtens Ehrgeiz und Durchhaltevermögen ausgezahlt: Das Projekt öffnete mir so manche Tür für berufliche Aufstiegschancen. Außerdem konnte ich mein Wissen in den zwei Jahren enorm erweitern, so dass ich es jetzt in vielen anderen Bereichen einsetzen kann. Viel wichtiger ist jedoch, dass ich mir einen Traum erfüllen konnte. Während der Entwicklung wurde mir jedoch eines besonders klar: Es ist ein Irrtum zu glauben, Programmierung sei nur die Anreihung von Befehlen zu einem Quelltext. Nein, Programmierung findet im Kopf statt. Einer Problemlösung oder der Bewältigung eines neuen Zusammenhangs ging immer eine intensive Phase des Probedenkens und -handelns voraus, die – parallel zur Arbeit an anderen Projektteilen – viele Monate andauern konnte. Diverse Skizzen und mathematische Modelle verhalfen mir zu einem besseren Grundverständnis. Das wichtigste Dogma der Programmierung fand in dem Projekt des öfteren Anwendung: „Think simple!“ oder „Die einfachste Lösung ist immer die beste“. Je komplexer ein Problem ist, desto einfacher sollten die Mittel zu dessen Lösung sein. Nur dies garantiert eine auf lange Sicht funktionierende Programmstruktur. Natürlich impliziert der Ausdruck „einfachste 2 siehe Kapitel 2.2.4 (DirectX) Lösung“ nicht, dass es leicht ist, eine solche Lösung zu finden. Im Gegenteil: Oft fand ich nach langen Überlegungen einen aufwendigen Ausweg, brauchte aber viele weitere Stunden, um eine adäquate einfache Umsetzung zu erarbeiten. Diese Dokumentation verschafft einen Einblick in das Phalanx-Projekt. Dabei wird sowohl auf die technischen Details als auch auf die chronologische Entwicklung geachtet. Darüber hinaus werden Grundlagen erklärt, die für das Verständnis der 3D-Spiele-Programmierung notwendig sind. Dennoch möchte ich darauf hinweisen, dass diese Arbeit nur einen begrenzten Überblick bieten kann. Die vollständige Ausführung der zwei Jahre Entwicklungszeit würde den Rahmen bei weitem sprengen. Daher habe ich besonders Wert darauf gelegt, gezielt Schwerpunkte zu setzen, um das Projekt dennoch umfassend zu charakterisieren. Auf der beigefügten CD finden Sie umfassendes Datenmaterial zu dem Projekt. Anhang A beinhaltet die Liste der enthaltenen Komponenten. 1 Skizzierung des Projekts Dieses Kapitel schildert den Rahmen der Projektentwicklung. Des Weiteren stellt es den Arbeitsaufwand der besonderen Lernleistung dar und differenziert, welche anderen Personen Teilaufgaben übernommen haben, um das vorliegende Ergebnis zu realisieren. 1.1 Leistungsumfang Die dieser Arbeit zu Grunde liegende Leistung ist die Programmierung eines Softwareprojekts sowie dessen Visualisierung. Ein Editor zur Entwicklung dreidimensionaler Umgebungen (Worldbuild, Kapitel 4.2) bildet den Kern des Projekts. Hinzu kommt ein weiteres primäres Programm (Racer, Kapitel 4.3), das die erzeugten Daten darstellt und animiert. Zahlreiche ebenfalls selbst erstellte Tools3 unterstützen die beiden Hauptprogramme mit der Bereitstellung von Ressourcen. Die Zielsetzung des Projekts ist die Kreierung eines Computerspiels. Die vorliegende Arbeit umfasst jedoch nicht das Endprodukt, sondern einen „Schnappschuss“ aus der bereits weit fortgeschrittenen Entwicklung: so beschreibt dieses Manuskript den Zeitraum vom 3. März 2000 bis zum 8. April 2002. Die als Ziel gesetzte Spielart ist ein 3D-Spiel, bei dem der Spieler mit „futuristischen“ Flugkörpern Wettrennen fliegen und seine Gegner durch Waffen außer Gefecht setzen kann. Der Einfluss des Benutzers auf das Spiel ist zur Zeit jedoch noch beschränkt und liegt noch in der Zukunft der Projektarbeit. Die Programmierleistung geht weit über die bloße Produktion von Quelltext hinaus: besonders die Planung der einzelnen Projektkomponenten und die Suche nach Problemlösungsstrategien erwiesen sich als sehr komplexe und zeitintensive Vorgänge. Außerdem musste ich mir das gesamte Spezialwissen selbst aneignen, besonders in Bezug auf die 3D-Programmierung. Dennoch setzte ich einige Elemente des Informatikunterrichts um, der zeitgleich zur Projektarbeit verlief. Die folgenden Unterrichtsthemen wurden näher vertieft und im Projekt umgesetzt. 3 Tool [engl.] = Werkzeug, EDV: Hilfsprogramm • Dynamische Strukturen (12.1) – dazu gehören insbesondere Verkettete Listen und Baumstrukturen. In den Kapiteln 5.1 und 5.2 wird diese Thematik genauer erläutert. • Rekursionen (12.1) – ebenfalls im Zusammenhang mit dynamischen Strukturen, nämlich den Baumstrukuren, die rekursiv angesprochen werden müssen. • Windows-Programmierung – ein Thema, das Gegenstand der Jahrgangsstufe 13.1 war. Die Programmierung mit der Sprache Delphi vertiefte mein zuvor erworbenes Wissen zur Entwicklung objektorientierter Anwendungen. • HTML-Scripting (13.2) – dieses Unterrichtsthema rundete meine Kenntnisse in der Erstellung von Internetseiten ab. Die Leistung der Visualisierung umfasst das vorliegende Dokument, sowie alle Aspekte, die in Kapitel 6 erläutert werden: Die Logbücher, der Hilfetext zu dem 3D-Editor, die Website, sowie Veröffentlichungen auf der Internetseite Flipcode. Hinzu kommt das Design von kleineren 3D-Umgebungen, die als Beispiele von den fertiggestellten Programmversionen benutzt werden. Diese sind auf der beigefügten CD-ROM enthalten. Programmierung und Visualisierung wurden vollständig in Eigenregie geplant und selbstständig umgesetzt. Allerdings wurde die Arbeit zeitweise durch weitere Personen unterstützt, die sich jedoch ausschließlich um Designfragen kümmerten. Diese Differenzierung wird im folgenden Kapitel getroffen. 1.2 Team- und Einzelarbeit Ursprünglich war die gesamte Entwicklung als Teamarbeit konzipiert. Meine Programmierarbeit sollte hauptsächlich durch zwei Personen aus meinem Freundeskreis ergänzt werden. Ihre Aufgabe war die Herstellung von Ressourcen, sprich das Erzeugen von Daten, auf die die von mir entwickelte Software zugreift. Insbesondere handelte es sich bei diesen Ressourcen um Texturen (2.2.6), 3D-Karten (4.2.1) und Models (4.2.2.4c), die Grundlage des Computerspiels sein sollten. Leider erwies sich die Arbeit in dem Team als sehr schleppend und ineffizient, da die erforderliche Arbeitsleistung nicht erbracht wurde. Eine Ursache war dafür möglicherweise, dass Worldbuild, das Programm zur Erstellung der 3D-Karten und Models, erst spät so ausgereift war, dass 3D-Umgebungen zuverlässig und sicher erstellt werden konnten. Im August 2001 löste ich das Team auf und beschloss, alleine weiterzuarbeiten. Ich nahm dabei in Kauf, dass die Entwicklung bis zum Endprodukt wesentlich mehr Zeit beanspruchen würde. Dennoch war ich von der Richtigkeit dieser Entscheidung überzeugt. In den knapp eineinhalb Jahren haben die anderen Mitglieder des Teams dennoch Ergebnisse hervorgebracht: ein umfangreiches Paket von Texturen wurde gezeichnet, ein Level wurde begonnen und einige Flugkörper wurden modelliert. Die Texturen kommen heute noch bei Worldbuild und Racer zum Einsatz. Das zu 40% fertige Level fand jedoch keine weitere Verwendung, die Models wurden aber teilweise in Test-Renderings4 verwendet. Alle auf der CD mitgelieferten 3D-Karten und einige Texturen stammen von mir selber. 1.3 Zielplattform Die für das Projekt erzeugte Software wurde für das Betriebssystem Microsoft Windows 98 geschrieben. Grund für die Ausrichtung auf dieses System ist, dass es auf der einen Seite den direkten Zugriff auf die Grafik-Hardware über DirectX ermöglicht, auf der anderen Seite jedoch auch noch die Ausführung von MS-DOS-Programmen gewährt. Einige SoftwareKomponenten des Projekts sind aufgrund ihres linearen Aufbaus für DOS geschrieben. Spätere Windows-Versionen besitzen jedoch keinen „DOS-Kern“ mehr und unterbinden das Starten solcher Programme. 1.4 Hilfsprogramme Zur Entwicklung des Projekts wurden bestimmte Programme zur Hilfe genommen. Für die Programmierung wurden die folgenden zwei Produkte hinzugezogen: • Visual C++ 6.0 (Microsoft) – Grundlage der Programme des Projekts ist die Programmiersprache C++. Visual C++ stellt eine visuelle Entwicklungsumgebung und einen Compiler zur Verfügung, um plattformübergreifende C++-Programme zu erstellen. Kapitel 3 setzt sich ausführlich mit der Sprache C++ auseinander. • DirectX 6/7/8/8.1 (Microsoft) – Die DirectX-Treiber stellen die Schnittstelle zur Grafikkarte unter Windows bereit. Kapitel 2.2.4 erläutert diese Software im Detail. 4 rendern = eine dreidimensionale Umgebung darstellen (siehe dazu Kapitel 2.2.1) Des Weiteren kamen diese Tools zum Einsatz: • Paint Shop Pro 7.0 (Jasc Software) – Dieses Bildbearbeitungsprogramm wurde hauptsächlich für das Zeichnen von Bildern und Texturen verwendet. • Frontpage 2000 (Microsoft) – Dieser Editor wurde primär für die Entwicklung der Website benutzt. • CuteFTP 4.2.3 (GlobalSCAPE) – Eingesetzt wurde dieses Programm für die Übertragung von Dateien auf die verschiedenen Internetserver (siehe 6.3.3). 2 Grundwissen Dieses Kapitel vermittelt das nötige Grundwissen, um die Zusammenhänge der Projektentwicklung besser verstehen zu können. Darüber hinaus ermöglicht es tiefere Einblicke in die Windows- und 3D-Programmierung, um diese beiden Fachgebiete zu veranschaulichen. Dabei wird besonders darauf geachtet, dass die Terminologie erklärt wird, die in späteren Kapiteln dieser Arbeit des öfteren Anwendung findet. 2.1 Grundlagen der Windows-Programmierung Das von Microsoft entwickelte Betriebssystem Windows ist aus der heutigen Computerwelt kaum noch wegzudenken. Auf dem Markt der Home user besitzt es eine Monopolstellung und ist wohl das bisher weit verbreitetste System überhaupt. 2.1.1 Windows und Spiele Die ersten Windows-Versionen waren auf DOS aufgesetzt und keine eigenständigen Betriebssysteme, da sie einen DOS-„Kern“ besaßen. Die Entwicklung zu einem von DOS unabhängigen System dauerte sehr lange. Aus einem einfachen Grund: Die ersten WindowsVersionen (3.1 und sogar noch 95) boten keine stabile Plattform an, auf der sich technisch anspruchsvolle Spiele spielen ließen (sieht man von den bekannten Kartenspielen ab). Auch wenn es schwer zu glauben ist: kaum eine Softwarebranche beeinflusst den Computermarkt – besonders den Hardware-Markt – so stark wie die ComputerspieleIndustrie. Dies lässt sich besonders an der rasanten Entwicklung von 3D-Beschleunigerkarten ablesen, auf die später noch eingegangen wird. Fest steht, dass die Anwender immer auf DOS zugreifen mussten, um „systemnahe“ Spiele zu spielen, die direkt auf die Hardware zugriffen. Da Microsoft das im Textmodus laufende Betriebssystem DOS jedoch ein Dorn im Auge war, entwickelte es eine Schnittstelle namens „DirectX“. DirectX sollte den direkten Zugriff auf verschiedene (deshalb „X“) Hardwarekomponenten ermöglichen und dabei vollständig unter Windows laufen. Zunächst scheiterte dieses Unterfangen: Die ersten DirectX-Versionen waren für den Programmierer umständlich zu handhaben und wurden kaum eingesetzt. Erst Version 3 kam bei Spielen richtig zum Einsatz (z.B. bei „Command & Conquer 2 – Red Alert“). Mit Version 5 gelang Microsoft schließlich der Durchbruch mit einer Version, die hardwarebeschleunigte (siehe 2.2.2) 3D-Programmierung erlaubte. Heute gehört DirectX neben OpenGL (Open Graphic Library) zu den führenden Schnittstellen für Computerspiele. Windows hatte sich damit als eine von DOS unabhängige Plattform etabliert. In den neueren Versionen wie Windows 2000 oder Windows XP ist der DOS-Modus nicht mehr offiziell vertreten und dem Durchschnittsanwender nicht zugänglich. Wer jahrelang DOS-Programme geschrieben hat und nun Windows-Programme entwickeln will, muss seine Denkweise komplett umstellen, denn diese Programme arbeiten anders. Ursache dafür ist unter anderem die grafische Oberfläche sowie das sogenannte Multi tasking und Multi threading. Nebenbei hat Microsoft kleine Änderungen an der Terminologie vorgenommen: Programme werden unter Windows als „Anwendungen“ bezeichnet und „Verzeichnisse“ wurden in „Ordner“ umbenannt. Im Folgenden werden die primären Änderungen zur Windows-Programmierung genauer erläuert. 2.1.2 Die grafische Oberfläche Die wohl offensichtlichste Änderung von DOS zu Windows ist die grafische Oberfläche, die einen größeren Arbeitskomfort gewährleisten soll. Windows-Anwendungen sind vielmehr auf Visualisierung ausgerichtet als die früheren Programme, deren Darstellungsqualität durch den Textmodus stark begrenzt war. Das Betriebssystem arbeitet in einem frei wählbaren Videomodus und erlaubt durch die höhere Auflösung mehr Arbeitsfläche und Übersicht. Dadurch macht der Programmierer beim Umstieg auf Windows eine Wandlung durch: er wird zum Designer. Denn für den kommerziellen Erfolg eines Projekts ist eine ansprechende Oberfläche erforderlich, die sehr benutzerfreundlich ist. Auch wenn es rational betrachtet nicht sinnvoll erscheint, so zieht der gewöhnliche Anwender in der Regel ein „schönes“, bildreiches Programm einem nur funktionalen vor. In modernen Software-Teams wird die Aufgabe der Oberflächen-gestaltung normalerweise an spezielle Mitarbeiter weitergegeben. Heutige Entwicklungsumgebungen für Programmierung, wie Microsoft Visual C++ oder Delphi, besitzen ebenso benutzerfreundliche Umgebungen, die es dem Entwickler erlauben, grafische Oberflächen mit Werkzeugen zu erstellen: zum Beispiel um Buttons oder Eingabefelder zu plazieren. In den meisten Fällen geht der Vorgang des Designens dem Prozess des Programmierens voraus. Daher benötigt die Erstellung einer Windows-Anwendung oft eine längere Vorbereitungszeit. 2.1.3 Von linearer Programmierung zum Messaging DOS-Programme unterscheiden sich in der Programmstruktur von Windows-Anwendungen deutlich in einer Eigenschaft: sie sind linear. Das Programm wird Zeile für Zeile ausgeführt, so wie es der Programmierer konstruiert hat. Alle Eingaben werden von Programmteilen entgegengenommen und ausgewertet, die der Autor selber geschrieben hat. Kurz lässt sich sagen: Der Programmierer von DOS-Programmen besitzt die volle Kontrolle über Eingabe, Ausgabe und interne Vorgänge. Dies ist wahrscheinlich auch der Hauptgrund dafür, dass die Entwicklung solcher Programme noch lange populär blieb. Windows-Anwendungen verhalten sich nicht linear, sondern arbeiten nach einem MessagingPrinzip. Jede Anwendung erhält Nachrichten und wertet diese aus. Bei einer Nachricht kann es sich z.B. um eine Mausbewegung oder um einen Klick auf einen Button handeln. Über alle Benutzereingaben und Änderungen der grafischen Oberfläche (z.B. die Größenänderung eines Fensters) wird das Programm benachrichtigt. So gibt es keine direkte Abfrage von Maus oder Tastatur. Nach dem Start durchläuft das Programm permanent eine sogenannte Message queue5. Dies ist eine Schleife, die solange ausgeführt wird, bis das Programm beendet werden soll. In ihr findet die oberste Ebene der Nachrichtenverarbeitung statt: Nachrichten werden erwartet, abgeholt und dann an die Funktion weitergesandt, die für die Auswertung zuständig ist (z.B. die Funktion für die Verarbeitung der Nachrichten des Hauptfensters). Beispiele für Nachrichten sind: Nachricht WM_CREATE WM_SIZE WM_QUIT WM_LBUTTONDOWN WM_COMMAND 5 Funktion Ein Fenster wird gerade erzeugt. Die Größe eines Fensters wird geändert. Die Anwendung wird beendet. Die linke Maustaste wird heruntergedrückt. Ein Kommando wird ausgelöst, z.B. durch die Auswahl eines Menüpunkts oder den Klick auf einen Button. Message queue [engl.] = Nachrichtenschleife, -warteschlange Die Liste der Nachrichten ist schier unbegrenzt, denn selbst bei kleinen Operationen werden viele Messages an die zuständigen Programme versandt. Wer sich schon einmal mit einem Spy-Programm6, einer Anwendung zur Anzeige des Nachrichtenverkehrs unter Windows, beschäftigt hat, der hat sicher bemerkt, dass die Bewegung der Maus vom rechten oberen zum linken unteren Rand über tausend Nachrichten auslösen kann. Das bedeutet keinesfalls, dass der Programmierer alle Messages berücksichtigen muss. Dies wäre wohl kaum realisierbar. Er sucht sich von den empfangenen Nachrichten die aus, die für sein Programm relevant sind. Der folgende Programmcode demonstriert eine einfache Nachrichtenschleife: MSG msg; do { // Nachricht abholen GetMessage( &msg, NULL, 0, 0 ); // und übersetzen und weiterleiten. TranslateMessage( &msg ); DispatchMessage( &msg ); } // Solange ausführen, wie Beendigungs-Nachricht nicht eingegangen ist. while( WM_QUIT != msg.message ); In jedem Fall muss der Programmierer im Aufbau seiner Applikation (≅ Anwendung) viel flexibler sein als bei der DOS-Programmierung, da der Anwender wesentlich mehr Eingabemöglichkeiten hat. Neben einer einfachen Texteingabe kann er zum Beispiel auch ein Fenster maximieren oder ein anderes Programm anwählen. 2.1.4 Multitasking und Multithreading Ein Begriff, der oft in Zusammenhang mit Windows fällt, ist das sogenannte Multitasking. Dabei handle es sich um die Fähigkeit des Betriebssystems, mehrere Tasks7 gleichzeitig auszuführen. Das Wort „gleichzeitig“ ist genau genommen sachlich falsch, da das Main board8 eines Computers, das mit nur einem Prozessor ausgestattet ist, auch nur einen Befehl auf einmal ausführen kann. Die eher seltenen Ausnahmen bilden Dual boards, die zwei Prozessoren betreiben können. Sie werden aber erst ab Windows 2000 unterstützt und sich aus Kostengründen wahrscheinlich nie für den „Normalgebrauch“ etablieren. 6 7 spy program [engl.] = Spionprogramm task [engl.] = Aufgabe Und trotzdem erscheint es dem Anwender so, als arbeiteten die gestarteten Anwendungen simultan. Dies hängt damit zusammen, dass Windows die Prozessorleistung auf die laufenden Anwendungen mit unterschiedlicher Priorität aufteilt. Zur Veranschaulichung ein Beispiel: Sie schreiben einen Text mit dem Programm Word, während ScanDisk im Hintergrund ihre Festplatten auf Fehler überprüft. Das System teilt nun die Prozessorleistung auf: Abwechselnd werden von Word und ScanDisk MaschinencodeBefehle an die CPU9 geleitet, wobei ScanDisk aufgrund seines permanenten Festplattenzugriffs mehr Priorität eingeräumt wird. Dies kann zum Beispiel bedeuten, dass erst drei ScanDisk-Befehle weitergeleitet werden, bevor ein Word-Befehl ausgeführt wird. Durch dieses Prinzip erhält der Anwender sehr viel Freiheit bei der Arbeit am Computer, wenn es auch zu Fehlern führen kann, wenn Programme zum Beispiel abstürzen, weil sie zu wenig CPU-Leistung zugeteilt bekommen (u.a. CD-Brennprogramme). Beim Schreiben von Windows-Anwendungen muss der Programmierer immer im Kopf behalten, dass er die verfügbaren Systemressourcen mit den anderen Anwendungen teilt und diese nach dem Gebrauch immer wieder freigeben muss. Nicht selten stürzen Applikationen ab und „reißen andere Programme mit in den Tod“, weil sie die benutzten Systemressourcen nicht wieder freigegeben haben. Die Folge kann ein systemweiter Absturz sein. Wesentlich unbekannter ist der Begriff Multithreading, doch er bedeutet im Grunde nichts anderes als Multitasking innerhalb einer Anwendung. Bei Bedarf kann der Programmierer einen sogenannten Thread10 starten, der synchron zum Hauptprogramm bestimmte Aufgaben erfüllt. Dies kann das Herunterladen einer Datei, das Durchsuchen eines Textes oder nur die Anzeige eines Prozentbalkens sein. Auch Threads können unterschiedliche Prioritäten besitzen. 2.1.5 Virtueller Speicher und DLL-Dateien Eine besondere Eigenschaft, um die sich der Programmierer jedoch nicht zwangsläufig kümmern muss, ist der virtuelle Speicher. Das Betriebssystem Windows arbeitet über ein komplexes Speichermanagement, das scheinbar immer freien Arbeitsspeicher findet, auch wenn das System ausgelastet sein müsste. Dies hängt damit zusammen, dass Windows Arbeitsspeicher in Form von Dateien auf die Festplatte auslagert, wenn kein freier RAM11- 8 main board [engl.] = EDV: Hauptplatine des Computers, die alle Periphäriegeräte vereinigt CPU, Central Processing Unit [engl.] = Hauptchip eines Personalcomputers 10 thread [engl.] = Faden 11 RAM, Read Access Memory [engl.] = Speicher zum Lesen und Schreiben, Arbeitsspeicher 9 Speicher mehr zu Verfügung steht. Bei Windows 98 heißt die Auslagerungsdatei ‚Win386.swp’ und kann eine beachtliche Größe annehmen. Natürlich ist die FestplattenAuslagerung ein zeitintensiver Vorgang, der die Systemleistung reduziert. Daher sollte ein PC immer ausreichend mit RAM ausgestattet sein. Mit einem weiteren Trick spart Windows Speicher ein: mit den sogenannten dynamischen Linkbibliotheken (DLL = Dynamic Link Library). Dabei handelt es sich um Bibliotheken, die Programmcode enthalten, der jedoch nicht eigenständig ausgeführt werden kann. Anwendungen können auf die Funktionen und Variablen, die in diesen Dateien enthalten sind, bei Bedarf zugreifen. Die gesamte grafische Anzeige von Windows ist beispielsweise in DLLDateien gepackt. Wie der Name schon sagt, sind diese Bibliotheken dynamisch: verschiedene Programme können gleichzeitig auf eine DLL-Datei zugreifen, außerdem kann ein solches Programmmodul wieder aus dem Speicher entfernt werden, wenn es nicht mehr benötigt wird. Dadurch wird im doppelten Sinne Speicher gespart. 2.1.6 WinAPI gegenüber MFC Es gibt zwei Wege, eine Fensteranwendung zu programmieren: Über die WinAPI oder über die sogenannten Microsoft Foundation Classes (MFC). Die WinAPI ist eine Schnittstelle (Windows Application Interface), die das Entwickeln von Windows-Anwendungen erlaubt. Sie bildet die Basis für die Programmierung und ist außerdem bei allen Herstellerfirmen von C++ identisch (Microsoft, Borland etc.). Dabei läuft die komplette Nachrichtenkontrolle über Funktionen und Prozeduren ab. Wer schon einmal eine Windows-Anwendung über die API geschrieben hat, weiß, dass dies mit sehr viel Schreibarbeit verbunden ist, denn jeder Schritt zur Grafikoberfläche muss einzeln programmiert werden: Das fängt mit der ‚Registrierung der Hauptfensterklasse’ an und hört mit der ‚Einrichtung der Werkzeugleisten’ auf. Eine einfache Anwendung kann ohne Weiteres auf mehrere hundert Zeilen kommen. Um eine effizientere und vor allen Dingen einfachere Programmierung zu gewährleisten, hat Microsoft die Foundation Classes (MFC) entwickelt. Hierbei handelt es sich um ein umfangreiches Klassenpaket, das alle Funktionen zur Fensterverwaltung kapselt und die schreibintensiven Schritte in vorprogrammierten Methoden zusammenfasst. Der Programmierer muss sich also nicht mehr um die „Kleinigkeiten“ kümmern, sondern kann sein Augenmerk viel mehr auf die Visualisierung richten. Alle Vorgänge laufen über objektorientierte Programmierung ab. Für die Verwaltungsaufgaben liegen Klassen bereit: CWinApp stellt das Gerüst einer Applikation dar, CFrameWnd ist eine Fensterklasse usw. Um auf die Funktionalität der Klassen zuzugreifen, leitet der Programmierer seine eigenen Klassen ab. CTheApp – abgeleitet von CWinApp – könnte zum Beispiel die Klasse sein, die die Verwaltung des Anwendungsgerüsts übernimmt. Die Nachrichten werden bei den MFC nicht mehr von Funktionen verarbeitet, sondern von Methoden innerhalb der Klassen. Die Methode CTheApp::OnClickSave könnte beispielsweise die Reaktion auf das Anklicken des ‚Speichern’-Buttons sein. Dass die MFC auf die WinAPI aufsetzt, wird vom Programmierer dabei kaum noch bemerkt. Die Programmierung objektorientierter Windows-Programme hat sich vielfach bewährt und ist in den meisten Programmiersprachen (VisualBasic, Delphi, Xbase++) Standard geworden. Dennoch besitzt jede Sprache unterschiedliche Umsetzungen dieses Prinzips. Bei der C++-Version von Borland heißt das Klassensystem zum Beispiel Object Windows Library (OWL). Bei der Entwicklung komplexer Anwendungen unter Visual C++ kommt man um die MFC nicht herum, denn sie bietet mehr Übersicht und benötigt weniger Programmcode. Dafür muss der Programmierer jedoch in Kauf nehmen, dass er die umfassende Kontrolle verliert, da die meisten kleinschrittigen Prozesse im Hintergrund (innerhalb der MFC-Klassen) ablaufen. Die meisten Windows-Anwendungen dieses Projekts (siehe 4) wurden mit den MFC entwickelt, da sie aufwendige grafische Oberflächen besitzen und dadurch schneller und übersichtlicher entwickelt werden können. Bei dem Programm ‚Racer’ (4.3) wurde jedoch bewusst die WinAPI verwendet, da es sehr nah am System arbeitet und eine hohe Ablaufgeschwindigkeit erfordert (siehe dazu: 4.3.3.1). 2.1.7 Dokumente und die „Document/View“-Architektur Bei Windows-Editoren, die zum Beispiel Texte erzeugen, ist immer von Dokumenten die Rede. Doch was verbirgt sich hinter diesem Begriff? Ein Dokument ist im Grunde nichts anderes als eine abgeschlossene Einheit von Daten, die vom Anwender bearbeitet werden kann. Das kann unter anderem ein Text, eine Grafik oder eine 3D-Landschaft sein. Anwendungen, die die Erzeugung von Dokumenten ermöglichen, werden als Editoren bezeichnet. Dazu gehören fast alle Office-Anwendungen (Word, Excel etc.), aber auch drei Komponenten dieses Projekts. Unter Windows existieren zwei Anwendungsgerüste, die die Arbeit mit Dokumenten unterstützen: Das SDI- und das MDI-Gerüst. SDI steht für „Single Document Interface“ und damit für eine Schnittstelle, mit der sich nur an einem Dokument arbeiten lässt (Beispiel: Notepad). „Multi Document Interface“-Applikationen (MDI) erlauben hingegen die Arbeit an mehreren Dokumenten gleichzeitig. Alle Editoren des Projekts sind MDI-Anwendungen. Programmiertechnisch macht dies kaum einen Unterschied. Für die effektive Programmierung von Editoren hat Microsoft die Document/ViewArchitektur (zu Deutsch: „Dokument/Ansicht-Architektur“) eingeführt, die Teil der MFC ist. Hinter dem kompliziert wirkenden Begriff verbirgt sich eine einfache Logik: Daten und Ansicht sollen in unterschiedlichen Klassen voneinander getrennt werden. Für die „Aufbewahrung“ der Dokument-Daten steht die Klasse CDocument zur Verfügung. Sie unterstützt auch das – leicht umzusetzende – Speichern und Laden. Die Anzeige dieser Daten geht durch Klassen vonstatten, die von CView abgeleitet werden. Die Trennung zwischen Datenverarbeitung und Visualisierung hat zwei wichtige Vorteile: zum einen bietet dies mehr Übersicht, zum anderen kann ein einzelnes Dokument mehrere unabhängige Ansichten besitzen. Dies ist beispielsweise bei dem 3D-Editor ‚Worldbuild’ (4.2) der Fall, der für ein Karten-Dokument vier verschiedene Ansichten bereitstellt. 2.1.8 Die Systemregistrierung In der sogenannten Systemregistrierung ist die Systemkonfiguration enthalten. Auch die installierten Programme speichern hier ihre Einstellungen. Es handelt sich dabei um eine große Datenbank, die zusammen mit Windows 95 eingeführt wurde und die früher verwendeten INI-Dateien ersetzt. Ähnlich einer Festplattenstruktur sind alle Einträge in einer hierarchischen Ordnung gespeichert. Ein „Ordner“ in der Datenbank wird als Schlüssel bezeichnet. Jeder Schlüssel kann weitere Unterschlüssel und Werte enthalten. In der „Registry“ befinden sich sechs Hauptschlüssel: • HKEY_CLASSES_ROOT – enthält die installierten Dateitypen und die Verknüpfungen zu den entsprechenden Programmen. ( Alias für HKEY_LOCAL_MACHINE\Software\Classes ) • HKEY_CURRENT_USER – beinhaltet alle Systemeinstellungen für den aktuellen Benutzer. • HKEY_LOCAL_MACHINE – hier sind die installierten Hardware-Komponenten gespeichert. • HKEY_USERS – Benutzerinformationen sind hier enthalten. • HKEY_CURRENT_CONFIG – hier werden bestimmte Hardware-Elemente konfiguriert, zum Beispiel die Anzeige oder der Drucker. • HKEY_DYN_DATA – enthält temporäre Daten, die nicht direkt in die Registry eingetragen werden konnten. Dieser Eintrag ändert sich bei jedem Systemstart. Auch einige Komponenten des Projekts greifen auf die Registry zu. Dazu gehören Worldbuild (4.2), Racer (4.3) und der Phalanx Updater (4.7). Die jeweilige Konfiguration wird unter dem folgenden Schlüssel gespeichert: HKEY_CURRENT_USER\Software\Name der Komponente Für den Phalanx Updater ist der Aufbau des Registrierschlüssels ausführlich in Kapitel 4.7.3.2a beschrieben. Unter Windows 95 und 98 ist die Datenbank in den Dateien system.dat und user.dat im Windowsordner gespeichert. Die Zusammenfassung der Einstellungen in einer Datenbank ermöglicht zwar mehr Übersicht, andererseits kann eine Beschädigung einer dieser Dateien das gesamte Betriebssystem zerstören. Außerdem sammeln sich nach längerem Gebrauch „Datenreste“ von früher installierten Programmen an. Nicht selten ist dies die Motivation für eine Neuinstallation des Systems. Das von Microsoft mitgelieferte Programm regedit.exe erlaubt die Editierung der Registrierung. 2.2 Einführung in die 3D-Programmierung Die 3D-Programmierung ist der Kern dieses Projekts. Die Intention war die Entwicklung eines 3D-Spiels. Der Programmierzweig, der als „3D-Programmierung“ bezeichnet wird, gehört zu den komplexesten und variabelsten, denn er ist ein Teilbereich der EDV, der am stärksten der „Alterung“ unterliegt. Die 3D-Programmierung macht heutzutage eine rasante Entwicklung durch, die sich gleichermaßen auf Software wie auf Hardware auswirkt. Ursache dafür ist das Bestreben, immer realistischere Darstellungsweisen zu entwickeln. Mittlerweile gibt es Produkte, die einen sehr hohen Realitätsgrad erreichen (z.B. der Kinofilm „Final Fantasy“). Trotzdem scheitert die Darstellung an den vielen Details, die unseren Naturbegriff ausmachen. Diese Detailvielfalt wird man technisch wahrscheinlich nie erreichen. Allerdings ist es nur noch eine Frage der Zeit, bis man sie simulieren kann. Die Anforderungen an den zukunftsorientierten Programmierer sind in diesem Sektor daher sehr hoch, da er immer „up-to-date“ sein muss. Dieses Kapitel liefert eine Einführung in die 3D-Programmierung und erklärt die Grundlagen, die für das bessere Verständnis der nachfolgenden Kapitel notwendig sind. Für einen tieferen Einblick in die 3D-Programmierung verweise ich auf das Quellenverzeichnis am Ende dieser Arbeit. Es enthält Bücher, die die Entwicklung von 3D-Anwendungen umfangreich erläutern. 2.2.1 Was ist 3D-Rendering? Ein dreidimensionales Gebilde, das mittels eines Computers visualisiert wird, wird als 3DObjekt oder Szene bezeichnet. Die Darstellung einer solchen nennt man Rendering (Verb: „rendern“). Doch was bedeutet dies eigentlich im engeren Sinne? Rendering ist der Vorgang, eine dreidimensional definierte Umgebung oder einen Körper auf eine zweidimensionale Fläche – nämlich den Bildschirm – zu projizieren. Unsere Augen unterliegen dabei also einer optischen Täuschung, da die tatsächlich dargestellte Anordnung von Bildschirmpixeln keinesfalls dreidimensional ist. Die Programme, die 3D-Rendering durchführen, erzeugen also 2D-Bilder. Obwohl diese Erkenntnis möglicherweise evident erscheint, ist sie die elementarste Grundlage der 3D-Programmierung, die gleichzeitig eines der größten Hindernisse darstellt. Das Bestreben einer 3D-Anwendung ist also die möglichst realistische Projektion eines dreidimensionalen Zusammenhangs auf den Bildschirm. 3D-Rendering kann in Anwendungen in zwei Formen auftreten: als Vor-Rendern oder als Echtzeit-Rendern. Programme, die Grafiken vorrendern, erzeugen in der Regel Bilder und Videos. Sie rendern 3D-Szenen erst und speichern sie dann in einem bestimmten Medienformat ab, das dann anschließend betrachtet werden kann. Anwendungen, die in Echtzeit rendern, erzeugen eine Grafik und zeigen sie direkt an, in der Regel, um auf Benutzereingaben zu reagieren. Vorrender-Programme sind dadurch im Vorteil, dass sie sich unbegrenzt Zeit lassen können, die 3D-Grafik zu erzeugen, da sie offline arbeiten, ihr Ergebnis also zeitlich unabhängig vom Rendering anzeigen. Solche Anwendungen kommen dann zum Einsatz, wenn qualitativ hochwertige Videos oder Bilder erzeugt werden. Diese Medien sind allerdings statisch und erlauben natürlich keinen Zugriff durch den Benutzer. Echtzeit-Programme sind dynamisch. Der Benutzer hat Einfluss auf die Darstellung und kann sich beispielsweise in der dargestellten 3D-Welt bewegen. Dies bedeutet für das Programm, dass es sowohl die 3D-Welt ständig neu berechnen und gleichermaßen Benutzereingaben abfragen muss. Echtzeit-Programme legen ihren Schwerpunkt mehr auf Geschwindigkeit als auf Qualität, da sie eine „flüssige“ Darstellung gewährleisten müssen. Sie versuchen möglichst viele Frames (Bilder) pro Sekunde zu rendern. Daraus ergibt sich die sogenannte Framerate, die für eine „ruckelfreie“ Darstellung und eine saubere Steuerung bei 30 bis 60 fps (= Frames pro Sekunde) liegen sollte. Die 3D-Anwendungen dieses Projekts sind auf Echtzeit-Rendering ausgerichtet. Um die Performance (Leistung) zu erhöhen, bedienen sich heutige Echtzeit-Anwendungen der Hardware, sofern eine entsprechende Unterstützung vorhanden ist. Das folgende Kapitel wird dies genau erläutern. 2.2.2 3D-Spiele – früher und heute Obwohl die Grafik-Programmierung zu den komplexesten Bereichen der IT12-Branche gehört, hat sie innerhalb von zehn Jahren einen gewaltigen Sprung vom ‚16-Farben 2D-Spiel’ zum hochrealistischen, hardwarebeschleunigten 3D-Rendering (siehe unten) durchgemacht. Allerdings möchte ich an dieser Stelle die wichtigsten Stationen der 3D-Spiele-Entwicklung nennen. Im Vorfeld möchte ich jedoch darauf hinweisen, dass es sich bei den folgenden Titeln größtenteils um auf dem deutschen Markt zensierte Produkte handelt, von deren Gewaltdarstellung ich mich distanziere. In technischer Hinsicht stellen sie aber dennoch Meilensteine der Entwicklung dar. • 5. Mai 1992: Wolfenstein (ID-Software) – Das erste kommerzielle 3D-Spiel, das eine „flüssige“ Tiefendarstellung zeigte. Die Speicherung der 3D-Geometrie ist jedoch zweidimensional. Der Spieler bewegte sich ausschließlich auf einer Höhenebene. • 10. Dezember 1993: Doom (ID-Software) – Eine technische Revolution. Der Spieler konnte sich nun auf verschiedenen Höhenebenen bewegen, z.B. Treppen hoch gehen, 12 IT, information technology [engl.] = Informationstechnologie von Gebäuden springen etc. Gespeichert wurde das 3D-Level immer noch in Form einer 2D-Karte, jedoch mit Höheninformationen. Da diese Methode immer „noch nicht ganz 3D“ war, wird sie oft als „Semi-3D-Spiel“ bezeichnet. Aufgrund der 2D-Karte erlaubt diese Technik nicht das Designen mehrerer Etagen übereinander. • Mitte 1996: Quake (ID-Software) – Das erste Spiel, das echtes 3D-Design darstellte. Die Geometrie der Karte wird auch in Form einer 3D-Karte gespeichert. Ab diesem Zeitpunkt waren dem 3D-Design keine Grenzen mehr gesetzt. • Ende 1997: Quake 2 (ID-Software) – Eines der ersten Spiele, das auf Hardwarebeschleunigung zugreift. • 1999: Unreal Tournament (Epic Megagames) – Qualitätiv hochwertige 3D-Grafik, die ohne Hardwarebeschleunigung kaum lauffähig ist. Das Budget für Computerspiele übersteigt zu diesem Zeitpunkt schon jenes großer Hollywood-Filmproduktionen. Es ist kaum zu übersehen, dass die größten Innovationen von der amerikanischen „SoftwareSchmiede“ ID-Software ausgingen. Nach diesen Vorbildern sind unzählige Clones13 entstanden, also Spiele, die die führende Technik kopierten. Mit der Erscheinung von Quake 2 begann ein Umdenken, das sich nachhaltig auf die heutige 3D-Industrie auswirkt: Die Spiele wandten sich von der software-orientierten zur hardwareorientierten Programmierung ab. Bis zu diesem Zeitpunkt wurde die komplette 3DDarstellung von den 3D-Programmen selber übernommen: Das fing bei den Berechnungen an und endete mit pixelweisen Übertragungen auf den Bildschirm. Da all diese Operationen vom Prozessor berechnet wurden, waren die 3D-Spiele dieser Zeit in Bezug auf Qualität und Geschwindigkeit eingeschränkt. Die Entwicklung der CPUs kam dem Wunsch der Programmierer nicht nach (und tut es heute noch nicht), eine höhere Grafikqualität zu erreichen. Hardwarehersteller wie zum Beispiel 3dfx entwickelten nun Grafikkarten, die Aufgaben der 3D-Darstellung übernahmen, welche zuvor die CPUs belasteten. Nach einiger Zeit wurden zu jedem Spiel gesonderte Versionen entwickelt, die auf diese „Entlastungsfunktionen“ der 3DKarten zugriffen. Diese Versionen waren wesentlich schneller, erlaubten höhere Bildauflösungen und gestatteten das Einsetzen besonderer Effekte. Durch das sogenannte bilineare Filtern wirkten die Texturen (siehe 2.2.6) zum Beispiel nicht mehr „pixelig“ sondern „weich gezeichnet“. 13 Clone [engl.] = Klon Immer mehr Grafikkarten mit immer neueren und vor allen Dingen schnelleren 3DFunktionen kamen auf den Markt. Da jetzt sehr viele verschiedene Hersteller Grafikkarten entwickelten, die einen unterschiedlichen Funktionsumfang boten, konnte ein Spiel nicht mehr mit Versionen herausgegeben werden, die alle Grafikkartentypen abdeckten. Daher wurden Schnittstellen wie zum Beispiel Microsofts DirectX oder OpenGL entwickelt. Im Zusammenhang mit Windows-Versionen ab 95 muss sich der Programmierer seitdem nicht mehr um den Grafikkarten-Typ, den er ansteuert, kümmern. Er baut einen Effekt ein, der dann von der Grafikkarte berechnet wird, wenn sie ihn unterstützt. Ansonsten wird er von DirectX softwaretechnisch berechnet und gerendert. Diese Schnittstellen verfügen über große Datenbanken, um mit den verschiedensten Grafikkarten zu kommunizieren. Der Programmierer hatte von nun an keinen direkten Kontakt mehr zur Grafikkarte, sondern steuert sie seitdem indirekt über ein Interface an. Die Komponenten dieses Projekts steuern die Grafikkarte über Microsofts Schnittstelle DirectX an, wie in Kapitel 2.2.4 näher erläutert wird. Seit der „Hardware-Revolution“ (etwa 1997) wird Software nicht mehr für Hardware entwickelt, sondern Hardware für Software. Heutzutage werden Spiele entwickelt, bei denen schon bekannt ist, dass der Spieler seinen Computer für einen hohen Betrag aufrüsten muss, um sie überhaupt starten zu können. Viele 3D-Programme werden nicht ausreichend optimiert, da ohnehin absehbar ist, dass sie mit größerem Hardwarefortschritt schneller laufen werden. Die Folge sind oft unausgereifte Produkte, die den Anwender „hardwaretechnisch zur Kasse bitten“. Diese Entwicklung ist sicherlich als negativ zu werten, in der konsumorientierten Marktwirtschaft leider jedoch kaum wegzudenken. 2.2.3 Mathematische Grundlagen Dieses Kapitel beschreibt die mathematischen Zusammenhänge näher, die Grundlage für die Programmierung von 3D-Anwendungen sind. Hierbei möchte ich drauf hinweisen, dass es sich bei den folgenden Unterkapiteln nur um Einblicke handelt. Die Mathematik der 3DProgrammierung ist dermaßen umfassend, dass darüber eigene Bücher verfasst wurden. Voraussetzung für das Verständnis dieser Zusammenhänge sind Kenntnisse über lineare Algebra („Vektorrechnung“), derer sich die 3D-Programmierung im größeren Maße bedient. 2.2.3.1 Kartesisches Koordinatensystem Die mathematischen Vorgänge der 3D-Programmierung werden in einem kartesischen Koordinatensystem berechnet. Dieses besitzt bekanntlich folgende Eigenschaften: • Alle Achsen liegen orthogonal zueinander. • Alle Achsen besitzen den gleichen Skalierungsfaktor. • Die Achsen schneiden sich in einem gemeinsamen Punkt, dem Ursprung. Jeder Punkt im Raum lässt sich durch seine Relation zu den Achsen beschreiben. 2.2.3.2 Die Welt als Dreiecke Die 3D-Welt des Computers besteht ausschließlich aus Dreiecken. Runde Strukturen (z.B. gewölbte Flächen) existieren im mathematischen Sinne nicht. Will man solche „organischen“ Strukturen allerdings darstellen, kann man sie durch ein Annäherungsverfahren simulieren. 3D-Szenen wirken dann dadurch rund, dass man sie in eine Vielzahl von kleinen Dreiecken zerlegt, die in einer runden Form angeordnet sind. Viele 3D-Editoren erlauben zwar den Einsatz sogenannter „Curved surfaces“ (abgerundete Oberflächen / Polygone), intern werden diese jedoch wie beschrieben in Dreiecke zerlegt. Die Ursache, warum das Dreieck als „kleinste Einheit“ gewählt wurde, sind drei Gründe: • Ein Dreieck ist immer konvex. Ein Polygon, das aus drei Punkten besteht, kann nicht konkav sein, da keine Ecke nach innen zeigen kann. Dies erleichtert unter anderem die sogenannten Füllalgorithmen. • Ein Dreieck ist immer koplanar. Drei Punkte spannen eine eindeutige Ebene auf. Würde man mit Vierecken arbeiten, müsste man bei jedem Rendering verschiedene Normalenvektoren berücksichtigen (siehe 2.2.3.3). • Egal wie komplex eine 3D-Szene ist, sie kann immer mit den gleichen Algorithmen gerendert werden. Ein n-Eck lässt sich in [n – 2] Dreiecke unterteilen. Das Programm Worldbuild (4.2) erlaubt den Einsatz von Polygonen, also von geometrischen Flächen, die mehr als drei Punkte umfassen. Allerdings werden diese beim Rendern in Dreiecke zerlegt. Stellt man eine dreidimensionale Szene im sogenannten Drahtgitter-Modus dar (nur die Kanten sind sichtbar), so werden diese Dreiecke erkennbar. 2.2.3.3 Grundstrukturen Die kleinsten „Bausteine“ der 3D-Welt sind zwei Strukturen: Vertices (Singular: Vertex) und Surfaces (oft auch als Polygone oder Faces bezeichnet). Ein Vertex ist ein Punkt im Raum, der verschiedene Eigenschaften besitzen kann. Die wichtigste davon ist natürlich seine Position, welche durch einen dreidimensionalen Vektor (X/Y/Z) dargestellt wird. Im Folgenden sind einige Eigenschaften aufgelistet, wobei die ersten drei am häufigsten Anwendung finden: • Position (3D-Vektor) • Normalenvektor (3D-Vektor) – siehe unten. • 2D-Koordinaten für Texturen (siehe Kapitel 2.2.6) • Farbe (32-Bit-Wert) Ein Surface definiert sich durch die Anordnung von Vertices zu einer Fläche. Existieren beispielsweise zehn Vertices, kann ein Surface theoretisch folgendermaßen festgelegt werden: TestSurface = ( Vertex 2, Vertex 1, Vertex 4, Vertex 8 ) Unser „TestSurface“ ist ein ‚3D-Rechteck’ im Raum, dessen Umriss durch vier Vertices bestimmt wird. Die Reihenfolge der Vertices ist bei der Definition eines Surfaces von elementarer Bedeutung, da alle Polygone – wie in 2.2.3.2 bereits erklärt – beim Rendern in Dreiecke zerlegt werden. Doch in welche? Und in welcher Anordnung? Das Rechteck aus dem obigen Beispiel kann auf zwei Arten in Dreiecke zerlegt werden. Da die Reihenfolge innerhalb der Dreiecke jedoch auch noch eine Rolle spielt, erhöht sich die Anzahl der Möglichkeiten um den Faktor 3!. Insgesamt gibt es also 12 Möglichkeiten, die Punkte auf Dreiecke aufzuteilen. Daher muss der Programmierer vor dem Rendern festlegen, wie die Punktanordnung zu interpretieren ist. Die folgenden Abbildungen verdeutlichen dies. Verschiedene Punktanordnungen: Triangle fan (links), Triangle strip (mitte) und Triangle list (rechts) Surfaces können nur „in eine Richtung“ zeigen, das heißt, je nach Drehung zum Betrachter sind sie sichtbar oder unsichtbar. Die Sichtbarkeit ergibt sich aus der Surfacenormale, also aus dem Vektor, der von der Ebene, den die Vertices aufspannen, wegzeigt. Zeigt die Normale auf den Betrachter zu, ist das Surface sichtbar, andernfalls nicht. Die Surfacenormale (s), die sich aus der Anordnung der Vertices ergibt Wie die Surfacenormale errechnet wird, ist abhängig vom gewählten System (Links- oder Rechtssytem). In den Programmen ‚Worldbuild’ (4.2) und ‚Racer’ (4.3) wird die Surfacenormale folgendermaßen berechnet: sn = Normalize( CrossProduct( (v2 – v1), (v0 – v1) ) ) Funktionsaufruf für die Berechnung einer Vertexnormalen (C++) sn = Surfacenormale, v0/v1/v2 = Drei Vertices, mit denen die Ebene des Surfaces aufgespannt wird In Worten: „Die Surfacenormale ist das normalisierte (Vektorlänge = 1) Kreuzprodukt der Spannvektoren [ Vertex 1 → Vertex 2 ] mit [ Vertex 1 → Vertex 0 ]“. Mit den Vertices v0 bis v2 sind hierbei die Ortsvektoren des Surfaces gemeint. Zum Verständnis: Worldbuild verwendet eine Dreiecksfläche (Triangle fan, siehe Abbildung oben) für das Festlegen der Vertex-Anordnung. Um eine 3D-Szene zu beleuchten, wird im Projekt Vertex lighting14 verwendet. Hierfür ist die sogenannte Vertexnormale von Bedeutung. Im Grunde ist dieser Begriff widersprüchlich, da ein Punkt im Raum unendlich viele Normalen besitzen kann. Die Vertexnormale ist von den Normalen der Surfaces abhängig, die diesen Vertex benutzen. Ein Beispiel verdeutlicht diesen Zusammenhang: Surface0 = ( Vertex 2, Vertex 1, Vertex 4, Vertex 8 ) Surface1 = ( Vertex 11, Vertex 5, Vertex 9, Vertex 4 ) Surface2 = ( Vertex 10, Vertex 4, Vertex 7, Vertex 0 ) Diese Surfaces könnten zum Beispiel drei Flächen eines Würfels darstellen, da sie alle einen gemeinsamen Punkt besitzen (wie bei der Ecke eines Würfels). Die Vertexnormale dieses Punkts („Vertex 4“) wird nun folgendermaßen berechnet: vn = Normalize(vSurfaceNormal0 + vSurfaceNormal1 + vSurfaceNormal2) Funktionsaufruf für die Berechnung der Vertexnormalen (C++) vn = Vertexnormale, vSurfaceNormal0 / vSurfaceNormal1 / vSurfaceNormal2 = Die Surfacenormalen In Worten: „Die Vertexnormale ist das normalisierte Ergebnis der Addition aller Surfacenormalen, deren Surfaces diesen Punkt benutzen.“ Des Weiteren gilt ein Sonderfall: Wenn ein Vertex von nur einem Surface verwendet wird, entspricht die Vertexnormale der Surfacenormalen. Wie bereits erwähnt, kommt die Vertexnormale bei der Lichtberechnung über Vertex lighting zum Tragen. Der Winkel zwischen dem Einfallswinkel des Lichts und der Normalen eines Vertices entscheidet über die Intensität des Lichts an diesem Eckpunkt. Alle Lichtintensitäten innerhalb des Surfaces werden von den Eckpunkten interpoliert. Beispiel: Wird ein Würfel über 24 Vertices definiert, so erhält jede Fläche eine eigene Lichtfarbe, da jeder Vertex die Normale seines „übergeordneten“ Surfaces übernimmt. Daher ist Flächenbeleuchtung scharfkantig („Flat shading“). Werden stattdessen nur 8 Vertices für den Würfel verwendet, so dass jeder Vertex dreifach benutzt wird, entsteht ein weicher Übergang zwischen den Würfelseiten („Gouraud shading“), da jede Vertexnormale drei Seiten einbezieht und somit eine weiche Interpolation möglich ist. Die nun folgenden Abbildungen illustrieren diesen Unterschied. 14 Vertex lighting = Beleuchtungsart, bei der die Lichtintensität von den Vertices eines Surfaces ausgeht, im Gegensatz zum Light mapping, wo jedem einzelnen Punkt auf dem Surface eine Intensität zugeordnet ist Links: Würfel aus 24 Vertices (Explosionsdarstellung), Rechts: Würfel aus 8 Vertices mit gemeinsamen Vertexnormalen Die primären mathematischen Strukturen der 3D-Programmierung sind Vektoren und Matrizen, die in C++ wie folgt definiert sind: typedef struct _D3DVECTOR { float x, y, z; } D3DVECTOR; // 3D-Vektor typedef struct _D3DMATRIX { float _11, _12, _13, float _21, _22, _23, float _31, _32, _33, float _41, _42, _43, } D3DMATRIX; // Matrix _14; _24; _34; _44; // // // // 1. 2. 3. 4. Zeile Zeile Zeile Zeile Zum Verständis: Die Strukturen ( „struct“) von C++ entsprechen den Records in Delphi. Die Ursache für die Verwendung von 4x4 Matrizen wird am Ende des folgenden Kapitels erklärt. 2.2.3.4 Transformation über Matrizen Alle geometrischen Transformationen laufen über Matrizen-Rechnung ab. Wenn also ein Vertex im Raum versetzt werden soll, wird eine Matrix aufgestellt, die die Bewegung beschreibt. Anschließend wird der Vertex über die Matrix abgebildet. Sicherlich ist auch ein „direkter“ Zugriff auf den Positionsvektor eines Vertices möglich und wäre oftmals einfacher. Wenn ein Vertex zum Beispiel einfach nur verschoben werden soll, reicht die Addition eines Verschiebungsvektors aus, wie die folgende Formel erklärt: Warum geht man dann eigentlich den scheinbar umständlicheren Weg über Matrizen? Immerhin ist dieser Weg beim oben genannten Beispiel sogar langsamer. Die Antwort ist recht einfach: Matrizen können umfassend verwendet werden, besonders wenn große Mengen an Vertices transformiert werden sollen. Sollen beispielsweise tausend Vertices gedreht und skaliert werden, reicht es aus, einmal eine Transformationsmatrix aufzustellen, die beide Vorgänge beschreibt, und anschließend alle Vertices über diese Matrix abzubilden. So können mehrere Operationen über eine Matrix auf eine komplette Geometrie angewendet werden. Daher sind Matrizen letztendlich doch einfacher zu handhaben, da weniger Berechnungsfunktionen aufgerufen werden müssen. Es gibt drei grundlegende Transformationsarten. Diese sind: Translation (Verschiebung), Skalierung (Verkleinerung oder Vergrößerung) und Rotation (Drehung). Die entsprechenden Matrizen sind im Folgenden aufgeführt. Zunächst folgt jedoch erst der Aufbau der sogenannten Identitätsmatrix, die keine Änderung bewirkt. Das Ergebnis einer Abbildung mit dieser Matrix ist „identisch“ mit der Eingabe. 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 Die einfachste Transformation stellt die Translationsmatrix dar: 1 0 0 0 0 1 0 0 0 0 1 0 xt yt zt 1 xt, yt, zt stehen in diesem Fall für die Verschiebung in x-, y- und z-Richtung. Ebenfalls einfach ist die Skalierungsmatrix, die wie folgt aufgebaut ist: sx 0 0 0 0 sy 0 0 0 0 sz 0 0 0 0 1 Der Skalierungsfaktor in den verschiedenen Dimensionen wird durch sx, sy und sy repräsentiert. Um beispielsweise die Größe eines geometrischen Objekts zu halbieren, müssen alle Faktoren 0,5 sein. Eine Skalierungsmatrix mit den Faktoren 1 entspricht der Identitätsmatrix (siehe oben). Etwas komplizierter sind die Rotationsmatrizen aufgebaut, da sie trigonometrische Funktionen enthalten und für alle Drehrichtungen anders definiert sind. Im Folgenden finden Sie die Matrizen für alle Drehungen: Um die X-Achse: Um die Y-Achse: Um die Z-Achse: 1 0 0 0 0 cos(ax) sin(ax) 0 0 -sin(ax) cos(ax) 0 0 0 0 1 cos(ay) 0 -sin(ay) 0 0 1 0 0 sin(ay) 0 cos(ay) 0 0 0 0 1 cos(az) sin(az) 0 0 -sin(az) cos(az) 0 0 0 0 1 0 0 0 0 1 ax, ay und az stehen bei den genannten Matrizen für die Drehwerte (in Bogenmaß) um die entsprechenden Achsen. Um nun Transformationen miteinander zu kombinieren, sie also in einer Matrix zu vereinigen, müssen die Matrizen miteinander multipliziert werden. Dabei ist jedoch zu beachten, dass die Reihenfolge der Multiplikation eine Rolle spielt, da gilt: Matrix1 * Matrix2 z Matrix2 * Matrix1 Wenn man beispielsweise erst eine Drehung und dann eine Translation durchführen will, stellt man zunächst eine Identitätsmatrix auf und multipliziert diese mit der Drehmatrix. Anschließend wird das Ergebnis mit der Translationsmatrix multipliziert. Der umgekehrte Weg liefert ein völlig anderes Ergebnis. Ist die Transformationsmatrix aufgestellt, können die Vertices (bzw. deren Positionsvektoren) abgebildet werden. Es erscheint sicherlich verwunderlich, dass dreidimensionale Vektoren über eine 4x4-Matrix abgebildet werden. Dies hängt unter anderem damit zusammen, dass die 4. Zeile der Matrix den Verschiebungsvektor (v) der affinen Abbildungsgleichung darstellt: r = bA + v (Affine Abbildung) Die folgende C++-Funktion ist für die Abbildung eines Vektors über eine Matrix zuständig: // TransformVector - Bildet einen Vektor über eine Matrix ab. // Parameter : vec = Abzubildender Vektor mat = Transformationsmatrix // Rückgabewert : Resultierender Vektor D3DVECTOR TransformVector(D3DVECTOR &vec, D3DMATRIX &mat) { // Vektor abbilden D3DVECTOR result; result.x = vec.x*mat._11 + vec.y*mat._21 + vec.z*mat._31 + mat._41; result.y = vec.x*mat._12 + vec.y*mat._22 + vec.z*mat._32 + mat._42; result.z = vec.x*mat._13 + vec.y*mat._23 + vec.z*mat._33 + mat._43; return result; // Ergebnis zurückliefern } Wie aus dem Quelltext hervorgeht, wird der Vektor zunächst über die oberen drei Zeilen und die linken drei Spalten der Matrix abgebildet (3x3-Matrix). Dies entspricht dem ersten Summanden der affinen Abbildungsgleichung (siehe oben). Dann werden die ersten drei Werte der 4. Zeile hinzuaddiert (blau dargestellt). Dies entspricht dem Verschiebungsvektor der affinen Abbildung (zweiter Summand). Neben den erwähnten Transformationsmatrizen gibt es noch Sonderfälle, wie zum Beispiel die Projektionsmatrix, die im nächsten Kapitel erklärt wird. Diese setzen sich jedoch aus den aufgeführten Standardtransformationen zusammen. 2.2.3.5 Vom 3D-Raum auf den Bildschirm Ein Vertex, der von seiner Position im dreidimensionalen Raum auf den Bildschirm projeziert wird, durchläuft die Transformation dreier Matrizen, die miteinander multipliziert werden: Der Weltmatrix, der Sichtmatrix und der Projektionsmatrix. Diese werden im Folgenden genauer erläutert: a) Weltmatrix Die Weltmatrix (engl.: world matrix) ist die erste Stufe der Transformation. Sie wird meistens benutzt, um Modellkoordinaten in Weltkoordinaten umzurechnen. Dies ist dann der Fall, wenn ein Modell – zum Beispiel ein Würfel – in der 3D-Welt bewegt werden soll, dessen Geometrie im Ursprung definiert ist. Solche Objekte sind deshalb im Ursprung positioniert, damit sie mittels einfacher Rotationsmatrizen um ihre Achsen gedreht werden können. Standardmäßig handelt es sich bei der Weltmatrix jedoch um eine Identitätsmatrix, die keinen Einfluss auf die Positionsvektoren nimmt. b) Sichtmatrix Die zweite Stufe wird durch die Sichtmatrix (engl.: view matrix) bestimmt. Sie ist dafür verantwortlich, die Welt vor die Kamera (≅ den Betrachter) zu bewegen. Im Gegensatz zur realen Welt, bewegt sich der Zuschauer nicht in der Welt, sondern die 3DWelt bewegt sich auf den Zuschauer zu. Der Betrachter steht demnach immer im Ursprung, was für die Projektion bedeutsam ist. Da sich nach heutiger Vorstellung Systeme relativ zueinander bewegen, macht dies ohnehin keinen optischen Unterschied. Für den Programmierer bedeutet dies jedoch, eine Matrix aufzustellen, die den Betrachter in der 3D-Welt positioniert, und die sichtbare Geometrie damit korrekt in den Ursprung verlagert. c) Projektionsmatrix Die letzte und gleichzeitig komplizierteste Stufe bestimmt, wie die Geometrie auf den Bildschirm projeziert wird. Die Projektionsmatrix (engl.: projection matrix) legt fest, wie die 3D-Weltkoordinate in eine 2D-Bildschirmkoordinate umgewandelt wird. Beim Aufstellen dieser Matrix entscheidet der Programmierer auch über den Sichtbarkeitsbereich. Dabei definiert er ein Volumen, das durch sechs Ebenen beschrieben wird. Dieses Volumen wird als View frustum bezeichnet und setzt sich aus folgenden Ebenen zusammen: • Die Far plane und die Near plane legen den Tiefenbereich fest, zwischen dem gerendert wird. • Die vier weiteren Ebenen (left, top, right, bottom plane) bestimmen die seitliche Abgrenzung. Daraus resuliert ein Pyramidenstumpf oder Sichtkanal, wie die folgende Abbildung zeigt: Das View frustum: Die Projektionsmatrix definiert den sichtbaren Bereich (grau ausgefüllt). Welchen Sichtwinkel der Betrachter besitzt, wird durch den Field of view (FOV)-Wert festgelegt, der Grundlage für das Aufstellen der Projektionsmatrix ist. Gewöhnlich bewegt sich dieser Wert zwischen 90° und 130°, kann jedoch auch davon abweichen. Die folgende C++-Funktion ermöglicht das Aufstellen einer Projektionsmatrix. // SetProjectMatrix – Stellt eine Projektionsmatrix auf // Parameter : mat fFOV fAspect = Resultierende Matrix (Referenz) = Sichtfeld-Winkel (in Bogenmaß) = Das Verhältnis zwischen Breite und Höhe des Sichtkanals. Hier wird normalerweise der Quotient aus Bildschirmbreite und -höhe eingesetzt. fNearPlane / = Entfernung zur nahen und weiten Ebene, fFarPlane zwischen denen gerendert wird. (in Weltkoordinaten) void SetProjectionMatrix( D3DMATRIX &mat, float fFOV, float fAspect, float fNearPlane, float fFarPlane ) { float w = fAspect * ( cosf(fFOV/2) / sinf(fFOV/2) ); float h = 1.0f * ( cosf(fFOV/2) / sinf(fFOV/2) ); float Q = fFarPlane / ( fFarPlane - fNearPlane ); mat._11 mat._21 mat._31 mat._41 = = = = w; mat._12 = 0; 0; mat._22 = h; 0; mat._32 = 0; -Q*fNearPlane; mat._13 mat._23 mat._33 mat._42 = = = = 0; 0; Q; 0; mat._14 mat._24 mat._34 mat._43 = = = = 0; 0; 1.0f; 0; mat._44 = 0; return S_OK; } Nachdem die 3D-Geometrie über die Multiplikation der drei Matrizen abgebildet wurde, enthalten die Positionsvektoren 2D-Koordinaten (x und y). Diese müssen nun nur noch auf die Bildschirmausmaße skaliert werden. Anschließend können die Surfaces dargestellt werden. 2.2.4 DirectX Die von Microsoft entwickelte Schnittstelle DirectX bildet die Brücke zwischen Soft- und Hardware. Dabei handelt es sich um Treiber, die die Programmierbefehle an die Hardware weiterleiten oder sie selbst verarbeiten. Das dahinter stehende System ist allein so komplex, dass eigene Bücher dazu verfasst wurden. Allein der mitgelieferte Hilfetext für die Programmierung ist knapp 10 Megabyte groß. Daher liefert dieses Teilkapitel nur eine kleine Einführung in das Thema DirectX, um die Übersicht zu gewährleisten. Hierbei verweise ich auf das Quellenverzeichnis am Ende dieser Arbeit. Die folgenden Erläuterungen beziehen sich auf die DirectX-Version 8.1, die bei der Entwicklung des Projekts als aktuell galt. 2.2.4.1 Installation DirectX ist im System nicht enthalten und muss zusätzlich installiert werden. Das Paket mit den entsprechenden Treibern kann im Internet (www.microsoft.com) heruntergeladen werden und richtet sich selbstständig ein. Bei der Installation werden einige DLL-Dateien in den Systemordner kopiert und registriert. Diese sind nach einem Neustart schließlich verfügbar. Um auf die DirectX-Schnittstelle programmiertechnisch anzusprechen, muss ein sogenanntes Software Development Kit (SDK) heruntergeladen werden. Dieses Paket enthält alle Headerdateien15 und Bibliotheken, die das Ansteuern der verschiedenen Komponenten (siehe 2.2.4.2) ermöglicht. Hinzukommt der bereits erwähnte Hilfetext sowie die Möglichkeit, DirectX-Anwendungen zu debuggen, um Fehler leichter zu lokalisieren. DirectX ist in erster Linie auf C++- und Visual Basic-Programmierung ausgerichtet. Mittlerweile gibt es jedoch Bibliotheken, die auch den Zugriff über andere Hochsprachen (zum Beispiel Delphi) ermöglichen. Um DirectX-Programme nun zu schreiben, genügt es, die entsprechenden Headerdateien und Bibliotheken in sein Projekt aufzunehmen. 2.2.4.2 Komponenten Das DirectX-Paket beinhaltet diverse Schnittstellen zu verschiedenen Hardwaresystemen. Diese sind im Folgenden aufgelistet: • DirectX Graphics ermöglicht den Zugriff auf hardware-beschleunigte und softwareemulierte Grafikroutinen, die direkt auf die Grafikkarte des Computers zugreifen. • DirectX Audio erlaubt das Abspielen und Aufnehmen von Soundeffekten. • DirectInput gewährt den Zugriff auf die meisten Eingabegeräte: neben Tastatur und Maus auch auf Joysticks, Lenkräder etc. • DirectPlay unterstützt das Herstellen von Netzwerkverbindungen. Dieser Teil ist für sogenannte Multiplayer-Spiele (Spiele mit mehreren Teilnehmern über ein Netzwerk). • DirectShow gestattet das Abspielen von hochwertigen Multimediaformaten, wie z.B. DVD-Videos. 15 Headerdateien = Schnittstellen (Interface) zu exteren Programmmodulen. Sie entsprechen etwa den Units bei Delphi. • DirectSetup erlaubt die spezifische Installation der DirectX-Treiber. Dies wird von DirectX-Spielen verwendet, um die notwendigen Treiber zu installieren. Das Phalanx-Projekt greift ausschließlich auf die Komponenten DirectX Graphics und DirectInput zu. 2.2.4.3 Zugang zu DirectX Graphics Als Beispiel wird nun der Zugriff auf DirectX Graphics erläutert, die Komponente, die den Hauptteil ausmacht. Zunächst erzeugt der Benutzer eine Instanz auf ein Interface mit Hilfe der Funktion Direct3DCreate8. Die Acht am Ende der Bezeichnung legt fest, dass auf die DirectX-Version 8 zugegriffen wird. DirectX ist nämlich abwärtskompatibel und ermöglicht auch den Zugriff auf ältere Versionen. Die Funktion gibt einen Zeiger auf die IDirect3D8Klasse zurück, die das Interface darstellt. Da sich in einem Computer mehrere Grafikkarten befinden können, muss der Programmierer festlegen, welche Karte er für das 3D-Rendering verwenden will. Mit Hilfe der IDirect3D8Klasse erhält er genaue Listen über die verfügbaren Adapter (≅ Grafikkarten) des Computers und die erlaubten Bildschirmauflösungen. Im nächsten Schritt muss festgelegt werden, welcher Gerätetyp verwendet werden soll. Standardmäßig stehen zwei zur Auswahl: Hardware acceleration layer (HAL) oder Reference driver (REF). Beim HAL-Typ wird das Rendering hauptsächlich von der Grafikkarte übernommen, was eine Entlastung des Prozessors bedeutet. Allerdings besitzt nicht jede Karte 3D-Funktionen, auch wenn dies heutzutage als Standard angesehen werden kann. Der REF-Typ lässt die gesamte Grafik vom Prozessor berechnen. Dadurch ist er normalerweise viel zu langsam, liefert jedoch ein korrektes Ergebnis. Heutige Programme lassen den Anwender darüber entscheiden, welchen Adapter und welchen Gerätetyp er verwenden will. Wenn alle Informationen zusammengetragen werden, kann das 3D-Gerät erzeugt werden. Das 3D-Gerät ist die direkte Schnittstelle zur Grafikkarte. Über die CreateDevice-Methode (Teil des IDirect3D8-Interfaces) lässt sich das Gerät erzeugen. Als Parameter erwartet die Funktion Informationen über den Adapater, den Gerätetyp, den Bildschirmmodus (Fenster oder Vollbild) und die Bildauflösung. Zurückgeliefert wird ein Zeiger auf eine IDirect3DDevice8-Klasse, die den Zugriff auf alle Rendering-Funktionen ermöglicht. Nun kann erst „gearbeitet“ werden. Zwar erscheinen die Vorgänge zur Vorbereitung müßig, allerdings sind sie unbedingt notwendig, seit DirectX-Version 5 aber auch stark vereinfacht worden. Der interne Arbeitsaufwand der DirectX-Treiber ist enorm, da eine Kommunikation zur Grafikkarte aufgebaut werden muss. Die IDirect3DDevice8-Klasse besitzt einen umfassenden Funktionssatz, der das Rendering ermöglicht. Nebenbei gibt es noch weitere Klassen, die die 3D-Darstellung unterstützen. Allerdings werde ich auf die Programmierung nicht weiter eingehen, da sie den Rahmen dieser Arbeit bei weitem sprengen würde. Hierbei verweise ich auf den DirectX-Hilfetext und erneut auf das Quellenverzeichnis am Ende dieser Arbeit. 2.2.4.4 Hardware-Beschleunigung und Software-Emulation DirectX verwendet in Bezug auf Hardwarebeschleunigung eine Art „Hybrid-Modus“. Wird das Hardware acceleration layer als Gerätetyp ausgewählt, überprüft DirectX, welche Beschleunigungsfunktionen die Grafikkarte „anbietet“. Wird nun zum Beispiel eine 3D-Szene gerendert, versucht die Schnittstelle, primär auf diese Funktionen zuzugreifen. Stehen diese jedoch hardwaretechnisch nicht zur Verfügung, wie z.B. bei älteren Grafikkarten, so werden sie emuliert: Die DirectX-Software berechnet die fehlenden Funktionen selber. Dabei wird der Prozessor natürlich stärker belastet, dennoch bußt die Anwendung nicht an Funktionalität ein. Die gleichberechtigte Verwendung von Hard- und Software garantiert umfassende Kompatibilität zu einer breiten Palette von Hardwareprodukten. Allerdings gibt es einige Operationen – besonders im Bereich der 3D-Grafikkarten –, die nicht software-emuliert werden sollten, da sie die Leistung sonst vehement herunterbremsen würden. Dann ist es Aufgabe des Programmierers, solche Funktionen auszuschalten, wenn die Unterstützung seitens der Hardware fehlt. 2.2.4.5 Mathematische Unterstützung Neben den Rendering-Funktionen bietet DirectX ein umfassendes Arsenal an mathematischen Funktionen an, die die Berechnung von Matrizen und Vektoren erleichtern. Das folgende Beispiel demonstriert das Aufstellen einer Rotations-, einer Skalierungs- und einer Translationsmatrix anhand dieser Funktionen. D3DXMATRIX GetTransform() { D3DXMATRIX matRotation, matScale, matTranslation, matResult; // Matrizen aufstellen D3DXMatrixRotationX(&matRotation, D3DX_PI); D3DXMatrixScaling(&matScale, 0.5f, 0.2f, 2); D3DXMatrixTranslation(&matTranslation, 2, 3, 0); // ... und multiplizieren. D3DXMatrixMultiply(&matResult, &matRotation, &matScale); D3DXMatrixMultiply(&matResult, &matResult, &matTranslation); return matResult; // Ergebnis zurückliefern } Alle Funktionen die mit “D3DX” beginnen sind von DirectX mitgeliefert. Wie deutlich erkennbar ist, handelt es sich dabei um eine sinnvolle Unterstützung. 2.2.5 Bildbuffer Bei der 3D-Programmierung kommen sogenannte Bildbuffer zum Einsatz. Dabei handelt es sich um Speicherblöcke, die das gerenderte Bild auf verschiedene Arten speichern. Sie sind die Grundlage des Rendervorgangs, denn sie „entscheiden“ über jeden sichtbaren Pixel. Die Größe eines Buffers ist selbstverständlich von der Renderauflösung abhängig und lässt sich modellhaft in folgender Formel festhalten: Größe in Byte = Breite * Höhe * Farbtiefe Wenn beispielsweise ein Buffer in der Auflösung von 640x480 Pixeln gerendert wird und eine Farbtiefe von 16 Bit besitzt, so belegt der Buffer nach der Formel etwa 614 Kilobyte. In Wirklichkeit ist der Buffer noch größer, was hier jedoch außer Acht gelassen werden kann. Tatsache ist jedoch, dass eine höhere Auflösung einen Leistungseinschub bedeutet, da ein größerer Speicherbereich verwaltet werden muss. Zwei elementare Buffertypen möchte ich im Folgenden vorstellen. 2.2.5.1 Front- und Backbuffer Der Front- und der Backbuffer enthalten das tatsächliche Bild, das der Anwender oder Spieler zu Gesicht bekommt. Angezeigt wird allerdings nur der Frontbuffer, der seinen Namen daher bekam, dass er der einzige dem Benutzer sichtbare Buffer ist. Der Backbuffer ist für das Rendering verantwortlich, der Frontbuffer für das Anzeigen des Bildes auf dem Bildschirm. Beim Bildaufbau wird zunächst ein Frame (≅ Bild) im Backbuffer gerendert. Dann werden Front- und Backbuffer durch eine schnelle Zeigeroperation getauscht: Während der neue Frontbuffer (ehemaliger Backbuffer) das soeben erzeugte Bild an den Bildschirm sendet, wird das nächste Frame bereits im neuen Backbuffer (ehemaliger Frontbuffer) gerendert, bevor die Buffer wiederum getauscht werden. Diese Architektur wird als Swap chain bezeichnet, weil die Buffer in einer Kette hin und her getauscht werden. Eine Anwendung kann durchaus mehrere Backbuffer (daher „Kette“) ansteuern. 2.2.5.2 Z-Buffer Der zweite wichtige Buffertyp ist der Z-Buffer. Wie der Name schon vermuten lässt, speichert dieser Buffer ausschließlich Tiefenwerte. Zu jedem Pixel im Backbuffer gibt es einen Tiefenwert im Z-Buffer. Damit wird eines der größten Probleme der 3D-Programmierung gelöst, nämlich die Frage: „In welcher Reihenfolge muss ich die Polygone rendern?“ Lässt man sie außer Acht, kann es passieren, dass man ein weit entferntes Gebäude der 3D-Welt vor einem nahe stehenden 3D-Baum rendert. Der Z-Buffer liefert ein optimales grafisches Ergebnis: Bevor ein Pixel im Backbuffer gezeichnet wird, wird der Tiefenwert an dieser Stelle überprüft. Ist dieser niedriger als der Tiefenwert des zu zeichnenden Pixels, so ist der bereits eingetragene Punkt näher am Betrachter als der ‚neue’. Daher wird der Bildpunkt nicht überschrieben. Ist der Tiefenwert des neuen Pixels jedoch niedriger als der vorhandene, wird das Pixel überschrieben und der „nähere“ Tiefenwert in den Z-Buffer eingetragen. Vor der Arbeit mit einem Z-Buffer muss dieser natürlich „geleert“ werden, das heißt, alle Tiefenwerte werden auf die weiteste Entfernung eingestellt. Ohne den Z-Buffer sind heutige „High-End-Grafiken“ nicht mehr denkbar. Zwar ist ein Verzicht möglich, indem man versucht, alle Polygone von hinten nach vorne darzustellen, das Ergebnis ist allerdings bei weitem nicht so gut. Eine Z-Buffer-Beschleunigung gilt schon seit Jahren als Standard bei 3D-Grafikkarten. Neben den vorgenannten gibt es noch spezielle Buffer, wie zum Beispiel den Stencil Buffer. Dafür verweise ich Sie jedoch an den DirectX-Hilfetext, der solche Spezialfälle erläutert. 2.2.6 Texturen Um in der 3D-Welt Realismus und intensivere Detailtiefe zu gewährleisten, kommen sogenannte Texturen zum Einsatz. Eine Textur ist nichts anderes als ein Bild (zum Beispiel eine Bitmap), das auf ein Polygon „gelegt“ wird. Wenn beispielsweise eine Mauer gerendert werden soll, ist es nicht erforderlich, jeden Ziegel dreidimensional zu berechnen. Stattdessen reicht ein rechteckiges Polygon aus, das mit einer Mauer-Textur versehen wird, also mit einem Bild, das Mauerwerk beinhaltet. Dadurch wird wesentlich weniger Rechenaufwand benötigt, was die Leistung erhöht. Natürlich ist der ‚Realitätsgrad’ eines Renderings von der Qualität der verwendeten Texturen abhängig. Kommen höher aufgelöste Bilder zum Einsatz, wird mehr Zeit zum Anzeigen der Textur benötigt. Daher muss auch hier ein Kompromiss gefunden werden. Allerdings besitzt heutzutage jede 3D-Grafikkarte Funktionen, die das Darstellen von Texturen erheblich beschleunigen. Texturen können heutzutage noch viele weitere Eigenschaften besitzen, zum Beispiel einen Alphakanal (siehe dazu 4.5.2 und 4.5.3.2). Außerdem können sie auf verschiedene Arten eingesetzt werden. Mit Hilfe von Bump Mapping kann eine Textur beispielsweise so dargestellt werden, dass ihre Oberfläche durch Simulation von Licht und Schatten plastisch wirkt. Der Gebrauch von Texturen definiert fast schon seit Anbeginn des 3D-Zeitalters einen Standard, denn erst er füllt eine 3D-Welt mit Leben. So kommen auch bei diesem Projekt Texturen zum Einsatz. Für das Verwalten von Texturen wurde ein eigenes Programm namens ‚Texture Container’ geschrieben, das in Kapitel 4.5 erläutert wird. 2.2.7 3D-Engine Zuletzt möchte ich noch den Begriff „3D-Engine“ erklären, der in der 3D-SoftwareEntwicklung oft genannt wird, jedoch nie besonders klar erscheint. Die Engine (zu Deutsch: „Maschine“) ist das zentrale Element der grafischen Darstellung. Ihre primäre Aufgabe ist das 3D-Rendering und alle damit verbundenen Aktionen, zum Beispiel das Culling (siehe Kapitel 5.2). Spricht man bei einem Spiel von einer „guten 3D-Engine“ ist oft die Rede von einer schnellen und effektiven 3D-Darstellung mit einem hohen Qualitätsgrad. Eine weitere wesentliche Aufgabe ist die Speicherverwaltung. Die Engine ist für die Reservierung von Speicher verantwortlich und bedient die Schnittstellen der Grafikkarte. Außerdem muss sie die entsprechenden Vorbereitungen für das Rendering durchführen, dazu gehören unter anderem das Einstellen der Bildschirmauflösung sowie das Laden der Texturen. Natürlich trägt die Engine ebenfalls dafür Sorge, dass der reservierte Speicher wieder freigegeben wird und das Betriebssystem wieder voll verfügbar ist. Nicht selten ist das System nach dem Beenden eines Spiels vollkommen ausgelastet und muss neugestartet werden, weil das Programm Memory leaks16 hinterlassen hat. Die Bezeichnung als „Maschine“ resultiert sicherlich daraus, dass die Engine nach dem Startvorgang (Speicher-Reservierung und Interface erzeugen) permanent läuft (EchtzeitRendering) und aktiv beendet werden muss. 2.3 Terminologie Dieses Teilkapitel ergänzt die bisherigen Zusammenhänge um grundlegende Fachbegriffe, die im Laufe dieser Arbeit auftauchen. 2.3.1 Programmversionen Ein Programm kann während der Entwicklung in drei verschiedene Versionstypen eingeordnet werden: Alpha-, Beta- und Final-Version. Diese drei Begriffe werden insbesondere im Kapitel 4 gebraucht, um die zeitliche Entwicklung der einzelnen Komponenten zu charakterisieren. Sie lassen sich einfach erklären. Ein Programm, das sich in der Alpha-Phase befindet, ist weder vollständig noch funktionsfähig. Bei Editoren bedeutet dies zum Beispiel, dass zwar schon einige Funktionen implementiert sind, die Daten jedoch nicht gespeichert und geladen werden können. 16 memory leak [engl.] = EDV: Nicht wieder freigegebener, ungenutzter und damit verlorener Arbeitspeicher Die Bezeichnung der Beta-Version ist einen Schritt weiter: Das Programm funktioniert in seinem Basisumfang. Bei Editoren können Daten gespeichert und geladen werden. Kurz: Mit dem Programm kann gearbeitet werden. Dennoch fehlen noch einige Funktionen, die den Aktionsradius des Benutzers vervollständigen. Normalerweise dauert die Beta-Phase eines Programms am längsten. Während dieser Zeit wird es jedoch schon Tests unterzogen. Sogenannte Beta-Tester prüfen die Software auf schwer zu findende Fehler. Die Final-Version steht für das fertige Programm, auch wenn ein Programm bekanntermaßen nie fertig wird. Bei Änderungen handelt es sich aber meist nur um kleinere Korrekturen oder Erweiterungen. 3 Programmiersprache Alle dem Projekt zugehörigen Softwarekomponenten wurden mit der Programmiersprache C++ entwickelt. Dieses Kapitel gewährt einen kleinen Einblick in diese Sprache und begründet weiterhin, warum gerade sie für diese Arbeit in Frage kam. Außerdem legt es das Fundament für das Verständnis der Quelltextbeispiele, die im weiteren Verlauf dieser Arbeit folgen. An vielen Stellen wird C++ mit Delphi verglichen, um bestimmte Eigenschaften dieser Sprache zu unterstreichen. 3.1 C++ als Erweiterung von C C++ ist eine Programmiersprache, die als Hochsprache bezeichnet wird. Der Programmierer greift also nicht wie bei einem Assembler17 direkt auf den Prozessor zu, sondern entwickelt seine Programme auf einer hohen Ebene, die auf umfangreiche Bibliotheken zugreift. Dennoch ist bei den meisten Anbietern dieser Sprache ein integrierter Assembler vorhanden. C++-Programme werden kompiliert, müssen also vor ihrer Ausführung in Maschinen-Code übersetzt werden. Im Gegensatz zu sogenannten Interpretern (Beispiel: QBasic), die auf dem Markt eher seltener geworden sind (sieht man von Internetsprachen wie PHP ab), wird eine ausführbare Datei bzw. eine EXE-File18 erzeugt. Ein Interpreter übersetzt den Quelltext zeilenweise in Maschinen-Code und führt ihn aus. Daher erfordert das Ausführen solcher Programme die Installation einer entsprechenden Entwicklungsumgebung. Die von C++ kompilierten und gelinkten19 Programme sind unabhängig von einem solchen Steuerungsprogramm und werden direkt vom Prozessor gelesen. Kompilierte Programme sind grundsätzlich wesentlich schneller als interpretierte. Die Sprache C++ wurde ab etwa 1980 von Bjarne Stroustrup als objektorientierte Sprache entwickelt, damals als sogenanntes „C mit Klassen“, 1983 durch die Bezeichnung „C++“ abgelöst. Wie aus dem Suffix „++“20 hervorgeht, ist C++ eine Erweiterung der Sprache C. Ein 17 Assembler sind Maschinencode-Sprachen, die direkt auf der Ebene des Prozessors (CPU) ablaufen. EXE-File = EXecutable File (engl. für “Ausführbare Datei”) 19 Wird ein Programm erzeugt, werden alle Quelltextdateien kompiliert und anschließend miteinander verbunden. Der zweite Vorgang wird als Linken bezeichnet. 20 „++“ ist in C der sogenannte Inkrementoperator, der einen Wert um Eins erhöht (wie das Inc bei Delphi). 18 C-Programm kann gewöhnlich ohne Schwierigkeiten mit einem C++-Compiler übersetzt werden. Die Sprache C besitzt diverse Ähnlichkeiten zu Pascal und Delphi: Variablen müssen vor dem Gebrauch auf ihren Datentyp hin deklariert werden, die Konventionen für Bezeichner sind identisch, es existieren gleichermaßen Funktionen mit Parametern und Rückgabewerten usw. Die Unterschiede sind jedoch prägnant: C/C++ differenziert zwischen Groß- und Kleinschreibung und stellt hohe Anforderungen an die exakte Speicherverwaltung. Der Grund, warum C so populär wurde, liegt sicherlich in der Konzeption der Zeiger21 und der dynamischen Speicherreservierung: Der Programmierer erhält einerseits die Möglichkeit, durch Zeiger auf jede Adresse im Speicher zuzugreifen, und kann andererseits zur Laufzeit dynamisch Speicher reservieren. Dadurch besitzt er unbegrenzte Freiheit in seinem Aktionsradius und genießt außerdem enorme Geschwindigkeitsvorteile. Delphi unterstützt zwar auch Zeiger, diese sind jedoch schon bei der Deklaration auf einen bestimmten Datentyp fixiert. Kapitel 5.1.1 beschreibt diese Vorteile im Zusammenhang mit dem Begriff der Blockspeicherung. Ein weiterer Vorteil ist die Fähigkeit, jeden Datentyp in jeden anderen umwandeln zu können, was bei vielen anderen Sprachen mit Schwierigkeiten verbunden ist. C/C++-Programme gelten grundsätzlich als „schneller“, da sie den Quelltext aufgrund der präzisen Syntax optimal in Maschinencode umwandeln können. Heutige Entwicklungsumgebungen wie Visual C++ von Microsoft bieten des Weiteren aufwendige Optimierungsmethoden an, die den Maschinencode noch schneller oder kleiner gestalten. Wie bei anderen Programmiersprachen haben sich verschiedene Dialekte entwickelt. Man einigte sich jedoch auf den sogenannten ANSI22-C-Standard. Dieser Standard ermöglichte es der Sprache unter anderem plattformunabhängig zu werden. C/C++-Programme finden sich unter allen möglichen Systemen: von DOS, über Windows, bis hin zu Linux etc. Ursprünglich war C für das UNIX-System konzipiert und weitete sich dann auf andere Plattformen aus. C++ ergänzt C um die Objektorientierung, also um die Integration von Klassen und Objekten, was heute zu einem der wichtigsten Werkzeuge für die Windows-Programmierung geworden ist. Außerdem wurde die Sprache vereinfacht: Beispielsweise mussten Variablen nicht mehr zwangsläufig zu Beginn einer Funktion deklariert werden. Weiterhin kamen Operatoren für Speicherreservierung und sogenannte Stream-Klassen hinzu, die zum Beispiel Bildschirmausgaben vereinfachen. C++ hat C nahezu vollständig abgelöst, da es noch wesentlich effizienter arbeitet. 21 Ein Zeiger ist ein Datentyp, der eine Speicheradresse enthält. Die Programmiersprache wird von verschiedenen Herstellern angeboten, primär jedoch von Microsoft und Borland. Grundlage dieses Projekts ist die Version Microsoft Visual C++ 6.0. Die Sprache C++ gehört auch heutzutage zu den am weitesten verbreiteten und macht besonders in der Programmierung unter Windows den dominierenden Anteil aus, da das Betriebssystem selbst zum Großteil in dieser Sprache entwickelt wurde. 3.2 Datentypen C++ besitzt ein großes Arsenal an Datentypen. Dieses Teilkapitel erläutert die wichtigsten Typen, die der Programmierer kennen muss, um mit der Sprache arbeiten zu können. 3.2.1 Einfache Datentypen Die einfachen oder Standard-Datentypen bilden die Basis der elektronischen Datenverwaltung. Ihre einzige Aufgabe ist es, Zahlenwerte zu speichern. Diese Typen werden unter verschiedenen Gesichtspunkten differenziert: • Größe in Bytes – 2 Byte (1 Byte = 8 Bit) können 65536 (216) verschiedene Werte speichern. • Dezimalzahl (integral type) oder Fließkommazahl (floating type) • Vorzeichenwert oder nicht? Wird ein Vorzeichen beachtet, wird für „+“ oder „-“ ein Bit reserviert. Dafür wird der Höchstwert reduziert. Bei einem 1-Byte-Wert ändert sich der Wertebereich durch ein Vorzeichenbit von [0; +255] auf [-127; +128]. Die folgende Tabelle liefert eine Übersicht über die gängigen Datentypen in C++: Datentyp Größe char 1 Byte Wertebereich (mit Vorzeichen) [-127; 128] short int / long 2 Bytes 4 Bytes [-32767; 32768] [-2147483647; 2147483648] int64 8 Bytes [-9223372036854775807; 9223372036854775808] 22 ANSI = American National Standards Institute (engl.) Gewöhnliche Anwendung Speicherung eines Zeichens oder einer Tastatureingabe Kleinere Zahlenwerte Normaler oder größere Zahlenwerte Speicherung extrem großer Werte float 4 Bytes [3,4 * 10-38; 3,4 * 10+38] double 8 Bytes [1,7 * 10-308; 3,4 * 10+308] Technische Werte, die eine normale Präzision erfordern Technische Werte, die eine sehr hohe Präzision erfordern Bei der Entwicklung von C++-Programmen ist zu beachten, dass die Größe eines Datentyps von der Zielplattform abhängt. Bei DOS-Programmen reserviert ein Integer-Wert beispielsweise nur 2 Bytes. Die oben stehende Tabelle ist auf Win3223-Programme bezogen. Die Deklaration einer Variable kann in C++ auch innerhalb einer Funktion erfolgen und nicht – wie bei Delphi – nur zu Beginn dieser. Außerdem kann direkt ein Wert zugewiesen werden. Das folgende Beispiel demonstriert dies: // C++ double f = 2.5; { Delphi } Var f : REAL; Begin f := 2.5; End; Hier wird ein klarer Unterschied zwischen den beiden Sprachen deutlich. Wo Delphi ein „:=“ für die Zuweisung verwendet, benutzt C++ ein „=“. Für einen Vergleich zwischen zwei Werten werden in beiden Sprachen ebenso andere Operatoren verwendet, in Delphi ein einfaches „=“, in C++ ein doppeltes („==“). Die strikte Unterscheidung des Zuweisungs- und des Unterscheidungs-Zeichens ist bei C++ sehr wichtig und gehört zu den häufigsten Fehlern, die Anfänger machen. Normalerweise berücksichtigen alle C++-Datentypen Vorzeichenhaftigkeit. Um auf negative Zahlen zu verzichten, muss einfach ein unsigned vor den Typ geschrieben werden. Die Umwandlung von einem Typ in einen anderen ist sehr einfach, und auch hier zeigt sich C++ flexibel. Das folgende Beispiel wandelt eine Fließkommazahl in eine Dezimalzahl um: // C++ double f = 2.5; int n = (int) f; 23 { Delphi } Var f : REAL; n : INTEGER; Begin f := 2.5; n := Trunc(n); End; Win32 = Windows-Version (95+) mit sogenanntem 32-Bit-System, das u.a. größere Datentypen unterstützt Während Delphi einen Funktionsaufruf (Trunc) benötigt, um die Nachkommastellen „abzuschneiden“, reicht bei C++ eine eingeklammerte oder umklammernde Typbezeichnung ( int(f) ) aus. Dies ist deshalb sinnvoll, da wirklich jeder Typ in jeden anderen umgewandelt werden kann, auch wenn dabei Datenverluste entstehen können. Der folgende Ausdruck wandelt einen long-Wert in einen char-Wert um, auch wenn dafür die ersten 24-Bit entfernt werden müssen: double l = 128854392318; unsigned char n = unsigned char(l); // l = 128854392318 // n = 254 Diese Vorgehensweise ist bei Delphi nicht möglich, da die beiden Typen dort als inkompatibel gelten. Neben Zahlenwerten kann auch mit Ascii-Zeichen, zum Beispiel mit Buchstaben, gearbeitet werden. In dem folgenden Beispiel wird der Ascii-Code des Zeichens „A“ (= 65) in einer char- und in einer int-Variable gespeichert: // C++ char c; int n; c = n = $ $ { Delphi } Var c : CHAR; n : INTEGER; Begin c := $ n := Ord( $ End; Wie Sie sehen, werden Zeichen- und Zahlenwert in C++ gleichermaßen zugewiesen, da der Compiler den Buchstaben „A“ automatisch in den entsprechenden Typ umwandelt. Bei Delphi ist damit wiederum ein Funktionsaufruf verbunden (Ord). Durch diese Technik spart C++ diverse Umwandlungsfunktionen ein und gewährt maximale Kompatibilität. 3.2.2 Komplexe Datentypen Komplexe Datentypen besitzen keine vom System vorbestimmte Größe. Solche Typen sind aus anderen zusammengesetzt und müssen vom Programmierer erst definiert werden. Die beiden wichtigsten dieser Art sind Strukturen und Klassen. In C++ werden sie durch die Schlüsselworte struct und class definiert. Strukturen dienen dazu, verschiedene Informationen zu einem Typ zu vereinigen. Sie entsprechen den Records in Delphi und werden in ähnlicher Weise definiert, wie das folgende Beispiel demonstriert: // C++ struct Uhrzeit { int Stunde, Minute, Sekunde; } { Delphi } Type Uhrzeit = Record Stunde, Minute, Sekunde : INTEGER; End; Nach der Definition kann der neue Datentyp verwendet werden. Der direkte Zugriff ist bei C++ und Delphi nahezu identisch und erfolgt über einen Punkt: // C++ { Delphi } Uhrzeit zeit; Var zeit : Uhrzeit; Begin zeit.stunde := 12; zeit.minute := 34; zeit.sekunde := 0; End; zeit.stunde = 12; zeit.minute = 34; zeit.sekunde = 0; Soll über einen sogenannten Zeiger auf eine Struktur zugegriffen werden, ist die Syntax etwas anders, wie in Kapitel 3.2.4 besprochen wird. Klassen sind die komplexesten Datentypen: Sie können neben den Daten auch Funktionen enthalten, die als Methoden bezeichnet werden. Darüber hinaus kann jedes Element einer Klasse verschiedene Zugriffsrechte besitzen, was den Zugriff von innen und außen betrifft. Außerdem können Klassen vererbt werden. Ich möchte hier nicht weiter darauf eingehen, da die Grundlagen allein zu umfangreich für diese Arbeit wären. Im Zusammenhang mit der Objektorientierten Programmierung (OOP) gibt es unzählige Bücher, die sich mit dem Thema Klassen auseinandersetzen. Dazu verweise ich auf das Quellenverzeichnis am Ende dieser Arbeit. Das folgende Beispiel zeigt die Deklaration und Implementation einer einfachen Klasse (in C++): // DEKLARATION class CUhrzeit { // Öffentliche Elemente public: // Konstruktor CUhrzeit(int nStunde, int nMinute, int nSekunde); // Methoden int GetStunde(); int GetMinute(); int GetSekunde(); // Geschützte Elemente protected: // Member-Variablen int m_nStunde, m_nMinute, m_nSekunde; }; // IMPLEMENTATION // Konstruktor – Initialisiert die Klasse mit Startwerten CUhrzeit::CUhrzeit(int nStunde, int nMinute, int nSekunde) { m_nStunde = nStunde; m_nMinute = nMinute; m_nSekunde = nSekunde; } // Methoden – Liefern die Uhrzeitwerte zurück int CUhrzeit::GetStunde() { return m_nStunde; // Stunde } int CUhrzeit::GetMinute() { return m_nMinute; } int CUhrzeit::GetSekunde() { return m_nSekunde; } // Minute // Sekunde // HAUPTPROGRAMM void main() { CUhrzeit jetzt(14, 47, 23); // Werte cout << cout << cout << } // Objekt erzeugen ausgeben 6WXQGH MHW]W*HW6WXQGH \nMinute : MHW]W*HW0LQXWH \nSekunde : MHW]W*HW6HNXQGH Dieses Beispiel demonstriert unter anderem die Zugriffsrechte einer Klasse. Auf die sogenannten Member-Variablen (≅Variablen, die der Klasse gehören) ist von außen kein Zugriff möglich, da sie geschützt (protected) sind. Die Methoden GetStunde, GetMinute und GetSekunde sind jedoch öffentlich (public) zugänglich. Über sie können die gespeicherten Werte abgefragt und ausgegeben werden. Klassen und Strukturen erzeugen Objekte. Das Objekt in dem oben aufgeführten Beispiel heißt „jetzt“. Die Arbeit mit Objekten erlaubt eine logische und übersichtliche Strukturierung. Daher wird die objektorientierte Programmierung auch bei der Windows- und 3D-Programmierung primär eingesetzt, da große Datenmengen verwaltet und strukturiert werden. 3.2.3 Arrays und Strings Ein Array dient dazu, eine Liste von Daten abzuspeichern. Die Deklaration und der Zugriff erfolgt über eckige Klammern. Im Gegensatz zu Delphi kann bei C++ kein Indexbereich (zum Beispiel 1-10) angegeben werden. Listen sind in C++ grundsätzlich nullindiziert (erster Eintrag besitzt den Index 0). Das folgende Beispiel zeigt Deklaration und Zugriff auf eine Liste in den beiden Sprachen: // C++ { Delphi } int zahlen[20], a; Var zahlen : ARRAY[10..29] OF INTEGER; a : INTEGER; Begin FOR a := 10 TO 29 DO zahlen[a] := a * 2; End; for(int a = 0; a < 20; a++) zahlen[a] = a * 2; Beide Versionen erzeugen eine Liste aus zwanzig Dezimalwerten. Die Delphiversion definiert jedoch den Indexbereich von 10 bis 29, der Zugriff auf den ersten Eintrag erfolgt also über zahlen[10]. Diese Möglichkeit bietet C++ zwar nicht, allerdings bietet diese Erweiterung geringe Vorzüge und kann durch ein Umdenken leicht kompensiert werden. Natürlich können Arrays wie in anderen Sprachen auch mehrdimensional sein. Bei Zeichenketten bzw. Strings ist Delphi klar im Vorteil. In beiden Sprachen werden Strings durch Arrays von Zeichen (char-Werten) dargestellt. In Delphi ist jedoch ein besonderer Datentyp namens STRING verfügbar, der Textverarbeitung stark vereinfacht. Über die Operatoren := und + kann auf einfache Weise Text zugewiesen und hinzugefügt werden. Bei C++ sind dafür Funktionsaufrufe notwendig. Das folgende Beispiel demonstriert den Unterschied: // C++ { Delphi } char name[256]; Var name : STRING; Begin name := $OH[DQGHU name := name + GHU*URH End; strcpy(name, strcat(name, $OH[DQGHU GHU*URH Um dieses Manko auszugleichen, gibt es in den Microsoft Foundation Classes (siehe Kapitel 2.1.6) eine Klasse namens CString, die ebenfalls Zugriff über Operatoren gewährt, wie der folgende Quelltext zeigt: // C++ (mit Nutzung der MFC) CString name; name := $OH[DQGHU name += GHU*URH Allerdings ist die Arbeit mit dem STRING-Datentyp von Delphi wesentlich komfortabler. C++ verwendet für die Eingrenzung von Stringkonstanten (wie in unserem Beispiel: „Alexander“) doppelte Anführungszeichen, für Zeichen (char-Datentyp) einfache. Delphi benutzt hingegen für beide Typen einfache Anführungszeichen. Strings werden in C++ genau wie in Delphi mit einem abschließenden Nullbyte (Ascii-Code: 0) begrenzt. 3.2.4 Zeiger Die Zeiger-Technologie hat C++ zu einer der besten Programmiersprachen gemacht. Ein Zeiger ist ein Datentyp, der eine Speicheradresse speichert. Er kann die Adresse einer Variablen, eines Objekts oder einer Funktion enthalten, oder einfach in irgendeinen Speichbereich des Computer zeigen, um zum Beispiel direkt auf den Bildschirmspeicher zu schreiben. Die Deklaration eines Zeigers sieht in C++ folgendermaßen aus: 1 2 int *pZeiger; int Zahl = 5; // Zeiger auf Dezimalzahl deklarieren // Dezimalzahl deklarieren und auf 5 setzen 3 pZeiger = &Zahl; // pZeiger „zeigt“ auf die Zahl-Variable // (speichert ihre Adresse) 4 5 cout << pZeiger; cout << *pZeiger; // Ausgabe der ADRESSE (Beispiel: 45FB:43AF) // Ausgabe der Zahl, die an dieser Adresse // gespeichert ist // Ausgabe: 5 6 Zahl = 9; // Der Dezimalzahl wird ein neuer Wert // zugewiesen 7 cout << *pZeiger; // Dezimal an der Speicheradresse ausgeben // Ausgabe: 9 Mit dem &-Operator wird ein Zeiger auf eine Variable ausgerichtet. Der *-Operator dient – auf den Zeiger angewendet – dazu, auf den Inhalt der Speicheradresse zuzugreifen, in diesem Fall auf eine Dezimalzahl. Da eine Änderung der Zahl (Zeile 6) nicht zu einer Änderung der Speicheradresse führt, muss der Zeiger nicht erneut ausgerichtet werden. Für den Zugriff auf eine Struktur über einen Zeiger muss eine andere Syntax beachtet werden. Hier kommt der ->-Operator ins Spiel. In dem folgenden Beispiel gehen wir von der oben definierten Klasse CUhrzeit aus: // Objekte erzeugen CUhrzeit jetzt(19, 54, 53), gleich(20, 15, 00); // Zeiger auf Objekt „jetzt“ ausrichten CUhrzeit *pZeit = &jetzt; // Uhrzeit ausgeben: „19:54:53“ cout << pZeit->GetStunde() << S=HLW->GetMinute() << << pZeit->GetSekunde(); // Zeiger auf Objekt „gleich“ ausrichten pZeit = &gleich; // Uhrzeit ausgeben: „20:15:00“ cout << pZeit->GetStunde() << S=HLW->GetMinute() << << pZeit->GetSekunde(); Im Gegensatz zu Delphi müssen Zeiger nicht auf einen bestimmten Datentyp fixiert werden und können ohne weiteres ineinander umgewandelt werden, wie das folgende Beispiel zeigt: // Objekte erzeugen CUhrzeit jetzt(19, 54, 53); // long-Zeiger auf das Objekt „jetzt“ long *pZeiger = (long*) &jetzt; // Uhrzeit ausgeben: „19:54:53“ (vorher den Zeiger umwandeln) cout << ( (CUhrzeit*) pZeiger )->GetStunde() << << ( (CUhrzeit*) pZeiger )->GetMinute() << << ( (CUhrzeit*) pZeiger )->GetSekunde(); Ein großer Vorteil der C++-Zeigerarithmetik ist die Verschiebbarkeit von Zeigern. Die in einem Zeiger enthaltene Speicheradresse kann ohne weiteres geändert werden, was sich besonders bei Arrays rentiert, da weniger Berechnungsaufwand betrieben werden muss. Das folgende Beispiel nutzt diese Technik, um eine Stringlänge zu ermitteln: // String erzeugen char Name[32]; strcpy(Name, $OH[DQGHUGHU*URße char *pStr = &Name[0]; // Zeiger auf das erste Zeichen int nLength = 0; while(*pStr != 0) { nLength++; pStr++; } // // // // // Länge zurücksetzen Schleife so lange ausführen, bis Nullbyte erreicht ist. Längenwert hochzählen Zeiger um eins weiterschieben // Länge ausgeben: „19“ cout << nLength; Hierbei verweise ich wiederum auf Kapitel 5.1.1, was die Vorteile dieser Technik näher beleuchtet. Zuletzt möchte ich noch eine Besonderheit erwähnen: den Zeiger auf eine Funktion. Zeiger können also nicht nur auf Daten zeigen, sondern auch auf Programmcode, da dieser ebenfalls im Arbeitsspeicher abgelegt ist. Ein solcher Zeiger ist schwieriger zu implementieren und muss den Rückgabewert und die Parameter einer Funktion berücksichtigen. Für weitere Informationen über Funktionen in C++ verweise ich auf das nächste Kapitel. Das folgende Beispiel veranschaulicht aber die notwendige Syntax für solche Zeiger. // Funktion zur Ermittlung eines Maximalwertes int Max(int a, int b) { if(a > b) return a; else return b; } // Funktion zur Ermittlung eines Minimalwertes int Min(int a, int b) { if(a < b) return a; else return b; } // Hauptprogramm void main() { // Funktionszeiger deklarieren int (*pCompare) (int, int); // Eingabe von zwei Werten int v1, v2; cin >> v1; cin >> v2; // Minimalwert ausgeben pCompare = Min; cout << Minimum: << pCompare(v1, v2); // Maximalwert ausgeben pCompare = Max; cout << \nMaximum: << pCompare(v1, v2); } Um zu signalisieren, dass ein Zeiger keine Ausrichtung besitzt, wird er auf die longKonstante „NULL“ gesetzt. Dies entspricht dem „NIL“ bei Delphi. Die Zeiger sind Ursache für die Popularität der Sprache C++. Andere Hochsprachen wie Delphi bieten keine so große Flexibilität beim Umgang mit diesem Datentyp. Die gewonnene Freiheit hat auch ihren Preis, denn der Programmierer muss mit den Speicheradressen extrem sorgsam umgehen, da ein falsch ausgerichteter Zeiger gravierende Folgen nach sich ziehen kann. Wenn unter Windows die bekannte Meldung „Zugriffsverletzung“ erscheint, hat das Betriebssystem gerade wieder verhindert, dass ein Programm auf einen falsch ausgerichteten Zeiger zugreift. Dennoch hat sich das Zeigerkonzept von C++ als so effektiv und schnell erwiesen, dass es heutzutage nicht mehr wegzudenken ist. 3.3 Funktionen Wie in anderen Hochsprachen können auch in C++ Unterprogramme definiert werden. Die Unterprogramme besitzen einen einfachen Aufbau: • Funktionskopf: Rückgabewert | Bezeichnung | Parameter • Körper (Code) Eine Unterscheidung von Funktionen und Prozeduren gibt es in C++ nicht. Prozeduren sind Funktionen mit dem Sonder-Rückgabewert void („nichts / unbestimmt“), der keinen Wert enthält. Darüber hinaus kann eine Funktion jederzeit über den Ausdruck return verlassen werden, was die Flexibilität dieser Sprache wiederum verdeutlicht. Das folgende Beispiel zeigt den Aufbau von typischen Funktionen in C++ und stellt sie der Syntax von Delphi gegenüber: // C++ { Delphi } int GetFaku(int f) function GetFaku(f:INTEGER):INTEGER; Var Faku, a : INTEGER; Begin IF f < 0 THEN result := -1 ELSE IF f = 0 THEN result := 1 ELSE Begin Faku := 1; FOR a := 2 TO f DO Faku := Faku * a; result := Faku; End; { if(f < 0) return –1; if(f = 0) return 1; int Faku = 1; for(int a = 2; a <= f; a++) Faku *= a; return Faku; } End; Dieses Beispiel berechnet die Fakultät einer Zahl und überprüft zuvor die übergebenen Parameter (Rückgabe: -1 = Fehlerhafter Parameter übergeben). Wie Sie sehen, bewahrt die C++-Version dadurch die Übersichtlichkeit, dass es die Funktion bei einem falschen Parameter über return direkt verlässt. In Delphi ist dafür eine Verschachtelung von Bedingungen notwendig, so dass der eigentliche Code stark eingerückt werden muss. Ansonsten weicht die Syntax zwischen den Sprachen kaum ab. Statt Begin und End verwendet C++ geschweifte Klammern, um Blöcke abzugrenzen. Die Funktionsköpfe differieren nur durch die Position vom Datentyp des Rückgabewerts. Allerdings benötigt C++ keinen separaten Deklarationsteil. Prozeduren und Referenzparameter24 sehen in C++ etwas anders aus. Der folgende Quelltext zeigt den Kopf der Fakultätsfunktion in Prozedurenschreibweise: // C++ { Delphi } void GetFaku(int f, int &Rueck); procedure GetFaku(f:INTEGER; Var Rueck:INTEGER); Das void-Schlüsselwort ersetzt den procedure-Ausdruck. Referenzparameter werden durch ein bitweises UND-Zeichen („&“) charakterisiert und nach dem Datentyp aufgeführt. Es steht für den Var-Ausdruck von Delphi. Die Referenz auf eine Dezimalzahl könnte in C++ aber auch mittels int* in Form eines Zeigers übergeben werden. Ein besonderes Feature von C++ ist die Fähigkeit, Funktionen zu überladen. Mehrere Funktionen können mit dem gleichen Namen aber unterschiedlichen Parametern und Rückgabewerten deklariert werden. So können über einen Funktionsnamen spezifische Aufgaben erfüllt werden. Ein einfaches Beispiel (C++) verdeutlicht dies: float Multiply(float v1, float v2) { return (v1 * v2); } // Multiplikation von // Fließkommazahlen (Float) int Multiply(int v1, int v2) { return (v1 * v2); } // Multiplikation von // Dezimalzahlen (Integer) void main() { // Werte deklarieren int d1 = 5, d2 = 7; float f1 = 2.7f, f2 = 3.1f; // Hauptprogramm // Integer-Version wird aufgerufen cout << Multiply(d1, d2) << \n // Float-Version wird aufgerufen cout << Multiply(f1, f2); } In der Praxis wird das Überladen von Funktionen oft eingesetzt, da es wiederum Flexibilität und Übersicht verschafft. Das Hauptprogramm ist in C++ ebenfalls eine Funktion. Ihr Name ist von der Plattform abhängig. Der Einfachheit halber wurde bei den aufgeführten Beispielen die DOS-Funktion main() gewählt. Unter Windows lautet sie WinMain(). Bei Bedarf kann main() auch einen Rückgabewert (Integer-Datentyp) zurückliefern oder an das Programm übergebene Parameter abfragen. In unseren Beispielen ist dies jedoch nicht notwendig. 3.4 Bedingungen In C++ gibt es verschiedene Möglichkeiten, Bedingungen abzufragen und darauf zu reagieren. Die gängigste Methode ist das if-Statement, das in den meisten Programmiersprachen enthalten ist. Die Syntax sieht folgendermaßen aus: if( <Bedingung> ) <Code> else if( <Bedingung> ) <Code> else <Code> Die Bedingung kann, muss aber nicht zwangsläufig ein Vergleich sein ( if(a > 3) ), es kann zum Beispiel auch ein Funktionsaufruf ( if(KeyPressed() ) oder eine Zuweisung sein, die dann wahr ist, wenn ein Wert ungleich Null zugewiesen wurde. Der Code, der auf eine wahre Bedingung reagiert, kann entweder aus einer Zeile oder einem Block bestehen. Das folgende Beispiel zeigt eine einfache Bedingungsstruktur: int Zahl; cin >> Zahl; // Eingabe eines Wertes // Analyse des Wertes if(Zahl == 1) cout << =DKOLVWJOHLFK else if(Zahl > 1) cout << =DKOLst größer als 1. else { // Als Block cout << =DKOLVWNOHLQHU cout << DOV } 24 Parameter, die keine Kopie einer Variable übergeben, sondern die Variable selbst. Dies wird intern über Zur Verknüpfung mehrerer Bedingungen dient der &&-Operator (logisches „Und“) und der ||Operator (logisches „Oder“). Der !-Operation negiert eine Bedingung, kehrt das Ergebnis also ins Gegenteil um (true wird zu false, false zu true). Das folgende Beispiel umfasst all diese Operatoren: if(!(Zahl == 1 || Zahl == 2)) cout << (LQJDEHLVWZHGHUQRFK\n if(Zahl >= 0 && Zahl <= 10) cout << =DKOOLHJWLP%HUHLFKYRQELV\n Das zweite Schlüsselwort, das bei Bedingungen oft Verwendung findet, ist der switchAusdruck. Er entspricht dem case von Delphi und sieht folgendermaßen aus: switch( <Variable> ) { case <Zustand>: <Reaktions-Code> [break;] default: <Standard-Code> } switch fragt Zustände einer Variablen ab. Die einzelnen Werte werden über case abgefragt. default bestimmt alle anderen (nicht aufgeführten) Zustände. Hinter dem case folgt der Reaktions-Code. break verlässt die switch-Abfrage. Fehlt es, werden alle folgenden Reaktions-Codes ausgeführt, bis das Ende der Abfrage oder ein break erreicht wird. Das folgende Beispiel zeigt dies: char Zeichen; cin >> Zeichen; // Eingabe eines Zeichens // Analyse des Zeichens switch(Zeichen) { case A : case B : case C : cout << =HLFKHQLVW$%RGHU& break; default: cout << =HLFKHQLVWXQEHNDQQW } Zeiger geregelt. Die letzte, aber sehr praktische Bedingungsabfrage wertet der ?-Operator aus und führt keinen Code aus, sondern wählt zwischen zwei Werten aus. Die folgende Syntax gilt: <Bedingung> ? <Wert1> : <Wert2> Ist die Bedingung wahr, wird Wert1 ausgewählt, ansonsten Wert2. Das folgende Beispiel zeigt die praktische Anwendung: int Zahl; cin >> Zahl; // Eingabe einer Zahl // Analyse und Ausgabe cout << =DKOLVW (Zahl < 0) ? 3.5 QHJDWLY SRVLWLY ; Schleifen Im Sprachumfang von C++ sind drei Schleifentypen vorhanden: • for-Schleife (entspricht dem FOR...TO...DO von Delphi) • while-Schleife (entspricht dem WHILE...DO von Delphi) • do...while-Schleife (entspricht dem verneinten REPEAT...UNTIL von Delphi) Die Syntax dieser drei Typen sieht folgendermaßen aus: for( <Initialisierung>; <Bedingung>; <Inkrementierung> ) <Code> while( <Bedingung> ) <Code> do <Code> while( <Bedingung> ) Die for-Schleife kommt normalerweise zum Einsatz, wenn ein bestimmter Zahlenbereich durchlaufen werden soll. while wird benutzt, wenn ein Vorgang ausgeführt werden soll, bis ein bestimmtes Ereignis eintritt bzw. eine Bedingung unwahr wird. do...while ist eine Variante davon, bei der der Code mindestens einmal ausgeführt wird, da die Bedingungsabfrage nach der Codeausführung erfolgt. Das folgende Beispiel demonstriert die drei Typen im Einsatz: // C++ { Delphi } Var a : INTEGER; Begin // For-Schleife for(int a = 1; a <= 10; a++) cout >> a; FOR a := 1 TO 10 DO Writeln(a); // While-Schleife a = 1; while(a <= 10) { cout >> a; a++; } a := 1; WHILE (a <= 10) DO Begin Writeln(a); Inc(a); End; // Do...while-Schleife a = 1; do { cout >> n; a := 1; REPEAT Writeln(a); Inc(a); UNTIL (a > 10); } while(++a <= 10); Zu beachten ist hierbei, dass die REPEAT...UNTIL-Schleife von Delphi eine Abbruchbedingung erwartet, die do...while-Schleife von C++ eine Durchlaufbedingung. Um die Delphi-Schleife also abzubrechen, muss die Schleifenbedingung positiv sein, bei der do...while-Schleife muss sie dagegen positiv sein, um den Schleifenlauf fortzusetzen. Auch in Bezug auf Schleifen ist die C++-Syntax sehr flexibel. Bei der for-Schleife sind zum Beispiel Initialisierung und Inkrementierung optional. Eine while-Schleife kann also ohne weiteres durch eine for-Schleife dargestellt werden: while( <Bedingung> ) <Code> → for( ; <Bedingung>; ) <Code> Im Gegensatz zu Delphi kann eine Schleife in C++ außerdem jederzeit verlassen werden. Das Schlüsselwort break unterbricht eine Schleife, continue springt dagegen zum Ende des Schleifencodes und führt den Schleifenkopf (Inkrementierung und Bedingungsprüfung) wieder aus. Im folgenden Beispiel (C++) finden Sie die beiden Schlüsselworte im Einsatz: for(int a = 1; a <= 100; a++) { if(a >= 50 && a <= 60) continue; // Zahlenbereich von 50 bis 60 // nicht ausgeben! cout >> a; } cout << %LWWHEHVWlWLJHQ6LHPLW(17(5 while(1) // Eigentlich eine Endlos-Schleife (Bedingung immer wahr) { char ch = getch(); // Abfrage einer Taste if(ch == 13) // Schleife verlassen, wenn ENTER break; // gedrückt wird. cout << \nFalsche Taste!“; } 3.6 Dynamische Speicherreservierung Neben den statischen Variablen, die zuvor erklärt wurden, kann der Programmierer Speicher auch dynamisch verwalten. Der Ausdruck „dynamisch“ leitet sich daraus ab, dass die Größe des zu reservierenden Speichers vor dem Programmstart noch nicht feststeht und zur Laufzeit bestimmt wird. Der Programmierer ist zunächst dafür verantwortlich, den dynamischen Speicher zu reservieren, muss dabei jedoch auch in Betracht ziehen, dass kein Speicher verfügbar sein kann. Nach dem Gebrauch ist in jedem Fall darauf zu achten, dass der reservierte Platz auch wieder freigegeben wird. Mit C wurden drei Funktionen eingeführt, die die dynamische Speicherverwaltung ermöglichen: void *malloc( size_t size ); void *realloc( void* memblock, size_t size); void free( void* memblock ); malloc() reserviert einen Speicherblock von der Größe size (in Bytes) und liefert einen Zeiger darauf zurück. Die NULL-Konstante wird jedoch zurückgegeben, wenn kein Speicher reserviert werden konnte. realloc() ändert die Größe eines Blocks und liefert eine möglicherweise neue Speicheradresse zurück. free() gibt belegten Speicher wieder frei und erwartet dafür die Speicheradresse, die von den anderen Funktionen generiert wurde. Mit C++ wurden zwei Operatoren eingeführt, die die Reservierung von Speicher wesentlich vereinfachen: new, um Speicher zu reservieren, und delete, um ihn wieder freizugeben. Ihre Syntax sieht folgendermaßen aus: pAdresse = new <Datentyp>[<Dimension>]; delete pAdresse; Das folgende Beispiel zeigt die praktische Umsetzung: void main() { // Eingabe: Anzahl der Werte int nAnzahl; cout << :LHYLHOH:HUWHJHQHULHUHQ" cin >> nAnzahl; // Speicher RESERVIEREN int *pWerte = new int[nAnzahl]; if(pWerte == NULL) { // Reservierung FEHLGESCHLAGEN! // => Fehlermeldung und Abbruch. cout << 6SHLFKHUNRQQWHQLFKWUHVHUYLHUWZHUGHQ return; } // Liste mit Werten füllen (über einen Zeiger, der von Element // zu Element wandert) int *pWert = pWerte; for(int a = 0; a < nAnzahl; a++, pWert++) *pWert = a * a; // Reservierten Speicher wieder FREIGEBEN delete pWerte; } Für eine Vertiefung dieses Themas verweise ich auf Kapitel 5.1, das sich genauer mit der dynamischen Speicherreservierung befasst. Wie dieser kleine Ausflug in C++ gezeigt hat, ist diese Programmiersprache sehr flexibel und erlaubt dem Programmierer große Freiheit in der Gestaltung seines Codes. Allerdings stellt sie entsprechende Anforderungen an ihre Benutzer und schreckt Anfänger nach eigener Erfahrung schnell ab. Wer die ersten Hürden jedoch überwunden hat, versteht schnell, warum C++ für dieses Projekt die einzig denkbare Wahl war. 4 Komponenten des Projekts Dieses Kapitel erläutert alle Programmkomponenten des Projekts. Neben der Beschreibung werden die Steuerung der Programme sowie die zentralen Aspekte der technischen Umsetzung beleuchtet. Des Weiteren werden die Projekte in den zeitlichen Gesamtkontext eingeordnet. Da eine umfassende Erklärung aller technischen Zusammenhänge den Rahmen dieser Arbeit allerdings sprengen würde, sind hier nur die wichtigsten aufgeführt, die die Funktionsweise der jeweiligen Programme veranschaulichen. Hierbei verweise ich zur Ergänzung auf die beiliegenden Logbücher und auf den Quelltext, der vollständig auf Englisch kommentiert ist. 4.1 Das Konzept Das Projekt umfasst mehrere Komponenten, die in einem funktionalen Verhältnis zueinander stehen. Das folgende Schema verdeutlicht dies: Hauptprogramme Worldbuild Racer Phalanx Updater Externe Kontrollklassen Werkzeuge Texture Container Ship Editor File Container WbCreateUpdate Funktionales Konzept des Projekts Durchgezogener Pfeil: direkter Zugriff – Gestrichelter Pfeil: Bereitstellung von Ressourcen Die beiden Hauptprogramme Worldbuild (4.2) und Racer (4.3) arbeiten unabhängig voneinander, wobei Racer auf die vom Worldbuild-Editor erzeugten Daten (kompilierte Karten) zurückgreift. Der Texture Container (4.5) stellt für die beiden Hauptprogramme Texturen bereit. Ebenso laden beide Programme während ihrer Ausführung die externen Kontrollklassen (4.4). Der Ship Editor (4.9) versorgt den Racer mit Informationen über verschiedene Schiffstypen, die ein Spieler fliegen kann. Der Phalanx Updater (4.7) ist ein eigenständiges Programm, dessen Daten über den File Container (4.6) und WbCreateUpdate (4.9.1) erzeugt werden. 4.2 Worldbuild 4.2.1 Funktion Worldbuild ist das Kernstück des Projekts und damit auch die komplexeste und umfangreichste Anwendung (fast 50.000 Zeilen). Hierbei handelt es sich um einen auf Windows basierenden Editor, der die Entwicklung dreidimensionaler Szenarien erlaubt. Der Benutzer hat die Möglichkeit, mit einfachen Mitteln die Architektur von Gebäuden und Landschaften zu kreieren. Dafür steht ihm eine breite Palette von Objekten und Effekten zur Verfügung, mit denen er die reale Darstellung und die Grafikqualität erheblich steigern kann (siehe 4.2.2). Neben dem Designaspekt erlaubt Worldbuild die Programmierung der 3DUmgebung. Externe Programmmodule (sogenannte DDLs: Dynamic Link Libraries) erlauben dynamische Kamerafahrten, sich öffnende Türen, die Bewegung von Objekten etc. Diese werden in Kapitel 4.4 erläutert. Die von Worldbuild erzeugten 3D-Szenarien werden als Karten (oder Maps) bezeichnet. Diese werden in einem eigenem Dateiformat (WBM: Worldbuild Map) abgespeichert. Jede neuere Programmversion ist abwärtskompatibel, was bedeutet, dass auch Maps älterer Versionen geladen werden können. Bevor eine Karte in einem Spiel gerendert werden kann, müssen die enthaltenen Daten in ein Format gebracht werden, das schneller gelesen werden kann und zusätzliche Informationen enthält, die eine schnelle Darstellung ermöglichen. Dieser Vorgang wird ähnlich wie in der Programmierung als Kompilieren bezeichnet. Worldbuild enthält einen internen Compiler, der diese Aufgabe durchführt. Er erzeugt ein Dateiformat, das entweder die Endung CMP (Compiled Worldbuild Map) oder CMD (Compiled Worldbuild Model) trägt. Der Unterschied wird in Kapitel 4.2.2 näher erläutert. 4.2.2 Der Ablauf einer Kartenerstellung Dieses Kapitel erklärt den chronologischen Ablauf, über den mit Hilfe von Worldbuild Karten erzeugt werden. 4.2.2.1 Die Geometrie Wie bereits in Kapitel 2.2.3.2 erklärt wurde, bestehen virtuelle 3D-Szenarien aus vielen kleinen Polygonen, die sich in Dreiecke zerlegen lassen. Diese Vielecke machen die sogenannte Geometrie einer Karte aus, also alle Böden, Wände, Decken usw. – im Grunde alles, was die Umgebung vom ‚leeren Raum’ abgrenzt. Beim 3D-Design unterscheidet man zwei Prinzipien: Das additive und das subtraktive. Beim additiven Prinzip, das sich weltweit als Standard durchgesetzt hat, ist die 3D-Welt zunächst leer – vergleichbar mit dem Universum – und der Designer muss diese Welt füllen (addieren). Das subtraktive Prinzip sieht die Welt als gefüllten Raum an, aus dem man überflüssige Bestandteile herausschneidet und wegnimmt (subtrahiert). Letzteres Prinzip kommt nur in wenigen Spielen zum Einsatz (Beispiel: UnrealTournament von Epic Megagames). Worldbuild verwendet ein additives Geometrieprinzip. Um ein komplexes Gebilde zu erstellen, fügt der Designer zunächst einen einfachen geometrischen Körper ein. Diese einfachen Körper werden als Primitiven bezeichnet. Dazu gehören in Worldbuild: Würfel, Kegel, Kugel, Rechteck und Kreis. Das Programm bietet nun diverse Funktionen an, um diesen Körper zu bearbeiten: • Transformation – die grundlegenden Funktionen wie bewegen, drehen, skalieren (vergrößern oder verkleinern) oder spiegeln • Cutting – das Zerschneiden und Zerlegen eines Körpers. Dies ist wohl eine der wichtigsten Funktionen, denn sie ermöglicht es, eine Primitive in ein komplexes Gebilde zu überführen. ( vergleichbar mit einem Klumpen Ton, aus dem man ein Gesicht formt ) • Verwaltung – hierzu gehören die typischen Bearbeiten-Befehle von Windows: Kopieren, Einfügen, Löschen, Duplizieren. Beispiel: Um einen Raum mit einem Fenster zu erzeugen, muss eine Würfel-Primitive eingefügt und in die richtige Größe skaliert werden. Danach wird eine Wand mit zwei horizontalen und zwei vertikalen Schnitten unterteilt. Daraus resultiert ein Polygon, das nur noch gelöscht werden muss. Die Abbildung auf der nächsten Seite veranschaulicht dies. 3D-Polygone werden im Projekt als Surfaces bezeichnet, die in Gruppen zusammengefasst werden können. Wird beispielsweise ein Würfel eingefügt, so besteht er aus 6 Surfaces, die zu einer Gruppe zusammengefasst sind. 4.2.2.2 Texturierung Nachdem die Geometrie festgelegt wurde, werden die Surfaces der Karte texturiert, also mit Bildern belegt. Die Sorgfalt, mit der dieser Schritt durchgeführt wird, ist entscheidend für den Grad des Realismus’, den eine Karte erreicht. Für die Wahl einer Textur steht in Worldbuild ein komfortables Auswahlfenster zur Verfügung. Problematisch dagegen ist jedoch die Ausrichtung dieses Bildes auf den Surfaces. Die Koordinaten einer Textur auf einem Surface werden mit u und v benannt. In Worldbuild kann die Lage und Größe einer Textur über vier Parameter beschrieben werden: • Verschiebung auf dem Surface (in u/v-Richtung) • Skalierung der Textur (um den Faktor u/v) • Drehung • Spiegelung (an der u/v-Achse) Diese Ausrichtung ergibt sich immer relativ zu einer Kante eines Surfaces. Diese ist vorgegeben, kann aber auch manuell ausgewählt werden. Für eine Modifikation dieser vier Parameter stehen verschiedene Benutzerfunktionen zur Verfügung: • Das Bild kann auf dem Surface mit der Maus in der 3D-Darstellung (siehe 4.2.3) verschoben, skaliert und gedreht werden. • Über eine Tastenkombination kann die Textur an den Achsen (u/v) gespiegelt werden. • Ein Anpassungswerkzeug hilft, Texturen benachbarter Surfaces homogen (also ohne sichtbare Grenze) aneinanderzulegen. • Eine Interpolations-Funktion korrigiert kleine Texturierungsfehler. • Nichts zuletzt können diese Parameter per Hand eingegeben werden. Worldbuild verwendet ein eigenes Format für seine Texturen. Darauf wird später noch detailliert eingegangen (siehe Kapitel 4.5). 4.2.2.3 Beleuchtung Nachdem die Geometrie erzeugt und texturiert wurde, wird die Beleuchtung der Karte relevant. Mit ihr kann einer Map eine gezielte Atmosphäre verliehen werden. Worldbuild erlaubt das Einfügen von Lichtern (sogenannten Lights) in die Karte. Dabei werden drei Arten von Lichtquellen unterschieden: • Punkt-Licht (Point light) – Das Licht besitzt eine Position und Reichweite. Dieser Typ wird am häufigsten verwendet. • Spot-Licht (Spot light) – Das Licht besitzt Angaben für Position, Reichweite und Lichtkegel, der durch Winkelwerte festgelegt wird. • Richtungs-Licht (Directional light) – Das Licht besitzt nur einen Richtungsvektor, also weder Position noch Reichweite. Diese selten verwendete Art wird zum Beispiel benutzt, wenn Sonnenlicht zum Einsatz kommen soll. Die Sonne hat als extrem große Lichtquelle keine wirkliche Position und ihre Helligkeit nimmt relativ zur Entfernung nicht wahrnehmbar ab. Der Benutzer kann im Grunde unbegrenzt viele Lichter in eine Map einfügen. Oft stellt sich jedoch heraus, dass ein gezieltes, sparsames Einsetzen von Lichtquellen ein optisch besseres und schnelleres Rendering liefert. 4.2.2.4 Besondere Objekte Zusätzlich zur Geometrie und den Lichtern können noch spezielle Objekte in die Karte eingefügt werden, die ich im Folgenden kurz erläutern möchte. a) Lens flares („Linsen–Leuchten“) Hierbei handelt es sich um einen besonderen Effekt, der in der Realität auftritt, wenn man mit einer Kamera in eine helle Lichtquelle filmt oder fotografiert. Auf einer Achse werden verschiedene Leuchtmuster (sogenannte Flares) sichtbar. In Worldbuild wird dies dadurch realisiert, dass der Benutzer eine ‚Flare-Quelle’ bzw. ein Lens flare an einer bestimmten Position einfügt und die Leuchtmuster in einer Liste festlegt. Lens flares wirken wie Lichtquellen. Sie sind es in Wahrheit jedoch nicht, da sie ihre Umgebung nicht erleuchten. b) Sprites Sprites sind Bilder, die sich immer zum Benutzer hindrehen, egal aus welcher Perspektive er auf dieses Objekt blickt. Eingesetzt werden diese zum Beispiel, wenn Bäume dargestellt werden sollen: Statt die Geometrie eines kompletten Baumes zu entwickeln, verwendet man ein einziges Bild davon. Egal von wo aus ein Spieler nun auf das Sprite blickt, er sieht immer den kompletten Baum (als 2D-Bild). Natürlich geht beim Verzicht auf die Tiefendarstellung des Baumes Realitätsnähe verloren, wenn sich der Betrachter dem Sprite nähert. Daher finden sie oft nur bei weit entfernten oder einfachen Objekten (z.B. Rauch) Anwendung. c) Models Ein Model ist standardmäßig ein geometrisches Objekt, das öfter als einmal verwendet werden soll. Es wurde zuvor mit Worldbuild entwickelt und dann als Model kompiliert (siehe 4.2.2.6). Will man beispielsweise eine Straße mit Laternen darstellen, so entwickelt man eine einzelne Laterne einmal und kompiliert sie als Model. Dann kann sie beliebig oft an verschiedenen Stellen der Straße eingefügt werden, ohne die Übersichtlichkeit zu reduzieren. d) Positionsmarkierungen Positionsmarkierungen (sogenannte Position marker) oder kurz „Marker“ sind die einzigen geometrischen Bestandteile einer Karte, die nicht sichtbar sind. Es handelt sich dabei um ein Objekt, dessen einzige Aufgabe es ist, eine Position und Ausrichtung im 3D-Szenario zu speichern. Benutzt werden diese zum Beispiel, wenn Kamerafahrten simuliert werden sollen. Der Designer legt mit Hilfe der Marker die Punkte fest, an denen die Kamera entlang laufen soll. Daher können Positionsmarkierungen auch zu einem Pfad miteinander verbunden werden. 4.2.2.5 Das Interface Nach dem Design der Geometrie und dem Einfügen der Objekte ist die Karte im Grunde fertig. Das sogenannte Interface (zu Deutsch: Schnittstelle) gibt der Map den letzten Schliff und ermöglicht die Programmierung der Karte. Es wird durch ein eigenes Fenster realisiert, das die Erfüllung von drei Aufgaben gewährleistet: a) Programmierung von Ereignissen Worldbuild ermöglicht das Einfügen von Ereignissen. Ein Ereignis ist eine Veränderung in der 3D-Welt, sei es das Öffnen einer Tür, ein Kameraschwenk oder einfach die Änderung einer Lichtfarbe. Die Events sind in Worldbuild in einer Liste aufgeführt. Um nun eins hinzuzufügen, muss der Designer eine Klasse auswählen, die das einzutretende Ereignis ausführt. Für eine Kamerafahrt fügt er beispielsweise die Klasse ‚CCameraFly’ ein. Nun erwartet das Ereignis noch Parameter, also Werte, die seine Aufgabe definieren. Im Falle unseres Beispiels müsste der Benutzer nun die Positionsmarkierung (siehe 4.2.2.4d) angeben, der den Start des Kameraweges bestimmt. Ereignisse erfüllen eine Karte erst wirklich mit Leben, denn sie machen eine statische, unbewegliche Landschaft zu einer belebten. b) Einbau von Nebeleffekten Nebel beeinflussen die Atmosphäre einer Szene nachhaltig und sind in heutigen Computerspielen kaum noch wegzudenken. Daher ist ihre Implementierung in eine Worldbuild-Karte entsprechend einfach gestaltet: Ein Nebel kann mit einem Klick eingefügt werden. Jetzt müssen nur noch Farbe und Reichweite festgelegt werden und er kann in der 3D-Darstellung (siehe 4.2.3) direkt getestet werden. Die Einsatzmöglichkeiten sind unbegrenzt: In der Kanalisation einer futuristischen 3D-Stadt würde man wohl einen dunkelgrünen Nebel mit stark eingeschränkter Sichtweite einbauen, auf einem anderen Planeten – wie dem Mars – eher einen roten mit größerer Sichtweite. c) Festlegung von Hintergründen (Sky boxes) Da man sich in vielen Spielen nicht nur innerhalb von Gebäuden aufhält, müssen für Außenlandschaften Hintergründe zur Verfügung stehen (welcher Spieler erfreut sich heute noch an einer einfarbigen Fläche als Hintergrund?). In Worldbuild wird dies über sogenannte Sky boxes realisiert. Der Name resultiert aus der Tatsache, dass es sich bei diesen Hintergründen um dreidimensionale Räume handelt. Eine einfache Sky box kann dadurch realisiert werden, dass ein würfelartiger Raum erzeugt wird, dessen Decke mit einer Wolkenund die Wände mit einer Gebirgs-Textur belegt werden. Dadurch erscheint es dem Spieler so, als würde er sich unter Wolken befinden und in der Ferne Berge erblicken. Um einen 3D-Hintergrund in eine Karte einzubauen, reicht es aus, den bereits erwähnten Raum zu entwickeln und als Sky box im Interface zu definieren. 4.2.2.6 Kompilieren Nachdem die Karte nun vollständig entwickelt und ausgefeilt wurde, kann sie kompiliert werden, um sie in ein effektives Datenformat umzuwandeln. Hierbei werden überflüssige Informationen weggelassen, die nur zum Design der Map dienen, und weitere Informationen berechnet und hinzugefügt, die für eine schnellere Aufbereitung und Darstellung der Daten sorgen. Der Benutzer kann vor dem Vorgang entscheiden, ob er eine kompilierte Map erzeugen will, um sie zum Beispiel in einem Spiel – wie dem Racer (siehe 4.3) – zu testen, oder ob er sie als Model wiederverwenden will (siehe 4.2.2.4c). 4.2.3 Bedienung Die Bedienung von Worldbuild ist der komplexen Aufgabe dieser Anwendung entsprechend umfangreich. Daher habe ich in monatelanger Arbeit einen Hilfetext verfasst, der den kompletten Funktionsumfang und dessen Steuerung erklärt. Für detaillierte Informationen zum Umgang mit dem 3D-Editor verweise ich hier auf diesen Hilfetext, wozu nähere Informationen in Kapitel 6.2 aufgeführt sind. Für das bessere Verständis wird an dieser Stelle jedoch die Fensteroberfläche kurz erläutert, die wie folgt aussieht: a) Das Arbeitsfenster Das Arbeitsfenster macht selbstverständlich den Hauptteil der Fensteroberfläche aus. Es ist in vier Unterfenster unterteilt: drei 2D-Fenster und ein 3D-Fenster. Die 2D-Fenster stellen die Karte – wie der Name schon sagt – zweidimensional dar, und zwar jedes von einer anderen Seite. Die Darstellung ist nicht perspektivisch, da immer eine bestimmte Koordinate weggelassen wird (bei der Sicht von vorne fehlt beispielsweise die Z-Koordinate). Die 2D-Fenster sind primär für die geometrischen Vorgänge verantwortlich: Hier werden Objekte eingefügt, bewegt, gedreht, geschnitten etc. Das 3D-Fenster hingegen zeigt die Karte perspektivisch und auf Wunsch mit Texturierung, Beleuchtung, Nebel und allen weiteren Effekten an. Zu den Hauptaufgaben dieses Fensters gehört neben der Visualisierung die Texturierung (4.2.2.2). Hier kann der Designer Texturen mit der Maus ausrichten und sich das Ergebnis direkt ansehen. Mit der Maus hat man darüber hinaus die Möglichkeit, durch seine Karte zu „fliegen“. b) Die Menüleiste / Werkzeugleiste Am oberen Rand des Fensters befinden sich die Menü- und die Werkzeugleiste. Sie erlauben die Ausführung der meisten Editor-Funktionen: das Laden und Speichern von Karten, das Einfügen und Modifizieren von Objekten, sowie Texturieroperationen und Sonderfunktionen, die das Dokument auf Fehler überprüfen und diese beseitigen. Außerdem ist der Aufruf der Konfiguration möglich. Die Werkzeugleiste ermöglicht den Schnellzugriff auf oft benutzte Aktionen, unter anderem ‚Neu’, ‚Laden’ und ‚Speichern’. c) 3D-Toolbar Am unteren Fensterrand befindet sich eine Werkzeugleiste, die die Konfiguration der 3DEinstellung gestattet. Über einen Klick kann hier zum Beispiel die Beleuchtung an- und ausgeschaltet werden oder der Sichtmodus geändert werden. d) Layer bar Unter der 3D-Toolbar befindet sich eine Leiste, die die Verwaltung von Layers25 ermöglicht. Über Layer lässt sich eine Karte in verschiedene Arbeitsbereiche unterteilen, die visuell hinzu- und weggeschaltet werden können, um eine größere Übersichtlichkeit zu gewährleisten. e) Eigenschaftsfenster Am rechten Bildrand befindet sich das Eigenschaftsfenster. Es zeigt Informationen über die Objekte an, die der Benutzer zur Zeit ausgewählt hat. Klickt er beispielsweise auf ein Licht, so kann er in diesem Fenster dessen Typ und Farbe festlegen. Bei Surfaces können in diesem Fall die Texturen ausgewählt werden. 25 Layer [engl.] = Schicht f) Primitivenfenster Unter dem Eigenschaftsfenster ist das Primitivenfenster positioniert Hier werden die Parameter der Primitive festgelegt, die als nächstes eingefügt werden soll (siehe 4.2.2.1); bei einem Kegel ist dies zum Beispiel die Anzahl der erzeugten Flächen (Tesslation). g) Statuszeile Die Statuszeile zeigt Informationen über die Mausposition, die aktuelle Auswahl und die laufenden Vorgänge an. Zu der Bedienung von Worldbuild ist abschließend zu sagen, dass es immer in meinem Bestreben lag, sie so benutzerfreundlich und gleichsam funktional wie möglich zu gestalten. Das Erzeugen einer 3D-Welt ist ein sehr komplexer Vorgang. In Worldbuild reichen jedoch sehr wenige Maus- und Tastenkombinationen aus, um Aktionen durchzuführen. Die Fensteraufteilung ist darüber hinaus so konstruiert, dass die Modifizierung von Objekten sehr schnell vonstatten geht. 4.2.4 Technische Umsetzung Die technische Umsetzung von Worldbuild findet im Kleinen statt und lässt sich nicht wie bei Racer auf eine Makro-Darstellung (siehe 4.3.3.2) bringen. Unzählige kleine Räder greifen ineinander und ergeben das Gesamtprogramm. Dennoch werden im Folgenden die Grundelemente erklärt: 4.2.4.1 Grundrahmen Worldbuild ist eine Windows 98-Applikation, die auf die Microsoft Foundation Classes (MFC) zugreift. Grund dafür ist das komplexe Fenstersystem, dessen Entwicklung durch diese Klassen wesentlich vereinfacht wurde. Außerdem ist kein systemnaher Zugriff erforderlich, wie dies bei Racer der Fall ist (siehe 4.3.3.1). Der 3D-Editor verwendet die „Document/View“-Architektur (siehe 2.1.7) und ist eine MDIAnwendung, unterstützt also das Bearbeiten mehrerer Karten gleichzeitig in verschiedenen Fenstern. 4.2.4.2 Das System Das System von Worldbuild lässt sich an dem folgenden Schema skizzieren: Dokument #1 Karten-Daten 2D-Views (1-3) 3D-View (4) Dokument #n … CMainDoc CWorld CMap2dView CMap3dView CMainDoc ... Texturensystem CTextureSystem Model-Manager CModelMan Kontrollklassen-Manager CClassManager Schematische Darstellung des Worldbuild-Systems Links: Systemkomponenten – Rechts: zugehörige Klassenbezeichnungen a) Die Dokument-Klassen und CWorld Üblich für Programme, die „Document/View“-Architektur verwenden, befinden sich in den Dokument-Klassen (CMainDoc) alle Daten, die vom Benutzer editiert werden können. Gespeichert werden diese in Form einer anderen Klasse, die in CMainDoc enthalten ist: CWorld. Diese enthält die komplette Karte: Geometrie, alle weiteren Objekte, das Interface und alle Layer (siehe 4.2.3d). Außerdem besitzt sie viele Methoden (≅ Funktionen), um diese Daten zu editieren, zum Beispiel alle Methoden zur Bewegung von Objekten. Die CWorld-Klasse ist also der technische Kern des Programms, denn in ihr spielen sich alle datenverarbeitenden Prozesse ab. Außerdem ist sie für das Speichern und Laden einer Karte zuständig. Neben der Datenklasse CWorld hat die Dokument-Klasse Zugriff auf die vier Views, die die Daten der CWorld-Klasse anzeigen (siehe 4.2.3a). b) Texturensystem Die Klasse CTextureSystem ist für das Bereitsstellen von Texturen verantwortlich. Die Hauptaufgabe liegt darin, Texturen zu laden und zuzuteilen. Wenn mehrere 3D-Ansichten die gleichen Texturen verwenden, so werden diese immer nur für eines geladen, um Speicher zu sparen. Die entsprechenden Texturen werden dann in die 3D-Ansicht umgeladen, in der momentan gerendert wird. c) Model-Manager Die Aufgabe des Model-Managers, der durch die Klasse CModelMan realisiert wurde, ist es, nach Model-Dateien (CMD-Dateien) im Programmverzeichnis zu suchen und ihre Geometrie zu laden, sobald sie in einer Worldbuild-Karte Verwendung finden. Um Speicher zu sparen, werden nur die Surfaces der Models geladen. Dadurch ist ihre Texturierung, Beleuchtung etc. in der 3D-Ansicht zwar nicht sichtbar, dafür wird die Programmleistung jedoch nicht übermäßig stark eingeschränkt, wenn mehrere Models eingesetzt werden. d) Kontrollklassen-Manager Die CClassManager-Klasse sucht nach externen Modulen, die Ereignis-Klassen enthalten (4.4) und lädt sie. Diese Klassen werden – wie bereits erwähnt – im Interface (4.2.2.5a) verwendet. Im Racer gibt es ebensfalls einen Kontrollklassen-Manager, der eine ähnliche Aufgabe erfüllt (siehe 4.3.3.2e). 4.2.5 Zeitliche Entwicklung Worldbuild ist die Basis und der Kern des gesamten Projekts. Es ist das erste Programm, und die Arbeit an ihm wurde bis zum Frühjahr 2002 selten unterbrochen. Die erste Zeile wurde am 3. März 2000 geschrieben. Am 12. November des gleichen Jahres endete die Alpha-Phase. Maps konnten erstellt und gespeichert werden. Während der Beta-Phase wurde das Programm um viele Features erweitert. Außerdem hielt ich es immer auf dem aktuellen Stand der Technik. So wurde das gesamte Grafiksystem zweimal neugeschrieben, weil Microsoft drei DirectX-Versionen (Versionen 6, 7 und 8 / 8.1) während dieser Zeit veröffentlichte, die vollkommen unterschiedlich angesteuert werden mussten. Neben dem Grafiksystem wurden auch andere große Programmteile umgestellt, weil sie sich im Laufe der Zeit als ineffizient herausgestellt hatten. Durch die lange Entwicklungszeit von etwa zwei Jahren ist das Programm bis heute auf einen Umfang von fast 50.000 Programmzeilen angestiegen. Der reine Quelltext umfasst eine Größe von etwa 1,3 Megabyte. Parallel zur Programmentwicklung schrieb ich seit dem 27. April 2001 an einem Hilfetext, der die Funktionsweise und Bedienung genau beschreibt. 4.3 Racer 4.3.1 Funktion Racer ist das zweitgrößte Programm. In der Endversion handelt es sich hierbei um das Spiel, was das ursprüngliche Ziel des Projekts darstellt: Mit futuristischen Fliegern werden Rennen in verschiedenen Szenarien geflogen. Neben der Rennfunktion wird sich ein Spieler mit diversen Waffen ausrüsten können, um seine Mitstreiter aufzuhalten. Zur Zeit besitzt diese Windows-Anwendung die Aufgabe, kompilierte Worldbuild-Karten zu laden, zu rendern und alle Ereignisse auszuführen. Es kann also als Viewer bezeichnet werden. Die Programmierung einer schnellen und stabilen 3D-Engine war dabei die schwierigste Aufgabe. 4.3.2 Bedienung Im Gegensatz zu Worldbuild ist die Bedienung von Racer nicht so umfangreich. a) Das Hauptfenster Racer besteht aus einem einzigen Fenster, das für die Grafikdarstellung einer Karte bzw. eines Levels verantwortlich ist. Bei Bedarf kann zwischen Vollbild- und Fenstermodus umgeschaltet werden (z.B. durch die Tastenkombination Strg+Enter). Im Fenstermodus befindet sich am oberen Bildrand eine Menüzeile, die die Haupt-Programmfunktionen ansteuert. b) Erzeugen eines Spiels Eine ausgeführte Karte wird als Spiel bezeichnet, da der Racer als Computerspiel konzipiert wurde. In Zukunft wird – wie bereits erwähnt – die Steuerung eines Fliegers möglich sein. Um ein Spiel zu erzeugen, muss einfach der Menüpunkt ‚File | Create’ aufgerufen werden. Nun kann eine kompilierte Worldbuild-Karte ausgewählt werden, woraufhin das Level geladen und ausgeführt wird. c) Zugriff auf das System Bei Bedarf kann der Benutzer von Racer auf das System Zugriff nehmen. Dies geht zum einen über den Menüpunkt ‚View | Events’, über den Ereignisse aktiviert und deaktiviert werden können, zum anderen enthält das Programm eine sogenannte Konsole. Eine Konsole ist eine grafische Oberfläche, die Meldungen ausgibt und Befehle annimmt. Im Racer kann die Konsole über die Taste Escape ein- und ausgeblendet werden. Wichtige Informationen über die 3D-Engine und Fehlermeldungen werden hier angezeigt. Diese werden zur korrekten Fehleranalyse extern in einer Textdatei gespeichert. Zusätzlich kann der Benutzer Befehle eingeben, um Einfluss auf das Programmverhalten oder das Spiel zu nehmen. Beispiele dafür sind: Befehl Funktion UHVWDUWHQJLQH startet die 3D-Engine komplett neu, für den Fall, dass Grafikfehler aufgetreten sind. HYHQWH! startet das Ereignis e. OLVWVN\ER[HV listet die verfügbaren 3D-Hintergründe der gestarteten Karte auf. IRJI! aktiviert den Nebel f der gestarteten Karte. SDXVH pausiert das Spiel. TXLW beendet den Racer. Um alle verfügbaren Befehle aufzulisten, genügt die Eingabe des Befehls „?“ oder „help“. Die Konsole wurde deshalb eingeführt, weil der Zugriff auf das Menü bei Vollbild nicht möglich ist, Spiele jedoch selten im Fenstermodus gespielt werden. Verlässliche Tests der Spielkarten sind daher nur im Vollbildmodus möglich. 4.3.3 Technische Umsetzung 4.3.3.1 Grundrahmen Racer ist eine auf Windows 98 basierende Applikation, die auf die Microsoft Foundation Classes (MFC) verzichtet. Stattdessen wird direkt in die WinAPI (siehe 2.1.6) programmiert, damit ein systemnaher Zugriff möglich ist, der keine Umwege macht, was bei 3D-Spielen in Bezug auf die Leistung enorm wichtig ist. Da ohnehin nur ein Fenster sichtbar ist (sieht man von den wenigen Unterfenstern ab) wird die direkte Programmierung ohne das objektorientierte Fenstersystem unwesentlich erschwert. 4.3.3.2 Das System Dem Programm Racer liegt ein komplexes, organisatorisches System zugrunde, das auf einem sehr abstrakten Level entwickelt wurde und auf einem objektorientierten Klassensystem basiert. Das folgende Schema verdeutlicht dies: Engine CEngine Spielmanager Spiel #1 CGameManager CGame Karten-Daten Spiel #2 Karten-Daten Spiel … 5.2.4 Zeitliche Entwicklung Texturensystem Kontrollklassen-Manager . Eingabesystem CMapData CGame CMapData CGame CTextureSystem CControlClassMan CInputSys Schematische Darstellung des Racer-Systems Links: Systemkomponenten – Rechts: zugehörige Klassenbezeichnungen Die oberste Ebene des Systems ist die Klasse CEngine. Sie enthält alle Elemente, die das Programm zum Laufen bringen. Die Engine besitzt folgende zentrale Aufgaben: • Programmstart: Initialisieren und Starten aller Systeme • Laufzeit: Organisation aller Systeme (insbesondere der einzelne Spiele), Aufruf der grafischen Darstellung • Programmende: Herunterfahren aller Systeme und Freigabe des reservierten Speichers Des Weiteren ist diese Klasse für das Einrichten der Schnittstelle zur Grafikkarte verantwortlich. Über den DirectX-Treiber (siehe Kapitel 2.2.4) wird eine Verbindung zur eventuell vorhandenen 3D-Beschleuniger-Karte hergestellt. Die Klasse ist global definiert, was einen programmweiten Zugriff ermöglicht. Alle Aktionen laufen über CEngine. Je nach Situation steuert diese Klasse die in ihr enthaltenen Systeme an, die im Folgenden beschrieben werden. a) Der Spielmanager: CGameManager Racer unterstützt das gleichzeitige Ausführen mehrerer Spiele. Die Klasse CGameManager ist für die Verwaltung der Spiele zuständig. Sie lädt und entlädt sie und leitet Befehle von der Engine an sie weiter. Angezeigt werden kann nur ein einziges Spiel, das sogenannte Active game. CGameManager „merkt“ sich das laufende Spiel über einen Zeiger und schickt nur ihm Befehle wie „Grafik aufbauen!“. Bei einer wichtigen Systemänderung trägt der Spielmanager dafür Sorge, dass alle Spiele darauf reagieren. Wird beispielsweise vom Fenster- in den Vollmodus geschaltet, werden alle Spiele davon benachrichtigt, damit sie die Verbindung zum 3D-Gerät aktualisieren. b) Das Spiel: CGame Die Klasse CGame repräsentiert hingegen ein einzelnes Spiel, das im Spielmanager enthalten ist. Sie führt das Rendern durch und trägt damit eine sehr wichtige Aufgabe. Beim Erzeugen eines Spiels werden zunächst die Rohdaten einer Karte geladen. Dies erledigt die Klasse CMapData (siehe c). Dann werden diese Daten aufbereitet, damit sie auf möglichst schnelle Weise gerendert werden (siehe 5.2). CGame enthält diverse Methoden (≅ Funktionen) für das Rendern aller Objekte, die in einer Karte vorkommen können. Außerdem steuert die Klasse die zeitabhängige Ausführung der Ereignisse (4.2.2.5a) und die Texturenanimation (siehe 4.5). c) Die Kartendaten: CMapData Die CMapData-Klasse lädt die Rohdaten einer Karte. Darüber hinaus stellt sie über einen sogenannten Event port26 (CEventPort) die Verbindung zwischen den Ereignissen und den externen Kontrollklassen (siehe 4.4) her. Einige Zusatzfunktionen erlauben das Aufbereiten der Daten. CMapData steuert außerdem das Texturensystem (siehe d) an, um die der Karte zugehörigen Texturen zu laden bzw. zu entladen. d) Das Texturensystem: CTextureSystem Das Texturensystem lädt die Texturen aus Texture containers (siehe 4.5) und verbindet sie mit dem aktuellen 3D-Gerät. Beim Entladen wird darauf geachtet, dass mehrere Spiele auf die selben Texturen zugreifen können: so wird eine Textur erst dann freigegeben, wenn sie definitiv nicht mehr benutzt wird. e) Der Kontrollklassen-Manager: CControlClassMan Die einzige Aufgabe des Kontrollklassen-Managers ist es, im Programmverzeichnis nach verfügbaren DLLs zu suchen, die Ereignis-Klassen enthalten. Der Manager erzeugt eine Liste aller vorhandenen Klassen. Diese Liste wird beim Laden einer Karte verwendet (siehe c). f) Eingabesystem: CInputSys Die CInputSys-Klasse stellt das Steuerungssystem dar. Es nimmt Eingaben des Spielers über Tastatur und Maus entgegen und schickt sie an das aktive Spiel weiter. CInputSys ermöglicht das selbstständige Bewegen in der 3D-Welt. 26 event port [engl.] = Ereignis-Schnittstelle 4.3.4 Zeitliche Entwicklung Racer ist das zuletzt begonnene Programm des Projekts. Dies hat darin seinen Grund, dass es keine Daten produziert, sondern im Grunde nur anzeigt. Es stellt die Ergebnisse da, die mit den Editoren des Projekts (Worldbuild und Texture container) erzeugt wurden. Der Racer ist das eigentliche Ziel, das ich während der gesamten Projektarbeit anstrebte. Die Arbeit an dem Programm begann am 22. Juni 2001. Erst am 13. Dezember 2001 endete die Alpha-Phase mit der Fertigstellung von Version 1.5 Alpha. In den fast sieben Monaten wurde intensiv an der Engine gearbeitet, um ein schnelles und stabiles Rendering zu gewährleisten. Bis heute befindet sich Racer in der Beta-Phase. 4.4 Externe Kontrollklassen 4.4.1 Funktion Die externen Kontrollklassen bezeichnen Bibliotheken, die Klassen enthalten, die für die Steuerung von Prozessen innerhalb einer Karte verantwortlich sind. Sie werden auch Module genannt. Alle Ereignisse (siehe 4.2.2.5a) sind in solchen Modulen untergebracht, auf die sowohl Worldbuild als auch Racer zugreifen. Die Ausdruck „extern“ bedeutet, dass die Bibliotheken nicht ‚hard coded’ sind, also in die Hauptprogramme eingearbeitet wurden, sondern unabhängig bzw. „außerhalb“ von diesen fungieren – sie sind dynamisch in DLL-Dateien ausgelagert. Der Vorteil dieses Prinzips liegt darin, dass die Karten-Steuerung nicht von den Programmen abhängt, die darauf zugreifen, sondern vollkommen frei entwickelt werden kann. Ein Beispiel dafür ist die Datei StandardClasses.dll, die die standardmäßigen EreignisKlassen für Karten enthält (z.B. CMoveObject, CCameraFly etc.). 4.4.2 Kommunikation zwischen Programm und Bibliothek Sowohl Worldbuild als auch Racer durchsuchen ihre Ordner nach DLL-Dateien, die mögliche Kontrollklassen enthalten können. Doch wie erkennen diese Programme, dass es sich um eine Bibliothek mit solchen Klassen handelt? Das Prinzip ist recht einfach. Beim Überprüfen einer DLL-Datei wird in ihr nach drei Funktionen gesucht: • GetModInfo() liefert Informationen über das Modul, das die Klassen enthält. • GetClassCount() gibt die Anzahl der vorhandenen Klassen in diesem Modul zurück. • GetClasses() liefert einen Zeiger auf die Liste der Klassen zurück. Fehlt eine dieser Funktionen, geht das Programm davon aus, dass es sich nicht um eine Kontrollklasse handelt und übergeht die Datei. Ansonsten liest es über GetModInfo zunächst die Informationen (Name, Version, Autor ...) über das Modul aus, dann wird die Liste aller Klassen über GetClasses und GetClassCount abgerufen. Über diese Liste können dann die Klassen-Objekte erzeugt werden. Im Racer erhalten diese Objekte dann Zeiger auf Ihren „Erzeuger“, so dass eine 2-Wege-Kommunikation möglich ist. Dadurch ist es Ihnen möglich, Informationen über die Karte zu erhalten, auf die sie angewendet werden. 4.5 Texture Container 4.5.1 Funktion Der Texture Container ist ein Editor, der Texturen in einem sogenannten Container (zu Deutsch: „Kontainer / Behälter“) gruppiert. Dieser Container wird in einem eigenen Dateiformat gespeichert (TEX-Datei). Das Vereinigen von Texturen zu einer Datei hat nämlich entscheidende Vorteile: • Beim Laden von Texturen müssen nur wenige Dateien (Container) geöffnet werden. Das bringt einen deutlichen Geschwindigkeitsgewinn, da das häufige Öffnen und Schließen von Dateien zeitintensiv ist. • Durch eine sinnvolle Gruppierung wird eine größere Übersichtlichkeit gewährleistet. • Die unerlaubte Manipulation von Texturen ist durch das spezifische Dateiformat von außen nicht ohne weiteres möglich. Darüber hinaus unterstützt der Texture Container weitere Features: • Zu jeder Textur kann ein Alpha-Kanal für Transparenz hinzugefügt werden (siehe 4.5.3). • Texturen-Animationen können festgelegt werden. Sowohl Worldbuild als auch Racer arbeiten mit diesen Texturen-Kontainern, da sich das Prinzip als sehr effektiv herausgestellt hat. Alle Texturen gehen ursprünglich aus BitmapDateien hervor. 4.5.2 Bedienung Um einen neuen leeren Container zu erzeugen, muss einfach der Menüpunkt File | New gewählt werden. Jetzt können Texturen hinzugefügt werden. Dies wird durch zwei Funktionen ermöglicht, die entweder das direkte Auswählen von Bitmaps oder die Angabe eines Ordners verlangen. Wird ein Ordner angegeben, werden alle sich darin befindlichen Bitmaps geladen. Beim Hinzufügen werden die Bitmaps in das programmeigene Format umgewandelt und können nun in einer Liste bearbeitet werden. Hier stehen folgende Funktionen zur Verfügung: • Umbenennen und Löschen einer Textur • Festsetzen einer Transparenzfarbe – Alle Bildpixel, die diesen Farbwert besitzen, sind dann in der 3D-Darstellung von Worldbuild oder Racer durchsichtig (sofern ein Surface als transparent deklariert wird). • Hinzufügen eines Alpha-Kanals – Hierbei wird hinter das Farbbild ein weiteres Schwarz-Weiß-Bild gelegt. Bei jedem Pixel erhält man nun neben den drei Farbkanälen (rot, grün, blau) zusätzlich einen vierten, der sich aus der Pixelhelligkeit des Schwarz-Weiß-Bildes ergibt: den Alpha-Kanal. Je niedriger dieser Wert ist, desto durchsichtiger ist die Textur bei diesem Pixel. Ein Wert von 255 stellt einen vollkommen undurchsichtigen Pixel dar. Der Alpha-Kanal kann auf Knopfdruck (in schwarz-weiß) angezeigt werden. • Einrichten von Texturen-Animation – Dafür müssen alle Texturen, die in die Animation miteinbezogen werden sollen, hintereinander angeordnet werden. Beim obersten Bild werden dann alle Daten für die Animation (Anzahl der einbezogenen Bilder, Geschwindigkeit, Art usw.) eingerichtet. Wird diese (oberste) Textur in einer Karte eingesetzt, führt Racer den Bildwechsel automatisch durch. Die Animation kann im Programm durch einen Button-Klick simuliert werden. • Definieren von Schriftarten – Es wird festgelegt, welche Buchstaben in der Textur dargestellt und wie sie voneinander zu trennen sind (durch eine Trennlinie oder einen fixierten Abstandswert). Der Fensteraufbau des Programms ist dreigeteilt: am linken Rand befindet sich die Liste der Texturen, oben rechts lassen sich die Eigenschaften jeder Textur einstellen, im Hauptteil des Fensters (unten rechts) wird dann eine Vorschau der ausgewählten Texturen angezeigt. Die meisten Texturen, die beim 3D-Design zum Einsatz kommen, müssen kachelbar sein. Kachelbare Texturen kann man nebeneinander legen, ohne dass man sieht, wo die Textur anfängt bzw. aufhört. Ein einfaches Beispiel dafür ist die Bitmap Kacheln.bmp, die standardmäßig in Windows 95/98 enthalten ist. Dafür ist im Texture Container ein Button vorgesehen, der die Textur gekachelt anzeigt. Container können gespeichert und geladen werden. Darüber hinaus gibt es die Möglichkeit, Bilder aus einem Container wieder zu exportieren. 4.5.3 Technische Umsetzung 4.5.3.1 Grundrahmen Der Texture Container ist eine Windows 98-Applikation, die auf die Microsoft Foundation Classes (MFC) zugreift. Der Editor ist darüber hinaus eine MDI-Anwendung und benutzt die „Document/View“-Architektur (siehe 2.1.7). Damit ist es möglich, mehrere Container gleichzeitig zu verwalten. 4.5.3.2 Bildformate Im Texture Container können Bilder in zwei Formaten geladen werden: • 24-Bit: Jedes Pixel besitzt jeweils 8 Bit für jeden Farbkanal (rot, grün, blau), aus dem sich die Farbe zusammensetzt. Die Farbe Gelb setzt sich zum Beispiel aus der Kombination [rot = 255, grün = 255, blau = 0] zusammen. Der gespeicherte Datenwert sieht dann folgenderweise aus: 0xFFFF00h (hexadezimal). 24-Bit-Texturen werden als Echtfarben-Bilder bezeichnet. • 8-Bit: Jedes 8-Bit-Bild besitzt eine Farbpalette mit 256 Einträgen. Die Einträge dieser Palette besitzen genaue Farbzusammensetzungen wie bei einem 24-Bit-Pixel (rot, grün und blau). Jeder Bildpixel enthält einen Index auf einen Eintrag der Palette. 8-Bit-Texturen werden als Paletten-Bilder bezeichnet. Beispiel: In der Palette ist an der Stelle 192 die Grundfarbe Türkis (0x00FFFFh). Soll das Bild nun komplett türkis eingefärbt werden, so muss jeder 8-Bit-Pixel den Wert 192 (0xC0h) 27 besitzen. Jedem Bild kann – wie bereits erwähnt – ein Alpha-Kanal hinzugefügt werden, der den Grad der Durchsichtigkeit bestimmt. Ein Alpha-Wert ist ein 8-Bit-Datentyp, wobei der Wert 0 keine und 255 vollständige Transparenz bedeutet. Die Größe eines Bildpixels wird somit um weitere 8-Bit erweitert. So belegt ein EchtfarbenBild nun 32 Bit und ein Paletten-Bild 16. 27 0xC0h = hexadezimale Schreibweise Die folgende Abbildung fasst diese Zusammenhänge zusammen: Echtfarben-Bild Paletten-Bild Ohne Alpha 0xRRGGBBh 0xNNh Mit Alpha 0xRRGGBBAAh 0xNNAAh Aufbau eines Bildpixels bei den verschiedenen Formaten (RR = Rot, GG = Grün, BB = Blau, NN = Palettenindex, AA = Alpha) 4.5.3.3 Datenspeicherung Die Speicherung eines Containers mit seiner Textur erfolgt über die interne Klasse CTextureContainer. Hierbei handelt es sich um den Kern des Programms, denn hier werden alle Daten sowohl gespeichert als auch verwaltet. Alle Texturen eines Containers werden in dieser Klasse in Form einer verketteten Liste gespeichert. Jede Textur bzw. jeder Knoten dieser Liste enthält folgende Daten: • Name, Größe (Breite und Höhe) der Textur • Typ (0 = Standard, 1 = Flare, 2 = Schriftart) • Bilddaten • Palettenbitmap? (Ja/Nein) • Farbpalette • Alpha-Kanal enthalten? (Ja/Nein) • Transparente Farbe definiert? Welche? • Animation? (Ja/Nein), Typ, Anzahl der involvierten Bilder, Geschwindigkeit, Runden • Schriftartdefinition (wenn Typ = 2) Die CTextureContainer-Klasse ist neben der Speicherung dieser Daten für das Speichern und Laden der TEX-Dateien verantwortlich. 4.5.4 Zeitliche Entwicklung Der Texture Container wurde erst relativ spät geschrieben. So begann die Entwicklung am 13. November 2000. Bis dahin konnten in Worldbuild nur Bitmaps geladen werden. Die Alpha-Phase war zeitlich verhältnismäßig kurz, dauerte nämlich nur bis zum 25. November desselben Jahres. Im April 2001 wurde schließlich die Möglichkeit hinzugefügt, Texturen um Transparenzeigenschaften (u.a. den Alpha-Kanal) zu erweitern. Seit dem Oktober 2001 können Schriftarten definiert werden. 4.6 File Container 4.6.1 Funktion Das Programm File Container ist eine vereinfachte Version des Texture Containers. Seine Aufgabe ist es, mehrere Dateien zu einem Container zusammenzufassen. Der Grund für den Einsatz liegt in Vorteilen, die auch Texturen-Container besitzen: bessere Übersicht, schnellerer Zugriff und erschwertere Manipulation. Der Einsatzbereich im Projekt ist im Gegensatz zu den anderen Editoren stark beschränkt. So kommt File Container nur beim Phalanx Updater (4.7) zum Einsatz. 4.6.2 Bedienung Die Bedienung entspricht der des Texture Containers, wobei die Funktionalität wesentlich geringer ist. Nur zwei Aktionen können durchgeführt werden: • Hinzufügen und Löschen von einer oder mehrerer Dateien in den Container • Exportieren von Dateien aus dem Container Auch der Bildaufbau ist gegenüber dem Texture Container vereinfacht. Das Arbeitsfenster besteht ausschließlich aus der Liste der im Container enthaltenen Dateien, die am linken Fensterrand zu finden ist. Natürlich ermöglicht auch dieses Programm das Speichern und Laden von Containern. Dabei werden Dateinamen mit der Endung CON verwendet. 4.6.3 Technische Umsetzung Für die Entwicklung des File Containers wurden viele Teile des Texture Containers übernommen und „abgespeckt“, da das Prinzip der beiden Programme nahezu identisch ist. 4.6.3.1 Grundrahmen Der Grundrahmen entspricht dem des Texture Containers: Der File Container ist ein Windows 98-Programm, das auf die Microsoft Foundation Classes (MFC) zugreift. Es ist eine MDI-Anwendung und arbeitet mit der „Document/View“-Architektur (siehe 2.1.7). Mehrere Container können daher gleichzeitig verwaltet werden. 4.6.3.2 Datenspeicherung Auch in der Datenspeicherung ist der Editor dem Texture Container sehr ähnlich. Der datentechnische Kern des Programms ist die Klasse CFileContainer. In ihr sind alle Dateien in Form einer verketteten Liste gespeichert. Jeder Eintrag besitzt folgende Eigenschaften: • Dateiname • Größe der Datei • Inhalt der Datei (≅ Puffer) Neben dem Hinzufügen und Löschen von Einträgen ist die Klasse auch für das Speichern und Laden der CON-Dateien verantwortlich. 4.6.4 Zeitliche Entwicklung Aufgrund des kleinen Einsatzgebietes und der geringen Komplexität dieses Programms, dauerte die Entwicklung der Software verhältnismäßig kurz. Hinzu kam, dass – wie bereits erwähnt – große Teile vom Texture Container übernommen werden konnten. Die Alpha-Phase dauerte nur zwei Tage (15. und 16. April 2001). In der Beta-Phase wurden einige kleine Verbesserungen durchgeführt, bei denen es hauptsächlich um Ästhetik und Komfort ging. Nach der Fertigstellung des Programms war noch kein konkreter Verwendungszweck ersichtlich. Erst am 28. April, nämlich zu Beginn der Arbeit am Phalanx Updater (siehe 4.7), war dieser gefunden. 4.7 Phalanx Updater 4.7.1 Funktion Wenn eine neue Version eines Programms des Projekts fertig ist, wird ein sogenanntes Release erzeugt: eine freigegebene Programmversion, die keine Debug-Informationen28 enthält und daher schneller läuft. Über den Phalanx Updater können die neuesten Versionen der Projektkomponenten via Internet heruntergeladen werden. Wie bereits in Kapitel 1.2 erläutert wurde, sollte das Projekt ursprünglich von einem Designer-Team unterstützt werden. So war der Phalanx Updater zunächst dafür konzipiert, dass sich die Mitglieder die neuesten Programmversionen schnell und komfortabel „ziehen“ konnten. Nach der Auflösung des Teams dient die Applikation nun ausschließlich den BetaTestern, die die Programme auf Fehler überprüfen. Der Phalanx Updater arbeitet völlig unabhängig von den anderen Hauptprogrammen, bezieht jedoch Ressourcen, die von File Container und WbCreateUpdate (siehe 4.9) erzeugt werden. Exkurs: Zur Benennung des Programms Der Name „Phalanx Updater“ resultiert aus der Idee, das ursprüngliche Team die „Phalanx Gruppe“ zu nennen. Das aus dem Griechischen übernommene Wort „Phalanx“ steht für die vorderste Kampfreihe im Heer und sollte Stärke und Zusammenhalt repräsentieren. 4.7.2 Bedienung Die Bedienung des Phalanx Updaters ist sehr einfach: Beim Programmstart erscheint die Liste der installierten Komponenten und ihrer Versionsnummern. Nun klickt der Benutzer auf den ‚Connect!’-Button, der eine Verbindung zu einem FTP-Server (File Transfer Protocol Server) aufbaut, auf dem die Komponenten abgelegt sind. Dafür muss der User bereits mit dem Internet verbunden sein. Jetzt wird die Liste der verfügbaren Komponenten heruntergeladen und mit den aktuellen Versionsnummern der installierten Software verglichen. Sollten neue Programme oder Versionen auf dem Server verfügbar sein, so werden sie in der Liste entsprechend hinzugefügt bzw. markiert. Nun kann der Anwender per Knopfdruck entscheiden, ob er einzelne oder gleich alle Komponenten updaten will. Beim Klick auf einen der ‚Update’-Buttons werden die neuen Versionen heruntergeladen und in das System 28 Informationen, um Programmfehler während der Ausführung aufzuspüren (nur für die Entwicklung gedacht) installiert. Möglicherweise wird ein Passwort verlangt oder eine detaillierte UpdateBeschreibung angezeigt. Bei jeder Komponente kann außerdem der Ordner festgelegt werden, in dem das entsprechende Update installiert werden soll. Nach dieser Update-Prüfung kann das Programm beendet werden, wobei die eventuell neue Komponentenliste in der Systemregistrierung gespeichert wird. Der Update-Vorgang lässt sich also in drei Schritten zusammenfassen: • Auf ‚Connect!’ klicken (Internetverbindung muss bestehen) • Durch Klick auf ‚Update All!’ alle verfügbaren Updates installieren (wenn vorhanden) • Programm beenden 4.7.3 Technische Umsetzung 4.7.3.1 Grundrahmen Der Phalanx Updater ist eine dialogbasierende Windows 98-Anwendung, die auf die Microsoft Foundation Classes (MFC) zugreift. Dialogbasierend bedeutet, dass die Programmoberfläche aus einem einzigen Fenster besteht, dessen Größe standardmäßig nicht veränderbar ist. Es enthält Elemente wie Editfelder, Listfelder etc., die zur Datenverwaltung dienen. Anwendungen sind oft als dialogbasierend konzipiert, wenn der für Ein- und Ausgaben benötigte Platz auf dem Bildschirm fest ist. Beispiele sind die Windows-Programme wie Scandisk und Rechner. 4.7.3.2 Linearer Prozessablauf Der Phalanx Updater ist einer der wenigen Windows-Anwendungen, die linear ablaufen. Die Abfolge der internen Prozesse ist umfangreicher als die, die der Benutzer sieht: • Laden der Installationsliste • Versionserkennung • Aufbau der Internet- und FTP-Verbindung • Download und Verarbeitung der Updateliste • Download der Updatedateien • Installation der Updates Im Folgenden werden diese Prozesse genauer erläutert: a) Laden der Installationsliste Die Liste der installierten Komponenten ist in der Systemregistrierung (siehe dazu Kapitel 2.1.8) gespeichert, und zwar unter dem Schlüssel: HKEY_CURRENT_USER\Software\Phalanx Updater Die Datenstruktur in diesem Schlüssel ist folgendermaßen aufgebaut: Phalanx Updater NumComponents Component0 Component1 Worldbuild Detection Folder Icon InfoSource Release Version Racer ... [ 2 ] [ Worldbuild ] [ Racer ] [ [ [ [ [ [ INFILE=Worldbuild.exe:”WORLDBUILD VERSION[“ ] C:\Worldbuild ] %FOLDER%Worldbuild.ico ] ] 200112130000 ] 1.7 Beta ] Beispiel für den Aufbau des Registrierschlüssels des Phalanx Updaters Der erste Wert im Schlüssel (NumComponents) bezeichnet die Anzahl der installierten Komponenten (= n). Die Werte Component0 bis Component(n-1) bezeichnen deren Namen. Unter diesen Namen sind Unterschlüssel („Worldbuild“, „Racer“ ...) gespeichert, die genauere Informationen zu den Komponenten liefern: • Detection bezeichnet die Art der Versionserkennng (siehe b). • Folder bestimmt den Ordner, in dem die Komponente installiert ist. • Icon legt die Datei fest, die das Symbol der Komponente enthält. „%FOLDER%“ repräsentiert den Installationsordner. • InfoSource ist eine Internetadresse oder der Verweis auf eine Datei der Festplatte, die weitere Informationen über die Komponente enthält. • Release ist der Releasestring. Er ist für die Versionsbestimmung verantwortlich und gibt den Zeitpunkt der Release-Erzeugung wieder (Format: JJJJMMTTSSMM). • Version gibt die Versionsbezeichnung wieder. Der Wert ist für die internen Vorgänge nicht von Bedeutung und nur für das bessere Verständnis des Benutzers gedacht. Relevant ist einzig der Releasestring. Diese Daten werden direkt beim Programmstart in Form einer verketteten Liste geladen. b) Versionserkennung Im nächsten Schritt werden die Versionen der installierten Komponenten ermittelt. Dies kann auf zwei Arten geschehen. Welche dieser Art gewählt wird, bestimmt der Detection-Wert der Komponente. 1. Über den Versionsstring in einer Datei (INFILE) Die Methode, die sich durchgesetzt hat und ausschließlich im Projekt verwendet wird, ist die Versionserkennung über einen Versionsstring innerhalb einer Datei. Bei den Komponenten Worldbuild steht beispielsweise unter Detection: INFILE=Worldbuild.exe:”WORLDBUILD VERSION[“ 1 2 3 Dies bedeutet übersetzt: Suche in der Datei (1) Worldbuild.exe (2) nach dem Versionsstring, der unmittelbar hinter „Worldbuild Version[“ (3) steht. Innerhalb der ausführbaren Datei von Worldbuild ist dieser String im Datenbereich vorhanden: ...SJHjXAaJZ()%$/§%§iWORLDBUILD VERSION[1.7 Beta/200112130000]JAS)($”§... Erkennungsstring Versionsbezeichnung Release-String Möglicher Auszug aus Worldbuild.exe Versionsbezeichnung und Release-String werden ausgelesen und in der Liste gespeichert. Aus dieser Technik geht hervor, dass die Update-Fähigkeit schon bei der Entwicklung der Komponenten berücksichtig werden muss. Vorteil dieser Methode: Jede Komponentenversion ist eindeutig ermittelbar. 2. Über die Registry (INREG) Das andere Verfahren ist die Speicherung von Versionsbezeichnung und Release-String in der Registry. Nach einem Update werden die neuen Versionsstrings in der Registry gespeichert und beim erneuten Starten des Phalanx Updaters geladen. Nachteil dieser Methode: Nach einer Neuinstallation des Systems (≅ Neuaufbau der Registry) müssen erst alle Updates durchgeführt werden, damit überhaupt mit dem Updater gearbeitet werden kann. Denn dann werden alle Komponenten zunächst als veraltet deklariert, da entsprechende Versionsstrings fehlen. Bei Verwendung dieser Methode ist der Detectionstring auf „INREG“ gesetzt. c) Aufbau der Internet- und FTP-Verbindung Alle Update-Dateien befinden sich auf einem FTP-Server im Internet. Um darauf zuzugreifen, wird über die MFC-Klassen CInternetSession und CFtpConnection eine Verbindung zum Server jove.prohosting.com aufgebaut. Mit Hilfe der zweitgenannten Klasse können jetzt Dateien heruntergeladen werden. d) Download und Verarbeitung der Update-Liste Die erste Datei, die vom Server heruntergeladen wird, ist ProjectUpdate.dat. Sie enthält die vollständige Liste der sich auf dem Server befindlichen Komponenten. Es ist eine Textdatei bei der die Zeilen für die Komponenten auf dem Server stehen. Jede Zeile ist folgendermaßen aufgebaut: Komponenten-Namen | Release-String | Versionsbezeichnung | Dateiname der Update-Datei | Standardordner | Versionserkennung | Informations-Quelle | Iconposition Die Einzelwerte finden sich in dieser oder ähnlicher Form auch in der Systemregistrierung wieder (siehe a). Nun werden alle Zeilen verarbeitet. Ist eine neue, also noch nicht installierte Komponente vorhanden wird sie der Installationsliste hinzugefügt. Fehlt eine Komponente in der UpdateDatei, die auf dem Computer installiert ist, wird sie aus der Installationsliste entfernt. Jetzt werden die Releasestrings der Installationsliste mit denen der Update-Liste verglichen. Besitzt eine installierte Komponente einen früheren Release-Zeitpunkt als der entsprechende in der Update-Liste, so muss sie upgedated werden. Sie wird entsprechend in der Liste markiert. e) Download der Update-Dateien Im vorletzten Schritt werden die Update-Dateien für die Komponenten heruntergeladen, die aktualisiert werden sollen. Den Dateinamen auf dem Server erhält der Phalanx Updater über die Update-Liste (siehe d), die zuvor heruntergeladen wurde. Für den Download wird ebenfalls die MFC-Klasse CFtpConnection verwendet. f) Installation der Updates Im letzten Schritt werden die heruntergeladenen Updatedateien installiert. Dabei werden zwei Dateitypen unterschieden: • CON-Dateien – Dies sind Dateien, die mit dem File Container (4.6) erzeugt wurden. Die Installation besteht darin, dass die im Container enthaltenen Dateien in den Programmordner entpackt werden. • WBU-/UPD-Dateien – Hierbei handelt es sich um Dateien, die mit WbCreateUpdate (4.9.1) hergestellt worden. Auch sie stellen eine Art Container dar, wobei die enthaltenen Dateien größtenteils mit Vigenère-Verschlüsselung29 chiffriert sind. Außerdem ist eine Datei namens Instruct.dat enthalten, die genaue Instruktionen für die Installation enthält. Bei der Installation werden alle Dateien zunächst einmal in einen temporären Ordner entpackt. Dann wird die Datei Instruct.dat ausgewertet. Sie enthält diverse Befehle für das Entschlüsseln und Kopieren der Dateien, das Einrichten der Systemregistrierung sowie zur Ausgabe von Informationen und Hinweisen. Außerdem kann sie die Eingabe eines Passwortes verlangen. 29 aus Geheime Botschaften, S. 66ff. Updates für Racer und Worldbuild gehen grundsätzlich über diesen Dateityp vonstatten, da die Daten zur Sicherheit verschlüsselt werden müssen und die ebenfalls verschlüsselte Passwortabfrage weitestgehend verhindert, dass die Software in falsche Hände gerät. 4.7.4 Zeitliche Einordnung Der Phalanx Updater ist ein Nachfolger des Programms WbUpdate („Worldbuild Update“), das ausschließlich für die Aktualisierung von Worldbuild verantwortlich war und im Juni 2000 (4. - 7.) entwickelt wurde. Im Gegensatz zum Vorgänger dauerte die Entwicklungszeit beim Phalanx Updater etwas länger, da ein Installationsmodell geschaffen werden musste, das für mehrere Komponenten konzipiert ist. Am 28. April 2001 schrieb ich die erste Zeile. Mit dem ersten Release endete die Alpha-Phase schließlich am 5. Mai. Während der Beta-Phase, die bis zum 9. Juni desselben Jahres andauerte, wurden immer wieder kleine Verbesserungen durchgeführt. Am 20. Juli 2001 wurde die erste Final-Version des Programms an die Beta-Tester ausgegeben. 4.8 Ship Editor 4.8.1 Funktion Der Spieler kann in Racer eines von mehreren Raumschiffen auswählen, die jeweils verschiedene Flugeigenschaften besitzen. Die Verwaltung der Schiffsliste und die Einstellung der verschiedenen Schiffsparameter sind die Aufgaben des Programms Ship Editor. 4.8.2 Bedienung Die Bedienung vom Ship Editor ist dem eingeschränkten Funktionsumfang entsprechend einfach gehalten. Alle Schiffe und ihre Parameter werden tabellarisch in einem Listenfeld aufgeführt. Durch einen Doppelklick können die verschiedenen Schiffswerte verändert werden. Desweiteren gibt es die gängigen Verwaltungsfunktionen wie Hinzufügen und Löschen von Einträgen. Schiffslisten können natürlich auch gespeichert und geladen werden. Dies geschieht in einem einfachen Dateiformat, das die Endung SHP trägt. Der Racer kann dieses Dateiformat auswerten. 4.8.3 Technische Umsetzung 4.8.3.1 Grundrahmen Der Ship Editor ist ein Windows 98-Programm, das auf die Microsoft Foundation Classes (MFC) zugreift. Es ist eine MDI-Anwendung und arbeitet mit der „Document/View“Architektur (siehe 2.1.7). Damit können mehrere Schiffslisten gleichzeitig verwaltet werden. 4.8.3.2 Datenspeicherung Alle Schiffe befinden sich in einer Datenbank, die durch die Klasse CShipList repräsentiert wird. Die Einträge der Schiffsliste sind in Form einer verketteten Liste gespeichert. Jedes Listenelement beinhaltet die Parameter, die ein Schiff besitzen kann, von denen einige hier aufgelistet sind: • Ship name bestimmt den Namen des Schiffs. • Model name bezeichnet den Namen der 3D-Model-Datei, mit der das Schiff dargestellt wird. • Description stellt eine optionale Beschreibung des Schiffs dar. • Maximum thrust umfasst Werte für die maximale Schubkraft in verschiedene Richtungen. Diese Klasse ist auch für das Laden und Speichern der Schiffslisten verantwortlich. 4.8.4 Zeitliche Entwicklung Der Ship Editor ist momentan das jüngste Programm des Projekts. So wurde es erst im Frühjahr 2002 entwickelt. Schon nach dem ersten Entwicklungstag, dem 25. März 2002, befand sich der Editor in der Beta-Phase, da der Basisumfang der Funktionen bereits implementiert war. Die wenigen Änderungen der nächsten Wochen und Monate waren größtenteils marginal oder bezogen sich nur auf die Schiffsparameter. 4.9 Weitere Werkzeuge Neben den bisher erwähnten Hauptprogrammen wurden kleinere Werkzeuge entwickelt, die Daten bereitstellen. Dabei handelt es sich um Programme, die auf DOS-Ebene laufen und – sieht man von den Übergabeparametern ab – keine Benutzereingaben erwarten. Dennoch sollte ihre Aufgabe nicht unterschätzt werden, wie im Folgenden erklärt wird. 4.9.1 WbCreateUpdate Das Programm WbCreateUpdate (Worldbuild Update Creator) erzeugt die WBU- bzw. UPDContainer, die der Phalanx Updater verarbeitet, wobei die beiden Präfixe WBU und UPD keinen Unterschied bedeuten. Das „Worldbuild“ in der Programmbezeichnung stammt ebenso wie die Dateiendung WBU noch aus der Zeit des Worldbuild Updaters, der – wie in Kapitel 4.7.4 beschrieben – nur für das Updaten von Worldbuild zuständig war. Das Programm erwartet zwei Parameter. Die Syntax sieht folgendermaßen aus: Syntax: WbCUpdate <Listendatei> <Ausgabedatei> Beispiel: WbCUpdate Worldbuild.lst Worldbuild.wbu Die Listendatei hat gewöhnlich die Endung „LST“. Hierbei handelt es sich um eine Textdatei, bei der jede Zeile für eine einzubindende Datei steht. Auch bei diesen Zeilen gilt eine bestimmte Syntax: Syntax: <PAK/PAK_CODED> <Dateiname>[> <Dateiname im Container>] Beispiel: <PAK_CODED D:\Projekte\Worldbuild\Worldbuild.exe>Install.001 Das erste Schlüsselwort („PAK“ oder „PAK_CODED“) entscheidet, ob eine Datei verschlüsselt oder unverschlüsselt in die Ausgabedatei aufgenommen werden soll. Danach wird durch ein Leerzeichen abgetrennt der Dateiname (mit komplettem Pfad) der einzubindenden Datei angegeben. Als drittes kann optional der Dateiname angegeben werden, den die Datei im Container besitzen soll. Wichtig ist im Zusammenhang mit dem Updater, dass die Listendatei die Datei Instruct.dat enthält, die wie in Kapitel 4.7.3.2f bereits erwähnt die Installationsinstruktionen enthält. Hierbei handelt es sich ebenfalls um eine Textdatei, die Befehle enthält. Beispiele sind im Folgenden aufgelistet: Befehl Funktion '(&2'(VRXUFH!WDUJHW! entschlüsselt die Datei source und kopiert sie nach target. &+(&.B3$66:25'SDVV! verlangt vom Benutzer die Eingabe des Passwortes pass. Sollte dies nicht erfolgen, wird der Installationsvorgang abgebrochen. 6(7&21),*.(<NH\! legt den Hauptschlüssel key der Systemregistierung fest, auf den sich alle folgenden Registrierungseingriffe beziehen. '(/(7(B&21),*9$/8(VHFWLRQ! löscht die Sektion section in der Systemregistrierung (unter dem eingestellten Hauptschlüssel) 6+2:LQIRILOH!FDSWLRQ! zeigt die Informationsdatei infofile an, wobei das Anzeigefenster den Titel caption trägt. Bei infofile kann es sich um eine Html- oder um eine Textdatei handeln. Die Ausgabedatei bezeichnet den Dateinamen der erzeugten Container-Datei. Normalerweise trägt sie die Endung WBU. Nach dem Vorgang kann sie direkt auf den Update-Server geladen werden. Das Programm wurde für den Worldbuild Updater am 4. Juni 2000 geschrieben und später für den Phalanx Updater weiterverwendet. Für das Projekt garantiert es durch die Verschlüsselungstechnik Sicherheit vor illegalen Server-Downloads. Eine heruntergeladene WBU-Datei ist ohne den Phalanx Updater und das entsprechende Passwort für die meisten Internet-User unbrauchbar. 4.9.2 StatCreator Das letzte Programm, das ich erwähnen möchte, hat nichts mit den bisher erwähnten Komponenten zu tun. Es ist vielmehr Teil der Visualisierung der Projekts (Kapitel 6). Seine Aufgabe ist es, anhand der Internet-Logbücher (siehe 6.1) Statistiken aufzustellen. Daraus resultiert auch der Programmname „StatCreator“ („Statistic creator“). Das Projekt erwartet keine Parameter, denn die Verweise auf die zu verwendenen Programme sind „hard coded“, sprich: fest in den Quelltext einprogrammiert. Ein dynamisches, abstraktes Modell wäre in diesem Fall ohnehin absolut fehl am Platz, da das Programm nicht an Andere weitergegeben wird und die Festplattenverweise daher gleich bleiben. Das Programm ermittelt aus den Html-Logbüchern, an welchen Tagen ich an dem Projekt gearbeitet habe. Daraus generiert es drei Statistiken: • eine Zeittafel, die besagt, wann an welchem Projekt gearbeitet wurde (siehe Abbildung in Kapitel 4.10) • die Anzahl der Arbeitstage für jede Komponente • die Quelltext-Größen der einzelnen Komponenten in Zeilen und Zeichen Diese Statistiken erlauben einen interessanten Einblick in die Entwicklung des Projektes und in den Aufwand, der in die verschiedenen Komponenten investiert wurde. Der StatCreator erzeugt zwei Html-Dateien (deutsch und englisch), die direkt auf die Website hochgeladen werden können. Das Ergebnis kann auf der Internetseite (www.phalanxsoftware.de.vu) in der Sektion „Statistic“ betrachtet werden. Das Programm wurde am 30. Juni 2001 geschrieben. Am 11. Juli desgleichen Jahres kam die Zweisprachigkeit hinzu. 4.10 Zeitlicher Gesamtkontext Das folgende Diagramm fasst die Entwicklungszeiten aller Komponenten zusammen: .RPSRQHQWH0$0--$621'-)0$0--$621'-)0 5DFHU :RUOGEXLOG )LOH&RQWDLQHU :RUOGEXLOG +LOIHWH[W 7H[WXUH &RQWDLQHU :RUOGEXLOG 8SGDWHU 3KDODQ[ 8SGDWHU 6KLS(GLWRU Die Grafik wurde mit Hilfe des Programms StatCreator (4.9.2) erzeugt. Sie zeigt wann und wie intensiv an den einzelnen Komponenten gearbeitet wurde. Je stärker die Intensität eines Farbfeldes ist, desto mehr Aufwand wurde in diesem Monat (X-Achse) an einer Komponente (Y-Achse) betrieben. Graue Felder stehen dagegen für Monate, in denen ich mich nicht mit dem jeweiligen Projektteil beschäftigte. Wie Sie sehen können, wurde im Grunde durchgehend an Worldbuild gearbeitet. Zu Recht kann es als Kern des Projekts bezeichnet werden. Erst seit Sommer 2001 kam Racer dazu, denn dafür musste Worldbuild erst richtig ausgereift sein. Jeweils in August 2000 und 2001 ist eine deutliche Arbeitspause zu erkennen. Das Projekt wurde 2000 aufgrund eines Austauschprogramms mit Australien unterbrochen. In dieser Zeit programmierte ich fast nicht, arbeitete jedoch Lösungsstrategien für damals aktuelle Probleme aus. Der August 2001 kann ebenfalls als „schöpferische“ Pause bezeichnet werden. Ansonsten wurde jedoch bis Ende 2001 durchgehend intensiv am Projekt gearbeitet. 5 Ausgewählte Problemsituationen In diesem Kapitel werden ausgewählte Problemsituationen erläutert, die während der Projektentwicklung auftraten. Neben der Beschreibung werden die gefundenen Überlegungen und Strategien genau erklärt, die zur Lösung verhalfen. Die Auswahl betrachtet nur eine kleine Auslese der Schwierigkeiten, die innerhalb der zwei Jahre Entwicklungszeit überwunden werden mussten. Eine vollständige Ausführung aller Probleme würde den Rahmen dieser Arbeit sprengen. Hierbei verweise ich erneut auf die Logbücher, die alle Zusammenhänge chronologisch auflisten. 5.1 Blockspeicherung vs. Verkettete Listen Im Januar 2001 wurde die Funktionalität von Worldbuild des öfteren in der Praxis getestet. Dabei fiel auf, dass der 3D-Editor ab einer bestimmten Anzahl keine weiteren Surfaces mehr erzeugen konnte. Außerdem wurde das Programm bei der Erzeugung von Objekten immer langsamer, wenn eine gewisse Menge erreicht wurde. Schnell wurde klar, dass dies mit dem Verfahren zusammenhing, mit dem die Objekte in Worldbuild gespeichert wurden: der Blockspeicherung. In einem sehr umfangreichen Verfahren wurde das gesamte Speicherprinzip auf verkettete Listen umgestellt. Die gesamte Umstellung dauerte ganze 14 Tage und war mit diversen Komplikationen verbunden, da im Kern des Programms gearbeitet wurde: Gut die Hälfte aller Programmfunktionen mussten teilweise neugeschrieben werden. Welche Vorteile bieten verkettete Listen gegenüber der Blockspeicherung? Um dies zu klären, schauen wir uns die beiden Prinzipien mal genauer an. 5.1.1 Was ist Blockspeicherung? Wie der Name schon sagt, werden bei der Blockspeicherung alle Daten einer Liste in einem einzigen Block gespeichert. Soll beispielsweise der Speicher für eine Liste von 50 IntegerWerten (Integer unter Win32: 4 Byte) reserviert werden, so werden zunächst 200 Byte von freiem nebeneinander liegendem Speicher gesucht. Soll ein Eintrag der Liste hinzugefügt werden, prüft das System, ob der Block einfach vergrößert werden kann, ohne dass von anderen Programmen belegter Speicher überschrieben würde. Ist dies nicht der Fall, muss neuer freier Platz gesucht werden. In Worldbuild wurden für die Reservierung und Freigabe von Blockspeicher die CFunktionen malloc und free verwendet. Die realloc-Funktion diente dazu, einen Speicherblock zu vergrößern oder zu verkleinern, wenn neue Einträge hinzukommen oder gelöscht werden mussten. Das ehemalige Speicherbild von Worldbuild lässt sich anhand des folgenden Bildes veranschaulichen, wobei jede Farbe einen anderen Objekttyp repräsentiert: Blockweise Speicherverteilung weiß = freier Speicher bunt = blockweise reserviert grau = nicht verfügbar Der Zugriff auf Blockspeicher ist sehr einfach. Da alle Daten im Speicher nebeneinander (in einem Block) liegen, ist die Ermittlung der Speicheradresse für den Wert Nr. x sehr einfach: Adresse = Startadresse + (Größe eines Eintrags) * x Hierbei ist zu beachten, dass für den ersten Eintrag [x = 0] gilt. Wie das folgende CodeBeispiel zeigt, ist der Zugriff auf ein einzelnes Element mathematisch bequem über Zeiger möglich. void main() { // Speicher für 20 Zahlen reservieren // (Startadresse wird zurückgegeben, NULL = kein Speicher frei) int *pListZahlen = malloc( sizeof(int) * 20 ); // ... weiterer Programmcode (z.B. Eingabe der Zahlenwerte) // Zeiger auf das dritte Element // (Startadresse um 2 Integer-Größen verschoben) int *pNummer = pListZahlen + 2; // Zugriff auf den Inhalt an dieser Adresse int nNummer = *pNummer; // Speicher wieder freigeben free(pListZahlen); } In dem Beispiel ist die Multiplikaktion mit der Größe eines Eintrags (siehe Formel) nicht notwendig, da dies automatisch durch den Zeigertyp int* geschieht. Wie in anderen Programmiersprachen üblich kann jeder Eintrag der Liste auch über eckige Klammern angesprochen werden. In unserem Beispiel würde das dann folgendermaßen aussehen: nNummer = pListZahlen[2]; Die Berechnung der Speicheradresse geht in diesem Fall intern vonstatten. Die Verwendung von Zeigern ist dann effektiv, wenn alle Einträge einer Liste verarbeitet werden sollen. In unserem Beispiel könnte man nun alle Listeneinträge ausgeben lassen: int *pNummer = pListZahlen; // Zeiger auf die Startadresse for(int a = 0; a < 20; a++) // Schleife mit 20 Durchläufen { cout << *pNummer; // Ausgabe des Inhalts an der // Speicheradresse pNummer++; // Versetzung des Zeigers um eine // Integer-Größe } Der Vorteil dieses Prinzips liegt auf der Hand: Um alle Einträge der Liste zu erreichen, muss ein Zeiger (pNummer) auf die Startadresse, also auf den ersten Eintrag ausgerichtet werden. Um nun jeden weiteren Wert in der Liste zu erreichen, reicht jeweils die Verschiebung des Zeigers um 1 aus (pNummer++). Im Gegensatz zu dem Zugriff über eckige Klammern ist hier kein wiederholtes Ausrechnen der Speicheradresse notwendig. In Worldbuild gibt es diverse Funktionen, die sich auf komplette Listen beziehen. Dies ist gerade bei den Objektlisten (Surfaces, Lichter etc.) der Fall. Die Verwendung von Blöcken ist in solchen Fällen also sehr schnell. Schwieriger ist hingegen das Hinzufügen eines neuen Eintrags. Wie bereits erwähnt ist dafür die Vergrößerung des Speicherblocks nötig. Das folgende Beispiel nutzt dafür die C-Funktion realloc. int nAnzahl = 20; int nNewValue = 237; // Anzahl von Einträgen // Neuer Wert // Vergrößerung des Speicherblocks int *pNewMemory = realloc(pListZahlen, sizeof(int) * (nAnzahl + 1)); if(pNewMemory == NULL) // Nicht möglich => Abbruch! return; // Speicherung der neuen Speicheradresse pListZahlen = pNewMemory; pListZahlen[nAnzahl] = nNewValue; nAnzahl++; // Zuweisung des neuen Werts // Erhöhung der Eintragsanzahl Die realloc-Funktion gibt bei erfolgreichem Aufruf einen Zeiger auf den vergrößerten Speicherblocks zurück, ansonsten die NULL-Konstante, wenn eine Vergrößerung nicht möglich war. Der vergrößerte Block muss sich keinesfalls an der selben Stelle wie der ursprüngliche befinden. Daher ist die Speicherung der neuen Adresse (pListZahlen = pNewMemory) unbedingt notwendig. Soll ein Eintrag gelöscht werden, ist der umgekehrte Weg möglich, nämlich die Verkleinerung des Speicherblocks, ebenfalls über die realloc-Funktion. Ein Gebrauch von realloc ist dann ein sehr zeitintensiver Vorgang, wenn die Liste eine bestimmte Größe erreicht hat und neuer Speicherplatz erst gesucht werden muss. Bei Worldbuild war dies nahezu immer der Fall. 5.1.2 Was sind verkettete Listen? Im Gegensatz zur Blockspeicherung können sich bei den sogenannten verketteten Listen alle Einträge an völlig anderen Speicheradressen befinden, wie das folgende Bild demonstriert: Speicherverteilung über verkettete Listen weiß = freier Speicher bunt = blockweise reserviert grau = nicht verfügbar Der Begriff "verkettet" resultiert daraus, dass jeder Eintrag die Speicherposition des nächsten Eintrags in der Liste speichert. Auch hier muss die Startadresse – also die Adresse des ersten Eintrags – gespeichert werden. Bei verketteten Listen bezeichnet man diese Adresse als Wurzel. Alle weiteren Elemente werden über Verbindungszeiger erreicht, die in den Listenelementen selber gespeichert sind. Das Hinzufügen und Löschen von Einträgen kann auf vielfältige Weise geschehen. Im einfachsten Fall wird ein neuer Eintrag an der Wurzel eingefügt und auch an der Wurzel wieder entfernt. Diesen Sonderfall bezeichnet man als Stapel. Bei verketteten Listen greift man in C++ für die Speicherreservierung gewöhnlich auf die Befehle new und delete zu, die eine größere Funktionalität (insbesondere bei Klassen) gewährleisten. Das Beispiel auf der nachfolgenden Seite zeigt ein einfaches Programm, das diese Technik umsetzt. Schon am Umfang des Beispielprogramms lässt sich ablesen, dass die Implementierung einer verketteten Liste sehr aufwendig ist. Um alle Einträge der Liste zu erfassen – beispielsweise für eine vollständige Ausgabe – funktioniert eine mathematische Formel wie beim Blockspeicher nicht, da sich alle Elemente unabhängig vom ersten an völlig unterschiedlichen Speicheradressen befinden können. Wie die Funktion List() des Beispiels zeigt, wird dafür die gesamte Verkettungslinie abgegangen: Ein Zeiger wird zunächst auf die Wurzel und anschließend solange auf den "nächsten" (pNext) gesetzt, bis kein weiterer Eintrag mehr vorhanden ist. Soll der fünfte Eintrag einer Liste angesprochen werden, so bedeutet das theoretisch: pEintrag5 = g_pRoot->pNext->pNext->pNext->pNext; Hier wird ein gravierender Nachteil der verketteten Liste deutlich, der ihre Anwendbarkeit in Hochleistungsprogrammen stark einschränkt: Um einen beliebigen Eintrag zu erfassen, müssen alle Vorgänger verarbeitet werden. Der Zugriff auf den Verkettungs-Zeiger (pNext) ist in zeitkritischen Anwendungen ein großes Manko. Sogar bei der Freigabe aller Einträge (siehe Beispiel-Funktion Cleanup()), müssen alle Einträge schrittweise abgegangen werden. Eine Sammelfunktion wie free() steht bei verketteten Listen nicht zur Verfügung. Vorteilhaft ist jedoch die Tatsache, dass bei verketteten Listen nie mit Speichermangel gerechnet werden muss, da für jeden Eintrag einzelne, kleine Blöcke reserviert werden. Die Verwaltung großer Mengen von Elementen ist also ohne weiteres möglich. #include <iostream.h> // Header für Ausgabe struct Entry { int nWert; Entry *pNext; } // STRUKTUR FÜR EINEN EINTRAG Entry *g_pRoot = NULL; // WURZEL (+ auf Null setzen) char Push(int nNewValue) { Entry *pNewEntry = new Entry; if(pNewEntry == NULL) return false; pNewEntry->nValue = nNewValue; // WERT AUF DEN STAPEL LEGEN // Inhalt // Zeiger // Speicher für neuen Eintrag // reservieren. Abbrechen, // wenn kein Speicher frei. // Wert speichern. pNewEntry->pNext = g_pRoot; g_pRoot = pNewEntry; // Eintrag an den Anfang der // Liste einfügen. return true; // Ok! } char Pop(int &nValue) { if(g_pRoot == NULL) return false; // WERT VOM STAPEL HOLEN // Wenn die Liste leer ist, // Abbruch. nValue = g_pRoot->nValue; // Obersten Wert speichern Entry *pDelEntry = g_pRoot; g_pRoot = g_pRoot->pNext; delete pDelEntry; // Wurzel auf den nächsten // Eintrag schieben und // Speicher freigeben. return true; // Ok! } void Cleanup() { int nTemp; while(Pop(nTemp) == true); } // GESAMTEN SPEICHER FREIGEBEN void List() { Entry *pEntry = g_pRoot; while(pEntry != NULL) { cout << pEntry->pValue; pEntry = pEntry->pNext; } } // ALLE EINTRÄGE AUFLISTEN // Solange Einträge holen, // bis die Liste leer ist. // // // // // void main() // { Push(14); // Push(10); // int nValue; Pop(nValue); // Cleanup(); // } Einfaches Beispielprogramm für die Einrichtung eines Stapels Zeiger auf die Wurzel Solange, wie Eintrag vorhanden ist. Aktuellen Wert ausgeben. Zeiger auf nächsten Eintrag. HAUPTPROGRAMM Werte auf den Stapel legen. Wert vom Stapel holen. Speicher freigeben. 5.1.3 Auswahl des Speicherprinzips Nun stellt sich die Frage: Wann benutzt man welchen Speichertyp? Blockspeicher gewährt schnellen Zugriff, ist bei einer Größenänderung der Liste jedoch langsam. Außerdem kann es vorkommen, dass die realloc-Funktion keinen freien Speicher mehr findet, wenn der Block zu groß wird. Verkettete Listen können zwar nahezu "unbegrenzt" vergrößert werden, sind in der Zugriffszeit jedoch erheblich langsamer. Die Antwort liegt auf der Hand: Bei statischen Listen wird Blockspeicher verwendet, bei dynamischen verkettete Listen. Ist die Anzahl der Einträge klar festgelegt, also statisch, so kann man auf Blockspeicher zugreifen und dessen Geschwindigkeitsvorteil nutzen. Dafür ist auch nicht der Gebrauch von realloc notwendig, weil die Listengröße einmal bei der Erzeugung bestimmt wird. Dies ist auch der Grund, warum Racer, der auf eine sehr hohe Programmleistung angewiesen ist, für alle Objektlisten Blockspeicher verwendet. Sind die zu verwaltenden Daten dynamisch, werden also oft Daten hinzugefügt oder gelöscht, greift man auf verkettete Listen zu. Zwar ist der Zugriff auf die einzelnen Elemente langsamer, das Einfügen und Entfernen von Daten jedoch schneller und keinesfalls speicherkritisch. Wie bereits zuvor erwähnt, wurde Worldbuild in einem extrem aufwendigen Verfahren auf verkettete Listen umgestellt. Alle Karten-Objekte waren bisher in Speicherblöcken gespeichert und musste nun in das neue Prinzip umgeschrieben werden. Da die Aufgabe des 3D-Editors gerade darin besteht, auf die Objektlisten der Karte zuzugreifen, mussten alle Zugriffsfunktionen, also ein Großteil des Programms, neugeschrieben werden. Dies wirkte sich dann indirekt auf andere Funktionen aus (z.B. die Rückgängig-Funktion), die dann ebenfalls komplett überarbeitet werden mussten. Der enorme Aufwand machte sich bezahlt: Zwar wurde Worldbuild sichtbar langsamer, auch wenn dies durch Optimierungen im Laufe der Entwicklung verbessert wurde. Objekte konnten jedoch auch in großen Mengen bei konstanter Geschwindigkeit erzeugt werden, ohne dass Speicherprobleme auftraten. ,QWHUQHW KWWSZZZSKDODQ[VRIWZDUHGHYX !:RUOGEXLOG±,QVLGH 5.2 Das Culling Der wichtigste Aspekt einer 3D-Engine (besonders bei Racer) ist das sogenannte Culling (zu Deutsch: "auslesen"). Keine Grafikkarte verkraftet das Rendern aller Polygone eines Levels ohne Geschwindigkeitsverlust. Daher entscheidet das Culling-Verfahren, welche Surfaces gerendert werden sollen und welche nicht. Die Lösung erscheint im ersten Moment einfach: Nur die Surfaces rendern, die dem Spieler sichtbar sind. Doch welche sind denn sichtbar und welche nicht? Bei diesem Problem muss man auf zwei Dinge achten: Zunächst einmal hat der Programmierer nur mathematische Daten zur Verfügung, sprich dreidimensionale Punkte (Vertices), die zu Surfaces zusammengesetzt werden, und die Kameraperspektive. Die Lösung muss also auch mathematisch betrachtet werden, was die Schwierigkeit ausmacht. Viel wichtiger ist jedoch Folgendes: Das Auslesen muss effektiv und schnell sein. Wenn es ein optimales Ergebnis liefert (z.B. dass nur die wirklich sichtbaren Surfaces gerendert werden), das jedoch mehr Zeit für das "Herausfinden" erfordert als es durch das Weglassen nicht-sichtbarer Surfaces gewinnt, so ist es unbrauchbar. Wie in vielen Bereichen des Computers (Beispiel: JPEG-Bilder) muss hier ein Kompromiss zwischen Qualität und Performance gefunden werden. Und gerade hier liegt das eigentliche Problem: Ein perfektes Ergebnis zu erhalten ist leicht, es schnell zu erhalten, ist äußerst schwer. Oft kämpft der Programmierer dabei um Nanosekunden. Nach langer Entwicklungszeit fanden vier Verfahren in Racer Anwendung, die im Folgenden näher erläutert werden: 5.2.1 OcTree Culling Das erste Prinzip, das angewendet wird, ist das OcTree30-Verfahren. Dabei wird die gesamte Levelgeometrie über eine Baumstruktur in dreidimensionale „Boxen“ (oder: Nodes) verpackt: Das Level wird von einer Box umfasst, die acht31 Unterboxen beinhaltet, die wiederum acht Unterboxen enthalten. Nach einer bestimmten Anzahl an Unterteilungen enthalten die Boxen der „untersten Ebene“ die Surfaces, die zu rendern sind. 30 31 OcTree, octal tree [engl.] = Oktalbaum von octo [lat.] = acht Der Render-Prozess überprüft nun rekursiv, ob alle Boxen im sichtbaren Bereich also im View Frustum (siehe 2.2.3.5c) liegen. Rekursiv bedeutet, dass zunächst überprüft wird, ob die Levelbox (die alles umfasst), dem Spieler sichtbar ist. Wenn ja, was höchstwahrscheinlich ist, werden nun die acht Unterboxen mathematisch auf ihre Sichtbarkeit überprüft. Das Verfahren wiederholt sich, bis alle sichtbaren Boxen der untersten Ebene gefunden sind, die die zu rendernen Surfaces enthalten. Der Vorteil: Ist eine einzige Box außerhalb des Sichtbereichs, können alle Surfaces der Unterboxen vom Rendering ausgeschlossen werden, da sie dann ja auch nicht sichtbar sein können. Dieses Bild veranschaulicht das Prinzip in zweidimensionaler Ansicht: Der graue Bereich, der durch die zwei roten Linien eingeschlossen wird, bestimmt den sichtbaren Bildausschnitt. Die Kamera befindet sich demnach am Ausgangspunkt unten im Bild und ist nach oben gerichtet. Die blauen Kästchen sind sichtbare, die schwarzen ignorierte OcTreeNodes. Wie Sie sehen können, werden große Boxen von Anfang an ausgelassen (unten links), sowie Unterboxen, deren übergeordnete Nodes am Rande des Sichtbereichs liegen (Mitte links). Ein absolut optimales Ergebnis liefert das OcTree Culling zwar nicht, da auch Boxen vollständig gerendet werden, die nur teilweise im Sichtbereich liegen. Die Zahl der gerenderten Surfaces wird jedoch erheblich reduziert. In jedem Fall ist dieses Verfahren sehr schnell, verbraucht also kaum Performance, und bildet daher eine wichtige Vorstufe für die anderen Verfahren. Die OcTree-Struktur wird von Worldbuild beim Kompilieren einer Karte (siehe 4.2.2.6) erzeugt und vom Racer geladen und angewendet. 5.2.2 BeamTree Culling Das OcTree-Verfahren ist zwar sehr effizient, rendert jedoch auch Objekte in einer Szene, die überhaupt nicht sichtbar sein können, wenn es sich z.B. um einen Levelkomplex handelt, der erst viel später erreicht wird, jedoch mathematisch gesehen im Sichtbereich liegt. Dafür ist das BeamTree-Verfahren zuständig. Dieses Verfahren überprüft, ob gewisse Objekte hinter anderen Objekten liegen und deshalb nicht sichtbar sein können. Demnach können Objekte ausgelassen werden, die sich z.B. hinter einer großen Wand befinden. Für diese Prüfung müssen Surfaces und die "Verdeckvolumen", die sie erzeugen, in einer sogenannten BeamTree-Struktur gespeichert werden. Nach längerer Entwicklungszeit wurden verschiedene Variationen erprobt: Variation I: Surface - Surface Die erste Variation liefert ein sehr exaktes Ergebnis. Sie überprüft welche Surfaces komplett hinter anderen Surfaces liegen. Übrig bleiben dann tatsächlich nur die Surfaces, die wirklich sichtbar sein können. Das Problem dieses Verfahrens ist der inakzeptable Performanceverbrauch. Bei Tests wurde die Framerate von 60 fps auf 1 fps reduziert. Für ein Echtzeitrendering kommt dies also nicht in Frage. Das trifft auch dann zu, wenn die Surfaces auf die 100 bis 300 (der Kamera) nahesten beschränkt werden. Variation II: Surface - OcTree Um die Performance zu erhöhen, wurden nur noch OcTree-Boxen auf ihre Sichtbarkeit überprüft, sprich: ob irgendeine OcTree-Box komplett hinter einem der Surfaces liegt (also in dem Volumen hinter dem Surface). Auch hier ist das Ergebnis relativ gut, jedoch nicht optimal. Außerdem ist der Performanceverlust immer noch zu hoch (im Test: 60 fps auf 14 fps), weil für eine gute Auslese viele OcTree-Nodes notwendig sind, was wiederum Programmleistung kostet. Ein weiteres Problem der beiden Variationen ist die Frage, welche Surfaces überhaupt in den BeamTree eingefügt werden sollten. Werden kleine Surfaces eingefügt, wird unnötig Zeit verschwendet, weil sie ein kleines Volumen bilden. Häufen sich mittelgroße Surfaces an, erhält man ein sehr schlechtes Ergebnis, weil es kein Volumen gibt, das viele OcTree-Nodes verdeckt. Standardmäßig ist die Ausbeute also nicht besonders groß und vor allen Dingen nicht gerade schnell. Variation III: Manuelle Separator-Surfaces - OcTree Die Variation, die nun umgesetzt wurde, macht dem Leveldesigner etwas mehr Arbeit, da er die BeamTree-Surfaces manuell einfügen muss. Dabei kann man Levelbereiche durch große Trennsurfaces (separator surfaces) voneinander abgrenzen. Da diese "Trennwände" im Allgemeinen sehr groß sind, resultiert daraus immer eine optimale Auslese. Außerdem sind selbst in einem großen Level nur wenige gezielt gesetzte Separators notwendig, was die Anzahl der Volumen-Prüfungen erheblich reduziert. Durch die Vorbestimmtheit, welche Surfaces in den BeamTree eingefügt werden sollen, muss diese Struktur außerdem nur einmal - nämlich nach dem Laden der Leveldaten - erzeugt werden, was erneut Zeit spart. Deshalb kann der BeamTree auch mit dem OcTree „zusammengeschaltet“ werden, was die Gesamtzeit verkürzt. In diesem Bild sieht man deutlich das Separator-Surface (grün), das die OcTreeNodes in dem Volumen, das es hinter sich bildet, ausschließt. 5.2.3 Der Performance-Test - OcTree und BeamTree Dieser Test veranschaulicht, wie sich die beiden Verfahren auf die Programmleistung auswirken. Getestet wurde ein Computer mit folgender Ausstattung: Prozessor: Duron 750Mhz RAM: 256MB SDRAM Grafikkarte: GeForce2 GTS 64MB DDR Betriebssystem: Windows 98 Bei der Testszene handelt es sich um eine Tunnelröhre, hinter der sich in der Ferne ein weiterer Levelabschnitt befindet, der vom Standpunkt aus bei normalem Rendering nicht gesehen werden kann. Ein Separator-Surface trennt die beiden Bereiche ab. Das Bild kann auf der Website (siehe Adresse unten) betrachtet werden. Und hier das Ergebnis: 2F7UHHDNWLYLHUW" %HDP7UHH DNWLYLHUW" $Q]DKOGHU 6XUIDFHVLP 5HQGHUSUR]HVV Framerat e [fps] 1HLQ -D 1HLQ -D 1HLQ 1HLQ -D -D Das Ereignis ist deutlich: Das optimalste und schnellste Resultat bietet die Kombination beider Verfahren. 5.2.4 Backface Clipping Jedes Surface besitzt eine Vorder- und Rückseite, die sich aus der Normalen des Polygons (der Vektor, der im rechten Winkel von der Fläche wegzeigt) ergibt. Wie bereits in 2.2.3.3 beschrieben, errechnet sich diese Normale aus der Anordnung bzw. Reihenfolge der Vertices. Die Vorderseite eines Surfaces ist genau dann sichtbar, wenn die Normale auf die Kamera hinzeigt. Das sogenannte Backface Clipping ist ein Verfahren, das grundsätzlich von allen 3DProgrammen angewendet wird. Es werden alle Surfaces aus dem Renderprozess ausgeschlossen, deren Rückseiten zur Kamera zeigen. Dabei wird eine drastische Anzahl von Surfaces übergangen, was einen wichtigen Leistungsschub bedeutet. 5.2.5 Fog Culling Die letzte Eigenschaft, die sich Racer für die Auslese von gerenderten Surfaces zunutze macht, ist der Einbau von Nebeleffekten (siehe 4.2.2.5b). Wird ein Nebel in einem Level verwendet, so lässt die 3D-Engine alle Surfaces weg, die aufgrund des eingestellten Nebels nicht mehr sichtbar sind, weil sie komplett darin verschwinden. Diese Methode macht besonders für komplexe und weite 3D-Szenarien Sinn, die Nebel einsetzen. Viele Spiele nutzen dieses Verfahren für eine Leistungssteigerung und überlassen die Einstellung der Sichtweite in einigen Fällen sogar dem Spieler selbst. All diese Verfahren finden im Racer Anwendung und tragen erfolgreich zur Aufrechterhaltung einer akzeptablen Framerate bei. ,QWHUQHW KWWSZZZSKDODQ[VRIWZDUHGHYX !5DFHU±,QVLGH 5.3 Interpolation Die externe Kontrollklasse CCameraFly ist für Kamerafahrten in den Karten verantwortlich. Als Parameter erfordert diese Ereignisklasse folgende Parameter: • Startmarker ist die erste Kameraposition. Die verbundenen Marker definieren den Pfad. • Speed ist die Geschwindigkeit mit der sich die Kamera bewegt. Um die Kamera über den Markerpfad zu bewegen, werden die Positions- (X, Y und Z) und Richtungsvektoren (Yaw, Pitch und Roll) zwischen jeweils zwei Markern interpoliert, also zuerst zwischen dem ersten und dem zweiten Marker, dann zwischen dem zweiten und dritten, bis der letzte Marker erreicht ist. Beim Testen der Kamerafahrten fiel jedoch auf, dass sie sehr „abgehackt“ wirkten. Der Betrachter merkte es, wenn ein neuer Marker angesteuert wurde. Um dies zu kompensieren, war es notwenig, sehr viele Marker einzusetzen, um eine möglichst "runde" Kamerafahrt zu gewährleisten (≅ Erhöhung der Tesslation). Doch trotzdem wirkten die Sichtwechsel immer noch nicht weich. Die Ursache liegt darin, dass die verwendete Interpolation, die die Einstellungen zwischen den Markern berechnete, linear war. Im Folgenden werden zwei verschiedene Varianten beschrieben. Doch zunächst wird die Frage geklärt, worum es sich bei Interpolation überhaupt handelt. 5.3.1 Was ist Interpolation? Der Begriff Interpolation umfasst im mathematischen Sinne die Suche nach Zwischenwerten zwischen zwei Variablen a und b. Die Funktion fa,b, die diese Werte liefert, wird als Interpolationsfunktion bezeichnet. Als Parameter werden die zu interpolierenden Variablen und ein Wert x, der im Intervall [0; 1] liegt, erwartet. Der Algorithmus ist so aufgebaut, dass Folgendes gilt: fa,b(0) = a ∩ fa,b(1) = b Das Einsetzen von 0 ergibt also den Ausgangswert a, 1 ergibt den Zielwert b. Alle x zwischen 0 und 1 ergeben die Werte zwischen a und b, die je nach Interpolationsvariante anders ausfallen. Die einfachste Variante ist die lineare Interpolation (5.3.2). Geometrisch betrachtet liegt die Wertemenge von fa,b auf einer Geraden. Komplexere Varianten werden nur dann angewendet, wenn mehrere Folgewerte in Form einer Kurve interpoliert werden sollen. Dies ist zum Beispiel dann der Fall, wenn in einem Liniendiagramm eine Tendenz angezeigt werden soll. Oder wenn man eine mathematische Kurvenfunktion anhand von wenigen Koordinaten zeichnen will – oder wenn eine Kamerafahrt weicher aussehen soll ... Bei den komplexeren Varianten sind neben a und b weitere Variablen notwendig, die die „Umgebung“ der beiden Werte charakterisieren, meist den vorigen und eventuell den nächsten Interpolationswert. Dies lässt sich daran erklären, dass der Algorithmus beim Zeichnen einer Kurve eine globale Tendenz berücksichtigt. Der Kurvenverlauf im Intervall [a; b] wird also von den anderen „Fixpunkten“ beeinflusst. Bei einer sogenannten Beziér-Kurve ist ein dritter Wert notwendig, bei der kubischen Interpolation (5.3.3) zwei weitere. Die Abbildung auf der nächsten Seite zeigt den Unterschied zwischen einer einfachen linearen und einer komplexen kubischen Interpolation, die insgesamt vier Variablen erwartet. Lineare (schwarz) und kubische Interpolation (rot) 5.3.2 Lineare Interpolation Die bereits mehrmals erwähnte lineare Interpolation ist die einfachste Variante. Die Wertemenge liegt geometrisch betrachtet auf einer Gerade. Neben den Grenzvariablen a und b sind deshalb auch keine weiteren Variablen notwendig, wie es bei Kurveninterpolationen der Fall ist. Die Formel für diese Funktion sieht folgendermaßen aus: fa,b(x) = a * (1 – x) + b * x (bei 0 ≤ x ≤ 1) Stellt man die Variablen um, ist die allgemeine Geradengleichung erkennbar: fa,b(x) = (b-a) * x + a (umgestellte Version) f (allgemeine Geradegleichung) (x) = m * x + b Die programmiertechnische Umsetzung ist ebenfalls sehr übersichtlich: float LinearInterpolate(float a, float b, float x) { return a * (1 – x) + b * x; } Die schwarze Linie in der Abbildung unter 5.3.1 stellt lineare interpolierte Punkte dar. 5.3.3 Kubische Interpolation Wesentlich komplexer ist die kubische Interpolation, die insgesamt vier Variablen (v0, v1, v2, v3) erwartet, um die Zwischenwerte zu berechnen. v1 und v2 bezeichnen die Werte, zwischen denen interpoliert wird, und sind daher mit a und b aus 5.3.2 gleichzusetzen. Der Zusatzwert v0 bezeichnet den vorigen, v1 den nächsten Interpolationswert. Wie zuvor erwähnt bestimmen diese beiden Werte den Kurvenverlauf. Das Ergebnis ist eine runde Funktionskurve, die durch alle Fixpunkte läuft. Die Berechnung ist etwas umfangreicher als bei der linearen Interpolation. Der Einfachheit halber wird der Funktionsterm in mehrere Teilschritte zerlegt: P = (v3 – v2) – (v0 – v1) Q = (v0 – v1) – p R = V2 – V0 S = V1 fv0,v1,v2,v3(x) = P * x³ + Q * x² + R * x + S; Die Bezeichnung „kubisch“ leitet sich daraus ab, dass es sich bei dem Funktionsterm um eine Funktion dritten Grades handelt. Die Umsetzung in C++ sieht folgendermaßen aus: float CubicInterpolate(float v0, float v1, float v2, float v3, float x) { float P = (v3 - v2) - (v0 - v1); float Q = (v0 - v1) - P; float R = v2 - v0; float S = v1; return P * x*x*x + Q * x*x + R * x + S; } Die Schreibweise x*x*x ist übrigens schneller als der Gebrauch der Fließkommafunktion pow(x, 3). Eine Frage, die sich jetzt stellt ist: Welche Parameter übergebe ich, wenn ich weniger als vier Werte zur Verfügung habe? Die Antwort ist einfach: Man verwendet Werte doppelt. Stehen beispielsweise nur zwei Werte zur Verfügung, setzt man v0 = v1 und v2 = v3. Das Ergebnis ist eine Gerade. Bei drei Werten ist die Entscheidung schwieriger, denn nur zwei Werte können gleichgesetzt werden. Wird dann v0 = v1 gesetzt, so beginnt der Funktionsverlauf gerade und endet rund. Bei v2 = v3 ist der Verlauf zunächst rund und dann gerade. Eine Gleichsetzung zweier Parameter ist eine Gewichtung auf den einen (v0) oder den anderen Grenzwert (v2). v1 darf selbstverständlich nicht mit v2 gleichgesetzt werden, da es die Interpolationsgrenzen sind. Ansonsten wäre das Ergebnis ein konstanter Wert, entweder v1 oder v2. Die rote Linie in der Abbildung von 5.3.1 stellt kubisch interpolierte Punkte dar. Am 19. November 2001 wurden alle Interpolationsvorgänge in der Ereignisklasse CCameraFly von linearer auf kubische Interpolation umgestellt. Seitdem sind alle Kamerafahrten „rund“ und weich. Außerdem sind nun wesentlich weniger Marker notwendig, um einen Kamerapfad zu bestimmen, da der Richtungswechsel zum nächsten Marker nicht mehr bemerkt werden kann. Auf der beigefügten CD ist das Programm ‚Interpolation’ im gleichnamigen Ordner enthalten, das den Unterschied zwischen den beiden Interpolationsarten demonstriert. ,QWHUQHW KWWSZZZSKDODQ[VRIWZDUHGHYX !5DFHU±,QVLGH 6 Visualisierung Das Projekt und dessen Komponenten wurden auf vielfältige Weise dokumentiert und veröffentlicht. Es wurde ein Hilfetext verfasst und Logbücher geführt. Des Weiteren wurde das Projekt ausführlich und regelmäßig auf einer eigenen Internetseite illustriert. Nicht zuletzt wurde es zweimal auf einer renommierten Website namens Flipcode.com publiziert, wo es auf sehr positive Resonanz stieß. Im Folgenden wird die Visualisierung der gesamten Unternehmung detailliert erläutert: 6.1 Logbücher Für alle Programme, die ich für das Projekt entwickelte, liegen Logbücher über die genaue Entwicklung vor. Bei jeder Programmänderung wurden sie ergänzt. Leider begann ich erst am 25. Mai 2000 mit ihrer Führung, als das Projekt „realistische“ Form annahm, so dass vom Worldbuild-Logbuch etwa zweieinhalb Monate der Anfangszeit fehlen. Seitdem wurden diese Dokumente jedoch gewissenhaft geführt und sind nahezu vollständig. Das Führen von Logbüchern besitzt einige Vorteile: • Die Entwicklung eines Programms kann bis zur ersten Zeile zurückverfolgt und bis zur letzten nachvollzogen werden. • Umständliche Organisationselemente können analysiert und in Zukunft vermieden werden. • Statistiken über den Arbeitsaufwand können erzeugt und ausgewertet werden. Das Programm StatCreator nutzt zum Beispiel die Logbücher für solche Statistiken (siehe dazu die Kapitel 4.9.2 und 4.10). • Nicht zuletzt geht von der Führung eines Logbuchs Motivation für die Weiterentwicklung aus. Die Logbücher sind als Html-Dateien gespeichert und chronologisch von neueren nach alten Einträgen sortiert. Große Änderungen werden dabei hervorgehoben oder gesondert markiert. Bei designtechnischen Erneuerungen sind weiterhin Screenshots32 oder ausführlichere Erklärungen verfügbar. Alle Logbücher sind auf der Website des Projekts verfügbar, wie im Kapitel 6.3 genauer erläutert wird. 6.2 Worldbuild-Hilfetext Worldbuild ist im Gegensatz zu den durchschnittlichen Windows-Programmen eine Anwendung, die sich nicht „von selbst“, also durch ihren strukturellen Aufbau erklärt. Das Design einer 3D-Welt ist komplex und trotz der angestrebten Anwenderfreundlichkeit nicht einfach, wie es aus dem Kapitel 4.2 wohl schon hervorging. Aus diesem Grunde schrieb ich einen umfangreichen Hilfetext für den 3D-Editor. 6.2.1 Inhalt Der Worldbuild-Hilfetext deckt den gesamten Funktionsumfang ab und gibt darüber hinaus viele hilfreiche Tricks für ein erfolgreicheres Leveldesign. Er ist darauf ausgerichtet, dass er von einem Anfänger von vorne bis hinten chronologisch durchgelesen werden kann, um das ganze Wissensspektrum zu erhalten. Diverse Querverweise ermöglichen jedoch auch ein funktionales Lernen, so dass Kapitel gezielt gelesen werden können, ohne wichtiges Vorwissen zu verpassen. Eines der wichtigsten Elemente, auf die ich Wert gelegt habe, sind die vielen Beispiele, die schwierige Zusammenhänge mit Hilfe von Bildern illustrieren, um ihre Anwendbarkeit besser zu verdeutlichen. Das Inhaltsverzeichnis des Hilfetextes ist folgendermaßen strukturiert: • Einführung gewährt erste Einblicke in die 3D-Welt von Worldbuild. • Grundlagen beschreibt die grundlegenden Elemente des Editors. Dazu gehören unter anderem der Fensteraufbau, die Navigation in den Arbeitsfenstern, sowie – und das gehört zu den wichtigsten Unterkapiteln – die Arbeit mit 3D-Objekten. Dieses Kapitel ist die unbedingte Voraussetzung für den Umgang mit dem Editor. • 32 Primitiven erklärt, wie Primitiven eingefügt und konfiguriert werden. screenshot [engl.] = EDV: Bildschirmphoto • Spezifische Funktionen erläutert die objektspezifischen Funktionen, also alle Operationen, die sich speziell auf einen bestimmten Objekttyp beziehen. Des Weiteren wird ausführlich erklärt, welche Eigenschaften jeder Typ hat und wie man diese modifiziert. • Texturierung stellt vor, wie 3D-Szenen texturiert werden. • Mit Layern arbeiten erklärt, wie man mit verschiedenen Arbeitsschichten arbeitet, sie also einrichtet und verwendet. • Das Interface behandelt das gesamte Handling des Interfaces (siehe 4.2.2.5). • Fortgeschrittene Funktionen erläutert Sonderfunktionen, die das Leveldesign vervollständigen und abrunden. Dazu gehört auch das Kompilieren (siehe 4.2.2.6). • Worldbuild konfigurieren enthält die komplette Erklärung zu allen Einstellungsmöglichkeiten des 3D-Editors. • Anhang enthält die Steuerung des Editors im Überblick. Der Leser wird von einem Eingangstext begrüßt, der unter anderem die Änderung seit der letzten Version enthält. 6.2.2 Zeitliche Entwicklung Am 27. April 2001 beschloss ich, den Hilfetext zu schreiben, da Worldbuild eine Komplexität erreichte, die einer Hilfestellung bedurfte. Die Intention lag darin, die Handhabung des 3DEditors für die Mitglieder des früheren Teams leichter verständlich zu machen. Deshalb wurde er auf Deutsch verfasst. Danach diente er hauptsächlich dazu, den Funktionsumfang von Worldbuild für Interessenten genauer zu skizzieren. Möglicherweise werde ich den Text in Zukunft ins Englische übersetzen, um ihn auch für Projekte in anderen Ländern zu öffnen. Die hauptsächliche Entwicklung ging bis zum Juli 2001 vonstatten. Danach wurden immer wieder Ergänzungen getätigt, wenn Worldbuild um neue Funktionen erweitert wurde. Durch den Umfang des Textes und die für die Beispiele verwendeten Bilder hat der Hilfetext bereits eine Größe von über zwei Megabyte erreicht. Auf der Projekt-Website (siehe nächstes Kapitel) befindet sich das Logbuch zur Entwicklung des Hilfetextes, der in der Sektion ‚Files’ auch zum Download zur Verfügung steht. 6.3 Die Website Die wohl umfangreichste Visualisierung des Projekts ist die Website. Sie dokumentiert die vollständige Entwicklung aller Komponenten und eröffnet detaillierte Informationen zu bestimmten Problemsituationen, die teilweise in Kapitel 5 aufgeführt sind. Außerdem stehen einige Anwendungen zum Download bereit. Die Idee, die hinter der Website steht, ist einfach: Ich wollte meine Arbeit einem interessierten ‚Publikum’ zugänglich machen, gleichzeitig jedoch auch eine Dokumentation schreiben, auf die ich später zurückgreifen könnte. In dem Sinne ist die Seite eine Erweiterung der Logbücher, da es tiefere Einblicke erlaubt. Ursprünglich war die komplette Seite in Deutsch verfasst. Nachdem jedoch auch Entwickler aus anderen Ländern Interesse an dem Projekt fanden (siehe 6.4), übersetzte ich große Teile ins Englische. Die Logbücher wurden aber erst ab dem 28. März 2002 in englischer Sprache geführt. Die älteren Einträge blieben aufgrund ihres Umfangs unübersetzt. Die Website kann unter folgender URL33 aufgerufen werden: KWWSZZZSKDODQ[VRIWZDUHGHYX Die folgenden Teilkapitel gewähren eine Übersicht über den Inhalt und die Entwicklung der Internetseite. 6.3.1 Layout Die Seite ist in ein typisches „Banner and Contents“-Frameset eingeteilt: Am oberen Rand befindet sich der Titel der Website. Darunter liegt an der linken Seite ein schmales Menü zur Auswahl der gewünschten Unterseite, welche beim Klick dann im Hauptteil rechts vom Menü erscheint. Von vorneherein war es mir wichtig, dass es eine ‚Entwickler-Website’ wurde, die im Gegensatz zu den meisten anderen Internetseiten ausschließlich auf Information ausgerichtet sein würde. Daher wurde auf überflüssige Animationen verzichtet, sowie auf das Einrichten eines Gästebuches. Zwar werden auch persönliche Einblicke gewährt (→ Home – 33 URL = Uniform Resource Locator (≅ Internetadresse) Meilensteine der Entwicklung), diese befinden sich jedoch „im Kleingedruckten“. Das Layout der Seite ist zwar schlicht, jedoch nicht unmodern: Der Hintergrund ist weiß, die Links in der Menüleiste (links) sind durch farbige Kästchen miteinander verbunden. Die einzige Schriftart, die zur Anwendung kommt, ist Arial, da sie in kleineren Schriftgrößen (8-10) lesbarer ist. Das Design kommt durch die gezielte Verwendung von Tabellen und Schrift-Farben zur Geltung. Der Vorteil dieses Layouts liegt auf der Hand: Es ist übersichtlich, informativ und wird darüber hinaus auch bei einer langsamen Verbindung schnell geladen, da wenig Bilder zum Einsatz kommen. 6.3.2 Inhalt Der Inhalt der Website gliedert sich in folgende Hauptteile: • Startseite (Home) • Statistiken • Downloads (Files) • Informationen rund um alle Komponenten • Kontakt zum Webmaster a) Startseite Auf der Startseite findet der Besucher die Neuigkeiten und den aktuellen Zustand der Projektkomponenten vor. In einem Kasten, der mit „What’s new?“ („Was ist neu?“) überschrieben ist, sind die jüngsten Änderungen an der Website und an dem Projekt aufgelistet. In dem darunter liegenden Kasten („Current project state“) findet der Besucher den „Aktuellen Projektstatus“: In tabellarischer Form ist zu jedem Programm die aktuelle Versionsbezeichnung, das Datum der letzten Herausgabe, sowie das Datum der letzten Änderung aufgeführt. In der letzten Spalte steht der Status eines Programms, zum Beispiel „In Progress“ für eine Komponente, die sich zur Zeit in der Entwicklung befindet. Wiederum darunter befindet sich ein Kasten namens „Meilensteine der Entwicklung“. Dieser hat mit dem eigentlichen Projekt nichts zu tun und umfasst persönliche Eckdaten, die mir während der Entwicklung wichtig waren. b) Statistiken In der Sektion Statistic findet der Besucher statistische Daten über den Arbeitsaufwand vor, der für die Gesamtentwicklung betrieben wurde. Diese zweisprachigen Html-Seiten stellen die Ausgabe dar, die das Programm StatCreator (Kapitel 4.9.2) erzeugt. Eine Zeittafel zeigt, wann an welchem Projektteil gearbeitet wurde. Des Weiteren ist zu jeder Komponente die Anzahl der Arbeitstage und die Größe des Quelltextes in Zeilen und Zeichen angegeben. c) Downloads Eine der wohl für den Besucher interessantesten Unterseiten ist die Files-Sektion. Hier findet er diverse Dateien zum Herunterladen vor. All diese Programme sind dabei Eigenprodukte, Fremdsoftware wird demnach nicht angeboten. Die Dateien sind in zwei Arten unterteilt: „Project files“ und „Tools and extras“. Unter ersterem stehen Projektdateien bereit, allerdings nur jene, die demonstrativen Charakter haben. So können nur der Racer (4.3) und der Worldbuild-Hilfetext (6.2) heruntergeladen werden. Die Editoren stehen deshalb nicht zur Verfügung, da sie nicht für die freie Verwendung („open source“) gedacht sind. Von dem Racer stehen allerdings alle Versionen zur Verfügung, damit die Entwicklung der 3D-Engine betrachtet werden kann. Die zweite Art beinhaltet Werkzeuge, die ich nebenbei programmiert habe, sowie andere Progamme, die ich teilweise in der Schule entwickelt habe. Diese Programme haben keinen direkten Bezug zum Projekt, sind teilweise jedoch sehr nützlich. Der Disc Space Reporter hilft beispielsweise beim Aufräumen der Festplatte: Zu jedem Ordner gibt er den prozentualen Anteil am Gesamtverbrauch des Festplattenspeichers detailliert aus. d) Informationen rund um alle Komponenten Der Hauptteil der Seite wird durch umfangreiche Informationen über die Projektkomponenten geprägt. Zu jeder einzelnen Komponente stehen eine Beschreibung und die bereits erwähnten Logbücher (6.1) zur Verfügung. Bei den größeren Programmen kann der Besucher außerdem auf die Versionslisten zugreifen, die alle großen Änderungen von der ersten bis zur aktuellen Version zusammenfassen. Bei den Kernprogrammen Racer (4.3) und Worldbuild (4.2) ist die Angebotspalette noch wesentlich größer. Zum einen können diverse Screenshots angezeigt werden, die die Entwicklung des Projekts und einige Besonderheiten illustrieren. Zum anderen gibt es bei beiden Komponenten eine Sektion namens „Technische Aspekte“, die neue Funktionen und Problemlösungsstrategien im Detail veranschaulicht. Einige davon werden im Kapitel 5 behandelt. e) Kontakt zum Webmaster Die letzte Sektion (Contact) dient dazu, Kontakt zu mir aufzunehmen, wenn Kritik oder Fragen auftreten sollten. Dafür sind eMail-Adresse und ICQ-Nummer34 angegeben. 6.3.3 Server Bei der Entwicklung der Website, die am 25. Mai 2000 zusammen mit dem ersten Logbucheintrag von Worldbuild begann, kamen diverse Internet-Server35 zum Einsatz, die im Folgenden aufgelistet sind: a) Prohosting (www.prohosting.com) Der erste Server, der zum Einsatz kam, wurde von dem Internetanbieter ProHosting angeboten. Er war kostenfrei, da ein Werbebanner automatisch auf den Html-Seiten angezeigt wurde. Die erste Version der Website, die nur aus einem einzigen Frame bestand und ausschließlich auf die Entwicklung von Worldbuild fokussierte, wurde auf diesen Server geladen. Damals konnte sie über die URL KWWSMRYHSURKRVWLQJFRPaPOWZHLVV anzeigt werden. Auf der beiliegenden CD ist eine Kopie dieser Seite unter dem Ordnernamen ‚ProHosting Website’ verfügbar. Als das Projekt umfangreicher wurde und zusätzliche Komponenten hinzukamen, musste die Seite jedoch in ein Frameset unterteilt werden, um mehr Übersicht zu gewährleisten. Da der Server jedoch einen Werbebanner in jeden Frame plazierte, musste ein neuer Server gewählt werden. Am 15. April 2001 wurde eine neue Website entwickelt, die auf einen Server namens Glaine (siehe b) gespeichert wurde. 34 ICQ ist ein weltweit verbreitetes Chatprogramm. Ein Internet-Server ist im Allgemeinen ein Computer, der an das Internet angeschlossen ist und für die Öffentlichkeit oder ausschließlich autorisierte Personen Dienste anbietet, wie zum Beispiel die Verwaltung von Speicherplatz im Internet (Webspace) zur Anzeige von Internetseiten. 35 Heute dient der ProHosting-Server für die Lagerung der Updatedateien, auf die der Phalanx Updater zugreift (siehe Kapitel 4.7). b) Glaine (www.glaine.net) Den Glaine-Server zu finden, war ein Glücksfall. Dieser Server wird von einer Privatperson betrieben, der eine eigens entwickelte Internet-Programmiersprache publizieren will. Als ich ihn fand, gestattete der Webmaster namens Teebo Jamme unbegrenzten und kostenfreien Webspace ohne Werbebanner: die ideale Voraussetzung für eine Frameset-orientierte Website, die Dateien zum Download anbietet. In naher Zukunft wird die Nutzung von Glaine.net wahrscheinlich kostenpflichtig. Die Seite lief zehn Monate über diesen Server, und war über KWWSZZZJODLQHQHWaSKDODQ[ erreichbar. Leider ließ die Geschwindigkeit und besonders die Zuverlässigkeit zu wünschen übrig. Der Webmaster war nicht erreichbar, der Server oft überlastet und als er mehrere Wochen offline war, suchte ich nach einer neuen kostenfreien Alternative, die keine Werbebanner enthielt. c) T-Online (www.t-online.de) Zuletzt griff ich als Benutzer von T-Online auf den Webspace zu, der jedem Kunden zur Verfügung steht. Die Vorteile: eine schnelle Serververbindung in Kombination mit einer zuverlässigen Stabilität. Die Seite kann direkt unter folgender URL aufgerufen werden: KWWSKRPHWRQOLQHGHKRPHLQGH[KWPO Ein gravierender Nachteil war jedoch die Speicherbegrenzung auf zehn Megabyte. So war es ab einem bestimmten Zeitpunkt nicht mehr möglich, weitere Dateien zum Download anzubieten. Deshalb kam ein vierter Server zum Einsatz. d) Tripod (www.tripod.de) Auf dem von dem deutschen Unternehmen Tripod angebotenen Speicherplatz werden alle zum Download angebotenen Dateien ausgelagert, da dieser genug Kapazität für jeden Kunden anbietet. Für das Anzeigen von Html-Dateien war er jedoch nicht geeignet, da er Werbebanner anzeigte, die das Layout zerstört hätten. 6.3.4 Domain Da die zuletzt und aktuell verwendete URL von T-Online unzumutbar lang ist und sich kaum merken lässt, wurde eine Domain eingerichtet. Das Network Information Center bot unter der Internetadresse ZZZQLFGHYX die Einrichtung einer kostenfreien Domain mit der Endung „.de.vu“ an. So richtete ich die Domain ZZZSKDODQ[VRIWZDUHGHYX ein, die bereits oben erwähnt wurde. Gibt der Internet-User diese URL ein, wird automatisch zu der oben genannten T-Online-Adresse weitergeleitet. Dafür erscheint beim Verlassen der Seite ein kleines Werbefenster. 6.4 Veröffentlichungen bei Flipcode Am 8. Juli 2001 beschloss ich, das Projekt im Internet zu veröffentlichen. Ich wollte mit dem Projekt etwas mehr in die Öffentlichkeit rücken und es von anderen Entwicklern beurteilen lassen. Nicht zuletzt suchte ich nach weiteren Mitarbeitern für das Projekt. Die Internetseite Flipcode (ZZZIOLSFRGHFRP) bot sich ideal dafür an. Flipcode ist eine englischsprachige Seite für Spieleentwickler, die täglich aktualisiert wird und einen breiten, internationalen Besucherkreis besitzt. Für die Sektion „Image of the day“ kann jeder Entwickler ein Bild und eine entsprechende Beschreibung einsenden. Nach einer Wartezeit von etwa einer Woche sind Bild und Text online und können von allen Besuchern der Seite in Form von Kommentaren bewertet werden. Die meisten Kommentare kommen von Entwicklern, die schon länger in der Branche tätig sind. Daher ist oft nur von konstruktiver Kritik die Rede. Insgesamt publizierte ich meine Arbeit zweimal, am 18. Juli 2001 und am 10. Januar 2002. Jedesmal sendete ich ein viergeteiltes Bild mit, das die Hauptkomponenten Worldbuild und Racer veranschaulichte. Da der Text in Englisch verfasst werden musste und ein bestimmtes Maß nicht überschreiten sollte, ohne wichtige Aspekte auszulassen, nahmen die Veröffentlichungen viel Zeit in Anspruch. Aber es hatte sich gelohnt. Mit meinem Projekt stieß ich überwiegend auf positive Resonanz und erhielt viele eMails, darunter auch Ausbildungsangebote und Anfragen zur Teilnahme an anderen Projekten. Unter den folgenden Internetadressen sind die beiden Veröffentlichungen verfügbar: 18. Juli 2001: KWWSZZZIOLSFRGHFRPFJLELQPVJFJL"VKRZ7KUHDG IRUXP LRWGLG 10. Januar 2002: KWWSZZZIOLSFRGHFRPFJLELQPVJFJL"VKRZ7KUHDG IRUXP LRWGLG Des Weiteren finden Sie im Anhang B und C die Originaltexte auf Englisch, mit denen ich das Projekt zu den jeweiligen Zeitpunkten charakterisierte. Anhang A CD-Inhalt Die beigefügte CD beinhaltet diverse Ordner rund um das Projekt, die im Folgenden aufgelistet sind: Ordner Inhalt Binary Enthält die ausführbaren Dateien der Projektkomponenten. Source Beinhaltet den kompletten Quelltext aller Komponenten. Doc Die aktuelle Version der Homepage ist hier gespeichert, inklusive aller Logbücher. ProHosting Website Hier ist die Website gespeichert, die bis zum 15. April 2001 online war (siehe 6.3.3a). Interpolation Das gleichnamige Programm in diesem Ordner demonstriert den Unterschied zwischen linearer und kubischer Interpolation (siehe 5.3). Anhang B Flipcode-Veröffentlichung am 18. Juli 2001 In diesem Anhang finden Sie den englischen Originaltext, der für die Veröffentlichung bei Flipcode (www.flipcode.com) am 18. Juli 2001 zur Charakterisierung des Projekts diente. Die Online-Veröffentlichung kann unter der folgenden URL aufgerufen werden: KWWSZZZIOLSFRGHFRPFJLELQPVJFJL"VKRZ7KUHDG IRUXP LRWGLG “I'm a seventeen year old student from Germany approaching the 12th grade. In March 2000 I decided to design a real 3D computer game after I have had spent the time before writing smaller games and database applications to earn some money. Fortunatly my school gave me the opportunity to get a higher score in my A-levels next year if I present and illustrate my project in an oral test. That's given me another motivation to work harder:-) Because of my philosophy not to use "foreign" programs I have attempted to do everything myself. And it has worked until now ... In March 2000 I started to write a 3D level editor called 'Worldbuild' (image at top left). I have worked with several editors before so I followed the aim to create an easy-to-use but highly effective editor. The work space is seperated into three 2D views and one 3D view. The user inserts primitives into the 2D view. These can be modified by cutting, moving, rotation etc. Additional objects like lights, lens-flares and sprites can also be created here. All these elements are directly rendered in the 3D view. It is responsible for all texturing operations aswell. The most important actions can be done by mouse and/or a few keys. It needed nine months to get the editor into the beta stage. After that I began to implement special functions like a landscape-generator, a texture interpolation function (to correct smaller texturing errors) etc. The program interacts with DirectX. The 2D view uses the DirectDraw components of DirectX7, the 3D view renders using DirectGraphics of DirectX8. In November 2000 I decided to create my own texture format to be independant from standard formats. So I developed the ’Texture container’. It is used to create a container of imported bitmaps. A special function of this program is the possibilty to do image animation. A compiler (image top right) is integrated into Worldbuild which converts the maps into a format which is faster to read. Moreover it pre-creates an OcTree structure which is used by the game. Some other programs followed like a file container (to group and contain files) and an updater program called ’Phalanx Updater’ which can be used to download the newest components of the project. This was designed and created for the beta-testers. On June 22nd I started with the core game: the ’Racer’ (I’m sorry about that stupid name. I’m still searching for a better one.) It is (or will be) an action racing game: you will fly races with small futuristic space-ships, armed with several weapons you can shoot your enemys. I think, I’ll write this part of the game in Autumn :-) At the moment I’m writing my own 3D engine. The first screenshots are shown in the image above (images bottom left and right). The progress of my project can be "viewed" on the homepage: www.glaine.net/~phalanx. I’m sorry that it’s mostly in German. But the screenshot and statistic sections might be of some interest for you:-) I’m still searching for a better name for my game ... I welcome every idea ;-) Malte Weiß“ Anhang C Flipcode-Veröffentlichung am 10. Januar 2002 Dieser Anhang beinhaltet den englischen Originaltext der Flipcode-Veröffentlichung vom 10. Januar 2002, der den damaligen Zustand charakterisierte. Die Internet-Veröffentlichung finden Sie unter der folgenden URL: KWWSZZZIOLSFRGHFRPFJLELQPVJFJL"VKRZ7KUHDG IRUXP LRWGLG “On July 18th I made my first publication of my project called 'Racer' (okay, I haven't found a good name, yet:-) In March 2000 I decided to write a real 3d computer game, which has grown to a large project. The most important program of the project is Worldbuild (images top left and bottom right), the 3d world editor. The workspace is separated into four views: three 2d views (front, top, side) and one 3d view. The 2d views are used for the basic operations: The user inserts primitives and modifies them by moving, rotating, cutting etc. (some of these can be made in 3d as well). Objects like lights and lens flares can be created here, too. The 3d view renders the scene. All lights and effects (e.g. mirrors) can be directly rendered in real-time. The user can 'fly' through his level with a few mouse movements. Several additional view modes are available for better performance: Solid / wire frame mode, depth complexity mode (to find hidden superfluous polygons) and geo mode (to view the scene geometry). Almost all texturing operations are done in 3d as well: Complex environments can be quickly texturized by using the mouse and some helper functions (align texture, interpolate etc.). An interface window supports setting up events (e.g. for opening doors), skyboxes (3d backgrounds) and fogs, which can be previewed in 3d, too. An internal compiler converts the map file into a more effective format, which contains pre-calculated oc-tree nodes. Moreover maps can be compiled as models. During the development I tried to make Worldbuild as user-friendly as possible. All actions can be done by using the mouse or with a few keys. Since the start of the project I've worked 22 months on this program, not just because three totally different DirectX versions have been published during that time. Currently Worldbuild runs with DirectX8.1. The 'Racer' (images top right and bottom left) is the actual game, which I started on June 22nd. In future it will be a racing game where you can shoot your opponents. It currently just loads the compiled maps of Worldbuild and renders them. Within five months I had to write a complete new engine because Worldbuild doesn't support effective object culling. Racer uses a combination of an oc-tree and a beam-tree technique. The engine is written on a high abstract level (using OOP), which can administrate more games and event systems at once. I’ve just finished the main part of it in order to start with the real game elements. The following features are currently supported: • Lens flares (flares are only visible if obstacles don’t block the ’flare ray’). • Real mirrors. • Models (created with Worldbuild), which can possess their own lens flares, mirrors and lights, which influence the environment. • Skyboxes (3d backgrounds), which can be changed in real-time. • Fog, which can be changed in real-time as well. • Animated textures. • Different alpha blending states. • Complex event system using external classes (e.g. "CMoveObject", "CCameraFly" etc.). The light system - I’m sorry :) - isn’t based on light maps but on vertex lighting. So I’m not able to render these cool shadows of the famous games, but I can do nice light animations. In order to get freedom over the texture handling I developed my own texture format. A special program (Texture container) allows me to handle texture lists, which can be animated. Additionally alpha channels (for transparency) can be added easily. Another goodie of the project is an online updater (Phalanx Updater), which was designed to contribute the newest versions of the programs to (fictional:-) team members. Yes, originally the game development was planned in a group of friends, which promised me heaven but did nothing ;-) I learned the hard way that people never really work without being paid :-( The current Racer demo can be downloaded from my homepage (www.phalanxsoftware.de.vu) in the Files-Section. It’s just a graphic demo, I’m gonna start with the controls in January. I’m sorry but the dominating part of the website is in German :-) I’m now 18 years old and doing my A-Levels in May. I got the opportunity by school to get a higher mark if I would present my project. However, please don’t think that I spend about two years of my life dedicated to schoolwork!!! No, the idea for that game is a dream of my early youth :-) Malte Weiß” Quellenverzeichnis Literatur • Daniel Mühlbacher, Peter J. Dobrovka, Jörg Brauer: Computerspiele – Design und Programmierung, MITP-Verlag GmbH, Bonn 2000 Ein detailliertes Werk zur Planung, Organisation und Durchführung eines Spieleprojekts • The Waite Group. Michael Radtke, Chris Lampton: 3D Programmierung mit C++, SAMS, München 1996 Programmierung von 3D-Programmen in C unter DOS. Zielsetzung ist die Entwicklung eines eigenen Flugsimulators • Singh, S.: Geheime Botschaften, Carl Hanser Verlag, München Wien 2000 Datenverschlüsselung von früher bis heute • Prof. Dr. Ulrich Breymann: C++ - Eine Einführung, Carl Hanser Verlag, München Wien 19942 Eine umfassende, sehr theoretisch orientierte Ausführung zur Programmiersprache C, insbesondere zur objektorientierten Programmierung Online-Hilfe • MSDN Library Visual Studio 6.0 Release Hilfetext zu den Komponenten des Visual Studio (u.a. Visual C) • Microsoft DirectX 8.1 Hilfetext Hilfetext zur Ansteuerung der DirectX-Schnittstelle Internet • http://www.gamedev.net - „All your game development needs” Umfangreiche Seite zum Design und zur Programmierung von Spielen • http://www.flipcode.com - „Daily Game Development News & Resources” Täglich aktualisierte Informationen zur Spieleentwicklung