You are on page 1of 41

Nauka programowania gier komputerowych w Javie.

Autor: Piotr Modzelewski


E-mail: keyer@mat.uni.torun.pl

Wstp
Celem tego referatu jest napisanie prostej gry w Javie, jednake nie trywialnej. W dziele tworzenia uywa bdziemy
dogodnoci programowania obiektowego. Tworzenie gry nie bdzie si odbywa od razu. Zaczniemy od maego projektu,
ktry bdzie si rozrasta. Kod bdziemy poprawia, ulepsza a nawet usuwa, aby zobaczy jak przy rnych jego
wersjach dziaa bdzie nasza aplikacja.
Celem tego kursu jest oprcz stworzenia gry:
Ulepszanie nauki Javy
Zdobycie podstaw na temat Swingu i AWT
Zrozumienie obsugi zdarze oraz komponentw
Nauka Javy2D
Nauka HashMapy i ArrayList
Sztuczek programistycznych

Pierwsze okno
Pierwszym krokiem tworzenia naszej aplikacji jest stworzenia okna. Wykorzystamy Bibliotek Swing (dokadnie JFrame).
Zakadamy, 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 bdzie nawet koczy programu w momencie jego zamknicia. Tak by nie moe. Oczywicie
naprawienie tego nie bdzie kopotem. Wystarczy konstruktor WojnaSwiatow( ) zmodyfikowa dodajc na jego kocu:
okno.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
Oraz dodatkowo zaimportowa obsug zdarze:
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

Zabawa w rysowanie
Podstaw w grze jest oczywicie grafika, a co za tym idzie, umiejtno rysowania. Kady 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 sprbujmy sprawdzi czy to wystarczy. Najprociej bdzie, jeeli co narysujem. Bez zbdnego gadania,
zrbmy to. Na pocztku 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 ktrym bdziemy rysowa, pora to zrobi, na razie dla testw bdzie to banalna figura
geometryczna. Zgadnijcie jaka. Oczywicie jak napisaem wyej, polega to na nadpisaniu metody paint( ), ktr umiecimy
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 jeli wszystko dobrze zrobilimy, widzimy jak si spodziewalimy czerwone keczko.

Wstawiamy rysunki
No mona bawi si w rysowanie, ale rysowanie zoonych grafik w Javie jest nie tylko czasochonne, ale i nieefektywne.
Dlatego te wczytywa bdziemy gotowe obrazki, stworzone choby w Gimpie. Obrazek musi znajdowa si w pewnym
miejscu na dysku, do ktrego ma dostp program. Miejscem wyjcia, gdy pracujemy w NetBeans, jest folderg src. Gdy
korzystasz z innego rodowiska musisz sam sprawdzi gdzie umieszczasz rysunki i jak do nich dotrze. Zakadamy, 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 obrazkw:
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 bdu przerwanie programu. To wane, bo bdziemy wiedzie jaki plik le zlokalizowalimy. Co
waniejsze jak widzimy, cieka jest ciek wzgldna, co umoliwia nam pobranie obrazka skdkolwiek, niezalenie
gdzie ley obecnie nasza gra (a moe w przyszoci 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 zamknity w naszym okienku.


Jeli jednak dokadnie przyjrzymy si kodowi, widzimy e poniewa metoda paint( ) jest wywoywana przy kadym
przerysowania okna, za kadym razem bdzie on adowany od pocztku. Niezbyt efektywne. Mona pomyle eby
adowa to od razu w konstruktorze raz a dobrze. No ale pomylmy, e tworzymy wikszy projekt, powiedzmy aplet z
setkami stworkw, tekstur i innych obrazkw. Za kadym razem, kto kto ciga nasz aplet musi czeka Az to wszystko si
zaaduje, a moliwe ze nawet niektrych z nich nigdy nie obejrzy, bo wystpuje powiedzmy w 50 levelu, gdy on skoczy
gr przy 10 Zastosujmy sztuczk nazywan deferred loading. Dodajemy do atrybutw 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 kopotliwe gdy bd setki grafik. Java oferuje jednak metode nazywana getSprite( ).
Sprite (z ang., dosownie duszek) to dwuwymiarowy obrazek uywany w systemach grafiki dwuwymiarowej i 2.5wymiarowej, ktry po przesuniciu i ewentualnie przeskalowaniu jest przenoszony na ekran. Sprite'y pozwalaj na bardzo
atwe uzyskiwanie na ekranie niezbyt wyszukanych obiektw animowanych. Wiele ukadw graficznych 2D jest
wyposaonych w zdolno do automatycznego generowania i animacji sprite'w. Namiastk trzeciego wymiaru mona
uzyska przez skalowanie sprite'w oraz ich wywietlanie w kolejnoci od dalszych do bliszych (w ten sposb blisze
czciowo zakrywaj dalsze). W systemach grafiki 3D zamiast sprite'w uywa si raczej modeli opartych na wieloktach.
Wic obrazki bdziemy adowa jako duszki. Poniewa moe ich by setki dobrze bdzie trzyma je w Hashmapie jako
pary (nazwa-spritea,zaladowany-plik). Na pocztku importujemy.
import java.util.HashMap;
Zmieniamy wic te atrybuty bo nasz stary potworek jest ju nam niepotrzebny, natomiast warto zadeklarowa
HashMape.
public static final int SZEROKOSC = 800;
public static final int WYSOKOSC = 600;
public HashMap sprites;
Na wstpie 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
Naley uwiadomi sobie, e kada gra dzieje si wg nastpujcego scenariusza:
1. Odwieenie stanu wiata tutaj odbywaj si ruchy potworw, akcje gracza,
2. Odwieenie ekranu tutaj odbywa si przeniesienie pierwszego na ekran
3. GOTO 1.
Nic cikiego, wic zaimplementujmy to. Gwna ptla bdzie odbywa si w metodzie gra( ). Odwieanie swiata
natomiast, to metoda OdswiezSwiat( ). Oczywicie na tym etapie, odwieanie to nie jest skomplikowane. Bdzie to
losowe umieszczanie potworka na planszy. Potrzeba nam do tego dwch nowych atrybutow, pozX i pozY.
public class
public
public
public
public

WojnaSwiatow extends Canvas{


static final int SZEROKOSC = 800;
static final int WYSOKOSC = 600;
HashMap sprites;
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 chodzio. Odpowied na pytanie dlaczego tak si dzieje jest do oczywiste. Wywoujemy rcznie
metod paint ( ). Nie przerysowuje ona okna, tylko nanosi przecie na ju przerysowane. Skoro tak, to po prostu nanosi to
co ma by zawarte prcz ta, a skoro te nie byo przerysowane, to po prostu nanosi na to co byo, musimy wic rcznie
czyci okno.
Na razie wystarczy, czyci okno przed narysowaniem potwora. Wystarczy podmieni metod paint( ):

public void paint(Graphics g){


g.setColor(getBackground());
g.fillRect(0,0,getWidth(),getHeight());
g.drawImage(getSprite("potworek.gif"), pozX, pozY,this);
}
dodatkowo pozbawimy uytkownika zmniejszania wielkoci okna dopisujc na kocu konstruktora:
okno.setResizable(false);
Gdy uruchomimy projekt okae si, e znw jest nie tak. Potworek rusza si tak szybko, e trudno go zauway.
Nieuniknione jest to, aby zapewni mu pewne opnienie. Zmienia si wic troche nasz wczeniejszy scenariusz:
1. Odwieenie stanu wiata tutaj odbywaj si ruchy potworw, akcje gracza,
2. Odwieenie ekranu tutaj odbywa si przeniesienie pierwszego na ekran
3. Czekaj troch.
4. GOTO 1.
Opnienie bdzie kolejnym atrybutem:
public class WojnaSwiatow
public static final int
public static final int
public static final int
public HashMap sprites;
public int pozX,pozY;

extends Canvas{
SZEROKOSC = 800;
WYSOKOSC = 600;
SZYBKOSC = 60;

...
Uyjemy go w gwnej ptli gry:
public void game() {
while (isVisible()) {
updateWorld();
paint(getGraphics());
try {
Thread.sleep(SZYBKOSC);
} catch (InterruptedException e) {}
}
}
No to co ju wida. OK. Pora na to by nasz potworek rusza si w bardziej rozsdny sposb dokadniej, horyzontalnie
odbijajc si od brzegw ekranu. W tym celu wprowadzimy szybko do atrybutw.
public int pozX,pozY,vX;
Nada mu odpowiedni warto w konstruktorze:
public WojnaSwiatow() {
pozX=SZEROKOSC/2;
pozY=WYSOKOSC/2;
vX=2;
...
No i wpiszemy odpowiedni ruch:
public void updateWorld() {
pozX += vX;
if (pozX < 0 || pozX > SZEROKOSC) vX = -vX;
}
Po uruchomieniu, przyjemno animacji zakci nam jeden aspekt. Paskudne migotanie. Dlaczego tak si dzieje? Dlatego
e nasze oko widzi ekran na chwil przed narysowaniem aktora, dlatego animacja nie jest spjna. Rozwizaniem tego

problemu jest tzw. podwjne buforowanie. Caa sztuczka, to przechowywanie w pamici obrazu o tych samych rozmiarach
co okno, nanoszenie tam zmian i dopiero potem rysowanie caoci na ekran. Brzmi troch strasznie, ale jest na tyle
popularne, e JDK od wersji 1.4 ma wbudowane instrumenty do realizacji tego pomysu. Klasa odpowiadajca za to
wszystko to BufferStrategy. Urzywanie jej jest bardzo proste:
1. Na pocztku musimy zdecydowa, ktre okno lub komponent bdziemy buforowa. Tutaj trzeba wybra
oczywicie ten, na ktrym bdziemy rysowa, w tym przypadku oczywicie klas WojnaSwiatow
2. wywoujemy metod createBufferStrategy(n), gdzie przekazujemy ilo buforw majcych by utworzonych. Nie
wszystkie jednak systemy pozwalaj na stworzenie wicej ni 2 buforw
3. Uywamy metody getBufferStrategy(), aby uzyska instancj BudderStrategy
4. Aby malowa na obrazku poza ekranem, uywamy metody getDrawGraphics()
5. Aby odsloni ukryty bufor uywamy metody show();
Na wstepie zaimportowa musimy:
import java.awt.image.BufferStrategy;
Dodajemy atrybut klasy:
public BufferStrategy strategia;
Na kocu kostruktora dopisujemy:
createBufferStrategy(2);
strategia = getBufferStrategy();
requestFocus();
podmieniamy metod paint() na:
public void paintWorld() {
Graphics g = strategia.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,getWidth(),getHeight());
g.drawImage(getSprite("potworek.gif"), pozX, pozY,this);
strategia.show();
}
No i oczywicie zmieniamy paint na paintWorld() w game().

FPS
Tworzc szat graficzn gry powinno si jak najszybciej stworzy licznik FPS (ang. Frame per second klatki na
sekund). Dlaczego jest to takie wane? Czsto tworzc gry chce si dokada coraz nowe i nowe rzeczy. Jednake
wszystko, czy to rysowanie, czy to liczenie wg skomplikowanego algorytmu ruchw przeciwnikw, potrzebuje czasu.
Natomiast animacja, aby zachowaa pynno musi mie odpowiedni ilo klatek na sekund. Oczywicie nie ma rnicy
midzy 1700 FPS a 120 FPS. Dlaczego? Dlatego, e monitor tak czsto ich nie odwiey. Obecnie, monitory pracuj z
czstotliwocia 100 Hz, co oznacza, e wiksza liczba klatek bdzie niezauwaalna. Licznik FPS jest wanym doradc.
Widzc przy konturowym nakreleniu stworw, ze spada do 70 klatek, wiemy, e naoenie tekstur spowoduje
spowolnienie poniej pynnoci.
Obliczenia wymaga bd nowego atrybutu:
public class WojnaSwiatow extends Canvas{
public static final int SZEROKOSC = 800;
public static final int WYSOKOSC = 600;
public static final int SZYBKOSC = 10;
public long usedTime;
public HashMap sprites;
public int pozX,pozY,vX;
public BufferStrategy strategia;
...
Licznik trzeba rwnie wywietli na ekranie:
public void paintWorld() {

Graphics g = strategia.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,getWidth(),getHeight());
g.drawImage(getSprite("potworek.gif"), pozX, pozY,this);
g.setColor(Color.white);
if (usedTime > 0)
g.drawString(String.valueOf(1000/usedTime)+" fps",5,WYSOKOSC-50);
else g.drawString("--- fps",5,WYSOKOSC-50);
strategia.show();
}
Wreszcie pora obliczy ten czekany FPS:
public void game() {
usedTime=1000;
while (isVisible()) {
long startTime = System.currentTimeMillis();
updateWorld();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(SZYBKOSC);
} catch (InterruptedException e) {}
}
}

Refactoring kodu
W tym momencie program niebezpiecznie zaczyna przybiera na wadze, i dodatkowo, staje si nieczytelny. Trzeba co z
tym zrobi. atwo domyle si jak to zrobimy. Wyodrbnimy samodzielne obiekty, ktre skadaj si na nasz kod i
podzielimy je na samodzielne pliki. Rzecz, ktra najbardziej rzuca si w oczy s Spritey. Stworzmy wic dla nich
handlera, zauwaajc, e jedyna metoda, ktra jest potrzebna publicznie, to getSprite( ).

SpriteCache.java
import
import
import
import

java.awt.image.BufferedImage;
java.net.URL;
java.util.HashMap;
javax.imageio.ImageIO;

public class SpriteCache {


public HashMap sprites;
public SpriteCache() {
sprites = new HashMap();
}
private 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;
}
}
public BufferedImage getSprite(String sciezka) {
BufferedImage img = (BufferedImage)sprites.get(sciezka);

if (img == null) {
img = loadImage("img/"+sciezka);
sprites.put(sciezka,img);
}
return img;
}
}
Nastpnie od razu nasuwa mi si pojcie sceny. Scena koordynuje wszystkim co dzieje si w grze ile jest potworw, ile
strzaw, ktry jest obecny level itd. Wan jej cech jest wyczno na kontakt z SpriteCache.

Stage.java
import java.awt.image.ImageObserver;
public interface Stage extends ImageObserver {
public static final int SZEROKOSC = 800;
public static final int WYSOKOSC = 600;
public static final int SZYBKOSC = 10;
public SpriteCache getSpriteCache();
}
Zajmijmy si stworzeniami. Oczywistym jest, e w zamierzamy mie wicej ni jednego stwora. Wszystkie bd podobne,
rni ich bdzie jedynie pozycja i szybko. Sprbujmy pomyle co wsplnego maja potwory, w celu zaprojektowania
ich klasy.
Oczywicie stworki maj pozycj jak powiedzielimy
Maj grafik, ktra jest pokazywana na ekranie
Maj rozmiar, ktry moe by specyficzny dla pojedynczego
Musz te cos czyni czy to walczy czy te porusza si po prostu. Klasa musi mie wic metod, ktra bdzie
im to umoliwia
Ale jeli pomylimy troch bardziej, atwo dojdziemy do wniosku, e te rzeczy s charakterystyczne nie tylko dla
potworkw, ale dla wszystkich poruszajcych si rzeczy na ekranie (kule, gracz, jakie spadajce bonusy). Wic mona
stworzy jedn klas, z ktrej inne bd dziedziczy. Zwyko si j nazywa aktorem.

Actor.java
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
public class Actor {
protected int x,y;
protected int width, height;
protected String spriteName;
protected Stage stage;
protected SpriteCache spriteCache;
public Actor(Stage stage) {
this.stage = stage;
spriteCache = stage.getSpriteCache();
}
public void paint(Graphics2D g){
g.drawImage( spriteCache.getSprite(spriteName), x,y, stage );
}
public int getX() { return x; }
public void setX(int i) { x = i; }
public int getY() { return y; }
public void setY(int i) { y = i; }
public String getSpriteName() { return spriteName; }

public void setSpriteName(String string) {


spriteName = string;
BufferedImage image = spriteCache.getSprite(spriteName);
height = image.getHeight();
width = image.getWidth();
}
public
public
public
public

int getHeight() { return height; }


int getWidth() { return width; }
void setHeight(int i) {height = i; }
void setWidth(int i) { width = i; }

public void act() { }


}
Teraz nie pozostaje nam napisa nic innego jak waciwego potwora.

Monster.java

public class Monster extends Actor {


protected int vx;
public Monster(Stage stage) {
super(stage);
setSpriteName("potworek.gif");
}
public void act() {
x+=vx;
if (x < 0 || x > Stage.SZEROKOSC)
vx = -vx;
}
public int getVx() { return vx; }
public void setVx(int i) {vx = i; }
}

No to pozostaje nam je rozmnoy w programie gwnym.

WojnaSwiatow.java
import
import
import
import
import
import
import
import
import
import
import

java.awt.Canvas;
javax.swing.JFrame;
javax.swing.JPanel;
java.awt.Color;
java.awt.Dimension;
java.awt.Graphics;
java.awt.event.WindowAdapter;
java.awt.event.WindowEvent;
java.awt.Graphics2D;
java.awt.image.BufferStrategy;
java.util.ArrayList;

public class WojnaSwiatow extends Canvas implements Stage{


public long usedTime;
public BufferStrategy strategia;
private SpriteCache spriteCache;
private ArrayList actors;
public WojnaSwiatow() {

spriteCache = new SpriteCache();


JFrame okno = new JFrame(".: Wojna Swiatow :.");
JPanel panel = (JPanel)okno.getContentPane();
setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
panel.setPreferredSize(new Dimension(Stage.SZEROKOSC,Stage.WYSOKOSC));
panel.setLayout(null);
panel.add(this);
okno.setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
okno.setVisible(true);
okno.addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
okno.setResizable(false);
createBufferStrategy(2);
strategia = getBufferStrategy();
requestFocus();
}
public void initWorld() {
actors = new ArrayList();
for (int i = 0; i < 10; i++){
Monster m = new Monster(this);
m.setX( (int)(Math.random()*Stage.SZEROKOSC) );
m.setY( i*20 );
m.setVx( (int)(Math.random()*3)+1 );
actors.add(m);
}
}
public void paintWorld() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,getWidth(),getHeight());
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.paint(g);
}
g.setColor(Color.white);
if (usedTime > 0)
g.drawString(String.valueOf(1000/usedTime)+" fps",0,Stage.WYSOKOSC-50);
else
g.drawString("--- fps",0,Stage.WYSOKOSC-50);
strategia.show();
}
public void updateWorld() {
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.act();
}
}
public SpriteCache getSpriteCache() {
return spriteCache;
}
public void game() {
usedTime=1000;
initWorld();

while (isVisible()) {
long startTime = System.currentTimeMillis();
updateWorld();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(Stage.SZYBKOSC);
} catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
WojnaSwiatow inv = new WojnaSwiatow();
inv.game();
}
}
Wida, e grasuje nam grupka potworkw:

Rys 4. Rodzinka.

Animacja
Niezalenie od gatunku gry obiekty 2D podczas poruszania powinny podlega jakiej animacji, ktra niemale zawsze
zaley od klatek. W celu wywoania iluzji ruchu aktor cykluje pomidzy sekwencj obrazkw.
Dla uproszczenia w tym przypadku nasz potworek bdzie mia tylko 2 klatki animacji.

Rys 5. Dwie twarze tego samego za


Nazwijmy je potworek0.gif i potworek1.gif.
Zmiemy wic odpowiednio klasy:
W Actor.java dodajemy atrybuty:

protected int currentFrame;


protected String[] spriteNames;
zmieniamy te konstruktor:
public Actor(Stage stage) {
this.stage = stage;
spriteCache = stage.getSpriteCache();
currentFrame = 0;
}
setSpriteName() zastpujemy:
public void setSpriteNames(String[] names) {
spriteNames = names;
height = 0;
width = 0;
for (int i = 0; i < names.length; i++ ) {
BufferedImage image = spriteCache.getSprite(spriteNames[i]);
height = Math.max(height,image.getHeight());
width = Math.max(width,image.getWidth());
}
}
Poprawiamy te metod act () .
public void act() {
currentFrame = (currentFrame + 1) % spriteNames.length;
}
Pozostaje ju tylko nanie drobne zmiany w:

Monster.java
public class Monster extends Actor {
protected int vx;
public Monster(Stage stage) {
super(stage);
setSpriteNames( new String[] {"potworek0.gif","potworek1.gif"});
}
public void act() {
super.act();
x+=vx;
if (x < 0 || x > Stage.SZEROKOSC)
vx = -vx;
}
public int getVx() { return vx; }
public void setVx(int i) {vx = i; }
}
No co tam miga. Ale za szybko, eby zauway, teoretycznie mona byoby w stringu mie 12x potworek0.gif i 12x
potworek1.gif tworzc tak jakby 24 klatki. Jednake jest to brzydkie, niepotrzebnie marnuje pami itd... itd Nie lepiej
po prostu nie zmienia obrazka zawsze tylko po kilku obejciach? Ustalmy wic dwa nowe atrybuty w Actor.java:
protected int frameSpeed;
protected int t;

frameSpeed to po prostu szybko zmieniania si klatek. Natomiast t to zmienna pomocnicza. Zainicjujmy je w


konstruktorze:
public Actor(Stage stage) {
this.stage = stage;
spriteCache = stage.getSpriteCache();
currentFrame = 0;
frameSpeed = 1;
t=0;
}
Dodajmy metody do kontroli frameSpeed:
public int getFrameSpeed() {return frameSpeed; }
public void setFrameSpeed(int i) {frameSpeed = i; }
No i wreszcie zmiemy metod act() na troch inteligentniejsz.
public void act() {
t++;
if (t % frameSpeed == 0){
t=0;
currentFrame = (currentFrame + 1) % spriteNames.length;
}
}
W Monster.java na koniec konstruktora dajemy
setFrameSpeed(25);
i uruchomiamy. W zalenoci od woli dostosowujemy szybko przerzucania klatek.

Gracz
No czas zamieni ten projekt w co co nie jest czyst animacj i swawol potworw. Pora wprowadzi kogo kto by troch
te potwory pogoni. Rol t ma peni gracz. Musimy mu zapewni:
Ruszanie si we wszystkich 8 kierunkach
Gracz porusza si tylko gdy klawisz jest wcinity
Widzimy, e gracz jest podobny do potwora, z wyjtkiem tego, e kontroluje go gracz i porusza si we wszystkich
kierunkach.

Rys 6. Statek naszego gracza.


Tworzymy wic plik dla gracza:

Player.java

public class Player extends Actor {


protected int vx;
protected int vy;
public Player(Stage stage) {
super(stage);
setSpriteNames( new String[] {"nave.gif"});
}
public void act() {
super.act();
x+=vx;
y+=vy;

if (x < 0 || x > Stage.SZEROKOSC)


vx = -vx;
if (y < 0 || y > Stage.WYSOKOSC)
vy = -vy;
}
public
public
public
public

int getVx() { return vx; }


void setVx(int i) {vx = i; }
int getVy() { return vy; }
void setVy(int i) {vy = i; }

}
Naley jeszcze zmieni glwn klas WojnaSwiatow.java. Co prawda gracz jest aktorem jak kady inny, z oczywistych
wzgldw bdziemy go traktowa troszeczk bardziej indywidualnie.

WojnaSwiatow.java
import
import
import
import
import
import
import
import
import
import
import

java.awt.Canvas;
javax.swing.JFrame;
javax.swing.JPanel;
java.awt.Color;
java.awt.Dimension;
java.awt.Graphics;
java.awt.event.WindowAdapter;
java.awt.event.WindowEvent;
java.awt.Graphics2D;
java.awt.image.BufferStrategy;
java.util.ArrayList;

public class WojnaSwiatow extends Canvas implements Stage{


public long usedTime;
public BufferStrategy strategia;
private SpriteCache spriteCache;
private ArrayList actors;
private Player player;
public WojnaSwiatow() {
spriteCache = new SpriteCache();
JFrame okno = new JFrame(".: Wojna Swiatow :.");
JPanel panel = (JPanel)okno.getContentPane();
setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
panel.setPreferredSize(new Dimension(Stage.SZEROKOSC,Stage.WYSOKOSC));
panel.setLayout(null);
panel.add(this);
okno.setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
okno.setVisible(true);
okno.addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
okno.setResizable(false);
createBufferStrategy(2);
strategia = getBufferStrategy();
requestFocus();
}
public void initWorld() {
actors = new ArrayList();
for (int i = 0; i < 10; i++){

Monster m = new Monster(this);


m.setX( (int)(Math.random()*Stage.SZEROKOSC) );
m.setY( i*20 );
m.setVx( (int)(Math.random()*3)+1 );
actors.add(m);
}
player = new Player(this);
player.setX(Stage.SZEROKOSC/2);
player.setY(Stage.WYSOKOSC - 2*player.getHeight());
player.setVx(5);
}
public void paintWorld() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,getWidth(),getHeight());
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.paint(g);
}
player.paint(g);
g.setColor(Color.white);
if (usedTime > 0)
g.drawString(String.valueOf(1000/usedTime)+" fps",0,Stage.WYSOKOSC-50);
else
g.drawString("--- fps",0,Stage.WYSOKOSC-50);
strategia.show();
}
public void updateWorld() {
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.act();
}
player.act();
}
public SpriteCache getSpriteCache() {
return spriteCache;
}
public void game() {
usedTime=1000;
initWorld();
while (isVisible()) {
long startTime = System.currentTimeMillis();
updateWorld();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
WojnaSwiatow inv = new WojnaSwiatow();
inv.game();
}
}

Rys 7. Do gry wchodzi gracz


No wspaniale, przydaoby nada graczowi w kocu kontrol nad statkiem. Uywa bdziemy do tego klawiszy kursorw,
w sposb trywialny, nacinicie klawisza lewo spowoduje poruszanie si w lewo. Jeli naciniemy klawisze lewo i
gra to bdzie si porusza w obu tych kierunkach. Co prawda mona by zaatwi interpretacj naciskania klawiszy w
gwnym pliku jest to do nieeleganckie i nawet kopotliwe przy rozbudowie projektu. Scenariusz obsugi zdarzenia
nacinicia klawisza bdzie nastpujcy:
1. WojnaSwiatow otrzymuje zdarzenia klawiatury
2. Sprawdza czy to nie klawisze specjalne dopowiadajce np. za Pauze, Restart, Wyjscie, coprawda nie mamy ich
zaimplementowanych, ale moe kiedy. Dopiero po tym oddaje to klasie gracza
3. Klasa gracza radzi sobie z interpretacja
Nic wic prostszego, jak to zrobi po prostu. W pliku WojnaSwiatow.java klas utworzymy suchaczem przyciskw:
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
public class WojnaSwiatow extends Canvas implements Stage, KeyListener{
na kocu kontruktora, musimy go zainicjowa:
public WojnaSwiatow() {
spriteCache = new SpriteCache();
JFrame okno = new JFrame(".: Wojna Swiatow :.");
JPanel panel = (JPanel)okno.getContentPane();
setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
panel.setPreferredSize(new Dimension(Stage.SZEROKOSC,Stage.WYSOKOSC));
panel.setLayout(null);
panel.add(this);
okno.setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
okno.setVisible(true);
okno.addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
okno.setResizable(false);
createBufferStrategy(2);
strategia = getBufferStrategy();
requestFocus();
addKeyListener(this);
}

W kocu dodajemy metody:


public void keyPressed(KeyEvent e) {
player.keyPressed(e);
}
public void keyReleased(KeyEvent e) {
player.keyReleased(e);
}
public void keyTyped(KeyEvent e) {}
I usuwamy linijk z initWorld()
player.setVx(5);
bo nie chcemy ju bazowej prdkoci. Najwicej zmian nastpi w Player.java oczywicie:

Player.java

import java.awt.event.KeyEvent;
public class Player extends Actor {
protected static final int PLAYER_SPEED = 4;
protected int vx;
protected int vy;
private boolean up,down,left,right;
public Player(Stage stage) {
super(stage);
setSpriteNames( new String[] {"nave.gif"});
}
public void act() {
super.act();
x+=vx;
y+=vy;
if (x < 0 || x > Stage.SZEROKOSC)
vx = -vx;
if (y < 0 || y > Stage.WYSOKOSC)
vy = -vy;
}
public
public
public
public

int getVx() { return vx; }


void setVx(int i) {vx = i; }
int getVy() { return vy; }
void setVy(int i) {vy = i; }

protected void updateSpeed() {


vx=0;vy=0;
if (down) vy = PLAYER_SPEED;
if (up) vy = -PLAYER_SPEED;
if (left) vx = -PLAYER_SPEED;
if (right) vx = PLAYER_SPEED;
}
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN : down = false; break;
case KeyEvent.VK_UP : up = false; break;
case KeyEvent.VK_LEFT : left = false; break;
case KeyEvent.VK_RIGHT : right = false; break;
}
updateSpeed();
}

public void keyPressed(KeyEvent e) {


switch (e.getKeyCode()) {
case KeyEvent.VK_UP : up = true; break;
case KeyEvent.VK_LEFT : left = true; break;
case KeyEvent.VK_RIGHT : right = true; break;
case KeyEvent.VK_DOWN : down = true;break;
}
updateSpeed();
}
}
Zauwamy, e staa PLAYER_SPEED mwi jasno, e kursory nie zmieniaj prdkoci, a jedynie kierunek statku.

Strzelanie
Pora wprowadzi do gry troch przemocy. Statek naszego gracza bdzie w stanie strzela promieniami lasera. Na razie
zajmiemy si jedynie ich lotem, tworzeniem i usuwaniem. Niszczeniem potworw zajmiemy si potem. Jasno wic
moemy stwierdzi co na razie musi robi nasza kula:
1. Wystrzelone pojawiaj si tu nad statkiem
2. Poruszaj si cigle naprzd ze staa prdkoci
3. Jest limit kul na ekranie
Jak wida, z punktu widzenia gry kule to po prostu aktorzy. Now rzecz jest to, e pojawiaj si i znikaj. Dodatkowo nie
zale od gwnej ptli, gdy nie ma ona wpywu na to, kiedy gracz wystrzeli. Jakby tego byo mao to klasa Player a nie
WojnySwiatow interpretuje naciniecie, a to przecie ona trzyma list aktorw Jak temu zaradzi? Z pomoc przychodzi
Stage:

Stage.java
import java.awt.image.ImageObserver;
public interface Stage extends ImageObserver {
public static final int SZEROKOSC = 800;
public static final int WYSOKOSC = 600;
public static final int SZYBKOSC = 20;
public SpriteCache getSpriteCache();
public void addActor(Actor a);
}
Zyskaa ona umiejtno tworzenia aktora. Usuwanie jednak nie jest ju tak atwe. Gdyby Stage moga kasowa wystpi
by problem adekwatny do Race Condition. Wyobramy sobie, e Stage chce usun obiekt a w tym samym momencie
gwna ptla chce na nim pracowa katastrofa. Mona co prawda zakada jaki semafor czy inne zabezpieczenia na
list, ale to dodatkowy ciar, a my dymy do jak najwikszej efektywnoci gry. Lepszym rozwizaniem jest dodatnie
dodatkowego pola dla aktora, ktry jest zgoszeniem aktora do usunicia z listy, i normalna ptla gry jak takowy napotka,
to wtedy usunie. Zmodyfikujmy wic Actor.java dodajc dodatkowe pole oraz metody:
protected boolean markedForRemoval;
public void remove() {
markedForRemoval = true;
}
public boolean isMarkedForRemoval() {
return markedForRemoval;
}

Teraz wystarczy doda dodatkow metod i zmodyfikowa ptl w WojnaSwiatow.java:

public void addActor(Actor a) {


actors.add(a);
}
public void updateWorld() {
int i = 0;
while (i < actors.size()) {
Actor m = (Actor)actors.get(i);
if (m.isMarkedForRemoval()) {
actors.remove(i);
} else {
m.act();
i++;
}
}
player.act();
}
Rys 8. Nasz straszny laser
Spokojnie moemy stworzy klas kuli:

Bullet.java

public class Bullet extends Actor {


protected static final int BULLET_SPEED=10;
public Bullet(Stage stage) {
super(stage);
setSpriteNames( new String[] {"bullet.gif"});
}
public void act() {
super.act();
y-=BULLET_SPEED;
if (y < 0)
remove();
}
}
Pozostaje nam tylko zwiza z klawiszem spacji wystrza kuli. Modyfikujemy wic Player.java
public void fire() {
Bullet b = new Bullet(stage);
b.setX(x);
b.setY(y - b.getHeight());
stage.addActor(b);
}
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP : up = true; break;
case KeyEvent.VK_LEFT : left = true; break;
case KeyEvent.VK_RIGHT : right = true; break;
case KeyEvent.VK_DOWN : down = true;break;
case KeyEvent.VK_SPACE : fire(); break;
}
updateSpeed();
}

Efekt jest ciekawy:

Rys 9. Jakie one kuloodporne

Bomby
W celu zrnicowania uzbrojenia, dajmy graczowi dodatkowy zasb. Bomby bd jakby fal uderzeniow zoon z 8 kul
ognia poruszajcych si we wszystkich kierunkach, i powiedzmy, e ma ich ograniczon ilo powiedzmy 5. Zasada
oczywicie jest podobna, i dziki programowaniu obiektowemu jest to niemal trywialne

Rys 10. Tak docelowo ma wygala efekt.

Bomb.java
public class
public
public
public
public
public
public
public
public

Bomb extends
static final
static final
static final
static final
static final
static final
static final
static final

Actor {
int UP_LEFT = 0;
int UP = 1;
int UP_RIGHT = 2;
int LEFT = 3;
int RIGHT = 4;
int DOWN_LEFT = 5;
int DOWN = 6;
int DOWN_RIGHT = 7;

protected static final int BOMB_SPEED = 5;


protected int vx;
protected int vy;
public Bomb(Stage stage, int heading, int x, int y) {
super(stage);
this.x = x;

this.y = y;
String sprite ="";
switch (heading) {
case UP_LEFT : vx = -BOMB_SPEED; vy = -BOMB_SPEED;
sprite="bombUL.gif";break;
case UP : vx = 0; vy = -BOMB_SPEED; sprite="bombU.gif";break;
case UP_RIGHT: vx = BOMB_SPEED; vy = -BOMB_SPEED;
sprite="bombUR.gif";break;
case LEFT : vx = -BOMB_SPEED; vy = 0; sprite = "bombL.gif";break;
case RIGHT : vx = BOMB_SPEED; vy = 0; sprite = "bombR.gif";break;
case DOWN_LEFT : vx = -BOMB_SPEED; vy = BOMB_SPEED;
sprite="bombDL.gif";break;
case DOWN : vx = 0; vy = BOMB_SPEED; sprite = "bombD.gif";break;
case DOWN_RIGHT : vx = BOMB_SPEED; vy = BOMB_SPEED; sprite =
"bombDR.gif";break;
}
setSpriteNames( new String[] {sprite});
}
public void act() {
super.act();
y+=vy;
x+=vx;
if (y < 0 || y > Stage.WYSOKOSC || x < 0 || x > Stage.SZEROKOSC)
remove();
}
}
Teraz tylko wystarczy zwiza adunek z klawiszem B dodajc w Player.java:
public void fireCluster() {
if (clusterBombs == 0)
return;
clusterBombs--;
stage.addActor(
stage.addActor(
stage.addActor(
stage.addActor(
stage.addActor(
stage.addActor(
stage.addActor(
stage.addActor(

new
new
new
new
new
new
new
new

Bomb(stage,
Bomb(stage,
Bomb(stage,
Bomb(stage,
Bomb(stage,
Bomb(stage,
Bomb(stage,
Bomb(stage,

Bomb.UP_LEFT, x,y));
Bomb.UP,x,y));
Bomb.UP_RIGHT,x,y));
Bomb.LEFT,x,y));
Bomb.RIGHT,x,y));
Bomb.DOWN_LEFT,x,y));
Bomb.DOWN,x,y));
Bomb.DOWN_RIGHT,x,y));

}
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP : up = true; break;
case KeyEvent.VK_LEFT : left = true; break;
case KeyEvent.VK_RIGHT : right = true; break;
case KeyEvent.VK_DOWN : down = true;break;
case KeyEvent.VK_SPACE : fire(); break;
case KeyEvent.VK_B : fireCluster(); break;
}
updateSpeed();
}
Efekt zaczyna by imponujcy:

Rys 11. Ka-Bum!

Wykrycie kolizji
Koniec z tym, e strzelimy do przezroczystych potworw. Czas je wytpi.

Rys 12. Standardowe porwnianie w grafice 2D przez nachodzenie prostoktw.


Cae szczcie wszystkie klasy shape, w tym rectangle, posiadaj metod intersects ( ) , ktra sprawdza przecinanie.
Wystarczy wic aby aktorzy zwracali swoje brzegi. Dodatkowo, aktor powinien mie jaka metod do reagowania na
kolizj. Dodajemy wic do Actor.java
public Rectangle getBounds() {
return new Rectangle(x,y,width,height);
}
public void collision(Actor a){}
zaczmy jeszcze Actor.java, oraz do WojnaSwiatow.java
import java.awt.Rectangle;
Nadpisujemy metod w Monster.java
public void collision(Actor a) {
if (a instanceof Bullet || a instanceof Bomb)
remove();
}
Pozostaje teraz stwierdzi sam kolizj. Oczywicie robimy to w klasie, ktra trzyma ca list czyli WojnaSwiatow.java
Dodajmy wic odpowiednia metod oraz zmieniamy ptl gwn:
public void checkCollisions() {
Rectangle playerBounds = player.getBounds();
for (int i = 0; i < actors.size(); i++) {
Actor a1 = (Actor)actors.get(i);

Rectangle r1 = a1.getBounds();
if (r1.intersects(playerBounds)) {
player.collision(a1);
a1.collision(player);
}
for (int j = i+1; j < actors.size(); j++) {
Actor a2 = (Actor)actors.get(j);
Rectangle r2 = a2.getBounds();
if (r1.intersects(r2)) {
a1.collision(a2);
a2.collision(a1);
}
}
}
}
public void game() {
usedTime=1000;
initWorld();
while (isVisible()) {
long startTime = System.currentTimeMillis();
updateWorld();
checkCollisions();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
}
}

Statystyki
Warto byoby mie w kocu statystyki, ilo ubitych potworw, ilo bomb, ilo tarczy. No i eby byo to wywietlane na
ekranie. Chcemy wyswietlac:
Punkty jakie uzyska gracz
ycie gracza
Ilo Bomb, ktra pozostaa
W tym celu dokonamy paru zmian. Dodatkowo podzielimy okno gry na pole gry, i na pole statystyk.. Powiedzmy, e 500
pikseli bdzie zajmowaa gra, a pod ni, 100 statystyki. Twoim zadaniem jest potem dobra te liczby troch lepiej, tak, aby
bardziej estetycznie to wygldao. Tymczasem dodajmy now sta do naszego interfacu Stage:
public static final int WYSOKOSC_GRY = 500;
Nastpnie dodajmy nowe atrybuty w klasie Player:
public static final int MAX_SHIELDS = 200;
public static final int MAX_BOMBS = 5;
private int score;
private int shields;
na koniec konstruktora tej klasy dodajemy:
clusterBombs=MAX_BOMBS;
shields = MAX_SHIELDS;
zmieni trzeba bdzie rwnie tej klasie metod klas ( potworom nie potrzeba bo poruszaj si jedynie w poziomie, kule
jedynie do przodu, mona zmieni poruszanie si bomb, to pozostawiam rwnie tobie w ramach wicze)
public void act() {
super.act();

x+=vx;
y+=vy;
if (x < 0 )
x = 0;
if (x > Stage.SZEROKOSC - getWidth())
x = Stage.SZEROKOSC - getWidth();
if (y < 0 )
y = 0;
if ( y > Stage.WYSOKOSC_GRY-getHeight())
y = Stage.WYSOKOSC_GRY - getHeight();
}
Dodajmy jeszcze typowe metody do zmiany i odczytu danych z klasy:
public
public
public
public
public
public

int getScore() {
return score; }
void setScore(int i) { score = i; }
int getShields() { return shields; }
void setShields(int i) { shields = i; }
int getClusterBombs() { return clusterBombs; }
void setClusterBombs(int i) { clusterBombs = i; }

Teraz zajmijmy si rysowaniem tego wszystkiego na ekranie. Na wstpie w pliku WojnaSwiatow.java zaimportujmy
odpowiednie klasy:
import java.awt.Font;
import java.awt.image.BufferedImage;
Dodajmy nowe metody graficzne:
public void paintShields(Graphics2D g) {
g.setPaint(Color.red);
g.fillRect(280,Stage.WYSOKOSC_GRY,Player.MAX_SHIELDS,30);
g.setPaint(Color.blue);
g.fillRect(280+Player.MAX_SHIELDSplayer.getShields(),Stage.WYSOKOSC_GRY,player.getShields(),30);
g.setFont(new Font("Arial",Font.BOLD,20));
g.setPaint(Color.green);
g.drawString("Shields",170,Stage.WYSOKOSC_GRY+20);
}
public void paintScore(Graphics2D g) {
g.setFont(new Font("Arial",Font.BOLD,20));
g.setPaint(Color.green);
g.drawString("Score:",20,Stage.WYSOKOSC_GRY + 20);
g.setPaint(Color.red);
g.drawString(player.getScore()+"",100,Stage.WYSOKOSC_GRY

+ 20);

}
public void paintAmmo(Graphics2D g) {
int xBase = 280+Player.MAX_SHIELDS+10;
for (int i = 0; i < player.getClusterBombs();i++) {
BufferedImage bomb = spriteCache.getSprite("bombUL.gif");
g.drawImage( bomb ,xBase+i*bomb.getWidth(),Stage.WYSOKOSC_GRY,this);
}
}
public void paintfps(Graphics2D g) {
g.setFont( new Font("Arial",Font.BOLD,12));
g.setColor(Color.white);
if (usedTime > 0)

g.drawString(String.valueOf(1000/usedTime)+" fps",Stage.SZEROKOSC50,Stage.WYSOKOSC_GRY);
else
g.drawString("--- fps",Stage.WIDTH-50,Stage.WYSOKOSC_GRY);
}
public void paintStatus(Graphics2D g) {
paintScore(g);
paintShields(g);
paintAmmo(g);
paintfps(g);
}
No i zmiemy rysowanie wiata:
public void paintWorld() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,getWidth(),getHeight());
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.paint(g);
}
player.paint(g);
paintStatus(g);
strategia.show();
}
Wynik bdzie zbliony do:

Rys 13. To powoli zaczyna przypomina gr


No dobrze, ale przydaoby si zlicza te punkty skoro ju je wypisujemy. Chcemy aby powiedzmy, przy zabiciu potwora,
dodatkowo dodawane byo 20 punktw. Jednake o zabiciu potwora wie klasa Monster, ktra nie ma dostpu do atrybutw
gracza. Moe rwnie zaistnie wiele sytuacji, w ktrych trzeba bdzie mie dostp do atrybutw gracza: zmiana osony,
dodanie bomb, dodanie bonusw. Potrzebujemy wic moliwoci uywania metod gracza, a do tego, potrzebujemy mie
referencj na samego gracza. W tym celu dodamy jako metod w Stage.java :
public Player getPlayer();
Zimplementujmy j w WojnaSwiatow.java
public Player getPlayer() { return player;}

Dodajmy graczowi moliwo zwikszania wyniku w player.java:


public void addScore(int i) { score += i;

No i wreszcie zmodyfikujmy kolizj w potworze:


public void collision(Actor a) {
if (a instanceof Bullet || a instanceof Bomb){
remove();
stage.getPlayer().addScore(20);
}
}

mier
No na razie nasz gracz jest niemiertelnym wojownikiem zabijajcym 10 bezbronnych potworw. Taka gra nie przycignie
tumw. Trzeba doda ryzyko mierci gracza. Na razie zrbmy to w momencie kolizji z potworem. Oczywicie mier
gracza oznacza bdzie koniec gry (lub stracenia ycia, jeli dasz graczowi wicej ni jedno. Wtedy gr koczy bdzie
stracenie ostatniego z y, my na razie zostamy przy jednym yciu) . Co chcemy wic uzyska
Przy zderzeniu z potworem gracz ma traci ycie, powiedzmy e zabija tym potwora i zyskuje wicej punktw.
Gdy ycie gracza si koczy gra zostaje przerwana i zostanie wywietlony napis Game Over
Na wstpie dodajmy moliwo koczenia gry do Stage.java
public void gameOver();
zaimplementujmy j w WojnaSwiatow.java
public void gameOver() { gameEnded = true;}
i dodajmy do WojnaSwiatow.java odpowiedni atrybut:
private boolean gameEnded=false;
Musimy oczywicie zmieni warunek ptli gwnej programu.:
public void game() {
usedTime=1000;
initWorld();
while (isVisible() && !gameEnded) {
long startTime = System.currentTimeMillis();
updateWorld();
checkCollisions();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
}
paintGameOver();
}
no i stworzy metod wywietlajc napis:
public void paintGameOver() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
g.setColor(Color.white);
g.setFont(new Font("Arial",Font.BOLD,20));
g.drawString("GAME OVER",Stage.SZEROKOSC/2-50,Stage.WYSOKOSC/2);
strategia.show();
}

Dodajmy teraz wykrycie kolizji w player.java


public void collision(Actor a) {
if (a instanceof Monster ) {
a.remove();
addScore(40);
addShields(-40);
if (getShields() < 0)
stage.gameOver();
}
}
Na razie mier mona uzyska jedynie szarujc na potory:

Rys 14. Aaaaa !!!


Gra jest jednak cigle zbyt atwa, a zgin w ten sposb moe jedynie bardzo niedowiadczony (lub ograniczony) gracz.
Dodajmy wic potworom troch agresji. Niechaj strzelaj jakim magicznym laserem. Dodatkowo, skorzystamy znw z
animacji, eby uzyska ciekawy efekt owego wystrzau. Przystpmy wic do stworzenia nowej klasy, z owym laserem
wanie.
Rys 15. Kolejne gify: disparo0.gif, disparo1.gif oraz disparo2.gif stworz animacj laseru

Laser.java

public class Laser extends Actor {


protected static final int BULLET_SPEED=3;
public Laser(Stage stage) {
super(stage);
setSpriteNames( new String[]
{"disparo0.gif","disparo1.gif","disparo2.gif"});
setFrameSpeed(10);
}
public void act() {
super.act();
y+=BULLET_SPEED;
if (y > Stage.WYSOKOSC_GRY)
remove();
}
}

wida, e jest ona podobna do klasy bullet, rni si jednak szybkoci I kierunkiem poruszania si oraz animacj.
Nauczymy wic teraz potwory strzela. Najpierw dodajmy sta czstotliwoci strzelania:
protected static final double FIRING_FREQUENCY = 0.01;
nastpnie zmiemy metod act():
public void fire() {
Laser m = new Laser(stage);
m.setX(x+getWidth()/2);
m.setY(y + getHeight());
stage.addActor(m);
}
public void act() {
super.act();
x+=vx;
if (x < 0 || x > Stage.SZEROKOSC)
vx = -vx;
if (Math.random()<FIRING_FREQUENCY)
fire();
}
No i zmodyfikujmy kolizj gracza:
public void collision(Actor a) {
if (a instanceof Monster ) {
a.remove();
addScore(40);
addShields(-40);
}
if (a instanceof Laser ) {
a.remove();
addShields(-10);
}
if (getShields() < 0)
stage.gameOver();
}
Efekt jest imponujcy, nareszcie co si dzieje:

Rys 16. Unikamy i strzelamy, taka gra.

Przewijajce si to
Wiele gier w tym stylu ma poruszajce si to. Jest to efektywny, aczkolwiek bardzo atwy do uzyskania w javie efekt.
Wystarczy zamiast jednym kolorem, wypenia bitmap. Zmieniajc po prostu jej bazowe koordynatach. Dla zrozumienia
tego zagadnienia spojrzmy na rysunki:

Rys. 17 zamy e uzywalibymy takiego ta.

Rys 18. Tak jeli wypenimy tekstur o wspodnych (0,0,256,256)

Rys 19 Tak natomiast gdy podamy wsprzdne tekstury (0,20,256,256)


Efekt to przesunicie teksturowania o 20 pikseli. W naszej grze bdziemy chcieli uzyska agodniejszy efekt przejcia wic
bdziemy uywa przesunicia o 1 piksel. Uyjemy oczywicie ciekawszego i bardziej adekwatnego ta: oceano.gif

Rys 20. Pikna powierzchnia oceanu to dobre pole bitwy


Jedyny plik jaki modyfikujemy to plik gwny programu

WojnaSwiatow.java
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

java.awt.Canvas;
java.awt.image.BufferedImage;
java.awt.Font;
java.awt.TexturePaint;
java.awt.Rectangle;
javax.swing.JFrame;
javax.swing.JPanel;
java.awt.Color;
java.awt.Dimension;
java.awt.Graphics;
java.awt.event.WindowAdapter;
java.awt.event.WindowEvent;
java.awt.Graphics2D;
java.awt.image.BufferStrategy;
java.util.ArrayList;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class WojnaSwiatow extends Canvas implements Stage, KeyListener{
public long usedTime;
public BufferStrategy strategia;
private SpriteCache spriteCache;
private ArrayList actors;
private Player player;s
private boolean gameEnded=false;
private BufferedImage ocean;
private int t;
...
public void paintWorld() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
ocean = spriteCache.getSprite("oceano.gif");
g.setPaint(new TexturePaint(ocean, new
Rectangle(0,t,ocean.getWidth(),ocean.getHeight())));
g.fillRect(0,0,getWidth(),getHeight());
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.paint(g);
}
player.paint(g);
paintStatus(g);
strategia.show();
}
...
public void game() {
usedTime=1000;
t = 0;
initWorld();
while (isVisible() && !gameEnded) {
t++;
long startTime = System.currentTimeMillis();
updateWorld();
checkCollisions();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {

Thread.sleep(20);
} catch (InterruptedException e) {}
}
paintGameOver();
}
Kilka linijek kodu zapewnio nam co najmniej ciekawy efekt:

Rys 21. Gra zaczyna wyglda profesjonalnie

Odradzanie si potworw
Po zabiciu naszych potworw gra przestaje posiadac jakikolwiek sens. Moemy to rozwiza przez:
Tworzenie nowych potworw gdy jaki ginie
Zmieni poziom na kolejny z nowymi potworkami i tem
Nowy potworek pojawiaby si co jaki czas, niezalenie od iloci zabitych potorw
Sprbujmy uczyni pierwszy wariant. Wystarczy zmodyfikowa plik Monster.java:
public void spawn() {
Monster m = new Monster(stage);
m.setX( (int)(Math.random()*Stage.SZEROKOSC) );
m.setY( (int)(Math.random()*Stage.WYSOKOSC_GRY/2) );
m.setVx( (int)(Math.random()*20-10)+1);
stage.addActor(m);
}
public void collision(Actor a) {
if (a instanceof Bullet || a instanceof Bomb){
remove();
spawn();
stage.getPlayer().addScore(20);
}
}

Proste dwiki
Dodanie dwiku i muzyki do naszej gry jest bardzo proste, tworzy jednak znakomity efekt. Wikszo producentw gier
przykada wicej stara do tworzenia grafiki i muzyki, ni do samego programowania, co owocuje potem wspaniaymi
efektami, ale fataln grywalnoci...
Uyjmy prostych plikw .wav :
musica.wav do muzyki powtarzajcej si w tle
photon.wav jako dwiku wystrzeliwania laseru przez potwory

explosion.wav jako dwiku odgrywanego przy mierci potwora


missile.wav jako dwiku wystrzelenia rakiety przez gracza
Dwiki to nic innego jak zasoby, i tak samo jak obrazki potrzebuj swj cache. Widzimy wic, e przyda si oglna klasa
ResourceCache:

ResourceCache.java
import java.net.URL;
import java.util.HashMap;

public abstract class ResourceCache {


protected HashMap resources;
public ResourceCache() {
resources = new HashMap();
}
protected Object loadResource(String name) {
URL url=null;
url = getClass().getClassLoader().getResource(name);
return loadResource(url);
}
protected Object getResource(String name) {
Object res = resources.get(name);
if (res == null) {
res = loadResource(name);
resources.put(name,res);
}
return res;
}
protected abstract Object loadResource(URL url);
}
Musimy wic troch zmieni SpriteCache:

SpriteCache.java

import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
public class SpriteCache extends ResourceCache{
protected Object loadResource(URL url) {
try {
return ImageIO.read(url);
} catch (Exception e) {
System.out.println("Przy otwieraniu " + url);
System.out.println("Wystapil blad : "+e.getClass().getName()+"
"+e.getMessage());
System.exit(0);
return null;
}
}
public BufferedImage getSprite(String name) {
return (BufferedImage)getResource("img/"+name);
}
}

no i stworzy wkocu:

SoundCache.java

import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;
public class SoundCache extends ResourceCache{
protected Object loadResource(URL url) {
return Applet.newAudioClip(url);
}
public AudioClip getAudioClip(String name) {
return (AudioClip)getResource("sound/"+name);
}
public void playSound(final String name) {
getAudioClip(name).play();
}
public void loopSound(final String name) {
getAudioClip(name).loop();
}
}
Oczywicie zakadam, e pliki z muzyk bdziemy trzyma w katalogu /sound. Poniewa kada klasa powinna mie
dostp do cacheu muzyki, trzeba zmodyfikowac stage.java:
import java.awt.image.ImageObserver;
public interface Stage extends ImageObserver {
public static final int SZEROKOSC = 800;
public static final int WYSOKOSC = 600;
public static final int SZYBKOSC = 20;
public static final int WYSOKOSC_GRY = 500;
public SpriteCache getSpriteCache();
public void addActor(Actor a);
public Player getPlayer();
public SoundCache getSoundCache();
public void gameOver();
}
Teraz pozostaje nam ju tylko doda dzwiki w odpowiednich miejscach:

WojnaSwiatow.java
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

java.awt.Canvas;
java.awt.image.BufferedImage;
java.awt.Font;
java.awt.TexturePaint;
java.awt.Rectangle;
javax.swing.JFrame;
javax.swing.JPanel;
java.awt.Color;
java.awt.Dimension;
java.awt.Graphics;
java.awt.event.WindowAdapter;
java.awt.event.WindowEvent;
java.awt.Graphics2D;
java.awt.image.BufferStrategy;
java.util.ArrayList;
java.awt.event.KeyEvent;
java.awt.event.KeyListener;

public class WojnaSwiatow extends Canvas implements Stage, KeyListener{


public long usedTime;
public BufferStrategy strategia;
private SpriteCache spriteCache;
private ArrayList actors;
private Player player;
private boolean gameEnded=false;
private SoundCache soundCache;
private BufferedImage ocean;
private int t;
public WojnaSwiatow() {
spriteCache = new SpriteCache();
soundCache = new SoundCache();
JFrame okno = new JFrame(".: Wojna Swiatow :.");
JPanel panel = (JPanel)okno.getContentPane();
setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
panel.setPreferredSize(new Dimension(Stage.SZEROKOSC,Stage.WYSOKOSC));
panel.setLayout(null);
panel.add(this);
okno.setBounds(0,0,Stage.SZEROKOSC,Stage.WYSOKOSC);
okno.setVisible(true);
okno.addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
okno.setResizable(false);
createBufferStrategy(2);
strategia = getBufferStrategy();
requestFocus();
addKeyListener(this);
}
public SoundCache getSoundCache() {
return soundCache;
}
...
W pliku Player.java usupeniamy metod fire ()
public void fire() {
Bullet b = new Bullet(stage);
b.setX(x);
b.setY(y - b.getHeight());
stage.addActor(b);
stage.getSoundCache().playSound("missile.wav");
}
a w Monster.java collision() oraz fire()
public void fire() {
Laser m = new Laser(stage);
m.setX(x+getWidth()/2);
m.setY(y + getHeight());
stage.addActor(m);
stage.getSoundCache().playSound("photon.wav");
}

public void collision(Actor a) {


if (a instanceof Bullet || a instanceof Bomb){
remove();
stage.getSoundCache().playSound("explosion.wav");
spawn();
stage.getPlayer().addScore(20);
}
}
Pojawia si jednak problem. Gra moe zaczc spowalnia. Dlaczego? Dlatego, e wszystko wykonujemy w jednej ptli,
ktra zawsze bya spowalniana poprzez wgrywanie dwiku. Wygodniej bdzie tworzy wtek, ktry ma to zrobi. Tak
unikniemy spowolnienia. Zmodyfikujmy wiec SoundCache.java
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;
public class SoundCache extends ResourceCache{
protected Object loadResource(URL url) {
return Applet.newAudioClip(url);
}
public AudioClip getAudioClip(String name) {
return (AudioClip)getResource("sound/"+name);
}
public void playSound(final String name) {
new Thread(
new Runnable() {
public void run() {
getAudioClip(name).play();
}
}
).start();
}
public void loopSound(final String name) {
new Thread(
new Runnable() {
public void run() {
getAudioClip(name).loop();
}
}
).start(); }
}

Optymalizacje kodu
Gra praktycznie jest gotowa. Pozostaje nam jeszcze tylko ulepszy j, optymalizujc kod. Pierwsz optymalizacj bdzie
trzymanie obrazkw w kompatybilnym formacie. Co to znaczy? Kompatybilny obrazek trzymany w pamici taki, e jego
cechy charakterystyczne, s bardzo zblione do trybu wideo, ktry wanie uywamy. Takie obrazki s o wiele szybsze do
narysowania, ni te ktrych teraz uywamy (czyli ImageIO). Scenariusz optymalizacji to:
1. Przeczyta obrazek z dysku za pomoc ImageIO
2. Stworzenie kompatybilnego rysunku o rozmiarach wczytanego
3. Przerysowanie wczytanego rysunku do nowo utworzonego
Zmodyfikujmy wic SpriteCache.java:
import
import
import
import

java.awt.Graphics;
java.awt.GraphicsConfiguration;
java.awt.GraphicsEnvironment;
java.awt.Image;

import
import
import
import
import

java.awt.Transparency;
java.awt.image.BufferedImage;
java.awt.image.ImageObserver;
java.net.URL;
javax.imageio.ImageIO;

public class SpriteCache extends ResourceCache implements ImageObserver{


protected Object loadResource(URL url) {
try {
return ImageIO.read(url);
} catch (Exception e) {
System.out.println("Przy otwieraniu " + url);
System.out.println("Wystapil blad : "+e.getClass().getName()+"
"+e.getMessage());
System.exit(0);
return null;
}
}
public BufferedImage createCompatible(int width, int height, int transparency)
{
GraphicsConfiguration gc =
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefa
ultConfiguration();
BufferedImage compatible =
gc.createCompatibleImage(width,height,transparency);
return compatible;
}
public BufferedImage getSprite(String name) {
BufferedImage loaded = (BufferedImage)getResource("img/"+name);
BufferedImage compatible =
createCompatible(loaded.getWidth(),loaded.getHeight(),Transparency.BITMASK);
Graphics g = compatible.getGraphics();
g.drawImage(loaded,0,0,this);
return compatible;
}
public boolean imageUpdate(Image img, int infoflags,int x, int y, int w, int
h) {
return (infoflags & (ALLBITS|ABORT)) == 0;
}
}

Nastpn optymalizacj bdzie ulepszenie efektu, ktry okropnie zwolni prac gry. Mowa o przewijanym tle jak si
susznie domylacie. Pozbdziemy si cigego teksturowania. Jak? Stworzymy obrazek w pamici, o szerokoci okna, ale
wysokoci wikszej, aby uchwyci ca ptle i bdziemy odpowiednio rysowa ten w ten sposb utworzony prostokt.
Zmieniamy WojnaSwiatow.java:
import
import
import
import
import
import
import
import
import

java.awt.Canvas;
java.awt.Transparency;
java.awt.image.BufferedImage;
java.awt.Font;
java.awt.TexturePaint;
java.awt.Rectangle;
javax.swing.JFrame;
javax.swing.JPanel;
java.awt.Color;

import
import
import
import
import
import
import
import
import

java.awt.Dimension;
java.awt.Graphics;
java.awt.event.WindowAdapter;
java.awt.event.WindowEvent;
java.awt.Graphics2D;
java.awt.image.BufferStrategy;
java.util.ArrayList;
java.awt.event.KeyEvent;
java.awt.event.KeyListener;

public class WojnaSwiatow extends Canvas implements Stage, KeyListener{


public long usedTime;
public BufferStrategy strategia;
private SpriteCache spriteCache;
private ArrayList actors;
private Player player;
private boolean gameEnded=false;
private SoundCache soundCache;
private BufferedImage background, backgroundTile;
private int backgroundY;
...
public void initWorld() {
actors = new ArrayList();
for (int i = 0; i < 10; i++){
Monster m = new Monster(this);
m.setX( (int)(Math.random()*Stage.SZEROKOSC) );
m.setY( i*20 );
m.setVx( (int)(Math.random()*3)+1 );
actors.add(m);
}
player = new Player(this);
player.setX(Stage.SZEROKOSC/2);
player.setY(Stage.WYSOKOSC - 2*player.getHeight());
soundCache.loopSound("musica.wav");
backgroundTile = spriteCache.getSprite("oceano.gif");
background = spriteCache.createCompatible(
Stage.SZEROKOSC,
Stage.WYSOKOSC+backgroundTile.getHeight(),
Transparency.OPAQUE);
Graphics2D g = (Graphics2D)background.getGraphics();
g.setPaint( new TexturePaint( backgroundTile,
new
Rectangle(0,0,backgroundTile.getWidth(),backgroundTile.getHeight())));
g.fillRect(0,0,background.getWidth(),background.getHeight());
backgroundY = backgroundTile.getHeight();
}
...
public void paintWorld() {
Graphics2D g = (Graphics2D)strategia.getDrawGraphics();
g.drawImage( background,
0,0,Stage.SZEROKOSC,Stage.WYSOKOSC,
0,backgroundY,Stage.SZEROKOSC,backgroundY+Stage.WYSOKOSC,this);
for (int i = 0; i < actors.size(); i++) {
Actor m = (Actor)actors.get(i);
m.paint(g);
}

player.paint(g);
paintStatus(g);
strategia.show();
}
...
public void game() {
usedTime=1000;
initWorld();
while (isVisible() && !gameEnded) {
long startTime = System.currentTimeMillis();
backgroundY--;
if (backgroundY < 0)
backgroundY = backgroundTile.getHeight();
updateWorld();
checkCollisions();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
try {
Thread.sleep(10);
} catch (InterruptedException e) {}
}
paintGameOver();
}
...
Kolejn optymalizacj bdzie pozbycie si kursora myszy.Wystarczy zmieni konstruktor dopisujc na jego kocu:
BufferedImage cursor =
spriteCache.createCompatible(10,10,Transparency.BITMASK);
Toolkit t = Toolkit.getDefaultToolkit();
Cursor c = t.createCustomCursor(cursor,new Point(5,5),"null");
setCursor(c);
Istenieje jeszcze pewien niezauwaalny prawie problem, jednake wiadczcy o nieprofesjonalimie. Gdy odelemy gr w
to i nastpnie spowrotem jna pierwszy plan, zobaczymy szare mignicie okna. Dlaczego? Dlatego, e AWT
automatycznie w tej sytuacji przemalowuje okna, oczywicie domylnym szarym, a nie nasz strategi. Mona to bardzo
atwo wyczy, dodajc na koniec kontruktora:
setIgnoreRepaint(true);
Mwimy tym AWT, e nie ma si kopota, gdy sami umiemy przemalowywa okno. Ostatnia optymalizacja, jest raczej
kluczowa. Nasza gra W duym stopniu zaley od szybkoci komputera. Poprzez sta ilo odespanego czasu czasem gra
bdzie dziaa bardzo szybko, czasem natomiast bardzo wolno. Dobrze byoby, gdyby gra dostosowywaa si do tego.
Szybko atwo dostosowa obserwujc FPS-y. Znw przyda nam si wtek. Zmiemy metod game() :
public void game() {
usedTime=1000;
initWorld();
while (isVisible() && !gameEnded) {
long startTime = System.currentTimeMillis();
backgroundY--;
if (backgroundY < 0)
backgroundY = backgroundTile.getHeight();
updateWorld();
checkCollisions();
paintWorld();
usedTime = System.currentTimeMillis()-startTime;
do {
Thread.yield();
} while (System.currentTimeMillis()-startTime< 17);

}
paintGameOver();
}

Sowa kocowe
Mam nadziej, e powyszy kurs wnis wiele w nauk javy i rozwin wasze skrzyda. Moliwoci na rozwinicia tej gry
s niezliczone. Na podstawie poznanych tu sztuczek i technik zapewne bdziecie w stanie stworzy wiele zupenie innych
gier. Wierz rwnie, e docenilicie programowanie obiektowe. Wszelkie uwagi, prosz kierowa na email. ycz
powodzenia w dalszych zabawach i testach! Dziekuje za lektur.

You might also like