játékfejlesztés.hu
FórumGarázsprojectekCikkekSegédletekJf.hu versenyekKapcsolatokEgyebek
Legaktívabb fórumozók:
Asylum:    5448
FZoli:    4892
Kuz:    4455
gaborlabor:    4449
kicsy:    4304
TPG:    3402
monostoria:    3284
DMG:    3172
HomeGnome:    2919
Matzi:    2520

Pretender:    2498
szeki:    2440
Seeting:    2306
Geri:    2188
Orphy:    1893
Joga:    1791
Bacce:    1783
MaNiAc:    1735
ddbwo:    1625
syam:    1491
(DB) Egy 2D játék alapjai DB Pro-ban 2006.08.10 15:00


2D játék alapjai DB Pro-ban



A Dark Basic egyszerű interfészt biztosít a spriteok kezelésére, így pillanatok alatt összedobható egy 2D-s játék. Elronthatatlanul egyszerű lenne? Első ránézésre igen, de ha jobban beleássuk magunkat, mindenféle alattomos buktatókkal találkozhatunk.

Tegyük fel, írunk egy 2D-s Shoot'em up játékot. Szépen elkezdjük fejlesztgetni: megírjuk az ellenségek kirajzolását végző kódot, megírjuk a játékos űrhajójáét, később már lövöldözni is lehet, aztán az interface-be is belekezdünk. Ekkor beállít a grafikusunk, hogy új robbanásanimációt készitett, igaz most 10 képkockával hosszabb. Amikor berakjuk a játékunkba azt vesszük észre az aszteroidák el-eltünedeznek. Azután észrevesszük hogyha túl sok lövedéket lövünk ki eltűnik néhány dolog az interface-ről. Később módosítunk a programon: a játék kezdetekor az ellenségek oldalról fognak beúszni a képbe. Ezalatt viszont leáll az űrhajónk animációja valamiért. Amikor közeledünk a befejezéshez már lassan kínszenvedés lesz a fejlesztés: minden új feature 3 új hibát hoz létre a legkülönbözőbb helyeken: ha újraindítjuk a játékot a pálya végén nem jelenik meg a főellenség, amikor meg másodjára lépünk vissza a menüből az interface fölé rajzolódik az űrhajó. Hiba hiba hátán, a programkód úgy néz ki mint egy spagetti a sok load image-től, ráadásul nem tudjuk melyik sprite vagy image létezik és melyik hely üres.

És ha átgondoltan írtuk volna meg?

Az alábbiakban ismertetésre kerül egy általános grafikai rendszer ami biztosítja hogy minden úgy álljon ahogy kell a programunkban, a lehető legkevesebbet kelljen aggódni a grafikai hibákért (szurkoljatok hogy ne hagyjak benne bugot ;). A fórumon többször is felmerült a kérdés hogy hogyan lehet animálni egy karaktert vagy görgethető terepet készíteni: többet ezzel se lesz gond. Használható lesz a legkisebb és legnagyobb projekthez egyaránt, legyen az lövöldözős, stratégiai, ügyességi, logikai, platform...

Az image-ket és a spriteokat nem fogjuk többé közvetlenül kezelni, hanem animációkon és 2D objektumokon keresztül. Ha valamit változtatni szeretnénk, saját függvényeket hívunk vagy tömbökben változtatunk adatot. Nem kell megijedni, minden egyszerűbb lesz! (Szép lenne egy objektumorientált megoldás csak ez DB...)

Még itt se ér véget a bevezető ;) Felmerülhet a kérdés: Jó-jó, de miért nem használok itt animált spriteot?.
  • Egyfelől azért, mert a logikája teljesen elüt a Dark Basicétől. Valamiért nem sikerült olyan elegánsan megoldani, mint az animált objektumokat, egy rakás idétlen paranccsal leszünk gazdagabbak. Pl. nem követi a "betöltöm a képet - csinálok spriteot" logikát a create animated sprite, ezenkívül a play sprite-t meg kell hívni folyamatosan, ciklusban a normál sprite mellett, mert magától nem fogja pörgetni az animációt. Ehhez a semmittevéshez még négy paraméter is kell neki.
  • Másfelől kényelmesebb a képkockákat külön-külön kezelni, mint egyetlen képpé összeállítani. Nehezebb lesz a csere, a szerkesztés, pixelre pontosan kell összeállítani, ne csússzon el - egységesen fekete háttereknél nem egyszerű. Szeret valaki mátrix-textúrát gyártani?
  • Harmadrészt amikor még az 1.052 idején próbálkoztam a módszerrel, egész csokor hibával találkoztam, pl. a kép méretétől függően rosszul darabolta fel azt és játszotta le a DBPro. Magamban a fentiek miatt használhatatlannak könyveltem el a módszert - egy óra alatt jobbat tudtam kitalálni és megírni - ezért 1.06-on nem is próbáltam ki (gondolom javult a helyzet azóta).

Persze próbálkozhatsz az animált spriteokkal, mindenkinek a szíve joga mit használ - ez a véleményem volt csak. Kitérő vége.

Vágjunk bele!


Kezdjük az animációkkal. Egy animáció betöltött képkockák sorozata a memóriában. Elrejti előlünk a képek számozását, a külön load image parancsokat, egyszerűen tudjuk kezelni őket.

Mit kell tudni egy animációról?
- Legyen egy neve (esetleg száma), hogy tudjuk melyik
- Tároljuk el az első képkockájának a számát
- Tároljuk el a hosszát
Ezek feltétlenül szükségesek, de tárolhatunk még egyéb adatokat is, például a méretét, a sebességét, az átlátszóságát, stb. Ezeket érdemes egy külső szöveges fájlban tárolni, és onnan beolvasni a programba. Jelen példánkban az animáció sebességére kíváncsiak leszünk, el is tároljuk.

Dark Basicül ez így hangzik:
Kód:
type animacio
nev as string
kezdet as integer
hossz as integer
sebesseg as integer
endtype
dim anim(100) as animacio

Mivel nem árt ügyelni a biztosságra, logolni fogjuk a dolgokat betöltés közben. Szúrjuk be ezt a két sort a kód elejére:
Kód:
if file exist("log.txt") then delete file "log.txt"
open to write 32,"log.txt"

Ha az adattípusunk megvan, jó lenne feltölteni adatokkal. Természetesen nem egyenként, hanem írunk egy betöltő függvényt, ami betölti az összes animációt egy könyvtárból. Minden animációnak lesz egy külön alkönyvtára, amiben a betöltendő fájlok sorakoznak 1.bmp, 2.bmp, ... n.bmp nevekkel, ezt kell végigjárnia. Az animációk neveit és a hozzájuk tartozó sebességüket egy külön fájlban tároljuk, legyen ez anim.cfg.

Az anim.cfg szerkezete így néz ki:

[animációnév] (egyben könyvtárnév is!)
[huzzá tartozó sebesség]


Most nézzük a beolvasó kódot! A függvény három számlálót használ:
1, szamlalo: az animáció tömbbe való írás helyét mutatja
2, imgszaml: a betöltendő kép számát mutatja - jelen példában 2000-ről indul
3, fajlszaml: a betöltendő kép fájlnevét mutatja
Kód:
function animtolt
imgszaml=2000: szamlalo=0
set dir "Anim"
open to read 1,"anim.cfg"
do
   eleje:
   `Ha vége, a fájlnak, kiugrunk
   if file end(1)=1 then exit
   read string 1,dir$
   `Az üres sorok, valamint a file end függvény hibája miatt
   if dir$="" then goto eleje
   inc szamlalo
   anim(szamlalo).nev=dir$
   anim(szamlalo).kezdet=imgszaml
   if path exist(dir$)=1
      set dir dir$
      else
      logol("Betöltés sikertelen. Rossz elérési út: "+dir$)
      dec szamlalo : goto eleje
      endif
   fajlszaml=1
   do
      if file exist(str$(fajlszaml)+".bmp")=1
         `Ha létezik az n.bmp, betöltjük
         load image str$(fajlszaml)+".bmp",imgszaml,1
         inc imgszaml
         else
         `Ha nem, beírjuk az animáció hosszát, és olvashatjuk a következőt
         anim(szamlalo).hossz=fajlszaml-1
         exit
         endif
      inc fajlszaml
   loop
   set dir ".."
   `Ha végeztünk a képek betöltésével, beolvassuk a sebességet is
   read string 1,seb$
   anim(szamlalo).sebesseg=val(seb$)
   logol("Animáció betöltve: " + dir$ + " (" + str$(anim(szamlalo).hossz) +" képkocka)")
loop
close file 1
set dir ".."
endfunction

function logol(szoveg$)
write string 32,szoveg$
endfunction

Az animációkat most be tudjuk tölteni, már csak meg kell tudni jeleníteni. Ezt 2D objektumokkal tehetjük meg. (A DBPro nem tartalmaz ilyet, saját kitaláció, nevezhetnénk tehénkének is akár. 2D objektumnak a spriteok felelnek meg, a mi objektumaink is spriteokat használnak, csak adnak nekik egy-két plusz funkciót. Az elnevezéssel nem akarok bajlódni, ha sprite-ot írok az sprite, ha objektumot az saját objektum.)
Egy ilyen 2D objektumot képzeljünk el úgy, hogy egy sprite-hoz hozzárendelünk egy animációt. Legyenek a következő tulajdonságai:
Kód:
type objektum
tipus as integer
x as integer
y as integer
img as integer
varakozas as integer
endtype

A tipus az animációjának típusa (sorszáma az anim tömbben). Az x és y az objektum helyét tárolják, az img az aktuális képkockát. A varakozas egy kis magyarázatra szorul: Amint majd később látjuk, egy kirajzoló eljárás folyamatosan fogja frissíteni a képet a főciklusban. Az animációnak van egy sebesseg adattagja: ez azt mondja meg, hányadik frissítésnél váltsa a képet. Tehát ha a sebességet 1-re állítjuk, minden frissítésnél változik az animáció, ha 7-et, akkor csak minden hetediknél. Azonban tudni kell, melyik a hetedik, erre van a varakozas adattag. Ennek az értéke minden frissítéskor nő eggyel, és ha eléri az animáció sebességének az értékét, váltjuk a képet, és lenullázzuk, hogy számoljon újra.
És honnan tudjuk, hogy melyik sprite tartozik az objektumhoz? Megoldjuk egyszerűen: az objektum száma egyben a sprite száma, tehát az obj(213) objektumé a 213-as sprite. Ha nem tetszik ez a rendszer, eltolhatjuk akár néhány ezer számmal is (tartozzon hozzá a 10213), a lényeg, hogy ezt az adatot nem kell tárolnunk.

Írjunk két függvényt: egyet, ami létrehoz egy objektumot, egy másikat ami törli. A létrehozó megkapja, hogy melyik animációból hozzon létre és milyen koordinátákra, erre visszaadja a létrehozott objektum számát. A törlő függvény megkapja a törlendő objektum számát és törli, nem ad vissza semmit.
Nem is két függvényt írunk, négyet: a létrehozó függvény kap két egyszerű segédfüggvényt, hogy ne unatkozzon:
Kód:
function ujobjektum(nev as string,x,y)
o=szabadobjektum()
obj(o).tipus=animkeres(nev)
obj(o).x=x
obj(o).y=y
obj(o).img=anim(obj(o).tipus).kezdet
obj(o).varakozas=0
endfunction szam

function szabadobjektum
`Visszaadja a legelső szabad objektum számát
for i=1 to 1000
   if obj(i).tipus=0 then exitfunction i
next i
logol("Hiba: Nem lehet új objektumot létrehozni, betelt a lista") : i=-1
endfunction i

function animkeres(nev as string)
for i=1 to 100
   if anim(i).nev=nev then exitfunction i
next i
logol("Hiba: Nem található animáció: " + nev) : i=-1
endfunction i

function objektumtorol(szam)
obj(szam).tipus=0
delete sprite szam
logol(str$(szam)+". objektum törölve")
endfunction

Jó lenne, ha látnánk is valamit a munkánkból, magyarul ki is kéne rajzolni az objektumjainkat. Ezt a rajzoló eljárást hívjuk meg a játékunk fő ciklusából:
Kód:
function rajzol
for i=1 to 1000
   if obj(i).tipus<>0 : rem ha létezik az objektum...
      obj(i).varakozas=obj(i).varakozas+1
      if obj(i).varakozas=anim(obj(i).tipus).sebesseg : rem ha váltani kell a képet...
         obj(i).img=obj(i).img+1
         if obj(i).img=anim(obj(i).tipus).kezdet+anim(obj(i).tipus).hossz then obj(i).img=anim(obj(i).tipus).kezdet
         obj(i).varakozas=0
         endif
      `Kirajzolás
      sprite i,obj(i).x,obj(i).y,obj(i).img : set sprite i,0,1
      endif
next i
endfunction

A függvény sorra veszi az objektumokat, amelyik létezik (azaz a tipus adattagja nem 0) azt kirajzolja. Ha eljött a képváltás ideje, akkor meg képet vált. A set sprite azért kell, hogy beállítsa a spriteot, hogy ne magától frissüljön, hanem a backdroppal együtt, amikor a sync parancsot kiadjuk. Nem a legszebb, hogy itt áll ciklusban - elég egyszer kiadni - de itt jön létre a sprite, itt a legegyszerűbb. Aki akar, kereshet neki más helyet, de nem emiatt lesz lassú a játék, az biztos.

És mi van a mi scrollozható terepünkkel, pályánkkal, hátterünkkel? Innentől már pofonegyszerűen meg lehet oldani! Bevezetünk egy 2D kamerát, amit szabadon mozgathatunk a pálya fölött. Először deklaráljuk a program elején...
Kód:
type koord
x as integer
y as integer
endtype
dim kamera as koord

... majd átírjuk a rajzol függvényben a sprite parancs sorát:
Kód:
sprite i,obj(i).x,obj(i).y,obj(i).img : set sprite i,0,1
helyett
sprite i,obj(i).x-kamera.x,obj(i).y-kamera.y,obj(i).img : set sprite i,0,1

Készen vagyunk? Igen! Bár a scrollozást még nem láthatjuk, mert nincs scrollozó függvényünk, de elkészült a 2d játékunk "motorja". Írok ide egy scrollozó függvényt, bár ez már játéktól függ, milyet használsz: ne tekints rá a fentiek szerves részeként!
Kód:
function scroll()
if mousex()=0 then dec kamera.x,1
if mousey()=0 then dec kamera.y,1
if mousex()=screen width()-1 then inc kamera.x,1
if mousey()=screen height()-1 then inc kamera.y,1
endfunction

Összefoglalás


Fent ismertetésre került egy animációs rendszer, amit felhasználhatsz akármilyen 2d játékhoz (ill. 2d játékok 3d részeihez). Úgy is tudod használni, hogy nem ásod bele magad az alapjaiba. A rendszer előnyei:
  • Automatizált betöltés, nem kell egyenként begépelni a load image parancsokat. Könnyen tudod cserélni a játékod grafikáit.
  • Nem kell számon tartani, melyik kép melyik számot használja. Rövidebb és egyszerűbb kódot kapsz.
  • Nem kell tudni, melyik sprite mit csinál: egy magasabb szintről, az objektumok szintjéről vezérelheted a grafikus műveleteket. De ha alacsonyabb szinten akarsz beavatkozni a dolgokba, ugyanúgy megteheted, mint eddig.
  • Biztosan szinkronban lesz minden, mivel egyszerre frissül az összes sprite és animáció.
  • Ha törölni kell mindent, néhány tömb nullázásával könnyen megteheted.
  • Hiba esetén könnyebben rájössz az okára, ha megnézed a log fájlt.


Mindehhez csak egy egyszerű "felületet" kell fejben tartani (azért túlzás API-nak nevezni):

Először betöltöd az animációkat az animtolt() meghívásával, azután:
Objektum létrehozása: ujobjektum(név,x,y) - Megmondod melyik animációt szeretnéd lerakni melyik koordinátára. Ő erre visszaadja az objektumod számát, amivel hivatkozhatsz rá. A koordinátának nem kell a képernyőn belül lennie, a képernyőt mint egy ablakot mozgathatod a pályán. Egy objektumnak nem kell szükségszerűen animáltnak lennie, ha csak egy képkockából áll, akkor sima sprite-ként viselkedik, amit továbbra is scrollozhatsz.
Objektum törlése objektumtorol(szam) - Törli a megadott számú objektumot.
Objektumok mozgatása obj(szám).x és obj(szám).y változók értékének állítása - Ne a spriteok koordinátáit állítsd, mert azok a kamera helyzetétől függnek.
Pálya scrollozása 2dkamera változó értékének átállítása - A koordináták állításával mozgathatod a 2d kamerát.
Ezenkívül a fő ciklusból meg kell hívni a rajzol() függvényt, hogy láss is valamit.

Hogyan tovább?


Van még fejlesztenivaló, még ha túl is vagyunk a nehezén.
Egyfelől lehet még több funkciót belepréselni ebbe a rendszerbe - a mellékelt kód picivel többet tud, raktam bele olyan funkciókat, amik tárgyalásával nem akartam a cikket bonyolítani, de még jól jöhet.
Másfelől integrálni kell a játékba, valahogy ráépíteni a játék többi részét. Úgy gondolom, ez nem bonyolult. A játék grafikai elemeihez nem egy sprite kapcsolódik majd, hanem egy objektum (illetve mindkettő, és még a számuk is megegyezik), nem kell mindenhez külön kezelő függvényeket írni. Egyetlen egységhez könnyedén csatolhatunk majd több objektumot (pl. lövedékek).
Te döntöd el, hol használod, mire - ha nem is használod sorról-sorra ugyanezt a kódot, remélem azért ötletet adott.

Jó fejlesztgetést, a teljes programkódot megtalálod itt az oldal alján.

VT
Fájl : (DB) Egy 2D játék alapjai DB Pro-ban
Méret: 0,129 Mb - Letöltve : 1034

Értékelés: 9.50

Új hozzászólás
VT          2009.12.29 04:05
A fájl végén belinkelt melléklet nem elérhető a jf.hu költözései miatt. A helyes link:

KATT IDE

(Amennyiben ez is elromlana az url-t írd át a jf.hu aktuális címére. Mivel saját fájlként van feltöltve az elérési út datas/users/5-cikkhez.rar marad.)
NacsaSoft          2007.09.24 11:15
Hali !!

Frankó a cikk csak sajnos nem lehet letölteni a forrást !!!!
Lécci nézz utána !!!!! Köszike !!!!
MRC          2007.01.01 07:57
Kód:
function ujobjektum(nev as string,x,y)
o=szabadobjektum()
obj(o).tipus=animkeres(nev)
obj(o).x=x
obj(o).y=y
obj(o).img=anim(obj(o).tipus).kezdet
obj(o).varakozas=0
endfunction szam

"endfunction szam" helyére "endfunction o" -t írjatok, akkor adja vissza a létrehozott obj. számát!
MRC          2006.12.31 04:54
MRC          2006.12.29 13:49
A cikk jó, a link nem.