Das Phalanx-Projekt - Racer 3Development Website

Werbung
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*UR‰H End;
strcpy(name,
strcat(name,
$OH[DQGHU GHU*UR‰H 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*UR‰H 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
Herunterladen