invaders - programowanie gier w Javie, Kursy i Nauka, Java
[ Pobierz całość w formacie PDF ]
Nauka programowania gier komputerowych w Javie.
Autor:
Piotr Modzelewski
E-mail:
keyer@mat.uni.torun.pl
Wstęp
Celem tego referatu jest napisanie prostej gry w Javie, jednakże nie trywialnej. W dziele tworzenia używać będziemy
dogodności programowania obiektowego. Tworzenie gry nie będzie się odbywać od razu. Zaczniemy od małego projektu,
który będzie się rozrastać. Kod będziemy poprawiać, ulepszać a nawet usuwać, aby zobaczyć jak przy różnych jego
wersjach działać będzie nasza aplikacja.
Celem tego kursu jest oprócz stworzenia gry:
Ulepszanie nauki Javy
Zdobycie podstaw na temat Swingu i AWT
Zrozumienie obsługi zdarzeń oraz komponentów
Nauka Javy2D
Nauka HashMapy i ArrayList
Sztuczek programistycznych
Pierwsze okno
Pierwszym krokiem tworzenia naszej aplikacji jest stworzenia okna. Wykorzystamy Bibliotekę Swing (dokładnie JFrame).
Zakładamy, że nasz projekt nazywa się WojnaSwiatow
import
javax.swing.JFrame;
public class
WojnaSwiatow{
public static final int
SZEROKOSC = 800;
public static final int
WYSOKOSC = 600;
public
WojnaSwiatow
() {
JFrame okno =
new
JFrame
("
.: Wojna Swiatow :.
");
okno.
setBounds
(0,0,SZEROKOSC,WYSOKOSC);
okno.
setVisible
(
true
);
}
public static void
main(String[] args) {
WojnaSwiatow inv =
new
WojnaSwiatow
();
}
}
Widzimy, że okno to nie będzie nawet kończyć programu w momencie jego zamknięcia. Tak być nie może. Oczywiście
naprawienie tego nie będzie kłopotem. Wystarczy konstruktor WojnaSwiatow( ) zmodyfikować dodając na jego końcu:
okno.
addWindowListener
(new
WindowAdapter
() {
public void
windowClosing
(WindowEvent e){
System.
exit
(0);
}
});
Oraz dodatkowo zaimportować obsługę zdarzeń:
import
java.awt.event.WindowAdapter;
import
java.awt.event.WindowEvent;
Zabawa w rysowanie
Podstawą w grze jest oczywiście grafika, a co za tym idzie, umiejętność rysowania. Każdy kto miał doczynienia z
okienkami wie, że rysowanie w oknie to nadpisanie metody paint( ) . Nasza klasa, WojnaSwiatow , nie jest jednak de facto
oknem. Aby jednak je w nią zamienić wystarczy dziedziczyć z Canvas :
import
javax.swing.JFrame;
…
public class
WojnaSwiatow
extends
Canvas{
Wspaniale. Teraz spróbujmy sprawdzić czy to wystarczy. Najprościej będzie, jeżeli coś narysujem. Bez zbędnego gadania,
zróbmy to. Na początku zaimportujmy:
import
javax.swing.JPanel;
import
java.awt.Color;
import
java.awt.Dimension;
import
java.awt.Graphics;
Teraz, aby utworzyć panel w oknie, modyfikujemy konstruktor:
public
WojnaSwiatow
() {
JFrame okno =
new
JFrame(".:Wojna Swiatow:.");
JPanel panel = (JPanel)okno.
getContentPane
();
setBounds
(0,0,SZEROKOSC,WYSOKOSC);
panel.
setPreferredSize
(
new
Dimension(SZEROKOSC,WYSOKOSC));
panel.
setLayout
(
null
);
panel.
add
(
this
);
okno.
setBounds
(0,0,SZEROKOSC,WYSOKOSC);
okno.
setVisible
(true);
okno.
addWindowListener
(
new
WindowAdapter() {
public void
windowClosing
(WindowEvent e) {
System.
exit
(0);
}
});
}
No to jak mamy panel, na którym będziemy rysować, pora to zrobić, na razie dla testów będzie to banalna figura
geometryczna. Zgadnijcie jaka. Oczywiście jak napisałem wyżej, polega to na nadpisaniu metody paint( ), którą umieścimy
zaraz pod konstruktorem WojnaSwiatow( ):
public void
paint
(Graphics g){
g.
setColor
(Color.red);
g.
fillOval
( SZEROKOSC/2-10, WYSOKOSC/2-10,20,20);
}
Uruchamiamy i jeśli wszystko dobrze zrobiliśmy, widzimy jak się spodziewaliśmy czerwone kółeczko.
Wstawiamy rysunki
No można bawić się w rysowanie, ale rysowanie złożonych grafik w Javie jest nie tylko czasochłonne, ale i nieefektywne.
Dlatego też wczytywać będziemy gotowe obrazki, stworzone choćby w Gimpie. Obrazek musi znajdować się w pewnym
miejscu na dysku, do którego ma dostęp program. Miejscem wyjścia, gdy pracujemy w NetBeans, jest folderg src. Gdy
korzystasz z innego środowiska musisz sam sprawdzić gdzie umieszczasz rysunki i jak do nich dotrzeć. Zakładamy, że w
folderze src mamy folder img z naszymi obrazkami.
Wstawmy najpierw jednego stworka:
Rys 1
: Oto nasz straszny stworek.
Zaczynam od zaimportowania nowych bibliotek:
import
java.awt.image.BufferedImage;
import
java.net.URL;
import
javax.imageio.ImageIO;
Teraz napiszemy metodę do wczytywania obrazków:
public
BufferedImage
loadImage
(String sciezka) {
URL url=null;
try
{
url =
getClass
().
getClassLoader
().
getResource
(sciezka);
return
ImageIO.
read
(url);
}
catch
(Exception e) {
System.out.
println
(
"Przy otwieraniu "
+ sciezka
+" jako "
+ url);
System.out.
println
(
"Wystapil blad : "
+e.getClass().getName()+"
"+e.getMessage());
System.
exit
(0);
return
null;
}
}
Jak widać w razie błędu przerwanie programu. To ważne, bo będziemy wiedzieć jaki plik źle zlokalizowaliśmy. Co
ważniejsze jak widzimy, ścieżka jest ścieżką względna, co umożliwia nam pobranie obrazka skądkolwiek, niezależnie
gdzie leży obecnie nasza gra (a może w przyszłości aplet).
Pozostaje zmienienie metody paint( ):
public void
paint
(Graphics g){
BufferedImage potworek =
loadImage
("img/potworek.gif");
g.
drawImage
(potworek, 40, 40,
this
);
}
Jako rezultat widzimy:
Rys 2
. Straszny potwor zamknięty w naszym okienku.
Jeśli jednak dokładnie przyjrzymy się kodowi, widzimy że ponieważ metoda paint( ) jest wywoływana przy każdym
przerysowania okna, za każdym razem będzie on ładowany od początku. Niezbyt efektywne. Można pomyśleć żeby
ładować to od razu w konstruktorze raz a dobrze. No ale pomyślmy, że tworzymy większy projekt, powiedzmy aplet z
setkami stworków, tekstur i innych obrazków. Za każdym razem, ktoś kto ściąga nasz aplet musi czekać Az to wszystko się
załaduje, a możliwe ze nawet niektórych z nich nigdy nie obejrzy, bo występuje powiedzmy w 50 level’u, gdy on skończy
grę przy 10… Zastosujmy sztuczkę nazywaną
deferred loading
. Dodajemy do atrybutów WojnaSwiatow naszego
potworka.
public class
WojnaSwiatow
extends
Canvas{
public static final int
SZEROKOSC = 800;
public static final
int WYSOKOSC = 600;
public
BufferedImage potworek =
null
;
…
Teraz zmieniamy metodę rysowania:
public void
paint(Graphics g){
if
(potworek==
null
)
potworek =
loadImage
("img/potworek.gif");
g.
drawImage
(potworek, 40, 40,
this
);
}
Skuteczne aczkolwiek nieeleganckie i kłopotliwe gdy będą setki grafik. Java oferuje jednak metode nazywana getSprite( ).
S
prite
(z ang., dosłownie
duszek
) to dwuwymiarowy obrazek używany w systemach grafiki dwuwymiarowej i 2.5-
wymiarowej, który po przesunięciu i ewentualnie przeskalowaniu jest przenoszony na ekran. Sprite'y pozwalają na bardzo
łatwe uzyskiwanie na ekranie niezbyt wyszukanych obiektów animowanych. Wiele układów graficznych 2D jest
wyposażonych w zdolność do automatycznego generowania i animacji sprite'ów. Namiastkę trzeciego wymiaru można
uzyskać przez skalowanie sprite'ów oraz ich wyświetlanie w kolejności od dalszych do bliższych (w ten sposób bliższe
częściowo zakrywają dalsze). W systemach grafiki 3D zamiast sprite'ów używa się raczej modeli opartych na wielokątach.
Więc obrazki będziemy ładować jako duszki. Ponieważ może ich być setki dobrze będzie trzymać je w Hashmapie jako
pary (nazwa-sprite’a,zaladowany-plik). Na początku importujemy.
import
java.util.HashMap;
Zmieniamy więc też atrybuty bo nasz stary potworek jest już nam niepotrzebny, natomiast warto zadeklarować
HashMap’e.
public static final int
SZEROKOSC = 800;
public static final int
WYSOKOSC = 600;
public
HashMap sprites;
Na wstępie konstruktora dopisujemy:
public
WojnaSwiatow
() {
sprites =
new
HashMap();
...
Tworzymy nową metodę pod metodą
loadImage
( ):
public
BufferedImage
getSprite
(String sciezka) {
BufferedImage img = (BufferedImage)sprites.
get
(sciezka);
if (img ==
null
) {
img =
loadImage
("img/"+sciezka);
sprites.
put
(sciezka,img);
}
return
img;
}
Ostatnim akordem zoptymalizowanego wczytywania jest zmiana metody print( ):
public void
paint
(Graphics g){
g.
drawImage
(
getSprite
("potworek.gif"), 40, 40,
this
);
}
Animacja
Należy uświadomić sobie, że każda gra dzieje się wg następującego scenariusza:
1. Odświeżenie stanu świata
– tutaj odbywają się ruchy potworów, akcje gracza,
2. Odświeżenie ekranu –
tutaj odbywa się przeniesienie pierwszego na ekran
3. GOTO 1.
Nic ciężkiego, więc zaimplementujmy to. Główna pętla będzie odbywać się w metodzie gra( ). Odświeżanie swiata
natomiast, to metoda OdswiezSwiat( ). Oczywiście na tym etapie, odświeżanie to nie jest skomplikowane. Będzie to
losowe umieszczanie potworka na planszy. Potrzeba nam do tego dwóch nowych atrybutow, pozX i pozY.
public class
WojnaSwiatow
extends
Canvas{
public static final int
SZEROKOSC = 800;
public static final int
WYSOKOSC = 600;
public HashMap
sprites;
public int
pozX,pozY;
public
WojnaSwiatow
() {
pozX=SZEROKOSC/2;
pozY=WYSOKOSC/2;
no i zaraz nad
main
( ) dodajemy:
public void
paint
(Graphics g){
g.
drawImage
(
getSprite
("potworek.gif"), pozX, pozY,this);
}
public void
updateWorld
() {
pozX = (
int
)(Math.
random
()*SZEROKOSC);
pozY = (
int
)(Math.
random
()*WYSOKOSC);
}
public void
game
() {
while
(
isVisible
()) {
updateWorld
();
paint
(
getGraphics
());
}
}
public static void
main(String[] args) {
WojnaSwiatow inv =
new
WojnaSwiatow
();
inv.
game
();
}
}
Po zobaczeniu rezultatu:
Rys 3
. Atak!?
Widać, że nie o to nam chodziło. Odpowiedź na pytanie dlaczego tak się dzieje jest dość oczywiste. Wywołujemy ręcznie
metodę paint ( ). Nie przerysowuje ona okna, tylko nanosi przecież na już przerysowane. Skoro tak, to po prostu nanosi to
co ma być zawarte prócz tła, a skoro te nie było przerysowane, to po prostu nanosi na to co było, musimy więc ręcznie
czyścić okno.
Na razie wystarczy, czyścić okno przed narysowaniem potwora. Wystarczy podmienić metodę paint( ):
[ Pobierz całość w formacie PDF ]