Spieleprogrammierung in Java
Transcription
Spieleprogrammierung in Java
Virgil Fenzl Spieleprogrammierung in Java Vortrag von: Virgil Fenzl Fachbereich: Informatik Vortrag am: 05. Juni 2013 Inhaltsverzeichnis 1 Breakout Clon 1.1 Main Methode und JFrame . . . . . . 1.2 Game Boards . . . . . . . . . . . . . . 1.2.1 Die Board Klasse . . . . . . . . 1.2.2 Timer . . . . . . . . . . . . . . 1.3 Die Klasse Sprite . . . . . . . . . . . . 1.3.1 Die Klasse Brick . . . . . . . . 1.3.2 Darstellung der Bricks . . . . . 1.3.3 Die Klasse Paddle . . . . . . . . 1.3.4 Darstellung des Paddle . . . . . 1.3.5 Die Klasse Ball . . . . . . . . . 1.3.6 Darstellung des Balls . . . . . . 1.4 GameOver und Nachrichten darstellen 1.5 Kollisionserkennung . . . . . . . . . . . 1.6 Finale . . . . . . . . . . . . . . . . . . 1.6.1 Nächste Woche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 3 3 4 5 7 9 11 14 15 18 18 21 24 25 2 Air Game 26 A Quellcode des Breakout Games 28 Literaturverzeichnis 41 i Kapitel 1 Breakout Clon Abbildung 1.1: Breakout Game 1 1.1 Main Methode und JFrame In diesem Kapitel soll Schritt für Schritt die Entwicklung eines Breakout Clons behandelt werden. Alle notwendigen Bilder (und auch untersützende Java Dateien) können unter folgender URL geladen werden: http://www.virgil-fenzl.de/?page id=5 1.1 Main Methode und JFrame Den Code für diesen Abschnitt findet ihr unter 01 - JFrame. Um das Spiel darstellen zu können verwenden wir in diesem Beispiel einen JFrame. Eine passende Java Klasse könnte wie folgt aussehen: import javax . swing . JFrame ; public class Breakout extends JFrame { public Breakout () { setTitle ( " Breakout " ) ; s e t D e f a u l t C l o s e O p e r a t i o n ( EXIT_ON_CLOSE ) ; setSize (300 , 400) ; set L o c a t i o n R e l a t i v e T o ( null ) ; setIgnoreRepaint ( true ) ; setResizable ( false ) ; setVisible ( true ) ; } public static void main ( String [] args ) { new Breakout () ; } } • setTitle() setzt den Titel des JFrame • setDefaultCloseOperation() legt fest, was beim Klick auf das X unseres JFrames passieren soll 2 1.2 Game Boards • setSize() legt die Größe des Fensters fest • setLocationRelativeTo() sorgt dafür, wie der JFrame positioniert wird, in diesem Fall zentriert • setIgnoreRepaint() ignoriert paint() Nachrichten des OS (bessere Performance) • setRezizable() legt fest, ob der JFrame vergrößert / verkleinert werden darf • setVisible() legt fest, ob der JFrame sichtbar ist 1.2 Game Boards Den Code für diesen Abschnitt findet ihr unter 02 - Board and KeyListener. Hierfür erweitern wir den Konstruktor der Breakout Klasse um den Aufruf add(new Board()); . Diesen setzen wir am besten direkt vor setTitle(). 1.2.1 Die Board Klasse Um dem Board Leben einzuhauchen, erstellen wir eine neue Klasse Board (im selben Ordner wie bereits die Klasse Breakout). Der Inhalt sollte zu Beginn folgendermaßen sein: import java . awt . event . KeyAdapter ; import java . awt . event . KeyEvent ; import javax . swing . JPanel ; public class Board extends JPanel { public Board () { addKeyListener ( new TAdapter () ) ; setFocusable ( true ) ; } 3 1.2 Game Boards private class TAdapter extends KeyAdapter { public void keyReleased ( KeyEvent e ) { System . out . println ( " KEY RELEASED " ) ; } public void keyPressed ( KeyEvent e ) { System . out . println ( " PRESSED " ) ; } } } Wie ihr bestimmt bemerkt habt, befindet sich in dieser Klasse (neben dem Konstruktor) bereits weiterer Code. Dieser Code sorgt dafür, dass Tastatureingaben bereits verarbeitet werden. Hierfür wird ein KeyListener, ein KeyAdapter und Funktionen für die Verarbeitung der Eingaben benötigt. • keyReleased() wird aufgerufen, wenn eine Taste losgelassen wird • keyPressed() wird aufgerufen, wenn eine Taste gedrückt wird Bisher erzeugen Tastendrücke nur Ausgaben in der Konsole. Dies wollen wir später ändern. 1.2.2 Timer Den Code für diesen Abschnitt findet ihr unter 03 - A timer. Um später in unserem JPanel etwas darzustellen brauchen wir eine Funktion, die mit einer gewissen Häufigkeit pro Sekunde aufgerufen wird. Man spricht hier auch von Frames per Second. Hierfür brauchen wir in unserer Board Klasse folgende neue Importe: import java . util . Timer ; import java . util . TimerTask ; 4 1.3 Die Klasse Sprite Der Timer sorgt für einen regelmäßigen Aufruf von TimerTask (Task == Aufgabe), sorgt also für die saubere Abarbeitung von Aufgaben. Als nächstes brauchen wir eine Instanz des Timers Timer timer ; am Anfang der Klasse. Im Konstruktor rufen wir diese dann folgendermaßen auf: timer = new Timer () ; timer . s c h ed u l eA t F ix e d Ra t e ( new ScheduleTask () , 1000 , 10) ; Der 2. Parameter 1000 gibt den Delay, also die Verzögerung für den Aufruf in ms an. Der Timer started also 1 Sekunde nach Aufruf der Klasse Board. Der 3. Parameter 10 entspricht der Wiederholungsrate in ms. Also alle 10ms wird das Bild später neu gezeichnet. Als letztes müssen wir noch die Innere Klasse ScheduleTast() implementieren: class ScheduleTask extends TimerTask { public void run () { System . out . println ( " I ’m a Frame " ) ; repaint () ; } } 1.3 Die Klasse Sprite Den Code für diesen Abschnitt findet ihr unter 04 - Sprite. Da alle Sprites später ähnliche Eigenschaften und Methoden haben, ist es sinnvoll, für diese eine gemeinsame Klasse zu schaffen, aus denen die verschiedenen Sprites später erben können. In unserem Fall erschaffen wir eine neue Klasse mit der Bezeichnung Sprite. Diese enthält folgende Variablen und deren Setter und Getter Methoden 5 1.3 Die Klasse Sprite protected protected protected protected protected int x ; int y ; int width ; int heigth ; Image image ; Setter und Getter werden bekanntermaßen verwendet, um einen direkten Zugriff zu vermeiden. Um uns später bei Collisionserkennung und so zu unterstützen wollen wir noch ein Rechteck zurückgeliefert bekommen (dieses hat später u.a. nützliche Funktionen wie intersects(), also das Überschneiden von 2 Rechtecken). Rectangle getRect () { return new Rectangle (x , y , image . getWidth ( null ) , image . getHeight ( null ) ) ; } Die gesamte Klasse sieht damit folgendermaßen aus import java . awt . Image ; import java . awt . Rectangle ; public class Sprite { protected protected protected protected protected int x ; int y ; int width ; int heigth ; Image image ; public void setX ( int x ) { this . x = x ; } public int getX () { return x ; } public void setY ( int y ) { 6 1.3 Die Klasse Sprite this . y = y ; } public int getY () { return y ; } public int getWidth () { return width ; } public int getHeight () { return heigth ; } Image getImage () { return image ; } Rectangle getRect () { return new Rectangle (x , y , image . getWidth ( null ) , image . getHeight ( null ) ) ; } } Zum Sehen gibt es an dieser Stelle nichts, da Sprite nur eine Hilfsklasse ist, um uns bei der Implementierung eines Balls, Paddle und Bricks zu unterstützen. WICHTIG: In Java wird bei Sprites und allen Koordinaten der Darstellung immer vom obersten linken Punkt ausgegangen. Die y-Achse wird also nach unten positiv und nach oben negativ. Der oberste linke Punkt ist (0/0). 1.3.1 Die Klasse Brick Den Code für diesen Abschnitt findet ihr unter 05 - Brick. 7 1.3 Die Klasse Sprite Um einigermaßen ruckelfrei weiterarbeiten zu können, löschen wir erstmal alle Systemausgaben aus der Board Klasse, denn wir wissen ja jetzt, dass diese Stellen funktionieren. Damit sind die folgenden Zeilen gemeint: System . out . println ( " KEY RELEASED " ) ; System . out . println ( " PRESSED " ) ; System . out . println ( " I ’m a Frame " ) ; Da die Darstellung der Bricks etwas aufwendiger wird, gehen wir kleine Schritte voran. Für die Brickklasse brauchen wir folgendes: 1. Da wir Elemente der Klasse ImageIcon verwenden, ist ein Import an oberster Stelle notwendig: import javax . swing . ImageIcon ; 2. Unsere Brickklasse erbt von Sprite, was in Java ähnlich wie bei der Breakoutklasse (erbt von JFrame) bzw. Boardklasse (erbt von JPanel) folgendermaßen dargestellt wird: public class Brick extends Sprite {} 3. Wir brauchen eine Referenz zu einer Brick-Grafik (die Grafik kann gerne auch selbst erstellt werden), die ins Programmverzeichnis kopiert werden muss String brickie = " brick . png " ; und eine Referenz, ob dieses Objekt zerstört wurde (inkl. Setter and Getter) boolean destroyed ; public boolean isDestroyed () { return destroyed ; } public void setDestroyed ( boolean destroyed ) { this . destroyed = destroyed ; 8 1.3 Die Klasse Sprite } 4. Der Konstruktor der Klasse Brick public Brick ( int x , int y ) { this . x = x ; this . y = y ; ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( brickie ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; destroyed = false ; } Hier wird der Wert links oben (also x, y Werte) gesetzt, das ImageIcon (also Bild) geladen, die Breite und Höhe der Grafik berechnet und festgelegt, ob die Grafik zerstört wurde (dann müsste die Gamelogik diese später nicht mehr zeichnen). 1.3.2 Darstellung der Bricks An dieser Stelle wird es endlich spannend, denn wir schreiben die Logik für die Darstellung der Bricks. Es sollen 6 Bricks pro Reihe und 5 Reihen an Bricks werden. 6 * 5, also 30 Bricks. Die Logik hierfür muss in die Board Klasse, denn hier wird der Spieleablauf moderiert. Hierfür sind erstmal 2 neue Importe notwendig import java . awt . Graphics ; import java . awt . Toolkit ; Dann erzeugen wir ein Objekt der Klasse Brick und legen gleich ein Array an Bricks an Brick bricks []; 9 1.3 Die Klasse Sprite und instanziieren diese im Konstruktor bricks = new Brick [30]; Um die Bricks zu befüllen, erschaffen wir die Funktion gameInit(), welche uns auch gleich als Initialisierungsfunktion des Spiels dient. Diese soll am Ende des Konstruktors aufgerufen werden. Ihr Code sieht folgendermaßen aus: public void gameInit () { int k = 0; for ( int i = 0; i < 5; i ++) { for ( int j = 0; j < 6; j ++) { bricks [ k ] = new Brick ( j * 40 + 30 , i * 10 + 50) ; k ++; } } } Wir erzeugen also 30 Bricks (k == 0 bis k == 29), wobei die Schleifen pro Durchlauf immer mit i eine Reihe erzeugen (i == 0 bis i == 4) und j dann den Bricks einer Reihe entspricht (j == 0 bis j == 5). Den Bricks werden als Parameter die x, y Parameter der Brickklasse übergeben (also Punkt oben links). Diese berechnen sich • x ist y * 40 (also Anzahl der Bricks einer Reihe * Brickbreite) + 30 (padding links) • y ist i * 10 (also Anzahl der Brickreihen * Brickhöhe) + 50 (padding top) Da wir jetzt die Bricks erzeugt haben, müssen wir diese nur noch zeichnen: public void paint ( Graphics g ) { super . paint ( g ) ; for ( int i = 0; i < 30; i ++) { if (! bricks [ i ]. isDestroyed () ) g . drawImage ( bricks [ i ]. getImage () , bricks [ i ]. getX () , bricks [ i ]. getY () , bricks [ i ]. getWidth () , bricks [ i ]. getHeight () , this ) ; 10 1.3 Die Klasse Sprite } Toolkit . ge tDefau ltTool kit () . sync () ; g . dispose () ; } Die Methode repaint() unseres ScheduleTask ruft die paint() Methode alle 10ms auf. Die paint() Methode läuft in einer for-Schleife über alle 30 Bricks und überprüft, ob diese bereits zerstört wurden (bislang natürlich nicht möglich). Wenn diese noch funktionstüchtig sind, wird jedes Sprite gezeichnet. Danach wird alles gezeichnete synchronisiert (einige Elemente könnten ja noch Buffern...) und die Grafiken dann aus dem Speicher entfernt, um Lücken im Speicher vorzubeugen. 1.3.3 Die Klasse Paddle Den Code für diesen Abschnitt findet ihr unter 06 - Paddle. Der erste Schritt besteht dahin eine Paddle Grafik zu erzeugen, bzw. die Paddle Grafik aus dem images Verzeichnis ins Programmverzeichnis zu kopieren. Anschließend brauchen wir eine neue Klasse, nämlich die Paddle Klasse. Diese ist sehr ähnlich, wie die Brick Klasse aufgebaut, enthält aber zusätzlichen Code, um das Paddle zu positionieren und es anhand der x-Achse zu bewegen. Zum Aufbau der Klasse: Zusätzlich zu dem aus der Brick Klasse bekannten Import import javax . swing . ImageIcon ; brauchen wir, um die Tastatureingaben abzufangen, den Import import java . awt . event . KeyEvent ; Unsere Klasse erweitert ebenfalls wieder Sprite public class Paddle extends Sprite {} und enthält den Pfad für die Paddle Grafik 11 1.3 Die Klasse Sprite String paddle = " paddle . png " ; Da wir das Paddle an der x-Achse bewegen möchten definieren wir noch einen Integer dx. Der Konstruktor lädt die Bildresource dann wieder in ein ImageIcon und liest die Breite und Höhe aus. Anm: Diesmal ist es nicht notwendig den linken oberen wert auszulesen, da wir das Paddle nur einmal erschaffen und nicht relativ zu anderen Objekten ausrichten müssen. public Paddle () { ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( paddle ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; } Damit ist der Konstruktor schon (fast) fertig. Um das Paddle zu positionieren brauchen wir eine Funktion, welche die Startposition des Paddles setzt: public void resetState () { x = 200; y = 360; } Diese resetState() rufen wir natürlich auch am Ende des Konstruktors auf. Jetzt brauchen wir noch den Code für die Bewegung. Dieser besteht aus 2 Dingen: 1. die Bewegung an sich 2. die Veränderung der dx Werte durch Tastatureingaben 1. Bewegung 12 1.3 Die Klasse Sprite Die Funktion, die später aus der Board Klasse aufgerufen wird, heißt move() und sieht so aus: public void move () { x += dx ; if ( x <= 2) x = 2; if ( x >= 250) x = 250; } Die Abfrage, ob x>=250 ist, sorgt dafür, dass x auf den Wert 250 gesetzt wird. Das kommt daher, dass das Fenster 300 Pixel breit ist und das Paddle 40 Pixel breit. Daher stößt beim x Wert (zur Erinnerung, x beginnt links, y oben) 260 das Paddle rechts an. So wird verhindert, dass das Paddle aus dem Bildschirm verschwindet (und es ist noch etwas Platz). Bei x<=2 wird das Paddle immer 2 Pixel von links gesetzt. 2. Tastatureingaben Die Tastatureingaben werden folgendermaßen geprüft: public void keyPressed ( KeyEvent e ) { int key = e . getKeyCode () ; if ( key == KeyEvent . VK_LEFT ) { dx = -2; } if ( key == KeyEvent . VK_RIGHT ) { dx = 2; } } public void keyReleased ( KeyEvent e ) { 13 1.3 Die Klasse Sprite int key = e . getKeyCode () ; if ( key == KeyEvent . VK_LEFT ) { dx = 0; } if ( key == KeyEvent . VK_RIGHT ) { dx = 0; } } Dabei unterscheidet man das Drücken und Loslassen einer Taste. Dies wird durch seperate Funktionen abgehandelt. Erst wird durch int key = e . getKeyCode () ; die gedrückte Taste geprüft. Wenn man also nach Links drückt, wird dx um -2 verändert (Bewegung nach links). Bei einem Tastendruck nach rechts wird dx um 2 erhöht (Bewegung nach rechts). Beim loslassen wird die Bewegung dx auf 0 gesetzt (keine weitere Veränderung der x-Position des Paddles). 1.3.4 Darstellung des Paddle Um das Paddle anzuzeigen (und zu bewegen) müssen wir ein paar Änderungen in unserer Board Klasse vornehmen. Zuerst holen wir uns am Beginn der Board Klasse eine Instanz der Paddle Klasse. Paddle paddle ; In der gameInit() Methode erzeugen wir uns dann ein Paddle Objekt paddle = new Paddle () ; In der paint() Methode drucken wir das Paddle dann auf den Bildschirm (nach dem super() Aufruf). g . drawImage ( paddle . getImage () , paddle . getX () , paddle . getY () , paddle . getWidth () , paddle . getHeight () , this ) ; Im TAdapter rufen wir die Tastendrücke des Paddle auf. Also in KeyReleased 14 1.3 Die Klasse Sprite paddle . keyReleased ( e ) ; In KeyPressed paddle . keyPressed ( e ) ; Als letzten Schritt müssen wir das Paddle bei jedem neuen Frame natürlich auch bewegen (für den Fall, dass Tastendrücke eingegangen sind). Daher kommt folgendes in die run() Methode des ScheduleTask (natürlich vor dem Neuzeichnen): paddle . move () ; 1.3.5 Die Klasse Ball Den Code für diesen Abschnitt findet ihr unter 07 - Ball. Was einem echten Breakout noch fehlt ist der Ball/Kugel, der verwendet wird, um die Blöcke zu zerstören. Zuerst erstellen wir eine neue Klasse Ball und importieren uns (wie bei Brick und Paddle bereits zuvor) import javax . swing . ImageIcon ; Als nächstes lassen wir unsere Klasse wieder von Sprite erben public class Ball extends Sprite {} Da wir den Ball nicht nur wie das Paddle auf der x-Ebene bewegen wollen, sondern dieser ja auch schräg fliegen soll, brauchen wir eine x und y Koordinate (Erinnerung Mathe: Erzeugung einer Gerade aus zwei Punkten). Dies ist programmiertechnisch einfacher, als die Bewegung des Balles durch Winkel zu bestimmen. private int xdir ; private int ydir ; Für diese Integer erzeugen wir dann auch gleich die Setter und Getter, um auch aus der Board Klasse auf die Werte zugreifen zu können. public void setXDir ( int x ) { xdir = x ; 15 1.3 Die Klasse Sprite } public void setYDir ( int y ) { ydir = y ; } public int getYDir () { return ydir ; } public int getXDir () { return xdir ; } Dann erzeugen wir einen String, der den Pfad zu der passenden Grafik enthält protected String ball = " ball . png " ; Jetzt brauchen wir einen Konstruktor. Dieser soll (wie auch bei den vorherigen Sprites) das Bild laden und die Höhe/Breite einlesen. Auch die grundlegende Flugrichtung legen wir hier fest. public Ball () { xdir = 1; ydir = -1; ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( ball ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; } xdir = 1 bedeutet Flugrichtung nach rechts und ydir = -1 bedeutet Flugrichtung nach oben. Jetzt brauchen wir noch die Angabe der Ursprungsposition des Balls vor dem 16 1.3 Die Klasse Sprite Flug. Diesen packen wir wieder in resetState() rein. public void resetState () { x = 230; y = 355; } resetState() müssen wir natürlich noch am Ende des Konstruktors aufrufen, damit die Funktion bei der Erstellung der Klasse aufgerufen wird. Jetzt brauchen wir noch eine Funktion, mit deren Aufruf wir den Ball bewegen können. Also eine move() Funktion muss her. public void move () { x += xdir ; y += ydir ; if ( x == 0) { setXDir (1) ; } if ( x == 280) { setXDir ( -1) ; } if ( y == 0) { setYDir (1) ; } } Hierbei ist zu sagen, dass beim Aufruf der Funktion der x und y Wert angepasst werden. Wenn x == 0 (also linke Seite erreicht wurde) soll der Ball wieder nach rechts fliegen. Wenn x == 280 (also rechte Seite fast erreicht wurde) soll er wieder nach links fliegen. Wenn y == 0 (also der Ball oben angekommen ist) soll er wieder nach unten fliegen. 17 1.4 GameOver und Nachrichten darstellen 1.3.6 Darstellung des Balls Da das Folgende quasi dem Paddle ziemlich ähnelt, wird es nur kurz dargestellt. Es wird wieder in der Board Klasse editiert. Wir brauchen ein Ball Objekt Ball ball ; und laden dieses in gameInit(). ball = new Ball () ; Dann zeichnen wir das Objekt in der paint() Methode g . drawImage ( ball . getImage () , ball . getX () , ball . getY () , ball . getWidth () , ball . getHeight () , this ) ; und bewegen es in der run() Methode des ScheduleTask. ball . move () ; Soweit so gut. Der Ball bewegt sich einmal, fliegt durch alles durch und verschwindet unten im Nirvana. Was können wir dagegen tun? Das klären wir bei der Kollisionserkennung. 1.4 GameOver und Nachrichten darstellen Den Code für diesen Abschnitt findet ihr unter 08 - Verbesserungen. Hier stellen sich folgende Fragen: • Ist es sinnvoll, den Vordergrund Bildschirm Buffer für die Berechnung und Darstellung der Grafiken zu verwenden und was kann man dagegen tun? • Wie erkennen wir das Ende des Spiels und wie können wir eine Nachricht ala Game Over darstellen? Zum Buffering von Grafiken: 18 1.4 GameOver und Nachrichten darstellen Auch wenn es bei kleinen Spielen nicht ins Gewicht fällt, kann das vordergründige Buffern von Grafiken eine Anwendung schon drastisch verlangsamen oder gar andere Prozesse verhungern lassen. Dies kann je nach Platform, worauf das Spiel läuft, andere Folgen (bis zum Anwendungsabsturz) nach sich ziehen. Eine einfache Möglichkeit dieses Problem in Java zu vermeiden, ist das Verwenden von DoubleBuffering. Dies setzen wir am besten in der Klasse Board im Konstruktor (vor gameInit()). setDoubleB uffere d ( true ) ; Darstellung von Nachrichten: Dies ist schon etwas komplexer, wird aber auch in der Board Klasse erledigt. Wir müssen dabei bedenken, dass wir die Game Over Nachricht nur haben wollen, wenn das Spiel vorbei ist. Hierfür importieren wir erstmal die Klassen für Font, FontMetrics und Farbe. import java . awt . Color ; import java . awt . Font ; import java . awt . FontMetrics ; Dann brauchen wir 2 neue Variablen am Anfang unserer Board Klasse String message = " Game Over " ; boolean ingame = true ; Der String message enhält die Nachricht, die wir am Spielende darstellen wollen, der boolean Wert ingame zeigt, dass das Spiel läuft (da true, bei false wäre das Spiel zu Ende). Dann brauchen wir eine Funktion stopGame() die den Timer stoppt und ingame auf false setzt public void stopGame () { ingame = false ; timer . cancel () ; } Da wir stopGame() ja auch irgendwann aufrufen wollen, schaffen wir die Funktion checkBallOutside(), welche prüft, ob der Ball unten am Paddle vorbei aus dem 19 1.4 GameOver und Nachrichten darstellen Fenster fliegt. public void checkBallOutside () { if ( ball . getRect () . getMaxY () > 390) { stopGame () ; } } Diese Funktion checkBallOutside() rufen wir dann in der run() Methode des ScheduleTask auf (vor repaint()), um zu gewährleisten, dass in jedem Frame geprüft wird, ob der Ball außerhalb des Bildschirms ist. Jetzt muss noch die paint() Methode angepasst werden. Und zwar soll jetzt nur noch gezeichnet werden, wenn wir ingame == true haben, ansonsten soll Game Over dargestellt werden. Um dies zu erreichen packen wir alle Zeichenfunktionen vor dem sync() in eine if-Bedingung if ( ingame ) { // Alle zeichnenden Funktionen hier rein } else { Font font = new Font ( " Verdana " , Font . BOLD , 18) ; FontMetrics metr = this . getFontMetrics ( font ) ; g . setColor ( Color . BLACK ) ; g . setFont ( font ) ; g . drawString ( message ,(300 - metr . stringWidth ( message ) ) / 2 , 300 / 2) ; } und holen im else-Zweig eine Schriftart per Font und ihre Font Metriken (zur Berechnung der Breite des Schriftzuges am Bildschirm). Die Darstellfarbe soll schwarz sein. Die x und y Position werden durch die Metriken berechnet (um die Schrift am Bildschirm zu zentrieren). 20 1.5 Kollisionserkennung 1.5 Kollisionserkennung Den Code für diesen Abschnitt findet ihr unter 09 - Kollisionserkennung. Der letzte Abschnitt für den Breakout Clon geht um Kollisionserkennung. Hier brauchen wir erstmal wieder einen Import in der Board Klasse import java . awt . Point ; Die Kollisionserkennung spielt sich in folgender Methode ab public void checkCollision () {} welche wir (damit sich auch was tut) in der run() Methode des ScheduleTask aufrufen müssen (vor repaint()). Diese Methode sollte 3 Fälle abdecken: • alle Bricks sind zerstört, daher stopGame() aufrufen • Ball kollidiert mit Paddle • Ball kollidiert mit Brick 1. Fall: Alle Bricks sind zerstört for ( int i = 0 , j = 0; i < 30; i ++) { if ( bricks [ i ]. isDestroyed () ) { j ++; } if ( j == 30) { message = " Victory " ; stopGame () ; } } Wir iterieren über alle 30 Bricks und prüfen, ob sie zerstört sind und zählen die Zerstörten in der Variable j. Wenn j die 30 erreicht, zeigen wir als Nachricht Victory an und rufen stopGame() auf. 2. Fall: Ball kollidiert mit Paddle 21 1.5 Kollisionserkennung if (( ball . getRect () ) . intersects ( paddle . getRect () ) ) { int paddleLPos = ( int ) paddle . getRect () . getMinX () ; int ballLPos = ( int ) ball . getRect () . getMinX () ; int int int int first = paddleLPos + 8; second = paddleLPos + 16; third = paddleLPos + 24; fourth = paddleLPos + 32; if ( ballLPos < first ) { ball . setXDir ( -1) ; ball . setYDir ( -1) ; } if ( ballLPos >= first && ballLPos < second ) { ball . setXDir ( -1) ; ball . setYDir ( -1 * ball . getYDir () ) ; } if ( ballLPos >= second && ballLPos < third ) { ball . setXDir (0) ; ball . setYDir ( -1) ; } if ( ballLPos >= third && ballLPos < fourth ) { ball . setXDir (1) ; ball . setYDir ( -1 * ball . getYDir () ) ; } if ( ballLPos > fourth ) { ball . setXDir (1) ; ball . setYDir ( -1) ; } } Erst überprüfen wir, ob das Rechteck vom Paddle sich mit dem Rechteck vom Ball überschneidet. Wenn das zutrifft holen wir uns die ganzlinke Koordinate des 22 1.5 Kollisionserkennung Paddles und die ganzlinke Koordinate des Balls. Danach teilen wir das Paddle in 4 (eigentlich 5) Bereiche auf und berechnen deren x-Koordinaten (um später die Richtungen entsprechend dem Auftreffpunkt des Balls zu koordinieren). Je nach Position, wo der Ball das Paddle berührt, geben wir jetzt die Richtung an (in 5 Abschnitten): 1. x = links, y = oben 2. x = links, y = oben * alte Y Richtung Ball 3. x = keine, y = oben 4. x = rechts, y = oben * alte Y Richtung Ball 5. x = rechts, y = oben 3. Fall: Ball kollidiert mit Brick for ( int i = 0; i < 30; i ++) { if (( ball . getRect () ) . intersects ( bricks [ i ]. getRect () ) ) { int int int int ballLeft = ( int ) ball . getRect () . getMinX () ; ballHeight = ( int ) ball . getRect () . getHeight () ; ballWidth = ( int ) ball . getRect () . getWidth () ; ballTop = ( int ) ball . getRect () . getMinY () ; Point pointRight = new Point ( ballLeft + ballWidth + 1 , ballTop ) ; Point pointLeft = new Point ( ballLeft - 1 , ballTop ) ; Point pointTop = new Point ( ballLeft , ballTop - 1) ; Point pointBottom = new Point ( ballLeft , ballTop + ballHeight + 1) ; if (! bricks [ i ]. isDestroyed () ) { if ( bricks [ i ]. getRect () . contains ( pointRight ) ) { ball . setXDir ( -1) ; } 23 1.6 Finale else if ( bricks [ i ]. getRect () . contains ( pointLeft ) ) { ball . setXDir (1) ; } if ( bricks [ i ]. getRect () . contains ( pointTop ) ) { ball . setYDir (1) ; } else if ( bricks [ i ]. getRect () . contains ( pointBottom ) ) { ball . setYDir ( -1) ; } bricks [ i ]. setDestroyed ( true ) ; } } } Hier wird jeder Brick einzeln durchgegangen. Wenn er sich mit dem Rechteck des Balls überschneidet, wird vom Ball der x/y Wert oben links, die Höhe und Breite des Balls berechnet. Danach werden Punkte berechnet. • pointRight: x = 1 Pixel rechte neben Ball, y = oben • pointLeft: x = 1 Pixel links neben Ball, y = oben • pointTop: x = links vom Ball, y = 1 Pixel über dem Ball • pointBottom: x = links vom Ball, 1 Pixel unter dem Ball Wenn ein Block noch nicht zerstört wurde, wird die Flugrichtung des Balles angepasst und am Ende der Block zerstört. D.h. Berührung rechts führt zu Ballrichtung links, Berührung Links zur Ballrichtung rechts, Berührung oben zur Ballrichtung unten und Berührung unten zur Ballrichtung oben. 1.6 Finale Das wars! Ihr habt erfolgreich eueren ersten Breakout Clon geschrieben. 24 1.6 Finale Im Anhang befindet sich nochmal der komplette Quellcode in Druckform und auf der Website befinden sich auch die verschiedenen Stadien des Programms, so dass ihr, falls ihr nicht mitgekommen seid, nochmal selbst zu Hause ran könnt. Auch die Grafiken findet ihr bei mir zum Download. 1.6.1 Nächste Woche • Verwendung von Sounds • Verwendung von Animationen • Verwendung von Hintergrundbildern • Anwendung einer kleinen Künstlichen Intelligenz • Fortgeschrittene Game Techniken 25 Kapitel 2 Air Game Abbildung 2.1: Intelligenten Raketen ausweichen in unendlicher Idylle In diesem Kapitel soll Schritt für Schritt die Entwicklung eines Air Games behandelt werden. 26 Die Sprites sind diesmal animierte Wolken, Helicopter, Raketen, Explosionen. Jedes Sprite hat seinen eigenen Sound und die Raketen sogar eine kleine künstliche Zielverfolgungsintelligenz. Zudem sehen wir, wie wir z.B. Hintergrundbilder einfügen und nicht dargestellte Grafiken aus dem Speicher entfernen. Alle notwendigen Bilder (und auch untersützende Java Dateien) können unter folgender URL geladen werden: http://www.virgil-fenzl.de/?page id=5 27 Anhang A Quellcode des Breakout Games Breakout.java import javax . swing . JFrame ; public class Breakout extends JFrame { public Breakout () { add ( new Board () ) ; setTitle ( " Breakout " ) ; s e t D e f a u l t C l o s e O p e r a t i o n ( EXIT_ON_CLOSE ) ; setSize (300 , 400) ; set L o c a t i o n R e l a t i v e T o ( null ) ; setIgnoreRepaint ( true ) ; setResizable ( false ) ; setVisible ( true ) ; } public static void main ( String [] args ) { new Breakout () ; } } Board.java import java . awt . Color ; 28 import java . awt . Font ; import java . awt . FontMetrics ; import import import import import java . awt . Graphics ; java . awt . Toolkit ; java . awt . event . KeyAdapter ; java . awt . event . KeyEvent ; javax . swing . JPanel ; import java . util . Timer ; import java . util . TimerTask ; import java . awt . Point ; public class Board extends JPanel { Timer timer ; Brick bricks []; Paddle paddle ; Ball ball ; String message = " Game Over " ; boolean ingame = true ; public Board () { addKeyListener ( new TAdapter () ) ; setFocusable ( true ) ; bricks = new Brick [30]; setD oubleB uffere d ( true ) ; timer = new Timer () ; timer . s c h ed u l eA t F ix e d Ra t e ( new ScheduleTask () , 1000 , 10) ; gameInit () ; } 29 public void gameInit () { paddle = new Paddle () ; ball = new Ball () ; int k = 0; for ( int i = 0; i < 5; i ++) { for ( int j = 0; j < 6; j ++) { bricks [ k ] = new Brick ( j * 40 + 30 , i * 10 + 50) ; k ++; } } } public void paint ( Graphics g ) { super . paint ( g ) ; if ( ingame ) { g . drawImage ( ball . getImage () , ball . getX () , ball . getY () , ball . getWidth () , ball . getHeight () , this ) ; g . drawImage ( paddle . getImage () , paddle . getX () , paddle . getY () , paddle . getWidth () , paddle . getHeight () , this ) ; for ( int i = 0; i < 30; i ++) { if (! bricks [ i ]. isDestroyed () ) g . drawImage ( bricks [ i ]. getImage () , bricks [ i ]. getX () , bricks [ i ]. getY () , bricks [ i ]. getWidth () , bricks [ i ]. getHeight () , this ) ; } } else { Font font = new Font ( " Verdana " , Font . BOLD , 18) ; FontMetrics metr = this . getFontMetrics ( font ) ; g . setColor ( Color . BLACK ) ; g . setFont ( font ) ; 30 g . drawString ( message ,(300 - metr . stringWidth ( message ) ) / 2 , 300 / 2) ; } Toolkit . ge tDefau ltTool kit () . sync () ; g . dispose () ; } private class TAdapter extends KeyAdapter { public void keyReleased ( KeyEvent e ) { paddle . keyReleased ( e ) ; } public void keyPressed ( KeyEvent e ) { paddle . keyPressed ( e ) ; } } class ScheduleTask extends TimerTask { public void run () { paddle . move () ; ball . move () ; checkBallOutside () ; checkCollision () ; repaint () ; } } public void stopGame () { ingame = false ; timer . cancel () ; } public void checkBallOutside () { 31 if ( ball . getRect () . getMaxY () > 390) { stopGame () ; } } public void checkCollision () { for ( int i = 0 , j = 0; i < 30; i ++) { if ( bricks [ i ]. isDestroyed () ) { j ++; } if ( j == 30) { message = " Victory " ; stopGame () ; } } if (( ball . getRect () ) . intersects ( paddle . getRect () ) ) { int paddleLPos = ( int ) paddle . getRect () . getMinX () ; int ballLPos = ( int ) ball . getRect () . getMinX () ; int int int int first = paddleLPos + 8; second = paddleLPos + 16; third = paddleLPos + 24; fourth = paddleLPos + 32; if ( ballLPos < first ) { ball . setXDir ( -1) ; ball . setYDir ( -1) ; } if ( ballLPos >= first && ballLPos < second ) { ball . setXDir ( -1) ; ball . setYDir ( -1 * ball . getYDir () ) ; 32 } if ( ballLPos >= second && ballLPos < third ) { ball . setXDir (0) ; ball . setYDir ( -1) ; } if ( ballLPos >= third && ballLPos < fourth ) { ball . setXDir (1) ; ball . setYDir ( -1 * ball . getYDir () ) ; } if ( ballLPos > fourth ) { ball . setXDir (1) ; ball . setYDir ( -1) ; } } for ( int i = 0; i < 30; i ++) { if (( ball . getRect () ) . intersects ( bricks [ i ]. getRect () ) ) { int int int int ballLeft = ( int ) ball . getRect () . getMinX () ; ballHeight = ( int ) ball . getRect () . getHeight () ; ballWidth = ( int ) ball . getRect () . getWidth () ; ballTop = ( int ) ball . getRect () . getMinY () ; Point pointRight = new Point ( ballLeft + ballWidth + 1 , ballTop ) ; Point pointLeft = new Point ( ballLeft - 1 , ballTop ) ; Point pointTop = new Point ( ballLeft , ballTop - 1) ; Point pointBottom = new Point ( ballLeft , ballTop + ballHeight + 1) ; if (! bricks [ i ]. isDestroyed () ) { 33 if ( bricks [ i ]. getRect () . contains ( pointRight ) ) { ball . setXDir ( -1) ; } else if ( bricks [ i ]. getRect () . contains ( pointLeft ) ) { ball . setXDir (1) ; } if ( bricks [ i ]. getRect () . contains ( pointTop ) ) { ball . setYDir (1) ; } else if ( bricks [ i ]. getRect () . contains ( pointBottom )) { ball . setYDir ( -1) ; } bricks [ i ]. setDestroyed ( true ) ; } } } } } Sprite.java import java . awt . Image ; import java . awt . Rectangle ; public class Sprite { protected protected protected protected protected int x ; int y ; int width ; int heigth ; Image image ; 34 public void setX ( int x ) { this . x = x ; } public int getX () { return x ; } public void setY ( int y ) { this . y = y ; } public int getY () { return y ; } public int getWidth () { return width ; } public int getHeight () { return heigth ; } Image getImage () { return image ; } Rectangle getRect () { return new Rectangle (x , y , image . getWidth ( null ) , image . getHeight ( null ) ) ; } } Brick.java 35 import javax . swing . ImageIcon ; public class Brick extends Sprite { String brickie = " brick . png " ; boolean destroyed ; public Brick ( int x , int y ) { this . x = x ; this . y = y ; ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( brickie ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; destroyed = false ; } public boolean isDestroyed () { return destroyed ; } public void setDestroyed ( boolean destroyed ) { this . destroyed = destroyed ; } } Paddle.java import java . awt . event . KeyEvent ; 36 import javax . swing . ImageIcon ; public class Paddle extends Sprite { String paddle = " paddle . png " ; int dx ; public Paddle () { ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( paddle ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; resetState () ; } public void move () { x += dx ; if ( x <= 2) x = 2; if ( x >= 250) x = 250; } public void keyPressed ( KeyEvent e ) { int key = e . getKeyCode () ; if ( key == KeyEvent . VK_LEFT ) { dx = -2; } 37 if ( key == KeyEvent . VK_RIGHT ) { dx = 2; } } public void keyReleased ( KeyEvent e ) { int key = e . getKeyCode () ; if ( key == KeyEvent . VK_LEFT ) { dx = 0; } if ( key == KeyEvent . VK_RIGHT ) { dx = 0; } } public void resetState () { x = 200; y = 360; } } Ball.java import javax . swing . ImageIcon ; public class Ball extends Sprite { private int xdir ; private int ydir ; protected String ball = " ball . png " ; public Ball () { xdir = 1; 38 ydir = -1; ImageIcon ii = new ImageIcon ( this . getClass () . getResource ( ball ) ) ; image = ii . getImage () ; width = image . getWidth ( null ) ; heigth = image . getHeight ( null ) ; resetState () ; } public void move () { x += xdir ; y += ydir ; if ( x == 0) { setXDir (1) ; } if ( x == 280) { setXDir ( -1) ; } if ( y == 0) { setYDir (1) ; } } public void resetState () { x = 230; y = 355; } public void setXDir ( int x ) { 39 xdir = x ; } public void setYDir ( int y ) { ydir = y ; } public int getYDir () { return ydir ; } } 40 Literaturverzeichnis [1] Davison, Andrew: Killer Game Programming in Java. O’Reilly Media; Auflage: 1 (31. Mai 2005) [2] http://www.zetcode.com/tutorials/javagamestutorial/ Java 2D Games Tutorial (Englisch) [3] http://www.virgil-fenzl.de/ Virgil Fenzl - Spieleprogrammierung 41