www.r-krell.de |
Webangebot für Schule und Unterricht, Software, Fotovoltaik und mehr |
Willkommen/Übersicht > Informatik > Java-Seite b4) (2018)
Teil b4)_2018
Programme P10 bis P12: Verschiedene Autorennen
Eine vollständige Übersicht aller meiner Seiten "Informatik mit Java" gibt's auf der
Informatik-Hauptseite!
Diese Seite ist der letzte Teil einer Reihe über das Programmieren in der Einführungsphase eines gymnasialen Informatik-Grundkurses
(erstes Jahr der SII) unter Verwendung des Java-Editors und seines GUI-Builders; frühere Seiten der Reihe sind die Seiten b1)_2017, b2)_2017 und b3)_2018.
Auf dieser Seite b4)_2018 finden Sie:
zum Seitenanfang / zum Seitenende
Bereits auf vielen anderen Seiten meines Webangebots finden sich Programme zur Simulation eines Autorennens - in durchaus unterschiedlichen Programmier-Sprachen, u.a.
Die Schülerinnen und Schüler in meinen früheren Informatik-Kursen hatten ihre Autorennen immer gerne entwickelt. Für mich lag es daher nahe, eine ähnliche Aufgabenstellung für 'normales' Java (ohne Stift&Co) auch in meinem letzen Anfänger-Grundkurs zu stellen. Nachdem ich aber mit den Programmen P7 bis P9 Vorschläge der Kursteilnehmer(innen) aus dem laufenden Unterricht aufgegriffen hatte (siehe Seite b2)_2017), blieb am Schuljahresende für das Autorennen keine Zeit mehr - vielleicht zum Glück, denn in im Folgenden zeigt sich, dass der in allen früheren Beispielen schnell erfolgreiche intuitive Ansatz bei der Java-Swing-Oberfläche auf Schwierigkeiten stößt, die erst mit zusätzlicher Hilfe bzw. Kenntnissen über so genannte threads überwunden werden können.
Die Aufgabe besteht darin, Autos auf einer geraden, mehrspurigen Rennstrecke fahren zu lassen und schließlich den Sieger auszurufen. Dabei fährt jedes Auto in einer eigenen Spur. Statt mit einem echten Formel-1-Rennen auf einer komplexen Rennstrecke ist die Aufgabe also eher mit der Kamel- oder Pferderennsimulation auf der Kirmes vergleichbar, wo Blech-Kamele bzw. Blech-Pferde auf parallelen Bahnen weiter rücken, wenn es dem zum Tier gehörenden Kirmes-Besucher gelingt, einen Ball in ein passendes Loch zu werfen. Ob die Autos durch die Schriftzeichen "O>o" in Textzeilen oder durch Grafiken repräsentiert werden, spielt nur eine untergeordnete Rolle.
Die Bilder zeigen zwei unterschiedliche Autorenn-Varianten vorm (Neu-)Start bzw. nach Erreichen des Ziels mit dem schnellsten Fahrzeug:
Spielbare Versionen der beiden abgebildeten Programme finden sich fast am Ende dieser Seite. Zuvor wird ihre Programmierung erläutert. Ganz unten folgen weiterführende Aufgaben.
zum Seitenanfang / zum Seitenende
Die Programme P10 bis P12 bilden - wie oben erwähnt - den Abschluss einer Programmier-Reihe in meinem Webangebot für Schülerinnen und Schüler im Anfängerjahr, d.h. in der Einführungsphase eines Informatik-Grundkurses der gymnasialen Oberstufe. Dabei steht nicht die Beherrschung kleinster Eigenheiten einer Programmiersprache, sondern die Erarbeitung wichtiger Konzepte und grundsätzlicher Ideen der Informatik am motivierenden Beispiel im Vordergrund. Ein handliches, leicht zu bedienendes Werkzeug erleichtert die Arbeit und erlaubt wegen der automatischen Quelltext-Erzeugung den Verzicht auf die detaillierte theoretische Auseinandersetzung mit einigen Programmkonstruktionen. 'Rapid prototyping' soll durch schnelle Ergebnisse und Rückmeldung zur Auseinandersetzung mit dem Programmierten und der sinnvollen Weiterentwicklung motivieren.
Das Autorennen wird in Java programmiert. Die Oberfläche soll - wie schon bei den Programmen P1 bis P9 - weitgehend mit dem integrierten GUI-Builder des gut zu bedienenden Javaeditors erstellt werden (Diesen sehr empfehlenswerten Javaeditor von Gerhard Röhner stelle ich im Teil a) von Informatik mit Java vor, habe ihn immer im Schul-Unterricht und für eigene Java-Projekte benutzt und zeige ihn auf ganz vielen Seiten meines Webangebots; seine offizielle Webseite ist javaeditor.org).
Das Vorgehen beim Autorennen-Projekt folgt dem Spiralmodell, d.h. nach Erstellen der Oberfläche werden die Funktionen nach und nach hinzugefügt, getestet und evtl. verbessert bzw. ergänzt. Insofern soll die erste lauffähige Version nach Erstellen der Oberfläche (bei P10 mit Textfeldern) beim Start zunächst nur ein Fahrzeug automatisch ins Ziel fahren. Der Weg dahin und weitere Stationen bzw. Phasen der Programmentwicklung werden im Folgenden beschrieben und mit Java-Quelltext vorgestellt.
zum Seitenanfang / zum Seitenende
Die Oberfläche wird dadurch erstellt, dass mit dem im Javaeditor eingebauten GUI-Builder verschiedene Objekte aus der Swing-1-Werkzeugleiste mit der Maus auf einen JFrame gezogen werden. Den zugehörigen Quelltext erzeugt der Javaeditor automatisch, wobei Namen und Texte im Objekt-Inspektor verändert bzw. eingegeben werden können und so in den Programmtext gelangen.
Der automatisch erzeugte Quelltext wird - anders als z.B. in "P9_A3_Galgenmann.java" auf Seite b3_2018) - jetzt nicht vollständig aufgeführt, sondern z.T. durch [...] abgekürzt. Das Augenmerk soll auf die Rahmenmethoden für die beiden Schaltflächen gerichtet werden, die am Ende der Datei "P10_Autorennen.java" vom Javaeditor (bzw. seinem integrierten GUI-Builder) automatisch leer erzeugt wurden:
import [...] /** * Autorennen mit Textfeldern, hier: GUI-Oberfläche * @author R. Krell, 5/2018 */ public class P10_Autorennen extends JFrame { // Anfang Attribute [...] setVisible(true); } // end of public P10_Autorennen // Anfang Methoden public static void main(String[] args) { new P10_Autorennen(); } // end of main public void jBtAufstellen_ActionPerformed(ActionEvent evt) { // TODO hier Quelltext einfügen } // end of jBtAufstellen_ActionPerformed public void jBtStart_ActionPerformed(ActionEvent evt) { // TODO hier Quelltext einfügen } // end of jBtStart_ActionPerformed // Ende Methoden } // end of class P10_Autorennen |
Das vorstehende Programm kann schon kompiliert und fehlerfrei gestartet werden, reagiert aber noch nicht auf das Drücken der Schaltflächen. Das Programm weiß ja noch nicht, was es dann tun soll. Dazu muss erst an den beiden mit // TODO hier Quelltext einfügen gekennzeichneten Stellen von Hand passender Programmtext eingetippt werden:
Das führt jetzt zu folgenden Methoden:
public void jBtAufstellen_ActionPerformed(ActionEvent evt) { jTfAuto1.setText("O>o"); jTfAuto2.setText("O>o"); jTfAuto3.setText("O>o"); jTfAusgabe.setText(""); } // end of jBtAufstellen_ActionPerformed public void jBtStart_ActionPerformed(ActionEvent evt) { // erster Versuch String auto1; // Auto mit führenden Leerzeichen "O>o", " O>o", .. for (int schritt=0; schritt<47 ;schritt++) // nach 47 Schritten stößt das Auto rechts an { auto1 = jTfAuto1.getText(); // Auto mit bisher zurückgelegter Strecke jTfAuto1.setText(" "+auto1); // Auto rückt um eine zusätzliche Stelle " "+ weiter nach rechts } // end of for } // end of jBtStart_ActionPerformed |
Der so ergänzte Programmtext wird wieder kompiliert und ausgeführt. Bei Knopfdruck passiert jetzt tatsächlich auch etwas, allerdings ist der Erfolg beim Betätigen des "Start"-Knopfes noch unbefriedigend: Offenbar ist der Computer viel zu schnell und man sieht gar nicht, wie das Auto fährt, weil es sofort am rechten Rand ankommt (oder etwas früher hält, wenn man statt 47 z.B. nur 35 angegeben hat. Bei 54 wäre das Auto hingegen sofort verschwunden, weil es über das Ziel hinaus geschossen und nicht mehr im Textfeld sichtbar ist). Wenigstens setzt der "Auf die Plätze.."-Knopf das Auto in jedem Fall wieder an den linken Rand auf die Startposition.
In dieser Situation würde ich meinen Schülerinnen und Schülern dann die (Hilfs-)Methode warte angeben bzw. austeilen, mit der das Fahren verlangsamt werden kann. Sicher kommen die Programmierer(innen) dann rasch auf die Idee, nach jedem Schleifendurchgang etwa eine zehntel Sekunde zu warten, bevor das Auto eine (Leer-)Stelle weiter nach rechts geschoben wird. Möglicherweise muss noch überlegt werden, dass dazu der warte-Aufruf in den Schleifenkörper und nicht davor oder dahinter geschrieben werden muss. Der schließlich gefundene, nachfolgende Programmtext sollte aber den gewünschten Effekt haben:
private void warte (int ms) // Zeit in Millisekunden, 100 = 0,1 Sekunde Wartezeit { try { Thread.sleep (ms); } catch (InterruptedException e) { System.out.println("** Fehler beim Warten: "+e); } } public void jBtAufstellen_ActionPerformed(ActionEvent evt) { jTfAuto1.setText("O>o"); jTfAuto2.setText("O>o"); jTfAuto3.setText("O>o"); jTfAusgabe.setText(""); } // end of jBtAufstellen_ActionPerformed public void jBtStart_ActionPerformed(ActionEvent evt) { // zweiter Versuch, mit warte String auto1; // Auto mit führenden Leerzeichen "O>o", " O>o", .. for (int schritt=0; schritt<47 ;schritt++) // nach 47 Schritten stößt Auto rechts an { auto1 = jTfAuto1.getText(); // Auto mit zurückgelegter Strecke jTfAuto1.setText(" "+auto1); // Auto rückt eine Stelle weiter nach rechts warte(100); // extra hinzugefügt, weil Auto sonst sofort am Ziel anlangt } // end of for } // end of jBtStart_ActionPerformed |
Leider ist das Ergebnis wieder enttäuschend: Der "Start"-Knopf bleibt auch nach kurzem Antippen jetzt immer 4,7 Sekunden lang eingedrückt, ohne dass währenddessen irgendeine Bewegung des Autos sichtbar wird - es bleibt offenbar am Start stehen. Wenn die Schaltfläche dann endlich wieder hochkommt, als wäre sie gerade erst losgelassen worden, steht das Auto schlagartig rechts am Ziel. Vom erhofften allmählichem Fahren ist nichts zu erkennen. Dabei war die Programmieridee durchaus richtig, wie man mit einer zusätzlichen Zeile im Programmtext leicht nachweist (nachfolgende rot eingerahmte Zeile 125): in der Konsole bzw. im Meldungsfenster des Javaeditors wird während der 4,7 Sekunden, in der sich in der Oberfläche nichts ändert, trotzdem alle Zehntelsekunde immer wieder eine neue Zeile mit einem jeweils etwas weiter gerücktem Auto ausgedruckt! Nur dummerweise nicht in der eigentlichen Oberfläche.
Das überrascht sogar etwas, denn die Variable auto1 holt sich ja den Bewegungsfortschritt zwischendurch immer wieder aus dem Textfeld jTfAuto1 (Zeile 122). Bliebe das Auto wirklich am Start stehen, dürfte auch im Kontrollausdruck im Meldungsfenster kein Fahren erfolgen. Offenbar wird das Textfeld intern durchaus aktualisiert, nur ist auf dem Bildschirm davon nichts zu sehen. Tatsächlich stoßen wir hier auf eine Eigenart der Swingoberfläche: Wird eine Schaltfläche gedrückt, führt Java erst die damit verbundene Aktion vollständig aus (wartet hier also, bis der Programmtext der Zeilen 118 bis 126 bis zum Ende abgearbeitet ist), bevor Java zur Oberfläche zurück kehrt und diese neu darstellt. Zwischendurch gemachte Änderungen bleiben unsichtbar; erst der Zustand der Oberfläche am Ende der Aktion wird wieder angezeigt! Bei Objekt-PAL (s.o.), bei der Java-Bibliothek Stift&Co (vgl. mein älteres java-autorennen.htm) oder bei der Webseiten-Sprache Javascript (vgl. if-javascript2.htm) gibt es dieses Warten der Oberfläche auf den Abschluss der ausgelösten Aktion nicht, sodass dort eine vergleichbare Programmkonstruktion wie die oben entwickelte wunderbar funktioniert.
zum Seitenanfang / zum Seitenende
Die Lösung des beschriebenen Java-Swing-Warte-Problems gelingt mit einem zusätzlichen Thread, einem nebenläufigen Programmprozess, der quasi gleichzeitig mit dem Haupt-Thread der Oberfläche ausgeführt werden kann. Die Oberfläche würde nur noch warten, bis der neue Thread zu Bewegung des Autos angestoßen ist, und sofort zur Anzeige zurück kehren. Der "Start"-Knopf würde sofort losgelassen und die Oberflächengraphik erneut angezeigt, während gleichzeitig in dem parallelen Thread das Auto noch bewegt wird. Meldet der neue Thread ständig die aktuelle Position des Autos an die Oberfläche, könnte sie dort laufend angezeigt werden, d.h. das Auto würde auch in der Oberfläche fahren.
Die Entwickler von Java haben in der Swing-Bibliothek auch den Bauplan für einen so genannten Swingworker-Thread bereit gestellt, der im Hintergrund arbeiten und über zwei vorgegebene Methoden mit der Swingoberfläche kommunizieren kann. Dieser Swingworker-Thread soll aber im Folgenden nicht verwendet werden. Die richtige Nutzung der vorgegebenen Methoden erfordert einige Einarbeit und sie sind trotzdem für unsere Aufgabenstellung ungünstig. Vielmehr lässt sich der sinnvolle Thread-Gebrauch leichter am allgemeinen Fall studieren. (Solche vom allgemeinen Fall abgeleitete Threads werden übrigens auch in einigen anderen meiner Programme verwendet, z.B. im Client und im Server auf meiner Netzwerk-Seite Informatik mit Java, Teil i) oder auch im Kartenspiel "Rot & Schwarz", einem Beispielprojekt zum Software-Engineering SWE-2.)
Ein Thread muss in Java von der allgemeinen Klasse Thread abgeleitet werden ("erben") und eine Methode run ohne Parameterübergabe und ohne Rückgabewert erhalten, die sich um die auszuführende Aktion kümmert. Würde in der ActionPerformed-Methode des "Start"-Knopfes allerdings diese run-Methode des Thread-Objekts direkt aufgerufen, wird wie bisher auf den Abschluss der Aktion gewartet und es wäre nichts gewonnen. Anders hingegen, wenn bei der Schaltflächen-Betätigung die im Programmtext gar nicht sichtbare vordefinierte Methode start aufgerufen wird: dann wird run automatisch im Hintergrund angestoßen, die Kontrolle kehrt sofort zur aufrufenden Stelle zurück und es wird nicht gewartet: Das aufrufende Programm läuft wieder weiter, während parallel noch der angestoßene Thread arbeitet. Beide Prozesse laufen praktisch unabhängig voneinander nebeneinander her.
Der Austausch von Informationen kann trotzdem gelingen. Dazu werden im Hauptprogramm definierte und erzeugte (nicht-primitive) Variablen bzw. Objekte einmal an den Thread übergeben (bevor run oder start aufgerufen werden, also am besten schon mit dem Konstruktor). Der Thread kennt dann die (Speicheradresse der) Variablen oder Objekte und kann deren Inhalte ändern, sodass automatisch auch im Hauptprogramm (und jeder anderen Stelle, die auf die gleichen Objekte zugreift) sofort die neuen Inhalte bekannt sind. Im Beispiel soll die ganze Textzeile, in der das Auto fährt, an einen Thread übergeben werden, der dort das Auto jeweils nach kurzer Wartezeit mit immer mehr führenden Leerzeichen hinein schreibt. Im Hauptprogramm bzw. der Swingoberfläche steht das veränderte Oberflächenobjekt unmittelbar zur Verfügung und zwingt die Oberfläche nach jeder Änderung zur Aktualisierung seiner Darstellung auf dem Bildschirm.
Der Thread wird in einer eigenen Datei "P10_AutoThread.java" notiert:
// R. Krell, 27.5.2018 (www.r-krell.de) // Bewegung eines Autos für P10_Autorennen import javax.swing.*; public class P10_AutoThread extends Thread { int nummer; // Nummer des Autos, also 1, 2 oder 3 JTextField rennbahn; // Textfeld der Oberfläche, in dem das Auto fährt int max; // Anzahl der Leerzeichen bis zum Ziel (hier 47) JTextField ausgabe; // Textfeld der Oberfläche für die Siegermeldung public P10_AutoThread (int nr, JTextField tf, int ende, JTextField jTfAus) { super(); // Konstruktoraufruf der Thread-Oberklasse nummer = nr; // Übernahme der kopierten primitiven Werte ... rennbahn = tf; // .. und der übergebenen Objekte. max = ende; // Achtung: Nur die Objekte .. ausgabe = jTfAus; // .. werden gemeinsam mit Hauptprogramm genutzt. } private void warte (int ms) // Zeit in Millisekunden, 100 = 0,1 Sekunde Wartezeit { // (Aus der Oberfläche kann warte wieder weg) try { Thread.sleep (ms); } catch (InterruptedException e) { System.out.println("** Fehler beim Warten: "+e); } } public void run() // diese Methode wird bei start aufgerufen! { // Variante 1: mit fester Wartezeit von 100 Millisekunden String auto; // Auto mit führenden Leerzeichen "O>o", " O>o", .. for (int schritt=0; schritt<max ;schritt++) // nach max Schritten stößt Auto rechts an { auto = rennbahn.getText(); // Auto mit zurückgelegter Strecke rennbahn.setText(" "+auto); // Auto rückt eine Stelle weiter nach rechts warte(100); // damit sich das Auto langsam bewegt } // end of for ausgabe.setText (ausgabe.getText()+"Auto "+nummer+" im Ziel! "); } } |
Der AutoThread wurde extra so allgemein gehalten, dass er nicht nur fürs erste, sondern später auch für weitere Autos unverändert benutzt werden kann. In der Oberfläche "P10_Autorennen,java" wird eine entsprechend abgeänderte Methode jBtStart_ActionPerformed aufgerufen (s.u.); die Methode jBtAufstellen_ActionPerformed bleibt natürlich unverändert, während die warte-Methode jetzt ja inP10_AutoThread steht, in der Oberfläche nicht mehr gebraucht wird und dort gelöscht werden kann.
public void jBtStart_ActionPerformed(ActionEvent evt) { // dritter Versuch, mit einem Thread (für Auto 1) P10_AutoThread t1 = new P10_AutoThread( 1, jTfAuto1, 47, jTfAusgabe); t1.start(); // stößt die run-Methode des Threads t1 an } // end of jBtStart_ActionPerformed |
Der Test zeigt, dass nun das aus den beiden Klassen P10_Autorennen und P10_AutoThread bestehende Programm endlich wunschgemäß funktioniert: Nach Druck auf "Start" bewegt sich das erste Auto sichtbar bis ins Ziel. Das ermutigt, jetzt auch die anderen beiden Auto ebenso zu bewegen, wofür wieder nur die ActionPerformed-Methode in der Oberfläche P10_Autorennen erweitert werden muss:
public void jBtStart_ActionPerformed(ActionEvent evt) { // vierter Versuch, mit je einem Thread pro Auto P10_AutoThread t1 = new P10_AutoThread( 1, jTfAuto1, 47, jTfAusgabe); P10_AutoThread t2 = new P10_AutoThread( 2, jTfAuto2, 47, jTfAusgabe); P10_AutoThread t3 = new P10_AutoThread( 3, jTfAuto3, 47, jTfAusgabe); t1.start(); // stößt die run-Methode des Threads t1 an: Auto 1 startet t2.start(); // stößt die run-Methode des Threads t2 an: Auto 2 startet t3.start(); // stößt die run-Methode des Threads t3 an: Auto 3 startet } // end of jBtStart_ActionPerformed |
Nach dieser Änderung funktioniert das Rennen schon. Allerdings sind die Autos auf dem Bildschirm scheinbar gleich schnell: sie fahren immer nebeneinander. Das verwundert nicht, denn die drei Autos fahren mit den parallel laufenden Threads t1, t2 und t3 (alle nach dem gleichen Bauplan von P10_AutoThread) auch alle gleich schnell: immer wird die Strecke von einem Leerzeichen in einer Zehntelsekunde zurück gelegt. Deshalb erstaunt es eher, dass bei mehrfachem Start unterschiedliche Zieleinläufe gemeldet werden: mal findet sich dort die Meldung "Auto 3 im Ziel! Auto 1 im Ziel! Auto 2 im Ziel! ", während beim nächsten Rennen "Auto 2 im Ziel! Auto 3 im Ziel! Auto 1 im Ziel! " oder irgendein anderer, nicht vorhersehbarer Zieleinlauf gemeldet wird, obwohl kein Auto sichtbar schneller war. Grund hierfür ist, dass unvermeidbare minimalste Zeitunterschiede von vielleicht weniger als einer tausendstel Sekunde zwischen den Threads (die in Wirklichkeit übrigens nicht unbedingt parallel, sondern meist abwechselnd stückchenweise quasi-parallel abgearbeitet werden) sich bei der Zielmeldung auswirken. Außerdem können nicht alle drei Threads wirklich gleichzeitig jeweils mit ihrer ausgabe auf das eine Objekt jTfAusgabe zugreifen - Java muss solche Zugriffe immer irgendwie hintereinander ausführen.
Natürlich eignet sich diese Variante schon, um spannende Wetten auf den Zieleinlauf abzuschließen. Schöner wäre allerdings, wenn die Autos auch erkennbar unterschiedlich schnell wären. Das kann erreicht werden, wenn im Thread nicht immer die gleiche Zeit für die Bewegung um eine Leerstelle gewartet wird, sondern zufällige Zeiten verwendet werden. Da der Zufallsgenerator Math.random() Kommazahlen zwischen 0.00000 und 0.99999 liefert, muss mit verschiedenen Mindestwartezeiten (hier 70) und Faktoren (im Beispiel 200) experimentiert werden, bis man mit der Bewegung aller drei Rennwagen zufrieden ist. Die nachfolgend nicht mehr aufgeführte warte-Methode bleibt natürlich ebenso wie der Konstruktor unverändert im AutoThread.
private void warteZufällig() { warte((int)(70.0 + 200.0*Math.random())); // erzeugt zufällige Wartezeit, hier zwischen 70 und 269 Millisekunden } public void run() // diese Methode wird bei start aufgerufen! { // Variante 2: mit variabler Wartezeit String auto; // Auto mit führenden Leerzeichen "O>o", " O>o", .. for (int schritt=0; schritt<max &&(ausgabe.getText()).equals(""); schritt++) // nach max Schritten stößt Auto rechts an { auto = rennbahn.getText(); // Auto mit zurückgelegter Strecke rennbahn.setText(" "+auto); // Auto rückt eine Stelle weiter nach rechts warteZufällig(); // damit sich das Auto mit zufälliger Geschwindigkeit bewegt } // end of for ausgabe.setText (ausgabe.getText()+"Auto "+nummer+" im Ziel! "); } |
Mit dem optionalen Zusatz && (ausgabe.getText()).equals("") (im Programmtext braun hervor gehoben) stoppen die Autos übrigens, wenn das erste im Ziel ist (und wegen dessen Ziel-Ankufts-Meldung das gemeinsam genutzte Ausgabefeld nicht mehr leer ist). Dann erinnert das Autorennen mehr an das anfangs erwähnte Kirmesspiel. Allerdings sollte dann oben auch noch die Meldung über den angeblichen Zieleinlauf des zweiten und dritten Autos unterdrückt werden, der ja gar nicht mehr stattfindet. Ohne die Zusatzbedingung in der for-Schleife endet das Zufalls-Rennen erst, wenn alle Autos im Ziel sind. Beidesmal lädt das jetzt erfolgreich programmierte Zufallsrennens aber zum Wetten auf das Spielergebnis ein.
zum Seitenanfang / zum Seitenende
Nach der ersten Freude über den Programmiererfolg von P10 und einigen gewonnene Wetten über den Zieleinlauf der Autos stellt sich beim Spiel von P10 möglicherweise bald Langeweile ein, weil man nicht ins Spielgeschehen eingreifen kann, sondern zum passiven Zuschauen verurteilt ist. Das soll jetzt geändert werden. Drei menschliche Spieler sorgen durch Tastendrücke jeweils für die Bewegung 'ihres' Autos. Gespielt wird gemeinsam an einem Computer; die Tasten [Q], [B] und [P] liegen weit genug auseinander und können ohne große gegenseitige Behinderung von drei Leuten auf einer Tastatur erreicht werden.
Nach jedem Tastendruck darf sich das Auto aber nur um ein kleines Stückchen (etwa um eine Leerstelle) bewegen. Würde es dann von alleine weiterfahren, wäre ja keine weitere Interaktion mehr nötig und möglich. Anders als oben in P10_AutoThread darf also keine for-Schleife mehr im Programmtext stehen. Nach jedem Bewegungsschritt muss das Auto vielmehr auf den erneuten Druck seiner Taste warten.
Sicher gibt es verschiedene Möglichkeiten, diese Anforderungen umzusetzen. In der Schule sollen auf jeden Fall verschiedene Ideen und Ansätze überlegt und diskutiert werden. Für die Darstellung auf dieser Webseite habe ich mich für eine Realisierung entschieden, die auf dem bisher (in den Programmen P1 bis P10) Erlernten fußt und auch von Schülerinnen und Schülern nach einem Jahr Informatik-Grundkurs gefunden, zumindest aber durchschaut und programmiert werden kann. Um die direkte Tastenabfrage nicht auch noch selbst programmieren zu müssen, soll eine Eingabezeile genutzt werden (Eingabezeilen sind schon seit dem Programm P1 bekannt und im Gebrauch). Alle Tastendrücke erfolgen in dieses gemeinsame Textfeld und können dann von einer zentralen Auswerte-Methode gelesen und in die Bewegungen der Autos umgesetzt werden:
Klar ist, dass die Auswertung wieder parallel zur Oberfläche ausgeführt werden muss (also in einem eigenen Thread), damit in der Oberfläche gleichzeitig weitere Tastendrücke empfangen und die Bewegungen der Autos dargestellt werden können. Denn die Schleife in der Auswertung und damit die Auswertung selbst endet ja erst, wenn - je nach Wunsch - ein Auto oder alle Autos im Ziel sind. Und zwischendurch soll die Oberfläche ja ständig den aktuellen Zwischenstand anzeigen. Die Bewegung eines Autos um ein kleines Stück muss hingegen nicht mehr unbedingt in einem Thread erfolgen - sie nimmt ja nur ganz kurze Zeit in Anspruch, sodass die Auswertung (die ohnehin auf Tastendrücke warten muss und meist im ganz rechten Kanal leer läuft), problemlos darauf warten kann.
Entsprechend scheint zur Bewegung eines/jedes Autos um ein Stück die nachfolgende Klasse P11_Auto geeignet (die als abgespeckte
Version aus P10_AutoThread hervorging):
// R. Krell, 28.5.2018 (www.r-krell.de) // Bewegung eines Autos um ein Stück für P11_Autorennen bzw. P11_Auswertung import javax.swing.*; public class P11_Auto { int nummer; // Nummer des Autos, also 1, 2 oder 3 JTextField rennbahn; // Textfeld der Oberfläche, in dem das Auto fährt int max; // Anzahl der Leerzeichen bis zum Ziel JTextField ausgabe; // Textfeld der Oberfläche für die Siegermeldung int schritte = 0; // zählt mit, wie weit das Auto ist (=Anzahl Leerzeichen) public P11_Auto (int nr, JTextField tf, int ende, JTextField jTfAus) { nummer = nr; // Übernahme der kopierten primitiven Werte ... rennbahn = tf; // .. und der übergebenen Objekte. max = ende; // Achtung: Nur die Objekte .. ausgabe = jTfAus; // .. werden gemeinsam mit Hauptprogramm genutzt. } public void einStückWeiter() { if (schritte < max) { String auto = rennbahn.getText(); // Auto mit bisher gefahrenem Weg rennbahn.setText(" "+auto); // Auto rückt eine Stelle weiter nach rechts schritte++; // Auto hat einen Schritt mehr zurückgelegt } if (schritte == max) { ausgabe.setText (ausgabe.getText()+"Auto "+nummer+" im Ziel! "); schritte++; // damit später nicht noch eine Ziel-Erreicht-Meldung kommt } } } |
In einStückWeiter muss und darf nicht gewartet werden, sodass die warte-Methode in die Auswertung wandert, die -
entsprechend obigem Struktogramm - damit folgende Gestalt erhalten kann:
// R. Krell, 28.5.2018 (www.r-krell.de) // Auswertung der Tastendrücke für P11_Autorennen import javax.swing.*; public class P11_Auswertung extends Thread { JTextField bahn1; JTextField bahn2; JTextField bahn3; int bahnlänge; JTextField eingabefeld; JTextField ausgabefeld; public P11_Auswertung (JTextField b1, JTextField b2, JTextField b3, int max, JTextField ein, JTextField aus) { bahn1 = b1; bahn2 = b2; bahn3 = b3; bahnlänge = max; eingabefeld = ein; ausgabefeld = aus; } private void warte (int ms) // Zeit in Millisekunden, 100 = 0,1 Sekunde Wartezeit { try { Thread.sleep (ms); } catch (InterruptedException e) { System.out.println("** Fehler beim Warten: "+e); } } public void run() // diese Methode wird aus der Oberfläche mit start angestoßen { P11_Auto wagen1 = new P11_Auto (1, bahn1, bahnlänge, ausgabefeld); P11_Auto wagen2 = new P11_Auto (2, bahn2, bahnlänge, ausgabefeld); P11_Auto wagen3 = new P11_Auto (3, bahn3, bahnlänge, ausgabefeld); while ((ausgabefeld.getText()).equals("")) // noch kein Auto im Ziel { if ((eingabefeld.getText()).length() > 0) // Es gab (mind.) einen Tastendruck { String alleZeichen = eingabefeld.getText(); // sichert den gesamten Inhalt der Eingabezeile char eingabe = (alleZeichen.toUpperCase()).charAt(0); // vorderstes Zeichen eingabefeld.setText(alleZeichen.substring(1)); // alles ohne vorderstes Zeichen zurück eingabefeld.requestFocus(); // damit der Cursor im Eingabefeld steht switch (eingabe) { case 'Q': wagen1.einStückWeiter(); break; case 'B': wagen2.einStückWeiter(); break; case 'P': wagen3.einStückWeiter(); break; } // end of switch } warte(5); } // end of while } } |
In der - hier nicht vollständig wiedergegebenen Oberfläche P11_Autorennen - müssen die Schaltflächen-Methoden dann wie folgt gefüllt werden. Zu beachten ist, dass in der Oberfläche P11 noch ein Textfeld jTfEingabe hinzugekommen ist, das die Tastendrück aufnimmt und bei "Auf die Plätze.." ebenfalls geleert wird.
public void jBtAufstellen_ActionPerformed(ActionEvent evt) { jTfAuto1.setText("O>o"); jTfAuto2.setText("O>o"); jTfAuto3.setText("O>o"); jTfAusgabe.setText(""); jTfEingabe.setText(""); } // end of jBtAufstellen_ActionPerformed public void jBtStart_ActionPerformed(ActionEvent evt) { jTfEingabe.requestFocus(); P11_Auswertung auswertung = new P11_Auswertung(jTfAuto1,jTfAuto2,jTfAuto3,47,jTfEingabe,jTfAusgabe); auswertung.start(); } // end of jBtStart_ActionPerformed |
jTfEingabe.requestFocus() sorgt dafür, dass das Eingabefeld in der Oberfläche ausgewählt wird und die Tastendrücke hier ankommen. Damit hat das Autorennen P11 seine endgültige Fassung erhalten und kann gespielt werden. Dabei nützt es übrigens keinem Spieler, wenn er seine Taste festhält und hofft, durch die einsetzende automatische Tastenwiederholung schneller ans Ziel zu gelangen, als mit ständigem Tippen: sowie eine Mitspielerin ihre Taste drückt, ist die Wiederholung aufgehoben und das Auto der Mitspielerin bewegt sich. Die Taste des Spielers muss losgelassen und erneut gedrückt werden, damit wieder sein Auto fährt.
Entstanden ist also das geplante interaktive Wettkampf-Geschicklichkeitsspiel P11 für drei Personen, wo die spielende Person gewinnt, die am häufigsten ihre Taste drücken kann. Langweile kommt beim Spiel mit dieser Version nicht mehr auf - höchstens Sehnenscheidenentzündung.
zum Seitenanfang / zum Seitenende
Jetzt soll noch erklärt werden, wie man die grafische Darstellung etwas aufhübscht und bunte Bildchen statt der "O>o"-Texte für die Autos verwendet. Natürlich ist die neue Darstellung sowohl für das Zufallsrennen (wie in P10) als auch für das Tastenrennen (wie in P11) möglich. Sofern es überhaupt Unterschiede in der Oberfläche gibt, wird im Folgenden die letzte Variante mit dem Eingabefeld für die Tastendrücke gezeigt.
Entscheidend ist der Ersatz der Textfelder durch Oberflächenobjekte, die Grafiken aufnehmen können. Das geht z.B. wie in P9_A3 mit JPanels, aber auch mit Objekten vom Typ JLabel, die im Kurs bisher nur für Beschriftungs-Texte auf der Oberfläche verwendet wurden. Für JLabel bietet der Objekt-Inspektor des Javaeditors die Möglichkeit an, bequem Grafiken auszuwählen und zu positionieren. Insofern wurden hier JLabels gewählt, um die meiste Arbeit mit der Maus im GUI-Builder erledigen zu können und sich den Programmtext automatisch erzeugen zu lassen.
Als Vorarbeit sollten aber - entweder durch eigenes Zeichnen oder durch Abändern, Färben und Verkleinern frei benutzbarer Cliparts mit Hilfe irgendeines geeigneten Grafikprogramms - drei verschiedene Autobilder angefertigt und am besten in einem Unterordner mit dem Namen "images" unter dem Arbeitsordner für den Programmtext von P12 gespeichert werden. Dann kann die Oberfläche wie folgt erzeugt werden. (Natürlich muss die Oberfläche von P12 nicht wieder von null an neu erzeugt werden, sondern kann als Abwandlung aus P11 hervor gehen. Dazu öffnet man die Oberfläche von P11 im Javaeditor, gibt bei 'Datei' > Speicher unter' den neuen Namen "P12_Autorennen" ein, löscht danach die drei Textfelder der Autos und zieht stattdessen dort JLabels (in der Swing-1-Werkzeugleist mit J gekennzeichnet) hin. Im Objekt-Inspektor werden wieder sprechende Namen vergeben, der Vorschlagstext 'text' gelöscht und dafür das Icon über die drei Punkte am rechten Rand ausgewählt:
Grundsätzlich sind verschiedene Bildformate möglich; Ende Mai 2018 hatte der Objekt-Inspektor des Javaeditors aber mal Probleme mit gif-Bildern: Einfach ausprobieren und ggf. ein anderes Format wählen! Seit Version 15.23 (Anfang Juni 2018) lassen sich aber wieder alle Formate fehlerfrei auswählen.
Typischerweise erzeugt(e) der Java-Editor automatischen Text für die Attribute (wobei im Objekt-Inspektor die Namensvorschläge jLabelX auf jLbAuto1 bis jLbAuto3 abgeändert wurden):
private JLabel jLbAuto1 = new JLabel();
private ImageIcon jLbAuto1Icon = new ImageIcon("images/F1_rt.png");
private JLabel jLbAuto2 = new JLabel();
private ImageIcon jLbAuto2Icon = new ImageIcon("images/F1_bl.png");
private JLabel jLbAuto3 = new JLabel();
private ImageIcon jLbAuto3Icon = new ImageIcon("images/F1_gr.png");
(Achtung/Zusatz: Vorstehende Attribute reichen für den Gebrauch in der Entwicklungsumgebung völlig aus. Wer aber sein Programm P12 später in eine ausführbare .jar-Datei packen will, muss den Javatext ggf. von Hand durch die rot eingefügten Teile ergänzen, damit die Bildchen dann auch unter den Einschränkungen einer .jar-Datei wirklich geladen und sichtbar werden. Auf meine Anregung hin werden künftige Versionen des Javaeditors den Quelltext wohl automatisch direkt mit dem roten Zusatztext erstellen, sodass keine händische Nachbearbeitung mehr nötig wird:
private JLabel jLbAuto1 = new JLabel();
private ImageIcon jLbAuto1Icon = new ImageIcon(getClass().getResource("images/F1_rt.png"));
private JLabel jLbAuto2 = new JLabel();
private ImageIcon jLbAuto2Icon = new ImageIcon(getClass().getResource("images/F1_bl.png"));
private JLabel jLbAuto3 = new JLabel();
private ImageIcon jLbAuto3Icon = new ImageIcon(getClass().getResource("images/F1_gr.png"));
)
Etwas später wird dann beschrieben, wo und wie groß die Label auf der Oberfläche cp sind:
jLbAuto1.setBounds(32, 40, 107, 33);
jLbAuto1.setText("");
jLbAuto1.setIcon(jLbAuto1Icon);
cp.add(jLbAuto1);
jLbAuto2.setBounds(32, 72, 115, 33);
jLbAuto2.setText("");
jLbAuto2.setIcon(jLbAuto2Icon);
cp.add(jLbAuto2);
jLbAuto3.setBounds(32, 104, 107, 33);
jLbAuto3.setText("");
jLbAuto3.setIcon(jLbAuto3Icon);
cp.add(jLbAuto3);
Die Angaben in Klammern hinter setBounds geben die x- und y-Koordinaten der linken oberen Ecke sowie die Breite und Höhe der JLabels mit den Autobildchen an. Die Details müssen gar nicht alle verstanden werden. Wichtig ist lediglich, dass die Koordinaten der linken oberen Ecken in die setLocation-Aufrufe in der jBtAufstellen_ActionPerformed-Methode (für den "Auf die Plätze.."-Knopf) übernommen werden, damit die Autos nach einem Rennen wieder an die gleichen Stellen zurückgestellt werden. Die ganze jBtAufstellen_ActionPerformed-Methode von P12 ist in der vorstehenden Bildschirmansicht in den Zeilen 126 bis 132 sichtbar und braucht nicht nochmal wiederholt werden. Vom Aufbau entspricht sie der gleichnamigen Methode von P11 nur mit anderen Rücksetz-Befehlen.
Bevor auf die in der Bildschirmansicht teilweise verdeckte "Start"-Methode eingegangen wird, soll zunächst der Auswertungs-Thread
betrachtet werden. Statt den Text "O>o" durch eine davor eingefügte Leerstelle nach rechts zu schieben, wird jetzt das ganze JLabel mit dem
Autobild um 5 Pixel nach rechts gerückt, d.h. die x-Koordinate der rechten oberen Ecke (deren letzter Wert mit getX() abgefragt
werden kann) wird um 5 vergrößert. Die y-Koordinate bleibt unverändert. Bei P11 fand das einStückWeiter-Schieben des
Autos in einer eigenen Klasse P11_Auto statt. Allerdings wurde dort mehr Programmtext für die Parameter-Übergabe, den
Konstruktor und den Methodenkopf geschrieben, als für das Weiterrücken. Im Folgenden wird auf eine eigene Klasse P12_Auto (und
auf nach ihrem Bauplan erzeugte Objekte wagen1, wagen2 und wagen3) verzichtet, und die Arbeit direkt in P12_Auswertung
erledigt:
// R. Krell, 29.5.2018 (www.r-krell.de) // Auswertung der Tastendrücke für P12_Autorennen import javax.swing.*; public class P12_Auswertung extends Thread { JLabel bahn1; JLabel bahn2; JLabel bahn3; int bahnlänge; JTextField eingabefeld; JTextField ausgabefeld; public P12_Auswertung (JLabel lb1, JLabel lb2, JLabel lb3, int max, JTextField ein, JTextField aus) { super(); bahn1 = lb1; bahn2 = lb2; bahn3 = lb3; bahnlänge = max; eingabefeld = ein; ausgabefeld = aus; } private void warte (int ms) // Zeit in Millisekunden, 100 = 0,1 Sekunde Wartezeit { try { Thread.sleep (ms); } catch (InterruptedException e) { System.out.println("** Fehler beim Warten: "+e); } } public void run() { while ((bahn1.getX() < bahnlänge) && (bahn2.getX() < bahnlänge) && (bahn3.getX() < bahnlänge)) { if ((eingabefeld.getText()).length() > 0) // Es gab (mind.) einen Tastendruck { String alleZeichen = eingabefeld.getText(); char eingabe = (alleZeichen.toLowerCase()).charAt(0); // vorderstes Zeichen eingabefeld.setText(alleZeichen.substring(1)); // alles ohne vorderstes Zeichen eingabefeld.requestFocus(); // damit der Cursor im Eingabefeld steht switch (eingabe) { case 'q': bahn1.setLocation(bahn1.getX()+5, bahn1.getY()); break; case 'b': bahn2.setLocation(bahn2.getX()+5, bahn2.getY()); break; case 'p': bahn3.setLocation(bahn3.getX()+5, bahn3.getY()); break; } } // end of if warte(5); } // end of while if (bahn1.getX() >= bahnlänge) { ausgabefeld.setText ("Auto 1 (rot, 'q') hat gewonnen!"); } else if (bahn2.getX() >= bahnlänge) { ausgabefeld.setText ("Auto 2 (blau, 'b') hat gewonnen!"); } else if (bahn3.getX() >= bahnlänge) { ausgabefeld.setText ("Auto 3 (grün, 'p') hat gewonnen!"); } } } |
Bitte beachten: trotz z.T. gleicher Namen in P11_Auswertung und P12_Auswertung verbergen sich dahinter oft Variablen verschiedener Typen! So ist z.B. bahn1 in P11 ein JTextField, aber hier in P12 ein JLabel. Zur Abwechslung wurde außerdem die Eingabe jetzt in Klein- statt in Großbuchstaben übersetzt und das Ende wird hier nicht mehr durch die Kontrolle des Ausgabefelds auf eine Ziel-Meldung erkannt, sondern mit einer Mehrfachbedingung in der while-Schleife überwacht: Das P12-Spiel läuft nur, wenn noch alle drei Autos vorm Ziel sind. Die Bahnlänge bleibt zwar eine Ganzzahl, wird jetzt aber in Pixeln statt in Leerzeichen gemessen. (Dass, wie in P11_Auto noch befürchtet, die Ziel-Meldung eines Autos eventuell mehrfach kommt, ist bei der Kirmes-Variante ausgeschlossen, da das Spiel endet, wenn ein Auto das Ziel erreicht hat. Will man alle Autos ins Ziel lassen, müsste hier noch passende Vorsorge getroffen werden).
Damit kann der "Start"-Knopf in der Oberfläche P12_Autorennen schließlich wie folgt programmiert werden:
public void jBtStart_ActionPerformed(ActionEvent evt) { jTfEingabe.requestFocus(); P12_Auswertung fahre = new P12_Auswertung (jLbAuto1, jLbAuto2, jLbAuto3, 285, jTfEingabe, jTfAusgabe); fahre.start(); } // end of jBtStart_ActionPerformed |
Das geschieht ganz analog zur Programmierung in P11. Und ob das Objekt vom Typ P12_Auswertung nun fahre oder wie bei P11_Auswertung etwas fantasielos auswertung heißt, ist natürlich egal.
Jedenfalls funktioniert das Tastenrennen jetzt in P12 mit ansprechender Grafik. Wer der drei Mitspieler seine Taste am häufigsten drückt, wird für den körperlichen Einsatz mit dem Sieg seines schönen Rennwagens belohnt!
zum Seitenanfang / zum Seitenende
Zum Ausprobieren biete ich zwei lauffähige, spielbare Autorennen an, wie sie hier entwickelt wurden. Das Aussehen der Programme kennen Sie ja bereits vom Anfang dieser Seite.
Wenn Ihr Browser das (noch) zulässt (und Sie die Java-JRE 1.8 oder höher installiert haben), können Sie jedes der beiden Programme direkt hier aus dem Browser heraus starten ("Webstart"). Gibt es Probleme oder wollen Sie das Autorennen auch unabhängig vom Besuch dieser Webseite spielen, empfiehlt sich das Herunterladen der jar-Datei(en). Der Download sollte immer gelingen. Bei installierter JRE können Sie die herunter geladene (=gespeicherte) jar-Datei später auf Ihrem Computer per Doppelklick ausführen/starten und so das Autorennen spielen.
[Die kostenlose, aktuelle JRE (Java Runtime Environment; Java Ausführungs-Umgebung) erhalten Sie immer direkt von Oracle (https://java.com/de/download/, wo im Februar 2019 die auch für 64-Bit-Betriebssysteme völlig ausreichende und empfohlene 32-Bit-Version 1.8 bzw. "Version 8 Update 201" von Mitte Januar 2019 angeboten wurde), oder - nicht immer ganz so aktuell - auch von den Webseiten vieler Computerzeitschriften, wie etwa vom PC-Magazin)].
Webstart (jnlp-Datei nicht speichern/herunterladen, sondern mit dem Launcher öffnen, Risiko akzeptieren und ausführen) |
Download (jar-Datei auf den eigenen Rechner herunter laden und dort später ausführen) *) |
|
Zufallsrennen mit "O>o"-Autos | Webstart P10_Autorennen.jnlp | P10_Autorennen(r-krell.de).jar (8kB) |
Tastenrennen mit Auto-Bildchen | Webstart P12_Autorennen.jnlp | P12_Autorennen(r-krell.de).jar (16kB) |
*) Achtung: Manche Browser (wie etwa MS Edge) verändern beim Download einer .jar-Datei deren Endung automatisch und ungefragt in .zip. Sollte das bei Ihnen passieren, müssen Sie die Endung (trotz Warnung) erst wieder in .jar ändern. Durch das Umbenennen wird die Datei keineswegs unbrauchbar, sondern ist im Gegenteil erst dann wieder durch Doppelklick auf den Dateinamen ausführbar. Voraussetzung für den Programmstart per Doppelklick ist eine installierte Java-Laufzeitumgebung (JRE).
zum Seitenanfang / zum Seitenende
zum Seitenanfang / zum Seitenende
zurück zur Informatik-Hauptseite
zurück zur vor-vorigen Seite (Programme P1 bis P4) Seite
b1)_2017
zurück zur vorigen Seite (Programme P5 bis P9) Seite b2)_2017
zur Vertiefungs-Seite für P9 (Programme P9_A3x) Seite b3)_2018