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

Documents pareils