![]() |
![]() |
Dieses Kapitel braucht ein kleines Vorwort. Es war nämlich gar nicht geplant. Anlass war, dass mein kleiner grosser Sohn (4 1/2 Jahre) sich vor zwei Monaten sehr für Ritter begeisterte. Zeitgleich schaute ich mal in der Wikipedia nach einem Artikel über den Uralt-CGA-Grafikadapter, den ich massgeblich mitverfasst hatte. Und siehe da: Die Beispielgrafiken waren rausgenommen, weil in Deutschland das Copyright des Spieleautors durch Screenshots verletzt wird, wenn kein explizites Einverständnis vorliegt. Das Dritte, was hinzukam, war der Gedanke: "Wie lange brauchst du, so ein kleines pixeliges 2D-Jump-and-Run-Spiel mit den Mitteln von Freebasic und eines modernen PC zu programmieren?" Und es kam sogar noch eine vierte Komponente hinzu: Ich war gerade beim Folgekapitel "Soundlib", so dass so ein kleines Spiel eine willkommene Gelegenheit war, die Verwendung von Sound zu demonstrieren. Und fertig war das Vorhaben: Programmieren eines kleines Spiels im CGA-Look mit Rittern.
Herausgekommen ist allerdings kein fertiges Spiel, sondern eher eine Art Fingerübung dafür. Es fehlt noch eine Menge zu einem guten kleinen Spiel, vor allem ein etwas spannenderes Verhalten der gegenerischen Ritter, mehr Bildschirmelemente (Burg, Mauern), Verwaltung von Punkten, Leben, neuen Rittern, Highscore usw. Auf der anderen Seite: Ich habe alles in allem ca. 20 Stunden reingesteckt. Das ist schon deutlich weniger als die Programmierer, die vor 20 Jahren solche Spiele kommerziell erstellten und vertrieben...
...kann man sich hier runterladen. (290K) In ein leeres Verzeichnis entpacken, oma_rit1.exe starten.
Das war's schon.
Ach ja: Die Tastatur reagiert manchmal ziemlich träge. Längeres oder mehrfaches Drücken hilft. Auf dieses Problem komme ich weiter unten noch zu sprechen.
In oma_rit1.bas findet man den Quellcode. Immerhin schon mal 40K lang. Ich habe alle Debuggingkommandos dringelassen und nur auskommentiert.
Aus aus dem Vorwort hervorgeht: Der Code ist nicht als Lehrbeispiel konzipiert. Man könnte noch vieles von dem, was wir bisher gelernt haben, einbauen, vor allem eine saubere Projektsttruktur, aber auch eine sauberere OVP usw. Betrachten Sie das als Übungsgelegenheit!
Das Spiel hat auch noch einen expliziten Grafikfehler. Siehe Spritemanagement weiter unten.
Im folgenden will ich auf einige Aspekte der Programmierung eingehen. Das ist er erste Zweck des Exkurses. Der zweite ist es, dieses Programm als Skelett für die weiteren Kapitel herzunehmen, um immer wieder hier und da von dem Gelernten einzubauen und es so zu erweitern.
Weiter vorne haben wir uns ja schon einmal ausführlicher mit Sprites befasst und sogar eine Mini-Spritelib erstellt. Wir sind allerdings nicht drauf eingegangen, wie man mit vielen Spritevorlagen arbeitet und wie man die Sprites am besten zeichnet. Ach, a propos "Zeichnen": Das ist meine grosse Minderbegabung. Für solche Kunsthelden wie mich ist es ein Ausweg, auf solche Pixelauflösungen zurückzugreifen: Da kann man nicht allzuviel Schlimmes anrichten...
Um die Sprites zu zeichnen, benutzt man ein Bitmap-Malprogramm. Da gibts richtig teure Sachen wie Adobe Photoshop, aber für unseren Zweck tun's da schon Programme, die man umsonst oder für lau bekommt. Ich hab als Bitmap-Zeichner eine alte Version von Paintshop Pro (Version 5), die mal auf der Heft-CD einer Computerzeitschrift dabei war. Aber die 4er-Version tut's auch und die kann man sich kostenlos runterladen. Im Freeware-Bereich führend ist übrigens Gimp, aber das ist dann schon wieder Overkill für unseren Zweck.
Als erstes initialisiert man sich ein neues Bild mit Schwarz als Hintergrund. Grösse: Gross genug, 800 x 600 oder sowas. Man sollte sich allerdings schon mal überlegen, wie gross die Sprites ungefähr werden sollten. Anvisieren sollte man sowas wie 20 x 20, 30 x 30 oder so.
OK, in unserem Fall haben wir nur vier feste (hässliche) Farben zur Verfügung: Schwarz, Weiss, Magenta und Cyan (Türkis). Damit hab ich dann erstmal das Pferd gemalt. Grosses Zoom, damit man sich nicht einen abbricht. Und sehr wichtig: Man holt sich die Farben nicht geradewohl aus der Palette, sondern "pickt" sich mit der Pipette wieder genau das Magenta oder Cyan, das man vorher benutzt hatte! Sonst bekommt man im Programm mit dem Farbmanagement ziemliche Probleme.
Well, als das Pferd fertig war, hab ich das gleich mal mehrfach dupliziert. Duplizieren ist wichtig! Das stellt sicher, dass die verschiedenen Sprites sich genau entsprechen.
Wie man auf dem Bild sieht, wollte ich ursprünglich die Sprites kleiner machen. Das hat dann nicht funktioniert. Dann hab ich das Pferd nochmal grösser gemalt (links oben). Und das gab mir dann die Grösse der Sprites vor. Erst dann wurden die gelben Rahmen erstellt.
Die Sprites werden animiert, wobei ein Sprite 2 bis 3 Bilder aufnimmt. Meistens geht es um die Beine des Pferds. Hier ist natürlich besonders wichtig, dass es sich ansonsten um ein exaktes Duplikat des Ritters plus Pferd handelt! Also Copy und Paste benutzen!
Im Prinzip kann man natürlich jedes einzelne Bildchen in eine einzelne Datei stellen. Aber das sieht dann irgendwie im Programmverzeichnis nicht so schön aus, wenn man das Programm immer weiter ausbaut und dann mal schnell zweihundert Winzdateien rumfliegen hat. Ausserdem wird das auch beim Programmieren unübersichtlich. Eine bewährte Technik ist es, alles in ein bmp zu packen. Und zwar so, dass man im Programm dann die einzelnen Sprites leicht auseinanderhalten kann. Daher die Form der untereinanderliegen, durch eine gelbe 1-Pixellinie getrennten Sprites in sprite.bmp.
Bei der Spriteverwaltung habe ich einen nicht ganz so schönen Weg gewählt. Wie wäre der schöne Weg? Man definiert eine Spriteklasse tsprite und liest bei der Initialisierung des jeweiligen Objekts die entsprechenden Icons ein. Dann sind alle Daten sauber in Objekte getrennt. Nachteil allerdings: Das bmp-File muss x-mal eingelesen werden.
Das ist übrigens ein allgemeines Dilemma der OVP- und OOP-Programmierung: Sie geht oft (zunächst) zu Lasten der Ressourcen.
Ich habe einen Kompromiss gewählt: Zunächst werden in der Routine loadsprites() alle Sprites in einen Puffer gepackt, in ein flaches globales Spritearray. Die Spriteobjekte selbst erhalten nur einen Zeiger auf diesen Puffer. Vorteil: In der Routine loadsprites() können noch alle Vor-Transformationen zentral vorgenommen werden und das File muss nur einmal eingelesen werden.
Zentrales Problem beim Einlesen von Sprites ist immer: Wie mache ich den Hintergrund transparent? Wie wir im ersten Spritekapitel erfahren hatten, muss, damit die Abbildung mittels PUT (x,y),img,TRANS funktioniert, der Hintergrund den RGB-Code (&hFF,0,&hFF) tragen. Da muss man nun aufpassen: Das ist genau das Magenta, das eigentlich Teil unseres CGA-Spektrums ist. Durch Editieren der Farbpalette von sprite.bmp (z.B. in Irfanview) sollte man die Magentafarbe also leicht anders einstellen, z.B. leicht dunkler.
Hauptaufgabe ist nun, das Schwarz des Hintergrunds (das sehr wahrscheinlich kein reines Schwarz ist!) in die Trans-Farbe (&hFF,0,&hFF) umzuwandeln. Im damaligen Kapitel hatten wir das an der bmp-Datei selbst gemacht. Dieses Mal gehen wir einen anderen Weg, der auch gut funktioniert.
Wir malen zunächst die ganze sprite.bmp mit PUT auf einen leeren Screen. Dann speichern wir jedes Sprite Pixel für Pixel in einem uinteger-Array. Dabei können wir die notwendige Umwandlung gleich vornehmen. Das geschieht dadurch, dass wir uns den Farbcode des allerersten Pixels in Pixelzeile 2 von sprite.bmp merken - das ist der Code für das verwendete Schwarz. Und den ersetzen wir im Weiteren immer durch (&hFF,0,&hFF). Den resultierenden Code speichern wir im uinteger-Buffer.
Mit diesem Buffer selbst können wir natürlich nichts anfangen. Wir müssen die Pixel in das Image-Format von Freebasic bringen. Dazu benutzen wir nun einen zweiten Durchgang, bei dem wir Sprite für Sprite nochmal auf den Screen plotten und mittels GET() ins richtige Format und in den globalen Sprite-Buffer sprites() holen. Dieser besteht übrigens aus einem Array von any-Pointern. Erst bei der Erzeugung des jeweiligen Sprites wird der zugehörige Speicherplatz mit imagecreate() erzeugt.
Wir nutzen also für unsere Einleseroutine, dass Freebasic selbst schon bmp-Dateien einlesen kann. Allerdings scheitert unsere Methode, wenn wir mehr als 20 Sprites einlesen wollen, an der begrenzten Höhe des Screens. Daher ist mit loadsprites_frombmp(buf()) die Möglichkeit da, mehrere bmp-Files in mehrere Buffer einzulesen. Auf diese Art und Weise werden zunächst die ersten 20 und dann die letzten 6 Sprites eingelesen:
loadsprites_frombmp(buf1(),20,"sprite.bmp") loadsprites_frombmp(buf2(),6,"sprite3.bmp")
An sich wäre das vornehm. Aber es ist einfach nicht notwendig. Selbst auf einer relativ langsamen Maschine sieht man das kurze Aufblitzen des Lade-Screens kaum oder gar nicht. Wen es dennoch stört, der kann durchaus diesen ganzen Zwischenplotvorgang auf Screenpage 1 verlegen, während Screenpage 0 im Vordergrund schwarz bleibt. Wenn jemand mal Tausende Sprites einliest, kann er ja auch im Vordergrund auf Screen 0 einen netten blauen Verlaufsbalken zeichnen...
Diese sieht man am Beginn des Sourcefiles auf einen Blick:
'----------------------------------------------------- TYPE tsprite img(10) AS ANY PTR nimg AS INTEGER bg AS ANY PTR x AS INTEGER y AS INTEGER delay AS INTEGER xsize AS INTEGER ysize AS INTEGER x1bg AS INTEGER y1bg AS INTEGER x2bg AS INTEGER y2bg AS INTEGER NAME0 AS STRING*80 bgsaved AS INTEGER parent AS ANY PTR END TYPE '----------------------------------------------------- TYPE tknight sprite AS tsprite leftsprite AS tsprite rightsprite AS tsprite leftweapsprite AS tsprite rightweapsprite AS tsprite rightkackersprite AS tsprite leftkackersprite AS tsprite player AS INTEGER DIR AS INTEGER weapon AS INTEGER cross AS tsprite crosson AS INTEGER visible AS INTEGER health AS INTEGER lastattack AS INTEGER id AS INTEGER END TYPE
Diese Klasse ist sozusagen die elementarere Ebene und wir haben sie ansatzweise ja schon früher definiert. Nun ist allerdings noch einiges dazugekommen:
Ein Ritter ist weitaus mehr als sein Sprite. Zunächst einmal kann jeder Ritter ja mehrere "Posen" einnehmen - er kann eine Waffe haben oder nicht, er kann Haufen machen oder nicht, er kann nach rechts gehen oder nach links.
Ein immer wiederkehrendes Problem bei solchen Jump-and-Run-Spielen ist die Kollisionskontrolle. Sie läuft dem Objektprinzip etwas zuwider, weil die Prüfung die Kenntnis sämtlicher aktueller Sprite benötigt. (Wir hatten das schon mal bei unseren "Uhren-Sprites".) Daher finden wir nach der Definition von tknight die Deklaration eines Zeigerarrays allknights(). Dort werden Zeiger auf alle Ritter gespeichert. Die Ritter selbst bleiben im Hauptprogramm lokal definiert. Aber die Kollisionsprüfung kann sich dann dieses Array zunutze machen.
Warum nicht gleich die Ritter global definieren? Ganz einfach: Weil damit sehr viel Flexibilität verloren ginge. Die ganzen tknight-Behandlungsroutinen sind so geschrieben, dass sie auf praktisch beliebige tknight-Objekte angewandt werden können. Quasi eine "tknight-lib". Man kann also jederzeit noch irgendwo andere Ritter erzeugen, auf dem Schirm herumwandern lassen etc. Diese Flexibilität ginge verloren, wenn wir durch eine globale Deklaration eine starre Vorgabe machen würden.
Dieser Punkt erwies sich als ziemlich komplex. Obwohl alles zunächst sehr einfach ausschaut: Wir haben eine Hauptschleife und bei jedem Durchgang wird die Tastatur abgefragt. Soviel zur Theorie. Tatsächlich besteht die Hauptschleife nur aus einem Punkt, nämlich die Bewegung Feinde. Und die dauert ziemlich lange. Was daran liegt, dass jede Feindbewegung den Ablauf der Animation einschliesst und innerhalb der Animation eine etliche Millisekunden lange Verzögerung eingebaut ist. Da liegt der Hund begraben.
In der Folge würde es viele, viele Millisekunden dauern, bis die Feindbewegung vorbei ist und mal wieder die Tastatur gescannt wird. Selbst, wenn man die Tastaturkontrolle immer zwischen der Bewegung zweier Feinde machen würde, wäre das viel zu langsam. Wohin also damit?
Normalerweise ist das die Fragestellung für eine typische Technik der Programmierung, die man innerhalb der Maschinensprache/Assembler Interrupt nennt; das Gleiche heisst auf OOP-Ebene Ereignisorientierung. Es läuft auf dasselbe hinaus: Bei Eintritt eines bestimmten Ereignisses (hier: Tastendruck) wird der normale Programmablauf unterbrochen und eine spezielle Routine angesprungen, die Ereignisbehandlungsroutine. In der OOP sind die Ereignisse Objekten zugeordnet, daher handelt es sich hier dann um die entsprechende Methode "on_key_xxx" oder "on_click_xxx". In der OOP ist so ein Ereignis sogar immer der Startpunkt des Programms. Gewöhnungsbedürftig, dass es davon also mehrere gibt (soviel, wie es Ereignisarten gibt).
Wie dem auch sei: Freebasic hat das (noch) nicht. Man kann also nur versuchen, so etwas zu "simulieren". Dazu muss die Abfrage der Tastatur von der Methode aus passieren, die die meiste Zeit frisst. Und das ist in unserem Fall movesprite():
'Setze Sprite an neuer Stelle FOR i=0 TO sprite.nimg-1 PUT (sprite.x,sprite.y),sprite.img(i),TRANS SLEEP sprite.delay,1 IF (i=0 AND INKEY$<>"" AND block_keyevent=0 AND sprite.NAME0<>"bullet") THEN keyevent() (...)
Wir sehen, dass sleep hier mit dem Argument 1 übergeben wird: Die Verzögerung kann durch die Tastatur nicht unterbrochen werden. Das hat spielerische Gründe. Wäre das der Fall, könnte der Spieler jede Feindbewegung durch Dauerdruck auf eine Taste blockieren. Das soll natürlich nicht der Fall sein. Der Spieler erhält also nur ein "festes" Kontingent an Aufmerksamkeit für seine Tasten. Es sie gleich bekannt: Das ist eine "Krückstock"lösung, weil sie zu einer "zähen" Tastatur führt. Da gegen wir weiter unten nochmal drauf ein.
In der nächsten Zeile wird dann unter bestimmten Bedingungen die Tastaturkontrolle angesprungen. Erstens darf es sich bei der momentanen Bewegung nicht um das Geschoss handeln. Zweitens muss das block_keyevent-Flag ausgeschaltet sein. Andersrum: Man kann damit die Tastaturkontrolle unterdrücken. Drittens erfolgt die Kontrolle nur beim jeweils ersten Bildchen der Animation. Das ist eine weitere "Bremse", damit der Spieler mit seinem Ritter nicht zu schnell abhauen kann. Schliesslich muss natürlich eine Taste gedrückt sein (inkey$<>"").
In keyevent() wird ja nun nicht nur die Tastatur geprüft, sondern es muss auch darauf reagiert werden. Das ist, OVP, wie wir programmieren, prinzipiell kein Problem, da eigentlich alle Aktionen, die wir nun von dort anstossen, gekapselt vom derzeitigen Programmzustand ablaufen sollten. Mit drei Ausnahmen:
Punkt 1. ist gar nicht so einfach zu begegnen. Wir können nicht wie früher einfach ein "STOP" oder "END" dahinschreiben. Wir wollen das Programm ja geordnet beenden, d.h. alle Ressourcen wieder freigeben, ev. einen Eingangsbildschirm zeigen usw. Aus diesem Grund wird auf Druck der ESC-Taste lediglich das globale Flag proceed auf Null gesetzt. Das ist eine saubere Lösung, allerdings mit einer Konsequenz: Alle anderen Teile des Programms müssen vorsehen, dass sich das Programm derzeit im Zustand "Abbruch" befinden könnte und die in ihrem Bereich liegenden entsprechenden Vorkehrungen treffen.
Punkt 2. kann dazu führen, dass bei wiederholtem Tastendruck die Feindbewegung blockiert wird. Das ist misslich. Was wir nämlich gerne hätten, wäre, dass langlaufende Aktionen und die Feindbewegung parallel weiterlaufen. Um es vorwegzunehmen: Das erlaubt unser bisheriges Programmdesign nicht. Dazu müssten wir die langlaufende Aktion in lauter kleine Schritte zerschneiden, die dann jeweils aus movesprite() heraus angestossen werden - ein grober Verstoss gegen das Kontextprinzip der OVP/OOP, dass verschiedene Teilaufgaben unabhhängig voneinander zu lösen sind. Tja, besser, per Tastatur erstmal nur schnelle Aktionen auszulösen.
Punkt 3: Es kann passieren, dass der aktuelle Feind, dessen movesprite() die Tastaturkontrolle und damit die Aktion aufgerufen hat, genau diesen Feind erlegt. Das heisst, die Schleife, aus der keyevent() aufgerufen wird, muss jederzeit miteinbeziehen, dass der Bewegungsvorgang gar nicht mehr definiert ist. Daher:
IF (sprite.parent<>NULL) THEN pk=sprite.parent IF (pk->visible=0) THEN logd("Catched",0.0) EXIT FOR END IF END IF
Heisst: Falls es sich überhaupt um ein Rittersprite handelt (und nicht etwa um das Fadenkreuz), dann schaue dir mal den parent an: Ist der überhaupt sichtbar? Wenn nicht, dann solltest du hier schleunigst Schluss machen: EXIT FOR.
Der letzte Punkt ist ein gutes Beispiel für das (oben schon erwähnte) Kontextproblem: Man ist bestrebt, die einzelnen Aufgaben kontextfrei zu erledigen. Aber manchmal klappt das nicht. An sich sollte movesprite() so geschrieben sein, dass es an jeder Stelle egal ist, zu welchem Elternobjekt das Sprite gehört. Grundsätzlich gilt: Kontextfreiheit ist immer möglich. Auch an dieser Stelle. Aber es erfordert, dass man das auftretende Kontextproblem abstrahiert, d.h. sich überlegt, welches ganz allgemeine Problem für ein Sprite dahintersteckt. In diesem Fall ist es, dass ein Sprite jederzeit "verschwinden" können sollte. Also müssten wir eigentlich der Klasse tsprite genau dieses Feature mit auf den Weg geben. Mit anderen Worten: Auch Sprites müssen die Eigenschaft "visible" haben. Und dort, wo der Ritter unsichtbar gemacht wird, muss eine Methode aufgerufen werden, die diese Aktion an das Sprite durchreicht und auch dieses unsichtbar macht. Das wäre die saubere Lösung. Eine gute Übung für Sie!
Es gilt also: Wenn man ganz sauber OVP-codet, sollten Rückgriffe auf Elternzeiger in der obigen Art nicht notwendig sein.
Unter "Multithreading" versteht man, dass von einem Programm mehrere parallel laufende Prozesse (Threads) angestossen werden. Im Punkt "Tastaturkontrolle" haben wir eine typische Situation angesprochen, die letztendlich selbst nicht mit einer sauberen Ereigniskontrolle befriedigend behandelt werden kann, sondern genau nach solchen Threads ruft. Man will, dass die Feindbewegung flüssig und unbeeindruckt von jeglicher Spieleraktion abläuft. Das wäre nur dann gewährleistet, wenn wir als Ausgangspunkt eine Schleife wählen würden, die nur Aufrufe enthält, die garantiert nicht länger als 2, 3 ms dauern. Ein Desaster für die Programmierung. Aber auch unnötig, denn genau so etwas macht eine Multithreading-API: Laufen zwei (oder mehr) Prozesse parallel, dann zerschneidet sie diese Prozesse und kleine Scheibchen und lässt immer abwechselnd das eine und das andere ablaufen.
Wir müssten unser Programm nur in zwei Prozesse aufteilen: In einen, der die Feindbewegung umsetzt und in den anderen, der den Rest erledigt, in der Hauptsache Kontrolle des Spieler-Ritters.
Freebasic hat genialerweise eine Multithreading-API. Aber so einfach umgesetzt, wie das eben klang, ist ein solches Multithreading keineswegs. Das Problem liegt in der Speicherverwaltung: Beide Prozesse laufen ja asynchron und sämtliche Methoden müssen sich bewusst sein, dass ihnen der Speicher quasi "unterm Hintern weg" geändert werden kann. Sonst kommt es zu ganz widerlichen Fehlern. Und Debugging von multithreaded Programmen ist eines der wüstesten Sachen, die es für Programmierer gibt. Wir müssten uns also erstmal gründlich mit der Frage befassen, wie man eine Methode und ein Programm threadsafe macht. Die OVP-Struktuierung hilft dabei beträchtlich: Sind alle Objekte perfekt voneinander gekapselt und sind wirklich alle Informationen in den Objekten enthalten, dann kann per definitionem kein "Thread Failure" auftreten. Aber hier rächt sich, dass wir eben doch noch mit globalen Zuständen arbeiten. Jede dieser globalen Variablen müssen wir sehr genau darauf prüfen, ob sie in irgendeiner Methode "Chaos" stiften könnte. Oder, besser: Wir schreiben das Programm so um, dass wir sie gar nicht mehr benötigen. Aber das verschieben wir auf später.
Wir hatten beim Uhrensprite-Beispiel ja schon das Problem sich überlagernder Sprites besprochen. Wenn Sie einmal 2D-Grafik-Spiele, also Arcade- oder Jump-and-Run-Spiele wie Pacman, Giana Sisters etc. anschauen, dann werden Sie bemerken, dass deren Programmierer die Überlappung von Sprites strikt ausgeschlossen haben. Well, wir haben es an einer Stelle hier nicht getan: Das Fadenkreuz kann und soll die anderen Sprites überlagern. Und da wir hier keine grossen Massnahmen ergriffen haben, kann es auch tatsächlich passieren, dass hier plötzlich Spritefehler auftauchen. Es passiert selten, aber es passiert. Verkaufen könnten wir das Programm so nicht - oder nur, wenn wir mehr als 1 Milliarde Dollar Umsatz pro Jahr machen ;-)).
Allerdings ist es hier nicht aussichtslos, sondern eher herausfordernd, das Problem in den Griff zu bekommen. Wir haben bei den Uhrensprites die prinzipiellen Lösungswege schon angesprochen:
In unserem Zusammenhang ist die erste Methode gar nicht so kompliziert, da in diesem Fall es ja nur ein Sprite gibt, dessen Hintergrund aktualisiert werden muss: Das Fadenkreuz. Jedes Sprite, das zu einem Feind gehört, müsste also den Hintergrundspeicher des Fadenkreuzes aktualisiern.
Diese Methode hat nur einen Riesennachteil: Sie ist nicht threadsafe!. Sie ist so thread-unsafe, wie sie nur sein kann, da Fadenkreuz und Ritter mit Garantie zu unterschiedlichen Prozessen gehören würden.
Die sauberere Methode wäre als B.: Zeichne bei jeder Spritebewegung den ganzen Bildschirm neu. Das dürfte sogar gut funktionieren, da wir ja ohnehin viel zuviel Rechenzeit haben (sleep....).
Übung 1:Bauen Sie das Programm dementsprechend um!
Übung 2:Schnellere Lösung: Programmieren Sie eine refresh()-Routine, die am Ende von enemymove() den kompletten Bildschirm neu zeichnet. Denken Sie daran, dass OVP- und OOP-Programmieren heisst: Welche Aufgaben man an eine Klasse delegieren kann, die delegiere! Hinweis: In der Sound-Version des Spiels im nächsten Kapitel ist diese "Reparatur" eingebaut.
Dieses Programm habe ich nicht mit DBIde geschrieben, sondern mit dem Editor proton, fbc auf der Kommandozeile und gdb. proton hat einen für mich unschlagbaren Vorteil: Es kennt mehrere, durchnumerierte Bookmarks. Damit ist die Navigation im Quelltext sehr schnell, z.B. zwischen abzugleichenden Methoden oder zwischen Klassendefinition und Methode usw.. Shift-Strg-1 (Mark1 setzen), Weghüpfen, Shift-Strg-2 (Mark2) setzen, irgendetwas verändern oder kopieren, Strg-1 und schon ist man wieder an der ursprünglichen Stelle. Ach Mist, was vergessen? Strg-2 drücken...
Was auch sehr hilft beim Navigieren ist eine Suche, die sich die vorausgegangenen Suchwörter merkt.
Die dritte Technik konstanter "Bookmarks" sind Marks in Kommentaren, die bei mir %%1,%%2 usw. heissen.
Ich habe bei diesem Programm nur selten den gdb benutzt. Das hat auch den Grund, dass der gdb bei grafiklastigen Programmen irgendwie schleichend langsam ist. Ein Bildschirm-Log verbietet sich bei Grafikprogrammen ohnehin. Sehr hilfreich dagegen war eine Log-Ausgabe in eine Datei, die nach jeder Ausgabe die Datei schloss. Das ist die Routine logd(), die übrigens diese Warning bei der Kompilation erzeugt! Vorteil: Man kann das Programm an verschiedenen Stellen durch Einfügen von "Sleep" unterbrechen und dann im Log alle ausgegebenen Werte anschauen. Man kann aber auch zu vorangegangenen Ausgaben zurückspulen und Werte vergleichen.
Ein Hilfsmittel, das ich hier nur wenig angewandt habe, das aber an sich eine grosse Hilfe ist, sind reports. Jede Klasse sollte eine report()-Routine besitzen, bei deren Aufruf die Werte sämtlicher Eigenschaften kommentiert ins Logfile protokolliert werden. Damit hat das Rätseln über die Frage, warum um Himmelswillen dieses Objekt dieses Verhalten zeigt, meist ein schnelles Ende. Siehe tsprite_report() und tknight_report()
Man kann sogar grafische Logs schreiben. Wenn man z.B. Spritefehler hat, dann ist es nützlich, den momentanen Hintergrund-Buffer in ein bmp-File zu dumpen und dieses dann während des Programmablaufs zu verfolgen.