Upload
lyduong
View
216
Download
2
Embed Size (px)
Citation preview
Miskolci Egyetem
Gépészmérnöki és informatikai kar
Automatizálási és kommunikáció-technológiai tanszék
Játék készítése Java-ban
Szakdolgozat
Készítette:
Búr Béla
N5GIZ2
3860 Encs, Vadvirág út 20.
Tervezésvezető:
Elek Tibor
Informatikai tanszék
Tartalom
I. Bevezetés ..................................................................................................... I-1
II. A Java és a játékfejlesztés ........................................................................... II-3
Grafikus csomagok a Java-hoz ........................................................................ II-4
III. Java2D ..................................................................................................... III-5
Mire való a Java2D? ....................................................................................... III-5
A megjelenítés alapjai ..................................................................................... III-5
Geometriai alakzatok ...................................................................................... III-5
Képek ............................................................................................................. III-7
Grafikai elemek megjelenítése ........................................................................ III-8
Szövegek ...................................................................................................... III-12
IV. Swing ...................................................................................................... IV-14
Általános ismeretek ...................................................................................... IV-14
V. Hangfájlok lejátszása a Java-ban, a Java Sound API ................................ V-16
A Java Sound-ról általánosan ........................................................................ V-16
A javax.sound.sampled csomag .................................................................... V-16
VI. Java és az animáció ............................................................................... VI-19
Animációs alapok .......................................................................................... VI-19
Bufferelés és teljes képernyős mód a Javaban ............................................. VI-21
VII. Az elkészült program ismertetése .......................................................... VII-23
A játék rövid bemutatása ............................................................................. VII-23
Az elkészítés során felmerült feladatok ....................................................... VII-24
Felhasznált programok ................................................................................ VII-27
Az osztályok ismertetése ............................................................................. VII-27
GameFrame ............................................................................................. VII-27
GameExecutorThread .............................................................................. VII-28
A játék képernyői ...................................................................................... VII-29
A játék elemei ........................................................................................... VII-38
A pályát létrehozó és kezelő osztályok: ................................................... VII-48
A Field interfész, és a hozzá tartozó osztályok: ....................................... VII-50
Segédosztályok ........................................................................................ VII-51
A játék folyamatainak leírása ....................................................................... VII-54
Futási szálak ............................................................................................ VII-54
Egy periódus leírása ................................................................................ VII-55
VIII. Összefoglalás ....................................................................................... VIII-57
Játék készítése Java-ban
I-1
I. Bevezetés
A Java napjaink egyik legnépszerűbb programozási nyelve, azonban a
játékfejlesztés kapcsán csak ritkán hallani róla.
A szakdolgozat feladatom kigondolásakor az elsődleges célom Java, a Java2D és
az objektum-orientált programok tervezésének bemutatása volt. Azért
választottam ehhez a játékfejlesztést, mert úgy gondoltam, hogy egy ilyen
összetettebb feladat során rengeteg, máshol is előforduló probléma merülhet fel,
ezért az elkészítés során szerzett tapasztalatokat talán később is hasznosíthatom.
A nyelv az egyre inkább előtérbe kerülő, és véleményem szerint még mindig
rengeteg kiaknázatlan lehetőséget tartalmazó indie játék1 területen ideális
választás lehet, mivel elég absztrakt ahhoz, hogy egyszerűen és gyorsan stabil
kódot írjunk, és elég hatékony ahhoz, hogy egy ilyen projekt szükségleteit
kielégítse.
A játék egy kétdimenziós platformer lesz, ami manapság újra népszerű 8-bites
korszakot fogja megidézni. A játékmenet abból fog állni, hogy egy figurát irányítva
különböző akadályokon és ellenségeken átjutva, platformokon ugrálva kell eljutni a
célig.
A forráskódot úgy szeretném megírni, hogy az egyes feladatok jól elkülönüljenek
egymástól, illetve átlátható, és könnyen bővíthető legyen, ezek mellett a játék
egyes elemeit, a kód átírása, vagy ismerete nélkül is egyszerűen meg lehessen
változtatni.
Az ilyen típusú játékhoz folyamatos animációt, a bemeneti (billentyűzet), kimeneti
(monitor) perifériák kezelését, hangok lejátszását, illetve alapvető grafikai
objektumok megjelenítését kell biztosítani.
A dolgozat első felében a nyelv és a játékfejlesztés sajátosságairól írok, és
bemutatom a felhasznált Java API-kat, amik a JDK2 részei:
A Java2D-t, amivel a játék grafikai elemeit valósítottam meg
A Swing alapjait, ami egy GUI3 felületek létrehozására alkalmas API
A Java Sound API-t, amit a játék hangjainak lejátszásáért felelős
1 Kis, jellemzően független fejlesztőcsapat által készített játék, játékkiadók pénzügyi támogatása
nélkül. 2 Java Development Kit: a nyelv hivatalos fejlesztőcsomagja
3 A Graphical User Interface, vagyis a grafikus felhasználói felület rövidítése.
Játék készítése Java-ban
I-2
Ezek után a számítógépes játékok mozgókép-megjelenítésének alapproblémáit
írom le, bemutatva a Java által nyújtott megoldásukat.
A szakdolgozat második része a konkrét programról fog szólni: először az
elkészült játékot írom le röviden, majd az elkészítés során felmerült problémákat,
és azok megoldásait ismertetem. A következő fejezetben felvázolom a projekt
osztályainak struktúráját, és a működésüket. A futási szálak, illetve a játék egy
ciklusa alatt történő események leírásával zárom a játék ismertetését, majd az
összefoglalóban összegzem a megszerzett tapasztalatokat.
Játék készítése Java-ban
II-3
II. A Java és a játékfejlesztés
Sokan gondolják, hogy csak azért nem használják a nyelvet a játékiparban,
mert nem lehet vele elég „gyors” programot írni. Ez az állítás régebben megállta a
helyét, azonban manapság, a folyamatos fejlődés következményeként, a
teljesítményi mutatói egyesek szerint megközelítették/elhagyták az iparban
preferált C/C++ nyelvekét. (Davison, 2005)
Ha ez igaz is, akkor sem kell a közeljövőben jelentős változást várni ilyen téren,
mivel az Java-ra való átállást rengeteg más, külső tényező is gátolja, olyanok,
mint:
Mivel a másik két nyelv sokkal inkább elterjedt az iparágban,
ezekben tanultak meg fejleszteni a legjobb szakemberek. Rengeteg
időbe és pénzbe kerülhet, míg az eddig jól bevált módszereket
újragondolják, és a Javahoz igazítják.
A legtöbb konzol, és az Iphone sem tartalmaz JVM4-et, ezért
konzolos kiadásnál nem tudnák újra használni a PC verzióhoz megírt
kódot, egyszerűbb a C++ verziót átírni.
A játékfejlesztéshez használt két legnépszerűbb API, a DirectX és az
OpenGL csak mérsékelten támogatják a Java-t, emellett az ezeket
használó motorok is a másik két nyelvre lettek írva.
Az elsődleges alkalmazási helye ezen a területen az Android-ra való fejlesztés,
ennek egyszerű az oka: ez a platformon alkalmazott elsődleges nyelv. (Ismeretlen,
Why is'nt Java more widely used for game development)
Az előbb említett hátrányok a független, és az anyagi szempontokat
másodlagosként kezelő projektnél nem olyan lényegesek, fontosabb a gyorsan
megírt, mégis stabil és átlátható kód, emellett az itt szintén kevésbé fontos
hatékonyság sem feltétlenül rosszabb, mint más nyelveknél. Ilyen független
projekt volt például a Minecraft, ami Markus Persson nevéhez fűződik: ez a kiadás
után nem sokkal hatalmas siker lett, és ma már kapható Xbox 360-ra, Android-ra
és iOS-re is (Ismeretlen, Minecraft Wikipédia oldala) .
4 Java Virtual Machine: Java Virtuális Gép, fő feladata a kód futtatása az adott platformon
Játék készítése Java-ban
II-4
Grafikus csomagok a Java-hoz
Manapság a számítógépes játékok körében a két legnépszerűbb API az OpenGL
és a DirectX, amik 2D és 3D grafika megjelenítésére szolgálnak, hardveres
gyorsítás segítségével. Míg a DirectX a Microsoft cég csomagja, és a cég
termékeihez lett írva, addig az OpenGL platformfüggetlen, és a legtöbb platformon
létezik rá megvalósítás. Az általam használt Java2D mellett a játékkészítők
körében népszerűek az alábbi API-k:
Java OpenGL: Az OpenGL egyik Java implementációja, ami a legtöbb
OpenGL funkciót elérhetővé teszi a JNI5-n keresztül. (Ismeretlen, Java
OpenGL Wikipédia oldala)
LightWeight Java Gaming Libraly: Az LWJGL egy Java
játékfejlesztőknek készített csomag, ami nagy teljesítményű,
platformfüggetlen könyvtárak használatát teszi elérhetővé 3D játékok
készítéséhez. Segítségével lehetőség van olyan eszközöket kezelni, mint a
gamepad, a joystick-ok, vagy a kormány. A csomagot alapnak szánták
összetettebb könyvtárak, és játékmotorok létrehozásához. (Ismeretlen,
LWJGL weboldal)
Java3D: A Java3D egy az Oracle által fejlesztett, 3 dimenziós grafika
kezelésére írt API, ami platformtól függően használja a JOGL-t, az
OpenGL-t és a Direct3D-t.
A felsorolt csomagok nyílt forráskódúak, és BSD licenc alatt terjesztik őket.
5 Java Native Interface: egy keretrendszer, ami lehetővé teszi a Java és más alacsonyabb szintű
programozási nyelvek, mint a C vagy a C++ együttműködését.
Játék készítése Java-ban
III-5
III. Java2D
Mire való a Java2D?
A Java2D nevű API kétdimenziós grafika kezelésére használatos osztályok
gyűjteménye, amely lehetővé teszi az alakzat, a szöveg, kép és egyéb grafikai
elemek megjelenítését. Segítségével felhasználói felületek, rajzprogramok, vagy
bármilyen más grafikai elemeket megjelenítő programok készíthetőek.
A megjelenítés alapjai
A Java2D objektumok az „user space” (~felhasználói terület) nevű
koordinátarendszerben definiálhatóak, ennek az értékeit tárolják. A megjelenítő
eszközök közti különbségek „user space” szinten nem számítanak, láthatatlanok a
program számára. A megjelenítés során a renderelő rutinok a user space
koordinátákat „device space” (~eszköz koordinátarendszer) koordinátákká
alakítják: a device space a megjelenítő eszköz saját koordinátarendszere: ez az
eszköz lehet egy ablak a képernyőn, a monitor, de akár a nyomtató, vagy
bármilyen más kimenet. Az alapértelmezett transzformációnál (így a monitorra
rajzolásnál is) a 0,0 koordináta a bal felső sarok, és az x, y koordináta pedig
jobbra, illetve lefelé növekszik. Általánosan integer változókkal definiálják az
értékeket, de lehetővé tették a double, vagy float használatát is erre a célra. Egy
elem akkor kerül megjelenítésre, amikor egy paint vagy update metódus hívódik
fel a megfelelő Graphics osztályú kontextussal. Ez egy absztrakt típus, a Java2D
által használt implementációja a Graphics2D, ami hozzáférést biztosít az API
kiterjesztett grafikai és renderelő lehetőségeihez.
Geometriai alakzatok
A Java2D API széles választékot nyújt megjeleníthető geometriai elemekből,
emellett lehetőség van a kombinálásuk által saját alakzatok létrehozására. Ezek a
java.awt.geom csomagban találhatóak. Definiálásukhoz elengedhetetlen
következők ismerete:
Játék készítése Java-ban
III-6
Point2D: egy koordinátát reprezentáló osztály. Ez a fogalom nem egyezik
meg a pixellel: egy pontnak nincs kiterjedése és színe, ezért nem lehet
renderelni.
Shape: interfész: egy olyan geometriai alakzatot definiál, aminek van
körvonala és területe.
Ez utóbbit implementálják az alapvető alakzatok:
Line2D: egy vonal adatait tárolja. A koordinátáit double-ként kapja meg. A
végpontok metódusok segítségével módosíthatóak.
QuadCurve2D: exponenciális görbe. Legegyszerűbben 3 pont segítségével
határozható meg.
CubicCurve2D: ezt a görbét egy harmadfokú egyenlet írja le, beállítható 4
pont segítségével.
Rectangle2D: egy négyzet. Deklarálásánál a bal felső sarkának
koordinátáját, a szélességét, és a magasságát kell megadni.
RoundRectangle2D: lekerekített sarkú négyzet. A négyzet adatai mellett a
sarokszögek szélességét és magasságát is meg kell adni a definiálásánál.
Arc2D: Egy ív rajzolására alkalmas, ami egy ellipszis részlete. Leírható egy
befoglaló négyzettel, a kezdő szöggel, az ív hosszúságával és a lezárással.
Lezárás típusok:
o Open: csak a szög ívét rajzolja meg.
o Chord: a szög 2 végét összeköti.
o Pie: a szög 2 végét a középponttal köti össze.
RoundRectangle2D: lekerekített sarkú négyzet.
Ellipse2D: ellipszis.
QuadCurve2D: négyzetes görbe. Két pont és egy kontroll-pont definiálja.
CubicCurve2D: Két végpont és két kontrollpont segítségével definiált
görbe.
A felsorolt osztályok absztraktak, viszont mindegyik tartalmaz Float és Double
nevű belső osztályt, amik a leszármazottjaik, és példányosíthatóak.
Összetettebb alakzatokat a következő osztályok segítségével lehet létrehozni:
GeneralPath: egy útvonal, amit vonalakkal és görbékkel lehet definiálni.
Játék készítése Java-ban
III-7
Area: Shape objektumokból kiindulva definiál új alakzatokat, olyan
műveletekkel, mint a kivonás, a metszet vagy az unió.
Képek
A Java2D-ben a képek adatait általában négyzet alakú kétdimenziós pixel-tömbök
írják le. Az egyes pixeleknek a színe van letárolva, pozíciójukat pedig a tömbben
való elhelyezkedésük adja meg. A közös absztrakt ősosztályuk az Image, aminek
a leggyakrabban használt leszármazottja a BufferedImage: ennek két fontos
mezője van:
Az egyik egy ColorModel osztályú objektum, ami azt adja meg, hogy a
pixeladatokból hogyan képződjenek a színek.
A másik pedig a Raster osztály egy példánya, ami a következő feladatokat
látja el:
o a kép négyzetes koordinátáit tárolja
o karbantartja a képadatokat a memóriában
o lehetőséget biztosít több részkép készítésére egy képadatokat
tartalmazó bufferből
o metódusokat nyújt meghatározott pixelek közvetlen eléréséhez
A képek beolvasásának legegyszerűbb módja az ImageIO osztály read
metódusának használata: paramétere lehet InpustStream, File vagy URL.
Visszatérési értéke egy BufferedImage. Kezeli a GIF, PNG, JPEG, BMP és WBMP
formátumokat, és leszármaztatható, ezáltal újabb típusok kezelését is meg lehet
valósítani.
Működése: az ImageIO értelmezi a fájl formátumát, majd dekódolja
BufferedImage-é, amit már a Java közvetlenül is használhat.
Az API lehetőséget nyújt beépített szűrők alkalmazására: ezek ősosztálya a
BufferedImageOp. Ha egy ilyet megkap a Graphics2D objektum a rajzoló
metódusnál, akkor az eredeti kép helyett egy, az eredeti képre alkalmazott
algoritmus eredményeként kapott képet jeleníthetünk meg. Néhány fontosabb
szűrő osztály:
Játék készítése Java-ban
III-8
RescaleOp: a kép színrendszerének megfelelően lehet a pixelek színét
változtatni, tónusokat erősíteni, vagy esetleg átlátszóságot állítani (ARGB
színrendszer esetében).
ConvoleOp: élesíteni és elmosni lehet vele.
AffineTransformOp: pixelek pozícióját változtatja Affine transzformációk
segítségével.
Grafikai elemek megjelenítése
Az API által definiált geometriai elemek megjelenítése egy Graphics2D
objektumon történik, aminek az ehhez kapcsolódó metódusait 2 részre lehet
osztani:
Megjelenítő metódusok: paraméterként egy Shape, Image vagy String objektumot
kapnak, amit az előzőleg beállított megjelenítési attribútumok alapján renderelnek:
draw: egy alakzat körvonalának megjelenítése.
fill: alakzat megjelenítése a határvonalain belül eső területek
beszínezésével.
drawString: Egy String-ként megadott szöveg megjelenítése. A font
attribútum felhasználásával a String elemet alakzatokká konvertálja, amikre
a fill metódust hívja meg. Erről bővebben a „Szövegek” részben.
drawImage: kép megjelenítése. Mivel a kép nem tartalmaz UserSpace
koordinátákat, ezért a bal-felső sarok koordinátáját meg kell kapnia.
Megkaphat egy ImageObserver paramétert is, ami a kép betöltésekor jelez
az alkalmazásnak, azonban ez a BufferedImage esetében nem szükséges:
itt az értéke lehet null.
Az 1:1-es leképzés mellett olyan műveleteket is el lehet végezni, mint a
nyújtás, a vágás vagy a szűrés. Paraméterként, a forrás (kép) és cél (user
space) koordináták megadásával meg lehet megadni, hogy a Graphics
saját koordinátarendszerében hol jelenjen meg az adott képrészlet. Ez a
metódus kaphatja meg paraméterként a BufferedImageOp egy példányát.
Megjelenítést módosítók: a megjelenítő metódusok viselkedését lehet velük
beállítani. Ezek közül néhány fontosabb:
Játék készítése Java-ban
III-9
setStroke: a rajzolási körvonalak vastagságát, csatlakozásuk módját állítja
be. Paraméterként egy Stroke interfészt implementáló osztályt vár. Az
egyik leggyakrabban használt ilyen a BasicStroke nevű, amivel a
következő tulajdonságokat lehet beállítani:
o width: vonalvastagság
o join: szakaszok csatlakozásainak megjelenítése. 3 konstans értéket
vehet fel:
1. ábra – Csatlakozási típusok
A JOIN_MITER beállításhoz tartozhat egy miterlimit nevű
paraméter, ami azt adja meg, hogy mekkora az a maximum szög,
aminél még nem vágja le a sarkakat. Az értéknél nagyobb szögekre
a JOIN_BEVEL megjelenítés érvényes.
o cap: szakaszok lezárása:
2. ábra – Lezárási típusok
o dash: a vonal szaggatási mintája. Egy float tömbbel adhatjuk meg,
ami az egyes szakaszok és szakaszközök hosszúságát jelzi.
3. ábra – a tömb értékei: 5f,10f,15f,25f,35f,45f.
A dash_phase-el ezt a szaggatást eltolhatjuk:
4. ábra - Az előző szaggatás 15f értékű eltolása. Ugyanaz jelenik meg, mint ha 0 eltolással ezt a tömböt adtuk volna meg: 15,25f,35f,45f,5f,10f.
Az interfész fontos metódusa a createStrokedShape nevű metódust, ami
egy alakzatot kap paraméterként, és a Stroke által készített körvonalának
körvonalát adja vissza egy másik alakzatként. Ha az eredményt kitöltjük a
Játék készítése Java-ban
III-10
rajzolás színével, a megjelenített kép ugyanaz lesz, mint ha beállítanánk a
Stroke-ot és utána meghívnánk a draw-t az eredeti alakzatra.
setPaint: Egy Paint interfészt implementáló osztályt vár paraméterként. Ez
határozza meg, hogy a kitöltő (fill) műveletek során a belső pixeleket milyen
színre állítsa. Néhány alapvető implementáció:
o Color: az összes érintett pixelt a megadott színre állítja. Egy kör
kitöltése sárga színnel egy sárga színű kört eredményez.
o GradientPaint: két különböző színű pont közötti egyenletes
színátmenettel tölt ki.
o TexturePaint: egy kép és egy négyzet segítségével ismétlődő mintát
generál. A négyzet a teljes kép egy előfordulását határozza meg,
ehhez lesz igazítva a többi ismétlődése
setComposite: a rajzolóműveleteknél a legenerált képpontadatokat
forrásként, a már kirajzolt képpontokat pedig célként értelmezve az
általános megfeleltetés az, hogy a forráspixelek felülírják a célt, de a
kompozíciók megengednek másféle megfeleltetést is: megkapják a forrás
és a kirajzolt pixelek adatait, amikből olyan képpontokat generál, amikben
ezek hatással vannak egymásra. Egy Composite interfészt implementáló
osztályt vár paraméterként, aminek az egyik leggyakrabban használt
implementációja az AlphaComposite nevű, ami a megjelenítendő elemet
áttetszővé tudja tenni.
setTransform: beállítja a transzformációs attribútumot, amivel mozgatni,
nyújtani, nyírni lehet az objektumokat megjelenítés során. Egy
AffineTransform osztályú objektumot vár, ami Affine transzformációt ír le:
ez olyan átalakítás, aminek során a párhuzamos vonalak párhuzamosak
maradnak.
A transzformációt létre lehet hozni osztályszintű gyártó metódusokkal,
melyek olyan példányt adnak vissza, aminek csak az egyik paramétere van
beállítva.
Az összetettebb transzformációk megadásánál a többi attribútumot is be
kell állítani, vagy az osztály konstruktorában megadni őket.
Népszerűbb osztályszintű gyártó metódusok:
Játék készítése Java-ban
III-11
o getRotateInstance: forgatási transzformációt ad vissza, ami
paraméterként a forgási szöget várja.
o getScaleInstance: nyújtási transzformáció, ami a nyújtási arányt
várja.
o getShearInstance: vágási transzformáció, aminek módját 2 double
határozza meg.
o getTranslateInstance: mozgatási transzformáció: az x és y
tengelyen való eltolást várja paraméterként.
A létrejött transzformációkat a concatenate nevű metódussal tudjuk
összefűzni.
setClip: egy alakzat, vagy 4 pont segítségével beállítja a rajzolási
célterületet.
clip: az aktuális célterületet úgy módosítja, hogy az megegyezzen a
paraméterként kapott területtel való metszetével.
setFont: a beállított Font objektum határozza meg hogyan jelennek meg a
string szövegek a képernyőn. A metódus és az osztály részletesebb leírása
a „Szövegek” fejezetben található.
setRenderingHint: a renderelési folyamat működését a Graphics2D egy
RenderingHints objektumban tárolja. A kívánt beállításokat 2 módon
adhatjuk meg:
o létrehozhatunk egy RenderingHints objektumot, amit a metódusnak
átadva lecseréli az aktuális objektumot
o a metódus megkaphat kulcs-érték párt is, amivel az aktuális
objektumnak csak a kívánt paramétereit változtatja
Az osztálynak azért Hints, vagyis tanácsok a neve, mert nem feltétlenül
fognak életbe lépni a kívánt változtatások. Ha valamelyiknek nem
lehetséges a megvalósítása, azt ignorálja a JVM. A leggyakrabban beállított
paraméterek:
KEY_ANTIALIASING: az élsimítást lehet vele ki- és bekapcsolni, ami az
alakzatok határán kialakuló „fűrészfogas” részt simítja árnyalással és az
elrendezések módosításával.
Játék készítése Java-ban
III-12
5. ábra - VALUE_ANTIALIAS_ON 6. ábra - VALUE_ANTIALIAS_OFF
KEY_DITHERING: ha be van kapcsolva, akkor a színábrázolási korlátokat,
a pixelek átcsoportosításával és a pixelsűrűség változtatásával
kompenzálja a renderelő, a látás sajátosságait kihasználva.
KEY_INTERPOLATION: a képekre alkalmazott affine transzformációs
torzítások szabályait adja meg.
Szövegek
A szövegek megjelenítésének legkézenfekvőbb módja a Graphics osztály
drawString metódusa, ami a paraméterként kapott szöveget az előzőleg beállított
betűtípus attribútum alapján képzi a megjelenítendő alakzatokat, aminek a neve
font és a Font osztály, vagy annak egy leszármazottjának példánya.
Azokat az alakzatokat, amikből a kész alakzatokat képzi, glyph-eknek hívja a Java
dokumentáció. Egy karakter állhat több ilyenből is (pl. ékezetes karakterek), de
néhány esetben több karakter jelenik meg egy glyph-ként (pl. a „fi” szó helyett ϕ).
A Font osztály értelmezhető ilyen elemek gyűjteményeként. Az egyes glyph-ek
ugyanolyan tulajdonságokkal egy betűtípust (pl. Arial félkövér dőlt) alkotnak, az
egyes betűtípusok csoportja pedig betűtípus-családot (pl. Arial).
A betűtípusokhoz 3 attribútum tartozik:
Logikai név: egy rendszeren belüli betűtípushoz tartozik. Ilyen például az
„Arial félkövér dőlt”. A getName metódus segítségével lehet lekérdezni.
Betűtípus család név: a tipográfiai jellemzőket adja meg. Az előző példában
szereplő Font betűtípus családja az „Arial”. A getFontFamilyName adja
vissza.
Betűtípus név (font face name): azonosítja a betűtípust a rendszeren belül.
Gyakran megegyezik a logikai névvel. Ezt kell használni a betűtípusra való
hivatkozáskor. A getFontName metódussal lehet lekérdezni.
Az objektumról további információkat lehet megtudni a getAttributes által
visszaadott Map-ből.
Játék készítése Java-ban
III-13
Az megjelenítés során fontos lépés a mérés: az ebből kapott információk
szükségesek a későbbi Text műveleteknél is, mint például az írásjelek
pozícionálása, vagy a kijelölés. A glyph-ek alakja, mérete és pozíciója sokszor
környezetfüggő: például a kézírásos betűtípusoknál számít az, hogy milyen
betűhöz kapcsolódnak, ettől függően többféle kép tartozhat ugyanahhoz a
betűhöz.
A Java-ban a szövegek a memóriában találhatóak logikai sorrendben rendezve:
aszerint, hogy melyik karaktert írjuk és olvassuk. Ez nem mindig egyezik a vizuális
rendezéssel: függ az aktuális nyelv szabályaitól.
Ha nem „monospaced” betűtípussal dolgozunk, akkor a karaktereknek különböző
szélességük lehet, tehát az elhelyezés során számít az is hogy milyen karakterek
szerepelnek a szövegben, nem csak az, hogy mennyi.
Szerepelhetnek egymás mellett különböző betűtípusok és stílusok, ebben az
esetben ugyanannak a karakternek is lehet többféle kiterjedése, tehát a
rendezésnél figyelembe kell venni a típusokat is. A TextLayout osztály ezt elvégzi
a programozó helyett, de lehetőség van saját rendezésre is.
Egy összefüggő többsoros szöveg kiíratását a megadott szélességű területre a
LineBreakMeasurer osztály segítségével tehetjük meg. A megjelenítendő
szöveget, és annak attribútumait tárolhatja az AttributedString osztály, amin az
AttributedCharacterItarator használatával megy végig, majd az egyes sorokat
TextLayout objektumként adja vissza, ami megváltoztathatatlan, stílussal
rendelkező karakteradatokat tartalmaz. (Ismeretlen, Oracle Java dokumentációk)
Játék készítése Java-ban
IV-14
IV. Swing
Általános ismeretek
Amikor az első Java verziót kiadták, a felhasználói felületek létrehozásához
nyújtott funkciók meglehetősen szűkösek voltak. Az erre a feladatra hivatott
Abstract Windows Toolkit (röviden: AWT) minden elemet támogatott, amit egy
HTML-formba be lehet építeni, és ezek mellé néhány fontosabb objektumot,
viszont komplexebb felületek létrehozása nehézkes volt a használatával. Erősen
függött a futtatókörnyezet ablakozó rendszerétől, ezért a hordozhatósággal járó
előnyök mellett nem lehetett egységes megjelenítést elérni.
Ezen problémák megoldását az 1997-ben a Java Foundation Classes részeként
kifejlesztett Swing megjelenésével orvosolták, amiben már olyan összetettebb
komponensek is szerepeltek, mint a táblázat vagy a lapfülek.
Létrehozásukat az tette lehetővé, hogy az új API pehelysúlyú komponenseket
használ, vagyis csak kis mértékben függ a futtatókörnyezet ablakozó rendszerétől.
Az egész Java-ban lett írva, és csak saját objektumokkal dolgozik, tehát az
vezérlésüket is teljes körűen birtokolja. A Java2 megjelenésével a JDK része lett a
csomag.
A Swing model-view-controller nevű architektúra szerint építi fel a
komponenseit.
Ebben a model az elem értékeiről tartalmaz információkat. Például egy csúszkánál
tartalmazhatja a kurzor aktuális értékét, a minimum és maximum értékeket, illetve
a léptetés „durvaságát”, azaz hogy milyen pontossággal lehet állítani az értéket. A
model adatai mindig függetlenek attól, hogy a komponens hogy jelenik meg a
képernyőn.
A view azt adja meg, hogy a komponens hogyan jelenjen meg. Az megjelenítés
módját különféle LookAndFeel objektumokkal lehet beállítani. A népszerűbb
operációs rendszerek design-jának megfelelő osztályok a csomag részei.
A controller azt adja meg, hogy a komponens hogyan reagál a külső
eseményekre. Ilyen esemény például egy egérkattintás, vagy egy másik
komponens értékének beállítása.
Játék készítése Java-ban
IV-15
Egy swing felület általános működtetéséhez több szálat kell alkalmazni. Ezek 3
csoportba sorolhatóak:
indító szál: a Swing programokban az indító szál dolga általában az
inicializálást végző Runnable objektum létrehozása, és annak felíratása az
eseménykezelőre.
eseménykezelő szál: az eseménykezelő kódot futtatja. A felhasználói felület
változásait ebben a szálban érdemes intézni a konkurenciaprobléma
elkerülése érdekében.
egyéb háttér-szálak: ha egy Swing programnak hosszú futási idejű feladatot
kell elvégeznie, nem célszerű az eseménykezelő szálban megtennie,
hiszen az események sorban dolgozódnak fel. Új szálat indítva a feladat
elvégzésére, pedig a felhasználói felület módosítása okozhat konkurencia
problémát. Ezért hozták létre egy SwingWorker osztályt, amely egy
háttérszálban végzi el a kívánt feladatot, de lehetőséget ad a felhasználói
felület módosítására, amelyet az eseménykezelő szálban végez el.
Játék készítése Java-ban
V-16
V. Hangfájlok lejátszása a Java-ban, a
Java Sound API
A Java Sound-ról általánosan
Az elkészítendő játék hangjainak lejátszásához a Java Sound API-t választottam.
Ez egy alacsonyszintű API, ami a hang bemeneteket és kimeneteket vezérli. A
hozzá tartozó csomagok:
javax.sound.sampled: ebben a digitális felvételek lejátszásához,
felvételéhez és keveréséhez szükséges osztályok vannak. Ezek azok az
adattípusok, amik a hanghullámokból bizonyos időközönként vett diszkrét
értékű mintákból állnak. A játékban ezt a csomagot használtam.
javax.sound.midi: a MIDI-khez tartozó műveleteket biztosítja. A MIDI adatok
nem magát a hangot írják le, hanem a létrehozásához szükséges
eseményeket. Ilyen például a számítógépre kötött MIDI eszköz egy
billentyűjének lenyomása.
javax.sound.sampled.spi, javax.sound.midi.spi: ezek új audio és MIDI
eszközök készítésére alkalmasak, melyeket a Java Sound API
kiegészítéseként lehet használni. Olyanokat, mint egy MIDI szintetizátor,
egy saját fájlformátumot feldolgozó osztály, vagy egy konverter.
A javax.sound.sampled csomag
A fő feladata az, hogy az audió anyag bájtjait a rendszerből- és a rendszerbe
mozgassa. Ez magában foglalja az audio be- és kimenetek nyitását, és a bufferek
menedzselését.
Az API megkülönbözteti egymástól az adat- és a fájlformátum fogalmakat. Az
adatformátumot egy AudioFormat objektum adja meg, ami a következő
attribútumokat kezeli:
kódolás (általában PCM)
csatornák száma (1: monó, 2: sztereó)
mintaszám (egy csatornára egy másodperc alatt küldött minták száma)
Játék készítése Java-ban
V-17
egy mintában a bitek száma
keret nagyság
keret nagysága byte-okban
byte rendezés
A fájlformátumok az audio fájl struktúráját adják meg. Ez egy AudioFileFormat
objektumban van tárolva, és a következőket tartalmazza:
fájl típus (pl.: WAV)
fájl hossza bájtokban
az audio adatok hossza, keretekben megadva
az adatokhoz tartozó AudioFormat objektum
Az adatok olvasására különböző fájlformátumokból az AudioSystem osztály ad
lehetőséget. Az adatokból lehetséges AudioInputStream objektum készítése, ami
az InputStream leszármazottja: azzal egészíti ki, hogy kezeli az adatformátumhoz
kapcsolódó adatokat.
Az API-ban az audioeszközöket a Mixer objektum képviseli. Ez lehet például egy
hangkártya elérhető szolgáltatásainak együttese, vagy egy beépített mikrofon.
Ennek a feladata a ki- és bemeneti adatfolyamok kezelése.
A különféle csatornák a Line, (vonal) interfészt implementálják. A csatorna lehet
egy Mixer, de egy port (pl. fülhallgató kimenet), vagy akár egy adatfolyam is. Az
interfész biztosítja a következő műveleteket:
a csatornán áthaladó adat kezelése, manipulálása
nyitott és zárt státusz kezelése
események kezelése: esemény a nyitás és a zárás, de az egyes
implementációk más típusúakat is kezelnek. Amikor egy esemény
generálódik, azt elküldi az összes olyan objektumnak, ami fel van íratkozva
a vonal eseményfigyelőjére.
A Line interfésznek van egy belső osztálya: Line.Info . Ez arra szolgál, hogy
információkat adjon vissza az aktuális objektumról. Az implementáló osztályok ezt
leszármaztathatják, hogy specifikus információkat is tároljon.
Az audióeszközök felderítésére a legegyszerűbb mód az AudioSystem
használata: lekérdezi az elérhető Mixer objektumokat, amiken Line-okat biztosít,
emellett konvertálni tud az audió formátumok között, illetve a fájlokat audió
adatfolyammá alakítja.
A Mixer objektumok információinak lekérdezése a getMixerInfo() nevű statikus
metódussal történik. Ha ezek közül megtalálta a program a megfelelőt. a
getMixer(Mixer.Info info) metódussal lekérhetjük hozzá a Mixer objektumot.
Játék készítése Java-ban
V-18
Az egyes Line implementációk eléréséhez az AudioSystem.getLine metódusát,
illetve az egyes Mixer-ek getSourceLineInfo, és getTargetLineInfo metódusát
érdemes használni, amiket Line.Info alapján keres.
A hangok lejátszásához két Line implementáció van:
Clip: ez az összes hangadatot egyszerre kapja meg. Akkor érdemes használni, ha
azokat be lehet tölteni a memóriába, és nem valós időben játszunk le.
Az alapértelmezett kimeneten egy Clip-et az AudioSystem.getClip metódussal
kapunk meg. Ez egy magasabb szintű metódus, ami a getMixer-t és a getLine-t is
használja.
Lejátszás előtt először le kell foglalni a vonalat az open metódus segítségével.
A lejátszás kezdeti pozíciójának beállításához a setFramePosition illetve, a
setMicroSecondPosition metódusokat lehet használni. A setLoopPoints-al azt
a részletet lehet beállítani, amit ismételni szeretnénk.
A lejátszás a start és a stop metódussal lehet indítani, illetve leállítani.
A SourceDataLine folyamatos írásra lett kialakítva. Akkor érdemes használni, ha
a hang nem fér el a memóriában egyben, vagy lejátszás során még nincs meg az
összes adat (pl. telefonbeszélgetés). Az open metódusnál itt meg lehet adni egy
AudioFormat objektumot, és egy buffer méretet. Ha ilyeneket nem kap, akkor a
rendszer alapértelmezett értékekkel fog dolgozni. A start felhívása után lehet
meghívni a write metódust, amivel az adatokat byte tömb formátumban
elhelyezhetjük a bufferben, amit a lehető leghamarabb elkezd üríteni. Ha nagyobb
adatot akarunk elküldeni, mint amekkorát a buffer tartani képes, akkor a metódus
addig nem tér vissza, amíg annyi adatot ki nem ürített (lejátszás), hogy az utolsó
byte is beleférjen. Ez egy bevett gyakorlat a lejátszás kivárására.
A drain metódus addig várakoztat, amíg a buffert ki nem üríti.
Játék készítése Java-ban
VI-19
VI. Java és az animáció
Animációs alapok
A célként kitűzött platformjátéknál mozgóképre van szükség, amit a játéktér
folyamatos újrarajzolásával állít elő a program. Az animáció folyamatosságának
egyik mérőszáma a másodpercenkénti újrarajzolások száma, vagyis az FPS
(Frame Per Seconds). Ahhoz hogy az emberi szem folyamatosnak lássa a
mozgást, ennek a mérőszámnak körülbelül 10 felett kell lennie, tehát 10-szer kell
mindent újrarajzolni egy másodperc alatt. Egy bizonyos mértékig minél nagyobb
ez a szám, annál gördülékenyebbnek látszik az animáció, viszont egy szint fölé
már fölösleges emelni, mivel már a szem nem érzékeli a különbséget, illetve a
monitor frissítési kapacitása is határos.
Ha a játéktér újrarajzolásánál a folyamat az elemeket egymás után rajzolja ki a
képernyőre, a képet vibrálni láthatjuk. Ez azért van, mert a ciklus során
folyamatosan jelennek meg az objektumok, amint az összes elem látható, törölni
kell őket, majd egy másik képet megjelenítenie, tehát az egész periódusnak csak
a legvégén láthatjuk a kívánt képet. Ez egy jól ismert probléma, amit flickering-
nek hívnak. A megoldása az, ha a folyamat a rajzolás során már a végleges képet
jeleníti meg. Erre a legkézenfekvőbb módszer a dupla buffer-es renderelés,
melynek többféle kivitelezése lehetséges:
Szoftveres megvalósítás: a rajzoló műveletek a számítógép
memóriájában tárolják az eredményüket. Amikor minden rajzoló művelet
befejeződött, a program jellegétől függően az egész területet, vagy csak a
változásokat átmásolja a VRAM6-ba, aztán megjeleníti. A megjelenítés alatt
lévő buffert első-, a rajzolás alatt lévőt pedig háttérbuffernek nevezik.
A dupla bufferelés több memóriát és számítási időt igényel, mint az egyszerű
renderelés, ezeket a műveleteket kell még az eredetiek mellett elvégezni:
o memória allokálása a háttérbuffernek
o másolás a VRAM-ba (blitting)
6 A videokártya memóriája.
Játék készítése Java-ban
VI-20
o várás a szinkronizálásra
Page flipping: Itt mindkét buffer a VRAM-ban van, de értelemszerűen a
képernyőn egyszerre csak az egyik buffer jelenik meg, eközben a másik
rajzolódik. Ezt úgy lehet megvalósítani, hogy folyamatosan cserélgetik a
megjelenítendő adatok kezdő pointerét a videó memóriában.
Ez a módszer sokkal gyorsabb, mint az előző, mivel kihagyja a blitting-nek
nevezett műveletet.
Dupla bufferes megvalósításnál előjöhet egy másik gyakori hiba, az ún. tearing,
vagyis könnyezés. Ez akkor adódik, amikor a monitor frissítési frekvenciája nincs
összehangolva a megjelenítési periódussal: amikor a monitor frissíti a képernyőt,
még nem fejeződött be az új kép rajzolása, egyszerre látszik az előző és az új kép
is.
7. ábra - Tearing
A két periódus összehangolása a VSync.
Egy olyan szabállyal meg lehet oldani, ami nem engedi újrarajzolni a képet, amíg
az meg nem jelent a monitoron.
A duplabufferes renderelésnél ez addig számít jó megoldásnak, amíg az eredeti
FPS szám a frissítési frekvencia felett van, egyébként az FPS csak olyan diszkrét
értékeket vehet fel, amik a frekvencia és egy pozitív egész szám hányadosai.
A diszkrét FPS-számot el lehet kerülni tripla bufferelés alkalmazásával:
Játék készítése Java-ban
VI-21
Tripla bufferelés: A dupla bufferelésnél a programnak várnia kell, hogy az
elkészült rajz átmásolódjon a memóriába, vagy kicserélődjön a pointer
(page flipping), mielőtt elkezdené az új rajzolást. Ezt itt úgy küszöbölték ki,
hogy a 2 háttérbuffert alkalmaznak, ezért az új kép rajzolása rögtön
elkezdődhet, amint kész az előző. Így a szoftver és a videokártya
egymástól függetlenül végezhetik a műveleteket. (Ismeretlen, How VSync
works, and why people loathe it)
Bufferelés és teljes képernyős mód a Javaban
A J2SE 1.4 egyik újdonsága, hogy lehetőséget nyújt a programozónak, hogy
megkerülje az ablakozó rendszert, és közvetlenül a képernyőre rajzoljon. Ehhez
teljes képernyős módra kell váltani, így át lehet venni az irányítást a megjelenítő
eszköz felett.
A megjelenítő eszközt egy GraphicsDevice objektum kezeli. Az elérhető
eszközök listáját a GraphicsEnvironment getScreenDevices(), az aktuálisat pedig
a getDefaultScreenDevice() metódusával kapjuk meg. A GraphicsEnvironment
osztály a platformon elérhető megjelenítő eszközöket és betűtípusokat tárolja.
Azt, hogy engedélyezett-e a teljes képernyős mód, az
isFullScreenModeSupported() metódus mondja meg. A
setFullScreenWindow(Window) metódus, a paraméterül kapott ablakot teljes
képernyősre állítja. Ha a teljes képernyős mód nem támogatott, akkor átméretezi
úgy, hogy kitöltse az egész képernyőt. Ha a paraméter null, visszatér az eredeti
üzemmódba.
A DisplayMode objektumok kezelik az eszköz következő információit: felbontás,
színmélység, frissítési frekvencia. Az elérhető megjelenítési módokat a
GraphicsDevice getDisplayModes() metódusa adja vissza egy tömbként. A
beállításnál ennek a tömbnek az értékeiből lehet választani.
Teljes képernyős módban nem szükséges figyelni az operációs rendszer által kért
újrarajzoló műveleteket, ezek helyett érdemes egy saját ciklust írni ezekre. Ezt
aktív renderelésnek hívják, és sok fölösleges számítást lehet vele elkerülni.
Játék készítése Java-ban
VI-22
Teljes képernyős módban közvetlenül a VRAM-ot is lehet használni, ami
lehetőséget nyújt a Page-flipping megvalósításához. A különböző bufferelési
stratégiák kivitelezését a BufferStrategy használatával érdemes elvégezni. Ennek
a megjelenítés a feladata, tekintet nélkül a tárolók számára, vagy a megjelenítési
technikára. Két legfontosabb metódusa a getDrawGraphics, ami visszaadja a
rajzolási területet, és a show, ami megjeleníti azt a képernyőn.
A bufferelési lehetőségeket a BufferCapabilities osztály objektumai tárolják.
Olyan metódusokkal lehet vizsgálni őket, mint:
isPageFliping: visszatérési értéke true, ha a hardveres page-flipping
elérhető.
isFullScreenRequied: ha true, akkor a page-flipping használatához
szükséges a teljes képernyős mód.
getFlipContents: azt adja meg, hogy a bufferek közötti váltás hogyan
történik.
Az elérhetőeket GraphicsConfiguration példány getBufferCapabilities() metódus
adja vissza. Ez egy megjelenítő eszköz beállításait tárolja. Egy GraphicsDevice
objektumhoz tartozhat több ilyen beállítás is.
Játék készítése Java-ban
VII-23
VII. Az elkészült program ismertetése
A játék rövid bemutatása
8. ábra – Egy kép a játékból
Az elkészített játékban, mint ahogy a bevezetésben is említettem: egy kis figurát
(lila dinoszaurusz) irányítva, a különböző akadályokon és ellenségeken
átjutva/átugrálva kell eljutni a célig.
A pálya statikus, nem mozdítható elemei a blokkok, ezekből épülnek fel a
platformok, de egy ilyen blokk a célt jelző fekete négyzet is.
A blokkokon járnak, és ezeknek ütköznek neki a szereplők (a játékos, és az
ellenfelek).
A szereplőkre hat a gravitációs erő, illetve a pályabeállításoktól függően a szél.
Az alapvető mozgástípusok a futás, az állás és az ugrás.
A játékosnak kezdéskor 3 élete van: ezeket az ellenfelekkel való érintkezések
során veszíti el. Lehetőség van az ellenfél kiiktatására: ehhez rá kell ugrani.
A képernyő jobb alsó sarkából lehet leolvasni a fennmaradó életek számát.
Ha a játékos elérte a célt, az ENTER billentyűvel lehet a következő szintre lépni.
Játék készítése Java-ban
VII-24
A futást a nyilakkal irányíthatjuk, az ugrás pedig a SPACE billentyű lenyomásakor
történik. Ha a billentyűket a SHIFT-el együtt nyomja meg a felhasználó, az
gyorsabb futást és nagyobb ugrást eredményez.
Az elkészítés során felmerült feladatok
Ebben a fejezetben a játék megvalósításához kapcsolódó követelményeket, és
azoknak azt a kivitelezését mutatom be, ami a folyamatos újraírások után már a
végleges verzióban szerepel.
A játék legyen képes grafikai elemeket megjeleníteni és pozícionálni.
A játék elemei képként jelennek meg. A beolvasásukhoz, és az ehhez hasonló
feladatokhoz készült egy fájlkezelő osztály, aminek az egyik metódusa a kép
elérési útvonalát megkapva, BufferedImage osztályú objektumot ad vissza. Ezek
a képek vannak megjelenítve a játék ablakában. A pozícionáláshoz a Graphics
draw metódusánál megadható paraméterek szolgálnak, amiket a programban két
szám összege határoz meg: az abszolút pozíció, amit egy külön osztályban
tároltam, illetve az eltolás, amit a későbbiekben ismertetek. A képek
átméretezésére is szükség volt, amit szintén a draw metódus paramétereivel
végeztem el. A szélesség és magasságértékek a pozíciót tároló osztályba
kerültek.
A játék szereplői változtassák a pozíciójukat.
A pozíció állításához a szereplőkhöz tartozik egy függőleges és vízszintes irányú
sebességérték, amit egy metódus hozzáad a pozíciók értékéhez.
A folyamatos mozgáshoz mozgóképet kellett megjeleníti: ehhez egy végtelen
ciklus beállítja az összes szereplő pozícióját a sebesség alapján, majd újrarajzolja
őket. Ennek pontos leírása az „A játék folyamatainak leírása” c. részben
olvasható.
A szereplők ütközzenek a pályaelemekkel, ne tudjanak rajtuk átmenni.
Az ütközések kezeléséhez listázni kellett az ütköztethető elemeket. A játék egy
szintjének blokkjait egy kétdimenziós tömb tárolja, a szereplőket pedig egy lista.
Ahhoz hogy a szereplők nekiütközhessenek a blokkoknak, minden periódusban
meg kell vizsgálni, hogy valamelyik szereplő érintkezik-e valamelyik blokkal. A
Játék készítése Java-ban
VII-25
blokkok tömbjét tartalmazó objektumnak vannak olyan metódusai, amik egy
szereplőt megkapva paraméterként, annak pozíciója alapján visszaadják a
legközelebb balra, jobbra, felül illetve alul lévő elemeket. Ezek építenek a blokkok
tömbbeli elhelyezkedésére, emiatt a blokkok elhelyezésük utáni mozgatása nem
lehetséges.
Azt, hogy ütköznek-e az így kapott blokkokkal, a szereplők ősosztályában definiált
metódusok adják meg, ahogy az ütközés során történő viselkedést is. A
leszármaztatásukkal értem el, hogy a különböző osztályba tartozóak különböző
módon viselkedjenek.
A szereplőket mozgassák külső erőhatások.
A külső mozgató elemek, mint a gravitáció egy közös interfészt implementálnak. A
játék egy periódusa alatt az összes ilyen mező mozgató metódusa meghívódik az
összes szereplőre.
Az ugrás megvalósítása az eddig felvázolt rendszerben a felfelé mutató sebesség
állítását jelenti. Ha 10-re van állítva, akkor a játékos az első periódusban 10
egységgel feljebb kerül, a másodikban már csak 9-el a gravitációs mező hatása
miatt, és így tovább, amíg el nem kezd lefelé gyorsulni. Az ugrás csak akkor
lehetséges, ha a szereplő a földön van, ezért ezt a tulajdonságot egy boolean
értékben tárolom, aminek az alsó blokkal való ütközések vizsgálatakor változik az
értéke.
Az ellenfelek változtassák a mozgásuk irányát.
Egyes ellenfelek mozgását az ősosztályok különböző események hatására
felhívódó metódusok irányítják.
A játékos és az ellenfél ütközése legyen vizsgálva, és lekezelve.
Az ütközés az összes periódusban vizsgálva van a szereplők pozíciója és mérete
alapján. Az ütközések során, ha a játékos felülről esik rá az ellenfélre, akkor az
meghal, egyébként a játékos életeinek száma csökken. Arra, hogy megállapítsuk
az egymáshoz viszonyított elhelyezkedést, a szereplők koordináta állítása előtt
eltárolódik az eredeti érték. A játékos akkor iktatja ki az ellenfelet, ha érintkezéskor
az előző, legalacsonyabban lévő Y koordinátája magasabban volt, mint az ellenfél
legmagasabban lévő Y koordinátája.
Játék készítése Java-ban
VII-26
A megjelenített játéktér mozogjon együtt a játékossal.
Az elemek kirajzolása során pozícióértékekhez hozzáadok egy függőleges és egy
vízszintes eltolás értéket, ami attól függ, hogy hol áll a játékos.
A játék nyelve legyen állítható.
A játék során megjelenő szövegeket a ResourceBoundle használata helyett
kísérletezés céljából egy általam írt osztály kezeli. Ez ugyanúgy kulcs-érték
párokkal dolgozik, amiket az egyes szövegfájlokból olvas be HashMap-ekbe.
A hangok lejátszása ne szakítsa meg a játékmenetet.
Mivel a játékciklus, és a billentyűzetesemények figyelője sem várhatja meg, amíg
egy hang lejátszódik, minden egyes effekt lejátszása külön szálon történik.
Legyen lehetőség teljes képernyős mód használatára.
A teljes képernyő módhoz az előző fejezetben leírt módszert alkalmazom.
A játék futásakor a kép ne vibráljon.
A vibrálás eltűntetését a JFrame bufferelési módjának beállításával értem el.
A játék tartalmazzon menüt, ahol be lehet állítani a nyelvet, a teljes
képernyős módot, és ki lehet kapcsolni a hangokat.
A játék menüjét saját elemekből építettem fel. Ezeknek kétféle típusa van:
o Alap menüelem: egy négyzet rajta szöveggel.
o Választási opciókkal rendelkező menüelem: az alap négyzet és
szöveg mellett más menüelemeket is tartalmaz, amiket aktív
állapotban megjelenít. A játékhoz tartozó beállításokat ilyen
menüelemekkel lehet állítani: egy opció választása után a
beállításokért felelős segédosztály elvégzi a hozzá tartozó
műveleteket.
A játék legyen gyors.
A gyorsaság növelését az egyes részfeladatok optimalizálásával értem el. Az
újraírások során vizsgáltam a metódusok futási idejét, és a legkedvezőbb értékhez
Játék készítése Java-ban
VII-27
tartozó megoldást alkalmaztam. Nagyon kis időtartamokat kellett vizsgálni, ezért a
System.currentTimeInMillis() nem minden esetben ad elég pontos információt,
helyette a System.nanoTime()- ot alkalmaztam. A mérésre alkalmazott módszer:
1. egy long változóba eltároltam a System.nanoTime() értékét
2. a program elvégezte a metódust
3. A System.nanoTime() aktuális értékéből kivontam a változó értékét, így az
eredménye egyenlő a metódus futási idejével nanoszekundumokban.
Felhasznált programok
A kód megírásához az Eclipse fejlesztőkörnyezetet választottam. A projekt
változásainak kezelését eleinte nem terveztem, azonban a sorozatos refaktorálás
során felmerülő új hibák miatt szükségességét éreztem valamilyen verziókezelő
rendszer használatának: az SVN-re esett a választásom, amihez a TortoiseSVN-t
és az Eclipse letölthető Subversive nevű plugin-jét telepítettem fel. A kód a Java
Development Kit 7u17-hez igazodik, a felhasznált képek pedig Photoshop-al
készültek.
Az osztályok ismertetése
A működés teljes megértéséhez szükséges, az osztályhierarchia és az egyes
osztályok ismerete. Ebben a fejezetben a fontosabb tudnivalókat írom le róluk.
GameFrame
9. ábra - GameFrame
A projekt egyetlen futtatható osztálya, ami egy JFrame-ből van leszármaztatva.
Itt, és az összes később leírt olyan osztálynál, ahol szerepel a singleton()
metódus, a singleton tervezési mintát alkalmaztam. Ez azt jelenti, hogy az
osztálynak csak egyetlen példányosított objektuma lehet. Emiatt a konstruktora
Játék készítése Java-ban
VII-28
private módosítót kapott. Ez itt csak egy statikus blokkban van felhívva, ahol a
SINGLETON nevű tagot példányosítja, amit a singleton() metódus ad vissza. Ezt a
kialakítást azért választottam, mert azt feltételeztem, hogy egy JVM egyszerre
csak egy játékot fog futtatni, így kihasználhatom a mintának azt az előnyét, hogy
nem kell minden GameFrame objektummal kapcsolatban lévő osztálynak
referenciát tárolnia, elég, ha a GameFrame.singleton() metódust felhívja.
A konstruktor a játékhoz olyan jellemzőket állít be, mint az átméretezhetőség
letiltása, a fókusz kérése, vagy az ignoreRepaint paraméter igaz-ra állítása,
aminek hatására figyelmen kívül hagyja az operációs rendszertől érkező
újrarajzolási kéréseket.
A main metódus beolvassa a szövegfájlokból a beállításokat
(SettingsUtility.singleton().readSettingsFromFiles()), és felhívja a
GameExecutorThread.singleton().startGame(); metódust.
GameExecutorThread
10. ábra - GameExecutorThread
Ez az osztály tartalmazza az aktuális képernyő (játék, főmenü, vagy beállítások)
adatainak frissítéséért, és a megjelenítésért felelős periódust. Implementálja a
Runnable interfészt: a fenn említett metódus létrehoz egy futtatási szálat az
osztályból (animator), majd elindítja azt.
Fontosabb adattagjai:
Játék készítése Java-ban
VII-29
content: a játék egy képernyője
strategy: a GameFrame osztály getBufferStrategy álltal visszaadott
objektumának kell lennie
Fontosabb metódusai:
run(): ebben a metódusban található a képernyő adatainak frissítéséért, és
megjelenítéséért felelős periódus, ami a program futása során
folyamatosan ismétlődik. A pontos működését az „Egy periódus leírása”
című fejezetben írom le.
pause() és start(): ezek a metódusok megállítják, illetve elindítják a ciklus
ismétlődését.
setContent(): a képernyőt váltja. Ez a content értékének változtatása
mellett a következő feladatokat látja el:
o a GameFrame KeyListener-jét az új content-re állítja.
o a content-nek felhívja az initialize metódusát.
A játék képernyői
A játékban háromféle képernyő van, amik az AbstractEngine osztály
leszármazottjai.
11. ábra – Az AbstractEngine osztályhoz tartozó hierarchia
Játék készítése Java-ban
VII-30
AbstractEngine
12. ábra - AbstractEngine
Fontosabb metódusai:
render(): a paraméterként kapott Graphics2D objektumra rajzolja a
képernyő elemeit
update(): a megjelenítendő elemek adatainak frissítése
keyTyped(KeyEvent), keyPressed(KeyEvent), keyReleased(KeyEvent):
absztrakt metódusok, a billentyűzetesemények kezelésére. Az osztály
implementálja a KeyListener interfészt
initialize(): a képernyő inicializálása/újrainicializálása
nextEngine és previousEngine getter és setter-ek: a képernyők közötti
navigálásokat oldják meg. A getNextEngine metódusban a következő
képernyő previousEngine-je megkapja az aktuális képernyőt, ha az
isReturnAble metódus true-t ad vissza. A returnToPreviousEngine az előző
képernyőre ugrik vissza.
A játék indításakor az MainMenuEngine az első képernyő, ami az
AbstractMenuEngine-ből származik. Az AbstractMenuEngine egy
MenuContainer osztályú tagjának kezeléséért felelős. A MenuContainer osztály
legfontosabb adattagja egy MenuElement-ekből álló lista.
Játék készítése Java-ban
VII-31
MenuElement
13. ábra - MenuElement
Fontosabb adattagjai:
selected: azt tárolja, hogy ki van-e választva a menüelem.
14. ábra – A játék menüje
key: az azonosítást, és a felirat kiválasztását szolgálja. A konstruktor a
játék szövegeit kezelő osztály (TextHandler) segítségével megkeresi a
kulcshoz tartozó aktuális szöveget, aminek az értékét a text mező kapja
meg.
text: a menü felirata
background: fix szélességű és magasságú (konstansok) négyzet, a
szöveg háttere.
Játék készítése Java-ban
VII-32
Fontosabb metódusai:
drawElement(Graphics2D) a paraméterként kapott objektum színét, és
betűtípusát megfelelő értékekre állítva megjeleníti a téglalapot, kirajzolja a
szöveget (drawString), majd visszaállítja az eredeti értékeket.
MenuContainer
15. ábra - MenuContainer
Fontosabb adattagjai:
menuElements: az osztály feladata az ebben a listában lévő menüelemek
kezelése.
Fontosabb metódusai:
addMenuElement(MenuElement): a listához hozzáadja a paraméterként
kapott elemet, majd az yPos értékét növeli az elem magasságának és az
elemek közti távolságnak (ELEMENT_SPACE) az összegével.
addMenuElement(String): egy MenuElement osztályú objektumot hoz
létre, az xPos, yPos és a paraméterként kapott kulcs értékének
segítségével, amire meghívja az addMenuElement(MenuElement element)
metódust.
Játék készítése Java-ban
VII-33
addMenuElementWithOptions(String): az előző metódushoz hasonló a
működése azzal a különbséggel, hogy a MenuElement-ből származó
MenuElementWithOptions objektumot hoz létre.
drawMenu(Graphics2D): a lista összes elemére meghívja a
drawElement(g2d) metódust.
cursorUp() és cursorDown() metódusok: A listában lévő aktív elemet lehet
a segítségükkel kiválasztani.
index és key getter és setter metódusok: a listában kiválasztott elem
sorszámát, illetve annak kulcsát adják vissza/állítják be.
AbstractMenuEngine
16. ábra - AbstractMenuEngine
Fontos adattagjai:
container: a képernyő ezt az elemet kezeli és jeleníti meg
Fontosabb metódusai:
createContainer(): absztrakt metódus. Visszetérési értéke egy
MenuContainer elem, ennek az értékét kapja meg az initialize metódusban
a container változó. Azt a célt szolgálja, hogy a különböző leszármazott
osztályokban változtatható legyen, az elem inicializálásnak módja.
keyPressed(): felhívja a navigationActions nevű absztrakt metódust, aztán
kezeli a fel- (actionForUp(MenuElement))le-
Játék készítése Java-ban
VII-34
(actionForDown(MenuElement)) nyíl, illetve az
Enter(actionForEnter(MenuElement)) billentyűk lenyomását. Az említett
metódusok a kiválasztott menüelemet kapják meg paraméterként. A nyilak
hatására az alapértelmezett metódus, a container-ben való kiválasztás.
render: törli a megjelenítendő területet, majd feketére színezi, és
megjeleníti a container elemet.
Az update() metódus itt üres, mivel csak a felhasználói interakcióra kell pozíciókat
változtatni. Az isReturnable() metódus false-t ad vissza azért, hogy ne lehessen
egy ilyen típusú képernyőre visszalépni.
MainMenuEngine:
17. ábra – MainMenuEngine
Ez a képernyő a játék főmenüje.
Fontosabb metódusai:
createContainer(): a főmenü elemeihez tartozó kulcsokból készíti a
container elemeit. Amennyiben a játék még tart7, a lista tartalmazni fogja az
oda való visszalépést.
actionForEnter(MenuElement): a kiválasztott elem kulcsát megvizsgálva
beállítja a következő képernyőt, vagy kilép a játékból.
MenuElementWithOptions
A „Beállítások” képernyőt az OptionMenuEngine valósítja meg, ami általános
MenuElement elemek mellett MenuElementWithOptions osztályúakat is
7 GameEngine.singleton() != null && !GameEngine.singleton().isGameOver() &&
GameEngine.singleton().isReturnAble() feltétel
Játék készítése Java-ban
VII-35
tartalmaz. Ez utóbbi arra a célra lett tervezve, hogy a játék beállításait jelenítse,
illetve változtassa meg.
18. ábra - MenuElementWithOptions
Fontosabb adattagjai:
container: az elem kulcsához tartozó választási lehetőségeket tartalmazza.
active: állapotjelző
Fontosabb metódusai:
createContainer: a SettingsUtility osztályból lekéri az elem kulcsához
tartozó további kulcsokat egy listában, amik alapján feltölti a container
elemet, ezek után ugyanennek az osztálynak a segítségével lekérdezi, és
beállítja az éppen aktív elemet.
drawElement(Graphics2D):az alap MenuElement rajzolása mellett aktív
állapot esetén a container drawMenu(Graphics2D) metódusát is meghívja.
setActive(): abban az esetben, ha épp most válik inaktívvá az elem,
meghívja a setOption() metódust, ami a
SettingsUtility.setSettings(String,String) segítségével elvégzi a kívánt
beállítást.
Játék készítése Java-ban
VII-36
OptionsMenuEngine
19. ábra - OptionsMenuEngine
Fontosabb metódusai:
createcontainer(): MenuElementWithOptions objektumokat is elhelyez a
container-ben.
Billentyűzetleütést kezelő metódusai:
actionForEnter(MenuElement): ha a kiválasztott elem
OptionsMenuElement, akkor azt aktívvá teszi (setActive(true)).
actionForUp(MenuElement), actionForDown(MenuElement): ha az
éppen kiválasztott elem egy MenuElementWithOptions és az állapota aktív,
akkor azon belül léptetnek, egyébként az ősosztály metódusát hívják fel.
navigationActions(KeyEvent): ha az eseményt az Escape billentyű
lenyomása okozta, és a kiválasztott elem MenuElementWithOptions és
aktív, akkor inaktívvá teszi, egyébként visszalép az előző menübe.
Játék készítése Java-ban
VII-37
GameEngine
20. ábra - GameEngine
Ez a játék képernyője.
Fontosabb adattagjai:
gameActors: a játékban aktívan résztvevő szereplőket tárolja: az
ellenfeleket és a játékost
map: az aktuális pálya, az összes elérhető pálya neve a mapNames[]
tömbben van tárolva
level: az aktuális szint
player: a játékos
Játék készítése Java-ban
VII-38
hud: a játék közbeni információk megjelenítéséért felelős objektum8
backGround: a játék háttere
Fontosabb metódusai:
drawObjects(Graphics2D): A felsorolt adattagok megjelenítését végzi:
Részletes működése az „Egy periódus leírása” részben található.
update(): a szereplők pozíciójának frissítését végzi. Részletes leírása az
„Egy periódus leírása” részben található.
render(Graphics2D): a paraméterre meghívja a clearRect metódust a
terület törléséhez, majd ha a játék még tart, meghívja a
drawObjects(Graphics2D)-t, egyébként a paintGameOver(Graphics2D)-
t, ami kiírja a képernyőre, hogy vége a játéknak.
keyPressed(KeyEvent): az Escape billentyű hatására, vagy ha vége a
játéknak visszalép az előző képernyőre. Ha a játékos továbbléphet a
következő szintre (canPlayerExit()), akkor az ENTER billentyű hatására
megteszi azt(setNextLevel()). Ezen vizsgálatok utána átadja az eseményt
a player objektumnak.
keyReleased(KeyEvent): a player eseménykezelőjének továbbítja a
paramétert.
A játék elemei
A szereplők és pályaelemek ősosztálya az AbstractGameElement. Ennek két
mezője van: egy a pozíció tárolására, egy pedig azt mutatja, hogy éppen látható-e
az elem. A pozíciót az ElementPosition nevű osztály, és annak leszármazottjai
kezelik. Ezek azért kerültek külön osztályba, hogy a koordinátaadatok jellege ne
játsszon szerepet az AbstractGameElement-hez tartozó hierarchia kialakítása
során.
8 A HUD itt a Heads Up Display rövidítése: a kifejezést a játékképernyőn általában áttetszően vagy
kis ikonokkal kijelzett adatok összességére használják.
Játék készítése Java-ban
VII-39
21. ábra – ElementPosition, MovingElementPosition, MovingElementPositionWithDirection
ElementPosition
Az ElementPosition az x, y koordináták, a szélesség és a magasság
attribútumokat kezeli.
Fontosabb metódusai:
interSectedWith(ElementPosition),
intersectedWithRectangle(int,int,int,int): ezek a metódusok azt adják
vissza, hogy a paraméterekhez tartozó négyzettel, vagy pozícióval
érintkezik-e az objektum.
MovingElementPosition
A MovingElementPosition függőleges és vízszintes sebességadatokkal egészíti
ki az ősét.
Fontosabb metódusai:
move():a sebességadatokat hozzáadja a koordinátákhoz
MovingElementPositionWithDirection
A MovingElementPositionsWithDirection minden koordinátaállításnál letárolja
az előző értéket. Ezek ütközésdetektálásnál, az egymáshoz viszonyított pozíciók
megállapításához vannak felhasználva.
Játék készítése Java-ban
VII-40
AbstractGameElement
22. ábra - AbstractGameElement
Fontosabb metódusai:
drawToGraphics(Graphics2D,int,int), a képernyőre való rajzolást
valósítja meg, a két int paraméterrel való eltolással.
getType(): az elem típusára vonatkozó String-et ad vissza (pl. a játékos
objektumára visszaadva az értéke: „PLAYER”).
createPosition(int, int, int, int) a konstruktorban van felhívva. Visszatérési
értékét kapja a position objektum.
az koordinátákat kezelő metódusok a position objektum azonos nevű
metódusait hívják fel.
Játék készítése Java-ban
VII-41
23. ábra – Az AbstractGameElement-hez tartozó hierarchia
ImageGameElement
24. ábra - ImageGameElement
Azoknak a játékelemeknek, amik egy képet jelenítenek meg, ez az ősosztályuk.
Mivel az egyes osztályok azonos típusú elemeket jelölnek, az image lehetséges
értékei osztályszintű elemekben van tárolva.
Fontosabb metódusai:
drawToGraphics(Graphics2D): a Graphics2d osztály drawImage
metódusának az image objektumot, illetve a position x, y, width és height
koordinátáit adja át.
Játék készítése Java-ban
VII-42
AbstractBlockElement
25. ábra - AbstractBlockElement
A pálya statikus az AbstractBlockElement leszármazottjai.
Fontosabb adattagjai:
destroyable: azt tárolja, hogy a játékos el tudja-e tűntetni a blokkot
collosionEnabled: ha az értéke true, nekiütközhetnek a szereplők.
Fontosabb metódusai:
destroy():az isVisible és a collosionEnabled értékét false-ra állatja, ezálltal
eltűnteti a blokkot.
SimpleBlock, StoneBlock, ExitDoor
Ezek már nem absztrakt osztályok. A megjelenített képben, és az említett két
boolean értékében különböznek:
26. ábra – SimpleBlock, StoneBlock, ExitDoor
Játék készítése Java-ban
VII-43
A pályaelemek táblázata
SimpleBlock StoneBlock ExitDoor
Megjelenített kép:
destroyable true false false
collosionEnabled true true false
Actor
A játék résztvevői ennek osztálynak leszármazottjai.
27. ábra - Actor
Játék készítése Java-ban
VII-44
Status belső osztály:
Fontosabb adattagjai:
direction: a mozgás irányát adja meg: ez a Direction nevű enum típus.
Lehetséges értékei: bal, jobb.
movementType: a mozgás típusát adja meg: ez a MovementType enum.
Lehetséges értékei: állás, futás, ugrás
Ezeknek az összes lehetséges kombinációja el van tárolva osztályszintű
változókban, emellett egy külön státusz a halott, amikor a mozgás iránya, és
típusa is null.
Fontosabb adattagjai:
onGround: értéke akkor true, ha a szereplő talajon jár. Az olyan műveletek
felhívásánál fontos a vizsgálata, mint az ugrás, vagy a gravitációs
gyorsulás.
isDead: a státusz beállításánál, illetve az egymással való ütközés
kezelésénél fontos.
jumpPower: az ugrás nagyságát határozza meg (a kezdeti felfelé mutató
sebesség). Az ugrást kivitelező metódusok alapértelmezetten erre az
értékre állítják a felfelé mutató sebességet.
Fontosabb metódusai:
createPosition(): MovingElementPositionWithDirection objektumot ad
vissza. Az osztály ennek a metódusait is implementálja.
setStatus: a status értékeit állítja be a sebességek alapján, majd beállítja
az ahhoz tartozó képet.
animate(Image[]): feladata a szereplő képeinek folyamatos cserélgetése a
megadott időközönként (ANIMATION_LENGTH).
checkAndSetOnGroundFromBlock(AbstractBlockElement),
sideCollosion(AbstractBlockElement),
upCollosion(AbstractBlockElement): ezek kezelik a blokkokkal való
ütközést. Ebben az osztályban található az alapértelmezett viselkedés, ami
egy adott irányú ütközés során megállítja az irányba történő mozgást.
Játék készítése Java-ban
VII-45
Az Actor-ból származtatott osztályok betöltésük során egy statikus blokkban
inicializálják státuszokhoz tartozó képeket, majd azokat egy HashMap-ben
(imageForStatusAssignment) tárolják.
RedEnemy, GreenEney, YellowEnemy
28. ábra – RedEnemy, GreenEnemy, YellowEnemy
Mivel az ellenfeleket nem a játékos irányítja, viselkedésüket az egyes események
hatására felhívott metódusok felülírásával lehet beállítani.
Az ellenfelek táblázata
RedEnemy GreenEnemy YellowEnemy
Kép
Viselkedés:
Oldalirányú ütközésnél
véletlenszerűen felugrik
vagy megfordul.
Oldalirányú ütközésnél a
sebességet ellentétes
előjelűvé állítja
A játékost
követi.
Felülírt
metódusok:
sideCollosion
(AbstractBlockElement)
sideCollosion
(AbstractBlockElement) update()
Játék készítése Java-ban
VII-46
Player
29. ábra -Player
Fontosabb adattagjai:
lives: életek száma
running: ha az értéke true, akkor a játékos gyorsabban fut és nagyobbat
ugrik
Fontosabb metódusai:
keyPressed(KeyEvent): a bal és jobb irány lenyomására beállítja a
megfelelő irányú sebességet, a Space-re felhívja az ugrás metódust, a
Shift-re pedig az isRunning változó értékét true-ra változtatja.
Játék készítése Java-ban
VII-47
keyReleased(KeyEvent): visszaállítja az eredeti értékeket.
die(): csökkenti az életek számát, majd elvégzi az ehhez tartozó
metódusokat
upCollosionAdditonalActions(AbstractBlockElement): az ősosztályban
implementált viselkedés mellett az eltűntethető blokkokat eltűnteti.
HeadsUpDisplay
30. ábra - HeadsUpDisplay
Ennek az osztálynak az a feladata, hogy a felhasználót tájékoztassa az aktuális
eseményekről. A működése annyiból áll, hogy a játékos életeinek számát jelzi úgy,
hogy a playerHeadImage képet annyiszor jelenít meg egymás mellett, ahány
élete van, illetve ha a játékos érintkezik a pálya ExitDoor elemével, akkor kiírja,
hogy az ENTER lenyomásával továbbléphet a következő szintre.
Játék készítése Java-ban
VII-48
A pályát létrehozó és kezelő osztályok:
GameMapLevel
31. ábra - GameMapLevel
Ez egy szintet tárol a játékban, vagyis egy játékteret.
Fontosabb adattagjai:
screenBlocks: a pálya elemeinek tömbje, amit egy szöveges fájlból olvas
be.
A beolvasás során, ami a konstruktorban történik, végighalad rajta sorról
sorra, és az abban lévő kód alapján elhelyez a pályaelemek tömbje, vagy a
szereplők listája között egy elemet (ez utóbbit aztán átadja a GameEngine-
nek, és nem használja tovább).
Játék készítése Java-ban
VII-49
exitDoor: a szint kijárata
Fontosabb metódusok:
getNearestBlockLeft(Actor), getNearestBlockRight(Actor),
getLowestRoffOverActor(Actor), getHighestGroundUnderActor(Actor):
Ezek a metódusok az Actor osztályú objektum pozíciója alapján a hozzá
képest legközelebbi balra, jobbra fölötte és alatta lévő elemet adják vissza.
getScreenBlockAt(int,int,boolean): visszaadja az adott koordinátánál lévő
blokkot. Mivel épít az elemek tömbben lévő pozíciójára, a blokkok
mozgatása helytelen működéshez vezet.
GameMap
A GameMapLevel-ek szervezéséért felelős.
32. ábra - GameMap
Ez konstruktorában egy könyvtár nevét kapja meg paraméterként, amiben kell,
hogy legyen egy fieldList.txt, illetve egy levelList.txt fájl. Előbbiben a pályához
tartozó szintek leírását tartalmazó további szöveges fájlok nevei, vannak,
utóbbiban pedig a pálya gravitációs mezők melletti hatásainak a neve és
erőssége.
Játék készítése Java-ban
VII-50
CollosionDetection
33. ábra - CollosionDetection
Ez az osztály kezeli a játék során fellépő ütközéseket.
Fontosabb metódusai:
collosionDetection(Actor): feladata a szereplő ütköztető metódusainak
felhívása a megfelelő pályaelemmel.
checkActorContact(Actor,Actor):két szereplő ütközését vizsgálja.
Lehetséges visszatérési eredményei:
o 0, ha a két paraméter megegyezik, valamelyik „halott”, vagy nem
érintkeznek.
o 1, ha az első ráesett/ugrott a másodikra. Ez a feltétel akkor igaz, ha
az előző legalacsonyabb pozíciója (getPreviousYMax() )
magasabban van, mint a második legmagasabb pozíciója.
o -1 minden egyéb esetben.
A Field interfész, és a hozzá tartozó osztályok:
34. ábra – GravityField, Wind
Ezek a szereplők pozícióját befolyásolják: a doMove(Actor) metódus a
paraméterként kapott szereplő pozícióját módosítja.
Játék készítése Java-ban
VII-51
GravityField
A gravitációs gyorsulást szimulálja: ha az actor a levegőben van, és a függőleges,
lefelé történő gyorsulása nem érte még el a határértéket, akkor azt növeli.
Wind
A függőleges pozíciót változtatja, ezzel azt a hatást keltve, mint ha az objektumot
elfújná a szél.
Segédosztályok
A játék során felmerülő általános feladatok elvégzésére a game.utility csomagban
lévő segédosztályok hivatottak.
SettingsUtility
35. ábra - SettingsUtility
Ez az osztály kezeli a játék beállításait.
Fontosabb adattagjai:
options: HashMapben tárolja a lehetséges beállításokat (kulcsok), és azok
opcióit (értékek).
Játék készítése Java-ban
VII-52
settings: a SettingsHolder nevű HashMap-ből leszármaztatt belső osztály
egy példánya. Erre az osztályra azért volt szükség, hogy az értékek
felvitelekor elvégezze a megfelelő beállításokat:
ResourceUtility
36. ábra - ResourceUtility
A fájlok kezeléséért felelős.
Fontsabb metódusai:
Szerepelnek benne különböző getter metódusok, melyek a fájlokhoz tartozó
útvonalakat adják vissza.
readKeysAndValues(File,String): a paraméterként kapott fájl alapján
olyan HashMap-et hoz létre, ami a fájlban található kulcs-érték párokat
String-ként tartalmazza.
readKeysAndArrayListValues(File,String,String): A fájl tartalma alapján
egy String kulcsokat és String listát tartalmazó HashMap-et ad vissza.
Játék készítése Java-ban
VII-53
TextHandler:
37. ábra - TextHandler
A játék szövegeit kezeli. A megjelenítendő szövegek a két HashMap
attribútumban vannak hozzárendelve a megfelelő kulcshoz. A reinitialize(String)
metódus újraolvassa a kapott kulcshoz tartozó szövegfájlokat.
SoundUtility
38. ábra - SoundUtility
Az osztály a hangok lejátszásáért felelős. A Sound enum típus kétféle hangot
tárol, amiket a play() metódussal lehet lejátszani. A SoundThread run() metódusa
a konstruktorban kapott Sound attribútumot játssza le.
Fontosabb metódusai:
Játék készítése Java-ban
VII-54
playSound(Sound) abban az esetben, ha a soundOn értéke true, elindítja
a paraméter alapján példányosított SoundThread szálat.
DisplayUtility
39. ábra - DisplayUtility
Feladata a megjelenítési adatok kezelése.
Fontosabb metódusai:
setFullScreenDisplayMode(GraphicsDevice) a GameFrame-et teljes
képernyős módra állítja, majd megpróbálja beállítani a legelőnyösebb
BufferStrategy objektumot, és üzemmódot (amit a két DisplayMode
elemeket tartalmazó tömbből választ ki). A beállításhoz meg kell állítani a
GameExecutorThread fő periódusát.
A játék folyamatainak leírása
Futási szálak
A futás közben a következő szálak végzik a játékkal kapcsolatos műveleteket:
Indításkor az inicializáló szál előkészíti a futást: meghívja a GameFrame
konstruktorát, beolvassa a beállításokat, majd elindítja a
GameExecutorThread-hez tartozó szálat.
Az eseménykezelő szál, figyeli a billentyűzetleütéseket, és ennek
megfelelően hívja fel a metódusokat. Az AbstractEngine implementálja a
KeyListener interfészt. A képernyők eseménykezelése a GameFrame-hez
van rendelve.
Játék készítése Java-ban
VII-55
A GameExecutorThread-hez tartozó animator változó az objektumból
Thread-et készít, amit a startGame metódusban indít el. Ez a szál felelős a
játék adatainak frissítéséért és a megjelenítésért. Az „Egy periódus
leírása” című részben részletesen le van írva a működése.
Minden egyes hang lejátszásához külön szál jön létre.
Egy periódus leírása
A GameExecutorThread egy periódusa a következő műveletekből áll:
1. Lekéri az aktuális rendszeridőt a System.nanoTime()-al.
2. Frissíti a képernyő adatait: content.update(): ez csak játék képernyő
esetén végez feladatot, ilyenkor ha még tart a játék, az összes
szereplőn elvégzi a következő műveleteket:
o pozíciók állítása a sebesség alapján
o a játékossal való interakció vizsgálata: ha ütköznek, akkor attól
függően, hogy milyen az egymáshoz viszonyított pozíciójuk, az
ellenfél meghal, a játékos felugrik, és lejátszódik az ehhez
kapcsolódó hang, vagy a játékos hal meg.
o elvégzi a pályához tartozó mezők műveleteit
o megvizsgálja a pályaelemekkel való ütközéseket, és elvégzi az
ezekhez kapcsolódó műveleteket
3. A strategy attribútumon megjeleníti a képernyő elemeit, ami
GameFrame esetén a következő feladatokból áll:
o Megrajzolja a hátteret.
o Beállítja a pályaelemek és szereplők eltolásának értékét a
játékos pozíciója alapján.
o Megrajzolja a pályaelemeket.
o Megrajzolja a szereplőket.
o Felrajzolja a hud-ot.
Ezután meghívja a Toolkit.getDefaultToolkit().sync() metódust. Ez
azért fontos, mert az ablakozó rendszer várakoztatási sorba teheti az
újrarajzolási kérelmeket.
Játék készítése Java-ban
VII-56
4. A System.nanoTime() metódus által visszaadott értékből kiszámolja,
hogy meddig tartott ezeket a műveleteket elvégezni.
5. Ha az alapértelmezett periódus tovább tartott az alapértelmezett
periódusidőnél, és a két időtartam különbsége meghaladja az
overSleepTime értékét, a futtató szálnak várakoznia kell. Ha többet vár,
mint a különbség értéke, akkor az overSleepTime értékét beállítja a
többlet értékére.
6. Ha a periódus tovább tartott a beállított értéknél, akkor újra felhívja a
képernyő frissítési metódusát, hogy a következőként megjelenített kép
szinkronba kerüljön az elvárt működéssel.
7. Utolsó lépésként felhívja a switchContent() nevű metódust, ami, ha az
aktuális képernyő getNextContent() metódusa nem null-t ad vissza,
akkor az általa visszaadott értékkel felhívja a
setContent(AbstractEngine) metódust.
Játék készítése Java-ban
VIII-57
VIII. Összefoglalás
A szakdolgozat elkészítése során megismerkedtem a Java és a játékfejlesztés
kapcsolatával, majd a Java2D és a JavaSound API-kal. Utánanéztem az ilyen
jellegű játékoknál jelentkező leggyakoribb animációs problémáknak, és azok
megoldási lehetőségeinek. Megterveztem és megvalósítottam egy platformjátékot,
amiben szerepelnek az előzetesen felállított követelmények. A kivitelezés során
rengeteget segítettek a különböző játékfejlesztéssel kapcsolatos internetes
fórumok, amiken az egyes részfeladatok megoldásait kerestem.
Az elkészült játékon még lehetne fejleszteni:
Az ütközésdetektálás gyorsabb lenne, ha a pályán az egymás mellett lévő
blokkokat nagyobb téglalapokká csoportosítaná a program, hogy ne kelljen
mindegyiket külön vizsgálni. Az ezt megvalósító algoritmust még nem
implementáltam.
A játéktér elemeinek mozgatása hibás működéshez vezet, ennek
elkerüléséhez a blokk kereső metódusokat kell átírni.
A hátteret a program minden periódusban újrarajzolja, ami a periódus
időintervallumaihoz képest hosszú ideig tart. Ennek a javítására még
keresem a megoldást, amit talán a JPane rétegezés használata jelent majd.
A folyamatosan felmerülő problémák miatt olykor az osztályok hierarchiáján is
változtatni kellet, ez nagyban megnövelte a fejlesztési időt. Egy hasonló program
megvalósítása most már sokkal kevesebb ideig tartana, mivel a tervezésnél
felhasználnám az eddigi tapasztalataimat, de a megszerzett tudás hasznos lehet
más típusú grafikus alkalmazás készítésekor is.
Summary
During making of my thesis, I gained a good understanding of the relationship
between Java, the game industry, Java2D and the Java Sound API. I experienced
some of the animation problems with these type of games, and the possible
solutions. I planned and made the game, which implements in advance the
established requirements. During the execution I learned a lot from Internet
Forums about the Java Game Development, where I tried find some solutions for
the problems.
There are still some things to do with the game:
The collosion detection would be faster, if the program used containing
rectangles for the blocks. I have not made the algorythm for this feature yet.
The moving of the map elements cause invalid behaviour. The solution
would be the rewriting of the block finding methods.
The background is painted in every period, and it takes quite a long time
compared to the other operations. Maybe I should use some kind of JPane
layers for fixing this.
Because of the arising problems I had to rewrite the hirerarchy of the classes
many times, which increased the development time. Developing the same kind of
program would be faster for me in the future, because of my experience gained,
and the knowledge will be useful during any graphics application development in
the future.
Irodalomjegyzék
Brian Cole, R. E. (2002). Java Swing, 2nd Edition. O'Reilly.
Davison, A. (2005). Killer Game Programming in Java. O'Reilly.
Ismeretlen. (dátum nélk.). Minecraft Wikipédia oldala. Letöltés dátuma: 2013.
április 10, forrás: Wikipédia: http://hu.wikipedia.org/wiki/Minecraft
Ismeretlen. (dátum nélk.). Oracle Java dokumentációk. Letöltés dátuma: 2013.
április 8, forrás: Oracle: http://docs.oracle.com/javase/
Ismeretlen. (dátum nélk.). Why is'nt Java more widely used for game development.
Letöltés dátuma: 2013. április 10, forrás: Programmers:
http://programmers.stackexchange.com/questions/55104/why-isnt-java-more-
widely-used-for-game-development