Das schnelle Entwickeln von GUI-Anwendungen ist eine der Stärken von Tcl/Tk. Ein einfaches Spiel zeigt verschiedene Aspekte des Canvas-Widgets, von Animation über Bilder bis Zahlendarstellung.
Zu ihrem vierten Geburtstag wollte ich meiner Tochter etwas Besonderes schenken: ein selbst geschriebenes Spiel. Es sollte irgendwas mit Zahlen zu tun haben und natürlich Spaß machen – Edutainment also, für spielerisches Lernen. Eine Idee war schnell gefunden: Die Spielerin soll Zahlen erkennen, die auf dem Bildschirm auftauchen, und jeweils die passende Taste drücken.
Soweit zu “Edu”, fehlt noch das “tainment”, die nette Verpackung. Beim Vorlesen der Geschichte vom kleinen Wassermann kam die zündende Idee: ein Fisch, der Luftblasen mit den Zahlen aufsteigen lässt. Da ich Karpfen nicht besonders lustig finde, gibt es stattdessen einen Bitterling, zu dem auch gleich eine Teichmuschel als Lebensgefährten gehört. Diese Muschel produziert – gewissermaßen als Belohnung für jede gefundene Zahl – eine Perle.
Nach der Idee machte ich mich an die Umsetzung – natürlich mit Tcl. Das Ergebnis ist in Abbildung 1 zu sehen. Da das Tcl-Programm recht überschaubar ist, steht der komplette Code in einer einzelnen Datei, inklusive aller Bilder. Aus Platzgründen enthält Listing 1 keine Daten für die Bilder, die vollständigen Quellen stehen wie immer auf dem FTP-Server des Linux-Magazins bereit[2].
Bunte Unterwasserszene
Das Spielfeld wird mit dem Canvas-Widget dargestellt, hier gilt es, den Hintergrund zu zeichnen, ein paar Wasserpflanzen und Sand als Dekoration, den Fisch, die Muschel und die Blasen natürlich. Mit meinen Zeichenkünsten ist es leider nicht sonderlich weit her, also habe ich den Bitterling und die Teichmuschel per Google gesucht. Den Hintergrund bildet ein großes Bitmap, das auch die Muschel und die Wasserpflanzen zeigt (Gimp sei Dank).
Von Haus aus kann Tk nur wenige Bitmap-Typen lesen und darstellen. Anders als bei anderen Bildformaten kann Tk aber Bilder im GIF-Format nicht nur aus einer Datei lesen, sondern auch direkt aus einem String. Dieser String muss die Bilddaten Base-64-kodiert enthalten (auch Mime-encoded genannt). Das Umwandeln geht mit Hilfe der Tcllib[1] in wenigen Zeilen:
package require base64 set fd [open bitterling.gif] fconfigure $fd -translation binary set bin [read $fd] close $fd set bild [base64::encode $bin]
Nun enthält »$bild« das Bild in der Base-64-Darstellung. Diese Variante stellt die Binärdaten ausschließlich durch Ascii-Zeichen dar. Das hat den Vorteil, dass man den String-Inhalt direkt in den Quellcode einfügen kann, ohne den Editor oder den Interpreter zu verwirren. Aus dem String lässt sich dann wieder ein Bild zum Anzeigen erzeugen:
package require Tk image create photo bitterling -data $bild label .test -image bitterling pack .test
Die Tcllib ist in den meisten Linux-Distributionen bereits enthalten, die neuste Version kann man von[1] holen. Das Installieren übernimmt ein Skript, das im Paket zu finden ist.
Die Leinwand
Das Fischbild und den Hintergrund malt das Spiel auf ein Canvas (eine Leinwand; siehe Zeile 37). Pro Foto genügt ein Kommando dieses Widgets:
$canvas create image 0 500 -image hintergrund -anchor sw
Wenn alle Bilder positioniert sind, schwebt der Bitterling an einem Tannenwedel und knabbert daran. Aber kein echter Fisch verharrt permanent auf der gleichen Stelle, also muss sich auch der Bitterling bewegen. Diese Aufgabe übernimmt die Prozedur »fischBewegen« ab Zeile 55. Zuerst berechnet sie einen zufälligen Wert für die Verschiebung in x- und y-Richtung, dann bewegt sie den Fisch entsprechend. Der Animationstrick steht in der letzte Zeile: Hier sorgt die Prozedur mit »after« dafür, dass sie von der Event-Queue nach einer bis zehn Millisekunden wieder aufgerufen wird. Zwischendurch kann die Queue andere Events abarbeiten.
Blubbernde Luftblasen
Als Nächstes sind die Luftblasen dran. Eine neue Blase entsteht mit Hilfe der Prozedur »blaseNeu« (Zeile 68), sie kombiniert einen ausgefüllten Kreis mit einer Zahl. Um die Bestandteile eindeutig zu identifizieren, können Canvas-Objekte ein Tag (als Markierung) tragen. Als Basis dient eine per »clock clicks« erhaltene Zeitmarke. Da Tags nicht mit einer Zahl anfangen dürfen, setzt Zeile 78 ein »t« davor. Neben dem Tag, das die ganze Blase bezeichnet, braucht das Programm später eine eigene Kennung für Kreis und Zahl: Zeile 78 vergibt daher zusätzlich ein Tag mit dem Präfix »blase«, Zeile 87 entsprechend »text«.
Als Startpunkt der Blase dienen die Koordinaten »$x« und »$y«, also der Mund des Fisches. Es sind globale Variablen, da »fischBewegen« den Mund (inklusive Fisch) permanent bewegt. Die Blase soll beim Drücken der richtigen Taste platzen, deshalb vereinbart Zeile 90 per »bind« die »blaseWeg«-Prozedur. Das Bind-Kommando hat die Form »bind Widget Event Befehl«. Das Widget ist hier einfach das ganze Fenster ».«, der Event das Drücken der richtigen Zahlentaste, der Befehl lautet »blaseWeg« mit der gewünschten ID.
Bei den Zahlen gibt es eine Besonderheit: Sie kommen zweimal auf der Tastatur vor. Die Events aus dem Nummernfeld lauten jedoch – egal ob Numlock gedrückt ist – nicht auf die Zahl, sondern tragen einen eindeutigen Namen. Die Zuordnung erfolgt per Array (Zeile 12 bis 23), ein zusätzlicher Bind-Aufruf in Zeile 91 kümmert sich um das Binding.
Animation
Wenn die Prozedur »blaseNeu« fertig ist, hat der Fisch noch eine ziemlich kleine Blase vor seinem Maul. Als Nächstes soll sie größer werden und dann aufsteigen. Hierfür ist »blaseAktualisieren« (Zeile 97) verantwortlich. Diese Prozedur überprüft zuerst, ob die Blase schon gelöscht wurde oder aus dem sichtbaren Bereich verschwunden ist, und ruft gegebenenfalls »blaseLöschen« und »blaseNeu« auf. Andernfalls hängt das Verhalten vom Alter der Blase ab.
Da die ID der Blase eine Zeitmarke ist, lässt sich ihr Alter ziemlich einfach ermitteln. In den ersten 1200 Millisekunden soll die Blase größer werden. Dazu löscht Zeile 116 den alten Kreis, danach malt die Prozedur einen größeren. Bei der Zahl verändert sie nur per »$canvas itemconfigure« die Schriftgröße. Da der neue Kreis über der Zahl steht und sie verdeckt, bringt »$canvas raise« die Ziffer wieder nach vorn. Hier dienen die oben besprochenen Tags zur Identifizierung von Kreis und Zahl.
Ist die Blase älter als 1200 Millisekunden, fängt sie an zu steigen. Die Steiggeschwindigkeit (Zeile 131) hängt vom Alter ab, damit die Blase oben schneller aufsteigt. Die zufällige Verschiebung in horizontaler Richtung (Zeile 130) ist notwendig, da das Gebilde sonst einfach nicht wie eine Blase aussieht. Das Kommando »$canvas move« verschiebt Kreis und Zahl gleichzeitig, da es das gemeinsame Tag »t$id« verwendet. Wie die »fischBewegen«-Prozedur lässt sich auch »blaseAktualisieren« regelmäßig von der Event-Queue aufrufen (Zeile 136), die verwendeten 50 Millisekunden reichen für die fließende Bilddarstellung (20 Frames pro Sekunde) vollkommen aus.
Trifft die Spielerin die richtige Taste, führt deren Binding die Prozedur »blaseWeg« (Zeile 149) auf, die ihrerseits die Aufgabe zuerst an »blaseLöschen« (Zeile 139) weitergibt. Diese entfernt die Objekte mit »$canvas delete« aus dem Canvas; die beiden »bind«-Kommandos löschen das bisherige Binding.
Ein wenig Motivation muss sein, deshalb gibt es für jede getroffene Zahl eine neue Perle (»perleNeu«, Zeile 155). Die Perlen rollen aus der Muschel und reihen sich rechts unten am Spielfeldrand ein (»perleBewegen«, Zeile 171). Enthält die Reihe zehn Perlen, werden sie gelöscht, der Zähler ganz rechts unten erhöht sich entsprechend.
Der Quelltext zum Erzeugen der Perlen bietet nichts Neues, das Aufreihen der Perlen dagegen schon. Statt die Endposition jeder Perle zu berechnen, wird sie so lange verschoben, bis sie an die anderen Perlen anstößt (Zeile 183). Diese bereits vorhandenen Perlen sind mit dem Tag »perlen« gekennzeichnet. Ist die neue Perle bei ihnen angekommen, erhält auch sie mit »$canvas addtag« das »perlen«-Tag und die nächste Perle kann kommen. Das funktioniert zuverlässig, auch wenn mehrere Perlen gleichzeitig rollen.

Abbildung 1: Zahlenspiele mit Tcl: Die Spielerin soll die Zahl in der Luftblase erkennen und die passende Taste drücken, dann erhält sie eine Perle (rechts unten).

Abbildung 2: Das Zeichenprogramm Impress speichert Grafiken als Tcl-Code. Das Bitterling-Bitmapbild (oben) lässt sich damit als Vektorgrafik nachbilden.

Abbildung 3: Das Tcl-Plugin führt Tcl-Skripte – ähnlich einem Java-Applet – direkt im Webbrowser aus. Hier läuft das Spiel im Galeon-Browser.
Nur 215 Zeilen
Mit diesen 215 Zeilen Quelltext ist das komplette Spiel entwickelt. Für Verbesserungen bleibt aber genug Raum, zum Beispiel bei den Bildern: Entsprechendes Talent vorausgesetzt ist hier viel mehr möglich. Es würde sich anbieten, statt der starren Bilder mehr animierte Vektorgrafiken einzusetzen.
Der Import von Bitmaps ist für Tk kein Problem, aber auch für Vektorgrafiken gibt es verschiedene Wege. Liegt die Zeichnung in Xfig, Tgif, Sodipodi oder ähnlichen Tools vor, verwandelt Pstoedit[3] deren Postscript-Ausgabe in Tcl-Code. Alternativ ist Impress[4] ein gutes Zeichenprogramm, das komplett in Tcl geschrieben ist und die Bilder als Tcl/Tk-Code speichert. Auch Xfig kennt Tk als Exportformat.
Bin ich schon drin?
Ein weiterer Punkt für Verbesserungen wäre Sound, vom freundlich Blubb beim Erzeugen einer Blase bis zum Bling bei ihrem Platzen. Ebenso denkbar wäre, dass das Spiel die Zahlen vorliest oder eine nette Hintergrundmusik abspielt. Von der Tcl-Seite ist das dank Snack[5] kein Problem, es fehlen nur die entsprechenden Audiodateien. Vielleicht findet sich ja ein audiophiler Leser, der diese ergänzt? (fjl)
Das Neueste |
|
Für einen Jahresrückblick ist es zwar etwas spät, unter[6] findet sich aber eine sehr interessante Übersicht über das vergangene Skriptsprachen-Jahr. Sie fasst die wesentlichen Entwicklungen für Lua, Perl, Python, Ruby und natürlich Tcl zusammen. Das Canvas-Widget dient nicht nur als Basis für einfache Spiele. Tkvnc[7] zeigt, dass es dem Widget sogar gelingt, komplexe Desktops remote anzuzeigen. Damit das Ganze auch mit gesicherten VNC-Servern funktioniert, hat Jochen Loewer die DES-Verschlüsselung in reinem Tcl implementiert[8]. Activestate stellte eine Betaversion ihres Tcl Dev Kit[9] vor. Der Nachfolger des Tcl-Pro-Pakets enthält neben einigen Werkzeugen zum Debuggen und Wrappen von Anwendungen eine aktualisierte Version des Tcl-Plugins. Damit lassen sich Programme auch im Webbrowser verwenden (siehe Abbildung 3). Aus Sicherheitsgründen überwacht und verhindert das Plugin dabei einige potenziell gefährliche Operationen. Tcl-Programmierer können so bei ihrer Sprache bleiben und müssen nicht auf Java-Applets setzen. Auch im Netzbereich angesiedelt ist Sockspy[10]. Dieser Proxy lässt sich zwischen beliebige Netzanwendungen einklinken und protokolliert den Netzverkehr. Das Anzeigen der übertragenen Daten übernimmt Sockspy selbst. Das Debuggen von Client-Server-Anwendungen wird so wesentlich erleichtert. |
Listing 1: Das Spiel |
001 #!/bin/sh
002 #
003 exec wish $0 $@
004
005 package require Tk
006 package require opt
007
008 # Zeitauflösung
009 set res 50
010
011 # Äquivalenztabelle für Zahlen und Zahlenfeld
012 array set zahl2kp {
013 0 KP_Insert
014 1 KP_End
015 2 KP_Down
016 3 KP_Next
017 4 KP_Left
018 5 KP_Begin
019 6 KP_Right
020 7 KP_Home
021 8 KP_Up
022 9 KP_Prior
023 }
024
025 # Anzahl Perlen
026 set perlen 0
027
028 # Bilder:
029 image create photo fisch -data "..."
030 image create photo hintergrund -data "..."
031 image create photo perle -data "..."
032
033 proc gui {} {
034 global canvas x y
035
036 # Canvas erzeugen und mit Bildern füllen
037 set canvas [canvas .canvas -width 500 -height 500 -bg grey20]
038 grid $canvas -row 0 -column 0 -sticky nesw
039 $canvas create image 0 500 -image hintergrund -anchor sw
040 $canvas create image 140 390 -image fisch -anchor sw -tag fisch
041
042 # Blasenstartpunkt festlegen
043 set x 167
044 set y 365
045
046 # Fisch-Animation starten
047 fischBewegen
048
049 wm sizefrom . program
050 wm geometry . 500x500
051 bind . <q> "exit 0"
052 bind . <Control-q> "exit 0"
053 }
054
055 proc fischBewegen {} {
056 global canvas x y res
057
058 set dx [expr {round(rand() * 1 * ((rand()<0.5)*2 -1))}]
059 set dy [expr {round(rand() * 1 * ((rand()<0.5)*2 -1))}]
060 $canvas move fisch $dx $dy
061
062 incr x $dx
063 incr y $dy
064
065 after [expr {round(rand()*2*$res)}] fischBewegen
066 }
067
068 proc blaseNeu {} {
069 global canvas x y zahl2kp res
070
071 # Tag generieren
072 set id [clock clicks -milliseconds]
073
074 # Blase
075 $canvas create oval
076 [expr {$x -3}] [expr {$y -3}]
077 [expr {$x +3}] [expr {$y +3}]
078 -tags [list "t$id" "blase$id"]
079 -fill skyblue
080
081 # Text
082 set zahl [expr {round( floor( rand() * 9.0 ) )}]
083 $canvas create text $x $y
084 -font "Helvetica 5 bold"
085 -text $zahl
086 -anchor center
087 -tags [list "t$id" "text$id" ]
088
089 # Auf richtigen Tastendruck hören
090 bind . <KeyPress-$zahl> "blaseWeg $id"
091 bind . <KeyPress-$zahl2kp($zahl)> "blaseWeg $id"
092
093 # Start
094 after $res blaseAktualisieren $id
095 }
096
097 proc blaseAktualisieren {id} {
098 global canvas x y res
099
100 # Blase noch da?
101 if {[string eq [$canvas type "blase$id"] ""]} {
102 return;
103 }
104
105 # Blase schon aus dem Canvas raus?
106 if {[lindex [$canvas bbox "blase$id"] 1] < 0} {
107 blaseLöschen $id
108 blaseNeu
109 return;
110 }
111
112 # wie alt?
113 set alter [expr {[clock clicks -millisec] - $id}]
114 if { $alter < 1200 } {
115 # Blase vergrößern
116 $canvas delete "blase$id"
117 set radius [expr {$alter / $res * 0.75}]
118 $canvas create oval
119 [expr {$x -$radius}] [expr {$y -$radius}]
120 [expr {$x +$radius}] [expr {$y +$radius}]
121 -tags [list "t$id" "blase$id"]
122 -fill skyblue -outline ""
123
124 # Schrift größer und nach vorn setzen
125 set punkte [expr {round($alter / $res * 0.5)}]
126 $canvas itemconfigure "text$id" -font "Helvetica $punkte bold"
127 $canvas raise text$id blase$id
128 } else {
129 # Blase aufsteigen lassen
130 set dx [expr {cos( rand() * 20 )*1.2}]
131 set vy [expr {$::tempo * ($alter - 700) / double(3e5)}]
132 set dy [expr {-$vy * $res}]
133
134 $canvas move "t$id" $dx $dy
135 }
136 after $res blaseAktualisieren $id
137 }
138
139 proc blaseLöschen {id} {
140 # Welche Zahl steht in der Blase?
141 set zahl [$::canvas itemcget "text$id" -text]
142 # Blase im Canvas löschen
143 $::canvas delete "t$id"
144 # Bindings löschen
145 bind . <KeyPress-$zahl> ""
146 bind . <KeyPress-$::zahl2kp($zahl)> ""
147 }
148
149 proc blaseWeg {id} {
150 blaseLöschen $id
151 perleNeu
152 blaseNeu
153 }
154
155 proc perleNeu {} {
156 global canvas perlen
157
158 # ID generieren
159 set id [clock clicks -milliseconds]
160
161 set einer [expr {int(fmod($perlen, 10))}]
162 if { $einer == 0 && $perlen != 0 } {
163 perleZähler
164 }
165 $canvas create image 140 480 -image perle
166 -tags p$id
167 incr perlen
168 perleBewegen $id
169 }
170
171 proc perleBewegen {id} {
172 global canvas res
173
174 # Position der anderen Perlen
175 set xSoll [lindex [$canvas bbox perlen] 0]
176 if {[string match $xSoll ""]} {
177 set xSoll 420
178 }
179
180 # Eigene Position
181 set xIst [lindex [$canvas bbox p$id] 0]
182
183 if { $xIst >= $xSoll - 30 } {
184 $canvas addtag perlen withtag p$id
185 } else {
186 $canvas move p$id 5 0
187 after $res "perleBewegen $id"
188 }
189 }
190
191 proc perleZähler {} {
192 global canvas perlen
193
194 $canvas delete perlen
195 $canvas delete zähler
196
197 $canvas create text 480 480 -text $perlen
198 -font {Palatino 25 bold }
199 -fill #b0c5db
200 -tags zähler
201 }
202
203 tcl::OptProc main {
204 {-tempo -int 10 "Tempo, normal ist 10"}
205 } {
206
207 if { $tempo < 1 || $tempo > 100} {
208 error "tempo muss zwischen 1 und 100 liegen"
209 }
210 set ::tempo $tempo
211 gui
212 blaseNeu
213 }
214
215 eval main $argv
|
Infos |
|
[1] Tcllib: [http://www.tcl.tk/software/tcllib/] [2] Quellen: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2003/04/Feder-Lesen/] [3] Pstoedit: [http://www.pstoedit.net] [4] Impress-Grafikprogramm: [http://www.ntlug.org/~ccox/impress/] [5] Snack: [http://www.speech.kth.se/snack/] [6] Jahresübersicht der Skriptsprachen: [http://www.vendian.org/language_year/] [7] Tkvnc, ein VNC-Client in Tcl: [http://www.ifost.org.au/Software/tkvnc/] [8] DES in Tcl: [http://mini.net/tcl/8196] [9] Tcl Dev Kit: [http://www.activestate.com/Products/Tcl_Dev_Kit/] [10] Sockspy: [http://sockspy.sourceforge.net/sockspy.html] |
Der Autor |
|
Carsten Zerbst arbeitet bei Atlantec an einem PDM-System für den Schiffbau. Daneben beschäftigt es sich mit dem Einsatz von Tcl/Tk. Auf seiner Homepage [http://www.groy-groy.de/czerbst/tcltk.html] sind einige Beispielprogramme zu finden. |





