Sie sind auf Seite 1von 354

Liebe Leserin, lieber Leser,

welche der folgenden Aussagen trifft am ehesten auf Sie zu? Ich kann C++. a) Nein, aber klingt interessant. (0 Punkte) b) Ja, ein bisschen. (5 Punkte) c) Klar! (10 Punkte) d) Nein, aber ich kann C#. (100 Punkte) Ich programmiere, a) weil man sich damit nette Sachen basteln kann. (5 Punkte) b) also bin ich. (10 Punkte) c) weil ich Geld verdienen muss /die Studienordnung mich dazu zwingt. (0 Punkte) Ich habe Humor. a) Ja. (10 Punkte) b) Nein. (0 Punkte) c) Vielleicht. (5 Punkte) Wenn Sie in diesem Test weniger als 15 Punkte erzielt haben, rate ich Ihnen vom Kauf dieses Buchs dringend ab. Sie wrden keine Freude daran haben. Alle anderen sind herzlich zum Entwickeln und Mitdiskutieren eingeladen! Programmieren Sie Eliza, Nachfahrt, Bermuda und Co. nach und fachsimpeln Sie mit dem Autor und anderen Lesern im Forum (http://www.galileocomputing.de/forum/gp/forumID-241) darber, wie man die Programme noch weiterentwickeln knnte. Ich hoffe, dass Ihnen dieses Buch beim Lesen genauso viel Freude bereiten wird wie uns beim Produzieren. Sollten Sie dennoch einmal Kritik oder Anregungen vorbringen mchten, schreiben Sie mir bitte eine E-Mail. Viel Spa mit Arnold Willemer und C++ wnscht Ihnen nun

Ihre Christine Siedle

Lektorat Galileo Computing

christine.siedle@galileo-press.de www.galileocomputing.de Galileo Press Rheinwerkallee 4 53227 Bonn

Auf einen Blick


1 2 3 4 5 6 7 8 9 C++ Das Portrt ..................................................................... Der Computer im Dialog .......................................................... Pfadfinder ................................................................................ Spiele mit der Physik ............................................................... Regionales C++ ........................................................................ 13 21 45 63 87

Labyrinth .................................................................................. 107 Sprunghaft ............................................................................... 137 Esoterische Software ............................................................... 151 Die dritte Dimension ............................................................... 167

10 Shorts im Nebel ....................................................................... 207 11 Achtung Baustelle: Einsturzgefahr! ......................................... 241 12 Der Frosch und das Eichhrnchen ............................................ 255 13 Musik ist mit Wellen verbunden ............................................. 277 A B C D E KDevelop ................................................................................. 311 Bloodshed Dev C++ .................................................................. 315 Installation von wxWidgets .................................................... 319 Installation PortAudio ............................................................. 327 Computer-Oldies ..................................................................... 331

Der Name Galileo Press geht auf den italienischen Mathematiker und Philosophen Galileo Galilei (15641642) zurck. Er gilt als Grndungsgur der neuzeitlichen Wissenschaft und wurde berhmt als Verfechter des modernen, heliozentrischen Weltbilds. Legendr ist sein Ausspruch Eppur se muove (Und sie bewegt sich doch). Das Emblem von Galileo Press ist der Jupiter, umkreist von den vier Galileischen Monden. Galilei entdeckte die nach ihm benannten Monde 1610. Lektorat Christine Siedle Gutachten Marcus Bckmann Korrektorat Barbara Decker Typograe und Layout Vera Brauner Herstellung Stef Ehrentraut Satz Arnold Willemer Einbandgestaltung Barbara Thoben, Kln Titelbild Fotolia / Danomyte Druck und Bindung Bercker Graphischer Betrieb, Kevelaer Dieses Buch wurde gesetzt aus der Linotype Syntax Serif (9,25/13,25 pt) in LaTeX. Gerne stehen wir Ihnen mit Rat und Tat zur Seite: christine.siedle@galileo-press.de bei Fragen und Anmerkungen zum Inhalt des Buches service@galileo-press.de fr versandkostenfreie Bestellungen und Reklamationen britte.behrens@galileo-press.de fr Rezensions- und Schulungsexemplare

Bibliograsche Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliograe; detaillierte bibliograsche Daten sind im Internet ber http://dnb.d-nb.de abrufbar. ISBN 978-3-8362-1512-1

Galileo Press, Bonn 2010 1. Auage 2010


Das vorliegende Werk ist in all seinen Teilen urheberrechtlich geschtzt. Alle Rechte vorbehalten, insbesondere das Recht der bersetzung, des Vortrags, der Reproduktion, der Vervielfltigung auf fotomechanischem oder anderen Wegen und der Speicherung in elektronischen Medien. Ungeachtet der Sorgfalt, die auf die Erstellung von Text, Abbildungen und Programmen verwendet wurde, knnen weder Verlag noch Autor, Herausgeber oder bersetzer fr mgliche Fehler und deren Folgen eine juristische Verantwortung oder irgendeine Haftung bernehmen. Die in diesem Werk wiedergegebenen Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. knnen auch ohne besondere Kennzeichnung Marken sein und als solche den gesetzlichen Bestimmungen unterliegen.

Inhaltsverzeichnis
Geleitwort des Fachgutachters ............................................................................... 11

C++ Das Portrt ........................................................................ 13


1.1 1.2 1.3 Buchkonzept ........................................................................................... Blick zurck ............................................................................................. Spa mit C++ .......................................................................................... 13 14 18

Der Computer im Dialog ........................................................... 21


2.1 Eliza .......................................................................................................... 2.1.1 Unsere kleine Eliza .................................................................. 2.1.2 Die Datenmodellierung ......................................................... 2.1.3 Der Programmablauf .............................................................. 2.1.4 Ein Sitzungsprotokoll ............................................................. 2.1.5 Was denn noch? ...................................................................... Tiere raten ............................................................................................... 2.2.1 Die Spielidee ............................................................................ 2.2.2 Datenstruktur Binrbaum ...................................................... 2.2.3 Eine Rekursion muss her ....................................................... 2.2.4 Ein Spielprotokoll .................................................................... 2.2.5 Was denn noch? ...................................................................... 21 24 24 28 34 35 36 36 37 38 42 43

2.2

Pfadnder ..................................................................................... 45
3.1 3.2 Das Programm sucht die krzeste Verbindung ................................ Datenmodellierung ............................................................................... 3.2.1 Knoten ...................................................................................... 3.2.2 Kanten ...................................................................................... 3.2.3 Adjazenzmatrix ........................................................................ Einlesen der Graphen ............................................................................ Das Hauptprogramm ............................................................................. Betrachtungen ber die Straendaten ............................................... Navigationssysteme ............................................................................... Was denn noch? .................................................................................... 47 53 53 54 54 57 58 59 61 61

3.3 3.4 3.5 3.6 3.7

Inhaltsverzeichnis

Spiele mit der Physik ................................................................. 63


4.1 Die Mondlandung ................................................................................. 4.1.1 Ein wenig Physik ..................................................................... 4.1.2 Die Tabellenkalkulation ......................................................... 4.1.3 Das Ganze in C++ .................................................................... 4.1.4 Die Raumfhre ......................................................................... 4.1.5 Das Hauptprogramm .............................................................. 4.1.6 Was denn noch? ...................................................................... Der Kaugummi und der schiefe Wurf ................................................ 4.2.1 Die Formel ............................................................................... 4.2.2 Hilfe von der Tabellenkalkulation ........................................ 4.2.3 Spielfeld aufbauen .................................................................. 4.2.4 Spielablauf ................................................................................ 4.2.5 Was denn noch? ...................................................................... 64 64 66 69 70 72 73 74 75 76 78 81 85

4.2

Regionales C++ ............................................................................ 87


5.1 Hessisch? ................................................................................................. 5.1.1 Ei, wie sprischt mer des? ....................................................... 5.1.2 Die hessische Tabelle .............................................................. 5.1.3 Was hinten herauskommt ..................................................... Mer programmiere in Hessisch ........................................................... 5.2.1 Tabellengesteuert .................................................................... 5.2.2 Erzeugen der Header-Datei ................................................... Mer lese Listings .................................................................................... 5.3.1 Zugriff auf die bersetzungstabelle ..................................... 5.3.2 Durchlesen des Quelltextes .................................................. 5.3.3 String gegen Zeiger ................................................................. 89 90 90 91 92 93 95 96 97 97 99

5.2

5.3

Labyrinth ....................................................................................... 107


6.1 Labyrinthmodell ..................................................................................... 6.1.1 Vorgehensweise ...................................................................... 6.1.2 Datenstrukturen ...................................................................... 6.1.3 Zuflliger Richtungswechsel .................................................. 6.1.4 Selbstentznder ....................................................................... 6.1.5 Ausgang erzeugen ................................................................... 6.1.6 Ausgabe auf dem Terminal .................................................... 6.1.7 Aufrufparameter des Hauptprogramms .............................. 107 108 110 114 115 119 120 122

Inhaltsverzeichnis

6.2

6.3

Wandelgang ............................................................................................ 6.2.1 Wnde im Raum ..................................................................... 6.2.2 Bewegung durch das Labyrinth ............................................ 6.2.3 Mitmachaktion: eine Multifunktionsroutine ..................... 6.2.4 ASCII-Panel .............................................................................. Was denn noch? ....................................................................................

123 124 127 129 131 135

Sprunghaft .................................................................................... 137


7.1 7.2 7.3 Grasche Oberchen .......................................................................... Das Superprogramm .............................................................................. Ereignisorientiert ................................................................................... 7.3.1 Der Grundrahmen ................................................................... 7.3.2 Fensterelementebau ............................................................... 7.3.3 Ereignisbehandlung ................................................................ Was denn noch? .................................................................................... 137 139 140 140 143 145 149

7.4

Esoterische Software ................................................................. 151


8.1 Strahlungen und Energien .................................................................... 8.1.1 Messen der Good Vibrations ............................................ 8.1.2 Sonnenstrahlen sind nicht blau ............................................ 8.1.3 Schwingungen mechanisch messen ..................................... Programmieren ....................................................................................... 8.2.1 Fensterklassen ......................................................................... 8.2.2 Ereignisse .................................................................................. 8.2.3 Ellipsen ..................................................................................... 8.2.4 Menspielereien ..................................................................... Was denn noch? .................................................................................... 152 152 153 154 154 155 156 158 162 163

8.2

8.3

Die dritte Dimension ................................................................. 167


9.1 Fluglandung ............................................................................................ 9.1.1 Faszination Fliegen ................................................................. 9.1.2 Flugsimulator-Computer contra Jackintosh ........................ 9.1.3 Apple-Fluglandung ................................................................. 9.1.4 Ein Blick ins Listing ................................................................. 9.1.5 Ein paar strende Fakten ....................................................... 9.1.6 Simulation des Fluges ............................................................ 9.1.7 Die Ansicht der Landebahn .................................................. 9.1.8 Die grasche Oberche ....................................................... 167 167 168 170 170 171 172 174 177

Inhaltsverzeichnis

9.2

9.1.9 Besinnung ................................................................................. 9.1.10 Eine Portierung nach Win32 ................................................. 9.1.11 Was denn noch? ...................................................................... Nightdriver .............................................................................................. 9.2.1 Modellierung des Straenverlaufs ....................................... 9.2.2 Grasche Umsetzung .............................................................. 9.2.3 Die Anwendungsklasse .......................................................... 9.2.4 Was denn noch? ......................................................................

182 183 190 191 192 193 200 204

10 Shorts im Nebel ........................................................................... 207


10.1 Spielregeln .............................................................................................. 10.2 Datenmodellierung ............................................................................... 10.2.1 Ein Schiff ................................................................................... 10.2.2 Das Wasser ............................................................................... 10.3 Die Spielimplementierung ................................................................... 10.4 Konsolenversion ..................................................................................... 10.5 Etwas Komfort ........................................................................................ 10.6 Nun alles im Fenster ............................................................................. 10.7 Grak kann mehr ................................................................................... 10.7.1 Grundaufbau des Feldes ........................................................ 10.7.2 Die Diagonalen als Knobelspiel ............................................ 10.7.3 Przise Maussteuerung .......................................................... 10.7.4 Men mit Haken ..................................................................... 10.7.5 Image ndern ........................................................................... 10.7.6 Spielergebnisse zeigen ........................................................... 10.7.7 Die Peilung rotiert .................................................................. 10.7.8 Flackernden Neuaufbau unterbinden ................................. 10.8 Portabilitt .............................................................................................. 10.9 Was denn noch? .................................................................................... 208 208 209 209 210 214 216 217 223 224 224 227 230 231 232 234 236 237 239

11 Achtung Baustelle: Einsturzgefahr! ....................................... 241


11.1 11.2 11.3 11.4 Unsauberes Konzept ............................................................................. Chaotische Implementierung .............................................................. Auen hui ............................................................................................... Was denn noch? .................................................................................... 243 244 249 254

Inhaltsverzeichnis

12 Der Frosch und das Eichhrnchen .......................................... 255


12.1 Rahmenprogramm ................................................................................. 12.2 Diashow .................................................................................................. 12.2.1 Hintergrundwechsler .............................................................. 12.3 Es bewegt sich etwas ............................................................................ 12.4 Aufstellung der Figuren ........................................................................ 12.5 Die Zeit setzt in Bewegung .................................................................. 12.6 Malerei .................................................................................................... 12.7 Angetastet ............................................................................................... 12.8 Spielende ................................................................................................. 12.9 Was denn noch? .................................................................................... 256 261 261 263 266 268 270 273 274 275

13 Musik ist mit Wellen verbunden ............................................ 277


13.1 Musik digitalisieren ............................................................................... 13.2 Musik und Gerusche abspielen ......................................................... 13.3 Synthesizer .............................................................................................. 13.3.1 Das Sgezahndrama ................................................................ 13.3.2 Gemeinheiten des Soundsystems ........................................ 13.3.3 Die weiche Welle .................................................................... 13.4 Achtung Aufnahme! .............................................................................. 13.5 Stimmungskanone ................................................................................. 13.5.1 Die Daten ................................................................................. 13.5.2 Das Hauptprogramm .............................................................. 13.5.3 Was denn noch? ...................................................................... 13.6 Der gute Ruf des Spielers ..................................................................... 13.6.1 Akustischer Hau-den-Lukas .................................................. 13.6.2 Die Umsetzung ........................................................................ 13.6.3 Was denn noch? ...................................................................... 279 280 283 284 288 289 290 296 297 298 300 302 303 303 306

Anhang .................................................................................................. 309


A KDevelop ........................................................................................................... A.1 Neues Projekt ......................................................................................... A.2 Kompilieren und starten ....................................................................... A.3 Weitere Mglichkeiten ......................................................................... Bloodshed Dev C++ .......................................................................................... B.1 Installation .............................................................................................. B.2 Ein Projekt anlegen ............................................................................... B.3 bersetzen und starten ........................................................................ 311 312 313 314 315 315 316 317

Inhaltsverzeichnis

Installation von wxWidgets ............................................................................ C.1 Installation unter KDevelop ................................................................. C.2 Installation unter Bloodshed Dev C++ ............................................... C.2.1 Buch-CD ................................................................................... C.2.2 Internetinstallation ................................................................. C.2.3 Erzeugen einer Beispielapplikation ...................................... C.2.4 Buchprojekte ............................................................................ C.3 Installation unter Visual Studio C++ .................................................. C.4 Installation unter Macintosh ............................................................... Installation PortAudio ...................................................................................... D.1 Unter Linux ............................................................................................. D.2 Unter Windows ...................................................................................... D.2.1 Compileraufruf ......................................................................... D.2.2 Selbstbau der Bibliothek ........................................................ D.3 Lizenz ....................................................................................................... Computer-Oldies .............................................................................................. E.1 Apple ][ .................................................................................................... E.2 Atari 400/800 ......................................................................................... E.3 Sinclair ZX80 ........................................................................................... E.4 Commodore C-64 .................................................................................. E.5 Schneider/Amstrad CPC ....................................................................... E.6 Epson HX-20 .......................................................................................... E.7 IBM PC .................................................................................................... E.8 Apple Macintosh ................................................................................... E.9 Atari ST .................................................................................................... E.10 Commodore Amiga ...............................................................................

319 319 320 320 320 321 322 322 324 327 327 327 328 329 330 331 331 335 337 338 340 342 344 346 348 350

Index ............................................................................................................................ 353

10

Geleitwort des Fachgutachters


Bekanntlich beginnt jedes C++-Buch so:
#include <iostream> using namespace std; int main() { cout << "Hallo Welt!" << endl; return 0; }

Wie langweilig! Meist bleibt nach der Lektre eines Buchs ber C++-Programmierung eine gewisse Leere zurck. Man kennt zwar nun Schleifen, Verzweigungen, Klassen, Funktionen usw., aber so richtig Spa hat das eigentlich nicht gemacht. Es ist ja schn und gut, Listen zu sortieren u. ., aber wer ernsthaft daran Gefallen ndet, der geht zum Lachen wahrscheinlich auch in den Keller. Mit anderen Worten: Wir Programmierer brauchen auer der lstigen Picht auch mal ein paar Spielsachen. Wenn ich an meine zahlreichen Jahre im Bereich der Softwareentwicklung zurckdenke, ist jedenfalls eins auffllig: Jeder meiner Kollegen hatte eigentlich immer noch irgendein Nebenprojekt. Klar, am Tag programmiert man am Firmenprojekt, aber dann am Abend oder Wochenende, da wurden die Projekte schon wesentlich spielerischer. Mal hackte man sich einen speziellen Portscanner zusammen oder entwickelte ein Steuerungsprogramm fr einen Roboter (der dann die Kaffeemaschine bedienen konnte) oder programmierte eine Flugsimulation fr eine Mondlandefhre. Das war dann wirklich programmieren. Wenn Sie auch vom Programmiereber angesteckt sind, dann wissen Sie sicher, was ich meine. Jemand hat mal gesagt: Programmierer sind wie Katzen. Das bezog sich weniger auf die Haare, als vielmehr auf die Eigenschaften verspielt und eigensinnig. Mit dem eigensinnig kann ich Ihnen nicht wirklich helfen, das mssen Sie schon selbst auf die Reihe bekommen. Aber zum Thema verspielt halten Sie gerade das richtige Buch in der Hand. Dem Autor gelingt es wirklich, uns am Ende jedes Kapitels ein Programm zu zeigen, das all die C++-Schmankerl zu etwas wirklich Unterhaltsamen zusammenfhrt, und man fast automatisch nach der ersten Version aus dem Buch an einer zweiten, eigenen baut. Und an einer noch besseren dritten, und dann geht da doch noch was... Mir hat es wirklich Spa gemacht, mir die vorgestellten Programme anzusehen und eigene Ideen und Modikationen einzubauen. An dem

11

Geleitwort des Fachgutachters

Mondlander aus dem Buch habe ich ganz unbeabsichtigt einen halben Tag versenkt. (Nur noch fnf Minuten, Schatz!) Apropos Mondlander: Wenn Sie bereits ber das eine oder andere Jahr an Erfahrung verfgen, dann werden Sie ein paar der hier vorgestellten Spieleideen sogar noch kennen. Einige dieser Programme waren vor 25 Jahren echter Kult sozusagen das Counterstrike der 80er. Zum Glck haben wir hier einen Autor, der das selbst noch mit der Muttermilch aufgesogen hat und nebenbei 25 Jahre Softwaregeschichte vorstellt. Probieren Sie mal den Flugsimulator aus! Und dem Labyrinth fehlt nur noch ein Pacman. Falls Sie jetzt vielleicht befrchten, dass Sie hier mit technisch veralteten Dingen konfrontiert werden, kann ich Entwarnung geben. Sie werden wxWidgets (eine plattformunabhngige GUI-Bibliothek) erproben, dem MVC-Modell begegnen, fr Linux und/oder Windows programmieren, mit Timern und Audiodaten hantieren u. v. m. Mangelnder Tiefgang? Das wird nicht Ihr Problem sein. Jetzt wnsche ich Ihnen aber viel Spa und Vergngen beim Lesen, Ausprobieren und Erweitern der vorgestellten Programme! Marcus Bckmann (Marc++us) Betreiber von c++.de www.c-plusplus.de

12

Programmieren macht Spa.

C++ Das Portrt

Sie programmieren in C++. Dann sind Sie hier richtig. Sollten Sie diese Sprache dagegen noch nicht beherrschen, will ich nicht ausschlieen, dass Sie dieses Buch an der einen oder anderen Stelle etwas unverstndlich nden werden. Aber da gibt es Abhilfe. Im gleichen Verlag nden Sie ein gutes Buch namens Einstieg in C++. Die Namensgleichheit des Autors ist nicht einmal zufllig. Vielleicht lesen Sie zunchst jenes Buch und dann dieses. Die Anfnger drften nun das Buch zugeklappt haben und erst in ein paar Tagen oder Wochen wieder zu uns stoen, nachdem sie C++ gelernt haben. Sie als Kenner der Sprache C++ werden in diesem Kapitel die Gelegenheit haben, etwas ber die Geschichte und den Charakter von C++ zu erfahren. Sie werden also all das erfahren, was Sie noch nie hren wollten oder lngst schon wissen. Daneben werden Sie lesen, wie dieses Buch konzipiert ist. Falls Sie das Buch aber bereits gekauft und bezahlt haben oder wenn Sie lieber berrascht werden wollen, schlage ich vor, Sie berblttern dieses Kapitel und sparen damit Zeit. Sptestens jetzt msste ich allein sein und sollte mir die Frage stellen, warum ich dieses Kapitel geschrieben habe, wenn es doch keiner liest.

1.1

Buchkonzept

Programmieren ist eine besondere Form des Knobelns. Wer programmiert, lst tglich Denksportaufgaben. Der eine oder andere hat diese Knobelei zu seinem Beruf gemacht. Aber die typischen Aufgaben im Beruf sind meist eher technischer Natur. Und so wie die nichtprogrammierenden Artgenossen das Sudoku in der Tageszeitung lsen, hat der Programmierer hin und wieder Lust, etwas zu programmieren, was einfach nur Spa macht. In diesem Buch sind Themen aufgegriffen worden, die sich fr solche Knobeleien eignen. Die Kapitel bauen nicht aufeinander auf, sondern knnen weitgehend in beliebiger Reihenfolge gelesen werden. Sie sind nicht vllig ohne Sinn und Verstand in dieser Reihenfolge angeordnet worden, aber beinahe.

13

C++ Das Portrt

In jedem Kapitel wird zunchst in das Thema eingefhrt. Dann wird ein Lsungsansatz vorgestellt und der Quelltext beschrieben. Schlielich werden Ihnen ein paar Ideen aufgezeigt, wie Sie die Programme erweitern knnen. Ich kann mir vorstellen, dass Sie selbst eigene Ideen haben. Ich verstehe dieses Buch durchaus als Mitmachbuch. Warum sollte ich den Spa auch allein gehabt haben. Der Verlag stellt freundlicherweise zu diesem Buch ein Forum bereit: http://www.galileocomputing.de/forum/gp/forumID-241. Stellen Sie dort Ihre Lsungen vor. Es wre nett, von Ihnen zu hren. Die vorgestellten Programme sind also nicht komplett und fertig, sondern eher Appetithappen. Sie erheben auch nicht den Anspruch, vorbildliches C++ zu predigen. Und darum werden Sie vielleicht an der einen oder anderen Stelle bemngeln, dass die Fehlerbehandlung fehlt, Klassen mit ffentlichen Membervariablen deniert sind und andere Verbrechen gegen die Ideologie der sauberen Programmentwicklung begangen wurden. Der Grund dafr ist, dass die Programme mglichst kurz und bersichtlich sein sollten. Sie sollen Spa machen, nicht Vorbild sein.

1.2

Blick zurck

Wer die Geschichte der Programmiersprache C++ betrachten mchte, sollte ein Fernglas verwenden, schlielich geht es weit zurck in die Vorzeit. Denn damals, kurz vor der Erndung der Rasierklinge, entwickelten Brian Kernighan und Dennis Ritchie die Programmiersprache C, um das Betriebssystem UNIX nicht in Assembler schreiben zu mssen. Eine unbekannte Quelle (Bernhard L. Hayes, NetNews-Gruppe) schrieb in einem Artikel unter dem Titel Ernder von UNIX und C geben zu: alles Quatsch!, dass UNIX und C nur ein Aprilscherz gewesen sei. In einer Ankndigung, die die Computerindustrie verblffte, haben Ken Thompson, Dennis Ritchie und Brian Kernigham zugegeben, dass das Betriebssystem Unix und die Programmiersprache C ein rafnierter Aprilscherz sind, der sich ber 20 Jahre am Leben erhalten hat. Bei einem Vortrag vor dem letzten UnixWorld-Software-Entwicklungsforum enthllte Thompson: 1969 hatte AT&T gerade die Arbeit am GE/Honeywell/AT&T-Multics-Projekt beendet. Brian und ich experimentierten zu dem Zeitpunkt mit einer frhen Pascal-Version von Professor Niklaus Wirth vom ETH-Laboratorium in der Schweiz und waren beeindruckt von seiner Einfachheit und Mchtigkeit. Dennis hatte Der Herr der

14

Blick zurck

1.2

Klinge gelesen, eine spttische Parodie auf Tolkiens groe Triologie Der Herr der Ringe. Im bermut beschlossen wir, Parodien zur Multics-Umgebung und zu Pascal zu verfassen. Dennis und ich waren fr die Betriebssystemumgebung verantwortlich. Wir sahen uns Multics an und entwarfen ein neues System, das so komplex und kryptisch wie mglich sein sollte, um die Frustration der gelegentlichen Nutzer zu maximieren. Wir nannten es Unix, in Anspielung auf Multics, und fanden es auch nicht gewagter als andere Verballhornungen. Danach entwickelten Dennis und Brian eine wirklich perverse Pascal-Version namens A. Als wir bemerkten, dass einige Leute tatschlich versuchten, in A zu programmieren, fgten wir schnell einige zustzliche Fallstricke hinzu und nannten es B, BCPL und schlielich C. Wir hrten damit auf, als wir eine saubere bersetzung der folgenden Konstruktion erhielten:
for(;P("\n"),R--;P("!"))for(e=C;e--;P("_"+(*u++/8)%2))

Der Gedanke, dass moderne Programmierer eine Sprache benutzen wrden, die solch eine Anweisung zulie, lag jenseits unseres Vorstellungsvermgen. Wir dachten allerdings daran, alles den Sowjets zu verkaufen, um ihren Computerfortschritt 20 Jahre und mehr zu behindern. Unsere berraschung war gro, als dann AT&T und andere US-Unternehmen tatschlich begannen, Unix und C zu verwenden! Sie haben 20 weitere Jahre gebraucht, gengend Erfahrungen zu sammeln, um einige bedeutungslose Programme in C zu entwickeln und das mit einer Parodie auf die Technik der 60er-Jahre! Dennoch sind wir beeindruckt von der Hartnckigkeit (falls nicht doch Gemeinsinn) des gewhnlichen Unix- und C-Anwenders. Jedenfalls haben Brian, Dennis und ich in den letzten Jahren nur in Pascal auf einem Apple Macintosh programmiert, und wir fhlen uns echt schuldig an dem Chaos, der Verwirrung und dem wirklich schlechten Programmierstil, der von unserem verrckten Einfall vor so langer Zeit ausging. Namenhafte Unix- und C-Anbieter und -Benutzer, einschlielich AT&T, Microsoft, Hewlett-Packard, GTE, NCR und DEC haben vorlug jede Stellungnahme abgelehnt. Borland International meinte, sie htten diesen Verdacht schon seit Jahren gehegt und wrden nun dazu bergehen, ihre Pascal-Produkte zu verbessern und weitere Bemhungen um die C-Entwicklung stoppen. Ein IBM-Sprecher brach in unkontrolliertes Gelchter aus. Den Artikel nden Sie in verschiedenen Varianten unter den Stichworten UNIX, Quatsch und Aprilscherz ber jede gut sortierte Suchmaschine mehrfach im Internet zitiert, beispielsweise auch unter folgender URL: http://www.c-plusplus.de/geschichte.htm

15

C++ Das Portrt

Taufe Die Namensgebung von C ist etwas ungewhnlich, und so spinnen sich Legenden um deren Entstehung. Eine unbesttigte Legende besagt, dass zuerst der Name A im Gesprch war. Aber die Paten frchteten, dass das A vielleicht in einigen Jahren von Apple patentiert werden wrde. Und so ging man das Alphabeth durch. B kam nicht in Frage, weil es mit B-Ware oder gar mit BASIC in Verbindung gebracht werden knnte. C hatte dagegen eine freundliche Assoziation. Man knnte Zitronen whlen, um die Lehrbcher ber die neue Programmiersprache zu schmcken, und das Gelb wrde einfach gut aussehen. Der Haken an dieser Legende ist, dass die Firma Apple erst ein Jahrzehnt spter auf den Plan trat. Auerdem htten die Namensgeber dann bersehen, dass die Sprache COBOL mit C anfngt. Und das ist sicher eine noch schlimmere Assoziation als BASIC. Eine deutlich huger wiederholte Geschichte besagt, dass man die Sprache erst A nannte, eine sptere Entwicklungsstufe B und dann zu C kam. Und dann htten sie einfach keine Lust mehr gehabt, die Sprache schon wieder zu verndern. Es war auch ein guter Moment zu stoppen. Die nachfolgenden Buchstaben wren durch D-Zug, E-Mail, F-Wort, G-Punkt und H-Spalterei belegt gewesen. Und wer htte schon eine Programmiersprache namens I ernst genommen? Bjarne Stroustrup wusste wohl um die Problematik, sodass er seine Erweiterung der Sprache C um Klassen eben C++ nannte, also ein Inkrement der Sprache C. Tempo! Ganz egal wie es nun zu dem Namen kam. Die Qualitten der Sprache waren durch das Umfeld gezeichnet. Bis dahin wurden Betriebssysteme in Assembler geschrieben. Dafr gab es vor allem drei Grnde. Assemblerprogramme waren klein, schnell und hatten leichten Zugriff auf die Hardware. Assembler hatte aber auch den Nachteil, dass es das entstehende Betriebssystem zwingend mit einem Prozessor verheiratete. Weitere Nachteile lagen darin, dass die Programme lang und unbersichtlich waren und spter kaum wartbar waren. Als UNIX entwickelt wurde, sollte es von der Hardware unabhngig sein und dennoch der Geschwindigkeit von Assembler mglichst nahe kommen. Maschinennah Der Inkrementoperator ist ein Beispiel fr diese Optimierung. Fast jeder Prozessor hat einen Inkrementoperator, der schneller ist als die Addition mit 1. Dieser konnte von C direkt angesprochen werden. Die Zeiger waren von Anfang an so ausgelegt, dass ein direkter Zugriff auf die Controller-Bausteine mglich ist. Controller-Bausteine haben eine feste Speicherstelle

16

Blick zurck

1.2

im Adressraum. Weist man diese Adresse der Zeigervariablen direkt zu, kann anschlieend der Inhalt des Controllers direkt gelesen und geschrieben werden, und schon war die Verwendung von Assembler zu diesem Zweck unntig. C bietet also die Mglichkeit, einen Computer systemnah zu programmieren. Der Zugriff auf Zeiger, das Anfordern und Freigeben von Speicher und der Umgang mit systemnahen Komponenten erfordert eine gewisse Disziplin. Wie in vielen anderen Lebensbereichen ist es auch hier gut, wenn man wei, was man tut. Die Idee, dass eine Programmiersprache den Computer vor dem Programmierer beschtzt, ist jedenfalls in C nicht implementiert. Moderne Sprachkonzepte C war aber mehr als ein portabler Assembler. Die damals mit ALGOL aufgekommene strukturierte Programmierung wurde bereits untersttzt. Es gab Schleifenbefehle, Funktionen mit Parametern und lokale Variablen. Darber hinaus konnten Datenstrukturen modelliert werden, was in jenen Tagen nicht selbstverstndlich war. In jenen Tagen gab es einen erbitterten Kleinkrieg zwischen den Verfechtern der klassischen Programmiersprachen wie COBOL, FORTRAN und BASIC und der aufkommenden strukturierten Programmierung. Die alten Programmiersprachen verwendeten ausgiebig den Befehl GOTO, was dazu fhrte, dass es sehr schwierig war, dem Source-Code zu folgen, weil die logischen Strnge so durcheinander waren wie die Spaghettis auf einem Teller. Die Sprache C stammte hnlich wie PASCAL aus der ALGOL-Linie, die Mechanismen bot, die diesen Spaghetti-Code vermeiden konnte. Allerdings gibt es Programmierer, die mit jeder Sprache in FORTRAN programmieren knnen. Ein etwas umfangreicherer Artikel, der leicht ironisch die damalige Diskussion widerspiegelt heit Echte Programmierer meiden Pascal und ist vollstndig unter folgender URL zu nden: http://www.leo.org/information/freizeit/fun/pascal.html Wer es schtzt, Dinge in der Originalsprache zu lesen, ndet eine englische Version unter der folgenden URL: http://www.crusoe.de/pascal.htm Interessanterweise gab es zu jener Zeit auch heftigen Streit zwischen den Cund den PASCAL-Programmierern, welche Sprache die beste sei. Oft fanden sich die Wortgefechte in den einschlgigen Newsgroups. So manch einer Diskussion merkte man an, dass Programmierer wohl nicht so oft wie andere Menschen ihre diesbezglichen animalischen Veranlagungen zu Stadiengesngen bei der Bundesliga auslebten.

17

C++ Das Portrt

Neben den Elementen zur strukturierten Programmierung bietet C einen Prprozessor, mit dessen Hilfe Makroprogrammierung mglich ist. Da dieser auf einer textuellen Ersetzung basiert, ist es leicht, generische Funktionen zu schreiben. Bjarne Stroustrup ergnzte C um Klassen fr die objektorientierte Programmierung und erweiterte C damit zur eierlegenden Wollmilchsau. Das Ziel war es, eine Sprache zur objektorientierten Programmierung zu schaffen, ohne die Qualitten von C aufzugeben. Aus diesem Grund gehrte die Geschwindigkeit zum Design. Es ist zwar keineswegs so, dass C++ die erste objektorientierte Programmiersprache war. Aber die objektorientierte Programmierung wurde erst durch C++ populr. Gerade die Tatsache, dass C++ niemanden zur OOP zwingt, machte es mglich, dass Programmierer diese Schritt fr Schritt nutzen konnten und dadurch langsam zu berzeugungsttern wurden.

1.3

Spa mit C++

C++ hat eher das Image einer pragmatischen, technischen und etwas sprden Programmiersprache. Das ist wohl der Fluch der Efzienz. Zwar ist C++ auf allen Plattformen zu Hause, bedient sich allerdings dort typischerweise der plattformspezischen API. Auf diese Weise sind C++-Programme zwar rattenschnell, aber die Portabilitt bleibt dann auf der Strecke, wenn die Besonderheiten einer Plattform, einer Bibliothek oder einer Datenbank genutzt werden. Insbesondere im Bereich der graschen Oberche gibt es keine Standardbibliothek, die zum Sprachumfang gehrt. Fr das Buch hatte ich einige Themen herausgesucht, die eh keine Grak bentigten. So werden die Innereien eines Navigationssystems betrachtet. Sie werden sehen, wie man mit Labyrinthen spielen kann und wie ein einfacher Sprachbersetzer funktioniert. Aber ich hatte auch einige lustige Themen, die eben eine grasche Umgebung bentigten. Grakbibliotheken hneln sich in der Regel so weit, dass eine bertragung auf andere nicht sehr kompliziert ist. Ich fragte Programmiererkollegen zum Thema portable Grak, und die empfahlen mir die Bibliothek wxWidgets. Diese sei leicht erlernbar, portabel, schnell und efzient. Da die Bibliothek sehr bersichtlich ist, habe ich sie fr die Beispiele verwendet. Da ich durchaus schon unter einer Reihe anderer grascher Oberchen programmiert habe, kann ich besttigen, dass sie auch fr grere Projekte einsetzbar ist. Sie bietet neben der eigentlichen graschen Oberche auch andere Elemente, die ansonsten plattformabhngig wren.

18

Spa mit C++

1.3

Da sich die verschiedenen Oberchen vom Prinzip der ereignisgesteuerten Programmierung und der Fensterobjekte gar nicht so stark unterscheiden, dass sich die wxWidgets-Programme bei Kenntnis einer anderen Oberche leicht portieren lassen. Wer also lieber die Win32-API oder MFC benutzt, wird die Programme mehr oder weniger nur intelligent abtippen mssen. Fr alle anderen sind die wxWidgets-Beispiele vielleicht eine Anregung fr eigene Experimente mit dieser Bibliothek. Mir hat wxWidgets jedenfalls viel Spa gemacht.

19

Manch einer sagt, dass er Selbstgesprche fhrt, weil er nur so sicher sein kann, einen intelligenten Gesprchspartner zu haben. Aber oft ist der Dialog nur eine Illusion.

Der Computer im Dialog

Der Dialog mit dem Computer wurde in diversen Filmen thematisiert. Am eindringlichsten ist sicher der Computer HAL1 im Film 2001: Odyssee im Weltraum, der sogar ein Eigenleben entwickelt. Schlielich ttet er sogar aus Sorge um seine eigene Existenz. Auch der Computer im Raumschiff Enterprise kann mit gesprochenen Befehlen gesteuert werden. Typischerweise beugt sich Mr. Spock ber die Konsole und spricht kurz und scharf: Computer! Es folgen klare Ansagen darber, was der Computer nun tun soll. Besonders unterhaltsam ist die Szene im Spiellm Star Trek IV, in der Scotty in der Vergangenheit landet und einem Macintosh gegenbersteht. Auch er spricht ihn einfach mit Computer! an. Als dieser absolut nicht antworten will, reicht ihm Pille die Maus. Scotty nimmt sie und spricht hinein Hallo Computer!. Der Besitzer des Macs meint leise Das da ist die Tastatur. Und Scotty antwortet: Tastatur? Wie rckstndig. Die Sehnsucht nach einer direkten Kommunikation mit dem Computer ist also offensichtlich. Sollte sie eines Tages erfllt sein, steht allerdings zu befrchten, dass wir von einer anderen Sehnsucht erfllt sein werden: der Ruhe vor dem ewig quasselnden Computer.

2.1

Eliza

Das Programm Eliza wurde zur Berhmtheit in der Geschichte der Informatik. Es wurde 1966 von Joseph Weizenbaum geschrieben, um zu demonstrieren, dass der Computer in der Lage sein kann, menschliche Sprache so weit zu verstehen, dass eine Computersteuerung auch im direkten Sprachdialog erfolgen kann, anstatt auf kryptische Befehle zurckzugreifen.

1 Es wird vermutet, dass der Computer deshalb HAL heit, weil durch die Abkrzung IBM entsteht, wenn man von jedem Buchstaben den Nachfolger im Alphabet nimmt.

21

Der Computer im Dialog

Zu dieser Zeit ging es allerdings noch gar nicht darum, die gesprochene Sprache zu verstehen. Es ging lediglich darum, dass der Computer die Befehle von Menschen verstehen knnte, die von Computern keine Ahnung haben, sondern mit dem Computer so reden, wie mit ihrem Nachbarn, allerdings mit dem feinen Unterschied, dass sie die Tastatur benutzen. Pygmalion Der Name Eliza entstammt dem Schauspiel Pygmalion von George Bernard Shaw. Seine Geschichte wurde durch das Musical My Fair Lady bekannt, und seither wei jedes Kind, dass es so grn grnt, wenn Spaniens Blten erst einmal blhen. Der eigentliche Inhalt der Geschichte beschreibt allerdings nicht den Chlorophyllgehalt der Flora auf der iberischen Halbinsel, sondern befasst sich damit, dass ein einfaches Blumenmdchen namens Eliza Doolittle durch Sprachtraining so dressiert werden kann, dass die feinere Gesellschaft sie als eine der ihren aufnimmt. Gesprchsmechanismen Joseph Weizenbaum brachte den Computer dazu, auf Eingaben des Benutzers so zu reagieren, dass der Mensch glauben konnte, der Computer trte in einen Dialog mit ihm ein. Dabei arbeitete Eliza mit recht einfachen Kniffen. So drehte es oft den Aussagesatz in einen Fragesatz um und stellt ein Warum davor. Eltern wissen von ihren Kindern, dass man durch wiederholte Warum-Fragen in Argumentationsnte kommen kann. Das ist brigens ein beliebter Trick in der Rhetorik. Stellen Sie einfach mehrfach hintereinander Warum-Fragen. Wenn Sie dies etwas abwechslungsreich gestalten, wird Ihr Gegenber gar nicht merken, warum er in die Defensive gert. Da der Zuhrer von Eliza nicht rhetorisch an die Wand gedrckt werden, sondern das Gefhl einer freundlichen Unterhaltung bekommen sollte, musste es neben den Warum-Fragen auch noch andere Mechanismen geben. Dem Programm wurden ein paar Schlsselwrter mitgegeben, auf die es standardisierte Antworten gab. So wrde es auf das Stichwort Auto vielleicht mit der Aussage reagieren: Sie mgen es, schnell vorwrts zu kommen?. Auf das Stichwort Arbeit knnte das Programm sagen: Sie sollten sich nicht nur ber Ihren Beruf denieren. Haben Sie keine Hobbies? Werden Vater oder Mutter erwhnt, knnte das Programm bemerken: Sie sind ein Familienmensch, nicht wahr? Die Antworten mssen so geartet sein, dass sie den Gesprchsfaden nicht abschlieen, sondern den Ball an den Menschen zurckspielen. Die Antwort sollte also immer eine mehr oder weniger offensichtlich getarnte Rckfrage enthalten.

22

Eliza

2.1

Der Mensch wird in der Regel nicht bemerken, dass die Verbindung zu dem neuen Satz ber einfache Stichworttabellen funktioniert. Es kann nur in Ausnahmefllen schiefgehen. Wenn der Mensch beispielsweise den furchtbaren Satz von Heraklit zitiert Der Krieg ist der Vater aller Dinge, wird er sicher etwas verblfft auf die Frage Sie sind ein Familienmensch, nicht wahr? reagieren. In der Regel wird der Mensch weitererzhlen, auch wenn er mit derartigen Phrasen konfrontiert ist. Signalisieren die Aussagen ihm, dass das Gegenber an ihm persnlich interessiert ist, werden ihn inhaltliche Fehlschlge nicht weiter stren. Da die meisten Menschen eher darauf xiert sind, von sich selbst zu erzhlen, wirken Menschen, die zuhren und andere reden lassen, automatisch sympathisch. Und denen nimmt man es auch nicht bel, wenn sie sich wiederholen. Abgesehen davon wirkt das Wiederholen bestimmter Phrasen durchaus menschlich. Achten Sie mal auf der nchsten Stehparty darauf, wie oft jemand sagt: Ja, die Welt ist ein Dorf. Sie kennen sicher auch andere Phrasen, die ihnen auf jeder Party mehrfach begegnen. Das geschickte Mischen solcher Antwortmechanismen funktionierte bei Weizenbaums Eliza wunderbar. Die Probanden hatten tatschlich das Gefhl, das Programm sprche mit ihnen. Um eine Legende fr den Anlass des Gesprchs zu schaffen, wurde den Probanden erzhlt, sie redeten mit einem Psychotherapeuten. So konnte das Programm immer im Gesprch bleiben, ohne dass spezielles Fachwissen erwartet wurde. Das Erschtternde war, dass die Probanden sich vom Computer verstanden fhlten und Behandlungserfolge meldeten. Besonders irritierend war, dass berlegungen aufkamen, dieses Programm tatschlich zur Therapie einzusetzen. Joseph Weizenbaum begann darber nachzudenken, wie es in unserer Gesellschaft aussieht, wenn ein einfaches Programm solche Wirkungen zeigte. Er interessierte sich in der Folgezeit fr die Fragen, wie das Phnomen Computer die Gesellschaft beeinusst und wurde ein Pionier auf diesem Gebiet. Die komplizierte Sprache Das Programm Eliza ist mehrfach nachprogrammiert worden. So befand sich das Programm auf der Masterdiskette des Apple II (siehe Seite E.1). Allerdings protiert das Programm sehr von den einfachen Grammatikstrukturen der englischen Sprache. Die englische Sprache hat an dieser Stelle einige Vorteile gegenber dem Deutschen. Das Umstellen von Satzfetzen ist weniger schwierig, und so ist die Illusion eines Dialogs leichter zu erreichen. Beispielsweise knnte das Programm auf Verben reagieren, die in irgendeiner Form mit illegalen Aktivitten zu tun haben und einfach fragen, ob das Gegenber sich dabei schuldig fhlt. Es knnte der folgende Dialog entstehen:

23

Der Computer im Dialog

Mensch: I always steal the show from my friends. Computer: Do you feel guilty, when you always steel the show from your friends.

Um diese Konstruktion zu erstellen, muss das Programm lediglich nach den persnlichen Frwrtern suchen und sie in die zweite Person umsetzen. Danach setzt es seinen Fragehalbsatz Do you feel guilty, when davor. Schlimmstenfalls kann noch ein am oder ein Im auftauchen, die in are oder youre zu bersetzen sind. Im Deutschen sind die Folgen weitreichender. Jedes Verb wird verndert, wenn der Satz von der ersten in die zweite Person transferiert wird. Die Verben unterscheiden sich beim Beugen, so dass ein Automatismus nicht einsetzbar ist. Vor allem verndert sich aber die komplette Satzstruktur, wenn aus einem Hauptsatz ein Nebensatz gemacht wird. Das Verb wandert beispielsweise an das Ende. Um das nachbilden zu knnen, msste der Computer zunchst einmal wissen, woran er ein Verb erkennen soll. Aber gerade das Aufnehmen von Teilstzen des Gegenbers macht die Illusion perfekt, dass der Computer einen Dialog aufnimmt.

2.1.1

Unsere kleine Eliza

Trotz der Behinderungen durch die deutsche Sprache knnen wir eine kleine Eliza erstellen. Das Programm arbeitet vor allem mit der Stichworterkennung. Durch die Aufnahme der Stichwrter in den Antwortsatz wird gegenber dem Anwender immerhin der Anschein erweckt, dass der Computer den Gesprchsfaden aufnimmt. Erkennt das Programm kein Stichwort in dem Text des Anwenders, werden eiig Phrasen gedroschen. Ein einfacher Algorithmus verhindert, dass sich der Computer allzu oft wiederholt. Und schon ist der kleine Psychiater fr den Hausgebrauch fertig. Sie knnen Ihre Nachbarn zur Freudschen Analyse einladen.

2.1.2

Die Datenmodellierung

Das Programm muss Stichwrter speichern und fr jedes Stichwort einen passenden Satz ausgeben. Besser wre es aber, wenn es mglich ist, fr jedes Stichwort mehrere Stze zu hinterlegen. Und damit man nicht so viele Stze eintippen muss, wre es gut, wenn Antwortstze auch ber mehrere Stichwrter erreicht werden knnten. Verwirrt? Gut! Zum Beispiel fragt das Programm die Eingabe auf den Vater ab. Als Antwort kommt der Satz Dein Vater ist Dir wichtig, nicht wahr?. Diese Antwort passt aber auch prima auf den Bruder, den Onkel oder den Papa. Andererseits passen auf alle diese Verwandten auch die Antwort Hast Du mit Deinem Vater schon einmal darber gesprochen?. Darber hinaus kann es auch Antwortstze geben,

24

Eliza

2.1

die auf fast alle Stichwrter passen, ob es Verwandte oder Gegenstnde sind. Dazu gehrt beispielsweise Erzhle mir mehr ber Deinen Vater! Um diese Verechtungen zu ermglichen, wird eine Klasse fr das Stichwort, eine fr die Antworten und eine Klasse fr die Beziehung zwischen den beiden entstehen. Falls Ihnen die Lsung aus dem Entity Relationship Modell der Datenbanken her bekannt vorkommt, ist das kein Zufall. Entity Relationship Modell Das Relationale Datenbankmodell besagt, dass die Daten in Tabellen organisiert werden. Dabei wird sowohl eine Einheit (Entity) als auch eine Beziehung (Relationship) in einer Tabelle abgelegt. Elemente in einer Tabelle knnen in der Regel ber einen eindeutigen Schlssel direkt angesprochen werden. So kann es bei einer Wohnungsvermittlung eine Datenbank geben, in der Kunden und Wohnungen in je einer Tabelle gespeichert werden. Aber auch das Mietverhltnis kann in einer Tabelle abgelegt werden. Solange jeder Kunde genau eine Wohnung hat, kann in der Tabelle des Kunden eine Spalte die eindeutige Adresse der Wohnung enthalten. Um von der Wohnung auf den passenden Kunden zu verweisen, kann die Wohnungstabelle die Kundennummer enthalten. Dies nennt man eine 1:1-Beziehung, weil jeder Kunde nur eine Wohnung und jede Wohnung nur einen Kunden hat. Bei einer 1:1Beziehung wird eine eigene Tabelle nicht bentigt. Wenn aber ein Kunde eine Wohnung, eine Ferienwohnung und einen weiteren Wohnsitz an seinem Arbeitsort hat, ist es eine 1:n-Beziehung. Die Wohnungen werden aufgesprt, indem alle Wohnungen gesucht werden, die die gleiche Kundennummer haben. Nach dem Entity Relationship Modell wird stattdessen eine eigene Tabelle fr die Beziehung erstellt, die die Kundennummer und die Wohnungsadresse enthlt. Sie kann auch weitere Daten ber die Beziehung enthalten, wie beispielsweise das Kndigungsdatum des Vertrags. Diese Tabelle kann auch leicht eine n:m-Beziehung darstellen. Wenn wir annehmen, dass eine Wohnung beispielsweise bei einer Wohngemeinschaft von mehreren Kunden gemietet werden kann, kann ein Kunde n Wohnungen und eine Wohnung m Kunden haben. Auch hier wird eine Tabelle fr die Beziehung des Mietens aufgebaut. Das Stichwort und die Antwort Eine solche Relation wird hier mit der Klasse tBindung erzeugt.

25

Der Computer im Dialog

class tStichwort { public: string Wort; int ID; }; class tAntwort { public: long Aufrufe; string Antwort; }; class tBindung { public: long WortID; long AntwortID; };
Listing 2.1 Datenmodell der Eliza

Jedes Stichwort enthlt eine ID. Alle Stichwrter mit gleicher ID werden zu einer Gruppe zusammengefasst. Eine Bindung verbindet eine Stichwort-ID mit dem Index des Array-Eintrags der Antwort. Die Antwort hat keine ID, weil sie ber den Index im Array zugegriffen wird. Dort ergibt sich die ID durch die Position der Antwort im Array. Die Antwort hat noch einen Aufrufzhler. Damit wird erreicht, dass die Antworten sich nicht wiederholen. Immer dann, wenn eine Antwort benutzt wird, wird der Aufrufzhler um 10 erhht. Jedesmal, wenn die Antwort nicht benutzt wird, wird sie um 1 reduziert. Bei mehreren mglchen Antworten wird die mit der geringsten Aufrufzahl verwendet. Um diese Klassen mit Daten zu fllen, werden ein paar Array-Variablen mit Konstanten belegt. Zunchst sehen Sie die Stichwrter.
tStichwort Stichwoerter[] = { {"Bruder", 1}, {"Vater", 1}, {"Sohn", 1}, {"Opa", 1}, { "Freund", 1}, { "Mutter", 2}, {"Schwester", 2}, { "Tochter", 2}, {"Oma", 2}, {"Gewalt", 3}, {"Druck", 3}, {"Schweigen", 3} };
Listing 2.2 Die Stichwrter

Sie sehen hier, dass wir mnnliche und weibliche Familienmitglieder in zwei Gruppen getrennt haben. Das hat nichts mit Chauvinismus zu tun, sondern

26

Eliza

2.1

schlicht damit, dass die deutsche Sprache deutliche Unterschiede zwischen den Geschlechtern in den Antwortstzen erfordert. Die dritte Gruppe knnte man unter der Rubrik negative Belastungen zusammenfassen. Die Antworten, die das Programm geben wird, sind ebenfalls einfach in einem Array abgelegt.
tAntwort Antworten[] = { {0, "Dein $ ist Dir sehr wichtig, nicht wahr?" }, {0, "Httest Du darber nicht mit Deinem $ sprechen" " sollen?"}, {0, "Erzhle mehr ber die Beziehung zu Deinem $!"}, {0, "Deine $ ist Dir sehr wichtig, nicht wahr?" }, {0, "Httest Du darber nicht mit Deiner $ sprechen" " sollen?"}, {0, "Erzhle mehr ber die Beziehung zu Deiner $!"}, {0, "$ ist keine echte Lsung."}, {0, "Es ist nicht gut, mit $ zu leben."}, {0, "Sollte die Welt nicht auf $ verzichten?"}, {0, "Was bedeutet das eigentlich fr Dich: $?"} };
Listing 2.3 Die Antworten

Sie sehen, dass die erste Antwort der vierten entspricht, wenn man von der weiblichen Form absieht. Die ersten sechs Zeilen beziehen sich also auf die Verwandten. Die nchsten drei Antworten reagieren auf Gewalt und hnliche Stichwrter. Die letzte Antwort passt auf alles. Sie sehen sicher die Dollarzeichen in den Antworten. Das sind Platzhalter fr die Stichwrter, die hier eingesetzt werden knnen. Auf diese Weise wirkt die Antwort sehr viel authentischer. Nun fehlt das Bindeglied zwischen Stichwort und Antwort.
tBindung Bindungen[] = { {1, 0}, {1, 1}, {1, 2}, {2, 3}, {2, 4}, {2, 5}, {3, 6}, {3, 7}, {3, 8}, {1, 9}, {2, 9}, {3, 9} };
Listing 2.4 Die Verbindungen zwischen Stichwrtern und Antworten

Die erste Zahl steht fr das Stichwort, die zweite fr die Antwort. Die Stichwrter mit der ID 1 sind also mit den ersten drei Antworten verbunden. Aber weiter hinten sehen Sie auch, dass die letzte Antwort auch von dieser ID verwendet werden kann.

27

Der Computer im Dialog

Jetzt braucht das Programm noch ein paar gelungene Phrasen, mit denen es sich rauswinden kann, wenn in der Eingabe gar kein verwendbares Stichwort fllt. Die Phrasen sind ebenfalls vom Typ tAntwort. Auch die Phrasen mssen rotieren. Da sie vermutlich huger zum Zuge kommen, ist es hier sogar sehr viel wichtiger. Fr die Phrasen werden allerdings keine Bindungen bentigt.
tAntwort { 0, { 0, { 0, { 0, { 0, { 0, Phrasen[] = { "Ich verstehe Deine Zurckhaltung." }, "Solltest Du nicht offener von Dir reden?" }, "Was meinst Du, ist denn die Ursache von all dem?" }, "Kannst Du etwas prziser werden?" }, "Du solltest nicht alles in Dich hineinfressen." }, "Fhlst Du Dich in dieser Hinsicht unsicher?" } };

Listing 2.5 Ein paar Phrasen

Die Phrasen sollten einerseits den Eindruck erwecken, als wrden Sie sich auf eine Aussage des Gegenbers beziehen. Andererseits drfen sie nicht so speziell sein, dass sie als unpassend empfunden werden. In jedem Fall ist es gut, wenn das Programm sein Gegenber dazu auffordert, eine weitere Antwort zu geben. Im Einzelfall kann dies auch mal wegfallen, weil die meisten Menschen Gesprchspausen nutzen, um fortzufahren.

2.1.3

Der Programmablauf

Die Beschreibung beginnt mit dem Hauptprogramm. Hier wird von der Tastatur die Eingabe des Anwenders eingelesen und an die Funktion zerlegeInWoerter bergeben. Dort wird sie in ihre Wrter zerlegt und in einem Vector abgelegt. Der STL-Vector kann wie eine Liste Stck fr Stck verlngert werden. Diese Wortliste wird dann von der Funktion sucheNachAntwort bergeben. Dort wird sie durchstbert und eine passende Antwort ermittelt, die dann auf dem Bildschirm ausgegeben wird. Das wiederholt sich in einer Schleife bis zum nchsten Stromausfall.
int main() { string Aussage; while (true) { getline(cin, Aussage); vector<string> Woerter; zerlegeInWoerter(Aussage, Woerter); string Antwort; sucheNachAntwort(Woerter, Antwort);

28

Eliza

2.1

cout << Antwort << endl; } }


Listing 2.6 Das Hauptprogramm

Vielleicht knnte das Programm zu Anfang noch erzhlen, was es tut. Aber das kann durch ein paar gezielte cout-Anweisungen leicht nachgereicht werden. Das Wort im Munde verdrehen Die Funktion zerlegeInWoerter zerlegt die Eingabezeile in Wrter und hngt diese an den bergebenen Vektor. Dazu luft die Funktion durch die Zeile und berspringt alles, was kein Buchstabe ist. Sobald der erste Buchstabe auftaucht, wird der Anfang markiert und so lange gesucht, bis etwas anderes als ein Buchstabe auftaucht. Das, was zwischen den Positionen ist, muss wohl ein Wort sein und kann mit der String-Funktion substr entnommen werden. Das Wort wird im gleichen Zug per push_back an die Wortliste angehngt.
void zerlegeInWoerter(const string &Satz, vector<string> &Woerter) { int Laenge = Satz.length(); int Pos = 0; int Anfang; Woerter.clear(); while (Pos<Laenge) { while (Pos<Laenge && !istBuchstabe(Satz[Pos])) Pos++; Anfang = Pos; while (Pos<Laenge && istBuchstabe(Satz[Pos])) Pos++; Woerter.push_back(Satz.substr(Anfang, Pos-Anfang)); } }
Listing 2.7 Auf der Suche nach dem Wort

Dieser Vorgang wird so lange wiederholt, bis die Zeile abgearbeitet ist. Die Bestimmung der Buchstaben ist in eine separate Funktion ausgelagert. Die Abfrage, ob das Zeichen zwischen A und Z liegt, ist relativ simpel.
bool istBuchstabe(char Zeichen) { if (Zeichen>='a' && Zeichen<='z') return true; if (Zeichen>='A' && Zeichen<='Z') return true;

29

Der Computer im Dialog

return false; }
Listing 2.8 Zeichenvergleich

Die deutschen Sonderzeichen Aber das ist nicht die ganze Wahrheit. Eigentlich mssten wir prfen, ob nicht ein Umlaut vorliegt. Deutsche Sonderzeichen benden sich nach dem ISO-Code an einzelnen Positionen weit jenseits der normalen Buchstaben. In lteren Programmen htte man also einfach weitere Zeilen des folgenden Musters hinzugefgt:
if (Zeichen=='') return true;

Aber leider funktioniert das nicht mehr, seit es den UNICODE gibt. Die Entwicklung der Codierung der deutschen Umlaute ist sehr abwechslungsreich. Zunchst gab es sie gar nicht. Jedenfalls nicht in den Computern. Die Programmierer gewhnten sich daran, statt einem ein Ae zu schreiben. Es galt der ASCIICode. Und da steht das A fr American. Und die Amerikaner gingen davon aus, dass es auer ihnen kein intelligentes Leben im Universum gbe. Hchstens noch ein paar Klingonen, aber darum wrden sich schon Captain Kirk und Mister Spock kmmern. Also denierte der ASCII-Zeichensatz in seinen ersten 128 Zeichen nur den angloamerikanischen Zeichensatz, die Zahlen, die Satzzeichen und ein paar Zeichen, die Programmierer sehr gut gebrauchen konnten. Das letzte freie Bit in jedem Byte wurde fr Steuerzwecke missbraucht. Mehr als 128 Buchstaben brauchte die freie Welt ja nicht, wenn alle Englisch miteinander reden. Irgendwann im Zuge der nchsten Kontinentalverschiebung gelangten die amerikanischen Computer nach Europa und irgendwann auch nach Deutschland. Dort fragten einige Omas, die die Ausdrucke der ersten Neunnadeldrucker ihrer Enkel pichtgem bewunderten, warum diese hochintelligenten Gerte eigentlich keine Umlaute beherrschten. So entstand das erste Mal eine Spannung zwischen Programmierern und Anwendern, ein Konikt, der bis in die Gegenwart anhlt. Denn das Problem der Oma wurde auf Kosten der Programmierer gelst. Die eckigen und geschweiften Klammern und einige Sonderzeichen, die ein Anwender nie brauchen wrden, wurden kurzerhand durch die Umlaute ersetzt, wenn man den Drucker durch kleine DIP-Switches auf Deutsch umschaltete. Wenn ein Programmierer sich ein Listing ausdrucken wollte, konnte er ja das Schalterchen umlegen. Das funktionierte ein paar Jahre beinahe gut, bis die Computerhersteller feststellten, dass sie vielleicht doch noch das letzte Bit hergeben knnten, damit die Europer ihre merkwrdigen Zeichen unterbringen konnten. Daraufhin hie

30

Eliza

2.1

die Norm nicht mehr ASCII, sondern ISO. Das I steht fr International. Nun waren Programmierer und Anwender vershnt. Ein Ausdruck konnte sogar geschweifte Klammern und Umlaute gleichzeitig enthalten. Der einzige Haken war, dass die alten Texte nach der bernahme durch den neuen Computer statt der Umlaute merkwrdige Sonderzeichen enthielt. In der bergangsphase verwendete man wieder oe statt , bis auch das letzte Programm auf ISO umgestellt war und wieder vertrauensvoll Umlaute verwendet werden konnten. Das wre auch alles gutgegangen, htte Captain Kirk nicht ein paar Klingonen durchutschen lassen, die sich prompt als Chinesen auf der Erde niederlieen und ganz besonders viele merkwrdige Zeichen erfanden, nur um der amerikanischen Computerindustie zu schaden. Um einen Krieg zu verhindern, beschloss man, dass eben nun zwei Byte verwendet werden mssten, um einen Buchstaben zu denieren. Das hatte nun die Konsequenz, dass die meisten Programmiersprachen ihre Zeichenvariablen nicht mehr fr Zeichen verwenden konnten, weil die neuen Zeichen einfach nicht mehr hineinpassten. Schnell wurde fr C++ der Typ wchar_t geschaffen, der zwei Bytes belegt. Kaum war das beschlossene Sache, entdeckten einige Sprachwissenschaftler dann noch ein paar klingonische Zeichen, die man bersehen hatte und fragten an, dass das mit den zwei Bytes wohl zu kurz gegriffen wre, wie es denn mit drei oder vier Byte wre. Auf der anderen Seite fragten einige amerikanische Computeranwender, warum sie eigentlich den doppelten Speicher fr Zeichen vorhalten sollten, wenn Sie berhaupt kein Chinesisch verstehen, mit Chinesen auch nichts zu tun htten und Klingonen noch nie gut fanden. So wurde dann zur allgemeinen Verstimmung ein weiterer Kompromiss gefunden, den man UTF-8 nannte. Fr diesen Standard wurden wieder die alten charVariablen verwendet. Solange es sich um keine Sonderzeichen handelt, galt wieder der gute, alte ASCII. Sobald ein internationales Sonderzeichen folgen sollte, wurde das Highbit gesetzt, und in den nachfolgenden Bytes wurde beschrieben, um welches Sonderzeichen es sich handeln wrde. Auf diese Weise wird nun fr deutsche Umlaute zwei Bytes verwendet und damit sind sie wieder einmal an eine andere Stelle in der Codetabelle gewandert. Die Abfragen in den Programmen funktionieren gar nicht mehr, und ich bin inzwischen dazu bergegangen, in meinen Adressverzeichnissen keine Umlaute mehr zu notieren. Sie verschwinden eh alle paar Jahre. Bei der kleinen Eliza habe ich Glck gehabt. Alle Stichwrter enthalten keinen Umlaut. Und falls Sie das Wort Glck auffangen wollen, rate ich Ihnen dringend ab. Werden Sie doch konkreter! Sprechen Sie von Freude oder Zufriedenheit. Das ist leichter fassbar und hat vor allem keine Umlaute.

31

Der Computer im Dialog

Bei den Antworten stellen Umlaute kein Problem dar, da sie einfach nur jeweils zwei Byte an Speicher belegen, aber nicht verglichen werden mssen. Auf der Suche nach Antworten Nun verfgt das Programm ber eine Liste der Wrter, die der Anwender genannt hat. Diese Liste wird von der Funktion sucheNachAntwort durchlaufen und mit der Stichwortliste verglichen. Kommt es zu einem Treffer, werden alle Bindungen durchsucht, zu welchen Antworten Verknpfungen bestehen. Die beste Antwort scht die Funktion holeAntwort heraus und liefert dessen Index im Array.
void sucheNachAntwort(vector<string> &Woerter, string& Antwort) { long MaxStichwoerter=sizeof(Stichwoerter) /sizeof(Stichwoerter[0]); vector<string>::iterator it; for (it=Woerter.begin(); it!=Woerter.end(); ++it) { for (long wi=0; wi<MaxStichwoerter; ++wi) { if (*it == Stichwoerter[wi].Wort) { long ID = Stichwoerter[wi].ID; long AntwortID = holeAntwort(ID); // Nun sollten wir eine ID haben if (AntwortID>=0) { Antwort = Antworten[AntwortID].Antwort; ersetzePlatzHalter(Antwort, *it); // Diese Antwort etwas zurckstellen Antworten[AntwortID].Aufrufe+=10; // wir gehen mal einfach return; } } } } // Kein Stichwort gefunden. Wir faseln... long AntwortID = holePhrase(); Antwort = Phrasen[AntwortID].Antwort; // Diese Antwort etwas zurckstellen Phrasen[AntwortID].Aufrufe+=10; }
Listing 2.9 sucheNachAntwort

Anschlieend bereitet die Funktion ersetzePlatzHalter die Antwort auf, indem es nach einem Dollarzeichen sucht und dieses durch das Stichwort ersetzt. Der Aufrufzhler der gefundenen Antwort wird signikant hochgezhlt, damit die gleiche Antwort nicht so schnell wiederholt wird.

32

Eliza

2.1

Wurde kein Stichwort in der Aussage des Anwenders gefunden, muss sich das Programm in Phrasen ergehen. Die Funktion holePhrase erledigt dies. Solange es Phrasen gibt, kann das gar nicht schiefgehen. Auch hier wird der Aufrufzhler der Antwort so erhht, dass diese Phrase in nchster Zeit nicht mehr gewhlt wird. Vor dem Holen der Antwort behandeln wir den einfacheren Fall der Phrase. Hier spielt die Bindung nicht hinein. Das Programm durchluft einfach alle Phrasen und sucht diejenige mit den geringsten Aufrufen. Dabei wird jede der Antworten einmal dekrementiert. Wenn Sie nur dekrementieren, wenn der Aufrufzhler grer als 0 ist, erreichen Sie eine automatische Alterung.
long holePhrase() { long min=999999; long minID = -1; long MaxPhrasen = sizeof(Phrasen)/sizeof(Phrasen[0]); for (long i=0; i<MaxPhrasen; ++i) { if (min>Phrasen[i].Aufrufe) { minID=i; min=Phrasen[i].Aufrufe; --Phrasen[i].Aufrufe; } } return minID; }

Listing 2.10 holePhrase

Das Holen der Antwort ist sehr hnlich. Bevor allerdings die eigentliche Antwort geholt wird, muss aus dem Bindungsarray noch der richtige Index geholt werden. Die Schleife durchluft also nicht die Antworten, sondern die Bindungen und bercksichtigt anschlieend nur die Fragen, zu denen eine Bindung besteht.
long holeAntwort(long WortID) { long min=999999; long minID = -1; long AntwortID; long MaxBindungen=sizeof(Bindungen)/sizeof(Bindungen[0]); for (long bi=0; bi<MaxBindungen; ++bi) { if (Bindungen[bi].WortID==WortID) { AntwortID = Bindungen[bi].AntwortID; if (min>Antworten[AntwortID].Aufrufe) { minID=AntwortID; min=Antworten[AntwortID].Aufrufe;

33

Der Computer im Dialog

--Antworten[AntwortID].Aufrufe; } } } return minID; }


Listing 2.11 sucheNachAntwort

Das Ersetzen des Platzhalters ist dank der Elementfunktionen der Klasse string kein Problem. Die Funktion find ermittelt die Position des Dollarzeichens. Falls es eine gltige Position gibt, wird das Zeichen an dieser Stelle durch die Funktion replace ersetzt.
void ersetzePlatzHalter(string &Antwort, const string &Wort) { unsigned long pos = Antwort.find("$"); if (pos!=string::npos) { Antwort.replace(pos, 1, Wort); } }
Listing 2.12 Platzhalter ersetzen

2.1.4

Ein Sitzungsprotokoll

Damit ein Eindruck entsteht, wie das Programm arbeitet, habe ich mit ihm ein kleines Schwtzchen gehalten. Ich mchte darauf hinweisen, dass alles frei erfunden ist und keine uerung meinerseits auf meinen Geisteszustand hinweist. Ich bitte auch darum, von Therapievorschlgen im Forum abzusehen, auch wenn sie noch so lieb gemeint sind. Hier das Protokoll:
Ich mag meinen Freund Erwin nicht mehr leiden. Dein Freund ist Dir sehr wichtig, nicht wahr? Eigentlich schon. Aber Vater hat mir die Augen geffnet. Httest Du darber nicht mit Deinem Vater sprechen sollen? Ich hatte ihn angesprochen, aber Mutter mischte sich ein. Deine Mutter ist Dir sehr wichtig, nicht wahr? Sie mag meine Schwester aber viel lieber als mich Httest Du darber nicht mit Deiner Schwester sprechen sollen? Mit wem soll ich noch alles sprechen? Mit meiner Oma? Erzhle mehr ber die Beziehung zu Deiner Oma! Oma ist doch tot. Was bedeutet das eigentlich fr Dich: Oma? Na die Mutter meines Vaters halt. Deine Mutter ist Dir sehr wichtig, nicht wahr?

34

Eliza

2.1

Ach was, die kmmert sich nur um meine Schwester. Httest Du darber nicht mit Deiner Schwester sprechen sollen? Hey, Du wiederholst Dich! Kannst Du etwas prziser werden? Du hast laufend gesagt, ich soll mit meiner Schwester reden. Erzhle mehr ber die Beziehung zu Deiner Schwester! Sie hat mir im Sandkasten immer die Schaufel geklaut. Du solltest nicht alles in Dich hineinfressen. Ich will sie auch nicht fressen. Fhlst Du Dich in dieser Hinsicht unsicher? Nein, absolut nicht. Ich fresse doch nicht meine Schwester. Was bedeutet das eigentlich fr Dich: Schwester? Auf keinen Fall Nahrungsmittel Ich verstehe Deine Zurckhaltung. Wer will sowas auch essen? Solltest Du nicht offener von Dir reden? Gern, ich bin aber inzwischen hungrig geworden. Was meinst Du ist denn die Ursache von all dem? Dein Stichwort mit der Fresserei. Kannst Du etwas prziser werden? Du hast gemeint, ich soll nicht alles in mich hineinfressen. Du solltest nicht alles in Dich hineinfressen. Genau. Das hast Du gesagt. Ich verstehe Deine Zurckhaltung.

Vor allem die Wiederholungen zerstren den Eindruck einer realen Unterhaltung. Die Abrisse im Gesprchsverlauf sind zwar erkennbar. Aber in einem normalen Gesprch kommt es ja auch oft vor, dass sich der Gesprchspartner auf ein ganz anderes Thema versteift als das, was den Sprecher gerade interessiert.

2.1.5

Was denn noch?

Die einfachste Methode aus der kleinen Eliza eine groe zu machen, ist die Ergnzung der Stichwortlisten und der Antworten. Bei der Zerlegung der Eingabe in Wrterlisten liee sich das Programm um Statistiken erweitern, welche Wrter der Anwender verwendet. So knnte das Verwenden vieler negativer Worte wie nein oder nicht von dem Programm aufgegriffen werden, ob der Patient nicht zu negativ ist. Stichwrter wie Geld oder Euro knnten als Neigung zum Materialismus interpretiert werden. Im Unterschied zur bisherigen Stichwortliste mssten die Stichwrter gezhlt werden. Erst bei Erreichen einer bestimmten Schwelle wre eine solche Analyse gerechtfertigt.

35

Der Computer im Dialog

Um die Antworten noch menschlicher zu gestalten, knnte eine Synonymtabelle dafr sorgen, dass das Programm nicht einfach das Stichwort verwendet, sondern ein anderes Wort gleicher Bedeutung verwendet. Bei der Suche nach Stichwrtern wird der erste Treffer sofort verwertet. Falls es in einer Zeile mehrere Treffer geben sollte, wre die Antwort besser, wenn man die Stichwrter unterschiedlich gewichten knnte. Das Programm knnte bereits vorgeben, dass das Wort Mann nicht so signikant ist wie Hinterachsensperrdifferential. Auf der anderen Seite knnte auch ein Hugkeitszhler dynamisch ermitteln, welche Wrter schon huger genannt wurden und vielleicht inzwischen etwas ausgelutscht sind.

2.2

Tiere raten

In diesem Abschnitt behandeln wir eine kleine Spielerei, in der wir den Computer zu einem kleinen Ratespiel nutzen. Sie kennen sicher die Zahlenratespiele: Du denkst Dir eine Zahl und ich errate sie. Ist die Zahl grer als 10? In diesem Fall soll aber der Dialog mit Begriffen arbeiten und die Fragen und Antworten speichern. Der Computer rt keine Zahlen, sondern Tiere. Auch dieses Spiel hat sein Vorbild auf dem Apple II. Dort heit das Programm Animals und bendet sich auf der Masterdiskette des Apple II europlus. Das vorgestellte Programm wird zeigen, wie einfach Programme durch Rekursion werden knnen, und es beweist wieder einmal, dass die Grammatik der deutschen Sprache maschinell sehr viel schwieriger zu behandeln ist, als dies bei der englischen Sprache der Fall ist. Der Optimist mag dies als Sieg der Kultur ber den Computer feiern.

2.2.1

Die Spielidee

Das Programm kennt von vornherein ein Tier. Hier ist es eine Maus. Der Mensch soll sich ein Tier ausdenken, das der Computer raten will. Der Computer fragt beim ersten Mal einfach, ob es eine Maus ist. Die Wahrscheinlichkeit ist gro, dass es sich um ein anderes Tier handelt. Unterwrg gesteht der Computer sein Versagen und bittet um eine Frage, mit der man das neue Tier von der Maus unterscheiden kann. Die Frage soll nur mit ja oder nein zu beantworten sein. Nun baut der Computer das neue Tier und die Frage zur Unterscheidung in sein Wissen ein. Das Programm benutzt die Fragen des Menschen, um sich durch die Tiere hindurchzufragen.

36

Tiere raten

2.2

Bei einer optimal ausgewogenen Fragenhierarchie reichen zehn Fragen, um ein Tier aus 1024 Tieren herauszunden.

2.2.2

Datenstruktur Binrbaum

Der gebte Programmierer erkennt sofort die Struktur des Binrbaums. Von jeder Frage gehen exakt zwei Wege ab, die jeweils in einer weiteren Frage oder in einer Lsung enden. Jedes Blatt stellt also eine Lsung und jeder innere Knoten eine Frage dar. Der Einfachheit halber wird dies im Programm durch eine einzige, sehr bersichtliche Datenstruktur reprsentiert.
class tTierFrage { public: tTierFrage() { Inhalt=""; Ja=0; Nein=0; } string Inhalt; // der Text der Frage oder Loesung tTierFrage *Ja; // Verweis auf naechsten Knoten tTierFrage *Nein; // Verweis auf naechsten Knoten };
Listing 2.13 Frage und Antwort als Datenstruktur

Die beiden Zeiger Ja und Nein zeigen auf ein Element des eigenen Typs. Dies erscheint zunchst verwirrend. Aber diese Zeiger zeigen bei einem Objekt natrlich nicht auf sich selbst, sondern auf ein anderes Objekt gleichen Typs. Auf diese Weise gehen im gnstigsten Fall von einem Objekt zwei Zeiger zu je einem weiteren Objekt. Von jenem knnen wiederum zwei Zeiger auf je zwei weitere Objekte zeigen, und so spannt sich ein Binrbaum auf. Eine Datenstruktur die einen Zeiger auf den eigenen Datentyp enthlt, nennt man einen rekursiven Datentyp, so wie man eine Funktion, die sich selbst aufruft eine Rekursion nennt. Die Nhe der Begriffe ist nicht rein zufllig, weil rekursive Datenstrukturen in der Regel recht einfach mit rekursiven Funktionen bearbeitet werden knnen. Sind beide Zeiger des Objekts ungesetzt, handelt es sich um ein Blatt und damit um eine Lsung. Zeigt mindestens ein Zeiger auf ein anderes Datenelement, handelt es sich um einen inneren Knoten, und der Inhalt muss eine Frage sein. Es werden allerdings immer beide Zeiger entweder gesetzt oder ungesetzt sein, da das Programm mit einem neuen Tier auch immer eine neue Frage erhlt. Die Elementvariable Inhalt vom Typ string ist der Text, den das Programm auf dem Bildschirm ausgeben wird. Es kann sowohl die Lsung (eine Maus) als auch eine Frage sein. Das Programm kann anhand der Zeiger unterscheiden, ob

37

Der Computer im Dialog

es sich um das eine oder das andere handelt. Mehr Inhalt wird in diesem Fall nicht bentigt. Die folgende Abbildung zeigt den entstehenden Baum.

Elefant

Maus Lwe Boa Regenwurm

Stozahn?

grau? Beine?
Abbildung 2.1 Der Binrbaum

frisst Muse?

Das Programm navigiert durch diesen Baum, indem es immer den Zeigern folgt, die sich durch die Antwort des Anwenders ergeben. Gibt es keinen Zeiger mehr, ist es wohl eine Antwort. Ist die Antwort falsch, gibt es das Tier offenbar nicht im Wissensbaum. Also wird der Mensch nach seinem Tier gefragt und der Antwort, die zu dem Tier fhrt. Die falsche Antwort wird mit dem neu genannten Tier nun an die neue Frage gehngt und diese wird an der Stelle im Baum eingehngt, wo zuvor noch die falsche Antwort stand.

2.2.3

Eine Rekursion muss her

Immer, wenn ein Programmierer auf einen Binrbaum trifft, ist eine Rekursion unausweichlich. Nicht jeder Programmierer liebt Rekursionen. Manche halten sie fr schwer verstndlich und so hrte ich jemanden sagen: Um das Verfahren der Rekursionen zu verstehen, muss man jemanden fragen, der sie verstanden hat oder man muss sie selbst bereits verstanden haben. So schlimm wird es schon nicht werden. Eine Rekursion ist eine Funktion, die sich selbst aufruft. Dadurch bildet sich eine Wiederholung, die ber den Aufruf der Funktion fhrt. Da die Rekursion in Fllen wie diesem zu deutlich krzeren und damit bersichtlicheren Programmen fhrt, lohnt sich die Beschftigung damit. Im folgenden Listing ist die Rekursionsfunktion kurz zusammengefasst, damit die Selbstaufrufe deutlicher zu sehen sind.

38

Tiere raten

2.2

void stelleFrage(tTierFrage *Frage) { if (Frage==0) return; if (Frage->Ja==0 && Frage->Nein==0) { cout << "Ist es " << Frage->Inhalt << endl; if (Antwort[0]=='n' || Antwort[0]== 'N') { cout << "Ich gebe auf! Was ist es? " << endl; // Tier einfuegen } else { cout << "Hurra, ich habe es gewusst!" << endl; } } else { // Stelle die Frage und erfrage die Antwort. if (Antwort[0]=='n' || Antwort[0]== 'N') { stelleFrage(Frage->Nein); } else { stelleFrage(Frage->Ja); } } }
Listing 2.14 Kurzgefasste Rekursion

Die erste Abfrage stellt fest, ob das Element berhaupt existiert. In unserem Fall kann das eigentlich nur passieren, wenn der Programmierer einen Fehler gemacht hat. Aber auch so etwas soll es tatschlich schon einmal gegeben haben. Die nchste Abfrage unterscheidet einen inneren Knoten von einem Blatt. Im Falle eines Blattes stellt das Programm seine Lsung vor. Die Rekursion wird nicht vertieft. Es kann hchstens dazu kommen, dass ein neues Tier eingefgt wird. Die Funktion wird zum Aufrufer zurckspringen. Da in der Funktion keine Schleife vorliegt, wird auch der Aufrufer sofort beenden. Das geschieht auch mit allen Vorgngern, sodass die Rekursionsschleife damit beendet ist. Bendet sich das Programm allerdings noch innerhalb des Baums auf einem Knoten, stellt es die hier hinterlegte Frage. Je nach Antwort wird die Funktion stelleFrage mit dem entsprechenden Zeiger aufgerufen. Nun wird der gleiche Ablauf auf den Folgezeiger angewandt, bis irgendwann ein Blatt erreicht wird und damit die Rekursion endet. Anlegen eines Tieres Der restliche Inhalt der Funktion stelleFrage besteht fast nur aus Ein- und Ausgaben, ist also nicht besonders berraschend. Lediglich das Einfgen eines

39

Der Computer im Dialog

neuen Tieres muss behutsam passieren. Es wre doch zu rgerlich, wenn so ein Zeiger pltzlich ins Nirvana zeigt. Zunchst werden die Informationen ber das neue Tier gesammelt.
void stelleFrage(tTierFrage *Frage) { ... if (Antwort[0]=='n' || Antwort[0]== 'N') { cout << "Ich gebe auf! Was ist es? " << endl << " Bitte gebe die Antwort mit unbestimmtem " "Artikel ein," << endl << " wie beispielsweise 'eine Maus'!" << endl; string NeuTier; getline(cin, NeuTier); cout << "Ok, wie unterscheiden sich " << NeuTier << " und " << Frage->Inhalt << "?" << endl; cout << "Gebe eine Frage an, die mit ja oder " << "nein beantwortet wird." << endl; string NeuFrage; getline(cin, NeuFrage); cout << "Und wie lautet die Antwort fuer " << NeuTier << "?" << endl; string NeuAntwort; getline(cin, NeuAntwort); cout << "Ich kenne nun bestimmt alle Tiere!" << endl;
Listing 2.15 Den Anwender aushorchen

Das neue Tier steht in der Variablen NeuTier, die passende Frage in NeuFrage und die Antwort fr das neue Tier in der Variablen NeuAntwort. Um genau zu sein, steht dort die Eingabe des Anwenders. Das Programm geht davon aus, dass ein groes oder kleines N eine negative Antwort ist und alle anderen positiv sind. Zeigerumschichtungen Nun wird umgehngt, und das sieht ein wenig wild aus. Die Funktion hat als bergabe einen Zeiger auf die aktuelle Antwort bergeben bekommen. Wir wissen jetzt, dass die Antwort falsch war. Wir wissen aber nicht, aus welchem Vorgngerknoten der Zeiger stammt, den wir bergeben bekommen haben. Also wird ein neues Blatt angelegt und die falsche Antwort aus dem aktuellen Blatt hineinkopiert. Da hier nun die alte Antwort steht, haben wir den Zeiger alt genannt. Nun kann die falsche Antwort mit der neuen Frage berschrieben werden. Es wird ein weiteres neues Blatt fr das neue Tier angelegt und die

40

Tiere raten

2.2

Antwort NeuTier in den Inhalt kopiert. Nun mssen nur noch die richtigen Tiere mit den richtigen Zeigern verknpft werden, und das Einhngen ist fertig.
// Neues Element einhaengen tTierFrage *alt = new tTierFrage; alt->Inhalt = Frage->Inhalt; Frage->Inhalt = NeuFrage; tTierFrage *neu = new tTierFrage; neu->Inhalt = NeuTier; if (NeuAntwort[0]=='n' || NeuAntwort[0]=='N') { Frage->Nein = neu; Frage->Ja = alt; } else { Frage->Ja = neu; Frage->Nein = alt; } ... }
Listing 2.16 Anlegen und Einhngen des neuen Tiers

Ich hoffe mal, dass das nun alles richtig ist. Wenn bei solch einer Aktion ein Zeiger danebenrutscht, durchluft das Programm uninitialisierten Speicherraum und gibt allen mglichen Bldsinn aus. Einen solchen Fall kann auch die abgebrhteste Marketingabteilung dem Kunden dann nicht mehr als Feature verkaufen. Der Rest des Programms ist simpel. Es muss eine Verankerung fr den Binrbaum geschaffen werden. Das ist einfach ein Zeiger, der mit 0 initialisiert wird, damit man wei, dass er noch keine Informationen hat.
#include <string> #include <iostream> using namespace std; ... tTierFrage *Anker = 0; ... int main() { cout << "Ich kann Tiere raten!" << endl; Anker = new tTierFrage; Anker->Inhalt = "eine Maus"; while(true) { stelleFrage(Anker); } }
Listing 2.17 Die Hauptfunktion

41

Der Computer im Dialog

Kurz nach der Meldung beim Benutzer wird der Anker mit einem neuen Blatt versehen. In den Inhalt kommt irgend ein Tier, das den Anfang machen soll. Das reicht auch schon. Die Zeiger werden vom Konstruktor auf 0 gesetzt. Nun kann das Fragespiel in einer Endlosschleife beginnen.

2.2.4

Ein Spielprotokoll

Der Ablauf des Spiels ist ein Dialog zwischen dem Computer und dem Menschen. Allerdings hngt der funktionierende Dialog davon ab, dass der Mensch die Eingaben so abgibt, dass sie vom Programm wieder in den Dialog eingebaut werden knnen. So muss das Programm fr die Frage nach dem Tier den unbestimmten Artikel kennen. Im Englischen ist das relativ einfach. Das Programm setzt ein a davor, und wenn der erste Buchstabe des Tieres ein Selbstlaut ist, wird daraus ein an. Das ist im Deutschen nur dann mglich, wenn man dem Programm einen jugendlichen Slang verpasst und fragen lsst Isses enne Maus?. Es gibt also im Deutschen eine nur fr Spezialisten verstndliche Logik bei der Wahl des Geschlechts der Hauptwrter. Darum wird ein Unglcklicher, der Deutsch lernen muss, die Geschlechter einfach auswendig lernen. Ich darf aus einem lesenswerten Artikel von Mark Twain zitieren, der einige Zeit in Deutschland verbrachte und Deutsch gelernt hatte. Er behauptete sogar, die deutsche Sprache zu lieben. Was sich liebt, das neckt sich. Ein Baum ist mnnlich, seine Knospen sind weiblich, seine Bltter sind schlich; Pferde sind geschlechtslos, Hunde sind mnnlich, Katzen sind weiblich; jemandes Mund, Hals, Busen, Ellbogen, Finger, Ngel, Fe und Leib gehren dem mnnlichen Geschlecht an, und sein Kopf ist mnnlich oder schlich, je nach dem Wort, das zur Bezeichnung gewhlt wird, und nicht nach dem Geschlecht der Person, die ihn trgt denn in Deutschland tragen alle Frauen entweder mnnliche oder geschlechtslose Kpfe; jemandes Nase, Lippen, Schultern, Brust, Hnde, Hften und Zehen gehren dem weiblichen Geschlecht an; und seine Haare, Ohren, Augen, Kinn, Beine, Knie, Herz und Gewissen haben berhaupt kein Geschlecht. Der Ernder der Sprache hat wahrscheinlich das, was er vom Gewissen wusste, vom Hrensagen erfahren.2 Wenn man es positiv sehen will: Eine der schwersten Sprachen haben wir als Babys gelernt. Damit sollte uns das Lernen der Fremdsprachen eigentlich leicht fallen. Einen greren Ausschnitt des Aufsatzes von Mark Twain nden Sie beispielsweise auf der folgenden Website: http://www.alvit.de/vf/de/mark-twain-die-schreckliche-deutsche-sprache.php

2 Mark Twain: Bummel durch Europa.

42

Tiere raten

2.2

Nun soll hier noch einmal der Ablauf einer Tierraterunde protokolliert werden, damit Sie sehen, wie das Programm aus Sicht des Anwenders funktioniert.
Ich kann Tiere raten! Ist es eine Maus? n Ich gebe auf! Was ist es? Bitte gebe die Antwort mit unbestimmtem Artikel ein, wie beispielsweise 'eine Maus'! ein Lwe Ok, wie unterscheiden sich ein Lwe und eine Maus? Gebe eine Frage an, die mit ja oder nein beantwortet wird. Hat das Tiere eine Mhne? Und wie lautet die Antwort fuer ein Lwe? j Ich kenne nun bestimmt alle Tiere! Hat das Tiere eine Mhne? n Ist es eine Maus? n Ich gebe auf! Was ist es? Bitte gebe die Antwort mit unbestimmtem Artikel ein, wie beispielsweise 'eine Maus'! eine Schlange Ok, wie unterscheiden sich eine Schlange und eine Maus? Gebe eine Frage an, die mit ja oder nein beantwortet wird. Hat das Tier Beine? Und wie lautet die Antwort fuer eine Schlange? n Ich kenne nun bestimmt alle Tiere! Hat das Tiere eine Mhne?

So geht das Spiel nun weiter, bis irgendwann der Strom ausfllt oder ein Benutzer die Tastenkombination Strg + C eingibt.

2.2.5

Was denn noch?

Das Spiel vergisst alle seine Tiere, sobald es einmal beendet wurde. Viel netter wre es natrlich, wenn das Spiel beim nchsten Start noch die Tiere der letzten Runde wsste. Dazu mssen die Fragen und Antworten in eine Datei geschrieben werden. Dabei werden Sie sowohl beim Schreiben als auch beim Lesen nicht um eine Rekursion umhinkommen. So wird es besonders spannend, ob die Tiere auch wieder genau an die Stelle kommen, wo sie beim Sichern gewesen sind. Fr die berprfbarkeit knnte es sich als sinnvoll erweisen, eine Funktion zu schreiben, die den ganzen Binrbaum

43

Der Computer im Dialog

ausgibt. Und Sie ahnen schon, dass auch diese Funktion sinnvollerweise eine Rekursion sein sollte. Prinzipiell wre auch dieses Spiel eine Basis fr eine Eliza. So knnte das Programm abschweifen, Fragen stellen und antworten. Falls das Programm mit der Gesprchsfhrung nicht zurechtkommt und mit den Antworten des Gegenbers gar nichts anfangen kann, kann es leicht wieder auf das Spiel zurckkommen. Falls Sie sich mit der Programmierung eine graschen Oberche auskennen, knnte man das Spiel auch etwas aufpeppen. Vielleicht verwenden Sie auch die Bibliothek wxWidgets, die in anderen Kapiteln des Buches vorgestellt wird. Mit Buttons sieht das Spiel gleich viel schicker aus.

44

Wenn mglich, bitte wenden!

Pfadnder

Bei den Pfadndern wre ich nicht einmal als Briefffner etwas geworden. Mein Orientierungssinn ist uerst mangelhaft. Ich erinnere mich an Fahrten durch Hamburg, in denen ich mehrfach ausgestiegen bin, um die Straenschilder zu suchen. Irgendwer hat sie in fremdenfeindlicher Weise so postiert, dass sie niemand nden kann, und ich wusste nicht, wo ich berhaupt bin. Selbst wenn die kleinen Zwerg-Computer nicht wirklich den krzesten Weg weisen, so nde ich mit ihrer Hilfe immerhin jederzeit heraus, wo ich bin und wie ich in endlicher Zeit nach Hause komme. Damit hat das Mrchen Hnsel und Gretel fr mich jeden Schrecken verloren. Aber woher wissen diese kleinen Dinger, wie ich nach Hause komme? Eine Anbindung an das Internet ist bei den meisten Gerten nicht erforderlich. Also muss es mglich sein, mit der Leistung und den Daten, die in einem Mobiltelefon zur Verfgung stehen, einen krzesten Weg durch das gesamte Land zu nden. Dazu werden Sie Herrn Dijkstra kennenlernen, der das grundlegende Verfahren dazu bereits Ende der 1950er Jahre beschrieben hat. In diesem Kapitel wird ein kleines Programm vorgestellt, das auf diesem Verfahren basiert und aus einem beliebigen Straenplan die krzeste Strecke sucht. Die Tatsache, dass es ber 40 Jahre gedauert hat, bis aus der Entdeckung des Algorithmus ein praktikabler Routenplaner geworden ist, lsst schon erahnen, dass das Hauptproblem darin liegt, dass fr den Einsatz ein kompletter Straenplan in aufbereiteter Form vorliegen muss und dass solche Datenmengen in den frheren Computergenerationen nicht verarbeitet werden konnten. Auch ich kann Ihnen keine solchen Daten zur Verfgung stellen. Aber Sie knnen anhand eines Beispielprogramms den Navis auf die Finger schauen. Dijkstra Das grundlegende Verfahren zum Aufnden des krzesten Weges hat Edsger Wybe Dijkstra bereits zu einer Zeit aufgezeigt, als noch niemand auf den Gedanken gekommen wre, dass man je einen Computer in einem Auto transportieren knnte. Dijkstra gehrt ganz eindeutig zu den wichtigen Pionieren der Informatik. Er konzipierte die Prozesssynchronisation mit Semaphoren und setzte sich

45

Pfadnder

vehement fr die strukturierte Programmierung und damit fr den Kampf gegen den Befehl GOTO ein. In diesem Zusammenhang gibt es von ihm diverse, sprachgewaltige Zitate ber FORTRAN, COBOL und BASIC. Mein Lieblingszitat lautet: Die Verwendung von COBOL verkrppelt den Verstand. Darum sollte der Unterricht darin als kriminelles Delikt betrachtet werden.1 Dijkstra beschreibt einen Algorithmus, der in einem Graphen den krzesten Weg von einem Startpunkt zu jedem beliebigen anderen Punkt sucht und eine Liste aller krzesten Strecken erstellt. Dazu werden vom Startpunkt aus alle angrenzenden Kanten beschritten, um die Nachbarknoten zu nden. Dort wird hinterlegt, wie weit er auf diesem Weg vom Ausgangspunkt ist und welches der Vorgngerknoten war. Dann wird von allen bisher gesehenen Knoten derjenige gewhlt, der am dichtesten vom Ausgangspunkt ist, und dort beginnt das gleiche Spiel. Wurde der Knoten allerdings bereits mit einer besseren Zeit besucht, braucht die Suche nicht fortgesetzt werden. Sind alle Knoten einmal Ausgangspunkt gewesen, endet der Algorithmus. Die Suche eines Navigationsprogramms will nicht alle Stdte untersuchen, sondern wird abbrechen, sobald der Zielort als Ausgangspunkt ausgewhlt wird. Das klingt etwas abstrakt? Das ist es auch. Versuchen wir es mit einem Bild. Farbverlufe Stellen Sie sich das Straennetz als Rhrensystem vor. Nun heben Sie den Startpunkt an und gieen dort Farbe hinein. Die Farbe breitet sich aus und durchiet die Kreuzungspunkte. Nehmen wir weiter an, dass beim ersten Eintreffen der Farbe an einer Kreuzung ein Hebel aktiviert wird, der die Richtung anzeigt, aus der zum ersten Mal Farbe kam. Nun wartet Sie am Zielpunkt auf das Eintreffende der Farbe. Sie verfolgen den Weg zurck, auf dem sie gekommen ist und schauen auf der nchsten Kreuzung, aus welcher Richtung die Farbe kam. Auf diese Weise knnen Sie den Weg zurckverfolgen, der am schnellsten war. Der chinesische Kaiser Die Tchter chinesischer Kaiser haben nicht erst seit Lukas dem Lokomotivfhrer die Phantasie kleiner Jungen entammt. So ist es nicht verwunderlich, dass Tobias Stamm eine hbsche Geschichte erzhlt, die die Entstehung des DijkstraAlgorithmus in dieses Umfeld verlagert. Sie nden sie auf der Seite: http://mandalex.manderby.com/d/dijkstra.php

1 The use of COBOL cripples the mind; its teaching should, therefore, be regarded as a criminal offense. 1975

46

Das Programm sucht die krzeste Verbindung

3.1

Das Mrchen ist sehr lesenswert, anschaulich und mit wundervollen Anspielungen durchsetzt. Dort erzhlt Tobias Stamm, dass eine Liste aufgestellt werden soll, die die krzesten Wege von der Hauptstadt zu jedem beliebigen Ort enthalten soll. Die Lsung wird dadurch erbracht, dass von der Hauptstadt aus ein Bote zu jeder Nachbarstadt aufbricht und auf einem Zettel hinterlsst, wie weit er zur Nachbarstadt gelaufen ist. Dort wird die Liste kopiert, und es laufen Boten zu jeder weiteren Nachbarstadt. Wurde diese bereits besucht, hat sich die weitere Suche erledigt. Zum Schluss werden alle Listen in die Hauptstadt gebracht, und der Kaiser wei, auf welchem Weg er jeden Ort seines Reichs am schnellsten erreicht.

3.1

Das Programm sucht die krzeste Verbindung

Die Beschreibung beginnt am sinnvollsten mit der zentralen Funktion. Von hier aus stellen wir fest, welche Daten und Funktionen noch bentigt werden. Die Suche nach dem krzesten Pfad bernimmt die Funktion suchePfad. Aufrufparameter Sie erhlt als Parameter den Start- und den Zielknoten. Die Knoten sind durchnummeriert und folgen direkt aufeinander. Im dritten Parameter gibt die Funktion die gefundene Strecke zurck. Prinzipiell handelt es sich um eine Folge von Knotennummern. Dazu knnte man ein Array verwenden. Der Nachteil ist, dass nicht abschtzbar ist, wie lang dieses Array wird. Das Array wrde sehr viel Speicher verschwenden, obwohl in der Regel nur ein Bruchteil bentigt wird. In solchen Fllen denkt der C-Programmierer an eine verkettete Liste. Diese braucht allerdings fr jede gespeicherte Nummer einen eigenen Zeiger, um auf das nchste Element zu zeigen. Auerdem macht die Verwaltung der verketteten Liste das Programm etwas unbersichtlich. Der C++Programmierer ndet hier eine ideale Anwendung der STL-Container. Die STL gehrt zum Sprachumfang von C++ und stellt vor allem Speicherstrukturen zur Verfgung. Der Pfad wird in einem Vektor gespeichert. Man kann leicht hinten neue Elemente anhngen. Ansonsten verhlt er sich wie ein Array. Der vierte Parameter liefert an den Aufrufer die Gesamtlnge des gefundenen Pfades zurck. Selbstverstndlich knnte man hier auch eine Referenz verwenden. Ich empnde die Verwendung eines Zeigers deshalb glcklicher, weil fr den Aufruf eine Adresse bergeben werden muss und so aus dem Listing erkennbar ist, dass die Variable durch den Aufruf verndert wird. Das ist aber kein Dogma, sondern Geschmacksache.

47

Pfadnder

int suchePfad (int Aktuell, int Ziel, vector<int> &Pfad, long* GesamtDistanz) { map<int, tKnoten> PrioQueue; int i; tAdjazenz *adj = tAdjazenz::getInstance(); *GesamtDistanz = 0 ; // Startknoten hat keinen Vorgaenger und keine Distanz PrioQueue[Aktuell].Markiert = true; PrioQueue[Aktuell].Distanz = 0; // Solange wir noch nicht angekommen sind... while (Aktuell!= Ziel) { // In der Adjazenzmatrix wird nach Nachbarn gesucht for (i=1; i<=adj->getMaxKnoten(); i++) { long NachbarnDistanz = adj->get(Aktuell, i); if (NachbarnDistanz != unendlich) { // Wurde Knoten noch nicht markiert? if (PrioQueue[i].Markiert == false) { // ermittle die Entfernung zzgl der // bisherigen Distanz zum Start long NeueDistanz = PrioQueue[Aktuell].Distanz + NachbarnDistanz; // Kleiner? Dann ist das die bessere Strecke if (NeueDistanz < PrioQueue[i].Distanz) { PrioQueue[i].Vorgaenger = Aktuell; PrioQueue[i].Distanz = NeueDistanz; } } } } // Nachbarnsuche beendet
Listing 3.1 Suche die Nachbarn

In einer ersten groen Schleife sucht die Funktion fr den aktuellen Ausgangspunkt alle Nachbarknoten und prft, ob sie erstens unmarkiert sind und ob sie zweitens nicht bereits auf einem anderen Weg schneller erreichbar sind. Die Markierung erhlt jeder Knoten, der einmal als Ausgangsknoten verwendet wurde. Im ersten Schritt ist dies also nur der Startknoten. Fr die Berechnung der Distanz zwischen den Knoten muss die Funktion jemanden fragen, der sich mit so etwas auskennt. Dazu muss das Programm eine Datenstruktur besitzen, in der die Distanz der benachbarten Knoten gespeichert wird. Diese Datenstruktur wird Adjazenzmatrix genannt und ab Seite 54 ausfhrlicher behandelt. Sie enthlt im Grunde den Straenplan und damit auch die Information, ob es eine direkte Verbindung zwischen den Knoten gibt.

48

Das Programm sucht die krzeste Verbindung

3.1

Die Distanz zwischen dem aktuellen Ausgangsknoten und dem Nachbarn wird in der Variablen NachbarDistanz festgehalten. Priority Queue Jeder dieser Nachbarn wird auerdem in einer lokalen Liste eingetragen, die meist Ereignisliste oder Priority Queue genannt wird. Beide Namen sind nicht besonders aussagekrftig. Im Programm heit diese Liste PrioQueue und schiet damit auch nicht gerade den Vogel der Originalitt ab. In diese Liste werden nacheinander alle Knoten eingetragen, die vom Startpunkt aus direkt oder mittelbar erreichbar sind. Dort wird hinterlegt, wie weit sie vom Startknoten entfernt sind und ber welchen Knoten man sie erreicht hat. Schlielich gibt es noch eine Markierung, die besagt, ob sie bereits als Ausgangspunkt verwendet worden sind. Gleichzeitig steht die Markierung auch dafr, dass diese Entfernung entgltig der krzeste Weg zu diesem Knoten ist. Fr jeden Nachbarn wird geprft, ob er bereits auf einem anderen Wege erreicht worden ist und ob er auf jenem Wege oder auf diesem Wege schneller erreicht werden kann. Sollte dieser Weg der bessere sein, wird die Distanz und der Vorgnger des Nachbarn gendert. Ihnen ist sicher nicht entgangen, dass auch die PrioQueue ein STL-Container ist. Hier handelt es sich um eine Map. Eine Map indiziert ihre Daten ber einen Schlssel. Als Schlssel wird die Knotennummer verwendet. Sie knnen den Schlssel syntaktisch wie den Index bei einem Array verwenden. Allerdings wird bei einem solchen Zugriff automatisch ein Element fr diesen Schlssel angelegt, wenn es bisher noch nicht existierte. In diesem Fall ist dieses Verhalten sogar recht praktisch, weil wir sowieso ein Element anlegen mssen, wenn es in der Adjazenzmatrix einen Hinweis auf Nachbarschaft gibt. Nachdem um den Ausgangspunkt herum alle Nachbarn mit neuen Distanzen versehen wurden, kommt nun der Moment, einen Nachfolger fr den Ausgangspunkt zu suchen, der dessen wrdige Nachfolge antritt. Das kann nicht irgendein Knoten sein, sondern es muss immer der Knoten sein, der noch nie Ausgangspunkt war, der aber unter den mglichen Ausgangspunkten den krzesten Abstand vom ursprnglichen Startpunkt hat. Das ist wichtig, weil dem Programm ansonsten eine Abkrzung entgleitet, die zwar ber viele kleine Straen fhrt, aber aufgrund der Krze der Straen doch den krzesten Weg bildet. Dazu mssen nun alle Elemente der Priority Queue ausgelesen werden. Ein Zugriff ber den Index wre an dieser Stelle kontraproduktiv, stattdessen wird das Standardverfahren verwendet, das bei allen STL-Containern funktioniert. Der Container wird mithilfe eines Iterators durchsucht. Ein Iterator verhlt sich wie ein Zeiger. Er wird durch den Rckgabewert der Elementfunktion begin initiali-

49

Pfadnder

siert und dann inkrementiert. Sobald er gleich dem Rckgabewert der Funktion end wird, bendet er sich auerhalb des Datenbereichs. Darum beginnt ein Lauf durch einen STL-Container typischerweise mit zwei Zeilen wie diesen:
map<int, tKnoten>::iterator it; for (it=PrioQueue.begin(); it!=PrioQueue.end(); ++it) {

Aus den Elementen der Priority Queue wird jenes mit der kleinsten Distanz herausgesucht. Diejenigen Elemente, die bereits einmal als Ausgangspunkt gedient haben, tragen eine Markierung und drfen nicht ausgewhlt werden.
// Aus den unmarkierten der PrioQueue wird der // naechstgelegene Ausgangspunkt ermittelt long min = unendlich; Aktuell = 0; // ungueltig machen // Durch alle Elemente der PrioQueue laufen map<int, tKnoten>::iterator it; for (it=PrioQueue.begin(); it!=PrioQueue.end(); ++it) { i = it->first; // Index ermitteln if (PrioQueue[i].Markiert == false && PrioQueue[i].Distanz<min) { // bisher der naechste von der Startposition min = PrioQueue[i].Distanz; Aktuell = i; } } // Wenn Quelle oder Ziel isoliert, ist hier Ende if (Aktuell == 0) { return 0; } // Ansonsten wird der neue Kandidat markiert PrioQueue [Aktuell].Markiert = true; } // solange wir nicht am Ziel sind...
Listing 3.2 Suche die Nachbarn

Wurde kein unmarkierter Knoten mehr gefunden, dann hat die Variable Aktuell immer noch den Wert 0. Das ist ein Zeichen, dass das Ziel nicht erreicht wurde. Da aber alle erreichbaren Knoten abgeklappert wurden, gibt es dann wohl auch keine Verbindung zwischen Start und Ziel. Andernfalls wird dieser Knoten als aktueller Ausgangsknoten markiert und fr die nchste Runde verwendet. Im Schleifenkopf wird geprft, ob der Knoten das Ziel ist. Dann wird die Schleife beendet. Da der Knoten immer das Element ist, das

50

Das Programm sucht die krzeste Verbindung

3.1

am dichtesten am Startknoten ist und noch nicht markiert wurde, kann es keinen Weg zu diesem Knoten mehr geben, der krzer ist als der bisher gefundene. Rckschritte Der Zielknoten ist gefunden und nun lohnt es sich, dass die Funktion immer schn die Vorgngerknoten notiert hat. Der Zielknoten kann feststellen, wer sein Vorgnger war. Der wiederum kennt seinen Vorgnger. Auf diese Weise ist es ganz einfach, eine Liste der Knoten zu erstellen, die passiert wurden.
// Ermittle den Pfad vom Ziel zum Startpunkt int SchrittZahl = 0; while (Aktuell!= 0) { SchrittZahl++; Pfad.push_back(Aktuell); Aktuell = PrioQueue[Aktuell].Vorgaenger; } // Ermittle die Distanz von der Quelle bis zum Ziel int KantenAnfang, KantenEnde; for (i=0; i<SchrittZahl-1; i++) { KantenEnde = Pfad[i]; KantenAnfang = Pfad[i+1]; *GesamtDistanz += adj->get(KantenAnfang, KantenEnde); } return SchrittZahl; }
Listing 3.3 Krzesten Pfad absammeln

Zum Schluss wird noch die Gesamtdistanz ermittelt und die Anzahl der Knoten zurckgegeben. Durchgespielt Wie das Verfahren arbeitet, lsst sich an einem Beispiel durchspielen. Wir konstruieren einen etwas hinterlistigen Straenplan mit vier Knoten. Knoten 1 ist der Startknoten, Knoten 2 der Zielknoten. Es gibt einen Weg zu Knoten 2 von 15 km. Von Knoten 1 zu Knoten 3 ist es 1 km. Von Knoten 3 zu 4 ist es wiederum 1 km, und auch die Entfernung zwischen Knoten 4 und Knoten 2 betrgt 1 km. So gibt es zwei Wege von Knoten 1 zu Knoten 2. Der direkte Weg betrgt 15 km, und der Weg ber die Knoten 3 und Knoten 4 betrgt insgesamt 3 km. Diese Strecke sollte ein gutes Navigationssystem also vorschlagen.

51

Pfadnder

Abbildung 3.1 Suche den krzesten Weg von 1 nach 2

Der erste Schritt ist, dass der Startpunkt 1 als Ausgangspunkt ausgewhlt wird und so in die Priority Queue eingetragen wird. Seine Entfernung zum Startpunkt ist 0. Er wird seine Nachbarn Knoten 2 und Knoten 3 ebenfalls, aber unmarkiert, in die Queue eintragen. Diese Situation ist in der folgenden Tabelle dargestellt:
Markierte Knoten 1 (Entfernung: 0, Vorgnger: 0) Unmarkierte Knoten 2 (E: 15, V: 1), 3 (E: 1, V: 1)

Tabelle 3.1 Priority Queue nach Abarbeitung des Startknotens

Nun wird der Nachfolger des Startknotens ausgewhlt. Dazu kommen nur Knoten 2 und Knoten 3 infrage. Der mit der krzeren Entfernung ist eindeutig Knoten 3. Er kann lediglich Knoten 4 mit der Entfernung 1 erreichen. Knoten 4 erhlt also die Entferung 2 als Summe der Strecke zwischen 3 und 4 und die Entfernung, die Knoten 3 vom Startknoten hat.
Markierte Knoten 1 (E: 0, V: 0), 3 (E: 1, V: 1) Unmarkierte Knoten 2 (E: 15, V: 1), 4 (E: 2, V: 3)

Tabelle 3.2 Priority Queue nach Abarbeitung des Knotens 3

Wieder wird ein Nachfolger gesucht und wieder ist es nicht der Knoten 2, da Knoten 4 eine wesentlich geringere Entfernung zum Startknoten hat. Bei den Nachbarn von Knoten 4 taucht nun noch einmal der Knoten 2 auf. Er war zwar bereits von Knoten 1 einmal erreicht worden, aber noch nicht markiert. Die Entfernung ber Knoten 4 ist mit 3 Einheiten aber deutlich geringer als die 15 Einheiten ber Knoten 1, also wird dieser Wert ersetzt. Auch der Vorgnger wird nun gendert. Der bessere Wert wird ja ber Knoten 4 erreicht.

52

Datenmodellierung

3.2

Markierte Knoten 1 (E: 0, V: 0), 3 (E: 1, V: 1), 4 (E: 2, V: 3)

Unmarkierte Knoten 2 (E: 3, V: 4)

Tabelle 3.3 Priority Queue nach Abarbeitung des Knotens 4

In dieser Situation ist nun Knoten 2 der nchste Kandidat als Nachfolger und wird als Ausgangsknoten ausgewhlt. Da er auch der Zielpunkt ist, wird damit die Suchschleife abgebrochen. Egal, wie viele Knoten noch in der Adjazenzmatrix liegen sollten, sie wrden nicht in die Priority Queue eingetragen werden. Alle anderen Wege wrden auf jeden Fall lnger sein.

3.2

Datenmodellierung

Nachdem der Lsungsablauf klar ist, muss die Datenstruktur etwas ausfhrlicher beschrieben werden. Zunchst betrachten wir die Nomenklatur, die wir bisher intuitiv verwendet haben. Eine Strae wird in der Graphentheorie als Kante, eine Kreuzung als Knoten bezeichnet. Dabei unterscheidet man zwischen einer gerichteten und einer ungerichteten Kante. Fr den Straenplan verwendet man nur gerichtete Kanten. Immerhin existieren Einbahnstraen. Soll zwischen zwei Knoten der Verkehr in beide Richtungen ieen knnen, mssen eben zwei Kanten angelegt werden.

3.2.1

Knoten

Der Knoten wird als Datenstruktur nur in der Priority Queue bentigt. Dort wird fr jeden Knoten gespeichert, ob er bereits markiert wurde. Bei seiner Initialisierung darf er natrlich noch nicht markiert sein. Dann gibt es den Eintrag Distanz, der angibt, wie weit dieser Knoten vom Ausgangspunkt entfernt ist. Wurde der Knoten noch nicht angesprochen, steht die Distanz auf unendlich. Leider kann sich der Computer keinen Begriff von der Unendlichkeit machen, so beschrnken wir uns darauf, den maximalen Zahlenwert einzusetzen, den der Typ erlaubt. Um spter den krzesten Pfad zu nden, enthlt der Knoten die Information, ber welchen Vorgngerknoten der Besucher gekommen ist. Bei der Initialisierung wird dieser Wert auf 0 gesetzt.
#define unendlich LONG_MAX class tKnoten { public: tKnoten();

53

Pfadnder

int Vorgaenger; long Distanz; bool Markiert; }; tKnoten::tKnoten() { Vorgaenger = 0; Distanz = unendlich; Markiert = false; }
Listing 3.4 Die Klasse tKnoten

3.2.2

Kanten

Die Kanten besitzen als Eigenschaft vor allem eine Lnge. Dabei sollte die Lnge abstrakt gesehen werden. Es kann durchaus sein, dass es sich nicht um eine Entfernung handelt, sondern um die Zeit, die bentigt wird, um die Kante abzufahren. Aus diesem Grund spricht man auch nicht von der Lnge, sondern von der Gewichtung der Kante. Darber hinaus hat die Kante einen Start- und einen Endknoten. Die Kante hat damit eine Richtung. Sind die Knoten in beiden Richtungen verbunden, werden zwei Kanten angelegt. Um diese Informationen zu speichern, gibt es aber keinen Datentyp Kante. Stattdessen werden diese Informationen in der Adjazenzmatrix abgelegt, die die Verbindung aus Kanten und Knoten darstellt.

3.2.3

Adjazenzmatrix

Die Adjazenzmatrix sorgt dafr, dass die Gewichtung jeder Kante ber Start- und Endknoten zugreifbar ist. Die Gewichtung lsst sich bei reinen Entfernungen einfach in einem Wert von Typ long festhalten. Die Adjazenzmatrix knnte als zweidimensionales Array implementiert werden. Der eine Index ist der Startknoten und der andere der Endknoten. Allerdings ist die Matrix typischerweise dnn besetzt, sodass fr wenig Kanten ein erheblicher Speicher verschleudert wird. Dieses Problem berlsst das Programm dem STLContainer map.
class tAdjazenz { public: static tAdjazenz *getInstance(); int get(int, int); void set(int index1, int index2, int Wert);

54

Datenmodellierung

3.2

long getMaxKnoten() { return MaxKnoten; } private: tAdjazenz() { MaxKnoten=0; } static tAdjazenz *adjazenz; map<tKnotenpaar, int> adj; long MaxKnoten; };
Listing 3.5 Die Klasse tAdjazenz

Die Klasse liefert ansonsten nur eine Funktion zum Eintragen und eine Funktion zum Auslesen eines Wertes. Als Schlssel dient ein Knotenpaar, das als eigene Klasse implementiert wird. Da es nur eine Adjazenzmatrix gibt, ist es sinnvoll, sie als Singleton zu implementieren. Eine Singleton-Klasse sorgt dafr, dass es nur eine Instanz der Klasse gibt. Dazu wird der Konstruktor privatisiert. Damit kann ihn nur noch eine Elementfunktion der Klasse aufrufen.
tAdjazenz* tAdjazenz::adjazenz = 0; tAdjazenz* tAdjazenz::getInstance() { if (adjazenz == 0) { adjazenz = new tAdjazenz(); } return adjazenz; }
Listing 3.6 Singleton

Es wird eine statische Zeigervariable angelegt, die mit 0 initialisiert wird. Und in einer statischen Elementfunktion wird geprft, ob die Zeigervariable bereits initialisiert wurde. Wenn nicht, wird sie angelegt. Die Funktion liefert immer einen Zeiger auf die einzig existierende Instanz. Auf diese Methode kann jeder, der den Singleton braucht, die Funktion getInstance aufrufen. Das Objekt wird genau dann erzeugt, wenn es zum ersten Mal bentigt wird. Es kann nicht passieren, dass durch Umstellen der Aufrufreihenfolge eine Funktion mit einer nicht initialisierten Adjazenzmatrix arbeitet. STL-Container map Damit die nur dnn besetzte Matrix nicht unntig viel Platz verbraucht, hilft die Standard Template Library (STL) mit dem Container map. Die map speichert Daten mit einem zugehrigen Schlssel ab, ber den sie spter schnell wiedererlangt

55

Pfadnder

werden knnen. Der Zugriff ist zwar sehr schnell, wenn auch nicht so ott wie bei einem Array-Zugriff. Ein kleines Problem besteht darin, dass die map als Schlssel normalerweise nur einen Wert akzeptiert, fr den der Kleiner-Operator deniert ist. Da hier die beiden Knotennummern als Index verwendet werden sollen, ist der Kleiner-Operator nicht deniert. Aber glcklicherweise lsst sich dieser in C++ ja leicht berladen. Knotenpaar Die Klasse tKnotenpaar hat nur die Aufgabe, einen Schlssel aus zwei Integern fr die map der Adjazenzmatrix zu ermglichen. Sie nimmt also lediglich zwei Integer auf und deniert fr diese eine eindeutige Reihenfolge, die mit dem KleinerOperator abgefragt werden kann, damit die map glcklich ist.
class tKnotenpaar { public: tKnotenpaar(int a, int b) { ind1 = a; ind2 = b; } bool operator<(const tKnotenpaar a) const { if (ind1<a.ind1) return true; if (ind1==a.ind1 && ind2<a.ind2) return true; return false; } private: int ind1; int ind2; };
Listing 3.7 Die Klasse tKnotenpaar

Die Reihenfolge der Knotenpaare wird durch die erste Knotennummer bestimmt. Ist diese gleich, wird die zweite Knotennummer herangezogen. Letztlich ist die Reihenfolge egal, sie muss nur eindeutig sein. Als Beispiel ist das an der Funktion get zu erkennen. Aus den zwei Koordinaten, wird ber den Konstruktor von tKnotenpaar ein Objekt erzeugt, das beim Aufruf der Elementfunktion find an die map gereicht wird.
long tAdjazenz::get(int index1, int index2) { tKnotenpaar adr(index1, index2); if (adj.find(adr)==adj.end()) { return unendlich; } else { return adj[adr]; } }
Listing 3.8 Die Funktion get der Adjazenzmatrix

56

Einlesen der Graphen

3.3

3.3

Einlesen der Graphen

Die Funktion bauStrassenNetz hat die Aufgabe, die Adjazenzmatrix mit Daten zu fllen, die spter fr die Suche nach der besten Route verwendet werden. Im Falle einer Routenberechnung muss man fr die Testlufe erst einmal ein Straennetz aufstellen und wie immer, wenn die Testdaten leicht umfangreich werden, stellt man sich die Frage, wie man die Daten in das Programm bekommt, ohne sich einen Wolf zu tippen. Es gibt da drei Varianten. Die erste deniert die Testdaten als Konstante im Programm. Bei nderungen geht man ins Programm, ndert die Daten, kompiliert und versucht es noch einmal. Die zweite Variante ist, dass man eine Datei mit Testdaten einliest, die man mit einem Editor schnell ndern kann. Das ist schon praktischer, erfordert aber immer eine Funktion fr das Einlesen der Daten. Und dann gibt es noch die Methode fr Faulenzer. Man schreibt ganz normale Eingabebefehle. Das kann man anfangs von der Tastatur eingeben. Und sobald man keine Lust mehr dazu hat, schreibt man die Eingabe in eine Textdatei und bergibt diese mit dem Kleinerzeichen an das Programm. Raten Sie mal, welche Variante ich gewhlt habe.
void bauStrassenNetz () { int i=1, Herkunft, Ende; long Laenge ; cout << endl; tAdjazenz *adj = tAdjazenz::getInstance(); while(true) { cout << "Gebe Kante " << i << " ein (0 0 um die Eingabe zu beenden): "; cin >> Herkunft; cin >> Ende; cout << endl; if ((Herkunft == 0) && (Ende == 0)) break; cout << "Gebe die Laenge dieser Kante an: "; cin >> Laenge; if (Herkunft == 0 || Ende == 0) { cout << "Ungueltige Kante!" << endl; } else { adj->set(Herkunft, Ende, Laenge); cout << endl << "von " << Herkunft << " nach " << Ende << " Distanz: " << Laenge << endl << endl;

57

Pfadnder

} i++; } }
Listing 3.9 Die Funktion bauStrassenNetz

Sie sehen, dass die Funktion nicht besonders fehlertolerant auf die Eingaben reagiert. Wenn Sie also versuchen, den Kantengraphen von Hand einzugeben, werden Sie ausreichend Gelegenheit nden, den Autor dieser Zeilen zu veruchen. Da es dabei mich treffen wrde, empfehle ich, eine Textdatei zu entwerfen und diese als Eingabe zu verwenden.
./dijkstra < input.txt

Sie knnen natrlich auch das Programm ergnzen und eine Textdatei auslesen, die die Daten enthlt. Bevor Sie allerdings allzu viel Zeit in dieses Projekt stecken, sollten Sie bedenken, dass die Straendaten bei professionellen Systemen in Datenbanken stehen. Sollten Sie also Greres planen, wre es vielleicht klug, gleich diesen Ansatz zu verfolgen. Fr das oben vorgestellte Beispiel wrde die Eingabedatei input.txt folgendermaen aussehen:
1 1 3 4 0 1 2 0 2 3 4 2 0 15 1 1 1

3.4

Das Hauptprogramm

Das Hauptprogramm sammelt alle Brckchen zusammen und bringt sie in die richtige Reihenfolge. Die Daten des Straennetz werden eingelesen. Dann wird zur Kontrolle noch einmal die Adjazenzmatrix angezeigt. Und dann kann der Benutzer beliebig oft Start und Ziel eingeben und erhlt die krzeste Strecke angezeigt.
int main() { bauStrassenNetz(); zeigeAdjazenzMatrix();

58

Betrachtungen ber die Straendaten

3.5

for (;;) { // forever: bis Benutzer abbricht int Quelle, Ziel; cout << endl; cout << "Gebe den Startpunkt an (0 = Abbruch):"; cin>> Quelle; if (Quelle == 0) exit (1); cout << endl; cout << "Gebe den Zielpunkt an (0 = Abbruch):"; cin>> Ziel; if (Ziel == 0) exit (1); vector<int> Pfad; // Der kuerzeste Pfad long KuerzesteDistanz; int SchrittZahl; SchrittZahl = suchePfad (Quelle, Ziel, Pfad, &KuerzesteDistanz); if (KuerzesteDistanz != 0) { cout << "Die kuerzeste Strecke ist: " << KuerzesteDistanz << endl; cout << "Der kuerzeste Pfad ist:"; int i; for (i=SchrittZahl-1; i>0; i--) { cout << Pfad[i] << " "; } cout << Pfad[i] << endl; } else { cout << "Es gibt keinen Weg vom Start zum Ziel" << endl; } } }
Listing 3.10 Das Hauptprogramm

3.5

Betrachtungen ber die Straendaten

Bevor die Suche nach dem krzesten Weg beginnt, mssen zunchst die Daten ber das Straennetz vorliegen. Da sind die kleinen Testgrafen natrlich nicht aufregend. Wenn Sie beabsichtigen, in der Pro-Liga mitzuspielen, brauchen Sie umfangreiche Daten. Selbst wenn Sie nur den Huserblock, in dem Sie wohnen, erfassen wollen, werden Sie einiges zu tippen haben, bevor Sie eine krzeste Route gewinnen.

59

Pfadnder

Schnell kommen Sie auf den Gedanken, dass auch schon andere Programmierer vor diesem Problem gestanden haben mssen. Denn irgendwie sind all die vielen Straen ja auch in all die Navigationsgerte bei Aldi gekommen. Und wenn Sie das Kleingedruckte in den Packungsbeilagen lesen, die Ihrem Arzt und Ihrem Apotheker brigens vllig egal sind, werden Sie feststellen, dass es Anbieter fr solche Daten am Markt gibt und wenn Sie auch all die anderen Anbieter betrachten, die nicht bei Aldi anbieten, werden Sie merken, dass es wesentlich weniger Anbieter fr Daten als fr Navigationsgerte gibt. Der Grund liegt in der Komplexitt der Daten. Sie brauchen die Lnge und Koordinaten jeder Strae, die Einbahnstraen, Abbiegevorschriften und alle Streckenbeschrnkungen. Eine Strecke, die ein Fahrradfahrer benutzen kann, steht nicht unbedingt jedem LKW zur Verfgung. Andererseits sind die meisten Fahrradfahrer entrstet, wenn sie auf die Autobahn gefhrt werden. All diese Informationen stecken in diesen Daten. Dennoch gibt es auch eine Quelle fr nichtkommerzielle Daten. Diese sind entstanden, weil eine Gruppe von Technikbegeisterten auf den Gedanken kam, ihre GPS-Gerte dazu zu nutzen, um freie Daten zu sammeln und per Internet zur Verfgung zu stellen. Auch das ist eine Mglichkeit, huger an die frische Luft zu kommen, ohne sich gleich einen Hund anschaffen zu mssen. Es entstand das Projekt Open Street Map: http://www.openstreetmap.org Hier werden die Daten von einem Heer von Freiwilligen gesammelt, die auf ihren Wegen das GPS-Gert mitlaufen lassen. Andere erfassen die lokalen Besonderheiten der Wege wie die Ortsnamen, Straennamen oder wichtige Gebude. Und obwohl die angezeigten Karten schon sehr beeindruckend sind, fehlen aber noch entscheidende Daten, um damit eine verlssliche, umfassende Navigation zu gewhrleisten. So fehlen beispielsweise noch einige Informationen um Norgaardholz-Sd herum. Aber vielleicht mchten Sie ja die erste freie Navigationssoftware fr die freien Daten schreiben. Vielleicht sind die Daten genau dann vollstndig, wenn Ihr Programm fertig ist. Ein weiterer Aspekt, den Sie bercksichtigen sollten, bevor Sie mit den Originaldaten spielen wollen, ist die Datenmenge, die es zu verarbeiten gibt. In der Bundesrepublik gibt es mehrere Millionen Kreuzungen und in etwa gleich viele Straen. Sie mssen die Lnge der Straen, die Namen und die Geschwindigkeitsbeschrnkungen speichern. Sie mssen die Verknpfungen zwischen den Straen herstellen und beim Ermitteln der Route Kreuzungen markieren, an denen Sie bereits gewesen sind.

60

Navigationssysteme

3.6

Da auch die Rechenzeit mit dem Quadrat der Eingabedaten steigt, werden Sie verstehen, dass in diesem Buch nur mit Spieldaten operiert wurde.

3.6

Navigationssysteme

Vielleicht haben Sie schon einmal zwei Navigationsgerte verschiedener Anbieter gegeneinander antreten lassen und waren berrascht, dass die Ergebnisse so gar nicht zusammenpassten. Vielleicht ist Ihnen aber auch schon einmal aufgefallen, dass Ihr Gert jedesmal ber einen Riesenumweg fhrt, wo es doch so eine prima Abkrzung gibt. Die Ursache solcher Probleme liegen in der Speichergre und der Rechenleistung, die bei den tragbaren Gerten zur Verfgung steht. Um die Anzahl der mglichen Wege zu reduzieren, kann ein solches System beispielsweise die Route nur auf den Autobahnen und Bundesstraen berechnen. Dann wird der Weg von Start und Ziel zur nchsten Bundesstrae berechnet. Diese Route ist relativ schnell berechnet, aber eben ungenau. Zieloptimierung Der krzeste Weg ist allerdings nicht immer der gewnschte. Ich hatte einmal mein Navigationsgert den krzesten Weg zwischen Hamburg und Flensburg suchen lassen. Das Ergebnis war, dass ich mitten in der Nacht auf jeden Feldweg geschickt wurde, der der Vogeluglinie am Nchsten kam. Als ich pltzlich an einer Fhre ber den Nord-Ostsee-Kanal stand, wusste ich, dass ich das so schnell nicht wiederholen wrde. Zwar war der Weg sicherlich der krzestmgliche. Aber durch die vielen kleinen Wege war er weder schnell noch angenehm zu fahren. Und da ich stndig abbremsen und beschleunigen musste, war nicht einmal der Verbrauch optimal. Statt die Kanten mit der Lnge zu belegen, verwendet man die Zeit, die bentigt wird, um die Strecke zurckzulegen. Diese Zeit kann man wiederum knstlich verlngern, wenn die Strecke besonders kurvenreich ist oder eine Geschwindigkeitsbegrenzung vorliegt. Bei zweispurigen Straen wiederum kann man die Zeit verkrzen. Auf diese Weise ist es mglich, eine Strecke nden zu lassen, die der Autofahrer als angenehm empndet.

3.7

Was denn noch?

Bisher gingen wir davon aus, dass Straen zu jeder Zeit gleiche Fahrgeschwindigkeiten ermglichen. In jeder Grostadt kann man selbst in der Nhe des Haupt-

61

Pfadnder

bahnhofs um 3 Uhr nachts so schnell fahren, dass man seinen Fhrerschein fr einige Monate verlieren kann. Dagegen wird man um 8 Uhr morgens von jedem beliebigen Fahrradfahrer abgehngt. Dafr braucht man sich ber Radarfallen keine Gedanken zu machen. Eine Erweiterung des Algorithmus um die Uhrzeit ist auch interessant, wenn man statt Straenlngen den Fahrplan des ffentlichen Nahverkehrs integriert. In diesem Fall knnen neben der Zeit und der Entfernung auch die Kosten eine Rolle spielen. Dabei sollten Sie darauf achten, dass auch der Fumarsch nicht kostenfrei ist. Whrend das Abwetzen der Sohlen wohl nicht relevant ist, knnte eine allzu lange Wanderung durch die Verpegung, die Hotelkosten und den Verdienstausfall doch teurer werden als eine Fahrkarte. Es ist auch sehr interessant zu berlegen, wie das Programm erweitert werden knnte, um der Praxis zu gengen. Wie wrden Sie in dem Programm Straennamen unterbringen? Wie msste die Funktion umgeschrieben werden, wenn der Anfangs- und Zielpunkt nicht auf einer Kreuzung, sondern inmitten einer Kante liegt? Auch Optimierungsfragen sind hochinteressant. Welche Datenstrukturen mssen exibel sein? Sind die hier verwendeten Container wirklich optimal? Whrend bei anderen Programmen eine Optimierung meist mehr dem Vorschlagendem dient, sind bei Millionen von Knoten schon geringfgige Verbesserungen von erstaunlicher Wirkung. Zu guter Letzt mchte ich Ihnen nicht vorenthalten, dass in der freien BoostBibliothek auch ein Dijkstra-Algorithmus zur Verfgung steht: http://www.boost.org/doc/libs/1_38_0/libs/graph/doc/dijkstra_shortest_paths.html Das Hauptproblem bei dieser Verwendung der Boost-Funktion ist es, die Daten aufzubereiten, die der Funktion zugefhrt werden. Im Normalfall errechnet diese Funktion den Graphen mit den krzesten Verbindungen fr den Eingabegraphen vom Startpunkt zu allen anderen Knoten aus. Wenn Sie aber den krzesten Weg von einem Start- zu einem Zielpunkt ermitteln wollen, sollten Sie sich noch mit dem Visitor-Konzept befassen. Es wrde zu weit fhren, wenn ich an dieser Stelle dieses Konzept komplett erlutere. Aber soviel darf ich vielleicht andeuten: Der Visitor wird von der Funktion mitgenommen und immer dann gerufen, wenn eine bestimmte Aktion erfolgt. Hier mssten Sie schauen, dass Sie den Visitor erwischen, der bei der Wahl des Zielpunktes als Auswahlknoten informiert wird. An dieser Stelle mssten Sie das Weiterlaufen stoppen.

62

Besonders der freie Fall eignet sich prchtig zum Spielen. Einerseits sind die physikalischen Gesetze leicht verstndlich. Andererseits steckt wohl in uns allen eine gewisse Hhenangst, die man leicht kitzeln kann.

Spiele mit der Physik

Der erste Computer, den ich je gesehen habe, war eine Olivetti P652. Es war das Heiligtum unserer Schule, besa 2 KByte RAM, wurde mit 256 Byte Magnetstreifen gefttert und hatte keinen Bildschirm. Am Hauptgert in der Gre eines kleineren Khlschranks gab es eine etwas erweiterte Zahlentastatur und einen Kassenstreifen. Damit wurden die Programme eingegeben und ausgedruckt.

Abbildung 4.1 Olivetti P652 (Foto: Serge Devidts)

Ich war der festen berzeugung gewesen, alle diese Gerte wren inzwischen den Weg allen Schrottes gegangen, doch Serge Devidts hat nicht nur ein Exemplar gerettet, sondern stellt es auch auf seiner Website1 zur Schau. Er hat mir

1 http://www.devidts.com/be-calc/desk_17784.html

63

Spiele mit der Physik

freundlicherweise erlaubt, das Foto in Abbildung 4.1 in diesem Buch zu verwenden. Einen Monitor gab es wie gesagt nicht. Dafr besa die Schule den Luxus einer elektrischen Typenhebelschreibmaschine. Unter jeder Taste befand sich ein elektrischer Magnet, der die Taste auslsen konnte und mit dem Tastendrcke erfasst wurden. So konnte diese elektrische Schreibmaschine fr Ein- und Ausgabe von Texten verwendet werden. Allzu lang wurden die Texte allerdings nicht, wenn das Speichermedium maximal 256 Byte aufnahm. Aber auch in dieser Umgebung war es mglich, Spiele zu programmieren. Ich mchte Ihnen zwei Programme vorstellen, die auch in dieser Umgebung noch gelaufen wren. Die Spiele bentigen weder Monitor noch Joystick, sondern nur die Mglichkeit, Zahlen einzugeben und Werte nacheinander auszugeben wenn es sein muss sogar auf einem Kassenzettel.

4.1

Die Mondlandung

Die Mondlandung ist ein Klassiker der Computerspiele, das auf der Anziehungskraft des Mondes basiert. Die Idee ist simpel. Die Landung einer Mondlandefhre wird durch Raketen abgebremst, deren Treibstoff wie alles im Leben begrenzt ist. Die Anziehungskraft des Mondes beschleunigt die Fhre auf immer grere Geschwindigkeit. Der Spieler muss zum richtigen Zeitpunkt mit der richtigen Intensitt die Bremsraketen znden. Bremst er zu frh, ist der Sprit verbraucht, bevor es ernst wird. Bei zu viel Brennstoff wird die Bremsrakete sogar zur Startrakete, und die Fhre hebt ab. Bremst der Spieler zu spt, hinterlsst er einen Krater fr die Ewigkeit. Mit etwas Glck wird er seinen Namen tragen. Fr manchen wre das vielleicht der einzige Eindruck, den er je in seinem Leben hinterlassen hat. In der einfachsten Variante reicht es, dem Spieler anzuzeigen, in welcher Hhe er sich bendet, welche Fallgeschwindigkeit er hat und wie viel Treibstoff zur Verfgung steht. Und dann soll er eingeben, wie viel Treibstoff im nchsten Zeitintervall an die Raketen gegeben werden soll. Es ist also gar keine Grak erforderlich, es ist auch nicht zwingend erforderlich, dass der Spieler in Echtzeit die Rakete fttert.

4.1.1

Ein wenig Physik

Die physikalischen Grundlagen fr das Landen der Mondlandefhre nden wir in den Formeln, die den freien Fall betrachten.

64

Die Mondlandung

4.1

Berechnung der Hhe Dazu zunchst eine Formel, die angibt, zu welchem Zeitpunkt die Fhre welche Hhe hat. 1 h(t) = gt2 + v0 t + h0 2 Dabei bedeutet h(t) die Hhe zu einem Zeitpunkt t. h0 ist die Starthhe und v0 die Ausgangsgeschwindigkeit. Die Variable g bezeichnet die Anziehungskraft des Planeten. Sie betrgt bei der Erde im Mittel 9,81 m/s2 . Der Mond ist kleiner, und so ist auch die Kraft nur ein Sechstel so gro. Die Mondanziehungskraft betrgt 1,635. Die Formel ermglicht es also, beim freien Fall eines Gegenstands zu ermitteln, in welcher Hhe sich der Gegenstand nach einigen Sekunden des Falls bendet, wenn der Gegenstand aus einer bestimmten Hhe fallengelassen wurde. Dann betrgt die Anfangsgeschwindigkeit 0. Es ist aber auch mglich, an einem beliebigen Zeitpunkt aufzusetzen, die Hhe und die Geschwindigkeit vorzugeben und von dort aus fr die nchsten Sekunden zu berechnen, wo der Gegenstand dann sein wird. Berechnung der Geschwindigkeit Die zweite interessante Formel berechnet die Geschwindigkeit beim freien Fall. Sie besagt also wie schnell der Gegenstand nach ein paar Sekunden ist. v(t) = gt + v0 Auch hier knnen wir eine Geschwindigkeit vorgeben, mssen also nicht bei 0 beginnen. Aus diesen beiden Formeln knnen Sie eine Kurve errechnen, die beschreibt, wie eine Mondlandung ausgeht, wenn es keine Bremsraketen gibt und die Fhre wie ein Stein nach unten fllt. Das ist aber nicht das Ziel des Spiels. Es sollen Bremsraketen gezndet werden und den freien Fall abbremsen. Die Bremsraketen Eine Bremsrakete bewirkt eine Beschleunigung der Fhre entgegen der Anziehungskraft des Mondes. Anders ausgedrckt ist die Mondanziehungskraft zu Zeitpunkten verndert, wenn die Raketen gezndet werden. Zu anderen Zeiten wirkt die Anziehungskraft vollstndig, eben wenn die Raketen nicht gezndet sind. Dies ist in den Formeln so nicht vorgesehen. Aber man kann sie leicht so verwenden, indem der Fall in einzelne Fallphasen von beispielsweise einer Sekunde unterteilt wird.

65

Spiele mit der Physik

Es wird also h(1) durch die Hhenfunktion und v(1) durch die Geschwindigkeitsfunktion berechnet. Werden in der nchsten Sekunde die Bremsraketen gezndet, erhlt g einen anderen Wert. Um die Raketenwirkung in die Formel einzubringen, muss g durch den Schub der Rakete verringert werden. Der Wert von h0 wird durch h(1) ersetzt, und der Wert von v0 wird durch v(1) ersetzt. Danach werden wiederum h(1) und v(1) berechnet. Diese Werte sind dann allerdings schon die Werte nach zwei Sekunden. Es wird also Sekunde um Sekunde berechnet und jeweils die Werte der letzten Sekunde als Ausgangswerte eingesetzt. Anstatt also eine Kurve ber die gesamte Zeit zu berechnen, wird Schritt fr Schritt aus dem Zustand der letzten Sekunde der Zustand fr die nchste Sekunde berechnet. Wird in der Sekunde die Bremsrakete gezndet, wird g reduziert und gleichzeitig der Kraftstoffvorrat verkleinert.

4.1.2

Die Tabellenkalkulation

Auf einem gut sortierten Computer ndet sich immer eine Tabellenkalkulation. Sollten Sie keine haben, mssen Sie das ndern. Das Internet wird Ihnen helfen: http://www.openofce.org Eine Tabellenkalkulation ist in vielen Lebenslagen uerst ntzlich. Fr das Programmierprojekt Mondlandung hilft sie, ein Gefhl dafr zu bekommen, wie sich die Fhre auf dem Weg zum Boden verhlt. Sie knnen die Funktionsformeln eingeben und die Auswirkungen der Zndung einer Bremsrakete bis zur Landung sehen. Sie knnen ausprobieren, wie viel Schub zu welchem Zeitpunkt fr Auswirkungen hat und knnen die Eingaben jederzeit wieder zurcknehmen. Und genau das werden wir nun tun. Abbildung 4.2 zeigt die Tabelle fr die Mondlandung. In den ersten Zeilen sind die Parameter fr die Funktionsverlufe eingetragen. Dabei dient die erste Zeile der Beschriftung, erst in der zweiten Zeile stehen die eigentlichen Werte. Die Formeln werden diese Werte einbeziehen. Der Zeitticker ist der Abstand zwischen zwei Zeiteinheiten. Hier sind vier Sekunden gewhlt, damit die Tabelle nicht gar so lang wird. Die Starthhe wird auf 4000 Meter gesetzt, und die Anfangsgeschwindigkeit steht auf 0. Ab Zeile 5 beginnen spaltenweise die Werte fr die Landung. Spalte A enthlt die Zeit, Spalte B die Hhe, Spalte C die Beschleunigung und die Spalte D die Geschwindigkeit. Die Zeile 5 wird noch individuell eingesetzt, damit die Ausgangswerte stimmen. Dabei enthalten die einzelnen Felder: A5 = 0, B5 = B2, C5 = 1,635 und D5 = 0. A5, C5 und D5 sind einfach konstante Werte. B5 enthlt einen Verweis auf B2, nmlich die Ausgangshhe. Um das zu erreichen, geben Sie ein Gleichheitszeichen ein und dann B2. Wenn Sie anschlieend die Return-

66

Die Mondlandung

4.1

taste drcken, erscheint in der Tabelle der gleiche Wert wie in B2, aber in der Eingabezeile erscheint =B2.

Abbildung 4.2 Tabellenkalkulierte Mondlandung

In die Zeile 6 werden die folgenden Formeln eingegeben:


A6: B6: C6: D6: =A5+$A$2 =-0,5*C6*A$2*A$2+B5+D5*A$2 1,635 =-C6*A$2+D5

Listing 4.1 Formeln in der Tabellenkalkulation

Die Formeln verwenden einige Werte aus der Zeile darber. So errechnet sich der Wert von A6 aus dem Wert von A5 plus dem Wert in A2. Das Dollarzeichen ignorieren Sie bitte einmal fr einen Moment. In A5 stand vorher 0, in A2 steht 4, also wird in A6 anschlieend 4 stehen. In der Eingabezeile steht aber nach wie vor die Formel. Geben Sie nun in A5 eine 2 ein, so wird sich der Wert von A6 auf 6 verndern. Auf hnliche Weise berechnen sich auch die B- und die C-Spalte aus den Feldern der Zeile darber.

67

Spiele mit der Physik

Kopieren der Zellen Das Kopieren ist bei einer Tabellenkalkulation nicht nur die Mglichkeit der Vervielfltigung, sondern kann Iterationen bewirken. Wenn die Zeile 6 mehrfach nach unten kopiert wird, wird jede Zelle den Wert aus der Zeile darber einberechnen, und so ergibt sich eine Zahlenreihe. Zunchst werden die Felder A6 bis D6 markiert. Dazu ziehen Sie entweder bei gedrckter Maustaste einen Rahmen durch diese Felder oder Sie setzen den Cursor auf A6, drcken die Shift-Taste2 und bewegen ihn bei gedrckter Shift-Taste mit der Cursor-Taste zum Feld D6. Anschlieend sollten die Felder A6 bis D6 dunkel hinterlegt sein. Mit der Tastenkombination Strg + C kopieren Sie den markierten Bereich in den Hintergrundspeicher, den man auch Clipboard nennt. Nun muss der Tabellenkalkulation mitgeteilt werden, wohin kopiert wird. Gehen Sie mit dem Cursor auf das Feld A7 und drcken Sie die Tastenkombination Strg + V und Sie werden feststellen, dass die vier Felder von A7 bis D7 belegt sind. Das Gleiche passiert noch einmal, wenn Sie nach A8 gehen und dort Strg + V drcken. Um mehrere Zeilen zu fllen, markieren Sie die Spalte A nach unten, beginnend mit Feld A7. Ziehen Sie die Markierung 30 oder 40 Zeilen nach unten. Das knnen Sie wiederum mit der Maus ziehen oder mit der Kombination aus der Shift-Taste und einer Cursortaste. Als Ergebnis sollten die Felder von A9 bis A30 oder A40 dunkel hinterlegt sein. Dann drcken Sie wieder Strg + V und nun werden die vier Zellen zeilenweise nach unten bis zum Ende Ihrer Markierung kopiert. Wenn Sie nun einmal mit dem Cursor auf das Feld A6 zurckgehen, die Eingabezeile oberhalb der Tabelle beobachten und schrittweise eine Zeile nach unten gehen, werden sie folgende Formeln sehen:
A6: A7: A8: A9: =A5+$A$2 =A6+$A$2 =A7+$A$2 =A8+$A$2

Sie sehen, wie sich der Referenz auf die Zeile darber beim Kopieren verndert hat. Allerdings hat sich die Referenz auf A2 nicht verndert. Verantwortlich dafr sind die Dollarzeichen. Die Puristen unter uns werden nun anmerken, dass es gereicht htte, A$2 zu verwenden. Und Puristen haben natrlich immer recht. Aber $A$2 ist auch nicht falsch und trainiert die Finger. Das Dollarzeichen bewirkt, dass sich die Adresse beim Kopieren nicht ndert. Steht das Dollarzeichen
2 Das ist die Taste, mit der man statt eines Kleinbuchstabens einen Grobuchstaben erhlt. In manchen Bchern wird sie Umschalttaste genannt.

68

Die Mondlandung

4.1

vor dem A, wird das A nicht zu B, steht es vor der 2, wird die 2 nicht zur 3. In diesem Beispiel ist das Dollarzeichen vor dem A also wie ein Hustenbonbon. Es nutzt zwar nichts, es schadet aber auch nicht. Jedenfalls sind damit die Grundprinzipien einer Tabellenkalkulation. Der Rest ist durch bung und Handbuchlesen leicht zu erlernen. Durch die Kopie sollten Sie den Verlauf der Landung vor Augen haben. Die Spalten sollten so aussehen wie in der Abbildung 4.2. Wenn es anders aussieht, haben Sie oder ich etwas falsch gemacht. Das klren wir dann am besten im Forum dieses Buches. Die Zeiten, in denen solche Dinge um Mitternacht unter der groen Eiche geklrt werden, sind glcklicherweise vorbei. Ich kann nmlich weder schieen noch fechten. Die Spalte C enthlt konstant den Wert der Mondbeschleunigung. Nun knnen Sie in dieser Spalte einige Felder durch 2 oder 1 ersetzen und damit quasi die Bremsraketen znden. Sie sehen, dass sich der bergang von positiven zu negativen Zahlen in Spalte B nach unten verlagert, sie landen also spter. Sie knnen auch sehen, wie sich die Geschwindigkeit in Spalte D ndert. Sollten Sie begeistert sein und mehr ber die Wunder einer Tabellenkalkulation wissen wollen, empfehle ich Ihnen ein Buch ber OpenOfce.org. So bietet ein Verlag, der fr seine exzellenten Autoren berhmt ist, das Buch von Thomas Krumbein OpenOfce.org 3 Einstieg und Umstieg an.

4.1.3

Das Ganze in C++

Nun mssen die Formeln nur in C++-Funktionen umgesetzt zu werden. Die Funktion calcHoehe ermittelt, wie der Name schon andeutet die Hhe fr die nchsten Sekunden.
double calcHoehe(double Beschleunigung, double altGeschw, double sekunden, double altHoehe) { return -0.5*Beschleunigung*sekunden*sekunden +altHoehe+altGeschw*sekunden; }
Listing 4.2 Berechnung der Hhe

Die Funktion bentigt als Parameter die derzeit geltende Beschleunigung. Diese ist normalerweise die Mondanziehungskraft. Wenn der Benutzer allerdings die Raketen zndet, wird die Beschleunigung um deren Schub vermindert. Der Schub muss so dimensioniert werden, dass die Gesamtbeschleunigung negativ wird, sonst wird die Geschwindigkeit nicht geringer.

69

Spiele mit der Physik

Die bisherige Geschwindigkeit und die bisherige Hhe wird bergeben. Auf diese Weise kann die errechnete Situation fr das nchste Zeitintervall als Ausgangsbasis bergeben werden. Ein weiterer Parameter sind die Sekunden. Diese sind aber hier immer 1, da immer die Differenz seit der letzten Sekunde gerechnet wird. Sie knnten dieses Intervall verkrzen oder verlngern. Sie erhhen oder vermindern damit vor allem die Mglichkeit des Spielers in den Ablauf einzugreifen. Neben der Hhe muss auch die Geschwindigkeit nach dem nchsten Zeitintervall berechnet werden. Immerhin wird die Geschwindigkeit auch als Basis fr die Hhenberechnung bentigt.
double calcGeschwindigkeit(double Beschleunigung, double sekunden, double altGeschw) { return -Beschleunigung*sekunden+altGeschw; }
Listing 4.3 Berechnung der Geschwindigkeit

Der Inhalt der Funktionen ist nicht berraschend. Sie enthalten genau das, was in der Tabellenkalkulation bereits berechnet wurde. Selbst die Schreibweise ist hnlich. Das haben wir der vielgescholtenen Programmiersprache FORTRAN zu verdanken, die zwar nicht zu besonders strukturierten Programmen fhrte, aber einen Standard fr die Schreibweise mathematischer Formeln in Programmiersprachen schuf. Diese wurde anschlieend von weitgehend allen Programmiersprachen bernommen.

4.1.4

Die Raumfhre

Die Erkenntnisse aus der Physik werden mit den wichtigsten Daten der Fhre in einer Klasse tFaehre zusammengefasst.
const double MondBeschleunigung=1.635; class tFaehre { public: tFaehre(); void bremse(double Schub); void zeigeStatus(int Sekunden); bool istTankVoll() { return Tank>0; } double getHoehe() { return Hoehe; } double getGeschw() { return Geschw; }

70

Die Mondlandung

4.1

private: double calcHoehe(double Beschleunigung, double altGeschw, double sekunden, double altHoehe); double calcGeschwindigkeit(double Beschleunigung, double sekunden, double altGeschw); double Hoehe; double Geschw; double Beschleunigung; double Tank; }; tFaehre::tFaehre() { Hoehe=4000.0; Geschw=0.0; Beschleunigung=MondBeschleunigung; Tank=5000; }
Listing 4.4 Die Klasse tFaehre

Die wichtigste Elementfunktion ist bremse. Der Parameter Schub gibt an, wie stark im nchsten Zeitabschnitt gebremst werden soll.
void tFaehre::bremse(double Schub) { if (Schub<0.0) Schub=0.0; if (Schub>100.0) Schub=100.0; Beschleunigung = MondBeschleunigung - Schub*0.04; if (Schub<=Tank) { Tank -= Schub; } else { Schub = Tank; Tank = 0; } Hoehe =calcHoehe(Beschleunigung, Geschw, 1.0, Hoehe); Geschw=calcGeschwindigkeit(Beschleunigung, 1.0, Geschw); }
Listing 4.5 Die Klasse tFaehre

Der Schub wird mit 0,04 multipliziert, sodass ein Wert zwischen 0 und 4 herauskommt, der dann von der Mondanziehungskraft abgezogen wird. Das Ergebnis liegt also zwischen der Mondanziehungskraft 1,635 und 2,365. Die Schubkraft ist damit wohldimensioniert, um eine glaubhafte Abbremsung zu erreichen.

71

Spiele mit der Physik

Der gewhlte Schub wird vom Tankinhalt abgezogen, der maximal 5000 Einheiten hat. Der Spieler kann also bis zu 50-mal Vollgas geben. Das ist ausreichend, damit auch ein unerfahrener Spieler die Fhre noch landen kann, auch wenn er zu frh gebremst hat.

4.1.5

Das Hauptprogramm

Das Hauptprogramm bernimmt die Spielsteuerung. Eine groe Schleife rotiert, solange die Fhre noch nicht gelandet ist. Fr die nchste Sekunde werden Hhe und Geschwindigkeit ermittelt und die Flugdaten auf dem Bildschirm ausgegeben. Dann erwartet das Programm die Eingabe fr den Schub, der zwischen 0 und 100 liegen soll. Dieser Schub wird an die Funktion tFaehre::bremse bergeben, die die Steuerung der Fhre bernimmt.
int main() { tFaehre Faehre; int Sekunden = 0; double Schub=0.0; while (Faehre.getHoehe()>0) { Sekunden++; Faehre.bremse(Schub); Faehre.zeigeStatus(Sekunden); if (Faehre.getHoehe()<=0.0) { Bewertung(Faehre.getGeschw()); } else if (Faehre.istTankVoll()) { cout << "Wieviel Schub (0-100): " << endl; Schub = ZahlenEingabe(); } else { sleep(1); // Falle in Echtzeit } } }
Listing 4.6 Hauptprogramm

Wenn kein Treibstoff mehr da ist, braucht der Spieler auch nichts mehr zu tun. Er kann nun nur noch beobachten, wie die Fhre nach unten fllt. Dazu wird die Funktion sleep verwendet. Zu Zeiten, als Microsoft sein Betriebssystem noch mit dem Attribut UNIX schmcken wollte, war dieser Aufruf auch dort vorhanden. Inzwischen wurde er dort zur Behinderung der Programmierer in _sleep umgewandelt. Allerdings erwartet _sleep die Dauer des Schlafs in Millisekunden und nicht in Sekunden wie sleep.

72

Die Mondlandung

4.1

Da die Schlafpause nur den Eindruck einer fallenden Fhre dramatischer darstellen soll, knnen Sie ihn natrlich auch einfach weglassen. Landung Fllt die Hhe unter 0, so ist die Fhre gelandet. Die Geschwindigkeit ist nun der entscheidende Faktor, ob die Landung erfolgreich war. Die Funktion Bewertung gibt ein paar Kommentare aus, ob die Landung Lust auf mehr machte.
void Bewertung(double Geschw) { if (Geschw<5.0) { cout << "Sehr gelungene Landung!"; } else if (Geschw<10.0) { cout << "Sauber gelandet!"; } else if (Geschw<20.0) { cout << "Verletzt gelandet!"; } else { cout << "Ein Krater wird Ihren Namen tragen!"; } cout << endl; }
Listing 4.7 Bewertung der Landung

Insgesamt ist die Mondlandung ein extrem einfaches, unkompliziertes Spiel, wenn man erst einmal die physikalischen Formeln an den Spielverlauf angepasst hat.

4.1.6

Was denn noch?

Die Schrittweite in Sekunden knnte erhht werden. Dadurch sind nicht so viele Eingaben erforderlich. Das Spiel wird krzer. Ob es dadurch an Spannung gewinnt oder verliert, ist eine Frage des Ausprobierens, vielleicht auch des persnlichen Geschmacks. Sie knnen auch den Treibstoffvorrat verkrzen. In der jetzigen Version ist das Spiel relativ leicht zu gewinnen. Alternativ knnen Sie auch in grerer Hhe beginnen. Sie knnen natrlich auch in Punkten bewerten, wie schnell gelandet wurde oder wie viel Treibstoff noch brig ist. Damit werden Highscores und Spielduelle mglich. Um das Spiel nicht allzu vorhersehbar zu machen, sollte die Hhe von Spiel zu Spiel verndert werden.

73

Spiele mit der Physik

Als Nachfolger dieser Konsolenversion gab es auf dem C-64 ein Programm namens Lunar Lander. Dieses zeigte die Mondlandefhre von der Seite, whrend sie el. Hinzu kam noch eine skizzierte Landschaft, die etwas zerklftet war. Die Fhre musste dann horizontal auf eine mglichst ebene Plattform gesteuert werden, damit sie bei der Landung nicht umel. Fr eine solche Lsung wre eine grasche Oberche natrlich unabdingbar.

4.2

Der Kaugummi und der schiefe Wurf

Zu meiner Jugendzeit war das Kaugummikauen noch verpnt. Der Kaugummi war als Chewing Gum durch die Amerikaner nach Deutschland gekommen, und die ltere Generation empfand es wohl als unanstndig, wenn das Gegenber durch stndige Kaubewegungen an einen Wiederkuer erinnerte. Vor allem Lehrer unterbaten sich diese Unhichkeit mit der Bemerkung, dass sie ansonsten gleich htten Kuhhirte werden knnen. Aus Sicht der Schler war dies bei manchem Lehrer ein durchaus verlockender Gedanke. So wurde im Unterricht heimlich gekaut und bei der Gefahr der Entdeckung auch schon mal einen Kaugummi heruntergeschluckt oder kurzfristig unter der Bank befestigt, dann typischerweise dort vergessen. Heutzutage ist bekannt, dass das stndige Kaugummikauen sogar wertvoll fr die Zhne ist, schlielich werden als Nebeneffekt auch die Zhne sauber. Der stndige Speicheluss sorgt fr gute Splung und verminderten Mundgeruch. Es gibt inzwischen sogar schon den dentalen Kaugummi, von Werbefernseh-Zahnrzten empfohlen. Htten wir schon damals gewusst, dass ein Kaugummi so gesund ist, htten wir vielleicht im Unterricht weitergekatscht und bei einer Entlarvung den Lehrer mit den Worten angegrinst: Esch ischt aber gut fr die Tschhne! Der in meiner Jugend gebruchliche Begriff Chewing Gum scheint in Vergessenheit geraten und dem Kaugummi gewichen zu sein. Seit der Kaugummi dental wertvoll geworden ist, scheint sich also der deutsche Begriff wieder durchzusetzen. Sollte sich dieser persnliche Eindruck besttigen und daraus die allgemeine Regel herleiten lassen, dass Anglizismen Unserises vertuschen, wirft das ein fatales Licht auf die Entwicklung des Fernsehens in den letzten Jahren. Kaugummispiele Whrend bei der Mondlandefhre die Gesetze des freien Falls zur Anwendung kamen, basiert das Spiel dieses Abschnitts auf dem schiefen Wurf. Es wird ein Kaugummi in einem Raum ber einen Schrank hinweg in einen Spucknapf gespuckt. Ein Treffer bedeutet natrlich den Sieg. Der Spieler erhlt als Feedback nur die Information, wo der Kaugummi klebt. Es ist denkbar, dass er gegen

74

Der Kaugummi und der schiefe Wurf

4.2

die Decke schlgt, vor dem Schrank, auf den Schrank oder gar auf die hintere Wand des Raums. Der Spieler muss dann entscheiden, ob der Winkel oder die Geschwindigkeit zu korrigieren ist. Das Szenario ist in Abbildung 4.3 zu sehen.

Abbildung 4.3 Der Spuckraum

4.2.1

Die Formel

Die Grundlage fr die Berechnung der Flugbahn des Kaugummis ergibt sich aus der berlagerung zweier Bewegungen. Die waagerechte Bewegung verluft linear, sofern wir den Luftwiderstand vernachlssigen. Das heit, das Kaugummi wird durch die Lippen beschleunigt und verlsst den Mund mit einer Geschwindigkeit, die sich bis zum Aufschlag nicht wesentlich verndert. Die senkrechte Bewegung wird vor allem durch die Erdanziehungskraft geprgt. Diese Komponente ist vergleichbar mit der Mondlandung. Der einzige Unterschied ist, dass das Objekt zunchst nach oben mit konstanter Geschwindigkeit geschossen wird und dann irgendwann wieder zur Erde zurckfllt. Dabei spielt die Erdanziehungskraft eine Rolle, die eine Beschleunigung ist, und damit eine quadratische Komponente hat. Legt man die waagerechte und die senkrechte Bewegung bereinander, ergibt sich daraus eine Parabel als Flugbahn. Die Formel dieser Flugbahn lsst sich aus einer beliebigen Formelsammlung entnehmen und lautet: g x2 y = tan() x 2 2v cos2 () Die Winkelfunktionen in der Formel ergeben sich aus der berlagerung der beiden Bewegungen durch den Spuckwinkel. Die Funktion liefert die Hhe des Kaugummis in Abhngigkeit von der Entfernung des Spuckenden. Letztlich werden wir diese Funktion in C++ umsetzen mssen. Das ist allerdings nicht besonders schwierig:

75

Spiele mit der Physik

y = tan(alpha)*x-(g*x*x/(2*v*v*cos(alpha)*cos(alpha))

Sie werden vielleicht schon bemerkt haben, dass die direkte bernahme der Funktion nicht ganz korrekt ist, da die Bahn in der Hhe 0 beginnt. Das wre nur erreichbar, wenn der Spieler sich auf den Boden legt. Eine Korrektur ist allerdings nicht sehr schwierig. Es muss nur die Hhe des Mundes jeweils addiert werden. Da Kaugummispucker durchtrainierte Athleten sind, habe ich eine Mundhhe von 1,70 Meter angenommen. Wenn Sie auch Kinder an den Start lassen wollen, mssten Sie diesen Wert korrigieren. Ob Sie es allerdings aus pdagogischen Grnden wirklich erlauben wollen, Kinder zum Kaugummispucken zu motivieren, mssen Sie selbst wissen.

4.2.2

Hilfe von der Tabellenkalkulation

Wie wohl den meisten Programmierern fehlt auch mir das Gefhl dafr, welche Geschwindigkeit ein gespucktes Kaugummi erreicht. Eine ungefhre Einschtzung dieser Lage ist aber erforderlich, um dem Spiel eine gewisse Realittsnhe zu geben. Wie praktisch, dass sich auf den meisten Computern eine Tabellenkalkulation bendet. Sollten Sie keine besitzen, schauen Sie mal auf der Homepage von OpenOfce.org vorbei. Die berraschung ist gro: Da gibt es ein kostenloses Ofce-Paket. Vielleicht ist die berraschung aber auch nicht mehr so gro, weil Sie schon den Abschnitt ber die Mondlandung gelesen haben. In der Tabellenkalkulation wird mithilfe der Flugbahnkurve durchgespielt, wie sich die Flugbahn verndert, wenn Winkel und Geschwindigkeit gendert werden. Abbildung 4.4 zeigt, wie die Funktion zu einer Wertetabelle und zu einer Grak fhrt. In der ersten Zeile stehen die Parameter der Funktionen. Das Feld B1 legt den Winkel und das Feld D1 die Geschwindigkeit fest. In der Spalte A steht die Entfernung vom Spuckenden, also die X-Koordinate. Als Abstand wird hier 20 cm, also 0,2 m eingesetzt. Schreiben Sie also in das Feld A2 die Zahl 0,2. Ins darunterliegende Feld A3 tippen Sie =A2+0,2 ein. Nach der Returntaste sollte dort 0,4 stehen. Bewegen Sie den Cursor wieder auf A3 und drcken Sie die Tastenkombination Strg + C . Damit ist die Funktion im Clipboard, dem Hintergrundspeicher, abgelegt.

76

Der Kaugummi und der schiefe Wurf

4.2

Abbildung 4.4 Tabellenkalkulierte Spuckkurve

Jetzt gehen Sie mit dem Cursor auf A4, drcken die und gehen gleichzeitig mit dem Cursor nach unten. Die Felder darunter sollten markiert werden. Wenn Sie etwa 30 Felder weit gekommen sind, geben Sie die Tastenkombination Strg + V ein und Sie sollten eine Zahlenkolonne sehen, die in Schritten von 0,2 stetig wchst. In der zweiten Spalte ab der Zelle B2 soll die Y-Koordinate stehen, also die Hhe des Kaugummis zum gegebenen Abstand in der linken Spalte. Die Formel fr die Tabellenkalkulation hat das folgende Aussehen:
=TAN(RAD($B$1))*A2-(9,81*A2^2/(2*$D$1^2*(COS(RAD($B$1))^2)))

Diese Formel geben Sie bitte in der Zelle B2 ein und kopieren Sie sie bitte auf die gleiche Weise nach unten, wie Sie das schon in der Spalte A gemacht haben. Nun kann durch Eingabe des Winkels in B1 und der Geschwindigkeit in D1 eine Zahlenkollone aufgebaut werden, die zeigt, wo sich der Kaugummi bendet. Der Winkel sollte zunchst 45 Grad sein. Damit erreicht man die grte Entfernung. Nun kann man schn mit den Geschwindigkeitswerten experimentieren.

77

Spiele mit der Physik

Als realistische Geschwindigkeit ergeben sich halbwegs sinnvolle Werte zwischen 2 und 10. Wie in der Physik blich, wird die Geschwindigkeit in Metern pro Sekunde gemessen. Das bedeutet, dass innerhalb von einer Sekunde ein grerer Raum durchspuckt werden kann, was sich vermutlich auch mit Ihren Erfahrungen beim Kaugummispucken decken drfte. Neben den beiden Spalten sehen Sie die Kurve, die sich aus den Werten der Spalte B ergibt. Eine solche Kurve nennt man in der Sprache der Tabellenkalkulation ein Diagramm. Und es ist kein Hexenwerk, sie zu erstellen. Zunchst bewegen Sie den Cursor etwa an die Stelle, wo die Kurve erscheinen soll, beispielsweise die Zelle C2. Dann rufen Sie das Men Einfgen Diagramm auf. Es erscheint ein Dialog, in dem Sie festlegen, wie das Diagramm auszusehen hat. Sie wollen keine Balken, sondern Linien. Dort wollen Sie statt der Punkte die Linien haben. Dadurch erhalten Sie eine durchgehend verbundene Kurve. Mit dem Button Weiter kommen Sie zum Datenbereich. Dort geben Sie den Bereich an, in dem die Daten fr die Kurve stehen. Und das ist natrlich B2:B30, oder wie weit auch immer die Spalte B bei Ihnen mit Werten gefllt ist. Anschlieend sind Sie fertig. Sie knnen nun die Grak mit der Maus nachtrglich gnstiger positionieren und an den Ecken auseinanderziehen, damit sie etwas besser aussieht. Nun kann man sich bereits bildlich vorstellen, wo sich der Spucknapf, der Schrank und die Decke benden mssen, damit diese Kombination aus Winkel und Geschwindigkeit ein Treffer wird.

4.2.3

Spielfeld aufbauen

Und genau so wird bei dem Spiel am einfachsten vorgegangen. Anstatt nach einer Bahn zu suchen, die durch einen vorgegebenen Raum zum Ziel fhrt, geht das Programm so vor, dass es eine Kurve aus Winkel und Geschwindigkeit festlegt und dann die Mbel und die Decke so rckt, dass die Kurve exakt durch die Lcken passt. Man knnte also boshaft sagen, dass der Spieler eigentlich nur die vom Programm gespuckte Kurve raten soll. Der Vorteil dieses Vorgehens liegt darin, dass sichergestellt ist, dass das Spiel immer lsbar ist. Flugbahnberechnung Die zentrale Funktion dient der Ermittlung der Flughhe. Diese Funktion wurde zu Anfang des Abschnitts bereits vorgestellt und in der Tabellenkalkulation ausgiebig ausgetestet. Mit ihr wird zunchst die gesuchte Bahn berechnet, um aus den gewonnenen Informationen den Raum zu gestalten. Und natrlich wird diese Funktion auch bentigt, um die Spuckversuche des Spielers zu wrdigen.

78

Der Kaugummi und der schiefe Wurf

4.2

const double g = 9.81; double Hoehe(double x, double alpha, double v) { const double Koerpergroesse = 1.7; double alphaBogen = alpha/180*3.1415926535; return Koerpergroesse + tan(alphaBogen)*x-(g*x*x/ (2*v*v*cos(alphaBogen)*cos(alphaBogen))); }
Listing 4.8 Hhenberechnung iegender Kaugummis

Innerhalb dieser Funktion wird auch die Krpergre des Sportlers mit 1,70 m hinzugerechnet. Das Programm geht also von einer stehenden Haltung aus. Der bergebene Winkel wird in Bogenma umgerechnet, da die Standardfunktionen dieses verwenden. Der Spieler wird eher in die klassische Gradeinteilung bevorzugen, in der ein rechter Winkel 90 Grad ist. Gehen Sie von Grad Celsius aus, werden Sie feststellen, dass Sie schon mit einem rechten Winkel eine nnische Sauna betreiben knnen. Die Erdanziehungskonstante wird hier originellerweise g genannt. Die bergabeparameter bestehen aus der Entfernung vom Spucksportler, dem Abspuckwinkel und der Geschwindigkeit des Projektils bei Verlassen des Mundes. Rahmenbedingungen Nun muss der Raum geschaffen werden, in dem es sich trefich spucken lsst. Es mssen die Deckenhhe und die Raumlnge festgelegt werden. Es soll der Schrank aufgestellt werden. Dieser hat eine Distanz zum Spieler, eine Hhe und eine Tiefe. Zu guter Letzt gibt es den Napf, der hinter dem Schrank aufgestellt werden soll. Und auch dieser Napf hat eine Ausdehnung. All diese Informationen werden in einer Klasse tRaum deniert. Sie speichert also vor allem die Geometrie des Raums. Dann wird die Klasse eine Elementfunktion zur Erzeugung des Raums zur Verfgung stellen. Und es wird eine Funktion zur Analyse der Spuckversuche enthalten sein. Die Tiefe des Schranks wird mit 40 cm der Einfachheit halber vorgegeben.
const double SchrankTiefe = 0.4; class tRaum { public: tRaum() {} void erzeuge(); tErfolg spuck(double winkel, double v);

79

Spiele mit der Physik

private: double double double double double double double };

Raumlaenge; Deckenhoehe; DistanzSchrank; TiefeSchrank; HoeheSchrank; NapfVorn; NapfHinten;

Listing 4.9 Die Klasse tRaum

Der Raum wird sich immer wieder ndern, wenn ein Kaugummi erfolgreich im Ziel gelandet ist. Ansonsten hat der Sportspucker keinen Spa mehr, sobald er die optimale Kurve gefunden hat. Also wird die Zufallsfunktion in Erscheinung treten. Die Funktion fr die Raumgestaltung ist zwar etwas lnglich, aber nicht sehr kompliziert. Die Lnge hat in erster Linie mit der Anzahl der Einrichtungsgegenstnde und der vielen Mae zu tun.
void tRaum::erzeuge() { // die Spuckgeschwindigkeit liegt zwischen 4 und 9 double vSpuck = 4.0 + double(rand() % 50)/10.0; // der Winkel double alpha = 60 - vSpuck - double(rand() % 20); double y, alt=0.0; double yMaxHoehe=0.0; double xMaxHoehe = 0.0; bool top = false; double x=0.0; while(true) { y = Hoehe(x, alpha, vSpuck); cout << "x: " << x << " y: " << y << endl; if (y>yMaxHoehe) { yMaxHoehe = y; xMaxHoehe = x; top = true; } else { if (top) { DistanzSchrank = xMaxHoehe; top = false; } } if (alt*y < 0) {

80

Der Kaugummi und der schiefe Wurf

4.2

// gelandet! break; } alt = y; x+=.1; } NapfVorn = x - 0.2; NapfHinten = x + 0.2; Raumlaenge = x + 2.0; Deckenhoehe = yMaxHoehe + 0.5; DistanzSchrank -= 0.2; HoeheSchrank = Hoehe(DistanzSchrank, alpha, vSpuck); double ZweiteHoehe = Hoehe(DistanzSchrank+SchrankTiefe, alpha, vSpuck); if (ZweiteHoehe<HoeheSchrank) HoeheSchrank = ZweiteHoehe; HoeheSchrank -= 0.3; }
Listing 4.10 Raumgestaltung

Die Funktion ermittelt, wo in etwa die maximale Hhe der Kurve ist und stellt den Schrank darunter. Auch der Landepunkt des Gummis wird ermittelt, und an diese Stelle wird der Spucknapf geschoben. Die Deckenhhe wird auf einen Wert gebracht, der von der Flugbahn nicht erreichbar ist. Die hintere Wand wird mit einem geringen Abstand hinter den Napf gestellt. Damit ist der Raum bereitet. Die Kaugummis knnen iegen.

4.2.4

Spielablauf

Die mglichen Ergebnisse eines Spielversuches sind bersichtlich:


Der Kaugummi kann an der Decke kleben. Ein sehr lascher Versuch kann zu einer Landung auf dem Boden vor dem Schrank fhren. Der Gummi klebt am Schrank. Er bleibt auf dem Schrank liegen. Der Kaugummi schafft es ber den Schrank, bleibt aber vor dem Napf liegen. Es gibt einen Volltreffer im Napf. Der Gummi beriegt den Napf und landet auf dem Boden hinter dem Napf. Der Flug fhrt direkt an die gegenberliegende Wand.

81

Spiele mit der Physik

Diese Ergebnisse werden durch die folgende Enumeration im Programm zusammengefasst.


enum tErfolg { Treffer, Decke, AmSchrank, AufSchrank, BodenVorSchrank, BodenVorNapf, BodenHinterNapf, Rueckwand };
Listing 4.11 Erfolgsaussichten

Die Funktion zeigeErfolg zeigt dem Spieler an, zu welchem Ergebnis sein Versuch gekommen ist.
void zeigeErfolg(tErfolg e) { switch(e) { case Treffer: cout << "Treffer" << endl; break; case Decke: cout << "An der Decke" << endl; break; ... case BodenHinterNapf: cout << "Boden hinter dem Napf" << endl; break; case Rueckwand: cout << "an der Rueckwand" << endl; break; default: cout << "was ist das?" << endl; } }
Listing 4.12 Erfolgsanzeige

Antritt der Athleten Es wird still auf den Rngen. Man riecht Mnnerschwei und hrt die Spannung knistern. Das Projektil wird noch einmal durchgekaut und durch die exible Zunge des Sportlers in eine aerodynamische Form gepresst. Die Backen blhen sich kurz auf, die Lippen werden spitz und da kommt es: Das aalglatt eingespeichelte Projektil bahnt sich seinen Weg durch die Luft. Diese Atmosphre kann am Computer nur schwer entstehen. Ob man sie vermisst, ist eine andere Frage. Der Spieler gibt Geschwindigkeit und Winkel ein, und damit ist die Aufgabe des Athleten beendet. Nun ist das Programm am Zuge.

82

Der Kaugummi und der schiefe Wurf

4.2

Es wird geprft, welche Bahn der Gummi nimmt. Um genau zu sein, ist eigentlich nur interessant, wo es aufschlgt. Dazu wird die Hhe an den kritischen Stellen des Raums ermittelt. Die erste Hrde ist der Schrank. Liegt an der Vorderkante des Schranks die Hhe unter 0, muss die Landung bereits vor dem Schrank erfolgt sein. Liegt sie unter der Schrankhhe, klebt der Kaugummi auf der Vorderseite des Schranks. Die zweite Hrde ist die hintere Kante des Schranks, die der Gummi beriegen muss, sonst landet er auf dem Schrank. Eine weitere interessante Stelle ist der Bereich des Napfes. Liegt dort die Nullstelle, wre es ein Treffer. Die anderen Flle sind ein Schlag gegen die Rckwand oder ein Deckentreffer.
tErfolg tRaum::spuck(double winkel, double v) { double Scheitel = MaxHoehe(winkel, v); double yMax = Hoehe(Scheitel, winkel, v); if (Scheitel<DistanzSchrank) { if (yMax>Deckenhoehe) return Decke; } double y = Hoehe(DistanzSchrank, winkel, v); if (y < 0) return BodenVorSchrank; if (y < HoeheSchrank) return AmSchrank; y = Hoehe(DistanzSchrank+SchrankTiefe, winkel, v); if (y < HoeheSchrank) return AufSchrank; double yWand = Hoehe(Raumlaenge, winkel, v); if (yWand>Deckenhoehe) return Decke; y = Hoehe(NapfVorn, winkel, v); if (y<0) return BodenVorNapf; double yNapf = Hoehe(NapfHinten, winkel, v); if (y*yNapf < 0) return Treffer; if (yWand<0) return BodenHinterNapf; return Rueckwand; }
Listing 4.13 Spucktest

Deckentreffer Um zu prfen, ob die Decke getroffen wurde, muss ermittelt werden, wie hoch die Flugkurve maximal steigt. Dazu ermittelt man den Scheitelpunkt der Flugkurve. Die folgende Formel ermittelt, den Punkt, ber dem die Kurve ihren hchsten Punkt erreicht: x = sin() cos() v2 g

83

Spiele mit der Physik

In C++ wird diese Formel in der Funktion MaxHoehe umgesetzt. Dazu muss wiederum zunchst der Winkel in Bogenma umgerechnet werden, dann klappt es auch mit den mathematischen Funktionen von C++.
double MaxHoehe(double alpha, double v) { double alphaBogen = alpha/180*3.1415926535; return sin(alphaBogen)*cos(alphaBogen)*v*v/g; }
Listing 4.14 MaxHoehe ermittelt den Scheitelpunkt.

Im Programm wird diese Funktion eingesetzt, um festzustellen, ob die Flugbahn die Raumhhe berschreitet. Dann erbrigen sich alle anderen Berechnungen. Das Hauptprogramm Die Steuerung des Spielablaufs liegt wieder einmal in der Hauptfunktion. Sie liest die Werte von der Tastatur ein, wandelt die so gewonnene Eingabezeile in einen Zahlenwert um.
#include <cstdlib> #include <cmath> #include <ctime> #include <iostream> #include <sstream> using namespace std; ... double str2double(string str) { istringstream inStream(str); double out; inStream >> out; return out; }
Listing 4.15 Zahleneingabe

Das Umwandeln von Texteingaben in Zahlenwerte ist ein Klassiker. In C wurde dazu die Funktion atof verwendet. Die saubere Methode in C++ verwendet einen Stringstream, dessen Inhalt in eine double-Variable umgeleitet wird. Die Zufallszahlen werden einmal durchgemischt. Die Verwendung von time(0) legt die aktuelle Sekunde als Startwert vor und garantiert, dass sich die Rume nicht stndig wiederholen.

84

Der Kaugummi und der schiefe Wurf

4.2

In der Schleife werden Geschwindigkeit und Winkel eingelesen. Die Eingaben werden geprft, der Spuckversuch gestartet und dessen Ergebnis auf dem Bildschirm angezeigt.
int main() { srand(time(0)); // Zufallszahlen ankurbeln tRaum Raum; Raum.erzeuge(); double v = 1.0; double winkel = 0.0; int Versuche=0; tErfolg Erfolg = Decke; while (Erfolg!=Treffer) { string Antwort=""; cout << "Geschwindigkeit: " << endl; getline(cin, Antwort); v = str2double(Antwort); if (v<=0.0) break; Antwort = ""; cout << "Winkel (1-89): " << endl; getline(cin, Antwort); winkel = str2double(Antwort); if (winkel<1.0 || winkel>89.0) break; Erfolg = Raum.spuck(winkel, v); zeigeErfolg(Erfolg); Versuche++; } if (Erfolg==Treffer) { cout << "Gewonnen nach "<<Versuche<<" Versuchen."; cout << endl; } }
Listing 4.16 Das Hauptprogramm

4.2.5

Was denn noch?

Der Reiz des Spiels liegt im Verborgenen. Der Spieler sieht nichts, sondern tastet sich spuckend seinem Ziel entgegen. Sie knnten dem Spieler eine Hilfe geben, indem Sie ihm die Dimensionen des Raums offenbaren. Die einfachste Methode ist, die nackten Daten auf dem Bildschirm auszugeben: die Raumlnge, der Abstand des Schranks und dessen Hhe.

85

Spiele mit der Physik

Mithilfe von Minuszeichen, senkrechten Stricken und Pluszeichen liee sich auch eine Skizze erstellen, die zumindest in etwa darstellt, wie der Raum aussieht. Das wrde jedenfalls dem Retrostil des Spiels entsprechen. Sie knnen noch weiter gehen, wenn Sie eine grasche Oberche wie beispielsweise wxWidgets einsetzen. Dann knnen Sie den Raum mastabsgerecht zeichnen. Darber hinaus knnten Sie die Versuche des Spielers als Kurven in den Raum einzeichnen. Schlielich knnten Sie sogar die Slider verwenden, wie sie im Programm Fluglandung verwendet werden und mit dem einen die Geschwindigkeit und mit dem anderen den Winkel einstellen. Das liee sich noch dadurch toppen, dass das Programm nach dem Erfolg alle Versuche des Spielers animiert und den Kaugummi durch den Raum iegen lsst. Um noch mehr Physik in dieses Spiel zu bringen, knnte statt des Kaugummis ein Kirschkern oder eine Gummikugel gespuckt werden. In diesem Fall knnte ein elastischer Sto mit Einfallwinkel gleich Ausfallwinkel simuliert werden und der Spucknapf so wie im Billard per Bande getroffen werden.

86

Programmiersprachen basieren auf der englischen Sprache. Warum eigentlich?

Regionales C++

Vorauseilend muss ich gestehen, dass es bei mir mit der Kenntnis der Mundarten nicht so weit her ist. Ich wurde immer zur Verwendung einer ordentlichen hochdeutschen Sprache angehalten. Einer der Grnde war, dass meine Eltern hug umzogen. Ich fand sie dennoch immer wieder, und so ist das Hessische die einzige Mundart, die ich lnger gehrt habe. Da ich wei, dass die Hessen eher gromtig sind, bin ich sicher, dass sie ber meine Fehler in ihrer Mundart gndig hinwegsehen. Ich habe die Beschreibung extra so angelegt, dass HessischExperten leicht meine Fehler korrigieren und Liebhaber anderer Mundarten ihre eigenen Ausdrcke verwenden knnen. Natrlich sind auch andere exotische Sprachen wie Dnisch, Flmisch oder Deutsch denkbar, die bisher als Grundlage moderner Programmiersprachen einfach zu kurz gekommen sind. Es war einmal in der A.U.G.E. Anfang der 1980er Jahre wurde ich zu einem Treffen der A.U.G.E. an der Universitt Frankfurt/Main eingeladen. Das war kein Treffen der Optiker, sondern so nannte sich die Apple User Group Europe (http://www.auge.de). Und da der Macintosh zu diesem Zeitpunkt noch nicht geboren war, drfte es klar sein, dass es sich um die Benutzer des Apple II handelte. Der Apple II gilt als der erste Personal Computer und wurde 1977 vorgestellt. Der Begriff PC leitet sich von dem englischen Begriff personal computer ab, was so viel bedeutet wie persnlicher Computer. Und genau das wollte der Erbauer Steve Wozniak herstellen: einen Computer, den jeder fr sich hatte. Der Apple II europlus hatte eine 6502-CPU, 1 MHz und 64 KByte RAM. Er verfgte ber eine Farbgrak von 280192 Pixeln und besa acht Slots fr Erweiterungen. Zum Sichern der Daten verfgte er ber einen Kassettenrecorderanschluss. Optional konnte ein Diskette mit 140 KByte Kapazitt dazugekauft werden.

87

Regionales C++

Abbildung 5.1 Apple II (Foto von Marcin Wichary unter Creative Commons (CC) Lizenz)

Bei jenem Treffen der A.U.G.E. zeigte jemand, dass sein Apple hessisch sprach. Er gab den Befehl ZEISCH ein, und es erschien das BASIC-Programm in hessischem Dialekt. Er konnte auch Programme auf Hessisch eintippen. Wenn dagegen jemand auf diesem Rechner die korrekten englischen Befehle eintippte, gab es Fehlermeldungen. Selbst Programme, die von einer fremden Diskette geladen wurden, zeigte dieser Apple II in hessischer Sprache an. Der Trick mit der Token-Tabelle Die Umstellung eines Apple II auf den hessischen Dialekt war zwar sehr beeindruckend, aber gar nicht so kompliziert. Der Apple II hatte wie die meisten gngigen Computer seiner Zeit die Programmiersprache BASIC im ROM. Es gab allerdings fr dieses Gert zwei BASIC-Dialekte. Das ltere BASIC stammte von Steve Wozniak und nannte sich Integer-BASIC und konnte wie der Name schon sagt, nicht mit Fliekommazahlen umgehen. Das neuere BASIC nannte sich Applesoft-BASIC und war von Microsoft entwickelt worden. Da aber schon einige Programme in dem bisherigen Dialekt vorlagen, hatte sich Steve Wozniak einen besonderen Kniff ausgedacht, um auch Programme fr diese Version ausfhren zu knnen. Er entwarf eine 16 KByte Speicherkarte, die parallel zum ROM lag und die man per Software umschalten konnte, wenn man vorher das alte BASIC dort hineingespeichert hatte.

88

Hessisch?

5.1

Der Tftler aus dem Apple-Club hatte stattdessen das Applesoft-BASIC in die Speicherkarte kopiert und hat dort die Token-Tabelle gesucht. Darin stehen die BASICBefehle im Klartext. Der Interpreter des Apple II optimierte die Ausfhrung der Programme, indem er die Befehle durchnummerierte. Jeder Befehl wurde bei seiner Eingabe in die zugehrige Nummer, den sogenannten Token, konvertiert. In den Listings standen nun einfache Nummern, die der Interpreter nicht mhevoll Buchstabe fr Buchstabe bersetzen musste. Hinzu kam eine nicht unerhebliche Platzersparnis. Fr die Ausgabe des Listings wurde dann ber die Nummer auf den Befehlsnamen in der Token-Tabelle zugegriffen und dieser dargestellt. In diesem Fall hatte der Besitzer die Token-Tabelle aufgesprt und deren Ablagestruktur analysiert. Dann hat er statt den englischen Befehlsnamen einfach die hessischen bersetzungen eingetragen, und schon sprach der Apple II pltzlich auch Hessisch. Das Ganze in C++ Knnte man so etwas nicht auch mit C++ machen? So elegant wie auf dem Apple funktioniert es natrlich nicht. Immerhin ist C++ keine Interpretersprache, sondern verwendet einen Compiler. Natrlich besitzt auch ein Compiler eine Token-Tabelle und selbstverstndlich knnte man sie ebenso verndern wie die Interpretertabelle. Dann akzeptiert dieser Compiler fortan Hessisch. Die Listings anderer Autoren bleiben aber Englisch, da der Quelltext ja durch einen Editor eingegeben wurde und dementsprechend nicht als Token vorliegt. Dank des Prprozessors ist die bersetzung hessischer Befehle in lupenreines C++ sogar sehr viel einfacher als beim BASIC des Apple II. Und wenn Sie gern fertige C++-Programme auf Hessisch lesen wollen, dann schreiben Sie doch ein Programm, das die Programme entsprechend darstellt.

5.1

Hessisch?

Ja, Hessisch! Natrlich empndet jeder seinen eigenen Dialekt als den einzig richtigen Dialekt, und da die Hessen nicht einmal ein Zehntel der Bevlkerung der Bundesrepublik Deutschland umfassen, sind sie eindeutig in der Minderheit. Allerdings haben auch die Bayern, die Sachsen und die Norddeutschen mit ihrem Plattdeutsch keine absolute Mehrheit. Aber dies ist ein freiheitlich, demokratisches Buch. Jeder kann den Dialekt seiner Wahl selbst erstellen. Da die Beispiele nun mal in Hessisch sind, werden hier ein paar Worte zu diesem Dialekt fallen.

89

Regionales C++

5.1.1

Ei, wie sprischt mer des?

Fr das bessere Verstndnis nden Sie hier eine Art Kurs 30 Sekunden Hessisch. Die Absurditt dieses Unterfanges steigt dadurch, dass ich selbst kein gebrtiger Hesse bin und dort auch seit vielen Jahren nicht mehr lebe. Damit ist meine Kompetenz fr dieses Kapitel klar umrissen. Echte Hessen werden also je nach Temperament viel Spa an diesem Abschnitt haben oder den Wunsch verspren, mir die Haare auszureien. Die meisten Hessen sind aber von Natur aus geduldig, freundlich und gndig, sodass ich auch nach der Verffentlichung des Buches vielleicht noch Anlass haben werde, den Friseur aufzusuchen. Zum Glck gibt es sehr viele unterschiedliche Frbungen des Hessischen. So kann ich mich herausreden, dass ich eben eine andere rtliche Frbung kennengelernt habe. Je nach Region klingt das Hessische durchaus etwas anders, aber das gilt wohl fr alle Dialekte. Darum gilt das im folgenden Absatz gesagte nur als grobe Richtung. Das Hessische ist ein sehr weicher Dialekt. Die Endsilbe werde oft offe gelasse. Ansonste sprischt der Hesse jedes weische ch als sch aus. Langgezogene Silbe wedde maschma sehr kozz ausgesproche. Gern wedde Selbstlaute verschobe. So kann sisch ei au sowohl zu u wie bei Uffschnitt als auch zu ei langgezogenes aa vernnern, wie bei Aach mal wedda da?. Wischtisch is aach, dass mer des k und des t schee weisch aussprischt. So gann dann alles schee iee und es muss aanfach butterweisch raassgomme. Wenn Sie Hessisch in Vollendung erleben wollen, sollten Sie sich den Sketch Hessi James von dem Duo Badesalz anhren. Der folgende Link fhrt Sie zu einer animierten Version: http://de.sevenload.com/videos/CeHwJI2-Hessi-James-Badesalz

5.1.2

Die hessische Tabelle

Die bersetzung eines Programms wird auf der Basis einer Tabelle durchgefhrt. Fr das Hessische habe ich eine Tabelle zusammengestellt, die Sie auch auf der beiliegenden CD nden. Zunchst kommt der mundartliche Begriff und dann das Schlsselwort, aus dem er bersetzt wird. Dabei mssen es nicht zwingend die reservierten Wrter von C++ sein. Es ist auch mglich, Befehle des Prprozessors oder Funktionen der Standardausgabe zu verndern. Damit die Seite optisch nicht so unausgewogen aussieht, habe ich je drei Zeilen zu einer zusammengefasst.

90

Hessisch?

5.1

wann if brisch break laaf for wennsda case nix void krisch catch neu new lang long kumbel friend nennma typedef folschende enum ingepackt struct klasse class fliess float alslang while hoerma cin

ansonste else gansezahl int verteilma switch gibwidder return eiversuchsdoch try loesch delete desismir private vereinischt union kozz short sooderso bool quatsch false schmeiss throw machhinne continue stimmt true seschma cout neuzeil endl

wischdischfunktion main zeische char annernfalls default verreschner operator festgemescht const oeffentlisch public sischer protected netklaanernull unsigned desda this mesch do huepp goto nemma using dobbel double sohaasts typename kuenstlisch virtual holma include

Sie merken schon, dass die bersetzungen teilweise sehr weit hergeholt sind. Aber es geht an dieser Stelle natrlich weder um sprachliche Exaktheit noch um programmiertechnische Efzienz, sondern um eine sprachliche Karrikatur. Immerhin muss mit vereinzelten Wrtern der Eindruck der Mundart erweckt werden. Die Variablen- und Funktionsnamen werden ja nicht bersetzt.

5.1.3

Was hinten herauskommt

Damit Sie vorweg einen Eindruck gewinnen, wie ein hessisches C++-Programm aussieht, habe ich ein Beispielprogramm aus dem Buch Einstieg in C++ herangezogen. Das Programm hat die Aufgabe, einen String in eine Fliekommavariable umzuwandeln. Die Funktion des Programms ist an dieser Stelle zweitrangig. Es soll ja nur darstellen, wie es in hessischer bersetzung aussieht.
#holma <iostream> nemma namespace std; festgemescht gansezahl MAX=256; gansezahl wischdischfunktion() { zeische input[MAX]; gansezahl i=0; dobbel Wert = 0; hoerma.getline(input, MAX); alslang (input[i]>='0' && input[i]<='9') { Wert *= 10;

91

Regionales C++

Wert += input[i] - '0'; i++; } wann (input[i]==',') { dobbel NK = 1; i++; alslang (input[i]>='0' && input[i]<='9') { NK *= 10; Wert += (input[i]-'0')/NK; i++; } } seschma << input << Wert << neuzeil; }
Listing 5.1 Zahleneingabe auf Hessisch

5.2

Mer programmiere in Hessisch

Da C++ keine Interpretersprache mit einer Token-Tabelle ist, muss die bersetzung in zwei Richtungen vorgenommen werden. Der einfachere Weg ist das Schreiben von Programmen mit hessischen Schlsselwrtern und die bersetzung durch den Compiler. Hier hilft der Prprozessor, den C++ von C geerbt hat. Der Prprozessor kommt zum Einsatz, bevor der Compiler den Quelltext bersetzt. Seine Befehle beginnen mit dem Zeichen #. Der Befehl #define deniert einen neuen Begriff. Steht hinter dem Begriff eine weitere Zeichenkette, wird der Begriff in die Zeichenkette bersetzt. Dieser Mechanismus ist ideal fr die Aufgabe, hessische Begriffe in Sprachelemente von C++ zu bersetzen, bevor der eigentliche Compiler den Code zu Gesicht bekommt. Die geschweiften Klammern Diese Eigenheit von C war seinerzeit sehr hilfreich, als ich auf meinem Apple II europlus meine ersten Programmierversuche in dieser Sprache unternahm. C verwendet extensiv die geschweiften Klammern als Blockgrenzen. Von der Programmiersprache PASCAL war ich die Befehle BEGIN und END gewohnt. Natrlich sind die geschweiften Klammern sehr viel krzer und es wre auch sehr viel praktischer gewesen, htte es nur geschweifte Klammern auf der Tastatur des Apple II europlus gegeben. Mit einem kleinen PASCAL-Programm erzeugte ich die Headerdatei klammern.h. Sie enthielt nur zwei Zeilen:

92

Mer programmiere in Hessisch

5.2

#define BEGIN { #define END }


Listing 5.2 klammern.h

Die geschweiften Klammern lieen sich erzeugen, indem ihr ASCII-Wert in einer char-Variablen abgelegt wurde. Mit dieser Header-Datei konnte ich nun in C programmieren, ohne geschweifte Klammern auf der Tastatur zu besitzen. Ich musste nur den Befehl #include "klammern.h" am Kopf meines Programms einfgen und konnte BEGIN und END statt der geschweiften Klammern verwenden. Hessische Header Fr das Programmieren in Mundart brauchen wir nur eine Header-Datei, die alle hessischen Ausdrcke in die korrekten C++-Schlsselwrter umsetzt. Das bedeutet, dass die Datei lauter Zeilen der folgenden Art hat:
#define hessisch cplusplusausdruck

Soll auf Hessisch programmiert werden, wird am Anfang der Quelldatei der Befehl #include "hessisch.h" eingesetzt, und schon kann Hessisch programmiert werden.

5.2.1

Tabellengesteuert

Das Programm zur Erstellung dieser Header-Datei liest die bersetzungsdatei ein, deren Name als Kommandozeilenoption angegeben wird. Auf diese Weise wird neben den Hessen auch den Sachsen, Bayern oder Westfalen eine einfache Mglichkeit gegeben, die Programmiersprache ihrer Region anzupassen. Auch Plattdeutsch, Dnisch, Franzsisch, Italienisch oder gar Hochdeutsch kann so leicht verwendet werden. Die Klasse tTabelle soll sich um die bersetzung kmmern. Die Klasse speichert vor der Weiterverarbeitung die Vokabeln in einer Tabelle. Als Basis fr die Speicherung der bersetzungen bietet sich der STL-Container map an. Er speichert beliebige Datenelemente, die ber einen Schlssel zurckgeholt werden knnen. Die einzige Voraussetzung des Schlssels ist, dass das Kleinerzeichen als Operator deniert sein muss. Dies trifft fr die Standard-C++-Strings zu. So kann fr jedes C++-Schlsselwort die neue bersetzung gefunden werden.
#ifndef TABELLE_H #define TABELLE_H #include <string> #include <map>

93

Regionales C++

class tTabelle { public: tTabelle() {} void ladeDatei(const char *Dateiname); std::string getUebersetzung(std::string wort); void createHeader(); private: std::map<std::string,std::string> Uebersetzung; }; #endif
Listing 5.3 tabelle.h

Der Konstruktor tut das, was ich am liebsten tue, nmlich gar nichts. Es gibt eine Funktion namens ladeDatei, die die bersetzungsliste aus der Datei liest und damit die Tabelle fttert. Dann gibt es eine einfache Funktion namens getUebersetzung, die die bersetzung fr einen Begriff liefert. Und schlielich kann man mit der Funktion createHeader eine Header-Datei aus der Liste generiert werden. Die Funktion ladeDatei ffnet die Datei, in der die bersetzungstabelle steht. Sie liest zeilenweise den Inhalt und sucht in jeder Zeile das Leerzeichen. Alles, was vor dem Leerzeichen steht, ist der mundartliche Ausdruck, was dem Leerzeichen folgt, ist das C++-Schlsselwort. Da die Schlsselwrter sowohl im Hessischen als auch bei C++ keine Leerzeichen enthalten drfen, ist eine derart einfache Handhabung legitim. Korrekterweise msste das Programm eine Fehlerbehandlung durchfhren, die dafr sorgt, dass Fehler in der Eingabedatei das Programm nicht sofort zerlegt. Diese Behandlung wrde die Beispielprogramme allerdings aufblhen. Auerdem gehe ich davon aus, dass es Sie nur ein kurzes Ohrenrunzeln kostet, dieses noch einzubauen.
#include "tabelle.h" #include <iostream> #include <string> #include <fstream> #include <map> using namespace std; // Laedt die Uebersetzungstabelle aus einer Datei. // In jeder Zeile steht die Uebersetzung, ein Leerzeichen // und dann das C++ Schluesselwort. void tTabelle::ladeDatei(const char *Dateiname)

94

Mer programmiere in Hessisch

5.2

{ fstream f; string str; f.open(Dateiname); while (! f.eof() ) { getline(f, str); // zeilenweise lesen unsigned long pos = str.find(" ", 0); if (pos != string::npos) { // Leerzeichen existiert string neu = str.substr(0, pos); // ueberlese alle vorhandenen Leerzeichen while (str[pos]==' ' && pos<str.length()) { pos++; } // Rest der Zeile ist der Schluessel string alt = str.substr(pos); // Einfuegen in die map Uebersetzung[alt] = neu; } } f.close(); }
Listing 5.4 tabelle.cpp: ladeDatei

5.2.2

Erzeugen der Header-Datei

Die Funktion createHeader bewegt sich per Iterator durch die gesamte map und gibt einfach auf dem Bildschirm die #define-Zeile aus, die spter verwendet werden kann, um Hessisch zu programmieren.
void tTabelle::createHeader() { map<string,string>::iterator it = Uebersetzung.begin(); while (it!=Uebersetzung.end()) { cout << "#define " << it->second << " " << it->first << endl; ++it; } }
Listing 5.5 tabelle.cpp: createHeader

Nach diesen Vorarbeiten ist das Erzeugen der Header-Datei schnell getan. Das Hauptprogramm legt ein Objekt Tabelle an. Je nachdem, ob der Anwender

95

Regionales C++

einen Dateinamen per Parameter bergeben hat, verwendet es diesen oder ldt die Datei hessisch. Anschlieend wird createHeader aufgerufen.
#include <iostream> using namespace std; #include "tabelle.h" int main(int argc, char **argv) { tTabelle Tabelle; // waehle die Uebersetzungssprache if (argc>1) { Tabelle.ladeDatei(argv[1]); } else { Tabelle.ladeDatei("hessisch"); } // erzeuge die Headerdatei Tabelle.createHeader(); }
Listing 5.6 create_h.cpp: Hauptprogramm

Die Prprozessorbefehle werden auf dem Bildschirm ausgegeben. Mit dem Grerzeichen kann die Bildschirmausgabe in eine Datei umgeleitet werden und fr die hessische Programmierung verwendet werden. Wenn Sie das bersetzte Programm createheader genannt haben, wird der folgende Befehl die Header-Datei hessisch.h anlegen.
createheader hessisch > hessisch.h

5.3

Mer lese Listings

Nun knnen hessische Programme erstellt werden und durch einen gewhnlichen C++-Compiler bersetzt werden. Es steht nun noch die andere Richtung aus. Es soll ein beliebiges C++-Programm, das bereits in englischer Sprache vorliegt, ins Hessische bersetzt werden. Hier muss schon tiefer in die Trickkiste gegriffen werden. Ein kleiner bersetzer macht aus den englischen Schlsselwrtern ordentliches Hessisch.

96

Mer lese Listings

5.3

5.3.1

Zugriff auf die bersetzungstabelle

Fr die bersetzung des Schlsselwortes wird wieder die Klasse tTabelle verwendet mit der bereits die Header-Datei erzeugt wurde. Die Elementfunktion getUebersetzung liefert einen String, der fr den Suchbegriff hinterlegt ist. Fr den Fall, dass es den Suchbegriff nicht gibt, wird ein leerer String zurckgegeben. Der Zugriff auf die map erfolgt nicht ber die typischen rechteckigen Klammern. Der Grund liegt darin, dass ein Zugriff ber die rechteckigen Klammern immer einen neuen Eintrag fr diesen Schlssel anlegt. Da aber nicht sichergestellt ist, dass der Suchbegriff existiert, wird stattdessen die Elementfunktion find verwendet. Sie liefert einen Iterator auf das gesuchte Element zurck. Er verhlt sich beim Zugriff auf die Daten hnlich wie ein klassischer Zeiger. Existiert der Suchbegriff nicht, zeigt der Iterator hinter das Ende der map, also genau dorthin, wohin der Rckgabewert der Elementfunktion end zeigt.
string tTabelle::getUebersetzung(string wort) { map<string,string>::iterator it; it = Uebersetzung.find(wort); if (it==Uebersetzung.end()) { return ""; } else { return it->second; } }
Listing 5.7 tabelle.cpp: getUebersetzung

5.3.2

Durchlesen des Quelltextes

Das Programm showlisting liest eine Datei ein und sucht die Schlsselwrter, die es bersetzen kann. Bezeichner erkennen Die Schlsselwrter der Programmiersprache C++ bestehen ausschlielich aus Kleinbuchstaben. Das lsst es auf den ersten Blick sinnvoll erscheinen, nur die kleinen Buchstaben zu betrachten und alle anderen Zeichen zu ignorieren. Leider funktioniert das nicht so prima, wie es im ersten Augenblick scheint. Trifft das Programm beispielsweise auf eine Variable mit dem Namen Schar, so wrde es das groe S als irrelevant berspringen und ab dem Buchstaben c analysieren. Dabei kme dann zur Verblffung der Zuschauer das Schlsselwort char heraus,

97

Regionales C++

das bekanntlich in der Tabelle erscheint. Die bersetzung lautet dann Szeische, was dann auch noch verdchtig nach dem fkalen Doppelzischlaut klingt. Das Programm muss also weitergreifen und alle Bezeichner erkennen. Glcklicherweise sind diese in einer Programmiersprache immer sehr eindeutig deniert. In C++ beginnen sie mit einem Buchstaben oder einem Unterstrich. Es folgen Buchstaben, Zahlen oder Unterstriche. Sobald ein anderes Zeichen erscheint, ist der Bezeichner abgeschlossen. Erst wenn der Bezeichner komplett erkannt wurde, darf er mit der Schlsselworttabelle abgeglichen werden. Kommentare und String-Konstanten Aber dann gibt es noch weitere Ausnahmen. In Kommentaren und String-Konstanten hat ein Scanner nichts verloren. Also muss unser bersetzer neben dem Anfang von Bezeichnern auch auf das Anfhrungszeichen und auf die Kommentarzeichen achten. Erkennt das Programm ein Anfhrungszeichen, luft es einfach weiter durch die Zeile, bis das andere Anfhrungszeichen auftaucht. Sptestens am Ende der Zeile sollte das zweite Anfhrungszeichen auftauchen, ansonsten lge ein syntaktischer Fehler vor. Ein Kommentar beginnt immer mit einem Schrgstrich. Aber das zweite Zeichen ist hier sehr wichtig. Folgt dem ersten Schrgstrich ein zweiter, kann der Rest der Zeile uninterpretiert bergangen werden. Wird dagegen die Schrgstrich-SternKombination erkannt, so muss damit gerechnet werden, dass der Kommentar erst in einer spteren Zeilen endet. Dazu wird die globale Variable istKommentar eingefhrt. Sobald sie gesetzt wurde, wird erst einmal nur nach dem Kommentarende gefandet. Auch bei einer neuen Zeile prft die Funktion, ob nicht noch ein Kommentar aus der Vorgngerzeile aktiv ist. Fr diese Analysen wird ein kleiner Teil eines Scanners geschrieben. Ein Scanner ist der Teil des Compilers, der erkennt, was fr ein Symbol vorliegt. Die modernen Programmiersprachen sind so gestaltet, dass der Scanner mglichst schon am ersten Zeichen erkennen kann, welches Token vorliegt. Das ist der Grund, warum ein Bezeichner nicht mit einer Ziffer beginnen darf. So ist er leicht von einer Zahlenkonstanten zu unterscheiden. Ob danach noch Ziffern erscheinen, ist nicht mehr wichtig, denn der Scanner hat die Entscheidung getroffen und wird bis zum Ende des Bezeichners warten, bevor er sich neu entscheidet.

98

Mer lese Listings

5.3

5.3.3

String gegen Zeiger

Das Kernstck des Programms ist ein einfacher Textscanner, der durch den Quelltext luft und Schlsselworte sucht. Das Durchlaufen eines Textes zur Analyse ist ein Bereich, indem der C-Programmierer schon immer die Nase vorn hatte. Keine andere Programmiersprache, sofern man mal von Assembler absieht, kann so schnell durch einen Text jagen, wie C mit seinem Zeiger, der einfach inkrementiert werden kann. Sobald der Wert, auf den Zeiger zeigt, eine 0 ist, ist Ende, denn der klassische C-String verwendet ein 0-Byte als Endemarkierung. Alles ist auf Geschwindigkeit optimiert. Und das Schnste ist: C++ hat das alles geerbt! Auf der anderen Seite stehen die Nachfahren der Mslifresser. Mslifresser? Ja, so wurden die PASCAL-Programmierer in dem berhmten Artikel Real Programmers dont use PASCAL1 genannt. Die echten, harten Mnner hatten keine Angst vor GOTOs und benutzten FORTRAN und Assembler. Jedenfalls pegen die Nachfahren dieser Weicheier eher zu der Klasse string zu greifen und damit den Geist der schnellen Zeiger zu verraten. Vielleicht stimmt das Klischee, dass Programmierer sich niemals fr Fuball interessieren. Jedenfalls werden Foren und Newsgroups zum Stadionersatz, wenn es um solche Fragen geht. Welches ist die bessere Programmiersprache, das bessere Betriebssystem oder soll man lieber die Klasse string verwenden oder ist ein Zeiger das einzig mgliche Werkzeug bei der Analyse von Text? Ist es die Geschwindigkeit, warum solche Programme durchaus gern mit Zeigern geschrieben werden? Es knnte natrlich auch daran liegen, dass einige C++-Programmierer mit den Zeigern aufgewachsen sind und ihre Gewohnheiten ungern ndern. Wirklich schneller? Was ist der Grund, warum die Zeigeroperationen schneller sind? Der Scanner luft durch den Text und schaut sich jedes Zeichen an. Bei einem Array-Zugriff wird jeweils der Array-Index auf die Adresse des Arrays aufaddiert. Da in C und C++ ein Array und ein Zeiger in gewissen Grenzen austauschbar gehandhabt werden knnen, arbeitet der Nutzer eines Arrays eben auch mit einem Zeiger, addiert aber bei jedem Zugriff den Index auf. Der Ausdruck a[4] ist in C identisch mit *(a+4).

1 Sie nden diesen Artikel beispielsweise unter http://www.crusoe.de/pascal.htm. Eine deutsche bersetzung nden Sie unter http://www.leo.org/information/freizeit/fun/pascal.html.

99

Regionales C++

Wird dagegen ein Zeiger verwendet, der von Buchstabe zu Buchstabe geschoben wird, entfllt die Addition. Stattdessen muss nur der Zeiger bei jedem Vorrcken inkrementiert werden. Muss auf das Zeichen mehrfach zugegriffen werden, weil geprft werden muss, ob es ein Klein- oder ein Grobuchstabe ist, wird der Zugriff per Array immer teurer. Bevor Sie allerdings in allzu groen Jubel ber den Zeitgewinn ausbrechen, sollten Sie bedenken, dass heutige Computer wirklich sehr schnell addieren. Sie mssen sich also schon sehr viel Text anschauen, damit sich der Zeitgewinn lohnt. Wirklich kompliziert? Auf der anderen Seite gelten alle Arten der Zeigerverwendung als undurchschaubar und riskant. Bei der Entwicklung neuerer Programmiersprachen wurde der Zeiger verboten, um kleine Programmieranfnger nicht mit solch gefhrlichen Waffen zu erschrecken oder gar auszustatten. Das sieht der Programmierer anders, der diese Technik regelmig benutzt. Tatschlich ist der Umgang mit Zeigern nicht so gefhrlich, wie manche tun. Es gibt jede Menge berlebender. Und ob das Risiko eines amoklaufenden Zeigers wirklich so gro ist, bezweie ich jedes Mal, wenn mir Java-Programme abstrzen. An sich drfte das doch gar nicht passieren, weil die Sprache Java eigentlich die Zeigerprobleme ausgerottet haben soll. Aber diese Erfahrungen sind natrlich alles andere als reprsentativ. Beide Varianten Ehrlich gesagt stehe ich persnlich wirklich zwischen den Sthlen. Da ich bereits mit C programmiert habe, sind mir Zeiger nicht fremd und schon gar nicht ungeheuer. Andererseits ist die Klasse string durchaus elegant in C++ eingebettet. Jedenfalls habe ich mir den Spa gemacht, die zentrale Scan-Funktion einmal mit Zeigern und einmal mit dem Standard C++-String zu implementieren. Entscheiden Sie doch selbst, was Sie besser nden! Die Funktion luft durch die Zeile und sucht nach einem Bezeichner. Das erste Zeichen eines Bezeichners ist ein Buchstabe oder ein Unterstrich. In den folgenden Stellen knnen zustzlich auch Ziffern auftauchen. Erst wenn der Bezeichner komplett erkannt ist, kann er mit der Tabelle verglichen werden. Dazu muss der gefundene Bezeichner quasi herausgeschnitten werden und der Funktion find der Tabelle bergeben werden. Die Verwendung der rechteckigen Klammern legt ein neues Element an, wenn der Index bisher nicht vorhanden war. Das liegt hier aber nicht in unserer Absicht.

100

Mer lese Listings

5.3

Gibt es das Wort, muss der bisher noch nicht ausgegebene Text ausgegeben werden, dann die bersetzung, und dann muss die Suche hinter dem gefundenen Wort wieder aufgesetzt werden. Die Funktion muss aber auch nach Zeichenkettenkonstanten und Kommentaren suchen, da in diesen der Text nicht bersetzt werden soll. Eine Besonderheit bei der Analyse eines C-Strings per Zeiger ist die Abfrage des String-Endes. Ein C-String endet immer mit einem Byte, das 0 ist. Darum liefert ein Ausdruck der Form *Zeiger einer Abfrage immer false, wenn das Ende der Zeichenkette erreicht wurde. Darum bewirkt die Zeile while (*pPos), dass die Schleife so lange durchlaufen wird, bis das Ende des Strings erreicht ist.
bool istKonstante = false; // Durchsuche Zeile nach Schluesselwoertern mit Zeiger void scan(char *pPos) { char *pZeilenstart = pPos; // fuer die spaetere Ausgabe while (*pPos) { // wird 0 am Ende der Zeile if (istKonstante) { while (*pPos && *pPos!='*') ++pPos; if (*pPos=='*') { ++pPos; if (*pPos=='/') { istKonstante = false; } } } else if (istIdentStart(*pPos)) { char *pBezeichner = pPos; // Bezeichneranfang // suche nun das Ende des Bezeichners pPos++; while (istIdentStart(*pPos) || (*pPos>='0' && *pPos<='9')) { pPos++; } // wir stehen hinter dem Bezeichner // Das Zeichen sichern und durch 0 ersetzen char Sicher = *pPos; *pPos=0; // Endemarke fuer die find-Funktion string uebersetzt = Tabelle.getUebersetzung(pBezeichner); if (uebersetzt!="") { *pBezeichner = 0; // Begrenze bisherige Zeile cout << pZeilenstart; // und gebe sie aus. cout << uebersetzt; // zeig die Uebersetzung

101

Regionales C++

pZeilenstart = pPos; // hier wieder aufsetzen } *pPos = Sicher; } else if (*pPos=='"') { // Stringkonstante ++pPos; while (*pPos && *pPos!='"') ++pPos; if (*pPos==0) break; // Stringende fehlt! } else if (*pPos=='/') { pPos++; if (*pPos=='/') break; // Restzeile Kommentar if (*pPos=='*') { istKonstante = true; } } pPos++; } // Rest der Zeile ausgeben cout << pZeilenstart << endl; }
Listing 5.8 Scannen per Zeiger

Tatschlich ist der Code mit der String-Bibliothek etwas schlanker. Die Positionen werden nicht durch Zeiger, sondern durch Integer-Indizes verwaltet. Dafr wirkt die Rechnerei mit den Positionen und den Lngen der String-Funktion substr nicht wirklich schn. Aber irgendwas ist ja immer.
// Durchsuche Zeile nach Schluesselwoertern mit String void scan(string Zeile) { unsigned long Pos = 0; unsigned long Zeilenstart = 0; while (Pos<Zeile.length()) { if (istKonstante) { while (Pos<Zeile.length() && Zeile[Pos]!='*') { ++Pos; } if (Zeile[Pos]=='*') { ++Pos; if (Zeile[Pos]=='/') { istKonstante = false; } } } else if (istIdentStart(Zeile[Pos])) { unsigned long Bezeichner = Pos;

102

Mer lese Listings

5.3

// suche Ende des Bezeichners Pos++; while (istIdentStart(Zeile[Pos]) || (Zeile[Pos]>='0' && Zeile[Pos]<='9')) { Pos++; } // wir stehen hinter dem Bezeichner string uebersetzt=Tabelle.getUebersetzung( Zeile.substr(Bezeichner, Pos-Bezeichner)); if (uebersetzt!="") { // den unuebersetzten Teil ausgeben cout << Zeile.substr(Zeilenstart, Bezeichner-Zeilenstart); // und die Uebersetzung des Schluesselworts cout << uebersetzt; // An dieser Stelle fortfahren Zeilenstart = Pos; } } else if (Zeile[Pos]=='"') { // Stringkonstante ++Pos; while (Pos<Zeile.length() && Zeile[Pos]!='"') { ++Pos; } if (Pos>=Zeile.length()) break; } else if (Zeile[Pos]=='/') { Pos++; if (Zeile[Pos]=='/') break; if (Zeile[Pos]=='*') { istKonstante = true; } } Pos++; } // unbearbeiteten Teil der Zeile ausgeben cout << Zeile.substr(Zeilenstart) << endl; }
Listing 5.9 Scanner per Standard-Strings

Der Rest des Programms besteht darin, die Scan-Funktion zu fttern. Die Funktion uebersetze erwartet den Dateinamen einer Quelldatei als Parameter, liest sie zeilenweise aus und ruft die Scan-Funktion auf.

103

Regionales C++

Sie sehen hier, dass das Umschalten zwischen string und char-Zeiger nicht ganz so simpel ist. Einer der Hauptgrnde liegt darin, dass viele Funktionen bei Zeigern auch noch eine weitere Angabe ber die Lnge des Strings haben wollen. Grundstzlich wre diese zustzliche Angabe bei wirklichen Texten ja nicht erforderlich. Immerhin gibt es dieses 0-Byte, das das Ende des Strings markiert. Aber da es schon einmal vorgekommen sein soll, dass ein Programmierer einen Fehler gemacht hat und das 0-Byte vergessen hat, fordern viele Funktionen zustzlich eine Lngenangabe an. Das verhindert im Zweifelsfall, dass der Zeiger eine Rundtour durch den Speicher macht und dabei alles verwendet, was ihm vor die Nase kommt.
void uebersetze(char *Dateiname) { fstream f; // entkommentieren fuer Zeigervariante // char str[1024]; // entkommentieren fuer Stringvariante string str; f.open(Dateiname); while (! f.eof() ) { // entkommentieren fuer Zeigervariante // f.getline(str, sizeof(str)); // entkommentieren fuer Stringvariante getline(f, str); // hier sucht sich C++ die richtige Version selbst scan(str); } f.close(); }
Listing 5.10 Die Funktion uebersetze

Anhand der Kommentare ist schon zu sehen, wie Sie die String- und die Zeigervariante aufrufen knnen. Sie mssen die Variablendenition so ndern, dass str eine klassische C-Zeichenkette ist. Allerdings wird die Funktion getline unterschiedlich aufgerufen, je nachdem, ob es ein C++-String oder ein klassischer C-String ist. Die Funktion scan ist berladen. Das heit, dass C++ selbst erkennt, welche Funktion aufgerufen werden muss, um mit dem Parameter klarzukommen. Das Hauptprogramm ist weitgehend berraschungsfrei. Es erkennt als Parameter den Dateinamen, der bersetzt dargestellt werden soll. Als zweiter Parameter kann eine andere Mundart angegeben werden, vorausgesetzt, es existiert die bersetzungstabelle.

104

Mer lese Listings

5.3

int main(int argc, char **argv) { // lade die Uebersetzungssprache if (argc>2) { Tabelle.ladeDatei(argv[2]); } else { Tabelle.ladeDatei("hessisch"); } // starte die Uebersetzung if (argc>1) { uebersetze(argv[1]); } }
Listing 5.11 Das Hauptprogramm

Ich knnte mir vorstellen, dass Sie dieses Hauptprogramm ganz furchtbar gern auch einmal in Hessisch sehen mchten. Und diese Freude mache ich Ihnen natrlich gern.
gansezahl wischdischfunktion(gansezahl argc, zeische **argv) { // lade die Uebersetzungssprache wann (argc>2) { Tabelle.ladeDatei(argv[2]); } ansonste { Tabelle.ladeDatei("hessisch"); } // starte die Uebersetzung wann (argc>1) { uebersetze(argv[1]); } }
Listing 5.12 Das Hauptprogramm

Nach Fertigstellung des Hessischen C++-bersetzers hoffe ich, dass ich demnchst im Hessenjournal des Hessischen Rundfunks mit einem Orden des Landes Hessen fr die Frderung der Hessischen Sprache in der Informations- und Kommunikationstechnologie ausgezeichnet werde.

105

Theseus, der Ariadnefaden und der Minos auf Kreta. Htte es das Labyrinth nicht gegeben, wre es nur ein ganz langweiliger Stierkampf gewesen.

Labyrinth

Verwirrung muss schn sein. Jedenfalls zahlen manche Menschen Eintritt, nur um sich in einem Labyrinth verwirren zu lassen. Noch mehr Freude als der Besucher hat der Beobachter. Kann er doch zusehen, wie Leute darin herumirren und immer wieder am Ausgang vorbeilaufen. In lndlichen Gegenden frsen Landwirte Pfade in ihre Maisfelder. Fr wenige Euro darf sich die ganze Familie frhlich verlaufen und ist doch sicher, dass man sich sptestens zur Erntezeit wiedersehen wird. Selbst wenn es Verluste gibt, kann sich die ganze Familie anschauen, an welcher Stelle die Schwiegermutter die richtige Abzweigung immer wieder verpasst hat. Vielleicht gestattet der Besitzer des Labyrinths sogar das Aufstellen eines dezenten Holzkreuzes, das zumindest bis zur nchsten Aussaat an die Schwiegermutter erinnert. Wer keinen freundlichen Landwirt als Nachbarn hat und weder Maisfeld noch Saatgut besitzt, kann sich sein Labyrinth selbst bauen. Ein kleines Programm erstellt beliebig umfangreiche Labyrinthe.

6.1

Labyrinthmodell

Als Schler habe ich auf einem Rechenblatt Labyrinthe erstellt. Mit etwas bung geht das leicht von der Hand. Auf einem leeren Rechenblatt wird Wand um Wand erstellt, bis die Verwirrung komplett ist.

Abbildung 6.1 Rechenblattlabyrinth

107

Labyrinth

In diesem Labyrinthmodell haben die Wnde quasi keine Ausdehnung. Sie bilden die Rahmen um die Zellen. Es gibt auch andere Mglichkeiten, ein Labyrinth zu modellieren. Dabei fhren unterschiedliche Modellierungsversuche zu verschiedenen Ansichten und damit auch zu verschiedenen Lsungsversuchen. Aufgrund der einfacheren Speicherung und Darstellung erschien es mir zunchst sehr elegant, das Labyrinth aus Baukltzen zu bauen. Man stellt eine Flche mit Baukltzen voll und entnimmt eine Reihe von Baukltzen. Daraus entsteht ein Gang. Fr eine Abzweigung nach rechts wird einfach ein Bauklotz entnommen.

Abbildung 6.2 Bauklotzmodell

Die Vorteile liegen ganz offensichtlich in einer einfachen Speicherung des Modells und einer sehr einfachen Darstellung auf dem Bildschirm. Der Nachteil wird erst deutlich, wenn man versucht, ein Labyrinth zu erzeugen. Die Baukltze sind nmlich keineswegs alle gleich. Einige gehren zu den Wnden, einige zu den Ecken. Wann genau muss eine Ecke entfernt werden? Es zeigt sich schnell, dass die scheinbar einfachere Lsung doch auch ihre Tcken hat. Das Modell soll hier nicht weiter verfolgt werden. Ob es wirklich besser oder schlechter ist, mag Geschmacksache sein. Es zeigt aber auch, dass eine Datenmodellierung erheblichen Einuss auf die sptere Struktur des Programms hat. Unser Labyrinthmodell entstammt also dem Rechenblatt, indem die Flchen die Felder sind und auf den Linien die Wnde stehen oder eben nicht.

6.1.1

Vorgehensweise

Es lsst sich die Regel aufstellen, dass in einem wirklich guten Labyrinth jeder Punkt von jedem anderen Punkt erreichbar ist. Abkrzungen sind kontraproduktiv. Es sollte keine zwei Wege zum gleichen Ziel geben. Es gilt also, Zyklen zu vermeiden.

108

Labyrinthmodell

6.1

Gezieltes Wndeeinreien Um ein optimales Labyrinth zu erzeugen, wird es zunchst mit Wnden vollgestellt. Im Gegensatz zu meiner Lsung in der Schule werden also nicht Linien gezeichnet, sondern zunchst alle denkbaren Trennwnde gezeichnet und anschlieend Stck um Stck quasi mit dem Radiergummi wieder entfernt. Es wird an einem beliebigen Punkt begonnen. Jedes besuchte Feld wird zunchst markiert. Eine Duftmarke wre prinzipiell in Ordnung, da sie spter nicht gelscht werden muss. Aus praktischen und hygienischen Grnden ist ein kleines x vorzuziehen. Nun schaut der Generator ber die Wnde auf das benachbarte Feld. Ist dort noch keine Marke, reit er die entsprechende Wand ein. Das unbesuchte Feld wird nun seinerseits wieder markiert, und die Untersuchung der Nachbarfelder geht auf die gleiche Weise weiter. Da sich der Generator von innen nach auen durcharbeitet, wird jedes Feld irgendwann besucht und die Verbindung dorthin eingerissen wurde.

Abbildung 6.3 Vorgehensweise beim Erstellen eines Labyrinths

In der Abbildung 6.3 ist zu sehen, wie vom oberen markierten Feld aus das rechte Nachbarfeld untersucht und die Wand entfernt wird. In der rechten Zeichnung ist zu sehen, dass nun das Feld rechts unten sowohl von oben als auch von links erreicht werden kann. Je nachdem, von welcher Seite die Wand zuerst durchbrochen wird, ergibt sich ein unterschiedliches Labyrinth. Da die Wand zu einem bereits besuchten Feld nicht eingerissen wird, ist sichergestellt, dass immer nur ein Weg von einem Feld zu irgendeinem anderen Feld existiert. Zufall einbauen Wird bei der Untersuchung der Nachbarfelder eine feste Reihenfolg verwendet, entsteht ein sehr schnes, regelmiges Muster. Leider ist das aber nicht der Zweck eines Labyrinths. Also ist es ganz wichtig, den Zufall einzubauen. Zunchst wird der Ausgangspunkt zufllig gewhlt. Dann wird bei der Suche nach unbesuchten Feldern die Reihenfolge verndert. Statt also immer im Uhrzeiger-

109

Labyrinth

sinn die umgebenden Felder abzusuchen, wird einmal links, dann unten, danach oben und zum Schluss rechts geschaut. Bei der nchsten Inspektion der Nachbarn gibt es dann eine andere Reihenfolge. Erst dann ergibt sich ein vernnftiges Labyrinth. Formale Zusammenfassung Wir fassen die Erstellung eines Labyrinths noch einmal so zusammen, dass man daraus ein Programm erstellen kann. 1. Whle einen beliebigen Punkt innerhalb des Labyrinths. 2. Markiere die Zelle als besucht. 3. Betrachte die Nachbarzellen in zuflliger Reihenfolge 4. Ist die Nachbarzelle bisher nicht besucht, reie die Wand ein. 5. Verwende die Nachbarzelle als Ausgangszelle und gehe damit zu Schritt 2. Da sich die Besuche vom Ausgangspunkt Schritt fr Schritt nach auen ausbreiten, drfte einleuchten, dass am Ende jede Zelle des Labyrinths einmal besucht wurde. Da durch das Entfernen der Wand auch immer der Zugang zur neu gefundenen Zelle aufgestoen wird, ist gesichert, dass jeder Punkt des Labyrinths von jedem anderen Punkt erreichbar ist.

6.1.2

Datenstrukturen

Das Programm braucht auf jeden Fall eine Klasse, die die Daten des Labyrinths versammelt und Funktionen zur Erzeugung und zur Anzeige des Labyrinths zur Verfgung stellt. Die Klasse tLabyrinth Im hier verfolgten Modell besteht ein Labyrinth aus Feldern oder Zellen, die von Wnden umgeben sein knnen. Die Wnde haben keine relevante Ausdehnung.
#ifndef LABYRINTH_H #define LABYRINTH_H class tLabyrinth { public: tLabyrinth(int pBreite, int pHoehe); ~tLabyrinth(); void generiere(int zufX, int zufY); void zeige(); bool istTrennung(int x, int y, int x1, int y1);

110

Labyrinthmodell

6.1

void bauAusgangWest(int y) { durchbrecheTrennung(0, y, -1, y); } void bauAusgangOst(int y) { durchbrecheTrennung(Breite, y, Breite-1, y); } void bauAusgangNord(int x) { durchbrecheTrennung(x, Hoehe, x, Hoehe-1); } void bauAusgangSued(int x) { durchbrecheTrennung(x, 0, x, -1); } void durchbrecheTrennung(int x, int y, int x1, int y1); protected: int Breite; int Hoehe; private: bool istZelleBesucht(int x, int y) { return ZelleBesucht[x + y*Breite]; } bool istZelleUnbesucht(int x, int y) { return !ZelleBesucht[x + y*Breite]; } void setZelleBesucht(int x, int y) { ZelleBesucht[x + y*Breite] = true; } void untersucheNachbarn(int x, int y, int x1, int y1); void untersucheRechts(int x, int y); void untersucheOben(int x, int y); void untersucheUnten(int x, int y); bool istGueltigeKoordinate(int x, int y); bool* getTrennung(int x, int y, int x1, int y1); bool *ZelleBesucht; // Das Feld fuer die Labyrinthzellen bool *Wand; // Die Trennwaende zwischen den Zellen bool *Boden; // Die Bodenplatten zwischen den Zellen }; #endif
Listing 6.1 tLabyrinth

Als ffentliche Elementfunktionen bietet die Klasse vor allem die Funktion generiere zur Erzeugung des Labyrinths und die Funktion zeige zur Darstellung des Labyrinths an. Die Funktion istTrennung ermittelt, ob eine Trennung zwischen den angegebenen Feldern vorliegt. Diese Funktion wird spter fr das Navigieren als ffentliche Funktion bentigt. Immerhin muss das Programm ja feststellen knnen, ob es dort durchlaufen kann. Auch die Funktionen, die mit bauAusgang beginnen, werden fr das Navigieren durch das Labyrinth bentigt. Ein Labyrinth sollte schon einen Ausgang haben, wenn jemand durchluft. Das gebietet die Fairness.

111

Labyrinth

Die Funktionen im privaten Bereich sind Hilfsfunktionen, die beim Bau des Labyrinths aufgerufen werden. Zelleninformationen In jeder Zelle muss gespeichert werden knnen, ob sie bereits besucht wurde. Weitere Informationen ber eine Zelle werden derzeit nicht bentigt, sodass die Matrix der Zellen als ein zweidimensionales Feld von booleschen Variablen gespeichert werden kann. Zweidimensionale Arrays C++ untersttzt zwar zweidimensionale Arrays. Allerdings nur dann, wenn sie zumindest in einer Dimension statisch festgelegt werden. Die Gre des Labyrinths soll aber frei einstellbar sein. Insofern ist es einfacher, mit einem eindimensionalen Array zu arbeiten und die Umrechnung der Dimension selbst zu bernehmen. Das ist nicht besonders kompliziert: Statt Zelle[x][y] verwendet man einfach Zelle[x+y*Breite]. Obwohl diese Vorgehensweise sicher leicht nachvollziehbar ist, ist sie dennoch fehleranfllig. Darum verbirgt man solche Unschnheiten hinter Funktionen, die die Umrechnung der Koordinaten bernehmen. In diesem Programm heit die Funktion getTrennung.
bool* tLabyrinth::getTrennung(int x, int y, int x1, int y1) // Eine der beiden Koordinaten kann -1 sein. { if ((x==x1) && (abs(y-y1)==1)) { // x-Koordinate gleich, also Boden! y = y>y1 ? y : y1 ; // das obere Feld ist relevant return &Boden[x + Breite*y]; } else if (y==y1 && abs(x-x1)==1) { // y-Koordinate gleich, also Wand! x = x>x1 ? x : x1 ; // das rechte Feld ist relevant return &Wand[x + (Breite+1)*y]; } else { cerr << "getTrennung unmoegliche Parameter: " << x << ","<< y << " - " << x1 << "," << y1 << endl; } return 0; // das sollte zum Absturz fuehren! }
Listing 6.2 Koordinatenumrechnung in getTrennung

112

Labyrinthmodell

6.1

Die Funktion liefert einen Zeiger auf das gesuchte Element. Sie wird von den anderen Funktionen aufgerufen, die an eine Trennwand heranmssen. Beispielsweise von den folgenden beiden Funktionen:
bool tLabyrinth::istTrennung(int x, int y, int x1, int y1) { bool *Trennung = getTrennung(x, y, x1, y1); return *Trennung; } void tLabyrinth::durchbrecheTrennung(int x, int y, int x1, int y1) { bool *Trennung = getTrennung(x, y, x1, y1); *Trennung = false; }
Listing 6.3 Koordinatenumrechnung in getTrennung

Die Funktion istTrennung liefert die Information, ob eine Trennwand vorliegt und die Funktion durchbrecheTrennung reit eine Trennwand ein. Wnde und Zellen Das Programm muss speichern knnen, ob zwischen zwei Zellen eine Wand verluft. Es knnte eine Klasse fr die Zellen erstellt werden, die sich merkt, welche Wnde jede Zelle hat. Damit wrde aber jede innere Wand zweimal gespeichert. Die rechte Wand der einen Zelle ist die linke Wand ihres rechten Nachbarn. Also reicht es, wenn jede Zelle jeweils die Wand nach links und die Wand nach unten speichert. Dieser Ansatz ist aber in der Handhabung etwas umstndlich. Wird die rechte Wand einer Zelle entfernt, muss zunchst der rechte Nachbar bestimmt werden und dessen linke Wand gelscht werden. Das ist nicht befriedigend. Wesentlich sinnvoller ist es, die Trennwnde im Labyrinth zu speichern und nicht an die Zellen zu heften. Betrachtet man nur die Wnde einer horizontalen Reihe, so gibt es bei n Zellen n+1 Wnde, wenn man die Auenwnde einbezieht.
| 1 | 2 | 3 | 4 | 1 2 3 4 5 ... | n | n n+1

Damit ergibt die Gesamtzahl der senkrechten Wnde als (Breite+1) * Hoehe. Die Zahl der waagerechten Wnde einer Zeile entspricht allerdings der Anzahl der Zellen. Allerdings gibt es eine Wand mehr als es Zeilen gibt, weil es sowohl oben als auch unten eine Wand gibt. Das ergibt die Gesamtzahl von Breite * (Hoehe+1). Um nicht mehr Daten zu speichern als notwendig sind, msste eine

113

Labyrinth

Matrix fr die Bden und eine fr die Wnde angelegt werden. Damit ergeben sich zwei Datenstrukturen: die Bden und die Wnde.
tLabyrinth::tLabyrinth(int pBreite, int pHoehe) : Breite(pBreite), Hoehe(pHoehe) { ZelleBesucht = new bool[Breite * Hoehe]; for(int i=0; i<Breite*Hoehe; ++i) { ZelleBesucht[i] = false; } Boden = new bool[Breite * (Hoehe+1)]; for(int i=0; i<Breite*(Hoehe+1); ++i) { Boden[i] = true; } Wand = new bool[(Breite+1) * Hoehe]; for(int i=0; i<(Breite+1)*Hoehe; ++i) { Wand[i] = true; } }
Listing 6.4 Konstruktor der Klasse tLabyrinth

6.1.3

Zuflliger Richtungswechsel

Es muss noch erreicht werden, dass das Labyrinth keine allzu regelmigen Linien bekommt. Ein erster Schritt ist, dass der Ausgangspunkt zufllig gewhlt wird. Ein strkerer Einuss auf die Unregelmigkeiten bietet die Reihenfolge, in der die Nachbarfelder untersucht werden. Statt also immer schn im Uhrzeigersinn die Nachbarn zu besuchen, wird die Reihenfolge jedesmal ausgewrfelt. Mischen ist possible Die einfachste Methode, so etwas zu erreichen, ist ein Array mit den vier Richtungen. Dann werden per Zufall zwei Elemente gewhlt und miteinander vertauscht. Nach ein paar Wiederholungen ist die Reihenfolge bereits ausreichend durcheinander.
tRichtung Reihenfolge[4] = { oben, unten, links, rechts }; // wir mischen die Reihenfolge ein wenig int i; for (i=0; i<12; i++) { // ein paar Mal durchlaufen // wir waehlen zufaellig Indizes int a = rand() % 4; int b = rand() % 4; if (a!=b) {

114

Labyrinthmodell

6.1

// wenn sie sich unterscheiden, tauschen wir! tRichtung zwischen = Reihenfolge[a]; Reihenfolge[a] = Reihenfolge[b]; Reihenfolge[b] = zwischen; } }
Listing 6.5 Zufllig umhersuchen

Nachdem nun die Reihenfolge der Suchschritte zufllig verteilt wurde, kann das Programm dazu bergehen, die Untersuchung der Nachbarn vorzunehmen. In einer Schleife wird das vorgemischte Feld durchlaufen.
for (int i=0; i<4; i++) { switch (Reihenfolge[i]) { case oben: untersucheNachbarn(x, case unten: untersucheNachbarn(x, case links: untersucheNachbarn(x, case rechts: untersucheNachbarn(x, } }
Listing 6.6 Unregelmig durchsuchen

y, y, y, y,

x, y+1); x, y-1); x-1, y); x+1, y);

break; break; break; break;

6.1.4

Selbstentznder

Bei der Betrachtung des Algorithmus fr das Erzeugen eines Labyrinths ahnt der erfahrene Programmierer schon, dass das Problem auf eine Rekursion hinausluft. Fibonacci Rekursionen sind bei Anfngern so beliebt wie eine Blase am Fu1. Das mag damit zusammen hngen, dass die Ntzlichkeit der Rekursion immer wieder gern anhand der Fibonacci-Zahlenreihe erklrt wird. Der Begeisterung fr die Rekursion erweist dies nicht unbedingt einen Dienst, denn die meisten Menschen empnden es nicht als Makel, wenn sie nicht wissen, wie die Fibonacci-Folge rekursiv errechnet wird. Sollten Sie nicht einmal wissen, wer oder was Fibonacci ist, so schauen Sie sich einfach einmal den aufschlussreichen Artikel unter Wikipedia an. Sie werden feststellen, welch eine Vielzahl an griechischen Buchstaben in einem Artikel unterzubringen ist und was diese Folge mit dem Goldenen Schnitt zu tun hat. Ich gestehe ganz offen, dass ich auch immer wieder erst nachschlagen

1 Es gibt deftigere Vergleiche, aber dieses Buch setzt sich fr den praktizierten Jugendschutz beim Umgang mit der deutschen Sprache ein.

115

Labyrinth

muss, wenn mich ein junger Kollege mittags in der Kantine fragt, ob man nicht eigentlich den Goldenen Schnitt mithilfe der Fibonacci-Folge errechnen kann. Wie gut, dass die Berechnung eines optimalen Labyrinths nachweist, dass Rekursionen auch fr wirklich wichtige Dinge im Leben gebraucht werden. Die Funktion generiere erstellt das Labyrinth und wird zunchst mit der zufllig ermittelten Startkoordinate aufgerufen. Sie schaut in zuflliger Reihenfolge in die Nachbarfelder und untersucht sie in der Funktion untersucheNachbarn. Diese wird dann wieder die Funktion generiere aufrufen, wenn der Nachbar noch nicht besucht wurde.
void tLabyrinth::generiere(int x, int y) { // ist diese Zelle bereits besucht worden? if ( istZelleBesucht(x, y)) return; // markiere sie als bereits abgearbeitet setZelleBesucht(x, y); int Reihenfolge[4] = { 0, 1, 2, 3 }; int Richtung[4][2] = { {-1, 0}, {1, 0}, {0, 1}, {0, -1} }; // wir mischen die Reihenfolge ein wenig for (int i=0; i<12; i++) { // ein paar Mal durchlaufen // wir waehlen zufaellig Indizes des Reihenfolge-Arrays int a = rand() int b = rand() if (a!=b) { // wenn sie sich unterscheiden, tauschen wir! int zwischen = Reihenfolge[a]; Reihenfolge[a] = Reihenfolge[b]; Reihenfolge[b] = zwischen; } } // untersucheNachbarn ruft wiederum generiere auf! for (int i=0; i<4; i++) { int j = Reihenfolge[i]; untersucheNachbarn(x, y, x+Richtung[j][0], y+Richtung[j][1]); } }
Listing 6.7 Die rekursive Funktion zur Erzeugung eines Labyrinths

Die Funktion untersucheNachbarn wird die Nachbarzelle oberhalb der bergebenen Koordinate untersuchen, und, sollte diese bisher nicht besucht worden sein,

116

Labyrinthmodell

6.1

die Zwischenwand einreien. Anschlieend wird die Funktion generiere mit der Koordinate des neuen Feldes als Parameter aufgerufen. Dort wird wiederum irgendwann die Funktion untersucheNachbarn aufgerufen. Das riecht nach einer Endlosschleife, ndet aber sein natrliches Ende darin, dass irgendwann keine unbesuchten Felder mehr existieren. In der Funktion untersucheNachbarn nden Sie dann auch den Aufruf von generiere. Die Rekursion ist hier also nicht ganz direkt, sondern verbirgt sich hinter dem Aufruf einer anderen Funktion.
void tLabyrinth::untersucheNachbarn(int x, int y, int x1, int y1) { if (istGueltigeKoordinate(x1, y1)) { if (istZelleUnbesucht(x1, y1)) { // Neuer Nachbar: Wand durchbrechen durchbrecheTrennung(x, y, x1, y1); // rekursiver Aufruf!!!! generiere(x1, y1); } } }
Listing 6.8 Untersuchung des Nachbarn

Liegt die Koordinate des Nachbarn allerdings nicht in den zulssigen Grenzen des Labyrinths oder ist der Nachbar bereits besucht worden, wird die Funktion verlassen, ohne generiere aufzurufen. Da sich Rekursionen bezglich ihrer Verstndlichkeit nicht unbedingt aufdrngen, soll das Vorgehen noch einmal anhand einer kleinen Grak demonstriert werden. Die Abbildung 6.4 zeigt ein Labyrinth whrend seiner Entstehung. Die Funktion steht an der Position mit dem x. Die Felder mit dem Kringel sind bisher nicht besucht worden. Die Rekursionstiefe an dieser Stelle nennen wir einfach 1.

Abbildung 6.4 Ausgangssituation

117

Labyrinth

Das Programm hat keine andere Wahl als das Feld links zu besuchen. Alle anderen Nachbarfelder sind ja bereits abgerumt. Die Wand wird durchbrochen, die Funktion ruft sich selbst mit der Koordinate des unbesuchten Feldes auf. Durch den Selbstaufruf ist die Rekursionstiefe nun 2. Die neue Situation zeigt die Abbildung 6.5.

Abbildung 6.5 Das Feld wurde gewechselt

Hier gibt es nun zwei Nachbarn, die nacheinander abgearbeitet werden sollen. Die Reihenfolge geschieht zufllig. Wir nehmen an dieser Stelle an, dass das Programm nach oben geht. Auf hier ruft sich die Funktion selbst auf und erreicht damit die Rekursionstiefe 3.

Abbildung 6.6 Es geht nach oben

In dieser Situation gibt es nun keine Nachbarn mehr. Die Funktion luft also direkt zu ihrem Ende, ohne einen erneuten Selbstaufruf zu starten. Damit kehrt sie zurck in die Rekursionsstufe 2. Dort hat sie allerdings noch nicht alles abgearbeitet. Das zweite Nachbarfeld unten fehlt noch. Auch diese Wand wird aufgebrochen, und die Funktion wird erneut aufgerufen. Die Rekursionstiefe ist wieder 3. Die Situation ist in Abbildung 6.7 dargestellt.

118

Labyrinthmodell

6.1

Abbildung 6.7 Das letzte Feld

Auch hier stellt sich wieder die Situation, dass es kein besuchenswertes Nachbarfeld gibt. Die Funktion kommt zum Ende, springt zurck in Rekursionstiefe 2. Hier sind inzwischen beide Nachbarfelder abgearbeitet. Auch diese Funktion kommt zum Ende und kehrt zurck in die Rekursionstiefe 1, mit der wir angefangen hatten. Anfang und Ende Da jeder Punkt des Labyrinths von jedem anderen erreichbar ist, ist die Wahl des Start- und Endpunktes sehr einfach: Suchen Sie sich etwas Nettes aus. Der lngste Abstand verspricht am meisten Spa. Also nehmen Sie einfach die linke untere Ecke als Eingang und die rechte obere Ecke als Ausgang. Sie knnen aber auch nur einen Ausgang erzeugen und eine Spielgur mitten in das Labyrinth setzen, die dann wieder herausnden muss.

6.1.5

Ausgang erzeugen

Ein Ausgang ist einfach ein Loch in der Auenwand. Um ein solches zu bohren, kann die Funktion durchbrecheTrennung verwendet werden, die auch beim Generieren des Labyrinths verwendet wird. Die Elementfunktion durchbrecheTrennung erwartet als Parameter die Koordinaten zweier Felder. Die Wand, die zwischen den beiden Feldern liegt, wird dann durchtrennt.
void durchbrecheTrennung(int x, int y, int x1, int y1);

Um eine Wand zu bezeichnen, werden die Koordinaten der Nachbarfelder dieser Wand bezeichnet. Bei Auenwnden ist eine der Koordinaten immer auerhalb des Labyrinths. Die folgenden vier Aufrufe durchtrennen die Auenwand unten, oben, links und rechts.

119

Labyrinth

Labyrinth.durchbrecheTrennung(1, -1, 1, 0); Labyrinth.durchbrecheTrennung(1, Hoehe, 1, Hoehe-1); Labyrinth.durchbrecheTrennung(0, 6, -1, 6); Labyrinth.durchbrecheTrennung(Breite, 4, Breite-1, 4);

Damit der Bau eines Ausgangs einfacher wird, werden vier kleine Funktionen in der Headerdatei deniert, die einen Ausgang an der jeweiligen Auenwand erzeugt.
void bauAusgangWest(int y) durchbrecheTrennung(0, y, -1, y); void bauAusgangOst(int y) durchbrecheTrennung(Breite, y, Breite-1, y); void bauAusgangNord(int x) durchbrecheTrennung(x, Hoehe, x, Hoehe-1); void bauAusgangSued(int x) durchbrecheTrennung(x, 0, x, -1);

6.1.6

Ausgabe auf dem Terminal

Die Ausgabe des Labyrinths kann relativ leicht auf einer beliebigen graschen Oberche mit dem Aufruf zum Ziehen einer Linie erfolgen. Aber so wie die Feuerzangenbowle oder Der dritte Mann ihren Charme nur in der SchwarzWei-Fassung haben, schreit das Labyrinth geradezu danach, als Buchstabengrak ausgegeben zu werden. Auf das Einfachste reduziert, lsst sich das Labyrinth mit Plus- und Minuszeichen sowie senkrechten Strichen wunderbar darstellen.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | | | | | + +--+ + + + +--+--+--+--+ + + +--+--+--+--+ | | | | | | | | | + + + + + +--+ +--+--+ +--+ +--+--+--+ + + | | | | | | | | | | | | +--+--+--+ +--+ + + + + + +--+ +--+ +--+ + | | | | | | | | | + +--+--+ + +--+--+--+--+--+ + +--+ +--+ + + | | | | | | | | | | + +--+--+--+--+ + +--+ + + + + +--+ +--+ + | | | | | | | | | | | +--+ + +--+--+--+ + +--+ +--+ +--+ + + +--+ | | | | | | | | | | | + +--+--+--+ + + + +--+--+--+--+ + +--+--+ + | | | | | | | + + +--+--+--+ +--+--+--+ + + +--+--+--+--+ + | | | | | | | | + +--+ + + + + +--+--+--+ +--+--+--+--+--+--+ | | | | | | | | | | + +--+ + +--+--+ + + +--+--+ + + +--+ + + | | | | | | | +--+ +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

120

Labyrinthmodell

6.1

Diese Labyrinthbersicht wird durch die Funktion zeige erzeugt, die gar nicht besonders kompliziert ist. Das Hauptproblem der Funktion ist, dass die Wnde so abgefragt werden, dass sie ohne Positionierung auf dem Bildschirm von oben nach unten ausgegeben werden knnen.
void tLabyrinth::zeige() { int x, y; // Durchlaufe alle Zeilen for(y=Hoehe-1; y>=0; y--) { // Zeichne die untere Linie for(x=0; x<Breite; x++) { if (istTrennung(x, y, x, y+1)) { // unten cout << "+--"; } else { cout << "+ "; } } cout << "+" << endl; // Es folgt der Zelleninhalt for(x=0; x<Breite; x++) { // Zeichne die linke Wand, so sie da ist. if (istTrennung(x, y, x-1, y)) { // links cout << "|"; } else { cout << " "; } // zeichne den Flaecheninhalt cout << " "; } // die rechte Wand ist immer da if (istTrennung(x, y, x-1, y)) { cout << "|"; } else { cout << " "; } cout << endl; } // Nun noch die Abschlusslinie for(x=0; x<Breite; x++) { if (istTrennung(x, 0, x, -1)) { // unten cout << "+--"; } else {

121

Labyrinth

cout << "+ } } cout << "+" << endl; }

";

Listing 6.9 Funktion zur Anzeige eines Labyrinths

6.1.7

Aufrufparameter des Hauptprogramms

Das Hauptprogramm erlaubt die bergabe von Parametern. Der erste Parameter bestimmt die Hhe und der zweite Parameter die Breite des Labyrinths in Feldern an. Fr die Bildschirmausgabe ist zu bercksichtigen, dass eine Zelle drei Buchstaben in der Breite und zwei Buchstaben in der Hhe einnimmt. Der dritte Parameter ist ein Startwert fr die Zufallsfunktion. Wird hier von Hand ein und derselbe Wert bergeben, entsteht immer das gleiche Labyrinth. Das ist sehr praktisch, wenn man testen mchte.
#include "labyrinth.h" #include <iostream> using namespace std; #include <cstdlib> #include <ctime> // Das Programm erhaelt als Parameter die Dimension des Labyrinths. int main(int argc, char* argv[]) { int Breite = 40; int Hoehe = 40; if (argc>=2) { Breite = atoi(argv[1]); if (Breite < 3) return 1; } if (argc>=3) { Hoehe = atoi(argv[2]); if (Hoehe < 3) return 2; } tLabyrinth Labyrinth(Breite, Hoehe); // Durch Angabe einer Nummer kann immer wieder das gleiche // Labyrinth erzeugt werden. int ZufallsStartwert=0; if (argc>=4) {

122

Wandelgang

6.2

ZufallsStartwert = atoi(argv[3]); } // Wenn der Aufrufer nichts anderes angibt, nehmen wir die // aktuelle Uhrzeit als Startwert. if (ZufallsStartwert == 0) { ZufallsStartwert = time(0); } srand(ZufallsStartwert); // Wir brauchen eine zufaellige Startkoordinate. int x = rand() % Breite; int y = rand() % Hoehe; // ... und erzeugen ein Labyrinth Labyrinth.generiere(x, y); // Unser Stolz gehoert auf den Bildschirm Labyrinth.zeige(); }
Listing 6.10 mainlabyrinth.cpp

6.2

Wandelgang

Das Programm erzeugt nun Labyrinthe, die Sie ausdrucken und mit der Familie an trben Regentagen lsen knnen. In der zweiten Stufe soll das Labyrinth begehbar werden. Dazu wird das Labyrinth um eine Funktion erweitert, die eine dreidimensionale Sicht ermglicht. Wenn eine Klasse erweitert werden soll, geschieht dies idealerweise durch Ableitung. Also wird eine neue Klasse t3DLabyrinth erstellt, die sich von tLabyrinth ableitet. Sie erhlt vor allem neue Funktionen zur Anzeige der dreidimensionalen Sicht.
#include "labyrinth.h" class t3DLabyrinth : public tLabyrinth { public: t3DLabyrinth(int x, int y); void zeigeNord(int x, int y); void zeigeSued(int x, int y); void zeigeWest(int x, int y); void zeigeOst(int x, int y); };
Listing 6.11 mainlabyrinth.cpp

123

Labyrinth

Auch die dreidimensionale Darstellung wird durch ASCII-Art umgesetzt. Das funktioniert auf jeder Plattform und schafft sogar noch das Flair von Nostalgie.

6.2.1

Wnde im Raum

Viele Programmierer haben den Kunstunterricht dazu genutzt, die Fehler in den Aufgabenzetteln der Computer AG zu korrigieren. Falls Sie daneben noch Mue fanden, den Worten des Kunstlehrers zu lauschen, werden Sie sich vielleicht noch erinnern, dass der perspektivische Eindruck durch einen oder zwei Fluchtpunkte erzeugt wird. Alle nicht senkrechten Linien werden in Richtung der Fluchtpunkte ausgerichtet. Es entsteht der Eindruck der Tiefe. Fr das Labyrinth reicht ein Fluchtpunkt. Der Knstler spricht von einer Zentralperspektive. Und da Programmierer ja auch Knstler sind, tun sie es auch. In der Zentralperspektive treffen sich alle parallelen Linien, die in die Tiefe des Raums gehen, im Fluchtpunkt. Dinge die weiter entfernt sind, erscheinen kleiner. Fr das Labyrinth heit das, dass die Wnde, die in die Tiefe des Raumes weisen, schrge Oberkanten erhalten. Der Einfachheit halber reicht es, die Wnde, die in die Tiefe des Raums gehen, oben und unten immer gleich anzuschrgen. Dass diese Linien sich nicht im Fluchtpunkt schneiden, wird dem Anwender gar nicht auffallen, sofern er nicht ein begnadeter Knstler ist. Wenn wir ASCII-Zeichen fr die Grak verwenden wollen, bleibt uns gar keine andere Wahl: Schrgstriche gibt es leider nicht in verschiedenen Winkeln. Die Wnde, denen man frontal begegnet, erhalten keine Schrge. Allerdings werden sie umso kleiner, je weiter sie entfernt sind. Wnde, die vor anderen Wnden stehen, verdecken jene also vollstndig. Die Abbildung 6.8 zeigt, wie ein Blick ins Labyrinth prinzipiell abgebildet wird.

Abbildung 6.8 Erzeugung der 3D-Ansicht

124

Wandelgang

6.2

Von hinten nach vorn Das Programm zeichnet die Wnde des Labyrinths von hinten nach vorn. Es werden also die weiter entfernten Wnde zuerst gezeichnet. Beim Zeichnen der vorderen Wnde, werden dahinterliegende Elemente bermalt. Um dies zu erreichen, wird von der aktuellen Position in einem Korridor jede Wand dargestellt. Jede neue Wand wird deckend ber alle existierenden Wnde gemalt. Dabei stellt sich heraus, dass Frontalwnde eine Zelle links und rechts des Korridors durch fehlende Seitenwnde sichtbar bleiben knnten. Also mssen auch diese gezeichnet werden. Im Programm wird der Blick in jede Himmelsrichtung durch eine eigene Funktion umgesetzt. Wie diese Funktionen arbeiten, zeigt die Funktion zeigeNord (siehe Seite 126). Das Programm kann maximal so weit nach vorn schauen, wie Zellen zwischen der Position und dem Rand liegen. Diese Zellenabstnde bezeichnet das Programm als Ebenen. Sie werden heruntergezhlt und bilden spter die Ausgangspostion dafr, wie gro die Wnde erscheinen werden. Innerhalb jeder Ebene wird zunchst die frontale Wand und deren beide Nachbarwnde gezeichnet. Die mittlere liegt in Blickrichtung. Da das Labyrinth kein greres freies Areal enthlt, kann nur maximal eine Wand links oder rechts durch eine Lcke gesehen werden.

Abbildung 6.9 Suche nach Trennwnden

Es muss auch bestimmt werden, ob eine Wand links oder rechts der Blickrichtung verluft. Diese aus Blickrichtung gesehen linke Wand liegt zwischen der x-

125

Labyrinth

Position und der (x1)-Position. Entsprechend wrde eine rechte Wand zwischen x-Position und (x+1)-Position liegen. Das klingt etwas abstrakt. Die Abbildung 6.9 zeigt das Prinzip auf. Die Abbildung geht davon aus, dass die aktuelle Position bei 2,1 liegt. Die fnf potenziellen Trennwandpositionen werden zunchst an der in Blickrichtung liegenden Labyrinthwand untersucht. Steht dort eine Wand, wird sie gezeichnet. Anschlieend bewegt sich die y-Position auf den Betrachter zu. Auch hier werden die fnf Positionen untersucht und existierende Wnde gezeichnet. Dieser Vorgang wird wiederholt, bis die Position 2,1 erreicht ist.
void t3DLabyrinth::zeigeNord(int xPos, int yPos) { Panel.clear(); cout << "Nord "<<xPos<<","<<yPos<<endl; int MaxEbene = Hoehe-yPos-1; for (int Ebene=MaxEbene; Ebene>=0; Ebene--) { // Die Wand vor uns // Es wird die Achse links, mitte und rechts betrachtet. // Sie koennen evtl durch die Luecken gesehen werden. for (int xKorr=xPos-1; xKorr<=xPos+1; xKorr++) { if (istTrennung(xKorr, Ebene+yPos+1, xKorr, Ebene+yPos)) { Panel.zeichneWand(Ebene, xKorr-xPos); } } // Wand links in Laufrichtung if (istTrennung(xPos, Ebene+yPos, xPos-1, Ebene+yPos)) { Panel.zeichneLWand(Ebene); } // Wand rechts in Laufrichtung if (istTrennung(xPos, Ebene+yPos, xPos+1, Ebene+yPos)) { Panel.zeichneRWand(Ebene); } } Panel.zeige(); }
Listing 6.12 zeigeNord: Ein Blick nach Norden

Die Blickrichtung nach Sden ist sehr hnlich. Die Rckwand hat die y-Position 0, und damit ist die maximale Anzahl der Ebenen durch die y-Position des Betrachters bestimmt. Die Position der linken und der rechten Wand ist vertauscht. Der Korridor liegt immer noch auf der x-Achse. Allerdings muss die Reihenfolge der Betrachtung von +1 nach 1 laufen, sonst erscheint die Wand spiegelbildlich.

126

Wandelgang

6.2

Beim Blick nach Westen oder Osten liegt die Blickrichtung auf der x-Achse, wogegen der Korridor auf der y-Achse verluft. Bei der Bestimmung der linken und rechten Wand muss nun die Wand entlang der y-Achse laufen. Der Blick in Ostrichtung unterscheidet sich also in zweierlei Hinsicht vom Blick gen Norden: Statt der y-Achse luft der Korridor ber die x-Achse. Und whrend die Nordrichtung die y-Achse dekrementiert, wird in Ostrichtung die x-Achse inkrementiert. Die Unterschiede zwischen den beiden Funktionen fallen also nur auf den zweiten Blick auf.
void t3DLabyrinth::zeigeOst(int xPos, int yPos) { Panel.clear(); cout << "Ost "<<xPos<<","<<yPos<<endl; int MaxEbene = Breite-xPos-1; for (int Ebene=MaxEbene; Ebene>=0; Ebene--) { // Die Wand vor uns // Es wird die Achse links, mitte und rechts betrachtet. // Sie koennen evtl durch die Luecken gesehen werden. for (int yKorr=yPos+1; yKorr>=yPos-1; yKorr--) { if (istTrennung(xPos+Ebene, yKorr, xPos+Ebene+1, yKorr)) { Panel.zeichneWand(Ebene, -(yKorr-yPos)); } } // Wand links if (istTrennung(xPos+Ebene, yPos, xPos+Ebene, yPos+1)) { Panel.zeichneLWand(Ebene); } // Wand rechts senkrecht if (istTrennung(xPos+Ebene, yPos, xPos+Ebene, yPos-1)) { Panel.zeichneRWand(Ebene); } } Panel.zeige(); }
Listing 6.13 zeigeOst: ein Blick nach Osten

6.2.2

Bewegung durch das Labyrinth

Die virtuelle Figur muss durch das Labyrinth bewegt werden. Dazu muss die Figur die Richtung ndern knnen, und sie muss sich bewegen knnen. Dies wird sehr einfach mit den Befehlen l fr links und r fr rechts erreicht. Mit dem Befehl g fr gehen geht die Figur in Blickrichtung einen Schritt weiter.

127

Labyrinth

Bevor es vorwrts geht, sollte geprft werden, ob eine Wand im Weg steht. Ist das der Fall, gibt es ein kleines Booiiing, und es geht nicht weiter. Andernfalls wird die Figur in Blickrichtung bewegt. Hat die Figur den Rand des Labyrinths berschritten, hat der Spieler den Ausgang gefunden, und es ist Zeit zu gratulieren.
int Richtung[4][2] = { {0, 1}, {1, 0}, {0, -1}, {-1, 0} }; ... while (Kommando!='q' && Kommando!='Q') { if (xDiff==-1) Labyrinth.zeigeWest(xPos, yPos); if (xDiff==1) Labyrinth.zeigeOst(xPos, yPos); if (yDiff==-1) Labyrinth.zeigeSued(xPos, yPos); if (yDiff==1) Labyrinth.zeigeNord(xPos, yPos); cin >> Kommando; switch (Kommando) { case 'L': case 'l': Laufrichtung--; if (Laufrichtung<0) Laufrichtung=3; xDiff=Richtung[Laufrichtung][0]; yDiff=Richtung[Laufrichtung][1]; break; case 'R': case 'r': Laufrichtung++; if (Laufrichtung>3) Laufrichtung=0; xDiff=Richtung[Laufrichtung][0]; yDiff=Richtung[Laufrichtung][1]; break; case 'G': case 'g': if (Labyrinth.istTrennung(xPos, yPos, xPos+xDiff, yPos+yDiff)) { cout << "Booiiing!!!!" << endl; } else { xPos +=xDiff; yPos +=yDiff; if (xPos<0 || xPos>=Breite || yPos<0 || yPos>=Hoehe) { // Ausgang gefunden! cout << "Hurra! Ausgang gefunden!" << endl; Kommando = 'Q'; } } break; } }
Listing 6.14 Bewegung durch das Labyrinth

128

Wandelgang

6.2

6.2.3

Mitmachaktion: eine Multifunktionsroutine

Beim Blick auf die Funktionen zeigeNord, zeigeSued, zeigeWest und zeigeOst entsteht bei den meisten Programmierern der natrliche Ehrgeiz, eine Funktion zu schaffen, die alle Blickrichtungen behandelt. Die gewnschte Richtung knnte per Parameter festgelegt werden. Die hssliche Fallunterscheidung im Hauptprogramm wrde entfallen, und die Klasse wre um drei Funktionen schlanker. Natrlich hat dieser Ehrgeiz auch mich gepackt. Um ehrlich zu sein, hatte ich zunchst mit der allgemeinen Funktion begonnen. Die Routine war bereits fast fertig und ich war sicher, dass jeder Leser schwer beeindruckt sein wrde, welch rafnierte Kniffe ich zu einer hocheleganten und wunderbar abstrakten Funktion zusammenfhren kann. Leider scheiterte diese elegante Funktion an allerlei Sonderfllen. Die kleinen Korrekturen machten das elegante Werk schnell unbersichtlich, und so entschloss ich mich, den Rckzug anzutreten und erst einmal jede Richtung separat zu behandeln. Wiederstrebende Ziele Neben der Eitelkeit gibt es sehr gute Grnde fr die Zusammenfassung. In den vier Einzelfunktionen erscheint mehr oder weniger viermal der gleiche Code. Stellt man spter einen Flchtigkeitsfehler fest, muss er an vier verschiedenen Stellen korrigiert werden. Dagegen steht das Prinzip der Klarheit. Wenn eine Funktion bersichtlich und leicht verstndlich ist, sinkt die Wahrscheinlichkeit fr Fehler erheblich. Bei kommerziellen Software-Projekten wird die Fehlerbehebung von einem anderen Programmierer durchgefhrt als die ursprngliche Erstellung. Dann ist es gut, wenn der Kollege nicht erst Knobelspiele anstellen muss, um zu begreifen, was der Code eigentlich tun soll. Lsungshinweise Der Prototyp der allgemeinen Funktion zeigeRichtung hat zwei Parameter mehr namens xDiff und yDiff, die die Richtung der Blickrichtung bergeben. Einer von beiden ist immer 0, weil das Programm niemals diagonal durch das Labyrinth streifen wird. Die Blickrichtung nach links wird durch eine 1 in xDiff festgelegt. Der Blick nach Norden wrde durch eine 1 in yDiff erreicht.
void t3DLabyrinth::zeigeRichtung(int xPos, int yPos, int xDiff, int yDiff);
Listing 6.15 Komplexe eierlegende Wollmilchfunktion

Die Tatsache, dass die jeweils ungenutzte Richtung 0 ist, kann eingesetzt werden, um in dieser Richtung unerwnschte Ergebnisse zu unterdrcken. Beispielsweise

129

Labyrinth

erreicht man den Aufbau des Korridors durch zwei verschachtelte Schleifen. Es werden die Variablen xKorr und yKorr von 1 nach 1 durchlaufen. Durch Quadrieren der Blickrichtung ist das Vorzeichen eindeutig positiv. Der Korridor fr die derzeit nicht bentigte Blickrichtung bleibt durch die Multiplikation mit 0 inaktiv.
for(int xKorr=(-1)*yDiff; xKorr*xKorr <= 1; xKorr=yDiff?xKorr+yDiff:2) { // Das Gleiche noch fuer y: for(int yKorr=xDiff; yKorr*yKorr <= 1; yKorr=xDiff?yKorr-xDiff:2) {
Listing 6.16 Der Korridor mit zwei Schleifen

Nehmen wir an, die Blickrichtung ist Norden, also xDiff=0 und yDiff=1. Dann muss yKorr von 1 nach 1 laufen. xKorr wird also mit -yDiff initialisiert. Die Endabfrage der Schleife ist etwas ungewhnlich. Da nicht sicher ist, ob die Schleife von 1 herunterzhlt oder von 1 heraufzhlt, wird xKorr fr die Abfrage quadriert. Ist sie 0 oder 1, bendet sie sich noch in ihrem Arbeitsbereich. Wird sie 2, ist der Korridor bearbeitet. So richtig wild wird es bei Erhhung der Laufvariablen. Wrde einfach yDiff aufaddiert oder abgezogen, wird die Schleife endlos, wenn yDiff 0 ist. Also wird der Fragezeichenoperator verwendet und ausgenutzt, dass 0 logisch falsch ist. Wenn yDiff also nicht 0 ist, dann xKorr um yDiff erhht. Da xKorr mit -yDiff initialisiert wurde, luft xKorr sauber in die richtige Richtung. Ist yDiff dagegen 0, muss die Schleife exakt einmal laufen. Das wird durch die direkte Zuweisung einer 2 erreicht. Die zweite Schleife ber yDiff luft fast genauso. Allerdings muss yKorr bei 1 beginnen, wenn xKorr auch 1 ist. Dadurch ndert sich nicht nur die Initialsierung, sondern auch die Erhhung der Laufvariablen. Jetzt muss xDiff abgezogen werden. Wird eine solche Kleinigkeit bersehen, funktioniert die Routine nicht mehr oder luft sogar in eine Endlosschleife. Auf diese Weise ist es nun mglich, die weiteren Abfragen und Funktionsaufrufe zu parametrisieren. Allerdings wird die Funktion zu einem unleserlichen Monstrum. Diese Art der Programmierung schmeichelt der Eitelkeit ungeheuer. Sie demonstrieren die Fhigkeit, hochkomplex zu denken. Bezahlt wird diese Eitelkeit vom Auftraggeber. Er muss die Zeiten des Denkens und vor allem aufwendige Wartungsarbeiten bezahlen. Ein kleiner Vorzeichenfehler fhrt schnell zur Katastrophe. Soll dieser auch noch von einem fremden Programmierer behoben werden, ist die Explosion einer solchen Zeitbombe vorprogrammiert.

130

Wandelgang

6.2

Vielleicht sind Sie anderer Ansicht. Immerhin wird in diesem Buch rein aus Spa programmiert. Vielleicht haben Sie eine Idee, wie das Problem mit ein paar kleinen Kniffen sehr bersichtlich gelst werden kann. Dann wetzen Sie doch bitte den Compiler und schreiben eine Funktion, die zeigeNord und Kollegen in Rente schickt. Senden Sie das Ergebnis bitte an den Verlag. Wir werden die besten Vorschlge gern im Forum zum Buch verffentlichen. Ich freue mich auf Ihre clevere Lsung.

6.2.4

ASCII-Panel

Mit einfachen Buchstaben kann tatschlich ein perspektivischer Eindruck erzeugt werden. Zugegebenermaen braucht es fr den Betrachter eine kleine Portion Fantasie.
\............................... |\.............................. ||\........................../+|||\......................../||X ||||\.......................|||X |||||\......................|||X ||||||\...................+-|||X |||||||\..................|X|||X ||||||||+--------------+..|X|||X |||||||||XXXXXXXXXXXXXX|..|X|||X |||||||||XXXXXXXXXXXXXX|--|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|XX|X|||X |||||||||XXXXXXXXXXXXXX|--|X|||X |||||||||XXXXXXXXXXXXXX|..|X|||X ||||||||+--------------+..|X|||X |||||||/..................|X|||X ||||||/...................+-|||X |||||/......................|||X ||||/.......................|||X |||/........................\||X ||/..........................\+|/.............................. /...............................

Die dreidimensionale Darstellung ist nicht ganz so trivial mit ASCII-Zeichen zu realisieren wie der berblick ber das Labyrinth.

131

Labyrinth

Fr die Zeichnung des 3D-Labyrinths mssen Positionen angesprochen werden knnen. Leider gibt es dafr keinen rechten Standard. Unter MS-DOS wurde die ANSI.SYS verwendet. Unter UNIX ist die Curses-Bibliothek Standard, die unter Linux in der ncurses ihre Weiterentwicklung ndet. Um portabel zu bleiben, verwendet das Programm einfach ein eigenes kleines Zeichenbrett in Form einer char-Matrix. Darin wird gezeichnet, und wenn alles fertig ist, kann es leicht auf dem Bildschirm ausgegeben werden. Dazu wird die Klasse tPanel verwendet.
class tPanel { public: tPanel(); void zeichneWand(int xPos, int yTiefe); void zeichneLWand(int Tiefe); void zeichneRWand(int Tiefe); void zeige(); void clear(); private: char Feld[MAXPANEL][MAXPANEL]; };
Listing 6.17 Die Klasse tPanel

Die Klasse bietet Funktionen zum Zeichnen der frontalen Wand und der Seitenwnde. Im privaten Bereich ist das zweidimensionale char-Array zu sehen, in dem die Malereien zwischengespeichert werden. zeichneWand Die Funktion zeichneWand hat zwei Parameter. In der Variablen xPos wird die Abweichung von der Mittelachse angegeben. Der Wert ist also 1, 0 oder 1. Der zweite Parameter ist die Tiefe. Dieser Wert gibt an, wie weit die Wand entfernt ist und bestimmt damit ihre Ausdehnung.
// pos geht von -1 (links), 0 (mitte), 1 (rechts) // Tiefe gibt die Entfernung in Zellen an void tPanel::zeichneWand(int Tiefe, int pos) { // Auch eine Wand der Tiefe 0 ist einen Schritt vor uns // Sonst koennten wir sie nicht sehen! Tiefe++; // Die Kantenlaenge ergibt sich aus der Perspektive int Kante = MAXPANEL-2*DIST*Tiefe; int yStart = (MAXPANEL-Kante)/2; int xStart = pos*(Kante) + yStart;

132

Wandelgang

6.2

int yEnde = yStart + Kante - 1; int xEnde = xStart + Kante - 1; if (xStart<0) xStart=0; if (xEnde>=MAXPANEL) xEnde=MAXPANEL; // die folgenden Grenzen sollten eigentlich nur bei einem // Programmfehler ueberschritten werden. if (yStart<0) yStart=0; if (yEnde>=MAXPANEL) yEnde=MAXPANEL-1; for (int xi=xStart; xi<xEnde; xi++) { for (int yi=yStart; yi<yEnde; yi++) { Feld[xi][yi] = 'X'; } Feld[xi][yStart] = '-'; Feld[xi][yEnde] = '-'; } for (int yi=yStart; yi<yEnde; yi++) { if (xStart!=0) Feld[xStart][yi] = '|'; if (xEnde!=MAXPANEL-1) Feld[xEnde][yi] = '|'; } if (xStart!=0) { Feld[xStart][yStart] = '+'; Feld[xStart][yEnde] = '+'; } if (xEnde!=MAXPANEL-1) { Feld[xEnde][yStart] = '+'; Feld[xEnde][yEnde] = '+'; } }
Listing 6.18 zeichneWand zeichnet die Frontalwnde.

Die Funktionen zeichneLWand und zeichneRWand besitzen nur den Parameter Tiefe. Mehr brauchen sie auch nicht, da nur die Seitenwnde des Korridors gezeichnet werden.
void tPanel::zeichneLWand(int Tiefe) { for (int y=DIST*Tiefe+1; y<MAXPANEL-DIST*Tiefe-1; y++) { Feld[DIST*Tiefe][y] = '|'; Feld[DIST*Tiefe+1][y] = '|'; } Feld[DIST*Tiefe][DIST*Tiefe] = '\'; Feld[DIST*Tiefe+1][DIST*Tiefe+1] = '\'; Feld[DIST*Tiefe][MAXPANEL-DIST*Tiefe-1] = '/'; Feld[DIST*Tiefe+1][MAXPANEL-DIST*Tiefe-2] = '/'; }

133

Labyrinth

void tPanel::zeichneRWand(int Tiefe) { for (int y=DIST*Tiefe+1; y<MAXPANEL-DIST*Tiefe-1; y++) { Feld[MAXPANEL-DIST*Tiefe-2][y] = '|'; Feld[MAXPANEL-DIST*Tiefe-1][y] = '|'; } Feld[MAXPANEL-DIST*Tiefe-2][DIST*Tiefe+1] = '/'; Feld[MAXPANEL-DIST*Tiefe-1][DIST*Tiefe] = '/'; Feld[MAXPANEL-DIST*Tiefe-2][MAXPANEL-DIST*Tiefe-2] = '\'; Feld[MAXPANEL-DIST*Tiefe-1][MAXPANEL-DIST*Tiefe-1] = '\'; }
Listing 6.19 zeichneWand zeichnet die Frontalwnde.

Es ist Ihnen vielleicht aufgefallen, dass die Seitenwnde mit senkrechten Linien gefllt sind. Dieser kleine Kniff sorgt dafr, dass die Abgrenzungen zwischen den Wnden nicht doppelt erscheinen. Da nun alles senkrechte Linien hat, wirkt es eher, als sei die Wand schrafert. Die Frontalwnde werden in der vorliegenden Version mit dem Grobuchstaben X gefllt und das leere Panel mit Punkten, die an Decke und Boden nach dem Zeichnen der Wnde erhalten bleiben. Ursprnglich waren die unterschiedlichen Markierungen fr Debug-Zwecke eingebaut. Sie vertiefen aber die dreidimensionale Wirkung sehr gut. So sind sie geblieben. Wenn alle Zeichnungen in dem Panel eingetragen sind, wird der Inhalt angezeigt. Das tut die Funktion zeige.
void tPanel::zeige() { for (int y=0; y<MAXPANEL; y++) { for (int x=0; x<MAXPANEL; x++) { cout << Feld[x][y]; } cout << endl; } cout << endl; }
Listing 6.20 Funktion zur Anzeige des Panels

134

Was denn noch?

6.3

6.3

Was denn noch?

Die vorgestellte Labyrinth-Klasse kann zu allerlei Spielerei ausgebaut werden. Sicher haben Sie selbst schon einige Ideen, was man aus dem Programm alles machen kann. High-Score Sie knnen die Schritte zhlen, die der Benutzer braucht, um aus dem Labyrinth zu entkommen. Alternativ stoppen Sie die Zeit. Damit sind die Versuche, dem Labyrinth zu entkommen, vergleichbar, und es ist mglich, eine Hall of Fame zu erstellen. Wenn die Spieler unter Zeitdruck durch das Labyrinth laufen, verlieren Sie die Konzentration, und es wird deutlich schwieriger, den Ausgang zu nden. Auerdem scheiden die klassischen Lsungen aus, wenn man nicht beliebig lang im Labyrinth herumlaufen kann. Sie werden den Ausgang des Labyrinths immer nden, wenn Sie strikt in die gleiche Richtung abzweigen, also beispielsweise immer die linke Hand an der Wand lassen. Schleimspur Markieren Sie doch, wo der Spieler entlanggelaufen ist. Wird das Labyrinth verlassen, kann angezeigt werden, wo der Spieler gewesen ist. Das ist gar nicht so schwierig. Sie brauchen nur eine weitere Flag in jedes Feld zu setzen, ob der Spieler schon hier war. Am Ende zeigen Sie das Labyrinth von oben und lassen in Abhngigkeit von dieser Markierung ein Zeichen in dem Feld stehen. Schatzsuche Sie knnen Schtze im Labyrinth verstecken, die der Spieler nden muss, bevor er das Labyrinth verlassen darf. Hochausende Grak Sie knnen das Spiel auf eine grasche Benutzeroberche portieren. Dann sind natrlich viel hbschere Zeichnungen mglich.

135

Wenn sich die Ereignisse berschlagen, hlt es auch die Buttons nicht mehr auf ihren Pltzen.

Sprunghaft

In der Zeit, als die Computer noch klein und teuer waren, war der Einstieg in die Programmierung billig. Der BASIC-Interpreter was immer Dijkstra auch davon gehalten haben mag war bereits im ROM eingebrannt, und die damit erstellten Programme sahen nicht viel anders aus als die Programme, die teures Geld kosteten. Zum Glck starb BASIC aus. Das ist auch nicht weiter schlimm. Mit C++ kann man schlielich viel besser programmieren. Inzwischen gibt es sogar kostenlose Entwicklungsumgebungen. Alles bestens! Oder auch nicht. Die normalen mit Standard C++ entstandenen Programme sehen nicht so aus wie die gekauften. Das liegt einfach daran, dass heutzutage Fenster Standard sind. Und diese sind nicht Bestandteil des Standards von C++. C++ hat den Vorteil, dass es C-APIs direkt ansprechen kann, und blicherweise sind die nativen Betriebssystem-APIs mit C-Schnittstellen versehen. Das garantiert schnellste Performance, aber leider auch Systemabhngigkeit. Und schon gibt es das Problem der Portierbarkeit. Das ist bitter, weil ich in diesem Kapitel endlich einmal ein Superprogramm schreiben wollte. Und Superprogramme haben heutzutage nunmal Fenster. Wenn ich fr dieses Projekt die Windows-API Win32 verwenden wrde, hagelte es sofort Proteste: Die nutzt doch keiner. Viel zu kompliziert! Da nimmt man MFC. Quatsch, das ist doch von gestern. Heutzutage wird .NET eingesetzt. Es meldet sich eine Stimme aus dem Hintergrund und fragt: Gibt es das auch fr Macintosh?. Untersttzt wird die lauter werdende Menge von einer Gruppe LinuxFans. Bekannt fr ihre Einigkeit singen sie einen mehrstimmigen Choral aus Qt, GTK und Motif.

7.1

Grasche Oberchen

Die heute so selbstverstndliche grasche Oberche hatte sich anfangs gar nicht so selbstverstndlich durchgesetzt. Das hing damit zusammen, dass die Computer jener Zeit nicht so leistungsfhig waren und die Darstellung von Grak verhltnismig viel Rechenleistung erforderte.

137

Sprunghaft

Abbildung 7.1 Xerox Alto mit Rechner (Foto: Von Joho345 als Public Domain freigegen.)1

So wurden die Befehle eiig eingetippt. Nur eine kleine Gruppe von Entwicklern baute am Forschungszentrum Xerox PARC den Xerox Alto bereits Anfang der 1970er Jahre mit einer graschen Oberche. Etwa 1981 wurde der Xerox Star entwickelt, der eine erste kommerzielle Nutzung der graschen Oberche darstellte.

1 http://de.wikipedia.org/wiki/Xerox_Alto

138

Das Superprogramm

7.2

Die Firma Xerox, deren Hauptgeschft Kopiergerte waren, hatte das Forschungszentrum eingerichtet. Die Forschungsergebnisse sollten Xerox fr die Zeit des papierlosen Bros vorbereiten. Allerdings hatte die Geschftsleitung wohl keine rechte Vorstellung, wie man aus diesen Entwicklungen ein marktreifes Produkt herstellen konnte. Steven Jobs, einer der beiden Grnder von Apple, besuchte 1979 Xerox PARC und lie sich die Konzepte der graschen Oberche vorfhren. Dieser Besuch beeindruckte ihn so sehr, dass er versuchte, einen marktfhigen Computer zu bauen, der sich grasch bedienen lie. Er einigte sich mit Xerox ber einen Aktiendeal, und einige der Entwickler des Xerox PARC wechselten zu Apple. Der erste Versuch war die Apple Lisa im Jahr 1983, die aber zu teuer war und so nur Verluste einbrachte. Dann kam 1984 der Apple Macintosh auf den Markt.

7.2

Das Superprogramm

Dieses Kapitel ist angetreten, das Superprogramm vorzustellen. Von nichts anderem wird auf der nchsten CeBit die Rede sein. Und Sie sind nicht nur Zeuge bei der Entstehung. Nein, Sie werden es selbst weiterentwickeln knnen! Durch die Synergien aus unseren Skills knnen wir uns gemeinsam den Herausforderungen stellen, die die Internet Economy stellt. Aus der innovativen Community wird eine Applikation geschaffen, die eine neue Destination hat und die Menschen dort abholt, wo sie stehen. Mit diesem Potenzial werden wir Visionen schaffen, die die Welt begeistern wird. Kommt Ihnen der Zungenschlag bekannt vor? Nein? Dann waren Sie offenbar noch nie auf der CeBit. Die CeBit ist das Treffen aller Marketing-Experten der ITBranche. Dort kommen sie vllig unbelastet von Knowhow mit ihren Kollegen aus den anderen Firmen zusammen, um sich gegenseitig alle Details von Dingen beschreiben, die sie nicht verstanden haben. Als Programmierer pegt man sich da am besten herauszuhalten. Genau fr diese Gruppe wird das Superprogramm geschrieben. Und das wichtigste fr diese Zielgruppe ist das Outt, die Corporate Identity und das BullshitBingo2. Also muss eine grasche Oberche her. Damit alle zu kurz kommen, habe ich weder Win32 noch Motif verwendet, sondern die wxWidgets-Bibliothek herangezogen. Damit hat keiner gewonnen und keiner verloren. Alle sind beleidigt, und das ist ganz gut so, denn Einigkeit macht stark. Bevor Sie nun das Buch in hohem Bogen in den Ofen werfen, lassen Sie mich noch erwhnen, dass das Buch in der Gasheizung nicht rckstandsfrei verbrennt und dass die verschie-

2 Spielregeln nden Sie auf http://de.wikipedia.org/wiki/Bullshit-Bingo.

139

Sprunghaft

denen Grakumgebungen gar nicht so unterschiedlich sind, sodass die Beispiele schnell in Ihre Lieblingsumgebung umzusetzen sind. Falls Sie nmlich eine Lieblingsumgebung haben, werden Sie sie beherrschen, und dann bereitet Ihnen die Portierung auch kein Problem.

7.3

Ereignisorientiert

Aus Sicht des Programmierers liegt das klassische Betriebssystem so lange faul in der Ecke herum, bis die Anwendung eine Anforderung hat. Bei graschen Oberchen ist das etwas anders. Hier geht alle Aktivitt vom Benutzer aus. Der Anwender klickt irgendwohin. Die GUI schaut, welches Programm getroffen wurde und meldet sich bei diesem mit einem Ereignis Mausklick. Sollte das Men getroffen worden sein, lautet das Ereignis allerdings Menauswahl. Das Programm reagiert auf das Ereignis, erledigt die gewnschten Anforderungen und bergibt die Kontrolle zurck. Tatschlich arbeiten alle graschen Oberchen nach diesem Prinzip, und mehr oder weniger offensichtlich verhalten sich auch die verschiedenen APIs in dieser Form.

7.3.1

Der Grundrahmen

wxWidgets ist eine Klassenbibliothek, die auf sehr vielen Plattformen verfgbar ist. Es gibt sie fr die relevanten Desktops, aber auch fr PDAs und Mobiltelefone. Die Bibliothek wurde inzwischen auch auf andere Programmiersprachen angepasst. Die wxWidgets-Bibliothek wird auf der Basis der Zielumgebung bersetzt. Damit ist die native Oberche in den Elementen der wxWidgets enthalten, und so haben die Programme auch das Aussehen der jeweiligen Umgebung. Trotz gleichen Quellcodes sieht ein Programm wie eine Windows-Applikation aus, wenn sie unter Windows bersetzt wurde, und wie eine Linux-Applikation, wenn Sie unter Linux bersetzt wurde. Das Rahmenprogramm fr eine Applikation wird immer etwas abstrakt und berfrachtet. Allerdings lsst sich diese kein Programmierer fr jedes Programm selbst einfallen. Entweder der Programmierer bittet die freundliche IDE, ihm ein Rahmenprogramm vorzugeben oder er bernimmt es aus einem Beispielprogramm. In jedem Fall passt er es fr seine Bedrfnisse an. Sie mssen also nicht alles vollstndig verstehen, um eine Anwendung zu schreiben. Wissen ist allerdings auch nicht wirklich strend.

140

Ereignisorientiert

7.3

Praktischerweise erzeugt sowohl Bloodshed Dev C++ unter Windows als auch KDevelop unter Linux ein solches Rahmenprogramm, wenn Sie eine wxWidgetsApplikation als neues Projekt auswhlen. Bloodshed Dev C++ ist kostenlos und auf der Buch-CD enthalten. KDevelop knnen Sie leicht unter jeder Linux-Distribution nachinstallieren. Im Anhang nden Sie eine nhere Beschreibung fr beide IDEs. Eine wxWidgets-Anwendung besitzt eine Applikationsklasse, die von wxApp abgeleitet wird und die die virtuelle Funktion OnInit implementiert. Die Hauptaufgabe dieser Klasse ist es, einen Fensterrahmen zu starten, der sich wiederum von wxFrame ableitet. Die Headerdatei unseres Superprogramms macht da keine Ausnahme und hat folgendes Aussehen:
#ifndef _HIDEBUTTON_H_ #define _HIDEBUTTON_H_ class HideButtonapp : public wxApp { public: virtual bool OnInit(); }; class HideButtonFrame : public wxFrame { public: HideButtonFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnQuit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnMouseEvent(wxMouseEvent& event); void OnSize(wxSizeEvent& event); void OnPaint(wxPaintEvent& event); private: wxButton *JaButton, *NeinButton; DECLARE_EVENT_TABLE() }; enum { Menu_File_Quit = 100, Menu_File_About }; #endif // _HIDEBUTTON_H_
Listing 7.1 Applikations-Header

Die Klasse fr das Rahmenfenster ist zwar schon grer als die Applikationsklasse, aber auf den zweiten Blick doch nicht so kompliziert. Sie enthlt einen Konstruktor. Die Funktionen, die mit dem Prx On beginnen, reagieren auf Ereignisse

141

Sprunghaft

der GUI. Im privaten Bereich werden zwei Buttons verwendet. Mit dem Makro DECLARE_EVENT_TABLE wird eine Ereignistabelle angekndigt. Die Implementierungsdatei beginnt mit ein paar selbst geschnitzten Konstanten, der Ereignistabelle und der schon erwhnten Funktion OnInit.
#include <wx/wx.h> #include "hidebutton.h" const int NEIN_ID=5; const int JA_ID=4; const const const const const int int int int int ButtonX=50; ButtonY=100; Breite=120; Abstand=50; Hoehe=30;

Listing 7.2 Konstanten

Die Konstanten werden spter vor allem fr den Fensteraufbau bentigt. Die beiden IDs zu Anfang werden fr die beiden Buttons bentigt. Sie mssen vor allem eindeutig sein. Die Konstante NEIN_ID wird allerdings in der Ereignistabelle bentigt. Die Ereignistabelle ist ein Makro, das vom Rahmenprogramm generiert wird. Es dient dazu, die Ereignisse mit den behandelnden Funktionen zu verbinden. So wird das Ereignis EVT_SIZE auftreten, wenn die Gre des Fensters verndert wird. Sobald es auftritt, wird nach dieser Tabelle die Funktion OnSize der Klasse HideButtonFrame aufgerufen. Sie wird also das Ereignis behandeln. Ereignisse, die nicht explizit behandelt werden, bernimmt das System selbst.
BEGIN_EVENT_TABLE( HideButtonFrame, wxFrame ) EVT_MENU( Menu_File_Quit, HideButtonFrame::OnQuit ) EVT_MENU( Menu_File_About, HideButtonFrame::OnAbout ) EVT_MOUSE_EVENTS(HideButtonFrame::OnMouseEvent) EVT_SIZE ( HideButtonFrame::OnSize) EVT_PAINT(HideButtonFrame::OnPaint) EVT_BUTTON(NEIN_ID, HideButtonFrame::OnQuit) END_EVENT_TABLE()
Listing 7.3 Ereignistabelle

Alternativ zur Ereignistabelle kann ein Ereignis auch durch Aufruf der Funktion Connect mit ihrer Behandlungsfunktion verbunden werden.

142

Ereignisorientiert

7.3

Das Makro IMPLEMENT_APP verbirgt die lstigen Details. In der Funktion OnInit wird der Fensterrahmen erzeugt und angezeigt. In den Parametern des Fensters wird der Name bergeben, der spter den Schiebebalken zieren wird. wxWidgets hat einen eigenen Stringtyp. Textkonstanten mssen mit der Funktion wxT gecastet werden. Als weitere Parameter erhlt das Fenster die Ausgangsposition, die vom Typ wxPoint ist und die anfngliche Ausdehnung vom Typ wxSize.
IMPLEMENT_APP(HideButtonapp) bool HideButtonapp::OnInit() { HideButtonFrame *frame = new HideButtonFrame( wxT("Superprogramm"), wxPoint(50,50), wxSize(3*Breite+4*Abstand, 340) ); frame->Show(true); SetTopWindow(frame); return true; }
Listing 7.4 Implementierung der Applikationsklasse

Sie sehen, dass die Gre des Fensters bereits von der Applikation auf dessen Inhalt vorbereitet ist. Die Position und Gre werden in der Regel auch verwendet. Allerdings steht es der GUI auch frei, andere Werte zu verwenden, wenn beispielsweise die Auflsung die gewnschten Werte nicht hergibt. Damit ist die Applikation bereits im Geschft. Nun geht es an den Fensterrahmen.

7.3.2

Fensterelementebau

Die nchste Stufe besteht darin, ein Fenster anzulegen. Das Fenster besteht wieder aus einer Klasse, die dieses Mal von der Klasse wxFrame abgeleitet wird. Die Implementierung des Konstruktors sorgt dafr, dass das Fenster erscheint und sich bei der GUI anmeldet. Dazu ruft der Konstruktor ber einen Initialisierer den Konstruktor der Basisklasse wxFrame auf.
HideButtonFrame::HideButtonFrame(const wxString& title, const wxPoint& pos, const wxSize& size ) : wxFrame((wxFrame *)0, -1, title, pos, size) { wxMenu *menuFile = new wxMenu; menuFile->Append( Menu_File_About, wxT("&ber...")); menuFile->AppendSeparator(); menuFile->Append( Menu_File_Quit, wxT("&Beenden"));

143

Sprunghaft

wxMenuBar *menuBar = new wxMenuBar; menuBar->Append( menuFile, wxT( "&Datei" ) ); SetMenuBar( menuBar ); CreateStatusBar(); SetStatusText(wxT("Das Superprogramm!")); NeinButton = new wxButton; NeinButton->Create(this, NEIN_ID, wxT("Nein danke")); JaButton = new wxButton(this, 1, wxT("Ja!")); }
Listing 7.5 Konstruktor der Fensterklasse

Mengestaltung In den ersten Schritten wird ein minimales Men aufgebaut. Das wre nicht unbedingt erforderlich, verleiht aber dem Programm einfach etwas Vollendetes. Das Men wird schrittweise erstellt. Dabei unterscheidet wxWidgets zwischen einer Menleiste (wxMenuBar), die in jedem Programm quer unterhalb des Verschiebebalkens liegt und einem Men, das aufklappt, wenn der Anwender beispielsweise Datei anklickt und die Menpunkte (wxMenuItem) enthlt. Zunchst wird das Men menuFile angelegt und mit der Funktion Append die Punkte nacheinander eingehngt. Da alle notwendigen Informationen bereits auf diesem Wege an das Men gelangt sind, muss keine spezielle Variable vom Typ wxMenuItem gebildet werden. Das kann aber erforderlich sein, wenn Sie Menpunkte mit Haken versehen oder sie whrend des Programms deaktivieren wollen. Jeder Menpunkt hat zwei Bestandteile: die ID, die das System an das Programm sendet, wenn der Menpunkt aufgerufen wurde, und der Text, der in dem Men erscheint. Die Stringkonstante wird ber wxT an die Funktion bergeben. Das liegt daran, dass wxWidgets, wie viele andere Klassenbibliotheken auch, eine eigene Stringklasse hat. Diese unterschiedlichen Stringklassen entstanden, als C++ noch keinen eigenen String anbieten konnte. Sobald das Men fertig ist, wird es seinerseits per Append in die Menleiste eingehngt. Mit dem Aufruf von SetMenuBar wird diese dann im Programmfenster eingehngt. Statusleiste Was der Fuchsschwanz dem Mantafahrer, ist die Statusleiste dem Programmierer. Das Fenster verbraucht etwas mehr Platz, die Statusleiste enthlt selten wirklichen Informationswert, lsst das Programm wichtig erscheinen und ist damit unverzichtbar. Immerhin ist die Einrichtung einfach. Mit CreateStatusBar ist sie bereits angelegt, und mit dem Aufruf von SetStatusText knnen wichtige Informationen abgelegt werden.

144

Ereignisorientiert

7.3

Eigene Kontrollelemente Hier im Konstruktor der Rahmenklasse ist auch der Ort, wo eigene Elemente angelegt werden. Fr unser Programm werden zwei Buttons erzeugt. Die Struktur fr NeinButton wird durch new angelegt. In einem zweiten Schritt wird der Button durch die Funktion Create dem System bekannt gemacht und angezeigt. Dabei wird mit this das Elternfenster bergeben. In diesem Fall bezeichnet this das Rahmenfenster. Es folgt die ID, die hnlich wie beim Menpunkt an die Applikation gesendet wird, um den Button zu identizieren. Schlielich gibt der Text an, was auf dem Button stehen soll. Fr JaButton werden die beiden Schritte in einem parametrisierbaren Konstruktor zusammengefasst. Damit sind alle Elemente an Bord und erzeugt. Es mssen nun noch die Funktionen zur Ereignisbehandlung geschrieben werden.

7.3.3

Ereignisbehandlung

Durch die Ereignistabelle ist festgelegt, welche Funktion auf das jeweilige Ereignis reagiert. Eine solche Funktion wird als Callback bezeichnet, weil sie vom System zurckgerufen wird, wenn ein Ereignis eintritt. Da die Funktion vom System aufgerufen werden knnen muss, sind ihre Parameter von diesem vorgegeben. Der Rckgabewert ist void. Der Parameter ist eine auf das Ereignis spezialisierte Ableitung von wxEvent und enthlt weitere Informationen, die zum jeweiligen Ereignis gehren. Mens und Buttons Die einfachste Ereignisfunktion ist OnQuit. Sie fhrt mit dem Aufruf von Close zum Schlieen des Fensters und damit zum Ende des Programms.
void HideButtonFrame::OnQuit(wxCommandEvent&) { Close(true); }
Listing 7.6 Ende der Vorstellung

In unserem Superprogramm wird die Funktion einmal nach dem Menpunkt Beenden und als Reaktion auf Drcken des Nein-Buttons aufgerufen. Analog ist die Funktion OnAbout eine Reaktion auf den Menpunkt ber. Diese Funktion ruft eine Messagebox auf.

145

Sprunghaft

void HideButtonFrame::OnAbout(wxCommandEvent& ) { wxMessageBox(wxT("Galileo Computing prsentiert..."), wxT("Das Superprogramm"), wxOK | wxICON_INFORMATION, this); }
Listing 7.7 Beginn einer Vorstellung

Eine Messagebox ist ein Spezialfall einer Dialogbox, der einen Text anzeigt und eine festgelegte Antwortsituation besitzt. Die Nachricht der Messagebox wird im ersten Parameter bergeben. Der zweite Parameter bestimmt den Text im Schiebebalken der Dialogbox. Im dritten Parameter wird kodiert, welche Antwortarten die Messagebox erlaubt. In diesem Fall gibt wxOK an, dass nur eine einfache Besttigung mglich ist. Darum ist die Untersuchung des Rckgabewertes in diesem Fall unntig. Bei Ja-Nein-Boxen ist das natrlich anders. Durch das Oder-Zeichen kann auch das Icon bestimmt werden, das in der Messagebox angezeigt wird. Der letzte Parameter enthlt das Elternfenster. Das ist hier das Rahmenfenster und wird durch this bezeichnet. Zeichnen Programme in Fensterumgebungen haben in der Regel eine Eigenverantwortung fr ihren Fensterinhalt. Wird das Fenster ganz oder teilweise verdeckt und anschlieend wieder sichtbar, erhlt das Programm ein Ereignis, das ihm meldet, dass es seine Toilette aufbessern soll. Da die fr dieses Ereignis angemeldete Funktion das Neuzeichnen bereits erledigt, ist es Unsinn, das Zeichnen an einer anderen Stelle noch einmal zu erledigen. Darum geht man in GUIs so vor, dass man die Datenstrukturen verndert, die den Fensterinhalt bestimmen und anschlieend selbst das Ereignis auslst, das zum Neuzeichnen des Fensters fhrt. Unter wxWidgets erledigt dies die Funktion Refresh. In dem Superprogramm dieses Kapitels ist die Funktion OnPaint fr diese Ereignisse angemeldet.
void HideButtonFrame::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); dc.DrawText(wxT("Das Superprogramm starten?"), ButtonX, ButtonY/2); }
Listing 7.8 Zeichnen auf dem Bildschirm

146

Ereignisorientiert

7.3

Die Funktion OnPaint legt als Erstes eine Variable vom Typ wxPaintDC an. DC steht fr device context, also das Gerteumfeld. Damit ist im Falle eines Fensters der Bildschirm und die Grakkarte gemeint. Diese Variable stellt alle Zeichenprimitive vom Punkt ber Kreis und Polygon bis hin zum Zeichnen von Text zur Verfgung. Sie verwaltet auch die Zeichenfarbe, die Dicke der Linie und den Zeichensatz. Im Beispiel oben wird die Frage an den Benutzer geschrieben, ob dieser das Superprogramm starten will. Die beiden Zahlen danach geben die X- und die YKoordinate an, an der der Text erscheinen soll. Dass diese Positionen mit dem Prx Button bezeichnet sind, sollte Sie nicht irritieren. Der Text soll sich an den Positionen des Buttons ausrichten. Links soll der Text bndig abschlieen, und in der Hhe soll er die Hlfte einnehmen. Da wxWidgets den Y-Nullpunkt oben hat, bendet sich der Text auf halber Hhe ber dem Button. Vergrerungen Das Programm empfngt auch die Information darber, dass das Fenster vergrert oder verkleinert wird. Diese Nachricht taucht erstmals auf, wenn das Fenster erzeugt wird. Darum mssen alle Dinge, die von der Fenstergre abhngen, in dieser Ereignisfunktion eingestellt werden. Das betrifft in unserem Falle die beiden Buttons.
void HideButtonFrame::OnSize(wxSizeEvent& event) { #ifndef __unix__ static bool erst = true; if (erst) erst = false; return; #endif JaButton->SetSize(ButtonX,ButtonY,Breite,Hoehe); NeinButton->SetSize(ButtonX+Breite+Abstand, ButtonY, Breite, Hoehe); }
Listing 7.9 Fenstervernderung

Sie sehen dort die nicht besonders schne Unterscheidung, ob es sich bei der Entwicklungsumgebung um ein UNIX handelt oder nicht. Unter Windows fhrt der erste Aufruf der Ereignisfunktion fr die Grenvernderung zu einem Absturz, wenn darin ein Kontrollelement vergrert wird. Offenbar ist Windows beim ersten Aufruf noch nicht so weit. Der Kniff mit der statischen booleschen Variablen fhrt dazu, dass die Folgezeilen erst beim zweiten Mal aufgerufen werden. Dann klappt es auch mit Windows. Bei Linux wird die Funktion OnSize kein zweites Mal aufgerufen, sofern nicht der Anwender die Fenstergre verndert. Also darf die Abfrage nur unter Windows bersetzt werden.

147

Sprunghaft

In Abbildung 7.2 sehen Sie das Superprogrmam nach dem Start. Links der JaButton, rechts Nein danke. Beide benden sich auf einer Hhe, gleich breit und gleich hoch. So sollte ein ordentliches Programm aussehen. Vielleicht knnte der eine oder andere nrgeln, dass die Platzausbeute nicht optimal ist. Rechts und unten ist so viel Platz, dass man fast die Aufschrift Hier knnte Ihre Werbung stehen! erwartet. Es steht Ihnen frei, dies zu ergnzen.

Abbildung 7.2 Screenshot Superprogramm

Mausreaktionen Die letzte Ereignisfunktion ist die interessanteste. Darum steht sie natrlich am Schluss. Diese Funktion ermittelt die aktuelle Mausposition auf dem Fenster. Sie prft, ob die Maus in die Nhe des Ja-Buttons kommt. Sobald das passiert, wird die Position des Buttons von links nach rechts neben den Nein-Buttons verschoben.
void HideButtonFrame::OnMouseEvent(wxMouseEvent& event) { int ButX = ButtonX; wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc)); int xpos = pt.x; int ypos = pt.y; if (abs(ButX-xpos) < Breite+Abstand) { ButX +=2*Breite+2*Abstand; } pt.x = ButX; pt.y = ButtonY; JaButton->SetSize(ButX, ButtonY, Breite,Hoehe); }
Listing 7.10 Mausbespitzelung

148

Was denn noch?

7.4

In Abbildung 7.3 ist die Maus in die Nhe des Ja-Buttons gekommen. Und siehe da, der Button springt nach rechts! Wie gut, dass der Bereich doch nicht als Werbeche vermietet wurde.

Abbildung 7.3 Superprogramm verndert die Position des Buttons.

Das Ergebnis ist, dass der Benutzer den Ja-Button niemals treffen kann. Sobald er mit der Maus in die Nhe kommt, ist der Button pltzlich auf der jeweils anderen Seite. So kann der Benutzer nur den Nein-Button anklicken, der seinerseits das Programm beendet.

7.4

Was denn noch?

Wie ich Ihnen versprochen habe, ist die Grundlage zu einem Superprogramm gelegt. Sie brauchen nur noch da weiterzuprogrammieren wo der Ja-Button getroffen wird und die Applikation Ihres Lebens dahinterlegen. Ach ja: Es wre natrlich nett, wenn Sie das Wegspringen des Buttons dann abschalten, sonst wird nie jemand in den Genuss Ihrer Anwendung kommen. Sollten Sie die Idee mit dem chtenden Button eigentlich ganz niedlich nden, wre es natrlich spektakulr, wenn der Button vor der Maus wegluft. Dazu mssten Sie unterscheiden, aus welcher Richtung die Maus kommt und den Button nur stckchenweise weiterschieben. Sobald der Button in eine Ecke geschoben wurde, wagen Sie einen Sprung auf die andere Seite des Mauszeigers. Sie knnten sich ab Seite 180 auch anschauen, wie unter wxWidget ein Timer programmiert wird. Mit einem Timer kann man wundervollen Schabernack treiben. Zhlen Sie doch Zehntelsekunden. Wenn 300 erreicht sind, lassen Sie den JaButton alle Zehntelsekunde um 1 Pixel nach oben steigen. Sie ziehen also jeweils 1 von der Y-Koordinate ab, bevor Sie SetSize aufrufen. Bei 0 sollten Sie allerdings stoppen. Sobald sich der Mauszeiger innerhalb des Fensters bewegt, setzen Sie

149

Sprunghaft

den Button wieder an die alte Y-Position und setzen auch den Zehntelsekundenzhler wieder auf 0, damit das Spiel wieder von vorn beginnen kann. Und wenn Sie schon einmal einen Timer in Betrieb haben, der konntrolliert, wie lange sich im Fester nichts bewegt, knnten Sie den Text des Buttons verndern. Statt Ja knnte nach einiger Zeit Los doch! oder Ich warte stehen. Dazu bentigen Sie die Funktion SetLabel. Die Funktion erwartet lediglich einen wxString als Parameter. Der Befehl lautet also beispielsweise so:
JaButton->SetLabel(wxT("Ich warte!"));

150

Mit Farben kann man seine inneren Energien verbessen. Oder: Wie die alten Enterprise-Folgen zur Gesundheit beitragen.

Esoterische Software

Mein vier Monate alter Sohn hatte pltzlich eine Kette aus Bernsteinsplittern um dem Hals. Spontan fragte ich mich, ob es klug sei, einem Kind diesen Alters eine Kette um den Hals zu legen, mit der es sich vielleicht erdrosseln knnte. Zumindest machte ich mir Gedanken, dass die Kette am Hals scheuern knnte. Ich wurde darber aufgeklrt, dass diese Kette ein Geschenk wre, das dazu beitragen solle, meinem Sohn das Zahnen zu erleichtern. Das wre eine jahrhundertealte Erkenntnis, und der moderne Mensch wrde viel zu leichtfertig das Wissen des Mittelalters verwerfen. Ich dachte intensiv darber nach und beschloss mit meinem Zahnarzt zu sprechen. Ich fragte ihn, warum er uns das nicht vorgeschlagen htte. Schlielich wre er doch zustndig fr die Zahngesundheit unserer Familie. Immerhin wre doch vollkommen einsichtig, dass das Tragen von Baumharz, das durch die Jahrhunderte mit Energie vollgeladen worden sei, den Zahnaufbau verbessern wrde. Woher sollten die Zhne denn sonst die Energie hernehmen? Er schaute mich etwas befremdet an, sagte aber nichts. Ich fuhr fort, dass diese Energie schlielich nachweisbar sei. Man brauche nur einen Bernstein lange genug zu reiben, dann wrde ein Mensch der ihn anfasst so mit Energie aufgeladen, dass ihm die Haare abstnden. Ich hatte das unbestimmte Gefhl, dass sich genau dieser Effekt bei meinem Zahnarzt einstellte, und das ganz ohne Bernstein. Dann schaute er mich noch ein Weilchen an und meinte, dass er in diesem wichtigen Thema leider gar nicht ausgebildet sei. Er habe aber einen Kollegen, der sich intensiv mit solchen Dingen beschftigen wrde. Wenn es mich interessiere, wrde er mir gern die Adresse geben. Ich wrde mir doch sicher nichts daraus machen, dass die Krankenkasse seine Behandlungen nicht bezahle. Schlielich sei das Wohl der Familie unbezahlbar. Ich sollte mich auch nicht wundern, dass der Kollege keinen Doktortitel habe. Das tote Wissen, das man an der Universitt lehre, wrde sowieso berschtzt. Und immerhin sei der Kollege bereits vor 20 Jahren dazu bergegangen, das Amalgam durch ein Gemisch aus Lehm, Baumharz und Lurchknochen zu ersetzen. Er habe herausgefunden, dass die Indianer

151

Esoterische Software

die Mischung verwendet haben und wie wir doch aus den einschlgigen Indianerlmen wissen, brauche ein Indianer nie zum Zahnarzt. Schlielich kennen Indianer keinen Schmerz. Und wer wolle nicht schmerzfrei leben. Ich ging dann doch nicht zu jenem Kollegen. Ich war mir nicht ganz sicher, ob mein Zahnarzt diese Lobrede nicht ein klein wenig ironisch gemeint haben knnte.

8.1

Strahlungen und Energien

Es wird Zeit, dass die Informatik endlich den Trend der Zeit erkennt und esoterische Zusammenhnge aufnimmt und in Software giet. Und da dieses Buch auch zu den Trendsettern gehren will, stellen wir ein Programm vor, das die Energien des Benutzers messen und in Farben umsetzen wird. Einen Monitor ausreichender Gre vorausgesetzt, werden die Farbwellen seinen Krper durchuten und so die krpereigenen Wellen untersttzen.

8.1.1

Messen der Good Vibrations

Wie aber erhlt der Computer Informationen ber die Wellen und die Strahlen des Anwenders? Eine Idee wre es, einen Bluetooth-Adapter anzuschlieen und die Signalschwankungen zu messen. Wie Sie sicher wissen, liegen die positiven Angranominalwellen des Krpers genau im Wellenbereich der BluetoothFrequenz. Wenn der Benutzer also intensiv seine monovertikalen Astralwindungen bei Vollmond mit den subvertiblen Metasensoren seiner Hyplenta koppelt, kann das hypophile Mentalwellenverhalten relativ genau gemessen werden. Das Ergebnis dieser Messung ist lediglich etwas schwer davon zu unterscheiden, wenn jemand ein Bluetooth-Handy in der Hosentasche hat und mit den Beinen wippt. Die Idee mit dem Bluetooth-Adpater habe ich dann aber doch lieber aufgegegeben, nachdem ich gelesen habe, dass die meisten Esoteriker Bluetooth schon deshalb ablehnen, weil die Strahlen, wie der Name schon sagt, blau sind. Gelb, Orange, Violett und Rosa wren ja noch gegangen, aber Blau ist einfach nicht gut. Bluetooth scheidet also aus. Ich hatte noch ber WLAN-Messung nachgedacht, aber wie Sie sicher wissen, berschneidet sich dessen Frequenz mit den Strahlungen der unteren Gespartie bei Magen-Darm-Grippe, und wer will schon per Computer seinen Fahrtwind an dieser Stelle messen.

152

Strahlungen und Energien

8.1

8.1.2

Sonnenstrahlen sind nicht blau

Viele Seiten im Internet befassen sich mit dem Problem des modernen Menschen, dass er in den modernen Bros von der Sonne abgeriegelt wird. Dieses Problem ist tatschlich ein solches und wird in Skandinavien sehr ernst genommen. Dort werden mit Erfolg spezielle Lampen eingesetzt, die das Sonnendezit im Winter ausgleichen sollen. Im Internet nden sich selbstlose Menschen, die fr nur 24,80 Euro pro 5 ml ein l anbieten, das garantiert 20 Stunden mit Sonne aufgetankt worden ist. Das sei viel praktischer. Man kann jederzeit einige Tropfen zu sich nehmen, und das Sonnendezit ist fr den ganzen Tag ausgeglichen. Es fllt mir allerdings etwas schwer, an den Idealismus dieser Anbieter zu glauben. Ich bin auch nicht berzeugt, dass dieses l die versprochene Wirkung erzielt. Welcher Winkelzug der Evolution hat es denn ermglicht, dass der Mensch die Kraft der Sonne durch den Magen-Darm-Trakt aufnehmen kann? Und selbst wenn es bei einigen Menschen funktioniert, warum kaufen diese Leute nicht einfach einen Apfel? Der war wochenlang der Sonne ausgesetzt und ist in der Regel deutlich billiger. Offenbar gibt es hier ein Bedrfnis nach Hilfe, bei dem vielleicht auch ein Programmierer ntzlich sein kann. Vielleicht geht es auch gar nicht um selbstlose Hilfe, sondern nur um einen Riesenmarkt, der jede Menge Geld fr lausige Ware verspricht. Aber auch daran will ich mich gern beteiligen. Es ist wohl auch dem Naivsten nicht beizupulen, dass ein Computer sein Sonnendezit heilen kann. Immerhin knnte man doch sicher ein Programm schreiben, um das Sonnendezit zu analysieren. Der sonnendurchutete Mensch msste doch durch den Computer von einem solchen mit Sonnenmangel zu unterscheiden sein. Als ich ber dieses Problem nachdachte, el mir ein lesenswerter Zeitschriftenartikel in der Aprilausgabe des Jahres 1987 oder 1988 ein.1 In diesem Artikel wurde beschrieben, wie man durch Auslesen der Controllerbausteine einer Grakkarte ein Blatt Papier vor dem Monitor einscannen kann, wenn die Schreibtischlampe, die man hinter das Blatt stellt, nur stark genug wre. Der Grundgedanke msste doch ausbaubar sein, um eine Sonnenlichtmessung des Anwenders durchzufhren.

1 Leider wei ich nicht mehr, in welcher Zeitschrift das stand. Wenn ein Leser diesen Artikel noch hat, wrde mich eine Kopie sehr interessieren.

153

Esoterische Software

Ich verwarf den Gedanken, als mir klar wurde, dass es die damals gebruchlichen Rhrenmonitore heutzutage kaum noch gibt. Ich kann mir nicht vorstellen, dass das mit einem TFT-Display immer noch funktioniert.

8.1.3

Schwingungen mechanisch messen

Etwas entmutigt schaute ich mir meinen Computer noch einmal auf dessen Sensorik hin genauer an. Und da el es mir wie Schuppen aus den Haaren: die Maus! Natrlich ist dieses etwas grobschlchtige Instrument nicht in der Lage, aus einer Ruheposition heraus die feinen Schwingungen und Energien des Menschen aufzunehmen. Wenn man die Maus allerdings in eine kreisende Bewegung versetzt, dann kann man aus den Ablenkungsschwankungen die zarten Wellen messen, die der Krper nach auen trgt.

8.2

Programmieren

Das hier vorgestellte Programm wird also die Schwingungen des Patienten analysieren und grasch darstellen. Dazu fhrt er mit der Maus kreisende Bewegungen durch. Dabei fhren die ethmoseitlichen Urbistralgorbitale zu Abweichungen von der glatten Bewegung. Diese Abweichungen sind bei jedem Menschen individuell. Die Werte werden akkumuliert und in Position, Gre und Farbe von Ellipsen ausgedrckt, die das Programm auf dem Fenster darstellt. Der medial begabte Berater kann aus dem entstandenen Bild nicht nur den Charakter, sondern auch die momentane Stimmung und den nanziellen Wohlstand vor und nach der Behandlung analysieren. Dabei hilft ihm zustzlich das Analysemodul, das basierend auf dem Verfahren von Professor von Allerhandzumut die Epoparallelpestiden als Linien auf dem Bildschirm darstellt. Wenn Sie an diesem Programm weiterentwickeln, dann sollten Sie die Nachtstunden bevorzugen. Das ist bei vielen Programmierern sowieso die Hauptschaffensperiode. Am besten sind Vollmondnchte, auch Neumond ist akzeptabel. Ideal wre ein Programmieren ohne Wechselstromeinsse. Das knnen Sie erreichen, indem Sie mit einem Notebook im Akkubetrieb arbeiten. Da es um Grak geht und wir keine Plattform benachteiligen wollen, verwenden wir wxWidgets. Das ist keine Zauberei. Allerdings hat die Buchstabenkombination wx etwas Magisches. Zumindest wsste ich nicht, wozu man sie sonst gebrauchen knnte.

154

Programmieren

8.2

8.2.1

Fensterklassen

Fr eine wxWidgets-Applikation werden minimal zwei Klassen bentigt. Die Applikationsklasse wxApp sorgt fr eine problemlose Anmeldung an der graschen Benutzeroberche und erffnet das Hauptfenster. Wer sich mit Win32-Programmierung befasst, wei, dass man mit den Details leicht eine DIN-A4-Seite fllen kann. Bei der Verwendung von wxWidgets reichen ein paar Zeilen, in denen Sie eine eigene Applikationsklasse von wxApp ableiten. In der Elementfunktion OnInit wird dann das Rahmenfenster gestartet. Dieses wird von wxFrame abgeleitet und steuert das Benutzerinterface.
#include <wx/wx.h> #include <vector> using namespace std; class esoterikapp : public wxApp { public: virtual bool OnInit(); }; class esoterikFrame : public wxFrame { public: esoterikFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnQuit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnMessung(wxCommandEvent& event); void OnAnalyse(wxCommandEvent& event); void OnMouseEvent(wxMouseEvent& event); void OnSize(wxSizeEvent& event); void OnPaint(wxPaintEvent& event); void OnTimer(wxTimerEvent& event); private: wxTimer timer; wxSize FensterGroesse; wxMenuItem* pMenuItemAnalyse; bool Analyse; bool Messung; DECLARE_EVENT_TABLE() }; enum { Menu_File_Quit = 100, Menu_File_About };
Listing 8.1 Fensterklassen

155

Esoterische Software

Als Ereignisse werden die Grenvernderung, ein Timer und die Aufforderung zum Neuzeichnen bedient. Darber hinaus gibt es noch vier Menpunkte. In den privaten Variablen nden Sie neben einem Timer und der Festergre ein Menelement. Dieses wird explizit als Variable gespeichert, um es whrend des Programmlaufs zu aktivieren und wieder zu deaktivieren. Die beiden booleschen Variablen steuern den Programmmodus.

8.2.2

Ereignisse

Die denierten Konstanten dienen in erster Linie dazu, Meneintrge und den Timer mit eindeutigen Nummern zu versehen. Die Konstante TIMERTICK stellt die Wiederholfrequenz des Timers auf 2000 Millisekunden, also zwei Sekunden. Das ist ausreichend, um gengend Daten zu sammeln und wirkt nicht hektisch. Auf der anderen Seite hat der Anwender immer noch das Gefhl, dass etwas passiert.
enum QUIT_ID = 100, ABOUT_ID, MESSUNG_ID, ANALYSE_ID ; const int TIMER_ID = 1; const int TIMERTICK = 2000; BEGIN_EVENT_TABLE( esoterikFrame, wxFrame ) EVT_MENU(MESSUNG_ID, esoterikFrame::OnMessung) EVT_MENU(ANALYSE_ID, esoterikFrame::OnAnalyse) EVT_MENU(ABOUT_ID, esoterikFrame::OnAbout) EVT_MENU(QUIT_ID, esoterikFrame::OnQuit) EVT_MOUSE_EVENTS(esoterikFrame::OnMouseEvent) EVT_SIZE(esoterikFrame::OnSize) EVT_PAINT(esoterikFrame::OnPaint) EVT_TIMER(TIMER_ID, esoterikFrame::OnTimer) END_EVENT_TABLE()
Listing 8.2 Ereignistabelle

Die ersten vier Eintrge in der Ereignistabelle weisen die Mennummern den entsprechenden Funktionen zu. Auch die anderen fr das Programm relevanten GUI-Ereignisse werden mit den behandelnden Funktionen verschaltet. Die Funktion OnInit startet das Rahmenfenster und bringt es nach vorn. Den Hintergrund setzen wir in helles, nicht zu krftiges Gelb. Das erinnert an Sonne, ohne gleich nach einer Werbung fr Sonnenschutzlotion auszusehen. Die Ellipsen sollen darauf ja harmonisch wirken.

156

Programmieren

8.2

IMPLEMENT_APP(esoterikapp) bool esoterikapp::OnInit() { esoterikFrame *frame=new esoterikFrame( wxT("Farbharmonieanalyse"), wxDefaultPosition, wxDefaultSize); frame->SetBackgroundColour(wxColour(255, 255, 160)); frame->Show(TRUE); SetTopWindow(frame); return TRUE; }
Listing 8.3 Applikationsobjekt

Der Konstruktor des Rahmenfensters baut ein Men auf, prpariert die Statusleiste, startet den Timer und setzt den Programmstatus. Es gibt zwei Mens, Datei und Hilfe. Im ersten benden sich neben dem Punkt zum Verlassen des Programms auch die Punkte Messung und Analyse. Da die Analyse erst auf der Basis von Messergebnissen stattnden kann, soll dessen Meneintrag dann aktiviert werden, wenn eine analysefhige Messung vorliegt. Aus diesem Grund wird der Analyse-Menpunkt etwas anders behandelt. Es wird eine eigene Variable vom Typ wxMenuItem angelegt. Als Elternfenster wird die Datei-Menleiste angegeben und natrlich die ID und die Beschriftung. Der Zeiger wird dann per Append eingehngt. ber den Zeiger pMenuItemAnalyse kann nun der Eintrag durch Aufruf der Funktion Enable jederzeit aktiviert oder deaktiviert werden.
esoterikFrame::esoterikFrame( const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) , timer(this, TIMER_ID) { wxMenu *menuHelp = new wxMenu; menuHelp->Append(ABOUT_ID, wxT("&ber...")); wxMenu *menuFile = new wxMenu; menuFile->Append(MESSUNG_ID, wxT("&Messung starten")); pMenuItemAnalyse = new wxMenuItem(menuFile, ANALYSE_ID, wxT("&Analyse")); menuFile->Append(pMenuItemAnalyse); menuFile->AppendSeparator(); menuFile->Append(QUIT_ID, wxT("&Beenden")); wxMenuBar *menuBar = new wxMenuBar;

157

Esoterische Software

menuBar->Append(menuFile, wxT("&Datei")); menuBar->Append(menuHelp, wxT("&Hilfe")); SetMenuBar(menuBar); pMenuItemAnalyse->Enable(false); CreateStatusBar(); SetStatusText(wxT("Dt. Inst. f. Farbthermik")); timer.Start(TIMERTICK); Analyse = false; Messung = false; } void esoterikFrame::OnQuit(wxCommandEvent&) { Close(true); } void esoterikFrame::OnAbout(wxCommandEvent&) { wxMessageBox(wxT("Deutsches Institut f. Farbthermik"), wxT("Farbharmonieanalyse"), wxOK | wxICON_INFORMATION, this); }
Listing 8.4 Konstruktor Fensterrahmen

Die Funktion OnQuit sorgt fr ein geregeltes Ableben des Programms, whrend OnAbout etwas ber das Programm erzhlt. Die Angabe irgendeines Instituts macht sich immer gut. Ein Institut fr Farbthermik gibt es jedenfalls bisher noch nicht, knnte sich aber vielleicht mit solchen Phnomenen befassen.

8.2.3

Ellipsen

Die Ellipsen mssen gespeichert werden. Schlielich muss das Programm ja den gleichen Zustand wiederherstellen knnen, wenn das Fenster kurzzeitig berdeckt war. Also wird ein Typ tEllipse geschaffen, indem eine Ellipse gespeichert werden kann. Um alle Ellipsen aufzunehmen, wird ein STL-Vektor angelegt.
class tEllipse { public: tEllipse() { x=0; y=0; radius=0; radius2=0; farbe.Set(0,0,0); } int x; int y;

158

Programmieren

8.2

int radius; int radius2; wxColour farbe; }; vector<tEllipse> Ellipsen; tEllipse NeuEllipse;
Listing 8.5 Die Klasse tEllipse

In NeuEllipse wird die jeweils neue Ellipse aufgearbeitet, bevor sie durch das Timer-Ereignis als abgeschlossen gespeichert wird. Mausereignisse Die Funktion zur Behandlung des Mausereignisses ist die zentrale Funktion fr die Ermittlung der Schwingungen. Allerding nur, wenn das Programm im Modus zur Erfassung der Messung ist. Also wird die entsprechende Variable zunchst geprft. Dann wird festgestellt, ob die Maus das Fenster verlassen oder betreten hat. Bendet sich die Maus nmlich nicht ber dem Fenster, empfngt das Programm auch keine Daten. Also wird der Timer angehalten. Bei Neueintritt wird der Timer wieder gestartet.
void esoterikFrame::OnMouseEvent(wxMouseEvent& event) { if (!Messung) return; if (event.Leaving()) timer.Stop(); if (event.Entering()) timer.Start(TIMERTICK); wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc)); NeuEllipse.x += pt.x; NeuEllipse.y += pt.y; NeuEllipse.radius += pt.x + pt.y; NeuEllipse.radius2 += pt.x<pt.y?pt.y-pt.x:pt.x-pt.y; int red = NeuEllipse.farbe.Red(); red += pt.x - pt.y; int green = NeuEllipse.farbe.Green(); green += - pt.x + pt.y; int blue = NeuEllipse.farbe.Blue(); blue += pt.x + pt.y; NeuEllipse.farbe.Set(red, green, blue); }
Listing 8.6 Mausbeobachtung

159

Esoterische Software

Aus der Parametervariablen ermittelt das Programm die derzeitige Position der Maus. Die Koordinaten werden in den Koordinaten fr die nchste Ellipse aufsummiert. Die Summe beider Koordinaten fhrt zu dem ersten Radius, die Summe der Differenz beider Koordinaten summiert sich auf den zweiten Radius auf. Die Koordinaten werden unterschiedlich miteinander verknpft und so auf den bisherigen Farbenwert aufsummiert. Zeichenfunktion Endlich wird gezeichnet! Wie bei einer Zeichenfunktion blich, wird zunchst der Device Context ermittelt. Dieser enthlt alle Informationen ber den Zeichenuntergrund, wie die Auflsung, die Anzahl der Farben und einige Informationen mehr. Die Zeichenfunktionen sind Elementfunktionen des Device Context. Dazu gehrt auch die Funktion SetPen, die dafr sorgt, dass der Zeichenstift transparent ist. Ansonsten wrde um jede Ellipse herum eine schwarze Linie gezeichnet. Die Funktion SetBrush setzt die Brste oder besser den Pinsel auf die Farbe, die das Programm errechnet hat. Diese Farbe bestimmt den Flcheninhalt. Anschlieend wird jede Zeichenoperation mit transparentem Stift und mit gefrbtem Pinsel arbeiten, bis beide wieder verndert werden. In einer Schleife ber die vorhandenen Ellipsen wird nun gezeichnet. Sollte die Analyse aktiv sein, werden noch Linien zwischen den Ausgangspunkten der Ellipsen gezeichnet. Dazu muss der Pen wieder solide und schwarz sein.
void esoterikFrame::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); dc.SetPen(*wxTRANSPARENT_PEN); for (unsigned int i=0; i<Ellipsen.size(); ++i) { dc.SetBrush(Ellipsen[i].farbe); dc.DrawEllipse(Ellipsen[i].x, Ellipsen[i].y, Ellipsen[i].radius, Ellipsen[i].radius2); } if (Analyse) { dc.SetPen(wxPen(*wxBLACK, 1, wxSOLID)); for (unsigned int i=1; i<Ellipsen.size(); ++i) { dc.DrawLine(Ellipsen[i-1].x, Ellipsen[i-1].y, Ellipsen[i].x, Ellipsen[i].y); } } }
Listing 8.7 Malerei

160

Programmieren

8.2

Das Ergebnis dieser Zeichnung ist in Abbildung 8.1 zu sehen.

Abbildung 8.1 Schwingungen in Ellipsenform mit Analyse

Sie knnen die Ellipsen erkennen. Sie erscheinen whrend der Mausbewegung immer wieder. Die Linien werden durch den Menpunkt Analyse erzeugt und verbinden die Ellipsen in der Reihenfolge ihrer Entstehung. Das die Linien links oberhalb der Ellipsen stehen, hat damit zu tun, dass dies deren Ursprungskoordinate ist. Sie knnen allerdings jeweils die Radien auf die Koordinaten aufaddieren und damit die Linien zu den Mittelpunkten fhren. Das wirkt dann allerdings vielleicht zu ordentlich, und das Bild erhlt dadurch vielleicht zu sehr ein Kinderspielzeugdesign. Aber experimentieren Sie doch selbst! Der Timer bernimmt die Ellipse Der Timer bereitet zunchst die akkumulierten Werte auf, damit die Astralwellen den Bereich des Fensters nicht verlassen und die Radien den Bildschirm nicht vllig berragen. Da hilft ein wenig Modulo-Rechnung ganz enorm.
void esoterikFrame::OnTimer(wxTimerEvent& event) { if (!Messung) return; NeuEllipse.radius %= 60;

161

Esoterische Software

NeuEllipse.radius2 %= 60; NeuEllipse.x %= FensterGroesse.GetWidth() - 2*NeuEllipse.radius; NeuEllipse.y %= FensterGroesse.GetHeight() - 2*NeuEllipse.radius2; int red = NeuEllipse.farbe.Red(); red %= 64 + 192; int green = NeuEllipse.farbe.Green(); green %= 64 + 192; int blue = NeuEllipse.farbe.Blue(); blue %= 64 + 192; NeuEllipse.farbe.Set(red, green, blue); Ellipsen.push_back(NeuEllipse); if (Ellipsen.size()>2) { pMenuItemAnalyse->Enable(true); } Refresh(); // Erzwinge Aufruf von OnPaint }
Listing 8.8 Timer

Bevor die Farben gespeichert werden, werden die drei Farbwerte etwas erhht. So gibt es wundervolle Pastellfarben. Es wre ja zu bld, wenn jemand seine Schwingungen messen lsst und nur schlammbraune Matschecken auftauchen wrden. Es soll ja dem Benutzer auch ein wenig warm ums Herz werden. Denn wenn das Herz richtig warm ist, kann man das Gehirn ruhig kalt lassen. Die so vorbereitete Ellipse wird nun an den Ellipsenvektor angehngt, und die Funktion Refresh lst ein Neuzeichnen aus. Vorher wird allerdings noch einmal gezhlt, wie viele Ellipsen denn nun schon gespeichert sind. Sind es mehr als zwei, kann der Menpunkt fr die Analyse wieder freigegeben werden.

8.2.4

Menspielereien

ber den Menpunkt Datei Messung starten wird die boolesche Variable Messung eingeschaltet und die boolesche Variable Analyse ausgeschaltet. Zuvor wird die Meldung in der Statusleiste auf Messung umgeschaltet. Alle bisherigen Ellipsen werden gelscht. Zu guter Letzt wird mit dem Aufruf von Refresh das Neuzeichnen des Bildschirms erzwungen.

162

Was denn noch?

8.3

void esoterikFrame::OnMessung(wxCommandEvent& ) { SetStatusText(wxT("Messung luft!")); Ellipsen.clear(); Analyse = false; Messung = true; Refresh(); }
Listing 8.9 Messung

Der Menpunkt Datei Analyse kann nur aufgerufen werden, wenn bereits gengend Messungen fr die Analyse vorliegen. Die Aktivierung sorgt fr das Abschalten der Variablen Messung. Dadurch wird die Funktion fr die Mausereignisse keine Ereignisse mehr sammeln und die Timerfunktion keine neuen Ellipsen mehr erzeugen. Das Einschalten der booleschen Variable Analyse reicht bereits vllig aus, damit die Funktion OnPaint die Analyselinien in die Ellipsen zeichnet. Nun kann der Menpunkt Analyse wieder abgeschaltet werden. Der Aufruf von Refresh veranlasst das Neuzeichnen des Bildschirms.
void esoterikFrame::OnAnalyse(wxCommandEvent& ) { SetStatusText(wxT("Analyse erfolgt")); Analyse = true; Messung = false; pMenuItemAnalyse->Enable(false); Refresh(); }
Listing 8.10 Analyse

8.3

Was denn noch?

Nun, das Programm lsst sich beliebig erweitern und das Schne an der Esoterik ist ja, dass Sie sich bei der Weiterentwicklung durch keinerlei naturwissenschaftliche Erkenntnisse behindert fhlen mssen. Sie knnen beispielsweise eine Messung verwerfen, wenn nicht gengend Messdaten vorliegen. Sehr nett wre es auch, wenn Sie mit einem zweiten Timer prfen, ob sich die Maus auch schn regelmig bewegt. Sie knnten beispielsweise durch Farben signalisieren, ob der Anwender die Maus richtig bewegt. Machen Sie es nicht zu leicht. Es soll ja nicht so aussehen, als knne jeder Depp ordentliche Schwingungen produzieren.

163

Esoterische Software

Eine hbsche Idee wre es, wenn die Analyse unverstndliche Stze zusammenbastelt, die den Anwender beim Verstndnis der Analyse vollstndig verwirrt. So lassen sich Stze wie Die zentrale Abralvirkulose erhht die Neigung zum timbronalen Senestralevolit. leicht aus Textbausteinen zusammensetzen. Wenn Sie jeden Satz in Subjekt, Prdikat und Objekt aufteilen, knnen Sie diese immer wieder neu kombinieren und erhalten eine erhebliche Erweiterung Ihrer Weisheiten. Als Basis fr die Auswahl werden Sie selbstverstndlich keinen profanen Zufallsgenerator zu Hilfe nehmen. Verwenden Sie doch wiederum die Daten aus den Mausschwingungen. Das Ergebnis soll ja persnlich zu dem Probanden passen. Es ist aber wichtig, dass Sie viele Bausteine haben und dass keiner von diesen leicht verstndlich ist. Ansonsten iegen Sie mit dem Programm sofort auf. Um Ihre Seriositt zu unterstreichen, sollten Sie die Begriffe auch stotterfrei aufsagen knnen. Also ben Sie ein wenig die Aussprache. brigens mssen schwer verstndliche Vokabeln nicht zwingend auch schwer auszusprechen sein. Sie sollten allerdings auch Ihren Gesichtsausdruck kontrollieren. Ein breites Grinsen, ja selbst ein dnnes Lcheln ist immer verkehrt. Sie sollten immer bedeutsam wirken. ben Sie auch den Ausdruck von Besorgnis, Erstaunen und Grbeln. Es wird Ihnen ntzen. Die Praxis ruft Der nchste Schritt hat dann nichts mit Programmierung zu tun. Besorgen Sie sich ein gelbes oder rosa Notebook. Sollte das nicht greifbar sein, versehen Sie ein beliebiges Notebook mit Engelaufklebern. Engel sind absolut in. Sie knnten auch Tarot-Karten aufkleben. Aber es wre eine bse Falle, wenn Sie eine Karte erwischen, die Unheil oder gar Lge verkndet. Es ist keine gute Idee, eine Beratung in Anspruch zu nehmen. Die Tarot-Kartenleger verstehen unter einer Beratung den Vorgang, das Opfer nanziell zu rupfen. So gerstet, suchen Sie nach dem nchsten Esoterik-Stammtisch in Ihrer Umgebung. So etwas gibt es wirklich! Dort knnen Sie nach Herzenslust mit diesem Programm Beratungen durchfhren. Achten Sie dabei auf Ihren Wortschatz. Sie nden im Internet viele, viele Seiten mit ganz tollen Anregungen. Wollen Sie das Besondere, schauen Sie sich ein paar ltere Folgen von Raumschiff Enterprise an. Die technischen Begriffe, die dort verwendet werden, sind in der Regel nicht von dieser Welt. Sie eignen sich also in idealer Weise fr die Konversation mit den Glubigen. Mit diesen Begriffen werden Sie bestimmt der Star auf jedem Esoterikertreffen! Bevor Sie aber irgendetwas verkaufen, sollten Sie daran denken, dass jedes Gericht Ihnen vorwerfen kann, dass das, was Sie anbieten, keinen Nutzen hat. Das gilt im Grunde fr fast alle esoterischen Waren und Dienstleistungen. Aber Vorsicht! Sobald man Ihnen nachweisen kann, dass Sie selbst nicht an die Wirkung

164

Was denn noch?

8.3

glauben, kann man Sie wegen Betrugs anzeigen. Glauben Sie dagegen den Unsinn, den Sie verbreiten, kann man Ihnen nichts anhaben. Also seien Sie berzeugt! Aber bevor Sie sich auf diese Zeilen berufen, mchte ich Sie darauf hinweisen, dass ich Informatiker und kein Jurist bin. Und da Sie sicher auch nicht auf den Gedanken kommen, bei Ihrem Scheidungsanwalt ein paar Programme in Auftrag zu geben, sollten Sie sich auch lieber nicht auf den juristischen Rat eines Programmierers vom Dorf verlassen.

165

Insbesondere bei Spielen geht ohne 3D gar nichts mehr. Dabei ist alles nur eine optische Tuschung. Denn die Bildschirme werden immer acher.

Die dritte Dimension

Wir nehmen die Welt um uns herum dreidimensional wahr. Darum ist es auch ganz natrlich, dass wir Programme, die uns einen dreidimensionalen Eindruck gewhren, sehr viel mehr schtzen als solche, die uns eine Vogelperspektive anbieten. Schlielich knnen wir nicht iegen. Na ja, seit Lilienthal ist das besser geworden.

9.1

Fluglandung

Das Klatschen nach der Landung eines Flugzeugs ist eine Unart, die anzeigt, wie verbreitet das Phnomen der Flugangst ist. Das wrde jedenfalls erklren, warum niemand den Busfahrer beklatscht, wenn er vor dem Wartehuschen an der Haltestelle stehen bleibt.

9.1.1

Faszination Fliegen

Es wird erzhlt, dass die Firma IBM fr seinen ersten PC eigentlich CP/M ausliefern wollte. CP/M war seinerzeit ein weitverbreitetes Betriebssystem der Firma Digital Research Inc. (DRI). Doch statt mit den Abgesandten der Firma IBM zu sprechen, war der Chef Gary Kildall mit seinem Privatugzeug unterwegs. So kam es nicht zur Vertragsunterzeichnung. IBM wandte sich an Bill Gates, und der verkaufte ihnen ein Betriebssystem, das er selbst Tim Patterson abgekauft hatte. Gary Kildall wurde nur 52 Jahre alt und starb 1994. Insofern war seine Entscheidung, den Tag zu genieen statt reich zu werden, gar nicht so unklug.1 Ob diese Geschichte wirklich so abgelaufen ist, wird schwer zu beweisen sein. Aber offensichtlich gibt es Menschen, die es nachvollziehbar nden, dass Gary Kildall lieber iegen wollte und darber das grte Geschft aller Zeiten verpasste. Die Faszination des Fliegens ist also offenbar erheblich. Diese Faszination und der Preis eines Pilotenscheins erklrt die Begeisterung der 1980er Jahre fr

1 Die Geschichte nden Sie etwas ausfhrlicher unter: http://www.zop.ch/0e/CPM.html.

167

Die dritte Dimension

Flugsimulationen. Bereits auf dem Apple II gab es die ersten Versionen. Und so kann man sich vorstellen, dass sogar eigene Firmen gegrndet wurden, nur um den optimalen Flugsimulator zu erschaffen.

9.1.2

Flugsimulator-Computer contra Jackintosh

Flugsimulationen galten in der Mitte der 1980er Jahre als die Krnung der Computerspiele. So gab es sogar eine Firma, die einen Computer baute, dessen wichtigste Aufgabe es war, eine Flugsimulation so real wie mglich erscheinen zu lassen. Diese Firma mit Namen Amiga war von Jay Miner gegrndet worden. Jay Miner hatte zuvor bei Atari gearbeitet und hatte dort unter anderem an den zentralen Bausteinen fr den Atari 400 und den Atari 800 gearbeitet. Diese Firma Amiga sollte in den Showdown zwischen Jack Tramiel, Commodore und Atari geraten.

Abbildung 9.1 Amiga (Foto: Von Kaiiv nach Creative Commons Lizenz freigegeben)2

Jack Tramiel Jack Tramiel hatte die Firma Commodore Business Machines International (CBM) gegrndet und mit dem legendren PET 2001, dem VC-20 und dem C-64 Computerlegenden produziert. So hatte er die Firma Commodore gro gemacht und gleichzeitig der Konkurrenz heftige Probleme bereitet. Insbesondere die Firma
2 http://de.wikipedia.org/wiki/Amiga

168

Fluglandung

9.1

Atari geriet in Schwierigkeiten. Im Jahr 1984 kam es zum Bruch, weil Tramiel gegen den Willen des Vorstands seine Shne in die Geschftsleitung bringen wollte. Er musste Commodore verlassen. Wie so oft im Leben wurde auch hier aus Liebe Hass. Tramiel kaufte die Firma Atari, die zu diesem Zeitpunkt in einem desolaten Zustand war, und brachte sie in kurzer Zeit wieder nach oben. Zunchst hatte er die 8-Bit-Modelle wieder konkurrenzfhig gemacht. Doch Tramiel wollte in den 16-Bit-Markt. Es war ihm klar, dass in diesem Segment die Zukunft der nchsten Jahre liegen wrde. Die bernahme von Amiga Dies war der Zeitpunkt, als die Firma Amiga verzweifelt nach Geld suchte. Es sah nicht gut aus. Jack Tramiel lieh der maroden Firma eine Million Dollar fr einen Monat. Da Amiga das Geld nicht zurckzahlen konnte, hoffte er, die Firma fr wenig Geld bernehmen zu knnen. Doch kurz bevor die Zahlungsfrist ablief, kam seine alte Firma Commodore fr den Kredit auf und bot Amiga einen etwas grozgigeren Preis pro Aktie und schnappte Tramiel so die Firma Amiga direkt vor der Nase weg. Der Jackintosh Nun wurde Tramiel hektisch. In aller Eile wurde ein Computer auf Basis des 68000er-Prozessors von Motorola gebaut. Das Betriebssystem stammte zu groen Teilen von der Firma Digital Research. Sie erinnern sich? Das war die Firma, die ihr CP/M nicht bei IBM unterbringen konnte. Inzwischen hatten sie eine grasche Oberche namens GEM entwickelt, die der Oberche des Macintosh sehr hnlich sah. Mit einem exzellenten Schwarz-Wei-Monitor stellte Atari das Modell ST in direkte Konkurrenz zum Macintosh, verlangte aber nur ein Drittel des Preises. Darum wurde der Atari ST rmenintern gern als Jackintosh bezeichnet. Die Hardware der ST-Serie wurde von Shiraz Shivji entwickelt, der von Commodore abgeworben worden war. Dort hatte er seinerzeit entscheidend an der Entwicklung des C-64 mitgearbeitet. Nun hatte es auch Commodore eilig, ebenfalls noch in den 16-Bit-Bereich zu kommen. Der Umbau des Spielcomputers zum endgltigen Amiga dauerte, und es ging das Gercht um, dass das Produkt, das Commodore dann endlich auf den Markt warf, in der Herstellung teurer war als im Verkauf. Dabei war der Amiga immer noch deutlich teurer als der Atari ST. Erst ein Jahr spter konnte Commodore mit dem Amiga 500 ein Gert ausliefern, das Gewinn brachte und preiswerter war.3

3 Nachzulesen unter: http://www.commodore.ca/history/company/chronology_portcommodore.htm

169

Die dritte Dimension

9.1.3

Apple-Fluglandung

Obwohl die Flugsimulationen inzwischen eine Perfektion erreicht haben, dass sie sogar in der Ausbildung von Piloten eine zentrale Rolle einnehmen, habe ich nur wenig Zeit damit verbracht. Wirklich fasziniert hat mich aber ein ganz simples Spiel auf dem Apple II. Bei dieser Simulation ging es nur um die Landung und auch bei dieser nur in Bezug auf Hhenruder und Geschwindigkeitsregler. Wer den Apple II kennt, erinnert sich, dass dem Gert zwei Drehregler beilagen, die Paddles genannt wurden. Mit dem einen wurden das Hhenruder und mit dem anderen die Geschwindigkeit geregelt. Auf diese beiden Gren war der Einuss auf den Spielverlauf beschrnkt. Auf dem Bildschirm wurden die Werte fr die Hhe, die Entfernung und die Geschwindigkeit dargestellt. Darber war ein kleines, immer grer werdendes Trapez zu sehen, das die Umrisse der Flugbahn anzeigt.

Abbildung 9.2 Screenshot Fluglandung

9.1.4

Ein Blick ins Listing

Das Programm auf dem Apple II wurde in BASIC geschrieben. Erst spt gab es einen C-Compiler, von C++ war zu den Glanzzeiten des Apple II berhaupt nicht die Rede. Immerhin hat BASIC den Vorteil, dass man dem Autor auf die Finger schauen kann. Beim Blick ins Listing el mir sofort das Zitat von Dijkstra ein: Es ist praktisch unmglich, einem Studenten gutes Programmieren beizubringen, wenn er vorher in BASIC programmiert hat. Als potenzielle Programmierer sind

170

Fluglandung

9.1

sie geistig verstmmelt ohne Hoffnung auf Erholung.4 Der aufmerksame Leser wird sofort erkennen, dass ich offenbar BASIC kenne und ich gestehe, dass ich darin auch programmiert habe. Nun hege ich die Hoffnung, dass es bei mir mit der geistigen Verstmmelung nicht so schlimm ist oder dass Dijkstra in diesem einen Punkt vielleicht doch unrecht hat. Tatschlich wurde der Code auf den damaligen Maschinen der 1-MHz-Klasse auf mglichst schnelle Ausfhrung geschrieben. Das bedeutete, dass alles vermieden wurde, was den Interpreter belastete. Es wurden keine Kommentare eingefgt und die Variablennamen mglichst kurz gehalten, also nicht mehr als ein oder zwei Buchstaben lang. Da das BASIC dieser Zeit darber hinaus hinter jedem IF nur einen GOTO-Befehl zulie, ist das Listing voller Sprnge. Das Ausmisten und Geradeziehen des Codes brachte dann eine berraschung: Die Simulation entbehrte jeglicher naturwissenschaftlicher Grundlage. Weder die Steuerung des Flugzeugs noch die 3D-Ansicht hatten irgendetwas mit Physik oder Geometrie zu tun. Einer der Grnde ist zweifellos darin zu nden, dass der Autor auf Zeitgewinn programmierte. Jede komplexe Berechnung htte die Simulation zum Erliegen gebracht. Immerhin hat er aber die Simulation so glaubwrdig umgesetzt, dass ich diesen Mangel nie bemerkt habe. Bei der Neuprogrammierung konnte ich das BASIC-Programm vollstndig vergessen und etwas Neues schreiben.

9.1.5

Ein paar strende Fakten

Vor der Programmierung habe ich ein paar Informationen aus dem Internet, von diversen Fernsehlmen und dem profunden Wissen von Leuten, die sich wirklich auskennen oder zumindest jemanden kennen, der jemanden kennt, zusammengetragen. Diesen Quell der Weisheit wollte ich Ihnen keinesfalls vorenthalten. Das Hhenruder Das Hhenruder beeinusst natrlich die Hhe eines Flugzeugs. Das ist einleuchtend. Wozu soll ein Hohenruder auch sonst gut sein? Aber es beeinusst auch die Geschwindigkeit. Ein Flugzeug wird nie so schnell wie auf dem direkten Weg senkrecht nach unten. Aber auch wenn es nicht abstrzt, geht es schneller nach unten als nach oben. Das wissen Sie vermutlich aus eigener Erfahrung beim Treppensteigen. Auf der anderen Seite wird ein Flugzeug im Steigug einen Groteil seiner Energie in die berwindung der Erdanziehungskraft stecken. Sptestens jetzt sind Sie bestimmt berzeugt, dass das Hhenruder auch einen gewissen Einuss auf die Geschwindigkeit hat.
4 Edsger Wybe Dijkstra: How do we tell truths that might hurt? 18. Juni 1975. Das Original: It is practically impossible to teach good programming to students that have had a prior exposure to BASIC: As potential programmers they are mentally mutilated beyond hope of regeneration.

171

Die dritte Dimension

Throttle Der Throttle fhrt den Turbinen Energie zu. Gelangt mehr Energie in die Triebwerke, darf man erwarten, dass das Flugzeug schneller wird. Allerdings ist das nicht der einzige Effekt. Ein schneller iegendes Flugzeug wird auch mehr Auftrieb bekommen und damit steigen. Und im Gegenzug wird ein langsameres Flugzeug auch etwas sinken. Wenn das Flugzeug allerdings zu langsam iegt, wird es abschmieren und damit ganz besonders schnell sinken. Gesammeltes unntzes Wissen Ein paar Daten kann man im Internet nden. Insbesondere Wikipedia erzhlt viel ber Landeange, Sinkge, Aufsetzgeschwindigkeiten, und wo der ideale Aufsetzpunkt ist. Da werden wohl ein paar Piloten aus dem Nhkstchen geplaudert haben. Ein paar Daten seien hier erwhnt: Die Aufsetzgeschwindigkeit sollte je nach Flugzeug zwischen 50 km/h und 300 km/h liegen. Die berziehungsgeschwindigkeit multipliziert mit 1,3 entspricht der Anuggeschwindigkeit, die als VREF bezeichnet wird. Die Landeanuggeschwindigkeit wiederum sollte nicht mehr als 15 kts ber VREF liegen. 180 kts (Knoten) entsprechen 3 nm/Min. (nautische Meilen), was wiederum ein Flugzeug um 900 Fu je Minute vorantreibt. ber dem Aufsetzpunkt einer Landebahn sollte das landende Flugzeug eine Hhe von 50 Fu nicht berschreiten. Ansonsten kann auch die lngste Landebahn zu kurz sein. Das Flugzeug sollte nicht mit mehr als 1000 Fu pro Minute sinken. Die Konsequenzen sind mir noch nicht ganz klar. Aber all diese Erkenntnisse fhren nicht zu einer guten Simulation des Landeanugs. Immerhin erhhen sie die Achtung gegenber Pilotenscheinbesitzern. Ergebnis ist, dass der Landeanug mit ein paar sehr simplen Mechanismen gebaut wird.

9.1.6

Simulation des Fluges

Das Ergebnis einer ausgiebigen Internetrecherche und eines ausgiebigen Nachdenkens ergibt, dass es nicht ganz einfach ist, die physikalischen Verhltnisse einer Fluglandung nachzubilden. Es ist fr jedermann leicht nachzuvollziehen, dass ein Segelugzeug anders als ein Jumbojet landet. Zumindest wrde mich das Gegenteil sehr wundern. Die exakten Formeln kann eigentlich nur ein Fachmann liefern, und nur ein Pilot knnte feststellen, ob sich diese realittsnah anfhlen. Die meisten Programmierer sind allerdings keine Piloten, aber die meisten Spieler glcklicherweise auch nicht.

172

Fluglandung

9.1

Wenn es also nicht mglich ist, die reale Physik heranzuziehen, wie es bei der Mondlandung und dem Kirschkernspucken mglich war, so mssen wir versuchen, eine Simulation zu erzeugen, die dem Spieler wenigstens glaubwrdig erscheint. Es gilt also zusammenzustellen, welchen Einssen eine Landung unterliegt und welche Wirkung die Steuerung des Spielers auf den Landeanug hat. Die Klasse Game enthlt die Daten und Funktionen, die fr die Simulation relevant sind. Die zentrale Funktion heit steuereFlugzeug. Sie berechnet bei jedem Aufruf die Geschwindigkeit, den Anstieg als dynamische Werte und die Positionswerte Hhe und Distanz. Als Steuerungselemente verwendet sie das Hhenruder und den Throttle, also quasi das Gaspedal des Flugzeugs.
const double MinGeschwindigkeit=60; void Game::steuereFlugzeug() { Geschwindigkeit = 0.6*Geschwindigkeit + 0.3*Throttle - 0.1*Anstieg*Anstieg; Anstieg = 0.8*Anstieg + 0.2*(0.1*Hoehenruder - Geschwindigkeit/1000 - 0.1*Throttle); Hoehe=Hoehe+Anstieg; Distanz=Distanz-Geschwindigkeit; // Abschmieren bei zu geringer Geschwindigkeit if (Geschwindigkeit<MinGeschwindigkeit) { Anstieg=-20; // Verhindert, dass das Flugzeug rckwrts fliegt if (Geschwindigkeit<10) { Geschwindigkeit = 10; } } }
Listing 9.1 Funktion zur Steuerung des Flugzeugs

Die neue Geschwindigkeit bestimmt sich zu 60 % aus der bisherigen Geschwindigkeit. Nur zu 30 % spielt der Throttle, also der Geschwindigkeitsregler, eine Rolle. Immerhin lsst sich mit einem Flugzeug keine Vollbremsung hinlegen, und auch die Beschleunigungswerte von 0 auf 100 drften in der Luft eher langweilig sein. Der Throttle ist im Original der Paddle-Wert. Beim Apple II ging er von 0 bis 256. Ich habe keinen Grund gesehen, dies zu ndern. Zu guter Letzt spielt zu 10 % das Quadrat des Anstiegs hinein. Das bedeutet, dass leichtes Steigen und Fallen nahezu keinen Einuss auf die Geschwindigkeit hat. Ein Sturzug wird die

173

Die dritte Dimension

Geschwindigkeit erhhen und ein erhebliches Steigen, die Geschwindigkeit verringern. Diese Unterteilung von 60 zu 30 zu 10 basiert nicht auf irgendwelchen Sachzwngen, sondern wurde einfach ausprobiert. Das Flugzeug sollte auf den Throttle noch reagieren, aber nicht bertrieben hektisch. Wenn Ihnen also das Flugverhalten nicht gefllt, drfen Sie sich frei fhlen, alle Werte zu verndern. Der Anstieg wird hnlich berechnet wie die Geschwindigkeit. Zu 80 % bleibt der bisherige Anstieg erhalten. Der restliche Anteil von 20 % ergibt sich aus einer Mischung aus Hhenruder, bisheriger Geschwindigkeit und dem Throttle. Auch diese Werte sind ausprobiert. Sie sollten ein wenig indirekt sein, aber auch noch das Gefhl von Kontrolle vermitteln. Die Geschwindigkeit des Flugzeugs spielt natrlich auch in seine Hhennderung hinein. Ob die Werte physikalisch zu vertreten sind, wage ich zu bezweifeln. Die Hhe und die Distanz werden einfach durch Aufaddieren der Steigung und der Geschwindigkeit berechnet. Sptestens hier wird deutlich, dass Sie besser nicht danach fragen sollten, ob die metrischen oder die angelschsischen Einheiten verwendet werden. Im letzten Schritt habe ich den Sonderfall behandelt, dass die Geschwindigkeit zu gering wird. Erstens darf das Flugzeug nicht rckwrts iegen oder stehen bleiben. Also hat es von mir die Mindestgeschwindigkeit von 10 bekommen. Auerdem wei ich aus den relevanten Fernsehserien, dass ein zu langsames Flugzeug abschmiert. Es geht also unter 60 den Abstieg 20. Der Wert kann immer noch abgefangen werden, aber erzeugt ein paar dramatische Sekunden. Die wenigen Piloten, die auf diese Simulation treffen, werden zu einem gewissen Teil darber schmunzeln und schweigen. Die anderen werden vielleicht sagen, dass diese Simulation mit der Realitt nichts zu tun hat. Aber da dies vermutlich als angeberisches Gehabe abgetan wird, kann der Programmierer durchaus ein wenig mauscheln. Der Landeanug sollte nicht zu lange dauern. Schlielich soll das Spiel schnell beendet werden. Das Landen darf nicht zu schwierig sein. Ist die hundertste virtuelle Maschine am Boden zerschellt, wird auch der hartnckigste Spieler aufgeben und sich freuen, wenn er endlich wieder mit der Tabellenkalkulation arbeiten darf. Landet sich die Maschine fast von selbst, macht es keinen Spa.

9.1.7

Die Ansicht der Landebahn

In der Fliegerei ist eine Landung nach Instrumenten durchaus mglich, aber richtig schick wird es, wenn man sieht, wie die Landebahn immer nher kommt und der perspektivische Eindruck entsteht, dass man gleich aufsetzt.

174

Fluglandung

9.1

Eine Landebahn ist ein langgezogenes Rechteck. In der Perspektive eines landenden Flugzeugs ist es allerdings ein gleichschenkliges Trapez. Um das Trapez zu bestimmen, muss der Abstand zwischen der hinteren und der vorderen Kante bereichnet werden. In horizontaler Richtung wird die Lnge beider Kanten bentigt. Fr die Berechnung der Perspektive bietet sich der Strahlensatz an. Das Auge bendet sich hinter einer Mattscheibe. Gesucht werden die Punkte auf der Mattscheibe, durch die der Strahl von den Kanten ins Auge trifft. Die Abbildung 9.3 zeigt dies anschaulich von der Seite.

Abbildung 9.3 Strahlensatz

Der Strahlensatz besagt fr die untere Kante: p h = e e1 Dabei ist h die Hhe des Flugzeugs, e die Entfernung zum Anfang der Landebahn und p der Projektionspunkt auf der Mattscheibe. Die 1 steht fr einen Meter Abstand zwischen Auge und Projektionswand. Aufgelst fr den Projektionspunkt ergibt sich: p= h (e 1 ) e

Damit ist der erste Punkt gefunden. Es wird aber der Abstand gesucht. Der ergibt sich durch die vernderte Entfernung um die Lnge der Landebahn, die wir mit b bezeichnen. h (e + b 1 ) h (e 1 ) ydist = e+b e Auf hnliche Weise wird die Lnge der Kanten berechnet. Wenn Sie diese Formeln anwenden, werden Sie feststellen, was die genaue Betrachtung der Formeln schon verrt: Eine ganze Zeit lang sieht der Spieler nichts auer einem kleinen Punkt. Dieser wird erst im letzten Augenblick des Landeanugs grer. Das entstandene Trapez wird dann aber bald nicht mehr brauchbar, weil die Projektion grer als der Bildschirm wird.

175

Die dritte Dimension

Das ist fr ein Spiel wenig befriedigend. Also gilt es, eine glaubwrdige Darstellung zu erschummeln. Der hier gewhlte Ansatz verfolgt die Idee, das Flugfeld durch eine lineare Vergrerung nherzubringen. Dazu wird einfach die aktuelle Entfernung zur Landebahn durch die Ausgangsentfernung geteilt. Dieser Prozentsatz ist bei voller Entfernung 1 und wird kleiner, je nher die Landebahn ist. Natrlich muss es genau umgekehrt sein. Also zieht man diesen Wert von 1 ab. Zu guter Letzt wird der Wert mit der maximalen Auflsung multipliziert, und fertig ist die Berechnung. Allerdings ist auch die lineare Nherung nicht ganz befriedigend. Man ist es gewohnt, dass Gegenstnde, in der Nhe strker wachsen als in der Ferne. Dies lsst sich manipulieren, indem der Wert quadriert wird, bevor er mit der maximalen Auflsung multipliziert wird. Da ein Zahlenwert unter 1 nach dem Quadrieren auch unter 1 bleibt, ist dieses Vorgehen vllig unkritisch. Das funktioniert wunderbar mit der Lnge der vorderen und hinteren Kante. Allerdings fehlt bisher die Hhe, die vor allem in y-Richtung einen erheblichen Einuss auf das Trapez hat. Also wird fr die y-Distanz der prozentuale Abstand mit einem Quotienten aus Hhe und Abstand multipliziert. Und schon verndert sich das Trapez in gewnschter Richtung, wenn das Flugzeug steigt oder sinkt. Danach ist das Grerwerden der Landebahn schon deutlich glaubwrdiger. Allerdings gibt es im letzten Augenblick das Problem, dass das Trapez zum Rechteck umschlgt. Dies lsst sich am einfachsten verhindern, indem der Bezugspunkt hinter die Landebahn gesetzt wird.
void getProjektion(long Distanz, double StartDistanz, long RollbahnLaenge, long Hoehe, long &BildY, long &BildXVorn, long &BildXHinten) { double ProzAbstand = (Distanz+RollbahnLaenge) /(StartDistanz+RollbahnLaenge); // Prozentualer Abstand mal Aufloesung des Bildschirms BildY = (1-ProzAbstand)*(double(Hoehe)/Distanz) * MaxY; BildXVorn = (1-ProzAbstand)*(1-ProzAbstand) * MaxX; // Abfang des Ueberlaufs if (BildY>=MaxY) { BildY=MaxY-1; } if (BildXVorn>=MaxX) BildXVorn=MaxX-1; BildXHinten = double(Hoehe) / (Distanz+RollbahnLaenge) * BildXVorn;

176

Fluglandung

9.1

if (BildXHinten>BildXVorn) { BildXHinten = BildXVorn; } }


Listing 9.2 getProjektion

9.1.8

Die grasche Oberche

Das Rahmenprogramm fr die grasche Oberche mit der Klassenbibliothek wxWidgets ist bersichtlich. Fr die Steuerung der Simulation werden zwei Slider verwendet. Sie ersetzen die Paddles, die bei heutigen PCs leider nicht mehr mitgeliefert werden. Ein Timer wird aktiviert, der jede Viertelsekunde eine neue Zeichnung veranlasst. Bei jedem Neuaufbau werden auch die Slider ausgelesen, so dass der Flieger nahezu in Echtzeit gesteuert wird. Die Fensterklasse enthlt die Ereignisempfnger fr den Timer und das PaintEreignis. Als grasche Elemente nden sich hier die Slider fr Throttle und Hhenruder.
#ifndef _FLUGLANDUNG_H_ #define _FLUGLANDUNG_H_ class fluglandungapp : public wxApp { public: virtual bool OnInit(); }; class fluglandungFrame : public wxFrame { public: fluglandungFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnPaint(wxPaintEvent& event); void OnTimer(wxTimerEvent& event); private: wxPanel *panel; wxTimer timer; wxSlider *Throttle; wxSlider *Ruder; }; #endif // _FLUGLANDUNG_H_
Listing 9.3 Die Header-Datei fr den wxWidget-Rahmen

177

Die dritte Dimension

Die Implementierung ist nicht weiter berraschend. In der Funktion OnInit wird der Rahmen fr das Programmfenster gebaut. Dann wird es mit der Funktion Show zur Ansicht gebracht und als Fenster nach vorn gebracht.
#include <wx/wx.h> #include "fluglandung.h" #include "game.h" IMPLEMENT_APP(fluglandungapp); const const const const int TIMERTICK=300; wxWindowID THROTTLE_ID=12; wxWindowID RUDER_ID=13; int TIMER_ID=1;

bool fluglandungapp::OnInit() { fluglandungFrame *frame = new fluglandungFrame( wxT("Fluglandung"), wxPoint(50,50), wxSize(660,540)); frame->Show(TRUE); SetTopWindow(frame); return TRUE; }
Listing 9.4 Der wxWidget-Rahmen fr die Fluglandung

Im Konstruktor der eigenen Frame-Klasse wird zunchst ein Panel angelegt. Ein Panel dient der Zeichnung von Elementen und kann bestimmte Ereignisse empfangen. Dann werden die beiden Slider initialisiert. Sie sollen vertikal stehen und als Schiebeweg die Werte von 0 bis 255 liefern, genau wie die Paddles auf dem Apple II. Dann wird das Timerereignis mit der Funktion OnTimer verbunden und die Zeichenfunktion mit der Funktion OnPaint. Zu guter Letzt wird der Timer gestartet.
fluglandungFrame::fluglandungFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) , timer(this, TIMER_ID) { // ohne ein Panel gibt es keine Tastaturereignisse panel = new wxPanel(this, 5, wxPoint(pos), wxSize(size)); Throttle = new wxSlider(panel, THROTTLE_ID, 128, 0, 255, wxPoint(5,10), wxSize(20,200),

178

Fluglandung

9.1

wxSL_VERTICAL|wxSL_AUTOTICKS); Ruder = new wxSlider(panel, RUDER_ID, 128, 0, 255, wxPoint(630,10), wxSize(20,200), wxSL_VERTICAL|wxSL_AUTOTICKS); Connect(wxEVT_TIMER, wxTimerEventHandler(fluglandungFrame::OnTimer)); panel->Connect(wxEVT_PAINT, wxPaintEventHandler(fluglandungFrame::OnPaint), NULL,this); timer.Start(TIMERTICK); }
Listing 9.5 Der Konstruktor der eigenen Frame-Klasse

Zeichnen des Fensters Programme einer GUI zeichnen immer indirekt. Es gibt eine Funktion, die vom System aufgerufen wird, wenn das Fenster neu gezeichnet werden muss. Diese Funktion muss jederzeit den Zustand des Fensters wiederherstellen knnen. Darum wird diese Funktion auch dazu benutzt, wenn eine Zeichnung gendert werden muss. Die Wiederherstellung wird eingeleitet, und dann lst das Programm selbst das Restaurierungsereignis des Systems auf. Zuerst wird der Device Context ermittelt. Er bendet sich in der Variablen dc. Ein solcher Kontext kennt die Auflsung und andere Informationen der Grakumgebung. In dieser Umgebung werden dann die eigentlichen Zeichenfunktionen ausgefhrt. Der Vorteil dieses Konzeptes ist, dass die gleichen Funktionen sowohl fr den Bildschirm als auch fr den Drucker aufgerufen werden knnen. Der erste Teil der Funktion zeichnet das Trapez der Flugbahn mithilfe der Funktion DrawPolygon. In der zweiten Hlfte der Funktion werden die Zahlenwerte fr Hhe, Entfernung, Geschwinidgkeit und Steigung angezeigt. Dies erfolgt ber die Funktion DrawText.
#include <string> using namespace std; void fluglandungFrame::OnPaint(wxPaintEvent& evt) { wxPaintDC dc(panel); Game *game = Game::getInstance(); BahnKoord bahn = game->getBahnKoord(); wxPoint wxBahn[4]; for(int i=0; i<4; i++) { wxBahn[i]=wxPoint(bahn.Bahn[i].x+30, bahn.Bahn[i].y); }

179

Die dritte Dimension

dc.DrawPolygon(WXSIZEOF(wxBahn), wxBahn, 0,0); wxString str; dc.DrawText(wxT("Hhe"), HoeheX, InstrY1); str << game->getHoehe(); dc.DrawText(str, HoeheX, InstrY2); dc.DrawText(wxT("Entf"), EntfX, InstrY1); str = wxT(""); str << game->getDistanz(); dc.DrawText(str, EntfX, InstrY2); dc.DrawText(wxT("Geschw."), GeschwX, InstrY1); str = wxT(""); str << game->getGeschwindigkeit(); dc.DrawText(str, GeschwX, InstrY2); dc.DrawText(wxT("Steig."), SteigX, InstrY1); str = wxT(""); str << long(game->getAnstieg()*100); dc.DrawText(str, SteigX, InstrY2); }
Listing 9.6 Zeichnen mit der OnPaint-Funktion

Wem die Millisekunde schlgt Einen Timer gibt es unter allen GUIs. Er ist vor allem bei bewegten Spielen sehr wichtig. So wird erreicht, dass das Programm immer wieder geweckt wird, um die nchste Szene zu zeigen. Ein weiterer Vorteil ist, dass die Spiele auf allen Computern gleich schnell ablaufen. Auf den 8-Bit-Computern wurden oft Zhlschleifen in die Spiele eingebaut, um das Spiel so weit herunterzubremsen, dass es spielbar war. Wenn man dann ber ein schnelleres Gert verfgte, waren die alten Spiele teilweise gar nicht mehr spielbar. Die Zhlschleifen hatten einen weiteren Nachteil. Die CPU wird fortlaufend mit Zhlen beschftigt. Man bezeichnet diese Technik auch mit busy waiting. Eine Abart des busy waitings ist das Polling. Damit bezeichnet man ein Verhalten, dass das Programm in einer Endlosschleife wartet, bis ein Ereignis eintritt, beispielsweise eine Datei erscheint oder die Zustandsnderung eines Controllers. Bei einem einfachen Single-Tasking-Computer wie dem C-64 ist das nicht relevant. Bei den heutigen Multitasking-Systemen ist dieses fr alle anderen Prozesse einschlfernd, weil die anderen Prozesse die CPU nicht nutzen knnen. Bei den heutigen hochgetackteten Systemen kommt hinzu, dass die CPU unter Last hei wird und dabei natrlich viel Energie verbraucht. Es ist also immer besser, Ereignisse zu fangen, als darauf zu warten. Wenn es im Einzelfall unumgnglich ist, darauf zu warten, hilft es sehr, einen Sleep-Befehl einzufgen. Dieser sorgt dafr, dass der Prozess einen Augenblick in die Warteschlange gesteckt wird und die anderen Prozesse Luft bekommen.

180

Fluglandung

9.1

Um einen Timer unter wxWidgets zu aktivieren, mssen drei Dinge passieren:

Der Timer muss initialisiert werden. Dies passiert sinnigerweise bei der Initialisierung der Frame-Klasse. Der Timer erhlt dabei typischerweise ber this den Zeiger auf das Frame-Objekt und eine ID, ber die er spter angesprochen werden kann. Der Timer wird mit der Funktion verbunden, die aufgerufen werden soll, wenn das Timer-Ereignis eintrifft. Diese Verbindung wird ber den Aufruf der Funktion Connect hergestellt. Der Timer wird ber die Elementfunktion Start in Betrieb genommen. Der Timer kann beliebig oft gestoppt und wieder gestartet werden.

Der Timer steht in der Frameklasse als private Elementvariable. Letztlich kann die Variable natrlich auch an anderen Stellen stehen.
private: ... wxTimer timer;
Listing 9.7 Ein Timer wird deniert

Im folgenden Listing ist zu sehen, wie der Timer innerhalb de Konstruktors der Fensterklasse initialisiert und gestartet wird.
fluglandungFrame::fluglandungFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) , timer(this, TIMER_ID) { ... Connect(wxEVT_TIMER, wxTimerEventHandler(fluglandungFrame::OnTimer)); timer.Start(TIMERTICK); } }
Listing 9.8 Ein Timer wird initialisiert

Wenn ein Timer-Ereignis eintrifft, wird die Funktion OnTimer aufgerufen. Sie liest zunchst den aktuellen Wert der Slider aus. Anschlieend wird die Funktion naechsterTick der Klasse Game aufgerufen. Diese berechnet anhand der SliderWerte die nchste Flugstrecke und bereitet die Werte fr die Zeichenfunktion OnPaint vor. An dem Rckgabewert der Funktion naechsterTick kann die Funktion ablesen, ob das Spiel bereits beendet ist. Sie lst dann die Dialogbox fr

181

Die dritte Dimension

die Endemeldung aus. Zum Schluss lst sie durch den Aufruf von Refresh ein Neuzeichnen des Fensters und damit den Aufruf von OnPaint aus.
void fluglandungFrame::OnTimer(wxTimerEvent& event) { Game *game = Game::getInstance(); game->setThrottle(255-Throttle->GetValue()); game->setRuder(255-Ruder->GetValue()); if (!game->naechsterTick()) { timer.Stop(); wxMessageBox( wxT( "Spielende" ), wxT( "Fluglandung" ), wxOK | wxICON_INFORMATION, this ); game->neuesSpiel(); timer.Start(TIMERTICK); } this->Refresh(); }
Listing 9.9 Ein Timer wird gefangen

Grasche Oberchen Wie an diesem Beispiel zu sehen ist, ist die Programmierung grascher Oberchen nicht so kompliziert, wie ihr Ruf. Natrlich protiert die wxWidgetsBibliothek bereits von den Erfahrungen diverser Versuche, portable GUI-Bibliotheken zu schreiben. Aber selbst eine Portierung auf eine Win32-Umgebung ist nicht unbedingt so kompliziert. In der Regel verwenden Sie sowieso das Rahmenprogramm, das Ihnen die freundliche IDE zur Verfgung stellt oder Sie nehmen eine kleine Beispielapplikation des Systemherstellers und passen sie an. Bei manchen Projekten ist es einfach, die eigentliche Anwendung von der graschen Umgebung zu trennen. Das macht eine Portierung sehr einfach. Aber diese Trennung kann im Einzelfall mehr Aufwand bedeuten, als es bei der Portierung jemals sparen kann.

9.1.9

Besinnung

Wie ich bereits erwhnt habe, fand ich dieses Programm auf einer Sammeldiskette fr meinen Apple II in BASIC. Das Programm hatte mich von Anfang an fasziniert. Und als ich seinerzeit den Apple II gegen den Atari ST eintauschte, hatte ich mir das Listing bereits schon einmal angeschaut, in der Hoffnung, dass ich das Programm vielleicht portieren knnte. Ich hatte die Idee damals sofort aufgegeben, nachdem ich den Spaghetti-Code des Autors gesehen hatte.

182

Fluglandung

9.1

Als ich mich entschloss, das Projekt fr dieses Buch zu verwenden, hatte ich noch in Erinnerung, dass man eine Portierung nur schaffen konnte, wenn man es neu schreibt. Als ich jedoch mit den physikalischen Zusammenhngen nicht weiter kam, hatte ich doch das Listing intensiver durchgearbeitet, um herauszunden, wie es der Kollege gelst hatte. Dabei kam ich zu verschiedenen Ergebnissen.

Es ist ein Riesenglck, dass die Sprache BASIC, wie sie auf den damaligen Computern verwendet wurde, heute keine Rolle mehr bei der Software-Entwicklung spielt. Der Befehl GOTO richtet sogar noch mehr Schaden an, als sich der grte Pessimist in seinen wildesten Trumen je vorstellen kann. Wenn das Dickicht eines Programmes nur dicht genug ist, kann man beliebig viele und beliebig schwere Fehler darin verbergen. Eine Simulation muss keineswegs auf naturwissenschaftlichen Erkenntnissen gegrndet sein, um real zu wirken.

9.1.10

Eine Portierung nach Win32

Die Beispiele in diesem Buch, die eine grasche Oberche bentigen, basieren auf der wxWidget-Bibliothek, da C++ keine eigene standardisierte Oberche bereithlt. Ein weiterer Vorteil von wxWidgets ist, dass es recht kompakt ist und damit die Sicht auf die Funktionalitt des Programms freigibt. Um zu zeigen, dass wxWidgets als exemplarische API geeignet ist, habe ich das Spiel Fluglandung nach Win32 portiert. Dabei el die Wahl aus mehreren Grnden auf Win32. Win32 ist die grundlegende API fr Windows und damit vermutlich einer greren Zahl von Programmierern bekannt. Wie die meisten lteren APIs ist Win32 als C-Schnittstelle implementiert. Entsprechend arbeitet sie weder mit Klassen noch mit Ableitungen. Win32 verwendet statt Callbacks fr die einzelnen Ereignisse eine groen Fensterfunktion, die alle Ereignisse fngt und es dem Programmierer berlsst, die relevanten Ereignisse auszultern. Dennoch ist eine Portierung nicht problematisch, wenn man die Win32 kennt. Und dementsprechend ist eine Portierung auf eine andere Oberche eher einfacher. Vielleicht hilft diese Portierung dem einen oder anderen Programmierer bei der Portierung eines der Beispiele. Bloodshed Dev C++ erstellt das Rahmenprogramm automatisch, wenn Sie als neues Projekt eine Win32-Applikation whlen. Die IDE streut noch ein paar Kommentare hinein, die ich fr den Abdruck im Buch allerdings weitgehend entfernt habe. Erstens waren die Kommentare auf Englisch und zweitens soll

183

Die dritte Dimension

dieser Abschnitt nur den Aspekt der Portierung zeigen und kein Grundkurs in Win32 darstellen. Wenn Sie mehr ber die Grundlagen der Win32-Programmierung wissen wollen, knnen Sie gern auf meiner Website nachschauen: http://www.willemer.de/informatik/windows/winprg.htm Unter der folgenden URL nden Sie eine Beschreibung des Win32-Rahmenprogramms. http://www.willemer.de/informatik/windows/winframe.htm Ich will nicht verheimlichen, dass diese Seiten schon etwas lter sind und Microsoft versucht, die Programmierer bei Laune zu halten, indem sie hin und wieder ein paar kleinere nderungen an der API ernden. Wenn Sie also die neuesten Variationen kennenlernen wollen oder wenn Sie detaillierte Informationen brauchen, sollten Sie sich an das Microsoft Developer Network wenden. Die Startseite nden Sie unter folgender URL: http://msdn.microsoft.com/de-de/default.aspx Window Procedure contra Callback Grundstzlich arbeitet die Windows API wie jede andere grasche Oberche mit Nachrichten. Das System sendet alle Nachrichten, die das Fenster der Applikation betreffen. Diese Nachrichten werden bei einer Win32-Applikation von einer Fensterfunktion (Window Procedure) gefangen und ausgewertet. Typischerweise enthlt diese Fensterfunktion eine groe Fallunterscheidung, die nach der Art der Nachricht verzweigt. Die Nachrichten haben Namen wie WM_PAINT oder WM_TIMER fr das Neuzeichnen des Fensters oder das Empfangen des Timers. Aus den weiteren Parametern der Fensterfunktion entnimmt jedes Ereignis seine speziellen Parameter. Ab diesem Moment unterscheidet sich die Programmierung nicht von der Situation in einer Callback-Funktion. Eine Besonderheit betrifft die Nachricht WM_CREATE. Diese Nachricht wird einmal gesendet, wenn das Fenster erstmals erzeugt wird. Es entspricht damit dem Fensterkonstruktor bei wxWidgets. Whrend unter Windows, OS/2 und GEM (Atari ST) die Fensterfunktionen blich sind, werden Sie bei X11 und Motif (UNIX und Linux) vor allem Callback-Funktionen antreffen. Wie Sie aber oben gesehen haben, ist der Unterschied trotz der unterschiedlichen Syntax nicht unberbrckbar.

184

Fluglandung

9.1

Kontrollelemente Alle Kontrollelemente werden unter Windows als Fenster betrachtet. Darum werden Sie auch mit dem Aufruf CreateWindow erzeugt. Da einige Kontrollelemente in den ersten Windows-Versionen nicht enthalten waren, muss teilweise noch zustzlich die Header-Datei commctrl.h eingebunden werden. Das betrifft insbesondere die beiden Schieberegler. Fr die Common Controls muss auch eine zustzliche Bibliothek eingebunden werden. Diese heit comctl32.lib und wird ber die Projekteigenschaften unter dem Stichwort Linker eingetragen. Applikationsinitialisierung Wie bei wxWidgets gibt es auch bei der Win32 eine Applikationsklasse, die allerdings nicht als Klasse in C++ abgebildet wird, weil die Win32 ja eine C-API ist. Sie nden hier viele technische Details, die sich bei jeder Applikation wiederholen. Aus diesem Grund ist die Verwendung von Klassen auch eine Erleichterung. Das hat Microsoft auch so gesehen und hat darum die Microsoft Foundation Class (MFC) geschaffen. Allerdings ist diese eigentlich schon berholt, weil inzwischen .NET hipp ist.
#include <windows.h> #include <commctrl.h> #include <sstream> using namespace std; #include "positionen.h" #include "game.h" LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM); char szClassName[ ] = "Win32Fluglandung"; int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nFunsterStil) { HWND hwnd; MSG messages; WNDCLASSEX wincl; wincl.hInstance = hThisInstance; wincl.lpszClassName = szClassName; wincl.lpfnWndProc = WindowProcedure; wincl.style = CS_DBLCLKS;

185

Die dritte Dimension

wincl.cbSize = sizeof (WNDCLASSEX); wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION); wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION); wincl.hCursor = LoadCursor (NULL, IDC_ARROW); wincl.lpszMenuName = NULL; /* No menu */ wincl.cbClsExtra = 0; wincl.cbWndExtra = 0; wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND; if (!RegisterClassEx (&wincl)) return 0; // Rahmenfenster erzeugen hwnd = CreateWindowEx(0, szClassName, "WIN32-Fluglandung", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, FensterBreite, FensterHoehe, HWND_DESKTOP, NULL, // Kein Menue hThisInstance, NULL); ShowWindow (hwnd, nFunsterStil); // Nachrichtenschlange aufbauen while (GetMessage (&messages, NULL, 0, 0)) { TranslateMessage(&messages); DispatchMessage(&messages); } return messages.wParam; }
Listing 9.10 Applikationsinitialisierung

Die zentrale Funktion heit WinMain. Sie hat zunchst die Aufgabe die WindowsKlasse zu parametrisieren und dann ber den Aufruf von RegisterClassEx anzumelden. Dann wird das Rahmenfenster durch den Aufruf von CreateWindowEx erzeugt und mit ShowWindow angezeigt. Falls Sie sich ber die Nachsilbe Ex wundern: Das hat nichts mit Bill Gates Jugendliebe zu tun, sondern soll fr Extended stehen. Diverse Funktionen sind beim Umstieg von Windows 3.11 auf Windows 95 erheblich erweitert worden, obwohl ihre Grundfunktionalitt gleich blieb. Es folgt eine Schleife, die nacheinander Nachrichten empfngt und verteilt. Die Nachricht zur Beendigung des Programms gibt eine 0 zurck, sodass die Schleife dann endet und damit auch das Programm. All diese technischen Details knnen bei einer Klassenbibliothek unter der Haube des Konstruktors verschwinden.

186

Fluglandung

9.1

WindowProcedure Wie schon erwhnt ndet die Hauptaktivitt in der Fensterfunktion statt. Sie hat als Parameter das Handle des Fensters, die Nachricht message und zwei Parameter, die Informationen fr die unterschiedlichen Ereignisse enthalten knnen.
const int TIMERTICK=250; const int THROTTLE_ID=1; string double2string(const double wert) { ostringstream ostr; ostr << wert; string str = ostr.str(); return str; } LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; Game *game; CREATESTRUCT *cs; static HWND hwndThrottle; int Throttle; static HWND hwndRuder; int Ruder; string str; switch (message) {
Listing 9.11 Fensterfunktion

Die Hilfsfunktion double2string wird spter fr die Ausgabe der Zahlenwerte bei WM_PAINT bentigt. Auffallend ist die Anreihung von lokalen Variablen am Anfang der Funktion, wie man sie aus alten C-Programmen kennt. Die lokalen Variablen werden hier deniert, weil die meisten Compiler eine Variabendenition innerhalb einer switch-Anweisung nicht erlauben, weil dann deren Initialisierung nicht gesichert sei. Quasikonstruktor WM_CREATE Die Nachricht WM_CREATE wird an das Programm gesendet, wenn das Fenster erzeugt wird. Es entspricht weitgehend dem Konstruktor des Rahmenfensters in der wxWidgets-Bibliothek. Hier wird der Timer gestartet.

187

Die dritte Dimension

Nach dem Aufruf von InitCommonControls knnen nun auch die Schieberegler erzeugt werden.
case WM_CREATE: SetTimer(hwnd, 1, TIMERTICK, 0); cs = (LPCREATESTRUCT)lParam; InitCommonControls(); hwndThrottle = CreateWindow(TRACKBAR_CLASS, "", WS_CHILD|WS_VISIBLE|TBS_AUTOTICKS|TBS_VERT, ThrottleX,10,20,200, hwnd, (HMENU)THROTTLE_ID, cs->hInstance, NULL); SendMessage(hwndThrottle, TBM_SETRANGEMAX, FALSE, 255); hwndRuder = CreateWindow(TRACKBAR_CLASS, "", WS_CHILD|WS_VISIBLE|TBS_AUTOTICKS|TBS_VERT, RuderX,10,20,200, hwnd, (HMENU)THROTTLE_ID, cs->hInstance, NULL); SendMessage(hwndRuder, TBM_SETRANGEMAX, FALSE, 255); break;
Listing 9.12 Fenstererzeugung

Sie sehen, wie der Parameter lParam durch ein Casting einem Zeiger auf eine LPCREATESTRUCT-Struktur gebogen wird. Anschlieend kann ber diesen Zeiger die Applikationsinstanz erlangt werden, die fr die Erzeugung von Kontrollelementen bentigt wird. Neuzeichnen des Fensters Dieser Abschnitt der Fensterfunktion entspricht der Funktion OnPaint. Die hnlichkeiten gehen damit weiter, dass zuerst der Device Context ermittelt wird. Bei Win32 beschafft diesen die Funktion BeginPaint. In der Folge wird das Array mit den aktuellen Rautenkoordinaten aus der Klasse
Game geholt und gezeichnet. Auch die Darstellung der Zahlen auf dem Fenster

hneln dem Vorgehen bei wxWidgets.


case WM_PAINT: { hdc=BeginPaint(hwnd, &ps); game = Game::getInstance(); BahnKoord bahn = game->getBahnKoord(); POINT PolyBahn[5]; for(int i=0; i<4; i++) { PolyBahn[i].x = bahn.Bahn[i].x + 30; PolyBahn[i].y = bahn.Bahn[i].y; }

188

Fluglandung

9.1

PolyBahn[4].x = bahn.Bahn[0].x + 30; PolyBahn[4].y = bahn.Bahn[0].y; Polyline(hdc, PolyBahn, 5); TextOut(hdc, 10, 400, "Hoehe", 4); str = double2string(game->getHoehe()); TextOut(hdc, 10,420, str.c_str(), str.length()); TextOut(hdc, 100, 400, "Entf", 4); str = double2string(game->getDistanz()); TextOut(hdc,100,420, str.c_str(), str.length()); TextOut(hdc, 200, 400, "Geschw", 6); str =double2string(game->getGeschwindigkeit()); TextOut(hdc,200,420, str.c_str(), str.length()); TextOut(hdc, 300, 400, "Steig.", 6); str = double2string(game->getAnstieg()*100); TextOut(hdc,300,420, str.c_str(), str.length()); EndPaint(hwnd, &ps); } break;
Listing 9.13 Neuzeichnen des Fensters

Erst am Ende zeigt sich der Vorteil der Klasse. Unter wxWidgets sorgt der Destruktor fr die Auflsung des Device Context. Bei der C-API muss der Programmierer daran denken, den Device Context mit EndPaint wieder freizugeben. Der Timer tickt Der Timer sorgt fr die Bewegung. Erst wird der aktuelle Stand der Schieberegler ermittelt und an die Spielklasse weitergegeben. Der nchste Tick wird ausgelst und damit die nchste Szene angefordert. Aus dem Rckgabewert ermittelt die Fensterfunktion, ob das Spiel am Ende ist. Eine ausfhrliche Unterscheidung, ob das Spiel gewonnen oder verloren ist, habe ich mir hier erspart.
case WM_TIMER: game = Game::getInstance(); Throttle = SendMessage(hwndThrottle, TBM_GETPOS, 0, 0); game->setThrottle(255-Throttle); Ruder = SendMessage(hwndRuder, TBM_GETPOS, 0, 0); game->setRuder(255-Ruder); if (!game->naechsterTick()) {

189

Die dritte Dimension

KillTimer(hwnd, 1); MessageBox(hwnd, "Spielende", "Fluglandung", MB_OK); game->neuesSpiel(); SetTimer(hwnd, 1, TIMERTICK, 0); } InvalidateRect(hwnd, 0, TRUE); break;
Listing 9.14 Timer

Die Erzwingung des Neuzeichnens wird unter Win32 mit dem Aufruf von InvalidateRect erreicht. Auch hier hnelt sich der Ablauf der Programme. Und Schluss! Die Nachricht WM_DESTROY erreicht das Programm, wenn das System die Aufforderung bekommen hat, das Programm zu beenden, beispielsweise durch das Schliekreuz. Diese Nachricht gibt dem Programm noch die Mglichkeit, seine Angelegenheiten zu ordnen und dann durch den Aufruf von PostQuitMessage dafr zu sorgen, dass die Nachrichtenschleife in WinMain und damit das gesamte Programm beendet wird.
case WM_DESTROY: PostQuitMessage (0); break; default: return DefWindowProc (hwnd, message, wParam, lParam); } return 0; }
Listing 9.15 Ende der Fensterfunktion

Der Fall default wird immer dann aufgerufen, wenn keiner der Flle auf den Inhalt der Nachrichtenvariablen passt. Das sind dann Ereignisse, die das Programm nicht weiter interessiert. Diese Nachrichten werden durch die Funktion DefWindowProc an die Standardbehandlung von Windows weitergeleitet.

9.1.11

Was denn noch?

In diesem Projekt ist natrlich die Simulation eine interessante Spielwiese. Es ist ungeheuer interessant, welchen Einuss die Gewichtung einzelner Parameter auf das Flugverhalten hat. Die hier vorgestellte Lsung ist alles andere als perfekt und ich bin sicher, Sie nden eine sehr viel elegantere Lsung. Vielleicht ndet

190

Nightdriver

9.2

sich auch ein programmierender Pilot oder ein iegender Programmierer, der dem Anug eine wirklichkeitsnhere Simulation verpasst. Es wre sicher auch ganz interessant, einen echten Hhenmesser als Instrument einzubauen. Es knnte auch interessant sein, die Flughhe im Anug zu protokollieren und nach dem Ende eine Kurve auf dem Bildschirm darzustellen, um zu zeigen, welch kurioses Auf und Ab das Flugzeug im Landeanug genommen hat.

9.2

Nightdriver

Nightdriver ist ein Klassiker unter den Spielen. Es wurde erstmals 1976 von Atari verffentlicht und gilt als eines der ersten Spiele mit einem dreidimensionalen Effekt. Die Grak berzeugt durch Einfachheit. Die Fahrt durch die Nacht wurde vor allem deshalb gewhlt, weil man nachts nichts sieht und so auch nichts darstellen muss. Nur der Rand der Fahrbahn, also die Fahrbahnmarkierungen leuchten im Scheinwerferlicht auf.

Abbildung 9.4 Screenshot Nightdriver

191

Die dritte Dimension

9.2.1

Modellierung des Straenverlaufs

Der Verlauf der Strae wird durch ein kleines Array festgehalten. Oben wird ein neues Element eingelegt, das die weitere Richtung der Strae bestimmt. Bei jedem Takt wandern die Elemente einen Schritt nach unten. Damit kann der Straenverlauf simuliert werden. Wenn das neue Element eine Linkskurve ist, wird ein Wert von der bisherigen Position abgezogen und bei einer Rechtskurve aufaddiert. Der berhmte dreidimensionale Effekt entsteht dadurch, dass der linke und der rechte Pfosten jeweils umso weiter nach auen gezogen werden, je nher sie zum unteren Bildschirmrand stehen. Als letzter Einuss auf den Verlauf der Strae kommt die Lenkung hinzu. Wird das Auto gelenkt, bewegen sich die weiter entfernten Objekte mehr als die nahen Objekte. Dabei muss ein Kompromiss gefunden werden, der einerseits die Lenkung in der Nhe ermglicht, ohne dass das Straenbild in der Entfernung komplett zerrissen wird. Randguren Whrend die Original-Version des Spiels nur Punkte als Randbegrenzung verwendete, gnnt sich diese Variante den Luxus, die Leitpfosten an der Strae nachzubilden. In Deutschland hat die linke Begrenzung zwei runde Reektionspunkte bereinander. Der rechte Pfosten trgt einen lnglichen, aufrechten, rechteckigen Reektionsstreifen. Da es Nacht ist, sieht man nichts auer diesen Reektoren. Steuerung Eigentlich gehrt zu einer Autosimulation ein Lenkrad. Aber leider besitzen nur die wenigsten Haushalte ein an den Computer anschliebares Lenkrad. Ein Paddle, wie es beim Apple ][ europlus beilag, wre auch schon eine gute Nherung. Aber auch die Paddles wurden bei der Modellpege zum Apple //e bereits wieder eingestellt. Viele Autorennspiele verwendeten die Pfeiltasten der Tastatur als Steuerung fr das Auto. Das empnde ich als Zumutung. Wer wrde sein Auto denn mit Cursortasten lenken wollen? Bei einem Auto muss es mglich sein, schnell von links nach ganz rechts zu wechseln. Das ist mit Tasten schwer zu machen. Ein Joystick wre eine Alternative, aber ich beispielsweise besitze berhaupt keinen. Um genau zu sein, besitze ich ihn nicht mehr, seit ihn meine Kinder entdeckt haben. Allerdings habe ich es auch nicht fr ntig befunden, mit der

192

Nightdriver

9.2

Autoritt des Erzbrs5 dessen Herausgabe zu fordern. Da ich davon ausgehe, dass einige der Leser hnlichen Schicksalsschlgen erlegen sind, habe ich nach einer anderen Mglichkeit gesucht. Passend wre ein Slider wie er bei der Fluglandung fr das Hhenruder und den Throttle verwendet wird. Aber der Slider wird durch Drcken und Schieben der Maus verndert. Das wrde bei diesem Spiel nicht schnell genug sein oder zu einem Krampf im Finger fhren. Stattdessen habe ich mich entschlossen, die Maus direkt abzufragen. Solange die Maus im Fenster ist, wird ihre Position abgefragt und die horizontale Komponente als Lenkrichtung missbraucht. Die Idee mit dem Slider ist aber auch nicht so verkehrt, denn Sie knnen einen Slider auch aus dem Programm herausstellen. Und so dient er als optisches Feedback fr die Steuerung des Autos.

9.2.2

Grasche Umsetzung

Wir verwenden auch hier die wxWidgets-Bibliothek. Es wird eine Header-Datei eingesetzt, in der vor allem die Klassen fr die Applikation und fr den Fensterrahmen zu nden ist.
#ifndef _NIGHTDRIVER_H_ #define _NIGHTDRIVER_H_ class Nightdriverapp : public wxApp { public: virtual bool OnInit(); }; class NightdriverFrame : public wxFrame { public: NightdriverFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnQuit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnMouseEvent(wxMouseEvent& event); void OnSize(wxSizeEvent& event); void OnPaint(wxPaintEvent& event); void OnTimer(wxTimerEvent& event); private:

5 Erzbr leitet sich nicht von dem Ausruf her Ich habe drei Haare auf der Brust. Ich bin ein Br!, sondern von der beispielsweise in Schulen gebruchlichen Abkrzung fr Erziehungsberechtigter (z. B.: Unterschrift des Erz. Ber.).

193

Die dritte Dimension

wxTimer timer; wxSize FeldGroesse; wxSlider *Lenkung; DECLARE_EVENT_TABLE() }; enum { Menu_File_Quit = 100, Menu_File_About }; #endif // _NIGHTDRIVER_H_
Listing 9.16 Header-Datei

Die Klasse Nightdriverapp ist extrem bersichtlich. Sie wird von der Basisklasse wxApp abgeleitet und enthlt nur die Funktion OnInit, in der das Objekt des Fensterrahmens initialisiert wird. Dieser Fensterrahmen wird durch die Klasse NightdriverFrame deniert. In der Header-Datei kann man an den Elementfunktionen, die mit dem Prx On beginnen, bereits erkennen, welche Ereignisse im Spielverlauf bentigt werden. Die ersten beiden Funktionen bedienen Menpunkte. Dann werden alle Mausereignisse gefangen. Hier wird bestimmt, an welcher Stelle sich die Maus bendet. Die Grenvernderung wird nur abgefragt, um den Slider sauber in das Fenster einzupassen. Die Funktion OnPaint wird sich um die grasche Darstellung kmmern. Ein Timer wird auch bentigt, um die Strae in regelmigen Abstnden zu aktuallisieren. Als private Elemente ist der Timer zu erkennen. Die Variable FeldGroesse erweist sich als praktisch, um neben dem Slider auch die Darstellung der Strae mittig auszurichten. Der Slider taucht ebenfalls als Element auf. Das Makro DECLARE_EVENT_TABLE wird verwendet, um die Ereignistabelle in der Implementierungsdatei anzukndigen. Die Implementierung Die Implementierung beginnt mit einigen Konstanten. Die Konstanten TIMER_ID und LENKER_ID bezeichnen mit einer eindeutigen Nummer den Timer und das Slider-Kontrollelement. Es ist mglich, mehrere Timer in einem Programm zu vewenden. Damit sie unterschieden werden knnen, erhalten sie unterschiedliche Nummern. Dass es mehr als ein Kontrollelement je Programm geben kann, ist offensichtlich. Darum nden Sie solche IDs in allen GUIs, allerdings sind sie manchmal in Ressource-Dateien verborgen. Der TIMERTICK betrgt 100 Millisekunden. Das bedeutet, dass das Programm im Takt einer Zehntelsekunde unterbrochen wird, um unter anderem einen neuen Bildschirm aufzubauen. Diese Geschwindigkeit bewirkt nicht nur die Ablaufge-

194

Nightdriver

9.2

schwindigkeit des Programms und damit durchaus seinen Schwierigkeitsgrad, sondern vor allem bestimmt sie, wie ieend die Bewegungsablufe erscheinen. Die Konstante LenkSchritte bestimmt, wie viele Unterteilungen und welchen Maximalwert der Slider besitzt. Mit der PfostenHoehe wird die Anzahl der Pixels bestimmt, die eine Leitpfostenhhe einnehmen kann. Der SeitenAbstand stellt ein, wie breit die Strae wird.
const const const const const const int int int int int int TIMER_ID=1; TIMERTICK=100; LENKER_ID=2; LenkSchritte = 40; PfostenHoehe = 16; SeitenAbstand = 8;

BEGIN_EVENT_TABLE(NightdriverFrame, wxFrame) EVT_MENU(Menu_File_Quit, NightdriverFrame::OnQuit) EVT_MENU(Menu_File_About, NightdriverFrame::OnAbout) EVT_MOUSE_EVENTS(NightdriverFrame::OnMouseEvent) EVT_SIZE(NightdriverFrame::OnSize) EVT_PAINT(NightdriverFrame::OnPaint) EVT_TIMER(TIMER_ID, NightdriverFrame::OnTimer) END_EVENT_TABLE() IMPLEMENT_APP(Nightdriverapp)
Listing 9.17 Konstanten und Ereignistabelle

Es folgt die Ereignistabelle, die den Ereignissen ihre Callback-Funktionen zuweist. In Abhngigkeit der Art des Ereignisses wird die ID des auslsenden Elements aufgefhrt. Initialisierung der Applikation Das Macro IMPLEMENT_APP erspart dem wxWidgets-Programmierer eine ganze Menge immer gleich ablaufender Initialisierungen. Wenn Sie schon einmal mit der Win32-API zu tun hatten, wissen Sie, wovon hier die Rede ist. Mit der Elementfunktion OnInit der Applikation wird das Rahmenfenster initialsiert.
bool Nightdriverapp::OnInit() { NightdriverFrame *frame = new NightdriverFrame( wxT("Night Driver"), wxPoint(50,50), wxSize(600,500)); frame->SetBackgroundColour(wxColour(0, 0, 0));

195

Die dritte Dimension

frame->Show(TRUE); SetTopWindow(frame); return TRUE; }


Listing 9.18 OnInit

Mit der Erzeugung erhlt die Applikation ihren Namen, den sie spter im Schiebebalken tragen wird. Auch die Fensterposition und -gre wird hier vorgegeben. Im Gegensatz zu den anderen Programmen im Buch erhlt dieses Fenster eine andere Hintergrundfarbe. Sie wird durch die drei RGB-Werte 0 bestimmt. Wie Sie schon ahnen, knnte das die Farbe Schwarz sein. Das Fenster wird dann gezeigt und in den Vordergrund geschoben. Im Konstruktor der Fensterklasse werden vor allem die Fensterelemente erstellt. Dazu gehrt das Men, der Statusbar und der Slider. Aber auch der unsichtbare Timer wird hier prpariert. Zuerst wird im Initialisierer der Konstruktor der Basisklasse aufgerufen. Dann wird der Timer initialisiert.
NightdriverFrame::NightdriverFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) , timer(this, TIMER_ID) { wxMenu *menuFile = new wxMenu; menuFile->Append(Menu_File_About, wxT("&ber...")); menuFile->AppendSeparator(); menuFile->Append(Menu_File_Quit, wxT("&Beenden")); wxMenuBar *menuBar = new wxMenuBar; menuBar->Append(menuFile, wxT("&Datei")); SetMenuBar( menuBar ); CreateStatusBar(); SetStatusText( wxT( "Fahr mich nach Hause!" ) ); Lenkung = new wxSlider(this, LENKER_ID, 28, 0, LenkSchritte, wxPoint(0, 0), wxDefaultSize, // wxSize(SliderLaenge, 30), wxSL_HORIZONTAL|wxSL_AUTOTICKS); Lenkung->Show(); Lenkung->SetValue(LenkSchritte/2); }
Listing 9.19 Konstruktor der Rahmenfensterklasse

196

Nightdriver

9.2

Die Erstellung des Mens ist nicht weiter kompliziert. Wenn Sie sich fr mehr Details interessieren, schlagen Sie bitte auf Seite 144 nach. Die Statusleiste wird dort zwar auch beschrieben, ist aber eigentlich leicht zu berschauen. Zum Schluss wird der Slider eingerichtet. Er wird mittig positioniert, damit das Programm erst einmal geradeaus fhrt, bevor der Anwender die Maus in das Fenster setzt und die Kontrolle bernimmt. Im Gegensatz zu anderen Applikationen, die den Timer verwenden, wird er hier nicht im Konstruktor gestartet. Stattdessen wird er gestartet, wenn die Funktion, die die Mausereignisse fngt, feststellt, dass der Mauszeiger in den Bereich des Applikationsfensters geschoben wird. Mausereignis Die Ereignisfunktion OnMouseEvent wird immer dann gerufen, wenn die Maus innerhalb des Applikationsfensters irgendetwas tut. Da jede Bewegung der Maus ein Ereignis auslst, verwendet man dieses Ereignis nicht so gern. Wird nicht jede Bewegung bentigt, wird das Programm beispielsweise auf das Ereignis Mausklick warten und dann ermitteln, an welcher Position sich die Maus derzeit bendet. Der weitere Punkt ist, dass die Applikation nur dann die Mausereignisse erhlt, wenn sich der Mauszeiger in einem Fenster der Applikation bendet. Verlsst der Mauszeiger das Fenster, bekommt das Programm keine neuen Informationen mehr und steuert das Auto in die bisherige Richtung. Es kann sein, dass der Spieler gar nicht bemerkt, dass er das Fenster verlassen hat und das Verhalten des Programms in diesem Sinne nicht schtzt. Die Applikation erhlt aber auch eine Information, wenn der Mauszeiger das Fenster verlsst oder wieder betritt. Durch Stoppen und Starten des Timers kann durch diese Methode erreicht werden, dass das Spiel nur dann luft, wenn es auch steuerbar ist. Verlsst der Mauszeiger versehentlich das Spiel, stoppt es. Das wird den Spieler sicher auch nicht mit Freude erfllen, aber er bemerkt es immerhin.
void NightdriverFrame::OnMouseEvent(wxMouseEvent& event) { if (event.Leaving()) timer.Stop(); if (event.Entering()) timer.Start(TIMERTICK); wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc)); Lenkung->SetValue( pt.x*LenkSchritte/FeldGroesse.GetWidth()); }
Listing 9.20 Ereignisfunktion Maus

197

Die dritte Dimension

Im Parameter einer Ereignisfunktion sind die Informationen abgelegt, die in Verbindung mit dem Ereignis stehen. So kann die Variable event abgefragt werden, ob das Fenster betreten oder verlassen wurde. Im ersten Fall wird der Timer gestartet, und das Spiel luft, bis der Mauszeiger das Fenster verlsst und der Timer gestoppt wird. Die Variable event enthlt natrlich auch die Position der Maus. Allerdings gibt es zwei unterschiedliche Betrachtungen der Mausposition. Das System sieht eine Mausposition als eine Koordinate auf dem Bildschirm. Diese teilt sie dem Fenster auch mit. Die Applikation interessiert sich aber nur dafr, wo sich die Maus innerhalb des Fensters bendet. Sie msste also die Position des eigenen Fensters auf dem Bildschirm ermitteln, die Breite des Fensterrahmens und diese Werte von der Globalposition der Maus abziehen. Diese Umrechnung erledigt die Funktion GetLogicalPosition. Sie braucht aber fr die Umrechnung einen Grakkontext. Dieser wird darum zuvor bestimmt. Das Programm bentigt nur die X-Koordinate dieses Ergebnisses. Nur die Querbewegungen steuern das Lenkrad. Mit diesem Wert wird der Schieber des Sliders positioniert. Fenstergre Die Fenstergre wird an das Programm erstmalig bertragen, wenn das Hauptfenster gestartet wird.
void NightdriverFrame::OnSize(wxSizeEvent& event) { FeldGroesse = event.GetSize(); #ifndef __unix__ static bool erst = true; if (erst) erst = false; return; #endif Lenkung->SetSize(0, 0, FeldGroesse.GetWidth(), 30); }
Listing 9.21 Ereignisfunktion Fenstergre

Das Programm ermittelt die Fenstergre und speichert sie in der Elementvariablen FeldGroesse. Wenn Sie eben genau hingeschaut haben, wird diese Variable auch dazu verwendet, um den Schieber fr die Lenkung zu justieren. An dieser Stelle wird der Slider auf die Lnge des Fensters angepasst. Ansonsten wrde der Slider merkwrdig aussehen, wenn der Anwender auf den Gedanken kommt, die Fenstergre zu verndern. Allerdings funktioniert die Vernderung des Schiebers unter Windows nicht. Das Kontrollelement kann nicht positioniert werden, bevor das Rahmenfenster aufge-

198

Nightdriver

9.2

baut ist. Hier hilft die unschne Konstruktion mit dem #ifdef. Unter allen UNIXAblegern und so auch unter Linux ist __unix__ deniert. Darstellung des Fensterinhalts Die Funktion OnPaint sorgt fr die Bildschirmausgaben. Zum Zeichnen bentigt die Funktion einen device context. Dann wird ein Clipping ber den Fensterbereich durchgefhrt. Das bedeutet, dass das System alle Versuche, auerhalb des vorgegebenen Bereichs zu zeichnen, abschneidet. Die Funktion holt die aktuelle Straenlinie von der Anwendungsklasse tGame aus dem Array Pos und setzt die Informationen in Bildschirmkoordinaten um. Dabei sorgt die Funktion auch gleich fr die dreidimensionale Darstellung, indem es die vorne liegenden Symbole auseinanderzieht.
void NightdriverFrame::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); dc.SetClippingRegion(0,0,FeldGroesse.GetWidth(), FeldGroesse.GetHeight()); tGame *game = tGame::getInstance(); for (int i=0; i<MaxElements; i++) { int x = FeldGroesse.GetWidth()/2 + game->getPosr(i)*SeitenAbstand; int y = i*PfostenHoehe; dc.DrawCircle(x - i*SeitenAbstand, y, PfostenHoehe/8); dc.DrawCircle(x - i*SeitenAbstand, y+PfostenHoehe/2, PfostenHoehe/8); dc.DrawRectangle(x + i*SeitenAbstand, y-PfostenHoehe/4, PfostenHoehe/4, PfostenHoehe-4); } }
Listing 9.22 Ereignisfunktion OnPaint

Zunchst werden die X- und die Y-Koordinate ermittelt. Die Klasse tGame rechnet in ganzzahligen Schritten. Die einfachere Umrechnung ist die der Y-Koordinate. Der Positionsindex wird mit der Anzahl der Pixel multipliziert, die fr jeden Spielindex vorgesehen ist. Diese Konstante heit PfostenHoehe und gibt in Pixeln an, wie viel Raum fr das Zeichnen eines Pfostens in seiner Hhe bentigt wird.

199

Die dritte Dimension

Die X-Koordinate sucht ihren Platz in der Mitte des Fensters. Das Spiel liefert die Differenz von dieser Position. Auch hier mssen die Positionen mit einem Faktor von Pixeln multipliziert werden, damit der Abstand auf dem Bildschirm sichtbar wird. Dieser Abstand heit SeitenAbstand. Damit sind die Positionen bestimmt, es fehlt aber noch die perspektivische Sicht. Dazu wird, ausgehend von der XKoordinate noch einmal Indexzhler i mit dem SeitenAbstand multipliziert. Dieser Wert wird vom linken Pfosten abgezogen und auf den rechten Pfosten aufaddiert. Timerereignis Der Timer liefert jede Zehntelsekunde ein Ereignis. Die behandelnde Funktion ermittelt den Stand des Sliders, also die Position des Lenkrads, und leitet sie an die Anwendungsklasse weiter. Durch den Aufruf der Funktion naechsterSchritt wird die Berechnung der neuen Straenpositionen ausgelst. Diese Funktion ermittelt auch, ob der Fahrer von der Strae abgekommen ist und liefert dies zurck.
void NightdriverFrame::OnTimer(wxTimerEvent& event) { tGame *game = tGame::getInstance(); game->setLenkung((LenkSchritte/2-Lenkung->GetValue())); if (!game->naechsterSchritt()) SetStatusText( wxT( "Bang" ) ); else SetStatusText( wxT( "Gut" ) ); Refresh(); // Erzwinge Aufruf von OnPaint }
Listing 9.23 Ereignisfunktion OnTimer

Das Programm notiert derzeit lediglich einen Hinweis in der Statusleiste. Alternativ htte hier eine Messagebox erscheinen knnen, die das Spiel fr beendet erklrt und den Trauergesang einstimmt. Alternativ knnte man aber auch die bertritte zhlen, nach einer gewissen Zeit abbrechen und dann den Punktestand verknden. Das einmalige berfahren des Straenrandes knnte auch akustisch untermalt werden und nur bei dauerhafter bertretung zum Spielende fhren.

9.2.3

Die Anwendungsklasse

Sofern die eigentliche Anwendung nicht allzu eng mit den Elementen der graschen Oberche verwoben ist, lohnt es sich immer, die Anwendung von deren Darstellung zu trennen. Auch wenn wxWidgets portabel ist und inzwischen selbst

200

Nightdriver

9.2

auf Mobiltelefonen anzutreffen ist, kann es passieren, dass die Anwendung in einer vllig anderen Umgebung laufen soll. Unabhngig davon sorgt es bei greren Projekten immer fr sehr viel mehr bersicht, wenn die eigentliche Funktionalitt unabhngig von der Darstellung behandelt wird. Singleton Die Klasse tGame ist als Singleton gestaltet. Es gibt im Programm nur eine Instanz von einem Spiel. Ein Singleton ist ein sehr einfaches Entwurfsmuster, das dafr sorgt, dass es nicht mehr als eine Instanz gibt. Erreicht wird dies, indem der Konstruktor nicht ffentlich ist. Damit ist das Anlegen einer Instanz unmglich. Damit berhaupt eine Instanz erzeugt werden kann, muss dies nun die Klasse selbst tun. Zu diesem Zweck verwendet sie eine statische Zeigervariable, die auf 0 initialisiert wird. Ein Anwender der Klasse muss diesen Instanzzeiger ber die statische Funktion getInstance anfordern. Die Funktion prft, ob der Zeiger auf 0 steht. Dann gibt es noch keine Instanz und die Funktion erzeugt eine Instanz und belegt damit den Zeiger. Jeder weitere Aufruf ndet den Zeiger bereits belegt und liefert ihn zurck. Damit bleibt es bei dieser einen Instanz. Der kritische Punkt kann in einer Multitasking-Umgebung liegen, wenn zwischen der erfolgten Prfung und dem Anlegen der Instanz ein Taskwechsel erfolgt und der andere Task ebenfalls eine Instanz anlegen will. Dadurch kann zweimal eine Instanz angelegt werden. Mit diesem Problem wird der Nightdriver allerdings nicht zu kmpfen haben. Der Singleton ist in seiner Handhabung sehr angenehm und nicht schwer zu programmieren. Er ist in jedem Fall angenehmer als eine globale Variable. Hinzu kommt, dass ein Singleton erst dann erstmals Ressourcen verbraucht, wenn er das erste Mal bentigt wird.
#ifndef GAME_H #define GAME_H const int MaxElements=25; class tGame { public: static tGame *getInstance(); // Singleton bool naechsterSchritt(); void setLenkung(int Schritte); int getPos(int i) { return Pos[i]; } protected: tGame(); static tGame* instance;

201

Die dritte Dimension

private: int getNextZiel(); int Lenker; int Pos[MaxElements]; }; #endif


Listing 9.24 Header-Datei fr die Anwendung

Die Funktion naechsterSchritt wird vom Timer aufgerufen und bewirkt eine Neuberechnung der Strecke. Die Funktion setLenkung wird von der Mausbehandlung aufgerufen und setzt die aktuelle Lenkposition, die in der privaten Variablen Lenker gehalten wird. Das Array Pos enthlt die Position der Strae. Von auen ist nur ein lesender Zugriff erforderlich, der durch die Funktion getPos ermglicht wird. Die Konstante MaxElements bestimmt die Anzahl der Positionen. Implementierung des Singletons In der Implementierungsdatei wird die statische Zeigervariable des Singletons deniert und dabei auf 0 initialisiert. Die Funktion getInstance birgt keine groen berraschungen. Der dazugehrige Konstruktor initialisiert die Lenkung und die Positionen.
#include "game.h" tGame* tGame::instance = 0; tGame *tGame::getInstance() { if (instance == 0) { instance = new tGame(); } return instance; } tGame::tGame() { for (int i=0; i<MaxElements; i++) { Pos[i] = 0; } Lenker = 0; }
Listing 9.25 Implementierung des Singletons

202

Nightdriver

9.2

Straenbau Die Positionen fr die Strae werden von unten nach oben durchgereicht. Dafr sorgt die for-Schleife. Dabei wird der neuen Position auch gleich noch der Lenkradeinschlag mitgegeben. Damit der Lenkradeinschlag umso strker wirkt, je weiter das Element entfernt ist, wird die Variable Lenker durch das Quadrat des Laundex geteilt. Die Quadrierung bewirkt, dass die Lenkausschlge nicht gar so heftig ausfallen.
bool tGame::naechsterSchritt() { for (int i=MaxElements-1; i>0; i--) { Pos[i] = Pos[i-1]+Lenker/(i*i); } Pos[0] += getNextZiel(); if (Pos[MaxElements-1]>10 || Pos[MaxElements-1]<-10) { return false; } return true; }
Listing 9.26 Berechnung der Straenpositionen

Nach dem Umschichten der Positionen wird das nchste Ziel angefordert. Dieses Ziel wird durch die private Funktion getNextZiel ermittelt. Derzeit liefert sie lediglich eine sich leicht hin und her windende Strae. Wie stark das Lenkverhalten nur durch geringe nderungen in den Parametern verndert wird, zeigt die folgende Variante:
for (int i=MaxElements-1; i>0; i--) { Pos[i] = Pos[i-1] + Lenker/2; }

Hier wird in der Schleife der Lenker nicht durch den Abstand geteilt, sondern durch eine Konstante. Das bewirkt, dass sich das ganze Bild eher parallel hin und her bewegt. So erwecken starke Lenkbewegungen eher den Eindruck eines Hochseekutters bei heftigem Seegang. Immerhin funktioniert die Lenkung wesentlich direkter, was dazu fhrt, dass das Spiel wesentlich leichter steuerbar ist. Bei einigem Nachdenken wird der Konikt deutlich. Wenn man das Auto nur um einen geringen Winkel dreht, wird die Strae schnell aus dem direkten Sichtfeld verschwinden. In der Realitt kompensieren wir das, indem wir der Strae mit den Augen folgen. In der Simulation gibt es die Alternative, dass die Strae stndig aus dem Sichtfeld verschwindet und den Spieler orientierungslos zurcklsst oder eine geflschte Darstellung, die nicht der Wirklichkeit entspricht.

203

Die dritte Dimension

Am Ende der Funktion naechsterSchritt wird geprft, ob der Fahrer die Strae verlassen hat. Hier wird ein willkrlicher Tunnel der Differenz von 10 Positionen als korrekt bewertet. Wird diese Abweichung berschritten, liefert die Funktion den Wert false.
int tGame::getNextZiel() { static int i=-1; const int Ziele[] = { 1, 2, 3, 2, 1, 0, -1, -2, -3, -2, -1, 0 }; i++; if (i>=sizeof(Ziele)/sizeof(int)) i=0; return Ziele[i]; }
Listing 9.27 Berechnung der Straenpositionen

Die Funktion getNextZiel sorgt dafr, dass die Strae immer weitere Windungen vollfhrt. In der derzeitigen Version ist sie sehr einfach gehalten. Ein einfaches Array enthlt jeweils den nchsten Zielpunkt. Wenn Sie die Werte im Array betrachten, werden Sie sehen, dass die Kurven langsam strker ausschlagen. Alles andere wirkt abgehackt und unnatrlich.

9.2.4

Was denn noch?

Das Spiel ist derzeit ein netter Rohbau. Vieles knnte noch integriert werden, um das Spiel zu erweitern. Wie Sie sicher bemerkt haben, sind in dem Spiel eine Reihe von Parametern, die durch eiiges Verbiegen irgendwann zu einem spielbaren Ergebnis gefhrt haben. Alle diese Werte knnen beliebig verndert werden. Sie werden feststellen, wie schnell bei nderung der Parameter das Spiel unspielbar wird. Perspektivenmngel Die perspektivische Darstellung ist wie beim Original sehr einfach gehalten. In bestimmten Situationen ist sie auch denitiv falsch. Das betrifft vor allem scharfe Kurven. Whrend sich ein reales Auto in die Kurve hineindreht, zieht die Art der Perspektivenberechnung bei dem Spiel die Kurve nur nher heran. Das fhrt dazu, dass die Kurve sehr viel schrfer wird als in der Realitt. Der Aufwand, eine Drehung herbeizufhren, ist aber erheblich grer. Interessant ist auch, dass Sie bei leichten Lenkbewegungen sehr viel leichter durch den Parcours kommen. Sobald Sie hektisch lenken, wird die Linienfhrung der Strae etwas unsauber. Allerdings knnte man diesen Effekt sogar als pdagogisch

204

Nightdriver

9.2

wertvoll einstufen. Schlielich ist es auch in der Realitt nicht klug, das Lenkrad aufgeregt hin und her zu reien. Streckenverlauf Der Streckenverlauf des Spiels wird derzeit nur durch das einfache Array bestimmt, das die Strae immer nur hin und her schwanken lsst. Hier liee sich natrlich ein Riesenparcours ablegen. Wenn Sie die Daten haben, knnten Sie beispielsweise den Hockenheimring einfttern. Die sanften Einschwenkungen in die Kurven mssen natrlich nicht von Hand eingetragen werden. Nach dem alten Motto Never do what a computer can do better knnte das die Funktion getNextZiel jeweils berechnen. Dann mssten im Array nur noch die Extremwerte fr den Streckenverlauf abgelegt werden. Die verbindenden Elemente wrde die Funktion errechnen. Wenn das Programm die bergnge selbst anpasst, ist es auch relativ einfach, die nchsten Ziele durch einen Zufallsgenerator erzeugen zu lassen. Geschwindigkeitsregler Bisher fhrt das Auto mit gleichbleibendem Tempo duch den Parcours. Es wre natrlich viel glaubwrdiger, wenn es mglich wre, das Auto zu beschleunigen und zu bremsen. Wenn man schon mit der Maus lenkt, knnte man die Maustasten als Gas und als Bremse verwenden. Mit der Funktion wxMouseEvent::GetButton kann ermittelt werden, ob eine Maustaste gedrckt wurde. Als Rckgabeparameter knnen wxMOUSE_BTN_NONE, wxMOUSE_BTN_LEFT, wxMOUSE_BTN_MIDDLE oder wxMOUSE_BTN_RIGHT auftreten. Um eine realistische Simulation zu erzeugen, sollte das Auto bei langsamer Fahrt auch engere Kurven fahren knnen. Dazu musste das Lenkvermgen erhht werden. Siegertreppchen ber die Art der Punkteermittlung elen ja bereits ein paar Worte. Das kurze Schneiden einer Ecke muss nicht zwangslug zum Ende des Rennens fhren. Es knnte aber die Geschwindigkeit reduzieren oder als Fehlerpunkte gewertet werden. Wer die ganze Zeit neben der Strae fhrt, muss natrlich auch irgendwann einmal ausscheiden. Wenn Sie unterschiedliche Geschwindigkeiten erlauben, muss die gefahrene Geschwindigkeit auch Einuss auf die Punkte haben. Wenn Punkte verteilt werden, machen diese erst richtig Spa, wenn sie vergleichbar sind. Also liegt eine High-Score-Liste nahe.

205

Das Bermuda-Dreieck ist immer ein spannendes Thema. Allerdings geht es hier weniger um Nebel und Kompass, sondern eher um eine Mischung aus Schiffe versenken und Sudoku.

10

Shorts im Nebel

Genau zu dem Zeitpunkt, als ich meinen ersten Computer gekauft hatte, brachte die Firma Ravensburger das Spiel Galaxis heraus. Es war eine Kombination aus Such- und Knobelspiel, ausgestattet mit einem Kleinst-Computer, der mit zwei Drehschaltern, vier Leuchtdioden und einem Lautsprecher das Zentrum des Spiels darstellte.

Abbildung 10.1 Das Spiel Galaxis aus den 1980ern (Foto: AW)

Dieses kleine Gert entleerte allerdings die Batterien in einem Tempo, wie Boris Jelzin seinerzeit Wodka-Flaschen. Es musste etwas unternommen werden. Wie man Jelzin htte helfen knnen, wei ich auch nicht. Meine Lsung bezglich

207

10

Shorts im Nebel

des stromdurstigen Spiels war es, das Spiel in vielen Nchten auf meinem neu erworbenen Computer zu implementieren. Mein Bruder kaufte ein Steckernetzteil. Die Namensgebung Galaxis war insofern etwas unpassend gewhlt, weil das Spiel darin bestand, auf einer zweidimensionalen Spielche nach Objekten zu suchen. Satelliten benden sich aber im Raum und mssten ber drei Koordinaten geortet werden. Darum war ich der Meinung, dass das Spiel besser auf die Erde zurckgeholt werden sollte. Es war viel glaubwrdiger, dass auf einer Ebene nach verschwundenen Schiffen im Meer gesucht wrde. Und bei verschwundenen Schiffen im Meer fllt Ihnen sicher auch als Erstes der Name Bermuda ein.

10.1

Spielregeln

Auf einem Feld von neun mal sieben Feldern sind vier Schiffe verloren gegangen, die durch Peilungen gefunden werden sollen. Dazu wird bei jedem Zug eine Koordinate bestimmt, von der aus in alle vier Windrichtungen und in die vier Diagonalen gepeilt wird. Der Peilsender zhlt, in wie viele Richtungen Reektionen auftreten, die durch ein Schiff verursacht wird. Die Peilung liefert als Ergebnis also die Anzahl der Richtungen, nicht die Anzahl der Schiffe, da mehrere Schiffe die in einer Richtung hintereinander stehen, sich gegenseitige verdecken und als eines gezhlt werden. Durch die Kombination der bisherigen Ergebnisse knnen Sie Schlsse ziehen, wo sich Schiffe benden und wo die Wahrscheinlichkeit eher gering ist. Eine Peilung mit dem Ergebnis 0 bedeutet eindeutig, dass in allen Richtungen kein Schiff zu nden ist. Sie knnen die Positionen auf allen Linien als Position fr ein Schiff ausschlieen. In dem Original-Spiel gibt es dafr schwarze Stpsel, mit denen Sie diese Felder markieren knnen. Erkennt die Peilung vier Reexionen, mssen sich alle Schiffe auf den Linien dieser Peilung benden. Wenn Sie zwei Peilungen mit je drei Meldungen bekommen haben, wissen Sie, dass sich mindestens zwei Schiffe auf einem der Schnittpunkte der Peilkoordinaten benden mssen. Durch geschicktes Setzen von Peilungen und Bewerten der Schnittpunkte knnen Sie also sehr viel schneller zum Ziel kommen als durch einfaches Raten wie bei dem Spiel Schiffe versenken.

10.2

Datenmodellierung

Das Spiel enthlt ein Spielfeld und vier Schiffe. Das Spielfeld speichert die bisherigen Rateversuche. Die Schiffe kennen Ihre Position und ob sie bereits entdeckt wurden.

208

Datenmodellierung

10.2

10.2.1

Ein Schiff

Ein Schiff ist sehr unkompliziert. Es hat eine Position, die der Spieler erraten soll. Und es hat eine Marke, an der erkannt wird, ob dieses Schiff bereits vom Spieler gefunden wurde. Das ist wichtig, um zu erkennen, ob das Spiel bereits beendet ist.
#ifndef SCHIFF_H #define SCHIFF_H class tSchiff { public: tSchiff() {gefunden=false;} ~tSchiff() {} void setPosition(int px, int py) {x=px; y=py;} int getX() {return x;} int getY() {return y;} bool istGefunden() {return gefunden;} void setGefunden() {gefunden = true;} private: int x, y; bool gefunden; }; #endif
Listing 10.1 schiff.h: Ein Schiff wird deklariert

Da die Elementfunktionen der Klasse nur aus dem Setzen und Auslesen der lokalen Variablen bestehen, kann auf eine separate Implementierungsdatei verzichtet werden.

10.2.2 Das Wasser


Das Spielfeld ist das Meer, auf dem vier Schiffe verborgen sind. Damit hat das Spielfeld alle Informationen, die das Spiel ausmachen.
#ifndef FELD_H #define FELD_H #include "schiff.h" const const const const int int int int MaxX=9; MaxY=7; MaxSchiff=4; SCHIFF = 9; // eins mehr als Richtungen

209

10

Shorts im Nebel

class tFeld { public: tFeld(); ~tFeld(); void zeige(); int zaehlePeilungen(int x, int y); bool istGewonnen(); private: tSchiff Schiff[MaxSchiff]; char Wasser[MaxX][MaxY]; int pruefeRichtung(int px, int py, int dx, int dy); int istHierEinSchiff(int px, int py); }; #endif
Listing 10.2 feld.h: Das Spielfeld

Von den Elementvariablen enthlt das zweidimensionale Array Wasser das eigentliche Spielfeld. Die Buchstaben, die sich hierin benden, werden dem Spieler angezeigt. Hier steht ein +, wenn der Spieler das Feld noch nicht abgefragt hat. Ein Stern symbolisiert ein gefundenes Schiff, und eine Ziffer zeigt das Ergebnis erfolgter Peilungen an. Das Spielfeld enthlt auch die vier Schiffe. Fr die Speicherung reicht ein einfaches Array, da sich die Anzahl der Schiffe whrend des Spiels nicht ndert.

10.3

Die Spielimplementierung

Der Konstruktor des Spielfelds verteilt die Schiffe auf ihre Positionen. Dazu kommt der Zufallszahlengenerator des Systems zu Hilfe, und der wird mit der Systemzeit in Sekunden vorgewrfelt, sodass wir auf recht zufllige Werte hoffen drfen. Erhlt ein Schiff eine Position auf der bereits ein anderes Schiff liegt, muss es neu verteilt werden. Der Konstruktor bereitet auch das Wasser so vor, dass alle Positionen ungeraten sind.
tFeld::tFeld() { int i, j; int x, y; srand(time(0)); i=0;

210

Die Spielimplementierung

10.3

while (i<MaxSchiff) { x = rand() % MaxX; y = rand() % MaxY; Schiff[i].setPosition(x,y); for (j=0; j<i; j++) { if (Schiff[j].getX()==x && Schiff[j].getY()==y) { // Die Schiffe sind gleich, wir brauchen ein // Neues! i--; break; } } i++; } for(i=0; i<MaxX; i++) { for (j=0; j<MaxY; j++) { Wasser[i][j] = '+'; } } }
Listing 10.3 Konstruktor

Der Destruktor tut nichts. Da keine Ressource dynamisch angefordert wird, gibt es hier zu hektischer Betriebsamkeit auch keinerlei Anlass. Der eigentliche Kern des Spiels ist die Ermittlung einer Peilung. Dafr ist die Funktion zaehlePeilungen verantwortlich. Als Parameter erhlt sie die Koordinate, und sie liefert die Anzahl der gefundenen Richtungen zurck. Das knnen maximal acht sein, nmlich die Himmelsrichtungen und die Diagonalen. Da aber nur vier Schiffe verteilt sind, steht zu erwarten, dass sie bestenfalls in vier Richtungen zu nden sind. Um keine Verwechslung mit der Anzahl der Richtungen zuzulassen, liefert die Funktion eine 9, wenn direkt ein Schiff getroffen wurde. Die Zahl 9 verbirgt sich hinter der Konstanten SCHIFF. Das funktioniert auch dann noch, wenn Sie die Anzahl der mglichen Schiffe auf ber acht erhhen.
int tFeld::zaehlePeilungen(int px, int py) { // haben wir bereits ein Schiff getroffen? int Anzahl = istHierEinSchiff(px, py); if (Anzahl>0) { Wasser[px][py] = '*'; Schiff[Anzahl-1].setGefunden(); return SCHIFF; }

211

10

Shorts im Nebel

// suche in alle Richtungen for (int dx=-1; dx<=1; dx++) { for (int dy=-1; dy<=1; dy++) { Anzahl += pruefeRichtung(px, py, dx, dy); } } Wasser[px][py] = (char)('0'+Anzahl); return Anzahl; }
Listing 10.4 feld.cpp: Konstruktor

Wie schon angedeutet, prft die erste Hlfte der Funktion, ob ein Schiff direkt getroffen wurde, markiert es als gefunden und meldet den Fund sofort zurck. Wie Sie vielleicht bemerkt haben, dekrementiert die Funktion vor der Markierung den Schiffsindex. Das hngt damit zusammen, dass die Funktion istHierEinSchiff diesen vor der Rckgabe um eins erhht, damit sich der Index 0 von der booleschen Interpretation, dass hier kein Schiff sei, abhebt. Eine weitere Untersuchung der Koordinate ist nicht erforderlich, wenn ein Schiff gefunden wurde. In der zweiten Hlfte sorgen zwei verschachtelte Schleifen dafr, dass alle Richtungen abgeklappert werden. Die Schleifen lassen die Variablen dx und dy von 1 bis 1 laufen. Diese werden von der Funktion pruefeRichtung schrittweise auf die Ausgangsposition aufaddiert. Enthlt dx die Zahl 1 und dy die Zahl 0, wird die Koordinate durch Aufaddieren dieser Werte einen Schritt weiter links sein. Durch die Schleife werden alle acht mglichen Richtungen erzeugt. Zustzlich gibt es aber auch den Fall, dass beide Variablen 0 sind. Das fngt die Funktion pruefeRichtung ab.
int tFeld::pruefeRichtung(int px, int py, int dx, int dy) { // laufe eine Linie entlang if (dx==0 && dy==0) return 0; while (px>=0 && px<MaxX && py>=0 && py<MaxY) { if (istHierEinSchiff(px, py)>0) return 1; px += dx; py += dy; } return 0; }
Listing 10.5 feld.cpp: tFeld::pruefeRichtung

Die Funktion pruefeRichtung inkrementiert die Koordinaten der aktuellen Position in Abhngigkeit von der Richtung, die in den letzten beiden Parametern

212

Die Spielimplementierung

10.3

bergeben wurde. Sie bricht ab, sobald einer der Rnder erreicht wurde und prft bei jeder Position, ob sich dort ein Schiff bendet. Sobald ein Schiff gefunden wurde, wird die gesamte Funktion abgebrochen, da immer nur das erste Schiff in einer Richtung gezhlt wird. Die private Elementfunktion istHierEinSchiff geht das Array fr die vier Schiffe durch und prft, ob eines an der angegebenen Koordinate steht.
int tFeld::istHierEinSchiff(int px, int py) { // ist hier etwas? int i; for (i=0; i<MaxSchiff; i++) { if (Schiff[i].getX()==px && Schiff[i].getY()==py) { return i+1; } } return 0; }
Listing 10.6 feld.cpp: tFeld::istHierEinSchiff

Die Funktion erhht den Schiffsindex vor der Rckgabe um eins, da dieser ja bei 0 beginnt und die 0 fr kein Schiff steht. Sie kann das Schiff allerdings nicht als gefunden markieren, weil sie auch aufgerufen wird, um festzustellen, ob in Peilrichtung ein Schiff steht. Auf hnliche Weise arbeitet die Elementfunktion istGewonnen. Auch sie durchluft alle Schiffe, um zu sehen, ob alle gefunden sind.
bool tFeld::istGewonnen() { bool gewonnen = true; for (int i=0; i<MaxSchiff; i++) { if (!Schiff[i].istGefunden()) { gewonnen = false; } } return gewonnen; }
Listing 10.7 feld.cpp:tFeld::istGewonnen

213

10

Shorts im Nebel

10.4

Konsolenversion

Bermuda lsst sich wunderbar ohne Hilfestellung einer graschen Umgebung spielen. Fr die Programmierung muss also der Rahmen, den der Standard von C++ bietet, nicht verlassen werden. Das Spielfeld kann als Buchstabenraster ausgegeben werden. Das erledigt die Funktion zeige.
void tFeld::zeige() { int i, j; cout << " "; for (i=0; i<MaxX; i++) { cout << (char)('A'+i) <<" " ; } cout << endl; for (j=0; j<MaxY; j++) { cout << j+1 << " "; for(i=0; i<MaxX; i++) { cout << Wasser[i][j] << " "; } cout << endl; } }
Listing 10.8 feld.cpp: tFeld::zeige

Nun mssen nur noch die entsprechenden Funktionalitten in einen Rahmen gesetzt werden, und schon funktioniert das Programm. Ein extrem einfaches Hauptprogramm ist hier vorgestellt.
#include "feld.h" #include <iostream> using namespace std; void leseKoord(int &x, int &y) { char CharCoord; int IntCoord; cin >> CharCoord >> IntCoord; x = CharCoord - 'A'; y = IntCoord - 1; } int main() {

214

Konsolenversion

10.4

int x, y; tFeld Feld; do { Feld.zeige(); leseKoord(x, y); Feld.zaehlePeilungen(x, y); } while (!Feld.istGewonnen()); return 0; }
Listing 10.9 main.cpp: Hauptprogramm

Die Funktion leseKoord ist allerdings sehr einfach gestrickt und bentigt die wohlwollende Zusammenarbeit des Spielers. Falls Sie Bermuda als Konsolenanwendung verwenden wollen, knnten Sie hier eine etwas tolerantere Variante bauen. Der folgende Bildschirmabzug zeigt, wie das Programm gespielt wird. Mit A5 wurde eine 0 gefunden. Damit ist sicher, dass die gesamte Zeile 5 kein Schiff enthlt. Der Zweier auf der Position B4 kann in der Diagonalen nach rechts oben auch kein Schiff sehen, also wurde D4 als Schnittpunkt zwischen A4 und D5 gewhlt. Und prompt fand sich dort ein Schiff.
A5 1 2 3 4 5 6 7 D4 1 2 3 4 5 6 7 A + + + + 0 + + A + + + + 0 + + B + + + 2 + + * B + + + 2 + + * C + 0 + + + + + C + 0 + + + + + D 1 + + + 3 + + D 1 + + * 3 + + E + + + + + + + E + + + + + + + F + + 2 + + + + F + + 2 + + + + G + + + + + + + G + + + + + + + H 1 + + + 1 1 + H 1 + + + 1 1 + I + + + + + + + I + + + + + + +

Nun sieht B4 in zwei Richtungen Schiffe, nmlich auf D4 und B7. Daraus folgt, dass auf allen Diagonalen von B4 kein Schiff mehr sein kann. Auch nach oben und nach links kann es aus diesem Grund kein Schiff mehr geben. Allerdings

215

10

Shorts im Nebel

ist es denkbar, dass auf den Richtungen nach rechts oder nach unten durchaus weitere Schiffe verborgen sind.

10.5

Etwas Komfort

Dem Originalspiel Galaxis ist eine stattliche Anzahl von Pins beigefgt, mit denen der Spieler Positionen markiert, auf denen sich denitiv kein Schiff benden kann. Diese Markierungen sind auch wirklich hilfreich. Also sollte es so etwas auch beim Computerspiel geben. Die grundlegende Funktion fr das Markieren ist nicht schwer zu implementieren. Auf den Wunsch des Anwenders, eine Markierung zu setzen, wird beispielsweise ein Punkt oder ein anderes bislang nicht verwendetes Zeichen in das zu markierende Feld gesetzt. Allerdings wird der Anwender nicht wollen, dass seine bisherigen Anfragen einfach wegmarkiert werden. Also wird vor einer Markierung gefragt, ob das Feld noch in seinem ursprnglichen Zustand ist. Was aber, wenn der Spieler ein bereits markiertes Feld abermals markiert. Es kann bedeuten, dass er sich geirrt hat und dort gar keine Marke haben mchte. Wenn die Funktion aber von einem Automatismus aufgerufen wird, der eine ganze Linie markiert, muss eine berlappung von Markierungen als gesetzte Markierung bleiben. Um das steuern zu knnen, wird der boolesche Parameter kipp verwendet.
void tFeld::markiere(int x, int y, bool kipp) { char Inhalt = Wasser[x][y]; if (Inhalt=='+') { Wasser[x][y] = '.'; } else if (kipp && Inhalt=='.') { Wasser[x][y] = '+'; } }
Listing 10.10 Markierungen setzen und lschen

Eine automatische Markierung kann vorgenommen werden, wenn eine Peilung kein Schiff gefunden hat. Dann ist in keiner der Richtungen ein Schiff zu sehen. Also knnen alle Positionen in den acht Peilrichtungen markiert werden. Einen solchen Automatismus kann das Programm selbst erledigen. Vom Ablauf funktioniert das ganz hnlich wie die Suche nach Schiffen. Die Funktion markiereNull verwendet eine verschachtelte Schleife, um die acht Richtungen zu

216

Nun alles im Fenster

10.6

durchlaufen und ruft die Funktion markiereRichtung, die dann die Linie entlangluft und jede Position markiert.
void tFeld::markiereRichtung(int px, int py, int dx, int dy) { if (dx==0 && dy==0) return; while (px>=0 && px<MaxX && py>=0 && py<MaxY) { markiere(px, py, false); px += dx; py += dy; } } void tFeld::markiereNull(int px, int py) { // suche in alle Richtungen for (int dx=-1; dx<=1; dx++) { for (int dy=-1; dy<=1; dy++) { markiereRichtung(px, py, dx, dy); } } }
Listing 10.11 Markierungen automatisch setzen

Die Funktion markiere wird so aufgerufen, dass ein zweimaliges Markieren die bisherige Markierung nicht umkippt. Da die Funktion markiere dafr sorgt, dass bisherige Peilungen oder gefundene Schiffe nicht berschrieben werden, gehen keine Informationen verloren.

10.6

Nun alles im Fenster

Die Eingabe ber Koordinaten ist sehr mhsam. Das Markieren einzelner Positionen wird ber Koordinateneingaben so umstndlich, dass es wohl kaum ein Spieler nutzen wird. Da wre die Verwendung der Maus wesentlich angenehmer und vor allem auch zeitgemer. Im ersten Schritt wird das Spiel einfach nur in ein Fenster umgetopft. Die Peilungen werden weiterhin als Ziffern dargestellt und die Schiffe als Sterne. Aber nun kann mit der linken Maustaste die Schiffe gesucht und mit der rechten Maustaste die Markierungen gesetzt werden. Um dies zu erreichen, ist gar nicht viel Aufwand erforderlich. Es mssen die Mausklicks ausgewertet und der Bildschirm gezeichnet werden. Die Fenster-

217

10

Shorts im Nebel

koordinaten mssen in Spielfeldkoordinaten umgerechnet werden und umgekehrt. Bei jedem Klick wird getestet, ob der Spieler gewonnen hat, und schon ist das Programm fertig. Nun wird wxWidgets als portable und kompakte Oberchenbibliothek eingesetzt. Wenn Sie mit graschen Oberchen vertraut sind, wird Ihnen das Lesen der Quelltexte und eine eventuelle Umsetzung in Ihre Lieblings-GUI kein Problem bereiten. Ansonsten sollten Sie ab Seite 140 eine auffhrlichere Beschreibung des Grundrahmens von wxWidgets nden. Die Headerdatei einer wxWidgets-Anwendung deklariert die Klassen fr die wxWidgets-Applikation und das Rahmenfenster. Fr eine Applikation werden eigene Klassen von diesen Klassen abgeleitet und angepasst. Die Applikationsklasse erzeugt in ihrer OnInit das Rahmenfenster. Dieses empfngt die Ereignisse und steuert das Aussehen des Programms. Die Rahmenfensterklasse verfgt in diesem Fall ber einen Konstruktor und zwei Elementfunktionen, die die beiden Ereignisse fangen, die das Spiel interessieren: die Mausaktionen und das Zeichnen des Fensterinhalts.
#ifndef _WXBERMUDA_H_ #define _WXBERMUDA_H_ class wxBermudaapp : public wxApp { public: virtual bool OnInit(); }; class wxBermudaFrame : public wxFrame { public: wxBermudaFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnMouseEvent(wxMouseEvent& event); void OnPaint(wxPaintEvent& event); private: DECLARE_EVENT_TABLE() }; #endif // _WXBERMUDA_H_
Listing 10.12 Headerdatei

Als privates Element wird die Ereignistabelle aufgefhrt, in der die Ereignisse der Oberche den behandelnden Funktionen zugeordnet wird. Mit dieser Tabelle beginnt dann auch die Implementierungsdatei. Sie behandelt die Ereignisse EVT_MOUSE_EVENTS und EVT_PAINT.

218

Nun alles im Fenster

10.6

#include <wx/wx.h> #include "wxbermuda.h" #include "feld.h" const int Distanz = 50; BEGIN_EVENT_TABLE(wxBermudaFrame, wxFrame) EVT_MOUSE_EVENTS(wxBermudaFrame::OnMouseEvent) EVT_PAINT(wxBermudaFrame::OnPaint) END_EVENT_TABLE() IMPLEMENT_APP(wxBermudaapp) bool wxBermudaapp::OnInit() { wxBermudaFrame *frame = new wxBermudaFrame( wxT("Bermuda"), wxPoint(50,50), wxSize(Distanz*(MaxX+3),Distanz*(MaxY+3))); frame->Show(true); SetTopWindow(frame); return true; }
Listing 10.13 Ereignistabelle und Applikationsklasse

Die Konstante Distanz hlt den Abstand in Pixeln zwischen den Zeilen und Spalten. Die Konstante wird zum ersten Mal bentigt, um die Gre des Fensters abzuschtzen. In der Breite wird die Anzahl der Spalten verwendet. Drei Spalten werden fr die Nummerierung und die Fensterrahmenbreite hinzugenommen. Dieser Wert wird mit der Konstanten multipliziert. hnlich wird die Hhe des Fensters abgeschtzt. Konstruktor der Rahmenfensterklasse Da das Fenster keinerlei Men oder Statuszeile enthlt, ist der Konstruktor der Rahmenfensterklasse sehr bersichtlich. Im Initialisierer wird der Konstruktor der Basisklasse aufgerufen. In der Funktion wird eine Instanz von tFeld angelegt und damit das eigentliche Spiel gestartet.
tFeld *Feld; wxBermudaFrame::wxBermudaFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)0, -1, title, pos, size)

219

10

Shorts im Nebel

{ Feld = new tFeld; }


Listing 10.14 Konstruktion des Fensters

OnPaint zeichnet das Fenster Wenn Sie die Zeichenfunktion OnPaint genauer betrachten, wird Ihnen die hnlichkeit zur Funktion zeige auffallen. Eigentlich tut sie auch nichts anderes, nur dass die Buchstaben nicht auf der Konsole ausgeworfen, sondern als Grak gezeichnet werden. Hierbei werden Fensterkoordinaten verwendet, die in Pixeln wesentlich hochausender sind als die Koordinaten des Spiels. Um die Positionen im Fenster zu nden, wird die Spielkoordinate mit der Konstaten Distanz multipliziert. Damit die Beschriftungen der Zeilen und Spalten noch Platz nden, wird zuvor ein Offset auf die Koordinate aufaddiert. Fr die Ausgabe von Text verwendet wxWidgets den String-Typ wxString. Von diesem wird eine Variable angelegt, die mit einem beliebigen Buchstaben initialisiert wird. Er wird eh sofort wieder ersetzt. Zunchst wird die Buchstabenreihe ber dem Spielfeld angezeigt. Die Buchstaben beginnen mit dem Zeichen A und werden durch Aufaddieren des Zhlschleifenindexes erhht. Der so erzeugte String wird durch die Funktion DrawText dargestellt. Die Position ergibt sich aus den Positionen, die mit der erwhnten Konstanten Distanz multipliziert werden.
void wxBermudaFrame::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); wxString str = wxT("A"); // Die Buchstabenreihe oben for (int i=0; i<MaxX; i++) { str[0] = (char)('A'+i); dc.DrawText(str, Distanz*(i+Offset), Distanz); } for (int j=0; j<MaxY; j++) { // Die Zahlenreihe links str[0] = (char)('1'+j); dc.DrawText(str, Distanz, Distanz*(j+Offset)); for(int i=0; i<MaxX; i++) { // Die Felder str[0] = Feld->getFeld(i, j); dc.DrawText(str, Distanz*(i+Offset), Distanz*(j+Offset)); }

220

Nun alles im Fenster

10.6

} }
Listing 10.15 Bildschirmaufbau

In der verschachtelten Schleife werden einfach die Zeichen, die im Spielfeld gespeichert sind, ausgegeben. Der Umgang mit der Maus Die Umrechnung der Spielkoordinaten durch Einrckung des Offsets und Multiplikation mit der Konstanten Distanz muss bei der Bestimmung der Mausposition umgekehrt werden, um die richtigen Spielpositionen aus den Fensterkoordinaten ermitteln zu knnen. Die Mausereignisse werden gefangen und bestimmen den Spielablauf. In dem Parameter event empfngt die Ereignisfunktion OnMouseEvent unter anderem die Information, wo sich die Maus auf dem Monitor bendet. Diese Koordinaten werden durch die Funktion GetLogicalPosition in Fensterkoordinaten umgerechnet. Der Wert bendet sich anschlieend in der Variablen pt. Liegt ein Mausklick der linken Taste vor, wird die Fensterkoordinate durch die Distanz geteilt und die Einrckung wieder abgezogen. Damit ist die Spielposition ermittelt. Es wird noch geprft, ob sich das Ergebnis korrekte Spielkoordinaten sind, und dann wird die Zhlung der Peilungen veranlasst. Der Bildschirm wird durch den Aufruf von Refresh neu aufgebaut. Anschlieend wird geprft, ob alle Schiffe gefunden wurden, um eine entsprechende Meldung auszugeben.
void wxBermudaFrame::OnMouseEvent(wxMouseEvent& event) { wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc)); if (event.LeftDown()) { int x = pt.x/Distanz - Offset; int y = pt.y/Distanz - Offset; if (x>=0 && y>=0 && x<MaxX && y<MaxY) { Feld->zaehlePeilungen(x, y); Refresh(); if (Feld->istGewonnen()) { wxMessageBox(wxT("Gewonnen"), wxT("Super"), wxOK | wxICON_INFORMATION, this); delete Feld; Feld = new tFeld; Refresh(); } }

221

10

Shorts im Nebel

} else if (event.RightDown()) { int x = pt.x/Distanz - Offset; int y = pt.y/Distanz - Offset; if (x>=0 && y>=0 && x<MaxX && y<MaxY) { Feld->markiere(x, y); Refresh(); } } }
Listing 10.16 Behandlung der Mausereignisse

Im Falle der rechten Maustaste wird ebenfalls umgerechnet und geprft und dann die Markierungsfunktion aufgerufen. Auch hier sorgt der Aufruf der Funktion Refresh fr ein Neuzeichnen des Fensters. Das Ergebnis dieses Programms ist in Abbildung 10.2 zu sehen.

Abbildung 10.2 Bermuda mit Fenster und Maus

222

Grak kann mehr

10.7

Die Umstellung auf die grasche Oberche ist mit zwei DIN-A4-Seiten Quelltext kein besonders groer Aufwand. Dafr wurde die Bedienung erheblich verbessert. Allerdings wirkt das Programm noch nicht sehr beeindruckend.

10.7

Grak kann mehr

Von einem Spiel in einer graschen Oberche erwartet man etwas mehr, als dass Buchstaben und Zahlen erscheinen. Bei Bermuda sollten die gefundenen Schiffe auch wie Schiffe aussehen. Die Zahlen knnte man beispielsweise durch leuchtende Punkte ersetzen. Bei bis zu vier Schiffen ist das auch vllig in Ordnung. Allerdings wird das Abzhlen der Punkte schwierig, wenn es mehr Schiffe werden. Wenn der Platz klein genug ist, sind sechs von sieben Punkten kaum noch zu unterscheiden, und der Spieler wnscht sich verzweifelt die Zahlen wieder her. Eine Markierung knnte als Fahne oder als kleiner Punkt dargestellt werden. Die Fahne wrde eher ein Signal dafr sein, dass hier etwas versteckt liegt. Eine Markierung sagt bei Bermuda genau das Gegenteil aus. Die Wahl der Symbole sollte also gut durchdacht sein. Fr die Darstellungen der Schiffe und Ziffern mssen kleine Bilder erzeugt werden, die dann im Programm an die passende Stelle gezeichnet werden. Dazu bentigt der Programmierer ein kleines Malprogramm. Auf jeden Fall sollten Linien gezogen werden, die die einzelnen Punkte in den Himmelsrichtungen und in den Diagonalen verbinden. Das erhht die bersicht deutlich. Bisher erfordern die Anforderungen ein sehr statisches Bild. Das ist fr ein Knobelspiel auch nicht vllig unangemessen. Dennoch wrde etwa Bewegung dem Ganzen nicht schaden. Das erzhlt mir mein Arzt auch laufend. Allerdings vermute ich, dass er etwas anderes meint. Wir haben nmlich die Verabredung, dass ich seine Diagnosen nicht hinterfrage und er dafr meine Programme in Ruhe lsst. Eine Chance, dem Spiel etwas mehr Lebendigkeit zu verleihen, bietet sich, wenn der Spieler eine neue Position anwhlt. Hier knnte das Programm ein wenig Theater machen. So knnte beispielsweise eine Schssel aus dem Feld entspringen und einmal in die Runde peilen. Das sieht sicher schick aus und erhht die Spannung.

223

10

Shorts im Nebel

10.7.1

Grundaufbau des Feldes

Die Spielpositionen sollten mit Linien verbunden werden, damit der Spieler leichter sehen kann, welche Felder angepeilt werden. Dazu wird zunchst eine Reihe senkrechter und eine Reihe waagerechter Linien gezogen. Damit die Linien aber auch etwas hergeben, sollen sie ein wenig dreidimensional wirken. Das erreicht man dadurch, dass sie in der Mitte etwas heller als am Rand sind. Dies gelingt auf einfache Weise, indem drei Linien mit unterschiedlicher Strke bereinander gezeichnet werden. Zuerst zieht das Programm einen dicken Strich in Blau, dann einen etwas schmaleren Strich in Grn und zuletzt eine dnne, weie Linie.
for (int yi=0; yi<MaxY; yi++) { dc.SetPen(wxPen(*wxBLACK, 8)); dc.DrawLine(Distanz*Offset, Distanz*(yi+Offset), Distanz*(1+MaxX), Distanz*(yi+Offset)); dc.SetPen(wxPen(*wxBLUE, 6)); dc.DrawLine(Distanz*Offset, Distanz*(yi+Offset), Distanz*(1+MaxX), Distanz*(yi+Offset)); dc.SetPen(wxPen(*wxWHITE, 1)); dc.DrawLine(Distanz*Offset, Distanz*(yi+Offset), Distanz*(1+MaxX), Distanz*(yi+Offset)); } for (int xi=0; xi<MaxX; xi++) { dc.SetPen(wxPen(*wxBLUE, 8)); dc.DrawLine(Distanz*(xi+Offset), Distanz*(Offset), Distanz*(xi+Offset), Distanz*(MaxY+1)); dc.SetPen(wxPen(*wxWHITE, Offset)); dc.DrawLine(Distanz*(xi+Offset), Distanz*(Offset), Distanz*(xi+Offset), Distanz*(MaxY+1)); }
Listing 10.17 Rasteraufbau

Mit je einer Schleife fr die waagerechten und die senkrechten Linien ist der Aufbau reine Routine. Die senkrechten Linien werden nur mit den Farben Blau und Wei erzeugt. Dadurch unterscheiden sie sich ein wenig von den waagerechten Strichen und wirken etwas realistischer. Fr die Diagonalen wird sogar noch in den Topf mit der grnen Farbe gegriffen.

10.7.2

Die Diagonalen als Knobelspiel

Es gibt Programmieraufgaben, die hren sich simpel an, entpuppen sich aber schnell als echte Knobelaufgaben. Dazu gehrt das Zeichnen der Diagonalen in

224

Grak kann mehr

10.7

einem nichtquadratischen Rechteck. Die Schleife fr die waagerechten oder senkrechten Linien ist recht einfach. Aber bei der Diagonale reicht es nicht, die Koordinaten einfach durchzuzhlen. Beginnt man am Punkt links oben, zhlt die XKoordinate hoch und zieht die Diagonalen nach rechts unten, so muss sptestens bei Erreichen der unteren rechten Ecke nicht nur die X-Koordinate, sondern auch die Y-Koordinate des Zielpunktes angepasst werden. Anschlieend muss dann noch einmal eine Schleife geschrieben werden, die diese Diagonalen im linken Bereich des Rechtecks zeichnet. Das hiee, dass fr das Zeichnen der beiden Diagonalen insgesamt vier Schleifen bentigt wrden. Es muss eine andere Lsung geben. Es muss mglich sein, in einer Schleife alle Diagonalen einer Richtung zu zeichnen. Aber wo startet sie, wie lang zhlt sie und wie errechnet man die Koordinaten der Quell- und Zielpunkte? Ich hatte das Programmieren just an dieser Stelle unterbrechen mssen, weil wir zu einer Party eingeladen waren, die sich auch nicht einfach wegen eines anstehenden Programmierproblems schnell mal absagen lie. Ich musste die Lsung des Problems tatschlich verschieben. Irgendwann im Laufe des Abends schaute mich meine Frau an und fragte: Wo bist denn wieder mit Deinen Gedanken?. Ich antwortete ganz ehrlich: Ich denke gerade darber nach, wie man Diagonalen durch ein nichtquadratisches Rechteck zieht. In dem Moment, als ich es aussprach, war mir klar, dass es ein Fehler war. Und der Blick in die Gesichter meiner Tischnachbarn offenbarte mir, dass es hier wohl niemanden gab, der gern mit mir ein wenig denken wollte. Im Gegenteil musste ich eher annehmen, dass meiner Frau das Mitgefhl der ganzen Gesellschaft sicher war. Wenn Sie also wirklich Freude am Knobeln haben, versuchen Sie doch selbst eine Lsung zu nden, bevor Sie weiterlesen. Die Aufgabe lautet: Zeichnen Sie in einer Schleife alle Diagonalen innerhalb eines nichtquadratischen Rechtecks. Ein Tipp: Die Diagonalen von links unten nach rechts oben sind einfacher. Auflsung Beginnen wir mit den Diagonalen von links unten nach rechts oben und betrachten zunchst die erste Diagonale in der linken oberen Ecke. Diese Diagonale geht von der Koordinate 1,0 (oben) zu der Koordinate 0,1 (links). Die nchste Diagonale fhrt analog von 2,0 nach 0,2. Damit steht zu vermuten, dass die Lsung etwa dieses Aussehen hat:
for (int i=0; i<... { DrawLine(i,0, 0,i); }

Das Ende der Schleife lasse ich hier noch offen. DrawLine hat als Parameter die Anfangs- und die Endkoordinaten jeweils in x-y-Reihenfolge.

225

10

Shorts im Nebel

Spannend wird es, sobald die Zhlvariable die untere Ecke erreicht. Beim Spiel Bermuda ist das in der siebten Zeile, also bei der Diagonalen von 6,0 nach 0,6. Die nchste Diagonale fhrt dann von 7,0 nach 1,6. Danach folgt 8,0 nach 2,6. Dann ist die Ecke an der rechten oberen Ecke erreicht, und die Startkoordinate verndert sich analog nach 8,1. Die dazugehrige Gegenkoordinate ist 3,6. Letztlich luft die Schleife um die Ecke, und damit ist auch das Ende der Schleife bekannt. Die Laufvariable wird bis zur Summe der beiden Maximalkoordinaten fr x und y hochgezhlt.

Abbildung 10.3 Diagonalen zeichnen

Die anderen Diagonalen werden hnlich berechnet. Dabei muss allerdings die eine von beiden Koordinaten von oben nach unten gezhlt werden. Das bedeutet, dass der Wert des Index von der maximal mglichen Koordinate abgezogen wird. Ansonsten werden auch hier die Koordinaten um die Ecke gefhrt. Im Programm werden beide Diagonalen in der gleichen Schleife gezeichnet.
for (int i=1; i<MaxX+MaxY-2; i++) { int xa = i; int ya = 0; if (i>=MaxX) { xa=MaxX-1; ya=i-MaxX+1; } int xe = 0; int ye = i; if (i>=MaxY) { ye=MaxY-1; xe=i-MaxY+1; } dc.SetPen(wxPen(*wxBLUE, 8)); dc.DrawLine(Distanz*(xa+Offset), Distanz*(ya+Offset), Distanz*(xe+Offset), Distanz*(ye+Offset)); dc.SetPen(wxPen(*wxGREEN, 4));

226

Grak kann mehr

10.7

dc.DrawLine(Distanz*(xa+Offset), Distanz*(xe+Offset), dc.SetPen(wxPen(*wxWHITE, 1)); dc.DrawLine(Distanz*(xa+Offset), Distanz*(xe+Offset), xa=0; ya=MaxY-i-1; if (ya<0) { xa = -ya; ya = 0; } xe = i; ye = MaxY-1; if (i>=MaxX) { ye -= i - MaxX + 1; xe = MaxX-1; } dc.SetPen(wxPen(*wxBLUE, 8)); dc.DrawLine(Distanz*(xa+Offset), Distanz*(xe+Offset), dc.SetPen(wxPen(*wxGREEN, 4)); dc.DrawLine(Distanz*(xa+Offset), Distanz*(xe+Offset), dc.SetPen(wxPen(*wxWHITE, 2)); dc.DrawLine(Distanz*(xa+Offset), Distanz*(xe+Offset), }
Listing 10.18 Zeichnen der Diagonalen

Distanz*(ya+Offset), Distanz*(ye+Offset)); Distanz*(ya+Offset), Distanz*(ye+Offset));

Distanz*(ya+Offset), Distanz*(ye+Offset)); Distanz*(ya+Offset), Distanz*(ye+Offset)); Distanz*(ya+Offset), Distanz*(ye+Offset));

10.7.3

Przise Maussteuerung

Bisher hatte das Programm nicht so genau auf die Maus geachtet. Wenn der Spieler irgendwo zwischen zwei Koordinaten hinklickte, wurde eines der beiden Felder ausgewhlt. Aus Sicht des Spielers konnten da einige berraschungen auftreten. Und berraschungen sollte man bei Programmen immer vermeiden. Es wre sehr elegant, wenn der Mausklick nur dann mglich wre, wenn sich der Mauszeiger ber dem Kstchen der Spielkoordinate bewegt. Damit der Anwender versteht, wann sein Klick akzeptiert wird, ndert das Programm die Form des Mauszeigers. An den Stellen, wo geklickt werden kann, erscheint statt des Pfeils ein Zielkreuz.

227

10

Shorts im Nebel

Ein Programm kann unterschiedliche Mausereignisse fangen. So ist es mglich, nur auf Klicks zu reagieren. Die Funktion OnMouseEvent ist in weiser Voraussicht so angemeldet, dass sie alle Mausereignisse empfngt. So kann sie auch gleich fr die nderung des Mauszeigers sorgen.
void wxBermudaFrame::OnMouseEvent(wxMouseEvent& event) { wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc)); int x = (pt.x+Kasten/2)/Distanz - Offset; int y = (pt.y+Kasten/2)/Distanz - Offset; SetCursor(wxCURSOR_ARROW); if (x>=0 && y>=0 && x<MaxX && y<MaxY) { // horizontal im Kasten? if (pt.x-(x+Offset)*Distanz<Kasten/2 && pt.y-(y+Offset)*Distanz<Kasten/2) { SetCursor(wxCURSOR_CROSS);
Listing 10.19 Mausposition ermitteln

Das Mausereignis enthlt die Mausposition bezogen auf den kompletten Bildschirm. Das hilft dem Programm aber nicht weiter. Es braucht die Mausposition bezogen auf den eigenen Fensterinhalt. Darum wird zunchst ein Device Context des Fensterinhalts, der bei graschen Oberchen Client genannt wird, ermittelt. Mit dessen Hilfe kann aus der absoluten Bildschirmmausposition ber die Funktion GetLogicalPosition die Mauskoordinate ermittelt werden, die sich auf den Fensterinhalt bezieht. Nun wird aus der Mauskoordinate die logische Position ermittelt, indem die Koordinaten durch die Konstante Distanz geteilt, und die Hlfte der Kastenbreite aufgerechnet wird. Damit ist die Grenze zwischen den Positionen der linke Rand des Kstchens. Die nchste logische Position wird beim Erreichen der nchsten linken Kante erreicht. Der Mauszeiger wird erst einmal auf wxCURSOR_ARROW umgeschaltet. Das schadet nicht, das sollte der Normalzustand sein. War er aber vorher auf Hand geschaltet, ist er nun erst einmal ein Pfeil. In der ersten Abfrage wird geprft, ob sie die logische Position berhaupt im Spielfeld bendet. Dann erfolgt die Prfung, ob der Zeiger im Kasten ist. Dazu wird die logische Position wieder in eine grasche Koordinate zurckgerechnet. Diese Koordinate wird von der ursprnglichen Koordinate abgezogen. Das Ergebnis muss kleiner als die Hlfte des Kastens sein. Warum die Hlfte? Weil bei der Umrechnung in die logische Position bereits einmal die Hlfte abgerechnet wurde.

228

Grak kann mehr

10.7

Jetzt, wo sicher ist, dass der Mauszeiger im Kasten ist, kann sein Erscheinungsbild auf das Kreuz umgeschaltet werden. So weit haben wir nun die Position geklrt und knnen das Klickereignis behandeln. Wenn nun ein Mausklick eintrifft, wird die logische Position in den globalen Variablen PosX und PosY abgelegt. Auerdem wird die Variable Phase mit 1 vorbelegt. Diese sagt aus, dass die rotierende Schssel rotieren und mit der Richtung 1 beginnen soll. Damit die Bewegung startet, wird der Timer aktiviert.
if (event.LeftDown()) { PosX = x; PosY = y; Phase = 1; timer.Start(TIMERTICK); int Ergebnis = Feld->zaehlePeilungen(x, y); if (Ergebnis==0) { Feld->markiereNull(x, y); } Refresh(); if (Ergebnis==SCHIFF && Feld->istGewonnen()) { wxMessageBox(wxT("Gewonnen"), wxT("Super"), wxOK | wxICON_INFORMATION, this); delete Feld; Feld = new tFeld; Refresh(); } } else if (event.RightDown()) { Feld->markiere(x, y); Refresh(); } } } }
Listing 10.20 Mausposition ermitteln

Nachdem die Aktivitten der Peilschssel ausgelst wurden, wird die Anzahl der Peilungen bestimmt. Wurde kein Schiff in irgendeiner Richtung gesichtet, wird die Nullmarkierung ausgelst. Wurde ein Schiff direkt getroffen, wird geprft, ob vielleicht schon alle Schiffe gefunden wurden. In diesem Fall ist es Zeit fr eine Siegerehrung. Danach kann das ganze Spiel eingestampft und neu erzeugt werden, damit die nchste Runde beginnt. Der Aufruf von Refresh sorgt schlieliche fr einen Neuaufbau des Fensters.

229

10

Shorts im Nebel

Falls Sie sich fragen, warum der Timer nicht gestoppt wird, machen Sie sich keine Sorgen. Er wird nicht hier gestoppt, sondern durch die Ereignisfunktion des Timers, wenn alle Richtungen abgeklappert wurden. Wurde die rechte Maustaste gedrckt, ist der Aufwand nicht so hoch. Die Position wird markiert, und die Fensterneuzeicnung wird durch die Funktion Refresh ausgelst.

10.7.4 Men mit Haken


Wenn das Programm schon ordentlich gemacht wird, gehren auch Men und Statusleiste hinzu. Das Men wird fr den eleganten Ausstieg vorbereitet und um die natrliche Neugier des Anwenders zu befriedigen, welchem begabten Knner er diese Perle der Programmierkunst zu verdanken hat. Vor allem kann ber das Men die automatische Markierung abgeschaltet werden, die ausgelst wird, wenn eine Peilung keine Schiffe sieht. Dazu verwendet das Programm einen Meneintrag, der mit einem Haken versehen werden kann. Wie der grundstzliche Menaufbau funktioniert, wurde schon an anderer Stelle im Buch beschrieben. Sie nden ab Seite 144 eine ausfhrliche Erklrung. An dieser Stelle wird der Menpunkt mit einem Haken beschrieben werden. Er unterscheidet sich von einem normalen Menpunkt nur darin, dass er anders eingehngt wird. Normale Menpunkte werden mit der Funktion Append in das Men eingehngt. Bei einem Hakeneintrag muss die Sonderform AppendCheckItem verwendet werden. Die Funktion liefert einen Zeiger vom Typ wxMenuItem zurck. Dieser ist fr das Programm wertvoll, um spter den Zustand des Hakens abzufragen.
wxMenu *menuFile = new wxMenu; pMenuItemMark = menuFile->AppendCheckItem(MenuMark, wxT("&Auto-Markierung")); menuFile->Append(MenuAbout, wxT("&Info..."));
Listing 10.21 Menaufbau

Der Haken wird vom System automatisch gesetzt und gelscht, wenn der Punkt angeklickt wird. Es ist also gar nicht unbedingt erforderlich, eine Funktion zu implementieren und zu aktivieren, die auf das Anklicken dieses Menpunkts reagiert. Eine Behandlung durch das Programm wre nur dann sinnvoll, wenn mit der Zustandsnderung auch sofort Aktionen im Programm erforderlich wren. Das Programm kann ber den Meneintragszeiger einfach den Zustand des Menhakens ber die Funktion IsChecked ablesen.

230

Grak kann mehr

10.7

if (Ergebnis==0 && pMenuItemMark->IsChecked()) { Feld->markiereNull(x, y); }


Listing 10.22 Abfrage der Menmarkierung

In diesem Fall wird erst geprft, ob das Ergebnis 0 ist. Nur wenn auch der Haken an dem Menpunkt fr die automatische Markierung gesetzt ist, wird die Funktion markiereNull aufgerufen.

10.7.5 Image ndern


Natrlich erwartet der Spieler bei einer graschen Oberche, dass die gefundenen Schiffe nicht durch Sternchen simuliert werden. Darum sind die Figuren das Salz in der Suppe eines gelungenen Spiels. Die Ziffern werden in dem Spiel durch die Bilder von kleinen Leuchtdioden ersetzt. Als Markierungen werden Kreise verwendet, und ein gefundenes Schiff wird durch den Umriss eines Schiffes angezeigt. Die Zahlen als LEDs sind eine Anlehnung an das Originalspiel, in dem die Treffer auch durch Dioden angezeigt wurden. Der Haken an dieser Lsung ist, dass diese Darstellung mit vier Schiffen noch halbwegs bersichtlich ist. Wenn allerdings mehr als vier Richtungen angezeigt werden sollen, msste nach einer Alternative gesucht werden. Die Erzeugung der Bilder kann unter Windows mit Einschrnkungen sogar mit dem Programm Paint erfolgen. Wenn Sie allerdings transparente Hintergrnde brauchen, sollten Sie beispielsweise GIMP verwenden, das bei jeder Linux-Distribution standardmig mitinstalliert wird. Sie nden es aber auch fr Macintosh oder Windows im Internet zum Download: http://www.gimp.org Zum Erstellen eines Images legen Sie eine neue Datei ber das Men mit Datei Neu... an. GIMP fragt Sie nach dem Aussehen der Datei, insbesondere nach der Gre des Bildes. Bei Bermuda sind die Rechtecke, auf denen sich das pralle Leben abspielt, 40 auf 40 Pixel gro. Also geben Sie je 40 Pixel fr Breite und Hhe an. Damit Sie das nicht einmal briefmarkengroe Bild auch bearbeiten knnen, empelt sich eine Vergrerung ber Ansicht Vergrerung 4:1. Im einem separaten Werkzeugfenster sollten Sie den Stift einstellen. Unter dem Stiftmen nden Sie die Stiftstrke unter dem Begriff Pinsel. Bei so kleinen Objekten sollte dieser besser einen statt 11 Pixel gro sein, wie es die Vorgabe anbietet. Durch Anklicken auf die Farbchen fr Vorder- und Hintergrundfarben knnen Sie auch andere Farben als Schwarz und Wei auswhlen.

231

10

Shorts im Nebel

Bevor Sie allerdings beginnen, sollten Sie berlegen, ob Sie einen transparenten Hintergrund bentigen. In diesem Fall sollten Sie zunchst Farben Farbe zu Transparenz... anklicken. In der erscheinenden Dialogbox wird Ihnen angeboten, die Farbe Wei zu Transparenz umzuwandeln. Da Ihr gesamtes Bild derzeit wei ist, htten Sie dann einen durchgngig transparenten Hintergrund, auf dem Sie dann Ihre Bilder malen knnen. Der letzte wichtige Punkt ist das Speichern des Bildes. ber den Menpunkt Datei Speichern unter... erreichen Sie einen Dialog, in dem Sie den Dateinamen angeben knnen. GIMP erkennt an der Extension, welchen Dateityp Sie verwenden wollen. Wenn Sie die Transparenz behalten wollen, sollten Sie GIF verwenden, Ihr Bild also beispielsweise bild.gif nennen. Je nach Dateityp kommt eine Nachfrage, ob die Vorlage in dieses Format umgewandelt werden soll. Hier mssen Sie den Button Export anklicken. Neben den statischen Figuren gibt es bei Bermuda noch eine rotierende Schssel, die die Peilung animiert. Dafr werden acht Bilder bentigt, die unter GIMP sehr schnell durch Rotieren des Ausgangsbildes erzeugt werden knnen. Auf der beiliegenden CD nden Sie natrlich alle fr die Programme bentigten Bilder vor. Es steht Ihnen aber auch frei, Ihre eigenen Bilder zu gestalten. Achten Sie allerdings darauf, dass die Bilder die gleichen Gren haben. Ansonsten knnten die Ergebnisse etwas schief aussehen.

10.7.6 Spielergebnisse zeigen


Natrlich mssen auch die Spielergebnisse zu sehen sein. Wie schon bei der Ausgabe auf der Konsole durch die Funktion zeige werden zwei verschachtelte Schleifen aufgebaut. Innerhalb dieser Schleifen werden die Ksten mit den bisher ausgefhrten Peilungen und den Markierungen gefllt. Zunchst wird jeder Kasten wei gestrichen. Dann wird ermittelt, welches Ergebnis fr dieses Feld vorliegt. Dabei handelt es sich um das Zeichen, wie es in der Konsolenversion auch verwendet wird. Ein Zeichen ist aber nicht unpraktisch, weil man darber ein switch aufbauen kann, der dann von Fall zu Fall unterscheidet, was gezeichnet werden soll.
dc.SetBrush(wxBrush(*wxWHITE)); dc.SetPen(wxPen(*wxBLACK, 1)); for (int j=0; j<MaxY; j++) { for(int i=0; i<MaxX; i++) { // male zunchst den Hintergrund weiss wxRect rect(wxPoint(Distanz*(i+Offset)-Kasten/2, Distanz*(j+Offset)-Kasten/2), wxSize(Kasten, Kasten));

232

Grak kann mehr

10.7

dc.DrawRectangle(rect); // ermittle den Inhalt und verzweige danach char Zeichen = Feld->getFeld(i, j); int index; wxString str = wxT("A"); switch (Zeichen) { case '*': dc.DrawBitmap(imgSchiff, Distanz*(i+Offset)-Kasten/2, Distanz*(j+Offset)-Kasten/2, true); break; case '.': dc.DrawCircle(Distanz*(i+Offset), //-Kasten/2, Distanz*(j+Offset), Kasten/4); break; case '+': break; // lass es einfach so case '0': case '1': case '2': case '3': case '4': index = Zeichen-'0'; dc.DrawBitmap(imgFinde[index], Distanz*(i+Offset)-Kasten/2, Distanz*(j+Offset)-Kasten/2, true); break; default: str[0] = Zeichen; dc.DrawText(str, Distanz*(i+Offset), Distanz*(j+Offset)); } } }
Listing 10.23 Spielergebnisse anzeigen

Hier wird unterschieden, ob es ein Stern und damit ein Schiff ist. Das wird dann gezeichnet. Eine Markierung wird einfach als kleinerer Kreis dargestellt. Das trgt nicht auf, ist aber dennoch sofort zu erkennen. Statt eines Pluszeichens wird die Flche einfach wei gelassen. Die Ziffern von 0 bis 4 werden durch Symbole dargestellt. Dabei handelt es sich um vier kleine LEDs, die entweder rot leuchten oder schwarz sind. Sollten sonstige Zeichen vorliegen, werden sie einfach in den Kasten gezeichnet. Das betrifft derzeit nur die Ziffern von 5 bis 8, die auftreten knnten, wenn die Anzahl der Schiffe vergrert wrden, ohne daran zu denken, auch Symbole fr die hheren Ziffern zu schaffen.

233

10

Shorts im Nebel

10.7.7

Die Peilung rotiert

Nun kommt Bewegung in das Spiel. Wenn der Anwender auf ein Feld klickt, erscheint eine Peilschssel, die sich in alle acht Richtungen dreht und jeweils einen roten Strahl in diese Richtung bis zum Rand des Spielfelds sendet. Unter Kontrolle des Timers Beim Klick auf ein Feld wird der Timer gestartet. Dieser steuert nun den Ablauf der rotierenden Peilung. Die globale Variable Phase wird bei jedem Timertick erhht, und ein Refresh wird ausgelst. So kommt die Funktion OnPaint zum Zug, die die nchste Phase darstellt.
void wxBermudaFrame::OnTimer(wxTimerEvent& event) { if (Phase>0 && Phase<8) { Phase++; } else { Phase=0; timer.Stop(); } Refresh(); // Erzwinge Aufruf von OnPaint }
Listing 10.24 OnTimer

Sind alle acht Phasen durchlaufen, wird der Timer wieder gestoppt. Ein letztes Mal wird die Funktion Refresh aufgerufen, die dafr sorgt, dass die Spuren des rotierenden Strahls und die Schssel wieder durch den normalen Fensteraufbau ersetzt wird. Die nchste Bewegung wird erst in Betrieb genommen, sobald wieder ein Klick erfolgt und dadurch der Timer aktiviert wird. Linien ziehen und Schsseln rotieren Die Darstellung der Rotation erfolgt durch die Funktion OnPaint. Nachdem Sie das Fenster vollstndig gezeichnet hat, fragt sie die Variable Phase ab, um festzustellen, ob es sich nur um den Wiederaufbau des Fensters handelt, oder ob auch eine Phase der Peilung dargestellt werden soll. Zuerst wird der Peilstrahl gezeichnet. Dieser geht von der gewhlten Position bis zum Rand. Das Wandern zum Rand erfolgt auf bewhrte Weise durch Addition der Differenzwerte, bis ein Wert den Rand erreicht. Bei jeder Phase wird in eine andere Richtung gezogen. Die Linien knnen einfach ber das existierende Bild gezogen werden, denn beim nchsten Timer-Impuls wird ja zunchst das Fensterbild neu aufgebaut, und dadurch verschwinden die Linien wieder.

234

Grak kann mehr

10.7

int Richtung[] = { 0,-1, 0, 1,

1,-1, -1, 1,

1,0, -1,0,

1, 1, -1,-1 };

void wxBermudaFrame::OnPaint(wxPaintEvent& event) { // Hier wird das Fenster neu gezeichnet .... if (Phase) { // berechne Position links unten int x = Distanz*(PosX+Offset)-Kasten/2; int y = Distanz*(PosY+Offset)-Kasten/2; // zeichne den Kasten dc.SetBrush(wxBrush(*wxWHITE)); dc.SetPen(wxPen(*wxBLACK, 1)); wxRect rect(wxPoint(x, y), wxSize(Kasten, Kasten)); dc.DrawRectangle(rect); // zeichne den Laser dc.SetPen(wxPen(*wxRED, 4)); int xz = PosX; int yz = PosY; // Die Grenzen zu weit, damit auch Randsteine eine // Peilung aufnehmen while (xz>=0 && xz <MaxX && yz>=0 && yz < MaxY) { xz += Richtung[Phase*2-2]; yz += Richtung[Phase*2-1]; } // Nun die ueberzaehlige Peilung zuruecknehmen xz -= Richtung[Phase*2-2]; yz -= Richtung[Phase*2-1]; // Peilung zeichnen dc.DrawLine(x+Kasten/2, y+Kasten/2, Distanz*(xz+Offset), Distanz*(yz+Offset)); // Schuessel zeichnen dc.DrawBitmap(imgSat[Phase-1], x, y, true); } }
Listing 10.25 Linie bis zum Rand

Wenn die Variable Phase gesetzt worden ist, kann die gewhlte Position aus den Variablen PosX und PosY gelesen werden. Der Kasten wird von eventuellen Resten der letzten Zeichnung gesubert. Das Array Richtung enthlt paarweise die Richtungen fr jede Phase. Diese Richtungen werden auf die aktuelle Position aufaddiert, bis der Rand berschritten wurde. Wrde direkt auf das Erreichen des Randes abgefragt, wrde die Rotation

235

10

Shorts im Nebel

nicht durchgefhrt, wenn auf ein Randelement geklickt wird. Dieses berschreiten wird durch anschlieendes Abziehen wieder korrigiert. Nun sind die Koordinaten bekannt, und es kann eine Linie von der angeklickten Position und dem zugehrigen Rand gezogen werden. Direkt anschlieend wird die Schssel in der entsprechenden Richtung gezeigt. Dadurch entsteht der Eindruck, dass der Strahl aus der Schssel kommt.

10.7.8 Flackernden Neuaufbau unterbinden


Die schnellen Aufbauten whrend des Peilens knnten zu ackernden Effekten fhren. Dies wird durch verschiedene Manahmen unterbunden. Zunchst wird verhindert, dass das System den Fensterhintergrund neu aufbaut. Dazu wird das Ereignis EVT_ERASE_BACKGROUND auf eine eigene Funktion umgeleitet, die dann einfach gar nichts tut.
#include <wx/dcbuffer.h> BEGIN_EVENT_TABLE(wxBermudaFrame, wxFrame) ... EVT_ERASE_BACKGROUND(wxBermudaFrame::OnEraseBackground) END_EVENT_TABLE() void wxBermudaFrame::OnEraseBackground(wxEraseEvent& event) { }
Listing 10.26 Flackerunterdrckung

Als Ausgleich dazu muss dann die Zeichnungsfunktion den Hintergrund selbst zeichnen. Dazu wird einfach ein chendeckendes Rechteck in Hintergrundfarbe gezeichnet.
void wxBermudaFrame::OnPaint(wxPaintEvent& event) { wxBufferedPaintDC dc(this); wxRect rect(wxPoint(0, 0), GetClientSize()); wxColour back = GetBackgroundColour(); dc.SetBrush(wxBrush(back)); dc.SetPen(wxPen(back, 1)); dc.DrawRectangle(rect);
Listing 10.27 Flackerunterdrckung

236

Portabilitt

10.8

Die eigentliche Verhinderung des Flackerns ist bei wxWidgets sehr einfach, indem eine spezielle Variable vom Typ wxBufferedPaintDC als Device Context verwendet wird. Sie sorgt dafr, dass in einen Hintergrundspeicher gezeichnet wird und das Bild erst nach dessen Fertigstellung der erfolgreichen Zeichnung mit einem Schlag auf den Bildschirm gegeben wird. Zum Abschluss zeigt die Abbildung 10.4 einen Screenshot von dem Programm, der geschossen wurde, whrend die Peilschssel rotiert.

Abbildung 10.4 Bermuda mit rotierender Peilung

10.8

Portabilitt

Falls der Eindruck entstanden sein sollte, dass es ganz einfach ist, fr mehrere Plattformen zu entwickeln, dann mchte ich Sie an dieser Stelle desillusionieren. Die Programme habe ich unter Linux entwickelt und dann noch einmal unter Windows gegengetestet. In einigen Fllen hat das auch auf Anhieb geklappt.

237

10

Shorts im Nebel

Das ist aber nicht immer so. Und die Probleme ergeben sich manchmal an den Stellen, wo man sie nicht erwartet. So htte ich nicht damit gerechnet, dass der harmlose Aufruf von sleep bei der Mondlandung unter Windows ein Problem sein knnte. Ich bin recht sicher, dass das vor zehn Jahren noch wunderbar klappte. Die wxWidgets-Bibliothek ist tatschlich erstaunlich portabel. So war es etwas berraschend, dass unter Linux Grakdateien im Format BMP mit transparentem Hintergrund liefen, unter Windows war die Transparenz dahin. Die JPEG-Bilder, die Linux problemlos darstellte, konnten unter Windows nur mit einem ImageHandler geladen werden. Und wenn beim Aufruf von LoadFile kein zweiter Parameter auf den Dateityp hinwies, bereitete das unter Linux kein Problem. Die Windows-Version erwartete dann allerdings die Bilder nicht in einer Datei, sondern in der Ressource. Unter Linux war eine Flackerverhinderung nicht notwendig, sodass ich sie weglie. Nach dem Start der Windows-Version wusste ich, wofr sie da war. Das Drehen des Peilstrahls hatte ich in der ursprnglichen Version so gebaut, dass die Fensteraufbau-Funktion entweder den Peilstrahl zeichnete oder das Fenster neu aufbaute. So blieb jeder Strahl stehen, bis der letzte Strahl gezeichnet war. Das sah toll aus. Leider funktionierte das unter Windows nicht. Das Spielfeld verschwand fr die Zeit der Rotation und kam danach erst wieder. Ich ersetzte den gepufferten Device Context durch einen normalen und siehe da: es funktionierte auch unter Windows. Aber mit dem ungepufferten Device Context passierte der gleiche Effekt nun unter Linux. Schlielich baute ich die Funktion OnPaint so um, wie sie jetzt ist. Und das funktioniert unter beiden Oberchen. Ich will nicht den Eindruck erwecken, unter Linux liefe alles und unter Windows geht alles schief. Das ist mir so gegangen, weil ich mit Linux begonnen habe. Htte ich unter Windows entwickelt und es dann auf Linux portiert, wre ich vielleicht auf andere Probleme gestoen, und der Eindruck wre genau anders herum entstanden. Warum erzhle ich Ihnen das alles? Weil ich ein mitteilsamer Mensch bin. Darum schreibe ich auch Bcher. Der andere Grund ist, dass Sie vielleicht an diesen Beispielen erkennen, wo Sie mit Portabilittsproblemen rechnen mssen: berall da, wo die Systeme Interpretationsfreirume haben knnten. Das ist leider an sehr vielen Stellen, da nicht alles verbindlich standardisiert sein kann. So hat die Verwendung von transparenten GIF-Dateien auf beiden Plattformen auf Anhieb geklappt, weil diese Eigenschaft bei GIF eindeutig deniert ist. Die andere wichtige Erfahrung besagt, dass Sie nie sicher sein knnen, dass ein Programm wirklich portabel ist, bevor Sie es nicht ausprobiert haben. Und was

238

Was denn noch?

10.9

Sie nicht ausprobiert haben, wird sicher beim Kunden gegen die Wand fahren. Seien Sie tapfer! Falls Sie also nun auf eine Inkompatibilitt stoen, die ich bersehen habe, seien Sie gndig mit mir! Sie drfen gerne uchen. Aber dann sollten Sie sich wieder beruhigen und im Forum des Buches schreiben, wo ich nicht aufgepasst habe. Und wenn Sie sehr freundlich zu Ihren Mitmenschen sind, schreiben Sie vielleicht noch hinzu, wie das Problem zu lsen ist. Vielen Dank!

10.9

Was denn noch?

Es ist relativ einfach, das Feld und die Anzahl der Schiffe zu vergrern. Es mssen nur die Konstanten gendert werden. Allerdings sollte dem Spieler die Mglichkeit gegeben werden, zwischen der normalen und der Proversion zu wechseln. Auf der Konsole sollte dies mit Optionen geschehen. Bei der graschen Version knnten Sie einen Menpunkt mit Haken versehen. Falls es mehr als eine Version gibt, knnten Sie auch eine Dialogbox verwenden. Das Mitzhlen der Zge oder das Stoppen der Zeit durch das Mitfhren eines zweiten Timers ist einigermaen naheliegend und sollte leicht umzusetzen sein. Die Zwischenergebnisse knnen Sie prima in der Statuszeile anzeigen. Besonders interessant wre ein vollautomatisches Setzen der Markierungen. Dazu msste das Programm nach einer Peilung feststellen, ob die Schiffe, die die Peilung sieht, bereits aufgedeckt sind. Dann knnen alle anderen Richtungen markiert werden. Aber auch dann, wenn ein Schiff gefunden wird, knnten die bereits angezeigten Peilungen daraufhin berprft werden, ob sie nun alle ihre Schiffe sehen und dadurch Markierungen in den anderen Richtungen gesetzt werden knnen. Wenn Sie so weit sind, knnten Sie auch einen virtuellen Gegenspieler aufbauen. Der wrde die Markierungen vollautomatisch setzen. Wenn er dann noch die Schnittpunkte der hochdotierten Peilungen fr die nchste Raterunde verwendet, wird er nahezu unschlagbar.

239

Was ich eigentlich immer schon mal programmieren wollte, aber nie fertig bekommen habe. Und es sieht so aus, als ob es mir auch diesmal nicht gelungen ist.

11

Achtung Baustelle: Einsturzgefahr!

Das Programm dieses Kapitels ist unvollstndig, voller Fehler, nicht vllig durchdacht, schlecht dokumentiert und hat nicht einmal ein ordentliches Konzept. Die Umsetzung wirkt zusammengehauen und gestckelt. Als Abschlusstest wurde an willkrlichen Stellen in das Fenster geklickt. Und da das Programm nach fnf Minuten der Benutzung noch nicht abstrzen wollte, wurden die Arbeiten als abgeschlossen betrachtet und ein Zertikat ausgestellt. Also kurz gesagt sind alle Merkmale der professionellen Software-Entwicklung vorhanden. Wenn ein Programmierer auf derart erstellte Software stt, neigt mancher zu dem Vorschlag, dass es das Beste sei, die bestehenden Quelltexte zu lschen und neu zu schreiben. Und genau dies ist es, was ich Ihnen vorschlagen mchte. Schauen Sie sich diese Lsung an, und schreiben Sie sie neu! Dieses Projekt ist so ausgewhlt, dass Sie weiterknobeln knnen. Die Aufgabe ist so dimensioniert, dass sie nicht in Arbeit ausartet, aber auch nicht so trivial, dass es keinen Spa macht. Das Thema des Projekts ist die Simulation eines Brettspiels, dass vor einigen Jahren unter dem Namen Avalanche vertrieben wurde. Es besteht aus einer leicht geneigten Platte mit acht Bahnen. In die mittleren sechs Spuren knnen Kugeln eingeworfen werden, die sich ihren Weg nach unten suchen. Dabei stoen Sie auf Hebel, die so geartet sind, dass sie eine Kugel aufnehmen knnen. Treffen Kugeln auf das untere Ende, wird der Hebel umgelegt, und eine vorher aufgenommene Kugel fllt herunter. Ziel ist es also, die Kugeln so einzuwerfen, dass mglichst viele im Brett liegende Kugeln befreit werden und unten herausfallen. Das Brettspiel sieht vor, dass mehrere Spieler gegeneinander antreten und so einander die Kugeln wegnehmen. Abbildung 11.1 zeigt das Spielfeld nach verschiedenen Einwrfen.

241

11

Achtung Baustelle: Einsturzgefahr!

Abbildung 11.1 Das Spiel Avalanche

Solche Spiele laden frmlich zur Computersimulation ein, und so hatte ich dieses Spiel auch seinerzeit auf meinem BASIC-Computer programmieren wollen. Aber es wollte mir damals nicht gelingen. Das Spiel widersetzte sich mit allen Mitteln. Dabei hatte ich so gute Ideen. Beispielsweise war es doch schade, dass man fr die Koordinaten immer zwei Variablen brauchte. Es wre doch viel besser, x und y in einer Variablen zu speichern. Leider gab es aber in BASIC keine Strukturen. So kam ich auf den Gedanken, eine Fliekommavariable zu missbrauchen. In der Vorkommastelle sollte die X-Koordinate stehen. Die Y-Koordinate wurde dann in den Nachkommastellen abgestellt. Das wollte aber nicht so recht funktionieren. Es passierten immer wieder Merkwrdigkeiten, die ich nicht verstand. Ich wusste zwar, dass auch Fliekommazahlen binr kodiert werden, aber nicht, dass 0,1 binr ebenso eine periodische Zahl ist, wie das bei einem Drittel im de-

242

Unsauberes Konzept

11.1

zimalen Zahlensystem der Fall ist. Wenn man in 0,1-Schritten aufaddiert, kommt es eben irgendwann zu einer Ungenauigkeit. Genau dieser Effekt stellte sich ein, und nach einigen Tagen gab ich auf. Ich habe es nie wieder versucht.

11.1

Unsauberes Konzept

Das Spiel hat ein paar lstige Unregelmigkeiten. So gibt es acht Bahnen, auf denen die Kugeln nach unten laufen knnen. In diesen Bahnen liegen Hebel, die sich jeweils ber zwei Bahnen erstrecken und die dafr sorgen knnen, dass eine Kugel ihre Bahn wechselt. In der ersten Reihe sind es vier Hebel. In der zweiten Reihe sind es nur drei Hebel, die die bisher nicht verbundenen Bahnen berbrcken. Und dann gibt es am Rand in allen geradzahligen Reihen eine Bahn, die berhaupt keinen Hebel besitzt. In dieser Reihe wrde die Kugel ungehindert nach unten fallen. Das hrt sich nach vielen Sonderfllen an. Beginnen wir mit den einfacheren Dingen. Die Hebel sind als Objekte leicht identizierbar und verstndlich. Diese Klasse wird als Erstes umgesetzt. Ein Hebel geht ber zwei Bahnen. Steht der obere Teil des Hebels auf der gleichen Spur, auf der eine Kugel herunterfllt, wird der Hebel die Kugel stoppen und festhalten. Die nchste Kugel, die diesen Hebel trifft, wird ihn auf der anderen Bahn als der Stoppspur verlassen, den Hebel umlegen und die gestoppte Kugel befreien. Diese wird nun auch nach unten sausen und dabei den Hebel wieder in die Ausgangsstellung zurcksetzen. Der Hebel bendet sich nach diesem Manver also in der gleichen Stellung wie zuvor, allerdings nun ohne Belegung. Das sieht also alles gut aus und kann in eine Klasse gegossen werden. Was gibt es noch? Die Kugel bietet sich an. Sie ist rund, aber das interessiert eigentlich niemanden. Im Originalspiel sind die Kugeln farbig. Das hat durchaus seinen Sinn. Das Ziel ist es dort, eine mehrfarbige Karte mit den entsprechendfarbigen Kugeln zu fllen. Man knnte das Spiel also als eine Art Farben-Bingo ansprechen. Statt aber die Farben zu verlesen, muss der Spieler sie aus dem Spielfeld holen. Also wird tKugel als Aufzhlungstyp mit diversen Farben deniert. Das Hauptproblem besteht darin, wie das Spielfeld und die Hebel in eine vernnftige Beziehung gesetzt werden knnen. Der hier verfolgte Ansatz sieht vor, dass jede Position im Spielfeld als Zeiger auf einen Hebel implementiert wird. Wo kein Hebel ist, bleibt der Zeiger 0. Wenn zwei Positionen sich einen Hebel teilen, zeigen diese Positionen einfach auf denselben Hebel. Diese Lsung hat den Nachteil, dass mit verschiedenen Zeigern auf dasselbe Objekt gezeigt wird. Dafr ist es einfach, von jeder Position aus den richtigen

243

11

Achtung Baustelle: Einsturzgefahr!

Hebel zu nden. Auerdem ist gewhrleistet, dass kein Hebel doppelt existiert und so Konsistenzprobleme auftauchen knnten. Ein Alternativansatz knnte die Hebel durch Zeiger miteinander verbinden und so einen Graph erstellen, auf dem sich die Kugeln bewegen. Ob das Ergebnis besser wird, msste man ausprobieren. Tiefere berlegungen ber das Konzept habe ich nicht angestellt. Sie wissen ja, Programmierer werden nicht fr das Nachdenken bezahlt, sondern nach der Anzahl der produzierten Programmzeilen bemessen. Und ein undurchdachtes Konzept garantiert in der Regel haufenweise Codemeter. Auerdem ist jeder Chef glcklich, wenn der Programmierer in die Tasten haut. Das wirkt entschlossen und optimistisch, whrend ein denkender Entwickler ein beunruhigendes Gefhl vermittelt und darum bei der nchsten Gehaltserhhung bergangen wird.

11.2

Chaotische Implementierung

Es wird zwar wieder zwischen Spielimplementierung und Oberche getrennt, aber es ist nicht ganz einfach, das eine ohne das andere zu testen. In diesem Fall ist es tatschlich einfacher, zuerst die grasche Oberche zu testen und sicherzustellen, ob bei Vorgabe der Spielkoordinaten die graschen Elemente auch da landen, wo sie hinsollen. Dennoch beginnen wir mit der Beschreibung des Spielemoduls. Hier ist die Kugel als Aufzhlungstyp fr Farben deniert. Mehr Eigenschaften hat eine Kugel derzeit nicht. Der Hebel ist schon interessanter. Er kennt die linke und die rechte Bahnnummer. Puristen knnten die rechte Bahn weglassen, da sie durch Inkrementieren der linken leicht zu ermitteln ist. Der Hebel kann eine Kugel halten, und durch die Variable StoppSpalte ist die Position des Hebels gespeichert.
#ifndef AVALANCHE_H #define AVALANCHE_H enum tKugel { Keine=0, Weiss, Rot, Blau, Gelb, Gruen }; class tHebel { public: tHebel(int Links, int Rechts); void Wurf(int Eingang, int Reihe, tKugel Kugel); bool stehtLinks() { return StoppSpalte==Links; } tKugel getBelegung() { return Belegung; } private: int Links;

244

Chaotische Implementierung

11.2

int Rechts; tKugel Belegung; int StoppSpalte; }; const int MaxX = 8; const int MaxY = 4; class tFeld { public: tFeld(); ~tFeld(); tHebel *getHebel(int x, int y) { return Pos[x][y]; } void Wurf(int x, int y, tKugel Kugel); private: tHebel* Pos[MaxX][MaxY]; }; #endif
Listing 11.1 Header-Datei

Die Konstanten MaxX und MaxY bestimmen die Gre des Spielfelds. Sie knnen beliebig erhht werden, allerdings mssen sie gradzahlig sein. Das Spielfeld wird sich entsprechend vergrern. Die Klasse tFeld enthlt die Zeiger auf die Hebel und liefert die Funktion zum Einwurf einer Kugel. Der Konstruktor zeigt die etwas abenteuerliche Konstruktion, wie die Hebel mit den Spielpositionen verbunden sind.
tFeld::tFeld() { for (int xi=0; xi<MaxX; xi++) { for (int yi=0; yi<MaxY; yi++) { Pos[xi][yi] = 0; } } for (int yi=0; yi<MaxY/2; yi++) { for (int xi=0; xi<MaxX/2; xi++) { tHebel *NeuHebel = new tHebel(xi*2, xi*2+1); Pos[xi*2][yi*2] = NeuHebel; Pos[xi*2+1][yi*2] = NeuHebel; } for (int xi=1; xi<MaxX/2; xi++) { tHebel *NeuHebel = new tHebel(xi*2-1, xi*2); Pos[xi*2-1][yi*2+1] = NeuHebel;

245

11

Achtung Baustelle: Einsturzgefahr!

Pos[xi*2][yi*2+1] = NeuHebel; } } }
Listing 11.2 Konstruktor des Spielfelds

Zunchst werden alle Positionen auf 0 gesetzt. Dann wird fr je zwei Felder ein Hebel erzeugt. Bei seiner Konstruktion erfhrt der Hebel auch gleich, fr welche Spalten er zustndig ist. Es werden je zwei Positioen mit dem neuen Hebel verbunden. Sie sehen auch an der gelungenen Form der berechneten ArrayIndizes, dass dieser Code absolut wirtschaftsspionagesicher ist. Bis jemand diesen Code verstanden hat, ist lngst die Version 7 auf dem Markt. Zwar wird die Wichtigkeit eines Destruktors immer wieder berschtzt, aber ich mchte darauf hinweisen, dass es tatschlich mglich ist, fr diese Konstruktion einen passenden Destruktor zu schreiben. Er wurde nur noch nie getestet.
tFeld::~tFeld() { for ( int yi=0; yi<MaxY/2; yi++ ) { for ( int xi=0; xi<MaxX/2; xi++ ) { delete Pos[xi*2][yi*2]; } for ( int xi=1; xi<MaxX/2; xi++ ) { delete Pos[xi*2][yi*2+1]; } } }
Listing 11.3 Destruktor des Spielfelds

Viel interessanter als die destruktiven Bestandteile des Programms ist die Funktion, die dafr sorgt, dass der Ball ins Rollen kommt. Ich meine natrlich die Kugel. Da auch diese Funktion nicht der groe Wurf ist, tut man gut daran, sie wenigstens so zu nennen.
void tFeld::Wurf(int x, int y, tKugel Kugel) { if ( Pos[x][y]==0 ) { y++; if (y<MaxY) { Wurf(x, y, Kugel); } } else { Pos[x][y]->Wurf(x, y, Kugel);

246

Chaotische Implementierung

11.2

} // Nun die FIFO abarbeiten while ( Fifo.size() ) { tPosten pos = Fifo.front(); Fifo.pop_front(); if ( pos.y<MaxY ) { Wurf(pos.x, pos.y, pos.Kugel); } } }
Listing 11.4 Eine Kugel kommt ins Spiel.

Die Parameter x und y bestimmen den Einstiegspunkt in das Spiel. Der Spieler kann natrlich immer nur oben eine Kugel einwerfen. Fr ihn wird y also immer 0 sein. Im nchsten Schritt wird geprft, welcher Hebel zustndig ist. Gibt es keinen, wird y einfach erhht und per Wurf wieder ins Spiel gebracht. Sollte es aber einen Hebel geben, so soll der sich doch darum kmmern. Darum wird dessen Funktion Wurf aufgerufen. Der zweite Teil der Funktion mutet im ersten Moment verwirrend an. Da ist von einem FIFO die Rede. Der gebildete Humanist wei sofort, dass es sich nur um einen First In First Out-Speicher handeln kann. Ein Speicher, der wie eine Warteschlange arbeitet. Wer zuerst kam, kommt als Erster an die Reihe. Tatschlich ist es bei diesem Spiel ja so, dass durch den Einwurf einer Kugel pltzlich andere Kugeln befreit werden knnen, die ebenfalls der Gravitation strebsam folgend nach unten ihr Ziel suchen. Damit diese Kugeln ebenfalls in gesitteter Reihenfolge behandelt werden knnen, mssen sie gespeichert werden. Und das geschieht in dem FIFO. Wie die Kugeln in den FIFO gelangen, werden Sie bei der Wurffunktion von tHebel sehen. An dieser Stelle wird jedenfalls eine Kugel nach der anderen aus dem FIFO geholt, sofern er nicht leer ist. Dann ruft sich die Funktion selbst auf und wirft die Kugel auf diese Weise noch einmal ins Spiel und damit quasi sich selbst zu. Der Wurf des Hebels Bevor wir uns dem FIFO zuwenden, mssen wir schauen, was der Hebel mit dem Wurf macht. Der einfachte Fall ist, dass die Kugel auf einen leeren Hebel trifft, der so steht, dass die Kugel es sich darauf gemtlich machen kann. Dann wird die Kugel gespeichert. Es bewegt sich nichts, und das Programm kann in Ruhe der nchsten Kugel entgegensehen. Sollte bereits eine Kugel auf dem Hebel liegen,

247

11

Achtung Baustelle: Einsturzgefahr!

werden beide Kugeln den Hebel verlassen und ihn in der vorgefundenen Stellung hinterlassen. Diese beiden Kugeln werden auf dem Weg ber die Funktion Einwurf in den FIFO geschrieben. Im anderen Falle wird nur der Hebel umgeworfen, und die Kugel zieht weiter ihre Bahn. Diese weiterziehende Kugel ruft dann die Funktion Einwurf auf, die dafr sorgt, dass die Kugel im FIFO landet. Von dort werden die abgestellten Kugeln von der Spielfeldfunktion Wurf in der richtigen Reihenfolge wieder vorgeholt und drfen weiterrollen.
void tHebel::Wurf(int Spalte, int Reihe, tKugel Kugel) { if ( Spalte==StoppSpalte ) { if ( Belegung==Keine ) { Belegung = Kugel; return; } } if ( Belegung!=Keine ) { // Fr die erste Kugel int ErsteSpalte = StoppSpalte; // Die erste Kugel faellt entgegen der StoppSpalte if ( StoppSpalte==Links ) { ErsteSpalte = Rechts; } else { ErsteSpalte = Links; } // Die eingeworfene Kugel kommt raus, Einwurf(ErsteSpalte, Reihe+1, Kugel); // lass die belegte Kugel frei Einwurf(StoppSpalte, Reihe+1, Belegung); // StoppSpalte bleibt, weil sie gleich zurueckgeht Belegung = Keine; // wird return; // danach darf nichts mehr passieren } if ( StoppSpalte==Links ) { StoppSpalte = Rechts; } else { StoppSpalte = Links; } Einwurf(Spalte, Reihe+1, Kugel); }
Listing 11.5 Des Hebels Wurf

248

Auen hui

11.3

Der FIFO und die STL Die Funktion Einwurf ist dank des Containers deque der Standard Template Library (STL) extrem schlank. Die STL gehrt zum Standard von C++ und sollte von jedem nur halbwegs aktuellen Compiler mitgeliefert werden. Fr den FIFO wird eine Struktur bentigt, in der alle Parameter eines Einwurfs abgelegt werden knnen. Das sind die Spalte, die Reihe und die Kugel. Von dieser Struktur wird ein deque gebildet. Dabei handelt es sich um ein dynamisches Array, an das jederzeit hinten und vorn Elemente angebaut und wieder abgebaut werden knnen. Also braucht die Funktion Einwurf nichts weiter zu tun, als die Parameter in die Struktur zu packen und diese mit dem Aufruf der Elementfunktion push_back an den FIFO anzuhngen.
class tPosten { public: int x, y; tKugel Kugel; }; deque<tPosten> Fifo; void Einwurf (int Spalte, int Reihe, tKugel Kugel) { tPosten pos; pos.x = Spalte; pos.y = Reihe; pos.Kugel = Kugel; Fifo.push_back(pos); }
Listing 11.6 Abstellen im FIFO

Die Wurffunktion des Feldes wird spter mit der Elementfunktion front den ersten Wert auslesen und weiterverarbeiten und mit der Funktion pop_front aus dem FIFO lschen.

11.3

Auen hui

Ein solches Spielfeld mit derart speziellen Hebeln kann natrlich nur auf einer graschen Oberche vernnftig dargestellt werden.

249

11

Achtung Baustelle: Einsturzgefahr!

Fr die Funktionalitt werden nicht viele Ereignisse bentigt. Die Funktion zum Neuzeichnen des Fensters ist sowieso immer dann erforderlich, wenn grasch etwas dargestellt werden soll. Ansonsten muss noch der Mausklick gefangen werden. Dann wei das Programm, in welche Bahn eine Kugel eingeworfen wird. Fr die Darstellung bentigen wir ein paar Kreise fr die Kugeln, ein paar Linien fr die Bahngrenzen und dann noch die Hebel. Diese knnen mit einem Zeichenprogramm relativ schnell gezeichnet werden und werden dann als Image im Programm dargestellt. Es ist natrlich klug, gleich ein Paar fr links und rechts anzufertigen. Das ist durch die Spiegelungsfunktion des Grakprogramms ein Klacks. Die beiden Graken werden durch den Konstruktor geladen. Da die Graken als GIF gezeichnet wurden, muss der GIF-Handler erst geladen werden. Zum Schluss wird das Spielfeld initialisiert.
wxavalancheFrame::wxavalancheFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) { wxImageHandler *GifHandler = new wxGIFHandler(); wxImage::AddHandler(GifHandler); imgHebelLinks.LoadFile(wxT(PATH "hebellinks.gif"), wxBITMAP_TYPE_GIF); imgHebelRechts.LoadFile(wxT(PATH "hebelrechts.gif"), wxBITMAP_TYPE_GIF); Feld = new tFeld(); }
Listing 11.7 Der Rahmenfensterkonstruktor

Fr die Darstellung der Hebel wird ein linker und ein rechter Hebel als GIF geladen. Die Hintergrnde der Bilder sind transparent. So knnen Sie einen beliebigen Hintergrund verwenden. Mausklick Diese Sparversion einer Mausfunktion ermittelt nur die X-Koordinate und prft, ob diese in dem Bereich liegt, den das Programm verkraften kann.
void wxavalancheFrame::OnMouseEvent(wxMouseEvent& event) { wxClientDC dc(this); wxPoint pt(event.GetLogicalPosition(dc));

250

Auen hui

11.3

int x = 2*(pt.x - Distanz/4)/Distanz - Offset; if (x<1 || x>MaxX-1) return; if (event.LeftDown()) Feld->Wurf(x, 0, Weiss); Refresh(); }
Listing 11.8 Mausereignis bearbeiten

Die Funktion rechnet dazu die Bildschirmposition in die Fensterkoordinaten um, ermittelt daraus die Spielkoordinaten und ruft dann die Funktion Wurf und bringt damit das Spiel in Gang. Damit das Ergebnis der Berechnung sichtbar wird, wird die Funktion Refresh aufgerufen, die einen Neuaufbau des Fensters auslst. Die Zeichenfunktion Wie erwartet, wird es bei der Bildschirmdarstellung etwas unbersichtlich. Es mssen die Hebel, die Kugeln und die Bahnen gezeichnet werden. Tatschlich scheint das nicht viel, aber die Positionen mssen aus den Spielpositionen errechnet werden und das fhrt in der Regel zu relativ unbersichtlichen Thermen. Tatschlich nimmt OnPaint den grten Raum ein, obwohl die Ausgabe eher spartanisch gehalten ist. Zuerst werden ein paar Kreise ber das Spielfeld gezeichnet. Sie sollen dem Benutzer anzeigen, wo er klicken kann, um eine Kugel einzuwerfen. Dann luft eine Schleife ber die Hlfte der Reihen. Die Hlfte deshalb, weil in jedem Durchgang zwei Reihen gezeichnet werden. Die eine Reihe hat vier Hebel und die andere drei. Das sind dann zwar viele Zeilen Code, die sich auch noch an vielen Stellen wiederholen, aber es wre vermutlich auch nicht besser geworden, wenn diese Unterschiede als Sonderflle Zeile fr Zeile mit der Hilfe geschickter Modulo-Operationen erstellt worden wren. Es steht Ihnen aber frei, eine elegantere Lsung zu nden und im Forum dieses Buches vorzustellen. In jeder Zeile wird zunchst ein senkrechter Strich als optische Trennung der Bahnen gezeichnet. Dann folgt ein Hebel, der als Bitmap dargestellt wird. Hier muss unterschieden werden, ob der Hebel links oder rechts steht, weil er dann einfach anders aussieht. Zu guter Letzt wird eine Kugel auf den Hebel gesetzt, wenn eine Belegung vorliegt. Dieser Vorgang wiederholt sich leider viermal mit leicht variierenden Positionen und Images, je nachdem, ob eine gerade oder ungerade Reihe vorliegt und ob der Hebel links oder rechts steht.

251

11

Achtung Baustelle: Einsturzgefahr!

void wxavalancheFrame::OnPaint(wxPaintEvent& event) { wxPaintDC dc(this); for (int xi=1; xi<MaxX-1; xi++) { dc.DrawCircle(Distanz*(xi)/2+Distanz*Offset, Distanz/2, KugelRadius); } for (int yi=0; yi<MaxY/2; yi++) { for (int xi=0; xi<MaxX/2; xi++) { tHebel *Hebel=Feld->getHebel(xi*2, yi*2); int GrafX = Distanz*(xi+Offset); int GrafY = Distanz*(yi*2+Offset); dc.SetPen(wxPen(*wxGREEN, 4)); dc.DrawLine(GrafX-KugelRadius, GrafY, GrafX-KugelRadius, GrafY+Distanz); dc.SetPen(wxPen(*wxBLACK, 1)); if (Hebel->stehtLinks()) { dc.DrawBitmap(imgHebelLinks, GrafX, GrafY, true); tKugel Kugel = Hebel->getBelegung(); if (Kugel!=Keine) { dc.DrawCircle(GrafX, GrafY, KugelRadius); } } else { dc.DrawBitmap(imgHebelRechts, GrafX, GrafY, true); tKugel Kugel = Hebel->getBelegung(); if (Kugel!=Keine) { dc.DrawCircle(GrafX+Distanz/2, GrafY, KugelRadius); } } } for (int xi=1; xi<MaxX/2; xi++) { tHebel *Hebel = Feld->getHebel(xi*2-1, yi*2+1); int GrafX = Distanz*(xi+Offset)-Distanz/2; int GrafY = Distanz*(yi*2+1+Offset); dc.SetPen(wxPen(*wxGREEN, 4)); dc.DrawLine(GrafX-KugelRadius, GrafY, GrafX-KugelRadius, GrafY+Distanz); dc.SetPen(wxPen(*wxBLACK, 1)); if (Hebel->stehtLinks()) { dc.DrawBitmap(imgHebelLinks, GrafX, GrafY,

252

Auen hui

11.3

true); tKugel Kugel = Hebel->getBelegung(); if (Kugel!=Keine) { dc.DrawCircle(GrafX, GrafY, KugelRadius); } } else { dc.DrawBitmap(imgHebelRechts, GrafX, GrafY, true); tKugel Kugel = Hebel->getBelegung(); if (Kugel!=Keine) { dc.DrawCircle(GrafX+Distanz/2, GrafY, KugelRadius); } } } // In der zweiten und vierten Reihe muss noch // rechts eine Abschlusslinie hinkommen dc.SetPen(wxPen(*wxGREEN, 4)); dc.DrawLine(Distanz*(MaxX/2+Offset)-Distanz/2 -KugelRadius, Distanz*(yi*2+1+Offset), Distanz*(MaxX/2+Offset)-Distanz/2 -KugelRadius, Distanz*(yi*2+2+Offset)); dc.SetPen(wxPen(*wxBLACK, 1)); } // Zwei Linien als Umrandung dc.SetPen(wxPen(*wxGREEN, 4)); dc.DrawLine(Distanz*(Offset)-KugelRadius, Distanz*(Offset), Distanz*(Offset)-KugelRadius, Distanz*(MaxY+Offset)); dc.DrawLine(Distanz*(MaxX/2+Offset)-KugelRadius, Distanz*(Offset), Distanz*(MaxX/2+Offset)-KugelRadius, Distanz*(MaxY+Offset)); dc.SetPen(wxPen(*wxBLACK, 1)); }
Listing 11.9 OnPaint, der Bildschirmaufbau

Zu guter Letzt gibt es noch ein paar senkrechte Linien. Das sind einmal die Trennlinien bei den Reihen, in denen kein Hebel vorliegt und dann mssen um

253

11

Achtung Baustelle: Einsturzgefahr!

das ganze Spiel noch ein paar Senkrechten gezeichnet werden, damit das Spielfeld geschlossen wirkt und der Wind nicht so durchzieht.

11.4

Was denn noch?

Mit dieser Simulation haben Sie nun eine wirkliche Spielwiese, die Sie verfeinern und zum Spiel ausbauen knnen. Sie haben viele Mglichkeiten. Es fngt beim Konzept an. Ich bin sicher, Sie knnen einen wesentlich sauberen und gelungeneren Entwurf erstellen. Das wrde dann zu einem besser lesbaren Quelltext fhren. Die Implementierung ber einen FIFO knnte man vielleicht auch ber eine Rekursion lsen. Das Spiel simuliert zwar sehr schn die fallenden Kugeln, mehr aber auch nicht. Um wirklich spielen zu knnen, mssten farbige Kugeln her. Und es wird Ihnen sicher schon aufgefallen sein, aber das Programm gibt nicht an, welche Kugeln unten aus dem Tablett herausfallen. Das wird ein Spieler aber unbedingt wissen wollen. Auch die Darstellung kann auf vielfltige Weise verbessert werden. Das fngt damit an, dass die Hebel grasch nicht rundum gelungen sind. Wenn Sie sie neu entwerfen, sollten Sie auch darauf achten, dass sie so positioniert sind, dass die Kugeln besser auf den Schalen liegen. Ein Hintergrund, der attraktiver als grau ist, sollte sich auch nden lassen. Richtig edel wre es, wenn Sie einen Timer verwenden und damit die Kugeln Schritt fr Schritt das Spielfeld herunterfallen lassen. Lassen Sie sich von dem Eichhrnchenspiel inspirieren! Spielregeln Das Spiel Avalanche wird mit mehreren Personen gespielt. Jede Person erhlt eine oder mehrere Karten mit 9 Feldern in verschiedenen Farben. Jeder Spieler erhlt auch mehrere Kugeln, die einzeln und abwechselnd in das Spiel geworfen werden. Alle Kugeln, die unten herausfallen, knnen auf die Karten passend zu ihren Farben verteilt werden. Wer zuerst eine Karte voll besetzt hat, hat gewonnen. Um aus der Simulation ein fertiges Spiel zu programmieren, gibt es also noch genug zu tun.

254

Seit wann ertrinken Frsche?

12

Der Frosch und das Eichhrnchen

Kennen Sie noch das Spiel Frogger? Auf dem Spielautomat in der Dorf-Disco konnte man einem Frosch ber die Strae helfen. War der Froschlenker nicht geschickt genug, wurde der arme Frosch plattgefahren. Es gab noch keine Krtentunnel und schon gar nicht in diesem Spiel. Dann aber musste der Frosch ber einen Fluss und nutzte vorbeiieende Bume, um trockenen Fues auf die andere Seite zu kommen. Verfehlte der ferngesteuerte Frosch einen Baumstamm, starb er. Ich habe mich immer gefragt, warum der arme Frosch in dieser Situation sterben muss. Der wird doch nicht ertrunken sein! Als ich diesen Satz murmelte, meinte der Typ hinter mir, dass der Flu so verdreckt sei, dass ein normal sterblicher Frosch darin vertzt wrde. Ein anderer ereiferte sich in der Theorie, dass das Baracudas waren und das Spiel im Amazonasgebiet spiele. Das erklre auch die vielen Bume im Wasser. Der Typ, der eben vom Brechen wieder hereinkam, fragte daraufhin, ob wir jemals im Amazonasgebiet so viele Autos gesehen htten. Dieser Einwurf war berechtigt. Doch dann kam der Wirt und meinte trocken: Egal woran der blde Frosch verreckt. Hauptsache, er ist tot, und ihr werft eine weitere Mark in den Automaten. Diese Geschichte hat mich nicht losgelassen, und so entschloss ich mich, dass ich eines Tages ein Buch schreiben wrde, in dem ich das Frogger-Spiel neu programmieren wrde. Aber diesmal wrde ich es richtig machen. Kein Frosch wrde die Hauptrolle spielen, sondern ein Eichhrnchen. Das kann wunderbar berfahren werden und wird in jedem Fall darauf achten, nicht ins Wasser zu fallen. Und wehe, es kommt mir irgendein Biologe daher und erzhlt, dass neueste Forschungen ergeben htten, dass Eichhrnchen nichts lieber tten als den genzen Tag zu baden. Die Abbildung 12.1 zeigt das Ergebnis dieser Bemhungen. Das Eichhrnchen ist trocken ber den Fluss gekommen und steht nun vor der Aufgabe, ungeplttet ber die Strae zu kommen. Endlich kann gespielt werden, ohne sich von abgrndigen Sinnfragen ablenken zu lassen.

255

12

Der Frosch und das Eichhrnchen

Abbildung 12.1 Mit dem Eichhrnchen unterwegs

12.1

Rahmenprogramm

Fr die grasche Darstellung wird wieder auf die wxWidgets-Bibliothek zurckgegriffen. Ein Blick auf die Header-Datei verschafft einen berblick ber die Anforderungen. Die Klasse squirrelapp erledigt nur den Brokram fr die grasche Oberche und ist weitgehend uninteressant. In der Klasse squirrelFrame werden dagegen die Aktivitten im Dialog mit der graschen Oberche umgesetzt. Die Elementfunktionen, die mit dem Prx On beginnen, behandeln wieder die Ereignisse der GUI. Fr das Spiel wird mit OnKeyDown das Tastaturereignis verarbeitet. Wie im Vorbild wird das Eichhrnchen mit den Tasten ber das Spielfeld geschickt. OnPaint kmmert sich um die Darstellung des Fensterinhalts. Die Bewegungen mssen durch einen Timer angestoen werden, dessen Takt die Funktion OnTimer fngt. Neu ist die Funktion OnEraseBackground. Sie stellt

256

Rahmenprogramm

12.1

das Lschen des Hintergrundes unter Applikationskontrolle, um ein Flackern zu vermeiden.


const int MaxBacks = 5; class squirrelFrame : public wxFrame { public: squirrelFrame(const wxString& title, const wxPoint& pos, const wxSize& size); void OnQuit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnPaint(wxPaintEvent& event); void OnEraseBackground(wxEraseEvent& event); void OnKeyDown(wxKeyEvent& event); void OnTimer(wxTimerEvent& event); void bereiteFeld(); private: wxPanel *panel; wxTimer timer; // die Images fr das Spiel wxBitmap imgSquirrel; // die Hauptfigur wxBitmap imgBaumstamm; // ein Baum im Wasser wxBitmap imgAutoLinks; // Auto Fahrtrichtung links wxBitmap imgAutoRechts; // Auto Fahrtrichtung rechts wxBitmap imgBack[MaxBacks]; }; class squirrelapp : public wxApp { public: virtual bool OnInit(); }; enum { Menu_File_Quit = 100, Menu_File_About };
Listing 12.1 Header-Datei squirrel.h

Als private Elementvariablen sind die Images fr den Hintergrund, die Autos, die Baumstmme und natrlich fr das Eichhrnchen. Das Panel wird bentigt, um die Tastaturereignisse zu fangen. Der Timer ist die Basis fr den guten Takt. Applikationsklasse Die wenigen Zeilen der Applikationsklasse werden nur bentigt, um ein neues Rahmenfenster zu initialisieren und zu starten.

257

12

Der Frosch und das Eichhrnchen

IMPLEMENT_APP(squirrelapp); bool squirrelapp::OnInit() squirrelFrame *frame = new squirrelFrame(wxT("Squirrel") , wxDefaultPosition, wxSize(MaxX,MaxY)); frame->Show(true); SetTopWindow(frame); return true;

Listing 12.2 Implementierung der Applikationsklasse

Die Vorbereitungen fr die Gestaltung des Hauptfensters geschieht im Konstruktor der Fensterklasse squirrelFrame. Im Initialisierer wird der passende Konstruktor der Basisklasse aufgerufen und der Timer initialisiert. Es wird ein Panel angelegt, das einfach ber das Fenster gelegt wird. Das Panel wird vor allem fr das Zeichnen eingesetzt. Es empfngt die Tastaturereignisse und die Paint-Ereignisse. Anschlieend wird das Men aufgebaut. Das ist beinahe selbsterklrend. Ausfhrlichere Informationen zum Aufbau von Mens unter wxWidgets nden Sie ab Seite 144.
squirrelFrame::squirrelFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame((wxFrame *)NULL, -1, title, pos, size) , timer(this, TIMER_ID) { // ohne ein Panel gibt es keine Tastaturereignisse panel = new wxPanel(this, 5, wxPoint(pos), wxSize(size)); // Das Menue gehoert sich einfach und kostet nicht viel wxMenu *menuFile = new wxMenu; menuFile->Append(Menu_File_About, wxT("&Was ist das...")); menuFile->AppendSeparator(); menuFile->Append(Menu_File_Quit, wxT("&Beenden")); wxMenuBar *menuBar = new wxMenuBar; menuBar->Append(menuFile, wxT("&Datei")); SetMenuBar(menuBar);
Listing 12.3 Der Konstruktor des Fensterrahmens

258

Rahmenprogramm

12.1

Im Konstruktor des Fensterrahmens werden die Images geladen. Fr das Spiel werden fnf Hintergrunddateien vom Typ JPEG verwendet. Dieses Format ist von Digitalkameras bekannt. Dieses Format ist verlustbehaftet komprimierend. Das bedeutet, dass das Bild geringfgig gendert wird, sodass es besser zusammenzupacken ist. Fr die Figuren wird dagegen das Format GIF verwendet. Auch GIF komprimiert, allerdings ist es bei chigen Bildern weitgehend verlustfrei. Die groe Besonderheit von GIF ist allerdings, dass es auch eine transparente Frbung kennt. An den transparenten Stellen kommt also der Hintergrund durch. Zum Zeichnen der Bilder verwendet das Programm aber ein internes Format, in das die Bilder umgewandelt werden mssen. Um die oben genannten Formate einlesen zu knnen, bentigt das Programm einen Imagehandler. Ein solcher wird fr jedes Bildformat geladen und der Klasse wxImage hinzugefgt.
~ wxImageHandler *JpegHandler = new wxJPEGHandler(); wxImage::AddHandler(JpegHandler); wxImageHandler *GifHandler = new wxGIFHandler(); wxImage::AddHandler(GifHandler);

Listing 12.4 Imagehandler

Um die Imagehandler einsetzen zu knnen, mssen entsprechende Include-Dateien eingebunden werden. Das sollte natrlich vor dem Aufruf, beispielsweise am Anfang der Datei erfolgen.
#include <wx/imagjpeg.h> #include <wx/imaggif.h>

Es werden die Images aus den Dateien geladen. Zunchst werden fnf Hintergrundbilder geladen. Aus der Gre des Hintergrundbildes ergibt sich die Spielfeldgre. Das Thema Images wird im Abschnitt 12.2 noch einmal ausfhrlicher behandelt.
// Die Images werden geladen. Zunaechst der Hintergrund for (int i=0; i<MaxBacks; i++) { char filename[] = PATH "landschaft0.jpg"; filename[strlen(PATH "landschaft")] += i; imgBack[i].LoadFile(wxString::FromAscii(filename), wxBITMAP_TYPE_JPEG); } // Der Hintergrund bestimmt die Groesse des Spielfelds Spielfeld.SetWidth(imgBack[0].GetWidth()); Spielfeld.SetHeight(imgBack[0].GetHeight()); imgBaumstamm.LoadFile(wxT(PATH "baum.gif"), wxBITMAP_TYPE_GIF);

259

12

Der Frosch und das Eichhrnchen

imgAutoLinks.LoadFile(wxT(PATH "autolinks.gif"), wxBITMAP_TYPE_GIF); imgAutoRechts.LoadFile(wxT(PATH "autorechts.gif"), wxBITMAP_TYPE_GIF); imgSquirrel.LoadFile(wxT(PATH "squirrel.gif"), wxBITMAP_TYPE_GIF); // Die Images stehen. Das Spielfeld wird aufgebaut. bereiteFeld();
Listing 12.5 Images laden

Mit dem Aufruf bereiteFeld werden die Spielguren platziert und initialisiert.
// Die Ereignisse werden mit den Callbacks verbunden Connect(wxEVT_TIMER, wxTimerEventHandler(squirrelFrame::OnTimer)); Connect(Menu_File_About, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(squirrelFrame::OnAbout)); Connect(Menu_File_Quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(squirrelFrame::OnQuit)); panel->Connect(wxEVT_PAINT, wxPaintEventHandler(squirrelFrame::OnPaint), NULL,this); panel->Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler( squirrelFrame::OnEraseBackground), NULL, this); panel->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(squirrelFrame::OnKeyDown), NULL, this); // Focus setzen, um die Tasteneingaben zu fangen panel->SetFocus(); timer.Start(TIMERTICK); // Nun startet alles! }
Listing 12.6 Ereignisse verbinden

Mit den Aufrufen der Funktion Connect werden die Ereignisse mit den Funktionen verbunden, die sie auffangen und bearbeiten sollen. Diese Vorgehensweise steht alternativ zu der Verwendung der Ereignistabelle, wie sie in den anderen wxWidgets-Programmen verwendet wird. Hier sieht man sehr deutlich, welche Ereignisse vom Fensterrahmen und welche vom Panel bernommen werden. Fokus Der Fokus wird auf den Panel gesetzt. Der Begriff Fokus bezeichnet die Tastatureingabe einer graschen Oberche. Auch wenn eine GUI beliebig viele

260

Diashow

12.2

Fenster zur Verfgung stellen kann, gibt es doch nur eine Tastatur. Und die Eingaben der Tastatur mssen sich an ein Fenster richten. Im Normalfall ist dies das zuoberst liegende Fenster. UNIX-Oberchen knnen aber beispielsweise so konguriert werden, dass die Tastatureingabe auch auf das Fenster gerichtet ist, ber dem der Mauszeiger steht. Benutzer, die mit dieser Art vertraut sind, knnen so sehr exibel mit mehreren Fenstern jonglieren. Der Fokus bezieht sich aber nicht nur auf den Fensterrahmen, sondern auch auf Fensterelemente. Sie kennen dies sicher von den Ok-Buttons, die in der Regel den Fokus erhalten. Durch Pfeiltasten, knnen Sie den Fokus allerdings auch auf einen anderen Button umlegen und diesen per Tastatur auslsen. Schlielich wird der Timer gestartet, und das Spiel kommt in Bewegung.

12.2

Diashow

Im Grunde passiert im gesamten Spiel nichts anderes, als dass immer wieder ein Hintergrundbild angezeigt wird und fr die verschiedenen Spielguren je ein Bild an eine bestimmte Position gebracht wird. Die Bilder, die dazu verwendet werden, sind prinzipiell die gleichen, die Sie mit Ihrer Digitalkamera schieen. Wenn Sie also ein Foto von sich schieen und entsprechend verkleinern, knnen Sie auch selbst als Hintergrundbild dienen. Oder Sie konvertieren das Bild zum GIF und knnten statt dem Eichhrnchen ber den Fluss und die Strae hetzen. Sie knnen auch ein Foto Ihres Autos von oben schieen und dieses ber die Strae fahren lassen. Oder Sie scannen ein Bild der Queen Mary ein und lassen das Eichhrnchen mal Luxusdampfer fahren anstatt Baumstamm. Die einzige Besonderheit der Spielguren liegt darin, dass sie teilweise transparent sind. Die Rnder um die eigentliche Figur sind transparent deniert und so bleibt der Hintergrund an diesen Stellen sichtbar. So umgibt das Gras den Frosch oder das Eichhrnchen eben vollstndig und erweckt den Eindruck, dass das Tier im Gras sitzt. Die Fhigkeit der Transparenz beherrscht allerdings nicht jedes Bildformat und auch nicht jedes Grakprogramm.

12.2.1

Hintergrundwechsler

Zuerst wird der Hintergrund gezeichnet. Zu den Zeiten, als Frogger noch eines der Top-Spiele war, wurde dieses durch eine einfache grne Farbe realisiert. Die heutigen Computer ermglichen da deutlich mehr. Die Graksysteme erlauben es, rechteckige Graken blitzschnell auf den Bildschirm zu kopieren. Der Inhalt

261

12

Der Frosch und das Eichhrnchen

des Rechtecks ist dabei vllig egal. So kann das Spiel auch aufwendige Graken verwenden. In diesem Fall hat mir mein Freund Jrg Osarek (http://www.berater-exzellenz.de) dabei geholfen, mit der Hilfe des Raytracers POV-Ray (http://www.povray.org) die Figuren und Hintergrnde zu erstellen, die fr das Spiel verwendet werden. Fr den Hintergrund wird eine Schablone angefertigt, die die Gestalt der Landschaft festlegt. Darber werden unterschiedliche Texturen gezogen, je nachdem, ob es sich um Gras, Asphaltdecke oder einen Fluss handelt. Der Fluss ist interessant, weil er eine gewisse Tiefe hat und sich in den Wellen das Licht spiegelt. Durch mehrere aufeinanderfolgende Bilder kann der Verlauf der Wellen erfasst werden. Werden die Bilder in schneller Abfolge gewechselt, entsteht der Eindruck eines pltschernden Gewssers. Statt eines Hintergrundbildes verwendet das Programm darum fnf und zeigt bei jedem Bildneuaufbau ein anderes Bild. Das fllt bei der Wiese nicht auf. Aber das Wasser iet. Im Konstruktor des Fensterrahmens werden die Bilderdateien geladen und in Image-Variablen gespeichert. Es existieren fnf Bilder, die originellerweise landschaft0.jpg bis landschaft4.jpg heien.
for (int i=0; i<MaxBacks; i++) { char filename[] = PATH "landschaft0.jpg"; filename[strlen(PATH "landschaft")] += i; imgBack[i].LoadFile(wxString::FromAscii(filename), wxBITMAP_TYPE_JPEG); }
Listing 12.7 Mehrere Hintergrnde laden

Beim Herumbasteln mit den Dateinamen muss bercksichtigt werden, dass wxWidgets seinen eigenen Stringtyp verwendet. Im Programm wird brigens nicht abgefragt, ob die Dateien wirklich da sind. Sollte Ihr Programm pltzlich kommentarlos in die Wiese beien, statt sie anzuzeigen, knnte es daran liegen, dass Ihre Wiese ganz woanders ist, als sie dachten. Im Listing 12.7 wird die Konstante PATH verwendet, um den Ort der Imagedateien anzugeben. Diese wird vorher deniert.
#define PATH "../../images/" //#define PATH "images/"

Die erste Zeile habe ich bei KDevelop verwendet. Dort liegt die ausfhrbare Datei in zwei recht tief gelegenen Verzeichnissen, je nachdem, ob eine Debug-Version

262

Es bewegt sich etwas

12.3

oder eine Release erzeugt wird. Das Verzeichnis mit den Graken liegt in einem Verzeichnis images auf der Hauptebene des Projekts. So kann aus der IDE heraus die Applikation gestartet werden, und sie ndet ihre Dateien. Die zweite Zeile ist fr Bloodshed Dev C++, der seine ausfhrbare Datei im gleichen Verzeichnis der Quelltexte ablegt. Hier wird brigens der Effekt von C++ ausgenutzt, dass zwei aufeinanderfolgende Zeichenkonstanten automatisch vom Compiler zu einer zusammengefasst werden.

12.3

Es bewegt sich etwas

Wenn ein Autor in einem Verlag, der sich nach Galileo benennt, ber bewegliche Teile schreibt, muss eigentlich ein bestimmtes Zitat fallen. Ich werde der Versuchung aber widerstehen. Die Baumstmme und die Autos haben einiges gemeinsam. Sie haben eine Koordinate, an der sie gezeichnet werden sollen, ein Image, mit dem sie auf dem Bildschirm vertreten sind, und sie sollen sich schrittweise in eine Richtung bewegen. Wer so viel gemeinsam hat, muss einfach einer Klasse beitreten. Und da Baumstmme und Autos normalerweise wenig Gemeinsames haben, heit die Klasse schlicht tBewegtes.
class tBewegtes { public: tBewegtes() { Inkrement=0; xPos=0; yPos=0; img=0; } void bewege(); void draw(wxDC &dc); bool istKollision(tBewegtes &Gegner); bool imFluss(); bool faehrtBaum(tBewegtes &Baum); void set(int x, int y, wxBitmap *pImg, int inkr); void setInkrement(int ink) { Inkrement=ink; } int getInkrement() { return Inkrement; } friend void squirrelFrame::bereiteFeld(); protected: int Inkrement; int xPos; int yPos; wxBitmap *img; };
Listing 12.8 Die Klasse tBewegtes

263

12

Der Frosch und das Eichhrnchen

Betrachten wir zunchst die Elementvariablen. Die Geschwindigkeit der Figur wird durch die Variable Inkrement festgelegt. xPos und yPos beschreiben den Standort und img das Image der Figur. Die Funktion squirrelFrame::bereiteFeld wird als Freund deklariert. So kann diese Funktion, die fr den Aufbau der Figuren zustndig ist, die Image-Variablen befllen. Bei einer so bewegten Klasse ist natrlich die Elementfunktion bewege besonders relevant, wenn auch nicht besonders aufregend. Die Variable Inkrement enthlt die Anzahl der Pixel, die der x-Position bei jedem Timer-Tick zuaddiert wird. Soll die Figur von rechts nach links wandern, enthlt Inkrement einfach einen negativen Wert. Erreicht sie einen der Rnder, fngt sie auf der anderen Seite wieder an.
void tBewegtes::bewege() { xPos += Inkrement; if (xPos < -img->GetWidth()) { xPos = Spielfeld.GetWidth(); } if (xPos > Spielfeld.GetWidth()) { xPos = -img->GetWidth(); } }
Listing 12.9 bewege

Sie sehen, dass das Objekt um eine Image-Lnge den Rand in jeder Richtung berschreitet. Das ist wichtig, damit die Figur langsam aus dem Bild herausfhrt. Das Clipping beim Zeichnen sorgt dann dafr, dass nur der Teil der Figur gezeichnet wird, der im Bild ist. Die meisten anderen Funktionen betreffen das Eichhrnchen, das ein Spezialfall von tBewegtes ist. Die Funktion istKollision prft, ob das Eichhrnchen gegen ein Auto gelaufen ist.
bool tBewegtes::istKollision(tBewegtes &Gegner) { return xPos > Gegner.xPos && xPos + img->GetWidth() < Gegner.xPos+Gegner.img->GetWidth() && yPos - img->GetHeight() <= Gegner.yPos && yPos > Gegner.yPos-Gegner.img->GetHeight(); }
Listing 12.10 istKollision

264

Es bewegt sich etwas

12.3

Beim Duell Eichhrnchen gegen Auto gewinnt das Eichhrnchen nie. Eine vllig andere Situation ergibt sich beim Zusammentreffen mit einem Baum. In diesem Fall wird das Eichhrnchen vom Baum mitgenommen.
bool tBewegtes::faehrtBaum(tBewegtes &Baum) { return xPos + img->GetWidth()/2 > Baum.xPos && xPos + img->GetWidth()/2 < Baum.xPos+Baum.img->GetWidth() && yPos-img->GetHeight() <= Baum.yPos && yPos > Baum.yPos-Baum.img->GetHeight(); }
Listing 12.11 faehrtBaum

Die Funktion draw bernimmt die eigentliche Zeichnung der Figuren. Beachten Sie, dass der Parameter ein wxDC ist. Das ist nmlich eine Basisklasse von wxPaintDC und wxBufferedPaintDC. Wir werden dies noch brauchen.
void tBewegtes::draw(wxDC &dc) { dc.DrawBitmap(*img, xPos, yPos, true); }
Listing 12.12 draw

Das Eichhrnchen Das Eichhrnchen ist natrlich auch eine Figur. Sie wird allerdings nicht durch den Timer-Tick bewegt, sondern durch die Tastaturereignisse. Das ist aber nicht die ganze Wahrheit. Wenn das Eichhrnchen auf einem Baumstamm sitzt, dann fhrt es mit und erhlt den Inkrement des Baumstamms. Da das Eichhrnchen aber darber hinaus auch der Tastatursteuerung folgt, bekommt es eine eigene Klasse tSqu, und die wird von der Klasse tBewegtes abgeleitet.
class tSqu : public tBewegtes { public: tSqu() { Inkrement=0; Step=25; } void left() { xPos -= Step; if (xPos<0) xPos=0; } bool up(); void right(); void down(); private: int Step; };
Listing 12.13 Die Klasse tSqu

265

12

Der Frosch und das Eichhrnchen

Die Elementfunktionen up, down, left und right reagieren auf die Tastendrcke und bewegen das Eichhrnchen schrittweise ber den Bildhschirm. Wie gro der Schritt ist, legt die Variable Step fest.

12.4

Aufstellung der Figuren

Die Funktion bereiteFeld positioniert die Figuren auf dem Spielfeld. Es muss mindestens sechs Baumstmme geben, die in drei Reihen nebeneinander herschwimmen. Pro Bahn mssen es zwei sein, sonst ist es kaum zu schaffen, ber die Bume zu wechseln. Die Reihen mssen unterschiedlich schnell sein. Das Eichhrnchen muss am unteren Rand positioniert werden, und es werden drei Bahnen mit Autos aufgebaut. Bei den Autos laufen nicht alle in die gleiche Richtung. Eine Spur kommt den anderen entgegen. Der grere Schwierigkeitsgrad wre natrlich dann erreicht, wenn es die mittlere Spur wre. Da wir aber mit dem Eichhrnchen auf dem Kreuzzug fr mehr Glaubwrdigkeit im Computerspiel sind, gibt es eine den deutschen Verkehrsregeln angepasste Fahrweise. Zunchst werden die Figuren als globale Variablen erzeugt. Das ist vielleicht nicht sehr elegant, aber macht die Dinge etwas berschaubarer.
tSqu SquirrelFigur; const int MaxAuto = 4; tBewegtes Auto[MaxAuto]; const int MaxBaum = 6; tBewegtes Baum[MaxBaum]; const int MaxFigur = MaxAuto+MaxBaum+1; tBewegtes *Figur[MaxFigur];
Listing 12.14 Die Funktion bereiteFeld

Das Array Figur enthlt die Zeiger auf alle bewegten Elemente des Spiels. Hier wird die Polymorphie genutzt, damit alle Figuren in einer Schleife bewegt werden knnen. Die Funktion bereiteFeld positioniert zuerst die Bume. Um zwei Baumstmme in die gleiche Bahn zu bringen, muss die Y-Koordinate gleich sein. Aber auch die Geschwindigkeit muss gleich sein, sonst werden die Baumstmme bereinander hinwegschwimmen, und das sieht nicht realittsnah aus. Und natrlich mssen sie unterschiedliche X-Positionen haben. Was ntzen zwei Baumstmme, die die ganze Zeit bereinander liegen? Um die Baumstmme voneinander zu trennen,

266

Aufstellung der Figuren

12.4

muss jeder zweite etwa in der Mitte des Spielfeldes starten. Die folgende Schleife verwendet die ganzzahlige Division durch 2, um die Baumstmme korrekt zu positionieren.
void squirrelFrame::bereiteFeld() { for (int i=0; i<MaxBaum; i++) { // i/2 ist int-Division! 4/2 == 5/2!!! Baum[i].set((i%2)*Spielfeld.GetWidth()/2, startFluss + (i/2)*50, &imgBaumstamm, 3 - (i/2)); Figur[i] = &Baum[i]; }
Listing 12.15 Die Baumschleife

Der erste Parameter der Funktion set ist die X-Position und der zweite die YPosition. Die Modulo-Operation sorgt dafr, dass bei ungeradem i die X-Position der Hlfte der Spielfeldbreite entspricht. Ansonsten ist sie 0. Bei der Y-Position wird die ganzzahlige Teilung durch 2 verwendet, die dafr sorgt, dass benachbarte Zahlen den gleichen Wert erhalten. Auf diese Weise hat der Baumstamm 2 die Koordinate (0, 330) und der Baumstamm 3 die Koordinate (320, 330). Der dritte Parameter ist das Image und der vierte der Inkrement. Da der Nullpunkt links oben ist, sind die oberen Baumstmme etwas schneller als die darunterliegenden. Auch hier wird mit der ganzzahligen Division gearbeitet. Reihenfolge Die Adressen der Bume werden nach ihrer Positionierung gleich in das Array
Figur gelegt. Danach wird die Adresse des Eichhrnchen in das Array Figur ein-

gefgt. Die Reihenfolge ist durchaus wichtig, denn die Elemente in Figur werden bei jedem Durchlauf in der dort angelegten Reihenfolge gezeichnet. Wenn die Figuren bereinandergezeichnet werden, wird das zuerst erscheinende Bild von dem folgenden verdeckt. Das Eichhrnchen muss also nach den Baumstmmen gezeichnet werden, damit es auf ihnen sitzen kann. Dagegen sollten die Autos nach dem Eichhrnchen gezeichnet werden. Sollten sie sich berlappen, wurde das Eichhrnchen gerade berfahren. Und es wirkt sehr viel glaubwrdiger, wenn das Eichhrnchen unter dem Auto liegt und nicht darauf sitzt.
SquirrelFigur.set(0, Spielfeld.GetHeight() - imgSquirrel.GetHeight(), &imgSquirrel, 0); Figur[MaxBaum] = &SquirrelFigur; for (int i=0; i<MaxAuto; ++i) { if (i%2==1) { // Richtung der Fahrt

267

12

Der Frosch und das Eichhrnchen

Auto[i].set(0, 50, &imgAutoLinks, -15); } else { Auto[i].set(0, 50, &imgAutoRechts, 15); } Figur[MaxBaum+1+i] = &Auto[i]; } Auto[2].xPos = Spielfeld.GetWidth()/2; Auto[1].setInkrement(-11); Auto[1].yPos = 100; Auto[3].setInkrement(-7); Auto[3].yPos = 150; }
Listing 12.16 Die Funktion bereiteFeld

Schlielich werden noch die Autos positioniert und in die richtige Richtung gebracht. Auch hier wird die Modulo-Rechnung ausgenutzt.

12.5

Die Zeit setzt in Bewegung

Bei diesem Spiel bewegen sich Autos, Baumstmme und natrlich das Eichhrnchen. Damit diese Bewegung gleitend wirkt, mssen die Figuren mindestens jede Zehntelsekunde bewegt werden. Der Zeitabstand zwischen zwei Timer-Ticks muss so bemessen sein, dass auf der einen Seite die Bewegungen mglichst gleichmig erscheinen und auf der anderen Seite die Aufgaben erledigt sind, bevor der nchste Tick ankommt. Durch einen erhhten Takt wird auch die Systemlast etwas erhht, allerdings ist dies bei unserem Programm noch nicht kritisch. Ein Timer wird unter wxWidgets als wxTimer-Variable angelegt. Er wird bei der Initialisierung dem Rahmenfenster mit einer ID zugewiesen. Die ID muss sich von allen anderen Timern des Programms unterscheiden. Das geschieht im Konstruktor des Rahmenfensters im Initialisierer. Dann wird das Timer-Ereignis mit der Funktion OnTimer verbunden. Im ersten Schritt wird fr jede bewegliche Figur die eigene Funktion bewege aufgerufen. Anschlieend muss sondiert werden, wo die Figuren hingelaufen sind. Eventuelle Bewegungen des Eichhrnchens werden nun erst einmal gestoppt. Es wird geprft, ob sich das Tier im Fluss bendet. In diesem Fall muss weitergetestet werden, ob es sich auf einem der Bume bendet. Sollte das nicht der Fall sein, muss es wohl im Wasser liegen und ist damit aus dem Rennen. Die boolesche Variable imWasser berwacht dies. Sollte eine berschneidung mit einem Baum

268

Die Zeit setzt in Bewegung

12.5

vorliegen, wird die Variable auf false gesetzt, und das Eichhrnchen bernimmt den Inkrement des Baumes, auf dem es sitzt. Auf diese Weise wird es sich mit der gleichen Geschwindigkeit beim nchsten Timertick weiterbewegen wie der Baum, so dass die Illusion erweckt ist, es sitzt auf dem Baum.
void squirrelFrame::OnTimer(wxTimerEvent& event) { for (int i=0; i<MaxFigur; i++) { Figur[i]->bewege(); } // Wo ist das Eichhoernchen? Kollisionen? SquirrelFigur.setInkrement(0); // noch keine Bewegung bool imWasser = false; // und nicht ertrunken if (SquirrelFigur.imFluss()) { imWasser=true; for (int i=0; i<MaxBaum; i++) { if (SquirrelFigur.faehrtBaum(Baum[i])) { // Gerettet: Es sitzt auf einem Baum. SquirrelFigur.setInkrement(Baum[i].getInkrement()); imWasser = false; // also nicht im Wasser // jetzt aber raus hier, sonst fliegt er in // der nchsten Schleife vielleicht doch // noch ins Wasser. Es sind ja schlielich // immer zwei Baumstaemme auf jeder Bahn. break; } } } if (imWasser) { wxMessageBox(wxT("Squirrel ist ertrunken!" ), wxT("Oh, wie schade!"), wxOK|wxICON_INFORMATION, this ); bereiteFeld(); } for (int i=0; i<MaxAuto; i++) { if (SquirrelFigur.istKollision(Auto[i])) { wxMessageBox(wxT("Squirrel wurde berfahren!"), wxT("Oh, wie schade!"), wxOK|wxICON_INFORMATION, this); bereiteFeld(); } } Refresh(); // Erzwinge Aufruf von OnPaint }
Listing 12.17 Ein Timer wird gefangen.

269

12

Der Frosch und das Eichhrnchen

Dann wird geprft, ob es ein Treffen mit einem Auto gegeben hat. Auch das wrde zum Ende des Spiels fhren. Nachdem alles prpariert ist, kann durch die Funktion Refresh das Neuzeichnen des Spielfelds ausgelst werden. Thread oder Timer? Da alle Bewegungen der Figuren scheinbar zeitgleich passieren und sich die Figuren dabei den Eindruck erwecken, sich autonom zu bewegen, kommt sofort der Gedanke auf, dass ein eigener Thread dafr erstellt werden sollte. Der Haken ist, dass die Bewegungen synchronisiert werden mssen, sonst fahren die Autos auf neueren Rechnern deutlich schneller als auf lteren Modellen. Es wrde also dennoch der Timer bentigt, um die Threads zu synchronisieren. Ein Thread ist immer dann ntig, wenn bei einem Programm die Berechnungen umfangreicher werden. Das Programm muss jederzeit in der Lage sein, auf die Ereignisse der graschen Oberche zu reagieren. Sie haben Programme, die es verabsumten, ihre Berechnungen in Threads abzulegen, sicher schon erlebt. Man erkennt sie daran, dass nach dem Minimieren und Maximieren des Fensters der Inhalt nicht wieder aufgebaut wird, sondern einfarbig aussieht.

12.6

Malerei

Die eigentliche Darstellung erfolgt durch ein Paint-Ereignis. Der Vorgang ist auf allen gngigen graschen Oberchen gleich. Wird ein Fenster berdeckt und spter wieder freigegeben, sendet das System dem Programm die Aufforderung, den Bildschirm neu zu zeichnen. Diese Nachricht kann aber auch durch den Aufruf der Funktion Refresh ausgelst werden, so wie es hier die Funktion OnTimer tut. In unserem Fall bernimmt die Funktion OnPaint das Zeichnen. Als Erstes wird der Device-Context ermittelt. Da die Zeichenfunktionen auch auf einem Drucker zeichnen knnten, brauchen sie diesen, um die hardwaretechnischen Gegebenheiten zu erfahren. Dann wird ein Clipping deniert. Das sorgt dafr, dass nur im Rahmen des Hintergrundbildes gezeichnet wird. Dieser Mechanismus ist sehr praktisch, wenn ein Auto oder ein Baum durch das Bild geschoben wird. Damit der Gegenstand nicht schlagartig auf der linken Seite erscheint, wird er links vom Bild gezeichnet und Stck fr Stck ins Bild geschoben. Durch das Clipping wird der Teil des Gegenstandes abgeschnitten, der auerhalb des Bildes steht. So iet der Gegenstand frmlich ins Bild und konsequenterweise auf die gleiche Form wieder hinaus.

270

Malerei

12.6

void squirrelFrame::OnPaint(wxPaintEvent& evt) { wxPaintDC dc(panel); // zeichne nur auf dem Bereich der Landschaft dc.SetClippingRegion(0,0,Spielfeld.GetWidth(), Spielfeld.GetHeight()); // zeichne immer einen weiteren Hintergrund static int step=0; step++; if (step>4) step=0; dc.DrawBitmap(imgBack[step], 0, 0); // Zeichne Baeume, Autos und auch das Eichhoernchen for (int i=0; i<MaxFigur; i++) { Figur[i]->draw(dc); } }
Listing 12.18 Die Funktion OnPaint

Der Hintergrund wird gezeichnet. Wegen des Wassers sind es fnf Bilder, die sich abwechseln. So ist nach einer viertel Sekunde die Welle wieder beim Ausgangsbild angekommen. Durch das bermalen des Hintergrundbildes werden alle bisherigen Zeichnungen des Fensters berklatscht. In einer Schleife werden alle Figuren gezeichnet, die sich auf dem Bildschirm bewegen. Das Array Figuren enthlt Zeiger auf alle Figuren. Deren Elementfunktion draw erledigt diese Aufgabe. Flackerei Wenn Sie dieses Spiel unter Windows laufen lassen, werden Sie ein Flackern bei den Figuren bemerken. Von Zeit zu Zeit luft sogar ein ackernder Balken durch das Bild. Dieser Effekt ist nicht vllig neu. Das Neuzeichnen des Spielfelds hat in frheren Zeiten oft zu einem Flackern des Bildschirms gefhrt. Dafr gab es unterschiedliche Grnde. Beispielsweise war der Sinclair ZX-80 (siehe Seite 337) so billig aufgebaut, dass er nicht einmal einen Grakprozessor hatte. Das hatte zur Konsequenz, dass der Bildschirm immer dann schwarz wurde, wenn die CPU beschftigt war. Beim Nachfolger ZX-81 hatte man zwar immer noch keinen Grakprozessor eingebaut, aber der Programmierer konnte das Gert in den Slow-Mode schalten. Dann wurden die Berechnungen nur in der Austastlcke des Fernsehers durchgefhrt. So wurde dem Anwender das wilde Geacker erspart. Auch andere Gerte dieser Zeit hatten Probleme mit ackernden Bildschirmdarstellungen. Zwar schaltete dort nicht gleich der ganze Bildschirm ab, aber beim

271

12

Der Frosch und das Eichhrnchen

Neuzeichnen der Spielche fhrte das Verschwinden und Neuzeichnen der Spielguren dazu, dass diese ackernd ber den Bildschirm zogen. Zu Zeiten des C-64 oder Apple II hatten die Computer einen Takt von einem MHz. Da dauerte das Neuzeichnen der Figuren eben manchmal seine Zeit. Seinerzeit wurde konsequent vermieden, dass auf dem Bildschirm mehr verndert wurde als unbedingt erforderlich. Es wurde also nicht der komplette Hintergrund neu gezeichnet. Wenn sich eine Figur vernderte, wurde der Hintergrund der Figur beim Zeichnen gesichert und wieder darber gezeichnet, bevor die vernderte Figur neu gezeichnet wurde. In spteren Zeiten kamen spezielle Grakprozessoren auf den Markt, die Blitter hieen. Sie waren in 16-Bit-Modellen Amiga und Atari ST gngig, die immerhin schon einen Takt von 8 MHz hatten. Sie waren in der Lage, schnell rechteckige Bereiche auf Bildschirm zu verschieben. Um hier das Flackern zu unterbinden, arbeitete man mit einem Speicherbereich, der logisch wie der Bildschirmspeicher ansprechbar war. Hier wurde in Ruhe das neue Bild aufgebaut, um es dann schlagartig mit Hilfe des Blitters schnell in den Bildschirmbereich zu kopieren. Man spricht in diesem Zusammenhang von Doppelpufferung (double buffering). Diesen Effekt verwenden wir auch hier, um das Spiel unter Windows ackerfrei zum Laufen zu bringen. Dazu muss zunchst verhindert werden, dass der Hintergrund vom System neu gezeichnet wird. Genau das ist der Grund, warum die Frameklasse dieses Ereignis fngt und auf eine eigene Funktion umleitet. Hier wird nmlich nichts getan, einfach gar nichts. Damit ackert es schonmal nicht mehr, wenn der Hintergrund das bisherige Bild lscht und das neue Hintergrundbild darber geladen wird. Im nchsten Schritt wird eine Pufferung der Zeichnungen durchgefhrt. Das bedeutet, dass der Bildschirm im Hintergrund in einem Speicher gezeichnet wird und dann, wenn alles fertig ist, mit einem Schlag auf den Bildschirm gebracht wird. Um das unter wxWidgets zu erreichen, braucht nur ein Device Context vom Typ wxBufferedPaintDC statt einer vom Typ wxPaintDC verwendet werden. Deklariert wird wxBufferedPaintDC in der Datei wx/dcbuffer.h, die natrlich eingebunden werden muss. Den Rest bernimmt dann dankenswerterweise wxWidgets.
void squirrelFrame::OnEraseBackground(wxEraseEvent& event) { } #include <wx/dcbuffer.h> void squirrelFrame::OnPaint(wxPaintEvent& evt) {

272

Angetastet

12.7

wxBufferedPaintDC dc(panel); // Hintergrund herstellen wxRect rect(wxPoint(0, 0), GetClientSize()); wxColour back = GetBackgroundColour(); dc.SetBrush(wxBrush(back)); dc.SetPen(wxPen(back, 1)); dc.DrawRectangle(rect); // normal weiterzeichnen
Listing 12.19 Hintergrundneuzeichnen verhindern

Ach ja, eine Kleinigkeit wre da noch: Da der Hintergrund nicht mehr vom System gelscht wird, muss das nun die Zeichenfunktion tun. Dazu wird, bevor das Hintergrundbild gezeichnet wird, ein Rechteck in Hintergrundfarbe auf den gesamten Client-Bereich des Fensters gelegt. Da dies im Hintergrund passiert, sorgt es nun nicht mehr fr das aufgeregte Flackern. Danach wird normal weitergezeichnet, wie ohne Pufferung.

12.7

Angetastet

Das Spiel wird ber die Tastatur gesteuert. Die GUI liefert bei einem Tastendruck sowohl eine Nachricht, wenn die Taste gedrckt wurde, als auch wenn sie losgelassen wird. Das Spiel interessiert sich nur dafr, wann eine Taste gedrckt wurde. Aus der Parametervariablen kann der Code der Taste ermittelt werden. Fr die Spezialtasten verwendet wxWidgets besondere Konstanten. Die Funktion reicht die Information ber die gedrckte Taste direkt an die zugehrige Elementfunktion des Eichhrnchens, das die Bewegung fr das nchste Neuzeichnen vorbereitet.
void squirrelFrame::OnKeyDown(wxKeyEvent& event) { int key = event.GetKeyCode(); if (key == WXK_UP || key == 'I') { if (SquirrelFigur.up()) { // oben angekommen? Gratuliere! wxMessageBox(wxT("Squirrel hat berlebt!" ), wxT("Hat doch was!"), wxOK|wxICON_INFORMATION, this); bereiteFeld(); } } else if (key == WXK_LEFT || key == 'J') { SquirrelFigur.left();

273

12

Der Frosch und das Eichhrnchen

} else if ( key == WXK_RIGHT || key == 'K') { SquirrelFigur.right(); } else if (key == WXK_DOWN || key == 'M') { SquirrelFigur.down(); } event.Skip(); }
Listing 12.20 Reaktion auf die Tastatur

Wenn allerdings das Eichhrnchen jenseits der Strae angekommen ist, hat es das Spiel gewonnen.

12.8

Spielende

Es gibt drei Situationen, die das Spiel beenden: Das Eichhrnchen ertrinkt im Fluss, es wird berfahren, oder das Eichhrnchen schafft es, am oberen Rand des Spielfelds anzukommen. Beim Privatfernsehen kme nun die Frage, in welchem Fall der Spieler wohl gewonnen htte, fr nur 50 Cent pro Minute aus dem deutschen Festnetz. Nasser Tod Um festzustellen, ob das Eichhrnchen ertrinkt, muss es zunchst einmal in der Hhe des Flusses sein. Anschlieend muss kontrolliert werden, ob es sich auf einem Baumstamm bendet. Dann ist es nmlich gerettet und bernimmt das Inkrement des Baumstammes, sodass es so aussieht, als ob es auf dem Baumstamm schwimmt. Ob das Eichhrnchen auf dem Baumstamm sitzt, wird etwas grozgiger bemessen. Es darf um eine halbe Breite ber den Stamm hinausragen. Kollision Ein Treffer mit einem Auto ist tdlich. Hier wird ein exakter Vergleich durchgefhrt. Sobald eine berdeckung zwischen dem Rechteck des Eichhrnchens und des Autos vorliegt, ist der Tod des Tieres amtlich. Sieg Der Sieg wird recht einfach durch die Y-Position ermittelt. Ist das Eichhrnchen am obersten Rand angestoen, hat es gewonnen.

274

Was denn noch?

12.9

Dialog und Neustart All diese Situationen werden in der vorliegenden Spielversion durch eine Dialogbox kommentiert, und nachdem der Spieler diese mit einem Druck auf den OkButton besttigt hat, beginnt das Spiel von vorn, indem bereiteFeld aufgerufen wird.

12.9

Was denn noch?

Dieses Spiel bietet viele Mglichkeiten der Erweiterung. Der Sieg wird erreicht, wenn das Eichhrnchen oben ankommt. Beim Originalspiel sind Krbe aufgestellt, die erreicht werden mssen. Erst wenn alle fnf Zielkrbe besetzt sind, war der Spieler erfolgreich. Grak verndern Um das Spiel schwieriger zu gestalten, knnen Sie die Baumstmme verkrzen. Das ist mit einem Grakprogramm schnell gemacht. Wenn Ihnen die Autos nicht gefallen, knnen Sie selbst welche gestalten. Allerdings sollten Sie darauf achten, dass Ihr Grakprogramm auch transparente Bereiche bei BMP-Dateien korrekt behandelt. Falls Sie das Eichhrnchen weniger gelungen nden oder Ihnen bei dem Gedanken an den Tod eines so possierlichen Tieres die Trnen in die Augen steigen, knnen Sie einen Hamster oder eine Katze gestalten und diese in den Tod treiben. Level Es lassen sich mehrere Level einfhren. So knnte je nach dem Level die Anzahl der Autos erhht werden oder die Geschwindigkeit gesteigert werden. Sie knnen die Autos per Zufall immer wieder schneller oder langsamer fahren lassen. Sie mssen nur darauf achten, dass Sie alle Autos einer Spur gleichmig beschleunigen. Gegen die Zeit Wenn man schon einmal einen Timer hat, ist es naheliegend, gegen die Zeit zu spielen. Dazu sollte die Zeit auf dem Bildschirm anzeigen. Und wenn man schon einmal die Zeit erfasst, bietet sich ein Highscore an.

275

Computer knnen nicht nur mit dem Ventilator Gerusche machen.

13

Musik ist mit Wellen verbunden

Die Verbundenheit der Computer mit der Musik hat Tradition. In den 1980er Jahren, als die ersten Home-Computer aufkamen, hatte jeder Computerbesitzer einen Kassettenrecorder neben seinem Rechner stehen. Zugegebenermaen benutzte er diesen nicht fr die Musik, sondern damals waren Audiokassetten das Medium, um Daten und Programme zu speichern. Die Speicherung auf Musikkassetten funktionierte unterschiedlich gut. Mein allererster Computer war ein Ohio-Scientic C4P. Er schrieb keine Prfsumme auf die Kassetten, sodass beim Einlesen immer wieder mal ein Bit unkontrolliert umkippte. Das bedeutete, dass ich vor dem Start der Programme zunchst durch das Listing ging und Fehler korrigierte. Dies war einer der Grnde, der mich schnell zum Apple II wechseln lie. Zunchst verwendete ich auch dort den Kassettenrecorder, wechselte aber schnell zur Floppy Disk. Eine Diskette kostete 10 DM, wenn man sie preiswert bekam und das Laufwerk konnte ich als Schnppchen fr 800 DM von einem Freund gebraucht kaufen. Der Apple II als Tonstudio Da die Daten nun auf den Disketten gespeichert wurden, war der Kassettenrecorderanschluss nun frei fr andere Experimente. Und so kam es, dass ein Freund ein ganz tolles kleines Assembler-Programm vorfhrte. Dieses Programm las den Port des Kassetteneingangs aus und speicherte dessen Wert fortlaufend in den Hauptspeicher, der beim Apple bis zu 64 KByte gro war. Anschlieend konnte das Programm den Speicherinhalt Byte fr Byte auf den Port des eingebauten Lautsprechers geben. Dieser Lautsprecher hatte normalerweise die Aufgabe, bei Fehlern einen herzzerreienden Piepton von sich zu geben. Das Programm wurde gestartet und ber den Kassettenrecordereingang eine Musikkassette eingespielt. Nachdem der Speicher voll war, hrten wir das erste Mal digitalisierte Musik. Der Speicher reichte, um den Refrain des Songs Bruttosozialprodukt in einer Qualitt zu spielen, dass es jedem Telefon peinlich gewesen wre. Aber wir saen mit verzckten Gesichtern da und waren begeistert.

277

13

Musik ist mit Wellen verbunden

Dieser Moment el mir ein, als ich das erste Mal meine gesamte CD-Sammlung auf einer Festplatte meines Computers speicherte, weil ich eher dort Musik hre als im Wohnzimmer. Mit meinen Ohren war der Klangunterschied inzwischen nicht mehr zu hren, und ich kann in Sekundenschnelle ein beliebiges Stck starten oder eine ganze Liste durchlaufen lassen. Einer der wichtigsten Pioniere im Musikbereich war der Atari ST. Zu dieser Ehre kam er in erster Linie dadurch, dass er serienmig mit einer MIDI-Schnittstelle ausgestattet war.1 Eine MIDI-Schnittstelle ermglicht es, einen Synthesizer oder ein Keyboard fernzusteuern. Alle Tasten, die der Keyboarder spielt, knnen aufgezeichnet und spter wieder abgespielt werden. Schnell entstanden SoftwareLsungen, mit denen komplette Arrangements auf dem Computer erstellt und bearbeitet werden konnten.

Abbildung 13.1 Atari 1040ST (Foto: Bill Bertram, 2006 nach Creative Commons Lizenz freigegeben)2

Die MIDI-Schnittstelle fhrte den Atari ST zwar in die Musikstudios, sie war allerdings nicht geeignet, um Musik zu digitalisieren. Dazu war die Hardware auch noch nicht leistungsfhig genug. Konsequenterweise wurde darum der schnellere Nachfolger der ST-Serie, der Atari Falcon, mit einem digitalen Soundprozessor (DSP) ausgeliefert, mit der er Musik in CD-Qualitt digitalisieren konnte. uerlich unterschied sich der Falcon vom Atari 1040ST nur durch die etwas dunklere
1 Ja, ich wei, es gab auch schon MIDI-Karten fr den C-64. 2 http://de.wikipedia.org/wiki/Datei:Atari_1040STf.jpg

278

Musik digitalisieren

13.1

Tastatur. Von seiner Ausstattung her war er einer der ersten Multimedia-Computer. Heute steht im Proberaum unserer Band wie selbstverstndlich ein Computer, der mittels Software ein Mehrspurtonband simuliert und es auch Amateuren fr kleines Geld ermglicht, unter studiohnlichen Bedingungen aufzunehmen.

13.1

Musik digitalisieren

Die Digitalisierung von Musik arbeitet nach einem einfachen Prinzip. Das Tonsignal wird in kurzen Intervallen abgetastet. Dabei interessiert nur der Ausschlag des Pegels. Die Intervalle sind dagegen so kurz, dass auch kurze Wellen mehrere Messpunkte erhalten. Die Geschwindigkeit dieser Abtastrate ist die Sample-Rate. Bei einer CD liegt sie bei 44.100 Hz. Da eine komplette Welle eine positive und eine negative Halbwelle hat und beide erfasst werden mssen, um ein Signal zu einzulesen, muss man diesen Wert halbieren, um die maximal abtastbare Frequenz zu ermitteln. Weil das menschliche Ohr bei Jugendlichen bis zu 16.000 Hz hren kann und die Hrfhigkeit in den oberen Frequenzen mit dem Alter stetig abnimmt, gilt diese Abtastrate als ausreichend.

Abbildung 13.2 Digitalisierung einer Welle

Abbildung 13.2 zeigt, wie die Digitalisierung funktioniert. Im Takt der Abtastrate wird die Strke des Signals gemessen. In der Grak stehen dazu 8 Bit, also die Werte von 0 bis 255 zur Verfgung. Die senkrechten Striche sollen die einzelnen Abtastungen darstellen. Die Sinuskurve beginnt mit 0, und bei dem vierten Strich

279

13

Musik ist mit Wellen verbunden

wrde etwa der Wert 80 vorliegen. In ihrem Maximum hat die Kurve einen Wert von 190. Bei jeder Abtastung gibt es also einen neuen Zahlenwert. Wenn diese Zahlenwerte in der gleichen Geschwindigkeit auf einen Lautsprecher gegeben wrden, wrde die Kurve also wieder erstehen. Aufgrund der normalen Trgheit eines Lautsprechers wrde man nicht einmal hren, dass die Kurve quasi digital zerhackt wurde. Im Gegenteil litt die analoge Audiotechnik darunter, dass fast alle Tontrger und Tonwandler die Wellen vernderten. Die Magnettonbnder zeichnen die Wellen bereits leicht verzerrt auf und verlieren allein durch Lagerung die hohen Frequenzen. Die Vinylplatten, die heute ein groes Comeback feiern, werden durch jedes Abspielen leicht abgeschliffen. Jeder elektronische Baustein, der zwischen der Originalschallquelle und dem Ausgabesignal steht, quetscht, staucht oder verzerrt die Welle. Bei der digitalen Tonaufzeichnung durchluft das Signal zunchst einen AnalogDigital-Wandler und wird danach als eine Folge von Zahlen gespeichert. Eine 80 bleibt immer eine 80, und eine 190 bleibt immer eine 190. Ein digitales Mischpult wird immer klarer und durchsichtiger klingen als ein analoges, weil das digitale Mischpult die Zahlen miteinander verrechnet und dabei die Kurven nicht verndert. Ein analoges Mischpult wird bei der Mischung zweier Quellen immer eine leichte Vernderung der Kurve herbeifhren. Lediglich bei der bersteuerung sieht die Sachlage anders aus. Wird ein digitalisiertes Audiosignal zu laut aufgenommen, hrt sich das Ergebnis besonders scheulich an. Eine analoge bersteuerung mit Transistoren fhrt zu einer scharfen, unangenehmen Verzerrung. Wird eine Rhre bersteuert, klingt die Verzerrung dagegen weicher und voluminser. Das ist auch der Grund, warum Gitarristen auch heute noch gern mit Rhrenverstrkern arbeiten. Der verzerrte Klang einer Hard-Rock-Gitarre wird eben durch bersteuerung erzeugt. Zum Abmischen der Aufnahmen verwendet der Musiker dann allerdings in der Regel lieber eine digitales System.

13.2

Musik und Gerusche abspielen

Wenn Sie den James-Bond-Film Im Geheimnis Ihrer Majestt gesehen haben, erinneren Sie sich vielleicht noch an die Erffnungsszene. Darin kommt es zu einer Schlgerei am Strand, in deren Verlauf Diana Rigg in den Aston Martin von James Bond springt und mit quietschenden Reifen vom Strand wegfhrt. Moment! Mit quietschenden Reifen? Ja, die Reifen quietschen, whrend man deutlich sieht, wie der Sand aufwirbelt.

280

Musik und Gerusche abspielen

13.2

Ein Freund von mir ist Sounddesigner und der erklrte mir, dass es keineswegs ein Versehen gewesen sein wird. Der Tontechniker wird auch gewusst haben, dass die meisten Autoreifen auf Sand keine Quietschgerusche verursachen. Aber das Quietschen der Reifen ist genau das Gerusch, das der Zuschauer erwartet, wenn ein Auto schnell beschleunigt wird. Er wre enttuscht, es nicht zu hren. Die Flucht von Diana Rigg vermittelte nicht die richtige Dramatik, wenn die quietschenden Reifen nicht zu hren wren. Entsprechendes gilt auch fr Computerspiele. Ohne Gerusche fehlt die Dynamik. Das springende Eichhrnchen aus Kapitel 12 sollte das Gerusch einer rostenden Feder verursachen. Kein Spieler will wissen, dass Eichhrnchen fast vllig geruschfrei durch den Wald hpfen knnen. Was dem einen seine quietschenden Reifen sind, ist dem anderen sein Doing. Der Computerspieler erwartet, dass jeder Schuss ein Gerusch erzeugt, dass Mnnchen mit Hut beim Laufen dudeln und der Sieger durch eine passende Fanfare geehrt wird. Aus diesem Grund ist es nicht verwunderlich, dass das Paket wxWidgets, mit dem die Grak in diesem Buch umgesetzt wurde, auch eine Mglichkeit anbietet, Gerusche zu erzeugen. Als Basis dient die wxSound-Bibliothek, die vor allem erlaubt, WAV-Dateien abzuspielen. Ein Programm, das wxSound nutzen will, muss zunchst eine weitere Header-Datei einbinden.
#include <wx/sound.h>

Vor dem Abspielen wird ein Objekt vom Typ wxSound mit einer WAV-Datei verbunden.
wxSound *sound = new wxSound(wxT("hupf.wav"));

Damit sind alle Vorbereitungen getroffen. Der Klang kann dann mit der Elementfunktion Play abgespielt werden.
sound->Play();

Wenn Sie in Ihrem neuesten Spiel immer dann, wenn Winnetou auftritt, die berhmte Melodie von Martin Bttcher abspielen, mssen Sie diese vielleicht stoppen mssen, wenn der bse Feind am Horizont erscheint. Mit dem Aufruf der Funktion isPlaying wissen Sie, ob der Sound noch gespielt wird, mit der Funktion Stop knnen Sie den Sound anhalten. Threads als Hintergrundmusiker Das Abspielen des Sounds geschieht in der Regel im Hintergrund. Bei meinen Experimenten hat das aber nicht klaglos funktioniert. Das Starten und Stoppen des Threads hat das Spiel kurzzeitig blockiert. So etwas macht einen schlechten

281

13

Musik ist mit Wellen verbunden

Eindruck. In solchen Fllen msste das Abspielen des Sounds in einem eigenen Thread durchgefhrt werden, damit das Spiel nicht ruckelt, wenn der Klang abgespielt wird. Glcklicherweise bietet die wxWidget-Bibliothek auch die Mglichkeit an, Threads zu realisieren. Dadurch bleibt das Programm trotz Thread portabel.
wxSound *sound; class soundHupfThread : public wxThread { public: soundHupfThread() { Create(); } virtual ExitCode Entry() { sound->Stop(); sound->Play(wxSOUND_ASYNC); return 0; } }; soundHupfThread *soundHupf; void doHupfSound() { soundHupf = new soundHupfThread(); soundHupf->Run(); }
Listing 13.1 Sounds aus dem Thread

Diese Lsung ist etwas einfach gestrickt, damit das Prinzip erkennbar bleibt. Der eigene Thread wird von der Klasse wxThread abgeleitet. Die Elementfunktion Entry enthlt den Code, den der Thread ausfhrt. Dieser besteht einfach nur darin, den Sound zu stoppen, falls er vom letzten Aufruf noch luft und ihn dann noch einmal zu starten. Der Konstruktor startet die Erzeugung des Threads. Die Funktion soundHupf erzeugt eine neue Instanz des Threads und startet ihn. Geruschquellen Wenn Sie ein Spiel untermalen wollen, mssen Sie natrlich auch an die Sounds kommen. Der einfachste Weg ist der Anschluss eines Mikrofons und die direkte Aufnahme. Jedes Betriebssystem liefert ein kleines Programm, mit dem Sie kleine Wave-Dateien aufnehmen knnen. Alle Gerusche, die Sie direkt erzeugen und aufnehmen knnen, stehen Ihnen so auf einfache Weise zur Verfgung.

282

Synthesizer

13.3

Zum Schneiden knnen Sie das Programm Audacity verwenden. Sie nden es kostenlos im Internet unter der folgenden URL: http://audacity.sourceforge.net Eine interessante Quelle sind auch freie Klingeltne, die Sie im Internet mithilfe einer Suchmaschine leicht nden knnen. Da nden Sie von Pferdegewieher ber Explosionen bis zu startenden Autos alles, was das Ohr begehrt. Schneiden Sie sich die Teile heraus, die Sie fr Ihr Spiel bentigen. Und falls Sie einen Bekannten haben, der ein modernes Keyboard besitzt, werden Sie feststellen, dass darauf diverse Klnge wie Meeresrauschen, Hubschrauber, Maschinengewehrfeuer oder Applaus zu nden ist. Das nehmen Sie einfach mit dem Notebook auf und schneiden es mit Audacity.

13.3

Synthesizer

Wenn der Ton vom Knatternden ins Heulende dreht und dabei ein Puls vom linken zum rechten Lautsprecher in immer greren Tempo hin und her wechselt, der Bass im Zwerchfell drckt und gleichzeitig kristallene Obertne in den Ohren klingen, sollten Sie vielleicht ganz schnell den Radio-Fensehtechniker anrufen. Das knnten die letzten Zuckungen Ihres Fernsehers oder Ihrer Stereoanlage sein. Oder Sie hren einen Synthesizer. Synthesizer sind rein elektronische Instrumente, die frei von einem natrlichen Vorbild Klnge erzeugen knnen, fr die es in der Natur kein Vorbild gibt. Der Klang entsteht auf der Basis reiner Sinuswellen, die durch Mischung oder Modulation verbunden werden und dadurch einen Klang entstehen lassen. Solche Klnge knnen auch vom Computer erzeugt werden, indem die Wellen errechnet werden und an die Soundkarte weitergeleitet werden. In diesem Abschnitt wird zunchst eine Sgezahnwelle und dann eine Sinuswelle erzeugt. Fr solche Zwecke bentigen wir den Zugriff auf die Soundkarte. Diesen Zugriff liefert plattformbergreifend die Bibliothek PortAudio, die als freie Software zur Verfgung steht. Sie nden ausfhrliche Informationen zu dieser Bibliothek im Internet: http://www.portaudio.com Bekannt ist PortAudio durch ihre Verwendung in dem oben bereits erwhnten freien Programm Audacity, das auf verschiedenen Plattformen verfgbar ist und eine Art Schweizer Taschenmesser fr die Soundbearbeitung darstellt. Mit dem Programm knnen sowohl Musikstcke geschnitten als auch Mehrspuraufnah-

283

13

Musik ist mit Wellen verbunden

men durchgefhrt werden. Als grasche Bibliothek verwendet Audacity brigens wxWidgets. Dieses Buch bendet sich also in bester Gesellschaft. Whrend die wxWidgets-Soundbibliothek wxSound in erster Linie dazu gedacht ist, Dateien mit Standardformaten abzuspielen, geht es bei Portaudio um das Abspielen und Aufnehmen von Klngen.

13.3.1

Das Sgezahndrama

Im Gegensatz zur Sinusfunktion, die sehr weich und rund klingt, hrt sich eine Sgezahnfunktion eher nach Hard Rock an. Bse Zungen wrden den Klang mit meinem defekten Rasierapparat vergleichen. Vielleicht stimmt beides. Der Grund fr die Wahl der Sgezahnfunktion liegt aber weniger in meiner Begeisterung fr Rockmusik. Die Sgezahnfunktion ist programmtechnisch sehr viel einfacher zu erzeugen als die Sinuskurve. Falls Sie aber mehr auf der weichen Welle schwimmen, halten Sie durch! Ftterung von PortAudio Zunchst soll berhaupt ein Klang auf dem Lautsprecher ausgegeben werden. Dazu wird PortAudio durch Aufruf der Funktion Pa_Initialize initialisiert. Im nchsten Schritt wird ein Datenstrom erffnet, also quasi fr die Weiterverarbeitung angemeldet. Dieser Strom kann nun durch den Aufruf von Pa_StartStream und Pa_StopStream immer wieder beliebig oft gestartet und gestoppt werden. Das Abspielen des Streams geschieht im Hintergrund, sodass ein Stopp verzgert werden muss. Dazu gibt es die Funktion Pa_Sleep. Soll der Stream nicht mehr ausgegeben werden, wird er wieder mit dem Aufruf Pa_CloseStream geschlossen. Das PortAudio wird schlielich durch den Aufruf von Pa_Terminate beendet. Das folgende Listing zeigt diese Schritte in der Zusammenstellung.
int main() { PaStream *Stream; PaError FehlerNr; FehlerNr = Pa_Initialize(); if (istFehler(FehlerNr)) return 1; FehlerNr = Pa_OpenDefaultStream( &Stream, 0, // Input-Kanaele 2, // Output-Kanaele paFloat32, // Das Ausgabeformat SAMPLE_RATE, FRAMES_PER_BUFFER,

284

Synthesizer

13.3

meinCallback, &Data); if (istFehler(FehlerNr)) return 2; FehlerNr = Pa_StartStream(Stream); if (istFehler(FehlerNr)) return 3; // Eine Runde schlafen, waehrend der Computer musiziert Pa_Sleep(NUM_SECONDS*1000); FehlerNr = Pa_StopStream(Stream); if (istFehler(FehlerNr)) return 4; FehlerNr = Pa_CloseStream(Stream); if (istFehler(FehlerNr)) return 5; FehlerNr = Pa_Terminate(); if (istFehler(FehlerNr)) return 6; }
Listing 13.2 Grundmuster des Programms

Die Funktionen liefern eine Fehlernummer zurck, die in der Funktion istFehler behandelt wird. Die Funktion prft lediglich, ob berhaupt ein Fehler vorliegt und gibt in einem solchen Fall die passende Nachricht auf dem Bildschirm aus.
bool istFehler(PaError err) { if (err==paNoError) return false; cerr << "PortAudio error: " << Pa_GetErrorText(err) << endl; return true; }
Listing 13.3 istFehler

Stromfreigabe Viel interessanter ist der Aufruf der Funktion Pa_OpenDefaultStream. Mit dieser Funktion wird der Stream erffnet. Der erste Parameter ist die Adresse auf einen Zeiger auf einen Datenstrom. Der Grund fr diesen Doppelzeiger liegt darin, dass die Funktion Pa_OpenDefaultStream auf diesem Weg die interne Datenstruktur zur Verwaltung des Stroms an die Anwendung zurckliefert.

285

13

Musik ist mit Wellen verbunden

Die nchsten beiden Parameter geben die Anzahl der Eingangs- und Ausgangskanle an. Wenn Sie zwei Kanle angeben, verwenden Sie Stereo. Die Kombination oben gibt an, dass es keinen Aufnahmekanal aber zwei Wiedergabekanle geben soll. Es ist auch denkbar, dass Sie gleichzeitig aufnehmen und wiedergeben knnen. Eine solche Kombination ist beispielsweise interessant, wenn Sie ein Mehrspurtonband simulieren wollen. Sie hren die Aufnahme der Band ber den Kopfhrer und nehmen den Gesang ber das Mikrofon auf. Das Sample-Format gibt an, wie viele Bits Auflsung verwendet werden. Je grer dieser Wert ist, desto hher ist die Dynamik, also der Abstand zwischen kleinstund grtmglicher Lautstrke. Mit der Sample-Rate wird festgelegt, bis zu welcher Frequenz die Aufnahmen gehen knnen. Die maximal mgliche Frequenz muss dabei verdoppelt werden. Nicht bei allen Gerten sind die Sampleraten frei whlbar. Der vorletzte Parameter bestimmt die Callback-Funktion, die hier originellerweise meinCallback heit. Sie wird die Datenzubereitung fr die Soundkarte bernehmen. Als letzter Parameter werden die Benutzerdaten bergeben. Hier steht ein Zeiger auf einer Datenstruktur, die die Ein- und Ausgabedaten beschreiben. Fr die Ausgabe der Sgezahnwelle reicht eine extrem einfache Struktur.
typedef struct { float LinkePhase; float RechtePhase; } tPhaserData; static tPhaserData Data;
Listing 13.4 Daten fr die Sgezahnwelle

Ruf mich bitte an: Callback Sobald der Datenstrom gestartet wurde, wird PortAudio die Funktion aufrufen, die ihr als vorletzter Parameter von Pa_OpenDefaultStream angegeben wurde. Im Beispiel heit diese Funktion meinCallback.
static int meinCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* TI, PaStreamCallbackFlags statusFlags, void *userData) { // Die Userdaten werden ins Originalformat gebracht

286

Synthesizer

13.3

tPhaserData *Data = (tPhaserData*)userData; float *out = (float*)outputBuffer; for (unsigned int i=0; i<framesPerBuffer; i++ ) { *out++ = Data->LinkePhase; *out++ = Data->RechtePhase; // Wir generieren einen einfachen Saegezahnphaser, // der zwischen -1.0 und 1.0 laeuft. Data->LinkePhase += 0.01f; // Wenn das Signal oben ist, ziehe es nach unten. if( Data->LinkePhase >= 1.0f ) { Data->LinkePhase -= 2.0f; } // Rechts hoehere Stimmlage, zur Unterscheidung Data->RechtePhase += 0.03f; if( Data->RechtePhase >= 1.0f ){ Data->RechtePhase -= 2.0f; } } return 0; }
Listing 13.5 Der Callback

Die Callback-Funktion bekommt die beiden Pufferzeiger fr die Ein- und die Ausgabe. Da das Programm nur etwas ausgiebt, interessiert der erste Puffer berhaupt nicht. In der Variablen framesPerBuffer steht, wie viele Daten in den Ausgabepuffer in diesem Schritt geschoben werden sollen. Also wird eine Zhlschleife aufgemacht, die den Ausgabepuffer mit Werten fllt. Dabei wird abwechselnd der linke und der rechte Kanal gefllt, indem aus der Benutzerdatenstruktur der Wert fr links und rechts bernommen wird. Anschlieend wird jeder der beiden Werte erhht und in der Datenstruktur zwischengelagert. Damit die beiden Kanle unterschiedliche Signale bekommen und der Stereo-Effekt auch zu hren ist, wird links nur um 0,1 und rechts um 0,3 erhht. berschreitet ein Wert die Grenze von 1,0, wird er um 2,0 reduziert. Das Ergebnis ist eine Funktion, die linear steigt und beim Wert von 1,0 senkrecht nach unten fllt. Es ergibt sich also eine Sgezahnfunktion. Der rechte Kanal wird hher klingen als der linke, weil er schneller wechselt. Die Hhe des Tons hngt davon ab, wie eng die Welle steht, oder anders ausgedrckt, wie schnell sich die Welle wiederholt. Der Callback bekommt also nicht einen Speicherbereich, in dem die Daten an den Ausgang geschickt werden, sondern eine Datenstruktur, in der der letzte Stand

287

13

Musik ist mit Wellen verbunden

des Ausgabewerts steht. Da es zwei Kanle gibt, heien die Werte LinkePhase und RechtePhase.

13.3.2

Gemeinheiten des Soundsystems

Sie nden auch hier alle Quelltexte auf der beiliegenden CD. Sie bentigen noch die Bibliotheken, die Sie je nach System von der CD holen oder aus dem Internet herunterladen. Dann bedarf es ein paar Kongurationshandgriffe, fr die Sie im Anhang noch einige Hinweise nden. Sie starten das endlich komplett bersetzte Programm, und es passiert gar nichts. Manche Menschen werden in solch einem Moment von dem Wunsch beseelt sein, etwas in die Ecke zu werfen. Sie sollten dann das Buch werfen und nicht ihre Computer-Hardware. Der Galileo-Verlag ist seit Jahren darin erfahren, Bcher zu produzieren, die einen solchen Wurf berstehen, ohne dass Buchstaben herausfallen. Ihre Hardware knnte aber bei mechanischer Beschleunigung dauerhaften Schaden erleiden. Dieser wre sicher sehr viel teurer als das Buch. Wenn Sie wieder etwas ruhiger geworden sind, darf ich Sie damit trsten, dass das Soundsystem eines Computers durchaus nicht immer trivial installierbar ist und der Grund des Versagens in der Konguration liegen kann. Bei meinen ersten Versuchen mit dem PortAudio-System habe ich eine ganze Weile mit dem System gehadert, weil keine Aufnahme klappen wollte. Ich war sicher, dass alle Funktionen korrekt parametrisiert waren. Dann fand ich heraus, dass in den Tiefen der Soundeinstellungen ein Regler falsch eingestellt war und darum keine Aufnahmen funktionierten. Ich hatte stattdessen immer weiter an meinem Programm geschraubt. Achten Sie also darauf, dass alle Regler in der Soundkartenkonguration, die infrage kommen, auch aktiviert sind und nicht auf 0 stehen. Schauen Sie dann nach, ob an Ihrem Gert ein Kopfhrer oder bei Aufnahmen ein Mikrofon angeschlossen ist. Manche Kopfhrer haben eingebaute Lautstrkeregler. Prfen Sie auch, ob Kopfhrer oder Mikro an einem anderen Gert funktionieren. Sobald Sie eine Tonquelle haben, sollten Sie mit dem Aufnahmeprogramm, das Ihrem Betriebssystem beiliegt, ausprobieren, ob die Tne auch wirklich Ihren Computer erreichen. Diverse Einstellungen knnen verhindern, dass der Computer etwas hrt. Und es ist einfach rgerlich, wenn Sie die Programme seit Stunden durch den Debugger schicken und dann merken, dass der Mikrofoneingang bei diesem Gert als Lautsprecheranschluss fr Surround-Sound 5.1 missbraucht wird.

288

Synthesizer

13.3

13.3.3 Die weiche Welle


Im nchsten Schritt werden wir uns an eine Sinuswelle wagen. Der Unterschied zu der Sgezahnwelle liegt natrlich im Klang, aber vor allem darin, dass der Wellenverlauf nun vor der Ausgabe im Speicher berechnet wird und dann der Speicher an die Soundkarte bertragen wird. Wenn die Ausgabe der Sinuswelle gelingt, dann kann die Welle auch manipuliert werden. Prinzipiell knnten Sie dann aus der Sinuswelle einen eigenen Synthesizer stricken. Zunchst wird ein kleines Array angelegt, das im Hauptprogramm mit einer Sinusfunktion gefllt wird. Diese Werte mssen dann in der Callback-Funktion ausgelesen und in den Datenstrom gefllt werden. Die Datenstruktur fr die Callback-Funktion enthlt ein Array von Fliekommawerten fr die Sinuskurve. Dazu kommen Indizes fr den linken und rechten Kanal, damit die Callback-Funktion festhalten kann, wo sie das letzte Mal mit dem Abspielen aufgehrt hat.
const int MAXWELLE=256; class tSoundData { public: float Welle[MAXWELLE]; int LinksIndex; int RechtsIndex; };
Listing 13.6 Datenstruktur fr die Welle

Bevor die Callback-Funktion das erste Mal aufgerufen wird, muss die Datenstruktur initialisiert werden. Dazu wird eine Sinuswelle berechnet, die genau in das Array passt.
for (int i=0; i<MAXWELLE; i++ ) { SoundData.Welle[i]=sin((double(i)/MAXWELLE)*PI*2.0); } SoundData.LinksIndex = SoundData.RechtsIndex = 0;
Listing 13.7 Datenstruktur fr die Welle

Die Callback-Funktion fllt nun den Ausgabepuffer mit den Daten aus dem Array. Die Position innerhalb des Arrays wird jeweils in den Variablen LinksIndex und RechtsIndex abgespeichert. Um einen hheren Ton auf dem rechten Kanal zu erzeugen, wird der entsprechende Index einfach schneller erhht.

289

13

Musik ist mit Wellen verbunden

static int sinusCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* TI, PaStreamCallbackFlags statusFlags, void *userData ) { tSoundData *SoundData = (tSoundData*)userData; float *out = (float*)outputBuffer; for (unsigned int i=0; i<framesPerBuffer; i++ ) { *out++ = SoundData->Welle[SoundData->LinksIndex]; *out++ = SoundData->Welle[SoundData->RechtsIndex]; SoundData->LinksIndex += 1; if (SoundData->LinksIndex >= MAXWELLE) { SoundData->LinksIndex -= MAXWELLE; } SoundData->RechtsIndex += 3; // etwas hoeher if (SoundData->RechtsIndex >= MAXWELLE) { SoundData->RechtsIndex -= MAXWELLE; } } return paContinue; }
Listing 13.8 Callback fr die Sinuswelle

Sie sehen, wie nun die Hauptschleife der Callback-Funktion die Puffer mit der vorbereiteten Welle aus dem Speicherbereich der Anwendung fllt. Am rechten Kanal knnen Sie sehen, wie die vorbereitete Welle in der Frequenz erhht wird.

13.4

Achtung Aufnahme!

Jetzt ist es nur noch ein kleiner Schritt bis zur Aufnahme. Ausgehend von der Wiedergabe der Sinuswelle muss nur noch die Richtung verdreht werden. Die Callback-Funktion muss die Daten aus dem Eingangspuffer der Soundkarte in den Anwendungsspeicher kopieren. Die Hauptfunktion muss den bentigten Speicher vor der Aufnahme zur Verfgung stellen, und einige Parameter mssen anders gesetzt werden. Fr die Aufzeichnung bentigen Sie natrlich zunchst eine Tonquelle. Das kann das eingebaute Mikrofon Ihres Notebooks sein. Sie knnen ein Mikrofon mit dem Mikrofoneingang verbinden oder Sie verbinden Ihren mp3-Player mit der Line-In-Buchse Ihres Computers. In neueren Notebooks gibt es keine separate

290

Achtung Aufnahme!

13.4

Line-In-Buchse mehr. Dafr vertragen aber die Mikrofonbuchsen auch den etwas hheren Pegel eines Line-In-Gertes. Programmtechnisch werden fr die Aufnahme die gleichen Funktionen verwendet wie bei der Wiedergabe, allerdings ndern sich die Parameter und natrlich die Datenstrukturen. Das Beispielprogramm soll eine Zeitlang aufnehmen, die Aufnahme im Hauptspeicher ablegen und dann gleich wieder abspielen. Die passende Datenstruktur tAufnahmeDaten, die wieder an die Callback-Funktion bergeben werden wird, enthlt einen Zeiger auf den Aufnahmespeicher. Dazu kommt der Index, den die Callback-Funktion setzt, um festzuhalten, wo sie zuletzt stand. Nach Ende der Aufnahme kann das Hauptprogramm an diesem erkennen, wie viele Daten aufgenommen wurden. MaxFrameIndex sagt der Callback-Funktion, wo das Ende des Speichers liegt, um zu verhindern, dass die Aufnahme den gesamten Hauptspeicher umpgt.
class tAufnahmeDaten { public: int FrameIndex; // Index des Aufnahme-Arrays int MaxFrameIndex; SAMPLE *AufnahmeSamples; };
Listing 13.9 Datenstruktur

Im Hauptprogramm wird die Datenstruktur vorbereitet. Die Anzahl der Frames fr die Aufnahme ergibt sich aus der Sample-Rate und der aufzunehmenden Sekunden. Der erforderliche Speicher muss mit der Anzahl der aufgenommenen Kanle multipliziert werden. Anschlieend wird PortAudio initialisiert und der Stream erffnet. Das funktioniert wie bei der Wiedergabe. Lediglich die Parameter von Pa_OpenDefaultStream sind wie erwartet etwas anders. Vor allem werden zwei Eingangskanle und kein Ausgabekanal erffnet.
int main() { PaStream PaError tAufnahmeDaten int int SAMPLE

*Stream; FehlerNr; Data; GesamtzahlFrames; AnzahlSamples; Maximum, Durchschnitt, Wert;

291

13

Musik ist mit Wellen verbunden

GesamtzahlFrames = DauerInSekunden * SampleRate; Data.MaxFrameIndex = GesamtzahlFrames; Data.FrameIndex = 0; AnzahlSamples = GesamtzahlFrames * AnzahlKanaele; Data.AufnahmeSamples = new SAMPLE[AnzahlSamples]; pruefeFehler(Pa_Initialize()); // Aufnahme starten... FehlerNr = Pa_OpenDefaultStream( &Stream, 2, // input Kanaele 0, // output Kanaele PA_SAMPLE_TYPE, SampleRate, FramePufferRate, recordCallback, &Data); pruefeFehler(FehlerNr); pruefeFehler(Pa_StartStream(Stream)); cout << "Aufnahme startet!" << endl; while (Pa_IsStreamActive(Stream)) { Pa_Sleep(100); } pruefeFehler(Pa_CloseStream(Stream));
Listing 13.10 Hauptprogramm fr Aufnahme und Wiedergabe

Nun wird der Stream gestartet. Die darauffolgende Schleife wartet, bis die Aufnahme gestoppt wird. Dazu fragt sie ab, ob der Aufnahmestrom noch aktiv ist. Wenn dies der Fall ist, geht das Programm noch einmal eine Zehntelsekunde schlafen. Diese Schlafenszeit ist wichtig, da das Programm fr diese Zeit seine Ressourcen anderen Prozessen und Threads zur Verfgung stellt. Fehlt dieser Schlafauftrag in der Schleife, ist zwar gewhrleistet, dass das Programm ohne jegliche Verzgerung nach der Aufnahme weiterluft, aber das Programm wrde ohne Unterbrechung die CPU mit der Abfrage belasten, ob die Aufnahme bereits beendet ist. Das System ginge in die Knie, und auch die Aufnahme selbst erhielte eventuell nicht mehr gengend Rechenzeit, um die Aufnahme einwandrei auszufhren.

292

Achtung Aufnahme!

13.4

Das Ende der Aufnahme wird durch die Aufnahme-Callback-Funktion entschieden. Wenn diese ihr Limit erreicht hat, bernimmt sie keine Daten mehr und liefert dem System eine 0 als Rckgabewert. Callback fr die Aufnahme ber den Benutzerdatenzeiger erhlt die Callback-Funktion die Daten fr die Aufnahme. Diese bestehen aus dem Zeiger auf den Speicherbereich, in den die aufgenommenen Sounddaten kopiert werden sollen, und dem Index, wie weit der Speicher bereits gefllt ist. Diese Variable FrameIndex wurde vom Hauptprogramm auf 0 gesetzt und wird von der Callback-Funktion hochgezhlt. Die Variable MaxFrameIndex informiert die Callback-Funktion darber, wann der Aufnahmespeicher voll ist.
static int recordCallback(const void *inputBuffer, void *outputBuffer, unsigned long FramesProPuffer, const PaStreamCallbackTimeInfo* TimeInfo, PaStreamCallbackFlags statusFlags, void *userData ) { tAufnahmeDaten *Data = (tAufnahmeDaten*)userData; SAMPLE *rptr = (SAMPLE*)inputBuffer; SAMPLE *wptr = &Data->AufnahmeSamples[Data->FrameIndex * AnzahlKanaele]; long framesToRecord; int Rueckgabe; unsigned long RestFrames = Data->MaxFrameIndex - Data->FrameIndex; int samplesToRecord; if (RestFrames<FramesProPuffer) { framesToRecord = RestFrames; Rueckgabe = 1; // entspricht true; } else { framesToRecord = FramesProPuffer; Rueckgabe = 0; // entspricht false; } samplesToRecord = framesToRecord * AnzahlKanaele; if (inputBuffer==0) { for (int i=0; i<samplesToRecord; i++) { *wptr++ = SAMPLE_SILENCE; } } else {

293

13

Musik ist mit Wellen verbunden

for (int i=0; i<samplesToRecord; i++) { *wptr++ = *rptr++; } } Data->FrameIndex += framesToRecord; return Rueckgabe; }
Listing 13.11 Callback fr die Aufnahme

Aufnahmeanalyse Das Hauptprogramm schliet anschlieend den Audio-Stream wieder. Die Daten liegen nun vor und knnen analysiert werden. Dazu wird das Array mit den Aufnahmedaten durchlaufen und die Werte betrachtet. Je hher der Wert maximal ist, desto strker ist der Ausschlag der Welle, also die Amplitude. Der Wert in der Variablen Maximum gibt also den hchsten Wert innerhalb der Aufnahme an.
// Statistik berechnen Maximum = 0; Durchschnitt = 0; for (int i=0; i<AnzahlSamples; i++) { Wert = Data.AufnahmeSamples[i]; if( Wert < 0 ) Wert = -Wert; // Absolutbetrag if( Wert > Maximum ) { Maximum = Wert; } Durchschnitt += Wert; } Durchschnitt = Durchschnitt / AnzahlSamples; cout << "Max. Amplitude = " << Maximum << endl; cout << " Durchschnitt = " << Durchschnitt << endl;
Listing 13.12 Statistik

Etwas kritischer ist die Glaubwrdigkeit des Durchschnittswertes. Zwar wird fr dessen Berechnung die negative Halbwelle negiert, also der Absolutbetrag gebildet, aber dennoch schwankt die natrliche Welle zwischen 0 und dem Scheitelpunkt der Welle. Das bedeutet, dass der Durchschnitt erwartungsgem selbst dann etwa die Hlfte des Maximums sein wird, wenn das Signal in konstanter Lautstrke vor sich hinschwingt.

294

Achtung Aufnahme!

13.4

Die Summe aller Werte wird in der Variablen Durchschnitt akkumuliert. Zum Schluss werden sie durch die Anzahl der Samples geteilt. Der Wert ist wie gesagt mit Vorsicht zu genieen und kann nur ein Indiz bilden. Wiedergabe Nach diesen Berechnungen wird der Speicherinhalt wieder ausgegeben. Das funktioniert fast genauso wie bei der Ausgabe der selbst erzeugten Sinuswelle. Jetzt ist der Speicher allerdings nicht vom Programm konstruiert, sondern durch eine Aufnahme gefllt worden. Das Hauptprogramm drfte Ihnen im weiteren Verlauf jedenfalls sehr vertraut erscheinen.
// Playback Data.FrameIndex = 0; cout << "Beginn Playback" << endl; FehlerNr = Pa_OpenDefaultStream( &Stream, 0, // Input Kanaele 2, // Output Kanaele PA_SAMPLE_TYPE, SampleRate, FramePufferRate, playCallback, &Data ); pruefeFehler(FehlerNr); if (Stream) { pruefeFehler(Pa_StartStream(Stream)); cout << "Bitte bis zum Ende der Wiedergabe warten!" << endl; while( Pa_IsStreamActive( Stream ) ) Pa_Sleep(100); pruefeFehler(Pa_CloseStream(Stream)); cout << "Fertig." << endl; } delete[] Data.AufnahmeSamples; Pa_Terminate(); return 0; }
Listing 13.13 Playback

In diesem Fall wird auch bei der Wiedergabe in einer Schleife geschlafen. Bei Ausgabe der Wellen und des Sgezahns haben wir die Dauer in Sekunden vor-

295

13

Musik ist mit Wellen verbunden

gegeben. Jetzt hngt sie davon ab, wann der aufgenommene Speicher abgespielt ist.

13.5

Stimmungskanone

Falls Sie zufllig Gitarre spielen oder unter jemandem in der nheren Verwandtschaft leiden, der Gitarre spielt, wissen Sie, dass eine Gitarre vor dem Spielen gestimmt werden muss. Das hat sich scheinbar noch nicht zu jedem Gitarristen herumgesprochen, was die Freude an diesem Instrument durchaus trbt. In frheren Jahrhunderten verwendete man dazu eine Stimmpfeife, deren Tonhhe durch Spannen der Saiten erreicht werden musste. Dazu bentigte man sein Gehr und ein wenig musikalisches Unterscheidungsvermgen. Heutzutage verwendet der Gitarrist ein Stimmgert. Dieses zeigt an, welcher Ton in etwa zu hren ist und ob die Tonlage der Saite darunter- oder darberliegt. Dazu bentigt er eine 9-Volt-Batterie. Solche Stimmungsmesser sind inzwischen auch auf Computern oder gar auf Mobiltelefonen verfgbar. Bei einigen Gitarren sind diese Gerte bereits eingebaut. Es gibt sogar schon Experimente, die Stimmwirbel durch kleine Elektromotoren zu betreiben und so die Stimmung vollautomatisch durchzufhren. Diese Technologie ist allerdings noch sehr teuer. Und so ist es vielleicht klger mit einem elektronischen Stimmgert zu arbeiten. Nachdem PortAudio die Mglichkeiten schafft, Tonaufnahmen in den Speicher zu fllen, kann nicht so schwer sein, ein Programm zu schreiben, das diese Aufgabe bernimmt. Ein solches Programm knnte leicht als Ausrede dazu dienen, ein Notebook anzuschaffen. Immerhin haben Sie dann ein Stimmgert dabei, falls sich der Gitarrist in der Fugngerzone schief anhrt. Um den Anfang fr ein Stimmgert zu machen, werden wir die aufgenommenen Wellen untersuchen und ermitteln, wie hoch die aufgenommene Frequenz ist. Aus verschiedenen Grnden ist es nicht ganz einfach, den Computer tatschlich als Stimmgert zu verwenden. Aber es ist mglich, grundstzlich Frequenzunterschiede zu erkennen. Die Tonhhe ist eine Frequenz, und Frequenzen werden in Hertz gemessen. Der Buchstabe t in diesem Wort ist zwar wichtig, aber dennoch lautet die Abkrzung Hz. Ein Hertz ist eine Schwingung pro Sekunde. Um die Hhe eines Tons zu messen, mssen also die aufgenommenen Daten auf Schwingungen untersucht werden.

296

Stimmungskanone

13.5

13.5.1

Die Daten

Die Datenstruktur, die die Eingangssignale aufnimmt, besteht wie bei der Aufnahme aus einem Zeiger auf einen Puffer, einer aktuellen Aufnahmeposition und einem Limit, damit die Callback-Funktion wei, wann Schluss ist. Wenn die Aufnahmefunktion ber das Ende des Puffers hinausgeht und alles berschreibt, was dem Programm lieb und teuer ist, wird es nicht mehr zur vollsten Zufriedenheit laufen.
class tEingang { public: int FramePos; int MaxFramePos; SAMPLE *Aufnahme; };
Listing 13.14 Datenstruktur fr die Stimmungsdaten

Die Sample-Rate muss fr dieses Programm eigentlich nicht CD-Qualitt sein. Sie knnen ja etwas experimentieren und die Rate herunterzhlen. Es kann sogar sein, dass die Genauigkeit der Ergebnisse besser wird. Immerhin sind fr die Tonhhe die Basisfrequenzen eines Tons relevant. Die Konstante Sekunden wird fr die Berechnung der Puffergre verwendet. Da es ein ganzzahliger Wert ist, steht er auf dem kleinstmglichen Wert, und der ist natrlich 1. Tatschlich werden aber mehrere Messungen pro Sekunde durchgefhrt. Wenn nur einmal pro Sekunde gemessen wird, reagiert das Programm zu trge. Stereo ist fr das Stimmen einer Gitarre denitiv nicht erforderlich. Also ist die Anzahl der Kanle auf 1 reduziert worden.
const const const const int int int int SampleRate=44100; Sekunden=1; SekundenBruchteil=10; Kanaele=1;

Listing 13.15 Konstanten fr die Stimmungssuche

Das Programm zeichnet den Datenstrom des Mikrofons fr eine gewisse Zeit auf. Aus diesen Daten muss die Anzahl der Schwingungen pro Sekunde ermittelt werden. Bei einer Sekunde braucht nur die Anzahl der Schwingungen innerhalb des Puffers gezhlt zu werden. Ist die Aufnahmezeit anders, muss das Ergebnis auf eine Sekunde umgerechnet werden. Bei der Ermittlung der Tonhhe ist nur die Grundschwingung interessant. Abgesehen von einer reinen Sinuswelle, die eigentlich nur knstlich erzeugt werden kann, bestehen alle anderen Klnge aus einer Mischung mehrerer Frequenzen.

297

13

Musik ist mit Wellen verbunden

Was als Tonhhe wahrgenommen wird, ist die niedrigste Frequenz aus diesem Gemisch. Alle anderen Frequenzen werden eingemischt und verndern zwar das Aussehen der Welle, aber nicht ihre Nulldurchgnge. Die Nullstellen sind einfach zu nden. Sie sind immer dort, wo ein Vorzeichenwechsel stattndet. Zur Bestimmung der Frequenz werden also die Vorzeichenwechsel gezhlt und anschlieend durch 2 geteilt, weil jede Schwingung zwei Vorzeichenwechsel hat. Die ermittelte Frequenz wird noch mit der Konstante SekundenBruchteil multipliziert, da ja nur ein Teil einer Sekunde gemessen wird.

13.5.2 Das Hauptprogramm


Die Daten werden initialisiert. Dazu wird der Aufnahmepuffer mit Speicher versorgt. Das Soundsystem wird initialisiert und ein Stream erffnet.
int main(void) { PaStream *Stream; PaError err; tEingang Eingang; int GesamtFrames; int numSamples; SAMPLE Amplitude, Signal; GesamtFrames = Sekunden * SampleRate; numSamples = GesamtFrames * Kanaele; Eingang.Aufnahme = new SAMPLE[numSamples]; checkError(Pa_Initialize()); // Erzeuge einen Stream fuer die Aufnahme err = Pa_OpenDefaultStream( &Stream, Kanaele, // input Kanaele 0, // output Kanaele PA_SAMPLE_TYPE, SampleRate, 256, // frames per buffer recordCallback, &Eingang); checkError(err);
Listing 13.16 Hauptprogramm Stimmungssuche

Die folgende Endlosschleife nimmt immer eine Zehntelsekunde auf, analysiert die Daten und gibt die Frequenz auf dem Bildschirm aus. Das Programm luft endlos. Um es zu beenden, muss es mit Strg-C beendet werden. Zum Start der

298

Stimmungskanone

13.5

Aufnahme wird Pa_StartStream aufgerufen. Durch Pa_Sleep legt sich die CPU eine Runde schlafen und anschlieend wird mit Pa_StopStream die Aufnahme angehalten. Das Starten und Stoppen kann ja beliebig oft erfolgen.
while (true) { Eingang.MaxFramePos = GesamtFrames; Eingang.FramePos = 0; checkError(Pa_StartStream(Stream)); cout << "Now recording!!" << endl; // Eine Zehntelsekunde pausieren while( Pa_IsStreamActive( Stream)) Pa_Sleep(1000/SekundenBruchteil); checkError(Pa_StopStream(Stream));
Listing 13.17 Achtung: Aufnahme

Die aufgenommenen Daten werden durchlaufen und jeder Vorzeichenwechsel wird gezhlt. Parallel wird das Maximum der Amplitude gemessen. Dies gibt einen gewissen Anhaltspunkt ber die Qualitt des gemessenen Signals. Ist der Ton zu leise, setzt er sich nicht ausreichend vom Grundrauschen ab und dann misst das Programm die Frequenz des Lftergeruschs.
Amplitude = 0; int Vorzeichen = +1; long Wechsel = 0; for (int i=0; i<Eingang.FramePos; i++ ) { Signal = Eingang.Aufnahme[i]; if (Signal < 0 ) { if (Vorzeichen>0) { Wechsel++; Vorzeichen=-1; } } else if (Vorzeichen<0) { Wechsel++; Vorzeichen=1; } if (Signal > Amplitude ) { Amplitude = Signal; } } long Frequenz = SekundenBruchteil * Wechsel/2; cout << "Frequenz: " << Frequenz; cout << " Maximale Amplitude:" << Amplitude << endl;

299

13

Musik ist mit Wellen verbunden

} checkError(Pa_CloseStream(Stream)); delete[] Eingang.Aufnahme; Pa_Terminate(); return 0; }


Listing 13.18 Datenanalyse

Wenn Sie sich den Wert Eingang.FramePos anzeigen lassen, werden Sie bei einer Zehntelsekunde einen Wert von etwa 5.000 erhalten. Das ist die Zahl der Werte, die das Programm in dieser Zeit aufgenommen hat. Diese Zahl der Daten gewhrleistet also eine brauchbare Messung. Ernchterung Wenn Sie das Programm verwenden, werden Sie feststellen, dass Sie einige Schwankungen in den Werten haben, sodass es keine Freude ist, eine Gitarre auf diese Weise zu stimmen. Es gibt da zweierlei Einsse, die ungnstig sind. Das erste Problem sind die Strgerusche wie das Rauschen und das Brummen. Das zweite Problem liegt in der Aussteuerung. Ist das Signal zu leise, ndet das Programm zwischen dem Grundrauschen das Signal nicht. Ist das Signal zu stark, entstehen massive Verzerrungen, die auch das Messergebnis verflschen. Zu guter Letzt kommt noch eine Besonderheit der Gitarre hinzu. Wenn Sie eine Saite anschlagen, wird diese nach einiger Zeit leiser. Sobald sie verklungen ist, messen Sie wieder die Strgerusche. Nachdem ich die Gitarre ber den eingebauten Tonabnehmer angeschlossen und korrekt ausgesteuert habe, erhielt ich recht gute Werte. Dennoch sind die Ergebnisse mit einem Stimmgert deutlich besser.

13.5.3 Was denn noch?


Mit dem vorgestellten Programm ist die Grundlage vorhanden, um Frequenzen zu messen. Allerdings gibt es gewisse Einschrnkungen, denen vielleicht mit programmtechnischen Mitteln beizukommen ist. Rauschen Je nach der verwendeten Hardware sind die Signale unterschiedlich gut. Beispielsweise hat eines meiner Notebooks durch seinen aufdringlichen Lfter immer ein Knattern in der Aufzeichnung des eingebauten Mikrofons. Auf diese Weise wird eventuell eher das Notebook gemessen als die Stimmung der Gitarre. Auch wer besonders leise Lfter und ein separates Mikrofon verwendet, wird ein gewisses Grundrauschen in den Aufnahmen haben. Um das Rauschen zu ent-

300

Stimmungskanone

13.5

fernen, msste eine Art Rauschlter eingebaut werden, der Signale unter einem gewissen Level ausschaltet. Eine besondere Herausforderung liegt im Anschlag der Saite. Dabei wird ein kurzes, sehr lautes Knacksignal erzeugt, das allerdings alle nur denkbaren Frequenzen enthlt. Wenn man dieses ausltert, knnte die Messung auch deutlich verbessert werden. Tonhhe Fr den Gitarristen ist die Frequenz keineswegs ein bekannter Wert. Auch wenn die meisten Musiker schon einmal gehrt haben, dass der Ton A eine Frequenz von 440 Hz hat, wird ihnen der Ton A mehr sagen als die Frequenz. Aus den Frequenzen kann man die Tonhhe bestimmten. Der Kammerton A hat wie gesagt eine Frequenz von 440 Hz. Der Ton a eine Oktave hher hat die doppelte Frequenz von 880 Hz. Eine weitere Oktave verdoppelt wieder die Frequenz. Daraus lsst sich bereits erahnen, dass die Tne nicht linear ber das Frequenzband verteilt liegen. Die folgende Tabelle zeigt die Frequenzen aller Tne der C-Dur-Tonleiter.
Frequenz 264 Hz 297 Hz 330 Hz 352 Hz 396 Hz 440 Hz 495 Hz 528 Hz Ton c d e f g a h c

Tabelle 13.1 Tonfrequenzen

Diese Tabelle kann in das Programm eingebettet werden. ber eine Folge von Vergleichen kann ermittelt werden, in welchem Tonbereich die Frequenz aus dem Mikrofon liegt. Fr das Stimmen der Gitarre sind die Tne der Tabelle 13.2 relevant. Eine Bassgitarre hat nur die ersten vier Saiten einer normalen Gitarre, allerdings eine Oktave tiefer gestimmt. Die Frequenzen sind also zu halbieren.

301

13

Musik ist mit Wellen verbunden

Frequenz 165 Hz 220 Hz 297 Hz 396 Hz 495 Hz 660 Hz

Ton E A D G H E

Tabelle 13.2 Die Tonlage der Saiten einer Gitarre

13.6

Der gute Ruf des Spielers

Als meine Kinder noch sehr klein waren, habe ich ein Computerspiel geschenkt bekommen, das nur ber das Mikrofon gesteuert werden konnte. Sie brauchten also weder mit der Tastatur noch mit der Maus umgehen zu knnen, um der kleinen Maus Marty durch ihren Alltag zu helfen. Zuerst sollten sie die Maus wecken und ihren Namen rufen. Spter sollten sie durch Pusten ein Segelboot antreiben, durch rhythmische Rufe einen Frosch zum Hpfen animieren und durch Schweigen dafr sorgen, dass die Maus nicht von der Katze erwischt wird. Es war faszinierend, wie viel Spa die Kinder an diesen sehr einfachen Mechanismen hatten. Besonders viel Spa hatten sie daran, laut zu brllen, wenn die Maus leise an der Katze vorbeischlich, weil dann die Maus immer wieder erschreckt in die letzte Deckung zurcklief und sich beklagte. Das Spiel muss allerdings von einem Warmduscher und Diskettenbeschrifter geschrieben worden sein. Die Katze ist nie erwacht und hat die Maus verspeist. Einer der Hhepunkte des Spiels war ein Rabe, dem die Kinder etwas vorsprechen konnten, was der Rabe dann wiederholte. Meine Kinder brachten dem Raben alle Worte bei, die ihnen von den Eltern verboten waren. Die einzelnen Stufen dieses Spiels sind gar nicht so schwer zu programmieren. Den Raben haben wir bereits abgehandelt, und der Rest besteht eigentlich nur darin, die Lautstrke am Mikrofon zu messen. Diese Messungen mssen gegebenenfalls noch zur rechten Zeit erfolgen, und schon ist die technische Seite des Spiels erledigt. In Anbetracht dessen ist es verwunderlich, dass nicht viel mehr Spiele mit der Sprache gesteuert werden knnen. So knnte doch bei einem Ego-Shooter das Auslsen der verschiedenen Waffen durch Sprachanalyse geschehen. Ich stelle es mir vor allem fr den Auenstehen-

302

Der gute Ruf des Spielers

13.6

den als witzig vor, wenn mit ratatatata das Maschinengewehr, mit ptui ptui die Walter mit Schalldmpfer und mit tschack das Kampfmesser gezckt wird.

13.6.1

Akustischer Hau-den-Lukas

Ein akustisches Spiel fr einen Wettstreit der Stimmen knnte ein echter Partyknller werden. Die Spielregeln sollten so einfach wie mglich sein. Es geht darum, einen Ton zu singen oder zu brummen. Das musikalische Talent steht gar nicht im Mittelpunkt. Es geht nur darum, mit der Stimme stetig gleich laut oder lauter zu werden und dies mglichst lang auszuhalten. Sobald der Spieler wieder leiser wird, wird das Spiel beendet. Es wird angezeigt, wie lange er durchgehalten hat und wie laut sein bester Wert ist. Der Computer misst dazu im Zeittakt von Zehntelsekunden den Maximalwert. Das Erstaunliche ist, dass das gar nicht so einfach ist. Ich habe bei meinen Testlufen im Schnitt fnf Runden geschafft.

13.6.2 Die Umsetzung


Die Datenstruktur unterscheidet sich nicht von denen der anderen Programme, die Daten aufnehmen sollten. Prinzipiell braucht das Programm einen Datenspeicher fr die aufgenommenen Klnge und einen Zeiger auf die Stelle, wo die bisherige Aufnahme zu Ende ist. Und natrlich wird auch diesmal der CallbackFunktion gesagt, wo der Speicher zu Ende ist.
class tAufnahme { public: int FramePos; int MaxFrameIndex; SAMPLE *Samples; };
Listing 13.19 Die Klasse tAufnahme

Diese Datenstruktur kann an die Aufnahme-Callback-Funktion bergeben werden. Die Callback-Funktion enthlt keine weiteren berraschungen, so dass sie hier an dieser Stelle nicht noch einmal vorgefhrt werden msste. nderungen zu vorigen Programmen liegen nur darin, dass fr diesen Kraftakt der menschlichen Stimme eine Stereo-Aufnahme nicht erforderlich ist. Das Hauptprogramm startet wie bei den bisherigen Programmen zur Aufnahme, indem die Daten fr die Aufnahme prpariert werden und PortAudio initialisiert wird. Der Stream wird erffnet. Wie gesagt, es reicht ein Kanal. Der Rest ist bereits bekannt.

303

13

Musik ist mit Wellen verbunden

int main(void) { PaStream PaError tAufnahme int int SAMPLE

*Stream; err; Aufnahme; GesamtFrames; AnzahlSamples; MaxAmplitude, Wert;

GesamtFrames = Sekunden * SampleRate; AnzahlSamples = GesamtFrames * Kanaele; Aufnahme.Samples = new SAMPLE[AnzahlSamples]; Aufnahme.MaxFrameIndex = GesamtFrames; for(int i=0; i<AnzahlSamples; i++) Aufnahme.Samples[i] = 0; checkError(Pa_Initialize()); // Bereite den Stream vor err = Pa_OpenDefaultStream( &Stream, 1, // input Kanaele 0, // output Kanaele PA_SAMPLE_TYPE, SampleRate, 256, // frames per buffer recordCallback, &Aufnahme ); checkError(err);
Listing 13.20 Vorbereitung zum Ruf

Nun wird eine Schleife gestartet, in der immer wieder eine Aufnahme fr eine Zehntelsekunde luft. Dazu wird Pa_StartStream aufgerufen. Mithilfe der Funktion Pa_Sleep wird eine Zehntelsekunde gewartet und dann wird die Aufnahme durch Pa_StopStream beendet.
int MaxAlt=0; cout << "Schrei mal!!" << endl; int Runde=0; while (true) { Aufnahme.FramePos = 0; checkError(Pa_StartStream(Stream)); Pa_Sleep(100); // Eine zehntel Sekunde pausieren checkError(Pa_StopStream(Stream));
Listing 13.21 Aufnahmeschleife

304

Der gute Ruf des Spielers

13.6

Die aufgenommenen Daten werden nun nach einem mglichst hohen Betrag durchsucht. Das Maximum wird erfasst. Bleibt das Maximum unter 1000, so wird dieser Wert auf 0 gesetzt. Es wird sich um Rauschen handeln. Entsprechend wird auch die Anzahl der Runden nicht hochgezhlt. Das Programm startet mit der Zhlung erst, wenn es Wellen empfngt, die ber dem Grundrauschen liegt. Sie knnen bei Ihrem Computer ja ausmessen, wie hoch das Grundrauschen ist. Vielleicht knnen Sie es niedriger ansetzen. Das ermglicht einen weiteren Dynamikbereich. Im nchsten Schritt wird geprft, ob das Maximum der letzten Zehntelsekunde auch hher war als das bisherige Maximum. Ansonsten wird die Schleife verlassen, und die Ergebnisse werden angezeigt.
// Measure maximum peak amplitude. MaxAmplitude = 0; for(int i=0; i<AnzahlSamples; i++) { Wert = Aufnahme.Samples[i]; if (Wert < 0 ) { Wert = -Wert; // Betrag } if (Wert > MaxAmplitude ) { MaxAmplitude = Wert; } } if (MaxAmplitude<1000) { MaxAmplitude=0; // Rauschen wegnehmen } else { Runde++; } cout << "Maximale Amplitude = " << MaxAmplitude << endl; if (MaxAmplitude<MaxAlt) break; MaxAlt = MaxAmplitude; } cout << "Danke, es reicht!" << endl; cout << "Runden: " << Runde << " Max. Amplitude: " << MaxAmplitude << endl; checkError(Pa_CloseStream(Stream)); delete[] Aufnahme.Samples; Pa_Terminate(); return 0; }
Listing 13.22 Datenanalyse

305

13

Musik ist mit Wellen verbunden

Wenn Sie das Spiel ausprobieren, werden Sie merken, dass es gar nicht so einfach ist, die Stimme so przise zu steuern. Um einen abendfllenden Wettkampf zu erzielen, ist die Ausfhrung vielleicht auch etwas zu technisch.

13.6.3 Was denn noch?


Sie knnen einerseits die Spielidee weiter ausbauen. Aber mit etwas Fantasie sind auch ganz andere Spiele mglich. Hau den Lukas etwas besser Wenn die Schleife alle Zehntelsekunde die maximale Amplitude anzeigt, aber die Wertung nur nach einer oder gar zwei Sekunden erfolgt, drfte das Spiel dem Spieler mehr Chancen geben, die Stimme noch zu erheben, bevor er aus der Runde geworfen wird. Vielleicht knnte ab fnf Sekunden noch die Komponente des Taktierens hinzukommen. Dann wird die Lautstrke nur minimal erhht, damit man noch viele Steigerungen erreichen kann. Dieses Taktieren drfte das Spiel spannender machen. Wenn mehrere Spieler gegeneinander antreten, liegt der Unterhaltungswert ja besonders in der Beobachtung der Gegenspieler. Wie lange hat der Gegner noch Luft, um die Stimme strker werden zu lassen? In dem Wettkampf liegt auch die nchste Erweiterungsmglichkeit. Es knnten Konten fr jeden Spieler erffnet werden, die der Computer verwaltet. Er kann dann Statistik fhren. Prinzipiell wre es auch denkbar, die Lautheit mit der Tonhhe zu koppeln. Das knnte dann auch den Vorteil des mnnlichen Geschlechts bezglich ihrer kernigen Stimme ausgleichen. Denn nach oben wird es fr die Herren irgendwann eng. Wenn Sie erst einmal so weit gekommen sind, braucht das Spiel natrlich eine grasche Oberche, am besten mit Wellenanzeige und LED-VU-Meter. Oder wre nicht sogar ein klassisches Zeigerinstrument viel kultiger? Sie sehen, dass man aus einer extrem simplen Spielidee eine groe Sache machen kann. Vielleicht wird dieses Spiel zum Karaoke der Sangesunkundigen. Immerhin war Tetris auch keine besonders originelle Idee, ndet sich aber heute auf jedem Mobiltelefon. Und nun stellen Sie sich einen Bus voller Schler vor, die langsam immer lauter in ihr Mobiltelefon singen und dabei die Stimme erheben. Wre das nicht schn?

306

Der gute Ruf des Spielers

13.6

Gesangstraining Statt immer lauter zu werden, knnten Sie auch den Gitarrenstimmer zur Spielidee ausbauen. Geben Sie eine Sinuswelle mit einer bestimmten Frequenz aus und lassen Sie den Spieler den Ton singen. berfordern Sie die Spieler aber nicht. Als Gitarrist kenne ich es, dass mein Stimmgert pltzlich seine Runde durch die Anwesenden macht und jeder mal versucht, die Nadel per Stimme zum Ausschlag zu bringen. Es ist erschtternd, wie wenige Menschen es schaffen, den Ton zu halten. Diejenigen, die es dann schaffen, melden sich sofort bei Deutschland sucht den Superstar an. Wie wre eine Ruderregatta? Sie knnten ein Spiel Ruderregatta schreiben, indem der Spieler den Steuermann bernimmt. Er muss immer so brllen, dass er kurz vor dem Ruderschlag brllt. Dann erhht sich das Tempo. Kommt der Brller unrhythmisch, kommen die oder der Ruderer aus dem Takt und verlieren erheblich an Tempo. Den Ruderer kann man mit einer Art ASCII-Grak simulieren. Beispielsweise she ein Zweier ohne Steuermann in den drei Phasen etwa so aus:
\ M / \M/ \ M / \M/ M V M /M\ / M \ /M\ / M \ V M ---M--M ---M--M V

Sie knnten aber auch das Spielfeld von Squirrel, dem Eichhrnchen, verwenden. Die Graken und den Quelltext von Squirrel nden Sie auf der CD. Mit dem Programm GIMP (siehe Seite 231) basteln Sie ein Ruderboot in mehreren Phasen: Ruder vorn, Ruder hinten und Ruder in der Mitte. Lassen Sie das Boot ber das Wasser gleiten. Aber nehmen Sie vorher die Bume aus dem Wasser. Die Autos knnen Sie am Straenrand parken. Wenn sich das Boot scheinbar bewegt, schieben Sie einfach die Autos gleichmig weg. In einer GUI drfen Sie natrlich nicht mit Pa_Sleep arbeiten. Verwenden Sie stattdessen den Timer.

307

Anhang
A B C D E KDevelop ........................................................................................... 311 Bloodshed Dev C++ ........................................................................ 315 Installation von wxWidgets ........................................................ 319 Installation PortAudio ................................................................... 327 Computer-Oldies ............................................................................. 331

309

KDevelop

Wer unter Linux oder UNIX Software in C++ entwickelt und eine leistungsfhige IDE (Integrated Development Environment) sucht, ndet in KDevelop eine sehr leistungsfhige Umgebung. KDevelop wurde ursprnglich primr fr die Entwicklung von Applikationen der KDE-Umgebung entwickelt, luft aber wie alle KDESoftware auch unter GNOME und ermglicht auch die Entwicklung fr andere Umgebungen. So ist die Untersttzung fr wxWidgets durchaus gelungen. KDevelop stellt alle gngigen Werkzeuge einer gngigen IDE zur Verfgung. Unter der Haube arbeitet der GNU-Compiler. Auch der GNU-Debugger wird eingesetzt, wenn es um die Suche nach Fehlern geht. Auch dieser ist ieend in die IDE eingearbeitet. Nach dem Start hnelt KDevelop in seiner Aufmachung den blichen IDEs.

Abbildung A.1 KDevelop

Die heutigen Linux-Installationen gehen nicht mehr davon aus, dass auf der anderen Seite der Tastatur ein Programmierer sitzt, sodass Sie KDevelop im Allgemeinen nachinstallieren mssen. Unter OpenSuse rufen Sie YAST auf und installieren das Paket kdevelop. Bei Ubuntu fragen Sie den Adept-Manager nach kdevelop. Sollten Sie die Kommandozeile bevorzugen oder Debian verwenden, geben Sie den folgenden Befehl ein:

311

KDevelop

sudo apt-get install kdevelop

Gegebenenfalls werden Sie auf Abhngigkeiten hingewiesen, die KDevelop fr seine Arbeit bentigt. So werden Sie, falls Sie bisher einen reinen GNOMEDesktop verwendeten, nun auch erhebliche Teile der KDE-Bibliothek bentigen. Das grte Fenster rechts oben wird vom Quelltexteditor eingenommen, der die verschiedenen Syntaxelemente verschiedenfarbig darstellt. Die Funktionen des Editors sind vielfltig und auch anpassbar. Auf der linken Seite ist ein bersichtsfenster, in dem Sie sich bersicht ber die verwendeten Klassen des Projekts schaffen knnen. Das Fenster ist aber umschaltbar. Sie knnen dort auch die Dateistruktur betrachten. Unter dem Editorfenster bendet sich ein Mehrzweckfenster, in dem beispielsweise die Compiler-Meldungen dargestellt werden, aber auch die Meldungen des Debuggers angezeigt werden knnen.

A.1

Neues Projekt

Um ein Programm zu schreiben, muss ein Projekt angelegt werden. Die Wahl des Projekts legt auch fest, ob Sie ein Programm fr die grasche Oberche oder fr die Kommandozeile schreiben wollen. Sie legen ein neues Projekt ber die Menfolge Projekt Neu... an und nicht, wie Sie das vielleicht von anderen IDEs kennen, ber Datei Neu... Es erscheint der in Abbildung A.2 gezeigte Dialog. Sie sehen, wie in dem Baumdiagramm unten links Simple Hello world program ausgewhlt wurde. Auf der rechten Seite sehen Sie eine Vorabschau und eine kurze Erklrung, was fr eine Applikation entstehen wird. Im unteren Bereich des Dialogs wird angegeben, wie das Projekt und damit das Programm spter heien soll. In der ersten Zeile habe ich den Projektnamen Hallo bereits eingetragen. Automatisch schlgt KDevelop das Arbeitsverzeichnis Hallo in meinem Heimatverzeichnis1 vor. Wenn Sie nun den Button Weiter drcken, knnen Sie weitere Einstellungen durchfhren, die Sie fr den Anfang allerdings nicht verndern mssen. Schlielich erhalten Sie einen fertigen Rahmen fr Ihr Projekt, der bereits die Hauptfunktion main enthlt.

1 Unter UNIX und damit auch unter Linux hat jeder Anwender ein eigenes Arbeitsverzeichnis, das unter dem Verzeichnis /home liegt und standardmig den Benutzernamen trgt.

312

Kompilieren und starten

A.2

Abbildung A.2 Auswahl der Projektart

A.2

Kompilieren und starten

Nun geben Sie Ihr erstes Programm ein. Bevor Sie es starten knnen, mssen Sie es bersetzen. Dazu whlen Sie im Men Erstellen Erstellen oder drcken die F8 -Taste. Um das Programm zu starten, klicken Sie das Symbol mit dem blauen Zahnrad oder drcken die Shift + F9 -Taste. Debugger KDevelop integriert den GNU-Debugger. Um Haltepunkte im Programm zu denieren, klicken Sie in den grauen Bereich links neben der Zeile, vor der die Ausfhrung stoppen soll. Es erscheint eine Markierung. Durch erneutes Ankli-

313

KDevelop

cken verschwindet der Haltepunkt. Kommt der Programmablauf im Debugger an dieser Stelle vorbei, stoppt der Debugger das Programm. Sie knnen den Debugger unter dem gleichnamigen Men mit Debug Start starten. Alternativ drcken Sie die Taste F9 . Das Programm wird gestartet und luft bis zum nchsten Haltepunkt. Die Markierung neben der Zeile wird rot, und ein kleiner Pfeil zeigt an, wo die Programmausfhrung derzeit steht. Sie knnen sich nun schrittweise weiterbewegen. Dazu gibt es in der Werkzeugleiste Symbole mit geschweiften Klammern. Sie knnen sich nmlich entscheiden, ob Sie beim Weiterlaufen in die Funktionen, an denen das Programm nun steht, F11 oder darber hinweg in die nchste Zeile springen hineinspringen mchten wollen F10 . Im linken Fenster sehen Sie whrend des Debuggens die aktuellen Variablen, die Sie nher untersuchen knnen. Wenn Sie F9 drcken, setzt der Debugger seinen Weg fort bis zum nchsten Haltepunkt.

A.3

Weitere Mglichkeiten

Sie knnen unter den Projektoptionen einen Dialog aufrufen, der sehr vielfltige Einstellungen zulsst. Die wichtigsten Einstellungen sind die Compiler-Parameter. Hier werden zustzliche Pfade fr Include-Dateien oder Bibliotheken eingestellt. Sie knnen auch einstellen, ob eine Debug-Version erstellt werden soll. Die Release-Versionen enthalten beispielsweise nicht die Namen der Funktionen. Dadurch wird das Programm kleiner. Sie knnen Ihr Projekt auch direkt mit einer Versionskontrolle koppeln. Derzeit werden CVS und Subversion untersttzt. Wenn im linken Fenster die Quellen als Dateiliste vorliegen, knnen Sie ber einen Rechtsklick ein Men erreichen, in dem auch die Versionsverwaltung auftaucht. Von hier aus knnen Sie die typischen Befehle geben bis hin zur Differenz mit vorigen Versionen. Sie knnen im Quelltext Variablen- oder Funktionsnamen mit der rechten Maustaste anklicken. Dort nden Sie die Mglichkeit, nach einem weiteren Auftreten des Namens zu suchen. Sie knnen ihn projektweit durch einen anderen Namen ersetzen oder den Befehl grep aufrufen, der alle Dateien nach diesem Begriff durchsucht.

314

Bloodshed Dev C++

Bloodshed Dev C++ luft unter MS-Windows und verbindet den GNU C++-Compiler mit einer IDE, die sich stark an der von Visual C++ orientiert. Es ist prinzipiell mglich, der IDE auch andere Compiler unterzuschieben. Die IDE ist mit einem Editor ausgestattet, der die Syntaxelemente hervorheben kann. Die IDE enthlt einen Klassenbrowser und ermglicht das schnelle Aufnden von Fehlern. Mit dem System knnen von Haus aus Konsolenapplikationen und Windows-Applikationen auf Basis der Win32-API entwickelt werden. Auch statische Bibliotheken und DLLs (Dynamic Link Library) knnen erstellt werden. Das Entwicklungssystem ist leicht erweiterbar und eine Community kmmert sich um Plugins und eine einfach Integration von Bibliotheken. Die Software unterliegt der GNU GPL (General Public Licence) der Free Software Foundation und kann frei weitergegeben werden.

B.1

Installation

Auf der CD nden Sie im Verzeichnis Software/Bloodshed Dev-C++ eine unter Windows ausfhrbare Datei. Sie meldet sich mit einer Licence Agreement, dass der Anwender der GNU General Public Licence zustimmt. Im nchsten Schritt werden Sie nach dem Umfang der Installation gefragt. Solange Sie nicht genau wissen, was Sie weglassen knnen, ist es durchaus sinnvoll, alles zu nehmen. Das Installationsprogramm fragt anschlieend nach dem Zielverzeichnis und schlgt C:\DevCpp vor.1 Sie werden gefragt, ob die IDE fr alle Benutzer des Computers installiert werden soll. Zuletzt werden Sie nach der Sprache gefragt. Vermutlich werden Sie German (Deutsch) bevorzugen. Unten knnen Sie ein Thema (New Look, Gnome und Blue) auswhlen. Unter Themen versteht man in diesem Zusammenhang das Design der Oberche. Suchen Sie sich eines aus, das Ihnen am besten gefllt. Jetzt startet bereits Bloodshed Dev-C++ und erffnet den Dialog mit dem Tipp des Tages.

1 Ordentlicher wre natrlich ein Verzeichnis unter dem Verzeichnis Programme. Das wrde dann auch eine saubere Trennung nach Anwender und Administration ermglichen.

315

Bloodshed Dev C++

B.2

Ein Projekt anlegen

Bevor Sie Ihr Listing in den Editor tippen, mssen Sie ein Projekt anlegen. In diesem Projekt merkt sich die IDE, ob eine Konsolenanwendung, eine Windows-Applikation, eine Bibliothek oder eine DLL erstellt werden soll. Nach der Installation von wxWidgets knnen Sie auch ein Projekt fr diese Bibliothek erzeugen. Die Projektdatei enthlt die Informationen ber die bentigten Quelltextdateien und deren Abhngigkeiten, Optionen wie beispielsweise die Pfade und auch Informationen ber das letzte Erscheinungsbild der IDE. Um ein neues Projekt anzulegen, starten Sie ber die Menpunkte Datei Neu Projekt... einen Dialog, in dem Ihnen mehrere Projektarten angeboten werden.

Abbildung B.1 Neues Projekt anlegen

Nach der Wahl der Projektart geben Sie einen Namen fr das Projekt ohne Dateiendung an. Es erscheint ein Dialog, ber den Sie die Projektdatei sichern knnen. Eine Projektdatei hat die Dateierweiterung .dev. Leider legt die IDE ihre Projekte immer standardmig unterhalb des Programmverzeichnisses von DevCpp an. Um Ihre Daten in einem eigenen Datenverzeichnis abzulegen, wie beispielsweise in den Eigenen Dateien, mssen Sie beim Sichern der Projektdatei explizit ein anderes Verzeichnis aufsuchen.

316

bersetzen und starten

B.3

Abbildung B.2 Automatisch erstellter Programmrahmen

Fr die meisten Projektarten wird automatisch ein leerer Programmrahmen angelegt, wie er in Abbildung B.2 fr eine Konsolenapplikation zu sehen ist. Der Editor hebt die Syntax farblich hervor. Es ist eine Spalte fr Breakpoints vorhanden. Sie sehen also, dass die IDE durchaus recht komfortabel ist.

B.3

bersetzen und starten

Zum bersetzen eines Projekts wird Ausfhren Kompilieren angewhlt oder alternativ die Tastenkombination Strg + F9 gedrckt. ber den Menpunkt Ausfhren Ausfhren oder alterantiv Strg + F10 knnen Sie eine erfolgreich bersetzte Applikation starten. Mit F9 kombinieren Sie beides. Bloodshed Dev-C++ nden Sie auf der beigelegten CD. Die aktuellste Version knnen Sie von der folgenden Webseite im Internet herunterladen: http://www.bloodshet.net

317

Installation von wxWidgets

Die Bibliothek wxWidgets wird fr die meisten graschen Projekte in diesem Buch verwendet. Sie wurde ausgewhlt, um zunchst eine Basis fr alle Plattformen zu schaffen. So knnen die Programme auf Windows, Linux und auch auf dem Macintosh zunchst einmal ausprobiert werden. Selbst eine Portierung auf HandheldComputer wre denkbar. Wenn es Ihnen gelingt, ein Programm aus diesem Buch auf einem PDA oder Mobiltelefon zum Laufen zu bringen, berichten Sie doch bitte davon im Forum! Die Bibliothek wxWidgets ist schon seit vielen Jahren verfgbar und wird intensiv gepegt und weiterentwickelt. Inzwischen enthlt sie lngst nicht mehr nur die Grakfunktionen. Eine ausfhrliche Beschreibung fr die Installation und eine Einfhrung in die wxWidget-Programmierung nden Sie auf der folgenden Website: http://tredosoft.com/wxwidgets1 Wenn Sie ber eine Suchmaschine nach bestimmten Stichworten zur wxWidgetsFunktionen suchen, werden Sie feststellen, dass es diverse Foren und Seiten gibt, die Ihnen bei Fragen zur Seite stehen.

C.1

Installation unter KDevelop

Ich gehe an dieser Stelle davon aus, dass Sie KDevelop und damit auch den GNUCompiler bereits installiert haben. Fr die Verwendung von wxWidgets bentigen Sie die Pakete fr die wxWidgets-Bibliotheken. Das sind einmal die Header-Dateien und dann das dev-Paket. Schlielich wird wxcommon installiert. Unter OpenSuse wird dies alles automatisch installiert, wenn Sie wxwidget.dev installieren. Bei Ubuntu whlen Sie im Adept-Manager die Pakete libwxgtk2.6-dev, wx2.6-headers und wx-common aus. Alle weiterhin notwendigen Pakete installieren sich automatisch. Wenn wxWidgets installiert ist, knnen Sie KDevelop starten und ber den Menpunkt Projekt Neu... ein neues Projekt anlegen. In dem Auswahlbaum nden Sie nun auch wxWidgets. Wenn Sie dies whlen, erhalten Sie einen Quelltext, der bei direkter bersetzung eine Applikation mit einem Rahmenfenster, einem Men, einer Statuszeile und einer Messagebox erstellt.

319

Installation von wxWidgets

Vor dem ersten bersetzen eines wxWidgets-Projekts in KDevelop sollten Sie den Menpunkt Erstellen Congure aufrufen anklicken. Dann sorgt KDevelop dafr, dass die richtige Umgebung fr wxWidgets ermittelt und konguriert wird.

C.2

Installation unter Bloodshed Dev C++

Wenn Sie Bloodshed Dev C++ eingerichtet haben, gibt es zwei Methoden, wie Sie die wxWidgets hinzuinstallieren knnen. Der eine Weg ist es, die Installation ber das Internet zu beschaffen. Das hat den Vorteil, dass Sie die neuesten Umgebungen haben. Die Alternative ist es, die Installationsdateien von der CD zu verwenden. Das hat den Vorteil, dass Sie die gleiche Umgebung verwenden, die fr das Buch getestet wurde. Auerdem bentigen Sie keinen Internetzugang.

C.2.1

Buch-CD

Dem Buch liegt wxWidgets als DevPak bei. Das sind Installationsdateien fr Bloodshed Dev C++. Die IDE muss also installiert sein, bevor wxWidgets installiert wird. Bevor wxWidgets selbst installiert wird, muss die Datei Imagelib-2.DevPak installiert werden. Ein Doppelklick gengt, im Hintergrund startet der Paketmanager der IDE, und das Paket installiert sich von selbst. Lediglich ein paar kurze Besttigungen braucht das Paket. Nachdem dies gelungen ist, knnen Sie an wxWidgets-2.6.1_unicode.DevPak herantreten und auch dieses Paket doppelt anklicken. Wieder startet der Paketmanager von Bloodshed Dev C++. Dieses Mal erfragt er zwei oder drei Besttigungen mehr und besttigt dann die vollstndige Installation. Das war es dann auch schon. Den nchsten Abschnitt knnen Sie getrost berspringen. Dort wird nmlich die Online-Installation erlutert. Wir sehen uns also auf Seite 321 wieder.

C.2.2

Internetinstallation

Wenn Sie einen schnellen Internetzugang haben, knnen Sie wxWidgets auch direkt aus dem Internet herunterladen. Dazu starten Sie ber das Men Werkzeuge Auf Updates und Pakete prfen. Im den erscheinenden Dialog wird nach dem Community Server gefragt, von dem das Paket geladen werden soll. Hier whlen Sie devpacks.org. Sie klicken dann den Punkt Check for updates. Es erscheint eine Liste von Bibliotheken.

320

Installation unter Bloodshed Dev C++

C.2

wxWidgets ist von mehreren anderen Bibliotheken abhngig, die zuerst geladen werden mssen:

zlib libjped libpng libtiff

Wenn diese vier Pakete erfolgreich installiert wurden, whlen Sie das Paket wxWidgets unicode. Auch dieses wird geladen und installiert. Anschlieend ist wxWidgets installiert. Es knnen wxWidgets-Projekte erzeugt werden. Die heruntergeladenen Pakete benden sich brigens als DevPak im Unterverzeichnis Packages von dem Verzeichnis, in das Bloodshed Dev C++ installiert worden ist. Dort knnen jederzeit von Hand durch Doppelklick nachinstalliert werden.

C.2.3

Erzeugen einer Beispielapplikation

Nach der Installation von wxWidgets kann Bloodshed Dev C++ neue Projekte fr wxWidgets mit einem kompletten Rahmenquelltext anlegen.

Abbildung C.1 Erstellung eines wxWidget-Projekts

321

Installation von wxWidgets

Sie starten die IDE und whlen Datei Neu Projekt. Es erscheint der Dialog aus Abbildung C.1. Oben whlen Sie die Lasche GUI, und dann knnen Sie direkt wxWidgets whlen. Daraufhin erstellt die IDE fr Sie eine Beispielapplikation. Mit der Taste F9 knnen Sie den Quelltext sofort bersetzen und starten. Es startet ein einfaches Rahmenprogramm mit Men und Statusleiste. Sobald Sie dies erreicht haben, ist es kein Problem mehr, die Beispiele aus dem Buch zu bersetzen.

C.2.4

Buchprojekte

Die einfachste Methode, ein Buchprojekt zu bersetzen, ist ein neues wxWidgetProjekt anzulegen. Kopieren Sie die Quelltextdateien von der CD in das Arbeitsverzeichnis des neu angelegten Projekts. Anschlieend binden Sie die Quelltextdateien in das Projekt ein, indem Sie Projekt Zu Projekt hinzufgen aus dem Men whlen. Sie knnen dann den von der IDE generierte Quelltext einfach lschen. Dazu klicken Sie in der Dateiliste des Projekts die Dateien base.cpp und base.h jeweils mit der rechten Maustaste an und whlen Sie Datei entfernen. Die Dateien ist vor dem ersten bersetzen noch nicht ins Projekt eingebunden und nicht einmal auf der Platte gesichert. Darum wird beim Entfernen auch die Fehlermeldung auftauchen, dass ungesicherte nderungen noch nicht gespeichert seien. Danach knnen Sie das Projekt bersetzen. Beim ersten bersetzungslauf taucht manchmal die Fehlermeldung auf, dass die Ressourcendatei nicht gefunden wurde oder nicht bersetzt werden konnte. Das ist nicht verwunderlich: Es gibt ja keine. Nach einem zweiten Ansto der Kompilierung verschwindet die Meldung aber wieder. Bei meinen Versuchen habe ich keine Schwierigkeiten feststellen knnen. Sollte wider Erwarten doch etwas nicht rund laufen, bitte ich Sie um Nachricht im Forum dieses Buches.

C.3

Installation unter Visual Studio C++

Microsoft liefert fr Windows Visual Studio aus, das auch von den meisten Firmen verwendet wird, die fr den Windows-Bereich Software erstellen. Die ExpressVersion von Visual Studio stellt Microsoft zum kostenlosen Download bereit: http://www.microsoft.com/germany/Express

322

Installation unter Visual Studio C++

C.3

Fr Visual Studio gibt es ein besonderes wxWidgets-Paket, das sich wxPack nennt. Sie nden es auf der CD oder im Internet unter der folgenden URL: http://wxpack.sourceforge.net/Main/Downloads Die Installation ist sehr einfach. Die Setupdatei wird gestartet, durchsucht den Computer nach einem installierten Visual Studio und richtet alle erforderlichen Dateien und den Projektassistenten ein. Nach der Installation knnen Sie als neue Projektkategorie ein wxWidgets-Projekt auswhlen. Es meldet sich der Assistent, der die Einstellungen vornimmt.

Abbildung C.2 Aufruf des wxWidgets-Assistenten

Der Assistent erzeugt zwar keinen Quelltext, aber Sie knnen die Beispiele von der CD bernehmen und bersetzen. In der aktuellen Version 2.8 von wxPack gibt es Probleme mit dem Imagehandler fr JPG-Dateien. Sie knnen sich helfen, indem Sie die Bilder mit der Hilfe von GIMP in GIF-Dateien umwandeln und die entsprechenden Anpassungen im Quelltext durchfhren. Es steht zu vermuten, dass diese Probleme in einer der nchsten Versionen von wxPack wieder verschwinden werden. Visual Studio hat unter Umstnden Probleme damit, zwei direkt hintereinander stehende C-String-Konstanten miteinander zu verbinden, wenn UNICODE aktiv ist. Das betrifft vor allem die Verwendung der Pfadnamen beim Laden der Image-

323

Installation von wxWidgets

Dateien. Zum Glck hat wxWidgets diese Probleme nicht. Sie knnen also statt wxT(PATH "bild.gif") einfach folgende Variante verwenden:
wxT(PATH) wxT("bild.gif")

Die Informationen zu diesem Abschnitt hat Marcus Bckmann beigesteuert. Er hat das Fachlektorat fr dieses Buch bernommen und betreibt die Website http://www.c-plusplus.de. Spezielle Informationen zu wxWidgets nden Sie dort unter der URL: http://magazin.c-plusplus.de/inhaltsverzeichnis#wxWidgets

C.4

Installation unter Macintosh

Auch fr Macintosh gibt es eine Version von wxWidgets. Mit der freundlichen Untersttzung von Norbert M. Doerner (http://www.cdnder.de) konnten die Beispielprogramme auf Anhieb bersetzt werden. Zunchst bentigen Sie eine Version von wxWidgets fr den Macintosh. Die Version heit wxMac und wird auf der folgenden Seite zum Download angeboten: http://www.wxwidgets.org/downloads Des Weiteren bentigen Sie eine Entwicklungsumgebung. Dazu bietet sich das Programmpaket Xcode an. Dieses nden Sie auf Ihrer MacOS-CD. Sie knnen allerdings auch eine aktuelle Version von der Apple-Homepage herunterladen, wenn Sie sich kostenlos als Software-Entwickler fr Macintosh auf den AppleSeiten anmelden: http://www.apple.com/de/macosx/developers Zunchst muss wxMac entpackt und bersetzt werden, da es nur als Source existiert. Nach dem Entpacken nden Sie im Verzeichnis src des Pakets die XcodeProjektdatei namens wxWindows.xcodeproj. Durch Doppelklick startet Xcode und Sie knnen wxWidgets erzeugen. Das dauert eine Weile. Suchen Sie sich einen Kaffee! Damit sind die Bibliotheken erzeugt. Nun sollen die Beispielprogramme bersetzt werden. Dazu folgen Sie der Anweisung unter der folgenden URL: http://wiki.wxwidgets.org/Mac_OS_X_And_Xcode_1.5_Project_Setup_Guide Sie beschreibt den Vorgang zwar fr Xcode 1.5, es funktioniert aber auch mit neueren Versionen. Sie starten Xcode und whlen ber File New Project Empty Project ein leeres Projekt. Whlen Sie einen Projektnamen und ein passendes Entwicklungsverzeichnis und klicken Sie Finish.

324

Installation unter Macintosh

C.4

Whlen Sie Project New Target. Whlen Sie Legacy Application und den Button Next. Geben Sie einen geeigneten Namen fr das Ziel an und klicken Sie Finish. Nun brauchen Sie eine Terminalsitzung, die Sie unter den Dienstprogrammen nden. Dort geben Sie den folgenden Befehl ein:
wx-config --cxxflags

Der Aufruf erzeugt eine Ausgabe, die Sie markieren und kopieren ( Apfel + C ). Nun whlen Sie im Projektfenster von Xcode ihr Ziel (Target) aus. Whlen Sie dann Project Edit Active Target IhrZiel Dort suchen Sie Settings Simple View GCC Compiler Settings. In das Feld Other C Compiler Flags fgen Sie durch die Tastenkombination Apfel + V das Ergebnis der Terminalausgabe ein. Nun wird dies fr die Linker-Einstellungen wiederholt. Geben Sie ber das Terminal den folgenden Befehl ein:
wx-config --libs

Auch hier kopieren Sie die Ausgaben und wechseln in das Zielfenster von Xcode. Dort suchen Sie nun Settings Simple View Linker Settings auf und dort das Eingabefeld Other Mach-O Linker Flags. Nun mssen die Quelldateien ber den Menpunkt Project hinzugefgt werden.

Add to Project

Wenn Sie nun im Projektfenster den Button Build and Go anwhlen, sollte nach kurzer bersetzung das Programm starten.

325

Installation PortAudio

PortAudio stellt eine portable Schnittstelle zwischen dem Soundsystem und dem Programmierer zur Verfgung. Das Paket wird im Internet als Paket verteilt, das die Quelltexte enthlt. Fr Linux stellen die Distributionen eine fertige Lsung zur Verfgung.

D.1

Unter Linux

Unter Linux muss die Entwicklungsversion von PortAudio nur ber die distributionseigenen Softwareverwaltungen nachgeladen werden. Installation Wenn Sie OpenSuse verwenden, starten Sie einfach das Administrationstool YAST und rufen die Softwareinstallation auf. Dort whlen Sie das Paket portaudio-devel aus und lassen es installieren. Unter Ubuntu ist das Paket PortAudio als Entwicklungsbibliothek vorhanden. Es muss nur das Paket portaudio19-dev mit Hilfe des Adept-Managers nachgeladen werden. Wer dies lieber unter von der Kommandozeile ausfhren mchte oder zu den Debian-Benutzern gehrt, gibt folgendes Kommando ein.
sudo apt-get install portaudio19-dev

Compileraufruf Nachdem das Paket installiert wurde, mssen bei der bersetzung nur die richtigen Bibliotheken eingesetzt werden. Um den Quelltext tut.cpp in das Programm tut zu bersetzen, gibt man den folgenden Befehl auf der Konsole ein:
g++ -o tut tut.cpp -lrt -lasound -ljack -lpthread -lportaudio

D.2

Unter Windows

Wie so oft im Programmiererleben ist es unter Windows etwas komplizierter. Fr PortAudio gibt es leider kein DevPak wie bei wxWidgets, sondern es muss alles in Handarbeit eingerichtet werden. Ich habe leider auch keine fertig bersetzte Bibliothek im Internet gefunden.

327

Installation PortAudio

D.2.1

Compileraufruf

Weil die Erstellung von Bibliotheken nicht bei jedem eitel Sonnenschein auslst, habe ich eine solche Bibliothek fr Bloodshed Dev C++ erstellt. Sie bendet sich auf der CD und heit portaudiolib.a. Neben dieser Bibliothek braucht eine Anwendung, die PortAudio nutzt auch noch die Systembibliothek WINMM.DLL. Um diese ansprechen zu knnen, muss bei der Erstellung des Programms libwinmm.a eingebunden werden. Nachdem Sie ein Projekt fr Ihre Sound-Anwendung erstellt haben, mssen Sie die Bibliotheken in den Projektoptionen hinzugefgen. Rufen Sie dazu Projekt Optionen auf. Im erscheinenden Dialog nden Sie eine Lasche namens Parameter. Rechts unten im Dialog nden Sie einen Button, mit dem Sie Bibliotheken oder Objektdateien hinzufgen knnen.

Abbildung D.1 Projekt-Optionen

Diesen klicken Sie bitte an und erhalten so eine Dateiauswahlbox. Damit laden Sie die oben genannte Datei portaudiolib.a hinzu. Anschlieend wiederholen Sie diesen Vorgang, laden diesmal allerdings die Datei libwinmm.a. Sie nden die Datei im Unterverzeichnis lib des Installationsverzeichnisses Ihres Bloodshed Dev C++. Unter der Lasche Verzeichnisse mssen Sie noch den Pfad zu den Include-Dateien von Portaudio angeben. Auf der CD nden Sie das Verzeichnis portaudio/include Es ist natrlich besser, Sie kopieren dieses auf Ihre Festplatte. Sie knnen den Pfad leicht auswhlen, indem Sie den Button rechts unten klicken. Es ffnet sich eine Verzeichnisauswahlbox.

328

Unter Windows

D.2

Abbildung D.2 Projekt-Optionen-Include-Verzeichnisse

Anschlieend drfen Sie nicht vergessen, den Button Hinzufgen anzuklicken, sonst wird der Pfad nicht bernommen. Danach sollte eine bersetzung ohne weitere Probleme mglich sein.

D.2.2

Selbstbau der Bibliothek

Wenn Sie das Erstellen der Bibliotheken lieber selbst in die Hand nehmen mchten, bentigen Sie die PortAudio-Quelldateien aus den folgenden Verzeichnissen:

portaudio/src/common portaudio/src/os/win portaudio/src/hostapi/wmme

Als Pfade fr die Includes muss zunchst portaudio/include eingetragen werden. Es werden aber auch die oben genannte Pfade bentigt, weil sie auch HeaderDateien enthalten. Bei der Verwendung der Bibliotheken mssen Sie angeben, welche Soundquelle PortAudio verwendet. Unter Windows gibt es als mgliche Quelle das System, DirectX und ASIO. ASIO ist eine Erweiterung der Firma Steinberg, die im Bereich der Musiker-Software Standard ist. Fr ASIO und DirectX bentigen Sie jeweils ein eigenes Software Development Kit (SDK). So viel Aufwand muss nicht sein. Sie knnen mit den folgenden Optionen ASIO und DirectX abschalten. Dazu mssen Sie dem C++-Compiler noch die folgenden zwei Zeilen angeben.

329

Installation PortAudio

-DPA_NO_DS -DPA_NO_ASIO

Sie ffnen dazu den Dialog unter Projekt Optionen. Unter dem Titel Parameter nden Sie eine Spalte C++ Compiler. Dort knnen Sie die Optionen einfach eingeben.

D.3

Lizenz

Die folgende Copyright-Notiz ndet sich im Zusammenhang mit mehreren Beispielen. Da die Beispiele dieses Buches sich natrlich an den Referenzbeispielen orientieren, mchten wir vermeiden, dass sich ein Rechteinhaber von dem Abdruck verletzt sieht. This program uses the PortAudio Portable Audio Library. For more information see: http://www.portaudio.com Copyright (c) 19992000 Ross Bencina and Phil Burk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation les (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Any person wishing to distribute modications to the Software is requested to send the modications to the original developer so that they can be incorporated into the canonical version. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

330

Computer-Oldies

An verschiedenen Stellen des Buchs werden einige ltere Computer erwhnt. Viele Programmierer erinnern sich noch heute gern an ihren ersten Computer, sofern er aus der Zeit vor dem Kompatibilittszwang stammte. Diese Gerte hatten ihre besonderen Eigenheiten, vielleicht vergleichbar mit alten Autos. Hier werden die Stars jener Zeit kurz vorgestellt. Neben den reinen technischen Daten nden Sie auch ein paar Hinweise zum Umfeld und zur Bedienung.

E.1

Apple ][

Der Apple ][ gilt mit dem Commodore PET 2001 als der erste Heimcomputer. Die Vter dieses Computers sind Steve Wozniak, der ein begnadeter Elektroniker war, und Steven Jobs, der auch aus der Computerbranche stammt, aber mit der Zeit sein groes Talent im Marketing bewies. Die beiden grndeten die Firma Apple mit dem Kapital, das sie durch den Verkauf eines VW-Busses und eines Taschenrechners zusammenbekamen, in der legendren Garage von Steven Jobs. Dort wurde zunchst der Apple I in Einzelanfertigung und als Bausatz verkauft. Der Nachfolger, der Apple ][, hatte dann ein Kunststoffgehuse, hatte eine solide Tastatur und war mit seinen acht Slots auf Erweiterbarkeit konzipiert. Die Zielgruppe waren die Computerbegeisterten, die endlich auch einmal einen eigenen Computer haben wollten. Aus diesem Grund war das Gert ausfhrlich dokumentiert. Der komplette Schaltplan und das Listing des ROM waren im Handbuch abgedruckt. Auf diese Weise war es Fremdherstellern mglich, Erweiterungen fr den Apple ][ zu entwickeln.
CPU RAM 6502 bei 1 MHz 48KB + optional 16KB Erweiterung

Textdarstellung 40 Zeichen in 24 Zeilen optional: Videx 80-Zeichenkarte Grak Low Grak High Diskette Baujahr 40 x 48 bei 16 Farben 140 x 192 bei 6 Farben, 280 x 192 mono >optional 140 KB 5,25 Zoll extern Apple ][: ab 1977, Apple ][ plus ab 1979

Tabelle E.1 Technische Daten

Das Gert enthlt 16 KB ROM, in denen eine BASIC-Programmierumgebung und ein paar Systemroutinen sind. Diese BASIC-Umgebung meldete sich auch nach

331

Computer-Oldies

dem Einschalten des Rechners. Die Vorgngergerte waren noch nicht mit diesem sogenannten Autostart-ROM versehen. Ab der Modellreihe ][+, in Europa Apple ][ europlus, war es serienmig vorhanden. Zur Massenspeicherung wurde zu Anfang ein handelsblicher Kassettenrecorder verwendet. Sehr frh gab es eine 5,25-Zoll-Diskettenstation. Wieder war es Wozniak, der mit wenigen Bauteilen eine Laufwerksteuerung entwarf, die zwar sehr schnell, aber ohne Diskettencontroller arbeitete. Die gesamte Steuerung wurde von der CPU mit bernommen. Aus diesem Grund ist es heute nicht mglich, Apple ][-Disketten mit einem normalen 5,25-Zoll-Diskettenlaufwerk in einem PC zu lesen. Nach dem Einschalten prft der Apple ][ die Slots von oben nach unten und bootet vom ersten bootfhigen Gert. Es gibt keinen Timeout. Der Rechner wartet auf das Einlegen einer Systemdiskette. Ist keine Bootdiskette vorhanden, kann der 1 Computer mit der Taste Reset den Bootvorgang abbrechen. Es meldet sich das BASIC im ROM. Der Apple ][ besitzt je nach Ausbaustufe 16 bis 48 KB RAM auf der Hauptplatine. ber den Slot 0 knnen noch einmal 16 KB eingebaut werden, die dann parallel zum ROM liegen. Diese Karten hatten zustzlich eine Flachbandkabelverbindung zu einem RAM-Baustein auf der Hauptplatine. Die Tastatur des Apple ][ konnte zunchst nur mit Grobuchstaben umgehen. Es gab aber bald Umbaustze, die den Button des dritten Paddles verwendete, um die Shifttaste abzufragen. Dazu wurde eine Drahtbrcke von der Tastatur zum Game-IO-Anschluss gefdelt. Dieses Verfahren wurde von vielen Programmen untersttzt und nannte sich Shift-Key-Modication. Ab der Modellreihe Apple //e waren derartige Tricks nicht mehr erforderlich, da diese auch Kleinbuchstaben erzeugen konnten. Modellgeschichte Der Apple ][ wurde von Steve Wozniak entwickelt. Er wollte zunchst einfach einen Computer fr sich allein haben. So bastelte er aus Standardteilen ein Gert, das nicht nur durch eine Tastatur bedienbar war, sondern auch einen Fernseher als Monitor nutzen konnte. Wozniak war es sehr wichtig, dass das Gert auch Farbe beherrschte. Er wollte, dass das Gert auch eine Plattform fr die gngigen Computerspiele war. Er fhrte dieses Gert im Computer-Club vor, und einige Mitglieder wollten auch so einen Computer haben. So wurde der Apple I geboren, der als Bausatz ohne Gehuse vertrieben wurde. Sein Freund Steven Jobs

1 Beim Apple //e muss die Taste ctrl dazugedrckt werden.

332

Apple ][

E.1

bernahm das Marketing und sorgte dafr, dass der Apple II ein ordentliches Kunststoffgehuse bekam. In der ersten Modellrevision, dem Apple ][plus erhielt das Gert einen vollen Speicherausbau bis zu 64 KByte. Das ist der maximale Speicherraum, den ein 8-Bit-Prozessor adressieren kann. Auerdem gab es ein neues BASIC. Das Folgemodell war der Apple //e. Er hatte nun eine Tastatur mit dem man auch Kleinbuchstaben erzeugen konnte. Der Slot 3 war so verndert, dass eine 80Zeichenkarte mit Speichererweiterung eingebaut werden konnte. Damit war ein Speicherausbau mit 128 KByte RAM mglich. Da der 6502 nur 64 KByte adressieren konnte, wurde der restliche Speicher durch das Umschalten von Speicherbnken genutzt.

Abbildung E.1 Apple IIc (Foto: AW)

Als Designknller kam der Apple IIc auf den Markt. Er war um einiges kleiner und enthielt bereits ein Diskettenlaufwerk. Als Andeutung fr seine leichte Transportierbarkeit erhielt er sogar einen Henkel. Dennoch brauchte man fr das Gert einen Monitor, der besonders klein war. Durch dessen geschwungenen Stnder wirkte es, als ob der Monitor ber dem Gert schwebt. Das letzte Modell der Apple ][-Reihe war der Apple IIgs. Er bekam einen 16Bit-Prozessor spendiert, der in einen 6502-Modus umschalten konnte. Das Gert besa einen Soundchip, einen Grakchip und eine Maus. Der IIgs konnte die alten DOS 3.3-Programme ausfhren, aber hatte eine MacOS-hnliche Oberche dabei.

333

Computer-Oldies

Programmiersprachen und Betriebssysteme Der Apple ][ begann mit dem Integer-BASIC, das Steve Wozniak geschrieben hatte. Dieses Minimal-BASIC wurde mit Einfhrung des ][ plus durch das Applesoft BASIC ersetzt. Es stammte von der Firma Microsoft. Wer eine 16-KB-Erweiterung besa, konnte das alte Integer-BASIC in diesen Speicherbereich laden und zwischen beiden Varianten umschalten. Die Kassettenrecorderbefehle und ein einfacher Systemmonitor befanden sich bereits im ROM. Mit den Disketten kam das Apple DOS. Die regulre Version blieb ber lange Zeit DOS 3.3. Mit dem UCSD-System kam ein komplett anderes Betriebssystem fr den Apple auf den Markt, das mit der Sprache PASCAL ausgeliefert wurde. Eine, besser zwei Diskettenstationen waren dringend erforderlich. Spter gab es auch die Sprache Modula-2 fr UCSD. Die Firma Microsoft verkaufte eine Z80-Prozessorkarte fr den Apple ][. Diese erlaubte es, den Apple mit CP/M zu betreiben. CP/M war der geistige Vorgnger von MS-DOS und stellte bereits viele Applikationen zur Verfgung, die dem IBMPC in spteren Jahren einen so glcklichen Start brachten. Dazu gehrte Anwendungssoftware wie Wordstar und dBase. Aus Programmierersicht waren nun fast alle Sprachen verfgbar. Von den alten Sprachen wie COBOL und FORTRAN bis zu C und LISP. Der wirkliche Durchbruch war Turbo-PASCAL, das bereits in der Version 2 mit einer eigenen kleinen IDE aufwartete, auf eine Diskette passte und in einem ungekannten Tempo kompilieren konnte. Auf der 6502-Linie brachte Apple ein neues DOS heraus, das auch Verzeichnisse untersttzte. Dieses hie ProDOS und war begleitet von einigen BusinessApplikationen. Wichtige Erweiterungen Die Slots ermglichten eine problemlose Erweiterung des Gerts. Da Apple diese Slots offen dokumentierte, gab es bald einen umfangreichen Markt fr die passenden Karten. Eine Z80-Karte wurde von Microsoft angeboten. Sie enthielt einen Z80-Prozessor. Durch entsprechende Software wurde diese zum Hauptprozessor des Computers. Die 6502 verwaltete nur noch die Peripherie. Eine 80-Zeichen-Karte ermglichte es, auch Texte am Apple zu erfassen. Fr den Apple ][ plus gab es eine Karte von der Firma Videx. Ab dem Apple //e gab es eine Erweiterung des Slot 3, in den eine spezielle Karte aus dem Hause Apple passte. Diese stellte auch noch weiteren Speicher zur Verfgung, der durch Bankswitching genutzt werden konnte.

334

Atari 400/800

E.2

Mit einer Centronics-Karte konnten die damals gngigen parallelen Drucker angeschlossen werden. Es gab auch unterschiedliche serielle Karten, die eine Kopplung mit anderen Computern ermglichte. Es gab auch Controller, um Disketten mit hherer Kapazitt anzuschlieen. Spter wurde sogar eine Festplatte mit 5 MB Kapazitt verfgbar, die allerdings damals ca. 10.000 DM kostete. Lautsprecher Der Apple benutzt einen sehr einfachen Soundmechanismus. Durch Lesen der Speicheradresse 0xC030 gibt es einen Tick am Lautsprecher. Je nachdem, wie schnell die Ticks aufeinander folgen, hrt man einen Ton. Da das Kassetteninterface auch auf diese Art, nur mit der Adresse 0xC020, ansprechbar ist, kann man den Sound auch auf einen Audioausgang umlegen.

E.2

Atari 400/800

Die Firma Atari hatte groe Erfolge im Bereich der Video-Spiele und Spielautomaten zu verzeichnen, als der Apple ][ erschien. Um den Markt der Heimcomputer nicht zu verpassen, entwickelte die Firma die Modelle 400 und 800. Der Atari 400 war noch offensichtlich eher als erweiterte Spielkonsole ausgelegt. Die Folientastatur und der magere Speicherausbau von 16 KByte sollte das Gert preiswert halten. Der Atari 800 dagegen hatte eine Schreibmaschinentastatur und 48 KByte RAM. Die Herkunft aus den Computerspielen zeigt sich daran, dass der Atari vier Anschlsse fr Joysticks mitbrachte und einen Schacht fr ROM-Erweiterungen, in die man vorzugsweise Spiele einstecken konnte.
CPU RAM MOS 6502C/1,79MHz 64KB

Text-Display 40 x 25 Grak Diskette Baujahr 320 x 192 (mono) optional extern 5,25 Zoll ab 1979

Tabelle E.2 Technische Daten

335

Computer-Oldies

Abbildung E.2 Atari 800 mit Floppy-Disk (Foto: Von McCornick als Public Domain freigegeben)2

Im Atari arbeiten Spezialbausteine fr Grak, Sound und Peripherie. Das Gert war deutlich hher getaktet als die typischen 1 MHz der damaligen Zeit. Der Atari verfgte ber einen eigenen seriellen Systembus, ber den Diskettenlaufwerke oder Druckerschnittstellen angeschlossen werden konnten. Modellgeschichte Die beiden Gerte wurden durch den Atari 600XL und den 800XL abgelst. Die Gerte unterschieden sich in erster Linie im Design mit geringfgigen nderungen. Es gab neue Diskettenlaufwerke, die nun mit 180 KB mehr als doppelt so viele Daten speichern konnten. Als Atari die ST-Reihe auf den Markt brachte, wurden auch die 8-Bit-Computer noch einmal in ein neues Kleid gesteckt, das dem der ST-Serie hnelte. Die Gerte hieen nun 65XE und 800XE und hatten bis zu 128 KByte RAM. Die 8-Bit-Gerte wurden im Preis noch einmal gesenkt und machten nun dem C-64 das Leben schwer.

2 siehe: http://de.wikipedia.org/wiki/Atari_Heimcomputer

336

Sinclair ZX80

E.3

E.3

Sinclair ZX80

Der Sinclair ZX80 war vor allem eines: billig. Der Rechner hatte 1 KB RAM. Von diesem ging auch noch der Bildschirmspeicher ab, sodass fr die Programme nur noch wenige Bytes zur Verfgung standen.

Abbildung E.3 Sinclair ZX81 (Foto: Von Journey234 als Public Domain freigegeben)3

Das Gert hatte keinen Video-Controller, sodass der Bildschirm dunkel wurde, wenn die CPU eine Taste entgegennahm oder ein Programm abarbeitete. Die Tasten waren keine. Der Sinclair hatte eine Folientastatur. Aber so war er der billigste Computer am Markt. Mit dem Nachfolger ZX81 im Folgejahr wurden zwar einige grundlegende Probleme beseitigt, aber das Gert blieb weiterhin die kleinste Mglichkeit, einen Computer zu besitzen.

3 siehe: http://de.wikipedia.org/wiki/Sinclair_ZX81

337

Computer-Oldies

CPU RAM

Z80 mit 3,25 MHz 1 KByte

Text-Display 33 Zeichen x 24 Zeilen Grak Baujahr Pseudoblockgrak von 64 x 44 1980 (ZX80) 1981 (ZX81)

Tabelle E.3 Technische Daten

Wie wenig 1 KB RAM sind, wird deutlich, wenn man berlegt, dass der Bildschirm allein bis zu 793 Byte bentigt. Aus diesem Grund fhrte kein Weg an einer Speichererweiterung vorbei, wenn man mit diesem Computer wirklich programmieren wollte. Es gab 16-KB-Erweiterungen von Fremdherstellern. ZX81Benutzer berichten aber immer wieder, dass der Anschluss nicht besonders verlsslich war. So fhrte manchmal bereits ein leichtes Wackeln am Gert zu einem unerwarteten Reset des Computers. Damit waren dann alle eingegebenen Daten verschwunden. Und dank der Speichererweiterung konnten das nun ja auch ein paar mehr sein. Als Nachfolger wurde der Sinclair Spectrum gebaut, der immerhin statt einer Folientastatur nun eine Gummitastatur mitbrachte, die das Gefhl von Radiergummis vermittelte. Diese Gummikappen drckten dann auf eine Folientastatur. Aber es gab nun auch einen separaten Video-Chip, der noch weitere Aufgaben in dem Computer erledigte. Damit wurde der Bildschirm nicht mehr schwarz. Es kam sogar Farbe ins Heim, und der Speicher war ab 16 KB so gro, dass man damit auch etwas anfangen konnte. Der letzte Sinclair Computer war der Sinclair QL, der auf dem 8-Bit-Prozessor 68008 basierte. Er brachte einige interessante Innovationen. So gab es ein OfcePaket, ein Microdrive, das mithilfe eines Bandes Diskettenfunktionen emulierte und eine Netzwerkkomponente. Das BASIC war multitaskingfhig und konnte Textfenster verwalten. Die Firma Sinclair wurde schlielich von der Firma Amstrad aufgekauft.

E.4

Commodore C-64

Der Commodore C-64 gilt als der meistverkaufte 8-Bit-Heimcomputer der Welt. Das Gert bot eine fr seine Zeit vollstndige Ausstattung und wurde zu einem verhltnismig gnstigen Preis angeboten. Mit dem eingebauten BASIC lieen sich bereits mit einfachen Mitteln gut animierte Computerspiele erstellen. Auf

338

Commodore C-64

E.4

diese Weise wurde der C-64 sicher fr eine ganze Generation zur Einstiegsdroge in die Computerwelt.

Abbildung E.4 Commodore C-64 (Foto: AW)

CPU RAM

MOS 6510A/1MHz, spter 8500 64KB

Text-Display 40 x 25 Grak Diskette System Baujahr 320 x 200 (mono), 160 x 200 (16 Farben) extern 5,25 Zoll z.B. 1541 Basic V.2 1982-1990, C-64C ab 1986, C-64G ab 1987

Tabelle E.4 Technische Daten

Die Firma Commodore hatte bereits Erfahrung im Bereich 8-Bit-Computer. Mit dem PET 2001 hatte sie kurz nach dem Apple ][ einen eigenen Computer auf den Markt gebracht. Es folgten einige eher auf den Bromarkt ausgelegte Computer, wie der 3016, der 4032 oder der 8096. All diese Gerte hatten den Grn-Monitor bereits integriert. Da Commodore der Konkurrenz in Sachen Farbe und Sound nichts entgegenzusetzen hatten, entwickelten Sie zunchst den VC-20, der in den USA VIC-20 hie. Der VC-20 wurde in Deutschland auch als Volks-Computer vermarktet, hatte aber mit 5 KByte Hauptspeicher einfach zu wenig. Immerhin wurde er millionenfach verkauft und bereitete den Weg fr den C-64. Der C-64 wurde der Spielecomputer schlechthin und als solcher ein Riesenerfolg. Bereits im Anwenderhandbuch wird die Programmierung von Sprites und Musik erlutert. Der Erfolg war so durchschlagend, dass er sogar noch lange parallel zum Amiga und in der Konkurrenz zu anderen 16-Bittern produziert wurde. Als Massenspeicher diente anfnglich die Datasette, ein speziell fr den C-64 gefertigte Kassetteneinheit. Dann wurde eine Diskettenstation VC1541 gefertigt, die seriell mit dem C-64 verbunden wurde und so nicht besonders hohe Daten-

339

Computer-Oldies

bertragungsraten ermglichte. Die Station enthielt einen eigenen vollstndigen Computer und das Betriebssystem lag im ROM der Diskettenstation.

E.5

Schneider/Amstrad CPC

Der Colour Personal Computer (CPC) wurde von der englischen Firma Amstrad entwickelt und in Deutschland von der Firma Schneider vertrieben. Es gab sogar einen DDR-Nachbau dieses Gertes namens KC compact. Es wurde pnktlich zur 40-Jahresfeier der DDR in Produktion genommen und wurde dann von den Ereignissen an der Berliner Mauer berrannt.

Abbildung E.5 CPC 664 (Foto: AW)

Das Netzteil des Gerts befand sich im Monitor, sodass dieser immer zum Gert dazugehrte. Ebenfalls immer dabei war der Massenspeicher, der mit in das Gehuse eingebaut wurde. Beim CPC 464 war dies ein Kassettenrecorder, bei den Nachfolgern CPC 664 und 6128 eine 3-Zoll-Diskette. Diese Bauform setzte sich allerdings nicht durch. So wurde die 3,5-Zoll-Diskette der Nachfolger der

340

Schneider/Amstrad CPC

E.5

5,25-Wabbelscheibe4 . Die 3-Zoll-Disketten sind heute Raritten und nur schwer erhltlich.
CPU RAM Text-Display Betriebssystem Monitor Auflsung Diskette (CPC 664 und CPC 6128) Baujahr Z80A 4 MHz 64 KB, CPC-6128: 128 KB 24 Zeilen zu je 80 Zeichen CP/M 2.2 und AMSDOS Farbe oder grn mit integr. Netzteil 640 x 400 3 Zoll 169 KB je Seite 1984 (CPC 464) 1985 (CPC 664 und CPC 6128)

Tabelle E.5 Technische Daten

Der CPC 664 brachte gegenber seinem Vorgnger vor allem eine Diskettenstation statt des Kassettenrecorders mit. Er war nicht lange am Markt prsent. Wenige Monate nach seiner Einfhrung wurde er durch den CPC 6128 ersetzt. Der CPC 6128 hatte 128 KB RAM und erhielt Schnittstellen mit ordentlichen Steckern statt den bis dahin verwendeten Platinenkontakten. Neben seinem AMSDOS und dem Locomotive BASIC konnten die CPC-Gerte mit Diskettenlaufwerk mit CP/M gebootet werden. Der CPC 6128 verwendete sogar CP/M 3.0. Bitterer Aprilscherz Im April 1985 verffentlichte die Computerzeitschrift Hebdogiciel einen Artikel ber einen angeblich neuen CPC, den CPC 5512. Mit Computerteilen und Papier wurde ein Fotomodell geschaffen, dass es gar nicht gab und an Details wie dem Hohen C, das die Soundkarte liefern konnte, als Aprilscherz erkennbar. Obwohl die Geschichte in der nchsten Ausgabe der Zeitschrift dementiert wurde, entstand das Gercht ber einen Nachfolger des CPC 6128, der in Frankreich den Verkauf des CPCs einbrechen lie. Die Zeitschrift wurde sogar von Amstrad verklagt.

4 Wabbelige Scheibe ist brigens die korrekte bersetzung fr den Begriff oppy disc.

341

Computer-Oldies

E.6

Epson HX-20

Der Epson HX-20 wird manchmal als erster Vorfahr des Laptops bezeichnet. Es ist ein tragbarer Computer im DIN-A4-Format und wird ber den Akku betrieben.

Abbildung E.6 Epson HX-20 (Foto: AW)

CPU RAM Text-Display Grak

Hitachi 6301 bei 2,45 MHz 8KB, aufgerstet auf 32KB 20 Zeichen x 4 Zeilen 120 x 32 (mono)

Massenspeicher Microkassettenlaufwerk System Baujahr Basic ca. 1983

Tabelle E.6 Technische Daten

Die Hardware Die Tastenbelegung ist deutsch und hat deutsche Umlaute. Das war in jener Zeit sehr ungewhnlich. Eingebaut war immer ein Drucker und ein LCD von 8 Zeilen

342

Epson HX-20

E.6

je 20 Zeichen. Optional war ein Microcassettenlaufwerk eingebaut, das sowohl Daten als auch Programme speichern konnte. Die Kassetten sind die gleichen, die man bei einem handelsblichen Diktiergert benutzt. Stromversorgung Es gibt einen 9V-Anschluss mit dem Pluspol auen. Das Originalnetzteil hat 600mA. Nach Empfehlung des Herstellers soll damit 8 Stunden lang der interne Akku aufgeladen werden. Es soll streng darauf geachtet werden, dass diese Zeit nicht berschritten wird. Der Betrieb selbst soll ohne Netzteil erfolgen. Diese Manahmen sollen den Akku schtzen. Kurz bevor der Akku leer ist, erhlt man die Meldung, dass der Akku wieder geladen werden muss. Tatschlich verblfft ein lange gelagerter HX 20 damit, dass er sich nach Anschluss an das Netzteil nicht sofort einschalten lsst. So wird manches Gert bereits als defekt betrachtet. Erst wenn der Akku eine gewisse Mindestladung hat, lsst sich das Gert am Netzteil einschalten. Allgemeine Bedienung Fr die damalige Zeit war ein Computer in der Regel nicht leicht transportabel. Es wurde eine Steckdose, ein Monitor oder Fernseher und ein Massenspeicher, meist ein Kassettenrecorder bentigt. Dazu kamen die Verbindungskabel. Der Epson HX-20 hatte einen Akku, Drucker und Kassettenrecorder an Bord. Das Gert war nicht sehr viel grer als ein DIN-A4-Blatt. Wer also Computerprogramme unterwegs einsetzen wollte, der war mit diesem Gert bestens bedient. Grak Auf dem kleinen LCD konnte sogar Grak angezeigt werden. Und auch diese wurde vom BASIC vollstndig untersttzt. Mit dem Befehl LCOPY konnte die erzeugte Grak auf dem Minidrucker ausgegeben werden. Zur Veranschaulichung dient folgendes Beispielprogramm:
10 SCREEN 0,0 20 CLS 50 ST=3 100 GCLS 200 FOR I=0 TO 31 STEP S 300 LINE (0,I)-(119,31-I),PSET 400 NEXT 800 COPY
Listing E.1 Grakprogramm fr den Epson HX-20

343

Computer-Oldies

E.7

IBM PC

Der Start des IBM PC war denitiv lieblos. Die Zeit war reif fr die 16-BitProzessoren und man rechnete damit, dass sich diesmal 68000 von Motorola durchsetzen wrde. Es wurde auch gemunkelt, dass IBM in den bisher ignorierten PC-Markt eingreifen wrde. IBM hatte zu dieser Zeit fast das Monopol auf alles, was Computer betraf. Nur im Bereich der PCs hatten sie das Feld den kleinen Firmen berlassen.

Abbildung E.7 IBM PC (Foto: AW)

Aber nun war der Moment gekommen, wo auch in den Rechenzentren hin und wieder ein Microcomputer eingesetzt werden sollte. Und die Rechenzentren erwarteten eine professionelle Lsung von IBM. IBM ging davon aus, dass dieser Markt wenig abwerfen wrde. Es konnte sein, dass man nur ein paar Gerte verkaufen knnte, aber es knnte auch passieren, dass der Markt doch grer als erwartet war. IBM entschloss sich, den ersten IBM PC aus Standardbausteinen herzustellen, die jederzeit leicht am Markt zu bekommen waren. Der Aufwand, ein Gert mit Spezialbausteinen zu entwerfen, knnte den Gewinn vielleicht bersteigen. Also agierte man konservativ.

344

IBM PC

E.7

Das Gert, das man auf den Markt brachte, hatte zwar eine CPU mit interner 16-Bit-Struktur, extern aber einen 8-Bit-Datenbus. Das Gert hatte keine Grak, sondern nur eine 80-Zeichenkarte, die in einem der Slots steckte. Ob es jeweils eine Grakkarte geben sollte, knnte man ja immer noch entscheiden.
CPU RAM Text-Display Grak Diskette Platte 8088 bei 4,77 MHz 512 KB ber Erweiterungskarte 25 Zeilen zu je 80 Zeichen (farbig: grn) optional Farbe 320x200 5,25-Zoll-Diskette mit 360 KB (doppelseitig) 10 MB MFM

Betriebssystem PC-DOS bzw. MS-DOS


Tabelle E.7 Technische Daten

Der Intel-Prozessor 8088 wurde seinerzeit gewhlt, da er einerseits kompatibel zum 8080 war und damit die Anwendersoftware von CP/M leicht portabel war. Der 8088 hatte gegenber dem 8080 den Vorteil, dass er einen Adressraum von 1 MB hatte. Gegenber dem 8086 brauchte der 8088 keinen 16-Bit-Adressbus, was das Layout deutlich verbilligte. Die Innovationen elen aus. Selbst einen Kassettenrecorderanschluss hatte der IBM PC noch. Die 5,25-Zoll-Diskettenlaufwerke waren mit 180 KB oder 360 KB in der Gre zeitgem. Der 5150 hat eine Festplatten mit 10 MB. Es handelt sich um MFM-Laufwerke. In meinem Gert werkelt eine Seagate ST-412. Das Format ist 5,25 Zoll mit voller Bauhhe, also doppelt so hoch wie die 5,25-Einschbe heutzutage sind. Das Gert hat fnf Slots. Hier werden Karten fr serielle oder parallele Schnittstellen eingesteckt, aber auch fr Speichererweiterung, Bildschirmkarte, der Disketten- und der Festplattencontroller. Statt des heute blichen BIOS wurde beim Original-PC noch die gesamte Hardware-Konguration ber DIP-Switches geschaltet. Sowohl der Speicherausbau als auch die Anzahl der Diskettenlaufwerke wurde auf diese Weise konguriert. Die Switches waren in DIL-Gehusen eingebaut und so gro, dass man sie gerade noch mit einer Kugelschreibermine umschalten konnte.

345

Computer-Oldies

E.8

Apple Macintosh

1984 brachte Apple unter groem PR-Getse den Macintosh auf den Markt. Tatschlich war er technisch und auch von der Zielrichtung etwas vllig Neues. Whrend sich bis dahin die Personal Computer noch zu einem nicht unerheblichen Teil an den Programmierer wendeten, wandte sich der Mac an den Anwender.

Abbildung E.8 Macintosh SE 30 mit Tasche (Foto: AW)

Die grasche Oberche war neu, wenn man mal von den gescheiterten Versuchen des Xerox Alto und der Apple Lisa absieht, die fr damalige Verhltnisse einfach zu teuer waren. Hinzu kam, dass der Normalbrger sich zu diesem Zeitpunkt gar nicht vorstellen konnte, wozu er berhaupt einen Computer brauchen knnte.
CPU RAM Grak Diskette Monitor 68000 bei 8 MHz ab 128 KB (maximal 4 MB) 512 x 384 mono 1,4 MB 3,5 Zoll s/w 9 Zoll intern

Herstellung ca. 1984


Tabelle E.8 Technische Daten

Das Gehuse war kaum hher als ein DIN-A4-Ordner. Darin befand sich ein 9 Zoll groer Schwarz-Wei-Monitor, ein Diskettenlaufwerk und die Hauptplatine des Computers. Lediglich Tastatur und Maus waren separat. Entwicklung des Betriebssystems Das Original-Betriebssystem war wie seinerzeit bei Personal Computern blich ein Single-Tasking-System. Mit der Version System 7 wurde es mglich, mehrere

346

Apple Macintosh

E.8

Applikationen gleichzeitig zu starten und zwischen den Anwendungen hin- und herzuschalten. Der Umbau zum Multitasking-System war nur notdrftig durch den Rest des Betriebssystems untersttzt worden. So musste die Verteilung des Speicherraums noch vom Benutzer eingestellt werden. Zwischendurch hatte Steve Jobs Apple verlassen und 1986 die Firma NeXT gegrndet. Sie baute einen sehr leistungsstarken Computer mit grascher Oberche und fortschrittlichem System auf der Basis eines UNIX-Microkernels. Zehn Jahre spter kaufte Apple die Firma NeXT inklusive ihres alten und neuen Chefs auf. Steve Jobs sorgte dafr, dass der Mac nun ein solides UNIX-Fundament fr sein Betriebssystem bekam. Die Nachfolgeversion von MacOS 9 erhielt den Namen MacOS X. Dabei stand das X sowohl fr die rmische Zahl 10 als auch fr den letzten Buchstaben von UNIX. Modellgeschichte Der Firma Apple gelang es, den Macintosh immer wieder umzubauen und weiterzuentwickeln. Zunchst wurde der erste Mac auf der Basis des Motorola 68000 geschaffen. Die Nachfolgemodelle wurden leistungsfhiger und verwendeten den 68020, 68030 und 68040. Mit IBM und Motorola entwickelte Apple dann den PowerPC-Prozessor. Er ersetzte den bisherigen Prozessor. Das Betriebssystem erkannte, wenn eine Software noch fr den alten Prozessor geschrieben war und emulierte ihn, damit die Software laufen konnte. Auf diese Weise konnten die Anwender ihre alte Software einfach weiterbenutzen. Die Performance-Einbuen durch die Emulation machte der schnellere Prozessor wieder wett. Der PowerPC wurde in verschiedenen Generationen geliefert, bis er gegen die Intel-Prozessoren getauscht wurden. Es war keineswegs ein Wechsel zum besseren Prozessor. Motorola hatte bei den geringeren Stckzahlen mehrfach Schwierigkeiten gehabt, dem Geschwindigkeitswettlauf mit Intel standzuhalten. Auch bei der Peripherie wurden Mastbe gesetzt. So erhielt der Mac bald den Apple Desktop Bus (ADP), ber den Tastatur und Maus angeschlossen wurden, der aber auch geeignet war, langsame Peripheriegerte anzusteuern. Die Festplatten wurden per SCSI angeschlossen, und jeder Mac hatte einen SCSI-Anschluss fr externe Gerte. Als Nachfolger wurde der Firewire entwickelt, der einen seriellen Anschluss schneller Peripherie hotplugged ermglichte. Es war also mglich, im laufenden Betrieb die Gerte zu wechseln. Schlielich wurde der ADP durch USB ersetzt. Der Mac erkannte bereits von Anfang an an der Steckerbelegung, welcher Monitor angeschlossen war und schaltete die Ansteuerung automatisch um. Es war typisch fr den Macintosh, dass eine hinzugekaufte Peripherie einfach nur angeschlossen werden musste. In der Regel brauchte der Anwender nichts weiter zu tun, um das Gert nutzen zu knnen.

347

Computer-Oldies

E.9

Atari ST

Der Atari ST wurde fr viele Computerbesitzer der Einstieg in die 16-Bit-Klasse und wurde in dieser Hinsicht quasi der Nachfolger des C-64. Das Gert lieferte die Leistungsdaten des Apple Macintosh, kostete aber nur einen Bruchteil des Vorbilds. Die grasche Oberche stellte das GEM dar, das der Oberche des Macs erstaunlich hnlich war. Auf der anderen Seite was das Basisbetriebssystem auf der Basis einer CP/M-Variante fr den 68000 Prozessor gebaut. Atari lieferte sogar kostenlos einen Z80-Software-Emulator mit CP/M aus, auf dessen Basis auch Wordstar, dBase und Turbo-Pascal liefen. So konnte er sogar die gebruchlichen Programme laufen lassen, die der IBM PC verwendete, allerdings in der 8-BitVariante.

Abbildung E.9 Atari Mega ST mit Festplatte und Wechselplatte (Foto: AW)

CPU RAM

68000 bei 8 MHz 1 MB auf max. 4 MB

Grak mono 640 x 400 Grak farbe Diskette Festplatte Datum 640 x 200 und 320 x 200 720 KB 3,5 Zoll optional ACSI extern ab 1985

Tabelle E.9 Technische Daten

348

Atari ST

E.9

Gemeinsam mit dem PC hatte er das Dateisystem und die Peripherieanschlsse. Whrend dieser zu jener Zeit aber nur MS-DOS kannte, hatte der Atari ST mit GEM eine grasche Oberche, eine Maus und die 3,5-Zoll-Disketten, wie man sie vom Mac kannte. Als Besonderheit brachte der Atari ST einen serienmigen MIDI-Anschluss, mit dem es mglich war, Synthesizer und anderes MusikerEquipment zu steuern. Der erste Atari 520 ST erschien etwas bereilt. Das Betriebssystem wurde aufgrund der vielen Fehler noch nicht ins ROM gebrannt, sondern musste von der Diskette geladen werden. Dadurch war dann aber auch der eigentlich ppige Speicher bereits zu einem groen Teil belegt. Schnell lieferte Atari daraufhin den 520 ST+ aus. Dieser hatte nun 1 MByte RAM. Wer das Gert aufmachte, konnte sehen, dass die zustzlichen Speicherbausteine auf die bisherigen aufgeltet waren und mithilfe eines frei verfdelten Drahtes angesteuert wurden. Der 520 ST hatte die Hauptplatine unterhalb der Tastatur. Die Diskette war extern. Beide Teile hatten ein eigenes externes Netzteil. Dadurch ergab sich ein umfangreiches Kabelgewirr. An Schnittstellen bekam er eine parallele und eine serielle Schnittstelle spendiert. Die Maus wurde an einen von zwei speziellen Anschlssen angeschlossen, an den auch ein Joystick passte. Es gab einen Cardridge-Anschluss und einen MIDI-Anschluss. Dieser machte das Gert vor allem fr Musiker interessant. Ein ACSI-Port, eine abgemagerte SCSI-Variante, ermglichte den Anschluss von Festplatten und Laserdrucker, die speziell von Atari geliefert wurden. Die ST-Serie lief mit 8 MHz und hatte einen maximalen Speicherausbau von 4 MB. Endlich kam mit dem Modell 1040 ST ein Gert heraus, der das Betriebssystem im ROM, einen regulren Speicher von 1 MB hatte und bei dem die Floppy wie auch das Netzteil in das Gehuse eingebaut wurde. Damit war der riesige Kabelwust verschwunden, den der 520ST mitbrachte. Der Atari Mega ST sah etwas schicker aus, da nun die Hauptplatine in einem separaten quadratischen, achen Gehuse untergebracht war. In diesem stapelbaren Gehuse wurden auch Festplatten und Wechselplatten geliefert. Ansonsten hielten sich die Innovationen in Grenzen. Mit dem Atari TT und dem Atari Falcon stieg Atari noch auf den 68030 um. Der Falcon wurde mit DSP-Bausteinen ausgestattet, mit denen Musikaufnahmen in CD-Qualitt mglich waren. Aber auf anderen Bereichen hatte Atari seine Produkte nicht auf dem Laufenden halten knnen. So wurde der Trend zum Multitasking und zur Vernetzung verschlafen. Zum Schluss erweckte es den Anschein, als wolle man keine Investitionen mehr machen, sondern nur solange wie mglich mit der existenten Technologie Geld verdienen und dann die Firma schlieen.

349

Computer-Oldies

E.10

Commodore Amiga

Nach einer heien bernahmeschlacht bernahm die Firma Commodore die nanziell angeschlagene Firma Amiga. Die brachte nun ihre Neuentwicklung, einen 16-Bit-Computer mit 68000er-Prozessor in die Firma ein. Jack Tramiel, der Chef von Atari, hatte die bernahmeschlacht verloren und musste nun umdenken. Er schaffte es aber, mit einer 16-Bit-Eigenentwicklung auf der Basis des 68000Prozessors und einer graschen Oberche noch vor dem Amiga auf den Markt zu kommen und damit in der zweiten Runde zu punkten.
CPU RAM Grak 68000 bei 7,16 MHz 256 KB 320 x 200 bei 32 Farben bis 640 x 400 bei 16 Farben

Diskette 880 KB 3,5 Zoll


Tabelle E.10 Technische Daten

Amiga heit Freundin, und so wurde dieser Computer im Anfang oft mit die Amiga bezeichnet. Aber nach einiger Zeit verlor das die gegen der Amiga. Am 23.7.1985 wurde der erste Amiga vorgestellt. Das Gehuse enthlt Hauptplatine, Diskettenstation und Netzteil. Die Tastatur ist durch ein Spiralkabel verbunden. Der Amiga 1000 hat einen etwas anderen seriellen und vor allem einen etwas anderen parallelen Stecker. Man durfte also nicht einfach PC-Kabel einstecken. Der Amiga verfgte von vornherein ber gute Farbgrakfhigkeiten und Stereo-Sound. Allerdings ackerte das Bild der Amiga relativ stark, sodass der Brosektor fr das Gert nicht in Frage kam. So wurde das Segment der Computerspiele, der Farbbilder und der Multimedia das Heimatgebiet dieses Rechners. Das Betriebssystem beherrschte bereits das premptive Multitasking, und die grasche Oberche war beeindruckend schnell. Das Gert wurde nur etwas mehr als ein Jahr lang produziert. Die Gerchte besagten, die Produktionskosten seien hher als der Verkaufspreis gewesen. Jedenfalls wurden bereits ein Jahr spter zwei Gerte als Nachfolger auf den Markt gebracht. Der Amiga 500 kam als Kompaktgert, das Tastatur und Hauptplatine in einem Gehuse unterbrachte. Alle Peripherie wurde extern angeschlossen. Der Amiga 2000 hatte dagegen wie sein Vorgnger eine separate Tastatur und die Floppy und das Netzteil in einem groen Gehuse, das nun sogar ber Slots verfgte, sodass der Amiga 2000 auch erweiterbar war. brigens wurde der Amiga 2000 in Braunschweig gefertigt. Allerdings bewirkte auch bei den Folgegerten das Interlace des Bildschirms ein heftiges Flackern.

350

Commodore Amiga

E.10

Ab etwa 1990 folgten der Amiga 1200 mit dem 68020 und die Modelle Amiga 3000 und 4000, die mit den Prozessoren 68030 und 68040 ausgestattet waren. Schlielich geriet die Firma Commodore in nanzielle Schwierigkeiten und wurde zerschlagen. Die Markennamen wurden mehrfach von verschiedenen Firmen gekauft und fr die Vermarktung diverser Produkte eingesetzt.

351

Index
A
Adjazenzmatrix 48, 54 ALGOL 17 Amstrad CPC 340 Apple Apple ][ 87, 331 Macintosh 346 Aprilscherz 14 ASCII 30 Assembler 16 Atari 168 Atari 400 und 800 335 Atari ST 348 Avalanche 241 Spielregeln 254 Esoterik 151

F
Fibonacci 115 FIFO 247, 249 Fliekommakodierung 242 Fluglandung 167 FORTRAN 17, 70 Forum 14 Freier Fall 64 Frogger 255

G
Galaxis 207 Gates, Bill 167 GIMP 231

B
Bermuda 208 Bildschirmackern 236 Binrbaum 37 Bloodshed Dev C++ 315 Busy Waiting 180

H
Hessisch 89

I
IBM 167 PC 344

C
CeBit 139 Commodore 168 Amiga 350 C-64 338 CP/M 167

J
Jelzin, Boris 207 Jobs, Steven 139, 331

D
deque 249 Diagonalen 224 Digital Research Inc. 167 Dijkstra, Edsger Wybe 45, 170

K
Kaugummispucken 74 KDevelop 311 Kernighan, Brian 14 Kildall, Gary 167 Kirk, James T. 30

E
Eichhrnchen 255 Eliza 21 Entity Relationship Modell 25 Epson HX-20 342

L
Labyrinth 107

353

Index

M
Macintosh 324 Marketing 139 Miner, Jay 168 Mondlandung 64

Superprogramm 139

T
Threads 281 Tiere raten 36 Timer 180 Tramiel, Jack 168 Twain, Mark 42

N
Nightdriver 191

U
Umlaute 30 UNICODE 30 UTF-8 31

O
Olivetti P652 63

P
PASCAL 17, 92 Patterson, Tim 167 Polling 180 Portabilittsprobleme 237 PortAudio 327 Pygmalion 22

V
Visual Studio 322

W
wchar_t 31 Weizenbaum, Joseph 21 Win32-API 183 Wozniak, Steve 87, 88, 331 wxWidgets 139 Bloodshed Dev C++ 320 Ereignis Fenstervergrerung 147 Fokus 260 Installation 319 KDevelop 319 Mac 324 Mausereignis 148, 159, 197 Maustasten 221 Menpunkt mit Haken 230 Mens 144, 157, 230 Messagebox 145 Sounds 280 Tastaturereignis 273 Timer 180 Visual Studio 322

R
Rekursion 38 Relationale Datenbank 25 Ritchie, Dennis 14

S
Schneider CPC 340 Scotty 21 Shaw, George Bernhard 22 Shivji, Shiraz 169 Sinclair ZX80 337 Singleton 55, 201 Spock, Mister 30 STL deque 249 map 49 vector 28, 47 string nd 34 replace 34

X
Xerox 137

354

Das könnte Ihnen auch gefallen